zrb 1.15.3__py3-none-any.whl → 2.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.

Potentially problematic release.


This version of zrb might be problematic. Click here for more details.

Files changed (204) hide show
  1. zrb/__init__.py +118 -133
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +55 -1
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/llm/chat.py +147 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  9. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  10. zrb/builtin/searxng/config/settings.yml +5671 -0
  11. zrb/builtin/searxng/start.py +21 -0
  12. zrb/builtin/shell/autocomplete/bash.py +4 -3
  13. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  14. zrb/callback/callback.py +8 -1
  15. zrb/cmd/cmd_result.py +2 -1
  16. zrb/config/config.py +555 -169
  17. zrb/config/helper.py +84 -0
  18. zrb/config/web_auth_config.py +50 -35
  19. zrb/context/any_shared_context.py +20 -3
  20. zrb/context/context.py +39 -5
  21. zrb/context/print_fn.py +13 -0
  22. zrb/context/shared_context.py +17 -8
  23. zrb/group/any_group.py +3 -3
  24. zrb/group/group.py +3 -3
  25. zrb/input/any_input.py +5 -1
  26. zrb/input/base_input.py +18 -6
  27. zrb/input/option_input.py +41 -1
  28. zrb/input/text_input.py +7 -24
  29. zrb/llm/agent/__init__.py +9 -0
  30. zrb/llm/agent/agent.py +215 -0
  31. zrb/llm/agent/summarizer.py +20 -0
  32. zrb/llm/app/__init__.py +10 -0
  33. zrb/llm/app/completion.py +281 -0
  34. zrb/llm/app/confirmation/allow_tool.py +66 -0
  35. zrb/llm/app/confirmation/handler.py +178 -0
  36. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  37. zrb/llm/app/keybinding.py +34 -0
  38. zrb/llm/app/layout.py +117 -0
  39. zrb/llm/app/lexer.py +155 -0
  40. zrb/llm/app/redirection.py +28 -0
  41. zrb/llm/app/style.py +16 -0
  42. zrb/llm/app/ui.py +733 -0
  43. zrb/llm/config/__init__.py +4 -0
  44. zrb/llm/config/config.py +122 -0
  45. zrb/llm/config/limiter.py +247 -0
  46. zrb/llm/history_manager/__init__.py +4 -0
  47. zrb/llm/history_manager/any_history_manager.py +23 -0
  48. zrb/llm/history_manager/file_history_manager.py +91 -0
  49. zrb/llm/history_processor/summarizer.py +108 -0
  50. zrb/llm/note/__init__.py +3 -0
  51. zrb/llm/note/manager.py +122 -0
  52. zrb/llm/prompt/__init__.py +29 -0
  53. zrb/llm/prompt/claude_compatibility.py +92 -0
  54. zrb/llm/prompt/compose.py +55 -0
  55. zrb/llm/prompt/default.py +51 -0
  56. zrb/llm/prompt/markdown/file_extractor.md +112 -0
  57. zrb/llm/prompt/markdown/mandate.md +23 -0
  58. zrb/llm/prompt/markdown/persona.md +3 -0
  59. zrb/llm/prompt/markdown/repo_extractor.md +112 -0
  60. zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
  61. zrb/llm/prompt/markdown/summarizer.md +21 -0
  62. zrb/llm/prompt/note.py +41 -0
  63. zrb/llm/prompt/system_context.py +46 -0
  64. zrb/llm/prompt/zrb.py +41 -0
  65. zrb/llm/skill/__init__.py +3 -0
  66. zrb/llm/skill/manager.py +86 -0
  67. zrb/llm/task/__init__.py +4 -0
  68. zrb/llm/task/llm_chat_task.py +316 -0
  69. zrb/llm/task/llm_task.py +245 -0
  70. zrb/llm/tool/__init__.py +39 -0
  71. zrb/llm/tool/bash.py +75 -0
  72. zrb/llm/tool/code.py +266 -0
  73. zrb/llm/tool/file.py +419 -0
  74. zrb/llm/tool/note.py +70 -0
  75. zrb/{builtin/llm → llm}/tool/rag.py +33 -37
  76. zrb/llm/tool/search/brave.py +53 -0
  77. zrb/llm/tool/search/searxng.py +47 -0
  78. zrb/llm/tool/search/serpapi.py +47 -0
  79. zrb/llm/tool/skill.py +19 -0
  80. zrb/llm/tool/sub_agent.py +70 -0
  81. zrb/llm/tool/web.py +97 -0
  82. zrb/llm/tool/zrb_task.py +66 -0
  83. zrb/llm/util/attachment.py +101 -0
  84. zrb/llm/util/prompt.py +104 -0
  85. zrb/llm/util/stream_response.py +178 -0
  86. zrb/runner/cli.py +21 -20
  87. zrb/runner/common_util.py +24 -19
  88. zrb/runner/web_route/task_input_api_route.py +5 -5
  89. zrb/runner/web_util/user.py +7 -3
  90. zrb/session/any_session.py +12 -9
  91. zrb/session/session.py +38 -17
  92. zrb/task/any_task.py +24 -3
  93. zrb/task/base/context.py +42 -22
  94. zrb/task/base/execution.py +67 -55
  95. zrb/task/base/lifecycle.py +14 -7
  96. zrb/task/base/monitoring.py +12 -7
  97. zrb/task/base_task.py +113 -50
  98. zrb/task/base_trigger.py +16 -6
  99. zrb/task/cmd_task.py +6 -0
  100. zrb/task/http_check.py +11 -5
  101. zrb/task/make_task.py +5 -3
  102. zrb/task/rsync_task.py +30 -10
  103. zrb/task/scaffolder.py +7 -4
  104. zrb/task/scheduler.py +7 -4
  105. zrb/task/tcp_check.py +6 -4
  106. zrb/util/ascii_art/art/bee.txt +17 -0
  107. zrb/util/ascii_art/art/cat.txt +9 -0
  108. zrb/util/ascii_art/art/ghost.txt +16 -0
  109. zrb/util/ascii_art/art/panda.txt +17 -0
  110. zrb/util/ascii_art/art/rose.txt +14 -0
  111. zrb/util/ascii_art/art/unicorn.txt +15 -0
  112. zrb/util/ascii_art/banner.py +92 -0
  113. zrb/util/attr.py +54 -39
  114. zrb/util/cli/markdown.py +32 -0
  115. zrb/util/cli/text.py +30 -0
  116. zrb/util/cmd/command.py +33 -10
  117. zrb/util/file.py +61 -33
  118. zrb/util/git.py +2 -2
  119. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  120. zrb/util/match.py +78 -0
  121. zrb/util/run.py +3 -3
  122. zrb/util/string/conversion.py +1 -1
  123. zrb/util/truncate.py +23 -0
  124. zrb/util/yaml.py +204 -0
  125. zrb/xcom/xcom.py +10 -0
  126. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
  127. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
  128. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
  129. zrb/attr/__init__.py +0 -0
  130. zrb/builtin/llm/chat_session.py +0 -311
  131. zrb/builtin/llm/history.py +0 -71
  132. zrb/builtin/llm/input.py +0 -27
  133. zrb/builtin/llm/llm_ask.py +0 -187
  134. zrb/builtin/llm/previous-session.js +0 -21
  135. zrb/builtin/llm/tool/__init__.py +0 -0
  136. zrb/builtin/llm/tool/api.py +0 -71
  137. zrb/builtin/llm/tool/cli.py +0 -38
  138. zrb/builtin/llm/tool/code.py +0 -254
  139. zrb/builtin/llm/tool/file.py +0 -626
  140. zrb/builtin/llm/tool/sub_agent.py +0 -137
  141. zrb/builtin/llm/tool/web.py +0 -195
  142. zrb/builtin/project/__init__.py +0 -0
  143. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  144. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  145. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  146. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  147. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  148. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  149. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  150. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  151. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  152. zrb/builtin/project/create/__init__.py +0 -0
  153. zrb/builtin/shell/__init__.py +0 -0
  154. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  155. zrb/callback/__init__.py +0 -0
  156. zrb/cmd/__init__.py +0 -0
  157. zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
  158. zrb/config/default_prompt/interactive_system_prompt.md +0 -35
  159. zrb/config/default_prompt/persona.md +0 -1
  160. zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
  161. zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
  162. zrb/config/default_prompt/summarization_prompt.md +0 -16
  163. zrb/config/default_prompt/system_prompt.md +0 -32
  164. zrb/config/llm_config.py +0 -243
  165. zrb/config/llm_context/config.py +0 -129
  166. zrb/config/llm_context/config_parser.py +0 -46
  167. zrb/config/llm_rate_limitter.py +0 -137
  168. zrb/content_transformer/__init__.py +0 -0
  169. zrb/context/__init__.py +0 -0
  170. zrb/dot_dict/__init__.py +0 -0
  171. zrb/env/__init__.py +0 -0
  172. zrb/group/__init__.py +0 -0
  173. zrb/input/__init__.py +0 -0
  174. zrb/runner/__init__.py +0 -0
  175. zrb/runner/web_route/__init__.py +0 -0
  176. zrb/runner/web_route/home_page/__init__.py +0 -0
  177. zrb/session/__init__.py +0 -0
  178. zrb/session_state_log/__init__.py +0 -0
  179. zrb/session_state_logger/__init__.py +0 -0
  180. zrb/task/__init__.py +0 -0
  181. zrb/task/base/__init__.py +0 -0
  182. zrb/task/llm/__init__.py +0 -0
  183. zrb/task/llm/agent.py +0 -243
  184. zrb/task/llm/config.py +0 -103
  185. zrb/task/llm/conversation_history.py +0 -128
  186. zrb/task/llm/conversation_history_model.py +0 -242
  187. zrb/task/llm/default_workflow/coding.md +0 -24
  188. zrb/task/llm/default_workflow/copywriting.md +0 -17
  189. zrb/task/llm/default_workflow/researching.md +0 -18
  190. zrb/task/llm/error.py +0 -95
  191. zrb/task/llm/history_summarization.py +0 -216
  192. zrb/task/llm/print_node.py +0 -101
  193. zrb/task/llm/prompt.py +0 -325
  194. zrb/task/llm/tool_wrapper.py +0 -220
  195. zrb/task/llm/typing.py +0 -3
  196. zrb/task/llm_task.py +0 -341
  197. zrb/task_status/__init__.py +0 -0
  198. zrb/util/__init__.py +0 -0
  199. zrb/util/cli/__init__.py +0 -0
  200. zrb/util/cmd/__init__.py +0 -0
  201. zrb/util/codemod/__init__.py +0 -0
  202. zrb/util/string/__init__.py +0 -0
  203. zrb/xcom/__init__.py +0 -0
  204. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/task/base/context.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import os
