fabricatio 0.2.9.dev1__cp312-cp312-win_amd64.whl → 0.2.9.dev2__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.
@@ -1,7 +1,7 @@
1
1
  """Dump the finalized output to a file."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Iterable, List, Optional, Type
4
+ from typing import Any, Iterable, List, Optional, Type
5
5
 
6
6
  from fabricatio.journal import logger
7
7
  from fabricatio.models.action import Action
@@ -33,6 +33,7 @@ class DumpFinalizedOutput(Action):
33
33
  "Could not find the path of file to dump the data.",
34
34
  )
35
35
  )
36
+ logger.info(f"Saving output to {dump_path.as_posix()}")
36
37
  ok(to_dump, "Could not dump the data since the path is not specified.").finalized_dump_to(dump_path)
37
38
  return dump_path.as_posix()
38
39
 
@@ -41,7 +42,11 @@ class PersistentAll(Action):
41
42
  """Persist all the data to a file."""
42
43
 
43
44
  output_key: str = "persistent_count"
45
+ """The number of objects persisted."""
44
46
  persist_dir: Optional[str] = None
47
+ """The directory to persist the data."""
48
+ override: bool = False
49
+ """Whether to remove the existing dir before dumping."""
45
50
 
46
51
  async def _execute(
47
52
  self,
@@ -64,16 +69,22 @@ class PersistentAll(Action):
64
69
  if persist_dir.is_file():
65
70
  logger.warning("Dump should be a directory, but it is a file. Skip dumping.")
66
71
  return count
67
-
72
+ if self.override and persist_dir.is_dir():
73
+ logger.info(f"Override the existing directory {persist_dir.as_posix()}.")
74
+ persist_dir.rmdir()
75
+ logger.info(f"Starting persistence in directory {persist_dir}")
68
76
  for k, v in cxt.items():
69
77
  final_dir = persist_dir.joinpath(k)
78
+ logger.debug(f"Checking key {k} for persistence")
70
79
  if isinstance(v, PersistentAble):
80
+ logger.info(f"Persisting object {k} to {final_dir}")
71
81
  final_dir.mkdir(parents=True, exist_ok=True)
72
82
  v.persist(final_dir)
73
83
  count += 1
74
84
  if isinstance(v, Iterable) and any(
75
85
  persistent_ables := (pers for pers in v if isinstance(pers, PersistentAble))
76
86
  ):
87
+ logger.info(f"Persisting collection {k} to {final_dir}")
77
88
  final_dir.mkdir(parents=True, exist_ok=True)
78
89
  for per in persistent_ables:
79
90
  per.persist(final_dir)
@@ -93,10 +104,39 @@ class RetrieveFromPersistent[T: PersistentAble](Action):
93
104
  """The class of the object to retrieve."""
94
105
 
95
106
  async def _execute(self, /, **__) -> Optional[T | List[T]]:
96
- logger.info(f"Retrieve `{self.retrieve_cls.__name__}` from persistent file: {self.load_path}")
107
+ logger.info(f"Retrieve `{self.retrieve_cls.__name__}` from {self.load_path}")
97
108
  if not (p := Path(self.load_path)).exists():
109
+ logger.warning(f"Path {self.load_path} does not exist")
98
110
  return None
99
111
 
100
112
  if p.is_dir():
113
+ logger.info(f"Found directory with {len(list(p.glob('*')))} items")
101
114
  return [self.retrieve_cls.from_persistent(per) for per in p.glob("*")]
102
115
  return self.retrieve_cls.from_persistent(self.load_path)
116
+
117
+
118
+ class GatherAsList(Action):
119
+ """Gather the objects from the context as a list.
120
+
121
+ Notes:
122
+ If both `gather_suffix` and `gather_prefix` are specified, only the objects with the suffix will be gathered.
123
+ """
124
+ output_key: str = "gathered"
125
+ """Gather the objects from the context as a list."""
126
+ gather_suffix: Optional[str] = None
127
+ """Gather the objects from the context as a list."""
128
+ gather_prefix: Optional[str] = None
129
+ """Gather the objects from the context as a list."""
130
+
131
+ async def _execute(self, **cxt) -> List[Any]:
132
+
133
+ if self.gather_suffix is not None:
134
+ result = [cxt[k] for k in cxt if k.endswith(self.gather_suffix)]
135
+ logger.debug(f"Gathered {len(result)} items with suffix {self.gather_suffix}")
136
+ return result
137
+ if self.gather_prefix is None:
138
+ logger.error(err:="Either `gather_suffix` or `gather_prefix` must be specified.")
139
+ raise ValueError(err)
140
+ result = [cxt[k] for k in cxt if k.startswith(self.gather_prefix)]
141
+ logger.debug(f"Gathered {len(result)} items with prefix {self.gather_prefix}")
142
+ return result
@@ -20,8 +20,6 @@ class Censor(Correct, Check):
20
20
  Inherits from both Correct and Check classes.
21
21
  Provides methods to censor objects and strings by first checking them against a ruleset and then correcting them if necessary.
22
22
 
23
- Attributes:
24
- ruleset (RuleSet): The ruleset to be used for censoring.
25
23
  """
26
24
 
