fabricatio 0.2.8.dev4__cp312-cp312-manylinux_2_34_x86_64.whl → 0.2.9__cp312-cp312-manylinux_2_34_x86_64.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 (46) hide show
  1. fabricatio/__init__.py +4 -11
  2. fabricatio/actions/__init__.py +1 -0
  3. fabricatio/actions/article.py +98 -110
  4. fabricatio/actions/article_rag.py +15 -10
  5. fabricatio/actions/output.py +60 -4
  6. fabricatio/actions/rag.py +2 -1
  7. fabricatio/actions/rules.py +72 -0
  8. fabricatio/capabilities/__init__.py +1 -0
  9. fabricatio/capabilities/censor.py +23 -6
  10. fabricatio/capabilities/check.py +46 -27
  11. fabricatio/capabilities/correct.py +35 -16
  12. fabricatio/capabilities/rag.py +5 -4
  13. fabricatio/capabilities/rating.py +56 -49
  14. fabricatio/capabilities/review.py +1 -1
  15. fabricatio/capabilities/task.py +2 -1
  16. fabricatio/config.py +5 -3
  17. fabricatio/fs/readers.py +20 -1
  18. fabricatio/models/action.py +59 -36
  19. fabricatio/models/extra/__init__.py +1 -0
  20. fabricatio/models/extra/advanced_judge.py +4 -4
  21. fabricatio/models/extra/article_base.py +124 -61
  22. fabricatio/models/extra/article_main.py +100 -17
  23. fabricatio/models/extra/article_outline.py +2 -3
  24. fabricatio/models/extra/article_proposal.py +15 -14
  25. fabricatio/models/extra/patches.py +17 -4
  26. fabricatio/models/extra/problem.py +31 -23
  27. fabricatio/models/extra/rule.py +39 -8
  28. fabricatio/models/generic.py +369 -78
  29. fabricatio/models/task.py +1 -1
  30. fabricatio/models/tool.py +149 -14
  31. fabricatio/models/usages.py +46 -42
  32. fabricatio/parser.py +5 -5
  33. fabricatio/rust.cpython-312-x86_64-linux-gnu.so +0 -0
  34. fabricatio/{_rust.pyi → rust.pyi} +42 -4
  35. fabricatio/{_rust_instances.py → rust_instances.py} +1 -1
  36. fabricatio/utils.py +5 -5
  37. fabricatio/workflows/__init__.py +1 -0
  38. fabricatio/workflows/articles.py +3 -5
  39. fabricatio-0.2.9.data/scripts/tdown +0 -0
  40. {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/METADATA +1 -1
  41. fabricatio-0.2.9.dist-info/RECORD +61 -0
  42. fabricatio/_rust.cpython-312-x86_64-linux-gnu.so +0 -0
  43. fabricatio-0.2.8.dev4.data/scripts/tdown +0 -0
  44. fabricatio-0.2.8.dev4.dist-info/RECORD +0 -56
  45. {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/WHEEL +0 -0
  46. {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/licenses/LICENSE +0 -0
fabricatio/__init__.py CHANGED
@@ -1,10 +1,6 @@
1
1
  """Fabricatio is a Python library for building llm app using event-based agent structure."""
2
2
 
3
- from importlib.util import find_spec
4
-
5
- from fabricatio import actions, toolboxes, workflows
6
- from fabricatio._rust import BibManager
7
- from fabricatio._rust_instances import TEMPLATE_MANAGER
3
+ from fabricatio import actions, capabilities, toolboxes, workflows
8
4
  from fabricatio.core import env
9
5
  from fabricatio.journal import logger
10
6
  from fabricatio.models import extra
@@ -14,6 +10,8 @@ from fabricatio.models.role import Role
14
10
  from fabricatio.models.task import Task
15
11
  from fabricatio.models.tool import ToolBox
16
12
  from fabricatio.parser import Capture, GenericCapture, JsonCapture, PythonCapture
13
+ from fabricatio.rust import BibManager
14
+ from fabricatio.rust_instances import TEMPLATE_MANAGER
17
15
 
18
16
  __all__ = [
19
17
  "TEMPLATE_MANAGER",
@@ -29,15 +27,10 @@ __all__ = [
29
27
  "ToolBox",
30
28
  "WorkFlow",
31
29
  "actions",
30
+ "capabilities",
32
31
  "env",
33
32
  "extra",
34
33
  "logger",
35
34
  "toolboxes",
36
35
  "workflows",
37
36
  ]
38
-
39
-
40
- if find_spec("pymilvus"):
41
- from fabricatio.capabilities.rag import RAG
42
-
43
- __all__ += ["RAG"]
@@ -0,0 +1 @@
1
+ """A module containing some builtin actins."""
@@ -2,24 +2,24 @@
2
2
 
3
3
  from asyncio import gather
4
4
  from pathlib import Path
5
- from typing import Any, Callable, List, Optional
5
+ from typing import Callable, List, Optional
6
+
7
+ from more_itertools import filter_map
6
8
 
7
- from fabricatio._rust import BibManager
8
9
  from fabricatio.capabilities.censor import Censor
9
- from fabricatio.capabilities.correct import Correct
10
10
  from fabricatio.capabilities.propose import Propose
11
11
  from fabricatio.fs import safe_text_read
12
12
  from fabricatio.journal import logger
13
13
  from fabricatio.models.action import Action
14
- from fabricatio.models.extra.article_base import ArticleRefSequencePatch
14
+ from fabricatio.models.extra.article_base import ArticleRef, SubSectionBase
15
15
  from fabricatio.models.extra.article_essence import ArticleEssence
16
16
  from fabricatio.models.extra.article_main import Article
17
17
  from fabricatio.models.extra.article_outline import ArticleOutline
18
18
  from fabricatio.models.extra.article_proposal import ArticleProposal
19
19
  from fabricatio.models.extra.rule import RuleSet
20
20
  from fabricatio.models.task import Task
21
+ from fabricatio.rust import BibManager, detect_language
21
22
  from fabricatio.utils import ok
22
- from more_itertools import filter_map
23
23
 
24
24
 
25
25
  class ExtractArticleEssence(Action, Propose):
@@ -52,7 +52,7 @@ class ExtractArticleEssence(Action, Propose):
52
52
  for ess in await self.propose(
53
53
  ArticleEssence,
54
54
  [
55
- f"{c}\n\n\nBased the provided academic article above, you need to extract the essence from it."
55
+ f"{c}\n\n\nBased the provided academic article above, you need to extract the essence from it.\n\nWrite the value string using `{detect_language(c)}`"
56
56
  for c in contents
57
57
  ],
58
58
  ):
@@ -105,35 +105,30 @@ class GenerateArticleProposal(Action, Propose):
105
105
  task_input: Optional[Task] = None,
106
106
  article_briefing: Optional[str] = None,
107
107
  article_briefing_path: Optional[str] = None,
108
- langauge: Optional[str] = None,
109
108
  **_,
110
109
  ) -> Optional[ArticleProposal]:
111
110
  if article_briefing is None and article_briefing_path is None and task_input is None:
112
111
  logger.error("Task not approved, since all inputs are None.")
113
112
  return None
114
113
 
115
- proposal = ok(
114
+ briefing = article_briefing or safe_text_read(
115
+ ok(
116
+ article_briefing_path
117
+ or await self.awhich_pathstr(
118
+ f"{ok(task_input).briefing}\nExtract the path of file which contains the article briefing."
119
+ ),
120
+ "Could not find the path of file to read.",
121
+ )
122
+ )
123
+
124
+ logger.info("Start generating the proposal.")
125
+ return ok(
116
126
  await self.propose(
117
127
  ArticleProposal,
118
- briefing := (
119
- article_briefing
120
- or safe_text_read(
121
- ok(
122
- article_briefing_path
123
- or await self.awhich_pathstr(
124
- f"{ok(task_input).briefing}\nExtract the path of file which contains the article briefing."
125
- ),
126
- "Could not find the path of file to read.",
127
- )
128
- )
129
- ),
128
+ f"{briefing}\n\nWrite the value string using `{detect_language(briefing)}` as written language.",
130
129
  ),
131
130
  "Could not generate the proposal.",
132
131
  ).update_ref(briefing)
133
- if langauge:
134
- proposal.language = langauge
135
-
136
- return proposal
137
132
 
138
133
 
139
134
  class GenerateInitialOutline(Action, Propose):
@@ -147,10 +142,17 @@ class GenerateInitialOutline(Action, Propose):
147
142
  article_proposal: ArticleProposal,
148
143
  **_,
149
144
  ) -> Optional[ArticleOutline]:
145
+ raw_outline = await self.aask(
146
+ f"{(article_proposal.as_prompt())}\n\nNote that you should use `{article_proposal.language}` to write the `ArticleOutline`\n"
147
+ f"Design each chapter of a proper and academic and ready for release manner.\n"
148
+ f"You Must make sure every chapter have sections, and every section have subsections.\n"
149
+ f"Make the chapter and sections and subsections bing divided into a specific enough article component.",
150
+ )
151
+
150
152
  return ok(
151
153
  await self.propose(
152
154
  ArticleOutline,
153
- article_proposal.as_prompt(),
155
+ f'{raw_outline}\n\n\n\noutline provided above is the outline i need to extract to a JSON,'
154
156
  ),
155
157
  "Could not generate the initial outline.",
156
158
  ).update_ref(article_proposal)
@@ -164,25 +166,33 @@ class FixIntrospectedErrors(Action, Censor):
164
166
 
165
167
  ruleset: Optional[RuleSet] = None
166
168
  """The ruleset to use to fix the introspected errors."""
169
+ max_error_count: Optional[int] = None
170
+ """The maximum number of errors to fix."""
167
171
 
168
172
  async def _execute(
169
173
  self,
170
174
  article_outline: ArticleOutline,
171
- ruleset: Optional[RuleSet] = None,
175
+ intro_fix_ruleset: Optional[RuleSet] = None,
172
176
  **_,
173
177
  ) -> Optional[ArticleOutline]:
174
- while pack := article_outline.find_introspected():
175
- component, err = ok(pack)
176
- logger.warning(f"Found introspected error: {err}")
177
- corrected = ok(
178
+ counter = 0
179
+ origin = article_outline
180
+ while pack := article_outline.gather_introspected():
181
+ logger.info(f"Found {counter}th introspected errors")
182
+ logger.warning(f"Found introspected error: {pack}")
183
+ article_outline = ok(
178
184
  await self.censor_obj(
179
- component,
180
- ruleset=ok(ruleset or self.ruleset, "No ruleset provided"),
181
- reference=f"# Original Article Outline\n{article_outline.display()}\n# Some Basic errors found from `{component.title}` that need to be fixed\n{err}",
185
+ article_outline,
186
+ ruleset=ok(intro_fix_ruleset or self.ruleset, "No ruleset provided"),
187
+ reference=f"{article_outline.display()}\n # Fatal Error of the Original Article Outline\n{pack}",
182
188
  ),
183
189
  "Could not correct the component.",
184
- )
185
- component.update_from(corrected)
190
+ ).update_ref(origin)
191
+
192
+ if self.max_error_count and counter > self.max_error_count:
193
+ logger.warning("Max error count reached, stopping.")
194
+ break
195
+ counter += 1
186
196
 
187
197
  return article_outline
188
198
 
@@ -195,27 +205,36 @@ class FixIllegalReferences(Action, Censor):
195
205
 
196
206
  ruleset: Optional[RuleSet] = None
197
207
  """Ruleset to use to fix the illegal references."""
208
+ max_error_count: Optional[int] = None
209
+ """The maximum number of errors to fix."""
198
210
 
199
211
  async def _execute(
200
212
  self,
201
213
  article_outline: ArticleOutline,
202
- ruleset: Optional[RuleSet] = None,
214
+ ref_fix_ruleset: Optional[RuleSet] = None,
203
215
  **_,
204
216
  ) -> Optional[ArticleOutline]:
205
- while pack := article_outline.find_illegal_ref(gather_identical=True):
206
- refs, err = ok(pack)
207
- logger.warning(f"Found illegal referring error: {err}")
208
- corrected_ref = ok(
209
- await self.censor_obj(
210
- refs[0], # pyright: ignore [reportIndexIssue]
211
- ruleset=ok(ruleset or self.ruleset, "No ruleset provided"),
212
- reference=f"# Original Article Outline\n{article_outline.display()}\n# Some Basic errors found that need to be fixed\n{err}",
213
- )
217
+ counter = 0
218
+ while pack := article_outline.find_illegal_ref():
219
+ logger.info(f"Found {counter}th illegal references")
220
+ cmp, ref, err = ok(pack)
221
+ logger.warning(f"Found illegal referring error: {err}\n\n{cmp.display()}")
222
+ new = ok(
223
+ await self.censor_obj_inplace(
224
+ cmp,
225
+ ruleset=ok(ref_fix_ruleset or self.ruleset, "No ruleset provided"),
226
+ reference=f"{article_outline.as_prompt()}\n# `ArticleRef` usage doc\n{ArticleRef.__doc__}\n# Invalid `ArticleRef`\n```json\n{ref}```\n\n# Some Ref errors found that need to be fixed for the `ArticleRef`\n{err}",
227
+ ),
228
+ "Could not correct the component",
214
229
  )
215
- for ref in refs:
216
- ref.update_from(corrected_ref) # pyright: ignore [reportAttributeAccessIssue]
217
230
 
218
- return article_outline.update_ref(article_outline)
231
+ logger.info(f"Applying correction:\n{new.display()}")
232
+ if self.max_error_count and counter > self.max_error_count:
233
+ logger.warning("Max error count reached, stopping.")
234
+ break
235
+ counter += 1
236
+
237
+ return article_outline
219
238
 
220
239
 
221
240
  class TweakOutlineForwardRef(Action, Censor):
@@ -229,32 +248,40 @@ class TweakOutlineForwardRef(Action, Censor):
229
248
  """Ruleset to use to fix the illegal references."""
230
249
 
231
250
  async def _execute(
232
- self, article_outline: ArticleOutline, ruleset: Optional[RuleSet] = None, **cxt
251
+ self, article_outline: ArticleOutline, ref_twk_ruleset: Optional[RuleSet] = None, **cxt
233
252
  ) -> ArticleOutline:
234
253
  return await self._inner(
235
254
  article_outline,
236
- ruleset=ok(ruleset or self.ruleset, "No ruleset provided"),
255
+ ruleset=ok(ref_twk_ruleset or self.ruleset, "No ruleset provided"),
237
256
  field_name="support_to",
238
257
  )
239
258
 
240
259
  async def _inner(self, article_outline: ArticleOutline, ruleset: RuleSet, field_name: str) -> ArticleOutline:
241
- for a in article_outline.iter_dfs():
242
- if judge := await self.evidently_judge(
243
- f"{article_outline.as_prompt()}\n\n{a.display()}\n"
244
- f"Does the `{a.__class__.__name__}`'s `{field_name}` field need to be extended or tweaked?"
245
- ):
246
- patch = ArticleRefSequencePatch.default()
247
- patch.tweaked = getattr(a, field_name)
260
+ await gather(
261
+ *[self._loop(a[-1], article_outline, field_name, ruleset) for a in article_outline.iter_subsections()],
262
+ )
248
263
 
249
- await self.censor_obj_inplace(
250
- patch,
251
- ruleset=ruleset,
252
- reference=f"{article_outline.as_prompt()}\n"
253
- f"The Article component titled `{a.title}` whose `{field_name}` field needs to be extended or tweaked.\n"
254
- f"# Judgement\n{judge.display()}",
255
- )
256
264
  return article_outline
257
265
 
266
+ async def _loop(
267
+ self, a: SubSectionBase, article_outline: ArticleOutline, field_name: str, ruleset: RuleSet
268
+ ) -> None:
269
+ if judge := await self.evidently_judge(
270
+ f"{article_outline.as_prompt()}\n\n{a.display()}\n"
271
+ f"# `ArticleRef` usage doc\n{ArticleRef.__doc__}\n"
272
+ f"# Ruleset\n{ruleset.display()}\n"
273
+ f"Does the `{a.__class__.__name__}`'s `{field_name}` field contains inappropriate ref that obviously violated the ruleset provided?\n"
274
+ f"true for it does violated, false for it does not violated."
275
+ ):
276
+ await self.censor_obj_inplace(
277
+ a,
278
+ ruleset=ruleset,
279
+ reference=f"{article_outline.as_prompt()}\n"
280
+ f"The Article component titled `{a.title}` whose `{field_name}` field needs to be tweaked.\n"
281
+ f"# `ArticleRef` usage doc\n{ArticleRef.__doc__}\n"
282
+ f"# Judgement\n{judge.display()}",
283
+ )
284
+
258
285
 
259
286
  class TweakOutlineBackwardRef(TweakOutlineForwardRef):
260
287
  """Tweak the backward references in the article outline.
@@ -266,11 +293,11 @@ class TweakOutlineBackwardRef(TweakOutlineForwardRef):
266
293
  ruleset: Optional[RuleSet] = None
267
294
 
268
295
  async def _execute(
269
- self, article_outline: ArticleOutline, ruleset: Optional[RuleSet] = None, **cxt
296
+ self, article_outline: ArticleOutline, ref_twk_ruleset: Optional[RuleSet] = None, **cxt
270
297
  ) -> ArticleOutline:
271
298
  return await self._inner(
272
299
  article_outline,
273
- ruleset=ok(ruleset or self.ruleset, "No ruleset provided"),
300
+ ruleset=ok(ref_twk_ruleset or self.ruleset, "No ruleset provided"),
274
301
  field_name="depend_on",
275
302
  )
276
303
 
@@ -285,7 +312,7 @@ class GenerateArticle(Action, Censor):
285
312
  async def _execute(
286
313
  self,
287
314
  article_outline: ArticleOutline,
288
- ruleset: Optional[RuleSet] = None,
315
+ article_gen_ruleset: Optional[RuleSet] = None,
289
316
  **_,
290
317
  ) -> Optional[Article]:
291
318
  article: Article = Article.from_outline(ok(article_outline, "Article outline not specified.")).update_ref(
@@ -296,51 +323,12 @@ class GenerateArticle(Action, Censor):
296
323
  *[
297
324
  self.censor_obj_inplace(
298
325
  subsec,
299
- ruleset=ok(ruleset or self.ruleset, "No ruleset provided"),
300
- reference=f"# Original Article Outline\n{article_outline.display()}\n# Error Need to be fixed\n{err}",
326
+ ruleset=ok(article_gen_ruleset or self.ruleset, "No ruleset provided"),
327
+ reference=f"{article_outline.as_prompt()}\n# `ArticleRef` usage doc\n{ArticleRef.__doc__}# Error Need to be fixed\n{err}\nYou should use `{subsec.language}` to write the new `Subsection`.",
301
328
  )
302
- for _, __, subsec in article.iter_subsections()
303
- if (err := subsec.introspect())
329
+ for _, _, subsec in article.iter_subsections()
330
+ if (err := subsec.introspect()) and logger.warning(f"Found Introspection Error:\n{err}") is None
304
331
  ],
305
- return_exceptions=True,
306
332
  )
307
333
 
308
334
  return article
309
-
310
-
311
- class CorrectProposal(Action, Censor):
312
- """Correct the proposal of the article."""
313
-
314
- output_key: str = "corrected_proposal"
315
-
316
- async def _execute(self, article_proposal: ArticleProposal, **_) -> Any:
317
- raise NotImplementedError("Not implemented.")
318
-
319
-
320
- class CorrectOutline(Action, Correct):
321
- """Correct the outline of the article."""
322
-
323
- output_key: str = "corrected_outline"
324
- """The key of the output data."""
325
-
326
- async def _execute(
327
- self,
328
- article_outline: ArticleOutline,
329
- **_,
330
- ) -> ArticleOutline:
331
- raise NotImplementedError("Not implemented.")
332
-
333
-
334
- class CorrectArticle(Action, Correct):
335
- """Correct the article based on the outline."""
336
-
337
- output_key: str = "corrected_article"
338
- """The key of the output data."""
339
-
340
- async def _execute(
341
- self,
342
- article: Article,
343
- article_outline: ArticleOutline,
344
- **_,
345
- ) -> Article:
346
- raise NotImplementedError("Not implemented.")
@@ -6,7 +6,7 @@ from typing import Optional
6
6
  from fabricatio.capabilities.censor import Censor
7
7
  from fabricatio.capabilities.rag import RAG
8
8
  from fabricatio.models.action import Action
9
- from fabricatio.models.extra.article_main import Article, ArticleParagraphSequencePatch, ArticleSubsection
9
+ from fabricatio.models.extra.article_main import Article, ArticleSubsection
10
10
  from fabricatio.models.extra.rule import RuleSet
11
11
  from fabricatio.utils import ok
12
12
 
@@ -29,11 +29,14 @@ class TweakArticleRAG(Action, RAG, Censor):
29
29
  ruleset: Optional[RuleSet] = None
30
30
  """The ruleset to be used for censoring the article."""
31
31
 
32
+ ref_limit: int = 30
33
+ """The limit of references to be retrieved"""
34
+
32
35
  async def _execute(
33
36
  self,
34
37
  article: Article,
35
38
  collection_name: str = "article_essence",
36
- ruleset: Optional[RuleSet] = None,
39
+ twk_rag_ruleset: Optional[RuleSet] = None,
37
40
  parallel: bool = False,
38
41
  **cxt,
39
42
  ) -> Optional[Article]:
@@ -45,7 +48,7 @@ class TweakArticleRAG(Action, RAG, Censor):
45
48
  Args:
46
49
  article (Article): The article to be processed.
47
50
  collection_name (str): The name of the collection to view for processing.
48
- ruleset (Optional[RuleSet]): The ruleset to apply for censoring. If not provided, the class's ruleset is used.
51
+ twk_rag_ruleset (Optional[RuleSet]): The ruleset to apply for censoring. If not provided, the class's ruleset is used.
49
52
  parallel (bool): If True, process subsections in parallel. Otherwise, process them sequentially.
50
53
  **cxt: Additional context parameters.
51
54
 
@@ -57,14 +60,14 @@ class TweakArticleRAG(Action, RAG, Censor):
57
60
  if parallel:
58
61
  await gather(
59
62
  *[
60
- self._inner(article, subsec, ok(ruleset or self.ruleset, "No ruleset provided!"))
63
+ self._inner(article, subsec, ok(twk_rag_ruleset or self.ruleset, "No ruleset provided!"))
61
64
  for _, __, subsec in article.iter_subsections()
62
65
  ],
63
66
  return_exceptions=True,
64
67
  )
65
68
  else:
66
69
  for _, __, subsec in article.iter_subsections():
67
- await self._inner(article, subsec, ok(ruleset or self.ruleset, "No ruleset provided!"))
70
+ await self._inner(article, subsec, ok(twk_rag_ruleset or self.ruleset, "No ruleset provided!"))
68
71
  return article
69
72
 
70
73
  async def _inner(self, article: Article, subsec: ArticleSubsection, ruleset: RuleSet) -> None:
@@ -88,13 +91,15 @@ class TweakArticleRAG(Action, RAG, Censor):
88
91
  f"{subsec.display()}\n"
89
92
  f"# Requirement\n"
90
93
  f"Search related articles in the base to find reference candidates, "
91
- f"prioritizing both original article language and English usage",
94
+ f"provide queries in both `English` and `{subsec.language}` can get more accurate results.",
92
95
  )
93
96
  )
94
- patch = ArticleParagraphSequencePatch.default()
95
- patch.tweaked = subsec.paragraphs
96
97
  await self.censor_obj_inplace(
97
- patch,
98
+ subsec,
98
99
  ruleset=ruleset,
99
- reference=await self.aretrieve_compact(refind_q, final_limit=30),
100
+ reference=f"{await self.aretrieve_compact(refind_q, final_limit=self.ref_limit)}\n\n"
101
+ f"You can use Reference above to rewrite the `{subsec.__class__.__name__}`.\n"
102
+ f"You should Always use `{subsec.language}` as written language, "
103
+ f"which is the original language of the `{subsec.title}`. "
104
+ f"since rewrite a `{subsec.__class__.__name__}` in a different language is usually a bad choice",
100
105
  )
@@ -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)
@@ -92,11 +103,56 @@ class RetrieveFromPersistent[T: PersistentAble](Action):
92
103
  retrieve_cls: Type[T]
93
104
  """The class of the object to retrieve."""
94
105
 
95
- async def _execute(self, /, **__) -> Optional[T | List[T]]:
96
- logger.info(f"Retrieve `{self.retrieve_cls.__name__}` from persistent file: {self.load_path}")
106
+ async def _execute(self, /, **_) -> Optional[T | List[T]]:
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 RetrieveFromLatest[T: PersistentAble](RetrieveFromPersistent[T]):
119
+ """Retrieve the object from the latest persistent file in the dir at `load_path`."""
120
+
121
+ async def _execute(self, /, **_) -> Optional[T]:
122
+ logger.info(f"Retrieve latest `{self.retrieve_cls.__name__}` from {self.load_path}")
123
+ if not (p := Path(self.load_path)).exists():
124
+ logger.warning(f"Path {self.load_path} does not exist")
125
+ return None
126
+
127
+ if p.is_dir():
128
+ logger.info(f"Found directory with {len(list(p.glob('*')))} items")
129
+ return self.retrieve_cls.from_latest_persistent(self.load_path)
130
+ logger.error(f"Path {self.load_path} is not a directory")
131
+ return None
132
+
133
+
134
+ class GatherAsList(Action):
135
+ """Gather the objects from the context as a list.
136
+
137
+ Notes:
138
+ If both `gather_suffix` and `gather_prefix` are specified, only the objects with the suffix will be gathered.
139
+ """
140
+
141
+ output_key: str = "gathered"
142
+ """Gather the objects from the context as a list."""
143
+ gather_suffix: Optional[str] = None
144
+ """Gather the objects from the context as a list."""
145
+ gather_prefix: Optional[str] = None
146
+ """Gather the objects from the context as a list."""
147
+
148
+ async def _execute(self, **cxt) -> List[Any]:
149
+ if self.gather_suffix is not None:
150
+ result = [cxt[k] for k in cxt if k.endswith(self.gather_suffix)]
151
+ logger.debug(f"Gathered {len(result)} items with suffix {self.gather_suffix}")
152
+ return result
153
+ if self.gather_prefix is None:
154
+ logger.error(err := "Either `gather_suffix` or `gather_prefix` must be specified.")
155
+ raise ValueError(err)
156
+ result = [cxt[k] for k in cxt if k.startswith(self.gather_prefix)]
157
+ logger.debug(f"Gathered {len(result)} items with prefix {self.gather_prefix}")
158
+ return result
fabricatio/actions/rag.py CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  from typing import List, Optional
4
4
 
5
+ from questionary import text
6
+
5
7
  from fabricatio.capabilities.rag import RAG
6
8
  from fabricatio.journal import logger
7
9
  from fabricatio.models.action import Action
8
10
  from fabricatio.models.generic import Vectorizable
9
11
  from fabricatio.models.task import Task
10
- from questionary import text
11
12
 
12
13
 
13
14
  class InjectToDB(Action, RAG):
@@ -0,0 +1,72 @@
1
+ """A module containing the DraftRuleSet action."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from fabricatio.capabilities.check import Check
6
+ from fabricatio.journal import logger
7
+ from fabricatio.models.action import Action
8
+ from fabricatio.models.extra.rule import RuleSet
9
+ from fabricatio.utils import ok
10
+
11
+
12
+ class DraftRuleSet(Action, Check):
13
+ """Action to draft a ruleset based on a given requirement description."""
14
+
15
+ output_key: str = "drafted_ruleset"
16
+ """The key used to store the drafted ruleset in the context dictionary."""
17
+
18
+ ruleset_requirement: Optional[str] = None
19
+ """The natural language description of the desired ruleset characteristics."""
20
+ rule_count: int = 0
21
+ """The number of rules to generate in the ruleset (0 for no restriction)."""
22
+
23
+ async def _execute(
24
+ self,
25
+ ruleset_requirement: Optional[str] = None,
26
+ **_,
27
+ ) -> Optional[RuleSet]:
28
+ """Draft a ruleset based on the requirement description.
29
+
30
+ Args:
31
+ ruleset_requirement (str): Natural language description of desired ruleset characteristics
32
+ rule_count (int): Number of rules to generate (0 for no restriction)
33
+ **kwargs: Validation parameters for rule generation
34
+
35
+ Returns:
36
+ Optional[RuleSet]: Drafted ruleset object or None if generation fails
37
+ """
38
+ ruleset = await self.draft_ruleset(
39
+ ruleset_requirement=ok(ruleset_requirement or self.ruleset_requirement, "No ruleset requirement provided"),
40
+ rule_count=self.rule_count,
41
+ )
42
+ if ruleset:
43
+ logger.info(f"Drafted Ruleset length: {len(ruleset.rules)}\n{ruleset.display()}")
44
+ else:
45
+ logger.warning(f"Drafting Rule Failed for:\n{ruleset_requirement}")
46
+ return ruleset
47
+
48
+
49
+ class GatherRuleset(Action):
50
+ """Action to gather a ruleset from a given requirement description."""
51
+
52
+ output_key: str = "gathered_ruleset"
53
+ """The key used to store the drafted ruleset in the context dictionary."""
54
+
55
+ to_gather: List[str]
56
+ """the cxt name of RuleSet to gather"""
57
+
58
+ async def _execute(self, **cxt) -> RuleSet:
59
+ logger.info(f"Gathering Ruleset from {self.to_gather}")
60
+ # Fix for not_found
61
+ not_found = next((t for t in self.to_gather if t not in cxt), None)
62
+ if not_found:
63
+ raise ValueError(
64
+ f"Not all required keys found in context: {self.to_gather}|`{not_found}` not found in context."
65
+ )
66
+
67
+ # Fix for invalid RuleSet check
68
+ invalid = next((t for t in self.to_gather if not isinstance(cxt[t], RuleSet)), None)
69
+ if invalid is not None:
70
+ raise TypeError(f"Invalid RuleSet instance for key `{invalid}`")
71
+
72
+ return RuleSet.gather(*[cxt[t] for t in self.to_gather])
@@ -0,0 +1 @@
1
+ """A module containing some high level capabilities."""