todo-agent 0.2.3__tar.gz → 0.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {todo_agent-0.2.3 → todo_agent-0.2.5}/PKG-INFO +1 -1
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_core/test_conversation_manager.py +1 -1
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_core/test_todo_manager.py +57 -0
- todo_agent-0.2.5/tests/test_infrastructure/test_calendar_utils.py +81 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_interface/test_cli.py +45 -0
- todo_agent-0.2.5/tests/test_interface/test_formatters.py +75 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_interface/test_tools.py +59 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/_version.py +3 -3
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/core/conversation_manager.py +1 -1
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/core/todo_manager.py +33 -0
- todo_agent-0.2.5/todo_agent/infrastructure/calendar_utils.py +64 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/inference.py +13 -2
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/llm_client_factory.py +4 -2
- todo_agent-0.2.5/todo_agent/infrastructure/prompts/system_prompt.txt +92 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/interface/cli.py +51 -4
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/interface/formatters.py +171 -17
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/interface/tools.py +80 -8
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent.egg-info/PKG-INFO +1 -1
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent.egg-info/SOURCES.txt +3 -0
- todo_agent-0.2.3/todo_agent/infrastructure/prompts/system_prompt.txt +0 -51
- {todo_agent-0.2.3 → todo_agent-0.2.5}/.gitignore +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/LICENSE +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/MANIFEST.in +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/Makefile +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/README.md +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/docs/publishing.md +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/pyproject.toml +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/requirements-dev.txt +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/requirements.txt +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/setup.cfg +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_inference.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_ollama_client.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_openrouter_client.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_todo_shell.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_linting.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_logger.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/tests/test_main.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/core/exceptions.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/llm_client.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/ollama_client.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/openrouter_client.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/todo_shell.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent/main.py +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.2.3 → todo_agent-0.2.5}/todo_agent.egg-info/top_level.txt +0 -0
@@ -14,7 +14,7 @@ class TestConversationManager:
|
|
14
14
|
"""Test ConversationManager initialization."""
|
15
15
|
manager = ConversationManager()
|
16
16
|
assert len(manager.history) == 0
|
17
|
-
assert manager.max_tokens ==
|
17
|
+
assert manager.max_tokens == 16000
|
18
18
|
assert manager.max_messages == 50
|
19
19
|
assert manager.system_prompt is None
|
20
20
|
|
@@ -32,6 +32,63 @@ class TestTodoManager(unittest.TestCase):
|
|
32
32
|
self.assertEqual(result, "Added task: Test task")
|
33
33
|
self.todo_shell.add.assert_called_once_with("Test task")
|
34
34
|
|
35
|
+
def test_add_task_with_invalid_priority(self):
|
36
|
+
"""Test adding a task with invalid priority raises ValueError."""
|
37
|
+
with self.assertRaises(ValueError) as context:
|
38
|
+
self.todo_manager.add_task("Test task", priority="invalid")
|
39
|
+
self.assertIn("Invalid priority", str(context.exception))
|
40
|
+
|
41
|
+
def test_add_task_with_invalid_priority_lowercase(self):
|
42
|
+
"""Test adding a task with lowercase priority raises ValueError."""
|
43
|
+
with self.assertRaises(ValueError) as context:
|
44
|
+
self.todo_manager.add_task("Test task", priority="a")
|
45
|
+
self.assertIn("Invalid priority", str(context.exception))
|
46
|
+
|
47
|
+
def test_add_task_with_invalid_priority_multiple_chars(self):
|
48
|
+
"""Test adding a task with multiple character priority raises ValueError."""
|
49
|
+
with self.assertRaises(ValueError) as context:
|
50
|
+
self.todo_manager.add_task("Test task", priority="AB")
|
51
|
+
self.assertIn("Invalid priority", str(context.exception))
|
52
|
+
|
53
|
+
def test_add_task_sanitizes_project_with_plus(self):
|
54
|
+
"""Test that project with existing + symbol is properly sanitized."""
|
55
|
+
self.todo_shell.add.return_value = "Test task"
|
56
|
+
result = self.todo_manager.add_task("Test task", project="+work")
|
57
|
+
self.assertEqual(result, "Added task: Test task +work")
|
58
|
+
self.todo_shell.add.assert_called_once_with("Test task +work")
|
59
|
+
|
60
|
+
def test_add_task_sanitizes_context_with_at(self):
|
61
|
+
"""Test that context with existing @ symbol is properly sanitized."""
|
62
|
+
self.todo_shell.add.return_value = "Test task"
|
63
|
+
result = self.todo_manager.add_task("Test task", context="@office")
|
64
|
+
self.assertEqual(result, "Added task: Test task @office")
|
65
|
+
self.todo_shell.add.assert_called_once_with("Test task @office")
|
66
|
+
|
67
|
+
def test_add_task_with_empty_project_after_sanitization(self):
|
68
|
+
"""Test adding a task with empty project after removing + raises ValueError."""
|
69
|
+
with self.assertRaises(ValueError) as context:
|
70
|
+
self.todo_manager.add_task("Test task", project="+")
|
71
|
+
self.assertIn("Project name cannot be empty", str(context.exception))
|
72
|
+
|
73
|
+
def test_add_task_with_empty_context_after_sanitization(self):
|
74
|
+
"""Test adding a task with empty context after removing @ raises ValueError."""
|
75
|
+
with self.assertRaises(ValueError) as context:
|
76
|
+
self.todo_manager.add_task("Test task", context="@")
|
77
|
+
self.assertIn("Context name cannot be empty", str(context.exception))
|
78
|
+
|
79
|
+
def test_add_task_with_invalid_due_date(self):
|
80
|
+
"""Test adding a task with invalid due date format raises ValueError."""
|
81
|
+
with self.assertRaises(ValueError) as context:
|
82
|
+
self.todo_manager.add_task("Test task", due="invalid-date")
|
83
|
+
self.assertIn("Invalid due date format", str(context.exception))
|
84
|
+
|
85
|
+
def test_add_task_with_valid_due_date(self):
|
86
|
+
"""Test adding a task with valid due date format."""
|
87
|
+
self.todo_shell.add.return_value = "Test task"
|
88
|
+
result = self.todo_manager.add_task("Test task", due="2024-01-15")
|
89
|
+
self.assertEqual(result, "Added task: Test task due:2024-01-15")
|
90
|
+
self.todo_shell.add.assert_called_once_with("Test task due:2024-01-15")
|
91
|
+
|
35
92
|
def test_list_tasks(self):
|
36
93
|
"""Test listing tasks."""
|
37
94
|
self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""
|
2
|
+
Tests for calendar utilities.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from unittest.mock import Mock, patch
|
6
|
+
|
7
|
+
import pytest
|
8
|
+
|
9
|
+
from todo_agent.infrastructure.calendar_utils import (
|
10
|
+
get_calendar_output,
|
11
|
+
get_current_month_calendar,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
class TestCalendarUtils:
|
16
|
+
"""Test calendar utility functions."""
|
17
|
+
|
18
|
+
def test_get_calendar_output_success(self):
|
19
|
+
"""Test successful calendar output generation."""
|
20
|
+
mock_output = "Calendar output from cal -3"
|
21
|
+
|
22
|
+
with patch("subprocess.run") as mock_run:
|
23
|
+
mock_result = Mock()
|
24
|
+
mock_result.stdout = mock_output
|
25
|
+
mock_run.return_value = mock_result
|
26
|
+
|
27
|
+
result = get_calendar_output()
|
28
|
+
|
29
|
+
assert result == mock_output
|
30
|
+
assert len(result) > 0
|
31
|
+
mock_run.assert_called_once_with(
|
32
|
+
["cal", "-3"], capture_output=True, text=True, check=True
|
33
|
+
)
|
34
|
+
|
35
|
+
def test_get_calendar_output_fallback(self):
|
36
|
+
"""Test calendar output fallback to Python calendar."""
|
37
|
+
with patch("subprocess.run", side_effect=FileNotFoundError("cal not found")):
|
38
|
+
result = get_calendar_output()
|
39
|
+
|
40
|
+
# Should return a string with calendar data
|
41
|
+
assert isinstance(result, str)
|
42
|
+
assert len(result) > 0
|
43
|
+
|
44
|
+
def test_get_current_month_calendar_success(self):
|
45
|
+
"""Test successful current month calendar generation."""
|
46
|
+
mock_output = "Current month calendar"
|
47
|
+
|
48
|
+
with patch("subprocess.run") as mock_run:
|
49
|
+
mock_result = Mock()
|
50
|
+
mock_result.stdout = mock_output
|
51
|
+
mock_run.return_value = mock_result
|
52
|
+
|
53
|
+
result = get_current_month_calendar()
|
54
|
+
|
55
|
+
assert result == mock_output
|
56
|
+
assert len(result) > 0
|
57
|
+
mock_run.assert_called_once_with(
|
58
|
+
["cal"], capture_output=True, text=True, check=True
|
59
|
+
)
|
60
|
+
|
61
|
+
def test_get_current_month_calendar_fallback(self):
|
62
|
+
"""Test current month calendar fallback to Python calendar."""
|
63
|
+
with patch("subprocess.run", side_effect=FileNotFoundError("cal not found")):
|
64
|
+
result = get_current_month_calendar()
|
65
|
+
|
66
|
+
# Should return a string with calendar data
|
67
|
+
assert isinstance(result, str)
|
68
|
+
assert len(result) > 0
|
69
|
+
|
70
|
+
def test_calendar_output_contains_expected_content(self):
|
71
|
+
"""Test that calendar output contains expected content."""
|
72
|
+
with patch("subprocess.run") as mock_run:
|
73
|
+
mock_result = Mock()
|
74
|
+
mock_result.stdout = "Calendar with July August September 2025"
|
75
|
+
mock_run.return_value = mock_result
|
76
|
+
|
77
|
+
result = get_calendar_output()
|
78
|
+
|
79
|
+
# Should contain some calendar content
|
80
|
+
assert len(result) > 0
|
81
|
+
assert isinstance(result, str)
|
@@ -295,6 +295,49 @@ class TestCLI:
|
|
295
295
|
f"Error: Failed to list tasks: {error_message}"
|
296
296
|
)
|
297
297
|
|
298
|
+
def test_done_command_success(self):
|
299
|
+
"""Test successful done command execution."""
|
300
|
+
expected_output = (
|
301
|
+
"x 2025-08-29 2025-08-28 Buy groceries\nx 2025-08-28 2025-08-27 Call mom"
|
302
|
+
)
|
303
|
+
|
304
|
+
# Mock the todo_shell.list_completed method
|
305
|
+
self.cli.todo_shell.list_completed.return_value = expected_output
|
306
|
+
|
307
|
+
with patch("builtins.print") as mock_print:
|
308
|
+
# Simulate the done command logic
|
309
|
+
try:
|
310
|
+
output = self.cli.todo_shell.list_completed()
|
311
|
+
print(output)
|
312
|
+
except Exception as e:
|
313
|
+
print(f"Error: Failed to list completed tasks: {e!s}")
|
314
|
+
|
315
|
+
# Verify todo_shell.list_completed was called
|
316
|
+
self.cli.todo_shell.list_completed.assert_called_once()
|
317
|
+
|
318
|
+
# Verify print was called with the expected output
|
319
|
+
mock_print.assert_called_once_with(expected_output)
|
320
|
+
|
321
|
+
def test_done_command_exception_handling(self):
|
322
|
+
"""Test done command handles exceptions properly."""
|
323
|
+
error_message = "Database connection failed"
|
324
|
+
|
325
|
+
# Mock exception in todo_shell.list_completed
|
326
|
+
self.cli.todo_shell.list_completed.side_effect = Exception(error_message)
|
327
|
+
|
328
|
+
with patch("builtins.print") as mock_print:
|
329
|
+
# Simulate the done command logic with error
|
330
|
+
try:
|
331
|
+
output = self.cli.todo_shell.list_completed()
|
332
|
+
print(output)
|
333
|
+
except Exception as e:
|
334
|
+
print(f"Error: Failed to list completed tasks: {e!s}")
|
335
|
+
|
336
|
+
# Verify error was handled and formatted correctly
|
337
|
+
mock_print.assert_called_once_with(
|
338
|
+
f"Error: Failed to list completed tasks: {error_message}"
|
339
|
+
)
|
340
|
+
|
298
341
|
def test_help_command_displays_available_commands(self):
|
299
342
|
"""Test that help command displays all available commands."""
|
300
343
|
with patch("builtins.print") as mock_print:
|
@@ -304,6 +347,7 @@ class TestCLI:
|
|
304
347
|
print(" history - Show conversation statistics")
|
305
348
|
print(" help - Show this help message")
|
306
349
|
print(" list - List all tasks (no LLM interaction)")
|
350
|
+
print(" done - List completed tasks (no LLM interaction)")
|
307
351
|
print(" quit - Exit the application")
|
308
352
|
print(" Or just type your request naturally!")
|
309
353
|
|
@@ -314,6 +358,7 @@ class TestCLI:
|
|
314
358
|
" history - Show conversation statistics",
|
315
359
|
" help - Show this help message",
|
316
360
|
" list - List all tasks (no LLM interaction)",
|
361
|
+
" done - List completed tasks (no LLM interaction)",
|
317
362
|
" quit - Exit the application",
|
318
363
|
" Or just type your request naturally!",
|
319
364
|
]
|
@@ -0,0 +1,75 @@
|
|
1
|
+
"""
|
2
|
+
Tests for formatters module.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
from rich.text import Text
|
7
|
+
|
8
|
+
from todo_agent.interface.formatters import TaskFormatter
|
9
|
+
|
10
|
+
|
11
|
+
class TestTaskFormatter:
|
12
|
+
"""Test TaskFormatter functionality."""
|
13
|
+
|
14
|
+
def test_format_task_list_preserves_ansi_codes(self):
|
15
|
+
"""Test that format_task_list preserves ANSI color codes."""
|
16
|
+
# Sample output with ANSI color codes (simulating todo.sh output)
|
17
|
+
raw_tasks = "\033[1;33m1\033[0m (A) \033[1;32m2025-08-29\033[0m Clean cat box \033[1;34m@home\033[0m \033[1;35m+chores\033[0m \033[1;31mdue:2025-08-29\033[0m"
|
18
|
+
|
19
|
+
result = TaskFormatter.format_task_list(raw_tasks)
|
20
|
+
|
21
|
+
# The result should be a Rich Text object
|
22
|
+
assert isinstance(result, Text)
|
23
|
+
|
24
|
+
# Check that the Rich Text object contains the original ANSI codes
|
25
|
+
# We can check this by looking at the raw text content
|
26
|
+
result_str = result.plain
|
27
|
+
assert "1" in result_str # Task number should be present
|
28
|
+
assert "(A)" in result_str # Priority should be present
|
29
|
+
assert "2025-08-29" in result_str # Date should be present
|
30
|
+
assert "@home" in result_str # Context should be present
|
31
|
+
assert "+chores" in result_str # Project should be present
|
32
|
+
assert "due:2025-08-29" in result_str # Due date should be present
|
33
|
+
|
34
|
+
def test_format_completed_tasks_preserves_ansi_codes(self):
|
35
|
+
"""Test that format_completed_tasks preserves ANSI color codes."""
|
36
|
+
# Sample completed task output with ANSI color codes
|
37
|
+
raw_tasks = "\033[1;32mx\033[0m \033[1;32m2025-08-29\033[0m \033[1;32m2025-08-28\033[0m Clean cat box \033[1;34m@home\033[0m \033[1;35m+chores\033[0m"
|
38
|
+
|
39
|
+
result = TaskFormatter.format_completed_tasks(raw_tasks)
|
40
|
+
|
41
|
+
# The result should be a Rich Text object
|
42
|
+
assert isinstance(result, Text)
|
43
|
+
|
44
|
+
# Check that the Rich Text object contains the original content
|
45
|
+
# We can check this by looking at the raw text content
|
46
|
+
result_str = result.plain
|
47
|
+
assert "x" in result_str # Completion marker should be present
|
48
|
+
assert "2025-08-29" in result_str # Completion date should be present
|
49
|
+
assert "2025-08-28" in result_str # Creation date should be present
|
50
|
+
assert "@home" in result_str # Context should be present
|
51
|
+
assert "+chores" in result_str # Project should be present
|
52
|
+
|
53
|
+
def test_format_task_list_handles_empty_input(self):
|
54
|
+
"""Test that format_task_list handles empty input gracefully."""
|
55
|
+
result = TaskFormatter.format_task_list("")
|
56
|
+
assert isinstance(result, Text)
|
57
|
+
assert "No tasks found" in str(result)
|
58
|
+
|
59
|
+
def test_format_completed_tasks_handles_empty_input(self):
|
60
|
+
"""Test that format_completed_tasks handles empty input gracefully."""
|
61
|
+
result = TaskFormatter.format_completed_tasks("")
|
62
|
+
assert isinstance(result, Text)
|
63
|
+
assert "No completed tasks found" in str(result)
|
64
|
+
|
65
|
+
def test_format_task_list_handles_whitespace_only(self):
|
66
|
+
"""Test that format_task_list handles whitespace-only input."""
|
67
|
+
result = TaskFormatter.format_task_list(" \n \t ")
|
68
|
+
assert isinstance(result, Text)
|
69
|
+
assert "No tasks found" in str(result)
|
70
|
+
|
71
|
+
def test_format_completed_tasks_handles_whitespace_only(self):
|
72
|
+
"""Test that format_completed_tasks handles whitespace-only input."""
|
73
|
+
result = TaskFormatter.format_completed_tasks(" \n \t ")
|
74
|
+
assert isinstance(result, Text)
|
75
|
+
assert "No completed tasks found" in str(result)
|
@@ -178,3 +178,62 @@ class TestToolErrorHandling:
|
|
178
178
|
|
179
179
|
assert result["error"] is False
|
180
180
|
assert result["output"] == "1. Test task"
|
181
|
+
|
182
|
+
|
183
|
+
class TestCalendarTool:
|
184
|
+
"""Test calendar tool functionality."""
|
185
|
+
|
186
|
+
def setup_method(self):
|
187
|
+
"""Set up test fixtures."""
|
188
|
+
self.mock_todo_manager = Mock(spec=TodoManager)
|
189
|
+
self.mock_logger = Mock()
|
190
|
+
self.tool_handler = ToolCallHandler(self.mock_todo_manager, self.mock_logger)
|
191
|
+
|
192
|
+
@patch("subprocess.run")
|
193
|
+
def test_get_calendar_success(self, mock_run):
|
194
|
+
"""Test successful calendar retrieval using system cal command."""
|
195
|
+
# Mock the subprocess.run to return a calendar
|
196
|
+
mock_run.return_value.stdout = " January 2025\nSu Mo Tu We Th Fr Sa\n 1 2 3 4\n 5 6 7 8 9 10 11\n12 13 14 15 16 17 18\n19 20 21 22 23 24 25\n26 27 28 29 30 31\n"
|
197
|
+
mock_run.return_value.returncode = 0
|
198
|
+
|
199
|
+
tool_call = {
|
200
|
+
"function": {
|
201
|
+
"name": "get_calendar",
|
202
|
+
"arguments": '{"month": 1, "year": 2025}',
|
203
|
+
},
|
204
|
+
"id": "test_id",
|
205
|
+
}
|
206
|
+
|
207
|
+
result = self.tool_handler.execute_tool(tool_call)
|
208
|
+
|
209
|
+
assert result["error"] is False
|
210
|
+
assert "January 2025" in result["output"]
|
211
|
+
assert result["tool_call_id"] == "test_id"
|
212
|
+
assert result["name"] == "get_calendar"
|
213
|
+
|
214
|
+
# Verify subprocess.run was called with correct arguments
|
215
|
+
mock_run.assert_called_once_with(
|
216
|
+
["cal", "1", "2025"], capture_output=True, text=True, check=True
|
217
|
+
)
|
218
|
+
|
219
|
+
@patch("subprocess.run")
|
220
|
+
def test_get_calendar_fallback_to_python(self, mock_run):
|
221
|
+
"""Test calendar fallback to Python calendar module when cal command fails."""
|
222
|
+
# Mock subprocess.run to raise FileNotFoundError
|
223
|
+
mock_run.side_effect = FileNotFoundError("cal command not found")
|
224
|
+
|
225
|
+
tool_call = {
|
226
|
+
"function": {
|
227
|
+
"name": "get_calendar",
|
228
|
+
"arguments": '{"month": 1, "year": 2025}',
|
229
|
+
},
|
230
|
+
"id": "test_id",
|
231
|
+
}
|
232
|
+
|
233
|
+
result = self.tool_handler.execute_tool(tool_call)
|
234
|
+
|
235
|
+
assert result["error"] is False
|
236
|
+
assert "January" in result["output"]
|
237
|
+
assert "2025" in result["output"]
|
238
|
+
assert result["tool_call_id"] == "test_id"
|
239
|
+
assert result["name"] == "get_calendar"
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '0.2.
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
31
|
+
__version__ = version = '0.2.5'
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 5)
|
33
33
|
|
34
|
-
__commit_id__ = commit_id = '
|
34
|
+
__commit_id__ = commit_id = 'gcb2b8bbf9'
|
@@ -35,7 +35,7 @@ class ConversationManager:
|
|
35
35
|
"""Manages conversation state and memory for LLM interactions."""
|
36
36
|
|
37
37
|
def __init__(
|
38
|
-
self, max_tokens: int =
|
38
|
+
self, max_tokens: int = 16000, max_messages: int = 50, model: str = "gpt-4"
|
39
39
|
):
|
40
40
|
self.history: List[ConversationMessage] = []
|
41
41
|
self.max_tokens = max_tokens
|
@@ -21,6 +21,39 @@ class TodoManager:
|
|
21
21
|
due: Optional[str] = None,
|
22
22
|
) -> str:
|
23
23
|
"""Add new task with explicit project/context parameters."""
|
24
|
+
# Validate and sanitize inputs
|
25
|
+
if priority and not (
|
26
|
+
len(priority) == 1 and priority.isalpha() and priority.isupper()
|
27
|
+
):
|
28
|
+
raise ValueError(
|
29
|
+
f"Invalid priority '{priority}'. Must be a single uppercase letter (A-Z)."
|
30
|
+
)
|
31
|
+
|
32
|
+
if project:
|
33
|
+
# Remove any existing + symbols to prevent duplication
|
34
|
+
project = project.strip().lstrip("+")
|
35
|
+
if not project:
|
36
|
+
raise ValueError(
|
37
|
+
"Project name cannot be empty after removing + symbol."
|
38
|
+
)
|
39
|
+
|
40
|
+
if context:
|
41
|
+
# Remove any existing @ symbols to prevent duplication
|
42
|
+
context = context.strip().lstrip("@")
|
43
|
+
if not context:
|
44
|
+
raise ValueError(
|
45
|
+
"Context name cannot be empty after removing @ symbol."
|
46
|
+
)
|
47
|
+
|
48
|
+
if due:
|
49
|
+
# Basic date format validation
|
50
|
+
try:
|
51
|
+
datetime.strptime(due, "%Y-%m-%d")
|
52
|
+
except ValueError:
|
53
|
+
raise ValueError(
|
54
|
+
f"Invalid due date format '{due}'. Must be YYYY-MM-DD."
|
55
|
+
)
|
56
|
+
|
24
57
|
# Build the full task description with priority, project, and context
|
25
58
|
full_description = description
|
26
59
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Calendar utilities for generating calendar output in system prompts.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import calendar
|
6
|
+
import subprocess
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
|
9
|
+
|
10
|
+
def get_calendar_output() -> str:
|
11
|
+
"""
|
12
|
+
Generate calendar output for previous, current, and next month.
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
Formatted calendar string showing three months side by side
|
16
|
+
"""
|
17
|
+
try:
|
18
|
+
# Use cal -3 to get three months side by side
|
19
|
+
result = subprocess.run(
|
20
|
+
["cal", "-3"], capture_output=True, text=True, check=True
|
21
|
+
)
|
22
|
+
return result.stdout.strip()
|
23
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
24
|
+
# Fallback to Python calendar module
|
25
|
+
return _get_python_cal_output()
|
26
|
+
|
27
|
+
|
28
|
+
def _get_python_cal_output() -> str:
|
29
|
+
"""
|
30
|
+
Generate calendar output using Python calendar module as fallback.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Calendar output formatted similar to cal command
|
34
|
+
"""
|
35
|
+
current_date = datetime.now()
|
36
|
+
|
37
|
+
# Calculate previous, current, and next month
|
38
|
+
prev_month = current_date - timedelta(days=current_date.day)
|
39
|
+
next_month = current_date.replace(day=1) + timedelta(days=32)
|
40
|
+
next_month = next_month.replace(day=1)
|
41
|
+
|
42
|
+
calendars = []
|
43
|
+
|
44
|
+
for date in [prev_month, current_date, next_month]:
|
45
|
+
cal = calendar.month(date.year, date.month)
|
46
|
+
calendars.append(cal.strip())
|
47
|
+
|
48
|
+
return "\n\n".join(calendars)
|
49
|
+
|
50
|
+
|
51
|
+
def get_current_month_calendar() -> str:
|
52
|
+
"""
|
53
|
+
Get calendar for current month only.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Calendar output for current month
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
result = subprocess.run(["cal"], capture_output=True, text=True, check=True)
|
60
|
+
return result.stdout.strip()
|
61
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
62
|
+
# Fallback to Python calendar
|
63
|
+
current_date = datetime.now()
|
64
|
+
return calendar.month(current_date.year, current_date.month).strip()
|
@@ -75,6 +75,15 @@ class Inference:
|
|
75
75
|
|
76
76
|
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
77
77
|
|
78
|
+
# Get calendar output
|
79
|
+
from .calendar_utils import get_calendar_output
|
80
|
+
|
81
|
+
try:
|
82
|
+
calendar_output = get_calendar_output()
|
83
|
+
except Exception as e:
|
84
|
+
self.logger.warning(f"Failed to get calendar output: {e!s}")
|
85
|
+
calendar_output = "Calendar unavailable"
|
86
|
+
|
78
87
|
# Load system prompt from file
|
79
88
|
prompt_file_path = os.path.join(
|
80
89
|
os.path.dirname(__file__), "prompts", "system_prompt.txt"
|
@@ -84,9 +93,11 @@ class Inference:
|
|
84
93
|
with open(prompt_file_path, encoding="utf-8") as f:
|
85
94
|
system_prompt_template = f.read()
|
86
95
|
|
87
|
-
# Format the template with the tools section
|
96
|
+
# Format the template with the tools section, current datetime, and calendar
|
88
97
|
return system_prompt_template.format(
|
89
|
-
tools_section=tools_section,
|
98
|
+
tools_section=tools_section,
|
99
|
+
current_datetime=current_datetime,
|
100
|
+
calendar_output=calendar_output,
|
90
101
|
)
|
91
102
|
|
92
103
|
except FileNotFoundError:
|
@@ -2,6 +2,8 @@
|
|
2
2
|
Factory for creating LLM clients based on configuration.
|
3
3
|
"""
|
4
4
|
|
5
|
+
# mypy: disable-error-code="no-redef"
|
6
|
+
|
5
7
|
from typing import Optional
|
6
8
|
|
7
9
|
try:
|
@@ -15,8 +17,8 @@ except ImportError:
|
|
15
17
|
from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
|
16
18
|
from infrastructure.logger import Logger # type: ignore[no-redef]
|
17
19
|
from infrastructure.ollama_client import OllamaClient # type: ignore[no-redef]
|
18
|
-
from infrastructure.openrouter_client import (
|
19
|
-
OpenRouterClient,
|
20
|
+
from infrastructure.openrouter_client import (
|
21
|
+
OpenRouterClient, # type: ignore[no-redef, misc]
|
20
22
|
)
|
21
23
|
|
22
24
|
|
@@ -0,0 +1,92 @@
|
|
1
|
+
You are a todo.sh assistant managing tasks in standard todo.txt format.
|
2
|
+
|
3
|
+
CURRENT DATE/TIME: {current_datetime}
|
4
|
+
{calendar_output}
|
5
|
+
|
6
|
+
CORE PRINCIPLES:
|
7
|
+
1. Strategic Tool Usage: Batch discovery tools ([list_tasks, list_completed_tasks, list_projects, list_contexts]) to minimize API calls
|
8
|
+
2. Conversational: Respond naturally without mentioning tools or technical details
|
9
|
+
3. Data Integrity: Only reference tasks/projects/contexts returned by actual tool calls - NEVER hallucinate
|
10
|
+
4. Safety: Always verify current state before modifications using list_tasks() and list_completed_tasks()
|
11
|
+
5. Todo.txt Compliance: Use standard format and ordering
|
12
|
+
|
13
|
+
TODO.TXT FORMAT:
|
14
|
+
- Priority: (A), (B), (C) • Completion: "x YYYY-MM-DD" • Creation: YYYY-MM-DD
|
15
|
+
- Projects: +project (single + symbol) • Contexts: @context (single @ symbol) • Due dates: due:YYYY-MM-DD
|
16
|
+
- Example: "(A) 2024-01-15 Call dentist +health @phone due:2024-01-20"
|
17
|
+
- CRITICAL: Never use double symbols like ++project or @@context - always use single + and @ symbols
|
18
|
+
|
19
|
+
WORKFLOW:
|
20
|
+
Discovery First: Gather context with batched tool calls before any action
|
21
|
+
Verify Before Action: Check for duplicates, conflicts, or existing completions
|
22
|
+
Sequential Processing: Tools execute in order within batches
|
23
|
+
|
24
|
+
TASK COMPLETION:
|
25
|
+
When users say something like "I finished X" or "I'm done with Y", search for matching tasks
|
26
|
+
using list_tasks() and handle ambiguity by showing numbered options. Always verify task
|
27
|
+
hasn't already been completed with list_completed_tasks().
|
28
|
+
|
29
|
+
COMPLETION INTELLIGENCE:
|
30
|
+
- If user's statement clearly matches exactly one task (e.g., "I finished mowing the lawn"
|
31
|
+
when there's only one task with "yard work" in the description), complete it immediately
|
32
|
+
- If user's statement matches multiple tasks, show numbered options and ask for clarification
|
33
|
+
- If user's statement is ambiguous or could match many tasks, ask for clarification
|
34
|
+
- When in doubt about ambiguity, ask for more information to clarify intent before taking any action
|
35
|
+
|
36
|
+
CONTEXT AND PROJECT INFERENCE:
|
37
|
+
- Extract temporal urgency from due dates and creation dates
|
38
|
+
- Identify task relationships through shared projects/contexts
|
39
|
+
- Determine scope boundaries from natural language (work vs personal tasks)
|
40
|
+
- Recognize priority patterns and dependencies
|
41
|
+
|
42
|
+
TASK CREATION INTELLIGENCE:
|
43
|
+
- When users request to add a task, automatically infer appropriate projects, contexts, and due dates based on the task content
|
44
|
+
- When intent is clear, create the task immediately without asking for confirmation
|
45
|
+
- Only ask for clarification when project/context/due date is genuinely ambiguous
|
46
|
+
- Use priority C for new tasks unless urgency is indicated
|
47
|
+
- DUE DATE INFERENCE: Automatically infer due dates using multiple intelligence sources:
|
48
|
+
* Explicit expressions: "tomorrow", "next week", "next Monday", "by Friday" → Convert to YYYY-MM-DD format
|
49
|
+
* Relative expressions: "in 3 days", "next month", "end of month" → Calculate appropriate date
|
50
|
+
* Urgency indicators: "urgent", "asap", "today" → Set to today's date
|
51
|
+
* Vague expressions: "sometime this week" → Set to end of current week
|
52
|
+
* Task nature inference: Use common sense based on task type and existing patterns:
|
53
|
+
- Work tasks → Consider work week patterns and existing work task due dates
|
54
|
+
- Personal tasks → Consider weekend availability and personal schedule patterns
|
55
|
+
- Health/medical → Consider urgency and typical scheduling patterns
|
56
|
+
- Shopping/errands → Consider when items are needed and store hours
|
57
|
+
- Bills/payments → Consider due dates and late fees
|
58
|
+
- Maintenance tasks → Consider frequency patterns and current state
|
59
|
+
* Calendar context: Use current date/time and calendar output to inform timing decisions
|
60
|
+
* Existing task patterns: Look at similar tasks and their due dates for consistency
|
61
|
+
* Always infer: Every task should have a reasonable due date based on available context
|
62
|
+
|
63
|
+
TASK ADVICE:
|
64
|
+
Think deeply and critically to categorize tasks and suggest actions:
|
65
|
+
- Consider real-life implications and importance to my responsibilities regardless of explicit priority
|
66
|
+
- When users request prioritization help, use Eisenhower Matrix:
|
67
|
+
Q1 (Urgent+Important: DO), Q2 (Important: SCHEDULE), Q3 (Urgent: DELEGATE), Q4 (Neither: ELIMINATE) [assign SPARINGLY].
|
68
|
+
|
69
|
+
ERROR HANDLING:
|
70
|
+
- Empty results: Suggest next steps
|
71
|
+
- Ambiguous requests: Show numbered options
|
72
|
+
- Large lists: Use filtering/summaries for 10+ items
|
73
|
+
- Failed operations: Explain clearly with alternatives
|
74
|
+
|
75
|
+
OUTPUT FORMATTING:
|
76
|
+
- Calendar Display: Show calendar output as plain text without backticks, code blocks, or markdown formatting
|
77
|
+
- Task Lists: Present tasks in conversational language, not raw todo.txt format
|
78
|
+
- Natural Language: Use conversational responses that feel natural and helpful
|
79
|
+
- No Technical Details: Avoid mentioning tools, API calls, or technical implementation details
|
80
|
+
|
81
|
+
CRITICAL RULES:
|
82
|
+
- Anti-hallucination: If no tool data exists, say "I need to check your tasks first"
|
83
|
+
- Use appropriate discovery tools extensively
|
84
|
+
- Never assume task existence without verification
|
85
|
+
- Maintain todo.txt standard compliance
|
86
|
+
- Format Compliance: Always use single + for projects and single @ for contexts (never ++ or @@)
|
87
|
+
- Display Formatting: When showing calendar output, display it as plain text without backticks or code blocks
|
88
|
+
- Proactive Task Creation: When users request to add a task, create it immediately with inferred tags and due dates unless genuinely ambiguous
|
89
|
+
- No Unnecessary Confirmation: Don't ask for confirmation when the task intent is clear and appropriate tags/due dates can be inferred
|
90
|
+
- Due Date Intelligence: Always infer reasonable due dates using task nature, calendar context, existing patterns, and common sense. Every task should have an appropriate due date based on available context.
|
91
|
+
|
92
|
+
AVAILABLE TOOLS: {tools_section}
|