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,82 @@
1
+ """The module containing the Event class."""
2
+
3
+ from typing import List, Self, Union
4
+
5
+ from fabricatio.config import configs
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ type EventLike = Union[str, List[str], "Event"]
9
+
10
+
11
+ class Event(BaseModel):
12
+ """A class representing an event."""
13
+
14
+ model_config = ConfigDict(use_attribute_docstrings=True)
15
+
16
+ segments: List[str] = Field(default_factory=list, frozen=True)
17
+ """ The segments of the namespaces."""
18
+
19
+ @classmethod
20
+ def instantiate_from(cls, event: EventLike) -> Self:
21
+ """Create an Event instance from a string or list of strings or an Event instance.
22
+
23
+ Args:
24
+ event (EventLike): The event to instantiate from.
25
+
26
+ Returns:
27
+ Event: The Event instance.
28
+ """
29
+ if isinstance(event, Event):
30
+ return event.clone()
31
+ if isinstance(event, str):
32
+ event = event.split(configs.pymitter.delimiter)
33
+
34
+ return cls(segments=event)
35
+
36
+ def derive(self, event: EventLike) -> Self:
37
+ """Derive a new event from this event and another event or a string."""
38
+ return self.clone().concat(event)
39
+
40
+ def collapse(self) -> str:
41
+ """Collapse the event into a string."""
42
+ return configs.pymitter.delimiter.join(self.segments)
43
+
44
+ def clone(self) -> Self:
45
+ """Clone the event."""
46
+ return Event(segments=list(self.segments))
47
+
48
+ def push(self, segment: str) -> Self:
49
+ """Push a segment to the event."""
50
+ if not segment:
51
+ raise ValueError("The segment must not be empty.")
52
+ if configs.pymitter.delimiter in segment:
53
+ raise ValueError("The segment must not contain the delimiter.")
54
+
55
+ self.segments.append(segment)
56
+ return self
57
+
58
+ def push_wildcard(self) -> Self:
59
+ """Push a wildcard segment to the event."""
60
+ return self.push("*")
61
+
62
+ def pop(self) -> str:
63
+ """Pop a segment from the event."""
64
+ return self.segments.pop()
65
+
66
+ def clear(self) -> Self:
67
+ """Clear the event."""
68
+ self.segments.clear()
69
+ return self
70
+
71
+ def concat(self, event: EventLike) -> Self:
72
+ """Concatenate another event to this event."""
73
+ self.segments.extend(Event.instantiate_from(event).segments)
74
+ return self
75
+
76
+ def __hash__(self) -> int:
77
+ """Return the hash of the event, using the collapsed string."""
78
+ return hash(self.collapse())
79
+
80
+ def __eq__(self, other: str | List[str] | Self) -> bool:
81
+ """Check if the event is equal to another event or a string."""
82
+ return self.collapse() == Event.instantiate_from(other).collapse()
@@ -0,0 +1,124 @@
1
+ """This module defines generic classes for models in the Fabricatio library."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Self
5
+
6
+ import orjson
7
+ from fabricatio._rust import blake3_hash
8
+ from fabricatio._rust_instances import template_manager
9
+ from fabricatio.config import configs
10
+ from fabricatio.fs.readers import magika, safe_text_read
11
+ from pydantic import (
12
+ BaseModel,
13
+ ConfigDict,
14
+ Field,
15
+ )
16
+
17
+
18
+ class Base(BaseModel):
19
+ """Base class for all models with Pydantic configuration."""
20
+
21
+ model_config = ConfigDict(use_attribute_docstrings=True)
22
+
23
+
24
+ class Named(Base):
25
+ """Class that includes a name attribute."""
26
+
27
+ name: str = Field(frozen=True)
28
+ """The name of the object."""
29
+
30
+
31
+ class Described(Base):
32
+ """Class that includes a description attribute."""
33
+
34
+ description: str = Field(default="", frozen=True)
35
+ """The description of the object."""
36
+
37
+
38
+ class WithBriefing(Named, Described):
39
+ """Class that provides a briefing based on the name and description."""
40
+
41
+ @property
42
+ def briefing(self) -> str:
43
+ """Get the briefing of the object.
44
+
45
+ Returns:
46
+ str: The briefing of the object.
47
+ """
48
+ return f"{self.name}: {self.description}" if self.description else self.name
49
+
50
+
51
+ class WithJsonExample(Base):
52
+ """Class that provides a JSON schema for the model."""
53
+
54
+ @classmethod
55
+ def json_example(cls) -> str:
56
+ """Return a JSON example for the model.
57
+
58
+ Returns:
59
+ str: A JSON example for the model.
60
+ """
61
+ return orjson.dumps(
62
+ {field_name: field_info.description for field_name, field_info in cls.model_fields.items()},
63
+ option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS,
64
+ ).decode()
65
+
66
+
67
+ class WithDependency(Base):
68
+ """Class that manages file dependencies."""
69
+
70
+ dependencies: List[str] = Field(default_factory=list)
71
+ """The file dependencies which is needed to read or write to meet a specific requirement, a list of file paths."""
72
+
73
+ def add_dependency[P: str | Path](self, dependency: P | List[P]) -> Self:
74
+ """Add a file dependency to the task.
75
+
76
+ Args:
77
+ dependency (str | Path | List[str | Path]): The file dependency to add to the task.
78
+
79
+ Returns:
80
+ Self: The current instance of the task.
81
+ """
82
+ if not isinstance(dependency, list):
83
+ dependency = [dependency]
84
+ self.dependencies.extend(Path(d).as_posix() for d in dependency)
85
+ return self
86
+
87
+ def remove_dependency[P: str | Path](self, dependency: P | List[P]) -> Self:
88
+ """Remove a file dependency from the task.
89
+
90
+ Args:
91
+ dependency (str | Path | List[str | Path]): The file dependency to remove from the task.
92
+
93
+ Returns:
94
+ Self: The current instance of the task.
95
+ """
96
+ if not isinstance(dependency, list):
97
+ dependency = [dependency]
98
+ for d in dependency:
99
+ self.dependencies.remove(Path(d).as_posix())
100
+ return self
101
+
102
+ @property
103
+ def dependencies_prompt(self) -> str:
104
+ """Generate a prompt for the task based on the file dependencies.
105
+
106
+ Returns:
107
+ str: The generated prompt for the task.
108
+ """
109
+ return template_manager.render_template(
110
+ configs.templates.dependencies_template,
111
+ {
112
+ (pth := Path(p)).name: {
113
+ "path": pth.as_posix(),
114
+ "exists": pth.exists(),
115
+ "description": (identity := magika.identify_path(pth)).output.description,
116
+ "size": f"{pth.stat().st_size / (1024 * 1024) if pth.exists() and pth.is_file() else 0:.3f} MB",
117
+ "content": (text := safe_text_read(pth)),
118
+ "lines": len(text.splitlines()),
119
+ "language": identity.output.ct_label,
120
+ "checksum": blake3_hash(pth.read_bytes()) if pth.exists() and pth.is_file() else "unknown",
121
+ }
122
+ for p in self.dependencies
123
+ },
124
+ )
@@ -0,0 +1,26 @@
1
+ """This module contains the types for the keyword arguments of the methods in the models module."""
2
+
3
+ from typing import List, NotRequired, TypedDict
4
+
5
+ from pydantic import NonNegativeFloat, NonNegativeInt, PositiveInt
6
+
7
+
8
+ class LLMKwargs(TypedDict):
9
+ """A type representing the keyword arguments for the LLM (Large Language Model) usage."""
10
+
11
+ model: NotRequired[str]
12
+ temperature: NotRequired[NonNegativeFloat]
13
+ stop: NotRequired[str | List[str]]
14
+ top_p: NotRequired[NonNegativeFloat]
15
+ max_tokens: NotRequired[PositiveInt]
16
+ stream: NotRequired[bool]
17
+ timeout: NotRequired[PositiveInt]
18
+ max_retries: NotRequired[PositiveInt]
19
+
20
+
21
+ class ChooseKwargs(LLMKwargs):
22
+ """A type representing the keyword arguments for the choose method."""
23
+
24
+ max_validations: NotRequired[PositiveInt]
25
+ system_message: NotRequired[str]
26
+ k: NotRequired[NonNegativeInt]
@@ -0,0 +1,48 @@
1
+ """Module that contains the Role class."""
2
+
3
+ from typing import Any, Self, Set
4
+
5
+ from fabricatio.core import env
6
+ from fabricatio.journal import logger
7
+ from fabricatio.models.action import WorkFlow
8
+ from fabricatio.models.advanced import ProposeTask
9
+ from fabricatio.models.events import Event
10
+ from fabricatio.models.tool import ToolBox
11
+ from fabricatio.models.usages import ToolBoxUsage
12
+ from pydantic import Field
13
+
14
+
15
+ class Role(ProposeTask, ToolBoxUsage):
16
+ """Class that represents a role with a registry of events and workflows."""
17
+
18
+ registry: dict[Event | str, WorkFlow] = Field(...)
19
+ """ The registry of events and workflows."""
20
+
21
+ toolboxes: Set[ToolBox] = Field(default_factory=set)
22
+
23
+ def model_post_init(self, __context: Any) -> None:
24
+ """Register the workflows in the role to the event bus."""
25
+ self.resolve_configuration().register_workflows()
26
+
27
+ def register_workflows(self) -> Self:
28
+ """Register the workflows in the role to the event bus."""
29
+ for event, workflow in self.registry.items():
30
+ logger.debug(
31
+ f"Registering workflow: `{workflow.name}` for event: `{Event.instantiate_from(event).collapse()}`"
32
+ )
33
+ env.on(event, workflow.serve)
34
+ return self
35
+
36
+ def resolve_configuration(self) -> Self:
37
+ """Resolve the configuration of the role."""
38
+ for workflow in self.registry.values():
39
+ logger.debug(f"Resolving config for workflow: `{workflow.name}`")
40
+ (
41
+ workflow.fallback_to(self)
42
+ .steps_fallback_to_self()
43
+ .inject_personality(self.briefing)
44
+ .supply_tools_from(self)
45
+ .steps_supply_tools_from_self()
46
+ )
47
+
48
+ return self
@@ -0,0 +1,276 @@
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 Any, List, Optional, Self
9
+
10
+ from fabricatio._rust_instances import template_manager
11
+ from fabricatio.config import configs
12
+ from fabricatio.core import env
13
+ from fabricatio.journal import logger
14
+ from fabricatio.models.events import Event, EventLike
15
+ from fabricatio.models.generic import WithBriefing, WithDependency, WithJsonExample
16
+ from pydantic import Field, PrivateAttr
17
+
18
+
19
+ class TaskStatus(Enum):
20
+ """An enumeration representing the status of a task.
21
+
22
+ Attributes:
23
+ Pending: The task is pending.
24
+ Running: The task is currently running.
25
+ Finished: The task has been successfully completed.
26
+ Failed: The task has failed.
27
+ Cancelled: The task has been cancelled.
28
+ """
29
+
30
+ Pending = "pending"
31
+ Running = "running"
32
+ Finished = "finished"
33
+ Failed = "failed"
34
+ Cancelled = "cancelled"
35
+
36
+
37
+ class Task[T](WithBriefing, WithJsonExample, WithDependency):
38
+ """A class representing a task with a status and output.
39
+
40
+ Attributes:
41
+ name (str): The name of the task.
42
+ description (str): The description of the task.
43
+ goal (str): The goal of the task.
44
+ dependencies (List[str]): The file dependencies of the task, a list of file paths.
45
+ namespace (List[str]): The namespace of the task, a list of namespace segment, as string.
46
+ """
47
+
48
+ name: str = Field(...)
49
+ """The name of the task."""
50
+
51
+ description: str = Field(default="")
52
+ """The description of the task."""
53
+
54
+ goal: List[str] = Field(default=[])
55
+ """The goal of the task, a list of strings."""
56
+
57
+ namespace: List[str] = Field(default_factory=list)
58
+ """The namespace of the task, a list of namespace segment, as string."""
59
+
60
+ _output: Queue = PrivateAttr(default_factory=lambda: Queue(maxsize=1))
61
+ """The output queue of the task."""
62
+
63
+ _status: TaskStatus = PrivateAttr(default=TaskStatus.Pending)
64
+ """The status of the task."""
65
+
66
+ _namespace: Event = PrivateAttr(default_factory=Event)
67
+ """The namespace of the task as an event, which is generated from the namespace list."""
68
+
69
+ def model_post_init(self, __context: Any) -> None:
70
+ """Initialize the task with a namespace event."""
71
+ self._namespace.segments.extend(self.namespace)
72
+
73
+ def move_to(self, new_namespace: EventLike) -> Self:
74
+ """Move the task to a new namespace.
75
+
76
+ Args:
77
+ new_namespace (EventLike): The new namespace to move the task to.
78
+
79
+ Returns:
80
+ Task: The moved instance of the `Task` class.
81
+ """
82
+ logger.debug(f"Moving task `{self.name}` to `{new_namespace}`")
83
+ self._namespace.clear().concat(new_namespace)
84
+ self.namespace = self._namespace.segments
85
+ return self
86
+
87
+ def nested_move_to(self, new_parent_namespace: EventLike) -> Self:
88
+ """Move the task to a new namespace by nesting it under the new parent namespace.
89
+
90
+ Args:
91
+ new_parent_namespace (EventLike): The new parent namespace to move the task to.
92
+
93
+ Returns:
94
+ Task: The nested moved instance of the `Task` class.
95
+ """
96
+ logger.debug(f"Nested moving task `{self.name}` to `{new_parent_namespace}`")
97
+ self._namespace.clear().concat(new_parent_namespace).concat(self.namespace)
98
+ self.namespace = self._namespace.segments
99
+ return self
100
+
101
+ @classmethod
102
+ def simple_task(cls, name: str, goal: str, description: str) -> Self:
103
+ """Create a simple task with a name, goal, and description.
104
+
105
+ Args:
106
+ name (str): The name of the task.
107
+ goal (str): The goal of the task.
108
+ description (str): The description of the task.
109
+
110
+ Returns:
111
+ Task: A new instance of the `Task` class.
112
+ """
113
+ return cls(name=name, goal=goal, description=description)
114
+
115
+ def update_task(self, goal: Optional[str] = None, description: Optional[str] = None) -> Self:
116
+ """Update the goal and description of the task.
117
+
118
+ Args:
119
+ goal (str, optional): The new goal of the task.
120
+ description (str, optional): The new description of the task.
121
+
122
+ Returns:
123
+ Task: The updated instance of the `Task` class.
124
+ """
125
+ if goal:
126
+ self.goal = goal
127
+ if description:
128
+ self.description = description
129
+ return self
130
+
131
+ async def get_output(self) -> T:
132
+ """Get the output of the task.
133
+
134
+ Returns:
135
+ T: The output of the task.
136
+ """
137
+ logger.debug(f"Getting output for task {self.name}")
138
+ return await self._output.get()
139
+
140
+ def status_label(self, status: TaskStatus) -> str:
141
+ """Return a formatted status label for the task.
142
+
143
+ Args:
144
+ status (TaskStatus): The status of the task.
145
+
146
+ Returns:
147
+ str: The formatted status label.
148
+ """
149
+ return self._namespace.derive(self.name).push(status.value).collapse()
150
+
151
+ @property
152
+ def pending_label(self) -> str:
153
+ """Return the pending status label for the task.
154
+
155
+ Returns:
156
+ str: The pending status label.
157
+ """
158
+ return self.status_label(TaskStatus.Pending)
159
+
160
+ @property
161
+ def running_label(self) -> str:
162
+ """Return the running status label for the task.
163
+
164
+ Returns:
165
+ str: The running status label.
166
+ """
167
+ return self.status_label(TaskStatus.Running)
168
+
169
+ @property
170
+ def finished_label(self) -> str:
171
+ """Return the finished status label for the task.
172
+
173
+ Returns:
174
+ str: The finished status label.
175
+ """
176
+ return self.status_label(TaskStatus.Finished)
177
+
178
+ @property
179
+ def failed_label(self) -> str:
180
+ """Return the failed status label for the task.
181
+
182
+ Returns:
183
+ str: The failed status label.
184
+ """
185
+ return self.status_label(TaskStatus.Failed)
186
+
187
+ @property
188
+ def cancelled_label(self) -> str:
189
+ """Return the cancelled status label for the task.
190
+
191
+ Returns:
192
+ str: The cancelled status label.
193
+ """
194
+ return self.status_label(TaskStatus.Cancelled)
195
+
196
+ async def finish(self, output: T) -> Self:
197
+ """Mark the task as finished and set the output.
198
+
199
+ Args:
200
+ output (T): The output of the task.
201
+
202
+ Returns:
203
+ Task: The finished instance of the `Task` class.
204
+ """
205
+ logger.info(f"Finishing task {self.name}")
206
+ self._status = TaskStatus.Finished
207
+ await self._output.put(output)
208
+ logger.debug(f"Output set for task {self.name}")
209
+ await env.emit_async(self.finished_label, self)
210
+ logger.debug(f"Emitted finished event for task {self.name}")
211
+ return self
212
+
213
+ async def start(self) -> Self:
214
+ """Mark the task as running.
215
+
216
+ Returns:
217
+ Task: The running instance of the `Task` class.
218
+ """
219
+ logger.info(f"Starting task `{self.name}`")
220
+ self._status = TaskStatus.Running
221
+ await env.emit_async(self.running_label, self)
222
+ return self
223
+
224
+ async def cancel(self) -> Self:
225
+ """Mark the task as cancelled.
226
+
227
+ Returns:
228
+ Task: The cancelled instance of the `Task` class.
229
+ """
230
+ logger.info(f"Cancelling task `{self.name}`")
231
+ self._status = TaskStatus.Cancelled
232
+ await env.emit_async(self.cancelled_label, self)
233
+ return self
234
+
235
+ async def fail(self) -> Self:
236
+ """Mark the task as failed.
237
+
238
+ Returns:
239
+ Task: The failed instance of the `Task` class.
240
+ """
241
+ logger.info(f"Failing task `{self.name}`")
242
+ self._status = TaskStatus.Failed
243
+ await env.emit_async(self.failed_label, self)
244
+ return self
245
+
246
+ async def publish(self) -> Self:
247
+ """Publish the task to the event bus.
248
+
249
+ Returns:
250
+ Task: The published instance of the `Task` class
251
+ """
252
+ logger.info(f"Publishing task `{(label := self.pending_label)}`")
253
+ await env.emit_async(label, self)
254
+ return self
255
+
256
+ async def delegate(self) -> T:
257
+ """Delegate the task to the event bus and wait for the output.
258
+
259
+ Returns:
260
+ T: The output of the task
261
+ """
262
+ logger.info(f"Delegating task `{(label := self.pending_label)}`")
263
+ await env.emit_async(label, self)
264
+ return await self.get_output()
265
+
266
+ @property
267
+ def briefing(self) -> str:
268
+ """Return a briefing of the task including its goal.
269
+
270
+ Returns:
271
+ str: The briefing of the task.
272
+ """
273
+ return template_manager.render_template(
274
+ configs.templates.task_briefing_template,
275
+ self.model_dump(),
276
+ )