todo-agent 0.2.6__tar.gz → 0.2.8__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 (61) hide show
  1. {todo_agent-0.2.6 → todo_agent-0.2.8}/PKG-INFO +1 -1
  2. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_core/test_conversation_manager.py +1 -1
  3. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_core/test_todo_manager.py +52 -0
  4. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/test_formatters.py +6 -6
  5. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/test_tools.py +67 -0
  6. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/_version.py +3 -3
  7. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/conversation_manager.py +57 -6
  8. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/todo_manager.py +29 -0
  9. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/inference.py +23 -1
  10. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/ollama_client.py +2 -2
  11. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/openrouter_client.py +2 -2
  12. todo_agent-0.2.8/todo_agent/infrastructure/prompts/system_prompt.txt +334 -0
  13. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/cli.py +3 -5
  14. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/formatters.py +3 -9
  15. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/tools.py +162 -7
  16. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/PKG-INFO +1 -1
  17. todo_agent-0.2.6/todo_agent/infrastructure/prompts/system_prompt.txt +0 -160
  18. {todo_agent-0.2.6 → todo_agent-0.2.8}/.gitignore +0 -0
  19. {todo_agent-0.2.6 → todo_agent-0.2.8}/LICENSE +0 -0
  20. {todo_agent-0.2.6 → todo_agent-0.2.8}/MANIFEST.in +0 -0
  21. {todo_agent-0.2.6 → todo_agent-0.2.8}/Makefile +0 -0
  22. {todo_agent-0.2.6 → todo_agent-0.2.8}/README.md +0 -0
  23. {todo_agent-0.2.6 → todo_agent-0.2.8}/docs/publishing.md +0 -0
  24. {todo_agent-0.2.6 → todo_agent-0.2.8}/pyproject.toml +0 -0
  25. {todo_agent-0.2.6 → todo_agent-0.2.8}/requirements-dev.txt +0 -0
  26. {todo_agent-0.2.6 → todo_agent-0.2.8}/requirements.txt +0 -0
  27. {todo_agent-0.2.6 → todo_agent-0.2.8}/setup.cfg +0 -0
  28. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/__init__.py +0 -0
  29. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_core/__init__.py +0 -0
  30. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/__init__.py +0 -0
  31. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_calendar_utils.py +0 -0
  32. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_config.py +0 -0
  33. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_inference.py +0 -0
  34. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  35. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_ollama_client.py +0 -0
  36. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_openrouter_client.py +0 -0
  37. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_todo_shell.py +0 -0
  38. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_token_counter.py +0 -0
  39. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/__init__.py +0 -0
  40. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/test_cli.py +0 -0
  41. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_linting.py +0 -0
  42. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_logger.py +0 -0
  43. {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_main.py +0 -0
  44. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/__init__.py +0 -0
  45. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/__init__.py +0 -0
  46. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/exceptions.py +0 -0
  47. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/__init__.py +0 -0
  48. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/calendar_utils.py +0 -0
  49. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/config.py +0 -0
  50. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/llm_client.py +0 -0
  51. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  52. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/logger.py +0 -0
  53. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/todo_shell.py +0 -0
  54. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/token_counter.py +0 -0
  55. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/__init__.py +0 -0
  56. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/main.py +0 -0
  57. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/SOURCES.txt +0 -0
  58. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/dependency_links.txt +0 -0
  59. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/entry_points.txt +0 -0
  60. {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/requires.txt +0 -0
  61. {todo_agent-0.2.6 → todo_agent-0.2.8}/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.2.6
3
+ Version: 0.2.8
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -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 == 16000
17
+ assert manager.max_tokens == 32000
18
18
  assert manager.max_messages == 50
19
19
  assert manager.system_prompt is None
20
20
 
@@ -89,6 +89,58 @@ class TestTodoManager(unittest.TestCase):
89
89
  self.assertEqual(result, "Added task: Test task due:2024-01-15")
90
90
  self.todo_shell.add.assert_called_once_with("Test task due:2024-01-15")
91
91
 
92
+ def test_add_task_with_valid_recurring_daily(self):
93
+ """Test adding a task with valid daily recurring format."""
94
+ self.todo_shell.add.return_value = "Test task"
95
+ result = self.todo_manager.add_task("Test task", recurring="rec:daily")
96
+ self.assertEqual(result, "Added task: Test task rec:daily")
97
+ self.todo_shell.add.assert_called_once_with("Test task rec:daily")
98
+
99
+ def test_add_task_with_valid_recurring_weekly_interval(self):
100
+ """Test adding a task with valid weekly recurring format with interval."""
101
+ self.todo_shell.add.return_value = "Test task"
102
+ result = self.todo_manager.add_task("Test task", recurring="rec:weekly:2")
103
+ self.assertEqual(result, "Added task: Test task rec:weekly:2")
104
+ self.todo_shell.add.assert_called_once_with("Test task rec:weekly:2")
105
+
106
+ def test_add_task_with_invalid_recurring_format(self):
107
+ """Test adding a task with invalid recurring format raises ValueError."""
108
+ with self.assertRaises(ValueError) as context:
109
+ self.todo_manager.add_task("Test task", recurring="invalid")
110
+ self.assertIn("Invalid recurring format", str(context.exception))
111
+
112
+ def test_add_task_with_invalid_recurring_frequency(self):
113
+ """Test adding a task with invalid recurring frequency raises ValueError."""
114
+ with self.assertRaises(ValueError) as context:
115
+ self.todo_manager.add_task("Test task", recurring="rec:invalid")
116
+ self.assertIn("Invalid frequency", str(context.exception))
117
+
118
+ def test_add_task_with_invalid_recurring_interval(self):
119
+ """Test adding a task with invalid recurring interval raises ValueError."""
120
+ with self.assertRaises(ValueError) as context:
121
+ self.todo_manager.add_task("Test task", recurring="rec:weekly:invalid")
122
+ self.assertIn("Invalid interval", str(context.exception))
123
+
124
+ def test_add_task_with_zero_recurring_interval(self):
125
+ """Test adding a task with zero recurring interval raises ValueError."""
126
+ with self.assertRaises(ValueError) as context:
127
+ self.todo_manager.add_task("Test task", recurring="rec:weekly:0")
128
+ self.assertIn("Must be a positive integer", str(context.exception))
129
+
130
+ def test_add_task_with_all_parameters_including_recurring(self):
131
+ """Test adding a task with all parameters including recurring."""
132
+ self.todo_shell.add.return_value = "Test task"
133
+ result = self.todo_manager.add_task(
134
+ "Test task",
135
+ priority="A",
136
+ project="work",
137
+ context="office",
138
+ due="2024-01-15",
139
+ recurring="rec:daily"
140
+ )
141
+ self.assertEqual(result, "Added task: (A) Test task +work @office due:2024-01-15 rec:daily")
142
+ self.todo_shell.add.assert_called_once_with("(A) Test task +work @office due:2024-01-15 rec:daily")
143
+
92
144
  def test_list_tasks(self):
93
145
  """Test listing tasks."""
94
146
  self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
@@ -15,12 +15,12 @@ class TestTaskFormatter:
15
15
  """Test that format_task_list preserves ANSI color codes."""
16
16
  # Sample output with ANSI color codes (simulating todo.sh output)
17
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
-
18
+
19
19
  result = TaskFormatter.format_task_list(raw_tasks)
20
-
20
+
21
21
  # The result should be a Rich Text object
22
22
  assert isinstance(result, Text)
23
-
23
+
24
24
  # Check that the Rich Text object contains the original ANSI codes
25
25
  # We can check this by looking at the raw text content
26
26
  result_str = result.plain
@@ -35,12 +35,12 @@ class TestTaskFormatter:
35
35
  """Test that format_completed_tasks preserves ANSI color codes."""
36
36
  # Sample completed task output with ANSI color codes
37
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
-
38
+
39
39
  result = TaskFormatter.format_completed_tasks(raw_tasks)
40
-
40
+
41
41
  # The result should be a Rich Text object
42
42
  assert isinstance(result, Text)
43
-
43
+
44
44
  # Check that the Rich Text object contains the original content
45
45
  # We can check this by looking at the raw text content
46
46
  result_str = result.plain
@@ -237,3 +237,70 @@ class TestCalendarTool:
237
237
  assert "2025" in result["output"]
238
238
  assert result["tool_call_id"] == "test_id"
239
239
  assert result["name"] == "get_calendar"
240
+
241
+
242
+ class TestParseDateTool:
243
+ """Test parse_date tool functionality."""
244
+
245
+ def setup_method(self):
246
+ """Set up test fixtures."""
247
+ self.mock_todo_manager = Mock(spec=TodoManager)
248
+ self.mock_logger = Mock()
249
+ self.tool_handler = ToolCallHandler(self.mock_todo_manager, self.mock_logger)
250
+
251
+ def test_parse_date_next_weekday(self):
252
+ """Test parsing 'next thursday'."""
253
+ tool_call = {
254
+ "function": {
255
+ "name": "parse_date",
256
+ "arguments": '{"date_expression": "next thursday"}',
257
+ },
258
+ "id": "test_id",
259
+ }
260
+
261
+ result = self.tool_handler.execute_tool(tool_call)
262
+
263
+ assert result["error"] is False
264
+ assert result["tool_call_id"] == "test_id"
265
+ assert result["name"] == "parse_date"
266
+ # Should return a valid YYYY-MM-DD format
267
+ assert len(result["output"]) == 10
268
+ assert result["output"].count("-") == 2
269
+
270
+ def test_parse_date_tomorrow(self):
271
+ """Test parsing 'tomorrow'."""
272
+ tool_call = {
273
+ "function": {
274
+ "name": "parse_date",
275
+ "arguments": '{"date_expression": "tomorrow"}',
276
+ },
277
+ "id": "test_id",
278
+ }
279
+
280
+ result = self.tool_handler.execute_tool(tool_call)
281
+
282
+ assert result["error"] is False
283
+ assert result["tool_call_id"] == "test_id"
284
+ assert result["name"] == "parse_date"
285
+ # Should return a valid YYYY-MM-DD format
286
+ assert len(result["output"]) == 10
287
+ assert result["output"].count("-") == 2
288
+
289
+ def test_parse_date_in_days(self):
290
+ """Test parsing 'in 3 days'."""
291
+ tool_call = {
292
+ "function": {
293
+ "name": "parse_date",
294
+ "arguments": '{"date_expression": "in 3 days"}',
295
+ },
296
+ "id": "test_id",
297
+ }
298
+
299
+ result = self.tool_handler.execute_tool(tool_call)
300
+
301
+ assert result["error"] is False
302
+ assert result["tool_call_id"] == "test_id"
303
+ assert result["name"] == "parse_date"
304
+ # Should return a valid YYYY-MM-DD format
305
+ assert len(result["output"]) == 10
306
+ assert result["output"].count("-") == 2
@@ -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.6'
32
- __version_tuple__ = version_tuple = (0, 2, 6)
31
+ __version__ = version = '0.2.8'
32
+ __version_tuple__ = version_tuple = (0, 2, 8)
33
33
 
34
- __commit_id__ = commit_id = 'g670ed96a8'
34
+ __commit_id__ = commit_id = 'g5675b007e'
@@ -35,14 +35,16 @@ class ConversationManager:
35
35
  """Manages conversation state and memory for LLM interactions."""
36
36
 
37
37
  def __init__(
38
- self, max_tokens: int = 16000, max_messages: int = 50, model: str = "gpt-4"
38
+ self, max_tokens: int = 32000, max_messages: int = 50, model: str = "gpt-4"
39
39
  ):
40
40
  self.history: List[ConversationMessage] = []
41
41
  self.max_tokens = max_tokens
42
42
  self.max_messages = max_messages
43
43
  self.system_prompt: Optional[str] = None
44
44
  self.token_counter = get_token_counter(model)
45
- self._total_tokens = 0 # Running total of tokens in conversation
45
+ self._total_tokens: int = 0 # Running total of tokens in conversation
46
+ self._tools_tokens: int = 0 # Cache for tools token count
47
+ self._last_tools_hash: Optional[int] = None # Track if tools have changed
46
48
 
47
49
  def add_message(
48
50
  self,
@@ -140,10 +142,44 @@ class ConversationManager:
140
142
  self._total_tokens -= message.token_count
141
143
  self.history.pop(index)
142
144
 
143
- def _trim_if_needed(self) -> None:
145
+ def get_request_tokens(self, tools: List[Dict[str, Any]]) -> int:
146
+ """
147
+ Get total request tokens (conversation + tools).
148
+
149
+ Args:
150
+ tools: Current tool definitions
151
+
152
+ Returns:
153
+ Total request tokens
154
+ """
155
+ # Check if tools have changed
156
+ tools_hash = hash(str(tools))
157
+ if tools_hash != self._last_tools_hash:
158
+ self._tools_tokens = self.token_counter.count_tools_tokens(tools)
159
+ self._last_tools_hash = tools_hash
160
+
161
+ return self._total_tokens + self._tools_tokens
162
+
163
+ def update_request_tokens(self, actual_prompt_tokens: int) -> None:
164
+ """
165
+ Update the conversation with actual token count from API response.
166
+
167
+ Args:
168
+ actual_prompt_tokens: Actual prompt tokens used by the API
169
+ """
170
+ # Calculate tools tokens by subtracting conversation tokens
171
+ tools_tokens = actual_prompt_tokens - self._total_tokens
172
+ if tools_tokens >= 0:
173
+ self._tools_tokens = tools_tokens
174
+ # Note: logger not available in this context, so we'll handle logging in the calling code
175
+
176
+ def _trim_if_needed(self, tools: Optional[List[Dict[str, Any]]] = None) -> None:
144
177
  """
145
178
  Trim conversation history if it exceeds token or message limits.
146
179
  Preserves most recent messages and system prompt.
180
+
181
+ Args:
182
+ tools: Optional tools for request token calculation
147
183
  """
148
184
  # Check message count limit
149
185
  if len(self.history) > self.max_messages:
@@ -160,8 +196,10 @@ class ConversationManager:
160
196
  # Recalculate total tokens after message count trimming
161
197
  self._recalculate_total_tokens()
162
198
 
163
- # Check token limit - remove oldest non-system messages until under limit
164
- while self._total_tokens > self.max_tokens and len(self.history) > 2:
199
+ # Check token limit using request tokens if tools provided
200
+ current_tokens = self.get_request_tokens(tools) if tools else self._total_tokens
201
+
202
+ while current_tokens > self.max_tokens and len(self.history) > 2:
165
203
  # Find oldest non-system message to remove
166
204
  for i, msg in enumerate(self.history):
167
205
  if msg.role != MessageRole.SYSTEM:
@@ -171,6 +209,11 @@ class ConversationManager:
171
209
  # No non-system messages found, break to avoid infinite loop
172
210
  break
173
211
 
212
+ # Recalculate request tokens
213
+ current_tokens = (
214
+ self.get_request_tokens(tools) if tools else self._total_tokens
215
+ )
216
+
174
217
  def _recalculate_total_tokens(self) -> None:
175
218
  """Recalculate total token count from scratch (used after major restructuring)."""
176
219
  self._total_tokens = 0
@@ -219,10 +262,15 @@ class ConversationManager:
219
262
  self.history.insert(0, system_message)
220
263
  self._total_tokens += token_count
221
264
 
222
- def get_conversation_summary(self) -> Dict[str, Any]:
265
+ def get_conversation_summary(
266
+ self, tools: Optional[List[Dict[str, Any]]] = None
267
+ ) -> Dict[str, Any]:
223
268
  """
224
269
  Get conversation statistics and summary.
225
270
 
271
+ Args:
272
+ tools: Optional tools for request token calculation
273
+
226
274
  Returns:
227
275
  Dictionary with conversation metrics
228
276
  """
@@ -256,6 +304,9 @@ class ConversationManager:
256
304
  return {
257
305
  "total_messages": len(self.history),
258
306
  "estimated_tokens": self._total_tokens,
307
+ "request_tokens": self.get_request_tokens(tools)
308
+ if tools
309
+ else self._total_tokens,
259
310
  "user_messages": len(
260
311
  [msg for msg in self.history if msg.role == MessageRole.USER]
261
312
  ),
@@ -19,6 +19,7 @@ class TodoManager:
19
19
  project: Optional[str] = None,
20
20
  context: Optional[str] = None,
21
21
  due: Optional[str] = None,
22
+ recurring: Optional[str] = None,
22
23
  ) -> str:
23
24
  """Add new task with explicit project/context parameters."""
24
25
  # Validate and sanitize inputs
@@ -54,6 +55,31 @@ class TodoManager:
54
55
  f"Invalid due date format '{due}'. Must be YYYY-MM-DD."
55
56
  )
56
57
 
58
+ if recurring:
59
+ # Validate recurring format
60
+ if not recurring.startswith("rec:"):
61
+ raise ValueError(
62
+ f"Invalid recurring format '{recurring}'. Must start with 'rec:'."
63
+ )
64
+ # Basic validation of recurring syntax
65
+ parts = recurring.split(":")
66
+ if len(parts) < 2 or len(parts) > 3:
67
+ raise ValueError(
68
+ f"Invalid recurring format '{recurring}'. Expected 'rec:frequency' or 'rec:frequency:interval'."
69
+ )
70
+ frequency = parts[1]
71
+ if frequency not in ["daily", "weekly", "monthly", "yearly"]:
72
+ raise ValueError(
73
+ f"Invalid frequency '{frequency}'. Must be one of: daily, weekly, monthly, yearly."
74
+ )
75
+ if len(parts) == 3:
76
+ try:
77
+ interval = int(parts[2])
78
+ if interval < 1:
79
+ raise ValueError("Interval must be at least 1.")
80
+ except ValueError:
81
+ raise ValueError(f"Invalid interval '{parts[2]}'. Must be a positive integer.")
82
+
57
83
  # Build the full task description with priority, project, and context
58
84
  full_description = description
59
85
 
@@ -69,6 +95,9 @@ class TodoManager:
69
95
  if due:
70
96
  full_description = f"{full_description} due:{due}"
71
97
 
98
+ if recurring:
99
+ full_description = f"{full_description} {recurring}"
100
+
72
101
  self.todo_shell.add(full_description)
73
102
  return f"Added task: {full_description}"
74
103
 
@@ -187,6 +187,18 @@ class Inference:
187
187
  messages=messages, tools=self.tool_handler.tools
188
188
  )
189
189
 
190
+ # Extract actual token usage from API response
191
+ usage = response.get("usage", {})
192
+ actual_prompt_tokens = usage.get("prompt_tokens", 0)
193
+ actual_completion_tokens = usage.get("completion_tokens", 0)
194
+ actual_total_tokens = usage.get("total_tokens", 0)
195
+
196
+ # Update conversation manager with actual token count
197
+ self.conversation_manager.update_request_tokens(actual_prompt_tokens)
198
+ self.logger.debug(
199
+ f"Updated with actual API tokens: prompt={actual_prompt_tokens}, completion={actual_completion_tokens}, total={actual_total_tokens}"
200
+ )
201
+
190
202
  # Handle multiple tool calls in sequence
191
203
  tool_call_count = 0
192
204
  while True:
@@ -237,6 +249,14 @@ class Inference:
237
249
  messages=messages, tools=self.tool_handler.tools
238
250
  )
