fabricatio 0.1.0__py3-none-any.whl → 0.1.2__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.
fabricatio/models/role.py CHANGED
@@ -1,14 +1,50 @@
1
- from typing import List
1
+ from typing import Any
2
2
 
3
- from pydantic import Field
3
+ from pydantic import Field, ValidationError
4
4
 
5
+ from fabricatio.core import env
6
+ from fabricatio.journal import logger
5
7
  from fabricatio.models.action import WorkFlow
6
- from fabricatio.models.generic import Memorable, WithToDo, WithBriefing, LLMUsage
8
+ from fabricatio.models.events import Event
9
+ from fabricatio.models.generic import LLMUsage, Memorable, WithBriefing, WithToDo
10
+ from fabricatio.models.task import Task
11
+ from fabricatio.parser import JsonCapture
7
12
 
8
13
 
9
- class Role[T: WorkFlow](Memorable, WithBriefing, WithToDo, LLMUsage):
10
- workflows: List[T] = Field(frozen=True)
11
- """A list of action names that the role can perform."""
14
+ class Role(Memorable, WithBriefing, WithToDo, LLMUsage):
15
+ """Class that represents a role with a registry of events and workflows."""
12
16
 
13
- async def act(self):
14
- pass
17
+ registry: dict[Event | str, WorkFlow] = Field(...)
18
+ """ The registry of events and workflows."""
19
+
20
+ def model_post_init(self, __context: Any) -> None:
21
+ """Register the workflows in the role to the event bus."""
22
+ for event, workflow in self.registry.items():
23
+ workflow.fallback_to(self)
24
+ logger.debug(
25
+ f"Registering workflow: {workflow.name} for event: {event.collapse() if isinstance(event, Event) else event}"
26
+ )
27
+ env.on(event, workflow.serve)
28
+
29
+ async def propose(self, prompt: str) -> Task:
30
+ """Propose a task based on the provided prompt."""
31
+ assert prompt, "Prompt must be provided."
32
+
33
+ def _validate_json(response: str) -> None | Task:
34
+ try:
35
+ cap = JsonCapture.capture(response)
36
+ logger.debug(f"Response: \n{response}")
37
+ logger.info(f"Captured JSON: \n{cap[0]}")
38
+ return Task.model_validate_json(cap[0] if cap else response)
39
+ except ValidationError as e:
40
+ logger.error(f"Failed to parse task from JSON: {e}")
41
+ return None
42
+
43
+ return await self.aask_validate(
44
+ f"{prompt} \n\nBased on requirement above, "
45
+ f"you need to construct a task to satisfy that requirement in JSON format "
46
+ f"written like this: \n\n```json\n{Task.json_example()}\n```\n\n"
47
+ f"No extra explanation needed. ",
48
+ _validate_json,
49
+ system_message=f"# your personal briefing: \n{self.briefing}",
50
+ )
@@ -0,0 +1,224 @@
1
+ """This module defines the Task class, which represents a task with a status and output.
2
+
3
+ It includes methods to manage the task's lifecycle, such as starting, finishing, cancelling, and failing the task.
4
+ """
5
+
6
+ from asyncio import Queue
7
+ from enum import Enum
8
+ from typing import Optional, Self
9
+
10
+ from pydantic import Field, PrivateAttr
11
+
12
+ from fabricatio.config import configs
13
+ from fabricatio.core import env
14
+ from fabricatio.journal import logger
15
+ from fabricatio.models.generic import WithBriefing, WithJsonExample
16
+
17
+
18
+ class TaskStatus(Enum):
19
+ """Enum that represents the status of a task."""
20
+
21
+ Pending = "pending"
22
+ Running = "running"
23
+ Finished = "finished"
24
+ Failed = "failed"
25
+ Cancelled = "cancelled"
26
+
27
+
28
+ class Task[T](WithBriefing, WithJsonExample):
29
+ """Class that represents a task with a status and output.
30
+
31
+ Attributes:
32
+ name (str): The name of the task.
33
+ description (str): The description of the task.
34
+ _output (Queue): The output queue of the task.
35
+ _status (TaskStatus): The status of the task.
36
+ goal (str): The goal of the task.
37
+ """
38
+
39
+ name: str = Field(...)
40
+ """The name of the task."""
41
+
42
+ description: str = Field(default="")
43
+ """The description of the task."""
44
+
45
+ goal: str = Field(default="")
46
+ """The goal of the task."""
47
+
48
+ _output: Queue = PrivateAttr(default_factory=lambda: Queue(maxsize=1))
49
+ """The output queue of the task."""
50
+ _status: TaskStatus = PrivateAttr(default=TaskStatus.Pending)
51
+ """The status of the task."""
52
+
53
+ @classmethod
54
+ def simple_task(cls, name: str, goal: str, description: str) -> Self:
55
+ """Create a simple task with a name, goal, and description.
56
+
57
+ Args:
58
+ name (str): The name of the task.
59
+ goal (str): The goal of the task.
60
+ description (str): The description of the task.
61
+
62
+ Returns:
63
+ Self: A new instance of the Task class.
64
+ """
65
+ return cls(name=name, goal=goal, description=description)
66
+
67
+ def update_task(self, goal: Optional[str] = None, description: Optional[str] = None) -> Self:
68
+ """Update the goal and description of the task.
69
+
70
+ Args:
71
+ goal (Optional[str]): The new goal of the task.
72
+ description (Optional[str]): The new description of the task.
73
+
74
+ Returns:
75
+ Self: The updated instance of the Task class.
76
+ """
77
+ if goal:
78
+ self.goal = goal
79
+ if description:
80
+ self.description = description
81
+ return self
82
+
83
+ async def get_output(self) -> T:
84
+ """Get the output of the task.
85
+
86
+ Returns:
87
+ T: The output of the task.
88
+ """
89
+ logger.debug(f"Getting output for task {self.name}")
90
+ return await self._output.get()
91
+
92
+ def status_label(self, status: TaskStatus) -> str:
93
+ """Return a formatted status label for the task.
94
+
95
+ Args:
96
+ status (TaskStatus): The status of the task.
97
+
98
+ Returns:
99
+ str: The formatted status label.
100
+ """
101
+ return f"{self.name}{configs.pymitter.delimiter}{status.value}"
102
+
103
+ @property
104
+ def pending_label(self) -> str:
105
+ """Return the pending status label for the task.
106
+
107
+ Returns:
108
+ str: The pending status label.
109
+ """
110
+ return self.status_label(TaskStatus.Pending)
111
+
112
+ @property
113
+ def running_label(self) -> str:
114
+ """Return the running status label for the task.
115
+
116
+ Returns:
117
+ str: The running status label.
118
+ """
119
+ return self.status_label(TaskStatus.Running)
120
+
121
+ @property
122
+ def finished_label(self) -> str:
123
+ """Return the finished status label for the task.
124
+
125
+ Returns:
126
+ str: The finished status label.
127
+ """
128
+ return self.status_label(TaskStatus.Finished)
129
+
130
+ @property
131
+ def failed_label(self) -> str:
132
+ """Return the failed status label for the task.
133
+
134
+ Returns:
135
+ str: The failed status label.
136
+ """
137
+ return self.status_label(TaskStatus.Failed)
138
+
139
+ @property
140
+ def cancelled_label(self) -> str:
141
+ """Return the cancelled status label for the task.
142
+
143
+ Returns:
144
+ str: The cancelled status label.
145
+ """
146
+ return self.status_label(TaskStatus.Cancelled)
147
+
148
+ async def finish(self, output: T) -> Self:
149
+ """Mark the task as finished and set the output.
150
+
151
+ Args:
152
+ output (T): The output of the task.
153
+
154
+ Returns:
155
+ Self: The finished instance of the Task class.
156
+ """
157
+ logger.info(f"Finishing task {self.name}")
158
+ self._status = TaskStatus.Finished
159
+ await self._output.put(output)
160
+ logger.debug(f"Output set for task {self.name}")
161
+ await env.emit_async(self.finished_label, self)
162
+ logger.debug(f"Emitted finished event for task {self.name}")
163
+ return self
164
+
165
+ async def start(self) -> Self:
166
+ """Mark the task as running.
167
+
168
+ Returns:
169
+ Self: The running instance of the Task class.
170
+ """
171
+ logger.info(f"Starting task {self.name}")
172
+ self._status = TaskStatus.Running
173
+ await env.emit_async(self.running_label, self)
174
+ return self
175
+
176
+ async def cancel(self) -> Self:
177
+ """Mark the task as cancelled.
178
+
179
+ Returns:
180
+ Self: The cancelled instance of the Task class.
181
+ """
182
+ self._status = TaskStatus.Cancelled
183
+ await env.emit_async(self.cancelled_label, self)
184
+ return self
185
+
186
+ async def fail(self) -> Self:
187
+ """Mark the task as failed.
188
+
189
+ Returns:
190
+ Self: The failed instance of the Task class.
191
+ """
192
+ logger.error(f"Task {self.name} failed")
193
+ self._status = TaskStatus.Failed
194
+ await env.emit_async(self.failed_label, self)
195
+ return self
196
+
197
+ async def publish(self) -> Self:
198
+ """Publish the task to the environment.
199
+
200
+ Returns:
201
+ Self: The published instance of the Task class.
202
+ """
203
+ logger.info(f"Publishing task {self.name}")
204
+ await env.emit_async(self.pending_label, self)
205
+ return self
206
+
207
+ async def delegate(self) -> T:
208
+ """Delegate the task to the environment.
209
+
210
+ Returns:
211
+ T: The output of the task.
212
+ """
213
+ logger.info(f"Delegating task {self.name}")
214
+ await env.emit_async(self.pending_label, self)
215
+ return await self.get_output()
216
+
217
+ @property
218
+ def briefing(self) -> str:
219
+ """Return a briefing of the task including its goal.
220
+
221
+ Returns:
222
+ str: The briefing of the task.
223
+ """
224
+ return f"{super().briefing}\n{self.goal}"
fabricatio/models/tool.py CHANGED
@@ -1,80 +1,101 @@
1
- from inspect import signature, getfullargspec
2
- from typing import Callable, List
3
-
4
- from pydantic import Field
5
-
6
- from fabricatio.models.generic import WithBriefing
7
-
8
-
9
- class Tool[**P, R](WithBriefing):
10
- """A class representing a tool with a callable source function."""
11
- source: Callable[P, R]
12
-
13
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
14
- """Invoke the tool's source function with the provided arguments."""
15
- return self.source(*args, **kwargs)
16
-
17
- def briefing(self) -> str:
18
- """Return a brief description of the tool.
19
-
20
- Returns:
21
- str: A brief description of the tool.
22
- """
23
- source_signature = str(signature(self.source))
24
- # 获取源函数的返回类型
25
- return_annotation = getfullargspec(self.source).annotations.get('return', 'None')
26
- return f"{self.name}{source_signature} -> {return_annotation}\n{self.description}"
27
-
28
-
29
- class ToolBox(WithBriefing):
30
- """A class representing a collection of tools."""
31
- tools: List[Tool] = Field(default_factory=list)
32
- """A list of tools in the toolbox."""
33
-
34
- def collect_tool[**P, R](self, func: Callable[P, R]) -> Callable[P, R]:
35
- """Add a callable function to the toolbox as a tool.
36
-
37
- Args:
38
- func (Callable[P, R]): The function to be added as a tool.
39
-
40
- Returns:
41
- Callable[P, R]: The added function.
42
-
43
- Raises:
44
- AssertionError: If the provided function is not callable or lacks a name.
45
- """
46
- assert callable(func), "The tool must be a callable function."
47
- assert func.__name__, "The tool must have a name."
48
-
49
- tool = Tool(source=func, name=func.__name__, description=func.__doc__ or "")
50
- self.tools.append(tool)
51
- return func
52
-
53
- def briefing(self) -> str:
54
- """Return a brief description of the toolbox.
55
-
56
- Returns:
57
- str: A brief description of the toolbox.
58
- """
59
- list_out = "\n\n".join([f'- {tool.briefing}' for tool in self.tools])
60
- toc = (f"## {self.name}: {self.description}\n"
61
- f"## {len(self.tools)} tools available:\n")
62
- return f"{toc}\n\n{list_out}"
63
-
64
- def invoke_tool[**P, R](self, name: str, *args: P.args, **kwargs: P.kwargs) -> R:
65
- """Invoke a tool by name with the provided arguments.
66
-
67
- Args:
68
- name (str): The name of the tool to invoke.
69
- *args (P.args): Positional arguments to pass to the tool.
70
- **kwargs (P.kwargs): Keyword arguments to pass to the tool.
71
-
72
- Returns:
73
- R: The result of the tool's execution.
74
-
75
- Raises:
76
- AssertionError: If no tool with the specified name is found.
77
- """
78
- tool = next((tool for tool in self.tools if tool.name == name), None)
79
- assert tool, f"No tool named {name} found."
80
- return tool(*args, **kwargs)
1
+ from inspect import getfullargspec, signature
2
+ from typing import Any, Callable, List, Self
3
+
4
+ from pydantic import Field
5
+
6
+ from fabricatio.models.generic import WithBriefing
7
+
8
+
9
+ class Tool[**P, R](WithBriefing):
10
+ """A class representing a tool with a callable source function."""
11
+
12
+ name: str = Field(default="")
13
+ """The name of the tool."""
14
+
15
+ description: str = Field(default="")
16
+ """The description of the tool."""
17
+
18
+ source: Callable[P, R]
19
+ """The source function of the tool."""
20
+
21
+ def model_post_init(self, __context: Any) -> None:
22
+ """Initialize the tool with a name and a source function."""
23
+ self.name = self.name or self.source.__name__
24
+ assert self.name, "The tool must have a name."
25
+ self.description = self.description or self.source.__doc__ or ""
26
+
27
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
28
+ """Invoke the tool's source function with the provided arguments."""
29
+ return self.source(*args, **kwargs)
30
+
31
+ @property
32
+ def briefing(self) -> str:
33
+ """Return a brief description of the tool.
34
+
35
+ Returns:
36
+ str: A brief description of the tool.
37
+ """
38
+ source_signature = str(signature(self.source))
39
+ # 获取源函数的返回类型
40
+ return_annotation = getfullargspec(self.source).annotations.get("return", "None")
41
+ return f"{self.name}{source_signature} -> {return_annotation}\n{self.description}"
42
+
43
+
44
+ class ToolBox(WithBriefing):
45
+ """A class representing a collection of tools."""
46
+
47
+ tools: List[Tool] = Field(default_factory=list)
48
+ """A list of tools in the toolbox."""
49
+
50
+ def collect_tool[**P, R](self, func: Callable[P, R]) -> Callable[P, R]:
51
+ """Add a callable function to the toolbox as a tool.
52
+
53
+ Args:
54
+ func (Callable[P, R]): The function to be added as a tool.
55
+
56
+ Returns:
57
+ Callable[P, R]: The added function.
58
+ """
59
+ self.tools.append(Tool(source=func))
60
+ return func
61
+
62
+ def add_tool[**P, R](self, func: Callable[P, R]) -> Self:
63
+ """Add a callable function to the toolbox as a tool.
64
+
65
+ Args:
66
+ func (Callable): The function to be added as a tool.
67
+
68
+ Returns:
69
+ Self: The current instance of the toolbox.
70
+ """
71
+ self.tools.append(Tool(source=func))
72
+ return self
73
+
74
+ @property
75
+ def briefing(self) -> str:
76
+ """Return a brief description of the toolbox.
77
+
78
+ Returns:
79
+ str: A brief description of the toolbox.
80
+ """
81
+ list_out = "\n\n".join([f"- {tool.briefing}" for tool in self.tools])
82
+ toc = f"## {self.name}: {self.description}\n## {len(self.tools)} tools available:"
83
+ return f"{toc}\n\n{list_out}"
84
+
85
+ def invoke_tool[**P, R](self, name: str, *args: P.args, **kwargs: P.kwargs) -> R:
86
+ """Invoke a tool by name with the provided arguments.
87
+
88
+ Args:
89
+ name (str): The name of the tool to invoke.
90
+ *args (P.args): Positional arguments to pass to the tool.
91
+ **kwargs (P.kwargs): Keyword arguments to pass to the tool.
92
+
93
+ Returns:
94
+ R: The result of the tool's execution.
95
+
96
+ Raises:
97
+ AssertionError: If no tool with the specified name is found.
98
+ """
99
+ tool = next((tool for tool in self.tools if tool.name == name), None)
100
+ assert tool, f"No tool named {name} found."
101
+ return tool(*args, **kwargs)
@@ -1,9 +1,11 @@
1
- from typing import Literal, Self, List, Dict
1
+ from typing import Dict, List, Literal, Self
2
2
 
