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.
@@ -7,15 +7,17 @@ from typing import Optional
7
7
  try:
8
8
  from todo_agent.infrastructure.config import Config
9
9
  from todo_agent.infrastructure.llm_client import LLMClient
10
- from todo_agent.infrastructure.openrouter_client import OpenRouterClient
11
- from todo_agent.infrastructure.ollama_client import OllamaClient
12
10
  from todo_agent.infrastructure.logger import Logger
11
+ from todo_agent.infrastructure.ollama_client import OllamaClient
12
+ from todo_agent.infrastructure.openrouter_client import OpenRouterClient
13
13
  except ImportError:
14
- from infrastructure.config import Config
15
- from infrastructure.llm_client import LLMClient
16
- from infrastructure.openrouter_client import OpenRouterClient
17
- from infrastructure.ollama_client import OllamaClient
18
- from infrastructure.logger import Logger
14
+ from infrastructure.config import Config # type: ignore[no-redef]
15
+ from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
16
+ from infrastructure.logger import Logger # type: ignore[no-redef]
17
+ from infrastructure.ollama_client import OllamaClient # type: ignore[no-redef]
18
+ from infrastructure.openrouter_client import ( # type: ignore[no-redef]
19
+ OpenRouterClient,
20
+ )
19
21
 
20
22
 
21
23
  class LLMClientFactory:
@@ -37,9 +39,11 @@ class LLMClientFactory:
37
39
  ValueError: If provider is not supported
38
40
  """
39
41
  logger = logger or Logger("llm_client_factory")
40
-
42
+
41
43
  if config.provider == "openrouter":
42
- logger.info(f"Creating OpenRouter client with model: {config.openrouter_model}")
44
+ logger.info(
45
+ f"Creating OpenRouter client with model: {config.openrouter_model}"
46
+ )
43
47
  return OpenRouterClient(config)
44
48
  elif config.provider == "ollama":
45
49
  logger.info(f"Creating Ollama client with model: {config.ollama_model}")
@@ -6,40 +6,39 @@ import logging
6
6
  import os
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
- from typing import Optional
10
9
 
11
10
 
12
11
  class Logger:
13
12
  """Custom logger that respects LOG_LEVEL environment variable and logs to screen and file."""
14
-
13
+
15
14
  def __init__(self, name: str = "todo_agent"):
16
15
  """
17
16
  Initialize the logger.
18
-
17
+
19
18
  Args:
20
19
  name: Logger name, defaults to "todo_agent"
