fabricatio 0.2.0.dev20__cp312-cp312-win_amd64.whl → 0.2.1__cp312-cp312-win_amd64.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.
Binary file
@@ -4,12 +4,12 @@ from fabricatio.models.action import Action
4
4
  from fabricatio.models.task import Task
5
5
 
6
6
 
7
- class Talk(Action):
8
- """Action that says hello to the world."""
7
+ class Examining(Action):
8
+ """Action that examines the input data."""
9
9
 
10
10
  name: str = "talk"
11
- output_key: str = "talk_response"
11
+ output_key: str = "examine_pass"
12
12
 
13
- async def _execute(self, task_input: Task[str], **_) -> str:
14
- """Execute the action."""
15
- return await self.aask(task_input.briefing, system_message=task_input.dependencies_prompt())
13
+ async def _execute(self, exam_target: Task[str], to_examine: str, **_) -> bool:
14
+ """Examine the input data."""
15
+ # TODO
@@ -21,14 +21,3 @@ class PublishTask(Action):
21
21
  logger.info(f"Sending task {send_task.name} to {send_targets}")
22
22
  for target in send_targets:
23
23
  await send_task.move_to(target).publish()
24
-
25
-
26
- class CycleTask(Action):
27
- """An action that cycles a task through a list of targets."""
28
-
29
- name: str = "cycle_task"
30
- """The name of the action."""
31
- description: str = "Cycle a task through a list of targets"
32
-
33
- async def _execute(self, task_input: Task, **_) -> None:
34
- """Execute the action by cycling the task through the specified targets."""
fabricatio/config.py CHANGED
@@ -89,7 +89,7 @@ class PymitterConfig(BaseModel):
89
89
  """
90
90
 
91
91
  model_config = ConfigDict(use_attribute_docstrings=True)
92
- delimiter: str = Field(default=".", frozen=True)
92
+ delimiter: str = Field(default="::", frozen=True)
93
93
  """The delimiter used to separate the event name into segments."""
94
94
 
95
95
  new_listener_event: bool = Field(default=False, frozen=True)
@@ -173,8 +173,8 @@ class GeneralConfig(BaseModel):
173
173
  workspace: DirectoryPath = Field(default=DirectoryPath(r"."))
174
174
  """The workspace directory for the application."""
175
175
 
176
- confirm_on_fs_ops: bool = Field(default=True)
177
- """Whether to confirm on file system operations."""
176
+ confirm_on_ops: bool = Field(default=True)
177
+ """Whether to confirm on operations."""
178
178
 
179
179
 
180
180
  class ToolBoxConfig(BaseModel):
@@ -185,6 +185,9 @@ class ToolBoxConfig(BaseModel):
185
185
  tool_module_name: str = Field(default="Toolbox")
186
186
  """The name of the module containing the toolbox."""
187
187
 
188
+ data_module_name: str = Field(default="Data")
189
+ """The name of the module containing the data."""
190
+
188
191
 
189
192
  class Settings(BaseSettings):
190
193
  """Application settings class.
fabricatio/decorators.py CHANGED
@@ -1,9 +1,11 @@
1
1
  """Decorators for Fabricatio."""
2
2
 
3
+ from asyncio import iscoroutinefunction
3
4
  from functools import wraps
4
5
  from inspect import signature
5
6
  from shutil import which
6
- from typing import Callable, Optional
7
+ from types import ModuleType
8
+ from typing import Callable, List, Optional
7
9
 
8
10
  from questionary import confirm
9
11
 
