zrb 1.16.1__py3-none-any.whl → 1.16.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
zrb/builtin/__init__.py CHANGED
@@ -9,6 +9,7 @@ from zrb.builtin.git import (
9
9
  from zrb.builtin.git_subtree import git_add_subtree, git_pull_subtree, git_push_subtree
10
10
  from zrb.builtin.http import generate_curl, http_request
11
11
  from zrb.builtin.jwt import decode_jwt, encode_jwt, validate_jwt
12
+ from zrb.builtin.llm.chat_trigger import llm_chat_trigger
12
13
  from zrb.builtin.llm.llm_ask import llm_ask
13
14
  from zrb.builtin.md5 import hash_md5, sum_md5, validate_md5
14
15
  from zrb.builtin.project.add.fastapp.fastapp_task import add_fastapp_to_project
@@ -7,13 +7,20 @@ conversation flow via XCom.
7
7
 
8
8
  import asyncio
9
9
  import sys
10
+ from typing import TYPE_CHECKING, Any
10
11
 
12
+ from zrb.builtin.llm.chat_trigger import llm_chat_trigger
11
13
  from zrb.config.llm_config import llm_config
12
14
  from zrb.context.any_context import AnyContext
13
15
  from zrb.util.cli.markdown import render_markdown
14
16
  from zrb.util.cli.style import stylize_blue, stylize_bold_yellow, stylize_faint
15
17
  from zrb.util.string.conversion import to_boolean
16
18
 
19
+ if TYPE_CHECKING:
20
+ from asyncio import StreamReader
21
+
22
+ from prompt_toolkit import PromptSession
23
+
17
24
 
18
25
  async def read_user_prompt(ctx: AnyContext) -> str:
19
26
  """
@@ -22,7 +29,7 @@ async def read_user_prompt(ctx: AnyContext) -> str:
22
29
  """
23
30
  _show_info(ctx)
24
31
  is_tty: bool = ctx.is_tty
25
- reader = await _setup_input_reader(is_tty)
32
+ reader: PromptSession[Any] | StreamReader = await _setup_input_reader(is_tty)
26
33
  multiline_mode = False
27
34
  is_first_time = True
28
35
  current_modes: str = ctx.input.modes
@@ -32,17 +39,19 @@ async def read_user_prompt(ctx: AnyContext) -> str:
32
39
  while True:
33
40
  await asyncio.sleep(0.01)
34
41
  previous_session_name: str | None = (
35
- ctx.input.previous_session if is_first_time else None
42
+ ctx.input.previous_session if is_first_time else ""
36
43
  )
37
44
  start_new: bool = ctx.input.start_new if is_first_time else False
38
- if is_first_time:
39
- is_first_time = False
40
- # Get user input based on mode
41
- if not multiline_mode:
42
- ctx.print("💬 >>", plain=True)
43
- user_input = await _read_next_line(reader, ctx)
44
- if not multiline_mode:
45
- ctx.print("", plain=True)
45
+ if is_first_time and ctx.input.message.strip() != "":
46
+ user_input = ctx.input.message
47
+ else:
48
+ # Get user input based on mode
49
+ if not multiline_mode:
50
+ ctx.print("💬 >>", plain=True)
51
+ user_input = await llm_chat_trigger.wait(reader, ctx)
52
+ if not multiline_mode:
53
+ ctx.print("", plain=True)
54
+ is_first_time = False
46
55
  # Handle user input
47
56
  if user_input.strip().lower() in ("/bye", "/quit", "/q", "/exit"):
48
57
  user_prompt = "\n".join(user_inputs)
@@ -151,7 +160,9 @@ def _show_subcommand(command: str, description: str) -> str:
151
160
  return f" {styled_command} {styled_description}"
152
161
 
153
162
 
154
- async def _setup_input_reader(is_interactive: bool):
163
+ async def _setup_input_reader(
164
+ is_interactive: bool,
165
+ ) -> "PromptSession[Any] | StreamReader":
155
166
  """Sets up and returns the appropriate asynchronous input reader."""
156
167
  if is_interactive:
157
168
  from prompt_toolkit import PromptSession
@@ -165,22 +176,6 @@ async def _setup_input_reader(is_interactive: bool):
165
176
  return reader
166
177
 
167
178
 
168
- async def _read_next_line(reader, ctx: AnyContext) -> str:
169
- """Reads one line of input using the provided reader."""
170
- from prompt_toolkit import PromptSession
171
-
172
- if isinstance(reader, PromptSession):
173
- return await reader.prompt_async()
174
-
175
- line_bytes = await reader.readline()
176
- if not line_bytes:
177
- return "/bye" # Signal to exit
178
-
179
- user_input = line_bytes.decode().strip()
180
- ctx.print(user_input, plain=True)
181
- return user_input
182
-
183
-
184
179
  async def _trigger_ask_and_wait_for_result(
185
180
  ctx: AnyContext,
186
181
  user_prompt: str,
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ from asyncio import StreamReader
3
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine
4
+
5
+ from zrb.context.any_context import AnyContext
6
+ from zrb.util.run import run_async
7
+
8
+ if TYPE_CHECKING:
9
+ from prompt_toolkit import PromptSession
10
+
11
+
12
+ ChatTrigger = Callable[[AnyContext], Coroutine[Any, Any, str] | str]
13
+
14
+
15
+ class LLMChatTrigger:
16
+
17
+ def __init__(self):
18
+ self._triggers: list[ChatTrigger] = []
19
+
20
+ def add_trigger(self, *trigger: ChatTrigger):
21
+ self.append_trigger(*trigger)
22
+
23
+ def append_trigger(self, *trigger: ChatTrigger):
24
+ for single_trigger in trigger:
25
+ self._triggers.append(single_trigger)
26
+
27
+ async def wait(
28
+ self, reader: "PromptSession[Any] | StreamReader", ctx: AnyContext
29
+ ) -> str:
30
+ trigger_tasks = [
31
+ asyncio.create_task(run_async(self._read_next_line(reader, ctx)))
32
+ ] + [asyncio.create_task(run_async(trigger(ctx))) for trigger in self._triggers]
33
+ final_result: str = ""
34
+ try:
35
+ done, pending = await asyncio.wait(
36
+ trigger_tasks, return_when=asyncio.FIRST_COMPLETED
37
+ )
38
+ for task in done:
39
+ final_result = await task
40
+ if pending:
41
+ for task in pending:
42
+ task.cancel()
43
+ for task in done:
44
+ break
45
+ except asyncio.CancelledError:
46
+ ctx.print("Task cancelled.", plain=True)
47
+ final_result = "/bye"
48
+ except KeyboardInterrupt:
49
+ ctx.print("KeyboardInterrupt detected. Exiting...", plain=True)
50
+ final_result = "/bye"
51
+ return final_result
52
+
53
+ async def _read_next_line(
54
+ self, reader: "PromptSession[Any] | StreamReader", ctx: AnyContext
55
+ ) -> str:
56
+ """Reads one line of input using the provided reader."""
57
+ from prompt_toolkit import PromptSession
58
+
59
+ try:
60
+ if isinstance(reader, PromptSession):
61
+ return await reader.prompt_async()
62
+ line_bytes = await reader.readline()
63
+ if not line_bytes:
64
+ return "/bye" # Signal to exit
65
+ user_input = line_bytes.decode().strip()
66
+ ctx.print(user_input, plain=True)
67
+ return user_input
68
+ except KeyboardInterrupt:
69
+ ctx.print("KeyboardInterrupt detected. Exiting...", plain=True)
70
+ return "/bye"
71
+
72
+
73
+ llm_chat_trigger = LLMChatTrigger()
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from collections.abc import Callable
2
3
  from typing import TYPE_CHECKING
3
4
 
@@ -28,6 +29,7 @@ from zrb.callback.callback import Callback
28
29
  from zrb.config.config import CFG
29
30
  from zrb.config.llm_config import llm_config
30
31
  from zrb.context.any_context import AnyContext
32
+ from zrb.input.any_input import AnyInput
31
33
  from zrb.input.bool_input import BoolInput
32
34
  from zrb.input.str_input import StrInput
33
35
  from zrb.input.text_input import TextInput
@@ -37,10 +39,23 @@ from zrb.util.string.conversion import to_boolean
37
39
 
38
40
  if TYPE_CHECKING:
39
41
  from pydantic_ai import Tool
42
+ from pydantic_ai.toolsets import AbstractToolset
40
43
 
41
44
  ToolOrCallable = Tool | Callable
42
45
 
43
46
 
47
+ def _get_toolset(ctx: AnyContext) -> list["AbstractToolset[None] | str"]:
48
+ cwd = os.getcwd()
49
+ toolsets = []
50
+ for config_path in [
51
+ os.path.join(cwd, "mcp_config.json"),
52
+ os.path.join(cwd, "mcp-config.json"),
53
+ ]:
54
+ if os.path.isfile(config_path):
55
+ toolsets.append(config_path)
56
+ return toolsets
57
+
58
+
44
59
  def _get_tool(ctx: AnyContext) -> list["ToolOrCallable"]:
45
60
  tools = []
46
61
  if CFG.LLM_ALLOW_ANALYZE_REPO:
@@ -91,81 +106,89 @@ def _render_yolo_mode_input(ctx: AnyContext) -> list[str] | bool | None:
91
106
  return elements
92
107
 
93
108
 
94
- _llm_ask_inputs = [
95
- StrInput(
96
- "model",
97
- description="LLM Model",
98
- prompt="LLM Model",
99
- default="",
100
- allow_positional_parsing=False,
101
- always_prompt=False,
102
- allow_empty=True,
103
- ),
104
- StrInput(
105
- "base-url",
106
- description="LLM API Base URL",
107
- prompt="LLM API Base URL",
108
- default="",
109
- allow_positional_parsing=False,
110
- always_prompt=False,
111
- allow_empty=True,
112
- ),
113
- StrInput(
114
- "api-key",
115
- description="LLM API Key",
116
- prompt="LLM API Key",
117
- default="",
118
- allow_positional_parsing=False,
119
- always_prompt=False,
120
- allow_empty=True,
121
- ),
122
- TextInput(
123
- "system-prompt",
124
- description="System prompt",
125
- prompt="System prompt",
126
- default="",
127
- allow_positional_parsing=False,
128
- always_prompt=False,
129
- ),
130
- TextInput(
131
- "modes",
132
- description="Modes",
133
- prompt="Modes",
134
- default=lambda ctx: ",".join(llm_config.default_modes),
135
- allow_positional_parsing=False,
136
- always_prompt=False,
137
- ),
138
- BoolInput(
139
- "start-new",
140
- description="Start new session (LLM Agent will forget past conversation)",
141
- prompt="Start new session (LLM Agent will forget past conversation)",
142
- default=False,
143
- allow_positional_parsing=False,
144
- always_prompt=False,
145
- ),
146
- StrInput(
147
- "yolo",
148
- description="YOLO mode (LLM Agent will start in YOLO Mode)",
149
- prompt="YOLO mode (LLM Agent will start in YOLO Mode)",
150
- default=_get_default_yolo_mode,
151
- allow_positional_parsing=False,
152
- always_prompt=False,
153
- ),
154
- TextInput("message", description="User message", prompt="Your message"),
155
- PreviousSessionInput(
156
- "previous-session",
157
- description="Previous conversation session",
158
- prompt="Previous conversation session (can be empty)",
159
- allow_positional_parsing=False,
160
- allow_empty=True,
161
- always_prompt=False,
162
- ),
163
- ]
109
+ def _get_inputs(require_message: bool = True) -> list[AnyInput | None]:
110
+ return [
111
+ StrInput(
112
+ "model",
113
+ description="LLM Model",
114
+ prompt="LLM Model",
115
+ default="",
116
+ allow_positional_parsing=False,
117
+ always_prompt=False,
118
+ allow_empty=True,
119
+ ),
120
+ StrInput(
121
+ "base-url",
122
+ description="LLM API Base URL",
123
+ prompt="LLM API Base URL",
124
+ default="",
125
+ allow_positional_parsing=False,
126
+ always_prompt=False,
127
+ allow_empty=True,
128
+ ),
129
+ StrInput(
130
+ "api-key",
131
+ description="LLM API Key",
132
+ prompt="LLM API Key",
133
+ default="",
134
+ allow_positional_parsing=False,
135
+ always_prompt=False,
136
+ allow_empty=True,
137
+ ),
138
+ TextInput(
139
+ "system-prompt",
140
+ description="System prompt",
141
+ prompt="System prompt",
142
+ default="",
143
+ allow_positional_parsing=False,
144
+ always_prompt=False,
145
+ ),
146
+ TextInput(
147
+ "modes",
148
+ description="Modes",
149
+ prompt="Modes",
150
+ default=lambda ctx: ",".join(llm_config.default_modes),
151
+ allow_positional_parsing=False,
152
+ always_prompt=False,
153
+ ),
154
+ BoolInput(
155
+ "start-new",
156
+ description="Start new session (LLM Agent will forget past conversation)",
157
+ prompt="Start new session (LLM Agent will forget past conversation)",
158
+ default=False,
159
+ allow_positional_parsing=False,
160
+ always_prompt=False,
161
+ ),
162
+ StrInput(
163
+ "yolo",
164
+ description="YOLO mode (LLM Agent will start in YOLO Mode)",
165
+ prompt="YOLO mode (LLM Agent will start in YOLO Mode)",
166
+ default=_get_default_yolo_mode,
167
+ allow_positional_parsing=False,
168
+ always_prompt=False,
169
+ ),
170
+ TextInput(
171
+ "message",
172
+ description="User message",
173
+ prompt="Your message",
174
+ always_prompt=require_message,
175
+ allow_empty=not require_message,
176
+ ),
177
+ PreviousSessionInput(
178
+ "previous-session",
179
+ description="Previous conversation session",
180
+ prompt="Previous conversation session (can be empty)",
181
+ allow_positional_parsing=False,
182
+ allow_empty=True,
183
+ always_prompt=False,
184
+ ),
185
+ ]
186
+
164
187
 
165
188
  llm_ask: LLMTask = llm_group.add_task(
166
189
  LLMTask(
167
190
  name="llm-ask",
168
- input=_llm_ask_inputs,
191
+ input=_get_inputs(True),
169
192
  description="❓ Ask LLM",
170
193
  model=lambda ctx: None if ctx.input.model.strip() == "" else ctx.input.model,
171
194
  model_base_url=lambda ctx: (
@@ -184,6 +207,7 @@ llm_ask: LLMTask = llm_group.add_task(
184
207
  ),
185
208
  message="{ctx.input.message}",
186
209
  tools=_get_tool,
210
+ toolsets=_get_toolset,
187
211
  yolo_mode=_render_yolo_mode_input,
188
212
  retries=0,
189
213
  ),
@@ -193,7 +217,7 @@ llm_ask: LLMTask = llm_group.add_task(
193
217
  llm_group.add_task(
194
218
  BaseTrigger(
195
219
  name="llm-chat",
196
- input=[input for input in _llm_ask_inputs if input.name != "message"],
220
+ input=_get_inputs(False),
197
221
  description="💬 Chat with LLM",
198
222
  queue_name="ask_trigger",
199
223
  action=read_user_prompt,
@@ -120,6 +120,10 @@ async def analyze_repo(
120
120
  goal=goal,
121
121
  token_limit=extraction_token_threshold,
122
122
  )
123
+ if len(extracted_infos) == 0:
124
+ raise RuntimeError(
125
+ "No info can be extracted, adjust extensions or exclude_patterns."
126
+ )
123
127
  if len(extracted_infos) == 1:
124
128
  return extracted_infos[0]
125
129
  summarized_infos = extracted_infos
@@ -146,11 +150,11 @@ def _get_file_metadatas(
146
150
  if not any(file.endswith(f".{ext}") for ext in extensions):
147
151
  continue
148
152
  file_path = os.path.join(root, file)
149
- if is_excluded(file_path, exclude_patterns):
150
- continue
151
153
  try:
154
+ rel_path = os.path.relpath(file_path, dir_path)
155
+ if is_excluded(rel_path, exclude_patterns):
156
+ continue
152
157
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
153
- rel_path = os.path.relpath(file_path, dir_path)
154
158
  metadata_list.append({"path": rel_path, "content": f.read()})
155
159
  except Exception as e:
156
160
  print(f"Error reading file {file_path}: {e}")
@@ -304,7 +304,7 @@ def write_to_file(
304
304
  Writes content to a file, completely overwriting it if it exists or
305
305
  creating it if it doesn't.
306
306
 
307
- Use this tool to create new files or to replace the entire content of
307
+ Use this tool to create new files or to overwrite the entire content of
308
308
  existing files. This is a destructive operation, so be certain of your
309
309
  actions. Always read the file first to understand its contents before
310
310
  overwriting it, unless you are creating a new file.
@@ -497,7 +497,7 @@ async def analyze_file(
497
497
  ctx: AnyContext, path: str, query: str, token_limit: int | None = None
498
498
  ) -> dict[str, Any]:
499
499
  """
500
- Performs a deep, goal-oriented analysis of a single file using a sub-agent.
500
+ Performs a high level, goal-oriented analysis of a single file using a sub-agent.
501
501
 
502
502
  This tool is ideal for complex questions about a single file that go beyond
503
503
  simple reading or searching. It uses a specialized sub-agent to analyze the
@@ -1,4 +1,3 @@
1
- import json
2
1
  from collections.abc import Callable
3
2
  from textwrap import dedent
4
3
  from typing import TYPE_CHECKING, Any, Coroutine
@@ -9,7 +8,7 @@ from zrb.task.llm.config import get_model, get_model_settings
9
8
  from zrb.task.llm.prompt import get_system_and_user_prompt
10
9
 
11
10
  if TYPE_CHECKING:
12
- from pydantic_ai import Agent, Tool
11
+ from pydantic_ai import Tool
13
12
  from pydantic_ai.models import Model
14
13
  from pydantic_ai.settings import ModelSettings
15
14
  from pydantic_ai.toolsets import AbstractToolset
@@ -24,7 +23,7 @@ def create_sub_agent_tool(
24
23
  model: "str | Model | None" = None,
25
24
  model_settings: "ModelSettings | None" = None,
26
25
  tools: "list[ToolOrCallable]" = [],
27
- toolsets: list["AbstractToolset[Agent]"] = [],
26
+ toolsets: list["AbstractToolset[None]"] = [],
28
27
  yolo_mode: bool | list[str] | None = None,
29
28
  log_indent_level: int = 2,
30
29
  ) -> Callable[[AnyContext, str], Coroutine[Any, Any, dict[str, Any]]]:
@@ -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")
@@ -140,8 +146,8 @@ async def execute_action_with_retry(task: "BaseTask", session: AnySession) -> An
140
146
  handling success (triggering successors) and failure (triggering fallbacks).
141
147
  """
142
148
  ctx = task.get_ctx(session)
143
- retries = getattr(task, "_retries", 2)
144
- retry_period = getattr(task, "_retry_period", 0)
149
+ retries = task._retries if task._retries is not None else 2
150
+ retry_period = task._retry_period if task._retry_period is not None else 0
145
151
  max_attempt = retries + 1
146
152
  ctx.set_max_attempt(max_attempt)
147
153
 
@@ -163,8 +169,9 @@ async def execute_action_with_retry(task: "BaseTask", session: AnySession) -> An
163
169
  session.get_task_status(task).mark_as_completed()
164
170
 
165
171
  # Store result in XCom
166
- task_xcom: Xcom = ctx.xcom.get(task.name)
167
- task_xcom.push(result)
172
+ task_xcom: Xcom | None = ctx.xcom.get(task.name)
173
+ if task_xcom is not None:
174
+ task_xcom.push(result)
168
175
 
169
176
  # Skip fallbacks and execute successors on success
170
177
  skip_fallbacks(task, session)
@@ -201,7 +208,7 @@ async def run_default_action(task: "BaseTask", ctx: AnyContext) -> Any:
201
208
  This is the default implementation called by BaseTask._exec_action.
202
209
  Subclasses like LLMTask override _exec_action with their own logic.
203
210
  """
204
- action = getattr(task, "_action", None)
211
+ action = task._action
205
212
  if action is None:
206
213
  ctx.log_debug("No action defined for this task.")
207
214
  return None
@@ -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/llm/agent.py CHANGED
@@ -125,8 +125,8 @@ def get_agent(
125
125
  "list[ToolOrCallable] | Callable[[AnySharedContext], list[ToolOrCallable]]"
126
126
  ) = [],
127
127
  additional_tools: "list[ToolOrCallable]" = [],
128
- toolsets_attr: "list[AbstractToolset[Agent]] | Callable[[AnySharedContext], list[AbstractToolset[Agent]]]" = [], # noqa
129
- additional_toolsets: "list[AbstractToolset[Agent]]" = [],
128
+ toolsets_attr: "list[AbstractToolset[None] | str] | Callable[[AnySharedContext], list[AbstractToolset[None] | str]]" = [], # noqa
129
+ additional_toolsets: "list[AbstractToolset[None] | str]" = [],
130
130
  retries: int = 3,
131
131
  yolo_mode: bool | list[str] | None = None,
132
132
  ) -> "Agent":
@@ -149,8 +149,11 @@ def get_agent(
149
149
  tools = list(tools_attr(ctx) if callable(tools_attr) else tools_attr)
150
150
  tools.extend(additional_tools)
151
151
  # Get Toolsets for agent
152
- tool_sets = list(toolsets_attr(ctx) if callable(toolsets_attr) else toolsets_attr)
153
- tool_sets.extend(additional_toolsets)
152
+ toolset_or_str_list = list(
153
+ toolsets_attr(ctx) if callable(toolsets_attr) else toolsets_attr
154
+ )
155
+ toolset_or_str_list.extend(additional_toolsets)
156
+ toolsets = _render_toolset_or_str_list(ctx, toolset_or_str_list)
154
157
  # If no agent provided, create one using the configuration
155
158
  return create_agent_instance(
156
159
  ctx=ctx,
@@ -158,13 +161,32 @@ def get_agent(
158
161
  output_type=output_type,
159
162
  system_prompt=system_prompt,
160
163
  tools=tools,
161
- toolsets=tool_sets,
164
+ toolsets=toolsets,
162
165
  model_settings=model_settings,
163
166
  retries=retries,
164
167
  yolo_mode=yolo_mode,
165
168
  )
166
169
 
167
170
 
171
+ def _render_toolset_or_str_list(
172
+ ctx: AnyContext, toolset_or_str_list: list["AbstractToolset[None] | str"]
173
+ ) -> list["AbstractToolset[None]"]:
174
+ from pydantic_ai.mcp import load_mcp_servers
175
+
176
+ toolsets = []
177
+ for toolset_or_str in toolset_or_str_list:
178
+ if isinstance(toolset_or_str, str):
179
+ try:
180
+ servers = load_mcp_servers(toolset_or_str)
181
+ for server in servers:
182
+ toolsets.append(server)
183
+ except Exception as e:
184
+ ctx.log_error(f"Invalid MCP Config {toolset_or_str}: {e}")
185
+ continue
186
+ toolsets.append(toolset_or_str)
187
+ return toolsets
188
+
189
+
168
190
  async def run_agent_iteration(
169
191
  ctx: AnyContext,
170
192
  agent: "Agent[None, Any]",
zrb/task/llm/config.py CHANGED
@@ -4,7 +4,7 @@ if TYPE_CHECKING:
4
4
  from pydantic_ai.models import Model
5
5
  from pydantic_ai.settings import ModelSettings
6
6
 
7
- from zrb.attr.type import BoolAttr, StrAttr, fstring
7
+ from zrb.attr.type import BoolAttr, StrAttr, StrListAttr, fstring
8
8
  from zrb.config.llm_config import LLMConfig, llm_config
9
9
  from zrb.context.any_context import AnyContext
10
10
  from zrb.context.any_shared_context import AnySharedContext
@@ -13,7 +13,7 @@ from zrb.util.attr import get_attr, get_bool_attr, get_str_list_attr
13
13
 
14
14
  def get_yolo_mode(
15
15
  ctx: AnyContext,
16
- yolo_mode_attr: BoolAttr | StrAttr | None = None,
16
+ yolo_mode_attr: BoolAttr | StrListAttr | None = None,
17
17
  render_yolo_mode: bool = True,
18
18
  ) -> bool | list[str]:
19
19
  if yolo_mode_attr is None:
@@ -1,5 +1,6 @@
1
1
  import functools
2
2
  import inspect
3
+ import json
3
4
  import traceback
4
5
  import typing
5
6
  from collections.abc import Callable
@@ -172,18 +173,17 @@ def _get_edited_kwargs(
172
173
  return kwargs, False
173
174
  while len(user_edit_responses) < 3:
174
175
  user_edit_responses.append("")
175
- key, val = user_edit_responses[1:]
176
+ key, val_str = user_edit_responses[1:]
176
177
  if key not in kwargs:
177
178
  return kwargs, True
178
- if val != "":
179
- kwargs[key] = val
180
- return kwargs, True
181
- val = edit_text(
182
- prompt_message=f"// {key}",
183
- value=kwargs.get(key, ""),
184
- editor=CFG.DEFAULT_EDITOR,
185
- )
186
- kwargs[key] = val
179
+ is_str_param = isinstance(kwargs[key], str)
180
+ if val_str == "":
181
+ val_str = edit_text(
182
+ prompt_message=f"// {key}",
183
+ value=_get_val_str(kwargs[key]),
184
+ editor=CFG.DEFAULT_EDITOR,
185
+ )
186
+ kwargs[key] = val_str if is_str_param else json.loads(val_str)
187
187
  return kwargs, True
188
188
 
189
189
 
@@ -233,7 +233,7 @@ def _get_detail_func_param(args: list[Any] | tuple[Any], kwargs: dict[str, Any])
233
233
 
234
234
  def _get_func_param_item(key: str, val: Any) -> str:
235
235
  upper_key = key.upper()
236
- val_str = f"{val}"
236
+ val_str = _get_val_str(val)
237
237
  val_parts = val_str.split("\n")
238
238
  if len(val_parts) == 1:
239
239
  return f"- {upper_key} `{val}`"
@@ -244,6 +244,15 @@ def _get_func_param_item(key: str, val: Any) -> str:
244
244
  return "\n".join(lines)
245
245
 
246
246
 
247
+ def _get_val_str(val: Any) -> str:
248
+ if isinstance(val, str):
249
+ return val
250
+ try:
251
+ return json.dumps(val, indent=4)
252
+ except Exception:
253
+ return f"{val}"
254
+
255
+
247
256
  def _get_func_call_str(
248
257
  func: Callable, args: list[Any] | tuple[Any], kwargs: dict[str, Any]
249
258
  ) -> str:
zrb/task/llm_task.py CHANGED
@@ -80,7 +80,8 @@ class LLMTask(BaseTask):
80
80
  | Callable[[AnySharedContext], list["ToolOrCallable"]]
81
81
  ) = [],
82
82
  toolsets: (
83
- list["AbstractToolset[Agent]"] | Callable[[AnySharedContext], list["Tool"]]
83
+ list["AbstractToolset[None] | str"]
84
+ | Callable[[AnySharedContext], list["AbstractToolset[None] | str"]]
84
85
  ) = [],
85
86
  conversation_history: (
86
87
  ConversationHistory
@@ -169,7 +170,7 @@ class LLMTask(BaseTask):
169
170
  self._rate_limitter = rate_limitter
170
171
  self._additional_tools: list["ToolOrCallable"] = []
171
172
  self._toolsets = toolsets
172
- self._additional_toolsets: list["AbstractToolset[Agent]"] = []
173
+ self._additional_toolsets: list["AbstractToolset[None] | str"] = []
173
174
  self._conversation_history = conversation_history
174
175
  self._conversation_history_reader = conversation_history_reader
175
176
  self._conversation_history_writer = conversation_history_writer
@@ -200,10 +201,12 @@ class LLMTask(BaseTask):
200
201
  for single_tool in tool:
201
202
  self._additional_tools.append(single_tool)
202
203
 
203
- def add_toolset(self, *toolset: "AbstractToolset[Agent]"):
204
+ def add_toolset(self, *toolset: "AbstractToolset[None] | str"):
204
205
  self.append_toolset(*toolset)
205
206
 
206
- def append_toolset(self, *toolset: "AbstractToolset[Agent]"):
207
+ def append_toolset(self, *toolset: "AbstractToolset[None] | str"):
208
+ from pydantic_ai.mcp import load_mcp_servers
209
+
207
210
  for single_toolset in toolset:
208
211
  self._additional_toolsets.append(single_toolset)
209
212
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: zrb
3
- Version: 1.16.1
3
+ Version: 1.16.3
4
4
  Summary: Your Automation Powerhouse
5
5
  License: AGPL-3.0-or-later
6
6
  Keywords: Automation,Task Runner,Code Generator,Monorepo,Low Code
@@ -24,12 +24,12 @@ Requires-Dist: isort (>=6.0.1,<7.0.0)
24
24
  Requires-Dist: libcst (>=1.8.2,<2.0.0)
25
25
  Requires-Dist: markdownify (>=1.2.0,<2.0.0)
26
26
  Requires-Dist: mcp (>1.12.3)
27
- Requires-Dist: openai (>=1.99.9)
27
+ Requires-Dist: openai (>=1.107.2)
28
28
  Requires-Dist: pdfplumber (>=0.11.7,<0.12.0)
29
29
  Requires-Dist: playwright (>=1.54.0,<2.0.0) ; extra == "playwright" or extra == "all"
30
30
  Requires-Dist: prompt-toolkit (>=3)
31
31
  Requires-Dist: psutil (>=7.0.0,<8.0.0)
32
- Requires-Dist: pydantic-ai-slim[anthropic,bedrock,cohere,google,groq,huggingface,mistral,openai,vertexai] (>=1.0.1,<1.1.0)
32
+ Requires-Dist: pydantic-ai-slim[anthropic,bedrock,cohere,google,groq,huggingface,mistral,openai,vertexai] (>=1.0.10,<1.1.0)
33
33
  Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
34
34
  Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
35
35
  Requires-Dist: python-jose[cryptography] (>=3.5.0,<4.0.0)
@@ -2,25 +2,26 @@ zrb/__init__.py,sha256=qkCV2EnAGIgvsawBHYvKgPAp0zzPcikYSmbQXATLzg4,5060
2
2
  zrb/__main__.py,sha256=9SXH9MK4PVyU9lkEyHxiIUABbcsV2wseP94HmlqTR4M,2657
3
3
  zrb/attr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  zrb/attr/type.py,sha256=4TV5gPYMMrKh5V-yB6iRYKCbsXAH_AvGXMsjxKLHcUs,568
5
- zrb/builtin/__init__.py,sha256=NjXpvBgAiPH-dNsJx5Fa-zSZE5JVnmVb1GhMNtevpGQ,1614
5
+ zrb/builtin/__init__.py,sha256=qeLg_S7mRWe48AYrzNutAgAnLh2YE57-iEVK0ICd-3A,1672
6
6
  zrb/builtin/base64.py,sha256=UjaFttE2oRx0T7_RpKtKfgMtWfiQXfJBAJmA16ek8Ic,1507
7
7
  zrb/builtin/git.py,sha256=8_qVE_2lVQEVXQ9vhiw8Tn4Prj1VZB78ZjEJJS5Ab3M,5461
8
8
  zrb/builtin/git_subtree.py,sha256=7BKwOkVTWDrR0DXXQ4iJyHqeR6sV5VYRt8y_rEB0EHg,3505
9
9
  zrb/builtin/group.py,sha256=zYC5uw0VE97TXiLCr464kFJ-CJIJyeQ2RXjnVRY5ovs,2577
10
10
  zrb/builtin/http.py,sha256=L6RE73c65wWwG5iHFN-tpOhyh56KsrgVskDd3c3YXtk,4246
11
11
  zrb/builtin/jwt.py,sha256=3M5uaQhJZbKQLjTUft1OwPz_JxtmK-xtkjxWjciOQho,2859
12
- zrb/builtin/llm/chat_session.py,sha256=Q9-5aG3CLRDuRjUtjXPLMTHl9PEwx31HQo0_RkDn9Vs,10516
12
+ zrb/builtin/llm/chat_session.py,sha256=6q40xQdv56OtaTZCVSS16WDchn4l0sagZI2BGX_JyQM,10448
13
+ zrb/builtin/llm/chat_trigger.py,sha256=xaJmzrvBGz6LFPOpYnG9bMeT1dY6XqZPXamtr9e72-w,2427
13
14
  zrb/builtin/llm/history.py,sha256=LDOrL0p7r_AHLa5L8Dp7bHNsOALugmJd7OguXRWGnm4,3087
14
15
  zrb/builtin/llm/input.py,sha256=Nw-26uTWp2QhUgKJcP_IMHmtk-b542CCSQ_vCOjhvhM,877
15
- zrb/builtin/llm/llm_ask.py,sha256=FU5KonqW1U_vDch2gIGE7yoMmLLDNiV4QLA4VDv0zxk,6636
16
+ zrb/builtin/llm/llm_ask.py,sha256=8V5YAShUPes5qnlxLaS9Ks4KOs7oCQFGrpfkj-6rLMU,7546
16
17
  zrb/builtin/llm/previous-session.js,sha256=xMKZvJoAbrwiyHS0OoPrWuaKxWYLoyR5sguePIoCjTY,816
17
18
  zrb/builtin/llm/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  zrb/builtin/llm/tool/api.py,sha256=p55Fs6QTmsM2u-vVcBJb9xYxGAkiHIKX2wYKhsGlWFE,2417
19
20
  zrb/builtin/llm/tool/cli.py,sha256=sm_maE1WBB051odh1xXr8QQOWln_ewAU_7OScKAneT4,1244
20
- zrb/builtin/llm/tool/code.py,sha256=mtmXTDZTbUh6bEf4fsVPvfM0bokHMMETDWAHOXRmRmI,8798
21
- zrb/builtin/llm/tool/file.py,sha256=W4hWCg8Uo3Fo4bucpSDVgjQIGaXTLod5CYOeF23JC-s,23581
21
+ zrb/builtin/llm/tool/code.py,sha256=-MKUpXX4jkWm4rCqrUmTTzsYhjfzKle9_XsNPtq8PNM,8952
22
+ zrb/builtin/llm/tool/file.py,sha256=fDgt31CWwZtOYOk6lfBSBxX85NARLr-ZzXPvG1JP8C0,23589
22
23
  zrb/builtin/llm/tool/rag.py,sha256=aN8D8ZqzGXWCP_1F1LbN0QgfyzaK9CKrjfTPorDIYjw,9824
23
- zrb/builtin/llm/tool/sub_agent.py,sha256=_curhFBxO9cYAo19VWGkJqKkqsY7kiSknaM__ONJhpg,5087
24
+ zrb/builtin/llm/tool/sub_agent.py,sha256=nYluPfc8FlSobpP_4vnBIqkPARrDHq_SwKkmlh_ATUI,5067
24
25
  zrb/builtin/llm/tool/web.py,sha256=zDgxYRIQRj9A8QXb80-ZSPmGCvIOWy8bpJMGSAuTL8Y,7491
25
26
  zrb/builtin/md5.py,sha256=690RV2LbW7wQeTFxY-lmmqTSVEEZv3XZbjEUW1Q3XpE,1480
26
27
  zrb/builtin/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -337,17 +338,17 @@ zrb/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
337
338
  zrb/task/any_task.py,sha256=AXcBLnDWrJsiq_VihrXeNNMsQZuHzc5k6j-mS60qbyM,6443
338
339
  zrb/task/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
339
340
  zrb/task/base/context.py,sha256=ptz9-Am1s4yq6EHZUpnSrB0Ps3k7BgyakVbE-ZpiBzc,3923
340
- zrb/task/base/execution.py,sha256=scDLfNYBe8Bc8Ct1LCIKmFtjpPxm7FjqZ2bJXIQAzv8,11042
341
+ zrb/task/base/execution.py,sha256=ixWSk0byHDbBvT7lxh94eD8pmnbI6LRTIIkMmh0zing,11265
341
342
  zrb/task/base/lifecycle.py,sha256=c2v977pUm7S4EqrTMcUJKhnYOWugACVwU3qORDKiLXQ,7639
342
- zrb/task/base/monitoring.py,sha256=UAOEcPiYNtZR4FFxzWCosuOEFE_P3c4GT5vAhQmohqI,5663
343
+ zrb/task/base/monitoring.py,sha256=w4_q3e7dwcJureUfcJr7vhpwQ5RCikr0VHGlGA_crrM,5815
343
344
  zrb/task/base/operators.py,sha256=uAMFqpZJsPnCrojgOl1FUDXTS15mtOa_IqiAXltyYRU,1576
344
345
  zrb/task/base_task.py,sha256=upwuqAfwNDXTYM-uRDJhgZqqqARI03T6ksUbFHHLEH0,13321
345
346
  zrb/task/base_trigger.py,sha256=HVasUkIZc8ZdAkJCbhXeO1QTY9vF7BvENoxKRV3R_eY,7171
346
347
  zrb/task/cmd_task.py,sha256=myM8WZm6NrUD-Wv0Vb5sTOrutrAVZLt5LVsSBKwX6SM,10860
347
348
  zrb/task/http_check.py,sha256=Gf5rOB2Se2EdizuN9rp65HpGmfZkGc-clIAlHmPVehs,2565
348
349
  zrb/task/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
349
- zrb/task/llm/agent.py,sha256=UqQoxhPDg25jm5UwlC0mmPypfV56mXGNpk5hiZ75nbA,10949
350
- zrb/task/llm/config.py,sha256=zOPf4NpdWPuBc_R8d-kljYcOKfUAypDxiSjRDrxV66M,4059
350
+ zrb/task/llm/agent.py,sha256=mAiDc5YhQ87dJX9Lfv_Q8bTyY2laZDCaAcpHs2yxvtc,11724
351
+ zrb/task/llm/config.py,sha256=2usr_FjylQrrRjjPDOIBhb7pIwVQkicA0hYGeMy9qYg,4076
351
352
  zrb/task/llm/conversation_history.py,sha256=_ThBOCv4vs3V6B3P_s-Aad2sH0RqE46KzLqgwdwHMC0,6758
352
353
  zrb/task/llm/conversation_history_model.py,sha256=kk-7niTl29Rm2EUIhTHzPXgZ5tp4IThMnIB3dS-1OdU,3062
353
354
  zrb/task/llm/default_workflow/coding.md,sha256=2uythvPsnBpYfIhiIH1cCinQXX0i0yUqsL474Zpemw0,2484
@@ -358,9 +359,9 @@ zrb/task/llm/history_summarization.py,sha256=blTKfaSpgaqvORWzGL3BKrRWAsfNdZB03oJ
358
359
  zrb/task/llm/history_summarization_tool.py,sha256=5KQUoP75udhtVRbGVxJgnQ36WtVoX--aMvAfGIMBJmw,1760
359
360
  zrb/task/llm/print_node.py,sha256=Nnf4F6eDJR4PFcOqQ1jLWBTFnzNGl1Stux2DZ3SMhsY,8062
360
361
  zrb/task/llm/prompt.py,sha256=Mb3hz-KAp2bL9m6ZL0gcfLveRfBDRZvDVN_HHPGuEPM,12028
361
- zrb/task/llm/tool_wrapper.py,sha256=ifOP-smxHcDNpZvZMZm0XkKH49BicbyGTtpHNdcIA6Q,10241
362
+ zrb/task/llm/tool_wrapper.py,sha256=9HlfsOi73eGVC43iGZ1BasohT9clFD4IX9xSxu4nGU4,10520
362
363
  zrb/task/llm/typing.py,sha256=c8VAuPBw_4A3DxfYdydkgedaP-LU61W9_wj3m3CAX1E,58
363
- zrb/task/llm_task.py,sha256=OxJ9QpqjEyeOI1_zqzNZHtQlRHi0ANOvL9FYaWLzO3Y,14913
364
+ zrb/task/llm_task.py,sha256=uzHNAPziTeIiBr08-tg5_C3MOqJMhfbKmvkWEeS0GZM,15022
364
365
  zrb/task/make_task.py,sha256=PD3b_aYazthS8LHeJsLAhwKDEgdurQZpymJDKeN60u0,2265
365
366
  zrb/task/rsync_task.py,sha256=WfqNSaicJgYWpunNU34eYxXDqHDHOftuDHyWJKjqwg0,6365
366
367
  zrb/task/scaffolder.py,sha256=rME18w1HJUHXgi9eTYXx_T2G4JdqDYzBoNOkdOOo5-o,6806
@@ -410,7 +411,7 @@ zrb/util/todo_model.py,sha256=hhzAX-uFl5rsg7iVX1ULlJOfBtblwQ_ieNUxBWfc-Os,1670
410
411
  zrb/util/truncate.py,sha256=eSzmjBpc1Qod3lM3M73snNbDOcARHukW_tq36dWdPvc,921
411
412
  zrb/xcom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
412
413
  zrb/xcom/xcom.py,sha256=o79rxR9wphnShrcIushA0Qt71d_p3ZTxjNf7x9hJB78,1571
413
- zrb-1.16.1.dist-info/METADATA,sha256=xLE_P4uDT-BfY5pzkxxhzw5RTT4tOMQJUiYMAc-tAUw,9933
414
- zrb-1.16.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
415
- zrb-1.16.1.dist-info/entry_points.txt,sha256=-Pg3ElWPfnaSM-XvXqCxEAa-wfVI6BEgcs386s8C8v8,46
416
- zrb-1.16.1.dist-info/RECORD,,
414
+ zrb-1.16.3.dist-info/METADATA,sha256=XONEHMCkv5j0YPf35oaP5MYliI1CYXoPXUv-8FNKFI4,9935
415
+ zrb-1.16.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
416
+ zrb-1.16.3.dist-info/entry_points.txt,sha256=-Pg3ElWPfnaSM-XvXqCxEAa-wfVI6BEgcs386s8C8v8,46
417
+ zrb-1.16.3.dist-info/RECORD,,
File without changes