todo-agent 0.2.4__py3-none-any.whl → 0.2.5__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/todo_manager.py +33 -0
- todo_agent/infrastructure/calendar_utils.py +64 -0
- todo_agent/infrastructure/inference.py +13 -2
- todo_agent/infrastructure/llm_client_factory.py +4 -2
- todo_agent/infrastructure/prompts/system_prompt.txt +57 -16
- todo_agent/interface/cli.py +28 -9
- todo_agent/interface/formatters.py +144 -48
- todo_agent/interface/tools.py +80 -8
- {todo_agent-0.2.4.dist-info → todo_agent-0.2.5.dist-info}/METADATA +1 -1
- {todo_agent-0.2.4.dist-info → todo_agent-0.2.5.dist-info}/RECORD +15 -14
- {todo_agent-0.2.4.dist-info → todo_agent-0.2.5.dist-info}/WHEEL +0 -0
- {todo_agent-0.2.4.dist-info → todo_agent-0.2.5.dist-info}/entry_points.txt +0 -0
- {todo_agent-0.2.4.dist-info → todo_agent-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {todo_agent-0.2.4.dist-info → todo_agent-0.2.5.dist-info}/top_level.txt +0 -0
todo_agent/_version.py
CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '0.2.
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
31
|
+
__version__ = version = '0.2.5'
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 5)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
todo_agent/core/todo_manager.py
CHANGED
@@ -21,6 +21,39 @@ class TodoManager:
|
|
21
21
|
due: Optional[str] = None,
|
22
22
|
) -> str:
|
23
23
|
"""Add new task with explicit project/context parameters."""
|
24
|
+
# Validate and sanitize inputs
|
25
|
+
if priority and not (
|
26
|
+
len(priority) == 1 and priority.isalpha() and priority.isupper()
|
27
|
+
):
|
28
|
+
raise ValueError(
|
29
|
+
f"Invalid priority '{priority}'. Must be a single uppercase letter (A-Z)."
|
30
|
+
)
|
31
|
+
|
32
|
+
if project:
|
33
|
+
# Remove any existing + symbols to prevent duplication
|
34
|
+
project = project.strip().lstrip("+")
|
35
|
+
if not project:
|
36
|
+
raise ValueError(
|
37
|
+
"Project name cannot be empty after removing + symbol."
|
38
|
+
)
|
39
|
+
|
40
|
+
if context:
|
41
|
+
# Remove any existing @ symbols to prevent duplication
|
42
|
+
context = context.strip().lstrip("@")
|
43
|
+
if not context:
|
44
|
+
raise ValueError(
|
45
|
+
"Context name cannot be empty after removing @ symbol."
|
46
|
+
)
|
47
|
+
|
48
|
+
if due:
|
49
|
+
# Basic date format validation
|
50
|
+
try:
|
51
|
+
datetime.strptime(due, "%Y-%m-%d")
|
52
|
+
except ValueError:
|
53
|
+
raise ValueError(
|
54
|
+
f"Invalid due date format '{due}'. Must be YYYY-MM-DD."
|
55
|
+
)
|
56
|
+
|
24
57
|
# Build the full task description with priority, project, and context
|
25
58
|
full_description = description
|
26
59
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Calendar utilities for generating calendar output in system prompts.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import calendar
|
6
|
+
import subprocess
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
|
9
|
+
|
10
|
+
def get_calendar_output() -> str:
|
11
|
+
"""
|
12
|
+
Generate calendar output for previous, current, and next month.
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
Formatted calendar string showing three months side by side
|
16
|
+
"""
|
17
|
+
try:
|
18
|
+
# Use cal -3 to get three months side by side
|
19
|
+
result = subprocess.run(
|
20
|
+
["cal", "-3"], capture_output=True, text=True, check=True
|
21
|
+
)
|
22
|
+
return result.stdout.strip()
|
23
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
24
|
+
# Fallback to Python calendar module
|
25
|
+
return _get_python_cal_output()
|
26
|
+
|
27
|
+
|
28
|
+
def _get_python_cal_output() -> str:
|
29
|
+
"""
|
30
|
+
Generate calendar output using Python calendar module as fallback.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Calendar output formatted similar to cal command
|
34
|
+
"""
|
35
|
+
current_date = datetime.now()
|
36
|
+
|
37
|
+
# Calculate previous, current, and next month
|
38
|
+
prev_month = current_date - timedelta(days=current_date.day)
|
39
|
+
next_month = current_date.replace(day=1) + timedelta(days=32)
|
40
|
+
next_month = next_month.replace(day=1)
|
41
|
+
|
42
|
+
calendars = []
|
43
|
+
|
44
|
+
for date in [prev_month, current_date, next_month]:
|
45
|
+
cal = calendar.month(date.year, date.month)
|
46
|
+
calendars.append(cal.strip())
|
47
|
+
|
48
|
+
return "\n\n".join(calendars)
|
49
|
+
|
50
|
+
|
51
|
+
def get_current_month_calendar() -> str:
|
52
|
+
"""
|
53
|
+
Get calendar for current month only.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Calendar output for current month
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
result = subprocess.run(["cal"], capture_output=True, text=True, check=True)
|
60
|
+
return result.stdout.strip()
|
61
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
62
|
+
# Fallback to Python calendar
|
63
|
+
current_date = datetime.now()
|
64
|
+
return calendar.month(current_date.year, current_date.month).strip()
|
@@ -75,6 +75,15 @@ class Inference:
|
|
75
75
|
|
76
76
|
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
77
77
|
|
78
|
+
# Get calendar output
|
79
|
+
from .calendar_utils import get_calendar_output
|
80
|
+
|
81
|
+
try:
|
82
|
+
calendar_output = get_calendar_output()
|
83
|
+
except Exception as e:
|
84
|
+
self.logger.warning(f"Failed to get calendar output: {e!s}")
|
85
|
+
calendar_output = "Calendar unavailable"
|
86
|
+
|
78
87
|
# Load system prompt from file
|
79
88
|
prompt_file_path = os.path.join(
|
80
89
|
os.path.dirname(__file__), "prompts", "system_prompt.txt"
|
@@ -84,9 +93,11 @@ class Inference:
|
|
84
93
|
with open(prompt_file_path, encoding="utf-8") as f:
|
85
94
|
system_prompt_template = f.read()
|
86
95
|
|
87
|
-
# Format the template with the tools section
|
96
|
+
# Format the template with the tools section, current datetime, and calendar
|
88
97
|
return system_prompt_template.format(
|
89
|
-
tools_section=tools_section,
|
98
|
+
tools_section=tools_section,
|
99
|
+
current_datetime=current_datetime,
|
100
|
+
calendar_output=calendar_output,
|
90
101
|
)
|
91
102
|
|
92
103
|
except FileNotFoundError:
|
@@ -2,6 +2,8 @@
|
|
2
2
|
Factory for creating LLM clients based on configuration.
|
3
3
|
"""
|
4
4
|
|
5
|
+
# mypy: disable-error-code="no-redef"
|
6
|
+
|
5
7
|
from typing import Optional
|
6
8
|
|
7
9
|
try:
|
@@ -15,8 +17,8 @@ except ImportError:
|
|
15
17
|
from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
|
16
18
|
from infrastructure.logger import Logger # type: ignore[no-redef]
|
17
19
|
from infrastructure.ollama_client import OllamaClient # type: ignore[no-redef]
|
18
|
-
from infrastructure.openrouter_client import (
|
19
|
-
OpenRouterClient,
|
20
|
+
from infrastructure.openrouter_client import (
|
21
|
+
OpenRouterClient, # type: ignore[no-redef, misc]
|
20
22
|
)
|
21
23
|
|
22
24
|
|
@@ -1,51 +1,92 @@
|
|
1
1
|
You are a todo.sh assistant managing tasks in standard todo.txt format.
|
2
2
|
|
3
3
|
CURRENT DATE/TIME: {current_datetime}
|
4
|
+
{calendar_output}
|
4
5
|
|
5
6
|
CORE PRINCIPLES:
|
6
|
-
1.
|
7
|
-
2.
|
8
|
-
3.
|
9
|
-
4.
|
10
|
-
5.
|
7
|
+
1. Strategic Tool Usage: Batch discovery tools ([list_tasks, list_completed_tasks, list_projects, list_contexts]) to minimize API calls
|
8
|
+
2. Conversational: Respond naturally without mentioning tools or technical details
|
9
|
+
3. Data Integrity: Only reference tasks/projects/contexts returned by actual tool calls - NEVER hallucinate
|
10
|
+
4. Safety: Always verify current state before modifications using list_tasks() and list_completed_tasks()
|
11
|
+
5. Todo.txt Compliance: Use standard format and ordering
|
11
12
|
|
12
13
|
TODO.TXT FORMAT:
|
13
14
|
- Priority: (A), (B), (C) • Completion: "x YYYY-MM-DD" • Creation: YYYY-MM-DD
|
14
|
-
- Projects: +project • Contexts: @context • Due dates: due:YYYY-MM-DD
|
15
|
+
- Projects: +project (single + symbol) • Contexts: @context (single @ symbol) • Due dates: due:YYYY-MM-DD
|
15
16
|
- Example: "(A) 2024-01-15 Call dentist +health @phone due:2024-01-20"
|
17
|
+
- CRITICAL: Never use double symbols like ++project or @@context - always use single + and @ symbols
|
16
18
|
|
17
19
|
WORKFLOW:
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
Discovery First: Gather context with batched tool calls before any action
|
21
|
+
Verify Before Action: Check for duplicates, conflicts, or existing completions
|
22
|
+
Sequential Processing: Tools execute in order within batches
|
21
23
|
|
22
|
-
|
24
|
+
TASK COMPLETION:
|
25
|
+
When users say something like "I finished X" or "I'm done with Y", search for matching tasks
|
26
|
+
using list_tasks() and handle ambiguity by showing numbered options. Always verify task
|
27
|
+
hasn't already been completed with list_completed_tasks().
|
28
|
+
|
29
|
+
COMPLETION INTELLIGENCE:
|
30
|
+
- If user's statement clearly matches exactly one task (e.g., "I finished mowing the lawn"
|
31
|
+
when there's only one task with "yard work" in the description), complete it immediately
|
32
|
+
- If user's statement matches multiple tasks, show numbered options and ask for clarification
|
33
|
+
- If user's statement is ambiguous or could match many tasks, ask for clarification
|
34
|
+
- When in doubt about ambiguity, ask for more information to clarify intent before taking any action
|
35
|
+
|
36
|
+
CONTEXT AND PROJECT INFERENCE:
|
23
37
|
- Extract temporal urgency from due dates and creation dates
|
24
38
|
- Identify task relationships through shared projects/contexts
|
25
39
|
- Determine scope boundaries from natural language (work vs personal tasks)
|
26
40
|
- Recognize priority patterns and dependencies
|
27
41
|
|
42
|
+
TASK CREATION INTELLIGENCE:
|
43
|
+
- When users request to add a task, automatically infer appropriate projects, contexts, and due dates based on the task content
|
44
|
+
- When intent is clear, create the task immediately without asking for confirmation
|
45
|
+
- Only ask for clarification when project/context/due date is genuinely ambiguous
|
46
|
+
- Use priority C for new tasks unless urgency is indicated
|
47
|
+
- DUE DATE INFERENCE: Automatically infer due dates using multiple intelligence sources:
|
48
|
+
* Explicit expressions: "tomorrow", "next week", "next Monday", "by Friday" → Convert to YYYY-MM-DD format
|
49
|
+
* Relative expressions: "in 3 days", "next month", "end of month" → Calculate appropriate date
|
50
|
+
* Urgency indicators: "urgent", "asap", "today" → Set to today's date
|
51
|
+
* Vague expressions: "sometime this week" → Set to end of current week
|
52
|
+
* Task nature inference: Use common sense based on task type and existing patterns:
|
53
|
+
- Work tasks → Consider work week patterns and existing work task due dates
|
54
|
+
- Personal tasks → Consider weekend availability and personal schedule patterns
|
55
|
+
- Health/medical → Consider urgency and typical scheduling patterns
|
56
|
+
- Shopping/errands → Consider when items are needed and store hours
|
57
|
+
- Bills/payments → Consider due dates and late fees
|
58
|
+
- Maintenance tasks → Consider frequency patterns and current state
|
59
|
+
* Calendar context: Use current date/time and calendar output to inform timing decisions
|
60
|
+
* Existing task patterns: Look at similar tasks and their due dates for consistency
|
61
|
+
* Always infer: Every task should have a reasonable due date based on available context
|
62
|
+
|
28
63
|
TASK ADVICE:
|
29
64
|
Think deeply and critically to categorize tasks and suggest actions:
|
30
65
|
- Consider real-life implications and importance to my responsibilities regardless of explicit priority
|
31
66
|
- When users request prioritization help, use Eisenhower Matrix:
|
32
67
|
Q1 (Urgent+Important: DO), Q2 (Important: SCHEDULE), Q3 (Urgent: DELEGATE), Q4 (Neither: ELIMINATE) [assign SPARINGLY].
|
33
68
|
|
34
|
-
COMPLETED TASKS:
|
35
|
-
When users mention past accomplishments ("I did XXX today"):
|
36
|
-
1. add_task() with description
|
37
|
-
2. complete_task() with same ID using "x YYYY-MM-DD" format
|
38
|
-
|
39
69
|
ERROR HANDLING:
|
40
70
|
- Empty results: Suggest next steps
|
41
71
|
- Ambiguous requests: Show numbered options
|
42
72
|
- Large lists: Use filtering/summaries for 10+ items
|
43
73
|
- Failed operations: Explain clearly with alternatives
|
44
74
|
|
75
|
+
OUTPUT FORMATTING:
|
76
|
+
- Calendar Display: Show calendar output as plain text without backticks, code blocks, or markdown formatting
|
77
|
+
- Task Lists: Present tasks in conversational language, not raw todo.txt format
|
78
|
+
- Natural Language: Use conversational responses that feel natural and helpful
|
79
|
+
- No Technical Details: Avoid mentioning tools, API calls, or technical implementation details
|
80
|
+
|
45
81
|
CRITICAL RULES:
|
46
|
-
-
|
82
|
+
- Anti-hallucination: If no tool data exists, say "I need to check your tasks first"
|
47
83
|
- Use appropriate discovery tools extensively
|
48
84
|
- Never assume task existence without verification
|
49
85
|
- Maintain todo.txt standard compliance
|
86
|
+
- Format Compliance: Always use single + for projects and single @ for contexts (never ++ or @@)
|
87
|
+
- Display Formatting: When showing calendar output, display it as plain text without backticks or code blocks
|
88
|
+
- Proactive Task Creation: When users request to add a task, create it immediately with inferred tags and due dates unless genuinely ambiguous
|
89
|
+
- No Unnecessary Confirmation: Don't ask for confirmation when the task intent is clear and appropriate tags/due dates can be inferred
|
90
|
+
- Due Date Intelligence: Always infer reasonable due dates using task nature, calendar context, existing patterns, and common sense. Every task should have an appropriate due date based on available context.
|
50
91
|
|
51
92
|
AVAILABLE TOOLS: {tools_section}
|
todo_agent/interface/cli.py
CHANGED
@@ -78,8 +78,8 @@ class CLI:
|
|
78
78
|
self.inference = Inference(self.config, self.tool_handler, self.logger)
|
79
79
|
self.logger.debug("Inference engine initialized")
|
80
80
|
|
81
|
-
# Initialize rich console for animations with consistent width
|
82
|
-
self.console = Console(width=CLI_WIDTH)
|
81
|
+
# Initialize rich console for animations with consistent width and color support
|
82
|
+
self.console = Console(width=CLI_WIDTH, color_system="auto")
|
83
83
|
|
84
84
|
self.logger.info("CLI initialization completed")
|
85
85
|
|
@@ -136,21 +136,21 @@ class CLI:
|
|
136
136
|
"""Get session memory usage as a progress bar."""
|
137
137
|
# Get conversation manager to access memory limits and current usage
|
138
138
|
conversation_manager = self.inference.get_conversation_manager()
|
139
|
-
|
139
|
+
|
140
140
|
# Get current usage from conversation summary
|
141
141
|
summary = conversation_manager.get_conversation_summary()
|
142
142
|
current_tokens = summary.get("estimated_tokens", 0)
|
143
143
|
current_messages = summary.get("total_messages", 0)
|
144
|
-
|
144
|
+
|
145
145
|
# Get limits from conversation manager
|
146
146
|
max_tokens = conversation_manager.max_tokens
|
147
147
|
max_messages = conversation_manager.max_messages
|
148
|
-
|
148
|
+
|
149
149
|
# Create memory usage bar
|
150
150
|
memory_bar = PanelFormatter.create_memory_usage_bar(
|
151
151
|
current_tokens, max_tokens, current_messages, max_messages
|
152
152
|
)
|
153
|
-
|
153
|
+
|
154
154
|
return memory_bar
|
155
155
|
|
156
156
|
def run(self) -> None:
|
@@ -208,7 +208,9 @@ class CLI:
|
|
208
208
|
try:
|
209
209
|
output = self.todo_shell.list_tasks()
|
210
210
|
formatted_output = TaskFormatter.format_task_list(output)
|
211
|
-
task_panel = PanelFormatter.create_task_panel(
|
211
|
+
task_panel = PanelFormatter.create_task_panel(
|
212
|
+
formatted_output
|
213
|
+
)
|
212
214
|
self.console.print(task_panel)
|
213
215
|
except Exception as e:
|
214
216
|
self.logger.error(f"Error listing tasks: {e!s}")
|
@@ -218,6 +220,23 @@ class CLI:
|
|
218
220
|
self.console.print(error_msg)
|
219
221
|
continue
|
220
222
|
|
223
|
+
if user_input.lower() == "done":
|
224
|
+
self.logger.debug("User requested completed task list")
|
225
|
+
try:
|
226
|
+
output = self.todo_shell.list_completed()
|
227
|
+
formatted_output = TaskFormatter.format_completed_tasks(output)
|
228
|
+
task_panel = PanelFormatter.create_task_panel(
|
229
|
+
formatted_output, title="✅ Completed Tasks"
|
230
|
+
)
|
231
|
+
self.console.print(task_panel)
|
232
|
+
except Exception as e:
|
233
|
+
self.logger.error(f"Error listing completed tasks: {e!s}")
|
234
|
+
error_msg = ResponseFormatter.format_error(
|
235
|
+
f"Failed to list completed tasks: {e!s}"
|
236
|
+
)
|
237
|
+
self.console.print(error_msg)
|
238
|
+
continue
|
239
|
+
|
221
240
|
self.logger.info(
|
222
241
|
f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
|
223
242
|
)
|
@@ -225,10 +244,10 @@ class CLI:
|
|
225
244
|
|
226
245
|
# Format the response and create a panel
|
227
246
|
formatted_response = ResponseFormatter.format_response(response)
|
228
|
-
|
247
|
+
|
229
248
|
# Get memory usage
|
230
249
|
memory_usage = self._get_memory_usage()
|
231
|
-
|
250
|
+
|
232
251
|
# Create response panel with memory usage
|
233
252
|
response_panel = PanelFormatter.create_response_panel(
|
234
253
|
formatted_response, memory_usage=memory_usage
|
@@ -7,7 +7,6 @@ from typing import Any, Dict, Optional
|
|
7
7
|
from rich.align import Align
|
8
8
|
from rich.box import ROUNDED
|
9
9
|
from rich.panel import Panel
|
10
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
11
10
|
from rich.table import Table
|
12
11
|
from rich.text import Text
|
13
12
|
|
@@ -22,14 +21,13 @@ class TaskFormatter:
|
|
22
21
|
@staticmethod
|
23
22
|
def format_task_list(raw_tasks: str) -> Text:
|
24
23
|
"""
|
25
|
-
Format a raw task list
|
24
|
+
Format a raw task list while preserving ANSI color codes from todo.sh.
|
26
25
|
|
27
26
|
Args:
|
28
|
-
raw_tasks: Raw task output from todo.sh
|
29
|
-
title: Title for the task list
|
27
|
+
raw_tasks: Raw task output from todo.sh with ANSI codes
|
30
28
|
|
31
29
|
Returns:
|
32
|
-
Formatted task list as Rich Text object
|
30
|
+
Formatted task list as Rich Text object with preserved ANSI codes
|
33
31
|
"""
|
34
32
|
if not raw_tasks.strip():
|
35
33
|
return Text("No tasks found.")
|
@@ -38,19 +36,15 @@ class TaskFormatter:
|
|
38
36
|
formatted_text = Text()
|
39
37
|
task_count = 0
|
40
38
|
|
41
|
-
|
42
|
-
formatted_text.append("Tasks", style="bold blue")
|
43
|
-
formatted_text.append("\n\n")
|
39
|
+
|
44
40
|
|
45
41
|
for line in lines:
|
46
42
|
line = line.strip()
|
47
43
|
# Skip empty lines, separators, and todo.sh's own summary line
|
48
44
|
if line and line != "--" and not line.startswith("TODO:"):
|
49
45
|
task_count += 1
|
50
|
-
#
|
51
|
-
|
52
|
-
# Create a Text object that respects ANSI codes
|
53
|
-
task_text = Text.from_ansi(formatted_task)
|
46
|
+
# Preserve the original ANSI codes by using Text.from_ansi directly
|
47
|
+
task_text = Text.from_ansi(line)
|
54
48
|
formatted_text.append(task_text)
|
55
49
|
formatted_text.append("\n")
|
56
50
|
|
@@ -63,6 +57,48 @@ class TaskFormatter:
|
|
63
57
|
|
64
58
|
return formatted_text
|
65
59
|
|
60
|
+
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
def format_completed_tasks(raw_tasks: str) -> Text:
|
64
|
+
"""
|
65
|
+
Format a raw completed task list while preserving ANSI color codes from todo.sh.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
raw_tasks: Raw completed task output from todo.sh with ANSI codes
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
Formatted completed task list as Rich Text object with preserved ANSI codes
|
72
|
+
"""
|
73
|
+
if not raw_tasks.strip():
|
74
|
+
return Text("No completed tasks found.")
|
75
|
+
|
76
|
+
lines = raw_tasks.strip().split("\n")
|
77
|
+
formatted_text = Text()
|
78
|
+
task_count = 0
|
79
|
+
|
80
|
+
|
81
|
+
|
82
|
+
for line in lines:
|
83
|
+
line = line.strip()
|
84
|
+
# Skip empty lines, separators, and todo.sh's own summary line
|
85
|
+
if line and line != "--" and not line.startswith("TODO:"):
|
86
|
+
task_count += 1
|
87
|
+
# Preserve the original ANSI codes by using Text.from_ansi directly
|
88
|
+
task_text = Text.from_ansi(line)
|
89
|
+
formatted_text.append(task_text)
|
90
|
+
formatted_text.append("\n")
|
91
|
+
|
92
|
+
# Add task count at the end
|
93
|
+
if task_count > 0:
|
94
|
+
formatted_text.append("\n")
|
95
|
+
else:
|
96
|
+
formatted_text = Text("No completed tasks found.")
|
97
|
+
|
98
|
+
return formatted_text
|
99
|
+
|
100
|
+
|
101
|
+
|
66
102
|
@staticmethod
|
67
103
|
def _format_single_task(task_line: str, task_number: int) -> str:
|
68
104
|
"""
|
@@ -101,6 +137,33 @@ class TaskFormatter:
|
|
101
137
|
|
102
138
|
return formatted_line
|
103
139
|
|
140
|
+
@staticmethod
|
141
|
+
def _format_single_completed_task(task_line: str, task_number: int) -> str:
|
142
|
+
"""
|
143
|
+
Format a single completed task line with unicode characters.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
task_line: Raw completed task line from todo.sh
|
147
|
+
task_number: Task number for display
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Formatted completed task string
|
151
|
+
"""
|
152
|
+
# Parse completed task format: "x 2025-08-29 2025-08-28 Clean cat box @home +chores"
|
153
|
+
# The format is: "x completion_date creation_date description"
|
154
|
+
parts = task_line.split(
|
155
|
+
" ", 2
|
156
|
+
) # Split on first two spaces to separate x, dates, and description
|
157
|
+
if len(parts) < 3:
|
158
|
+
return f" {task_number:2d} │ │ {task_line}"
|
159
|
+
|
160
|
+
description = parts[2]
|
161
|
+
|
162
|
+
# Format with unicode characters
|
163
|
+
formatted_line = f" {task_number:2d} │ {description}"
|
164
|
+
|
165
|
+
return formatted_line
|
166
|
+
|
104
167
|
@staticmethod
|
105
168
|
def format_projects(raw_projects: str) -> str:
|
106
169
|
"""
|
@@ -279,6 +342,7 @@ class TableFormatter:
|
|
279
342
|
("help", "Show this help message"),
|
280
343
|
("about", "Show application information"),
|
281
344
|
("list", "List all tasks (no LLM interaction)"),
|
345
|
+
("done", "List completed tasks (no LLM interaction)"),
|
282
346
|
("quit", "Exit the application"),
|
283
347
|
]
|
284
348
|
|
@@ -340,28 +404,32 @@ class PanelFormatter:
|
|
340
404
|
)
|
341
405
|
|
342
406
|
@staticmethod
|
343
|
-
def create_task_panel(content: str, title: str = "📋 Current Tasks") -> Panel:
|
407
|
+
def create_task_panel(content: str | Text, title: str = "📋 Current Tasks") -> Panel:
|
344
408
|
"""Create a panel for displaying task lists."""
|
345
409
|
return Panel(
|
346
410
|
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
347
411
|
)
|
348
412
|
|
349
413
|
@staticmethod
|
350
|
-
def create_response_panel(
|
414
|
+
def create_response_panel(
|
415
|
+
content: str, title: str = "🤖 Assistant", memory_usage: Optional[Text] = None
|
416
|
+
) -> Panel:
|
351
417
|
"""Create a panel for displaying LLM responses."""
|
352
418
|
if memory_usage:
|
353
419
|
# Create the combined content with centered memory usage
|
420
|
+
combined_content = Text()
|
421
|
+
combined_content.append(content)
|
422
|
+
combined_content.append("\n\n")
|
423
|
+
combined_content.append("─" * (PANEL_WIDTH - 4)) # Separator line
|
424
|
+
combined_content.append("\n")
|
425
|
+
combined_content.append(memory_usage)
|
426
|
+
|
354
427
|
return Panel(
|
355
|
-
Align.center(
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
"\n",
|
361
|
-
memory_usage
|
362
|
-
)
|
363
|
-
),
|
364
|
-
title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
428
|
+
Align.center(combined_content),
|
429
|
+
title=title,
|
430
|
+
border_style="dim",
|
431
|
+
box=ROUNDED,
|
432
|
+
width=PANEL_WIDTH,
|
365
433
|
)
|
366
434
|
else:
|
367
435
|
return Panel(
|
@@ -415,43 +483,71 @@ class PanelFormatter:
|
|
415
483
|
)
|
416
484
|
|
417
485
|
@staticmethod
|
418
|
-
def create_memory_usage_bar(
|
486
|
+
def create_memory_usage_bar(
|
487
|
+
current_tokens: int, max_tokens: int, current_messages: int, max_messages: int
|
488
|
+
) -> Text:
|
419
489
|
"""
|
420
490
|
Create a rich progress bar showing session memory usage.
|
421
|
-
|
491
|
+
|
422
492
|
Args:
|
423
493
|
current_tokens: Current number of tokens in conversation
|
424
494
|
max_tokens: Maximum allowed tokens
|
425
495
|
current_messages: Current number of messages in conversation
|
426
496
|
max_messages: Maximum allowed messages
|
427
|
-
|
497
|
+
|
428
498
|
Returns:
|
429
499
|
Rich Text object with memory usage progress bar
|
430
500
|
"""
|
431
501
|
# Calculate percentage
|
432
502
|
token_percentage = min(100, (current_tokens / max_tokens) * 100)
|
433
|
-
|
434
|
-
#
|
435
|
-
if token_percentage >= 90:
|
436
|
-
color = "red"
|
437
|
-
elif token_percentage >= 75:
|
438
|
-
color = "yellow"
|
439
|
-
else:
|
440
|
-
color = "green"
|
441
|
-
|
442
|
-
# Create the progress bar text
|
503
|
+
|
504
|
+
# Create the progress bar text with new layout
|
443
505
|
memory_text = Text()
|
444
|
-
|
445
|
-
|
446
|
-
#
|
447
|
-
|
506
|
+
|
507
|
+
# Calculate available width (PANEL_WIDTH - 4 for borders = 94 chars)
|
508
|
+
# Account for emoji width by using a slightly reduced width
|
509
|
+
available_width = 92 # Balance between full width and emoji spacing
|
510
|
+
|
511
|
+
# Left section: Floppy disk + token count
|
512
|
+
left_section = f"💾 {current_tokens:,}/{max_tokens:,}"
|
513
|
+
left_width = len(left_section)
|
514
|
+
|
515
|
+
# Right section: Message count + floppy disk
|
516
|
+
right_section = f"{current_messages}/{max_messages} 💾"
|
517
|
+
right_width = len(right_section)
|
518
|
+
|
519
|
+
# Center section: Progress bar (2x wider = 50 chars) + percentage
|
520
|
+
bar_length = 50
|
448
521
|
token_filled = int((token_percentage / 100) * bar_length)
|
449
522
|
token_bar = "█" * token_filled + "░" * (bar_length - token_filled)
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
#
|
454
|
-
|
455
|
-
|
456
|
-
|
523
|
+
center_section = f"[{token_bar}] {token_percentage:.1f}%"
|
524
|
+
center_width = len(center_section)
|
525
|
+
|
526
|
+
# Calculate spacing to center the progress bar
|
527
|
+
total_content_width = left_width + center_width + right_width
|
528
|
+
remaining_space = available_width - total_content_width
|
529
|
+
|
530
|
+
# Ensure we have enough space, if not, reduce the progress bar length
|
531
|
+
if remaining_space < 0:
|
532
|
+
# Reduce bar length to fit everything
|
533
|
+
excess = abs(remaining_space)
|
534
|
+
bar_length = max(30, 50 - excess) # Minimum 30 chars
|
535
|
+
token_filled = int((token_percentage / 100) * bar_length)
|
536
|
+
token_bar = "█" * token_filled + "░" * (bar_length - token_filled)
|
537
|
+
center_section = f"[{token_bar}] {token_percentage:.1f}%"
|
538
|
+
center_width = len(center_section)
|
539
|
+
total_content_width = left_width + center_width + right_width
|
540
|
+
remaining_space = available_width - total_content_width
|
541
|
+
|
542
|
+
# Distribute remaining space equally for symmetrical layout
|
543
|
+
left_spacing = remaining_space // 2
|
544
|
+
right_spacing = remaining_space - left_spacing
|
545
|
+
|
546
|
+
# Build the final layout
|
547
|
+
memory_text.append(left_section, style="dim")
|
548
|
+
memory_text.append(" " * left_spacing)
|
549
|
+
memory_text.append(center_section, style="dim")
|
550
|
+
memory_text.append(" " * right_spacing)
|
551
|
+
memory_text.append(right_section, style="dim")
|
552
|
+
|
457
553
|
return memory_text
|
todo_agent/interface/tools.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
Tool definitions and schemas for LLM function calling.
|
3
3
|
"""
|
4
4
|
|
5
|
+
import subprocess
|
5
6
|
from typing import Any, Callable, Dict, List, Optional
|
6
7
|
|
7
8
|
try:
|
@@ -62,8 +63,12 @@ class ToolCallHandler:
|
|
62
63
|
"1) User wants to see their tasks, "
|
63
64
|
"2) You need to find a specific task by description, "
|
64
65
|
"3) You need to check for potential duplicates before adding new tasks, "
|
65
|
-
"4) You need to understand the current state before making changes
|
66
|
+
"4) You need to understand the current state before making changes, "
|
67
|
+
"5) User says they finished/completed a task and you need to find the matching task. "
|
66
68
|
"CRITICAL: ALWAYS use this before add_task() to check for similar existing tasks. "
|
69
|
+
"CRITICAL: ALWAYS use this when user mentions task completion to find the correct task number. "
|
70
|
+
"COMPLETION INTELLIGENCE: When searching for completion matches, if exactly one task clearly "
|
71
|
+
"matches the user's description, proceed to complete it immediately without asking for confirmation. "
|
67
72
|
"IMPORTANT: When presenting the results to the user, convert the raw todo.txt format "
|
68
73
|
"into conversational language. Do not show the raw format like '(A) task +project @context'. "
|
69
74
|
"STRATEGIC CONTEXT: This is the primary discovery tool - call this FIRST when you need to "
|
@@ -76,7 +81,7 @@ class ToolCallHandler:
|
|
76
81
|
"type": "string",
|
77
82
|
"description": (
|
78
83
|
"Optional filter string (e.g., '+work', '@office', '(A)') - "
|
79
|
-
"use when you want to see only specific tasks"
|
84
|
+
"use when you want to see only specific tasks or when searching for completion matches"
|
80
85
|
),
|
81
86
|
}
|
82
87
|
},
|
@@ -166,7 +171,10 @@ class ToolCallHandler:
|
|
166
171
|
"use list_tasks() and list_completed_tasks() to check for potential duplicates. Look for tasks with "
|
167
172
|
"similar descriptions, keywords, or intent. If you find similar tasks, "
|
168
173
|
"ask the user if they want to add a new task or modify an existing one. "
|
169
|
-
"
|
174
|
+
"AUTOMATIC INFERENCE: When project, context, or due date is not specified, automatically infer appropriate tags "
|
175
|
+
"and due dates based on the task content, natural language expressions, task nature, calendar context, and existing patterns. "
|
176
|
+
"DUE DATE INFERENCE: Extract temporal expressions and use common sense to infer appropriate due dates based on task type, "
|
177
|
+
"work patterns, personal schedules, and existing task due date patterns. Only ask for clarification when genuinely ambiguous. "
|
170
178
|
"Always provide a complete, natural response to the user. "
|
171
179
|
"STRATEGIC CONTEXT: This is a modification tool - call this LAST after using "
|
172
180
|
"discovery tools (list_tasks, list_projects, list_contexts list_completed_tasks) "
|
@@ -193,7 +201,7 @@ class ToolCallHandler:
|
|
193
201
|
},
|
194
202
|
"due": {
|
195
203
|
"type": "string",
|
196
|
-
"description": "Optional due date in YYYY-MM-DD format",
|
204
|
+
"description": "Optional due date in YYYY-MM-DD format. Automatically inferred from natural language expressions like 'tomorrow', 'next week', 'by Friday', 'urgent', 'asap'",
|
197
205
|
},
|
198
206
|
},
|
199
207
|
"required": ["description"],
|
@@ -206,11 +214,13 @@ class ToolCallHandler:
|
|
206
214
|
"name": "complete_task",
|
207
215
|
"description": (
|
208
216
|
"Mark a specific task as complete by its line number. IMPORTANT: "
|
209
|
-
"
|
210
|
-
"
|
217
|
+
"When user says they finished/completed a task, FIRST use list_tasks() to find matching tasks, "
|
218
|
+
"then list_completed_tasks() to verify it's not already done. "
|
219
|
+
"COMPLETION LOGIC: If user's statement clearly matches exactly one task, complete it immediately. "
|
220
|
+
"If multiple tasks match, show numbered options and ask for clarification. "
|
221
|
+
"If the match is ambiguous, ask for confirmation. "
|
211
222
|
"STRATEGIC CONTEXT: This is a modification tool - call this LAST after using "
|
212
|
-
"discovery tools (list_tasks, list_completed_tasks) "
|
213
|
-
"to verify the task exists and hasn't already been completed."
|
223
|
+
"discovery tools (list_tasks, list_completed_tasks) to verify the task exists and status."
|
214
224
|
),
|
215
225
|
"parameters": {
|
216
226
|
"type": "object",
|
@@ -434,8 +444,69 @@ class ToolCallHandler:
|
|
434
444
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
435
445
|
},
|
436
446
|
},
|
447
|
+
{
|
448
|
+
"type": "function",
|
449
|
+
"function": {
|
450
|
+
"name": "get_calendar",
|
451
|
+
"description": (
|
452
|
+
"Get a calendar for a specific month and year using the system 'cal' command. "
|
453
|
+
"Use this when: "
|
454
|
+
"1) User asks to see a calendar for a specific month/year, "
|
455
|
+
"2) User wants to plan tasks around specific dates, "
|
456
|
+
"3) User needs to see what day of the week a date falls on, "
|
457
|
+
"4) User wants to visualize the current month or upcoming months. "
|
458
|
+
"The calendar will show the month in a traditional calendar format with days of the week. "
|
459
|
+
"IMPORTANT: When displaying the calendar output, present it directly without wrapping in backticks or code blocks. "
|
460
|
+
"The calendar should be displayed as plain text in the conversation."
|
461
|
+
),
|
462
|
+
"parameters": {
|
463
|
+
"type": "object",
|
464
|
+
"properties": {
|
465
|
+
"month": {
|
466
|
+
"type": "integer",
|
467
|
+
"description": "Month number (1-12, where 1=January, 12=December)",
|
468
|
+
"minimum": 1,
|
469
|
+
"maximum": 12,
|
470
|
+
},
|
471
|
+
"year": {
|
472
|
+
"type": "integer",
|
473
|
+
"description": "Year (4-digit format, e.g., 2025)",
|
474
|
+
"minimum": 1900,
|
475
|
+
"maximum": 2100,
|
476
|
+
},
|
477
|
+
},
|
478
|
+
"required": ["month", "year"],
|
479
|
+
},
|
480
|
+
},
|
481
|
+
},
|
437
482
|
]
|
438
483
|
|
484
|
+
def _get_calendar(self, month: int, year: int) -> str:
|
485
|
+
"""
|
486
|
+
Get a calendar for the specified month and year using the system 'cal' command.
|
487
|
+
|
488
|
+
Args:
|
489
|
+
month: Month number (1-12)
|
490
|
+
year: Year (4-digit format)
|
491
|
+
|
492
|
+
Returns:
|
493
|
+
Calendar output as a string
|
494
|
+
"""
|
495
|
+
try:
|
496
|
+
# Use the cal command with specific month and year
|
497
|
+
result = subprocess.run(
|
498
|
+
["cal", str(month), str(year)],
|
499
|
+
capture_output=True,
|
500
|
+
text=True,
|
501
|
+
check=True,
|
502
|
+
)
|
503
|
+
return result.stdout.strip()
|
504
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
505
|
+
# Fallback to Python calendar module
|
506
|
+
import calendar
|
507
|
+
|
508
|
+
return calendar.month(year, month).strip()
|
509
|
+
|
439
510
|
def _format_tool_signature(self, tool_name: str, arguments: Dict[str, Any]) -> str:
|
440
511
|
"""Format tool signature with parameters for logging."""
|
441
512
|
if not arguments:
|
@@ -503,6 +574,7 @@ class ToolCallHandler:
|
|
503
574
|
"move_task": self.todo_manager.move_task,
|
504
575
|
"archive_tasks": self.todo_manager.archive_tasks,
|
505
576
|
"deduplicate_tasks": self.todo_manager.deduplicate_tasks,
|
577
|
+
"get_calendar": self._get_calendar,
|
506
578
|
}
|
507
579
|
|
508
580
|
if tool_name not in method_map:
|
@@ -1,28 +1,29 @@
|
|
1
1
|
todo_agent/__init__.py,sha256=RUowhd14r3tqB_7rl83unGV8oBjra3UOIl7jix-33fk,254
|
2
|
-
todo_agent/_version.py,sha256=
|
2
|
+
todo_agent/_version.py,sha256=9wrJ_4Dlc0arUzKiaIqvTY85rMJma3eb1nNlF3uHAxU,704
|
3
3
|
todo_agent/main.py,sha256=-ryhMm4c4sz4e4anXI8B-CYnpEh5HIkmnYcnGxcWHDk,1628
|
4
4
|
todo_agent/core/__init__.py,sha256=QAZ4it63pXv5-DxtNcuSAmg7ZnCY5ackI5yycvKHr9I,365
|
5
5
|
todo_agent/core/conversation_manager.py,sha256=gSCcX356UJ0T3FCTS1q0fOud0ytFKXptf9RyKjzpTYI,11640
|
6
6
|
todo_agent/core/exceptions.py,sha256=cPvvkIbKdI7l51wC7cE-ZxUi54P3nf2m7x2lMNMRFYM,399
|
7
|
-
todo_agent/core/todo_manager.py,sha256=
|
7
|
+
todo_agent/core/todo_manager.py,sha256=Dyc5NbDd_u21nJlS-C8KxefGJpEcHOLtL21qqQEit2Q,9142
|
8
8
|
todo_agent/infrastructure/__init__.py,sha256=SGbHXgzq6U1DMgOfWPMsWEK99zjPSF-6gzy7xqc5fsI,284
|
9
|
+
todo_agent/infrastructure/calendar_utils.py,sha256=HmF0ykXF_6GbdoJvZLIv6fKwT6ipixoywdTMkIXmkGU,1871
|
9
10
|
todo_agent/infrastructure/config.py,sha256=zyp6qOlg1nN_awphivlgGNBE6fL0Hf66YgvWxR8ldyQ,2117
|
10
|
-
todo_agent/infrastructure/inference.py,sha256=
|
11
|
+
todo_agent/infrastructure/inference.py,sha256=J6i9jtzOVo2Yy3e6yAUBH22ik3OqHs1I0zrADhs4IRk,10676
|
11
12
|
todo_agent/infrastructure/llm_client.py,sha256=ZoObyqaRP6i_eqGYGfJWGeWTJ-VNxpY70ay04vt2v_E,1390
|
12
|
-
todo_agent/infrastructure/llm_client_factory.py,sha256
|
13
|
+
todo_agent/infrastructure/llm_client_factory.py,sha256=-tktnVOIF7B45WR7AuLoi7MKnEyuM8lgg1jjc4T1FhM,1929
|
13
14
|
todo_agent/infrastructure/logger.py,sha256=2ykG_0lyzmEGxDF6ZRl1qiTUGDuFeQgzv4Na6vRmXcM,4110
|
14
15
|
todo_agent/infrastructure/ollama_client.py,sha256=6WsjSftsHNt-CeScK6GSrJ_CMe80OiATT3idcJgPCBk,5654
|
15
16
|
todo_agent/infrastructure/openrouter_client.py,sha256=GVOJTzPDOKNdHEu-y-HQygMcLvrFw1KB6NZU_rJR3-c,6859
|
16
17
|
todo_agent/infrastructure/todo_shell.py,sha256=z6kqUKDX-i4DfYJKoOLiPLCp8y6m1HdTDLHTvmLpzMc,5801
|
17
18
|
todo_agent/infrastructure/token_counter.py,sha256=PCKheOVJbp1s89yhh_i6iKgURMt9mVoYkwjQJCc2xCE,4958
|
18
|
-
todo_agent/infrastructure/prompts/system_prompt.txt,sha256=
|
19
|
+
todo_agent/infrastructure/prompts/system_prompt.txt,sha256=S4oJXNaIhyzCydVOF7VRbg9Be_l51DHiMXEUHBzhOwY,5830
|
19
20
|
todo_agent/interface/__init__.py,sha256=vDD3rQu4qDkpvVwGVtkDzE1M4IiSHYzTif4GbYSFWaI,457
|
20
|
-
todo_agent/interface/cli.py,sha256=
|
21
|
-
todo_agent/interface/formatters.py,sha256=
|
22
|
-
todo_agent/interface/tools.py,sha256=
|
23
|
-
todo_agent-0.2.
|
24
|
-
todo_agent-0.2.
|
25
|
-
todo_agent-0.2.
|
26
|
-
todo_agent-0.2.
|
27
|
-
todo_agent-0.2.
|
28
|
-
todo_agent-0.2.
|
21
|
+
todo_agent/interface/cli.py,sha256=eEtYXrizgdN00jA5Po7JOFi0um0Lje3YpslzQXs9XO4,11992
|
22
|
+
todo_agent/interface/formatters.py,sha256=Oc7ynL7vpb4i8f-XQM38gJlqTOVZfzyTBwWceeMHV_Y,18912
|
23
|
+
todo_agent/interface/tools.py,sha256=RkxsyqYbp0QYwtnJMXixBf727Atcp8DY6dm6949DTMY,32739
|
24
|
+
todo_agent-0.2.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
25
|
+
todo_agent-0.2.5.dist-info/METADATA,sha256=Hz4s2k3T2fNMmyscFirCqfr7U5WkWnTVVkxgxzWtr9M,10047
|
26
|
+
todo_agent-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
27
|
+
todo_agent-0.2.5.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
|
28
|
+
todo_agent-0.2.5.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
|
29
|
+
todo_agent-0.2.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|