@@ -46,6 +48,25 @@ def depend_on_external_cmd[**P, R](
46
48
  return _decorator
47
49
 
48
50
 
51
+ def logging_execution_info[**P, R](func: Callable[P, R]) -> Callable[P, R]:
52
+ """Decorator to log the execution of a function.
53
+
54
+ Args:
55
+ func (Callable): The function to be executed
56
+
57
+ Returns:
58
+ Callable: A decorator that wraps the function to log the execution.
59
+ """
60
+
61
+ @wraps(func)
62
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
63
+ logger.info(f"Executing function: {func.__name__}{signature(func)}")
64
+ logger.debug(f"{func.__name__}{signature(func)}\nArgs: {args}\nKwargs: {kwargs}")
65
+ return func(*args, **kwargs)
66
+
67
+ return _wrapper
68
+
69
+
49
70
  def confirm_to_execute[**P, R](func: Callable[P, R]) -> Callable[P, Optional[R]] | Callable[P, R]:
50
71
  """Decorator to confirm before executing a function.
51
72
 
@@ -55,18 +76,104 @@ def confirm_to_execute[**P, R](func: Callable[P, R]) -> Callable[P, Optional[R]]
55
76
  Returns:
56
77
  Callable: A decorator that wraps the function to confirm before execution.
57
78
  """
58
- if not configs.general.confirm_on_fs_ops:
79
+ if not configs.general.confirm_on_ops:
59
80
  # Skip confirmation if the configuration is set to False
60
81
  return func
61
82
 
62
- @wraps(func)
63
- def _wrapper(*args: P.args, **kwargs: P.kwargs) -> Optional[R]:
64
- if confirm(
65
- f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n🔑 Kwargs:{kwargs}\n",
66
- instruction="Please input [Yes/No] to proceed (default: Yes):",
67
- ).ask():
68
- return func(*args, **kwargs)
69
- logger.warning(f"Function: {func.__name__}{signature(func)} canceled by user.")
70
- return None
83
+ if iscoroutinefunction(func):
84
+
85
+ @wraps(func)
86
+ async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> Optional[R]:
87
+ if await confirm(
88
+ f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n🔑 Kwargs:{kwargs}\n",
89
+ instruction="Please input [Yes/No] to proceed (default: Yes):",
90
+ ).ask_async():
91
+ return await func(*args, **kwargs)
92
+ logger.warning(f"Function: {func.__name__}{signature(func)} canceled by user.")
93
+ return None
94
+
95
+ else:
96
+
97
+ @wraps(func)
98
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> Optional[R]:
99
+ if confirm(
100
+ f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n��� Kwargs:{kwargs}\n",
101
+ instruction="Please input [Yes/No] to proceed (default: Yes):",
102
+ ).ask():
103
+ return func(*args, **kwargs)
104
+ logger.warning(f"Function: {func.__name__}{signature(func)} canceled by user.")
105
+ return None
71
106
 
72
107
  return _wrapper
108
+
109
+
110
+ def use_temp_module[**P, R](modules: ModuleType | List[ModuleType]) -> Callable[[Callable[P, R]], Callable[P, R]]:
111
+ """Temporarily inject modules into sys.modules during function execution.
112
+
113
+ This decorator allows you to temporarily inject one or more modules into sys.modules
114
+ while the decorated function executes. After execution, it restores the original
115
+ state of sys.modules.
116
+
117
+ Args:
118
+ modules (ModuleType | List[ModuleType]): A single module or list of modules to
119
+ temporarily inject into sys.modules.
120
+
121
+ Returns:
122
+ Callable[[Callable[P, R]], Callable[P, R]]: A decorator that handles temporary
123
+ module injection.
124
+
125
+ Examples:
126
+ ```python
127
+ from types import ModuleSpec, ModuleType, module_from_spec
128
+
129
+ # Create a temporary module
130
+ temp_module = module_from_spec(ModuleSpec("temp_math", None))
131
+ temp_module.pi = 3.14
132
+
133
+ # Use the decorator to temporarily inject the module
134
+ @use_temp_module(temp_module)
135
+ def calculate_area(radius: float) -> float:
136
+ from temp_math import pi
137
+ return pi * radius ** 2
138
+
139
+ # The temp_module is only available inside the function
140
+ result = calculate_area(5.0) # Uses temp_module.pi
141
+ ```
142
+
143
+ Multiple modules can also be injected:
144
+ ```python
145
+ module1 = module_from_spec(ModuleSpec("mod1", None))
146
+ module2 = module_from_spec(ModuleSpec("mod2", None))
147
+
148
+ @use_temp_module([module1, module2])
149
+ def process_data():
150
+ import mod1, mod2
151
+ # Work with temporary modules
152
+ ...
153
+ ```
154
+ """
155
+ module_list = [modules] if isinstance(modules, ModuleType) else modules
156
+
157
+ def _decorator(func: Callable[P, R]) -> Callable[P, R]:
158
+ @wraps(func)
159
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
160
+ import sys
161
+
162
+ # Store original modules if they exist
163
+ for module in module_list:
164
+ if module.__name__ in sys.modules:
165
+ raise RuntimeError(
166
+ f"Module '{module.__name__}' is already present in sys.modules and cannot be overridden."
167
+ )
168
+ sys.modules[module.__name__] = module
169
+
170
+ try:
171
+ return func(*args, **kwargs)
172
+ finally:
173
+ # Restore original state
174
+ for module in module_list:
175
+ del sys.modules[module.__name__]
176
+
177
+ return _wrapper
178
+
179
+ return _decorator
fabricatio/fs/curd.py CHANGED
@@ -5,11 +5,25 @@ import subprocess
5
5
  from pathlib import Path
6
6
  from typing import Union
7
7
 
8
- from fabricatio.decorators import confirm_to_execute, depend_on_external_cmd
8
+ from fabricatio.decorators import depend_on_external_cmd, logging_execution_info
9
9
  from fabricatio.journal import logger
10
10
 
11
11
 
12
- @confirm_to_execute
12
+ @logging_execution_info
13
+ def dump_text(path: Union[str, Path], text: str) -> None:
14
+ """Dump text to a file. you need to make sure the file's parent directory exists.
15
+
16
+ Args:
17
+ path(str, Path): Path to the file
18
+ text(str): Text to write to the file
19
+
20
+ Returns:
21
+ None
22
+ """
23
+ Path(path).write_text(text, encoding="utf-8", errors="ignore")
24
+
25
+
26
+ @logging_execution_info
13
27
  def copy_file(src: Union[str, Path], dst: Union[str, Path]) -> None:
14
28
  """Copy a file from source to destination.
15
29
 
@@ -29,7 +43,7 @@ def copy_file(src: Union[str, Path], dst: Union[str, Path]) -> None:
29
43
  raise
30
44
 
31
45
 
32
- @confirm_to_execute
46
+ @logging_execution_info
33
47
  def move_file(src: Union[str, Path], dst: Union[str, Path]) -> None:
34
48
  """Move a file from source to destination.
35
49
 
@@ -49,7 +63,7 @@ def move_file(src: Union[str, Path], dst: Union[str, Path]) -> None:
49
63
  raise
50
64
 
51
65
 
52
- @confirm_to_execute
66
+ @logging_execution_info
53
67
  def delete_file(file_path: Union[str, Path]) -> None:
54
68
  """Delete a file.
55
69
 
@@ -68,7 +82,7 @@ def delete_file(file_path: Union[str, Path]) -> None:
68
82
  raise
69
83
 
70
84
 
71
- @confirm_to_execute
85
+ @logging_execution_info
72
86
  def create_directory(dir_path: Union[str, Path], parents: bool = True, exist_ok: bool = True) -> None:
73
87
  """Create a directory.
74
88
 
@@ -85,7 +99,7 @@ def create_directory(dir_path: Union[str, Path], parents: bool = True, exist_ok:
85
99
  raise
86
100
 
87
101
 
88
- @confirm_to_execute
102
+ @logging_execution_info
89
103
  @depend_on_external_cmd(
90
104
  "erd",
91
105
  "Please install `erd` using `cargo install erdtree` or `scoop install erdtree`.",
@@ -97,7 +111,7 @@ def tree(dir_path: Union[str, Path]) -> str:
97
111
  return subprocess.check_output(("erd", dir_path.as_posix()), encoding="utf-8") # noqa: S603
98
112
 
99
113
 
100
- @confirm_to_execute
114
+ @logging_execution_info
101
115
  def delete_directory(dir_path: Union[str, Path]) -> None:
102
116
  """Delete a directory and its contents.
103
117
 
@@ -52,7 +52,7 @@ class Action(HandleTask, ProposeTask):
52
52
  return f"# The action you are going to perform: \n{super().briefing}"
53
53
 
54
54
 
55
- class WorkFlow[A: Union[Type[Action], Action]](WithBriefing, ToolBoxUsage):
55
+ class WorkFlow(WithBriefing, ToolBoxUsage):
56
56
  """Class that represents a workflow to be executed in a task."""
57
57
 
58
58
  _context: Queue[Dict[str, Any]] = PrivateAttr(default_factory=lambda: Queue(maxsize=1))
@@ -61,7 +61,7 @@ class WorkFlow[A: Union[Type[Action], Action]](WithBriefing, ToolBoxUsage):
61
61
  _instances: Tuple[Action, ...] = PrivateAttr(...)
62
62
  """ The instances of the workflow steps."""
63
63
 
64
- steps: Tuple[A, ...] = Field(...)
64
+ steps: Tuple[Union[Type[Action], Action], ...] = Field(...)
65
65
  """ The steps to be executed in the workflow, actions or action classes."""
66
66
  task_input_key: str = Field(default="task_input")
67
67
  """ The key of the task input data."""
@@ -1,7 +1,7 @@
1
1
  """A module for advanced models and functionalities."""
2
2
 
3
3
  from types import CodeType
4
- from typing import List, Optional, Tuple, Unpack
4
+ from typing import Any, Dict, List, Optional, Tuple, Unpack
5
5
 
6
6
  import orjson
7
7
  from fabricatio._rust_instances import template_manager
@@ -67,6 +67,7 @@ class HandleTask(WithBriefing, ToolBoxUsage):
67
67
  self,
68
68
  task: Task,
69
69
  tools: List[Tool],
70
+ data: Dict[str, Any],
70
71
  **kwargs: Unpack[LLMKwargs],
71
72
  ) -> Tuple[CodeType, List[str]]:
72
73
  """Asynchronously drafts the tool usage code for a task based on a given task object and tools."""
@@ -82,17 +83,23 @@ class HandleTask(WithBriefing, ToolBoxUsage):
82
83
  to_extract := JsonCapture.convert_with(response, orjson.loads)
83
84
  ):
84
85
  return source, to_extract
86
+
85
87
  return None
86
88
 
89
+ q = template_manager.render_template(
90
+ configs.templates.draft_tool_usage_code_template,
91
+ {
92
+ "data_module_name": configs.toolbox.data_module_name,
93
+ "tool_module_name": configs.toolbox.tool_module_name,
94
+ "task": task.briefing,
95
+ "deps": task.dependencies_prompt,
96
+ "tools": [{"name": t.name, "briefing": t.briefing} for t in tools],
97
+ "data": data,
98
+ },
99
+ )
100
+ logger.debug(f"Code Drafting Question: \n{q}")
87
101
  return await self.aask_validate(
88
- question=template_manager.render_template(
89
- configs.templates.draft_tool_usage_code_template,
90
- {
91
- "tool_module_name": configs.toolbox.tool_module_name,
92
- "task": task.briefing,
93
- "tools": [tool.briefing for tool in tools],
94
- },
95
- ),
102
+ question=q,
96
103
  validator=_validator,
97
104
  system_message=f"# your personal briefing: \n{self.briefing}",
98
105
  **kwargs,
@@ -101,18 +108,20 @@ class HandleTask(WithBriefing, ToolBoxUsage):
101
108
  async def handle_fin_grind(
102
109
  self,
103
110
  task: Task,
111
+ data: Dict[str, Any],
104
112
  **kwargs: Unpack[LLMKwargs],
105
113
  ) -> Optional[Tuple]:
106
114
  """Asynchronously handles a task based on a given task object and parameters."""
107
- logger.info(f"Handling task: {task.briefing}")
115
+ logger.info(f"Handling task: \n{task.briefing}")
108
116
 
109
117
  tools = await self.gather_tools(task)
110
- logger.info(f"{self.name} have gathered {len(tools)} tools gathered")
118
+ logger.info(f"{self.name} have gathered {[t.name for t in tools]}")
111
119
 
112
120
  if tools:
113
- executor = ToolExecutor(execute_sequence=tools)
114
- code, to_extract = await self.draft_tool_usage_code(task, tools, **kwargs)
115
- cxt = await executor.execute(code)
121
+ executor = ToolExecutor(candidates=tools, data=data)
122
+ code, to_extract = await self.draft_tool_usage_code(task, tools, data, **kwargs)
123
+
124
+ cxt = executor.execute(code)
116
125
  if to_extract:
117
126
  return tuple(cxt.get(k) for k in to_extract)
118
127
 
@@ -68,7 +68,7 @@ class WithDependency(Base):
68
68
  """Class that manages file dependencies."""
69
69
 
70
70
  dependencies: List[str] = Field(default_factory=list)
71
- """The file dependencies of the task, a list of file paths."""
71
+ """The file dependencies which is needed to read or write to meet a specific requirement, a list of file paths."""
72
72
 
73
73
  def add_dependency[P: str | Path](self, dependency: P | List[P]) -> Self:
74
74
  """Add a file dependency to the task.
@@ -99,6 +99,26 @@ class WithDependency(Base):
99
99
  self.dependencies.remove(Path(d).as_posix())
100
100
  return self
101
101
 
102
+ def clear_dependencies(self) -> Self:
103
+ """Clear all file dependencies from the task.
104
+
105
+ Returns:
106
+ Self: The current instance of the task.
107
+ """
108
+ self.dependencies.clear()
109
+ return self
110
+
111
+ def override_dependencies[P: str | Path](self, dependencies: List[P]) -> Self:
112
+ """Override the file dependencies of the task.
113
+
114
+ Args:
115
+ dependencies (List[str | Path]): The file dependencies to override the task's dependencies.
116
+
117
+ Returns:
118
+ Self: The current instance of the task.
119
+ """
120
+ return self.clear_dependencies().add_dependency(dependencies)
121
+
102
122
  @property
103
123
  def dependencies_prompt(self) -> str:
104
124
  """Generate a prompt for the task based on the file dependencies.
fabricatio/models/role.py CHANGED
@@ -9,7 +9,6 @@ from fabricatio.models.advanced import ProposeTask
9
9
  from fabricatio.models.events import Event
10
10
  from fabricatio.models.tool import ToolBox
11
11
  from fabricatio.models.usages import ToolBoxUsage
12
- from fabricatio.toolboxes import basic_toolboxes
13
12
  from pydantic import Field
14
13
 
15
14
 
@@ -19,7 +18,7 @@ class Role(ProposeTask, ToolBoxUsage):
19
18
  registry: dict[Event | str, WorkFlow] = Field(...)
20
19
  """ The registry of events and workflows."""
21
20
 
22
- toolboxes: Set[ToolBox] = Field(default=basic_toolboxes)
21
+ toolboxes: Set[ToolBox] = Field(default_factory=set)
23
22
 
24
23
  def model_post_init(self, __context: Any) -> None:
25
24
  """Register the workflows in the role to the event bus."""
fabricatio/models/task.py CHANGED
@@ -51,8 +51,8 @@ class Task[T](WithBriefing, WithJsonExample, WithDependency):
51
51
  description: str = Field(default="")
52
52
  """The description of the task."""
53
53
 
54
- goal: str = Field(default="")
55
- """The goal of the task."""
54
+ goal: List[str] = Field(default=[])
55
+ """The goal of the task, a list of strings."""
56
56
 
57
57
  namespace: List[str] = Field(default_factory=list)
58
58
  """The namespace of the task, a list of namespace segment, as string."""
@@ -99,12 +99,12 @@ class Task[T](WithBriefing, WithJsonExample, WithDependency):
99
99
  return self
100
100
 
101
101
  @classmethod
102
- def simple_task(cls, name: str, goal: str, description: str) -> Self:
102
+ def simple_task(cls, name: str, goal: List[str], description: str) -> Self:
103
103
  """Create a simple task with a name, goal, and description.
104
104
 
105
105
  Args:
106
106
  name (str): The name of the task.
107
- goal (str): The goal of the task.
107
+ goal (List[str]): The goal of the task.
108
108
  description (str): The description of the task.
109
109
 
110
110
  Returns:
@@ -112,18 +112,18 @@ class Task[T](WithBriefing, WithJsonExample, WithDependency):
112
112
  """
113
113
  return cls(name=name, goal=goal, description=description)
114
114
 
115
- def update_task(self, goal: Optional[str] = None, description: Optional[str] = None) -> Self:
115
+ def update_task(self, goal: Optional[List[str] | str] = None, description: Optional[str] = None) -> Self:
116
116
  """Update the goal and description of the task.
117
117
 
118
118
  Args:
119
- goal (str, optional): The new goal of the task.
119
+ goal (str|List[str], optional): The new goal of the task.
120
120
  description (str, optional): The new description of the task.
121
121
 
122
122
  Returns:
123
123
  Task: The updated instance of the `Task` class.
124
124
  """
125
125
  if goal:
126
- self.goal = goal
126
+ self.goal = goal if isinstance(goal, list) else [goal]
127
127
  if description:
128
128
  self.description = description
129
129
  return self
@@ -272,5 +272,5 @@ class Task[T](WithBriefing, WithJsonExample, WithDependency):
272
272
  """
273
273
  return template_manager.render_template(
274
274
  configs.templates.task_briefing_template,
275
- self.model_dump(include={"name", "description", "dependencies", "goal"}),
275
+ self.model_dump(),
276
276
  )
fabricatio/models/tool.py CHANGED
@@ -3,11 +3,11 @@
3
3
  from importlib.machinery import ModuleSpec
4
4
  from importlib.util import module_from_spec
5
5
  from inspect import iscoroutinefunction, signature
6
- from sys import modules
7
6
  from types import CodeType, ModuleType
8
7
  from typing import Any, Callable, Dict, List, Optional, Self, overload
9
8
 
10
9
  from fabricatio.config import configs
10
+ from fabricatio.decorators import use_temp_module
11
11
  from fabricatio.journal import logger
12
12
  from fabricatio.models.generic import WithBriefing
13
13
  from pydantic import BaseModel, ConfigDict, Field
@@ -36,7 +36,7 @@ class Tool[**P, R](WithBriefing):
36
36
 
37
37
  def invoke(self, *args: P.args, **kwargs: P.kwargs) -> R:
38
38
  """Invoke the tool's source function with the provided arguments."""
39
- logger.info(f"Invoking tool: {self.name} with args: {args} and kwargs: {kwargs}")
39
+ logger.info(f"Invoking tool: {self.name}")
40
40
  return self.source(*args, **kwargs)
41
41
 
42
42
  @property
@@ -127,21 +127,37 @@ class ToolExecutor(BaseModel):
127
127
  """A class representing a tool executor with a sequence of tools to execute."""
128
128
 
129
129
  model_config = ConfigDict(use_attribute_docstrings=True)
130
- execute_sequence: List[Tool] = Field(default_factory=list, frozen=True)
130
+ candidates: List[Tool] = Field(default_factory=list, frozen=True)
131
131
  """The sequence of tools to execute."""
132
132
 
133
+ data: Dict[str, Any] = Field(default_factory=dict)
134
+ """The data that could be used when invoking the tools."""
135
+
133
136
  def inject_tools[M: ModuleType](self, module: Optional[M] = None) -> M:
134
- """Inject the tools into the provided module."""
137
+ """Inject the tools into the provided module or default."""
135
138
  module = module or module_from_spec(spec=ModuleSpec(name=configs.toolbox.tool_module_name, loader=None))
136
- for tool in self.execute_sequence:
139
+ for tool in self.candidates:
140
+ logger.debug(f"Injecting tool: {tool.name}")
137
141
  setattr(module, tool.name, tool.invoke)
138
142
  return module
139
143
 
144
+ def inject_data[M: ModuleType](self, module: Optional[M] = None) -> M:
145
+ """Inject the data into the provided module or default."""
146
+ module = module or module_from_spec(spec=ModuleSpec(name=configs.toolbox.data_module_name, loader=None))
147
+ for key, value in self.data.items():
148
+ logger.debug(f"Injecting data: {key}")
149
+ setattr(module, key, value)
150
+ return module
151
+
140
152
  def execute[C: Dict[str, Any]](self, source: CodeType, cxt: Optional[C] = None) -> C:
141
153
  """Execute the sequence of tools with the provided context."""
142
- modules[configs.toolbox.tool_module_name] = self.inject_tools()
143
- exec(source, cxt) # noqa: S102
144
- modules.pop(configs.toolbox.tool_module_name)
154
+ cxt = cxt or {}
155
+
156
+ @use_temp_module([self.inject_data(), self.inject_tools()])
157
+ def _exec() -> None:
158
+ exec(source, cxt) # noqa: S102
159
+
160
+ _exec()
145
161
  return cxt
146
162
 
147
163
  @overload
@@ -169,4 +185,4 @@ class ToolExecutor(BaseModel):
169
185
  for toolbox in toolboxes:
170
186
  tools.append(toolbox[tool_name])
171
187
 
172
- return cls(execute_sequence=tools)
188
+ return cls(candidates=tools)
@@ -225,19 +225,24 @@ class LLMUsage(Base):
225
225
  configs.templates.make_choice_template,
226
226
  {
227
227
  "instruction": instruction,
228
- "options": [m.model_dump(include={"name", "briefing"}) for m in choices],
228
+ "options": [{"name": m.name, "briefing": m.briefing} for m in choices],
229
229
  "k": k,
230
230
  },
231
231
  )
232
- names = [c.name for c in choices]
232
+ names = {c.name for c in choices}
233
+ logger.debug(f"Start choosing between {names} with prompt: \n{prompt}")
233
234
 
234
235
  def _validate(response: str) -> List[T] | None:
235
236
  ret = JsonCapture.convert_with(response, orjson.loads)
236
- if not isinstance(ret, List) or len(ret) != k:
237
+
238
+ if not isinstance(ret, List) or (0 < k != len(ret)):
239
+ logger.error(f"Incorrect Type or length of response: \n{ret}")
237
240
  return None
238
241
  if any(n not in names for n in ret):
242
+ logger.error(f"Invalid choice in response: \n{ret}")
239
243
  return None
240
- return ret
244
+
245
+ return [next(toolbox for toolbox in choices if toolbox.name == toolbox_str) for toolbox_str in ret]
241
246
 
242
247
  return await self.aask_validate(
243
248
  question=prompt,
@@ -247,6 +252,41 @@ class LLMUsage(Base):
247
252
  **kwargs,
248
253
  )
249
254
 
255
+ async def apick[T: WithBriefing](
256
+ self,
257
+ instruction: str,
258
+ choices: List[T],
259
+ max_validations: PositiveInt = 2,
260
+ system_message: str = "",
261
+ **kwargs: Unpack[LLMKwargs],
262
+ ) -> T:
263
+ """Asynchronously picks a single choice from a list of options using AI validation.
264
+
265
+ This method is a convenience wrapper around `achoose` that always selects exactly one item.
266
+
267
+ Args:
268
+ instruction (str): The user-provided instruction/question description.
269
+ choices (List[T]): A list of candidate options, requiring elements to have `name` and `briefing` fields.
270
+ max_validations (PositiveInt): Maximum number of validation failures, default is 2.
271
+ system_message (str): Custom system-level prompt, defaults to an empty string.
272
+ **kwargs (Unpack[LLMKwargs]): Additional keyword arguments for the LLM usage, such as `model`,
273
+ `temperature`, `stop`, `top_p`, `max_tokens`, `stream`, `timeout`, and `max_retries`.
274
+
275
+ Returns:
276
+ T: The single selected item from the choices list.
277
+
278
+ Raises:
279
+ ValueError: If validation fails after maximum attempts or if no valid selection is made.
280
+ """
281
+ return await self.achoose(
282
+ instruction=instruction,
283
+ choices=choices,
284
+ k=1,
285
+ max_validations=max_validations,
286
+ system_message=system_message,
287
+ **kwargs,
288
+ )[0]
289
+
250
290
  async def ajudge(
251
291
  self,
252
292
  prompt: str,
fabricatio/parser.py CHANGED
@@ -49,8 +49,12 @@ class Capture(BaseModel):
49
49
  return None
50
50
 
51
51
  if self.target_groups:
52
- return tuple(match.group(g) for g in self.target_groups)
53
- return match.group(1)
52
+ cap = tuple(match.group(g) for g in self.target_groups)
53
+ logger.debug(f"Captured text: {'\n\n'.join(cap)}")
54
+ return cap
55
+ cap = match.group(1)
56
+ logger.debug(f"Captured text: \n{cap}")
57
+ return cap
54
58
 
55
59
  def convert_with[T](self, text: str, convertor: Callable[[Tuple[str, ...]], T] | Callable[[str], T]) -> T | None:
56
60
  """Convert the given text using the pattern.
@@ -62,12 +66,12 @@ class Capture(BaseModel):
62
66
  Returns:
63
67
  str | None: The converted text if the pattern is found, otherwise None.
64
68
  """
65
- if cap := self.capture(text) is None:
69
+ if (cap := self.capture(text)) is None:
66
70
  return None
67
71
  try:
68
72
  return convertor(cap)
69
73
  except (ValueError, SyntaxError) as e:
70
- logger.error(f"Failed to convert text using convertor: {convertor.__name__}, error: \n{e}")
74
+ logger.error(f"Failed to convert text using {convertor.__name__} to convert.\nerror: {e}\n {cap}")
71
75
  return None
72
76
 
73
77
  @classmethod