todo-agent 0.1.1__py3-none-any.whl → 0.2.1__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/cli.py +51 -37
- todo_agent/interface/tools.py +47 -40
- todo_agent/main.py +1 -1
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.1.dist-info}/METADATA +72 -32
- todo_agent-0.2.1.dist-info/RECORD +27 -0
- todo_agent-0.1.1.dist-info/RECORD +0 -27
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.1.dist-info}/WHEEL +0 -0
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.1.dist-info}/entry_points.txt +0 -0
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {todo_agent-0.1.1.dist-info → todo_agent-0.2.1.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|
8
8
|
try:
|
9
9
|
import tiktoken
|
10
10
|
except ImportError:
|
11
|
-
tiktoken = None
|
11
|
+
tiktoken = None # type: ignore
|
12
12
|
|
13
13
|
|
14
14
|
class TokenCounter:
|
@@ -17,12 +17,12 @@ class TokenCounter:
|
|
17
17
|
def __init__(self, model: str = "gpt-4"):
|
18
18
|
"""
|
19
19
|
Initialize token counter for a specific model.
|
20
|
-
|
20
|
+
|
21
21
|
Args:
|
22
22
|
model: Model name to use for tokenization (default: gpt-4)
|
23
23
|
"""
|
24
24
|
self.model = model
|
25
|
-
self._encoder = None
|
25
|
+
self._encoder: Optional[Any] = None
|
26
26
|
self._initialize_encoder()
|
27
27
|
|
28
28
|
def _initialize_encoder(self) -> None:
|
@@ -32,152 +32,153 @@ class TokenCounter:
|
|
32
32
|
"tiktoken library is required for accurate token counting. "
|
33
33
|
"Install it with: pip install tiktoken"
|
34
34
|
)
|
35
|
-
|
36
35
|
|
37
36
|
self._encoder = tiktoken.get_encoding("cl100k_base")
|
38
|
-
|
37
|
+
|
39
38
|
def count_tokens(self, text: str) -> int:
|
40
39
|
"""
|
41
40
|
Count tokens in text using accurate tokenization.
|
42
|
-
|
41
|
+
|
43
42
|
Args:
|
44
43
|
text: Text to count tokens for
|
45
|
-
|
44
|
+
|
46
45
|
Returns:
|
47
46
|
Number of tokens
|
48
47
|
"""
|
49
48
|
if not text:
|
50
49
|
return 0
|
51
|
-
|
50
|
+
|
51
|
+
if self._encoder is None:
|
52
|
+
raise RuntimeError("Encoder not initialized")
|
52
53
|
return len(self._encoder.encode(text))
|
53
54
|
|
54
55
|
def count_message_tokens(self, message: Dict[str, Any]) -> int:
|
55
56
|
"""
|
56
57
|
Count tokens in a single message (including role, content, and tool calls).
|
57
|
-
|
58
|
+
|
58
59
|
Args:
|
59
60
|
message: Message dictionary with role, content, etc.
|
60
|
-
|
61
|
+
|
61
62
|
Returns:
|
62
63
|
Number of tokens
|
63
64
|
"""
|
64
65
|
tokens = 0
|
65
|
-
|
66
|
+
|
66
67
|
# Count role tokens (typically 1-2 tokens)
|
67
68
|
role = message.get("role", "")
|
68
69
|
tokens += self.count_tokens(role)
|
69
|
-
|
70
|
+
|
70
71
|
# Count content tokens
|
71
72
|
content = message.get("content", "")
|
72
73
|
if content:
|
73
74
|
tokens += self.count_tokens(content)
|
74
|
-
|
75
|
+
|
75
76
|
# Count tool calls tokens
|
76
77
|
tool_calls = message.get("tool_calls", [])
|
77
78
|
for tool_call in tool_calls:
|
78
79
|
tokens += self.count_tool_call_tokens(tool_call)
|
79
|
-
|
80
|
+
|
80
81
|
# Count tool call ID if present
|
81
82
|
tool_call_id = message.get("tool_call_id", "")
|
82
83
|
if tool_call_id:
|
83
84
|
tokens += self.count_tokens(tool_call_id)
|
84
|
-
|
85
|
+
|
85
86
|
return tokens
|
86
87
|
|
87
88
|
def count_tool_call_tokens(self, tool_call: Dict[str, Any]) -> int:
|
88
89
|
"""
|
89
90
|
Count tokens in a tool call.
|
90
|
-
|
91
|
+
|
91
92
|
Args:
|
92
93
|
tool_call: Tool call dictionary
|
93
|
-
|
94
|
+
|
94
95
|
Returns:
|
95
96
|
Number of tokens
|
96
97
|
"""
|
97
98
|
tokens = 0
|
98
|
-
|
99
|
+
|
99
100
|
# Count tool call ID
|
100
101
|
tool_call_id = tool_call.get("id", "")
|
101
102
|
tokens += self.count_tokens(tool_call_id)
|
102
|
-
|
103
|
+
|
103
104
|
# Count function call
|
104
105
|
function = tool_call.get("function", {})
|
105
106
|
if function:
|
106
107
|
# Count function name
|
107
108
|
function_name = function.get("name", "")
|
108
109
|
tokens += self.count_tokens(function_name)
|
109
|
-
|
110
|
+
|
110
111
|
# Count function arguments
|
111
112
|
arguments = function.get("arguments", "")
|
112
113
|
if arguments:
|
113
114
|
tokens += self.count_tokens(arguments)
|
114
|
-
|
115
|
+
|
115
116
|
return tokens
|
116
117
|
|
117
118
|
def count_messages_tokens(self, messages: List[Dict[str, Any]]) -> int:
|
118
119
|
"""
|
119
120
|
Count total tokens in a list of messages.
|
120
|
-
|
121
|
+
|
121
122
|
Args:
|
122
123
|
messages: List of message dictionaries
|
123
|
-
|
124
|
+
|
124
125
|
Returns:
|
125
126
|
Total number of tokens
|
126
127
|
"""
|
127
128
|
total_tokens = 0
|
128
|
-
|
129
|
+
|
129
130
|
for message in messages:
|
130
131
|
total_tokens += self.count_message_tokens(message)
|
131
|
-
|
132
|
+
|
132
133
|
return total_tokens
|
133
134
|
|
134
135
|
def count_tools_tokens(self, tools: List[Dict[str, Any]]) -> int:
|
135
136
|
"""
|
136
137
|
Count tokens in tool definitions.
|
137
|
-
|
138
|
+
|
138
139
|
Args:
|
139
140
|
tools: List of tool definition dictionaries
|
140
|
-
|
141
|
+
|
141
142
|
Returns:
|
142
143
|
Number of tokens
|
143
144
|
"""
|
144
145
|
if not tools:
|
145
146
|
return 0
|
146
|
-
|
147
|
+
|
147
148
|
# Convert tools to JSON string and count tokens
|
148
|
-
tools_json = json.dumps(tools, separators=(
|
149
|
+
tools_json = json.dumps(tools, separators=(",", ":"))
|
149
150
|
return self.count_tokens(tools_json)
|
150
151
|
|
151
152
|
def count_request_tokens(
|
152
|
-
self,
|
153
|
-
messages: List[Dict[str, Any]],
|
154
|
-
tools: Optional[List[Dict[str, Any]]] = None
|
153
|
+
self,
|
154
|
+
messages: List[Dict[str, Any]],
|
155
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
155
156
|
) -> int:
|
156
157
|
"""
|
157
158
|
Count total tokens in a complete request (messages + tools).
|
158
|
-
|
159
|
+
|
159
160
|
Args:
|
160
161
|
messages: List of message dictionaries
|
161
162
|
tools: Optional list of tool definitions
|
162
|
-
|
163
|
+
|
163
164
|
Returns:
|
164
165
|
Total number of tokens
|
165
166
|
"""
|
166
167
|
total_tokens = self.count_messages_tokens(messages)
|
167
|
-
|
168
|
+
|
168
169
|
if tools:
|
169
170
|
total_tokens += self.count_tools_tokens(tools)
|
170
|
-
|
171
|
+
|
171
172
|
return total_tokens
|
172
173
|
|
173
174
|
|
174
175
|
def get_token_counter(model: str = "gpt-4") -> TokenCounter:
|
175
176
|
"""
|
176
177
|
Get a token counter instance for the specified model.
|
177
|
-
|
178
|
+
|
178
179
|
Args:
|
179
180
|
model: Model name to use for tokenization
|
180
|
-
|
181
|
+
|
181
182
|
Returns:
|
182
183
|
TokenCounter instance
|
183
184
|
"""
|
todo_agent/interface/cli.py
CHANGED
@@ -2,42 +2,39 @@
|
|
2
2
|
Command-line interface for todo.sh LLM agent.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import threading
|
6
|
-
import time
|
7
|
-
from typing import Optional
|
8
|
-
|
9
5
|
try:
|
10
6
|
from rich.console import Console
|
11
7
|
from rich.live import Live
|
12
8
|
from rich.spinner import Spinner
|
13
9
|
from rich.text import Text
|
10
|
+
|
14
11
|
from todo_agent.core.todo_manager import TodoManager
|
15
12
|
from todo_agent.infrastructure.config import Config
|
16
|
-
from todo_agent.infrastructure.todo_shell import TodoShell
|
17
|
-
from todo_agent.infrastructure.logger import Logger
|
18
13
|
from todo_agent.infrastructure.inference import Inference
|
14
|
+
from todo_agent.infrastructure.logger import Logger
|
15
|
+
from todo_agent.infrastructure.todo_shell import TodoShell
|
19
16
|
from todo_agent.interface.tools import ToolCallHandler
|
20
17
|
except ImportError:
|
18
|
+
from core.todo_manager import TodoManager # type: ignore[no-redef]
|
19
|
+
from infrastructure.config import Config # type: ignore[no-redef]
|
20
|
+
from infrastructure.inference import Inference # type: ignore[no-redef]
|
21
|
+
from infrastructure.logger import Logger # type: ignore[no-redef]
|
22
|
+
from infrastructure.todo_shell import TodoShell # type: ignore[no-redef]
|
23
|
+
from interface.tools import ToolCallHandler # type: ignore[no-redef]
|
21
24
|
from rich.console import Console
|
22
25
|
from rich.live import Live
|
23
26
|
from rich.spinner import Spinner
|
24
27
|
from rich.text import Text
|
25
|
-
from core.todo_manager import TodoManager
|
26
|
-
from infrastructure.config import Config
|
27
|
-
from infrastructure.todo_shell import TodoShell
|
28
|
-
from infrastructure.logger import Logger
|
29
|
-
from infrastructure.inference import Inference
|
30
|
-
from interface.tools import ToolCallHandler
|
31
28
|
|
32
29
|
|
33
30
|
class CLI:
|
34
31
|
"""User interaction loop and input/output handling."""
|
35
32
|
|
36
|
-
def __init__(self):
|
33
|
+
def __init__(self) -> None:
|
37
34
|
# Initialize logger first
|
38
35
|
self.logger = Logger("cli")
|
39
36
|
self.logger.info("Initializing CLI")
|
40
|
-
|
37
|
+
|
41
38
|
self.config = Config()
|
42
39
|
self.config.validate()
|
43
40
|
self.logger.debug("Configuration validated")
|
@@ -85,9 +82,7 @@ class CLI:
|
|
85
82
|
initial_spinner = self._create_thinking_spinner("Thinking...")
|
86
83
|
return Live(initial_spinner, console=self.console, refresh_per_second=10)
|
87
84
|
|
88
|
-
|
89
|
-
|
90
|
-
def run(self):
|
85
|
+
def run(self) -> None:
|
91
86
|
"""Main CLI interaction loop."""
|
92
87
|
self.logger.info("Starting CLI interaction loop")
|
93
88
|
print("Todo.sh LLM Agent - Type 'quit' to exit")
|
@@ -122,15 +117,28 @@ class CLI:
|
|
122
117
|
print(f" Assistant messages: {summary['assistant_messages']}")
|
123
118
|
print(f" Tool messages: {summary['tool_messages']}")
|
124
119
|
print(f" Estimated tokens: {summary['estimated_tokens']}")
|
125
|
-
|
120
|
+
|
126
121
|
# Display thinking time statistics if available
|
127
|
-
if
|
122
|
+
if (
|
123
|
+
"thinking_time_count" in summary
|
124
|
+
and summary["thinking_time_count"] > 0
|
125
|
+
):
|
128
126
|
print(f" Thinking time stats:")
|
129
|
-
print(
|
130
|
-
|
131
|
-
|
132
|
-
print(
|
133
|
-
|
127
|
+
print(
|
128
|
+
f" Total thinking time: {summary['total_thinking_time']:.2f}s"
|
129
|
+
)
|
130
|
+
print(
|
131
|
+
f" Average thinking time: {summary['average_thinking_time']:.2f}s"
|
132
|
+
)
|
133
|
+
print(
|
134
|
+
f" Min thinking time: {summary['min_thinking_time']:.2f}s"
|
135
|
+
)
|
136
|
+
print(
|
137
|
+
f" Max thinking time: {summary['max_thinking_time']:.2f}s"
|
138
|
+
)
|
139
|
+
print(
|
140
|
+
f" Requests with timing: {summary['thinking_time_count']}"
|
141
|
+
)
|
134
142
|
continue
|
135
143
|
|
136
144
|
if user_input.lower() == "help":
|
@@ -150,11 +158,13 @@ class CLI:
|
|
150
158
|
output = self.todo_shell.list_tasks()
|
151
159
|
print(output)
|
152
160
|
except Exception as e:
|
153
|
-
self.logger.error(f"Error listing tasks: {
|
154
|
-
print(f"Error: Failed to list tasks: {
|
161
|
+
self.logger.error(f"Error listing tasks: {e!s}")
|
162
|
+
print(f"Error: Failed to list tasks: {e!s}")
|
155
163
|
continue
|
156
164
|
|
157
|
-
self.logger.info(
|
165
|
+
self.logger.info(
|
166
|
+
f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
|
167
|
+
)
|
158
168
|
response = self.handle_request(user_input)
|
159
169
|
print(response)
|
160
170
|
|
@@ -163,8 +173,8 @@ class CLI:
|
|
163
173
|
print("\nGoodbye!")
|
164
174
|
break
|
165
175
|
except Exception as e:
|
166
|
-
self.logger.error(f"Error in CLI loop: {
|
167
|
-
print(f"Error: {
|
176
|
+
self.logger.error(f"Error in CLI loop: {e!s}")
|
177
|
+
print(f"Error: {e!s}")
|
168
178
|
|
169
179
|
def handle_request(self, user_input: str) -> str:
|
170
180
|
"""
|
@@ -181,20 +191,22 @@ class CLI:
|
|
181
191
|
try:
|
182
192
|
# Process request through inference engine
|
183
193
|
response, thinking_time = self.inference.process_request(user_input)
|
184
|
-
|
194
|
+
|
185
195
|
# Update spinner with completion message and thinking time
|
186
|
-
live.update(
|
187
|
-
|
196
|
+
live.update(
|
197
|
+
self._create_thinking_spinner(f"(thought for {thinking_time:.1f}s)")
|
198
|
+
)
|
199
|
+
|
188
200
|
return response
|
189
201
|
except Exception as e:
|
190
202
|
# Update spinner with error message
|
191
203
|
live.update(self._create_thinking_spinner("Request failed"))
|
192
|
-
|
204
|
+
|
193
205
|
# Log the error
|
194
|
-
self.logger.error(f"Error in handle_request: {
|
195
|
-
|
206
|
+
self.logger.error(f"Error in handle_request: {e!s}")
|
207
|
+
|
196
208
|
# Return error message
|
197
|
-
return f"Error: {
|
209
|
+
return f"Error: {e!s}"
|
198
210
|
|
199
211
|
def run_single_request(self, user_input: str) -> str:
|
200
212
|
"""
|
@@ -206,5 +218,7 @@ class CLI:
|
|
206
218
|
Returns:
|
207
219
|
Formatted response
|
208
220
|
"""
|
209
|
-
self.logger.info(
|
221
|
+
self.logger.info(
|
222
|
+
f"Running single request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
|
223
|
+
)
|
210
224
|
return self.handle_request(user_input)
|
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
|
}
|