todo-agent 0.1.0__py3-none-any.whl
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/__init__.py +14 -0
- todo_agent/_version.py +34 -0
- todo_agent/core/__init__.py +16 -0
- todo_agent/core/conversation_manager.py +310 -0
- todo_agent/core/exceptions.py +27 -0
- todo_agent/core/todo_manager.py +194 -0
- todo_agent/infrastructure/__init__.py +11 -0
- todo_agent/infrastructure/config.py +59 -0
- todo_agent/infrastructure/inference.py +221 -0
- todo_agent/infrastructure/llm_client.py +62 -0
- todo_agent/infrastructure/llm_client_factory.py +48 -0
- todo_agent/infrastructure/logger.py +128 -0
- todo_agent/infrastructure/ollama_client.py +152 -0
- todo_agent/infrastructure/openrouter_client.py +173 -0
- todo_agent/infrastructure/prompts/system_prompt.txt +51 -0
- todo_agent/infrastructure/todo_shell.py +151 -0
- todo_agent/infrastructure/token_counter.py +184 -0
- todo_agent/interface/__init__.py +10 -0
- todo_agent/interface/cli.py +210 -0
- todo_agent/interface/tools.py +578 -0
- todo_agent/main.py +54 -0
- todo_agent-0.1.0.dist-info/METADATA +282 -0
- todo_agent-0.1.0.dist-info/RECORD +27 -0
- todo_agent-0.1.0.dist-info/WHEEL +5 -0
- todo_agent-0.1.0.dist-info/entry_points.txt +2 -0
- todo_agent-0.1.0.dist-info/licenses/LICENSE +674 -0
- todo_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,578 @@
|
|
1
|
+
"""
|
2
|
+
Tool definitions and schemas for LLM function calling.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict, List, Optional
|
6
|
+
|
7
|
+
try:
|
8
|
+
from todo_agent.core.todo_manager import TodoManager
|
9
|
+
from todo_agent.infrastructure.logger import Logger
|
10
|
+
except ImportError:
|
11
|
+
from core.todo_manager import TodoManager
|
12
|
+
from infrastructure.logger import Logger
|
13
|
+
|
14
|
+
|
15
|
+
class ToolCallHandler:
|
16
|
+
"""Handles tool execution and orchestration."""
|
17
|
+
|
18
|
+
def __init__(self, todo_manager: TodoManager, logger: Optional[Logger] = None):
|
19
|
+
self.todo_manager = todo_manager
|
20
|
+
self.logger = logger
|
21
|
+
self.tools = self._define_tools()
|
22
|
+
|
23
|
+
def _define_tools(self) -> List[Dict[str, Any]]:
|
24
|
+
"""Define available tools for LLM function calling."""
|
25
|
+
return [
|
26
|
+
{
|
27
|
+
"type": "function",
|
28
|
+
"function": {
|
29
|
+
"name": "list_projects",
|
30
|
+
"description": (
|
31
|
+
"Get all available projects from todo.txt. Use this when: "
|
32
|
+
"1) User mentions 'project' but doesn't specify which one, "
|
33
|
+
"2) User uses a generic term like 'work' that could match multiple projects, "
|
34
|
+
"3) You need to understand what projects exist before making decisions. "
|
35
|
+
"STRATEGIC CONTEXT: This is a discovery tool - call this FIRST when project ambiguity exists "
|
36
|
+
"to avoid asking the user for clarification when you can find the answer yourself."
|
37
|
+
),
|
38
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
39
|
+
}
|
40
|
+
},
|
41
|
+
{
|
42
|
+
"type": "function",
|
43
|
+
"function": {
|
44
|
+
"name": "list_contexts",
|
45
|
+
"description": (
|
46
|
+
"Get all available contexts from todo.txt. Use this when: "
|
47
|
+
"1) User mentions 'context' but doesn't specify which one, "
|
48
|
+
"2) User uses a generic term like 'office' that could match multiple contexts, "
|
49
|
+
"3) You need to understand what contexts exist before making decisions. "
|
50
|
+
"STRATEGIC CONTEXT: This is a discovery tool - call this FIRST when context ambiguity exists "
|
51
|
+
"to avoid asking the user for clarification when you can find the answer yourself."
|
52
|
+
),
|
53
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
54
|
+
}
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"type": "function",
|
58
|
+
"function": {
|
59
|
+
"name": "list_tasks",
|
60
|
+
"description": (
|
61
|
+
"List current tasks with optional filtering. Use this when: "
|
62
|
+
"1) User wants to see their tasks, "
|
63
|
+
"2) You need to find a specific task by description, "
|
64
|
+
"3) You need to check for potential duplicates before adding new tasks, "
|
65
|
+
"4) You need to understand the current state before making changes. "
|
66
|
+
"CRITICAL: ALWAYS use this before add_task() to check for similar existing tasks. "
|
67
|
+
"IMPORTANT: When presenting the results to the user, convert the raw todo.txt format "
|
68
|
+
"into conversational language. Do not show the raw format like '(A) task +project @context'. "
|
69
|
+
"STRATEGIC CONTEXT: This is the primary discovery tool - call this FIRST when you need to "
|
70
|
+
"understand existing tasks before making any modifications."
|
71
|
+
),
|
72
|
+
"parameters": {
|
73
|
+
"type": "object",
|
74
|
+
"properties": {
|
75
|
+
"filter": {
|
76
|
+
"type": "string",
|
77
|
+
"description": (
|
78
|
+
"Optional filter string (e.g., '+work', '@office', '(A)') - "
|
79
|
+
"use when you want to see only specific tasks"
|
80
|
+
),
|
81
|
+
}
|
82
|
+
},
|
83
|
+
"required": [],
|
84
|
+
},
|
85
|
+
}
|
86
|
+
},
|
87
|
+
{
|
88
|
+
"type": "function",
|
89
|
+
"function": {
|
90
|
+
"name": "list_completed_tasks",
|
91
|
+
"description": (
|
92
|
+
"List completed tasks from done.txt with optional filtering. Use this when: "
|
93
|
+
"1) User tries to complete a task that might already be done, "
|
94
|
+
"2) User asks about completed tasks, "
|
95
|
+
"3) You need to verify task status before taking action, "
|
96
|
+
"4) User wants to search for specific completed tasks by project, context, text, or date. "
|
97
|
+
"STRATEGIC CONTEXT: Call this BEFORE complete_task() to verify the task hasn't already "
|
98
|
+
"been completed, preventing duplicate completion attempts. "
|
99
|
+
"FILTERING: Use the filtering parameters to help users find specific completed tasks "
|
100
|
+
"when they ask about work tasks, home tasks, tasks from a specific date, etc. "
|
101
|
+
"DATE FILTERING LIMITATIONS: Due to todo.sh constraints, date filtering has specific behavior: "
|
102
|
+
"• When both date_from and date_to are provided, filtering uses the year-month pattern (YYYY-MM) "
|
103
|
+
"from date_from, matching all tasks in that month. "
|
104
|
+
"• When only date_from is provided, filtering uses the exact date pattern (YYYY-MM-DD). "
|
105
|
+
"• When only date_to is provided, filtering uses the year-month pattern (YYYY-MM) from date_to. "
|
106
|
+
"• Complex date ranges (e.g., spanning multiple months) are not supported by todo.sh."
|
107
|
+
),
|
108
|
+
"parameters": {
|
109
|
+
"type": "object",
|
110
|
+
"properties": {
|
111
|
+
"filter": {
|
112
|
+
"type": "string",
|
113
|
+
"description": (
|
114
|
+
"Optional raw filter string (e.g., '+work', '@office') - "
|
115
|
+
"use for advanced filtering when other parameters aren't sufficient"
|
116
|
+
),
|
117
|
+
},
|
118
|
+
"project": {
|
119
|
+
"type": "string",
|
120
|
+
"description": (
|
121
|
+
"Optional project name to filter by (without the + symbol) - "
|
122
|
+
"e.g., 'work', 'home', 'bills'"
|
123
|
+
),
|
124
|
+
},
|
125
|
+
"context": {
|
126
|
+
"type": "string",
|
127
|
+
"description": (
|
128
|
+
"Optional context name to filter by (without the @ symbol) - "
|
129
|
+
"e.g., 'office', 'home', 'phone'"
|
130
|
+
),
|
131
|
+
},
|
132
|
+
"text_search": {
|
133
|
+
"type": "string",
|
134
|
+
"description": (
|
135
|
+
"Optional text to search for in task descriptions - "
|
136
|
+
"e.g., 'review', 'call', 'email'"
|
137
|
+
),
|
138
|
+
},
|
139
|
+
"date_from": {
|
140
|
+
"type": "string",
|
141
|
+
"description": (
|
142
|
+
"Optional start date for filtering (YYYY-MM-DD format) - "
|
143
|
+
"e.g., '2025-08-01'. When used alone, matches exact date. "
|
144
|
+
"When used with date_to, uses year-month pattern (YYYY-MM) for month-based filtering."
|
145
|
+
),
|
146
|
+
},
|
147
|
+
"date_to": {
|
148
|
+
"type": "string",
|
149
|
+
"description": (
|
150
|
+
"Optional end date for filtering (YYYY-MM-DD format) - "
|
151
|
+
"e.g., '2025-08-31'. When used alone, uses year-month pattern (YYYY-MM) "
|
152
|
+
"for month-based filtering. When used with date_from, uses date_from's year-month pattern."
|
153
|
+
),
|
154
|
+
},
|
155
|
+
},
|
156
|
+
"required": [],
|
157
|
+
},
|
158
|
+
}
|
159
|
+
},
|
160
|
+
{
|
161
|
+
"type": "function",
|
162
|
+
"function": {
|
163
|
+
"name": "add_task",
|
164
|
+
"description": (
|
165
|
+
"Add a new task to todo.txt. CRITICAL: Before adding ANY task, you MUST "
|
166
|
+
"use list_tasks() and list_completed_tasks() to check for potential duplicates. Look for tasks with "
|
167
|
+
"similar descriptions, keywords, or intent. If you find similar tasks, "
|
168
|
+
"ask the user if they want to add a new task or modify an existing one. "
|
169
|
+
"If project or context is ambiguous, use discovery tools first. "
|
170
|
+
"Always provide a complete, natural response to the user. "
|
171
|
+
"STRATEGIC CONTEXT: This is a modification tool - call this LAST after using "
|
172
|
+
"discovery tools (list_tasks, list_projects, list_contexts list_completed_tasks) "
|
173
|
+
"to gather all necessary context and verify no duplicates exist."
|
174
|
+
),
|
175
|
+
"parameters": {
|
176
|
+
"type": "object",
|
177
|
+
"properties": {
|
178
|
+
"description": {
|
179
|
+
"type": "string",
|
180
|
+
"description": "The task description (required)",
|
181
|
+
},
|
182
|
+
"priority": {
|
183
|
+
"type": "string",
|
184
|
+
"description": "Optional priority level (A-Z, where A is highest)",
|
185
|
+
},
|
186
|
+
"project": {
|
187
|
+
"type": "string",
|
188
|
+
"description": "Optional project name (without the + symbol)",
|
189
|
+
},
|
190
|
+
"context": {
|
191
|
+
"type": "string",
|
192
|
+
"description": "Optional context name (without the @ symbol)",
|
193
|
+
},
|
194
|
+
"due": {
|
195
|
+
"type": "string",
|
196
|
+
"description": "Optional due date in YYYY-MM-DD format",
|
197
|
+
},
|
198
|
+
},
|
199
|
+
"required": ["description"],
|
200
|
+
},
|
201
|
+
}
|
202
|
+
},
|
203
|
+
{
|
204
|
+
"type": "function",
|
205
|
+
"function": {
|
206
|
+
"name": "complete_task",
|
207
|
+
"description": (
|
208
|
+
"Mark a specific task as complete by its line number. IMPORTANT: "
|
209
|
+
"Before completing, use list_completed_tasks() to check if it's already done. "
|
210
|
+
"If multiple tasks match the description, ask the user to clarify which one. "
|
211
|
+
"STRATEGIC CONTEXT: This is a modification tool - call this LAST after using "
|
212
|
+
"discovery tools (list_tasks, list_completed_tasks) "
|
213
|
+
"to verify the task exists and hasn't already been completed."
|
214
|
+
),
|
215
|
+
"parameters": {
|
216
|
+
"type": "object",
|
217
|
+
"properties": {
|
218
|
+
"task_number": {
|
219
|
+
"type": "integer",
|
220
|
+
"description": "The line number of the task to complete (required)",
|
221
|
+
}
|
222
|
+
},
|
223
|
+
"required": ["task_number"],
|
224
|
+
},
|
225
|
+
}
|
226
|
+
},
|
227
|
+
{
|
228
|
+
"type": "function",
|
229
|
+
"function": {
|
230
|
+
"name": "replace_task",
|
231
|
+
"description": (
|
232
|
+
"Replace the entire content of a task. IMPORTANT: Use list_tasks() first "
|
233
|
+
"to find the correct task number if user doesn't specify it. "
|
234
|
+
"If multiple tasks match the description, ask for clarification."
|
235
|
+
),
|
236
|
+
"parameters": {
|
237
|
+
"type": "object",
|
238
|
+
"properties": {
|
239
|
+
"task_number": {
|
240
|
+
"type": "integer",
|
241
|
+
"description": "The line number of the task to replace (required)",
|
242
|
+
},
|
243
|
+
"new_description": {
|
244
|
+
"type": "string",
|
245
|
+
"description": "The new task description (required)",
|
246
|
+
},
|
247
|
+
},
|
248
|
+
"required": ["task_number", "new_description"],
|
249
|
+
},
|
250
|
+
}
|
251
|
+
},
|
252
|
+
{
|
253
|
+
"type": "function",
|
254
|
+
"function": {
|
255
|
+
"name": "append_to_task",
|
256
|
+
"description": (
|
257
|
+
"Add text to the end of an existing task. Use this when user wants "
|
258
|
+
"to add additional information to a task without replacing it entirely."
|
259
|
+
),
|
260
|
+
"parameters": {
|
261
|
+
"type": "object",
|
262
|
+
"properties": {
|
263
|
+
"task_number": {
|
264
|
+
"type": "integer",
|
265
|
+
"description": "The line number of the task to modify (required)",
|
266
|
+
},
|
267
|
+
"text": {
|
268
|
+
"type": "string",
|
269
|
+
"description": "The text to append to the task (required)",
|
270
|
+
},
|
271
|
+
},
|
272
|
+
"required": ["task_number", "text"],
|
273
|
+
},
|
274
|
+
}
|
275
|
+
},
|
276
|
+
{
|
277
|
+
"type": "function",
|
278
|
+
"function": {
|
279
|
+
"name": "prepend_to_task",
|
280
|
+
"description": (
|
281
|
+
"Add text to the beginning of an existing task. Use this when user "
|
282
|
+
"wants to add a prefix or modifier to a task."
|
283
|
+
),
|
284
|
+
"parameters": {
|
285
|
+
"type": "object",
|
286
|
+
"properties": {
|
287
|
+
"task_number": {
|
288
|
+
"type": "integer",
|
289
|
+
"description": "The line number of the task to modify (required)",
|
290
|
+
},
|
291
|
+
"text": {
|
292
|
+
"type": "string",
|
293
|
+
"description": "The text to prepend to the task (required)",
|
294
|
+
},
|
295
|
+
},
|
296
|
+
"required": ["task_number", "text"],
|
297
|
+
},
|
298
|
+
}
|
299
|
+
},
|
300
|
+
{
|
301
|
+
"type": "function",
|
302
|
+
"function": {
|
303
|
+
"name": "delete_task",
|
304
|
+
"description": (
|
305
|
+
"Delete an entire task or remove a specific term from a task. "
|
306
|
+
"IMPORTANT: Use list_tasks() first to find the correct task number "
|
307
|
+
"if user doesn't specify it."
|
308
|
+
),
|
309
|
+
"parameters": {
|
310
|
+
"type": "object",
|
311
|
+
"properties": {
|
312
|
+
"task_number": {
|
313
|
+
"type": "integer",
|
314
|
+
"description": "The line number of the task to delete (required)",
|
315
|
+
},
|
316
|
+
"term": {
|
317
|
+
"type": "string",
|
318
|
+
"description": (
|
319
|
+
"Optional specific term to remove from the task "
|
320
|
+
"(if not provided, deletes entire task)"
|
321
|
+
),
|
322
|
+
},
|
323
|
+
},
|
324
|
+
"required": ["task_number"],
|
325
|
+
},
|
326
|
+
}
|
327
|
+
},
|
328
|
+
{
|
329
|
+
"type": "function",
|
330
|
+
"function": {
|
331
|
+
"name": "set_priority",
|
332
|
+
"description": (
|
333
|
+
"Set or change the priority of a task (A-Z, where A is highest). "
|
334
|
+
"IMPORTANT: Use list_tasks() first to find the correct task number "
|
335
|
+
"if user doesn't specify it."
|
336
|
+
),
|
337
|
+
"parameters": {
|
338
|
+
"type": "object",
|
339
|
+
"properties": {
|
340
|
+
"task_number": {
|
341
|
+
"type": "integer",
|
342
|
+
"description": "The line number of the task to prioritize (required)",
|
343
|
+
},
|
344
|
+
"priority": {
|
345
|
+
"type": "string",
|
346
|
+
"description": "Priority level (A-Z, where A is highest) (required)",
|
347
|
+
},
|
348
|
+
},
|
349
|
+
"required": ["task_number", "priority"],
|
350
|
+
},
|
351
|
+
}
|
352
|
+
},
|
353
|
+
{
|
354
|
+
"type": "function",
|
355
|
+
"function": {
|
356
|
+
"name": "remove_priority",
|
357
|
+
"description": (
|
358
|
+
"Remove the priority from a task. IMPORTANT: Use list_tasks() first "
|
359
|
+
"to find the correct task number if user doesn't specify it."
|
360
|
+
),
|
361
|
+
"parameters": {
|
362
|
+
"type": "object",
|
363
|
+
"properties": {
|
364
|
+
"task_number": {
|
365
|
+
"type": "integer",
|
366
|
+
"description": "The line number of the task to deprioritize (required)",
|
367
|
+
}
|
368
|
+
},
|
369
|
+
"required": ["task_number"],
|
370
|
+
},
|
371
|
+
}
|
372
|
+
},
|
373
|
+
{
|
374
|
+
"type": "function",
|
375
|
+
"function": {
|
376
|
+
"name": "get_overview",
|
377
|
+
"description": (
|
378
|
+
"Show task statistics and summary. Use this when user asks for "
|
379
|
+
"an overview, summary, or statistics about their tasks."
|
380
|
+
),
|
381
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
382
|
+
}
|
383
|
+
},
|
384
|
+
{
|
385
|
+
"type": "function",
|
386
|
+
"function": {
|
387
|
+
"name": "move_task",
|
388
|
+
"description": (
|
389
|
+
"Move a task from one file to another (e.g., from todo.txt to done.txt). "
|
390
|
+
"IMPORTANT: Use list_tasks() first to find the correct task number "
|
391
|
+
"if user doesn't specify it."
|
392
|
+
),
|
393
|
+
"parameters": {
|
394
|
+
"type": "object",
|
395
|
+
"properties": {
|
396
|
+
"task_number": {
|
397
|
+
"type": "integer",
|
398
|
+
"description": "The line number of the task to move (required)",
|
399
|
+
},
|
400
|
+
"destination": {
|
401
|
+
"type": "string",
|
402
|
+
"description": "The destination file name (e.g., 'done.txt') (required)",
|
403
|
+
},
|
404
|
+
"source": {
|
405
|
+
"type": "string",
|
406
|
+
"description": "Optional source file name (defaults to todo.txt)",
|
407
|
+
},
|
408
|
+
},
|
409
|
+
"required": ["task_number", "destination"],
|
410
|
+
},
|
411
|
+
}
|
412
|
+
},
|
413
|
+
{
|
414
|
+
"type": "function",
|
415
|
+
"function": {
|
416
|
+
"name": "archive_tasks",
|
417
|
+
"description": (
|
418
|
+
"Archive completed tasks by moving them from todo.txt to done.txt "
|
419
|
+
"and removing blank lines. Use this when user wants to clean up "
|
420
|
+
"their todo list or archive completed tasks."
|
421
|
+
),
|
422
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
423
|
+
}
|
424
|
+
},
|
425
|
+
{
|
426
|
+
"type": "function",
|
427
|
+
"function": {
|
428
|
+
"name": "deduplicate_tasks",
|
429
|
+
"description": (
|
430
|
+
"Remove duplicate tasks from todo.txt. Use this when user wants "
|
431
|
+
"to clean up duplicate entries or when you notice duplicate tasks "
|
432
|
+
"in the list."
|
433
|
+
),
|
434
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
435
|
+
}
|
436
|
+
},
|
437
|
+
|
438
|
+
]
|
439
|
+
|
440
|
+
def _format_tool_signature(self, tool_name: str, arguments: Dict[str, Any]) -> str:
|
441
|
+
"""Format tool signature with parameters for logging."""
|
442
|
+
if not arguments:
|
443
|
+
return f"{tool_name}()"
|
444
|
+
|
445
|
+
# Format parameters as key=value pairs
|
446
|
+
param_parts = []
|
447
|
+
for key, value in arguments.items():
|
448
|
+
if isinstance(value, str):
|
449
|
+
# Quote string values
|
450
|
+
param_parts.append(f"{key}='{value}'")
|
451
|
+
else:
|
452
|
+
param_parts.append(f"{key}={value}")
|
453
|
+
|
454
|
+
return f"{tool_name}({', '.join(param_parts)})"
|
455
|
+
|
456
|
+
def execute_tool(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
457
|
+
"""Execute a tool call and return the result."""
|
458
|
+
tool_name = tool_call["function"]["name"]
|
459
|
+
arguments = tool_call["function"]["arguments"]
|
460
|
+
tool_call_id = tool_call.get("id", "unknown")
|
461
|
+
|
462
|
+
# Handle arguments that might be a string (JSON) or already a dict
|
463
|
+
if isinstance(arguments, str):
|
464
|
+
import json
|
465
|
+
try:
|
466
|
+
arguments = json.loads(arguments)
|
467
|
+
if self.logger:
|
468
|
+
self.logger.debug(f"Parsed JSON arguments: {arguments}")
|
469
|
+
except json.JSONDecodeError as e:
|
470
|
+
if self.logger:
|
471
|
+
self.logger.warning(f"Failed to parse JSON arguments: {e}")
|
472
|
+
arguments = {}
|
473
|
+
|
474
|
+
# Format tool signature with parameters
|
475
|
+
tool_signature = self._format_tool_signature(tool_name, arguments)
|
476
|
+
|
477
|
+
# Log function name with signature at INFO level
|
478
|
+
if self.logger:
|
479
|
+
self.logger.info(f"Executing tool: {tool_signature} (ID: {tool_call_id})")
|
480
|
+
|
481
|
+
# Log detailed command information at DEBUG level
|
482
|
+
if self.logger:
|
483
|
+
self.logger.debug(f"=== TOOL EXECUTION START ===")
|
484
|
+
self.logger.debug(f"Tool: {tool_name}")
|
485
|
+
self.logger.debug(f"Tool Call ID: {tool_call_id}")
|
486
|
+
self.logger.debug(f"Arguments: {tool_call['function']['arguments']}")
|
487
|
+
|
488
|
+
# Map tool names to todo_manager methods
|
489
|
+
method_map = {
|
490
|
+
"list_projects": self.todo_manager.list_projects,
|
491
|
+
"list_contexts": self.todo_manager.list_contexts,
|
492
|
+
"list_tasks": self.todo_manager.list_tasks,
|
493
|
+
"list_completed_tasks": self.todo_manager.list_completed_tasks,
|
494
|
+
"add_task": self.todo_manager.add_task,
|
495
|
+
"complete_task": self.todo_manager.complete_task,
|
496
|
+
"replace_task": self.todo_manager.replace_task,
|
497
|
+
"append_to_task": self.todo_manager.append_to_task,
|
498
|
+
"prepend_to_task": self.todo_manager.prepend_to_task,
|
499
|
+
"delete_task": self.todo_manager.delete_task,
|
500
|
+
"set_priority": self.todo_manager.set_priority,
|
501
|
+
"remove_priority": self.todo_manager.remove_priority,
|
502
|
+
"get_overview": self.todo_manager.get_overview,
|
503
|
+
"move_task": self.todo_manager.move_task,
|
504
|
+
"archive_tasks": self.todo_manager.archive_tasks,
|
505
|
+
"deduplicate_tasks": self.todo_manager.deduplicate_tasks,
|
506
|
+
}
|
507
|
+
|
508
|
+
if tool_name not in method_map:
|
509
|
+
error_msg = f"Unknown tool: {tool_name}"
|
510
|
+
if self.logger:
|
511
|
+
self.logger.error(error_msg)
|
512
|
+
return {
|
513
|
+
"tool_call_id": tool_call_id,
|
514
|
+
"name": tool_name,
|
515
|
+
"output": f"ERROR: {error_msg}",
|
516
|
+
"error": True,
|
517
|
+
"error_type": "unknown_tool",
|
518
|
+
"error_details": error_msg
|
519
|
+
}
|
520
|
+
|
521
|
+
method = method_map[tool_name]
|
522
|
+
|
523
|
+
# Log method call details
|
524
|
+
if self.logger:
|
525
|
+
self.logger.debug(f"Calling method: {tool_name}")
|
526
|
+
|
527
|
+
try:
|
528
|
+
result = method(**arguments)
|
529
|
+
|
530
|
+
# Log successful output at DEBUG level
|
531
|
+
if self.logger:
|
532
|
+
self.logger.debug(f"=== TOOL EXECUTION SUCCESS ===")
|
533
|
+
self.logger.debug(f"Tool: {tool_name}")
|
534
|
+
self.logger.debug(f"Raw result: ====\n{result}\n====")
|
535
|
+
|
536
|
+
# For list results, log the count
|
537
|
+
if isinstance(result, list):
|
538
|
+
self.logger.debug(f"Result count: {len(result)}")
|
539
|
+
# For string results, log the length
|
540
|
+
elif isinstance(result, str):
|
541
|
+
self.logger.debug(f"Result length: {len(result)}")
|
542
|
+
|
543
|
+
return {"tool_call_id": tool_call_id, "name": tool_name, "output": result, "error": False}
|
544
|
+
|
545
|
+
except Exception as e:
|
546
|
+
# Log error details
|
547
|
+
if self.logger:
|
548
|
+
self.logger.error(f"=== TOOL EXECUTION FAILED ===")
|
549
|
+
self.logger.error(f"Tool: {tool_name}")
|
550
|
+
self.logger.error(f"Error type: {type(e).__name__}")
|
551
|
+
self.logger.error(f"Error message: {str(e)}")
|
552
|
+
self.logger.exception(f"Exception details for {tool_name}")
|
553
|
+
|
554
|
+
# Return structured error information instead of raising
|
555
|
+
error_type = type(e).__name__
|
556
|
+
error_message = str(e)
|
557
|
+
|
558
|
+
# Provide user-friendly error messages based on error type
|
559
|
+
if "FileNotFoundError" in error_type or "todo.sh" in error_message.lower():
|
560
|
+
user_message = f"Todo.sh command failed: {error_message}. Please ensure todo.sh is properly installed and configured."
|
561
|
+
elif "IndexError" in error_type or "task" in error_message.lower() and "not found" in error_message.lower():
|
562
|
+
user_message = f"Task not found: {error_message}. The task may have been completed or deleted."
|
563
|
+
elif "ValueError" in error_type:
|
564
|
+
user_message = f"Invalid input: {error_message}. Please check the task format or parameters."
|
565
|
+
elif "PermissionError" in error_type:
|
566
|
+
user_message = f"Permission denied: {error_message}. Please check file permissions for todo.txt files."
|
567
|
+
else:
|
568
|
+
user_message = f"Operation failed: {error_message}"
|
569
|
+
|
570
|
+
return {
|
571
|
+
"tool_call_id": tool_call_id,
|
572
|
+
"name": tool_name,
|
573
|
+
"output": f"ERROR: {user_message}",
|
574
|
+
"error": True,
|
575
|
+
"error_type": error_type,
|
576
|
+
"error_details": error_message,
|
577
|
+
"user_message": user_message
|
578
|
+
}
|
todo_agent/main.py
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Main entry point for todo.sh LLM agent.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import argparse
|
7
|
+
import sys
|
8
|
+
|
9
|
+
from .interface.cli import CLI
|
10
|
+
|
11
|
+
|
12
|
+
def main():
|
13
|
+
"""Main application entry point."""
|
14
|
+
parser = argparse.ArgumentParser(
|
15
|
+
description="Todo.sh LLM Agent - Natural language task management",
|
16
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
17
|
+
epilog="""
|
18
|
+
Examples:
|
19
|
+
todo-agent # Interactive mode
|
20
|
+
todo-agent "add buy groceries" # Single command mode
|
21
|
+
todo-agent "list my tasks" # List all tasks
|
22
|
+
todo-agent "complete task 3" # Complete specific task
|
23
|
+
""",
|
24
|
+
)
|
25
|
+
|
26
|
+
parser.add_argument(
|
27
|
+
"command",
|
28
|
+
nargs="?",
|
29
|
+
help="Single command to execute (optional, defaults to interactive mode)",
|
30
|
+
)
|
31
|
+
|
32
|
+
args = parser.parse_args()
|
33
|
+
|
34
|
+
try:
|
35
|
+
cli = CLI()
|
36
|
+
|
37
|
+
if args.command:
|
38
|
+
# Single command mode
|
39
|
+
response = cli.run_single_request(args.command)
|
40
|
+
print(response)
|
41
|
+
else:
|
42
|
+
# Interactive mode
|
43
|
+
cli.run()
|
44
|
+
|
45
|
+
except KeyboardInterrupt:
|
46
|
+
print("\nGoodbye!")
|
47
|
+
sys.exit(0)
|
48
|
+
except Exception as e:
|
49
|
+
print(f"Error: {e}")
|
50
|
+
sys.exit(1)
|
51
|
+
|
52
|
+
|
53
|
+
if __name__ == "__main__":
|
54
|
+
main()
|