From 4152ecfc1fce97f70de1ce6de6a7e3e8f0a7e4b9 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 08:52:43 +0530 Subject: [PATCH 01/48] adding textual to the project --- pyproject.toml | 1 + uv.lock | 61 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a9b895dfb..f097c43c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,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" diff --git a/uv.lock b/uv.lock index cc66c3d7e..866a86b35 100644 --- a/uv.lock +++ b/uv.lock @@ -1098,6 +1098,9 @@ scraping = [ { name = "protego" }, { name = "tavily-python" }, ] +textual = [ + { name = "textual" }, +] [package.dev-dependencies] dev = [ @@ -1197,6 +1200,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" }, @@ -1210,7 +1214,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", "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", "chromadb", "docs", "codegraph", "evals", "graphiti", "aws", "dlt", "baml", "dev", "debug", "redis", "monitoring", "docling", "textual"] [package.metadata.requires-dev] dev = [{ name = "pytest-timeout", specifier = ">=2.4.0" }] @@ -3816,6 +3820,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.78.0" @@ -4050,6 +4066,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.1" @@ -4216,6 +4237,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" @@ -8524,6 +8557,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" @@ -9008,6 +9058,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.15" From 40b8f2b501813e2b95e7d3f771924702b3748c1d Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 09:11:09 +0530 Subject: [PATCH 02/48] WIP - adding first screen for tui --- cognee/cli/_cognee.py | 1 + cognee/cli/commands/tui_command.py | 167 +++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 cognee/cli/commands/tui_command.py diff --git a/cognee/cli/_cognee.py b/cognee/cli/_cognee.py index 1539d1acf..32400fb85 100644 --- a/cognee/cli/_cognee.py +++ b/cognee/cli/_cognee.py @@ -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: diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py new file mode 100644 index 000000000..f0d7ffcad --- /dev/null +++ b/cognee/cli/commands/tui_command.py @@ -0,0 +1,167 @@ +import argparse +from typing import List + +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, ComposeResult + from textual.widgets import Header, Footer, ListView, ListItem, Static + from textual.containers import Container, Vertical + from textual.binding import Binding + + class CommandItem(Static): + """A custom widget for command items with icon and description.""" + + def __init__(self, icon: str, command: str, description: str): + self.icon = icon + self.command = command + self.description = description + super().__init__() + + def render(self) -> str: + return f"{self.icon} {self.command:<12} {self.description}" + + class CogneeTUI(App): + """A k9s-style TUI for cognee commands.""" + + CSS = """ + Screen { + background: $surface; + } + + #header { + dock: top; + height: 3; + background: $boost; + color: $text; + content-align: center middle; + border: solid $primary; + } + + #main-container { + height: 100%; + border: thick $primary; + background: $surface; + padding: 1; + } + + #title { + text-align: center; + color: $accent; + text-style: bold; + padding: 1; + } + + ListView { + height: auto; + background: $surface; + border: none; + padding: 0 2; + } + + ListItem { + background: $surface; + color: $text; + padding: 0 1; + height: auto; + } + + ListItem:hover { + background: $surface; + } + + ListItem.--highlight { + background: $primary; + color: $text; + } + + CommandItem { + width: 100%; + } + + #footer-info { + dock: bottom; + height: 3; + background: $boost; + color: $text-muted; + content-align: center middle; + border: solid $primary; + } + """ + + BINDINGS = [ + Binding("q", "quit", "Quit", priority=True), + Binding("escape", "quit", "Quit", priority=True), + Binding("enter", "select", "Select", priority=True), + ] + + def compose(self) -> ComposeResult: + yield Static("🧠 cognee v1.0.0", id="header") + + with Container(id="main-container"): + yield Static("Select Command", id="title") + yield ListView( + ListItem(CommandItem("📥", "add", "Add data to cognee")), + ListItem(CommandItem("🔍", "search", "Search data in cognee")), + ListItem(CommandItem("⚡", "cognify", "Process data in cognee")), + ListItem(CommandItem("🗑️", "delete", "Delete data from cognee")), + ListItem(CommandItem("⚙️", "config", "Configure cognee settings")), + ) + + yield Static( + "↑↓: Navigate • Enter: Select • q/Esc: Quit", + id="footer-info" + ) + + def on_mount(self) -> None: + """Focus the list view on mount.""" + self.query_one(ListView).index = 0 + + def on_list_view_selected(self, event: ListView.Selected) -> None: + """Handle command selection.""" + command_item = event.item.query_one(CommandItem) + command = command_item.command + fmt.echo(f"Selected command: {command}") + self.exit() + + def action_select(self) -> None: + """Select the current item.""" + list_view = self.query_one(ListView) + list_view.action_select_cursor() + + app = CogneeTUI() + app.run() + fmt.success("TUI exited successfully!") + 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, + ) \ No newline at end of file From c57d1a9b82fbf35e8ebe972b69f69fe1b8d85ce8 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 09:38:14 +0530 Subject: [PATCH 03/48] improvements to the home screen of the tui --- cognee/cli/commands/tui_command.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index f0d7ffcad..bc81d8a8f 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -1,11 +1,9 @@ import argparse -from typing import List - 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 - +from cognee.version import get_cognee_version class TuiCommand(SupportsCliCommand): @property @@ -66,12 +64,20 @@ class TuiCommand(SupportsCliCommand): background: $surface; padding: 1; } + #title-wrapper { + width: 100%; + height: auto; + align: center middle; + } #title { text-align: center; + width: auto; color: $accent; text-style: bold; - padding: 1; + padding: 0 3; + border: solid $accent; + margin-bottom: 2; } ListView { @@ -118,10 +124,12 @@ class TuiCommand(SupportsCliCommand): ] def compose(self) -> ComposeResult: - yield Static("🧠 cognee v1.0.0", id="header") + version = get_cognee_version() + yield Static(f"🧠 cognee v{version}", id="header") with Container(id="main-container"): - yield Static("Select Command", id="title") + with Container(id="title-wrapper"): + yield Static("Select Command", id="title") yield ListView( ListItem(CommandItem("📥", "add", "Add data to cognee")), ListItem(CommandItem("🔍", "search", "Search data in cognee")), From 99e7c206af56b5916ae54ed1ed52513cde09d481 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 10:21:11 +0530 Subject: [PATCH 04/48] the highlight kind of works --- cognee/cli/commands/tui_command.py | 54 +++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index bc81d8a8f..a5f49a0bd 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -60,7 +60,7 @@ class TuiCommand(SupportsCliCommand): #main-container { height: 100%; - border: thick $primary; + border: solid $primary; background: $surface; padding: 1; } @@ -79,6 +79,12 @@ class TuiCommand(SupportsCliCommand): border: solid $accent; margin-bottom: 2; } + + ListView > ListItem { + width: 100%; + padding: 0; + margin: 0; + } ListView { height: auto; @@ -92,19 +98,16 @@ class TuiCommand(SupportsCliCommand): color: $text; padding: 0 1; height: auto; + width: 100%; } - - ListItem:hover { - background: $surface; + + ListItem.highlighted { + background: $primary-darken-2; } - - ListItem.--highlight { - background: $primary; - color: $text; - } - + CommandItem { width: 100%; + background: transparent; } #footer-info { @@ -121,8 +124,15 @@ class TuiCommand(SupportsCliCommand): Binding("q", "quit", "Quit", priority=True), Binding("escape", "quit", "Quit", priority=True), Binding("enter", "select", "Select", priority=True), + Binding("up", "nav_up", "Up", priority=True), + Binding("down", "nav_down", "Down", priority=True), ] + def __init__(self): + super().__init__() + self.lv = None + self.current_index = 0 + def compose(self) -> ComposeResult: version = get_cognee_version() yield Static(f"🧠 cognee v{version}", id="header") @@ -145,10 +155,30 @@ class TuiCommand(SupportsCliCommand): def on_mount(self) -> None: """Focus the list view on mount.""" - self.query_one(ListView).index = 0 + 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) + 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: - """Handle command selection.""" command_item = event.item.query_one(CommandItem) command = command_item.command fmt.echo(f"Selected command: {command}") From aba6619d0220a60d8e006726f68ca2305ba50915 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 10:35:37 +0530 Subject: [PATCH 05/48] final polishing for the homepage --- cognee/cli/commands/tui_command.py | 63 +++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index a5f49a0bd..5f57ad4a6 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -4,6 +4,23 @@ from cognee.cli.config import DEFAULT_DOCS_URL import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException from cognee.version import get_cognee_version +from textual.app import App, ComposeResult +from textual.widgets import ListView, ListItem, Static +from textual.containers import Container, Horizontal +from textual.binding import Binding + + +def make_item(icon, command, description): + # 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 TuiCommand(SupportsCliCommand): @property @@ -24,11 +41,6 @@ class TuiCommand(SupportsCliCommand): def execute(self, args: argparse.Namespace) -> None: try: - from textual.app import App, ComposeResult - from textual.widgets import Header, Footer, ListView, ListItem, Static - from textual.containers import Container, Vertical - from textual.binding import Binding - class CommandItem(Static): """A custom widget for command items with icon and description.""" @@ -42,8 +54,6 @@ class TuiCommand(SupportsCliCommand): return f"{self.icon} {self.command:<12} {self.description}" class CogneeTUI(App): - """A k9s-style TUI for cognee commands.""" - CSS = """ Screen { background: $surface; @@ -75,7 +85,7 @@ class TuiCommand(SupportsCliCommand): width: auto; color: $accent; text-style: bold; - padding: 0 3; + padding: 0 10; border: solid $accent; margin-bottom: 2; } @@ -90,7 +100,7 @@ class TuiCommand(SupportsCliCommand): height: auto; background: $surface; border: none; - padding: 0 2; + padding: 0 0; } ListItem { @@ -118,6 +128,30 @@ class TuiCommand(SupportsCliCommand): content-align: center middle; border: solid $primary; } + + .cmd-row { + width: 100%; + height: auto; + align-horizontal: left; + padding: 0 1; + } + + .cmd-icon { + width: 4; + text-align: center; + } + + .cmd-name { + width: 14; + padding-left: 1; + } + + .cmd-desc { + width: 1fr; + overflow: auto; + padding-left: 1; + } + """ BINDINGS = [ @@ -141,11 +175,11 @@ class TuiCommand(SupportsCliCommand): with Container(id="title-wrapper"): yield Static("Select Command", id="title") yield ListView( - ListItem(CommandItem("📥", "add", "Add data to cognee")), - ListItem(CommandItem("🔍", "search", "Search data in cognee")), - ListItem(CommandItem("⚡", "cognify", "Process data in cognee")), - ListItem(CommandItem("🗑️", "delete", "Delete data from cognee")), - ListItem(CommandItem("⚙️", "config", "Configure cognee settings")), + 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"), ) yield Static( @@ -163,6 +197,7 @@ class TuiCommand(SupportsCliCommand): 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") From aec22ac33934fdc14172156a4d6f15d2eed386ba Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 12:43:10 +0530 Subject: [PATCH 06/48] minor UI change --- cognee/cli/commands/tui_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index 5f57ad4a6..49d879a43 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -61,11 +61,12 @@ class TuiCommand(SupportsCliCommand): #header { dock: top; - height: 3; background: $boost; color: $text; content-align: center middle; border: solid $primary; + text-style: bold; + padding: 1; } #main-container { From eb17ab502047d0306f1e1fe4f9c8453f523e2bfa Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 16:01:49 +0530 Subject: [PATCH 07/48] WIP - adding the config screen --- cognee/cli/commands/tui_command.py | 10 +- cognee/cli/tui/__init__.py | 0 cognee/cli/tui/config_tui.py | 361 +++++++++++++++++++++++++++++ 3 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 cognee/cli/tui/__init__.py create mode 100644 cognee/cli/tui/config_tui.py diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index 49d879a43..b9942ee28 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -3,6 +3,7 @@ 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 +from cognee.cli.tui.config_tui import ConfigTUIScreen from cognee.version import get_cognee_version from textual.app import App, ComposeResult from textual.widgets import ListView, ListItem, Static @@ -215,10 +216,11 @@ class TuiCommand(SupportsCliCommand): self._apply_highlight() def on_list_view_selected(self, event: ListView.Selected) -> None: - command_item = event.item.query_one(CommandItem) - command = command_item.command - fmt.echo(f"Selected command: {command}") - self.exit() + selected_index = event.index + if selected_index == 4: + self.app.push_screen(ConfigTUIScreen()) + else: + self.exit() def action_select(self) -> None: """Select the current item.""" diff --git a/cognee/cli/tui/__init__.py b/cognee/cli/tui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cognee/cli/tui/config_tui.py b/cognee/cli/tui/config_tui.py new file mode 100644 index 000000000..6dc6ca7ad --- /dev/null +++ b/cognee/cli/tui/config_tui.py @@ -0,0 +1,361 @@ +import argparse +import json +from typing import Optional, Tuple + +from cognee.cli.reference import SupportsCliCommand +from cognee.cli import DEFAULT_DOCS_URL +import cognee.cli.echo as fmt +from cognee.cli.exceptions import CliCommandException + +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import DataTable, Footer, Header, Input, Label, Button +from textual.containers import Container, Vertical, Horizontal +from textual.binding import Binding +from textual.coordinate import Coordinate + + +class EditModal(Screen): + """Modal screen for editing a config value.""" + + BINDINGS = [ + Binding("escape", "cancel", "Cancel"), + ] + + CSS = """ + EditModal { + align: center middle; + } + + #edit-dialog { + width: 60; + height: 13; + border: thick $primary; + background: $surface; + padding: 1 2; + } + + #edit-title { + text-align: center; + text-style: bold; + color: $accent; + margin-bottom: 1; + } + + #edit-key { + color: $text-muted; + margin-bottom: 1; + } + + #edit-input { + margin-bottom: 1; + } + + #edit-buttons { + align: center middle; + height: 3; + } + + Button { + margin: 0 1; + } + """ + + def __init__(self, key: str, default_value: str): + super().__init__() + self.key = key + self.default_value = default_value + self.result = None + + def compose(self) -> ComposeResult: + with Container(id="edit-dialog"): + yield Label("Edit Configuration", id="edit-title") + yield Label(f"Key: {self.key}", id="edit-key") + yield Label(f"Default: {self.default_value}", id="edit-key") + yield Input(placeholder="Enter new value", id="edit-input") + with Horizontal(id="edit-buttons"): + yield Button("Save", variant="primary", id="save-btn") + yield Button("Cancel", variant="default", id="cancel-btn") + + def on_mount(self) -> None: + self.query_one(Input).focus() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "save-btn": + input_widget = self.query_one(Input) + self.result = input_widget.value + self.dismiss(self.result) + else: + self.dismiss(None) + + def action_cancel(self) -> None: + self.dismiss(None) + + +class ConfirmModal(Screen): + """Modal screen for confirming reset action.""" + + BINDINGS = [ + Binding("escape", "cancel", "Cancel"), + ] + + CSS = """ + ConfirmModal { + align: center middle; + } + + #confirm-dialog { + width: 50; + height: 11; + border: thick $warning; + background: $surface; + padding: 1 2; + } + + #confirm-title { + text-align: center; + text-style: bold; + color: $warning; + margin-bottom: 1; + } + + #confirm-message { + text-align: center; + margin-bottom: 2; + } + + #confirm-buttons { + align: center middle; + height: 3; + } + + 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"Reset '{self.key}' to default?", id="confirm-message") + yield Label(f"Default value: {self.default_value}", id="confirm-message") + with Horizontal(id="confirm-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(Screen): + """Main config TUI screen.""" + + BINDINGS = [ + Binding("q", "quit_app", "Quit"), + Binding("escape", "go_back", "Back"), + Binding("e", "edit", "Edit"), + Binding("r", "reset", "Reset"), + Binding("up", "cursor_up", "Up", show=False), + Binding("down", "cursor_down", "Down", show=False), + ] + + CSS = """ + ConfigTUIScreen { + background: $surface; + } + + #config-header { + dock: top; + background: $boost; + color: $text; + content-align: center middle; + text-style: bold; + padding: 1; + border: solid $primary; + } + + #config-container { + height: 100%; + padding: 1; + } + + DataTable { + height: 1fr; + } + + #config-footer { + dock: bottom; + height: 3; + background: $boost; + content-align: center middle; + border: solid $primary; + } + """ + + # Config key mappings with defaults (from existing config.py) + CONFIG_KEYS = { + "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 compose(self) -> ComposeResult: + yield Label("🧠 cognee Config Manager", id="config-header") + + with Container(id="config-container"): + table = DataTable() + table.cursor_type = "row" + table.zebra_stripes = True + yield table + + yield Label( + "[↑↓] Navigate [e] Edit [r] Reset [Esc] Back [q] Quit", + id="config-footer" + ) + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_columns("KEY", "DEFAULT VALUE") + + # Add all config keys + for key, (method, default) in self.CONFIG_KEYS.items(): + display_default = "(empty)" if default == "" else str(default) + table.add_row(key, display_default) + + table.focus() + + def action_go_back(self) -> None: + """Go back to main menu.""" + self.app.pop_screen() + + def action_quit_app(self) -> None: + """Quit the entire application.""" + self.app.exit() + + def action_edit(self) -> None: + """Edit the selected config value.""" + table = self.query_one(DataTable) + + if table.cursor_coordinate.row < 0: + return + + row_key = table.get_row_at(table.cursor_coordinate.row) + key = str(row_key[0]) + default_value = str(row_key[1]) + + def handle_edit_result(value: Optional[str]) -> None: + if value is not None and value.strip(): + self._save_config(key, value) + + self.app.push_screen(EditModal(key, default_value), handle_edit_result) + + def action_reset(self) -> None: + """Reset the selected config to default.""" + table = self.query_one(DataTable) + + if table.cursor_coordinate.row < 0: + return + + row_key = table.get_row_at(table.cursor_coordinate.row) + key = str(row_key[0]) + + if key not in self.CONFIG_KEYS: + return + + method_name, default_value = self.CONFIG_KEYS[key] + display_default = "(empty)" if default_value == "" else str(default_value) + + def handle_confirm_result(confirmed: bool) -> None: + if confirmed: + self._reset_config(key, method_name, default_value) + + self.app.push_screen( + ConfirmModal(key, display_default), + handle_confirm_result + ) + + def _save_config(self, key: str, value: str) -> None: + """Save config value using cognee.config.set().""" + try: + import cognee + + # Try to parse as JSON (numbers, booleans, etc) + try: + parsed_value = json.loads(value) + except json.JSONDecodeError: + parsed_value = value + + cognee.config.set(key, parsed_value) + self.notify(f"✓ Set {key} = {parsed_value}", severity="information") + + except Exception as e: + self.notify(f"✗ Failed to set {key}: {str(e)}", severity="error") + + def _reset_config(self, key: str, method_name: str, default_value: any) -> None: + """Reset config to default using the mapped method.""" + try: + import cognee + + method = getattr(cognee.config, method_name) + method(default_value) + + display_default = "(empty)" if default_value == "" else str(default_value) + self.notify( + f"✓ Reset {key} to default: {display_default}", + severity="information" + ) + + except Exception as e: + self.notify(f"✗ Failed to reset {key}: {str(e)}", severity="error") + + +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: + class ConfigTUIApp(App): + """Simple app wrapper for config TUI.""" + + 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, + ) \ No newline at end of file From 99d0b82c8ea5044294b5d12b45f64741a26fd0e3 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 17:43:15 +0530 Subject: [PATCH 08/48] WIP - config tui screen --- cognee/cli/tui/config_tui.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cognee/cli/tui/config_tui.py b/cognee/cli/tui/config_tui.py index 6dc6ca7ad..59798705c 100644 --- a/cognee/cli/tui/config_tui.py +++ b/cognee/cli/tui/config_tui.py @@ -242,6 +242,16 @@ class ConfigTUIScreen(Screen): table.focus() + def action_cursor_up(self) -> None: + """Move cursor up in the table.""" + table = self.query_one(DataTable) + table.action_cursor_up() + + def action_cursor_down(self) -> None: + """Move cursor down in the table.""" + table = self.query_one(DataTable) + table.action_cursor_down() + def action_go_back(self) -> None: """Go back to main menu.""" self.app.pop_screen() From 2ece2e4b24b1f1c34ec58eb832ab0f5f10814a80 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 18:11:05 +0530 Subject: [PATCH 09/48] refactor to modularize to multiple screens --- cognee/cli/commands/tui_command.py | 199 +----------------- cognee/cli/tui/base_screen.py | 41 ++++ .../tui/{config_tui.py => config_screen.py} | 28 +-- cognee/cli/tui/home_screen.py | 182 ++++++++++++++++ 4 files changed, 238 insertions(+), 212 deletions(-) create mode 100644 cognee/cli/tui/base_screen.py rename cognee/cli/tui/{config_tui.py => config_screen.py} (94%) create mode 100644 cognee/cli/tui/home_screen.py diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index b9942ee28..d776cd82b 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -3,25 +3,10 @@ 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 -from cognee.cli.tui.config_tui import ConfigTUIScreen -from cognee.version import get_cognee_version -from textual.app import App, ComposeResult -from textual.widgets import ListView, ListItem, Static -from textual.containers import Container, Horizontal -from textual.binding import Binding +from cognee.cli.tui.home_screen import HomeScreen +from textual.app import App -def make_item(icon, command, description): - # 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 TuiCommand(SupportsCliCommand): @property @@ -42,190 +27,18 @@ class TuiCommand(SupportsCliCommand): def execute(self, args: argparse.Namespace) -> None: try: - class CommandItem(Static): - """A custom widget for command items with icon and description.""" - - def __init__(self, icon: str, command: str, description: str): - self.icon = icon - self.command = command - self.description = description - super().__init__() - - def render(self) -> str: - return f"{self.icon} {self.command:<12} {self.description}" - class CogneeTUI(App): + """Main TUI application for cognee.""" + CSS = """ Screen { background: $surface; } - - #header { - dock: top; - background: $boost; - color: $text; - content-align: center middle; - border: solid $primary; - text-style: bold; - padding: 1; - } - - #main-container { - height: 100%; - border: solid $primary; - background: $surface; - padding: 1; - } - #title-wrapper { - width: 100%; - height: auto; - align: center middle; - } - - #title { - text-align: center; - width: auto; - color: $accent; - text-style: bold; - padding: 0 10; - border: solid $accent; - margin-bottom: 2; - } - - ListView > ListItem { - width: 100%; - padding: 0; - margin: 0; - } - - ListView { - height: auto; - background: $surface; - border: none; - padding: 0 0; - } - - ListItem { - background: $surface; - color: $text; - padding: 0 1; - height: auto; - width: 100%; - } - - ListItem.highlighted { - background: $primary-darken-2; - } - - CommandItem { - width: 100%; - background: transparent; - } - - #footer-info { - dock: bottom; - height: 3; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } - - .cmd-row { - width: 100%; - height: auto; - align-horizontal: left; - padding: 0 1; - } - - .cmd-icon { - width: 4; - text-align: center; - } - - .cmd-name { - width: 14; - padding-left: 1; - } - - .cmd-desc { - width: 1fr; - overflow: auto; - padding-left: 1; - } - """ - BINDINGS = [ - Binding("q", "quit", "Quit", priority=True), - Binding("escape", "quit", "Quit", priority=True), - Binding("enter", "select", "Select", priority=True), - Binding("up", "nav_up", "Up", priority=True), - Binding("down", "nav_down", "Down", priority=True), - ] - - def __init__(self): - super().__init__() - self.lv = None - self.current_index = 0 - - def compose(self) -> ComposeResult: - version = get_cognee_version() - yield Static(f"🧠 cognee v{version}", id="header") - - with Container(id="main-container"): - with Container(id="title-wrapper"): - yield Static("Select Command", id="title") - 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"), - ) - - yield Static( - "↑↓: Navigate • Enter: Select • q/Esc: Quit", - id="footer-info" - ) - 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 - if selected_index == 4: - self.app.push_screen(ConfigTUIScreen()) - else: - self.exit() - - def action_select(self) -> None: - """Select the current item.""" - list_view = self.query_one(ListView) - list_view.action_select_cursor() + """Push the home screen on mount.""" + self.push_screen(HomeScreen()) app = CogneeTUI() app.run() diff --git a/cognee/cli/tui/base_screen.py b/cognee/cli/tui/base_screen.py new file mode 100644 index 000000000..b9d768c3d --- /dev/null +++ b/cognee/cli/tui/base_screen.py @@ -0,0 +1,41 @@ +from textual.screen import Screen +from textual.app import ComposeResult +from textual.widgets import Static + +from cognee.version import get_cognee_version + + +class BaseTUIScreen(Screen): + """Base screen class with constant header for all TUI screens.""" + + # Subclasses should override this CSS and add their own styles + CSS = """ + #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() \ No newline at end of file diff --git a/cognee/cli/tui/config_tui.py b/cognee/cli/tui/config_screen.py similarity index 94% rename from cognee/cli/tui/config_tui.py rename to cognee/cli/tui/config_screen.py index 59798705c..00f5d0139 100644 --- a/cognee/cli/tui/config_tui.py +++ b/cognee/cli/tui/config_screen.py @@ -1,6 +1,6 @@ import argparse import json -from typing import Optional, Tuple +from typing import Optional from cognee.cli.reference import SupportsCliCommand from cognee.cli import DEFAULT_DOCS_URL @@ -9,10 +9,11 @@ from cognee.cli.exceptions import CliCommandException from textual.app import App, ComposeResult from textual.screen import Screen -from textual.widgets import DataTable, Footer, Header, Input, Label, Button -from textual.containers import Container, Vertical, Horizontal +from textual.widgets import DataTable, Input, Label, Button +from textual.containers import Container, Horizontal from textual.binding import Binding -from textual.coordinate import Coordinate + +from cognee.cli.tui.base_screen import BaseTUIScreen class EditModal(Screen): @@ -158,7 +159,7 @@ class ConfirmModal(Screen): self.dismiss(False) -class ConfigTUIScreen(Screen): +class ConfigTUIScreen(BaseTUIScreen): """Main config TUI screen.""" BINDINGS = [ @@ -170,21 +171,11 @@ class ConfigTUIScreen(Screen): Binding("down", "cursor_down", "Down", show=False), ] - CSS = """ + CSS = BaseTUIScreen.CSS + """ ConfigTUIScreen { background: $surface; } - #config-header { - dock: top; - background: $boost; - color: $text; - content-align: center middle; - text-style: bold; - padding: 1; - border: solid $primary; - } - #config-container { height: 100%; padding: 1; @@ -217,15 +208,14 @@ class ConfigTUIScreen(Screen): "chunk_overlap": ("set_chunk_overlap", "10"), } - def compose(self) -> ComposeResult: - yield Label("🧠 cognee Config Manager", id="config-header") - + def compose_content(self) -> ComposeResult: with Container(id="config-container"): table = DataTable() table.cursor_type = "row" table.zebra_stripes = True yield table + def compose_footer(self) -> ComposeResult: yield Label( "[↑↓] Navigate [e] Edit [r] Reset [Esc] Back [q] Quit", id="config-footer" diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py new file mode 100644 index 000000000..f8f75fa7b --- /dev/null +++ b/cognee/cli/tui/home_screen.py @@ -0,0 +1,182 @@ +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 + + +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 + """ + HomeScreen { + background: $surface; + } + + #main-container { + height: 100%; + border: solid $primary; + background: $surface; + padding: 1; + } + + #title-wrapper { + width: 100%; + height: auto; + align: center middle; + } + + #title { + text-align: center; + width: auto; + color: $accent; + text-style: bold; + padding: 0 10; + border: solid $accent; + margin-bottom: 2; + } + + ListView > ListItem { + width: 100%; + padding: 0; + margin: 0; + } + + ListView { + height: auto; + background: $surface; + border: none; + padding: 0 0; + } + + ListItem { + background: $surface; + color: $text; + padding: 0 1; + height: auto; + width: 100%; + } + + ListItem.highlighted { + background: $primary-darken-2; + } + + .cmd-row { + width: 100%; + height: auto; + align-horizontal: left; + padding: 0 1; + } + + .cmd-icon { + width: 4; + text-align: center; + } + + .cmd-name { + width: 14; + padding-left: 1; + } + + .cmd-desc { + width: 1fr; + overflow: auto; + padding-left: 1; + } + + #home-footer { + dock: bottom; + height: 3; + background: $boost; + color: $text-muted; + content-align: center middle; + border: solid $primary; + } + """ + + def __init__(self): + super().__init__() + self.lv = None + self.current_index = 0 + + def compose_content(self) -> ComposeResult: + with Container(id="main-container"): + with Container(id="title-wrapper"): + yield Static("Select Command", id="title") + 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"), + ) + + def compose_footer(self) -> ComposeResult: + yield Static( + "↑↓: Navigate • Enter: Select • q/Esc: Quit", + id="home-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 + if selected_index == 4: + 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() \ No newline at end of file From 1d3f6dee0a9981821dc3d9d738cbe9d9ed339b1a Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 18:28:03 +0530 Subject: [PATCH 10/48] changes to config screen --- cognee/cli/tui/config_screen.py | 191 ++++++++++++++++---------------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index 00f5d0139..ec844b8bd 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -1,10 +1,8 @@ import argparse import json -from typing import Optional from cognee.cli.reference import SupportsCliCommand from cognee.cli import DEFAULT_DOCS_URL -import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException from textual.app import App, ComposeResult @@ -16,83 +14,6 @@ from textual.binding import Binding from cognee.cli.tui.base_screen import BaseTUIScreen -class EditModal(Screen): - """Modal screen for editing a config value.""" - - BINDINGS = [ - Binding("escape", "cancel", "Cancel"), - ] - - CSS = """ - EditModal { - align: center middle; - } - - #edit-dialog { - width: 60; - height: 13; - border: thick $primary; - background: $surface; - padding: 1 2; - } - - #edit-title { - text-align: center; - text-style: bold; - color: $accent; - margin-bottom: 1; - } - - #edit-key { - color: $text-muted; - margin-bottom: 1; - } - - #edit-input { - margin-bottom: 1; - } - - #edit-buttons { - align: center middle; - height: 3; - } - - Button { - margin: 0 1; - } - """ - - def __init__(self, key: str, default_value: str): - super().__init__() - self.key = key - self.default_value = default_value - self.result = None - - def compose(self) -> ComposeResult: - with Container(id="edit-dialog"): - yield Label("Edit Configuration", id="edit-title") - yield Label(f"Key: {self.key}", id="edit-key") - yield Label(f"Default: {self.default_value}", id="edit-key") - yield Input(placeholder="Enter new value", id="edit-input") - with Horizontal(id="edit-buttons"): - yield Button("Save", variant="primary", id="save-btn") - yield Button("Cancel", variant="default", id="cancel-btn") - - def on_mount(self) -> None: - self.query_one(Input).focus() - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "save-btn": - input_widget = self.query_one(Input) - self.result = input_widget.value - self.dismiss(self.result) - else: - self.dismiss(None) - - def action_cancel(self) -> None: - self.dismiss(None) - - class ConfirmModal(Screen): """Modal screen for confirming reset action.""" @@ -160,12 +81,13 @@ class ConfirmModal(Screen): class ConfigTUIScreen(BaseTUIScreen): - """Main config TUI screen.""" + """Main config TUI screen with inline editing.""" BINDINGS = [ Binding("q", "quit_app", "Quit"), - Binding("escape", "go_back", "Back"), + 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), @@ -185,6 +107,26 @@ class ConfigTUIScreen(BaseTUIScreen): height: 1fr; } + #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: 0; + } + + #inline-input { + width: 100%; + } + #config-footer { dock: bottom; height: 3; @@ -208,22 +150,29 @@ class ConfigTUIScreen(BaseTUIScreen): "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(id="config-container"): table = DataTable() 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 Label( - "[↑↓] Navigate [e] Edit [r] Reset [Esc] Back [q] Quit", + "[↑↓] Navigate [e] Edit [Enter] Save [r] Reset [Esc] Back [q] Quit", id="config-footer" ) def on_mount(self) -> None: table = self.query_one(DataTable) - table.add_columns("KEY", "DEFAULT VALUE") + table.add_columns("KEY", "VALUE") # Add all config keys for key, (method, default) in self.CONFIG_KEYS.items(): @@ -234,38 +183,90 @@ class ConfigTUIScreen(BaseTUIScreen): def action_cursor_up(self) -> None: """Move cursor up in the table.""" + if self.editing_key: + return # Don't navigate while editing table = self.query_one(DataTable) table.action_cursor_up() def action_cursor_down(self) -> None: """Move cursor down in the table.""" + if self.editing_key: + return # Don't navigate while editing table = self.query_one(DataTable) table.action_cursor_down() - def action_go_back(self) -> None: - """Go back to main menu.""" - self.app.pop_screen() + def action_cancel_or_back(self) -> None: + """Cancel editing or go back to main menu.""" + if self.editing_key: + self._cancel_edit() + else: + self.app.pop_screen() def action_quit_app(self) -> None: """Quit the entire application.""" self.app.exit() def action_edit(self) -> None: - """Edit the selected config value.""" - table = self.query_one(DataTable) + """Start inline editing for the selected config value.""" + if self.editing_key: + return # Already editing + table = self.query_one(DataTable) if table.cursor_coordinate.row < 0: return - row_key = table.get_row_at(table.cursor_coordinate.row) - key = str(row_key[0]) - default_value = str(row_key[1]) + row_data = table.get_row_at(table.cursor_coordinate.row) + key = str(row_data[0]) + default_value = str(row_data[1]) - def handle_edit_result(value: Optional[str]) -> None: - if value is not None and value.strip(): - self._save_config(key, value) + self.editing_key = key - self.app.push_screen(EditModal(key, default_value), handle_edit_result) + # Show the inline edit container + edit_container = self.query_one("#inline-edit-container") + edit_container.add_class("visible") + + # Update label and input + label = self.query_one("#edit-label", Label) + label.update(f"Editing: {key} (default: {default_value})") + + input_widget = self.query_one("#inline-input", Input) + input_widget.value = "" + input_widget.placeholder = f"Enter new value for {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() + + if value: + self._save_config(self.editing_key, value) + + self._cancel_edit() + + def _cancel_edit(self) -> None: + """Cancel the current edit and hide the input.""" + self.editing_key = None + + # Hide the inline edit container + edit_container = self.query_one("#inline-edit-container") + edit_container.remove_class("visible") + + # Clear input + input_widget = self.query_one("#inline-input", Input) + input_widget.value = "" + + # Return focus to table + table = self.query_one(DataTable) + table.focus() + + def on_input_submitted(self, event: Input.Submitted) -> None: + """Handle Enter key in the input field.""" + 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.""" From 8a660a6e588795d75914485f10bfc4e480819451 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 18:34:18 +0530 Subject: [PATCH 11/48] cleanup footer in config screen --- cognee/cli/tui/config_screen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index ec844b8bd..e767f3cf9 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -7,7 +7,7 @@ from cognee.cli.exceptions import CliCommandException from textual.app import App, ComposeResult from textual.screen import Screen -from textual.widgets import DataTable, Input, Label, Button +from textual.widgets import DataTable, Input, Label, Button, Static from textual.containers import Container, Horizontal from textual.binding import Binding @@ -131,6 +131,7 @@ class ConfigTUIScreen(BaseTUIScreen): dock: bottom; height: 3; background: $boost; + color: $text-muted; content-align: center middle; border: solid $primary; } @@ -165,8 +166,8 @@ class ConfigTUIScreen(BaseTUIScreen): yield Input(placeholder="Enter new value", id="inline-input") def compose_footer(self) -> ComposeResult: - yield Label( - "[↑↓] Navigate [e] Edit [Enter] Save [r] Reset [Esc] Back [q] Quit", + yield Static( + "↑↓: Navigate • e: Edit • Enter: Save • r: Reset • Esc: Back • q: Quit", id="config-footer" ) From 73ef9c2c27335bd9ea59bd4b6912e7bb73718520 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 18:54:39 +0530 Subject: [PATCH 12/48] WIP - adding a add data screen --- cognee/cli/tui/add_screen.py | 191 ++++++++++++++++++++++++++++++++++ cognee/cli/tui/home_screen.py | 5 +- 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 cognee/cli/tui/add_screen.py diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py new file mode 100644 index 000000000..bfcf3b007 --- /dev/null +++ b/cognee/cli/tui/add_screen.py @@ -0,0 +1,191 @@ +import asyncio +from textual.app import ComposeResult +from textual.widgets import Input, Label, Button, 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 + """ + AddTUIScreen { + background: $surface; + } + + #add-container { + height: auto; + padding: 1; + } + + #add-form { + width: 100%; + height: auto; + border: solid $primary; + padding: 2; + background: $surface; + } + + #form-title { + text-align: center; + width: 100%; + text-style: bold; + color: $accent; + margin-bottom: 2; + } + + .field-label { + color: $text-muted; + margin-top: 1; + margin-bottom: 0; + } + + Input { + width: 100%; + margin-bottom: 1; + } + + #data-input { + height: 8; + min-height: 8; + } + + #submit-btn { + width: 20; + margin-top: 2; + } + + #status-message { + margin-top: 2; + text-align: center; + height: auto; + } + + #add-footer { + dock: bottom; + height: 3; + background: $boost; + color: $text-muted; + content-align: center middle; + border: solid $primary; + } + """ + + def __init__(self): + super().__init__() + self.is_processing = False + + def compose_content(self) -> ComposeResult: + with Container(id="add-container"): + yield Label("Add Data to Cognee", id="form-title") + with Vertical(id="add-form"): + yield Label("Data (text, file path, URL, or S3 path):", classes="field-label") + yield TextArea( + "", + id="data-input", + ) + + yield Label("Dataset Name:", classes="field-label") + yield Input( + placeholder="main_dataset", + value="main_dataset", + id="dataset-input" + ) + + yield Button("Add Data [Ctrl+S]", variant="primary", id="submit-btn") + yield Static("", id="status-message") + + def compose_footer(self) -> ComposeResult: + yield Static( + "Ctrl+S: Submit • Esc: Back • q: Quit", + id="add-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 on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button press.""" + if event.button.id == "submit-btn" and 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("#status-message", 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 + self.query_one("#submit-btn", Button).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("#status-message", 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 + self.query_one("#submit-btn", Button).disabled = False + data_input.focus() \ No newline at end of file diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index f8f75fa7b..0c3cc8d18 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -5,6 +5,7 @@ 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 def make_item(icon: str, command: str, description: str) -> ListItem: @@ -167,7 +168,9 @@ class HomeScreen(BaseTUIScreen): def on_list_view_selected(self, event: ListView.Selected) -> None: selected_index = event.index - if selected_index == 4: + if selected_index == 0: # add + self.app.push_screen(AddTUIScreen()) + elif selected_index == 4: # config self.app.push_screen(ConfigTUIScreen()) else: self.app.exit() From f36f13a30b84583d4501e808d5c1c9b68b5e1939 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:01:03 +0530 Subject: [PATCH 13/48] cleanup of add screen --- cognee/cli/tui/add_screen.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index bfcf3b007..f36badde7 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -1,7 +1,7 @@ import asyncio from textual.app import ComposeResult from textual.widgets import Input, Label, Button, Static, TextArea -from textual.containers import Container, Vertical +from textual.containers import Container, Vertical, Horizontal from textual.binding import Binding from cognee.cli.tui.base_screen import BaseTUIScreen @@ -25,6 +25,7 @@ class AddTUIScreen(BaseTUIScreen): #add-container { height: auto; padding: 1; + content-align: center middle; } #add-form { @@ -59,11 +60,6 @@ class AddTUIScreen(BaseTUIScreen): min-height: 8; } - #submit-btn { - width: 20; - margin-top: 2; - } - #status-message { margin-top: 2; text-align: center; @@ -100,13 +96,11 @@ class AddTUIScreen(BaseTUIScreen): value="main_dataset", id="dataset-input" ) - - yield Button("Add Data [Ctrl+S]", variant="primary", id="submit-btn") - yield Static("", id="status-message") + yield Static("", id="status-message") def compose_footer(self) -> ComposeResult: yield Static( - "Ctrl+S: Submit • Esc: Back • q: Quit", + "Ctrl+S: Add • Esc: Back • q: Quit", id="add-footer" ) From 4e5eaa9f9dde95ee8de6824ccb40b51819e7ac47 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:06:23 +0530 Subject: [PATCH 14/48] WIP - added a basic version of cognify screen --- cognee/cli/tui/cognify_screen.py | 215 +++++++++++++++++++++++++++++++ cognee/cli/tui/home_screen.py | 3 + 2 files changed, 218 insertions(+) create mode 100644 cognee/cli/tui/cognify_screen.py diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py new file mode 100644 index 000000000..951137970 --- /dev/null +++ b/cognee/cli/tui/cognify_screen.py @@ -0,0 +1,215 @@ +import asyncio +from textual.app import ComposeResult +from textual.widgets import Input, Label, Button, 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 + """ + CognifyTUIScreen { + background: $surface; + } + + #cognify-container { + height: auto; + padding: 1; + content-align: center middle; + } + + #cognify-form { + width: 100%; + height: auto; + border: solid $primary; + padding: 2; + background: $surface; + } + + #form-title { + text-align: center; + width: 100%; + text-style: bold; + color: $accent; + margin-bottom: 2; + } + + .field-label { + color: $text-muted; + margin-top: 1; + margin-bottom: 0; + } + + Input { + width: 100%; + margin-bottom: 1; + } + + Checkbox { + margin-top: 1; + margin-bottom: 1; + } + + RadioSet { + margin-top: 0; + margin-bottom: 1; + height: auto; + } + + RadioButton { + height: 1; + } + + #status-message { + margin-top: 2; + text-align: center; + height: auto; + } + + #cognify-footer { + dock: bottom; + height: 3; + background: $boost; + color: $text-muted; + content-align: center middle; + border: solid $primary; + } + """ + + def __init__(self): + super().__init__() + self.is_processing = False + + def compose_content(self) -> ComposeResult: + with Container(id="cognify-container"): + yield Label("Cognify Data", id="form-title") + with Vertical(id="cognify-form"): + yield Label("Dataset Name (optional, leave empty for all):", classes="field-label") + yield Input( + placeholder="Leave empty to process all datasets", + value="", + id="dataset-input" + ) + + yield Label("Chunker Type:", classes="field-label") + 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("", id="status-message") + + def compose_footer(self) -> ComposeResult: + yield Static( + "Ctrl+S: Start • Esc: Back • q: Quit", + id="cognify-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 on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button press.""" + if event.button.id == "submit-btn" and 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("#status-message", 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 + self.query_one("#submit-btn", Button).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("#status-message", Static) + + try: + import cognee + from cognee.modules.chunking.TextChunker import TextChunker + + # Get chunker class + chunker_class = TextChunker + if chunker_type == "LangchainChunker": + try: + from cognee.modules.chunking.LangchainChunker import LangchainChunker + chunker_class = LangchainChunker + except ImportError: + status.update("[yellow]⚠ LangchainChunker not available, using TextChunker[/yellow]") + elif chunker_type == "CsvChunker": + try: + from cognee.modules.chunking.CsvChunker import CsvChunker + chunker_class = CsvChunker + except ImportError: + status.update("[yellow]⚠ CsvChunker not available, using TextChunker[/yellow]") + + # Prepare datasets parameter + datasets = [dataset_name] if dataset_name else None + + 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 + self.query_one("#submit-btn", Button).disabled = False + dataset_input.focus() \ No newline at end of file diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 0c3cc8d18..2c89daff2 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -6,6 +6,7 @@ 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 def make_item(icon: str, command: str, description: str) -> ListItem: @@ -170,6 +171,8 @@ class HomeScreen(BaseTUIScreen): selected_index = event.index if selected_index == 0: # add self.app.push_screen(AddTUIScreen()) + elif selected_index == 2: # cognify + self.app.push_screen(CognifyTUIScreen()) elif selected_index == 4: # config self.app.push_screen(ConfigTUIScreen()) else: From d0c6341a9d2791c22be7298d38fb039a0e18e0df Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:14:40 +0530 Subject: [PATCH 15/48] WIP - added a basic version of search screen --- cognee/cli/tui/home_screen.py | 3 + cognee/cli/tui/search_screen.py | 193 ++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 cognee/cli/tui/search_screen.py diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 2c89daff2..2e3ead9be 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -7,6 +7,7 @@ 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 def make_item(icon: str, command: str, description: str) -> ListItem: @@ -171,6 +172,8 @@ class HomeScreen(BaseTUIScreen): selected_index = event.index 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 == 4: # config diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py new file mode 100644 index 000000000..5bc1ddb0b --- /dev/null +++ b/cognee/cli/tui/search_screen.py @@ -0,0 +1,193 @@ +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 + """ + SearchTUIScreen { + background: $surface; + } + + #search-container { + height: 100%; + padding: 1; + } + + #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; + } + + #search-footer { + dock: bottom; + height: 3; + background: $boost; + color: $text-muted; + content-align: center middle; + border: solid $primary; + } + """ + + def __init__(self): + super().__init__() + self.is_searching = False + + def compose_content(self) -> ComposeResult: + with Container(id="search-container"): + with Vertical(id="search-form"): + yield Label("Query:") + yield Input(placeholder="Enter your search query...", id="query-input") + yield Label("Search Type:") + yield Select( + [ + ("Graph Completion (Recommended)", "GRAPH_COMPLETION"), + ("RAG Completion", "RAG_COMPLETION"), + ("Chunks", "CHUNKS"), + ("Summaries", "SUMMARIES"), + ("Code", "CODE"), + ], + 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", + id="search-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 \ No newline at end of file From cdd9518937c1b18ca862d2bb1686de43cfa13b36 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:18:45 +0530 Subject: [PATCH 16/48] WIP - added a basic version of delete screen --- cognee/cli/tui/delete_screen.py | 333 ++++++++++++++++++++++++++++++++ cognee/cli/tui/home_screen.py | 3 + 2 files changed, 336 insertions(+) create mode 100644 cognee/cli/tui/delete_screen.py diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py new file mode 100644 index 000000000..d53fa9ca3 --- /dev/null +++ b/cognee/cli/tui/delete_screen.py @@ -0,0 +1,333 @@ +import asyncio +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.get_deletion_counts import get_deletion_counts + + +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+d", "delete_all", "Delete All"), + ] + + CSS = BaseTUIScreen.CSS + """ + DeleteTUIScreen { + background: $surface; + } + + #delete-container { + height: auto; + padding: 2; + align: center top; + } + + #delete-form { + width: 80; + height: auto; + border: solid $primary; + background: $surface; + padding: 2; + } + + #form-title { + text-align: center; + text-style: bold; + color: $accent; + margin-bottom: 2; + } + + .input-group { + height: auto; + margin-bottom: 2; + } + + .input-label { + color: $text-muted; + margin-bottom: 1; + } + + Input { + width: 100%; + margin-bottom: 1; + } + + #button-group { + height: auto; + align: center middle; + margin-top: 2; + } + + Button { + margin: 0 1; + } + + #status-message { + text-align: center; + margin-top: 2; + height: auto; + } + + #delete-footer { + dock: bottom; + height: 3; + background: $boost; + color: $text-muted; + content-align: center middle; + border: solid $primary; + } + """ + + def __init__(self): + super().__init__() + self.is_processing = False + + def compose_content(self) -> ComposeResult: + with Container(id="delete-container"): + with Vertical(id="delete-form"): + yield Label("🗑️ Delete Data", id="form-title") + + with Vertical(classes="input-group"): + yield Label("Dataset Name (optional):", classes="input-label") + yield Input( + placeholder="Enter dataset name to delete specific dataset", + id="dataset-input" + ) + + with Vertical(classes="input-group"): + yield Label("User ID (optional):", classes="input-label") + yield Input( + placeholder="Enter user ID to delete user's data", + 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 Button("Cancel", variant="default", id="cancel-btn") + + yield Static("", id="status-message") + + def compose_footer(self) -> ComposeResult: + yield Static( + "Enter dataset/user • Click Delete • Ctrl+D: Delete All • Esc: Back • q: Quit", + id="delete-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_all(self) -> None: + """Trigger delete all action.""" + if not self.is_processing: + self._handle_delete_all() + + async def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button presses.""" + if self.is_processing: + return + + if event.button.id == "delete-btn": + await self._handle_delete() + elif event.button.id == "delete-all-btn": + self._handle_delete_all() + elif event.button.id == "cancel-btn": + self.app.pop_screen() + + async def _handle_delete(self) -> None: + """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) + status = self.query_one("#status-message", Static) + + 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...") + + try: + # Get preview of what will be deleted + preview_data = await get_deletion_counts( + dataset_name=dataset_name, + user_id=user_id, + all_data=False, + ) + + if not preview_data: + status.update("✓ No data found to delete") + self.is_processing = False + return + + # Show preview and confirm + preview_msg = ( + f"About to delete:\n" + f"Datasets: {preview_data.datasets}\n" + f"Entries: {preview_data.entries}\n" + f"Users: {preview_data.users}" + ) + status.update(preview_msg) + + # Perform deletion + import cognee + await cognee.delete(dataset_name=dataset_name, user_id=user_id) + + operation = f"dataset '{dataset_name}'" if dataset_name else f"data for user '{user_id}'" + status.update(f"✓ Successfully deleted {operation}") + + # Clear inputs + dataset_input.value = "" + user_input.value = "" + + except Exception as e: + status.update(f"✗ Error: {str(e)}") + finally: + self.is_processing = False + + def _handle_delete_all(self) -> None: + """Handle delete all operation with confirmation.""" + if self.is_processing: + return + + def handle_confirm(confirmed: bool) -> None: + if confirmed: + self.run_worker(self._perform_delete_all()) + + self.app.push_screen(DeleteAllConfirmModal(), handle_confirm) + + async def _perform_delete_all(self) -> None: + """Perform the actual delete all operation.""" + status = self.query_one("#status-message", Static) + self.is_processing = True + + try: + status.update("🔍 Checking all data...") + + # Get preview + preview_data = await get_deletion_counts( + dataset_name=None, + user_id=None, + all_data=True, + ) + + if not preview_data: + status.update("✓ No data found to delete") + self.is_processing = False + return + + preview_msg = ( + f"Deleting ALL data:\n" + f"Datasets: {preview_data.datasets}\n" + f"Entries: {preview_data.entries}\n" + f"Users: {preview_data.users}" + ) + status.update(preview_msg) + + # Perform deletion + import cognee + await cognee.delete(dataset_name=None, user_id=None) + + status.update("✓ Successfully deleted all data") + + # 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 + + +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: 13; + border: thick $error; + background: $surface; + padding: 2; + } + + #confirm-title { + text-align: center; + text-style: bold; + color: $error; + margin-bottom: 1; + } + + #confirm-message { + text-align: center; + margin-bottom: 1; + } + + #confirm-warning { + text-align: center; + color: $warning; + text-style: bold; + margin-bottom: 2; + } + + #confirm-buttons { + align: center middle; + height: 3; + } + + Button { + margin: 0 1; + } + """ + + 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", id="confirm-message") + yield Label("This operation is IRREVERSIBLE!", id="confirm-warning") + with Horizontal(id="confirm-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) \ No newline at end of file diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 2e3ead9be..3ab260247 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -8,6 +8,7 @@ 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: @@ -176,6 +177,8 @@ class HomeScreen(BaseTUIScreen): 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: From 06eba205c8f2903f642353a6aeb2276d63af2e7f Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:24:26 +0530 Subject: [PATCH 17/48] minor UI changes to delete screen --- cognee/cli/tui/delete_screen.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index d53fa9ca3..8da4f3fc5 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -14,6 +14,7 @@ class DeleteTUIScreen(BaseTUIScreen): BINDINGS = [ Binding("q", "quit_app", "Quit"), Binding("escape", "back", "Back"), + Binding("ctrl+s", "delete", "Delete"), Binding("ctrl+d", "delete_all", "Delete All"), ] @@ -110,13 +111,12 @@ class DeleteTUIScreen(BaseTUIScreen): with Horizontal(id="button-group"): yield Button("Delete", variant="error", id="delete-btn") yield Button("Delete All", variant="error", id="delete-all-btn") - yield Button("Cancel", variant="default", id="cancel-btn") yield Static("", id="status-message") def compose_footer(self) -> ComposeResult: yield Static( - "Enter dataset/user • Click Delete • Ctrl+D: Delete All • Esc: Back • q: Quit", + "Ctrl+s: Delete • Ctrl+d: Delete All • Esc: Back • q: Quit", id="delete-footer" ) @@ -134,10 +134,12 @@ class DeleteTUIScreen(BaseTUIScreen): """Quit the entire application.""" self.app.exit() + def action_delete(self) -> None: + """Delete the dataset.""" + self._handle_delete() + def action_delete_all(self) -> None: - """Trigger delete all action.""" - if not self.is_processing: - self._handle_delete_all() + self._handle_delete_all() async def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" From 978a6c735919d38cb5432799cd9a3e0240915f88 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:43:26 +0530 Subject: [PATCH 18/48] minor changes for home screen --- cognee/cli/tui/home_screen.py | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 3ab260247..744fc5944 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -50,6 +50,8 @@ class HomeScreen(BaseTUIScreen): width: 100%; height: auto; align: center middle; + content-align: center middle; + padding-bottom: 1; } #title { @@ -61,12 +63,33 @@ class HomeScreen(BaseTUIScreen): border: solid $accent; margin-bottom: 2; } + + #title-sub { + text-align: center; + width: auto; + color: $text-muted; + padding-bottom: 1; + margin-bottom: 1; + } 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; @@ -78,36 +101,48 @@ class HomeScreen(BaseTUIScreen): ListItem { background: $surface; color: $text; - padding: 0 1; - height: auto; width: 100%; + min-height: 2; + align-vertical: middle; + } + + ListItem:focus { + outline: none; } ListItem.highlighted { - background: $primary-darken-2; + background: $primary-darken-3; + color: $text; + } + ListItem.highlighted .cmd-name { + text-style: bold; + color: $accent; } .cmd-row { width: 100%; height: auto; align-horizontal: left; - padding: 0 1; + align-vertical: middle; } .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; } #home-footer { @@ -135,6 +170,8 @@ class HomeScreen(BaseTUIScreen): 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: From da32502b0b399785cbbdf656e680f4ec85c7296f Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 19:45:14 +0530 Subject: [PATCH 19/48] minor tweaks --- cognee/cli/commands/tui_command.py | 2 +- cognee/cli/tui/__init__.py | 1 + cognee/cli/tui/add_screen.py | 2 +- cognee/cli/tui/base_screen.py | 2 +- cognee/cli/tui/cognify_screen.py | 2 +- cognee/cli/tui/config_screen.py | 2 +- cognee/cli/tui/delete_screen.py | 2 +- cognee/cli/tui/home_screen.py | 2 +- cognee/cli/tui/search_screen.py | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index d776cd82b..c7136cfde 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -53,4 +53,4 @@ class TuiCommand(SupportsCliCommand): f"Failed to launch TUI: {str(ex)}", docs_url=self.docs_url, raiseable_exception=ex, - ) \ No newline at end of file + ) diff --git a/cognee/cli/tui/__init__.py b/cognee/cli/tui/__init__.py index e69de29bb..8b1378917 100644 --- a/cognee/cli/tui/__init__.py +++ b/cognee/cli/tui/__init__.py @@ -0,0 +1 @@ + diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index f36badde7..60136ecf1 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -182,4 +182,4 @@ class AddTUIScreen(BaseTUIScreen): data_input.disabled = False dataset_input.disabled = False self.query_one("#submit-btn", Button).disabled = False - data_input.focus() \ No newline at end of file + data_input.focus() diff --git a/cognee/cli/tui/base_screen.py b/cognee/cli/tui/base_screen.py index b9d768c3d..6cd3f8dcc 100644 --- a/cognee/cli/tui/base_screen.py +++ b/cognee/cli/tui/base_screen.py @@ -38,4 +38,4 @@ class BaseTUIScreen(Screen): """Compose the screen with header, content, and footer.""" yield from self.compose_header() yield from self.compose_content() - yield from self.compose_footer() \ No newline at end of file + yield from self.compose_footer() diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index 951137970..a13e14c71 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -212,4 +212,4 @@ class CognifyTUIScreen(BaseTUIScreen): chunker_radio.disabled = False background_checkbox.disabled = False self.query_one("#submit-btn", Button).disabled = False - dataset_input.focus() \ No newline at end of file + dataset_input.focus() diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index e767f3cf9..a0d5064b8 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -360,4 +360,4 @@ class ConfigTUICommand(SupportsCliCommand): f"Failed to launch config TUI: {str(ex)}", docs_url=self.docs_url, raiseable_exception=ex, - ) \ No newline at end of file + ) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 8da4f3fc5..a2b2386b9 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -332,4 +332,4 @@ class DeleteAllConfirmModal(BaseTUIScreen): self.dismiss(False) def action_cancel(self) -> None: - self.dismiss(False) \ No newline at end of file + self.dismiss(False) diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 744fc5944..e4957a3ea 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -228,4 +228,4 @@ class HomeScreen(BaseTUIScreen): def action_quit_app(self) -> None: """Quit the entire application.""" - self.app.exit() \ No newline at end of file + self.app.exit() diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 5bc1ddb0b..87ee1dd1f 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -190,4 +190,4 @@ class SearchTUIScreen(BaseTUIScreen): self.notify(f"Search failed: {str(e)}", severity="error") finally: - self.is_searching = False \ No newline at end of file + self.is_searching = False From de6bcbfe65997630fb3c7e1da7aa92d3cecb0e3e Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 21:42:43 +0530 Subject: [PATCH 20/48] home screen padding --- cognee/cli/tui/home_screen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index e4957a3ea..56a101f07 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -102,8 +102,7 @@ class HomeScreen(BaseTUIScreen): background: $surface; color: $text; width: 100%; - min-height: 2; - align-vertical: middle; + height: 3; } ListItem:focus { @@ -124,6 +123,7 @@ class HomeScreen(BaseTUIScreen): height: auto; align-horizontal: left; align-vertical: middle; + height: 1fr; } .cmd-icon { From bd82b65f5d081c8519a777836fb5419dca609c77 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 22:02:52 +0530 Subject: [PATCH 21/48] UI finetuning --- cognee/cli/tui/add_screen.py | 4 ++-- cognee/cli/tui/cognify_screen.py | 6 +++--- cognee/cli/tui/config_screen.py | 2 +- cognee/cli/tui/delete_screen.py | 3 ++- cognee/cli/tui/home_screen.py | 2 +- cognee/cli/tui/search_screen.py | 22 +++++++++++++++++++--- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index 60136ecf1..d647c5edf 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -47,7 +47,7 @@ class AddTUIScreen(BaseTUIScreen): .field-label { color: $text-muted; margin-top: 1; - margin-bottom: 0; + margin-bottom: 1; } Input { @@ -68,7 +68,7 @@ class AddTUIScreen(BaseTUIScreen): #add-footer { dock: bottom; - height: 3; + padding: 1 0; background: $boost; color: $text-muted; content-align: center middle; diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index a13e14c71..24eae1e6b 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -47,7 +47,7 @@ class CognifyTUIScreen(BaseTUIScreen): .field-label { color: $text-muted; margin-top: 1; - margin-bottom: 0; + margin-bottom: 1; } Input { @@ -78,7 +78,7 @@ class CognifyTUIScreen(BaseTUIScreen): #cognify-footer { dock: bottom; - height: 3; + padding: 1 0; background: $boost; color: $text-muted; content-align: center middle; @@ -92,7 +92,7 @@ class CognifyTUIScreen(BaseTUIScreen): def compose_content(self) -> ComposeResult: with Container(id="cognify-container"): - yield Label("Cognify Data", id="form-title") + yield Label("⚡ Cognify Data", id="form-title") with Vertical(id="cognify-form"): yield Label("Dataset Name (optional, leave empty for all):", classes="field-label") yield Input( diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index a0d5064b8..f8a17c91f 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -129,7 +129,7 @@ class ConfigTUIScreen(BaseTUIScreen): #config-footer { dock: bottom; - height: 3; + padding: 1 0; background: $boost; color: $text-muted; content-align: center middle; diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index a2b2386b9..bfd204147 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -42,6 +42,7 @@ class DeleteTUIScreen(BaseTUIScreen): text-style: bold; color: $accent; margin-bottom: 2; + width: 100%; } .input-group { @@ -77,7 +78,7 @@ class DeleteTUIScreen(BaseTUIScreen): #delete-footer { dock: bottom; - height: 3; + padding: 1 0; background: $boost; color: $text-muted; content-align: center middle; diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 56a101f07..77a3bbc0c 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -147,7 +147,7 @@ class HomeScreen(BaseTUIScreen): #home-footer { dock: bottom; - height: 3; + padding: 1 0; background: $boost; color: $text-muted; content-align: center middle; diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 87ee1dd1f..4bfc14341 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -20,6 +20,15 @@ class SearchTUIScreen(BaseTUIScreen): SearchTUIScreen { background: $surface; } + + #form-title { + text-align: center; + text-style: bold; + color: $accent; + margin-bottom: 2; + width: 100%; + } + #search-container { height: 100%; @@ -58,10 +67,16 @@ class SearchTUIScreen(BaseTUIScreen): height: 1fr; overflow-y: auto; } + + .field-label { + color: $text-muted; + margin-top: 1; + margin-bottom: 1; + } #search-footer { dock: bottom; - height: 3; + padding: 1 0; background: $boost; color: $text-muted; content-align: center middle; @@ -75,10 +90,11 @@ class SearchTUIScreen(BaseTUIScreen): def compose_content(self) -> ComposeResult: with Container(id="search-container"): + yield Label("🔍 Search Data", id="form-title") with Vertical(id="search-form"): - yield Label("Query:") + yield Label("Query:", classes="field-label") yield Input(placeholder="Enter your search query...", id="query-input") - yield Label("Search Type:") + yield Label("Search Type:", classes="field-label") yield Select( [ ("Graph Completion (Recommended)", "GRAPH_COMPLETION"), From 7670c3c172f5bd3b0b9a9f52d832ff52bc42846b Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 22:16:36 +0530 Subject: [PATCH 22/48] UI finetuning --- cognee/cli/tui/config_screen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index f8a17c91f..27ff0b3e6 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -100,11 +100,12 @@ class ConfigTUIScreen(BaseTUIScreen): #config-container { height: 100%; - padding: 1; + border: solid $primary; } DataTable { height: 1fr; + text-align: center; } #inline-edit-container { @@ -173,8 +174,7 @@ class ConfigTUIScreen(BaseTUIScreen): def on_mount(self) -> None: table = self.query_one(DataTable) - table.add_columns("KEY", "VALUE") - + key_col, value_col = table.add_columns("KEY", "VALUE") # Add all config keys for key, (method, default) in self.CONFIG_KEYS.items(): display_default = "(empty)" if default == "" else str(default) From ba6ee4a10a132a2c5d059a7899f0585fe2643ffb Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 22:40:58 +0530 Subject: [PATCH 23/48] WIP - making screens common --- cognee/cli/tui/add_screen.py | 70 +++---------------- cognee/cli/tui/base_screen.py | 3 +- cognee/cli/tui/cognify_screen.py | 70 +++---------------- cognee/cli/tui/common_styles.py | 113 +++++++++++++++++++++++++++++++ cognee/cli/tui/config_screen.py | 36 ++-------- cognee/cli/tui/delete_screen.py | 88 ++++-------------------- cognee/cli/tui/home_screen.py | 22 +----- cognee/cli/tui/search_screen.py | 34 ++-------- 8 files changed, 157 insertions(+), 279 deletions(-) create mode 100644 cognee/cli/tui/common_styles.py diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index d647c5edf..66ca7a7b4 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -18,62 +18,10 @@ class AddTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - AddTUIScreen { - background: $surface; - } - - #add-container { - height: auto; - padding: 1; - content-align: center middle; - } - - #add-form { - width: 100%; - height: auto; - border: solid $primary; - padding: 2; - background: $surface; - } - - #form-title { - text-align: center; - width: 100%; - text-style: bold; - color: $accent; - margin-bottom: 2; - } - - .field-label { - color: $text-muted; - margin-top: 1; - margin-bottom: 1; - } - - Input { - width: 100%; - margin-bottom: 1; - } - #data-input { height: 8; min-height: 8; } - - #status-message { - margin-top: 2; - text-align: center; - height: auto; - } - - #add-footer { - dock: bottom; - padding: 1 0; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } """ def __init__(self): @@ -81,27 +29,27 @@ class AddTUIScreen(BaseTUIScreen): self.is_processing = False def compose_content(self) -> ComposeResult: - with Container(id="add-container"): - yield Label("Add Data to Cognee", id="form-title") - with Vertical(id="add-form"): - yield Label("Data (text, file path, URL, or S3 path):", classes="field-label") + with Container(classes="tui-content-container"): + yield Label("Add Data to Cognee", classes="tui-title") + with Vertical(classes="tui-form"): + yield Label("Data (text, file path, URL, or S3 path):", classes="tui-label-spaced") yield TextArea( "", id="data-input", ) - yield Label("Dataset Name:", classes="field-label") + yield Label("Dataset Name:", classes="tui-label-spaced") yield Input( placeholder="main_dataset", value="main_dataset", id="dataset-input" ) - yield Static("", id="status-message") + yield Static("", classes="tui-status") def compose_footer(self) -> ComposeResult: yield Static( "Ctrl+S: Add • Esc: Back • q: Quit", - id="add-footer" + classes="tui-footer" ) def on_mount(self) -> None: @@ -136,7 +84,7 @@ class AddTUIScreen(BaseTUIScreen): """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("#status-message", Static) + status = self.query_one(".tui-status", Static) data = data_input.text.strip() dataset_name = dataset_input.value.strip() or "main_dataset" @@ -158,7 +106,7 @@ class AddTUIScreen(BaseTUIScreen): async def _add_data_async(self, data: str, dataset_name: str) -> None: """Async function to add data to cognee.""" - status = self.query_one("#status-message", Static) + status = self.query_one(".tui-status", Static) try: import cognee diff --git a/cognee/cli/tui/base_screen.py b/cognee/cli/tui/base_screen.py index 6cd3f8dcc..7256e0353 100644 --- a/cognee/cli/tui/base_screen.py +++ b/cognee/cli/tui/base_screen.py @@ -3,13 +3,14 @@ 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 = """ + CSS = COMMON_STYLES + """ #header { dock: top; background: $boost; diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index 24eae1e6b..5d33377ea 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -18,43 +18,6 @@ class CognifyTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - CognifyTUIScreen { - background: $surface; - } - - #cognify-container { - height: auto; - padding: 1; - content-align: center middle; - } - - #cognify-form { - width: 100%; - height: auto; - border: solid $primary; - padding: 2; - background: $surface; - } - - #form-title { - text-align: center; - width: 100%; - text-style: bold; - color: $accent; - margin-bottom: 2; - } - - .field-label { - color: $text-muted; - margin-top: 1; - margin-bottom: 1; - } - - Input { - width: 100%; - margin-bottom: 1; - } - Checkbox { margin-top: 1; margin-bottom: 1; @@ -69,21 +32,6 @@ class CognifyTUIScreen(BaseTUIScreen): RadioButton { height: 1; } - - #status-message { - margin-top: 2; - text-align: center; - height: auto; - } - - #cognify-footer { - dock: bottom; - padding: 1 0; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } """ def __init__(self): @@ -91,28 +39,28 @@ class CognifyTUIScreen(BaseTUIScreen): self.is_processing = False def compose_content(self) -> ComposeResult: - with Container(id="cognify-container"): - yield Label("⚡ Cognify Data", id="form-title") - with Vertical(id="cognify-form"): - yield Label("Dataset Name (optional, leave empty for all):", classes="field-label") + with Container(classes="tui-content-container"): + yield Label("⚡ Cognify Data", classes="tui-title") + with Vertical(classes="tui-form"): + yield Label("Dataset Name (optional, leave empty for all):", classes="tui-label-spaced") yield Input( placeholder="Leave empty to process all datasets", value="", id="dataset-input" ) - yield Label("Chunker Type:", classes="field-label") + 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("", id="status-message") + yield Static("", classes="tui-status") def compose_footer(self) -> ComposeResult: yield Static( "Ctrl+S: Start • Esc: Back • q: Quit", - id="cognify-footer" + classes="tui-footer" ) def on_mount(self) -> None: @@ -144,7 +92,7 @@ class CognifyTUIScreen(BaseTUIScreen): 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("#status-message", Static) + 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" @@ -164,7 +112,7 @@ class CognifyTUIScreen(BaseTUIScreen): 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("#status-message", Static) + status = self.query_one(".tui-status", Static) try: import cognee diff --git a/cognee/cli/tui/common_styles.py b/cognee/cli/tui/common_styles.py new file mode 100644 index 000000000..a71949e1a --- /dev/null +++ b/cognee/cli/tui/common_styles.py @@ -0,0 +1,113 @@ +"""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-content-container { + height: auto; + padding: 1; + content-align: center middle; +} + +.tui-form { + width: 100%; + height: auto; + border: solid $primary; + padding: 2; + background: $surface; +} + +.tui-form-compact { + border: solid $primary; + padding: 1; + 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; +} +""" \ No newline at end of file diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index 27ff0b3e6..9b0780fe5 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -34,26 +34,10 @@ class ConfirmModal(Screen): padding: 1 2; } - #confirm-title { - text-align: center; - text-style: bold; - color: $warning; - margin-bottom: 1; - } - #confirm-message { text-align: center; margin-bottom: 2; } - - #confirm-buttons { - align: center middle; - height: 3; - } - - Button { - margin: 0 1; - } """ def __init__(self, key: str, default_value: str): @@ -63,10 +47,10 @@ class ConfirmModal(Screen): def compose(self) -> ComposeResult: with Container(id="confirm-dialog"): - yield Label("⚠ Reset Configuration", id="confirm-title") + yield Label("⚠ Reset Configuration", classes="tui-dialog-title") yield Label(f"Reset '{self.key}' to default?", id="confirm-message") yield Label(f"Default value: {self.default_value}", id="confirm-message") - with Horizontal(id="confirm-buttons"): + with Horizontal(classes="tui-dialog-buttons"): yield Button("Reset", variant="error", id="confirm-btn") yield Button("Cancel", variant="default", id="cancel-btn") @@ -94,10 +78,6 @@ class ConfigTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - ConfigTUIScreen { - background: $surface; - } - #config-container { height: 100%; border: solid $primary; @@ -127,15 +107,6 @@ class ConfigTUIScreen(BaseTUIScreen): #inline-input { width: 100%; } - - #config-footer { - dock: bottom; - padding: 1 0; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } """ # Config key mappings with defaults (from existing config.py) @@ -158,6 +129,7 @@ class ConfigTUIScreen(BaseTUIScreen): def compose_content(self) -> ComposeResult: with Container(id="config-container"): + yield Static("⚙️ Change Config", classes="tui-title") table = DataTable() table.cursor_type = "row" table.zebra_stripes = True @@ -169,7 +141,7 @@ class ConfigTUIScreen(BaseTUIScreen): def compose_footer(self) -> ComposeResult: yield Static( "↑↓: Navigate • e: Edit • Enter: Save • r: Reset • Esc: Back • q: Quit", - id="config-footer" + classes="tui-footer" ) def on_mount(self) -> None: diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index bfd204147..69701cf69 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -19,10 +19,6 @@ class DeleteTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - DeleteTUIScreen { - background: $surface; - } - #delete-container { height: auto; padding: 2; @@ -31,33 +27,6 @@ class DeleteTUIScreen(BaseTUIScreen): #delete-form { width: 80; - height: auto; - border: solid $primary; - background: $surface; - padding: 2; - } - - #form-title { - text-align: center; - text-style: bold; - color: $accent; - margin-bottom: 2; - width: 100%; - } - - .input-group { - height: auto; - margin-bottom: 2; - } - - .input-label { - color: $text-muted; - margin-bottom: 1; - } - - Input { - width: 100%; - margin-bottom: 1; } #button-group { @@ -65,25 +34,6 @@ class DeleteTUIScreen(BaseTUIScreen): align: center middle; margin-top: 2; } - - Button { - margin: 0 1; - } - - #status-message { - text-align: center; - margin-top: 2; - height: auto; - } - - #delete-footer { - dock: bottom; - padding: 1 0; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } """ def __init__(self): @@ -92,18 +42,18 @@ class DeleteTUIScreen(BaseTUIScreen): def compose_content(self) -> ComposeResult: with Container(id="delete-container"): - with Vertical(id="delete-form"): - yield Label("🗑️ Delete Data", id="form-title") + with Vertical(id="delete-form", classes="tui-form"): + yield Label("🗑️ Delete Data", classes="tui-title") - with Vertical(classes="input-group"): - yield Label("Dataset Name (optional):", classes="input-label") + 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="input-group"): - yield Label("User ID (optional):", classes="input-label") + 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", id="user-input" @@ -113,12 +63,12 @@ class DeleteTUIScreen(BaseTUIScreen): yield Button("Delete", variant="error", id="delete-btn") yield Button("Delete All", variant="error", id="delete-all-btn") - yield Static("", id="status-message") + yield Static("", classes="tui-status") def compose_footer(self) -> ComposeResult: yield Static( "Ctrl+s: Delete • Ctrl+d: Delete All • Esc: Back • q: Quit", - id="delete-footer" + classes="tui-footer" ) def on_mount(self) -> None: @@ -161,7 +111,7 @@ class DeleteTUIScreen(BaseTUIScreen): dataset_input = self.query_one("#dataset-input", Input) user_input = self.query_one("#user-input", Input) - status = self.query_one("#status-message", Static) + status = self.query_one(".tui-status", Static) dataset_name = dataset_input.value.strip() or None user_id = user_input.value.strip() or None @@ -224,7 +174,7 @@ class DeleteTUIScreen(BaseTUIScreen): async def _perform_delete_all(self) -> None: """Perform the actual delete all operation.""" - status = self.query_one("#status-message", Static) + status = self.query_one(".tui-status", Static) self.is_processing = True try: @@ -295,34 +245,20 @@ class DeleteAllConfirmModal(BaseTUIScreen): margin-bottom: 1; } - #confirm-message { - text-align: center; - margin-bottom: 1; - } - #confirm-warning { text-align: center; color: $warning; text-style: bold; margin-bottom: 2; } - - #confirm-buttons { - align: center middle; - height: 3; - } - - Button { - margin: 0 1; - } """ 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", id="confirm-message") + yield Label("This will delete ALL data from cognee", classes="tui-dialog-message") yield Label("This operation is IRREVERSIBLE!", id="confirm-warning") - with Horizontal(id="confirm-buttons"): + with Horizontal(classes="tui-dialog-buttons"): yield Button("Delete All", variant="error", id="confirm-btn") yield Button("Cancel", variant="default", id="cancel-btn") diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index 77a3bbc0c..b7ea94db0 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -35,10 +35,6 @@ class HomeScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - HomeScreen { - background: $surface; - } - #main-container { height: 100%; border: solid $primary; @@ -64,13 +60,6 @@ class HomeScreen(BaseTUIScreen): margin-bottom: 2; } - #title-sub { - text-align: center; - width: auto; - color: $text-muted; - padding-bottom: 1; - margin-bottom: 1; - } ListView > ListItem { width: 100%; @@ -144,15 +133,6 @@ class HomeScreen(BaseTUIScreen): padding-left: 1; color: $text-muted; } - - #home-footer { - dock: bottom; - padding: 1 0; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } """ def __init__(self): @@ -177,7 +157,7 @@ class HomeScreen(BaseTUIScreen): def compose_footer(self) -> ComposeResult: yield Static( "↑↓: Navigate • Enter: Select • q/Esc: Quit", - id="home-footer" + classes="tui-footer" ) def on_mount(self) -> None: diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 4bfc14341..2ba42e673 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -17,10 +17,11 @@ class SearchTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - SearchTUIScreen { - background: $surface; + #search-container { + height: 100%; + padding: 1; } - + #form-title { text-align: center; text-style: bold; @@ -29,12 +30,6 @@ class SearchTUIScreen(BaseTUIScreen): width: 100%; } - - #search-container { - height: 100%; - padding: 1; - } - #search-form { height: auto; border: solid $primary; @@ -67,21 +62,6 @@ class SearchTUIScreen(BaseTUIScreen): height: 1fr; overflow-y: auto; } - - .field-label { - color: $text-muted; - margin-top: 1; - margin-bottom: 1; - } - - #search-footer { - dock: bottom; - padding: 1 0; - background: $boost; - color: $text-muted; - content-align: center middle; - border: solid $primary; - } """ def __init__(self): @@ -92,9 +72,9 @@ class SearchTUIScreen(BaseTUIScreen): with Container(id="search-container"): yield Label("🔍 Search Data", id="form-title") with Vertical(id="search-form"): - yield Label("Query:", classes="field-label") + yield Label("Query:", classes="tui-label-spaced") yield Input(placeholder="Enter your search query...", id="query-input") - yield Label("Search Type:", classes="field-label") + yield Label("Search Type:", classes="tui-label-spaced") yield Select( [ ("Graph Completion (Recommended)", "GRAPH_COMPLETION"), @@ -114,7 +94,7 @@ class SearchTUIScreen(BaseTUIScreen): def compose_footer(self) -> ComposeResult: yield Static( "Ctrl+S: Search • Esc: Back • q: Quit", - id="search-footer" + classes="tui-footer" ) def on_mount(self) -> None: From 91c01b76c082944d3617d22f4788d4654b4ce878 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 23:13:48 +0530 Subject: [PATCH 24/48] more cleanup of styles --- cognee/cli/tui/add_screen.py | 5 ++-- cognee/cli/tui/cognify_screen.py | 5 ++-- cognee/cli/tui/common_styles.py | 34 +++++++++++++++++---- cognee/cli/tui/config_screen.py | 25 +++++++--------- cognee/cli/tui/delete_screen.py | 12 ++------ cognee/cli/tui/home_screen.py | 51 ++++++++------------------------ cognee/cli/tui/search_screen.py | 18 ++--------- 7 files changed, 64 insertions(+), 86 deletions(-) diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index 66ca7a7b4..5c4f31eb8 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -29,8 +29,9 @@ class AddTUIScreen(BaseTUIScreen): self.is_processing = False def compose_content(self) -> ComposeResult: - with Container(classes="tui-content-container"): - yield Label("Add Data to Cognee", classes="tui-title") + 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, URL, or S3 path):", classes="tui-label-spaced") yield TextArea( diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index 5d33377ea..cc5a7610c 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -39,8 +39,9 @@ class CognifyTUIScreen(BaseTUIScreen): self.is_processing = False def compose_content(self) -> ComposeResult: - with Container(classes="tui-content-container"): - yield Label("⚡ Cognify Data", classes="tui-title") + 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 (optional, leave empty for all):", classes="tui-label-spaced") yield Input( diff --git a/cognee/cli/tui/common_styles.py b/cognee/cli/tui/common_styles.py index a71949e1a..95a4ddd8e 100644 --- a/cognee/cli/tui/common_styles.py +++ b/cognee/cli/tui/common_styles.py @@ -12,12 +12,40 @@ Screen { 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; @@ -26,12 +54,6 @@ Screen { background: $surface; } -.tui-form-compact { - border: solid $primary; - padding: 1; - background: $surface; -} - /* Common title styles */ .tui-title { text-align: center; diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index 9b0780fe5..e75c4d29b 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -78,11 +78,6 @@ class ConfigTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - #config-container { - height: 100%; - border: solid $primary; - } - DataTable { height: 1fr; text-align: center; @@ -128,15 +123,17 @@ class ConfigTUIScreen(BaseTUIScreen): self.editing_key = None # Track which key is being edited def compose_content(self) -> ComposeResult: - with Container(id="config-container"): - yield Static("⚙️ Change Config", classes="tui-title") - table = DataTable() - 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") + with Container(classes="tui-main-container"): + with Container(classes="tui-title-wrapper"): + yield Static("⚙️ Change Config", classes="tui-title-bordered") + with Container(classes="tui-bordered-wrapper"): + table = DataTable() + 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( diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 69701cf69..19dece6ce 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -19,12 +19,6 @@ class DeleteTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - #delete-container { - height: auto; - padding: 2; - align: center top; - } - #delete-form { width: 80; } @@ -41,10 +35,10 @@ class DeleteTUIScreen(BaseTUIScreen): self.is_processing = False def compose_content(self) -> ComposeResult: - with Container(id="delete-container"): + 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"): - yield Label("🗑️ Delete Data", classes="tui-title") - with Vertical(classes="tui-input-group"): yield Label("Dataset Name (optional):", classes="tui-label") yield Input( diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index b7ea94db0..e841a7f66 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -35,32 +35,6 @@ class HomeScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - #main-container { - height: 100%; - border: solid $primary; - background: $surface; - padding: 1; - } - - #title-wrapper { - width: 100%; - height: auto; - align: center middle; - content-align: center middle; - padding-bottom: 1; - } - - #title { - text-align: center; - width: auto; - color: $accent; - text-style: bold; - padding: 0 10; - border: solid $accent; - margin-bottom: 2; - } - - ListView > ListItem { width: 100%; padding: 0; @@ -141,18 +115,19 @@ class HomeScreen(BaseTUIScreen): self.current_index = 0 def compose_content(self) -> ComposeResult: - with Container(id="main-container"): - with Container(id="title-wrapper"): - yield Static("Select Command", id="title") - 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", - ) + 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( diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 2ba42e673..fa23e084d 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -17,19 +17,6 @@ class SearchTUIScreen(BaseTUIScreen): ] CSS = BaseTUIScreen.CSS + """ - #search-container { - height: 100%; - padding: 1; - } - - #form-title { - text-align: center; - text-style: bold; - color: $accent; - margin-bottom: 2; - width: 100%; - } - #search-form { height: auto; border: solid $primary; @@ -69,8 +56,9 @@ class SearchTUIScreen(BaseTUIScreen): self.is_searching = False def compose_content(self) -> ComposeResult: - with Container(id="search-container"): - yield Label("🔍 Search Data", id="form-title") + 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") From 5eaea4dbe0f1f3ea42bd6b066ed6e258077d3b5c Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 23:24:26 +0530 Subject: [PATCH 25/48] WIP - fixing functionality in delete screen. --- cognee/cli/tui/delete_screen.py | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 19dece6ce..2a1883760 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -1,9 +1,9 @@ import asyncio +import cognee 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.get_deletion_counts import get_deletion_counts @@ -15,14 +15,10 @@ class DeleteTUIScreen(BaseTUIScreen): Binding("q", "quit_app", "Quit"), Binding("escape", "back", "Back"), Binding("ctrl+s", "delete", "Delete"), - Binding("ctrl+d", "delete_all", "Delete All"), + Binding("ctrl+a", "delete_all", "Delete All"), ] CSS = BaseTUIScreen.CSS + """ - #delete-form { - width: 80; - } - #button-group { height: auto; align: center middle; @@ -61,7 +57,7 @@ class DeleteTUIScreen(BaseTUIScreen): def compose_footer(self) -> ComposeResult: yield Static( - "Ctrl+s: Delete • Ctrl+d: Delete All • Esc: Back • q: Quit", + "Ctrl+s: Delete • Ctrl+a: Delete All • Esc: Back • q: Quit", classes="tui-footer" ) @@ -81,24 +77,26 @@ class DeleteTUIScreen(BaseTUIScreen): def action_delete(self) -> None: """Delete the dataset.""" - self._handle_delete() + if not self.is_processing: + self._handle_delete() def action_delete_all(self) -> None: - self._handle_delete_all() + """Delete all data.""" + if not self.is_processing: + self._handle_delete_all() - async def on_button_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" if self.is_processing: return - if event.button.id == "delete-btn": - await self._handle_delete() + self._handle_delete() elif event.button.id == "delete-all-btn": self._handle_delete_all() elif event.button.id == "cancel-btn": self.app.pop_screen() - async def _handle_delete(self) -> None: + def _handle_delete(self) -> None: """Handle delete operation for dataset or user.""" if self.is_processing: return @@ -117,6 +115,13 @@ class DeleteTUIScreen(BaseTUIScreen): 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: # Get preview of what will be deleted preview_data = await get_deletion_counts( @@ -140,16 +145,11 @@ class DeleteTUIScreen(BaseTUIScreen): status.update(preview_msg) # Perform deletion - import cognee await cognee.delete(dataset_name=dataset_name, user_id=user_id) operation = f"dataset '{dataset_name}'" if dataset_name else f"data for user '{user_id}'" status.update(f"✓ Successfully deleted {operation}") - # Clear inputs - dataset_input.value = "" - user_input.value = "" - except Exception as e: status.update(f"✗ Error: {str(e)}") finally: @@ -162,7 +162,7 @@ class DeleteTUIScreen(BaseTUIScreen): def handle_confirm(confirmed: bool) -> None: if confirmed: - self.run_worker(self._perform_delete_all()) + asyncio.create_task(self._perform_delete_all()) self.app.push_screen(DeleteAllConfirmModal(), handle_confirm) From 9e86409e72fdb124fda9f251b9c38340511261ec Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 23:28:50 +0530 Subject: [PATCH 26/48] WIP - fixing functionality in delete screen. --- cognee/cli/tui/delete_screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 2a1883760..2bb1155b1 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -15,7 +15,7 @@ class DeleteTUIScreen(BaseTUIScreen): Binding("q", "quit_app", "Quit"), Binding("escape", "back", "Back"), Binding("ctrl+s", "delete", "Delete"), - Binding("ctrl+a", "delete_all", "Delete All"), + Binding("ctrl+a", "delete_all", "Delete All", priority=True), ] CSS = BaseTUIScreen.CSS + """ From 7224074b6cf97622dce3ba4b365b1702395259c5 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 23:42:47 +0530 Subject: [PATCH 27/48] WIP - debugging functionality to get parity with commands --- cognee/cli/tui/add_screen.py | 14 +++---------- cognee/cli/tui/cognify_screen.py | 35 ++++++++++++++++---------------- cognee/cli/tui/search_screen.py | 6 +++--- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index 5c4f31eb8..d902703b5 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -1,7 +1,8 @@ import asyncio +import cognee from textual.app import ComposeResult -from textual.widgets import Input, Label, Button, Static, TextArea -from textual.containers import Container, Vertical, Horizontal +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 @@ -76,11 +77,6 @@ class AddTUIScreen(BaseTUIScreen): if not self.is_processing: self._submit_data() - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button press.""" - if event.button.id == "submit-btn" and 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) @@ -100,7 +96,6 @@ class AddTUIScreen(BaseTUIScreen): # Disable inputs during processing data_input.disabled = True dataset_input.disabled = True - self.query_one("#submit-btn", Button).disabled = True # Run async add operation asyncio.create_task(self._add_data_async(data, dataset_name)) @@ -110,8 +105,6 @@ class AddTUIScreen(BaseTUIScreen): 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]") @@ -130,5 +123,4 @@ class AddTUIScreen(BaseTUIScreen): dataset_input = self.query_one("#dataset-input", Input) data_input.disabled = False dataset_input.disabled = False - self.query_one("#submit-btn", Button).disabled = False data_input.focus() diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index cc5a7610c..844cb345b 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -1,12 +1,25 @@ import asyncio + +import cognee +from cognee.modules.chunking.TextChunker import TextChunker from textual.app import ComposeResult -from textual.widgets import Input, Label, Button, Static, Checkbox, RadioSet, RadioButton +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 +try: + from cognee.modules.chunking.LangchainChunker import LangchainChunker +except ImportError: + LangchainChunker = None + +try: + from cognee.modules.chunking.CsvChunker import CsvChunker +except ImportError: + CsvChunker = None + class CognifyTUIScreen(BaseTUIScreen): """TUI screen for cognifying data in cognee.""" @@ -83,11 +96,6 @@ class CognifyTUIScreen(BaseTUIScreen): if not self.is_processing: self._submit_cognify() - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button press.""" - if event.button.id == "submit-btn" and 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) @@ -106,7 +114,6 @@ class CognifyTUIScreen(BaseTUIScreen): dataset_input.disabled = True chunker_radio.disabled = True background_checkbox.disabled = True - self.query_one("#submit-btn", Button).disabled = True # Run async cognify operation asyncio.create_task(self._cognify_async(dataset_name, chunker_type, run_background)) @@ -116,22 +123,17 @@ class CognifyTUIScreen(BaseTUIScreen): status = self.query_one(".tui-status", Static) try: - import cognee - from cognee.modules.chunking.TextChunker import TextChunker - # Get chunker class chunker_class = TextChunker if chunker_type == "LangchainChunker": - try: - from cognee.modules.chunking.LangchainChunker import LangchainChunker + if LangchainChunker is not None: chunker_class = LangchainChunker - except ImportError: + else: status.update("[yellow]⚠ LangchainChunker not available, using TextChunker[/yellow]") elif chunker_type == "CsvChunker": - try: - from cognee.modules.chunking.CsvChunker import CsvChunker + if CsvChunker is not None: chunker_class = CsvChunker - except ImportError: + else: status.update("[yellow]⚠ CsvChunker not available, using TextChunker[/yellow]") # Prepare datasets parameter @@ -160,5 +162,4 @@ class CognifyTUIScreen(BaseTUIScreen): dataset_input.disabled = False chunker_radio.disabled = False background_checkbox.disabled = False - self.query_one("#submit-btn", Button).disabled = False dataset_input.focus() diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index fa23e084d..e04165b60 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -1,4 +1,7 @@ import asyncio + +import cognee +from cognee.modules.search.types import SearchType from textual.app import ComposeResult from textual.widgets import Input, Label, Static, Select from textual.containers import Container, Vertical, ScrollableContainer @@ -136,9 +139,6 @@ class SearchTUIScreen(BaseTUIScreen): 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] From 3a05cca74bde5db2fa02e66858e1c2f2d88b5aa9 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sat, 29 Nov 2025 23:56:11 +0530 Subject: [PATCH 28/48] WIP - implementing the missing delete data functionality --- cognee/cli/commands/delete_command.py | 31 +++++--- cognee/cli/tui/delete_screen.py | 26 +++++-- cognee/modules/data/methods/__init__.py | 2 + .../data/methods/delete_data_by_user.py | 71 +++++++++++++++++++ .../data/methods/delete_datasets_by_name.py | 57 +++++++++++++++ 5 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 cognee/modules/data/methods/delete_data_by_user.py create mode 100644 cognee/modules/data/methods/delete_datasets_by_name.py diff --git a/cognee/cli/commands/delete_command.py b/cognee/cli/commands/delete_command.py index 8400d3b0f..9079b4ca3 100644 --- a/cognee/cli/commands/delete_command.py +++ b/cognee/cli/commands/delete_command.py @@ -1,12 +1,14 @@ import argparse import asyncio -from typing import Optional - +from uuid import UUID from cognee.cli.reference import SupportsCliCommand from cognee.cli import DEFAULT_DOCS_URL import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException, CliCommandInnerException from cognee.modules.data.methods.get_deletion_counts import get_deletion_counts +from cognee.modules.data.methods.delete_datasets_by_name import delete_datasets_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 DeleteCommand(SupportsCliCommand): @@ -93,18 +95,27 @@ Be careful with deletion operations as they are irreversible. # Run the async delete function async def run_delete(): try: - # NOTE: The underlying cognee.delete() function is currently not working as expected. - # This is a separate bug that this preview feature helps to expose. - if args.all: - await cognee.delete(dataset_name=None, user_id=args.user_id) + if args.dataset_name: + # Use delete_datasets_by_name for dataset deletion + user = await get_default_user() + result = await delete_datasets_by_name(args.dataset_name, user.id) + + if result["not_found"]: + fmt.warning(f"Dataset '{args.dataset_name}' not found") + return False + + fmt.success(f"Successfully deleted {result['deleted_count']} dataset(s)") + return True else: - await cognee.delete(dataset_name=args.dataset_name, user_id=args.user_id) + # For user_id deletion, use the original cognee.delete + result = await delete_data_by_user(UUID(args.user_id)) except Exception as e: raise CliCommandInnerException(f"Failed to delete: {str(e)}") from e + return True - asyncio.run(run_delete()) - # This success message may be inaccurate due to the underlying bug, but we leave it for now. - fmt.success(f"Successfully deleted {operation}") + success = asyncio.run(run_delete()) + if success and not args.dataset_name: + fmt.success(f"Successfully deleted {operation}") except Exception as e: if isinstance(e, CliCommandInnerException): diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 2bb1155b1..bb08a3a5f 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -1,11 +1,14 @@ import asyncio -import cognee +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.get_deletion_counts import get_deletion_counts +from cognee.modules.data.methods.delete_datasets_by_name import delete_datasets_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): @@ -145,10 +148,21 @@ class DeleteTUIScreen(BaseTUIScreen): status.update(preview_msg) # Perform deletion - await cognee.delete(dataset_name=dataset_name, user_id=user_id) - - operation = f"dataset '{dataset_name}'" if dataset_name else f"data for user '{user_id}'" - status.update(f"✓ Successfully deleted {operation}") + if dataset_name: + # Use delete_datasets_by_name for dataset deletion + user = await get_default_user() + result = await delete_datasets_by_name(dataset_name, user.id) + + if result["not_found"]: + status.update(f"⚠️ Dataset '{dataset_name}' not found") + self.is_processing = False + return + + status.update(f"✓ Successfully deleted {result['deleted_count']} dataset(s)") + else: + # For user_id deletion, use the new delete_data_by_user method + result = await delete_data_by_user(UUID(user_id)) + status.update(f"✓ Successfully deleted {result['datasets_deleted']} datasets and {result['data_entries_deleted']} data entries for user '{user_id}'") except Exception as e: status.update(f"✗ Error: {str(e)}") @@ -194,7 +208,7 @@ class DeleteTUIScreen(BaseTUIScreen): ) status.update(preview_msg) - # Perform deletion + # Perform deletion - delete all uses the original cognee.delete import cognee await cognee.delete(dataset_name=None, user_id=None) diff --git a/cognee/modules/data/methods/__init__.py b/cognee/modules/data/methods/__init__.py index 7936a9afd..34b58590d 100644 --- a/cognee/modules/data/methods/__init__.py +++ b/cognee/modules/data/methods/__init__.py @@ -16,6 +16,8 @@ from .get_dataset_ids import get_dataset_ids # Delete from .delete_dataset import delete_dataset +from .delete_datasets_by_name import delete_datasets_by_name +from .delete_data_by_user import delete_data_by_user from .delete_data import delete_data # Create diff --git a/cognee/modules/data/methods/delete_data_by_user.py b/cognee/modules/data/methods/delete_data_by_user.py new file mode 100644 index 000000000..258321b78 --- /dev/null +++ b/cognee/modules/data/methods/delete_data_by_user.py @@ -0,0 +1,71 @@ +from uuid import UUID +from sqlalchemy import select, delete as sql_delete +from cognee.infrastructure.databases.relational import get_relational_engine +from cognee.modules.data.models import Dataset, DatasetData +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) -> dict[str, int]: + """ + 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 + + Returns: + Dictionary containing deletion statistics: + - datasets_deleted: Number of datasets deleted + - data_entries_deleted: Number of data entries deleted + + Raises: + ValueError: If user is not found + """ + db_engine = get_relational_engine() + + async with db_engine.get_async_session() as session: + # Verify user exists + user = await get_user(user_id) + if not user: + raise ValueError(f"User with ID {user_id} not found") + + # Get all datasets owned by this user + datasets_query = select(Dataset).where(Dataset.owner_id == user_id) + user_datasets = (await session.execute(datasets_query)).scalars().all() + + datasets_deleted = 0 + data_entries_deleted = 0 + + # Delete each dataset and its data + for dataset in user_datasets: + # Get all data entries in this dataset + data_query = select(DatasetData).where(DatasetData.dataset_id == dataset.id) + dataset_data_links = (await session.execute(data_query)).scalars().all() + + # Delete dataset-data links + for link in dataset_data_links: + await session.execute( + sql_delete(DatasetData).where(DatasetData.id == link.id) + ) + data_entries_deleted += 1 + + # Delete the dataset itself + await session.execute( + sql_delete(Dataset).where(Dataset.id == dataset.id) + ) + datasets_deleted += 1 + + # Commit all changes + await session.commit() + + logger.info(f"Deleted {datasets_deleted} datasets and {data_entries_deleted} data entries for user {user_id}") + + return { + "datasets_deleted": datasets_deleted, + "data_entries_deleted": data_entries_deleted, + } \ No newline at end of file diff --git a/cognee/modules/data/methods/delete_datasets_by_name.py b/cognee/modules/data/methods/delete_datasets_by_name.py new file mode 100644 index 000000000..abccd8f7c --- /dev/null +++ b/cognee/modules/data/methods/delete_datasets_by_name.py @@ -0,0 +1,57 @@ +from typing import Union +from uuid import UUID +from sqlalchemy import select +from cognee.infrastructure.databases.relational import get_relational_engine +from ..models import Dataset + + +async def delete_datasets_by_name( + dataset_names: Union[str, list[str]], user_id: UUID +) -> dict[str, any]: + """ + Delete datasets by name for a specific user. + + Args: + dataset_names: Single dataset name or list of dataset names to delete + user_id: UUID of the dataset owner + + Returns: + Dictionary containing: + - deleted_count: Number of datasets deleted + - deleted_ids: List of deleted dataset IDs + - not_found: List of dataset names that were not found + """ + db_engine = get_relational_engine() + + async with db_engine.get_async_session() as session: + # Normalize input to list + if isinstance(dataset_names, str): + dataset_names = [dataset_names] + + # Retrieve datasets matching the names and user_id + datasets = ( + await session.scalars( + select(Dataset) + .filter(Dataset.owner_id == user_id) + .filter(Dataset.name.in_(dataset_names)) + ) + ).all() + + # Track results + deleted_ids = [] + found_names = set() + + # Delete each dataset + for dataset in datasets: + await db_engine.delete_entity_by_id(dataset.__tablename__, dataset.id) + deleted_ids.append(dataset.id) + found_names.add(dataset.name) + + # Identify datasets that were not found + not_found = [name for name in dataset_names if name not in found_names] + + return { + "deleted_count": len(deleted_ids), + "deleted_ids": deleted_ids, + "not_found": not_found + } \ No newline at end of file From 40846df92db48312484721e163a650f3bb024127 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 07:26:24 +0530 Subject: [PATCH 29/48] cleanup of delete dataset methods --- cognee/cli/commands/delete_command.py | 2 +- cognee/cli/tui/delete_screen.py | 2 +- .../sqlalchemy/SqlAlchemyAdapter.py | 53 ++++++++++------- cognee/modules/data/methods/__init__.py | 2 +- .../data/methods/delete_data_by_user.py | 49 +++------------- cognee/modules/data/methods/delete_dataset.py | 2 +- .../data/methods/delete_dataset_by_name.py | 30 ++++++++++ .../data/methods/delete_datasets_by_name.py | 57 ------------------- 8 files changed, 72 insertions(+), 125 deletions(-) create mode 100644 cognee/modules/data/methods/delete_dataset_by_name.py delete mode 100644 cognee/modules/data/methods/delete_datasets_by_name.py diff --git a/cognee/cli/commands/delete_command.py b/cognee/cli/commands/delete_command.py index 9079b4ca3..d50571fca 100644 --- a/cognee/cli/commands/delete_command.py +++ b/cognee/cli/commands/delete_command.py @@ -6,7 +6,7 @@ from cognee.cli import DEFAULT_DOCS_URL import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException, CliCommandInnerException from cognee.modules.data.methods.get_deletion_counts import get_deletion_counts -from cognee.modules.data.methods.delete_datasets_by_name import delete_datasets_by_name +from cognee.modules.data.methods.delete_dataset_by_name import delete_datasets_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 diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index bb08a3a5f..80bb9586d 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -6,7 +6,7 @@ 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.get_deletion_counts import get_deletion_counts -from cognee.modules.data.methods.delete_datasets_by_name import delete_datasets_by_name +from cognee.modules.data.methods.delete_dataset_by_name import delete_datasets_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 diff --git a/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py b/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py index 380ce9917..c02c486a4 100644 --- a/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py +++ b/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py @@ -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 @@ -235,35 +234,45 @@ 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): """ diff --git a/cognee/modules/data/methods/__init__.py b/cognee/modules/data/methods/__init__.py index 34b58590d..17073cd08 100644 --- a/cognee/modules/data/methods/__init__.py +++ b/cognee/modules/data/methods/__init__.py @@ -16,7 +16,7 @@ from .get_dataset_ids import get_dataset_ids # Delete from .delete_dataset import delete_dataset -from .delete_datasets_by_name import delete_datasets_by_name +from .delete_dataset_by_name import delete_datasets_by_name from .delete_data_by_user import delete_data_by_user from .delete_data import delete_data diff --git a/cognee/modules/data/methods/delete_data_by_user.py b/cognee/modules/data/methods/delete_data_by_user.py index 258321b78..0f94df852 100644 --- a/cognee/modules/data/methods/delete_data_by_user.py +++ b/cognee/modules/data/methods/delete_data_by_user.py @@ -1,14 +1,14 @@ from uuid import UUID -from sqlalchemy import select, delete as sql_delete +from sqlalchemy import select from cognee.infrastructure.databases.relational import get_relational_engine -from cognee.modules.data.models import Dataset, DatasetData +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) -> dict[str, int]: +async def delete_data_by_user(user_id: UUID): """ Delete all datasets and their associated data for a specific user. @@ -18,11 +18,6 @@ async def delete_data_by_user(user_id: UUID) -> dict[str, int]: Args: user_id: UUID of the user whose data should be deleted - Returns: - Dictionary containing deletion statistics: - - datasets_deleted: Number of datasets deleted - - data_entries_deleted: Number of data entries deleted - Raises: ValueError: If user is not found """ @@ -35,37 +30,7 @@ async def delete_data_by_user(user_id: UUID) -> dict[str, int]: raise ValueError(f"User with ID {user_id} not found") # Get all datasets owned by this user - datasets_query = select(Dataset).where(Dataset.owner_id == user_id) - user_datasets = (await session.execute(datasets_query)).scalars().all() - - datasets_deleted = 0 - data_entries_deleted = 0 - - # Delete each dataset and its data - for dataset in user_datasets: - # Get all data entries in this dataset - data_query = select(DatasetData).where(DatasetData.dataset_id == dataset.id) - dataset_data_links = (await session.execute(data_query)).scalars().all() - - # Delete dataset-data links - for link in dataset_data_links: - await session.execute( - sql_delete(DatasetData).where(DatasetData.id == link.id) - ) - data_entries_deleted += 1 - - # Delete the dataset itself - await session.execute( - sql_delete(Dataset).where(Dataset.id == dataset.id) - ) - datasets_deleted += 1 - - # Commit all changes - await session.commit() - - logger.info(f"Deleted {datasets_deleted} datasets and {data_entries_deleted} data entries for user {user_id}") - - return { - "datasets_deleted": datasets_deleted, - "data_entries_deleted": data_entries_deleted, - } \ No newline at end of file + 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) \ No newline at end of file diff --git a/cognee/modules/data/methods/delete_dataset.py b/cognee/modules/data/methods/delete_dataset.py index ff20ff9e7..7cc6a2fd0 100644 --- a/cognee/modules/data/methods/delete_dataset.py +++ b/cognee/modules/data/methods/delete_dataset.py @@ -5,4 +5,4 @@ from cognee.infrastructure.databases.relational import get_relational_engine async def delete_dataset(dataset: Dataset): db_engine = get_relational_engine() - return await db_engine.delete_entity_by_id(dataset.__tablename__, dataset.id) + return await db_engine.delete_entities_by_id(dataset.__tablename__, dataset.id) diff --git a/cognee/modules/data/methods/delete_dataset_by_name.py b/cognee/modules/data/methods/delete_dataset_by_name.py new file mode 100644 index 000000000..ae8f7704f --- /dev/null +++ b/cognee/modules/data/methods/delete_dataset_by_name.py @@ -0,0 +1,30 @@ +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) diff --git a/cognee/modules/data/methods/delete_datasets_by_name.py b/cognee/modules/data/methods/delete_datasets_by_name.py deleted file mode 100644 index abccd8f7c..000000000 --- a/cognee/modules/data/methods/delete_datasets_by_name.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Union -from uuid import UUID -from sqlalchemy import select -from cognee.infrastructure.databases.relational import get_relational_engine -from ..models import Dataset - - -async def delete_datasets_by_name( - dataset_names: Union[str, list[str]], user_id: UUID -) -> dict[str, any]: - """ - Delete datasets by name for a specific user. - - Args: - dataset_names: Single dataset name or list of dataset names to delete - user_id: UUID of the dataset owner - - Returns: - Dictionary containing: - - deleted_count: Number of datasets deleted - - deleted_ids: List of deleted dataset IDs - - not_found: List of dataset names that were not found - """ - db_engine = get_relational_engine() - - async with db_engine.get_async_session() as session: - # Normalize input to list - if isinstance(dataset_names, str): - dataset_names = [dataset_names] - - # Retrieve datasets matching the names and user_id - datasets = ( - await session.scalars( - select(Dataset) - .filter(Dataset.owner_id == user_id) - .filter(Dataset.name.in_(dataset_names)) - ) - ).all() - - # Track results - deleted_ids = [] - found_names = set() - - # Delete each dataset - for dataset in datasets: - await db_engine.delete_entity_by_id(dataset.__tablename__, dataset.id) - deleted_ids.append(dataset.id) - found_names.add(dataset.name) - - # Identify datasets that were not found - not_found = [name for name in dataset_names if name not in found_names] - - return { - "deleted_count": len(deleted_ids), - "deleted_ids": deleted_ids, - "not_found": not_found - } \ No newline at end of file From 1ac2c24cf5eedc656c3ce59ef6162bb23b5b32ea Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 07:29:42 +0530 Subject: [PATCH 30/48] cleanup of delete dataset methods --- cognee/cli/commands/delete_command.py | 4 ++-- cognee/cli/tui/delete_screen.py | 4 ++-- cognee/modules/data/methods/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cognee/cli/commands/delete_command.py b/cognee/cli/commands/delete_command.py index d50571fca..df0a1cbe3 100644 --- a/cognee/cli/commands/delete_command.py +++ b/cognee/cli/commands/delete_command.py @@ -6,7 +6,7 @@ from cognee.cli import DEFAULT_DOCS_URL import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException, CliCommandInnerException from cognee.modules.data.methods.get_deletion_counts import get_deletion_counts -from cognee.modules.data.methods.delete_dataset_by_name import delete_datasets_by_name +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 @@ -98,7 +98,7 @@ Be careful with deletion operations as they are irreversible. if args.dataset_name: # Use delete_datasets_by_name for dataset deletion user = await get_default_user() - result = await delete_datasets_by_name(args.dataset_name, user.id) + result = await delete_dataset_by_name(args.dataset_name, user.id) if result["not_found"]: fmt.warning(f"Dataset '{args.dataset_name}' not found") diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 80bb9586d..9b7e45c16 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -6,7 +6,7 @@ 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.get_deletion_counts import get_deletion_counts -from cognee.modules.data.methods.delete_dataset_by_name import delete_datasets_by_name +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 @@ -151,7 +151,7 @@ class DeleteTUIScreen(BaseTUIScreen): if dataset_name: # Use delete_datasets_by_name for dataset deletion user = await get_default_user() - result = await delete_datasets_by_name(dataset_name, user.id) + result = await delete_dataset_by_name(dataset_name, user.id) if result["not_found"]: status.update(f"⚠️ Dataset '{dataset_name}' not found") diff --git a/cognee/modules/data/methods/__init__.py b/cognee/modules/data/methods/__init__.py index 17073cd08..da8d64dea 100644 --- a/cognee/modules/data/methods/__init__.py +++ b/cognee/modules/data/methods/__init__.py @@ -16,7 +16,7 @@ from .get_dataset_ids import get_dataset_ids # Delete from .delete_dataset import delete_dataset -from .delete_dataset_by_name import delete_datasets_by_name +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 From 164e1d9442c1dc4b287969ca8f339d71c018a766 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 07:38:16 +0530 Subject: [PATCH 31/48] minor fix with delete dialog --- cognee/cli/tui/delete_screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 9b7e45c16..734fb9b0f 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -240,7 +240,7 @@ class DeleteAllConfirmModal(BaseTUIScreen): #confirm-dialog { width: 60; - height: 13; + height: 20; border: thick $error; background: $surface; padding: 2; From c05a459fc4468c56fe94046ed41296d8309b7d99 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 07:56:04 +0530 Subject: [PATCH 32/48] minor fix with delete screen --- cognee/cli/tui/delete_screen.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 734fb9b0f..db278ca24 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -126,32 +126,13 @@ class DeleteTUIScreen(BaseTUIScreen): status = self.query_one(".tui-status", Static) try: - # Get preview of what will be deleted - preview_data = await get_deletion_counts( - dataset_name=dataset_name, - user_id=user_id, - all_data=False, - ) - - if not preview_data: - status.update("✓ No data found to delete") - self.is_processing = False - return - - # Show preview and confirm - preview_msg = ( - f"About to delete:\n" - f"Datasets: {preview_data.datasets}\n" - f"Entries: {preview_data.entries}\n" - f"Users: {preview_data.users}" - ) - status.update(preview_msg) - # Perform deletion if dataset_name: # Use delete_datasets_by_name for dataset deletion - user = await get_default_user() - result = await delete_dataset_by_name(dataset_name, user.id) + if user_id is None: + user = await get_default_user() + user_id = user.id + result = await delete_dataset_by_name(dataset_name, user_id) if result["not_found"]: status.update(f"⚠️ Dataset '{dataset_name}' not found") From 192c2b736388bb1587b3ec3be996de2bc116a5e9 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 08:11:41 +0530 Subject: [PATCH 33/48] changes to config screen --- cognee/cli/tui/config_screen.py | 246 ++++++++++++++++++++------------ 1 file changed, 155 insertions(+), 91 deletions(-) diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index e75c4d29b..f062a0f01 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -1,17 +1,21 @@ 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 -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 +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): @@ -27,17 +31,32 @@ class ConfirmModal(Screen): } #confirm-dialog { - width: 50; - height: 11; + 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): @@ -47,9 +66,10 @@ class ConfirmModal(Screen): def compose(self) -> ComposeResult: with Container(id="confirm-dialog"): - yield Label("⚠ Reset Configuration", classes="tui-dialog-title") - yield Label(f"Reset '{self.key}' to default?", id="confirm-message") - yield Label(f"Default value: {self.default_value}", id="confirm-message") + 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") @@ -65,7 +85,7 @@ class ConfirmModal(Screen): class ConfigTUIScreen(BaseTUIScreen): - """Main config TUI screen with inline editing.""" + """Main config TUI screen with inline editing and live data fetching.""" BINDINGS = [ Binding("q", "quit_app", "Quit"), @@ -96,16 +116,22 @@ class ConfigTUIScreen(BaseTUIScreen): #edit-label { color: $text-muted; - margin-bottom: 0; + margin-bottom: 1; } #inline-input { width: 100%; } + + .dim-text { + color: $text-muted; + text-align: center; + margin-bottom: 1; + } """ - # Config key mappings with defaults (from existing config.py) - CONFIG_KEYS = { + # 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", ""), @@ -114,8 +140,8 @@ class ConfigTUIScreen(BaseTUIScreen): "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"), + "chunk_size": ("set_chunk_size", 1500), + "chunk_overlap": ("set_chunk_overlap", 10), } def __init__(self): @@ -125,12 +151,14 @@ class ConfigTUIScreen(BaseTUIScreen): def compose_content(self) -> ComposeResult: with Container(classes="tui-main-container"): with Container(classes="tui-title-wrapper"): - yield Static("⚙️ Change Config", classes="tui-title-bordered") + yield Static("⚙️ Configuration Manager", classes="tui-title-bordered") + with Container(classes="tui-bordered-wrapper"): - table = DataTable() + 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") @@ -142,121 +170,127 @@ class ConfigTUIScreen(BaseTUIScreen): ) def on_mount(self) -> None: + """Initialize the table with columns and current data.""" table = self.query_one(DataTable) - key_col, value_col = table.add_columns("KEY", "VALUE") - # Add all config keys - for key, (method, default) in self.CONFIG_KEYS.items(): - display_default = "(empty)" if default == "" else str(default) - table.add_row(key, display_default) + table.add_columns("Configuration Key", "Current Value") + self._load_table_data() table.focus() - def action_cursor_up(self) -> None: - """Move cursor up in the table.""" - if self.editing_key: - return # Don't navigate while editing + def _load_table_data(self) -> None: + """Fetch real config values and populate the table.""" table = self.query_one(DataTable) - table.action_cursor_up() + 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: - """Move cursor down in the table.""" - if self.editing_key: - return # Don't navigate while editing - table = self.query_one(DataTable) - table.action_cursor_down() + if self.editing_key: return + self.query_one(DataTable).action_cursor_down() def action_cancel_or_back(self) -> None: - """Cancel editing or go back to main menu.""" if self.editing_key: self._cancel_edit() else: self.app.pop_screen() def action_quit_app(self) -> None: - """Quit the entire application.""" self.app.exit() def action_edit(self) -> None: """Start inline editing for the selected config value.""" - if self.editing_key: - return # Already editing + if self.editing_key: return table = self.query_one(DataTable) - if table.cursor_coordinate.row < 0: - return + if table.cursor_row < 0: return - row_data = table.get_row_at(table.cursor_coordinate.row) - key = str(row_data[0]) - default_value = str(row_data[1]) + # 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 = key + self.editing_key = str(row_key.value) - # Show the inline edit container + # Show edit container edit_container = self.query_one("#inline-edit-container") edit_container.add_class("visible") - # Update label and input + # Update UI label = self.query_one("#edit-label", Label) - label.update(f"Editing: {key} (default: {default_value})") + label.update(f"Editing: [bold]{self.editing_key}[/bold]") input_widget = self.query_one("#inline-input", Input) input_widget.value = "" - input_widget.placeholder = f"Enter new value for {key}" + # 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 + if not self.editing_key: return input_widget = self.query_one("#inline-input", Input) value = input_widget.value.strip() - if value: - self._save_config(self.editing_key, value) - + # 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: - """Cancel the current edit and hide the input.""" self.editing_key = None - - # Hide the inline edit container edit_container = self.query_one("#inline-edit-container") edit_container.remove_class("visible") - - # Clear input - input_widget = self.query_one("#inline-input", Input) - input_widget.value = "" - - # Return focus to table - table = self.query_one(DataTable) - table.focus() + self.query_one("#inline-input", Input).value = "" + self.query_one(DataTable).focus() def on_input_submitted(self, event: Input.Submitted) -> None: - """Handle Enter key in the input field.""" 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 - if table.cursor_coordinate.row < 0: + 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 - row_key = table.get_row_at(table.cursor_coordinate.row) - key = str(row_key[0]) - - if key not in self.CONFIG_KEYS: - return - - method_name, default_value = self.CONFIG_KEYS[key] + _, 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, method_name, default_value) + self._reset_config(key) self.app.push_screen( ConfirmModal(key, display_default), @@ -264,39 +298,63 @@ class ConfigTUIScreen(BaseTUIScreen): ) def _save_config(self, key: str, value: str) -> None: - """Save config value using cognee.config.set().""" + """Save config value and update UI.""" try: import cognee - # Try to parse as JSON (numbers, booleans, etc) + # Parse value types (restore JSON behavior) try: parsed_value = json.loads(value) - except json.JSONDecodeError: - parsed_value = 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.notify(f"✓ Set {key} = {parsed_value}", severity="information") + self._update_table_row(key, parsed_value) + self.notify(f"✓ Set {key}", severity="information") except Exception as e: - self.notify(f"✗ Failed to set {key}: {str(e)}", severity="error") + self.notify(f"✗ Error setting {key}: {str(e)}", severity="error") - def _reset_config(self, key: str, method_name: str, default_value: any) -> None: - """Reset config to default using the mapped method.""" + def _reset_config(self, key: str) -> None: + """Reset config to default using mapped method and update UI.""" try: import cognee - method = getattr(cognee.config, method_name) - method(default_value) + method_name, default_value = self.CONFIG_MAP[key] - display_default = "(empty)" if default_value == "" else str(default_value) - self.notify( - f"✓ Reset {key} to default: {display_default}", - severity="information" - ) + 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.""" @@ -310,8 +368,14 @@ class ConfigTUICommand(SupportsCliCommand): 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()) @@ -329,4 +393,4 @@ class ConfigTUICommand(SupportsCliCommand): f"Failed to launch config TUI: {str(ex)}", docs_url=self.docs_url, raiseable_exception=ex, - ) + ) \ No newline at end of file From 1b8ac854ddf44185e0ab63223281f54def544d03 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 08:16:36 +0530 Subject: [PATCH 34/48] minor fixes in add screen --- cognee/cli/tui/add_screen.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index d902703b5..c74b539a6 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -1,5 +1,4 @@ import asyncio -import cognee from textual.app import ComposeResult from textual.widgets import Input, Label, Static, TextArea from textual.containers import Container, Vertical @@ -34,7 +33,7 @@ class AddTUIScreen(BaseTUIScreen): 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, URL, or S3 path):", classes="tui-label-spaced") + yield Label("Data (text, file path (/path/to/doc), URL, or S3 path (s3://bucket)):", classes="tui-label-spaced") yield TextArea( "", id="data-input", @@ -105,6 +104,8 @@ class AddTUIScreen(BaseTUIScreen): 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]") From b8927704e30b8b91023e43e0ccf11387b25880ca Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 08:18:17 +0530 Subject: [PATCH 35/48] change import to avoid circular imports --- cognee/cli/tui/cognify_screen.py | 4 +--- cognee/cli/tui/search_screen.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index 844cb345b..1108d6a7d 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -1,6 +1,4 @@ import asyncio - -import cognee from cognee.modules.chunking.TextChunker import TextChunker from textual.app import ComposeResult from textual.widgets import Input, Label, Static, Checkbox, RadioSet, RadioButton @@ -138,7 +136,7 @@ class CognifyTUIScreen(BaseTUIScreen): # Prepare datasets parameter datasets = [dataset_name] if dataset_name else None - + import cognee await cognee.cognify( datasets=datasets, chunker=chunker_class, diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index e04165b60..831178842 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -1,12 +1,9 @@ import asyncio - -import cognee from cognee.modules.search.types import SearchType 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 @@ -141,7 +138,7 @@ class SearchTUIScreen(BaseTUIScreen): try: # Convert string to SearchType enum search_type = SearchType[query_type] - + import cognee # Perform search results = await cognee.search( query_text=query_text, From 29959ada7f6c60154b3a5c1b85f6872c5a9ba9ff Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 08:26:33 +0530 Subject: [PATCH 36/48] change import to avoid circular imports --- cognee/cli/tui/cognify_screen.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index 1108d6a7d..15035a912 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -1,23 +1,11 @@ import asyncio -from cognee.modules.chunking.TextChunker import TextChunker 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 -try: - from cognee.modules.chunking.LangchainChunker import LangchainChunker -except ImportError: - LangchainChunker = None - -try: - from cognee.modules.chunking.CsvChunker import CsvChunker -except ImportError: - CsvChunker = None - class CognifyTUIScreen(BaseTUIScreen): """TUI screen for cognifying data in cognee.""" @@ -119,16 +107,24 @@ class CognifyTUIScreen(BaseTUIScreen): 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: From ad6bdd2be361799ed4af4cd02580d34b0dde1191 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 08:33:09 +0530 Subject: [PATCH 37/48] change import to avoid circular imports --- cognee/cli/tui/search_screen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 831178842..cb9152aff 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -1,12 +1,10 @@ import asyncio -from cognee.modules.search.types import SearchType 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.""" @@ -136,9 +134,11 @@ class SearchTUIScreen(BaseTUIScreen): 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] - import cognee # Perform search results = await cognee.search( query_text=query_text, @@ -157,7 +157,7 @@ class SearchTUIScreen(BaseTUIScreen): 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)]) + 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]) From 0a644999c590594f1c7b2729172f3ab0bbfa5a55 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 13:45:57 +0530 Subject: [PATCH 38/48] turnoff logging for tui mode --- cognee/cli/commands/tui_command.py | 4 ++-- cognee/shared/logging_utils.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index c7136cfde..08ba54353 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -5,7 +5,7 @@ import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException from cognee.cli.tui.home_screen import HomeScreen from textual.app import App - +from cognee.shared.logging_utils import setup_logging class TuiCommand(SupportsCliCommand): @@ -40,9 +40,9 @@ class TuiCommand(SupportsCliCommand): """Push the home screen on mount.""" self.push_screen(HomeScreen()) + setup_logging(enable_console_logging=False) app = CogneeTUI() app.run() - fmt.success("TUI exited successfully!") except ImportError: raise CliCommandException( "Textual is not installed. Install with: pip install textual", diff --git a/cognee/shared/logging_utils.py b/cognee/shared/logging_utils.py index e8efde72c..2fdbe1cbb 100644 --- a/cognee/shared/logging_utils.py +++ b/cognee/shared/logging_utils.py @@ -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. From e4a218882044baa282b5ad55079e6bd1aff0c4f4 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 14:08:30 +0530 Subject: [PATCH 39/48] revert changes for delete_command --- cognee/cli/commands/delete_command.py | 33 +++++++++------------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/cognee/cli/commands/delete_command.py b/cognee/cli/commands/delete_command.py index df0a1cbe3..1a52d5e1b 100644 --- a/cognee/cli/commands/delete_command.py +++ b/cognee/cli/commands/delete_command.py @@ -1,14 +1,12 @@ import argparse import asyncio -from uuid import UUID +from typing import Optional + from cognee.cli.reference import SupportsCliCommand from cognee.cli import DEFAULT_DOCS_URL import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException, CliCommandInnerException from cognee.modules.data.methods.get_deletion_counts import get_deletion_counts -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 DeleteCommand(SupportsCliCommand): @@ -95,29 +93,20 @@ Be careful with deletion operations as they are irreversible. # Run the async delete function async def run_delete(): try: - if args.dataset_name: - # Use delete_datasets_by_name for dataset deletion - user = await get_default_user() - result = await delete_dataset_by_name(args.dataset_name, user.id) - - if result["not_found"]: - fmt.warning(f"Dataset '{args.dataset_name}' not found") - return False - - fmt.success(f"Successfully deleted {result['deleted_count']} dataset(s)") - return True + # NOTE: The underlying cognee.delete() function is currently not working as expected. + # This is a separate bug that this preview feature helps to expose. + if args.all: + await cognee.delete(dataset_name=None, user_id=args.user_id) else: - # For user_id deletion, use the original cognee.delete - result = await delete_data_by_user(UUID(args.user_id)) + await cognee.delete(dataset_name=args.dataset_name, user_id=args.user_id) except Exception as e: raise CliCommandInnerException(f"Failed to delete: {str(e)}") from e - return True - success = asyncio.run(run_delete()) - if success and not args.dataset_name: - fmt.success(f"Successfully deleted {operation}") + asyncio.run(run_delete()) + # This success message may be inaccurate due to the underlying bug, but we leave it for now. + fmt.success(f"Successfully deleted {operation}") except Exception as e: if isinstance(e, CliCommandInnerException): raise CliCommandException(str(e), error_code=1) from e - raise CliCommandException(f"Error deleting data: {str(e)}", error_code=1) from e + raise CliCommandException(f"Error deleting data: {str(e)}", error_code=1) from e \ No newline at end of file From 37e862c0535071985ef2cc8b0286000dbe2a972e Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Sun, 30 Nov 2025 14:20:25 +0530 Subject: [PATCH 40/48] ruff changes --- cognee/cli/commands/delete_command.py | 2 +- cognee/cli/commands/tui_command.py | 1 + cognee/cli/tui/add_screen.py | 35 +++++++------ cognee/cli/tui/base_screen.py | 5 +- cognee/cli/tui/cognify_screen.py | 50 +++++++++++-------- cognee/cli/tui/common_styles.py | 2 +- cognee/cli/tui/config_screen.py | 34 ++++++++----- cognee/cli/tui/delete_screen.py | 35 +++++++------ cognee/cli/tui/home_screen.py | 10 ++-- cognee/cli/tui/search_screen.py | 19 ++++--- .../sqlalchemy/SqlAlchemyAdapter.py | 9 ++-- .../data/methods/delete_data_by_user.py | 2 +- .../data/methods/delete_dataset_by_name.py | 6 +-- 13 files changed, 120 insertions(+), 90 deletions(-) diff --git a/cognee/cli/commands/delete_command.py b/cognee/cli/commands/delete_command.py index 1a52d5e1b..8400d3b0f 100644 --- a/cognee/cli/commands/delete_command.py +++ b/cognee/cli/commands/delete_command.py @@ -109,4 +109,4 @@ Be careful with deletion operations as they are irreversible. except Exception as e: if isinstance(e, CliCommandInnerException): raise CliCommandException(str(e), error_code=1) from e - raise CliCommandException(f"Error deleting data: {str(e)}", error_code=1) from e \ No newline at end of file + raise CliCommandException(f"Error deleting data: {str(e)}", error_code=1) from e diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index 08ba54353..19f46a18e 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -27,6 +27,7 @@ class TuiCommand(SupportsCliCommand): def execute(self, args: argparse.Namespace) -> None: try: + class CogneeTUI(App): """Main TUI application for cognee.""" diff --git a/cognee/cli/tui/add_screen.py b/cognee/cli/tui/add_screen.py index c74b539a6..371023baa 100644 --- a/cognee/cli/tui/add_screen.py +++ b/cognee/cli/tui/add_screen.py @@ -17,12 +17,15 @@ class AddTUIScreen(BaseTUIScreen): Binding("ctrl+v", "paste", "Paste", show=False), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ #data-input { height: 8; min-height: 8; } """ + ) def __init__(self): super().__init__() @@ -33,25 +36,21 @@ class AddTUIScreen(BaseTUIScreen): 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 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 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" - ) + yield Static("Ctrl+S: Add • Esc: Back • q: Quit", classes="tui-footer") def on_mount(self) -> None: """Focus the data input on mount.""" @@ -91,7 +90,7 @@ class AddTUIScreen(BaseTUIScreen): self.is_processing = True status.update("[yellow]⏳ Processing...[/yellow]") - + # Disable inputs during processing data_input.disabled = True dataset_input.disabled = True @@ -102,21 +101,21 @@ class AddTUIScreen(BaseTUIScreen): 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 diff --git a/cognee/cli/tui/base_screen.py b/cognee/cli/tui/base_screen.py index 7256e0353..f03688ff6 100644 --- a/cognee/cli/tui/base_screen.py +++ b/cognee/cli/tui/base_screen.py @@ -10,7 +10,9 @@ 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 + """ + CSS = ( + COMMON_STYLES + + """ #header { dock: top; background: $boost; @@ -21,6 +23,7 @@ class BaseTUIScreen(Screen): padding: 1; } """ + ) def compose_header(self) -> ComposeResult: """Compose the constant header widget.""" diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index 15035a912..c939da02e 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -16,7 +16,9 @@ class CognifyTUIScreen(BaseTUIScreen): Binding("ctrl+s", "submit", "Submit"), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ Checkbox { margin-top: 1; margin-bottom: 1; @@ -32,6 +34,7 @@ class CognifyTUIScreen(BaseTUIScreen): height: 1; } """ + ) def __init__(self): super().__init__() @@ -42,26 +45,23 @@ class CognifyTUIScreen(BaseTUIScreen): with Container(classes="tui-title-wrapper"): yield Static("⚡ Cognify Data", classes="tui-title-bordered") with Vertical(classes="tui-form"): - yield Label("Dataset Name (optional, leave empty for all):", classes="tui-label-spaced") - yield Input( - placeholder="Leave empty to process all datasets", - value="", - id="dataset-input" + yield Label( + "Dataset Name (optional, leave empty for all):", classes="tui-label-spaced" ) - + yield Input( + placeholder="Leave empty to process all datasets", 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" - ) + yield Static("Ctrl+S: Start • Esc: Back • q: Quit", classes="tui-footer") def on_mount(self) -> None: """Focus the dataset input on mount.""" @@ -90,12 +90,16 @@ class CognifyTUIScreen(BaseTUIScreen): 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" + 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 @@ -104,10 +108,13 @@ class CognifyTUIScreen(BaseTUIScreen): # 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 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 @@ -119,7 +126,9 @@ class CognifyTUIScreen(BaseTUIScreen): if LangchainChunker is not None: chunker_class = LangchainChunker else: - status.update("[yellow]⚠ LangchainChunker not available, using TextChunker[/yellow]") + status.update( + "[yellow]⚠ LangchainChunker not available, using TextChunker[/yellow]" + ) elif chunker_type == "CsvChunker": try: from cognee.modules.chunking.CsvChunker import CsvChunker @@ -129,24 +138,25 @@ class CognifyTUIScreen(BaseTUIScreen): 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 diff --git a/cognee/cli/tui/common_styles.py b/cognee/cli/tui/common_styles.py index 95a4ddd8e..4cd97ce7c 100644 --- a/cognee/cli/tui/common_styles.py +++ b/cognee/cli/tui/common_styles.py @@ -132,4 +132,4 @@ Button { height: auto; margin-bottom: 2; } -""" \ No newline at end of file +""" diff --git a/cognee/cli/tui/config_screen.py b/cognee/cli/tui/config_screen.py index f062a0f01..508d6d9b3 100644 --- a/cognee/cli/tui/config_screen.py +++ b/cognee/cli/tui/config_screen.py @@ -97,7 +97,9 @@ class ConfigTUIScreen(BaseTUIScreen): Binding("down", "cursor_down", "Down", show=False), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ DataTable { height: 1fr; text-align: center; @@ -129,6 +131,7 @@ class ConfigTUIScreen(BaseTUIScreen): margin-bottom: 1; } """ + ) # Config key mappings: Key -> (Reset Method Name, Default Value) CONFIG_MAP = { @@ -166,7 +169,7 @@ class ConfigTUIScreen(BaseTUIScreen): def compose_footer(self) -> ComposeResult: yield Static( "↑↓: Navigate • e: Edit • Enter: Save • r: Reset • Esc: Back • q: Quit", - classes="tui-footer" + classes="tui-footer", ) def on_mount(self) -> None: @@ -184,6 +187,7 @@ class ConfigTUIScreen(BaseTUIScreen): try: import cognee + # Check if get method exists, otherwise warn has_get = hasattr(cognee.config, "get") except ImportError: @@ -205,11 +209,13 @@ class ConfigTUIScreen(BaseTUIScreen): table.add_row(key, value_display, key=key) def action_cursor_up(self) -> None: - if self.editing_key: return + if self.editing_key: + return self.query_one(DataTable).action_cursor_up() def action_cursor_down(self) -> None: - if self.editing_key: return + if self.editing_key: + return self.query_one(DataTable).action_cursor_down() def action_cancel_or_back(self) -> None: @@ -223,10 +229,12 @@ class ConfigTUIScreen(BaseTUIScreen): def action_edit(self) -> None: """Start inline editing for the selected config value.""" - if self.editing_key: return + if self.editing_key: + return table = self.query_one(DataTable) - if table.cursor_row < 0: return + if table.cursor_row < 0: + return # Get row data using the cursor row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key @@ -253,7 +261,8 @@ class ConfigTUIScreen(BaseTUIScreen): def action_confirm_edit(self) -> None: """Confirm the inline edit and save the value.""" - if not self.editing_key: return + if not self.editing_key: + return input_widget = self.query_one("#inline-input", Input) value = input_widget.value.strip() @@ -276,7 +285,8 @@ class ConfigTUIScreen(BaseTUIScreen): def action_reset(self) -> None: """Reset the selected config to default.""" table = self.query_one(DataTable) - if table.cursor_row < 0: return + 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) @@ -292,10 +302,7 @@ class ConfigTUIScreen(BaseTUIScreen): if confirmed: self._reset_config(key) - self.app.push_screen( - ConfirmModal(key, display_default), - handle_confirm_result - ) + 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.""" @@ -373,6 +380,7 @@ class ConfigTUICommand(SupportsCliCommand): class ConfigTUIApp(App): """Simple app wrapper for config TUI.""" + CSS = """ Screen { background: $surface; } """ @@ -393,4 +401,4 @@ class ConfigTUICommand(SupportsCliCommand): f"Failed to launch config TUI: {str(ex)}", docs_url=self.docs_url, raiseable_exception=ex, - ) \ No newline at end of file + ) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index db278ca24..37959cf4a 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -21,13 +21,16 @@ class DeleteTUIScreen(BaseTUIScreen): Binding("ctrl+a", "delete_all", "Delete All", priority=True), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ #button-group { height: auto; align: center middle; margin-top: 2; } """ + ) def __init__(self): super().__init__() @@ -42,26 +45,22 @@ class DeleteTUIScreen(BaseTUIScreen): yield Label("Dataset Name (optional):", classes="tui-label") yield Input( placeholder="Enter dataset name to delete specific dataset", - id="dataset-input" + 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", - id="user-input" - ) - + yield Input(placeholder="Enter user ID to delete user's data", 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" + "Ctrl+s: Delete • Ctrl+a: Delete All • Esc: Back • q: Quit", classes="tui-footer" ) def on_mount(self) -> None: @@ -133,17 +132,19 @@ class DeleteTUIScreen(BaseTUIScreen): user = await get_default_user() user_id = user.id result = await delete_dataset_by_name(dataset_name, user_id) - + if result["not_found"]: status.update(f"⚠️ Dataset '{dataset_name}' not found") self.is_processing = False return - + status.update(f"✓ Successfully deleted {result['deleted_count']} dataset(s)") else: # For user_id deletion, use the new delete_data_by_user method result = await delete_data_by_user(UUID(user_id)) - status.update(f"✓ Successfully deleted {result['datasets_deleted']} datasets and {result['data_entries_deleted']} data entries for user '{user_id}'") + status.update( + f"✓ Successfully deleted {result['datasets_deleted']} datasets and {result['data_entries_deleted']} data entries for user '{user_id}'" + ) except Exception as e: status.update(f"✗ Error: {str(e)}") @@ -191,6 +192,7 @@ class DeleteTUIScreen(BaseTUIScreen): # Perform deletion - delete all uses the original cognee.delete import cognee + await cognee.delete(dataset_name=None, user_id=None) status.update("✓ Successfully deleted all data") @@ -214,7 +216,9 @@ class DeleteAllConfirmModal(BaseTUIScreen): Binding("escape", "cancel", "Cancel"), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ DeleteAllConfirmModal { align: center middle; } @@ -241,6 +245,7 @@ class DeleteAllConfirmModal(BaseTUIScreen): margin-bottom: 2; } """ + ) def compose_content(self) -> ComposeResult: with Container(id="confirm-dialog"): diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index e841a7f66..d23061175 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -34,7 +34,9 @@ class HomeScreen(BaseTUIScreen): Binding("down", "nav_down", "Down", priority=True), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ ListView > ListItem { width: 100%; padding: 0; @@ -108,6 +110,7 @@ class HomeScreen(BaseTUIScreen): color: $text-muted; } """ + ) def __init__(self): super().__init__() @@ -130,10 +133,7 @@ class HomeScreen(BaseTUIScreen): ) def compose_footer(self) -> ComposeResult: - yield Static( - "↑↓: Navigate • Enter: Select • q/Esc: Quit", - classes="tui-footer" - ) + yield Static("↑↓: Navigate • Enter: Select • q/Esc: Quit", classes="tui-footer") def on_mount(self) -> None: """Focus the list view on mount.""" diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index cb9152aff..97a292762 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -5,6 +5,7 @@ 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.""" @@ -14,7 +15,9 @@ class SearchTUIScreen(BaseTUIScreen): Binding("ctrl+s", "search", "Search"), ] - CSS = BaseTUIScreen.CSS + """ + CSS = ( + BaseTUIScreen.CSS + + """ #search-form { height: auto; border: solid $primary; @@ -48,6 +51,7 @@ class SearchTUIScreen(BaseTUIScreen): overflow-y: auto; } """ + ) def __init__(self): super().__init__() @@ -75,13 +79,12 @@ class SearchTUIScreen(BaseTUIScreen): 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") + 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" - ) + yield Static("Ctrl+S: Search • Esc: Back • q: Quit", classes="tui-footer") def on_mount(self) -> None: """Focus the query input on mount.""" @@ -157,7 +160,9 @@ class SearchTUIScreen(BaseTUIScreen): 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)]) + 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]) diff --git a/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py b/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py index c02c486a4..eeeaefdde 100644 --- a/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py +++ b/cognee/infrastructure/databases/relational/sqlalchemy/SqlAlchemyAdapter.py @@ -235,10 +235,10 @@ class SQLAlchemyAdapter: return [] 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" + 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 one or more entities from the specified table based on their ID(s). @@ -265,6 +265,7 @@ class SQLAlchemyAdapter: # Handle SQLite's foreign key requirement if self.engine.dialect.name == "sqlite": from sqlalchemy import text + await session.execute(text("PRAGMA foreign_keys = ON;")) # Construct the DELETE statement using the 'in_()' operator diff --git a/cognee/modules/data/methods/delete_data_by_user.py b/cognee/modules/data/methods/delete_data_by_user.py index 0f94df852..ab4a5aad7 100644 --- a/cognee/modules/data/methods/delete_data_by_user.py +++ b/cognee/modules/data/methods/delete_data_by_user.py @@ -33,4 +33,4 @@ async def delete_data_by_user(user_id: UUID): 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) \ No newline at end of file + await db_engine.delete_entities_by_id(Dataset.__table__.name, user_datasets_ids) diff --git a/cognee/modules/data/methods/delete_dataset_by_name.py b/cognee/modules/data/methods/delete_dataset_by_name.py index ae8f7704f..7c5ed06b8 100644 --- a/cognee/modules/data/methods/delete_dataset_by_name.py +++ b/cognee/modules/data/methods/delete_dataset_by_name.py @@ -4,9 +4,7 @@ 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 -): +async def delete_dataset_by_name(dataset_name: str, user_id: UUID): """ Delete a single dataset by name for a specific user. @@ -25,6 +23,6 @@ async def delete_dataset_by_name( .filter(Dataset.name == dataset_name) ) ).first() - #Keeping this out of the first session, since delete_entities_by_id creates another session. + # 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) From 3a85713722ad9e5cfe9db697fe86b250c02458db Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 1 Dec 2025 07:17:43 +0530 Subject: [PATCH 41/48] fixes for delete dataset method --- cognee/cli/tui/delete_screen.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 37959cf4a..ae6661193 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -1,4 +1,5 @@ import asyncio +import time from uuid import UUID from textual.app import ComposeResult from textual.widgets import Input, Button, Static, Label @@ -99,13 +100,14 @@ class DeleteTUIScreen(BaseTUIScreen): 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) - status = self.query_one(".tui-status", Static) dataset_name = dataset_input.value.strip() or None user_id = user_input.value.strip() or None @@ -116,39 +118,24 @@ class DeleteTUIScreen(BaseTUIScreen): 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: - # Perform deletion if dataset_name: - # Use delete_datasets_by_name for dataset deletion if user_id is None: user = await get_default_user() user_id = user.id - result = await delete_dataset_by_name(dataset_name, user_id) - - if result["not_found"]: - status.update(f"⚠️ Dataset '{dataset_name}' not found") - self.is_processing = False - return - - status.update(f"✓ Successfully deleted {result['deleted_count']} dataset(s)") + await delete_dataset_by_name(dataset_name, user_id) else: - # For user_id deletion, use the new delete_data_by_user method - result = await delete_data_by_user(UUID(user_id)) - status.update( - f"✓ Successfully deleted {result['datasets_deleted']} datasets and {result['data_entries_deleted']} data entries for user '{user_id}'" - ) - + await delete_data_by_user(UUID(user_id)) except Exception as e: status.update(f"✗ Error: {str(e)}") finally: + status.update("✓ Successfully deleted dataset.") self.is_processing = False def _handle_delete_all(self) -> None: From 8b49b166869a500303607a461ecde0552258c369 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 1 Dec 2025 07:43:42 +0530 Subject: [PATCH 42/48] fix for delete all --- cognee/cli/tui/delete_screen.py | 53 ++++++++++++--------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index ae6661193..8c05b1e85 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -6,7 +6,6 @@ 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.get_deletion_counts import get_deletion_counts 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 @@ -51,7 +50,7 @@ class DeleteTUIScreen(BaseTUIScreen): 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", id="user-input") + 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") @@ -135,54 +134,34 @@ class DeleteTUIScreen(BaseTUIScreen): except Exception as e: status.update(f"✗ Error: {str(e)}") finally: - status.update("✓ Successfully deleted dataset.") + status.update(f"✓ Successfully deleted dataset '{dataset_name}'.") 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()) + asyncio.create_task(self._perform_delete_all(user_id)) self.app.push_screen(DeleteAllConfirmModal(), handle_confirm) - async def _perform_delete_all(self) -> None: + 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("🔍 Checking all data...") - - # Get preview - preview_data = await get_deletion_counts( - dataset_name=None, - user_id=None, - all_data=True, - ) - - if not preview_data: - status.update("✓ No data found to delete") - self.is_processing = False - return - - preview_msg = ( - f"Deleting ALL data:\n" - f"Datasets: {preview_data.datasets}\n" - f"Entries: {preview_data.entries}\n" - f"Users: {preview_data.users}" - ) - status.update(preview_msg) - - # Perform deletion - delete all uses the original cognee.delete - import cognee - - await cognee.delete(dataset_name=None, user_id=None) - - status.update("✓ Successfully deleted all data") + status.update("🔍 Deleting all data...") + if user_id is None: + user = await get_default_user() + user_id = user.id + await delete_data_by_user(user_id) + status.update(f"✓ Successfully deleted all data by user ") # Clear inputs dataset_input = self.query_one("#dataset-input", Input) @@ -195,6 +174,12 @@ class DeleteTUIScreen(BaseTUIScreen): 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.""" From ac17654dc5ab362cc0cf046e999df79bc7d8984b Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 1 Dec 2025 07:44:33 +0530 Subject: [PATCH 43/48] ruff changes --- cognee/cli/tui/delete_screen.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 8c05b1e85..8fd1cc99b 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -1,5 +1,4 @@ import asyncio -import time from uuid import UUID from textual.app import ComposeResult from textual.widgets import Input, Button, Static, Label @@ -50,7 +49,10 @@ class DeleteTUIScreen(BaseTUIScreen): 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") + 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") @@ -144,6 +146,7 @@ class DeleteTUIScreen(BaseTUIScreen): 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)) From c7ee3c37da607ca242ffe89df5377ea93c78b862 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 29 Dec 2025 19:28:20 +0530 Subject: [PATCH 44/48] CodeReview changes --- cognee/cli/commands/tui_command.py | 5 ++--- cognee/cli/tui/delete_screen.py | 20 +++++++++++-------- cognee/cli/tui/home_screen.py | 2 ++ cognee/cli/tui/search_screen.py | 2 +- .../data/methods/delete_data_by_user.py | 7 ++----- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index 19f46a18e..b13b1f078 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -4,8 +4,6 @@ from cognee.cli.config import DEFAULT_DOCS_URL import cognee.cli.echo as fmt from cognee.cli.exceptions import CliCommandException from cognee.cli.tui.home_screen import HomeScreen -from textual.app import App -from cognee.shared.logging_utils import setup_logging class TuiCommand(SupportsCliCommand): @@ -27,7 +25,8 @@ class TuiCommand(SupportsCliCommand): def execute(self, args: argparse.Namespace) -> None: try: - + from textual.app import App + from cognee.shared.logging_utils import setup_logging class CogneeTUI(App): """Main TUI application for cognee.""" diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index 8fd1cc99b..feeb26769 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -129,14 +129,16 @@ class DeleteTUIScreen(BaseTUIScreen): if dataset_name: if user_id is None: user = await get_default_user() - user_id = user.id - await delete_dataset_by_name(dataset_name, user_id) + resolved_user_id = user.id + else: + resolved_user_id = UUID(user_id) + await delete_dataset_by_name(dataset_name, resolved_user_id) else: - await delete_data_by_user(UUID(user_id)) + await delete_data_by_user(resolved_user_id) + status.update(f"✓ Successfully deleted dataset '{dataset_name}'.") except Exception as e: status.update(f"✗ Error: {str(e)}") finally: - status.update(f"✓ Successfully deleted dataset '{dataset_name}'.") self.is_processing = False self.clear_input() @@ -161,10 +163,12 @@ class DeleteTUIScreen(BaseTUIScreen): try: status.update("🔍 Deleting all data...") if user_id is None: - user = await get_default_user() - user_id = user.id - await delete_data_by_user(user_id) - status.update(f"✓ Successfully deleted all data by user ") + 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) diff --git a/cognee/cli/tui/home_screen.py b/cognee/cli/tui/home_screen.py index d23061175..22165bd1d 100644 --- a/cognee/cli/tui/home_screen.py +++ b/cognee/cli/tui/home_screen.py @@ -163,6 +163,8 @@ class HomeScreen(BaseTUIScreen): 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 diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 97a292762..d70d29bd1 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -71,7 +71,7 @@ class SearchTUIScreen(BaseTUIScreen): ("RAG Completion", "RAG_COMPLETION"), ("Chunks", "CHUNKS"), ("Summaries", "SUMMARIES"), - ("Code", "CODE"), + ("Coding Rules", "CODING_RULES"), ], value="GRAPH_COMPLETION", id="query-type-select", diff --git a/cognee/modules/data/methods/delete_data_by_user.py b/cognee/modules/data/methods/delete_data_by_user.py index ab4a5aad7..4358bd8e1 100644 --- a/cognee/modules/data/methods/delete_data_by_user.py +++ b/cognee/modules/data/methods/delete_data_by_user.py @@ -19,16 +19,13 @@ async def delete_data_by_user(user_id: UUID): user_id: UUID of the user whose data should be deleted Raises: - ValueError: If user is not found + EntityNotFoundError: If user is not found """ db_engine = get_relational_engine() async with db_engine.get_async_session() as session: # Verify user exists - user = await get_user(user_id) - if not user: - raise ValueError(f"User with ID {user_id} not found") - + 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() From 7d3586e1b20eb5fceffa2aaa5c64b05fcf5d8f1b Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 29 Dec 2025 20:12:50 +0530 Subject: [PATCH 45/48] improving search screen --- cognee/cli/tui/search_screen.py | 69 ++++++---- tests/reproduce_tui_search_logic.py | 189 ++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 23 deletions(-) create mode 100644 tests/reproduce_tui_search_logic.py diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index d70d29bd1..943825888 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -1,7 +1,7 @@ import asyncio from textual.app import ComposeResult -from textual.widgets import Input, Label, Static, Select -from textual.containers import Container, Vertical, ScrollableContainer +from textual.widgets import Input, Label, Static, Select, ListView, ListItem +from textual.containers import Container, Vertical from textual.binding import Binding from cognee.cli.tui.base_screen import BaseTUIScreen @@ -46,7 +46,7 @@ class SearchTUIScreen(BaseTUIScreen): margin-bottom: 1; } - #results-content { + #results-list { height: 1fr; overflow-y: auto; } @@ -78,19 +78,22 @@ class SearchTUIScreen(BaseTUIScreen): ) 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" - ) + yield ListView(id="results-list") 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.""" + """Focus the query input on mount and show initial help text.""" query_input = self.query_one("#query-input", Input) query_input.focus() + # Add initial help text to list + results_list = self.query_one("#results-list", ListView) + results_list.mount( + ListItem(Label("Enter a query and click Search to see results.")) + ) + def action_back(self) -> None: """Go back to home screen.""" self.app.pop_screen() @@ -128,17 +131,23 @@ class SearchTUIScreen(BaseTUIScreen): 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...") + results_list = self.query_one("#results-list", ListView) + results_list.clear() + results_list.mount(ListItem(Label("🔍 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.""" + results_list = self.query_one("#results-list", ListView) + try: import cognee from cognee.modules.search.types import SearchType + from cognee.infrastructure.databases.exceptions.exceptions import ( + EntityNotFoundError, + ) # Convert string to SearchType enum search_type = SearchType[query_type] @@ -150,29 +159,43 @@ class SearchTUIScreen(BaseTUIScreen): top_k=10, ) - # Update results display - results_text = self.query_one("#results-text", Static) + # Clear loading message + results_list.clear() if not results: - results_text.update("No results found for your query.") + results_list.mount( + ListItem(Label("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]) + for result in results: + results_list.mount(ListItem(Label(f"📝 {result}"))) elif query_type == "CHUNKS": - formatted = "\n\n".join( - [f"📄 Chunk {i + 1}:\n{result}" for i, result in enumerate(results)] - ) + for i, result in enumerate(results): + results_list.mount( + ListItem(Label(f"📄 Chunk {i + 1}:\n{result}")) + ) else: - formatted = "\n\n".join([f"• {result}" for result in results]) + for result in results: + results_list.mount(ListItem(Label(f"• {result}"))) + + self.notify(f"✓ Found {len(results)} result(s)", severity="information") - results_text.update(formatted) - - self.notify(f"✓ Found {len(results)} result(s)", severity="information") + except EntityNotFoundError: + results_list.clear() + results_list.mount( + ListItem( + Label( + "No data found. Please run 'cognee cognify' to process your data first." + ) + ) + ) + self.notify("Knowledge graph is empty", severity="warning") except Exception as e: - results_text = self.query_one("#results-text", Static) - results_text.update(f"❌ Error: {str(e)}") + results_list.clear() + results_list.mount(ListItem(Label(f"❌ Error: {str(e)}"))) self.notify(f"Search failed: {str(e)}", severity="error") finally: diff --git a/tests/reproduce_tui_search_logic.py b/tests/reproduce_tui_search_logic.py new file mode 100644 index 000000000..912674979 --- /dev/null +++ b/tests/reproduce_tui_search_logic.py @@ -0,0 +1,189 @@ +import sys +import asyncio +from unittest.mock import MagicMock, AsyncMock + +# 1. Setup Mocks for cognee dependencies +mock_cognee = MagicMock() +sys.modules["cognee"] = mock_cognee +sys.modules["cognee.version"] = MagicMock() +sys.modules["cognee.cli.tui.common_styles"] = MagicMock() +sys.modules["cognee.cli.tui.common_styles"].COMMON_STYLES = "" + +# Define the exception we want to catch +class EntityNotFoundError(Exception): + pass + +# Setup the exception in the mocked module structure +sys.modules["cognee.infrastructure"] = MagicMock() +sys.modules["cognee.infrastructure.databases"] = MagicMock() +sys.modules["cognee.infrastructure.databases.exceptions"] = MagicMock() +exceptions_mock = MagicMock() +exceptions_mock.EntityNotFoundError = EntityNotFoundError +sys.modules["cognee.infrastructure.databases.exceptions.exceptions"] = exceptions_mock + +# Setup search types +sys.modules["cognee.modules"] = MagicMock() +sys.modules["cognee.modules.search"] = MagicMock() +types_mock = MagicMock() + +# Mock SearchType to support item access +class MockSearchTypeMeta(type): + def __getitem__(cls, key): + return key +class MockSearchType(metaclass=MockSearchTypeMeta): + pass +types_mock.SearchType = MockSearchType +sys.modules["cognee.modules.search.types"] = types_mock + +import importlib.util +import os + +# ... existing mocks ... +# Ensure we have deep mocks for structure +sys.modules["cognee.cli"] = MagicMock() +sys.modules["cognee.cli.tui"] = MagicMock() + +# Mock BaseTUIScreen specifically +base_screen_mock = MagicMock() +class MockBaseScreen: + CSS = "" + def __init__(self): + pass + def compose_header(self): yield from () + def compose_footer(self): yield from () +base_screen_mock.BaseTUIScreen = MockBaseScreen +# Crucial: Mock the specific module path search_screen tries to import from +sys.modules["cognee.cli.tui.base_screen"] = base_screen_mock + +# Also mock textual.binding which is imported at top level +sys.modules["textual.binding"] = MagicMock() + +# Now load the file directly +module_path = os.path.join(os.getcwd(), "cognee/cli/tui/search_screen.py") +spec = importlib.util.spec_from_file_location("search_screen_mod", module_path) +search_screen_mod = importlib.util.module_from_spec(spec) + +# Before executing, ensure imports in that file will resolve to our mocks +# The file does: from textual... +# Real Textual is explicitly NOT mocked in sys.modules so it loads real textual (if installed) +# But we mocked textual.binding above? +# Actually, let's NOT mock textual.binding if we can avoid it, or mock it if it's simple. +# Real code: from textual.binding import Binding. +# If textual is installed, we should leverage it. If not, mock it. +try: + import textual +except ImportError: + # If textual not installed/available in this step runner, we must mock it all + sys.modules["textual"] = MagicMock() + sys.modules["textual.app"] = MagicMock() + sys.modules["textual.widgets"] = MagicMock() + sys.modules["textual.containers"] = MagicMock() + sys.modules["textual.binding"] = MagicMock() + # We need to provide Widget classes that search_screen inherits/uses + # It imports: Input, Label, Static, Select, ListView, ListItem + # It uses: ComposeResult (type) + + # Simple Mock widgets + class MockWidget: + def __init__(self, *args, **kwargs): pass + def focus(self): pass + def mount(self, *args): pass + def clear(self): pass + def update(self, *args): pass + + sys.modules["textual.widgets"].Input = MockWidget + sys.modules["textual.widgets"].Label = MockWidget + sys.modules["textual.widgets"].Static = MockWidget + sys.modules["textual.widgets"].Select = MockWidget + sys.modules["textual.widgets"].ListView = MockWidget + sys.modules["textual.widgets"].ListItem = MockWidget + + sys.modules["textual.containers"].Container = MagicMock() + sys.modules["textual.containers"].Vertical = MagicMock() + sys.modules["textual.app"].ComposeResult = MagicMock() + +# Execute the module +spec.loader.exec_module(search_screen_mod) +SearchTUIScreen = search_screen_mod.SearchTUIScreen + +async def test_empty_graph_handling(): + print("Testing Empty Graph (EntityNotFoundError) Handling...") + + # Instantiate screen + screen = SearchTUIScreen() + + # Mock query_one to return our list view mock + results_list_mock = MagicMock() + + def query_one_side_effect(selector, type_cls=None): + if "ListView" in str(type_cls) or "list" in str(selector): + return results_list_mock + return MagicMock() # For other queries like Static + + screen.query_one = MagicMock(side_effect=query_one_side_effect) + screen.notify = MagicMock() + + # Configure cognee.search to raise EntityNotFoundError + mock_cognee.search = AsyncMock(side_effect=EntityNotFoundError("Graph is empty")) + + # Run the method + # Note: query_type needs to be a valid key in our MockSearchType or just a string if we mocked it right + await screen._async_search("test query", "GRAPH_COMPLETION") + + # Verification + # 1. usage of clear() + results_list_mock.clear.assert_called() + + # 2. usage of mount() with correct message + assert results_list_mock.mount.called + + # Check that notify was called with warning + # We allow flexible matching for the exact message but check 'warning' severity + args = screen.notify.call_args + if args: + assert args[1].get('severity') == 'warning' or 'Knowledge graph is empty' in args[0][0] + else: + print("FAIL: notify was not called") + + print("SUCCESS: EntityNotFoundError was caught and handled correctly.") + +async def test_generic_error_handling(): + print("\nTesting Generic Error Handling...") + screen = SearchTUIScreen() + results_list_mock = MagicMock() + screen.query_one = MagicMock(return_value=results_list_mock) + screen.notify = MagicMock() + + # Configure generic error + mock_cognee.search = AsyncMock(side_effect=Exception("Something bad happened")) + + await screen._async_search("test query", "GRAPH_COMPLETION") + + screen.notify.assert_called() + args = screen.notify.call_args + # Check for error severity or message + assert args[1].get('severity') == 'error' or "Search failed" in args[0][0] + print("SUCCESS: Generic Exception was caught and handled correctly.") + +async def test_success_path(): + print("\nTesting Success Path...") + screen = SearchTUIScreen() + results_list_mock = MagicMock() + screen.query_one = MagicMock(return_value=results_list_mock) + screen.notify = MagicMock() + + # Configure success + mock_cognee.search = AsyncMock(return_value=["Result 1", "Result 2"]) + + await screen._async_search("test query", "GRAPH_COMPLETION") + + assert results_list_mock.clear.called + assert results_list_mock.mount.called + # We expect 2 mount calls for results + assert results_list_mock.mount.call_count == 2 + print("SUCCESS: Search results were displayed.") + +if __name__ == "__main__": + asyncio.run(test_empty_graph_handling()) + asyncio.run(test_generic_error_handling()) + asyncio.run(test_success_path()) From ac8d130af2187fd77d71d0098d484fe39aa533e3 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 29 Dec 2025 20:18:50 +0530 Subject: [PATCH 46/48] code review changes --- cognee/cli/commands/tui_command.py | 2 +- cognee/cli/tui/delete_screen.py | 14 ++++++++------ tests/reproduce_tui_search_logic.py | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cognee/cli/commands/tui_command.py b/cognee/cli/commands/tui_command.py index b13b1f078..3ff313be9 100644 --- a/cognee/cli/commands/tui_command.py +++ b/cognee/cli/commands/tui_command.py @@ -3,7 +3,6 @@ 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 -from cognee.cli.tui.home_screen import HomeScreen class TuiCommand(SupportsCliCommand): @@ -26,6 +25,7 @@ class TuiCommand(SupportsCliCommand): 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.""" diff --git a/cognee/cli/tui/delete_screen.py b/cognee/cli/tui/delete_screen.py index feeb26769..e3abb263b 100644 --- a/cognee/cli/tui/delete_screen.py +++ b/cognee/cli/tui/delete_screen.py @@ -126,16 +126,18 @@ class DeleteTUIScreen(BaseTUIScreen): """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: - if user_id is None: - user = await get_default_user() - resolved_user_id = user.id - else: - resolved_user_id = UUID(user_id) 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 dataset '{dataset_name}'.") + status.update(f"✓ Successfully deleted all data for user {resolved_user_id}.") except Exception as e: status.update(f"✗ Error: {str(e)}") finally: diff --git a/tests/reproduce_tui_search_logic.py b/tests/reproduce_tui_search_logic.py index 912674979..785a4a65d 100644 --- a/tests/reproduce_tui_search_logic.py +++ b/tests/reproduce_tui_search_logic.py @@ -139,11 +139,11 @@ async def test_empty_graph_handling(): # Check that notify was called with warning # We allow flexible matching for the exact message but check 'warning' severity + assert screen.notify.called, "notify was not called" args = screen.notify.call_args - if args: - assert args[1].get('severity') == 'warning' or 'Knowledge graph is empty' in args[0][0] - else: - print("FAIL: notify was not called") + assert args is not None + # Assert specific conditions on args + assert args[1].get('severity') == 'warning' or 'Knowledge graph is empty' in args[0][0] print("SUCCESS: EntityNotFoundError was caught and handled correctly.") From d055f0134422ac6825a092dc071100a3ccce181e Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 29 Dec 2025 21:30:58 +0530 Subject: [PATCH 47/48] reverting search screen changes --- cognee/cli/tui/search_screen.py | 71 ++++------- tests/reproduce_tui_search_logic.py | 189 ---------------------------- 2 files changed, 24 insertions(+), 236 deletions(-) delete mode 100644 tests/reproduce_tui_search_logic.py diff --git a/cognee/cli/tui/search_screen.py b/cognee/cli/tui/search_screen.py index 943825888..d70d29bd1 100644 --- a/cognee/cli/tui/search_screen.py +++ b/cognee/cli/tui/search_screen.py @@ -1,7 +1,7 @@ import asyncio from textual.app import ComposeResult -from textual.widgets import Input, Label, Static, Select, ListView, ListItem -from textual.containers import Container, Vertical +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 @@ -46,7 +46,7 @@ class SearchTUIScreen(BaseTUIScreen): margin-bottom: 1; } - #results-list { + #results-content { height: 1fr; overflow-y: auto; } @@ -78,22 +78,19 @@ class SearchTUIScreen(BaseTUIScreen): ) with Container(id="results-container"): yield Static("Results", id="results-title") - yield ListView(id="results-list") + 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 and show initial help text.""" + """Focus the query input on mount.""" query_input = self.query_one("#query-input", Input) query_input.focus() - # Add initial help text to list - results_list = self.query_one("#results-list", ListView) - results_list.mount( - ListItem(Label("Enter a query and click Search to see results.")) - ) - def action_back(self) -> None: """Go back to home screen.""" self.app.pop_screen() @@ -131,23 +128,17 @@ class SearchTUIScreen(BaseTUIScreen): self.notify(f"Searching for: {query_text}", severity="information") # Update results to show loading - results_list = self.query_one("#results-list", ListView) - results_list.clear() - results_list.mount(ListItem(Label("🔍 Searching..."))) + 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.""" - results_list = self.query_one("#results-list", ListView) - try: import cognee from cognee.modules.search.types import SearchType - from cognee.infrastructure.databases.exceptions.exceptions import ( - EntityNotFoundError, - ) # Convert string to SearchType enum search_type = SearchType[query_type] @@ -159,43 +150,29 @@ class SearchTUIScreen(BaseTUIScreen): top_k=10, ) - # Clear loading message - results_list.clear() + # Update results display + results_text = self.query_one("#results-text", Static) if not results: - results_list.mount( - ListItem(Label("No results found for your query.")) - ) + results_text.update("No results found for your query.") else: # Format results based on type if query_type in ["GRAPH_COMPLETION", "RAG_COMPLETION"]: - for result in results: - results_list.mount(ListItem(Label(f"📝 {result}"))) + formatted = "\n\n".join([f"📝 {result}" for result in results]) elif query_type == "CHUNKS": - for i, result in enumerate(results): - results_list.mount( - ListItem(Label(f"📄 Chunk {i + 1}:\n{result}")) - ) - else: - for result in results: - results_list.mount(ListItem(Label(f"• {result}"))) - - self.notify(f"✓ Found {len(results)} result(s)", severity="information") - - except EntityNotFoundError: - results_list.clear() - results_list.mount( - ListItem( - Label( - "No data found. Please run 'cognee cognify' to process your data first." + formatted = "\n\n".join( + [f"📄 Chunk {i + 1}:\n{result}" for i, result in enumerate(results)] ) - ) - ) - self.notify("Knowledge graph is empty", severity="warning") + 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_list.clear() - results_list.mount(ListItem(Label(f"❌ Error: {str(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: diff --git a/tests/reproduce_tui_search_logic.py b/tests/reproduce_tui_search_logic.py deleted file mode 100644 index 785a4a65d..000000000 --- a/tests/reproduce_tui_search_logic.py +++ /dev/null @@ -1,189 +0,0 @@ -import sys -import asyncio -from unittest.mock import MagicMock, AsyncMock - -# 1. Setup Mocks for cognee dependencies -mock_cognee = MagicMock() -sys.modules["cognee"] = mock_cognee -sys.modules["cognee.version"] = MagicMock() -sys.modules["cognee.cli.tui.common_styles"] = MagicMock() -sys.modules["cognee.cli.tui.common_styles"].COMMON_STYLES = "" - -# Define the exception we want to catch -class EntityNotFoundError(Exception): - pass - -# Setup the exception in the mocked module structure -sys.modules["cognee.infrastructure"] = MagicMock() -sys.modules["cognee.infrastructure.databases"] = MagicMock() -sys.modules["cognee.infrastructure.databases.exceptions"] = MagicMock() -exceptions_mock = MagicMock() -exceptions_mock.EntityNotFoundError = EntityNotFoundError -sys.modules["cognee.infrastructure.databases.exceptions.exceptions"] = exceptions_mock - -# Setup search types -sys.modules["cognee.modules"] = MagicMock() -sys.modules["cognee.modules.search"] = MagicMock() -types_mock = MagicMock() - -# Mock SearchType to support item access -class MockSearchTypeMeta(type): - def __getitem__(cls, key): - return key -class MockSearchType(metaclass=MockSearchTypeMeta): - pass -types_mock.SearchType = MockSearchType -sys.modules["cognee.modules.search.types"] = types_mock - -import importlib.util -import os - -# ... existing mocks ... -# Ensure we have deep mocks for structure -sys.modules["cognee.cli"] = MagicMock() -sys.modules["cognee.cli.tui"] = MagicMock() - -# Mock BaseTUIScreen specifically -base_screen_mock = MagicMock() -class MockBaseScreen: - CSS = "" - def __init__(self): - pass - def compose_header(self): yield from () - def compose_footer(self): yield from () -base_screen_mock.BaseTUIScreen = MockBaseScreen -# Crucial: Mock the specific module path search_screen tries to import from -sys.modules["cognee.cli.tui.base_screen"] = base_screen_mock - -# Also mock textual.binding which is imported at top level -sys.modules["textual.binding"] = MagicMock() - -# Now load the file directly -module_path = os.path.join(os.getcwd(), "cognee/cli/tui/search_screen.py") -spec = importlib.util.spec_from_file_location("search_screen_mod", module_path) -search_screen_mod = importlib.util.module_from_spec(spec) - -# Before executing, ensure imports in that file will resolve to our mocks -# The file does: from textual... -# Real Textual is explicitly NOT mocked in sys.modules so it loads real textual (if installed) -# But we mocked textual.binding above? -# Actually, let's NOT mock textual.binding if we can avoid it, or mock it if it's simple. -# Real code: from textual.binding import Binding. -# If textual is installed, we should leverage it. If not, mock it. -try: - import textual -except ImportError: - # If textual not installed/available in this step runner, we must mock it all - sys.modules["textual"] = MagicMock() - sys.modules["textual.app"] = MagicMock() - sys.modules["textual.widgets"] = MagicMock() - sys.modules["textual.containers"] = MagicMock() - sys.modules["textual.binding"] = MagicMock() - # We need to provide Widget classes that search_screen inherits/uses - # It imports: Input, Label, Static, Select, ListView, ListItem - # It uses: ComposeResult (type) - - # Simple Mock widgets - class MockWidget: - def __init__(self, *args, **kwargs): pass - def focus(self): pass - def mount(self, *args): pass - def clear(self): pass - def update(self, *args): pass - - sys.modules["textual.widgets"].Input = MockWidget - sys.modules["textual.widgets"].Label = MockWidget - sys.modules["textual.widgets"].Static = MockWidget - sys.modules["textual.widgets"].Select = MockWidget - sys.modules["textual.widgets"].ListView = MockWidget - sys.modules["textual.widgets"].ListItem = MockWidget - - sys.modules["textual.containers"].Container = MagicMock() - sys.modules["textual.containers"].Vertical = MagicMock() - sys.modules["textual.app"].ComposeResult = MagicMock() - -# Execute the module -spec.loader.exec_module(search_screen_mod) -SearchTUIScreen = search_screen_mod.SearchTUIScreen - -async def test_empty_graph_handling(): - print("Testing Empty Graph (EntityNotFoundError) Handling...") - - # Instantiate screen - screen = SearchTUIScreen() - - # Mock query_one to return our list view mock - results_list_mock = MagicMock() - - def query_one_side_effect(selector, type_cls=None): - if "ListView" in str(type_cls) or "list" in str(selector): - return results_list_mock - return MagicMock() # For other queries like Static - - screen.query_one = MagicMock(side_effect=query_one_side_effect) - screen.notify = MagicMock() - - # Configure cognee.search to raise EntityNotFoundError - mock_cognee.search = AsyncMock(side_effect=EntityNotFoundError("Graph is empty")) - - # Run the method - # Note: query_type needs to be a valid key in our MockSearchType or just a string if we mocked it right - await screen._async_search("test query", "GRAPH_COMPLETION") - - # Verification - # 1. usage of clear() - results_list_mock.clear.assert_called() - - # 2. usage of mount() with correct message - assert results_list_mock.mount.called - - # Check that notify was called with warning - # We allow flexible matching for the exact message but check 'warning' severity - assert screen.notify.called, "notify was not called" - args = screen.notify.call_args - assert args is not None - # Assert specific conditions on args - assert args[1].get('severity') == 'warning' or 'Knowledge graph is empty' in args[0][0] - - print("SUCCESS: EntityNotFoundError was caught and handled correctly.") - -async def test_generic_error_handling(): - print("\nTesting Generic Error Handling...") - screen = SearchTUIScreen() - results_list_mock = MagicMock() - screen.query_one = MagicMock(return_value=results_list_mock) - screen.notify = MagicMock() - - # Configure generic error - mock_cognee.search = AsyncMock(side_effect=Exception("Something bad happened")) - - await screen._async_search("test query", "GRAPH_COMPLETION") - - screen.notify.assert_called() - args = screen.notify.call_args - # Check for error severity or message - assert args[1].get('severity') == 'error' or "Search failed" in args[0][0] - print("SUCCESS: Generic Exception was caught and handled correctly.") - -async def test_success_path(): - print("\nTesting Success Path...") - screen = SearchTUIScreen() - results_list_mock = MagicMock() - screen.query_one = MagicMock(return_value=results_list_mock) - screen.notify = MagicMock() - - # Configure success - mock_cognee.search = AsyncMock(return_value=["Result 1", "Result 2"]) - - await screen._async_search("test query", "GRAPH_COMPLETION") - - assert results_list_mock.clear.called - assert results_list_mock.mount.called - # We expect 2 mount calls for results - assert results_list_mock.mount.call_count == 2 - print("SUCCESS: Search results were displayed.") - -if __name__ == "__main__": - asyncio.run(test_empty_graph_handling()) - asyncio.run(test_generic_error_handling()) - asyncio.run(test_success_path()) From f1dac4a5cc8100ce8173c354f21630ac35b706a1 Mon Sep 17 00:00:00 2001 From: rajeevrajeshuni Date: Mon, 29 Dec 2025 21:48:02 +0530 Subject: [PATCH 48/48] fix a minor bug --- cognee/cli/tui/cognify_screen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cognee/cli/tui/cognify_screen.py b/cognee/cli/tui/cognify_screen.py index c939da02e..e2f6e916c 100644 --- a/cognee/cli/tui/cognify_screen.py +++ b/cognee/cli/tui/cognify_screen.py @@ -46,10 +46,10 @@ class CognifyTUIScreen(BaseTUIScreen): yield Static("⚡ Cognify Data", classes="tui-title-bordered") with Vertical(classes="tui-form"): yield Label( - "Dataset Name (optional, leave empty for all):", classes="tui-label-spaced" + "Dataset Name:", classes="tui-label-spaced" ) yield Input( - placeholder="Leave empty to process all datasets", value="", id="dataset-input" + placeholder="Enter the dataset name here.", value="", id="dataset-input" ) yield Label("Chunker Type:", classes="tui-label-spaced")