fabricatio 0.2.6.dev3__cp39-cp39-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.
Files changed (42) hide show
  1. fabricatio/__init__.py +60 -0
  2. fabricatio/_rust.cp39-win_amd64.pyd +0 -0
  3. fabricatio/_rust.pyi +116 -0
  4. fabricatio/_rust_instances.py +10 -0
  5. fabricatio/actions/article.py +81 -0
  6. fabricatio/actions/output.py +19 -0
  7. fabricatio/actions/rag.py +25 -0
  8. fabricatio/capabilities/correct.py +115 -0
  9. fabricatio/capabilities/propose.py +49 -0
  10. fabricatio/capabilities/rag.py +369 -0
  11. fabricatio/capabilities/rating.py +339 -0
  12. fabricatio/capabilities/review.py +278 -0
  13. fabricatio/capabilities/task.py +113 -0
  14. fabricatio/config.py +400 -0
  15. fabricatio/core.py +181 -0
  16. fabricatio/decorators.py +179 -0
  17. fabricatio/fs/__init__.py +29 -0
  18. fabricatio/fs/curd.py +149 -0
  19. fabricatio/fs/readers.py +46 -0
  20. fabricatio/journal.py +21 -0
  21. fabricatio/models/action.py +158 -0
  22. fabricatio/models/events.py +120 -0
  23. fabricatio/models/extra.py +171 -0
  24. fabricatio/models/generic.py +406 -0
  25. fabricatio/models/kwargs_types.py +158 -0
  26. fabricatio/models/role.py +48 -0
  27. fabricatio/models/task.py +299 -0
  28. fabricatio/models/tool.py +189 -0
  29. fabricatio/models/usages.py +682 -0
  30. fabricatio/models/utils.py +167 -0
  31. fabricatio/parser.py +149 -0
  32. fabricatio/py.typed +0 -0
  33. fabricatio/toolboxes/__init__.py +15 -0
  34. fabricatio/toolboxes/arithmetic.py +62 -0
  35. fabricatio/toolboxes/fs.py +31 -0
  36. fabricatio/workflows/articles.py +15 -0
  37. fabricatio/workflows/rag.py +11 -0
  38. fabricatio-0.2.6.dev3.data/scripts/tdown.exe +0 -0
  39. fabricatio-0.2.6.dev3.dist-info/METADATA +432 -0
  40. fabricatio-0.2.6.dev3.dist-info/RECORD +42 -0
  41. fabricatio-0.2.6.dev3.dist-info/WHEEL +4 -0
  42. fabricatio-0.2.6.dev3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,179 @@
