todo-agent 0.1.1__py3-none-any.whl → 0.2.3__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/_version.py +2 -2
- todo_agent/core/__init__.py +3 -3
- todo_agent/core/conversation_manager.py +39 -17
- todo_agent/core/todo_manager.py +40 -30
- todo_agent/infrastructure/__init__.py +1 -1
- todo_agent/infrastructure/config.py +7 -5
- todo_agent/infrastructure/inference.py +109 -54
- todo_agent/infrastructure/llm_client_factory.py +13 -9
- todo_agent/infrastructure/logger.py +38 -41
- todo_agent/infrastructure/ollama_client.py +22 -15
- todo_agent/infrastructure/openrouter_client.py +37 -26
- todo_agent/infrastructure/todo_shell.py +12 -10
- todo_agent/infrastructure/token_counter.py +39 -38
- todo_agent/interface/__init__.py +16 -1
- todo_agent/interface/cli.py +119 -65
- todo_agent/interface/formatters.py +399 -0
- todo_agent/interface/tools.py +47 -40
- todo_agent/main.py +11 -3
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.3.dist-info}/METADATA +72 -32
- todo_agent-0.2.3.dist-info/RECORD +28 -0
- todo_agent-0.1.1.dist-info/RECORD +0 -27
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.3.dist-info}/WHEEL +0 -0
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.3.dist-info}/entry_points.txt +0 -0
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,399 @@
|
|
1
|
+
"""
|
2
|
+
Formatters for CLI output with unicode characters and consistent styling.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict
|
6
|
+
|
7
|
+
from rich.align import Align
|
8
|
+
from rich.box import ROUNDED
|
9
|
+
from rich.panel import Panel
|
10
|
+
from rich.table import Table
|
11
|
+
from rich.text import Text
|
12
|
+
|
13
|
+
# CLI width configuration
|
14
|
+
CLI_WIDTH = 100
|
15
|
+
PANEL_WIDTH = CLI_WIDTH - 2 # Leave 2 characters for borders
|
16
|
+
|
17
|
+
|
18
|
+
class TaskFormatter:
|
19
|
+
"""Formats task-related output with unicode characters and consistent styling."""
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def format_task_list(raw_tasks: str) -> Text:
|
23
|
+
"""
|
24
|
+
Format a raw task list with unicode characters and numbering.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
raw_tasks: Raw task output from todo.sh
|
28
|
+
title: Title for the task list
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Formatted task list as Rich Text object
|
32
|
+
"""
|
33
|
+
if not raw_tasks.strip():
|
34
|
+
return Text("No tasks found.")
|
35
|
+
|
36
|
+
lines = raw_tasks.strip().split("\n")
|
37
|
+
formatted_text = Text()
|
38
|
+
task_count = 0
|
39
|
+
|
40
|
+
# Add header
|
41
|
+
formatted_text.append("Tasks", style="bold blue")
|
42
|
+
formatted_text.append("\n\n")
|
43
|
+
|
44
|
+
for line in lines:
|
45
|
+
line = line.strip()
|
46
|
+
# Skip empty lines, separators, and todo.sh's own summary line
|
47
|
+
if line and line != "--" and not line.startswith("TODO:"):
|
48
|
+
task_count += 1
|
49
|
+
# Parse todo.txt format and make it more readable
|
50
|
+
formatted_task = TaskFormatter._format_single_task(line, task_count)
|
51
|
+
# Create a Text object that respects ANSI codes
|
52
|
+
task_text = Text.from_ansi(formatted_task)
|
53
|
+
formatted_text.append(task_text)
|
54
|
+
formatted_text.append("\n")
|
55
|
+
|
56
|
+
# Add task count at the end
|
57
|
+
if task_count > 0:
|
58
|
+
formatted_text.append("\n")
|
59
|
+
formatted_text.append(f"TODO: {task_count} of {task_count} tasks shown")
|
60
|
+
else:
|
61
|
+
formatted_text = Text("No tasks found.")
|
62
|
+
|
63
|
+
return formatted_text
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def _format_single_task(task_line: str, task_number: int) -> str:
|
67
|
+
"""
|
68
|
+
Format a single task line with unicode characters.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
task_line: Raw task line from todo.sh
|
72
|
+
task_number: Task number for display
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Formatted task string
|
76
|
+
"""
|
77
|
+
# Parse todo.txt format: "1 (A) 2025-08-29 Clean cat box @home +chores due:2025-08-29"
|
78
|
+
parts = task_line.split(
|
79
|
+
" ", 1
|
80
|
+
) # Split on first space to separate number from rest
|
81
|
+
if len(parts) < 2:
|
82
|
+
return f" {task_number:2d} │ │ {task_line}"
|
83
|
+
|
84
|
+
rest = parts[1]
|
85
|
+
|
86
|
+
# Extract priority if present (format: "(A)")
|
87
|
+
priority = ""
|
88
|
+
description = rest
|
89
|
+
|
90
|
+
if rest.startswith("(") and ")" in rest:
|
91
|
+
priority_end = rest.find(")")
|
92
|
+
priority = rest[1:priority_end]
|
93
|
+
description = rest[priority_end + 1 :].strip()
|
94
|
+
|
95
|
+
# Format with unicode characters
|
96
|
+
if priority:
|
97
|
+
formatted_line = f" {task_number:2d} │ {priority} │ {description}"
|
98
|
+
else:
|
99
|
+
formatted_line = f" {task_number:2d} │ │ {description}"
|
100
|
+
|
101
|
+
return formatted_line
|
102
|
+
|
103
|
+
@staticmethod
|
104
|
+
def format_projects(raw_projects: str) -> str:
|
105
|
+
"""
|
106
|
+
Format project list with unicode characters.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
raw_projects: Raw project output from todo.sh
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
Formatted project list string
|
113
|
+
"""
|
114
|
+
if not raw_projects.strip():
|
115
|
+
return "No projects found."
|
116
|
+
|
117
|
+
lines = raw_projects.strip().split("\n")
|
118
|
+
formatted_lines = []
|
119
|
+
|
120
|
+
for i, project in enumerate(lines, 1):
|
121
|
+
if project.strip():
|
122
|
+
# Remove the + prefix and format nicely
|
123
|
+
clean_project = project.strip().lstrip("+")
|
124
|
+
formatted_lines.append(f" {i:2d} │ {clean_project}")
|
125
|
+
|
126
|
+
if formatted_lines:
|
127
|
+
return "\n".join(formatted_lines)
|
128
|
+
else:
|
129
|
+
return "No projects found."
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
def format_contexts(raw_contexts: str) -> str:
|
133
|
+
"""
|
134
|
+
Format context list with unicode characters.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
raw_contexts: Raw context output from todo.sh
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Formatted context list string
|
141
|
+
"""
|
142
|
+
if not raw_contexts.strip():
|
143
|
+
return "No contexts found."
|
144
|
+
|
145
|
+
lines = raw_contexts.strip().split("\n")
|
146
|
+
formatted_lines = []
|
147
|
+
|
148
|
+
for i, context in enumerate(lines, 1):
|
149
|
+
if context.strip():
|
150
|
+
# Remove the @ prefix and format nicely
|
151
|
+
clean_context = context.strip().lstrip("@")
|
152
|
+
formatted_lines.append(f" {i:2d} │ {clean_context}")
|
153
|
+
|
154
|
+
if formatted_lines:
|
155
|
+
return "\n".join(formatted_lines)
|
156
|
+
else:
|
157
|
+
return "No contexts found."
|
158
|
+
|
159
|
+
|
160
|
+
class ResponseFormatter:
|
161
|
+
"""Formats LLM responses and other output with consistent styling."""
|
162
|
+
|
163
|
+
@staticmethod
|
164
|
+
def format_response(response: str) -> str:
|
165
|
+
"""
|
166
|
+
Format an LLM response with consistent styling.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
response: Raw response text
|
170
|
+
title: Title for the response panel
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
Formatted response string
|
174
|
+
"""
|
175
|
+
# If response contains task lists, format them nicely
|
176
|
+
if "No tasks found" in response or "1." in response:
|
177
|
+
# This might be a task list response, try to format it
|
178
|
+
lines = response.split("\n")
|
179
|
+
formatted_lines = []
|
180
|
+
|
181
|
+
for line in lines:
|
182
|
+
if line.strip().startswith(
|
183
|
+
("1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.")
|
184
|
+
):
|
185
|
+
# This looks like a numbered task list, format it
|
186
|
+
parts = line.split(".", 1)
|
187
|
+
if len(parts) == 2:
|
188
|
+
number = parts[0].strip()
|
189
|
+
content = parts[1].strip()
|
190
|
+
formatted_lines.append(f" {number:>2} │ {content}")
|
191
|
+
else:
|
192
|
+
formatted_lines.append(line)
|
193
|
+
else:
|
194
|
+
formatted_lines.append(line)
|
195
|
+
|
196
|
+
return "\n".join(formatted_lines)
|
197
|
+
|
198
|
+
return response
|
199
|
+
|
200
|
+
@staticmethod
|
201
|
+
def format_error(error_message: str) -> str:
|
202
|
+
"""
|
203
|
+
Format error messages consistently.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
error_message: Error message to format
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Formatted error string
|
210
|
+
"""
|
211
|
+
return f"❌ {error_message}"
|
212
|
+
|
213
|
+
@staticmethod
|
214
|
+
def format_success(message: str) -> str:
|
215
|
+
"""
|
216
|
+
Format success messages consistently.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
message: Success message to format
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Formatted success string
|
223
|
+
"""
|
224
|
+
return f"✅ {message}"
|
225
|
+
|
226
|
+
|
227
|
+
class StatsFormatter:
|
228
|
+
"""Formats statistics and overview information."""
|
229
|
+
|
230
|
+
@staticmethod
|
231
|
+
def format_overview(overview: str) -> str:
|
232
|
+
"""
|
233
|
+
Format task overview with unicode characters.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
overview: Raw overview string
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
Formatted overview string
|
240
|
+
"""
|
241
|
+
if "Task Overview:" in overview:
|
242
|
+
lines = overview.split("\n")
|
243
|
+
formatted_lines = []
|
244
|
+
|
245
|
+
for line in lines:
|
246
|
+
if line.startswith("- Active tasks:"):
|
247
|
+
formatted_lines.append(f"📋 {line[2:]}")
|
248
|
+
elif line.startswith("- Completed tasks:"):
|
249
|
+
formatted_lines.append(f"✅ {line[2:]}")
|
250
|
+
else:
|
251
|
+
formatted_lines.append(line)
|
252
|
+
|
253
|
+
return "\n".join(formatted_lines)
|
254
|
+
|
255
|
+
return overview
|
256
|
+
|
257
|
+
|
258
|
+
class TableFormatter:
|
259
|
+
"""Creates rich tables for various data displays."""
|
260
|
+
|
261
|
+
@staticmethod
|
262
|
+
def create_command_table() -> Table:
|
263
|
+
"""Create a table for displaying available commands."""
|
264
|
+
table = Table(
|
265
|
+
title="Available Commands",
|
266
|
+
box=ROUNDED,
|
267
|
+
show_header=True,
|
268
|
+
header_style="bold magenta",
|
269
|
+
width=PANEL_WIDTH,
|
270
|
+
)
|
271
|
+
|
272
|
+
table.add_column("Command", style="cyan", width=12)
|
273
|
+
table.add_column("Description", style="white")
|
274
|
+
|
275
|
+
commands = [
|
276
|
+
("clear", "Clear conversation history"),
|
277
|
+
("stats", "Show conversation statistics"),
|
278
|
+
("help", "Show this help message"),
|
279
|
+
("about", "Show application information"),
|
280
|
+
("list", "List all tasks (no LLM interaction)"),
|
281
|
+
("quit", "Exit the application"),
|
282
|
+
]
|
283
|
+
|
284
|
+
for cmd, desc in commands:
|
285
|
+
table.add_row(cmd, desc)
|
286
|
+
|
287
|
+
return table
|
288
|
+
|
289
|
+
@staticmethod
|
290
|
+
def create_stats_table(summary: Dict[str, Any]) -> Table:
|
291
|
+
"""Create a table for displaying conversation statistics."""
|
292
|
+
table = Table(
|
293
|
+
title="Conversation Statistics",
|
294
|
+
box=ROUNDED,
|
295
|
+
show_header=True,
|
296
|
+
header_style="bold magenta",
|
297
|
+
width=PANEL_WIDTH,
|
298
|
+
)
|
299
|
+
|
300
|
+
table.add_column("Metric", style="cyan", width=20)
|
301
|
+
table.add_column("Value", style="white")
|
302
|
+
|
303
|
+
# Basic stats
|
304
|
+
table.add_row("Total Messages", str(summary["total_messages"]))
|
305
|
+
table.add_row("User Messages", str(summary["user_messages"]))
|
306
|
+
table.add_row("Assistant Messages", str(summary["assistant_messages"]))
|
307
|
+
table.add_row("Tool Messages", str(summary["tool_messages"]))
|
308
|
+
table.add_row("Estimated Tokens", str(summary["estimated_tokens"]))
|
309
|
+
|
310
|
+
# Thinking time stats if available
|
311
|
+
if "thinking_time_count" in summary and summary["thinking_time_count"] > 0:
|
312
|
+
table.add_row("", "") # Empty row for spacing
|
313
|
+
table.add_row(
|
314
|
+
"Total Thinking Time", f"{summary['total_thinking_time']:.2f}s"
|
315
|
+
)
|
316
|
+
table.add_row(
|
317
|
+
"Average Thinking Time", f"{summary['average_thinking_time']:.2f}s"
|
318
|
+
)
|
319
|
+
table.add_row("Min Thinking Time", f"{summary['min_thinking_time']:.2f}s")
|
320
|
+
table.add_row("Max Thinking Time", f"{summary['max_thinking_time']:.2f}s")
|
321
|
+
table.add_row("Requests with Timing", str(summary["thinking_time_count"]))
|
322
|
+
|
323
|
+
return table
|
324
|
+
|
325
|
+
|
326
|
+
class PanelFormatter:
|
327
|
+
"""Creates rich panels for various content displays."""
|
328
|
+
|
329
|
+
@staticmethod
|
330
|
+
def create_header_panel() -> Panel:
|
331
|
+
"""Create the application header panel."""
|
332
|
+
header_text = Text("Todo.sh LLM Agent", style="bold blue")
|
333
|
+
return Panel(
|
334
|
+
Align.center(header_text),
|
335
|
+
title="🤖",
|
336
|
+
border_style="dim",
|
337
|
+
box=ROUNDED,
|
338
|
+
width=PANEL_WIDTH + 2,
|
339
|
+
)
|
340
|
+
|
341
|
+
@staticmethod
|
342
|
+
def create_task_panel(content: str, title: str = "📋 Current Tasks") -> Panel:
|
343
|
+
"""Create a panel for displaying task lists."""
|
344
|
+
return Panel(
|
345
|
+
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
346
|
+
)
|
347
|
+
|
348
|
+
@staticmethod
|
349
|
+
def create_response_panel(content: str, title: str = "🤖 Assistant") -> Panel:
|
350
|
+
"""Create a panel for displaying LLM responses."""
|
351
|
+
return Panel(
|
352
|
+
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
353
|
+
)
|
354
|
+
|
355
|
+
@staticmethod
|
356
|
+
def create_error_panel(content: str, title: str = "❌ Error") -> Panel:
|
357
|
+
"""Create a panel for displaying errors."""
|
358
|
+
return Panel(
|
359
|
+
content, title=title, border_style="red", box=ROUNDED, width=PANEL_WIDTH
|
360
|
+
)
|
361
|
+
|
362
|
+
@staticmethod
|
363
|
+
def create_about_panel() -> Panel:
|
364
|
+
"""Create a panel for displaying about information."""
|
365
|
+
from todo_agent._version import __commit_id__, __version__
|
366
|
+
|
367
|
+
about_content = Text()
|
368
|
+
about_content.append("Todo.sh LLM Agent\n", style="bold blue")
|
369
|
+
about_content.append("\n")
|
370
|
+
about_content.append(
|
371
|
+
"A natural language interface for todo.sh task management\n", style="white"
|
372
|
+
)
|
373
|
+
about_content.append("powered by LLM function calling.\n", style="white")
|
374
|
+
about_content.append("\n")
|
375
|
+
about_content.append("Version: ", style="cyan")
|
376
|
+
about_content.append(f"{__version__}\n", style="white")
|
377
|
+
if __commit_id__:
|
378
|
+
about_content.append("Commit: ", style="cyan")
|
379
|
+
about_content.append(f"{__commit_id__}\n", style="white")
|
380
|
+
about_content.append("\n")
|
381
|
+
about_content.append(
|
382
|
+
"Transform natural language into todo.sh commands:\n", style="italic"
|
383
|
+
)
|
384
|
+
about_content.append("• 'add buy groceries to shopping list'\n", style="dim")
|
385
|
+
about_content.append("• 'show my work tasks'\n", style="dim")
|
386
|
+
about_content.append("• 'mark task 3 as done'\n", style="dim")
|
387
|
+
about_content.append("\n")
|
388
|
+
about_content.append("GitHub: ", style="cyan")
|
389
|
+
about_content.append(
|
390
|
+
"https://github.com/codeprimate/todo-agent\n", style="blue"
|
391
|
+
)
|
392
|
+
|
393
|
+
return Panel(
|
394
|
+
Align.center(about_content),
|
395
|
+
title="i About",
|
396
|
+
border_style="dim",
|
397
|
+
box=ROUNDED,
|
398
|
+
width=PANEL_WIDTH + 2,
|
399
|
+
)
|
todo_agent/interface/tools.py
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
Tool definitions and schemas for LLM function calling.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
6
6
|
|
7
7
|
try:
|
8
8
|
from todo_agent.core.todo_manager import TodoManager
|
9
9
|
from todo_agent.infrastructure.logger import Logger
|
10
10
|
except ImportError:
|
11
|
-
from core.todo_manager import TodoManager
|
12
|
-
from infrastructure.logger import Logger
|
11
|
+
from core.todo_manager import TodoManager # type: ignore[no-redef]
|
12
|
+
from infrastructure.logger import Logger # type: ignore[no-redef]
|
13
13
|
|
14
14
|
|
15
15
|
class ToolCallHandler:
|
@@ -36,7 +36,7 @@ class ToolCallHandler:
|
|
36
36
|
"to avoid asking the user for clarification when you can find the answer yourself."
|
37
37
|
),
|
38
38
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
39
|
-
}
|
39
|
+
},
|
40
40
|
},
|
41
41
|
{
|
42
42
|
"type": "function",
|
@@ -51,7 +51,7 @@ class ToolCallHandler:
|
|
51
51
|
"to avoid asking the user for clarification when you can find the answer yourself."
|
52
52
|
),
|
53
53
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
54
|
-
}
|
54
|
+
},
|
55
55
|
},
|
56
56
|
{
|
57
57
|
"type": "function",
|
@@ -82,7 +82,7 @@ class ToolCallHandler:
|
|
82
82
|
},
|
83
83
|
"required": [],
|
84
84
|
},
|
85
|
-
}
|
85
|
+
},
|
86
86
|
},
|
87
87
|
{
|
88
88
|
"type": "function",
|
@@ -155,7 +155,7 @@ class ToolCallHandler:
|
|
155
155
|
},
|
156
156
|
"required": [],
|
157
157
|
},
|
158
|
-
}
|
158
|
+
},
|
159
159
|
},
|
160
160
|
{
|
161
161
|
"type": "function",
|
@@ -198,7 +198,7 @@ class ToolCallHandler:
|
|
198
198
|
},
|
199
199
|
"required": ["description"],
|
200
200
|
},
|
201
|
-
}
|
201
|
+
},
|
202
202
|
},
|
203
203
|
{
|
204
204
|
"type": "function",
|
@@ -222,7 +222,7 @@ class ToolCallHandler:
|
|
222
222
|
},
|
223
223
|
"required": ["task_number"],
|
224
224
|
},
|
225
|
-
}
|
225
|
+
},
|
226
226
|
},
|
227
227
|
{
|
228
228
|
"type": "function",
|
@@ -247,7 +247,7 @@ class ToolCallHandler:
|
|
247
247
|
},
|
248
248
|
"required": ["task_number", "new_description"],
|
249
249
|
},
|
250
|
-
}
|
250
|
+
},
|
251
251
|
},
|
252
252
|
{
|
253
253
|
"type": "function",
|
@@ -271,7 +271,7 @@ class ToolCallHandler:
|
|
271
271
|
},
|
272
272
|
"required": ["task_number", "text"],
|
273
273
|
},
|
274
|
-
}
|
274
|
+
},
|
275
275
|
},
|
276
276
|
{
|
277
277
|
"type": "function",
|
@@ -295,7 +295,7 @@ class ToolCallHandler:
|
|
295
295
|
},
|
296
296
|
"required": ["task_number", "text"],
|
297
297
|
},
|
298
|
-
}
|
298
|
+
},
|
299
299
|
},
|
300
300
|
{
|
301
301
|
"type": "function",
|
@@ -323,7 +323,7 @@ class ToolCallHandler:
|
|
323
323
|
},
|
324
324
|
"required": ["task_number"],
|
325
325
|
},
|
326
|
-
}
|
326
|
+
},
|
327
327
|
},
|
328
328
|
{
|
329
329
|
"type": "function",
|
@@ -348,7 +348,7 @@ class ToolCallHandler:
|
|
348
348
|
},
|
349
349
|
"required": ["task_number", "priority"],
|
350
350
|
},
|
351
|
-
}
|
351
|
+
},
|
352
352
|
},
|
353
353
|
{
|
354
354
|
"type": "function",
|
@@ -368,7 +368,7 @@ class ToolCallHandler:
|
|
368
368
|
},
|
369
369
|
"required": ["task_number"],
|
370
370
|
},
|
371
|
-
}
|
371
|
+
},
|
372
372
|
},
|
373
373
|
{
|
374
374
|
"type": "function",
|
@@ -379,7 +379,7 @@ class ToolCallHandler:
|
|
379
379
|
"an overview, summary, or statistics about their tasks."
|
380
380
|
),
|
381
381
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
382
|
-
}
|
382
|
+
},
|
383
383
|
},
|
384
384
|
{
|
385
385
|
"type": "function",
|
@@ -408,7 +408,7 @@ class ToolCallHandler:
|
|
408
408
|
},
|
409
409
|
"required": ["task_number", "destination"],
|
410
410
|
},
|
411
|
-
}
|
411
|
+
},
|
412
412
|
},
|
413
413
|
{
|
414
414
|
"type": "function",
|
@@ -420,7 +420,7 @@ class ToolCallHandler:
|
|
420
420
|
"their todo list or archive completed tasks."
|
421
421
|
),
|
422
422
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
423
|
-
}
|
423
|
+
},
|
424
424
|
},
|
425
425
|
{
|
426
426
|
"type": "function",
|
@@ -432,16 +432,15 @@ class ToolCallHandler:
|
|
432
432
|
"in the list."
|
433
433
|
),
|
434
434
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
435
|
-
}
|
435
|
+
},
|
436
436
|
},
|
437
|
-
|
438
437
|
]
|
439
438
|
|
440
439
|
def _format_tool_signature(self, tool_name: str, arguments: Dict[str, Any]) -> str:
|
441
440
|
"""Format tool signature with parameters for logging."""
|
442
441
|
if not arguments:
|
443
442
|
return f"{tool_name}()"
|
444
|
-
|
443
|
+
|
445
444
|
# Format parameters as key=value pairs
|
446
445
|
param_parts = []
|
447
446
|
for key, value in arguments.items():
|
@@ -450,7 +449,7 @@ class ToolCallHandler:
|
|
450
449
|
param_parts.append(f"{key}='{value}'")
|
451
450
|
else:
|
452
451
|
param_parts.append(f"{key}={value}")
|
453
|
-
|
452
|
+
|
454
453
|
return f"{tool_name}({', '.join(param_parts)})"
|
455
454
|
|
456
455
|
def execute_tool(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
@@ -458,10 +457,11 @@ class ToolCallHandler:
|
|
458
457
|
tool_name = tool_call["function"]["name"]
|
459
458
|
arguments = tool_call["function"]["arguments"]
|
460
459
|
tool_call_id = tool_call.get("id", "unknown")
|
461
|
-
|
460
|
+
|
462
461
|
# Handle arguments that might be a string (JSON) or already a dict
|
463
462
|
if isinstance(arguments, str):
|
464
463
|
import json
|
464
|
+
|
465
465
|
try:
|
466
466
|
arguments = json.loads(arguments)
|
467
467
|
if self.logger:
|
@@ -470,10 +470,10 @@ class ToolCallHandler:
|
|
470
470
|
if self.logger:
|
471
471
|
self.logger.warning(f"Failed to parse JSON arguments: {e}")
|
472
472
|
arguments = {}
|
473
|
-
|
473
|
+
|
474
474
|
# Format tool signature with parameters
|
475
475
|
tool_signature = self._format_tool_signature(tool_name, arguments)
|
476
|
-
|
476
|
+
|
477
477
|
# Log function name with signature at INFO level
|
478
478
|
if self.logger:
|
479
479
|
self.logger.info(f"Executing tool: {tool_signature} (ID: {tool_call_id})")
|
@@ -486,7 +486,7 @@ class ToolCallHandler:
|
|
486
486
|
self.logger.debug(f"Arguments: {tool_call['function']['arguments']}")
|
487
487
|
|
488
488
|
# Map tool names to todo_manager methods
|
489
|
-
method_map = {
|
489
|
+
method_map: Dict[str, Callable[..., Any]] = {
|
490
490
|
"list_projects": self.todo_manager.list_projects,
|
491
491
|
"list_contexts": self.todo_manager.list_contexts,
|
492
492
|
"list_tasks": self.todo_manager.list_tasks,
|
@@ -515,50 +515,57 @@ class ToolCallHandler:
|
|
515
515
|
"output": f"ERROR: {error_msg}",
|
516
516
|
"error": True,
|
517
517
|
"error_type": "unknown_tool",
|
518
|
-
"error_details": error_msg
|
518
|
+
"error_details": error_msg,
|
519
519
|
}
|
520
520
|
|
521
521
|
method = method_map[tool_name]
|
522
|
-
|
522
|
+
|
523
523
|
# Log method call details
|
524
524
|
if self.logger:
|
525
525
|
self.logger.debug(f"Calling method: {tool_name}")
|
526
|
-
|
526
|
+
|
527
527
|
try:
|
528
528
|
result = method(**arguments)
|
529
|
-
|
529
|
+
|
530
530
|
# Log successful output at DEBUG level
|
531
531
|
if self.logger:
|
532
532
|
self.logger.debug(f"=== TOOL EXECUTION SUCCESS ===")
|
533
533
|
self.logger.debug(f"Tool: {tool_name}")
|
534
534
|
self.logger.debug(f"Raw result: ====\n{result}\n====")
|
535
|
-
|
535
|
+
|
536
536
|
# For list results, log the count
|
537
537
|
if isinstance(result, list):
|
538
538
|
self.logger.debug(f"Result count: {len(result)}")
|
539
539
|
# For string results, log the length
|
540
540
|
elif isinstance(result, str):
|
541
541
|
self.logger.debug(f"Result length: {len(result)}")
|
542
|
-
|
543
|
-
return {
|
544
|
-
|
542
|
+
|
543
|
+
return {
|
544
|
+
"tool_call_id": tool_call_id,
|
545
|
+
"name": tool_name,
|
546
|
+
"output": result,
|
547
|
+
"error": False,
|
548
|
+
}
|
549
|
+
|
545
550
|
except Exception as e:
|
546
551
|
# Log error details
|
547
552
|
if self.logger:
|
548
553
|
self.logger.error(f"=== TOOL EXECUTION FAILED ===")
|
549
554
|
self.logger.error(f"Tool: {tool_name}")
|
550
555
|
self.logger.error(f"Error type: {type(e).__name__}")
|
551
|
-
self.logger.error(f"Error message: {
|
556
|
+
self.logger.error(f"Error message: {e!s}")
|
552
557
|
self.logger.exception(f"Exception details for {tool_name}")
|
553
|
-
|
558
|
+
|
554
559
|
# Return structured error information instead of raising
|
555
560
|
error_type = type(e).__name__
|
556
561
|
error_message = str(e)
|
557
|
-
|
562
|
+
|
558
563
|
# Provide user-friendly error messages based on error type
|
559
564
|
if "FileNotFoundError" in error_type or "todo.sh" in error_message.lower():
|
560
565
|
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
|
566
|
+
elif "IndexError" in error_type or (
|
567
|
+
"task" in error_message.lower() and "not found" in error_message.lower()
|
568
|
+
):
|
562
569
|
user_message = f"Task not found: {error_message}. The task may have been completed or deleted."
|
563
570
|
elif "ValueError" in error_type:
|
564
571
|
user_message = f"Invalid input: {error_message}. Please check the task format or parameters."
|
@@ -566,7 +573,7 @@ class ToolCallHandler:
|
|
566
573
|
user_message = f"Permission denied: {error_message}. Please check file permissions for todo.txt files."
|
567
574
|
else:
|
568
575
|
user_message = f"Operation failed: {error_message}"
|
569
|
-
|
576
|
+
|
570
577
|
return {
|
571
578
|
"tool_call_id": tool_call_id,
|
572
579
|
"name": tool_name,
|
@@ -574,5 +581,5 @@ class ToolCallHandler:
|
|
574
581
|
"error": True,
|
575
582
|
"error_type": error_type,
|
576
583
|
"error_details": error_message,
|
577
|
-
"user_message": user_message
|
584
|
+
"user_message": user_message,
|
578
585
|
}
|