27
25
  async def censor_obj[M: SketchedAble](
@@ -42,7 +40,7 @@ class Censor(Correct, Check):
42
40
  """
43
41
  imp = await self.check_obj(obj, ruleset, **override_kwargs(kwargs, default=None))
44
42
  if imp is None:
45
- return imp
43
+ return None
46
44
  return await self.correct_obj(obj, imp, **kwargs)
47
45
 
48
46
  async def censor_string(
@@ -11,6 +11,7 @@ from fabricatio.models.extra.problem import Improvement
11
11
  from fabricatio.models.extra.rule import Rule, RuleSet
12
12
  from fabricatio.models.generic import Display, WithBriefing
13
13
  from fabricatio.models.kwargs_types import ValidateKwargs
14
+ from fabricatio.rust import detect_language
14
15
  from fabricatio.utils import override_kwargs
15
16
 
16
17
 
@@ -56,9 +57,7 @@ class Check(AdvancedJudge, Propose):
56
57
 
57
58
  ruleset_patch = await self.propose(
58
59
  RuleSetBriefingPatch,
59
- f"# Rules Requirements\n{rule_reqs}\n# Generated Rules\n{Display.seq_display(rules)}\n\n"
60
- f"You need to write a concise and detailed patch for this ruleset that can be applied to the ruleset nicely.\n"
61
- f"Note that all fields in this patch will be directly copied to the ruleset obj, including `name` and `description`, so write when knowing the subject.\n",
60
+ f"{ruleset_requirement}\n\nYou should use `{detect_language(ruleset_requirement)}`!",
62
61
  **override_kwargs(kwargs, default=None),
63
62
  )
64
63
 
@@ -98,7 +97,7 @@ class Check(AdvancedJudge, Propose):
98
97
  Improvement,
99
98
  TEMPLATE_MANAGER.render_template(
100
99
  configs.templates.check_string_template,
101
- {"to_check": input_text, "rule": rule, "judge": judge.display(), "reference": reference},
100
+ {"to_check": input_text, "rule": rule.display(), "judge": judge.display(), "reference": reference},
102
101
  ),
103
102
  **kwargs,
104
103
  )
@@ -78,11 +78,11 @@ class Correct(Rating, Propose):
78
78
  TEMPLATE_MANAGER.render_template(
79
79
  configs.templates.fix_troubled_obj_template,
80
80
  {
81
- "problem": problem_solutions.problem,
81
+ "problem": problem_solutions.problem.display(),
82
82
  "solution": ok(
83
83
  problem_solutions.final_solution(),
84
84
  f"No solution found for problem: {problem_solutions.problem}",
85
- ),
85
+ ).display(),
86
86
  "reference": reference,
87
87
  },
88
88
  ),
@@ -111,11 +111,11 @@ class Correct(Rating, Propose):
111
111
  TEMPLATE_MANAGER.render_template(
112
112
  configs.templates.fix_troubled_string_template,
113
113
  {
114
- "problem": problem_solutions.problem,
114
+ "problem": problem_solutions.problem.display(),
115
115
  "solution": ok(
116
116
  problem_solutions.final_solution(),
117
117
  f"No solution found for problem: {problem_solutions.problem}",
118
- ),
118
+ ).display(),
119
119
  "reference": reference,
120
120
  "string_to_fix": input_text,
121
121
  },
fabricatio/config.py CHANGED
@@ -303,7 +303,7 @@ class CacheConfig(BaseModel):
303
303
 
304
304
  model_config = ConfigDict(use_attribute_docstrings=True)
305
305
 
306
- type: Optional[LiteLLMCacheType] = None
306
+ type: LiteLLMCacheType = LiteLLMCacheType.LOCAL
307
307
  """The type of cache to use. If None, the default cache type will be used."""
308
308
  params: CacheKwargs = Field(default_factory=CacheKwargs)
309
309
  """The parameters for the cache. If type is None, the default parameters will be used."""
@@ -1,7 +1,12 @@
1
- """Module that contains the classes for actions and workflows.
1
+ """Module that contains the classes for defining and executing task workflows.
2
2
 
3
- This module defines the Action and WorkFlow classes, which are used for
4
- creating and executing sequences of actions in a task-based context.
3
+ This module provides the Action and WorkFlow classes for creating structured
4
+ task execution pipelines. Actions represent atomic operations, while WorkFlows
5
+ orchestrate sequences of actions with shared context and error handling.
6
+
7
+ Classes:
8
+ Action: Base class for defining executable actions with context management.
9
+ WorkFlow: Manages action sequences, context propagation, and task lifecycle.
5
10
  """
6
11
 
7
12
  import traceback
@@ -50,28 +55,26 @@ class Action(WithBriefing, LLMUsage):
50
55
  self.description = self.description or self.__class__.__doc__ or ""
51
56
 
52
57
  @abstractmethod
53
- async def _execute(self, *_, **cxt) -> Any: # noqa: ANN002
54
- """Execute the action logic with the provided context arguments.
55
-
56
- This method must be implemented by subclasses to define the actual behavior.
58
+ async def _execute(self, *_:Any, **cxt) -> Any:
59
+ """Implement the core logic of the action.
57
60
 
58
61
  Args:
59
- **cxt: The context dictionary containing input and output data.
62
+ **cxt: Context dictionary containing input/output data.
60
63
 
61
64
  Returns:
62
- Any: The result of the action execution.
65
+ Result of the action execution to be stored in context.
63
66
  """
64
67
  pass
65
68
 
66
69
  @final
67
70
  async def act(self, cxt: Dict[str, Any]) -> Dict[str, Any]:
68
- """Perform the action and update the context with results.
71
+ """Execute action and update context.
69
72
 
70
73
  Args:
71
- cxt: The context dictionary containing input and output data.
74
+ cxt (Dict[str, Any]): Shared context dictionary.
72
75
 
73
76
  Returns:
74
- Dict[str, Any]: The updated context dictionary.
77
+ Updated context dictionary with new/modified entries.
75
78
  """
76
79
  ret = await self._execute(**cxt)
77
80
 
@@ -83,10 +86,10 @@ class Action(WithBriefing, LLMUsage):
83
86
 
84
87
  @property
85
88
  def briefing(self) -> str:
86
- """Return a formatted description of the action including personality context if available.
89
+ """Generate formatted action description with personality context.
87
90
 
88
91
  Returns:
89
- str: Formatted briefing text with personality and action description.
92
+ Briefing text combining personality and action description.
90
93
  """
91
94
  if self.personality:
92
95
  return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
@@ -98,10 +101,15 @@ class Action(WithBriefing, LLMUsage):
98
101
  return self
99
102
 
100
103
  class WorkFlow(WithBriefing, ToolBoxUsage):
101
- """Class that represents a sequence of actions to be executed for a task.
104
+ """Manages sequences of actions to fulfill tasks.
102
105
 
103
- A workflow manages the execution of multiple actions in sequence, passing
104
- a shared context between them and handling task lifecycle events.
106
+ Handles context propagation between actions, error handling, and task lifecycle
107
+ events like cancellation and completion.
108
+
109
+ Attributes:
110
+ steps (Tuple): Sequence of Action instances or classes to execute.
111
+ task_input_key (str): Key for storing task instance in context.
112
+ task_output_key (str): Key to retrieve final result from context.
105
113
  """
106
114
 
107
115
  description: str = ""
@@ -137,26 +145,29 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
137
145
  self._instances = tuple(step if isinstance(step, Action) else step() for step in self.steps)
138
146
 
139
147
  def inject_personality(self, personality: str) -> Self:
140
- """Set the personality for all actions that don't have one defined.
148
+ """Set personality for actions without existing personality.
141
149
 
142
150
  Args:
143
- personality: The personality text to inject.
151
+ personality (str): Shared personality context
144
152
 
145
153
  Returns:
146
- Self: The workflow instance for method chaining.
154
+ Workflow instance with updated actions
147
155
  """
148
156
  for action in filter(lambda a: not a.personality, self._instances):
149
157
  action.personality = personality
150
158
  return self
151
159
 
152
160
  async def serve(self, task: Task) -> None:
153
- """Execute the workflow to fulfill the given task.
154
-
155
- This method manages the complete lifecycle of processing a task through
156
- the workflow's sequence of actions.
161
+ """Execute workflow to complete given task.
157
162
 
158
163
  Args:
159
- task: The task to be processed.
164
+ task (Task): Task instance to be processed.
165
+
166
+ Steps:
167
+ 1. Initialize context with task instance and extra data
168
+ 2. Execute each action sequentially
169
+ 3. Handle task cancellation and exceptions
170
+ 4. Extract final result from context
160
171
  """
161
172
  logger.info(f"Start execute workflow: {self.name}")
162
173
 
@@ -206,10 +217,14 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
206
217
  await task.fail()
207
218
 
208
219
  async def _init_context[T](self, task: Task[T]) -> None:
209
- """Initialize the context dictionary for workflow execution.
220
+ """Initialize workflow execution context.
210
221
 
211
222
  Args:
212
- task: The task being served by this workflow.
223
+ task (Task[T]): Task being processed
224
+
225
+ Context includes:
226
+ - Task instance stored under task_input_key
227
+ - Any extra_init_context values
213
228
  """
214
229
  logger.debug(f"Initializing context for workflow: {self.name}")
215
230
  initial_context = {self.task_input_key: task, **dict(self.extra_init_context)}
@@ -230,7 +245,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
230
245
  Returns:
231
246
  Self: The workflow instance for method chaining.
232
247
  """
233
- self.provide_tools_to(self._instances)
248
+ self.provide_tools_to(i for i in self._instances if isinstance(i,ToolBoxUsage))
234
249
  return self
235
250
 
236
251
  def update_init_context(self, /, **kwargs) -> Self:
@@ -7,8 +7,6 @@ from typing import Generator, List, Optional, Self, Tuple, overload
7
7
 
8
8
  from fabricatio.models.generic import (
9
9
  AsPrompt,
10
- CensoredAble,
11
- Display,
12
10
  FinalizedDumpAble,
13
11
  Introspect,
14
12
  ModelHash,
@@ -16,6 +14,7 @@ from fabricatio.models.generic import (
16
14
  ProposedUpdateAble,
17
15
  ResolveUpdateConflict,
18
16
  SequencePatch,
17
+ SketchedAble,
19
18
  )
20
19
 
21
20
 
@@ -30,7 +29,7 @@ class ReferringType(StrEnum):
30
29
  type RefKey = Tuple[str, Optional[str], Optional[str]]
31
30
 
32
31
 
33
- class ArticleRef(CensoredAble, ProposedUpdateAble):
32
+ class ArticleRef(ProposedUpdateAble):
34
33
  """Reference to a specific chapter, section or subsection within the article. You SHALL not refer to an article component that is external and not present within our own article.
35
34
 
36
35
  Examples:
@@ -104,7 +103,7 @@ class ArticleRef(CensoredAble, ProposedUpdateAble):
104
103
  return ReferringType.CHAPTER
105
104
 
106
105
 
107
- class ArticleMetaData(CensoredAble, Display):
106
+ class ArticleMetaData(SketchedAble):
108
107
  """Metadata for an article component."""
109
108
 
110
109
  description: str
@@ -120,6 +119,9 @@ class ArticleMetaData(CensoredAble, Display):
120
119
  title: str
121
120
  """Do not add any prefix or suffix to the title. should not contain special characters."""
122
121
 
122
+ expected_word_count: int
123
+ """Expected word count of this research component."""
124
+
123
125
 
124
126
  class ArticleRefSequencePatch(SequencePatch[ArticleRef]):
125
127
  """Patch for article refs."""
@@ -14,11 +14,11 @@ from fabricatio.models.extra.article_base import (
14
14
  from fabricatio.models.extra.article_outline import (
15
15
  ArticleOutline,
16
16
  )
17
- from fabricatio.models.generic import CensoredAble, Display, PersistentAble, SequencePatch, WithRef
17
+ from fabricatio.models.generic import PersistentAble, SequencePatch, SketchedAble, WithRef
18
18
  from fabricatio.utils import ok
19
19
 
20
20
 
21
- class Paragraph(CensoredAble):
21
+ class Paragraph(SketchedAble):
22
22
  """Structured academic paragraph blueprint for controlled content generation."""
23
23
 
24
24
  description: str
@@ -73,8 +73,7 @@ class ArticleChapter(ChapterBase[ArticleSection]):
73
73
 
74
74
 
75
75
  class Article(
76
- Display,
77
- CensoredAble,
76
+ SketchedAble,
78
77
  WithRef[ArticleOutline],
79
78
  PersistentAble,
80
79
  ArticleBase[ArticleChapter],
@@ -9,7 +9,7 @@ from fabricatio.models.extra.article_base import (
9
9
  SubSectionBase,
10
10
  )
11
11
  from fabricatio.models.extra.article_proposal import ArticleProposal
12
- from fabricatio.models.generic import CensoredAble, Display, PersistentAble, WithRef
12
+ from fabricatio.models.generic import PersistentAble, SketchedAble, WithRef
13
13
 
14
14
 
15
15
  class ArticleSubsectionOutline(SubSectionBase):
@@ -25,8 +25,7 @@ class ArticleChapterOutline(ChapterBase[ArticleSectionOutline]):
25
25
 
26
26
 
27
27
  class ArticleOutline(
28
- Display,
29
- CensoredAble,
28
+ SketchedAble,
30
29
  WithRef[ArticleProposal],
31
30
  PersistentAble,
32
31
  ArticleBase[ArticleChapterOutline],
@@ -2,10 +2,10 @@
2
2
 
3
3
  from typing import Dict, List
4
4
 
5
- from fabricatio.models.generic import AsPrompt, CensoredAble, Display, PersistentAble, WithRef
5
+ from fabricatio.models.generic import AsPrompt, SketchedAble, PersistentAble, WithRef
6
6
 
7
7
 
8
- class ArticleProposal(CensoredAble, Display, WithRef[str], AsPrompt, PersistentAble):
8
+ class ArticleProposal(SketchedAble, WithRef[str], AsPrompt, PersistentAble):
9
9
  """Structured proposal for academic paper development with core research elements.
10
10
 
11
11
  Guides LLM in generating comprehensive research proposals with clearly defined components.
@@ -41,8 +41,8 @@ class ArticleProposal(CensoredAble, Display, WithRef[str], AsPrompt, PersistentA
41
41
  abstract: str
42
42
  """A concise summary of the research proposal, outlining the main points and objectives."""
43
43
 
44
- min_word_count: int
45
- """The minimum number of words required for the research proposal."""
44
+ expected_word_count: int
45
+ """The estimated word count of the final academic paper."""
46
46
 
47
47
  def _as_prompt_inner(self) -> Dict[str, str]:
48
48
  return {
@@ -33,45 +33,50 @@ class Base(BaseModel):
33
33
  """Base class for all models with Pydantic configuration.
34
34
 
35
35
  This class sets up the basic Pydantic configuration for all models in the Fabricatio library.
36
+ The `model_config` uses `use_attribute_docstrings=True` to ensure field descriptions are
37
+ pulled from the attribute's docstring instead of the default Pydantic behavior.
36
38
  """
37
-
38
39
  model_config = ConfigDict(use_attribute_docstrings=True)
39
40
 
40
41
 
41
42
  class Display(Base):
42
- """Class that provides a method to display the model in a formatted JSON string.
43
+ """Class that provides formatted JSON representation utilities.
43
44
 
44
- This class includes methods to display the model in both formatted and compact JSON strings.
45
+ Provides methods to generate both pretty-printed and compact JSON representations of the model.
46
+ Used for debugging and logging purposes.
45
47
  """
46
-
47
48
  def display(self) -> str:
48
- """Display the model in a formatted JSON string.
49
+ """Generate pretty-printed JSON representation.
49
50
 
50
51
  Returns:
51
- str: The formatted JSON string of the model.
52
+ str: JSON string with 1-level indentation for readability
52
53
  """
53
54
  return self.model_dump_json(indent=1)
54
55
 
55
56
  def compact(self) -> str:
56
- """Display the model in a compact JSON string.
57
+ """Generate compact JSON representation.
57
58
 
58
59
  Returns:
59
- str: The compact JSON string of the model.
60
+ str: Minified JSON string without whitespace
60
61
  """
61
62
  return self.model_dump_json()
62
63
 
63
64
  @staticmethod
64
65
  def seq_display(seq: Iterable["Display"], compact: bool = False) -> str:
65
- """Display a sequence of Display objects in a formatted JSON string.
66
+ """Generate formatted display for sequence of Display objects.
66
67
 
67
68
  Args:
68
- seq (Iterable[Display]): The sequence of Display objects to display.
69
- compact (bool): Whether to display the sequence in a compact format. Defaults to False.
69
+ seq (Iterable[Display]): Sequence of objects to display
70
+ compact (bool): Use compact format instead of pretty print
70
71
 
71
72
  Returns:
72
- str: The formatted JSON string of the sequence.
73
+ str: Combined display output with boundary markers
73
74
  """
74
- return "\n".join(d.compact() if compact else d.display() for d in seq)
75
+ return (
76
+ "--- Start of Extra Info Sequence ---"
77
+ + "\n".join(d.compact() if compact else d.display() for d in seq)
78
+ + "--- End of Extra Info Sequence ---"
79
+ )
75
80
 
76
81
 
77
82
  class Named(Base):
@@ -184,25 +189,29 @@ class WithRef[T](Base):
184
189
 
185
190
 
186
191
  class PersistentAble(Base):
187
- """Class that provides a method to persist the object.
192
+ """Class providing file persistence capabilities.
188
193
 
189
- This class includes methods to persist the object to a file or directory.
194
+ Enables saving model instances to disk with timestamped filenames and loading from persisted files.
195
+ Implements basic versioning through filename hashing and timestamping.
190
196
  """
191
-
192
197
  def persist(self, path: str | Path) -> Self:
193
- """Persist the object to a file or directory.
198
+ """Save model instance to disk with versioned filename.
194
199
 
195
200
  Args:
196
- path (str | Path): The path to save the object.
201
+ path (str | Path): Target directory or file path. If directory, filename is auto-generated.
197
202
 
198
203
  Returns:
199
- Self: The current instance of the object.
204
+ Self: Current instance for method chaining
205
+
206
+ Notes:
207
+ - Filename format: <ClassName>_<YYYYMMDD_HHMMSS>_<6-char_hash>.json
208
+ - Hash generated from JSON content ensures uniqueness
200
209
  """
201
210
  p = Path(path)
202
211
  out = self.model_dump_json()
203
212
 
204
213
  # Generate a timestamp in the format YYYYMMDD_HHMMSS
205
- timestamp = datetime.now().strftime("%Y%m%d")
214
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
206
215
 
207
216
  # Generate the hash
208
217
  file_hash = blake3_hash(out.encode())[:6]
@@ -219,15 +228,52 @@ class PersistentAble(Base):
219
228
  logger.info(f"Persisted `{self.__class__.__name__}` to {p.as_posix()}")
220
229
  return self
221
230
 
231
+ @classmethod
232
+ def from_latest_persistent(cls, dir_path: str | Path) -> Self:
233
+ """Load most recent persisted instance from directory.
234
+
235
+ Args:
236
+ dir_path (str | Path): Directory containing persisted files
237
+
238
+ Returns:
239
+ Self: Most recently modified instance
240
+
241
+ Raises:
242
+ NotADirectoryError: If path is not a valid directory
243
+ FileNotFoundError: If no matching files found
244
+ """
245
+ dir_path = Path(dir_path)
246
+ if not dir_path.is_dir():
247
+ raise NotADirectoryError(f"{dir_path} is not a valid directory")
248
+
249
+ pattern = f"{cls.__name__}_*.json"
250
+ files = list(dir_path.glob(pattern))
251
+
252
+ if not files:
253
+ raise FileNotFoundError(f"No persistent files found for {cls.__name__} in {dir_path}")
254
+
255
+ def _get_timestamp(file_path: Path) -> datetime:
256
+ stem = file_path.stem
257
+ parts = stem.split("_")
258
+ return datetime.strptime(f"{parts[1]}_{parts[2]}", "%Y%m%d_%H%M%S")
259
+
260
+ files.sort(key=lambda f: _get_timestamp(f), reverse=True)
261
+
262
+ return cls.from_persistent(files.pop(0))
263
+
222
264
  @classmethod
223
265
  def from_persistent(cls, path: str | Path) -> Self:
224
- """Load the object from a file.
266
+ """Load an instance from a specific persisted file.
225
267
 
226
268
  Args:
227
- path (str | Path): The path to load the object from.
269
+ path (str | Path): Path to the JSON file.
228
270
 
229
271
  Returns:
230
- Self: The current instance of the object.
272
+ Self: The loaded instance from the file.
273
+
274
+ Raises:
275
+ FileNotFoundError: If the specified file does not exist.
276
+ ValueError: If the file content is invalid for the model.
231
277
  """
232
278
  return cls.model_validate_json(safe_text_read(path))
233
279
 
@@ -236,7 +282,7 @@ class Language(Base):
236
282
  """Class that provides a language attribute."""
237
283
 
238
284
  language: str
239
- """The written language of this object, which should be aligned to the original requirement
285
+ """The written language of this object, which should be aligned to the original requirement
240
286
  For example if the requirement is in Chinese, the language should be set to `zh`, if the requirement is in English, the language should be set to `en` and etc."""
241
287
 
242
288
 
@@ -619,11 +665,11 @@ class Vectorizable(Base):
619
665
 
620
666
 
621
667
  class ScopedConfig(Base):
622
- """Class that manages a scoped configuration.
668
+ """Configuration holder with hierarchical fallback mechanism.
623
669
 
624
- This class includes attributes and methods to manage configuration settings scoped to the instance.
670
+ Manages LLM, embedding, and vector database configurations with fallback logic.
671
+ Allows configuration values to be overridden in a hierarchical manner.
625
672
  """
626
-
627
673
  llm_api_endpoint: Optional[HttpUrl] = None
628
674
  """The OpenAI API endpoint."""
629
675
 
@@ -698,13 +744,15 @@ class ScopedConfig(Base):
698
744
 
699
745
  @final
700
746
  def fallback_to(self, other: "ScopedConfig") -> Self:
701
- """Fallback to another instance's attribute values if the current instance's attributes are None.
747
+ """Merge configuration values with fallback priority.
748
+
749
+ Copies non-null values from 'other' to self where current values are None.
702
750
 
703
751
  Args:
704
- other (ScopedConfig): Another instance from which to copy attribute values.
752
+ other (ScopedConfig): Configuration to fallback to
705
753
 
706
754
  Returns:
707
- Self: The current instance, allowing for method chaining.
755
+ Self: Current instance with merged values
708
756
  """
709
757
  # Iterate over the attribute names and copy values from 'other' to 'self' where applicable
710
758
  # noinspection PydanticTypeChecker,PyTypeChecker
@@ -718,13 +766,15 @@ class ScopedConfig(Base):
718
766
 
719
767
  @final
720
768
  def hold_to(self, others: Union["ScopedConfig", Iterable["ScopedConfig"]]) -> Self:
721
- """Hold to another instance's attribute values if the current instance's attributes are None.
769
+ """Propagate non-null values to other configurations.
770
+
771
+ Copies current non-null values to target configurations where they are None.
722
772
 
723
773
  Args:
724
- others (Union[ScopedConfig, Iterable[ScopedConfig]]): Another instance or iterable of instances from which to copy attribute values.
774
+ others (ScopedConfig|Iterable): Target configurations to update
725
775
 
726
776
  Returns:
727
- Self: The current instance, allowing for method chaining.
777
+ Self: Current instance unchanged
728
778
  """
729
779
  if not isinstance(others, Iterable):
730
780
  others = [others]
@@ -777,14 +827,15 @@ class Patch[T](ProposedAble):
777
827
  str: The JSON schema of the model in a formatted string.
778
828
  """
779
829
  my_schema = cls.model_json_schema(schema_generator=UnsortGenerate)
780
- if (ref_cls := cls.ref_cls()) is not None:
830
+
831
+ ref_cls = cls.ref_cls()
832
+ if ref_cls is not None:
781
833
  # copy the desc info of each corresponding fields from `ref_cls`
782
- for field_name, field_info in cls.model_fields.items():
783
- if (ref_field := getattr(ref_cls, field_name, None)) is not None:
784
- if (desc := ref_field.field_info.description) is not None:
785
- my_schema["properties"][field_name]["description"] = desc
786
- if (example := ref_field.field_info.examples) is not None:
787
- my_schema["properties"][field_name]["examples"] = example
834
+ for field_name in [f for f in cls.model_fields if f in ref_cls.model_fields]:
835
+ my_schema["properties"][field_name]["description"] = (
836
+ ref_cls.model_fields[field_name].description or my_schema["properties"][field_name]["description"]
837
+ )
838
+ my_schema["description"] = ref_cls.__doc__
788
839
 
789
840
  return orjson.dumps(
790
841
  my_schema,
Binary file
fabricatio/rust.pyi CHANGED
@@ -1,5 +1,7 @@
1
1
  from pathlib import Path
2
- from typing import Any, Dict, List, Optional
2
+ from typing import List, Optional
3
+
4
+ from pydantic import JsonValue
3
5
 
4
6
  class TemplateManager:
5
7
  """Template rendering engine using Handlebars templates.
@@ -41,7 +43,7 @@ class TemplateManager:
41
43
  This refreshes the template cache, finding any new or modified templates.
42
44
  """
43
45
 
44
- def render_template(self, name: str, data: Dict[str, Any]) -> str:
46
+ def render_template(self, name: str, data: JsonValue) -> str:
45
47
  """Render a template with context data.
46
48
 
47
49
  Args:
@@ -55,7 +57,7 @@ class TemplateManager:
55
57
  RuntimeError: If template rendering fails
56
58
  """
57
59
 
58
- def render_template_raw(self, template: str, data: Dict[str, Any]) -> str:
60
+ def render_template_raw(self, template: str, data: JsonValue) -> str:
59
61
  """Render a template with context data.
60
62
 
61
63
  Args:
@@ -76,7 +78,7 @@ def blake3_hash(content: bytes) -> str:
76
78
  Hex-encoded BLAKE3 hash string
77
79
  """
78
80
 
79
- def detect_language(string:str)->str:
81
+ def detect_language(string: str) -> str:
80
82
  """Detect the language of a given string."""
81
83
 
82
84
  class BibManager:
@@ -165,7 +167,7 @@ class BibManager:
165
167
  Title if found, None otherwise
166
168
  """
167
169
 
168
- def get_field_by_key(self, key: str, field: str)-> Optional[str]:
170
+ def get_field_by_key(self, key: str, field: str) -> Optional[str]:
169
171
  """Retrieve a specific field by citation key.
170
172
 
171
173
  Args:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fabricatio
3
- Version: 0.2.9.dev1
3
+ Version: 0.2.9.dev2
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -1,43 +1,43 @@
1
- fabricatio-0.2.9.dev1.dist-info/METADATA,sha256=-IE_9trmCGkYuiIMorqcfBiHinbpt-bDX_HYF6pJj1A,5288
2
- fabricatio-0.2.9.dev1.dist-info/WHEEL,sha256=jABKVkLC9kJr8mi_er5jOqpiQUjARSLXDUIIxDqsS50,96
3
- fabricatio-0.2.9.dev1.dist-info/licenses/LICENSE,sha256=do7J7EiCGbq0QPbMAL_FqLYufXpHnCnXBOuqVPwSV8Y,1088
1
+ fabricatio-0.2.9.dev2.dist-info/METADATA,sha256=ElK7Y7uZngHDENVdObyZ5dxGFdvEt1tXligNdjKUKjc,5288
2
+ fabricatio-0.2.9.dev2.dist-info/WHEEL,sha256=jABKVkLC9kJr8mi_er5jOqpiQUjARSLXDUIIxDqsS50,96
3
+ fabricatio-0.2.9.dev2.dist-info/licenses/LICENSE,sha256=do7J7EiCGbq0QPbMAL_FqLYufXpHnCnXBOuqVPwSV8Y,1088
4
4
  fabricatio/actions/article.py,sha256=D-9aGLC1eW9w_aNICGNpdkUm7jx01h8nBcAPSW_Da7E,12788
5
5
  fabricatio/actions/article_rag.py,sha256=dkfskNqntVPi81Xm_NFdTQM8v2nR9FJIexzkK9rg-NU,4352
6
- fabricatio/actions/output.py,sha256=-FfTzXEHIb_zOT-j4T4b5ND96zHLDEGdjlmmPviIbiM,3754
6
+ fabricatio/actions/output.py,sha256=4b5fs_blwWDjm3qgEoHgOJV8Bb6YzE1uQH2J7KG3I8o,5761
7
7
  fabricatio/actions/rag.py,sha256=5nSih3YUkdt1uU02hSAMW6sADq9mkMOR1wDv7zIrIGQ,2737
8
8
  fabricatio/actions/rules.py,sha256=pA6ADNgpPWBp2a3FTEuF--0INHg0QJxc0kfxF1zMDXQ,1541
9
9
  fabricatio/actions/__init__.py,sha256=wVENCFtpVb1rLFxoOFJt9-8smLWXuJV7IwA8P3EfFz4,48
10
10
  fabricatio/capabilities/advanced_judge.py,sha256=selB0Gwf1F4gGJlwBiRo6gI4KOUROgh3WnzO3mZFEls,706
11
- fabricatio/capabilities/censor.py,sha256=J-YgwnDus3vKDpeFENhGvUSxvpC8FnhhnWJe1RffcqI,3869
12
- fabricatio/capabilities/check.py,sha256=uzxiar7bHGgxu7JqU8tRSncN3KP6xo5r3c8wvyBd3M8,8287
13
- fabricatio/capabilities/correct.py,sha256=I9RWmFjwXEjbYzIgtyFbqc0RvTpHeMHdGT9tuKHwCtY,9177
11
+ fabricatio/capabilities/censor.py,sha256=7LJrybFsv5yGjd_bfiempUw_YpmiYBze9XIzRLqH2CA,3787
12
+ fabricatio/capabilities/check.py,sha256=9NTRyJ8FvK-cC4o0KUolhZu58JLeOU8Mz9HIoBuNxP4,8044
13
+ fabricatio/capabilities/correct.py,sha256=b6GCcLzRruC6VjFS-uhC3m-7LGosr3Pso3cYLokQ03Y,9217
14
14
  fabricatio/capabilities/propose.py,sha256=hkBeSlmcTdfYWT-ph6nlbtHXBozi_JXqXlWcnBy3W78,2007
15
15
  fabricatio/capabilities/rag.py,sha256=8TTJSV2Tz0naXyOQ5c_RQ4h9ZxyOOSE7BvyWxKkQMU0,17722
16
16
  fabricatio/capabilities/rating.py,sha256=5HjWGk-tFscnMo4cFTAE820yeOaXT9utNSRKVrSTbSA,17012
17
17
  fabricatio/capabilities/review.py,sha256=-EMZe0ADFPT6fPGmra16UPjJC1M3rAs6dPFdTZ88Fgg,5060
18
18
  fabricatio/capabilities/task.py,sha256=JahC61X233UIPsjovxJgc_yqj_BjWZJBCzJZq11M2Xk,4417
19
19
  fabricatio/capabilities/__init__.py,sha256=v1cHRHIJ2gxyqMLNCs6ERVcCakSasZNYzmMI4lqAcls,57
20
- fabricatio/config.py,sha256=Q9VHk4T8ajJEPPckjUuhJ0vjvqc79zufUbek_5GHTbE,17679
20
+ fabricatio/config.py,sha256=fJHPY3LXSZWpEF17rAkY-IKeuB7oFlQrkRBIsModnlI,17687
21
21
  fabricatio/core.py,sha256=VQ_JKgUGIy2gZ8xsTBZCdr_IP7wC5aPg0_bsOmjQ588,6458
22
22
  fabricatio/decorators.py,sha256=C0Gi7wcXC-0sWITqsSv3JdBGcgVJOlRvOt0FfO0aUsA,7554
23
23
  fabricatio/fs/curd.py,sha256=p8y0LGKgVDk-CWOlm37E6wg7RK6RCD6denKo-VsW28c,4763
24
24
  fabricatio/fs/readers.py,sha256=EZKN_AZdrp8DggJECP53QHw3uHeSDf-AwCAA_V7fNKU,1202
25
25
  fabricatio/fs/__init__.py,sha256=PCf0s_9KDjVfNw7AfPoJzGt3jMq4gJOfbcT4pb0D0ZY,588
26
26
  fabricatio/journal.py,sha256=stnEP88aUBA_GmU9gfTF2EZI8FS2OyMLGaMSTgK4QgA,476
27
- fabricatio/models/action.py,sha256=C0PnZ5ycLcqcQ5s1h4KcLJXc1ZZmxYRfCQvz2DuSAOo,9229
27
+ fabricatio/models/action.py,sha256=N7h03z0fWSBVnSzc6uWRetrZluXr6Ibg4D5rD-vjUSU,9778
28
28
  fabricatio/models/adv_kwargs_types.py,sha256=dcYMLn6xcnWLZTLTBdtpgUZWi-VBeub721GzHRZFT1g,860
29
29
  fabricatio/models/events.py,sha256=QvlnS8FEELg6KNabcJMeh2GV_y0ZBzKOPphcteKYWYU,4183
30
30
  fabricatio/models/extra/advanced_judge.py,sha256=mi3KiB8FUuSYICzXPXKwVhCyRE1GL3gHLkwuc-4mh3c,999
31
- fabricatio/models/extra/article_base.py,sha256=1Fg3iWbod1TCU3nIJK9mOq_8K3oGBtSW03VUwi2I5tg,17058
31
+ fabricatio/models/extra/article_base.py,sha256=suOcuvxGwnTDLRyiWDEhCsw2h2zrClBclGzsO3273XI,17112
32
32
  fabricatio/models/extra/article_essence.py,sha256=xd6j-PDqjhrMjgUmyfk6HqkyMLu-sS9feUo0sZ3QABY,2825
33
- fabricatio/models/extra/article_main.py,sha256=0Xj54pRxUw5Iaj9npYmHKT-ZMvEIBlZ6G1ysNJRgceM,8225
34
- fabricatio/models/extra/article_outline.py,sha256=jFbVgiwlo7rnwCGS6ToVgeMUOoRe99Edgbx95THR6z8,1450
35
- fabricatio/models/extra/article_proposal.py,sha256=L2kPvH1XCCQSNcI1KQU3ULGq7C24Y88ssugX43LgbsE,2043
33
+ fabricatio/models/extra/article_main.py,sha256=6ULNb6kHW6wlFs1P-4hJsQdB4fSKs2yTqwXMHEcTO1g,8202
34
+ fabricatio/models/extra/article_outline.py,sha256=w7O0SHgC7exbptWVbR62FMHAueMgBpyWKVYMGGl_oj8,1427
35
+ fabricatio/models/extra/article_proposal.py,sha256=BLt0GBqrDDKSXy9ukrmYCM2HInYVGI5MVGkUYvoNBf8,2020
36
36
  fabricatio/models/extra/patches.py,sha256=-nBsse7g1astJh_IWZLXJdEQxpRla-DkHWqSHE-zbMU,989
37
37
  fabricatio/models/extra/problem.py,sha256=KwYkc7kjoEG7cwj9C8sWLoZgtBqJVuxlU_7KkvtSgO0,5828
38
38
  fabricatio/models/extra/rule.py,sha256=SDAPub17OzVJlPby358MQpKCyxmmojWucfuyr5zogeI,2091
39
39
  fabricatio/models/extra/__init__.py,sha256=XlYnS_2B9nhLhtQkjE7rvvfPmAAtXVdNi9bSDAR-Ge8,54
40
- fabricatio/models/generic.py,sha256=ioofSuSxJrgij6ZwucCV7RkVEU9XkW3C359k7s8ZhPA,28274
40
+ fabricatio/models/generic.py,sha256=Zfokr5BhIW2Rt6_cOfMoHPCsLDRSHENwqagdD-YqbEQ,30156
41
41
  fabricatio/models/kwargs_types.py,sha256=sMDA85SoC1AOJ5k6qC8qUiUv0Ne0_5ThU9FZITRNen4,5673
42
42
  fabricatio/models/role.py,sha256=-CRcj5_M3_ciLPzwiNn92grBmwoSLQ-n4koVZiCNTBM,2953
43
43
  fabricatio/models/task.py,sha256=YXvO3upJkTqMQjPgUGfp0bIiSyZzek2f4IagHdMW5Ik,10491
@@ -46,7 +46,7 @@ fabricatio/models/usages.py,sha256=atMrA1MlPthpKYjV5lIrvJwe-BFomOQXQAWAS85YFqw,3
46
46
  fabricatio/models/utils.py,sha256=Ac5g-8ic6q_w7dhNuh-iiofpL1sqOACxbjPPTljP2LY,4417
47
47
  fabricatio/parser.py,sha256=UOSvXigEXK-eXsr3m3b7glOhbBWs4kDJTeTNyuqA9ic,6315
48
48
  fabricatio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
- fabricatio/rust.pyi,sha256=iMEp-lQDKr8PD42ct6d24dPGNjpBiNAGwHyPZGtu-e4,5195
49
+ fabricatio/rust.pyi,sha256=DC4d8zQierX3J-M1oTaXyGBoBPus7N5uFt0hyGNl3P0,5212
50
50
  fabricatio/rust_instances.py,sha256=Byeo8KHW_dJiXujJq7YPGDLBX5bHNDYbBc4sY3uubVY,313
51
51
  fabricatio/toolboxes/arithmetic.py,sha256=WLqhY-Pikv11Y_0SGajwZx3WhsLNpHKf9drzAqOf_nY,1369
52
52
  fabricatio/toolboxes/fs.py,sha256=l4L1CVxJmjw9Ld2XUpIlWfV0_Fu_2Og6d3E13I-S4aE,736
@@ -56,6 +56,6 @@ fabricatio/workflows/articles.py,sha256=G5HGRr-DHuYuEcfhFdFAuDvTTJ9aSU_UQ2yYXEjT
56
56
  fabricatio/workflows/rag.py,sha256=-YYp2tlE9Vtfgpg6ROpu6QVO8j8yVSPa6yDzlN3qVxs,520
57
57
  fabricatio/workflows/__init__.py,sha256=5ScFSTA-bvhCesj3U9Mnmi6Law6N1fmh5UKyh58L3u8,51
58
58
  fabricatio/__init__.py,sha256=Rmvq2VgdS2u68vnOi2i5RbeWbAwrJDbk8D8D883PJWE,1022
59
- fabricatio/rust.cp312-win_amd64.pyd,sha256=_KH3Q40xx8oxJyKgmUNtRoWQXtCMmWZg239SqJaUxx8,2170880
60
- fabricatio-0.2.9.dev1.data/scripts/tdown.exe,sha256=XH14dECiES13OanYo8sGr0B9DJdaGRO-a_S4933s4D4,3395072
61
- fabricatio-0.2.9.dev1.dist-info/RECORD,,
59
+ fabricatio/rust.cp312-win_amd64.pyd,sha256=J7RO4JEsfwpgeZSsV2wWtlMd3TXL0a90AJdDLkqlX58,2170880
60
+ fabricatio-0.2.9.dev2.data/scripts/tdown.exe,sha256=VvK4W6LYfgPzIEwegvpCKkmuhXkBR8MgzIQEYJhNIXQ,3395072
61
+ fabricatio-0.2.9.dev2.dist-info/RECORD,,