1
+ """Decorators for Fabricatio."""
2
+
3
+ from asyncio import iscoroutinefunction
4
+ from functools import wraps
5
+ from inspect import signature
6
+ from shutil import which
7
+ from types import ModuleType
8
+ from typing import Callable, List, Optional
9
+
10
+ from questionary import confirm
11
+
12
+ from fabricatio.config import configs
13
+ from fabricatio.journal import logger
14
+
15
+
16
+ def depend_on_external_cmd[**P, R](
17
+ bin_name: str, install_tip: Optional[str], homepage: Optional[str] = None
18
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
19
+ """Decorator to check for the presence of an external command.
20
+
21
+ Args:
22
+ bin_name (str): The name of the required binary.
23
+ install_tip (Optional[str]): Installation instructions for the required binary.
24
+ homepage (Optional[str]): The homepage of the required binary.
25
+
26
+ Returns:
27
+ Callable[[Callable[P, R]], Callable[P, R]]: A decorator that wraps the function to check for the binary.
28
+
29
+ Raises:
30
+ RuntimeError: If the required binary is not found.
31
+ """
32
+
33
+ def _decorator(func: Callable[P, R]) -> Callable[P, R]:
34
+ @wraps(func)
35
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
36
+ if which(bin_name) is None:
37
+ err = f"`{bin_name}` is required to run {func.__name__}{signature(func)}, please install it the to `PATH` first."
38
+ if install_tip is not None:
39
+ err += f"\nInstall tip: {install_tip}"
40
+ if homepage is not None:
41
+ err += f"\nHomepage: {homepage}"
42
+ logger.error(err)
43
+ raise RuntimeError(err)
44
+ return func(*args, **kwargs)
45
+
46
+ return _wrapper
47
+
48
+ return _decorator
49
+
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
+
70
+ def confirm_to_execute[**P, R](func: Callable[P, R]) -> Callable[P, Optional[R]] | Callable[P, R]:
71
+ """Decorator to confirm before executing a function.
72
+
73
+ Args:
74
+ func (Callable): The function to be executed
75
+
76
+ Returns:
77
+ Callable: A decorator that wraps the function to confirm before execution.
78
+ """
79
+ if not configs.general.confirm_on_ops:
80
+ # Skip confirmation if the configuration is set to False
81
+ return func
82
+
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
106
+
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
@@ -0,0 +1,29 @@
1
+ """FileSystem manipulation module for Fabricatio."""
2
+
3
+ from fabricatio.fs.curd import (
4
+ absolute_path,
5
+ copy_file,
6
+ create_directory,
7
+ delete_directory,
8
+ delete_file,
9
+ dump_text,
10
+ gather_files,
11
+ move_file,
12
+ tree,
13
+ )
14
+ from fabricatio.fs.readers import MAGIKA, safe_json_read, safe_text_read
15
+
16
+ __all__ = [
17
+ "MAGIKA",
18
+ "absolute_path",
19
+ "copy_file",
20
+ "create_directory",
21
+ "delete_directory",
22
+ "delete_file",
23
+ "dump_text",
24
+ "gather_files",
25
+ "move_file",
26
+ "safe_json_read",
27
+ "safe_text_read",
28
+ "tree",
29
+ ]
fabricatio/fs/curd.py ADDED
@@ -0,0 +1,149 @@
1
+ """File system create, update, read, delete operations."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from os import PathLike
6
+ from pathlib import Path
7
+ from typing import Union
8
+
9
+ from fabricatio.decorators import depend_on_external_cmd
10
+ from fabricatio.journal import logger
11
+
12
+
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
+ def copy_file(src: Union[str, Path], dst: Union[str, Path]) -> None:
27
+ """Copy a file from source to destination.
28
+
29
+ Args:
30
+ src: Source file path
31
+ dst: Destination file path
32
+
33
+ Raises:
34
+ FileNotFoundError: If source file doesn't exist
35
+ shutil.SameFileError: If source and destination are the same
36
+ """
37
+ try:
38
+ shutil.copy(src, dst)
39
+ logger.info(f"Copied file from {src} to {dst}")
40
+ except OSError as e:
41
+ logger.error(f"Failed to copy file from {src} to {dst}: {e!s}")
42
+ raise
43
+
44
+
45
+ def move_file(src: Union[str, Path], dst: Union[str, Path]) -> None:
46
+ """Move a file from source to destination.
47
+
48
+ Args:
49
+ src: Source file path
50
+ dst: Destination file path
51
+
52
+ Raises:
53
+ FileNotFoundError: If source file doesn't exist
54
+ shutil.SameFileError: If source and destination are the same
55
+ """
56
+ try:
57
+ shutil.move(src, dst)
58
+ logger.info(f"Moved file from {src} to {dst}")
59
+ except OSError as e:
60
+ logger.error(f"Failed to move file from {src} to {dst}: {e!s}")
61
+ raise
62
+
63
+
64
+ def delete_file(file_path: Union[str, Path]) -> None:
65
+ """Delete a file.
66
+
67
+ Args:
68
+ file_path: Path to the file to be deleted
69
+
70
+ Raises:
71
+ FileNotFoundError: If file doesn't exist
72
+ PermissionError: If no permission to delete the file
73
+ """
74
+ try:
75
+ Path(file_path).unlink()
76
+ logger.info(f"Deleted file: {file_path}")
77
+ except OSError as e:
78
+ logger.error(f"Failed to delete file {file_path}: {e!s}")
79
+ raise
80
+
81
+
82
+ def create_directory(dir_path: Union[str, Path], parents: bool = True, exist_ok: bool = True) -> None:
83
+ """Create a directory.
84
+
85
+ Args:
86
+ dir_path: Path to the directory to create
87
+ parents: Create parent directories if they don't exist
88
+ exist_ok: Don't raise error if directory already exists
89
+ """
90
+ try:
91
+ Path(dir_path).mkdir(parents=parents, exist_ok=exist_ok)
92
+ logger.info(f"Created directory: {dir_path}")
93
+ except OSError as e:
94
+ logger.error(f"Failed to create directory {dir_path}: {e!s}")
95
+ raise
96
+
97
+
98
+ @depend_on_external_cmd(
99
+ "erd",
100
+ "Please install `erd` using `cargo install erdtree` or `scoop install erdtree`.",
101
+ "https://github.com/solidiquis/erdtree",
102
+ )
103
+ def tree(dir_path: Union[str, Path]) -> str:
104
+ """Generate a tree representation of the directory structure. Requires `erd` to be installed."""
105
+ dir_path = Path(dir_path)
106
+ return subprocess.check_output(("erd", dir_path.as_posix()), encoding="utf-8") # noqa: S603
107
+
108
+
109
+ def delete_directory(dir_path: Union[str, Path]) -> None:
110
+ """Delete a directory and its contents.
111
+
112
+ Args:
113
+ dir_path: Path to the directory to delete
114
+
115
+ Raises:
116
+ FileNotFoundError: If directory doesn't exist
117
+ OSError: If directory is not empty and can't be removed
118
+ """
119
+ try:
120
+ shutil.rmtree(dir_path)
121
+ logger.info(f"Deleted directory: {dir_path}")
122
+ except OSError as e:
123
+ logger.error(f"Failed to delete directory {dir_path}: {e!s}")
124
+ raise
125
+
126
+
127
+ def absolute_path(path: str | Path | PathLike) -> str:
128
+ """Get the absolute path of a file or directory.
129
+
130
+ Args:
131
+ path (str, Path, PathLike): The path to the file or directory.
132
+
133
+ Returns:
134
+ str: The absolute path of the file or directory.
135
+ """
136
+ return Path(path).expanduser().resolve().as_posix()
137
+
138
+
139
+ def gather_files(directory: str | Path | PathLike, extension: str) -> list[str]:
140
+ """Gather all files with a specific extension in a directory.
141
+
142
+ Args:
143
+ directory (str, Path, PathLike): The directory to search in.
144
+ extension (str): The file extension to look for.
145
+
146
+ Returns:
147
+ list[str]: A list of file paths with the specified extension.
148
+ """
149
+ return [file.as_posix() for file in Path(directory).rglob(f"*.{extension}")]
@@ -0,0 +1,46 @@
1
+ """Filesystem readers for Fabricatio."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict
5
+
6
+ import orjson
7
+ from magika import Magika
8
+
9
+ from fabricatio.config import configs
10
+ from fabricatio.journal import logger
11
+
12
+ MAGIKA = Magika(model_dir=configs.magika.model_dir)
13
+
14
+
15
+ def safe_text_read(path: Path | str) -> str:
16
+ """Safely read the text from a file.
17
+
18
+ Args:
19
+ path (Path|str): The path to the file.
20
+
21
+ Returns:
22
+ str: The text from the file.
23
+ """
24
+ path = Path(path)
25
+ try:
26
+ return path.read_text(encoding="utf-8")
27
+ except (UnicodeDecodeError, IsADirectoryError, FileNotFoundError) as e:
28
+ logger.error(f"Failed to read file {path}: {e!s}")
29
+ return ""
30
+
31
+
32
+ def safe_json_read(path: Path | str) -> Dict:
33
+ """Safely read the JSON from a file.
34
+
35
+ Args:
36
+ path (Path|str): The path to the file.
37
+
38
+ Returns:
39
+ dict: The JSON from the file.
40
+ """
41
+ path = Path(path)
42
+ try:
43
+ return orjson.loads(path.read_text(encoding="utf-8"))
44
+ except (orjson.JSONDecodeError, IsADirectoryError, FileNotFoundError) as e:
45
+ logger.error(f"Failed to read file {path}: {e!s}")
46
+ return {}
fabricatio/journal.py ADDED
@@ -0,0 +1,21 @@
1
+ """Logging setup for the project."""
2
+
3
+ import sys
4
+
5
+ from loguru import logger
6
+ from rich import pretty, traceback
7
+
8
+ from fabricatio.config import configs
9
+
10
+ pretty.install()
11
+ traceback.install()
12
+ logger.remove()
13
+ logger.add(
14
+ configs.debug.log_file,
15
+ level=configs.debug.log_level,
16
+ rotation=f"{configs.debug.rotation} weeks",
17
+ retention=f"{configs.debug.retention} weeks",
18
+ )
19
+ logger.add(sys.stderr, level=configs.debug.log_level)
20
+
21
+ __all__ = ["logger"]
@@ -0,0 +1,158 @@
1
+ """Module that contains the classes for actions and workflows."""
2
+
3
+ import traceback
4
+ from abc import abstractmethod
5
+ from asyncio import Queue, create_task
6
+ from typing import Any, Dict, Self, Tuple, Type, Union, final
7
+
8
+ from fabricatio.capabilities.correct import Correct
9
+ from fabricatio.capabilities.task import HandleTask, ProposeTask
10
+ from fabricatio.journal import logger
11
+ from fabricatio.models.generic import WithBriefing
12
+ from fabricatio.models.task import Task
13
+ from fabricatio.models.usages import ToolBoxUsage
14
+ from pydantic import Field, PrivateAttr
15
+
16
+
17
+ class Action(HandleTask, ProposeTask, Correct):
18
+ """Class that represents an action to be executed in a workflow."""
19
+
20
+ name: str = Field(default="")
21
+ """The name of the action."""
22
+ description: str = Field(default="")
23
+ """The description of the action."""
24
+ personality: str = Field(default="")
25
+ """The personality of whom the action belongs to."""
26
+ output_key: str = Field(default="")
27
+ """The key of the output data."""
28
+
29
+ @final
30
+ def model_post_init(self, __context: Any) -> None:
31
+ """Initialize the action by setting the name if not provided.
32
+
33
+ Args:
34
+ __context: The context to be used for initialization.
35
+ """
36
+ self.name = self.name or self.__class__.__name__
37
+ self.description = self.description or self.__class__.__doc__ or ""
38
+
39
+ @abstractmethod
40
+ async def _execute(self, **cxt) -> Any:
41
+ """Execute the action with the provided arguments.
42
+
43
+ Args:
44
+ **cxt: The context dictionary containing input and output data.
45
+
46
+ Returns:
47
+ The result of the action execution.
48
+ """
49
+ pass
50
+
51
+ @final
52
+ async def act(self, cxt: Dict[str, Any]) -> Dict[str, Any]:
53
+ """Perform the action by executing it and setting the output data.
54
+
55
+ Args:
56
+ cxt: The context dictionary containing input and output data.
57
+ """
58
+ ret = await self._execute(**cxt)
59
+ if self.output_key:
60
+ logger.debug(f"Setting output: {self.output_key}")
61
+ cxt[self.output_key] = ret
62
+ return cxt
63
+
64
+ @property
65
+ def briefing(self) -> str:
66
+ """Return a brief description of the action."""
67
+ if self.personality:
68
+ return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
69
+ return f"# The action you are going to perform: \n{super().briefing}"
70
+
71
+
72
+ class WorkFlow(WithBriefing, ToolBoxUsage):
73
+ """Class that represents a workflow to be executed in a task."""
74
+
75
+ _context: Queue[Dict[str, Any]] = PrivateAttr(default_factory=lambda: Queue(maxsize=1))
76
+ """ The context dictionary to be used for workflow execution."""
77
+
78
+ _instances: Tuple[Action, ...] = PrivateAttr(default_factory=tuple)
79
+ """ The instances of the workflow steps."""
80
+
81
+ steps: Tuple[Union[Type[Action], Action], ...] = Field(...)
82
+ """ The steps to be executed in the workflow, actions or action classes."""
83
+ task_input_key: str = Field(default="task_input")
84
+ """ The key of the task input data."""
85
+ task_output_key: str = Field(default="task_output")
86
+ """ The key of the task output data."""
87
+ extra_init_context: Dict[str, Any] = Field(default_factory=dict, frozen=True)
88
+ """ The extra context dictionary to be used for workflow initialization."""
89
+
90
+ def model_post_init(self, __context: Any) -> None:
91
+ """Initialize the workflow by setting fallbacks for each step.
92
+
93
+ Args:
94
+ __context: The context to be used for initialization.
95
+ """
96
+ temp = []
97
+ for step in self.steps:
98
+ temp.append(step if isinstance(step, Action) else step())
99
+ self._instances = tuple(temp)
100
+
101
+ def inject_personality(self, personality: str) -> Self:
102
+ """Inject the personality of the workflow.
103
+
104
+ Args:
105
+ personality: The personality to be injected.
106
+
107
+ Returns:
108
+ Self: The instance of the workflow with the injected personality.
109
+ """
110
+ for a in filter(lambda action: not action.personality, self._instances):
111
+ a.personality = personality
112
+ return self
113
+
114
+ async def serve(self, task: Task) -> None:
115
+ """Serve the task by executing the workflow steps.
116
+
117
+ Args:
118
+ task: The task to be served.
119
+ """
120
+ await task.start()
121
+ await self._init_context(task)
122
+ current_action = None
123
+ try:
124
+ for step in self._instances:
125
+ logger.debug(f"Executing step: {(current_action := step.name)}")
126
+ act_task = create_task(step.act(await self._context.get()))
127
+ if task.is_cancelled():
128
+ act_task.cancel(f"Cancelled by task: {task.name}")
129
+ break
130
+ modified_ctx = await act_task
131
+ await self._context.put(modified_ctx)
132
+ logger.info(f"Finished executing workflow: {self.name}")
133
+
134
+ if self.task_output_key not in (final_ctx := await self._context.get()):
135
+ logger.warning(
136
+ f"Task output key: {self.task_output_key} not found in the context, None will be returned. You can check if `Action.output_key` is set the same as `WorkFlow.task_output_key`."
137
+ )
138
+
139
+ await task.finish(final_ctx.get(self.task_output_key, None))
140
+ except RuntimeError as e:
141
+ logger.error(f"Error during task: {current_action} execution: {e}") # Log the exception
142
+ logger.error(traceback.format_exc()) # Add this line to log the traceback
143
+ await task.fail() # Mark the task as failed
144
+
145
+ async def _init_context[T](self, task: Task[T]) -> None:
146
+ """Initialize the context dictionary for workflow execution."""
147
+ logger.debug(f"Initializing context for workflow: {self.name}")
148
+ await self._context.put({self.task_input_key: task, **dict(self.extra_init_context)})
149
+
150
+ def steps_fallback_to_self(self) -> Self:
151
+ """Set the fallback for each step to the workflow itself."""
152
+ self.hold_to(self._instances)
153
+ return self
154
+
155
+ def steps_supply_tools_from_self(self) -> Self:
156
+ """Supply the tools from the workflow to each step."""
157
+ self.provide_tools_to(self._instances)
158
+ return self