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.
Files changed (53) hide show
  1. zrb/__init__.py +2 -2
  2. zrb/__main__.py +3 -1
  3. zrb/builtin/__init__.py +4 -2
  4. zrb/builtin/base64.py +4 -2
  5. zrb/builtin/{llm.py → llm/llm_chat.py} +18 -2
  6. zrb/builtin/llm/tool/cli.py +9 -0
  7. zrb/builtin/llm/tool/rag.py +189 -0
  8. zrb/builtin/llm/tool/web.py +74 -0
  9. zrb/builtin/md5.py +4 -2
  10. zrb/builtin/project/add/fastapp.py +1 -1
  11. zrb/builtin/project/add/fastapp_template/_zrb/helper.py +3 -3
  12. zrb/builtin/project/add/fastapp_template/_zrb/main.py +1 -1
  13. zrb/builtin/project/create/create.py +1 -1
  14. zrb/builtin/todo.py +122 -89
  15. zrb/config.py +12 -2
  16. zrb/runner/cli.py +8 -7
  17. zrb/runner/{web_server.py → web_app.py} +13 -13
  18. zrb/runner/{web_app → web_controller}/group_info_ui/controller.py +2 -2
  19. zrb/runner/{web_app → web_controller}/home_page/controller.py +2 -2
  20. zrb/runner/{web_app → web_controller}/task_ui/controller.py +2 -2
  21. zrb/task/cmd_task.py +24 -26
  22. zrb/task/http_check.py +5 -5
  23. zrb/task/llm_task.py +91 -36
  24. zrb/task/rsync_task.py +18 -18
  25. zrb/task/scaffolder.py +4 -4
  26. zrb/task/tcp_check.py +3 -5
  27. zrb/util/todo.py +139 -15
  28. {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/METADATA +6 -1
  29. {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/RECORD +53 -50
  30. /zrb/runner/{web_app → web_controller}/__init__.py +0 -0
  31. /zrb/runner/{web_app → web_controller}/group_info_ui/__init__.py +0 -0
  32. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_info.html +0 -0
  33. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_li.html +0 -0
  34. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_info.html +0 -0
  35. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_li.html +0 -0
  36. /zrb/runner/{web_app → web_controller}/group_info_ui/view.html +0 -0
  37. /zrb/runner/{web_app → web_controller}/home_page/__init__.py +0 -0
  38. /zrb/runner/{web_app → web_controller}/home_page/partial/group_info.html +0 -0
  39. /zrb/runner/{web_app → web_controller}/home_page/partial/group_li.html +0 -0
  40. /zrb/runner/{web_app → web_controller}/home_page/partial/task_info.html +0 -0
  41. /zrb/runner/{web_app → web_controller}/home_page/partial/task_li.html +0 -0
  42. /zrb/runner/{web_app → web_controller}/home_page/view.html +0 -0
  43. /zrb/runner/{web_app → web_controller}/static/favicon-32x32.png +0 -0
  44. /zrb/runner/{web_app → web_controller}/static/pico.min.css +0 -0
  45. /zrb/runner/{web_app → web_controller}/task_ui/__init__.py +0 -0
  46. /zrb/runner/{web_app → web_controller}/task_ui/partial/common-util.js +0 -0
  47. /zrb/runner/{web_app → web_controller}/task_ui/partial/input.html +0 -0
  48. /zrb/runner/{web_app → web_controller}/task_ui/partial/main.js +0 -0
  49. /zrb/runner/{web_app → web_controller}/task_ui/partial/show-existing-session.js +0 -0
  50. /zrb/runner/{web_app → web_controller}/task_ui/partial/visualize-history.js +0 -0
  51. /zrb/runner/{web_app → web_controller}/task_ui/view.html +0 -0
  52. {zrb-1.0.0a3.dist-info → zrb-1.0.0a4.dist-info}/WHEEL +0 -0
  53. {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
- DictList = list[dict[str, Any]]
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
- tool_schema: DictList | Callable[[AnySharedContext], DictList] = [],
42
- history: DictList | Callable[[AnySharedContext], DictList] = [],
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 completion
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.log_debug("USER MESSAGE", user_message)
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 = self._get_tool_schema(ctx)
99
- for tool_name, tool in available_tools.items():
100
- matched_tool_schema = [
101
- schema
102
- for schema in tool_schema
103
- if "function" in schema
104
- and "name" in schema["function"]
105
- and schema["function"]["name"] == tool_name
106
- ]
107
- if len(matched_tool_schema) == 0:
108
- tool_schema.append(callable_to_tool_schema(tool))
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 = completion(
112
- model=self._get_model(ctx), messages=messages, tools=tool_schema
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(ctx, self._model, "ollama_chat/llama3.1", auto_render=True)
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, self._system_prompt, "You are a helpful assistant", auto_render=True
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 _get_tool_schema(self, ctx: AnyContext) -> DictList:
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
- auto_render_remote_source_path: bool = True,
35
+ render_remote_source_path: bool = True,
36
36
  remote_destination_path: StrAttr | None = None,
37
- auto_render_remote_destination_path: bool = True,
37
+ render_remote_destination_path: bool = True,
38
38
  local_source_path: StrAttr | None = None,
39
- auto_render_local_source_path: bool = True,
39
+ render_local_source_path: bool = True,
40
40
  local_destination_path: StrAttr | None = None,
41
- auto_render_local_destination_path: bool = True,
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
- auto_render_shell=auto_render_shell,
62
+ render_shell=auto_render_shell,
63
63
  remote_host=remote_host,
64
- auto_render_remote_host=auto_render_remote_host,
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
- auto_render_remote_user=auto_render_remote_user,
68
+ render_remote_user=auto_render_remote_user,
69
69
  remote_password=remote_password,
70
- auto_render_remote_password=auto_render_remote_password,
70
+ render_remote_password=auto_render_remote_password,
71
71
  remote_ssh_key=remote_ssh_key,
72
- auto_render_remote_ssh_key=auto_render_remote_ssh_key,
72
+ render_remote_ssh_key=auto_render_remote_ssh_key,
73
73
  cwd=cwd,
74
- auto_render_cwd=auto_render_cwd,
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._auto_render_remote_source_path = auto_render_remote_source_path
85
+ self._render_remote_source_path = render_remote_source_path
86
86
  self._remote_destination_path = remote_destination_path
87
- self._auto_render_remote_destination_path = auto_render_remote_destination_path
87
+ self._render_remote_destination_path = render_remote_destination_path
88
88
  self._local_source_path = local_source_path
89
- self._auto_render_local_source_path = auto_render_local_source_path
89
+ self._render_local_source_path = render_local_source_path
90
90
  self._local_destination_path = local_destination_path
91
- self._auto_render_local_destination_path = auto_render_local_destination_path
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._auto_render_remote_source_path,
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._auto_render_remote_destination_path,
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._auto_render_local_source_path,
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._auto_render_local_destination_path,
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
- auto_render_source_path: bool = True,
29
+ render_source_path: bool = True,
30
30
  destination_path: StrAttr | None = None,
31
- auto_render_destination_path: bool = True,
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._auto_render_source_path = auto_render_source_path
69
+ self._render_source_path = render_source_path
70
70
  self._destination_path = destination_path
71
- self._auto_render_destination_path = auto_render_destination_path
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
- auto_render_host: bool = True,
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._auto_render_host = auto_render_host
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 TodoTask(BaseModel):
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 read_todo_from_file(todo_file_path: str) -> list[TodoTask]:
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
- todo_tasks: list[TodoTask] = []
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
- todo_tasks.append(parse_todo_line(todo_line))
46
- todo_tasks.sort(
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 todo_tasks
92
+ return todo_list
55
93
 
56
94
 
57
- def write_todo_to_file(todo_file_path: str, todo_task_list: list[TodoTask]):
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 todo_task_list:
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 parse_todo_line(line: str) -> TodoTask:
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 = parse_date(groups["date1"])
73
- date2 = parse_date(groups["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 TodoTask(
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 parse_date(date_str: str | None) -> datetime.date | None:
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: TodoTask) -> str:
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.0a3
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