zrb 1.0.0a3__py3-none-any.whl → 1.0.0a4__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.
- zrb/__init__.py +2 -2
- zrb/__main__.py +3 -1
- zrb/builtin/__init__.py +4 -2
- zrb/builtin/base64.py +4 -2
- zrb/builtin/{llm.py → llm/llm_chat.py} +18 -2
- zrb/builtin/llm/tool/cli.py +9 -0
- zrb/builtin/llm/tool/rag.py +189 -0
- zrb/builtin/llm/tool/web.py +74 -0
- zrb/builtin/md5.py +4 -2
- zrb/builtin/project/add/fastapp.py +1 -1
- zrb/builtin/project/add/fastapp_template/_zrb/helper.py +3 -3
- zrb/builtin/project/add/fastapp_template/_zrb/main.py +1 -1
- zrb/builtin/project/create/create.py +1 -1
- zrb/builtin/todo.py +122 -89
- zrb/config.py +12 -2
- zrb/runner/cli.py +8 -7
- zrb/runner/{web_server.py → web_app.py} +13 -13
- zrb/runner/{web_app → web_controller}/group_info_ui/controller.py +2 -2
- zrb/runner/{web_app → web_controller}/home_page/controller.py +2 -2
- zrb/runner/{web_app → web_controller}/task_ui/controller.py +2 -2
- zrb/task/cmd_task.py +24 -26
- zrb/task/http_check.py +5 -5
- zrb/task/llm_task.py +91 -36
- zrb/task/rsync_task.py +18 -18
- zrb/task/scaffolder.py +4 -4
- zrb/task/tcp_check.py +3 -5
- zrb/util/todo.py +139 -15
- {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/METADATA +6 -1
- {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/RECORD +53 -50
- /zrb/runner/{web_app → web_controller}/__init__.py +0 -0
- /zrb/runner/{web_app → web_controller}/group_info_ui/__init__.py +0 -0
- /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_info.html +0 -0
- /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_li.html +0 -0
- /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_info.html +0 -0
- /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_li.html +0 -0
- /zrb/runner/{web_app → web_controller}/group_info_ui/view.html +0 -0
- /zrb/runner/{web_app → web_controller}/home_page/__init__.py +0 -0
- /zrb/runner/{web_app → web_controller}/home_page/partial/group_info.html +0 -0
- /zrb/runner/{web_app → web_controller}/home_page/partial/group_li.html +0 -0
- /zrb/runner/{web_app → web_controller}/home_page/partial/task_info.html +0 -0
- /zrb/runner/{web_app → web_controller}/home_page/partial/task_li.html +0 -0
- /zrb/runner/{web_app → web_controller}/home_page/view.html +0 -0
- /zrb/runner/{web_app → web_controller}/static/favicon-32x32.png +0 -0
- /zrb/runner/{web_app → web_controller}/static/pico.min.css +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/__init__.py +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/partial/common-util.js +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/partial/input.html +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/partial/main.js +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/partial/show-existing-session.js +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/partial/visualize-history.js +0 -0
- /zrb/runner/{web_app → web_controller}/task_ui/view.html +0 -0
- {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/WHEEL +0 -0
- {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/task/llm_task.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
import json
|
2
|
+
import os
|
2
3
|
from collections.abc import Callable
|
3
4
|
from typing import Any
|
4
5
|
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
5
8
|
from zrb.attr.type import StrAttr
|
6
9
|
from zrb.config import LLM_MODEL, LLM_SYSTEM_PROMPT
|
7
10
|
from zrb.context.any_context import AnyContext
|
@@ -11,9 +14,16 @@ from zrb.input.any_input import AnyInput
|
|
11
14
|
from zrb.task.any_task import AnyTask
|
12
15
|
from zrb.task.base_task import BaseTask
|
13
16
|
from zrb.util.attr import get_str_attr
|
17
|
+
from zrb.util.cli.style import stylize_faint
|
14
18
|
from zrb.util.llm.tool import callable_to_tool_schema
|
15
19
|
|
16
|
-
|
20
|
+
ListOfDict = list[dict[str, Any]]
|
21
|
+
|
22
|
+
|
23
|
+
class AdditionalTool(BaseModel):
|
24
|
+
fn: Callable
|
25
|
+
name: str | None
|
26
|
+
description: str | None
|
17
27
|
|
18
28
|
|
19
29
|
def scratchpad(thought: str) -> str:
|
@@ -33,13 +43,19 @@ class LLMTask(BaseTask):
|
|
33
43
|
input: list[AnyInput | None] | AnyInput | None = None,
|
34
44
|
env: list[AnyEnv | None] | AnyEnv | None = None,
|
35
45
|
model: StrAttr | None = LLM_MODEL,
|
46
|
+
render_model: bool = True,
|
36
47
|
system_prompt: StrAttr | None = LLM_SYSTEM_PROMPT,
|
48
|
+
render_system_prompt: bool = True,
|
37
49
|
message: StrAttr | None = None,
|
38
50
|
tools: (
|
39
51
|
dict[str, Callable] | Callable[[AnySharedContext], dict[str, Callable]]
|
40
52
|
) = {},
|
41
|
-
|
42
|
-
|
53
|
+
history: ListOfDict | Callable[[AnySharedContext], ListOfDict] = [],
|
54
|
+
history_file: StrAttr | None = None,
|
55
|
+
render_history_file: bool = True,
|
56
|
+
model_kwargs: (
|
57
|
+
dict[str, Any] | Callable[[AnySharedContext], dict[str, Any]]
|
58
|
+
) = {},
|
43
59
|
execute_condition: bool | str | Callable[[AnySharedContext], bool] = True,
|
44
60
|
retries: int = 2,
|
45
61
|
retry_period: float = 0,
|
@@ -73,47 +89,64 @@ class LLMTask(BaseTask):
|
|
73
89
|
fallback=fallback,
|
74
90
|
)
|
75
91
|
self._model = model
|
92
|
+
self._render_model = render_model
|
93
|
+
self._model_kwargs = model_kwargs
|
76
94
|
self._system_prompt = system_prompt
|
95
|
+
self._render_system_prompt = render_system_prompt
|
77
96
|
self._message = message
|
78
97
|
self._tools = tools
|
79
|
-
self._tool_schema = tool_schema
|
80
98
|
self._history = history
|
99
|
+
self._history_file = history_file
|
100
|
+
self._render_history_file = render_history_file
|
101
|
+
self._additional_tools: list[AdditionalTool] = []
|
102
|
+
|
103
|
+
def add_tool(
|
104
|
+
self, tool: Callable, name: str | None = None, description: str | None = None
|
105
|
+
):
|
106
|
+
self._additional_tools.append(
|
107
|
+
AdditionalTool(fn=tool, name=name, description=description)
|
108
|
+
)
|
81
109
|
|
82
110
|
async def _exec_action(self, ctx: AnyContext) -> Any:
|
83
|
-
from litellm import
|
111
|
+
from litellm import acompletion
|
84
112
|
|
113
|
+
model_kwargs = self._get_model_kwargs(ctx)
|
114
|
+
ctx.log_debug("MODEL KWARGS", model_kwargs)
|
85
115
|
system_prompt = self._get_system_prompt(ctx)
|
86
116
|
ctx.log_debug("SYSTEM PROMPT", system_prompt)
|
87
117
|
history = self._get_history(ctx)
|
88
118
|
ctx.log_debug("HISTORY PROMPT", history)
|
89
|
-
user_message = self._get_message(ctx)
|
90
|
-
ctx.
|
91
|
-
messages =
|
92
|
-
[{"role": "system", "content": system_prompt}]
|
93
|
-
+ history
|
94
|
-
+ [{"role": "user", "content": user_message}]
|
95
|
-
)
|
119
|
+
user_message = {"role": "user", "content": self._get_message(ctx)}
|
120
|
+
ctx.print(stylize_faint(f"{user_message}"))
|
121
|
+
messages = history + [user_message]
|
96
122
|
available_tools = self._get_tools(ctx)
|
97
123
|
available_tools["scratchpad"] = scratchpad
|
98
|
-
tool_schema =
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
]
|
107
|
-
|
108
|
-
|
124
|
+
tool_schema = [
|
125
|
+
callable_to_tool_schema(tool, name)
|
126
|
+
for name, tool in available_tools.items()
|
127
|
+
]
|
128
|
+
for additional_tool in self._additional_tools:
|
129
|
+
fn = additional_tool.fn
|
130
|
+
tool_name = additional_tool.name or fn.__name__
|
131
|
+
tool_description = additional_tool.description
|
132
|
+
available_tools[tool_name] = additional_tool.fn
|
133
|
+
tool_schema.append(
|
134
|
+
callable_to_tool_schema(
|
135
|
+
fn, name=tool_name, description=tool_description
|
136
|
+
)
|
137
|
+
)
|
109
138
|
ctx.log_debug("TOOL SCHEMA", tool_schema)
|
139
|
+
history_file = self._get_history_file(ctx)
|
110
140
|
while True:
|
111
|
-
response =
|
112
|
-
model=self._get_model(ctx),
|
141
|
+
response = await acompletion(
|
142
|
+
model=self._get_model(ctx),
|
143
|
+
messages=[{"role": "system", "content": system_prompt}] + messages,
|
144
|
+
tools=tool_schema,
|
145
|
+
**model_kwargs,
|
113
146
|
)
|
114
147
|
response_message = response.choices[0].message
|
115
|
-
ctx.print(response_message)
|
116
|
-
messages.append(response_message)
|
148
|
+
ctx.print(stylize_faint(f"{response_message.to_dict()}"))
|
149
|
+
messages.append(response_message.to_dict())
|
117
150
|
tool_calls = response_message.tool_calls
|
118
151
|
if tool_calls:
|
119
152
|
# noqa Reference: https://docs.litellm.ai/docs/completion/function_call#full-code---parallel-function-calling-with-gpt-35-turbo-1106
|
@@ -128,33 +161,55 @@ class LLMTask(BaseTask):
|
|
128
161
|
"name": function_name,
|
129
162
|
"content": function_response,
|
130
163
|
}
|
131
|
-
ctx.print(tool_call_message)
|
164
|
+
ctx.print(stylize_faint(f"{tool_call_message}"))
|
132
165
|
messages.append(tool_call_message)
|
133
166
|
continue
|
167
|
+
if history_file != "":
|
168
|
+
os.makedirs(os.path.dirname(history_file), exist_ok=True)
|
169
|
+
with open(history_file, "w") as f:
|
170
|
+
f.write(json.dumps(messages, indent=2))
|
134
171
|
return response_message.content
|
135
172
|
|
136
173
|
def _get_model(self, ctx: AnyContext) -> str:
|
137
|
-
return get_str_attr(
|
174
|
+
return get_str_attr(
|
175
|
+
ctx, self._model, "ollama_chat/llama3.1", auto_render=self._render_model
|
176
|
+
)
|
138
177
|
|
139
178
|
def _get_system_prompt(self, ctx: AnyContext) -> str:
|
140
179
|
return get_str_attr(
|
141
|
-
ctx,
|
180
|
+
ctx,
|
181
|
+
self._system_prompt,
|
182
|
+
"You are a helpful assistant",
|
183
|
+
auto_render=self._render_system_prompt,
|
142
184
|
)
|
143
185
|
|
144
186
|
def _get_message(self, ctx: AnyContext) -> str:
|
145
187
|
return get_str_attr(ctx, self._message, "How are you?", auto_render=True)
|
146
188
|
|
189
|
+
def _get_model_kwargs(self, ctx: AnyContext) -> dict[str, Callable]:
|
190
|
+
if callable(self._model_kwargs):
|
191
|
+
return self._model_kwargs(ctx)
|
192
|
+
return self._model_kwargs
|
193
|
+
|
147
194
|
def _get_tools(self, ctx: AnyContext) -> dict[str, Callable]:
|
148
195
|
if callable(self._tools):
|
149
196
|
return self._tools(ctx)
|
150
197
|
return self._tools
|
151
198
|
|
152
|
-
def
|
153
|
-
if callable(self._tool_schema):
|
154
|
-
return self._tool_schema(ctx)
|
155
|
-
return self._tool_schema
|
156
|
-
|
157
|
-
def _get_history(self, ctx: AnyContext) -> DictList:
|
199
|
+
def _get_history(self, ctx: AnyContext) -> ListOfDict:
|
158
200
|
if callable(self._history):
|
159
201
|
return self._history(ctx)
|
202
|
+
history_file = self._get_history_file(ctx)
|
203
|
+
if (
|
204
|
+
len(self._history) == 0
|
205
|
+
and history_file != ""
|
206
|
+
and os.path.isfile(history_file)
|
207
|
+
):
|
208
|
+
with open(history_file, "r") as f:
|
209
|
+
return json.loads(f.read())
|
160
210
|
return self._history
|
211
|
+
|
212
|
+
def _get_history_file(self, ctx: AnyContext) -> str:
|
213
|
+
return get_str_attr(
|
214
|
+
ctx, self._history_file, "", auto_render=self._render_history_file
|
215
|
+
)
|
zrb/task/rsync_task.py
CHANGED
@@ -32,13 +32,13 @@ class RsyncTask(CmdTask):
|
|
32
32
|
remote_ssh_key: StrAttr | None = None,
|
33
33
|
auto_render_remote_ssh_key: bool = True,
|
34
34
|
remote_source_path: StrAttr | None = None,
|
35
|
-
|
35
|
+
render_remote_source_path: bool = True,
|
36
36
|
remote_destination_path: StrAttr | None = None,
|
37
|
-
|
37
|
+
render_remote_destination_path: bool = True,
|
38
38
|
local_source_path: StrAttr | None = None,
|
39
|
-
|
39
|
+
render_local_source_path: bool = True,
|
40
40
|
local_destination_path: StrAttr | None = None,
|
41
|
-
|
41
|
+
render_local_destination_path: bool = True,
|
42
42
|
cwd: str | None = None,
|
43
43
|
auto_render_cwd: bool = True,
|
44
44
|
max_output_line: int = 1000,
|
@@ -59,19 +59,19 @@ class RsyncTask(CmdTask):
|
|
59
59
|
input=input,
|
60
60
|
env=env,
|
61
61
|
shell=shell,
|
62
|
-
|
62
|
+
render_shell=auto_render_shell,
|
63
63
|
remote_host=remote_host,
|
64
|
-
|
64
|
+
render_remote_host=auto_render_remote_host,
|
65
65
|
remote_port=remote_port,
|
66
66
|
auto_render_remote_port=auto_render_remote_port,
|
67
67
|
remote_user=remote_user,
|
68
|
-
|
68
|
+
render_remote_user=auto_render_remote_user,
|
69
69
|
remote_password=remote_password,
|
70
|
-
|
70
|
+
render_remote_password=auto_render_remote_password,
|
71
71
|
remote_ssh_key=remote_ssh_key,
|
72
|
-
|
72
|
+
render_remote_ssh_key=auto_render_remote_ssh_key,
|
73
73
|
cwd=cwd,
|
74
|
-
|
74
|
+
render_cwd=auto_render_cwd,
|
75
75
|
max_output_line=max_output_line,
|
76
76
|
max_error_line=max_error_line,
|
77
77
|
execute_condition=execute_condition,
|
@@ -82,13 +82,13 @@ class RsyncTask(CmdTask):
|
|
82
82
|
fallback=fallback,
|
83
83
|
)
|
84
84
|
self._remote_source_path = remote_source_path
|
85
|
-
self.
|
85
|
+
self._render_remote_source_path = render_remote_source_path
|
86
86
|
self._remote_destination_path = remote_destination_path
|
87
|
-
self.
|
87
|
+
self._render_remote_destination_path = render_remote_destination_path
|
88
88
|
self._local_source_path = local_source_path
|
89
|
-
self.
|
89
|
+
self._render_local_source_path = render_local_source_path
|
90
90
|
self._local_destination_path = local_destination_path
|
91
|
-
self.
|
91
|
+
self._render_local_destination_path = render_local_destination_path
|
92
92
|
|
93
93
|
def _get_source_path(self, ctx: AnyContext) -> str:
|
94
94
|
local_source_path = self._get_local_source_path(ctx)
|
@@ -113,7 +113,7 @@ class RsyncTask(CmdTask):
|
|
113
113
|
ctx,
|
114
114
|
self._remote_source_path,
|
115
115
|
"",
|
116
|
-
auto_render=self.
|
116
|
+
auto_render=self._render_remote_source_path,
|
117
117
|
)
|
118
118
|
|
119
119
|
def _get_remote_destination_path(self, ctx: AnyContext) -> str:
|
@@ -121,7 +121,7 @@ class RsyncTask(CmdTask):
|
|
121
121
|
ctx,
|
122
122
|
self._remote_destination_path,
|
123
123
|
"",
|
124
|
-
auto_render=self.
|
124
|
+
auto_render=self._render_remote_destination_path,
|
125
125
|
)
|
126
126
|
|
127
127
|
def _get_local_source_path(self, ctx: AnyContext) -> str:
|
@@ -129,7 +129,7 @@ class RsyncTask(CmdTask):
|
|
129
129
|
ctx,
|
130
130
|
self._local_source_path,
|
131
131
|
"",
|
132
|
-
auto_render=self.
|
132
|
+
auto_render=self._render_local_source_path,
|
133
133
|
)
|
134
134
|
|
135
135
|
def _get_local_destination_path(self, ctx: AnyContext) -> str:
|
@@ -137,7 +137,7 @@ class RsyncTask(CmdTask):
|
|
137
137
|
ctx,
|
138
138
|
self._local_destination_path,
|
139
139
|
"",
|
140
|
-
auto_render=self.
|
140
|
+
auto_render=self._render_local_destination_path,
|
141
141
|
)
|
142
142
|
|
143
143
|
def _get_cmd_script(self, ctx: AnyContext) -> str:
|
zrb/task/scaffolder.py
CHANGED
@@ -26,9 +26,9 @@ class Scaffolder(BaseTask):
|
|
26
26
|
input: list[AnyInput] | AnyInput | None = None,
|
27
27
|
env: list[AnyEnv] | AnyEnv | None = None,
|
28
28
|
source_path: StrAttr | None = None,
|
29
|
-
|
29
|
+
render_source_path: bool = True,
|
30
30
|
destination_path: StrAttr | None = None,
|
31
|
-
|
31
|
+
render_destination_path: bool = True,
|
32
32
|
transform_path: TransformConfig = {},
|
33
33
|
transform_content: (
|
34
34
|
list[AnyContentTransformer] | AnyContentTransformer | TransformConfig
|
@@ -66,9 +66,9 @@ class Scaffolder(BaseTask):
|
|
66
66
|
fallback=fallback,
|
67
67
|
)
|
68
68
|
self._source_path = source_path
|
69
|
-
self.
|
69
|
+
self._render_source_path = render_source_path
|
70
70
|
self._destination_path = destination_path
|
71
|
-
self.
|
71
|
+
self._render_destination_path = render_destination_path
|
72
72
|
self._content_transformers = transform_content
|
73
73
|
self._path_transformer = transform_path
|
74
74
|
|
zrb/task/tcp_check.py
CHANGED
@@ -22,7 +22,7 @@ class TcpCheck(BaseTask):
|
|
22
22
|
input: list[AnyInput] | AnyInput | None = None,
|
23
23
|
env: list[AnyEnv] | AnyEnv | None = None,
|
24
24
|
host: StrAttr = "localhost",
|
25
|
-
|
25
|
+
render_host: bool = True,
|
26
26
|
port: IntAttr = 80,
|
27
27
|
interval: int = 5,
|
28
28
|
execute_condition: bool | str | Callable[[Context], bool] = True,
|
@@ -43,14 +43,12 @@ class TcpCheck(BaseTask):
|
|
43
43
|
fallback=fallback,
|
44
44
|
)
|
45
45
|
self._host = host
|
46
|
-
self.
|
46
|
+
self._render_host = render_host
|
47
47
|
self._port = port
|
48
48
|
self._interval = interval
|
49
49
|
|
50
50
|
def _get_host(self, ctx: AnyContext) -> str:
|
51
|
-
return get_str_attr(
|
52
|
-
ctx, self._host, "localhost", auto_render=self._auto_render_host
|
53
|
-
)
|
51
|
+
return get_str_attr(ctx, self._host, "localhost", auto_render=self._render_host)
|
54
52
|
|
55
53
|
def _get_port(self, ctx: AnyContext) -> str:
|
56
54
|
return get_int_attr(ctx, self._port, 80, auto_render=True)
|
zrb/util/todo.py
CHANGED
@@ -3,8 +3,16 @@ import re
|
|
3
3
|
|
4
4
|
from pydantic import BaseModel, Field, model_validator
|
5
5
|
|
6
|
+
from zrb.util.cli.style import (
|
7
|
+
stylize_bold_green,
|
8
|
+
stylize_cyan,
|
9
|
+
stylize_magenta,
|
10
|
+
stylize_yellow,
|
11
|
+
)
|
12
|
+
from zrb.util.string.name import get_random_name
|
13
|
+
|
6
14
|
|
7
|
-
class
|
15
|
+
class TodoTaskModel(BaseModel):
|
8
16
|
priority: str | None = Field("D", pattern=r"^[A-Z]$") # Priority like A, B, ...
|
9
17
|
completed: bool = False # True if completed, False otherwise
|
10
18
|
description: str # Main task description
|
@@ -34,16 +42,46 @@ TODO_TXT_PATTERN = re.compile(
|
|
34
42
|
)
|
35
43
|
|
36
44
|
|
37
|
-
def
|
45
|
+
def cascade_todo_task(todo_task: TodoTaskModel):
|
46
|
+
if todo_task.creation_date is None:
|
47
|
+
todo_task.creation_date = datetime.date.today()
|
48
|
+
if "id" not in todo_task.keyval:
|
49
|
+
todo_task.keyval["id"] = get_random_name()
|
50
|
+
return todo_task
|
51
|
+
|
52
|
+
|
53
|
+
def select_todo_task(
|
54
|
+
todo_list: list[TodoTaskModel], keyword: str
|
55
|
+
) -> TodoTaskModel | None:
|
56
|
+
for todo_task in todo_list:
|
57
|
+
id = todo_task.keyval.get("id", "")
|
58
|
+
if keyword.lower().strip() == id.lower().strip():
|
59
|
+
return todo_task
|
60
|
+
for todo_task in todo_list:
|
61
|
+
description = todo_task.description
|
62
|
+
if keyword.lower().strip() == description.lower().strip():
|
63
|
+
return todo_task
|
64
|
+
for todo_task in todo_list:
|
65
|
+
id = todo_task.keyval.get("id", "")
|
66
|
+
if keyword.lower().strip() in id.lower().strip():
|
67
|
+
return todo_task
|
68
|
+
for todo_task in todo_list:
|
69
|
+
description = todo_task.description
|
70
|
+
if keyword.lower().strip() in description.lower().strip():
|
71
|
+
return todo_task
|
72
|
+
return None
|
73
|
+
|
74
|
+
|
75
|
+
def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]:
|
38
76
|
with open(todo_file_path, "r") as f:
|
39
77
|
todo_lines = f.read().strip().split("\n")
|
40
|
-
|
78
|
+
todo_list: list[TodoTaskModel] = []
|
41
79
|
for todo_line in todo_lines:
|
42
80
|
todo_line = todo_line.strip()
|
43
81
|
if todo_line == "":
|
44
82
|
continue
|
45
|
-
|
46
|
-
|
83
|
+
todo_list.append(line_to_todo_task(todo_line))
|
84
|
+
todo_list.sort(
|
47
85
|
key=lambda task: (
|
48
86
|
task.completed,
|
49
87
|
task.priority if task.priority else "Z",
|
@@ -51,16 +89,16 @@ def read_todo_from_file(todo_file_path: str) -> list[TodoTask]:
|
|
51
89
|
task.creation_date if task.creation_date else datetime.date.max,
|
52
90
|
)
|
53
91
|
)
|
54
|
-
return
|
92
|
+
return todo_list
|
55
93
|
|
56
94
|
|
57
|
-
def
|
95
|
+
def save_todo_list(todo_file_path: str, todo_list: list[TodoTaskModel]):
|
58
96
|
with open(todo_file_path, "w") as f:
|
59
|
-
for todo_task in
|
60
|
-
f.write(todo_task_to_line(todo_task))
|
97
|
+
for todo_task in todo_list:
|
98
|
+
f.write(todo_task_to_line(todo_task) + "\n")
|
61
99
|
|
62
100
|
|
63
|
-
def
|
101
|
+
def line_to_todo_task(line: str) -> TodoTaskModel:
|
64
102
|
"""Parses a single todo.txt line into a TodoTask model."""
|
65
103
|
match = TODO_TXT_PATTERN.match(line)
|
66
104
|
if not match:
|
@@ -69,8 +107,8 @@ def parse_todo_line(line: str) -> TodoTask:
|
|
69
107
|
# Extract completion status
|
70
108
|
is_completed = groups["status"] == "x"
|
71
109
|
# Extract dates
|
72
|
-
date1 =
|
73
|
-
date2 =
|
110
|
+
date1 = _parse_date(groups["date1"])
|
111
|
+
date2 = _parse_date(groups["date2"])
|
74
112
|
# Determine creation_date and completion_date
|
75
113
|
completion_date, creation_date = None, None
|
76
114
|
if date2 is None:
|
@@ -87,7 +125,7 @@ def parse_todo_line(line: str) -> TodoTask:
|
|
87
125
|
key, val = keyval_str.split(":", 1)
|
88
126
|
keyval[key] = val
|
89
127
|
description = re.sub(r"\s*\+\S+|\s*@\S+|\s*\S+:\S+", "", raw_description).strip()
|
90
|
-
return
|
128
|
+
return TodoTaskModel(
|
91
129
|
priority=groups["priority"],
|
92
130
|
completed=is_completed,
|
93
131
|
description=description,
|
@@ -99,14 +137,14 @@ def parse_todo_line(line: str) -> TodoTask:
|
|
99
137
|
)
|
100
138
|
|
101
139
|
|
102
|
-
def
|
140
|
+
def _parse_date(date_str: str | None) -> datetime.date | None:
|
103
141
|
"""Parses a date string in the format YYYY-MM-DD."""
|
104
142
|
if date_str:
|
105
143
|
return datetime.date.fromisoformat(date_str)
|
106
144
|
return None
|
107
145
|
|
108
146
|
|
109
|
-
def todo_task_to_line(task:
|
147
|
+
def todo_task_to_line(task: TodoTaskModel) -> str:
|
110
148
|
"""Converts a TodoTask instance back into a todo.txt formatted line."""
|
111
149
|
parts = []
|
112
150
|
# Add completion mark if task is completed
|
@@ -133,3 +171,89 @@ def todo_task_to_line(task: TodoTask) -> str:
|
|
133
171
|
parts.append(f"{key}:{val}")
|
134
172
|
# Join all parts with a space
|
135
173
|
return " ".join(parts)
|
174
|
+
|
175
|
+
|
176
|
+
def get_visual_todo_list(todo_list: list[TodoTaskModel]) -> str:
|
177
|
+
if len(todo_list) == 0:
|
178
|
+
return "\n".join(["", " Empty todo list... 🌵🦖", ""])
|
179
|
+
max_desc_name_length = max(len(todo_task.description) for todo_task in todo_list)
|
180
|
+
if max_desc_name_length < len("DESCRIPTION"):
|
181
|
+
max_desc_name_length = len("DESCRIPTION")
|
182
|
+
# Headers
|
183
|
+
results = [
|
184
|
+
stylize_bold_green(
|
185
|
+
" ".join(
|
186
|
+
[
|
187
|
+
"".ljust(3), # priority
|
188
|
+
"".ljust(3), # completed
|
189
|
+
"COMPLETED AT".rjust(14), # completed date
|
190
|
+
"CREATED AT".rjust(14), # completed date
|
191
|
+
"DESCRIPTION".ljust(max_desc_name_length),
|
192
|
+
"PROJECT/CONTEXT/OTHERS",
|
193
|
+
]
|
194
|
+
)
|
195
|
+
)
|
196
|
+
]
|
197
|
+
for todo_task in todo_list:
|
198
|
+
completed = "[x]" if todo_task.completed else "[ ]"
|
199
|
+
priority = " " if todo_task.priority is None else f"({todo_task.priority})"
|
200
|
+
completion_date = stylize_yellow(_date_to_str(todo_task.completion_date))
|
201
|
+
creation_date = stylize_cyan(_date_to_str(todo_task.creation_date))
|
202
|
+
description = todo_task.description.ljust(max_desc_name_length)
|
203
|
+
additions = ", ".join(
|
204
|
+
[stylize_yellow(f"+{project}") for project in todo_task.projects]
|
205
|
+
+ [stylize_cyan(f"@{context}") for context in todo_task.contexts]
|
206
|
+
+ [stylize_magenta(f"{key}:{val}") for key, val in todo_task.keyval.items()]
|
207
|
+
)
|
208
|
+
results.append(
|
209
|
+
" ".join(
|
210
|
+
[
|
211
|
+
completed,
|
212
|
+
priority,
|
213
|
+
completion_date,
|
214
|
+
creation_date,
|
215
|
+
description,
|
216
|
+
additions,
|
217
|
+
]
|
218
|
+
)
|
219
|
+
)
|
220
|
+
return "\n".join(results)
|
221
|
+
|
222
|
+
|
223
|
+
def _date_to_str(date: datetime.date | None) -> str:
|
224
|
+
if date is None:
|
225
|
+
return "".ljust(14)
|
226
|
+
return date.strftime("%a %Y-%m-%d")
|
227
|
+
|
228
|
+
|
229
|
+
def add_durations(duration1: str, duration2: str) -> str:
|
230
|
+
total_seconds = _parse_duration(duration1) + _parse_duration(duration2)
|
231
|
+
# Format and return the result
|
232
|
+
return _format_duration(total_seconds)
|
233
|
+
|
234
|
+
|
235
|
+
def _parse_duration(duration: str) -> int:
|
236
|
+
"""Parse a duration string into total seconds."""
|
237
|
+
units = {"M": 2592000, "w": 604800, "d": 86400, "h": 3600, "m": 60, "s": 1}
|
238
|
+
total_seconds = 0
|
239
|
+
match = re.findall(r"(\d+)([Mwdhms])", duration)
|
240
|
+
for value, unit in match:
|
241
|
+
total_seconds += int(value) * units[unit]
|
242
|
+
return total_seconds
|
243
|
+
|
244
|
+
|
245
|
+
def _format_duration(total_seconds: int) -> str:
|
246
|
+
"""Format total seconds into a duration string."""
|
247
|
+
units = [
|
248
|
+
("w", 604800), # 7 days in a week
|
249
|
+
("d", 86400), # 24 hours in a day
|
250
|
+
("h", 3600), # 60 minutes in an hour
|
251
|
+
("m", 60), # 60 seconds in a minute
|
252
|
+
("s", 1), # seconds
|
253
|
+
]
|
254
|
+
result = []
|
255
|
+
for unit, value_in_seconds in units:
|
256
|
+
if total_seconds >= value_in_seconds:
|
257
|
+
amount, total_seconds = divmod(total_seconds, value_in_seconds)
|
258
|
+
result.append(f"{amount}{unit}")
|
259
|
+
return "".join(result) if result else "0s"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: zrb
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.0a4
|
4
4
|
Summary: Your Automation Powerhouse
|
5
5
|
Home-page: https://github.com/state-alchemists/zrb
|
6
6
|
License: AGPL-3.0-or-later
|
@@ -13,11 +13,16 @@ Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Provides-Extra: rag
|
16
17
|
Requires-Dist: autopep8 (>=2.0.4,<3.0.0)
|
18
|
+
Requires-Dist: beautifulsoup4 (>=4.12.3,<5.0.0)
|
17
19
|
Requires-Dist: black (>=24.10.0,<24.11.0)
|
20
|
+
Requires-Dist: chromadb (>=0.5.20,<0.6.0) ; extra == "rag"
|
21
|
+
Requires-Dist: fastapi[standard] (>=0.115.5,<0.116.0)
|
18
22
|
Requires-Dist: isort (>=5.13.2,<5.14.0)
|
19
23
|
Requires-Dist: libcst (>=1.5.0,<2.0.0)
|
20
24
|
Requires-Dist: litellm (>=1.52.12,<2.0.0)
|
25
|
+
Requires-Dist: pdfplumber (>=0.11.4,<0.12.0) ; extra == "rag"
|
21
26
|
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
22
27
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
23
28
|
Project-URL: Documentation, https://github.com/state-alchemists/zrb
|