todo-agent 0.3.1__tar.gz → 0.3.2__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.3.1 → todo_agent-0.3.2}/PKG-INFO +3 -3
- {todo_agent-0.3.1 → todo_agent-0.3.2}/README.md +2 -2
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_core/test_conversation_manager.py +1 -1
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_core/test_todo_manager.py +107 -60
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_calendar_utils.py +2 -2
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_inference.py +4 -4
- todo_agent-0.3.2/tests/test_infrastructure/test_ollama_client.py +211 -0
- todo_agent-0.3.2/tests/test_infrastructure/test_openrouter_client.py +273 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_todo_shell.py +17 -13
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/test_cli.py +15 -7
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/test_tools.py +27 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/_version.py +3 -3
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/core/conversation_manager.py +1 -1
- todo_agent-0.3.2/todo_agent/core/exceptions.py +78 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/core/todo_manager.py +115 -49
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/calendar_utils.py +2 -4
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/inference.py +95 -50
- todo_agent-0.3.2/todo_agent/infrastructure/llm_client.py +285 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/ollama_client.py +68 -77
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/openrouter_client.py +70 -73
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/prompts/system_prompt.txt +112 -70
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/todo_shell.py +2 -16
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/cli.py +109 -17
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/formatters.py +22 -0
- todo_agent-0.3.2/todo_agent/interface/progress.py +58 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/tools.py +142 -23
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/PKG-INFO +3 -3
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/SOURCES.txt +1 -0
- todo_agent-0.3.1/tests/test_infrastructure/test_ollama_client.py +0 -139
- todo_agent-0.3.1/tests/test_infrastructure/test_openrouter_client.py +0 -292
- todo_agent-0.3.1/todo_agent/core/exceptions.py +0 -27
- todo_agent-0.3.1/todo_agent/infrastructure/llm_client.py +0 -62
- {todo_agent-0.3.1 → todo_agent-0.3.2}/.gitignore +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/LICENSE +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/MANIFEST.in +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/Makefile +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/docs/publishing.md +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/pyproject.toml +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/requirements-dev.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/requirements.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/setup.cfg +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/test_formatters.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_linting.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_logger.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_main.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/main.py +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: todo-agent
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.2
|
4
4
|
Summary: A natural language interface for todo.sh task management
|
5
5
|
Author: codeprimate
|
6
6
|
Maintainer: codeprimate
|
@@ -68,7 +68,7 @@ todo-agent "show my work tasks"
|
|
68
68
|
|
69
69
|
**Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
|
70
70
|
|
71
|
-
**Work smarter** with automatic duplicate detection
|
71
|
+
**Work smarter** with automatic duplicate detection and calendar-aware scheduling.
|
72
72
|
|
73
73
|
**Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
|
74
74
|
|
@@ -163,7 +163,7 @@ todo-agent "what tasks are blocking other work?"
|
|
163
163
|
### Natural Language Intelligence
|
164
164
|
```bash
|
165
165
|
todo-agent "add dentist appointment next Monday"
|
166
|
-
|
166
|
+
|
167
167
|
todo-agent "move all completed tasks to archive"
|
168
168
|
todo-agent "show me tasks I can do from home"
|
169
169
|
```
|
@@ -21,7 +21,7 @@ todo-agent "show my work tasks"
|
|
21
21
|
|
22
22
|
**Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
|
23
23
|
|
24
|
-
**Work smarter** with automatic duplicate detection
|
24
|
+
**Work smarter** with automatic duplicate detection and calendar-aware scheduling.
|
25
25
|
|
26
26
|
**Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
|
27
27
|
|
@@ -116,7 +116,7 @@ todo-agent "what tasks are blocking other work?"
|
|
116
116
|
### Natural Language Intelligence
|
117
117
|
```bash
|
118
118
|
todo-agent "add dentist appointment next Monday"
|
119
|
-
|
119
|
+
|
120
120
|
todo-agent "move all completed tasks to archive"
|
121
121
|
todo-agent "show me tasks I can do from home"
|
122
122
|
```
|
@@ -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 == 64000
|
18
18
|
assert manager.max_messages == 50
|
19
19
|
assert manager.system_prompt is None
|
20
20
|
|
@@ -148,44 +148,6 @@ class TestTodoManager(unittest.TestCase):
|
|
148
148
|
self.todo_manager.set_due_date(1, "2025/1/5")
|
149
149
|
self.assertIn("Invalid due date format", str(context.exception))
|
150
150
|
|
151
|
-
def test_add_task_with_valid_recurring_daily(self):
|
152
|
-
"""Test adding a task with valid daily recurring format."""
|
153
|
-
self.todo_shell.add.return_value = "Test task"
|
154
|
-
result = self.todo_manager.add_task("Test task", recurring="rec:daily")
|
155
|
-
self.assertEqual(result, "Added task: Test task rec:daily")
|
156
|
-
self.todo_shell.add.assert_called_once_with("Test task rec:daily")
|
157
|
-
|
158
|
-
def test_add_task_with_valid_recurring_weekly_interval(self):
|
159
|
-
"""Test adding a task with valid weekly recurring format with interval."""
|
160
|
-
self.todo_shell.add.return_value = "Test task"
|
161
|
-
result = self.todo_manager.add_task("Test task", recurring="rec:weekly:2")
|
162
|
-
self.assertEqual(result, "Added task: Test task rec:weekly:2")
|
163
|
-
self.todo_shell.add.assert_called_once_with("Test task rec:weekly:2")
|
164
|
-
|
165
|
-
def test_add_task_with_invalid_recurring_format(self):
|
166
|
-
"""Test adding a task with invalid recurring format raises ValueError."""
|
167
|
-
with self.assertRaises(ValueError) as context:
|
168
|
-
self.todo_manager.add_task("Test task", recurring="invalid")
|
169
|
-
self.assertIn("Invalid recurring format", str(context.exception))
|
170
|
-
|
171
|
-
def test_add_task_with_invalid_recurring_frequency(self):
|
172
|
-
"""Test adding a task with invalid recurring frequency raises ValueError."""
|
173
|
-
with self.assertRaises(ValueError) as context:
|
174
|
-
self.todo_manager.add_task("Test task", recurring="rec:invalid")
|
175
|
-
self.assertIn("Invalid frequency", str(context.exception))
|
176
|
-
|
177
|
-
def test_add_task_with_invalid_recurring_interval(self):
|
178
|
-
"""Test adding a task with invalid recurring interval raises ValueError."""
|
179
|
-
with self.assertRaises(ValueError) as context:
|
180
|
-
self.todo_manager.add_task("Test task", recurring="rec:weekly:invalid")
|
181
|
-
self.assertIn("Invalid interval", str(context.exception))
|
182
|
-
|
183
|
-
def test_add_task_with_zero_recurring_interval(self):
|
184
|
-
"""Test adding a task with zero recurring interval raises ValueError."""
|
185
|
-
with self.assertRaises(ValueError) as context:
|
186
|
-
self.todo_manager.add_task("Test task", recurring="rec:weekly:0")
|
187
|
-
self.assertIn("Must be a positive integer", str(context.exception))
|
188
|
-
|
189
151
|
def test_add_task_with_invalid_duration_format(self):
|
190
152
|
"""Test adding a task with invalid duration format raises ValueError."""
|
191
153
|
with self.assertRaises(ValueError) as context:
|
@@ -216,24 +178,6 @@ class TestTodoManager(unittest.TestCase):
|
|
216
178
|
self.todo_manager.add_task("Test task", duration="")
|
217
179
|
self.assertIn("Duration must be a non-empty string", str(context.exception))
|
218
180
|
|
219
|
-
def test_add_task_with_all_parameters_including_recurring(self):
|
220
|
-
"""Test adding a task with all parameters including recurring."""
|
221
|
-
self.todo_shell.add.return_value = "Test task"
|
222
|
-
result = self.todo_manager.add_task(
|
223
|
-
"Test task",
|
224
|
-
priority="A",
|
225
|
-
project="work",
|
226
|
-
context="office",
|
227
|
-
due="2024-01-15",
|
228
|
-
recurring="rec:daily",
|
229
|
-
)
|
230
|
-
self.assertEqual(
|
231
|
-
result, "Added task: (A) Test task +work @office due:2024-01-15 rec:daily"
|
232
|
-
)
|
233
|
-
self.todo_shell.add.assert_called_once_with(
|
234
|
-
"(A) Test task +work @office due:2024-01-15 rec:daily"
|
235
|
-
)
|
236
|
-
|
237
181
|
def test_add_task_with_duration(self):
|
238
182
|
"""Test adding a task with duration parameter."""
|
239
183
|
self.todo_shell.add.return_value = "Test task"
|
@@ -253,15 +197,14 @@ class TestTodoManager(unittest.TestCase):
|
|
253
197
|
project="work",
|
254
198
|
context="office",
|
255
199
|
due="2024-01-15",
|
256
|
-
recurring="rec:daily",
|
257
200
|
duration="2h",
|
258
201
|
)
|
259
202
|
self.assertEqual(
|
260
203
|
result,
|
261
|
-
"Added task: (A) Test task +work @office due:2024-01-15
|
204
|
+
"Added task: (A) Test task +work @office due:2024-01-15 duration:2h",
|
262
205
|
)
|
263
206
|
self.todo_shell.add.assert_called_once_with(
|
264
|
-
"(A) Test task +work @office due:2024-01-15
|
207
|
+
"(A) Test task +work @office due:2024-01-15 duration:2h"
|
265
208
|
)
|
266
209
|
|
267
210
|
def test_list_tasks(self):
|
@@ -421,7 +364,6 @@ class TestTodoManager(unittest.TestCase):
|
|
421
364
|
"projects": ["+project1", "+project1", "+project2"], # Duplicate project1
|
422
365
|
"contexts": ["@context1", "@context1", "@context2"], # Duplicate context1
|
423
366
|
"due": "2024-01-01",
|
424
|
-
"recurring": None,
|
425
367
|
"other_tags": [],
|
426
368
|
}
|
427
369
|
|
@@ -599,6 +541,111 @@ class TestTodoManager(unittest.TestCase):
|
|
599
541
|
assert components["contexts"].count("@context1") == 1
|
600
542
|
assert components["contexts"].count("@context2") == 1
|
601
543
|
|
544
|
+
def test_get_current_datetime(self):
|
545
|
+
"""Test getting current date and time."""
|
546
|
+
with patch("todo_agent.core.todo_manager.datetime") as mock_datetime:
|
547
|
+
mock_now = Mock()
|
548
|
+
mock_now.strftime.side_effect = lambda fmt: {
|
549
|
+
"%Y-%m-%d %H:%M:%S": "2025-01-15 10:30:00",
|
550
|
+
"%A, %B %d, %Y at %I:%M %p": "Wednesday, January 15, 2025 at 10:30 AM",
|
551
|
+
}[fmt]
|
552
|
+
mock_now.isocalendar.return_value = (2025, 3, 3)
|
553
|
+
mock_now.astimezone.return_value.tzinfo = Mock()
|
554
|
+
mock_datetime.now.return_value = mock_now
|
555
|
+
|
556
|
+
result = self.todo_manager.get_current_datetime()
|
557
|
+
self.assertIn("2025-01-15 10:30:00", result)
|
558
|
+
self.assertIn("Week 3", result)
|
559
|
+
|
560
|
+
def test_created_completed_task_basic(self):
|
561
|
+
"""Test creating and immediately completing a task."""
|
562
|
+
# Mock the todo_shell methods
|
563
|
+
self.todo_shell.add.return_value = "Task added"
|
564
|
+
self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
|
565
|
+
self.todo_shell.complete.return_value = "Task completed"
|
566
|
+
|
567
|
+
result = self.todo_manager.created_completed_task("Test task")
|
568
|
+
|
569
|
+
# Verify the task was added
|
570
|
+
self.todo_shell.add.assert_called_once_with("Test task")
|
571
|
+
# Verify the task was completed (should find task number 1 since it contains "Test task")
|
572
|
+
self.todo_shell.complete.assert_called_once_with(1)
|
573
|
+
# Verify the result message
|
574
|
+
self.assertIn("Created and completed task: Test task", result)
|
575
|
+
|
576
|
+
def test_created_completed_task_with_project_and_context(self):
|
577
|
+
"""Test creating and completing a task with project and context."""
|
578
|
+
self.todo_shell.add.return_value = "Task added"
|
579
|
+
self.todo_shell.list_tasks.return_value = "1 Test task\n2 Another task"
|
580
|
+
self.todo_shell.complete.return_value = "Task completed"
|
581
|
+
|
582
|
+
result = self.todo_manager.created_completed_task(
|
583
|
+
"Test task", project="work", context="office"
|
584
|
+
)
|
585
|
+
|
586
|
+
# Verify the task was added with project and context
|
587
|
+
self.todo_shell.add.assert_called_once_with("Test task +work @office")
|
588
|
+
# Verify the task was completed (should find task number 1 since it contains "Test task")
|
589
|
+
self.todo_shell.complete.assert_called_once_with(1)
|
590
|
+
self.assertIn("+work @office", result)
|
591
|
+
|
592
|
+
def test_created_completed_task_with_custom_date(self):
|
593
|
+
"""Test creating and completing a task with a custom completion date."""
|
594
|
+
self.todo_shell.add.return_value = "Task added"
|
595
|
+
self.todo_shell.list_tasks.return_value = "1 Test task"
|
596
|
+
self.todo_shell.complete.return_value = "Task completed"
|
597
|
+
|
598
|
+
result = self.todo_manager.created_completed_task(
|
599
|
+
"Test task", completion_date="2025-01-10"
|
600
|
+
)
|
601
|
+
|
602
|
+
# Verify the task was completed (should find task number 1 since it contains "Test task")
|
603
|
+
self.todo_shell.complete.assert_called_once_with(1)
|
604
|
+
self.assertIn("completed on 2025-01-10", result)
|
605
|
+
|
606
|
+
def test_created_completed_task_with_invalid_date(self):
|
607
|
+
"""Test that invalid completion date raises ValueError."""
|
608
|
+
with self.assertRaises(ValueError) as context:
|
609
|
+
self.todo_manager.created_completed_task(
|
610
|
+
"Test task", completion_date="invalid-date"
|
611
|
+
)
|
612
|
+
self.assertIn("Invalid completion date format", str(context.exception))
|
613
|
+
|
614
|
+
def test_created_completed_task_sanitizes_project_and_context(self):
|
615
|
+
"""Test that project and context with existing symbols are properly sanitized."""
|
616
|
+
self.todo_shell.add.return_value = "Task added"
|
617
|
+
self.todo_shell.list_tasks.return_value = "1 Test task"
|
618
|
+
self.todo_shell.complete.return_value = "Task completed"
|
619
|
+
|
620
|
+
result = self.todo_manager.created_completed_task(
|
621
|
+
"Test task", project="+work", context="@office"
|
622
|
+
)
|
623
|
+
|
624
|
+
# Verify the task was added with properly sanitized project and context
|
625
|
+
self.todo_shell.add.assert_called_once_with("Test task +work @office")
|
626
|
+
self.assertIn("+work @office", result)
|
627
|
+
|
628
|
+
def test_created_completed_task_with_empty_project_after_sanitization(self):
|
629
|
+
"""Test that empty project after sanitization raises ValueError."""
|
630
|
+
with self.assertRaises(ValueError) as context:
|
631
|
+
self.todo_manager.created_completed_task("Test task", project="+")
|
632
|
+
self.assertIn("Project name cannot be empty", str(context.exception))
|
633
|
+
|
634
|
+
def test_created_completed_task_with_empty_context_after_sanitization(self):
|
635
|
+
"""Test that empty context after sanitization raises ValueError."""
|
636
|
+
with self.assertRaises(ValueError) as context:
|
637
|
+
self.todo_manager.created_completed_task("Test task", context="@")
|
638
|
+
self.assertIn("Context name cannot be empty", str(context.exception))
|
639
|
+
|
640
|
+
def test_created_completed_task_no_tasks_after_addition(self):
|
641
|
+
"""Test that RuntimeError is raised when no tasks are found after addition."""
|
642
|
+
self.todo_shell.add.return_value = "Task added"
|
643
|
+
self.todo_shell.list_tasks.return_value = "" # No tasks found
|
644
|
+
|
645
|
+
with self.assertRaises(RuntimeError) as context:
|
646
|
+
self.todo_manager.created_completed_task("Test task")
|
647
|
+
self.assertIn("Failed to add task", str(context.exception))
|
648
|
+
|
602
649
|
|
603
650
|
if __name__ == "__main__":
|
604
651
|
unittest.main()
|
@@ -17,7 +17,7 @@ class TestCalendarUtils:
|
|
17
17
|
|
18
18
|
def test_get_calendar_output_success(self):
|
19
19
|
"""Test successful calendar output generation."""
|
20
|
-
mock_output = "Calendar output from cal
|
20
|
+
mock_output = "Calendar output from cal"
|
21
21
|
|
22
22
|
with patch("subprocess.run") as mock_run:
|
23
23
|
mock_result = Mock()
|
@@ -29,7 +29,7 @@ class TestCalendarUtils:
|
|
29
29
|
assert result == mock_output
|
30
30
|
assert len(result) > 0
|
31
31
|
mock_run.assert_called_once_with(
|
32
|
-
["cal"
|
32
|
+
["cal"], capture_output=True, text=True, check=True
|
33
33
|
)
|
34
34
|
|
35
35
|
def test_get_calendar_output_fallback(self):
|
@@ -104,7 +104,7 @@ class TestInference:
|
|
104
104
|
def test_system_prompt_datetime_interpolation(self):
|
105
105
|
"""Test that current datetime is properly interpolated into system prompt."""
|
106
106
|
# Mock the file reading
|
107
|
-
mock_prompt_content = "CURRENT DATE/TIME: {current_datetime}\
|
107
|
+
mock_prompt_content = "CURRENT DATE/TIME: {current_datetime}\nCALENDAR: {calendar_output}"
|
108
108
|
|
109
109
|
with patch("builtins.open", mock_open(read_data=mock_prompt_content)):
|
110
110
|
# Call the method that loads the system prompt
|
@@ -122,9 +122,9 @@ class TestInference:
|
|
122
122
|
"System prompt should contain properly formatted datetime"
|
123
123
|
)
|
124
124
|
|
125
|
-
# Verify that
|
126
|
-
assert "{
|
127
|
-
assert "
|
125
|
+
# Verify that calendar_output is also interpolated
|
126
|
+
assert "{calendar_output}" not in result
|
127
|
+
assert "September" in result # Should contain calendar month information
|
128
128
|
|
129
129
|
def test_process_request_with_tool_calls(self):
|
130
130
|
"""Test request processing with tool calls."""
|
@@ -0,0 +1,211 @@
|
|
1
|
+
"""
|
2
|
+
Tests for OllamaClient.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from unittest.mock import Mock, mock_open, patch
|
7
|
+
|
8
|
+
import pytest
|
9
|
+
|
10
|
+
from todo_agent.infrastructure.config import Config
|
11
|
+
from todo_agent.infrastructure.ollama_client import OllamaClient
|
12
|
+
|
13
|
+
|
14
|
+
class TestOllamaClient:
|
15
|
+
"""Test OllamaClient functionality."""
|
16
|
+
|
17
|
+
def setup_method(self):
|
18
|
+
"""Set up test fixtures."""
|
19
|
+
self.config = Config()
|
20
|
+
self.config.provider = "ollama"
|
21
|
+
self.config.ollama_base_url = "http://localhost:11434"
|
22
|
+
self.config.ollama_model = "llama3.2"
|
23
|
+
|
24
|
+
# Patch the dependencies that are now initialized in the parent class
|
25
|
+
with patch(
|
26
|
+
"todo_agent.infrastructure.llm_client.Logger"
|
27
|
+
) as mock_logger, patch(
|
28
|
+
"todo_agent.infrastructure.llm_client.get_token_counter"
|
29
|
+
) as mock_token_counter:
|
30
|
+
mock_logger.return_value = Mock()
|
31
|
+
mock_token_counter.return_value = Mock()
|
32
|
+
|
33
|
+
self.client = OllamaClient(self.config)
|
34
|
+
|
35
|
+
def test_initialization(self):
|
36
|
+
"""Test client initialization."""
|
37
|
+
assert self.client.config == self.config
|
38
|
+
assert self.client.base_url == "http://localhost:11434"
|
39
|
+
assert self.client.model == "llama3.2"
|
40
|
+
assert self.client.logger is not None
|
41
|
+
assert self.client.token_counter is not None
|
42
|
+
|
43
|
+
def test_get_model_name(self):
|
44
|
+
"""Test getting model name."""
|
45
|
+
assert self.client.get_model_name() == "llama3.2"
|
46
|
+
|
47
|
+
def test_get_provider_name(self):
|
48
|
+
"""Test getting provider name."""
|
49
|
+
assert self.client.get_provider_name() == "ollama"
|
50
|
+
|
51
|
+
def test_get_request_timeout(self):
|
52
|
+
"""Test getting request timeout."""
|
53
|
+
assert self.client.get_request_timeout() == 120 # 2 minutes for Ollama
|
54
|
+
|
55
|
+
def test_get_request_headers(self):
|
56
|
+
"""Test request headers generation."""
|
57
|
+
headers = self.client._get_request_headers()
|
58
|
+
assert headers == {"Content-Type": "application/json"}
|
59
|
+
|
60
|
+
def test_get_request_payload(self):
|
61
|
+
"""Test request payload generation."""
|
62
|
+
messages = [{"role": "user", "content": "Hello"}]
|
63
|
+
tools = [{"name": "test_tool", "description": "A test tool"}]
|
64
|
+
|
65
|
+
payload = self.client._get_request_payload(messages, tools)
|
66
|
+
expected = {
|
67
|
+
"model": "llama3.2",
|
68
|
+
"messages": messages,
|
69
|
+
"tools": tools,
|
70
|
+
"stream": False,
|
71
|
+
}
|
72
|
+
assert payload == expected
|
73
|
+
|
74
|
+
def test_get_api_endpoint(self):
|
75
|
+
"""Test API endpoint generation."""
|
76
|
+
endpoint = self.client._get_api_endpoint()
|
77
|
+
assert endpoint == "http://localhost:11434/api/chat"
|
78
|
+
|
79
|
+
@patch("todo_agent.infrastructure.ollama_client.OllamaClient._make_http_request")
|
80
|
+
def test_chat_with_tools_success(self, mock_make_request):
|
81
|
+
"""Test successful chat with tools."""
|
82
|
+
# Mock the common HTTP request method
|
83
|
+
expected_response = {
|
84
|
+
"message": {"content": "Here are your tasks", "tool_calls": []}
|
85
|
+
}
|
86
|
+
mock_make_request.return_value = expected_response
|
87
|
+
|
88
|
+
messages = [{"role": "user", "content": "List my tasks"}]
|
89
|
+
tools = [{"function": {"name": "list_tasks", "description": "List tasks"}}]
|
90
|
+
|
91
|
+
response = self.client.chat_with_tools(messages, tools)
|
92
|
+
|
93
|
+
assert response == expected_response
|
94
|
+
mock_make_request.assert_called_once_with(messages, tools)
|
95
|
+
|
96
|
+
@patch("todo_agent.infrastructure.ollama_client.OllamaClient._make_http_request")
|
97
|
+
def test_chat_with_tools_api_error(self, mock_make_request):
|
98
|
+
"""Test chat with tools when API returns error."""
|
99
|
+
# Mock error response from the common method
|
100
|
+
error_response = {
|
101
|
+
"error": True,
|
102
|
+
"error_type": "general_error",
|
103
|
+
"provider": "ollama",
|
104
|
+
"status_code": 500,
|
105
|
+
"raw_error": "Internal Server Error"
|
106
|
+
}
|
107
|
+
mock_make_request.return_value = error_response
|
108
|
+
|
109
|
+
messages = [{"role": "user", "content": "List my tasks"}]
|
110
|
+
tools = [{"function": {"name": "list_tasks", "description": "List tasks"}}]
|
111
|
+
|
112
|
+
response = self.client.chat_with_tools(messages, tools)
|
113
|
+
assert response == error_response
|
114
|
+
|
115
|
+
def test_extract_tool_calls_with_tools(self):
|
116
|
+
"""Test extracting tool calls from response with tools."""
|
117
|
+
response = {
|
118
|
+
"message": {
|
119
|
+
"content": "I'll help you with that",
|
120
|
+
"tool_calls": [
|
121
|
+
{
|
122
|
+
"id": "call_1",
|
123
|
+
"function": {"name": "list_tasks", "arguments": "{}"},
|
124
|
+
}
|
125
|
+
],
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
tool_calls = self.client.extract_tool_calls(response)
|
130
|
+
assert len(tool_calls) == 1
|
131
|
+
assert tool_calls[0]["id"] == "call_1"
|
132
|
+
assert tool_calls[0]["function"]["name"] == "list_tasks"
|
133
|
+
|
134
|
+
def test_extract_tool_calls_without_tools(self):
|
135
|
+
"""Test extracting tool calls from response without tools."""
|
136
|
+
response = {
|
137
|
+
"message": {
|
138
|
+
"content": "I'll help you with that",
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
tool_calls = self.client.extract_tool_calls(response)
|
143
|
+
assert len(tool_calls) == 0
|
144
|
+
|
145
|
+
def test_extract_tool_calls_with_error_response(self):
|
146
|
+
"""Test extracting tool calls from error response."""
|
147
|
+
error_response = {
|
148
|
+
"error": True,
|
149
|
+
"error_type": "timeout",
|
150
|
+
"provider": "ollama"
|
151
|
+
}
|
152
|
+
|
153
|
+
tool_calls = self.client.extract_tool_calls(error_response)
|
154
|
+
assert len(tool_calls) == 0
|
155
|
+
|
156
|
+
def test_extract_content_with_content(self):
|
157
|
+
"""Test extracting content from response with content."""
|
158
|
+
response = {
|
159
|
+
"message": {
|
160
|
+
"content": "Here are your tasks",
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
content = self.client.extract_content(response)
|
165
|
+
assert content == "Here are your tasks"
|
166
|
+
|
167
|
+
def test_extract_content_without_content(self):
|
168
|
+
"""Test extracting content from response without content."""
|
169
|
+
response = {
|
170
|
+
"message": {}
|
171
|
+
}
|
172
|
+
|
173
|
+
content = self.client.extract_content(response)
|
174
|
+
assert content == ""
|
175
|
+
|
176
|
+
def test_extract_content_empty_response(self):
|
177
|
+
"""Test extracting content from empty response."""
|
178
|
+
response = {}
|
179
|
+
|
180
|
+
content = self.client.extract_content(response)
|
181
|
+
assert content == ""
|
182
|
+
|
183
|
+
def test_extract_content_with_error_response(self):
|
184
|
+
"""Test extracting content from error response."""
|
185
|
+
error_response = {
|
186
|
+
"error": True,
|
187
|
+
"error_type": "timeout",
|
188
|
+
"provider": "ollama"
|
189
|
+
}
|
190
|
+
|
191
|
+
content = self.client.extract_content(error_response)
|
192
|
+
assert content == ""
|
193
|
+
|
194
|
+
def test_process_response(self):
|
195
|
+
"""Test response processing and logging."""
|
196
|
+
response_data = {
|
197
|
+
"message": {
|
198
|
+
"content": "Hello",
|
199
|
+
"tool_calls": [
|
200
|
+
{"id": "call_1", "function": {"name": "test_tool"}}
|
201
|
+
]
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
with patch.object(self.client.logger, "info") as mock_info, \
|
206
|
+
patch.object(self.client.logger, "debug") as mock_debug:
|
207
|
+
self.client._process_response(response_data, 0.0)
|
208
|
+
|
209
|
+
# Should log response details
|
210
|
+
assert mock_info.call_count >= 1
|
211
|
+
assert mock_debug.call_count >= 1
|