todo-agent 0.1.0__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.
@@ -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
  """
@@ -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 'thinking_time_count' in summary and summary['thinking_time_count'] > 0:
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(f" Total thinking time: {summary['total_thinking_time']:.2f}s")
130
- print(f" Average thinking time: {summary['average_thinking_time']:.2f}s")
131
- print(f" Min thinking time: {summary['min_thinking_time']:.2f}s")
132
- print(f" Max thinking time: {summary['max_thinking_time']:.2f}s")
133
- print(f" Requests with timing: {summary['thinking_time_count']}")
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: {str(e)}")
154
- print(f"Error: Failed to list tasks: {str(e)}")
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(f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}")
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: {str(e)}")
167
- print(f"Error: {str(e)}")
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(self._create_thinking_spinner(f"(thought for {thinking_time:.1f}s)"))
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: {str(e)}")
195
-
206
+ self.logger.error(f"Error in handle_request: {e!s}")
207
+
196
208
  # Return error message
197
- return f"Error: {str(e)}"
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(f"Running single request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}")
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)
@@ -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 {"tool_call_id": tool_call_id, "name": tool_name, "output": result, "error": False}
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: {str(e)}")
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 "task" in error_message.lower() and "not found" in error_message.lower():
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
  }
todo_agent/main.py CHANGED
@@ -9,7 +9,7 @@ import sys
9
9
  from .interface.cli import CLI
10
10
 
11
11
 
12
- def main():
12
+ def main() -> None:
13
13
  """Main application entry point."""
14
14
  parser = argparse.ArgumentParser(
15
15
  description="Todo.sh LLM Agent - Natural language task management",