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.
@@ -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
  """
@@ -5,6 +5,21 @@ This module contains user interfaces and presentation logic.
5
5
  """
6
6
 
7
7
  from .cli import CLI
8
+ from .formatters import (
9
+ PanelFormatter,
10
+ ResponseFormatter,
11
+ StatsFormatter,
12
+ TableFormatter,
13
+ TaskFormatter,
14
+ )
8
15
  from .tools import ToolCallHandler
9
16
 
10
- __all__ = ["CLI", "ToolCallHandler"]
17
+ __all__ = [
18
+ "CLI",
19
+ "PanelFormatter",
20
+ "ResponseFormatter",
21
+ "StatsFormatter",
22
+ "TableFormatter",
23
+ "TaskFormatter",
24
+ "ToolCallHandler",
25
+ ]
@@ -2,42 +2,60 @@
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:
6
+ import readline
7
+
8
+ from rich.align import Align
10
9
  from rich.console import Console
11
10
  from rich.live import Live
12
11
  from rich.spinner import Spinner
13
12
  from rich.text import Text
13
+
14
14
  from todo_agent.core.todo_manager import TodoManager
15
15
  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
16
  from todo_agent.infrastructure.inference import Inference
17
+ from todo_agent.infrastructure.logger import Logger
18
+ from todo_agent.infrastructure.todo_shell import TodoShell
19
+ from todo_agent.interface.formatters import (
20
+ CLI_WIDTH,
21
+ PanelFormatter,
22
+ ResponseFormatter,
23
+ TableFormatter,
24
+ TaskFormatter,
25
+ )
19
26
  from todo_agent.interface.tools import ToolCallHandler
20
27
  except ImportError:
28
+ from core.todo_manager import TodoManager # type: ignore[no-redef]
29
+ from infrastructure.config import Config # type: ignore[no-redef]
30
+ from infrastructure.inference import Inference # type: ignore[no-redef]
31
+ from infrastructure.logger import Logger # type: ignore[no-redef]
32
+ from infrastructure.todo_shell import TodoShell # type: ignore[no-redef]
33
+ from interface.formatters import ( # type: ignore[no-redef]
34
+ CLI_WIDTH,
35
+ PanelFormatter,
36
+ ResponseFormatter,
37
+ TableFormatter,
38
+ TaskFormatter,
39
+ )
40
+ from interface.tools import ToolCallHandler # type: ignore[no-redef]
41
+ from rich.align import Align
21
42
  from rich.console import Console
22
43
  from rich.live import Live
23
44
  from rich.spinner import Spinner
24
45
  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
46
 
32
47
 
33
48
  class CLI:
34
49
  """User interaction loop and input/output handling."""
35
50
 
36
- def __init__(self):
51
+ def __init__(self) -> None:
52
+ # Initialize readline for arrow key navigation
53
+ readline.set_history_length(50) # Match existing conversation cap
54
+
37
55
  # Initialize logger first
38
56
  self.logger = Logger("cli")
39
57
  self.logger.info("Initializing CLI")
40
-
58
+
41
59
  self.config = Config()
42
60
  self.config.validate()
43
61
  self.logger.debug("Configuration validated")
@@ -58,8 +76,8 @@ class CLI:
58
76
  self.inference = Inference(self.config, self.tool_handler, self.logger)
59
77
  self.logger.debug("Inference engine initialized")
60
78
 
61
- # Initialize rich console for animations
62
- self.console = Console()
79
+ # Initialize rich console for animations with consistent width
80
+ self.console = Console(width=CLI_WIDTH)
63
81
 
64
82
  self.logger.info("CLI initialization completed")
65
83
 
@@ -85,22 +103,51 @@ class CLI:
85
103
  initial_spinner = self._create_thinking_spinner("Thinking...")
86
104
  return Live(initial_spinner, console=self.console, refresh_per_second=10)
87
105
 
88
-
89
-
90
- def run(self):
106
+ def _print_header(self) -> None:
107
+ """Print the application header with unicode borders."""
108
+ header_panel = PanelFormatter.create_header_panel()
109
+ self.console.print(header_panel)
110
+
111
+ subtitle = Text(
112
+ "Type your request naturally, or enter 'quit' to exit, or 'help' for commands",
113
+ style="dim",
114
+ )
115
+ self.console.print(Align.center(subtitle), style="dim")
116
+
117
+ def _print_help(self) -> None:
118
+ """Print help information in a formatted table."""
119
+ table = TableFormatter.create_command_table()
120
+ self.console.print(table)
121
+ self.console.print("Or just type your request naturally!", style="italic green")
122
+
123
+ def _print_about(self) -> None:
124
+ """Print about information in a formatted panel."""
125
+ about_panel = PanelFormatter.create_about_panel()
126
+ self.console.print(about_panel)
127
+
128
+ def _print_stats(self, summary: dict) -> None:
129
+ """Print conversation statistics in a formatted table."""
130
+ table = TableFormatter.create_stats_table(summary)
131
+ self.console.print(table)
132
+
133
+ def run(self) -> None:
91
134
  """Main CLI interaction loop."""
92
135
  self.logger.info("Starting CLI interaction loop")
93
- print("Todo.sh LLM Agent - Type 'quit' to exit")
94
- print("Commands: 'clear' (clear conversation), 'history' (show stats), 'help'")
95
- print("=" * 50)
136
+
137
+ # Print header
138
+ self._print_header()
139
+
140
+ # Print separator
141
+ self.console.print("─" * CLI_WIDTH, style="dim")
96
142
 
97
143
  while True:
98
144
  try:
