From a55ba9b0106ae429ed1c20124f528336f95806f5 Mon Sep 17 00:00:00 2001 From: Tactical-Camell Date: Sat, 12 Sep 2026 12:23:30 +0400 Subject: [PATCH 1/2] feat: add task categorization and tagging --- README.md | 9 ++- pytest.ini | 2 + src/agent.py | 135 +++++++++++++++++++++++++++++++++++++++++--- src/main.py | 48 ++++++++++++---- tests/conftest.py | 3 + tests/test_agent.py | 83 ++++++++++++++++++++++++++- 6 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/conftest.py diff --git a/README.md b/README.md index b2382f9..6e845be 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,16 @@ You'll see an interactive prompt. Here are the available commands: | Command | Description | Example | |---------|-------------|---------| -| `add ` | Add a new task | `add Buy groceries` | +| `add [--priority high\|medium\|low] [--category ]` | Add a new task with optional priority and category | `add Buy milk --priority high --category shopping` | | `tasks` | Display all tasks | `tasks` | | `complete ` | Mark a task as complete | `complete 1` | +| `priority ` | Change task priority (high, medium, low) | `priority 1 high` | +| `category ` | Set or update task category | `category 1 work` | +| `list ` | Filter and view tasks in a category | `list work` | +| `categories` | Display all categories and active task counts | `categories` | +| `sort` | Sort tasks by priority level | `sort` | +| `summary` | View active priority summary | `summary` | +| `tools` | List available tool functions | `tools` | | `ai ` | Ask AI a question | `ai How should I prioritize my tasks?` | | `exit` | Exit the application | `exit` | diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ad5c7cc --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . src diff --git a/src/agent.py b/src/agent.py index 5915e65..9a9814e 100644 --- a/src/agent.py +++ b/src/agent.py @@ -9,9 +9,9 @@ def __init__(self): self.tasks = [] self.client = OpenAI(api_key=OPENAI_API_KEY) - def add_task(self, task, priority="medium"): + def add_task(self, task, priority="medium", category=None, tags=None): """ - Add a new task with priority level. + Add a new task with priority level and optional category/tags. Priority can be: 'high', 'medium', 'low' """ priority = (priority or "medium").lower() @@ -22,18 +22,32 @@ def add_task(self, task, priority="medium"): if not task: return "Error: Task title cannot be empty." + clean_category = category.strip().lower() if category and category.strip() else None + tag_list = [] + if tags: + if isinstance(tags, str): + tag_list = [t.strip().lower() for t in tags.split(",") if t.strip()] + elif isinstance(tags, (list, tuple, set)): + tag_list = [str(t).strip().lower() for t in tags if str(t).strip()] + if clean_category and clean_category not in tag_list: + tag_list.insert(0, clean_category) + task_dict = { "title": task, "priority": priority, + "category": clean_category, + "tags": tag_list, "completed": False } self.tasks.append(task_dict) + if clean_category: + return f"Task added: {task} (Priority: {priority.capitalize()}, Category: {clean_category})" return f"Task added: {task} (Priority: {priority.capitalize()})" - def view_tasks(self, show_completed=False): + def view_tasks(self, show_completed=False, category=None): """ - View all tasks with their priority levels. + View all tasks with their priority levels and optional category filter. """ if not self.tasks: return "No tasks available." @@ -42,7 +56,12 @@ def view_tasks(self, show_completed=False): if not show_completed: tasks_to_show = [t for t in self.tasks if not t.get('completed', False)] - if not tasks_to_show: + if category: + cat_lower = category.strip().lower() + tasks_to_show = [t for t in tasks_to_show if t.get('category') == cat_lower or cat_lower in t.get('tags', [])] + if not tasks_to_show: + return f"No tasks found in category '{category}'." + elif not tasks_to_show: return "All tasks completed." priority_labels = { @@ -57,13 +76,111 @@ def view_tasks(self, show_completed=False): output_lines = [] for i, task in enumerate(sorted_tasks, 1): title = task['title'] - priority = task.get('priority', 'medium') - priority_display = priority_labels.get(priority, '[MEDIUM]') - status = "[DONE]" if task.get('completed') else "[ ]" - output_lines.append(f"{i}. {status} {priority_display} - {title}") + if category: + cat_display = f"[{task.get('category') or category.strip().lower()}]" + output_lines.append(f"{i}. {cat_display} {title}") + else: + priority = task.get('priority', 'medium') + priority_display = priority_labels.get(priority, '[MEDIUM]') + cat = task.get('category') + cat_display = f"[{cat}] " if cat else "" + status = "[DONE]" if task.get('completed') else "[ ]" + output_lines.append(f"{i}. {status} {priority_display} {cat_display}- {title}") return "\n".join(output_lines) + def set_category(self, task_number, category): + """ + Set or update category for an existing task. + """ + if task_number < 1 or task_number > len(self.tasks): + return "Error: Invalid task number." + + category = (category or "").strip() + if not category: + return "Error: Category cannot be empty." + + task = self.tasks[task_number - 1] + old_category = task.get('category') + cat_lower = category.lower() + task['category'] = cat_lower + + if 'tags' not in task or not task['tags']: + task['tags'] = [cat_lower] + elif cat_lower not in task['tags']: + task['tags'].append(cat_lower) + + if old_category: + return f"Category updated for '{task['title']}': {old_category} -> {cat_lower}" + return f"Category set for '{task['title']}': {cat_lower}" + + def add_tags(self, task_number, tags): + """ + Add one or more tags to an existing task. + """ + if task_number < 1 or task_number > len(self.tasks): + return "Error: Invalid task number." + + if not tags: + return "Error: Tags cannot be empty." + + if isinstance(tags, str): + new_tags = [t.strip().lower() for t in tags.split(",") if t.strip()] + else: + new_tags = [str(t).strip().lower() for t in tags if str(t).strip()] + + if not new_tags: + return "Error: Tags cannot be empty." + + task = self.tasks[task_number - 1] + if 'tags' not in task: + task['tags'] = [] + + added = [] + for t in new_tags: + if t not in task['tags']: + task['tags'].append(t) + added.append(t) + + if not task.get('category') and task['tags']: + task['category'] = task['tags'][0] + + return f"Tags added to '{task['title']}': {', '.join(added)}" + + def filter_tasks_by_category(self, category, show_completed=False): + """ + Filter tasks by category or tag. + """ + if not category or not str(category).strip(): + return "Error: Category cannot be empty." + return self.view_tasks(show_completed=show_completed, category=str(category).strip()) + + def get_categories(self): + """ + Get all unique categories across tasks. + """ + categories = set() + for task in self.tasks: + if task.get('category'): + categories.add(task['category']) + for tag in task.get('tags', []): + categories.add(tag) + return sorted(list(categories)) + + def list_categories(self): + """ + Display all categories with active task counts. + """ + categories = self.get_categories() + if not categories: + return "No categories available." + + lines = ["Available Categories:"] + for cat in categories: + count = sum(1 for t in self.tasks if (t.get('category') == cat or cat in t.get('tags', [])) and not t.get('completed', False)) + lines.append(f"- [{cat}]: {count} active task(s)") + return "\n".join(lines) + def complete_task(self, task_number): """ Mark a task as completed. diff --git a/src/main.py b/src/main.py index 679b920..2e1b996 100644 --- a/src/main.py +++ b/src/main.py @@ -6,15 +6,18 @@ print("TaskPilot AI - Task Management System") print("=" * 50) print("\nCommands:") -print(" add [--priority high|medium|low] - Add a new task") -print(" tasks - View all tasks") -print(" complete - Complete a task") -print(" priority - Change task priority") -print(" sort - Sort tasks by priority") -print(" summary - Show priority summary") -print(" tools - Show available tools") -print(" ai - Ask TaskPilot AI") -print(" exit - Exit the application") +print(" add [--priority high|medium|low] [--category ] - Add a new task") +print(" tasks - View all tasks") +print(" complete - Complete a task") +print(" priority - Change task priority") +print(" category - Set task category") +print(" list - Filter tasks by category") +print(" categories - Show all categories") +print(" sort - Sort tasks by priority") +print(" summary - Show priority summary") +print(" tools - Show available tools") +print(" ai - Ask TaskPilot AI") +print(" exit - Exit the application") print("\n" + "-" * 50) while True: @@ -28,18 +31,22 @@ parts = command[4:].split() task_parts = [] priority = "medium" + category = None i = 0 while i < len(parts): if parts[i] == "--priority" and i + 1 < len(parts): priority = parts[i + 1].lower() i += 2 + elif parts[i] == "--category" and i + 1 < len(parts): + category = parts[i + 1].lower() + i += 2 else: task_parts.append(parts[i]) i += 1 task = " ".join(task_parts) - print(agent.add_task(task, priority)) + print(agent.add_task(task, priority, category=category)) elif command.lower() == "tasks": print("\nYour Tasks:") @@ -65,6 +72,27 @@ else: print("Error: Please use: priority ") + elif command.lower().startswith("category "): + parts = command.split() + if len(parts) >= 3: + try: + task_number = int(parts[1]) + new_category = parts[2].lower() + print(agent.set_category(task_number, new_category)) + except ValueError: + print("Error: Please use: category ") + else: + print("Error: Please use: category ") + + elif command.lower().startswith("list "): + category = command[5:].strip() + print(f"\nTasks in [{category}]:") + print("-" * 50) + print(agent.filter_tasks_by_category(category)) + + elif command.lower() == "categories": + print("\n" + agent.list_categories()) + elif command.lower() == "sort": print(agent.sort_tasks()) print("\nSorted Tasks:") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5942608 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,3 @@ +import os + +os.environ.setdefault("OPENAI_API_KEY", "mock-test-key") diff --git a/tests/test_agent.py b/tests/test_agent.py index 62ac2c0..82fad56 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -64,4 +64,85 @@ def test_priority_summary(self, agent): result = agent.get_priority_summary() assert "High priority: 1" in result - assert "Medium priority: 1" in result \ No newline at end of file + assert "Medium priority: 1" in result + + +class TestCategoryFeatures: + def test_add_task_with_category(self, agent): + result = agent.add_task("Debug login issue", "high", category="work") + assert "Category: work" in result + assert "Priority: High" in result + assert len(agent.tasks) == 1 + assert agent.tasks[0]['category'] == 'work' + assert 'work' in agent.tasks[0]['tags'] + + def test_add_task_with_multiple_tags(self, agent): + result = agent.add_task("Buy groceries", "medium", category="shopping", tags=["errands", "food"]) + assert agent.tasks[0]['category'] == 'shopping' + assert agent.tasks[0]['tags'] == ['shopping', 'errands', 'food'] + + def test_set_category(self, agent): + agent.add_task("Draft report", "medium") + assert agent.tasks[0]['category'] is None + result = agent.set_category(1, "work") + assert "Category set" in result + assert agent.tasks[0]['category'] == 'work' + + def test_update_category(self, agent): + agent.add_task("Buy milk", category="personal") + result = agent.set_category(1, "shopping") + assert "Category updated" in result + assert "personal -> shopping" in result + assert agent.tasks[0]['category'] == 'shopping' + + def test_set_category_invalid_task_number(self, agent): + result = agent.set_category(99, "work") + assert "Error: Invalid task number" in result + + def test_set_category_empty(self, agent): + agent.add_task("Draft report") + result = agent.set_category(1, "") + assert "Error: Category cannot be empty" in result + + def test_add_tags_to_task(self, agent): + agent.add_task("Write docs", category="documentation") + result = agent.add_tags(1, ["api", "sphinx"]) + assert "Tags added" in result + assert "api" in agent.tasks[0]['tags'] + assert "sphinx" in agent.tasks[0]['tags'] + + def test_filter_tasks_by_category(self, agent): + agent.add_task("Debug login", category="work") + agent.add_task("Review PRs", category="work") + agent.add_task("Buy milk", category="shopping") + + work_list = agent.filter_tasks_by_category("work") + assert "[work] Debug login" in work_list + assert "[work] Review PRs" in work_list + assert "Buy milk" not in work_list + + shopping_list = agent.filter_tasks_by_category("shopping") + assert "[shopping] Buy milk" in shopping_list + assert "Debug login" not in shopping_list + + def test_filter_tasks_empty_category(self, agent): + agent.add_task("Task 1", category="work") + result = agent.filter_tasks_by_category("health") + assert "No tasks found in category 'health'" in result + + def test_get_categories_and_list(self, agent): + agent.add_task("Task 1", category="work") + agent.add_task("Task 2", category="shopping") + agent.add_task("Task 3", category="work") + + categories = agent.get_categories() + assert categories == ["shopping", "work"] + + listing = agent.list_categories() + assert "[work]: 2 active task(s)" in listing + assert "[shopping]: 1 active task(s)" in listing + + def test_view_tasks_displays_category(self, agent): + agent.add_task("Write report", "high", category="work") + output = agent.view_tasks() + assert "[HIGH] [work] - Write report" in output \ No newline at end of file From 8b7a5cb7114e9d677ff7be1b4ca8aa846cf5ae04 Mon Sep 17 00:00:00 2001 From: Tactical-Camell Date: Sat, 12 Sep 2026 12:35:49 +0400 Subject: [PATCH 2/2] fix(categories): update tools definition, CLI tag parsing, and completion indicators --- src/agent.py | 3 ++- src/main.py | 53 ++++++++++++++++++++++++++++----------------- src/tools.py | 9 +++++--- tests/test_agent.py | 21 +++++++++++++++++- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/agent.py b/src/agent.py index 9a9814e..543ed2d 100644 --- a/src/agent.py +++ b/src/agent.py @@ -78,7 +78,8 @@ def view_tasks(self, show_completed=False, category=None): title = task['title'] if category: cat_display = f"[{task.get('category') or category.strip().lower()}]" - output_lines.append(f"{i}. {cat_display} {title}") + done_suffix = " [DONE]" if task.get('completed') else "" + output_lines.append(f"{i}. {cat_display} {title}{done_suffix}") else: priority = task.get('priority', 'medium') priority_display = priority_labels.get(priority, '[MEDIUM]') diff --git a/src/main.py b/src/main.py index 2e1b996..3710615 100644 --- a/src/main.py +++ b/src/main.py @@ -6,23 +6,26 @@ print("TaskPilot AI - Task Management System") print("=" * 50) print("\nCommands:") -print(" add [--priority high|medium|low] [--category ] - Add a new task") -print(" tasks - View all tasks") -print(" complete - Complete a task") -print(" priority - Change task priority") -print(" category - Set task category") -print(" list - Filter tasks by category") -print(" categories - Show all categories") -print(" sort - Sort tasks by priority") -print(" summary - Show priority summary") -print(" tools - Show available tools") -print(" ai - Ask TaskPilot AI") -print(" exit - Exit the application") +print(" add [--priority high|medium|low] [--category ] [--tags tag1,tag2] - Add a new task") +print(" tasks - View all tasks") +print(" complete - Complete a task") +print(" priority - Change task priority") +print(" category - Set task category") +print(" list - Filter tasks by category") +print(" categories - Show all categories") +print(" sort - Sort tasks by priority") +print(" summary - Show priority summary") +print(" tools - Show available tools") +print(" ai - Ask TaskPilot AI") +print(" exit - Exit the application") print("\n" + "-" * 50) while True: command = input("\nYou: ") + if not command.strip(): + continue + if command.lower() == "exit": print("Goodbye!") break @@ -32,6 +35,7 @@ task_parts = [] priority = "medium" category = None + tags = [] i = 0 while i < len(parts): @@ -41,26 +45,29 @@ elif parts[i] == "--category" and i + 1 < len(parts): category = parts[i + 1].lower() i += 2 + elif parts[i] == "--tags" and i + 1 < len(parts): + tags = [t.strip() for t in parts[i + 1].split(",") if t.strip()] + i += 2 else: task_parts.append(parts[i]) i += 1 task = " ".join(task_parts) - print(agent.add_task(task, priority, category=category)) + print(agent.add_task(task, priority, category=category, tags=tags)) elif command.lower() == "tasks": print("\nYour Tasks:") print("-" * 50) print(agent.view_tasks()) - elif command.lower().startswith("complete "): + elif command.lower().startswith("complete ") or command.lower() == "complete": try: task_number = int(command.split()[1]) print(agent.complete_task(task_number)) except (IndexError, ValueError): print("Error: Please enter a valid task number. Example: complete 1") - elif command.lower().startswith("priority "): + elif command.lower().startswith("priority ") or command.lower() == "priority": parts = command.split() if len(parts) >= 3: try: @@ -72,7 +79,7 @@ else: print("Error: Please use: priority ") - elif command.lower().startswith("category "): + elif command.lower().startswith("category ") or command.lower() == "category": parts = command.split() if len(parts) >= 3: try: @@ -84,11 +91,17 @@ else: print("Error: Please use: category ") + elif command.lower() == "list": + print("Error: Please use: list ") + elif command.lower().startswith("list "): category = command[5:].strip() - print(f"\nTasks in [{category}]:") - print("-" * 50) - print(agent.filter_tasks_by_category(category)) + if not category: + print("Error: Please use: list ") + else: + print(f"\nTasks in [{category}]:") + print("-" * 50) + print(agent.filter_tasks_by_category(category)) elif command.lower() == "categories": print("\n" + agent.list_categories()) @@ -116,4 +129,4 @@ print(agent.ask_ai(message)) else: - print("Unknown command. Try: add, tasks, complete, priority, sort, summary, tools, ai, or exit") \ No newline at end of file + print("Unknown command. Try: add, tasks, complete, priority, category, list, categories, sort, summary, tools, ai, or exit") \ No newline at end of file diff --git a/src/tools.py b/src/tools.py index b9e2970..809daac 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,9 +1,12 @@ def get_available_tools(): return { - "add_task": "Add a new task with optional priority (high/medium/low)", - "view_tasks": "View all tasks with priority levels", + "add_task": "Add a new task with optional priority (high/medium/low) and category/tags", + "view_tasks": "View all tasks with priority levels and categories", "complete_task": "Mark a task as completed", "set_priority": "Change priority of an existing task", "sort_tasks": "Sort tasks by priority (High to Low)", - "get_priority_summary": "Get a summary of task priorities" + "get_priority_summary": "Get a summary of task priorities", + "set_category": "Set or update category for an existing task", + "filter_tasks_by_category": "Filter tasks by category or tag", + "list_categories": "Show all categories with active task counts" } \ No newline at end of file diff --git a/tests/test_agent.py b/tests/test_agent.py index 82fad56..69c0c6e 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -145,4 +145,23 @@ def test_get_categories_and_list(self, agent): def test_view_tasks_displays_category(self, agent): agent.add_task("Write report", "high", category="work") output = agent.view_tasks() - assert "[HIGH] [work] - Write report" in output \ No newline at end of file + assert "[HIGH] [work] - Write report" in output + + def test_filter_tasks_by_category_completed(self, agent): + agent.add_task("Write report", "high", category="work") + agent.complete_task(1) + + # By default, completed tasks are not shown in category filter + active_view = agent.filter_tasks_by_category("work") + assert "No tasks found in category 'work'" in active_view + + # When show_completed=True, completed tasks show [DONE] indicator + completed_view = agent.filter_tasks_by_category("work", show_completed=True) + assert "[work] Write report [DONE]" in completed_view + + def test_available_tools_include_category_features(self, agent): + tools = agent.get_tools() + assert "set_category" in tools + assert "filter_tasks_by_category" in tools + assert "list_categories" in tools + assert "category" in tools["add_task"].lower() \ No newline at end of file