21
20
  """
22
21
  self.name = name
23
22
  self.logger = logging.getLogger(name)
24
23
  self.logger.setLevel(logging.DEBUG)
25
-
24
+
26
25
  # Clear any existing handlers
27
26
  self.logger.handlers.clear()
28
-
27
+
29
28
  # Create logs directory if it doesn't exist
30
29
  self._ensure_logs_directory()
31
-
30
+
32
31
  # Set up file handler (always active)
33
32
  self._setup_file_handler()
34
-
33
+
35
34
  # Set up console handler with appropriate log level
36
35
  self._setup_console_handler()
37
-
38
- def _ensure_logs_directory(self):
36
+
37
+ def _ensure_logs_directory(self) -> None:
39
38
  """Ensure the logs directory exists in TODO_DIR."""
40
39
  logs_dir = self._get_logs_directory()
41
40
  logs_dir.mkdir(exist_ok=True)
42
-
41
+
43
42
  def _get_logs_directory(self) -> Path:
44
43
  """Get the logs directory path from TODO_DIR environment variable."""
45
44
  todo_dir = os.getenv("TODO_DIR")
@@ -48,81 +47,79 @@ class Logger:
48
47
  else:
49
48
  # Fallback to local logs directory if TODO_DIR is not set
50
49
  return Path("logs")
51
-
50
+
52
51
  def _get_log_level(self) -> int:
53
52
  """Get log level from LOG_LEVEL environment variable."""
54
53
  log_level_str = os.getenv("LOG_LEVEL", "INFO").upper()
55
-
54
+
56
55
  # Map string values to logging constants
57
56
  level_map = {
58
57
  "DEBUG": logging.DEBUG,
59
58
  "INFO": logging.INFO,
60
59
  "WARNING": logging.WARNING,
61
60
  "ERROR": logging.ERROR,
62
- "CRITICAL": logging.CRITICAL
61
+ "CRITICAL": logging.CRITICAL,
63
62
  }
64
-
63
+
65
64
  return level_map.get(log_level_str, logging.INFO)
66
-
65
+
67
66
  def _should_log_to_console(self) -> bool:
68
67
  """Check if we should log to console based on DEBUG environment variable."""
69
68
  return os.getenv("DEBUG") is not None
70
-
71
- def _setup_file_handler(self):
69
+
70
+ def _setup_file_handler(self) -> None:
72
71
  """Set up file handler for logging to file."""
73
72
  # Create log filename with timestamp
74
73
  timestamp = datetime.now().strftime("%Y%m%d")
75
74
  logs_dir = self._get_logs_directory()
76
75
  log_file = logs_dir / f"todo_agent_{timestamp}.log"
77
-
76
+
78
77
  file_handler = logging.FileHandler(log_file)
79
78
  file_handler.setLevel(logging.DEBUG)
80
-
79
+
81
80
  # Create formatter for file logging
82
81
  file_formatter = logging.Formatter(
83
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
82
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
84
83
  )
85
84
  file_handler.setFormatter(file_formatter)
86
-
85
+
87
86
  self.logger.addHandler(file_handler)
88
-
89
- def _setup_console_handler(self):
87
+
88
+ def _setup_console_handler(self) -> None:
90
89
  """Set up console handler for logging to screen with appropriate log level."""
91
90
  # Only add console handler if DEBUG environment variable is set
92
91
  if not self._should_log_to_console():
93
92
  return
94
-
93
+
95
94
  console_handler = logging.StreamHandler()
96
95
  console_handler.setLevel(self._get_log_level())
97
-
96
+
98
97
  # Create formatter for console logging (more concise)
99
- console_formatter = logging.Formatter(
100
- '%(levelname)s - %(message)s'
101
- )
98
+ console_formatter = logging.Formatter("%(levelname)s - %(message)s")
102
99
  console_handler.setFormatter(console_formatter)
103
-
100
+
104
101
  self.logger.addHandler(console_handler)
105
-
106
- def debug(self, message: str):
102
+
103
+ def debug(self, message: str) -> None:
107
104
  """Log a debug message."""
108
105
  self.logger.debug(message)
109
-
110
- def info(self, message: str):
106
+
107
+ def info(self, message: str) -> None:
111
108
  """Log an info message."""
112
109
  self.logger.info(message)
113
-
114
- def warning(self, message: str):
110
+
111
+ def warning(self, message: str) -> None:
115
112
  """Log a warning message."""
116
113
  self.logger.warning(message)
117
-
118
- def error(self, message: str):
114
+
115
+ def error(self, message: str) -> None:
119
116
  """Log an error message."""
120
117
  self.logger.error(message)
121
-
122
- def critical(self, message: str):
118
+
119
+ def critical(self, message: str) -> None:
123
120
  """Log a critical message."""
124
121
  self.logger.critical(message)
125
-
126
- def exception(self, message: str):
122
+
123
+ def exception(self, message: str) -> None:
127
124
  """Log an exception message with traceback."""
128
125
  self.logger.exception(message)
@@ -10,14 +10,14 @@ import requests
10
10
 
11
11
  try:
12
12
  from todo_agent.infrastructure.config import Config
13
+ from todo_agent.infrastructure.llm_client import LLMClient
13
14
  from todo_agent.infrastructure.logger import Logger
14
15
  from todo_agent.infrastructure.token_counter import get_token_counter
15
- from todo_agent.infrastructure.llm_client import LLMClient
16
16
  except ImportError:
17
- from infrastructure.config import Config
18
- from infrastructure.logger import Logger
19
- from infrastructure.token_counter import get_token_counter
20
- from infrastructure.llm_client import LLMClient
17
+ from infrastructure.config import Config # type: ignore[no-redef]
18
+ from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
19
+ from infrastructure.logger import Logger # type: ignore[no-redef]
20
+ from infrastructure.token_counter import get_token_counter # type: ignore[no-redef]
21
21
 
22
22
 
23
23
  class OllamaClient(LLMClient):
@@ -48,7 +48,7 @@ class OllamaClient(LLMClient):
48
48
  """
49
49
  return self.token_counter.count_tokens(text)
50
50
 
51
- def _log_request_details(self, payload: Dict[str, Any], start_time: float):
51
+ def _log_request_details(self, payload: Dict[str, Any], start_time: float) -> None:
52
52
  """Log request details including accurate token count."""
53
53
  # Count tokens for messages
54
54
  messages = payload.get("messages", [])
@@ -59,7 +59,9 @@ class OllamaClient(LLMClient):
59
59
  self.logger.info(f"Request sent - Token count: {total_tokens}")
