fabricatio 0.2.0__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.
@@ -0,0 +1,187 @@
1
+ """A module for defining tools and toolboxes."""
2
+
3
+ from importlib.machinery import ModuleSpec
4
+ from importlib.util import module_from_spec
5
+ from inspect import iscoroutinefunction, signature
6
+ from types import CodeType, ModuleType
7
+ from typing import Any, Callable, Dict, List, Optional, Self, overload
8
+
9
+ from fabricatio.config import configs
10
+ from fabricatio.decorators import use_temp_module
11
+ from fabricatio.journal import logger
12
+ from fabricatio.models.generic import WithBriefing
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class Tool[**P, R](WithBriefing):
17
+ """A class representing a tool with a callable source function."""
18
+
19
+ name: str = Field(default="")
20
+ """The name of the tool."""
21
+
22
+ description: str = Field(default="")
23
+ """The description of the tool."""
24
+
25
+ source: Callable[P, R]
26
+ """The source function of the tool."""
27
+
28
+ def model_post_init(self, __context: Any) -> None:
29
+ """Initialize the tool with a name and a source function."""
30
+ self.name = self.name or self.source.__name__
31
+
32
+ if not self.name:
33
+ raise RuntimeError("The tool must have a source function.")
34
+ self.description = self.description or self.source.__doc__ or ""
35
+ self.description = self.description.strip()
36
+
37
+ def invoke(self, *args: P.args, **kwargs: P.kwargs) -> R:
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}")
40
+ return self.source(*args, **kwargs)
41
+
42
+ @property
43
+ def briefing(self) -> str:
44
+ """Return a brief description of the tool.
45
+
46
+ Returns:
47
+ str: A brief description of the tool.
48
+ """
49
+ # 获取源函数的返回类型
50
+
51
+ return f"{'async ' if iscoroutinefunction(self.source) else ''}def {self.name}{signature(self.source)}\n{_desc_wrapper(self.description)}"
52
+
53
+
54
+ def _desc_wrapper(desc: str) -> str:
55
+ lines = desc.split("\n")
56
+ lines_indent = [f" {line}" for line in ['"""', *lines, '"""']]
57
+ return "\n".join(lines_indent)
58
+
59
+
60
+ class ToolBox(WithBriefing):
61
+ """A class representing a collection of tools."""
62
+
63
+ tools: List[Tool] = Field(default_factory=list, frozen=True)
64
+ """A list of tools in the toolbox."""
65
+
66
+ def collect_tool[**P, R](self, func: Callable[P, R]) -> Callable[P, R]:
67
+ """Add a callable function to the toolbox as a tool.
68
+
69
+ Args:
70
+ func (Callable[P, R]): The function to be added as a tool.
71
+
72
+ Returns:
73
+ Callable[P, R]: The added function.
74
+ """
75
+ self.tools.append(Tool(source=func))
76
+ return func
77
+
78
+ def add_tool[**P, R](self, func: Callable[P, R]) -> Self:
79
+ """Add a callable function to the toolbox as a tool.
80
+
81
+ Args:
82
+ func (Callable): The function to be added as a tool.
83
+
84
+ Returns:
85
+ Self: The current instance of the toolbox.
86
+ """
87
+ self.tools.append(Tool(source=func))
88
+ return self
89
+
90
+ @property
91
+ def briefing(self) -> str:
92
+ """Return a brief description of the toolbox.
93
+
94
+ Returns:
95
+ str: A brief description of the toolbox.
96
+ """
97
+ list_out = "\n\n".join([f"{tool.briefing}" for tool in self.tools])
98
+ toc = f"## {self.name}: {self.description}\n## {len(self.tools)} tools available:"
99
+ return f"{toc}\n\n{list_out}"
100
+
101
+ def get[**P, R](self, name: str) -> Tool[P, R]:
102
+ """Invoke a tool by name with the provided arguments.
103
+
104
+ Args:
105
+ name (str): The name of the tool to invoke.
106
+
107
+ Returns:
108
+ Tool: The tool instance with the specified name.
109
+
110
+ Raises:
111
+ ValueError: If no tool with the specified name is found.
112
+ """
113
+ tool = next((tool for tool in self.tools if tool.name == name), None)
114
+ if tool is None:
115
+ err = f"No tool with the name {name} found in the toolbox."
116
+ logger.error(err)
117
+ raise ValueError(err)
118
+
119
+ return tool
120
+
121
+ def __hash__(self) -> int:
122
+ """Return a hash of the toolbox based on its briefing."""
123
+ return hash(self.briefing)
124
+
125
+
126
+ class ToolExecutor(BaseModel):
127
+ """A class representing a tool executor with a sequence of tools to execute."""
128
+
129
+ model_config = ConfigDict(use_attribute_docstrings=True)
130
+ candidates: List[Tool] = Field(default_factory=list, frozen=True)
131
+ """The sequence of tools to execute."""
132
+
133
+ data: Dict[str, Any] = Field(default_factory=dict)
134
+ """The data that could be used when invoking the tools."""
135
+
136
+ def inject_tools[M: ModuleType](self, module: Optional[M] = None) -> M:
137
+ """Inject the tools into the provided module or default."""
138
+ module = module or module_from_spec(spec=ModuleSpec(name=configs.toolbox.tool_module_name, loader=None))
139
+ for tool in self.candidates:
140
+ logger.debug(f"Injecting tool: {tool.name}")
141
+ setattr(module, tool.name, tool.invoke)
142
+ return module
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
+
152
+ def execute[C: Dict[str, Any]](self, source: CodeType, cxt: Optional[C] = None) -> C:
153
+ """Execute the sequence of tools with the provided context."""
154
+
155
+ @use_temp_module([self.inject_data(), self.inject_tools()])
156
+ def _exec() -> None:
157
+ exec(source, cxt) # noqa: S102
158
+
159
+ _exec()
160
+ return cxt
161
+
162
+ @overload
163
+ def take[C: Dict[str, Any]](self, keys: List[str], source: CodeType, cxt: Optional[C] = None) -> C:
164
+ """Check the output of the tools with the provided context."""
165
+ ...
166
+
167
+ @overload
168
+ def take[C: Dict[str, Any]](self, keys: str, source: CodeType, cxt: Optional[C] = None) -> Any:
169
+ """Check the output of the tools with the provided context."""
170
+ ...
171
+
172
+ def take[C: Dict[str, Any]](self, keys: List[str] | str, source: CodeType, cxt: Optional[C] = None) -> C | Any:
173
+ """Check the output of the tools with the provided context."""
174
+ cxt = self.execute(source, cxt)
175
+ if isinstance(keys, str):
176
+ return cxt[keys]
177
+ return {key: cxt[key] for key in keys}
178
+
179
+ @classmethod
180
+ def from_recipe(cls, recipe: List[str], toolboxes: List[ToolBox]) -> Self:
181
+ """Create a tool executor from a recipe and a list of toolboxes."""
182
+ tools = []
183
+ while tool_name := recipe.pop(0):
184
+ for toolbox in toolboxes:
185
+ tools.append(toolbox[tool_name])
186
+
187
+ return cls(candidates=tools)