239
251
 
252
+ # Update with actual tokens from subsequent API calls
253
+ usage = response.get("usage", {})
254
+ actual_prompt_tokens = usage.get("prompt_tokens", 0)
255
+ self.conversation_manager.update_request_tokens(actual_prompt_tokens)
256
+ self.logger.debug(
257
+ f"Updated with actual API tokens after tool calls: prompt={actual_prompt_tokens}"
258
+ )
259
+
240
260
  # Calculate and log total thinking time
241
261
  end_time = time.time()
242
262
  thinking_time = end_time - start_time
@@ -270,7 +290,9 @@ class Inference:
270
290
  Returns:
271
291
  Dictionary with conversation metrics
272
292
  """
273
- return self.conversation_manager.get_conversation_summary()
293
+ return self.conversation_manager.get_conversation_summary(
294
+ self.tool_handler.tools
295
+ )
274
296
 
275
297
  def clear_conversation(self) -> None:
276
298
  """Clear conversation history."""
@@ -72,12 +72,12 @@ class OllamaClient(LLMClient):
72
72
  if "message" in response and "tool_calls" in response["message"]:
73
73
  tool_calls = response["message"]["tool_calls"]
74
74
  self.logger.info(f"Response contains {len(tool_calls)} tool calls")
75
-
75
+
76
76
  # Log thinking content (response body) if present
77
77
  content = response["message"].get("content", "")
78
78
  if content and content.strip():
79
79
  self.logger.info(f"LLM thinking before tool calls: {content}")
80
-
80
+
81
81
  for i, tool_call in enumerate(tool_calls):
82
82
  tool_name = tool_call.get("function", {}).get("name", "unknown")
83
83
  self.logger.info(f" Tool call {i + 1}: {tool_name}")
@@ -78,12 +78,12 @@ class OpenRouterClient(LLMClient):
78
78
  if "message" in choice and "tool_calls" in choice["message"]:
79
79
  tool_calls = choice["message"]["tool_calls"]
80
80
  self.logger.info(f"Response contains {len(tool_calls)} tool calls")
81
-
81
+
82
82
  # Log thinking content (response body) if present
83
83
  content = choice["message"].get("content", "")
84
84
  if content and content.strip():
85
85
  self.logger.info(f"LLM thinking before tool calls: {content}")
86
-
86
+
87
87
  for i, tool_call in enumerate(tool_calls):
88
88
  tool_name = tool_call.get("function", {}).get("name", "unknown")
89
89
  self.logger.info(f" Tool call {i + 1}: {tool_name}")