60
60
  # self.logger.debug(f"Raw request payload: {json.dumps(payload, indent=2)}")
61
61
 
62
- def _log_response_details(self, response: Dict[str, Any], start_time: float):
62
+ def _log_response_details(
63
+ self, response: Dict[str, Any], start_time: float
64
+ ) -> None:
63
65
  """Log response details including latency."""
64
66
  end_time = time.time()
65
67
  latency_ms = (end_time - start_time) * 1000
@@ -72,10 +74,12 @@ class OllamaClient(LLMClient):
72
74
  self.logger.info(f"Response contains {len(tool_calls)} tool calls")
73
75
  for i, tool_call in enumerate(tool_calls):
74
76
  tool_name = tool_call.get("function", {}).get("name", "unknown")
75
- self.logger.info(f" Tool call {i+1}: {tool_name}")
77
+ self.logger.info(f" Tool call {i + 1}: {tool_name}")
76
78
  elif "message" in response and "content" in response["message"]:
77
79
  content = response["message"]["content"]
78
- self.logger.debug(f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}")
80
+ self.logger.debug(
81
+ f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}"
82
+ )
79
83
 
80
84
  self.logger.debug(f"Raw response: {json.dumps(response, indent=2)}")
81
85
 
@@ -106,7 +110,7 @@ class OllamaClient(LLMClient):
106
110
  start_time = time.time()
107
111
  self._log_request_details(payload, start_time)
108
112
 