2
- from typing import TYPE_CHECKING
2
+ from typing import TYPE_CHECKING, Any
3
3
 
4
4
  from zrb.context.any_context import AnyContext
5
5
  from zrb.context.any_shared_context import AnySharedContext
@@ -26,25 +26,33 @@ def build_task_context(task: AnyTask, session: AnySession) -> AnyContext:
26
26
 
27
27
 
28
28
  def fill_shared_context_inputs(
29
- task: AnyTask, shared_context: AnySharedContext, str_kwargs: dict[str, str] = {}
29
+ shared_ctx: AnySharedContext,
30
+ task: AnyTask,
31
+ str_kwargs: dict[str, str] | None = None,
32
+ kwargs: dict[str, Any] | None = None,
30
33
  ):
31
34
  """
32
- Populates the shared context with input values provided via kwargs.
35
+ Populates the shared context with input values provided via str_kwargs.
33
36
  """
37
+ str_kwarg_dict = str_kwargs if str_kwargs is not None else {}
38
+ kwarg_dict = kwargs if kwargs is not None else {}
34
39
  for task_input in task.inputs:
35
- if task_input.name not in shared_context.input:
36
- str_value = str_kwargs.get(task_input.name, None)
37
- task_input.update_shared_context(shared_context, str_value)
40
+ if task_input.name not in shared_ctx.input:
41
+ task_input.update_shared_context(
42
+ shared_ctx,
43
+ value=kwarg_dict.get(task_input.name, None),
44
+ str_value=str_kwarg_dict.get(task_input.name, None),
45
+ )
38
46
 