3
- from pydantic import BaseModel, Field, ConfigDict
3
+ from pydantic import BaseModel, ConfigDict, Field
4
4
 
5
5
 
6
6
  class Message(BaseModel):
7
+ """A class representing a message."""
8
+
7
9
  model_config = ConfigDict(use_attribute_docstrings=True)
8
10
  role: Literal["user", "system", "assistant"] = Field(default="user")
9
11
  """
@@ -16,13 +18,10 @@ class Message(BaseModel):
16
18
 
17
19
 
18
20
  class Messages(list):
19
- """
20
- A list of messages.
21
- """
21
+ """A list of messages."""
22
22
 
23
23
  def add_message(self, role: Literal["user", "system", "assistant"], content: str) -> Self:
24
- """
25
- Adds a message to the list with the specified role and content.
24
+ """Adds a message to the list with the specified role and content.
26
25
 
27
26
  Args:
28
27
  role (Literal["user", "system", "assistant"]): The role of the message sender.
@@ -36,8 +35,7 @@ class Messages(list):
36
35
  return self
37
36
 
38
37
  def add_user_message(self, content: str) -> Self:
39
- """
40
- Adds a user message to the list with the specified content.
38
+ """Adds a user message to the list with the specified content.
41
39
 
42
40
  Args:
43
41
  content (str): The content of the user message.
@@ -48,8 +46,7 @@ class Messages(list):
48
46
  return self.add_message("user", content)
49
47
 
50
48
  def add_system_message(self, content: str) -> Self:
51
- """
52
- Adds a system message to the list with the specified content.
49
+ """Adds a system message to the list with the specified content.
53
50
 
54
51
  Args:
55
52
  content (str): The content of the system message.
@@ -60,8 +57,7 @@ class Messages(list):
60
57
  return self.add_message("system", content)
61
58
 
62
59
  def add_assistant_message(self, content: str) -> Self:
63
- """
64
- Adds an assistant message to the list with the specified content.
60
+ """Adds an assistant message to the list with the specified content.
65
61
 
66
62
  Args:
67
63
  content (str): The content of the assistant message.
@@ -72,8 +68,7 @@ class Messages(list):
72
68
  return self.add_message("assistant", content)
73
69
 
74
70
  def as_list(self) -> List[Dict[str, str]]:
75
- """
76
- Converts the messages to a list of dictionaries.
71
+ """Converts the messages to a list of dictionaries.
77
72
 
78
73
  Returns:
79
74
  list[dict]: A list of dictionaries representing the messages.
fabricatio/parser.py ADDED
@@ -0,0 +1,66 @@
1
+ from typing import Any, Self, Tuple
2
+
3
+ import regex
4
+ from pydantic import Field, PositiveInt, PrivateAttr
5
+ from regex import Pattern, compile
6
+
7
+ from fabricatio.models.generic import Base
8
+
9
+
10
+ class Capture(Base):
11
+ """A class to capture patterns in text using regular expressions.
12
+
13
+ Attributes:
14
+ pattern (str): The regular expression pattern to search for.
15
+ _compiled (Pattern): The compiled regular expression pattern.
16
+ """
17
+
18
+ target_groups: Tuple[int, ...] = Field(default_factory=tuple)
19
+ """The target groups to capture from the pattern."""
20
+ pattern: str = Field(frozen=True)
21
+ """The regular expression pattern to search for."""
22
+ flags: PositiveInt = Field(default=regex.DOTALL | regex.MULTILINE | regex.IGNORECASE, frozen=True)
23
+ """The flags to use when compiling the regular expression pattern."""
24
+ _compiled: Pattern = PrivateAttr()
25
+
26
+ def model_post_init(self, __context: Any) -> None:
27
+ """Initialize the compiled regular expression pattern after the model is initialized.
28
+
29
+ Args:
30
+ __context (Any): The context in which the model is initialized.
31
+ """
32
+ self._compiled = compile(self.pattern, self.flags)
33
+
34
+ def capture(self, text: str) -> Tuple[str, ...] | None:
35
+ """Capture the first occurrence of the pattern in the given text.
36
+
37
+ Args:
38
+ text (str): The text to search the pattern in.
39
+
40
+ Returns:
41
+ str | None: The captured text if the pattern is found, otherwise None.
42
+
43
+ """
44
+ match = self._compiled.search(text)
45
+ if match is None:
46
+ return None
47
+
48
+ if self.target_groups:
49
+ return tuple(match.group(g) for g in self.target_groups)
50
+ return (match.group(),)
51
+
52
+ @classmethod
53
+ def capture_code_block(cls, language: str) -> Self:
54
+ """Capture the first occurrence of a code block in the given text.
55
+
56
+ Args:
57
+ language (str): The text containing the code block.
58
+
59
+ Returns:
60
+ Self: The instance of the class with the captured code block.
61
+ """
62
+ return cls(pattern=f"```{language}\n(.*?)\n```", target_groups=(1,))
63
+
64
+
65
+ JsonCapture = Capture.capture_code_block("json")
66
+ PythonCapture = Capture.capture_code_block("python")
@@ -0,0 +1,7 @@
1
+ """Contains the built-in toolboxes for the Fabricatio package."""
2
+
3
+ from fabricatio.toolboxes.task import TaskToolBox
4
+
5
+ __all__ = [
6
+ "TaskToolBox",
7
+ ]
@@ -0,0 +1,4 @@
1
+ from fabricatio.models.task import Task
2
+ from fabricatio.models.tool import ToolBox
3
+
4
+ TaskToolBox = ToolBox(name="TaskToolBox", description="A toolbox for tasks management.").add_tool(Task.simple_task)