99
- user_input = input("\n> ").strip()
145
+ # Print prompt with unicode character
146
+ user_input = self.console.input("\n[bold cyan]▶[/bold cyan] ").strip()
100
147
 
101
148
  if user_input.lower() in ["quit", "exit", "q"]:
102
149
  self.logger.info("User requested exit")
103
- print("Goodbye!")
150
+ self.console.print("\n[bold green]Goodbye! 👋[/bold green]")
104
151
  break
105
152
 
106
153
  if not user_input:
@@ -110,61 +157,64 @@ class CLI:
110
157
  if user_input.lower() == "clear":
111
158
  self.logger.info("User requested conversation clear")
112
159
  self.inference.clear_conversation()
113
- print("Conversation history cleared.")
160
+ self.console.print(
161
+ ResponseFormatter.format_success(
162
+ "Conversation history cleared."
163
+ )
164
+ )
114
165
  continue
115
166
 
116
- if user_input.lower() == "history":
117
- self.logger.debug("User requested conversation history")
167
+ if user_input.lower() == "stats":
168
+ self.logger.debug("User requested conversation stats")
118
169
  summary = self.inference.get_conversation_summary()
119
- print(f"Conversation Stats:")
120
- print(f" Total messages: {summary['total_messages']}")
121
- print(f" User messages: {summary['user_messages']}")
122
- print(f" Assistant messages: {summary['assistant_messages']}")
123
- print(f" Tool messages: {summary['tool_messages']}")
124
- print(f" Estimated tokens: {summary['estimated_tokens']}")
125
-
126
- # Display thinking time statistics if available
127
- if 'thinking_time_count' in summary and summary['thinking_time_count'] > 0:
128
- 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']}")
170
+ self._print_stats(summary)
134
171
  continue
135
172
 
136
173
  if user_input.lower() == "help":
137
174
  self.logger.debug("User requested help")
138
- print("Available commands:")
139
- print(" clear - Clear conversation history")
140
- print(" history - Show conversation statistics")
141
- print(" help - Show this help message")
142
- print(" list - List all tasks (no LLM interaction)")
143
- print(" quit - Exit the application")
144
- print(" Or just type your request naturally!")
175
+ self._print_help()
176
+ continue
177
+
178
+ if user_input.lower() == "about":
179
+ self.logger.debug("User requested about information")
180
+ self._print_about()
145
181
  continue
146
182
 
147
183
  if user_input.lower() == "list":
148
184
  self.logger.debug("User requested task list")
149
185
  try:
150
186
  output = self.todo_shell.list_tasks()
151
- print(output)
187
+ formatted_output = TaskFormatter.format_task_list(output)
188
+ task_panel = PanelFormatter.create_task_panel(formatted_output)
189
+ self.console.print(task_panel)
152
190
  except Exception as e:
153
- self.logger.error(f"Error listing tasks: {str(e)}")
154
- print(f"Error: Failed to list tasks: {str(e)}")
191
+ self.logger.error(f"Error listing tasks: {e!s}")
192
+ error_msg = ResponseFormatter.format_error(
193
+ f"Failed to list tasks: {e!s}"
194
+ )
195
+ self.console.print(error_msg)
155
196
  continue
156
197
 
157
- self.logger.info(f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}")
198
+ self.logger.info(
199
+ f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
200
+ )
158
201
  response = self.handle_request(user_input)
159
- print(response)
202
+
203
+ # Format the response and create a panel
204
+ formatted_response = ResponseFormatter.format_response(response)
205
+ response_panel = PanelFormatter.create_response_panel(
206
+ formatted_response
207
+ )
208
+ self.console.print(response_panel)
160
209
 
161
210
  except KeyboardInterrupt:
162
211
  self.logger.info("User interrupted with Ctrl+C")
163
- print("\nGoodbye!")
212
+ self.console.print("\n[bold green]Goodbye! 👋[/bold green]")
164
213
  break
165
214
  except Exception as e:
166
- self.logger.error(f"Error in CLI loop: {str(e)}")
167
- print(f"Error: {str(e)}")
215
+ self.logger.error(f"Error in CLI loop: {e!s}")
216
+ error_msg = ResponseFormatter.format_error(str(e))
217
+ self.console.print(error_msg)
168
218
 
169
219
  def handle_request(self, user_input: str) -> str:
170
220
  """
@@ -181,20 +231,22 @@ class CLI:
181
231
  try:
182
232
  # Process request through inference engine
183
233
  response, thinking_time = self.inference.process_request(user_input)
184
-
234
+
185
235
  # Update spinner with completion message and thinking time
186
- live.update(self._create_thinking_spinner(f"(thought for {thinking_time:.1f}s)"))
187
-
236
+ live.update(
237
+ self._create_thinking_spinner(f"(thought for {thinking_time:.1f}s)")
238
+ )
239
+
188
240
  return response
189
241
  except Exception as e:
190
242
  # Update spinner with error message
191
243
  live.update(self._create_thinking_spinner("Request failed"))
192
-
244
+
193
245
  # Log the error
194
- self.logger.error(f"Error in handle_request: {str(e)}")
195
-
246
+ self.logger.error(f"Error in handle_request: {e!s}")
247
+
196
248
  # Return error message
197
- return f"Error: {str(e)}"
249
+ return ResponseFormatter.format_error(str(e))
198
250
 
199
251
  def run_single_request(self, user_input: str) -> str:
200
252
  """
@@ -206,5 +258,7 @@ class CLI:
206
258
  Returns:
207
259
  Formatted response
208
260
  """
209
- self.logger.info(f"Running single request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}")
261
+ self.logger.info(
262
+ f"Running single request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
263
+ )
210
264
  return self.handle_request(user_input)