39
47
 
40
- def fill_shared_context_envs(shared_context: AnySharedContext):
48
+ def fill_shared_context_envs(shared_ctx: AnySharedContext):
41
49
  """
42
50
  Injects OS environment variables into the shared context if they don't already exist.
43
51
  """
44
52
  os_env_map = {
45
- key: val for key, val in os.environ.items() if key not in shared_context.env
53
+ key: val for key, val in os.environ.items() if key not in shared_ctx.env
46
54
  }
47
- shared_context.env.update(os_env_map)
55
+ shared_ctx.env.update(os_env_map)
48
56
 
49
57
 
50
58
  def combine_inputs(
@@ -71,24 +79,36 @@ def combine_inputs(
71
79
  input_names.append(task_input.name) # Update names list
72
80
 
73
81
 
82
+ def combine_envs(
83
+ existing_envs: list[AnyEnv],
84
+ new_envs: list[AnyEnv | None] | AnyEnv | None,
85
+ ):
86
+ """
87
+ Combines new envs into an existing list.
88
+ Modifies the existing_envs list in place.
89
+ """
90
+ if isinstance(new_envs, AnyEnv):
91
+ existing_envs.append(new_envs)
92
+ elif new_envs is None:
93
+ pass
94
+ else:
95
+ # new_envs is a list
96
+ for env in new_envs:
97
+ if env is not None:
98
+ existing_envs.append(env)
99
+
100
+
74
101
  def get_combined_envs(task: "BaseTask") -> list[AnyEnv]:
75
102
  """
76
103
  Aggregates environment variables from the task and its upstreams.
77
104
  """
78
- envs = []
105
+ envs: list[AnyEnv] = []
79
106
  for upstream in task.upstreams:
80
- envs.extend(upstream.envs) # Use extend for list concatenation
81
-
82
- # Access _envs directly as task is BaseTask
83
- task_envs: list[AnyEnv | None] | AnyEnv | None = task._envs
84
- if isinstance(task_envs, AnyEnv):
85
- envs.append(task_envs)
86
- elif isinstance(task_envs, list):
87
- # Filter out None while extending
88
- envs.extend(env for env in task_envs if env is not None)
89
-
90
- # Filter out None values efficiently from the combined list
91
- return [env for env in envs if env is not None]
107
+ combine_envs(envs, upstream.envs)
108
+
109
+ combine_envs(envs, task._envs)
110
+
111
+ return envs
92
112
 
93
113
 
94
114
  def get_combined_inputs(task: "BaseTask") -> list[AnyInput]:
@@ -53,7 +53,9 @@ def check_execute_condition(task: "BaseTask", session: AnySession) -> bool:
53
53
  Evaluates the task's execute_condition attribute.
54
54
  """
55
55
  ctx = task.get_ctx(session)
56
- execute_condition_attr = getattr(task, "_execute_condition", True)
56
+ execute_condition_attr = (
57
+ task._execute_condition if task._execute_condition is not None else True
58
+ )
57
59
  return get_bool_attr(ctx, execute_condition_attr, True, auto_render=True)
58
60
 
59
61
 
@@ -63,8 +65,12 @@ async def execute_action_until_ready(task: "BaseTask", session: AnySession):
63
65
  """
64
66
  ctx = task.get_ctx(session)
65
67
  readiness_checks = task.readiness_checks
66
- readiness_check_delay = getattr(task, "_readiness_check_delay", 0.5)
67
- monitor_readiness = getattr(task, "_monitor_readiness", False)
68
+ readiness_check_delay = (
69
+ task._readiness_check_delay if task._readiness_check_delay is not None else 0.5
70
+ )
71
+ monitor_readiness = (
72
+ task._monitor_readiness if task._monitor_readiness is not None else False
73
+ )
68
74
 
69
75
  if not readiness_checks: # Simplified check for empty list
70
76
  ctx.log_info("No readiness checks")
@@ -82,56 +88,61 @@ async def execute_action_until_ready(task: "BaseTask", session: AnySession):
82
88
  run_async(execute_action_with_retry(task, session))
83
89
  )
84
90
 
85
- await asyncio.sleep(readiness_check_delay)
86
-
87
- readiness_check_coros = [
88
- run_async(check.exec_chain(session)) for check in readiness_checks
89
- ]
90
-
91
- # Wait primarily for readiness checks to complete
92
- ctx.log_info("Waiting for readiness checks")
93
- readiness_passed = False
94
91
  try:
95
- # Gather results, but primarily interested in completion/errors
96
- await asyncio.gather(*readiness_check_coros)
97
- # Check if all readiness tasks actually completed successfully
98
- all_readiness_completed = all(
99
- session.get_task_status(check).is_completed for check in readiness_checks
100
- )
101
- if all_readiness_completed:
102
- ctx.log_info("Readiness checks completed successfully")
103
- readiness_passed = True
104
- # Mark task as ready only if checks passed and action didn't fail during checks
105
- if not session.get_task_status(task).is_failed:
106
- ctx.log_info("Marked as ready")
107
- session.get_task_status(task).mark_as_ready()
108
- else:
109
- ctx.log_warning(
110
- "One or more readiness checks did not complete successfully."
111
- )
112
-
113
- except Exception as e:
114
- ctx.log_error(f"Readiness check failed with exception: {e}")
115
- # If readiness checks fail with an exception, the task is not ready.
116
- # The action_coro might still be running or have failed.
117
- # execute_action_with_retry handles marking the main task status.
92
+ await asyncio.sleep(readiness_check_delay)
118
93
 
119
- # Defer the main action coroutine; it will be awaited later if needed
120
- session.defer_action(task, action_coro)
121
-
122
- # Start monitoring only if readiness passed and monitoring is enabled
123
- if readiness_passed and monitor_readiness:
124
- # Import dynamically to avoid circular dependency if monitoring imports execution
125
- from zrb.task.base.monitoring import monitor_task_readiness
94
+ readiness_check_coros = [
95
+ run_async(check.exec_chain(session)) for check in readiness_checks
96
+ ]
126
97
 
127
- monitor_coro = asyncio.create_task(
128
- run_async(monitor_task_readiness(task, session, action_coro))
129
- )
130
- session.defer_monitoring(task, monitor_coro)
98
+ # Wait primarily for readiness checks to complete
99
+ ctx.log_info("Waiting for readiness checks")
100
+ readiness_passed = False
101
+ try:
102
+ # Gather results, but primarily interested in completion/errors
103
+ await asyncio.gather(*readiness_check_coros)
104
+ # Check if all readiness tasks actually completed successfully
105
+ all_readiness_completed = all(
106
+ session.get_task_status(check).is_completed
107
+ for check in readiness_checks
108
+ )
109
+ if all_readiness_completed:
110
+ ctx.log_info("Readiness checks completed successfully")
111
+ readiness_passed = True
112
+ # Mark task as ready only if checks passed and action didn't fail during checks
113
+ if not session.get_task_status(task).is_failed:
114
+ ctx.log_info("Marked as ready")
115
+ session.get_task_status(task).mark_as_ready()
116
+ else:
117
+ ctx.log_warning(
118
+ "One or more readiness checks did not complete successfully."
119
+ )
120
+
121
+ except Exception as e:
122
+ ctx.log_error(f"Readiness check failed with exception: {e}")
123
+ # If readiness checks fail with an exception, the task is not ready.
124
+ # The action_coro might still be running or have failed.
125
+ # execute_action_with_retry handles marking the main task status.
126
+
127
+ # Defer the main action coroutine; it will be awaited later if needed
128
+ session.defer_action(task, action_coro)
129
+
130
+ # Start monitoring only if readiness passed and monitoring is enabled
131
+ if readiness_passed and monitor_readiness:
132
+ # Import dynamically to avoid circular dependency if monitoring imports execution
133
+ from zrb.task.base.monitoring import monitor_task_readiness
134
+
135
+ monitor_coro = asyncio.create_task(
136
+ run_async(monitor_task_readiness(task, session, action_coro))
137
+ )
138
+ session.defer_monitoring(task, monitor_coro)
131
139
 
132
- # The result here is primarily about readiness check completion.
133
- # The actual task result is handled by the deferred action_coro.
134
- return None
140
+ # The result here is primarily about readiness check completion.
141
+ # The actual task result is handled by the deferred action_coro.
142
+ return None
143
+ except (asyncio.CancelledError, KeyboardInterrupt, GeneratorExit):
144
+ action_coro.cancel()
145
+ raise
135
146
 
136
147
 
137
148
  async def execute_action_with_retry(task: "BaseTask", session: AnySession) -> Any:
@@ -140,8 +151,8 @@ async def execute_action_with_retry(task: "BaseTask", session: AnySession) -> An
140
151
  handling success (triggering successors) and failure (triggering fallbacks).
141
152
  """
142
153
  ctx = task.get_ctx(session)
143
- retries = getattr(task, "_retries", 2)
144
- retry_period = getattr(task, "_retry_period", 0)
154
+ retries = task._retries if task._retries is not None else 2
155
+ retry_period = task._retry_period if task._retry_period is not None else 0
145
156
  max_attempt = retries + 1
146
157
  ctx.set_max_attempt(max_attempt)
147
158
 
@@ -163,15 +174,16 @@ async def execute_action_with_retry(task: "BaseTask", session: AnySession) -> An
163
174
  session.get_task_status(task).mark_as_completed()
164
175
 
165
176
  # Store result in XCom
166
- task_xcom: Xcom = ctx.xcom.get(task.name)
167
- task_xcom.push(result)
177
+ task_xcom: Xcom | None = ctx.xcom.get(task.name)
178
+ if task_xcom is not None:
179
+ task_xcom.push(result)
168
180
 
169
181
  # Skip fallbacks and execute successors on success
170
182
  skip_fallbacks(task, session)
171
183
  await run_async(execute_successors(task, session))
172
184
  return result
173
185
 
174
- except (asyncio.CancelledError, KeyboardInterrupt):
186
+ except (asyncio.CancelledError, KeyboardInterrupt, GeneratorExit):
175
187
  ctx.log_warning("Task cancelled or interrupted")
176
188
  session.get_task_status(task).mark_as_failed() # Mark as failed on cancel
177
189
  # Do not trigger fallbacks/successors on cancellation
@@ -201,7 +213,7 @@ async def run_default_action(task: "BaseTask", ctx: AnyContext) -> Any:
201
213
  This is the default implementation called by BaseTask._exec_action.
202
214
  Subclasses like LLMTask override _exec_action with their own logic.
203
215
  """
204
- action = getattr(task, "_action", None)
216
+ action = task._action
205
217
  if action is None:
206
218
  ctx.log_debug("No action defined for this task.")
207
219
  return None
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  from typing import Any
3
3
 
4
+ from zrb.context.print_fn import PrintFn
4
5
  from zrb.context.shared_context import SharedContext
5
6
  from zrb.session.any_session import AnySession
6
7
  from zrb.session.session import Session
@@ -12,7 +13,9 @@ from zrb.util.run import run_async
12
13
  async def run_and_cleanup(
13
14
  task: AnyTask,
14
15
  session: AnySession | None = None,
15
- str_kwargs: dict[str, str] = {},
16
+ print_fn: PrintFn | None = None,
17
+ str_kwargs: dict[str, str] | None = None,
18
+ kwargs: dict[str, Any] | None = None,
16
19
  ) -> Any:
17
20
  """
18
21
  Wrapper for async_run that ensures session termination and cleanup of
@@ -20,10 +23,12 @@ async def run_and_cleanup(
20
23
  """
21
24
  # Ensure a session exists
22
25
  if session is None:
23
- session = Session(shared_ctx=SharedContext())
26
+ session = Session(shared_ctx=SharedContext(print_fn=print_fn))
24
27
 
25
28
  # Create the main task execution coroutine
26
- main_task_coro = asyncio.create_task(run_task_async(task, session, str_kwargs))
29
+ main_task_coro = asyncio.create_task(
30
+ run_task_async(task, session, print_fn, str_kwargs, kwargs)
31
+ )
27
32
 
28
33
  try:
29
34
  result = await main_task_coro
@@ -67,17 +72,19 @@ async def run_and_cleanup(
67
72
  async def run_task_async(
68
73
  task: AnyTask,
69
74
  session: AnySession | None = None,
70
- str_kwargs: dict[str, str] = {},
75
+ print_fn: PrintFn | None = None,
76
+ str_kwargs: dict[str, str] | None = None,
77
+ kwargs: dict[str, Any] | None = None,
71
78
  ) -> Any:
72
79
  """
73
80
  Asynchronous entry point for running a task (`task.async_run()`).
74
81
  Sets up the session and initiates the root task execution chain.
75
82
  """
76
83
  if session is None:
77
- session = Session(shared_ctx=SharedContext())
84
+ session = Session(shared_ctx=SharedContext(print_fn=print_fn))
78
85
 
79
86
  # Populate shared context with inputs and environment variables
80
- fill_shared_context_inputs(task, session.shared_ctx, str_kwargs)
87
+ fill_shared_context_inputs(session.shared_ctx, task, str_kwargs, kwargs)
81
88
  fill_shared_context_envs(session.shared_ctx) # Inject OS env vars
82
89
 
83
90
  # Start the execution chain from the root tasks
@@ -172,7 +179,7 @@ async def log_session_state(task: AnyTask, session: AnySession):
172
179
  try:
173
180
  while not session.is_terminated:
174
181
  session.state_logger.write(session.as_state_log())
175
- await asyncio.sleep(0.1) # Log interval
182
+ await asyncio.sleep(0) # Log interval
176
183
  # Log one final time after termination signal
177
184
  session.state_logger.write(session.as_state_log())
178
185
  except asyncio.CancelledError:
@@ -17,9 +17,13 @@ async def monitor_task_readiness(
17
17
  """
18
18
  ctx = task.get_ctx(session)
19
19
  readiness_checks = task.readiness_checks
20
- readiness_check_period = getattr(task, "_readiness_check_period", 5.0)
21
- readiness_failure_threshold = getattr(task, "_readiness_failure_threshold", 1)
22
- readiness_timeout = getattr(task, "_readiness_timeout", 60)
20
+ readiness_check_period = (
21
+ task._readiness_check_period if task._readiness_check_period else 5.0
22
+ )
23
+ readiness_failure_threshold = (
24
+ task._readiness_failure_threshold if task._readiness_failure_threshold else 1
25
+ )
26
+ readiness_timeout = task._readiness_timeout if task._readiness_timeout else 60
23
27
 
24
28
  if not readiness_checks:
25
29
  ctx.log_debug("No readiness checks defined, monitoring is not applicable.")
@@ -41,8 +45,9 @@ async def monitor_task_readiness(
41
45
  session.get_task_status(check).reset_history()
42
46
  session.get_task_status(check).reset()
43
47
  # Clear previous XCom data for the check task if needed
44
- check_xcom: Xcom = ctx.xcom.get(check.name)
45
- check_xcom.clear()
48
+ check_xcom: Xcom | None = ctx.xcom.get(check.name)
49
+ if check_xcom is not None:
50
+ check_xcom.clear()
46
51
 
47
52
  readiness_check_coros = [
48
53
  run_async(check.exec_chain(session)) for check in readiness_checks
@@ -77,7 +82,7 @@ async def monitor_task_readiness(
77
82
  )
78
83
  # Ensure check tasks are marked as failed on timeout
79
84
  for check in readiness_checks:
80
- if not session.get_task_status(check).is_finished:
85
+ if not session.get_task_status(check).is_ready:
81
86
  session.get_task_status(check).mark_as_failed()
82
87
 
83
88
  except (asyncio.CancelledError, KeyboardInterrupt):
@@ -92,7 +97,7 @@ async def monitor_task_readiness(
92
97
  )
93
98
  # Mark checks as failed
94
99
  for check in readiness_checks:
95
- if not session.get_task_status(check).is_finished:
100
+ if not session.get_task_status(check).is_ready:
96
101
  session.get_task_status(check).mark_as_failed()
97
102
 
98
103
  # If failure threshold is reached
zrb/task/base_task.py CHANGED
@@ -3,8 +3,9 @@ import inspect
3
3
  from collections.abc import Callable
4
4
  from typing import Any
5
5
 
6
- from zrb.attr.type import BoolAttr, fstring
6
+ from zrb.attr.type import fstring
7
7
  from zrb.context.any_context import AnyContext
8
+ from zrb.context.print_fn import PrintFn
8
9
  from zrb.env.any_env import AnyEnv
9
10
  from zrb.input.any_input import AnyInput
10
11
  from zrb.session.any_session import AnySession
@@ -21,6 +22,7 @@ from zrb.task.base.execution import (
21
22
  )
22
23
  from zrb.task.base.lifecycle import execute_root_tasks, run_and_cleanup, run_task_async
23
24
  from zrb.task.base.operators import handle_lshift, handle_rshift
25
+ from zrb.util.string.conversion import to_snake_case
24
26
 
25
27
 
26
28
  class BaseTask(AnyTask):
@@ -54,7 +56,7 @@ class BaseTask(AnyTask):
54
56
  input: list[AnyInput | None] | AnyInput | None = None,
55
57
  env: list[AnyEnv | None] | AnyEnv | None = None,
56
58
  action: fstring | Callable[[AnyContext], Any] | None = None,
57
- execute_condition: BoolAttr = True,
59
+ execute_condition: bool | str | Callable[[AnyContext], bool] = True,
58
60
  retries: int = 2,
59
61
  retry_period: float = 0,
60
62
  readiness_check: list[AnyTask] | AnyTask | None = None,
@@ -66,10 +68,20 @@ class BaseTask(AnyTask):
66
68
  upstream: list[AnyTask] | AnyTask | None = None,
67
69
  fallback: list[AnyTask] | AnyTask | None = None,
68
70
  successor: list[AnyTask] | AnyTask | None = None,
71
+ print_fn: PrintFn | None = None,
69
72
  ):
70
- caller_frame = inspect.stack()[1]
71
- self.__decl_file = caller_frame.filename
72
- self.__decl_line = caller_frame.lineno
73
+ # Optimized stack retrieval
74
+ frame = inspect.currentframe()
75
+ if frame is not None:
76
+ caller_frame = frame.f_back
77
+ self.__decl_file = (
78
+ caller_frame.f_code.co_filename if caller_frame else "unknown"
79
+ )
80
+ self.__decl_line = caller_frame.f_lineno if caller_frame else 0
81
+ else:
82
+ self.__decl_file = "unknown"
83
+ self.__decl_line = 0
84
+
73
85
  self._name = name
74
86
  self._color = color
75
87
  self._icon = icon
@@ -79,10 +91,10 @@ class BaseTask(AnyTask):
79
91
  self._envs = env
80
92
  self._retries = retries
81
93
  self._retry_period = retry_period
82
- self._upstreams = upstream
83
- self._fallbacks = fallback
84
- self._successors = successor
85
- self._readiness_checks = readiness_check
94
+ self._upstreams = self._ensure_task_list(upstream)
95
+ self._fallbacks = self._ensure_task_list(fallback)
96
+ self._successors = self._ensure_task_list(successor)
97
+ self._readiness_checks = self._ensure_task_list(readiness_check)
86
98
  self._readiness_check_delay = readiness_check_delay
87
99
  self._readiness_check_period = readiness_check_period
88
100
  self._readiness_failure_threshold = readiness_failure_threshold
@@ -90,6 +102,14 @@ class BaseTask(AnyTask):
90
102
  self._monitor_readiness = monitor_readiness
91
103
  self._execute_condition = execute_condition
92
104
  self._action = action
105
+ self._print_fn = print_fn
106
+
107
+ def _ensure_task_list(self, tasks: AnyTask | list[AnyTask] | None) -> list[AnyTask]:
108
+ if tasks is None:
109
+ return []
110
+ if isinstance(tasks, list):
111
+ return tasks
112
+ return [tasks]
93
113
 
94
114
  def __repr__(self):
95
115
  return f"<{self.__class__.__name__} name={self.name}>"
@@ -131,18 +151,10 @@ class BaseTask(AnyTask):
131
151
  @property
132
152
  def fallbacks(self) -> list[AnyTask]:
133
153
  """Returns the list of fallback tasks."""
134
- if self._fallbacks is None:
135
- return []
136
- elif isinstance(self._fallbacks, list):
137
- return self._fallbacks
138
- return [self._fallbacks] # Assume single task
154
+ return self._fallbacks
139
155
 
140
156
  def append_fallback(self, fallbacks: AnyTask | list[AnyTask]):
141
157
  """Appends fallback tasks, ensuring no duplicates."""
142
- if self._fallbacks is None:
143
- self._fallbacks = []
144
- elif not isinstance(self._fallbacks, list):
145
- self._fallbacks = [self._fallbacks]
146
158
  to_add = fallbacks if isinstance(fallbacks, list) else [fallbacks]
147
159
  for fb in to_add:
148
160
  if fb not in self._fallbacks:
@@ -151,18 +163,10 @@ class BaseTask(AnyTask):
151
163
  @property
152
164
  def successors(self) -> list[AnyTask]:
153
165
  """Returns the list of successor tasks."""
154
- if self._successors is None:
155
- return []
156
- elif isinstance(self._successors, list):
157
- return self._successors
158
- return [self._successors] # Assume single task
166
+ return self._successors
159
167
 
160
168
  def append_successor(self, successors: AnyTask | list[AnyTask]):
161
169
  """Appends successor tasks, ensuring no duplicates."""
162
- if self._successors is None:
163
- self._successors = []
164
- elif not isinstance(self._successors, list):
165
- self._successors = [self._successors]
166
170
  to_add = successors if isinstance(successors, list) else [successors]
167
171
  for succ in to_add:
168
172
  if succ not in self._successors:
@@ -171,18 +175,10 @@ class BaseTask(AnyTask):
171
175
  @property
172
176
  def readiness_checks(self) -> list[AnyTask]:
173
177
  """Returns the list of readiness check tasks."""
174
- if self._readiness_checks is None:
175
- return []
176
- elif isinstance(self._readiness_checks, list):
177
- return self._readiness_checks
178
- return [self._readiness_checks] # Assume single task
178
+ return self._readiness_checks
179
179
 
180
180
  def append_readiness_check(self, readiness_checks: AnyTask | list[AnyTask]):
181
181
  """Appends readiness check tasks, ensuring no duplicates."""
182
- if self._readiness_checks is None:
183
- self._readiness_checks = []
184
- elif not isinstance(self._readiness_checks, list):
185
- self._readiness_checks = [self._readiness_checks]
186
182
  to_add = (
187
183
  readiness_checks
188
184
  if isinstance(readiness_checks, list)
@@ -195,18 +191,10 @@ class BaseTask(AnyTask):
195
191
  @property
196
192
  def upstreams(self) -> list[AnyTask]:
197
193
  """Returns the list of upstream tasks."""
198
- if self._upstreams is None:
199
- return []
200
- elif isinstance(self._upstreams, list):
201
- return self._upstreams
202
- return [self._upstreams] # Assume single task
194
+ return self._upstreams
203
195
 
204
196
  def append_upstream(self, upstreams: AnyTask | list[AnyTask]):
205
197
  """Appends upstream tasks, ensuring no duplicates."""
206
- if self._upstreams is None:
207
- self._upstreams = []
208
- elif not isinstance(self._upstreams, list):
209
- self._upstreams = [self._upstreams]
210
198
  to_add = upstreams if isinstance(upstreams, list) else [upstreams]
211
199
  for up in to_add:
212
200
  if up not in self._upstreams:
@@ -216,7 +204,10 @@ class BaseTask(AnyTask):
216
204
  return build_task_context(self, session)
217
205
 
218
206
  def run(
219
- self, session: AnySession | None = None, str_kwargs: dict[str, str] = {}
207
+ self,
208
+ session: AnySession | None = None,
209
+ str_kwargs: dict[str, str] | None = None,
210
+ kwargs: dict[str, Any] | None = None,
220
211
  ) -> Any:
221
212
  """
222
213
  Synchronously runs the task and its dependencies, handling async setup and cleanup.
@@ -235,12 +226,29 @@ class BaseTask(AnyTask):
235
226
  Any: The final result of the main task execution.
236
227
  """
237
228
  # Use asyncio.run() to execute the async cleanup wrapper
238
- return asyncio.run(run_and_cleanup(self, session, str_kwargs))
229
+ return asyncio.run(
230
+ run_and_cleanup(
231
+ self,
232
+ session=session,
233
+ print_fn=self._print_fn,
234
+ str_kwargs=str_kwargs,
235
+ kwargs=kwargs,
236
+ )
237
+ )
239
238
 
240
239
  async def async_run(
241
- self, session: AnySession | None = None, str_kwargs: dict[str, str] = {}
240
+ self,
241
+ session: AnySession | None = None,
242
+ str_kwargs: dict[str, str] | None = None,
243
+ kwargs: dict[str, Any] | None = None,
242
244
  ) -> Any:
243
- return await run_task_async(self, session, str_kwargs)
245
+ return await run_task_async(
246
+ self,
247
+ session=session,
248
+ print_fn=self._print_fn,
249
+ str_kwargs=str_kwargs,
250
+ kwargs=kwargs,
251
+ )
244
252
 
245
253
  async def exec_root_tasks(self, session: AnySession):
246
254
  return await execute_root_tasks(self, session)
@@ -266,6 +274,8 @@ class BaseTask(AnyTask):
266
274
  try:
267
275
  # Delegate to the helper function for the default behavior
268
276
  return await run_default_action(self, ctx)
277
+ except (KeyboardInterrupt, GeneratorExit):
278
+ raise
269
279
  except BaseException as e:
270
280
  additional_error_note = (
271
281
  f"Task: {self.name} ({self.__decl_file}:{self.__decl_line})"
@@ -276,7 +286,60 @@ class BaseTask(AnyTask):
276
286
  # Add definition location to the error
277
287
  if hasattr(e, "add_note"):
278
288
  e.add_note(additional_error_note)
279
- else:
289
+ elif hasattr(e, "__notes__"):
280
290
  # fallback: use the __notes__ attribute directly
281
291
  e.__notes__ = getattr(e, "__notes__", []) + [additional_error_note]
282
292
  raise e
293
+
294
+ def to_function(self) -> Callable[..., Any]:
295
+ from zrb.context.shared_context import SharedContext
296
+ from zrb.session.session import Session
297
+
298
+ def task_runner_fn(**kwargs) -> Any:
299
+ task_kwargs = self._get_func_kwargs(kwargs)
300
+ shared_ctx = SharedContext(print_fn=self._print_fn)
301
+ session = Session(shared_ctx=shared_ctx)
302
+ return self.run(session=session, kwargs=task_kwargs)
303
+
304
+ task_runner_fn.__doc__ = self._create_fn_docstring()
305
+ task_runner_fn.__signature__ = self._create_fn_signature()
306
+ task_runner_fn.__name__ = self.name
307
+ return task_runner_fn
308
+
309
+ def _get_func_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
310
+ fn_kwargs = {}
311
+ for inp in self.inputs:
312
+ snake_input_name = to_snake_case(inp.name)
313
+ if snake_input_name in kwargs:
314
+ fn_kwargs[inp.name] = kwargs[snake_input_name]
315
+ return fn_kwargs
316
+
317
+ def _create_fn_docstring(self) -> str:
318
+ from zrb.context.shared_context import SharedContext
319
+
320
+ stub_shared_ctx = SharedContext(print_fn=self._print_fn)
321
+ str_input_default_values = {}
322
+ for inp in self.inputs:
323
+ str_input_default_values[inp.name] = inp.get_default_str(stub_shared_ctx)
324
+ # Create docstring
325
+ doc = f"{self.description}\n\n"
326
+ if len(self.inputs) > 0:
327
+ doc += "Args:\n"
328
+ for inp in self.inputs:
329
+ str_input_default = str_input_default_values.get(inp.name, "")
330
+ doc += (
331
+ f" {inp.name}: {inp.description} (default: {str_input_default})"
332
+ )
333
+ doc += "\n"
334
+ return doc
335
+
336
+ def _create_fn_signature(self) -> inspect.Signature:
337
+ params = []
338
+ for inp in self.inputs:
339
+ params.append(
340
+ inspect.Parameter(
341
+ name=to_snake_case(inp.name),
342
+ kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
343
+ )
344
+ )
345
+ return inspect.Signature(params)