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.
Files changed (65) hide show
  1. {todo_agent-0.3.1 → todo_agent-0.3.2}/PKG-INFO +3 -3
  2. {todo_agent-0.3.1 → todo_agent-0.3.2}/README.md +2 -2
  3. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_core/test_conversation_manager.py +1 -1
  4. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_core/test_todo_manager.py +107 -60
  5. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_calendar_utils.py +2 -2
  6. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_inference.py +4 -4
  7. todo_agent-0.3.2/tests/test_infrastructure/test_ollama_client.py +211 -0
  8. todo_agent-0.3.2/tests/test_infrastructure/test_openrouter_client.py +273 -0
  9. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_todo_shell.py +17 -13
  10. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/test_cli.py +15 -7
  11. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/test_tools.py +27 -0
  12. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/_version.py +3 -3
  13. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/core/conversation_manager.py +1 -1
  14. todo_agent-0.3.2/todo_agent/core/exceptions.py +78 -0
  15. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/core/todo_manager.py +115 -49
  16. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/calendar_utils.py +2 -4
  17. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/inference.py +95 -50
  18. todo_agent-0.3.2/todo_agent/infrastructure/llm_client.py +285 -0
  19. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/ollama_client.py +68 -77
  20. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/openrouter_client.py +70 -73
  21. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/prompts/system_prompt.txt +112 -70
  22. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/todo_shell.py +2 -16
  23. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/cli.py +109 -17
  24. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/formatters.py +22 -0
  25. todo_agent-0.3.2/todo_agent/interface/progress.py +58 -0
  26. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/tools.py +142 -23
  27. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/PKG-INFO +3 -3
  28. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/SOURCES.txt +1 -0
  29. todo_agent-0.3.1/tests/test_infrastructure/test_ollama_client.py +0 -139
  30. todo_agent-0.3.1/tests/test_infrastructure/test_openrouter_client.py +0 -292
  31. todo_agent-0.3.1/todo_agent/core/exceptions.py +0 -27
  32. todo_agent-0.3.1/todo_agent/infrastructure/llm_client.py +0 -62
  33. {todo_agent-0.3.1 → todo_agent-0.3.2}/.gitignore +0 -0
  34. {todo_agent-0.3.1 → todo_agent-0.3.2}/LICENSE +0 -0
  35. {todo_agent-0.3.1 → todo_agent-0.3.2}/MANIFEST.in +0 -0
  36. {todo_agent-0.3.1 → todo_agent-0.3.2}/Makefile +0 -0
  37. {todo_agent-0.3.1 → todo_agent-0.3.2}/docs/publishing.md +0 -0
  38. {todo_agent-0.3.1 → todo_agent-0.3.2}/pyproject.toml +0 -0
  39. {todo_agent-0.3.1 → todo_agent-0.3.2}/requirements-dev.txt +0 -0
  40. {todo_agent-0.3.1 → todo_agent-0.3.2}/requirements.txt +0 -0
  41. {todo_agent-0.3.1 → todo_agent-0.3.2}/setup.cfg +0 -0
  42. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/__init__.py +0 -0
  43. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_core/__init__.py +0 -0
  44. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/__init__.py +0 -0
  45. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_config.py +0 -0
  46. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  47. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_infrastructure/test_token_counter.py +0 -0
  48. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/__init__.py +0 -0
  49. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_interface/test_formatters.py +0 -0
  50. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_linting.py +0 -0
  51. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_logger.py +0 -0
  52. {todo_agent-0.3.1 → todo_agent-0.3.2}/tests/test_main.py +0 -0
  53. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/__init__.py +0 -0
  54. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/core/__init__.py +0 -0
  55. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/__init__.py +0 -0
  56. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/config.py +0 -0
  57. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  58. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/logger.py +0 -0
  59. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/infrastructure/token_counter.py +0 -0
  60. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/interface/__init__.py +0 -0
  61. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent/main.py +0 -0
  62. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/dependency_links.txt +0 -0
  63. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/entry_points.txt +0 -0
  64. {todo_agent-0.3.1 → todo_agent-0.3.2}/todo_agent.egg-info/requires.txt +0 -0
  65. {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.1
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, recurring task handling, and calendar-aware scheduling.
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
- todo-agent "set up recurring daily vitamin reminder"
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, recurring task handling, and calendar-aware scheduling.
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
- todo-agent "set up recurring daily vitamin reminder"
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 == 32000
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 rec:daily duration:2h",
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 rec:daily duration:2h"
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 -3"
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", "-3"], capture_output=True, text=True, check=True
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}\n{tools_section}"
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 tools_section is also interpolated
126
- assert "{tools_section}" not in result
127
- assert "list_tasks" in result # Should contain tool information
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