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.
- {todo_agent-0.2.6 → todo_agent-0.2.8}/PKG-INFO +1 -1
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_core/test_conversation_manager.py +1 -1
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_core/test_todo_manager.py +52 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/test_formatters.py +6 -6
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/test_tools.py +67 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/_version.py +3 -3
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/conversation_manager.py +57 -6
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/todo_manager.py +29 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/inference.py +23 -1
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/ollama_client.py +2 -2
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/openrouter_client.py +2 -2
- todo_agent-0.2.8/todo_agent/infrastructure/prompts/system_prompt.txt +334 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/cli.py +3 -5
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/formatters.py +3 -9
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/tools.py +162 -7
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/PKG-INFO +1 -1
- todo_agent-0.2.6/todo_agent/infrastructure/prompts/system_prompt.txt +0 -160
- {todo_agent-0.2.6 → todo_agent-0.2.8}/.gitignore +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/LICENSE +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/MANIFEST.in +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/Makefile +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/README.md +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/docs/publishing.md +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/pyproject.toml +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/requirements-dev.txt +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/requirements.txt +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/setup.cfg +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_calendar_utils.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_inference.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_ollama_client.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_openrouter_client.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_todo_shell.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_interface/test_cli.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_linting.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_logger.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/tests/test_main.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/core/exceptions.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/calendar_utils.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/llm_client.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/todo_shell.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/interface/__init__.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent/main.py +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/SOURCES.txt +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.2.6 → todo_agent-0.2.8}/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 == 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.
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
31
|
+
__version__ = version = '0.2.8'
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 8)
|
33
33
|
|
34
|
-
__commit_id__ = commit_id = '
|
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 =
|
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
|
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
|
164
|
-
|
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(
|
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}")
|