109
- response = requests.post(
113
+ response = requests.post( # nosec B113
110
114
  f"{self.base_url}/api/chat", headers=headers, json=payload
111
115
  )
112
116
 
@@ -114,7 +118,7 @@ class OllamaClient(LLMClient):
114
118
  self.logger.error(f"Ollama API error: {response.text}")
115
119
  raise Exception(f"Ollama API error: {response.text}")
116
120
 
117
- response_data = response.json()
121
+ response_data: Dict[str, Any] = response.json()
118
122
  self._log_response_details(response_data, start_time)
119
123
 
120
124
  return response_data
@@ -122,7 +126,7 @@ class OllamaClient(LLMClient):
122
126
  def extract_tool_calls(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
123
127
  """Extract tool calls from API response."""
124
128
  tool_calls = []
125
-
129
+
126
130
  # Ollama response format is different from OpenRouter
127
131
  if "message" in response and "tool_calls" in response["message"]:
128
132
  tool_calls = response["message"]["tool_calls"]
@@ -130,16 +134,19 @@ class OllamaClient(LLMClient):
130
134
  for i, tool_call in enumerate(tool_calls):
131
135
  tool_name = tool_call.get("function", {}).get("name", "unknown")
132
136
  tool_call_id = tool_call.get("id", "unknown")
133
- self.logger.debug(f"Tool call {i+1}: {tool_name} (ID: {tool_call_id})")
137
+ self.logger.debug(
138
+ f"Tool call {i + 1}: {tool_name} (ID: {tool_call_id})"
139
+ )
134
140
  else:
135
141
  self.logger.debug("No tool calls found in response")
136
-
142
+
137
143
  return tool_calls
138
144
 
139
145
  def extract_content(self, response: Dict[str, Any]) -> str:
140
146
  """Extract content from API response."""
141
147
  if "message" in response and "content" in response["message"]:
142
- return response["message"]["content"]
148
+ content = response["message"]["content"]
149
+ return content if isinstance(content, str) else str(content)
143
150
  return ""
144
151
 
145
152
  def get_model_name(self) -> str:
@@ -10,14 +10,14 @@ import requests
10
10
 
11
11
  try:
12
12
  from todo_agent.infrastructure.config import Config
13
+ from todo_agent.infrastructure.llm_client import LLMClient
13
14
  from todo_agent.infrastructure.logger import Logger
14
15
  from todo_agent.infrastructure.token_counter import get_token_counter
15
- from todo_agent.infrastructure.llm_client import LLMClient
16
16
  except ImportError:
17
- from infrastructure.config import Config
18
- from infrastructure.logger import Logger
19
- from infrastructure.token_counter import get_token_counter
20
- from infrastructure.llm_client import LLMClient
17
+ from infrastructure.config import Config # type: ignore[no-redef]
18
+ from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
19
+ from infrastructure.logger import Logger # type: ignore[no-redef]
20
+ from infrastructure.token_counter import get_token_counter # type: ignore[no-redef]
21
21
 
22
22
 
23
23
  class OpenRouterClient(LLMClient):
@@ -34,53 +34,59 @@ class OpenRouterClient(LLMClient):
34
34
  def _estimate_tokens(self, text: str) -> int:
35
35
  """
36
36
  Estimate token count for text using accurate tokenization.
37
-
37
+
38
38
  Args:
39
39
  text: Text to count tokens for
40
-
40
+
41
41
  Returns:
42
42
  Number of tokens
43
43
  """
44
44
  return self.token_counter.count_tokens(text)
45
45
 
46
- def _log_request_details(self, payload: Dict[str, Any], start_time: float):
46
+ def _log_request_details(self, payload: Dict[str, Any], start_time: float) -> None:
47
47
  """Log request details including accurate token count."""
48
48
  # Count tokens for messages
49
49
  messages = payload.get("messages", [])
50
50
  tools = payload.get("tools", [])
51
-
51
+
52
52
  total_tokens = self.token_counter.count_request_tokens(messages, tools)
53
-
53
+
54
54
  self.logger.info(f"Request sent - Token count: {total_tokens}")
55
55
  # self.logger.debug(f"Raw request payload: {json.dumps(payload, indent=2)}")
56
56
 
57
- def _log_response_details(self, response: Dict[str, Any], start_time: float):
57
+ def _log_response_details(
58
+ self, response: Dict[str, Any], start_time: float
59
+ ) -> None:
58
60
  """Log response details including token count and latency."""
59
61
  end_time = time.time()
60
62
  latency_ms = (end_time - start_time) * 1000
61
-
63
+
62
64
  # Extract token usage from response if available
63
65
  usage = response.get("usage", {})
64
66
  prompt_tokens = usage.get("prompt_tokens", "unknown")
65
67
  completion_tokens = usage.get("completion_tokens", "unknown")
66
68
  total_tokens = usage.get("total_tokens", "unknown")
67
-
69
+
68
70
  self.logger.info(f"Response received - Latency: {latency_ms:.2f}ms")
69
- self.logger.info(f"Token usage - Prompt: {prompt_tokens}, Completion: {completion_tokens}, Total: {total_tokens}")
70
-
71
+ self.logger.info(
72
+ f"Token usage - Prompt: {prompt_tokens}, Completion: {completion_tokens}, Total: {total_tokens}"
73
+ )
74
+
71
75
  # Log tool call details if present
72
- if "choices" in response and response["choices"]:
76
+ if response.get("choices"):
73
77
  choice = response["choices"][0]
74
78
  if "message" in choice and "tool_calls" in choice["message"]:
75
79
  tool_calls = choice["message"]["tool_calls"]
76
80
  self.logger.info(f"Response contains {len(tool_calls)} tool calls")
77
81
  for i, tool_call in enumerate(tool_calls):
78
82
  tool_name = tool_call.get("function", {}).get("name", "unknown")
79
- self.logger.info(f" Tool call {i+1}: {tool_name}")
83
+ self.logger.info(f" Tool call {i + 1}: {tool_name}")
80
84
  elif "message" in choice and "content" in choice["message"]:
81
85
  content = choice["message"]["content"]
82
- self.logger.debug(f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}")
83
-
86
+ self.logger.debug(
87
+ f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}"
88
+ )
89
+
84
90
  self.logger.debug(f"Raw response: {json.dumps(response, indent=2)}")
85
91
 
86
92
  def chat_with_tools(
@@ -111,7 +117,7 @@ class OpenRouterClient(LLMClient):
111
117
  start_time = time.time()
112
118
  self._log_request_details(payload, start_time)
113
119
 
114
- response = requests.post(
120
+ response = requests.post( # nosec B113
115
121
  f"{self.base_url}/chat/completions", headers=headers, json=payload
116
122
  )
117
123
 
@@ -119,7 +125,7 @@ class OpenRouterClient(LLMClient):
119
125
  self.logger.error(f"OpenRouter API error: {response.text}")
120
126
  raise Exception(f"OpenRouter API error: {response.text}")
121
127
 
122
- response_data = response.json()
128
+ response_data: Dict[str, Any] = response.json()
123
129
  self._log_response_details(response_data, start_time)
124
130
 
125
131
  return response_data
@@ -140,15 +146,19 @@ class OpenRouterClient(LLMClient):
140
146
  def extract_tool_calls(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
141
147
  """Extract tool calls from API response."""
142
148
  tool_calls = []
143
- if "choices" in response and response["choices"]:
149
+ if response.get("choices"):
144
150
  choice = response["choices"][0]
145
151
  if "message" in choice and "tool_calls" in choice["message"]:
146
152
  tool_calls = choice["message"]["tool_calls"]
147
- self.logger.debug(f"Extracted {len(tool_calls)} tool calls from response")
153
+ self.logger.debug(
154
+ f"Extracted {len(tool_calls)} tool calls from response"
155
+ )
148
156
  for i, tool_call in enumerate(tool_calls):
149
157
  tool_name = tool_call.get("function", {}).get("name", "unknown")
150
158
  tool_call_id = tool_call.get("id", "unknown")
151
- self.logger.debug(f"Tool call {i+1}: {tool_name} (ID: {tool_call_id})")
159
+ self.logger.debug(
160
+ f"Tool call {i + 1}: {tool_name} (ID: {tool_call_id})"
161
+ )
152
162
  else:
153
163
  self.logger.debug("No tool calls found in response")
154
164
  else:
@@ -157,10 +167,11 @@ class OpenRouterClient(LLMClient):
157
167
 
158
168
  def extract_content(self, response: Dict[str, Any]) -> str:
159
169
  """Extract content from API response."""
160
- if "choices" in response and response["choices"]:
170
+ if response.get("choices"):
161
171
  choice = response["choices"][0]
162
172
  if "message" in choice and "content" in choice["message"]:
163
- return choice["message"]["content"]
173
+ content = choice["message"]["content"]
174
+ return content if isinstance(content, str) else str(content)
164
175
  return ""
165
176
 
166
177
  def get_model_name(self) -> str:
@@ -3,19 +3,19 @@ Subprocess wrapper for todo.sh operations.
3
3
  """
4
4
 
5
5
  import os
6
- import subprocess
7
- from typing import List, Optional
6
+ import subprocess # nosec B404
7
+ from typing import Any, List, Optional
8
8
 
9
9
  try:
10
10
  from todo_agent.core.exceptions import TodoShellError
11
11
  except ImportError:
12
- from core.exceptions import TodoShellError
12
+ from core.exceptions import TodoShellError # type: ignore[no-redef]
13
13
 
14
14
 
15
15
  class TodoShell:
16
16
  """Subprocess execution wrapper with error management."""
17
17
 
18
- def __init__(self, todo_file_path: str, logger=None):
18
+ def __init__(self, todo_file_path: str, logger: Optional[Any] = None) -> None:
19
19
  self.todo_file_path = todo_file_path
20
20
  self.todo_dir = os.path.dirname(todo_file_path) or os.getcwd()
21
21
  self.logger = logger
@@ -40,13 +40,13 @@ class TodoShell:
40
40
  self.logger.debug(f"=== RAW COMMAND EXECUTION ===")
41
41
  self.logger.debug(f"Raw command: {raw_command}")
42
42
  self.logger.debug(f"Working directory: {cwd or self.todo_dir}")
43
-
43
+
44
44
  try:
45
45
  working_dir = cwd or self.todo_dir
46
- result = subprocess.run(
46
+ result = subprocess.run( # nosec B603
47
47
  command, cwd=working_dir, capture_output=True, text=True, check=True
48
48
  )
49
-
49
+
50
50
  # Log the raw output
51
51
  if self.logger:
52
52
  self.logger.debug(f"=== RAW COMMAND OUTPUT ===")
@@ -54,7 +54,7 @@ class TodoShell:
54
54
  self.logger.debug(f"Raw stdout: {result.stdout}")
55
55
  self.logger.debug(f"Raw stderr: {result.stderr}")
56
56
  self.logger.debug(f"Return code: {result.returncode}")
57
-
57
+
58
58
  return result.stdout.strip()
59
59
  except subprocess.CalledProcessError as e:
60
60
  # Log error details
@@ -69,7 +69,7 @@ class TodoShell:
69
69
  if self.logger:
70
70
  self.logger.error(f"=== COMMAND EXECUTION EXCEPTION ===")
71
71
  self.logger.error(f"Raw command: {' '.join(command)}")
72
- self.logger.error(f"Exception: {str(e)}")
72
+ self.logger.error(f"Exception: {e!s}")
73
73
  raise TodoShellError(f"Todo.sh command failed: {e}")
74
74
 
75
75
  def add(self, description: str) -> str:
@@ -106,7 +106,9 @@ class TodoShell:
106
106
  command.append(term)
107
107
  return self.execute(command)
108
108
 
109
- def move(self, task_number: int, destination: str, source: Optional[str] = None) -> str:
109
+ def move(
110
+ self, task_number: int, destination: str, source: Optional[str] = None
111
+ ) -> str:
110
112
  """Move task from source to destination file."""
111
113
  command = ["todo.sh", "-f", "move", str(task_number), destination]
112
114
  if source: