fabricatio 0.3.14.dev8__cp313-cp313-manylinux_2_34_x86_64.whl → 0.3.15.dev5__cp313-cp313-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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  from asyncio import gather
4
4
  from pathlib import Path
5
- from typing import Callable, List, Optional
5
+ from typing import Callable, ClassVar, List, Optional
6
6
 
7
7
  from more_itertools import filter_map
8
8
  from pydantic import Field
@@ -15,14 +15,14 @@ from fabricatio.fs import dump_text, safe_text_read
15
15
  from fabricatio.journal import logger
16
16
  from fabricatio.models.action import Action
17
17
  from fabricatio.models.extra.article_essence import ArticleEssence
18
- from fabricatio.models.extra.article_main import Article
18
+ from fabricatio.models.extra.article_main import Article, ArticleChapter, ArticleSubsection
19
19
  from fabricatio.models.extra.article_outline import ArticleOutline
20
20
  from fabricatio.models.extra.article_proposal import ArticleProposal
21
21
  from fabricatio.models.extra.rule import RuleSet
22
22
  from fabricatio.models.kwargs_types import ValidateKwargs
23
23
  from fabricatio.models.task import Task
24
24
  from fabricatio.models.usages import LLMUsage
25
- from fabricatio.rust import CONFIG, TEMPLATE_MANAGER, BibManager, detect_language
25
+ from fabricatio.rust import CONFIG, TEMPLATE_MANAGER, BibManager, detect_language, word_count
26
26
  from fabricatio.utils import ok, wrapp_in_block
27
27
 
28
28
 
@@ -277,43 +277,139 @@ class LoadArticle(Action):
277
277
  class WriteChapterSummary(Action, LLMUsage):
278
278
  """Write the chapter summary."""
279
279
 
280
- output_key: str = "chapter_summaries"
280
+ ctx_override: ClassVar[bool] = True
281
281
 
282
282
  paragraph_count: int = 1
283
+ """The number of paragraphs to generate in the chapter summary."""
283
284
 
284
- summary_word_count: int = 200
285
-
285
+ summary_word_count: int = 120
286
+ """The number of words to use in each chapter summary."""
287
+ output_key: str = "summarized_article"
288
+ """The key under which the summarized article will be stored in the output."""
286
289
  summary_title: str = "Chapter Summary"
287
- write_to: Optional[Path] = None
290
+ """The title to be used for the generated chapter summary section."""
291
+
292
+ skip_chapters: List[str] = Field(default_factory=list)
293
+ """A list of chapter titles to skip during summary generation."""
294
+
295
+ async def _execute(self, article_path: Path, **cxt) -> Article:
296
+ article = Article.from_article_file(article_path, article_path.stem)
297
+
298
+ chaps = [c for c in article.chapters if c.title not in self.skip_chapters]
299
+
300
+ retained_chapters = []
301
+ # Count chapters before filtering based on section presence,
302
+ # chaps at this point has already been filtered by self.skip_chapters
303
+ initial_chaps_for_summary_step_count = len(chaps)
304
+
305
+ for chapter_candidate in chaps:
306
+ if chapter_candidate.sections: # Check if the sections list is non-empty
307
+ retained_chapters.append(chapter_candidate)
308
+ else:
309
+ # Log c warning for each chapter skipped due to lack of sections
310
+ logger.warning(
311
+ f"Chapter '{chapter_candidate.title}' has no sections and will be skipped for summary generation."
312
+ )
313
+
314
+ chaps = retained_chapters # Update chaps to only include chapters with sections
288
315
 
289
- async def _execute(self, article: Article, write_to: Optional[Path] = None, **cxt) -> List[str]:
290
- logger.info(";".join(a.title for a in article.chapters))
316
+ # If chaps is now empty, but there were chapters to consider at the start of this step,
317
+ # log c specific warning.
318
+ if not chaps and initial_chaps_for_summary_step_count > 0:
319
+ raise ValueError("No chapters with sections were found. Please check your input data.")
291
320
 
321
+ # This line was part of the original selection.
322
+ # It will now log the titles of the chapters that are actually being processed (those with sections).
323
+ # If 'chaps' is empty, this will result in logger.info(""), which is acceptable.
324
+ logger.info(";".join(a.title for a in chaps))
292
325
  ret = [
293
- f"== {self.summary_title}\n{raw}"
326
+ ArticleSubsection.from_typst_code(self.summary_title, raw)
294
327
  for raw in (
295
328
  await self.aask(
296
329
  TEMPLATE_MANAGER.render_template(
297
330
  CONFIG.templates.chap_summary_template,
298
331
  [
299
332
  {
300
- "chapter": a.to_typst_code(),
301
- "title": a.title,
302
- "language": a.language,
333
+ "chapter": c.to_typst_code(),
334
+ "title": c.title,
335
+ "language": c.language,
303
336
  "summary_word_count": self.summary_word_count,
304
337
  "paragraph_count": self.paragraph_count,
305
338
  }
306
- for a in article.chapters
339
+ for c in chaps
307
340
  ],
308
341
  )
309
342
  )
310
343
  )
311
344
  ]
312
345
 
313
- if (to := (self.write_to or write_to)) is not None:
314
- dump_text(
315
- to,
316
- "\n\n\n".join(f"//{a.title}\n\n{s}" for a, s in zip(article.chapters, ret, strict=True)),
346
+ for c, n in zip(chaps, ret, strict=True):
347
+ c: ArticleChapter
348
+ n: ArticleSubsection
349
+ if c.sections[-1].title == self.summary_title:
350
+ logger.debug(f"Removing old summary `{self.summary_title}` at {c.title}")
351
+ c.sections.pop()
352
+
353
+ c.sections[-1].subsections.append(n)
354
+
355
+ article.update_article_file(article_path)
356
+
357
+ dump_text(
358
+ article_path, safe_text_read(article_path).replace(f"=== {self.summary_title}", f"== {self.summary_title}")
359
+ )
360
+ return article
361
+
362
+
363
+ class WriteResearchContentSummary(Action, LLMUsage):
364
+ """Write the research content summary."""
365
+
366
+ ctx_override: ClassVar[bool] = True
367
+ summary_word_count: int = 160
368
+ """The number of words to use in the research content summary."""
369
+
370
+ output_key: str = "summarized_article"
371
+ """The key under which the summarized article will be stored in the output."""
372
+
373
+ summary_title: str = "Research Content"
374
+ """The title to be used for the generated research content summary section."""
375
+
376
+ paragraph_count: int = 1
377
+ """The number of paragraphs to generate in the research content summary."""
378
+
379
+ async def _execute(self, article_path: Path, **cxt) -> Article:
380
+ article = Article.from_article_file(article_path, article_path.stem)
381
+ if not article.chapters:
382
+ raise ValueError("No chapters found in the article.")
383
+ chap_1 = article.chapters[0]
384
+ if not chap_1.sections:
385
+ raise ValueError("No sections found in the first chapter of the article.")
386
+
387
+ outline = article.extrac_outline()
388
+ suma: str = await self.aask(
389
+ TEMPLATE_MANAGER.render_template(
390
+ CONFIG.templates.research_content_summary_template,
391
+ {
392
+ "title": outline.title,
393
+ "outline": outline.to_typst_code(),
394
+ "language": detect_language(self.summary_title),
395
+ "summary_word_count": self.summary_word_count,
396
+ "paragraph_count": self.paragraph_count,
397
+ },
317
398
  )
399
+ )
400
+ logger.success(
401
+ f"{self.summary_title}|Wordcount: {word_count(suma)}|Expected: {self.summary_word_count}\n{suma}"
402
+ )
403
+
404
+ if chap_1.sections[-1].title == self.summary_title:
405
+ # remove old
406
+ logger.debug(f"Removing old summary `{self.summary_title}`")
407
+ chap_1.sections.pop()
318
408
 
319
- return ret
409
+ chap_1.sections[-1].subsections.append(ArticleSubsection.from_typst_code(self.summary_title, suma))
410
+
411
+ article.update_article_file(article_path)
412
+ dump_text(
413
+ article_path, safe_text_read(article_path).replace(f"=== {self.summary_title}", f"== {self.summary_title}")
414
+ )
415
+ return article
@@ -3,11 +3,10 @@
3
3
  from pathlib import Path
4
4
  from typing import Any, Iterable, List, Mapping, Optional, Self, Sequence, Type
5
5
 
6
- from fabricatio.capabilities.persist import PersistentAble
7
6
  from fabricatio.fs import dump_text
8
7
  from fabricatio.journal import logger
9
8
  from fabricatio.models.action import Action
10
- from fabricatio.models.generic import FinalizedDumpAble, FromMapping, FromSequence
9
+ from fabricatio.models.generic import FinalizedDumpAble, FromMapping, FromSequence, PersistentAble
11
10
  from fabricatio.models.task import Task
12
11
  from fabricatio.models.usages import LLMUsage
13
12
  from fabricatio.rust import TEMPLATE_MANAGER
@@ -21,11 +20,11 @@ class DumpFinalizedOutput(Action, LLMUsage):
21
20
  dump_path: Optional[str] = None
22
21
 
23
22
  async def _execute(
24
- self,
25
- to_dump: FinalizedDumpAble,
26
- task_input: Optional[Task] = None,
27
- dump_path: Optional[str | Path] = None,
28
- **_,
23
+ self,
24
+ to_dump: FinalizedDumpAble,
25
+ task_input: Optional[Task] = None,
26
+ dump_path: Optional[str | Path] = None,
27
+ **_,
29
28
  ) -> str:
30
29
  dump_path = Path(
31
30
  dump_path
@@ -52,11 +51,11 @@ class RenderedDump(Action, LLMUsage):
52
51
  """The template name to render the data."""
53
52
 
54
53
  async def _execute(
55
- self,
56
- to_dump: FinalizedDumpAble,
57
- task_input: Optional[Task] = None,
58
- dump_path: Optional[str | Path] = None,
59
- **_,
54
+ self,
55
+ to_dump: FinalizedDumpAble,
56
+ task_input: Optional[Task] = None,
57
+ dump_path: Optional[str | Path] = None,
58
+ **_,
60
59
  ) -> str:
61
60
  dump_path = Path(
62
61
  dump_path
@@ -91,10 +90,10 @@ class PersistentAll(Action, LLMUsage):
91
90
  """Whether to remove the existing dir before dumping."""
92
91
 
93
92
  async def _execute(
94
- self,
95
- task_input: Optional[Task] = None,
96
- persist_dir: Optional[str | Path] = None,
97
- **cxt,
93
+ self,
94
+ task_input: Optional[Task] = None,
95
+ persist_dir: Optional[str | Path] = None,
96
+ **cxt,
98
97
  ) -> int:
99
98
  persist_dir = Path(
100
99
  persist_dir
@@ -124,7 +123,7 @@ class PersistentAll(Action, LLMUsage):
124
123
  v.persist(final_dir)
125
124
  count += 1
126
125
  if isinstance(v, Iterable) and any(
127
- persistent_ables := (pers for pers in v if isinstance(pers, PersistentAble))
126
+ persistent_ables := (pers for pers in v if isinstance(pers, PersistentAble))
128
127
  ):
129
128
  logger.info(f"Persisting collection {k} to {final_dir}")
130
129
  final_dir.mkdir(parents=True, exist_ok=True)
@@ -174,11 +173,11 @@ class RetrieveFromLatest[T: PersistentAble](RetrieveFromPersistent[T], FromMappi
174
173
 
175
174
  @classmethod
176
175
  def from_mapping(
177
- cls,
178
- mapping: Mapping[str, str | Path],
179
- *,
180
- retrieve_cls: Type[T],
181
- **kwargs,
176
+ cls,
177
+ mapping: Mapping[str, str | Path],
178
+ *,
179
+ retrieve_cls: Type[T],
180
+ **kwargs,
182
181
  ) -> List["RetrieveFromLatest[T]"]:
183
182
  """Create a list of `RetrieveFromLatest` from the mapping."""
184
183
  return [
@@ -5,7 +5,6 @@ from enum import StrEnum
5
5
  from pathlib import Path
6
6
  from typing import ClassVar, Generator, List, Optional, Self, Tuple, Type
7
7
 
8
- from fabricatio.capabilities.persist import PersistentAble
9
8
  from fabricatio.fs import dump_text, safe_text_read
10
9
  from fabricatio.fs.readers import extract_sections
11
10
  from fabricatio.journal import logger
@@ -16,12 +15,21 @@ from fabricatio.models.generic import (
16
15
  Introspect,
17
16
  Language,
18
17
  ModelHash,
18
+ PersistentAble,
19
19
  ProposedUpdateAble,
20
20
  SketchedAble,
21
21
  Titled,
22
22
  WordCount,
23
23
  )
24
- from fabricatio.rust import extract_body, replace_thesis_body, split_out_metadata, to_metadata, word_count
24
+ from fabricatio.rust import (
25
+ detect_language,
26
+ extract_body,
27
+ replace_thesis_body,
28
+ split_out_metadata,
29
+ strip_comment,
30
+ to_metadata,
31
+ word_count,
32
+ )
25
33
  from fabricatio.utils import fallback_kwargs, ok
26
34
  from pydantic import Field
27
35
 
@@ -52,10 +60,32 @@ class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
52
60
  aims: List[str]
53
61
  """List of writing aims of the research component in academic style."""
54
62
 
63
+ _unstructured_body: str = ""
64
+ """Store the source of the unknown information."""
65
+
55
66
  @property
56
67
  def typst_metadata_comment(self) -> str:
57
68
  """Generates a comment for the metadata of the article component."""
58
- return to_metadata(self.model_dump(include={"description", "aims", "expected_word_count"}, by_alias=True))
69
+ data = self.model_dump(
70
+ include={"description", "aims", "expected_word_count"},
71
+ by_alias=True,
72
+ )
73
+ return to_metadata({k: v for k, v in data.items() if v})
74
+
75
+ @property
76
+ def unstructured_body(self) -> str:
77
+ """Returns the unstructured body of the article component."""
78
+ return self._unstructured_body
79
+
80
+ def update_unstructured_body[S: "ArticleMetaData"](self: S, body: str) -> S:
81
+ """Update the unstructured body of the article component."""
82
+ self._unstructured_body = body
83
+ return self
84
+
85
+ @property
86
+ def language(self) -> str:
87
+ """Get the language of the article component."""
88
+ return detect_language(self.title)
59
89
 
60
90
 
61
91
  class FromTypstCode(ArticleMetaData):
@@ -67,13 +97,8 @@ class FromTypstCode(ArticleMetaData):
67
97
  data, body = split_out_metadata(body)
68
98
 
69
99
  return cls(
70
- heading=title,
71
- **fallback_kwargs(
72
- data or {},
73
- elaboration="",
74
- expected_word_count=word_count(body),
75
- aims=[],
76
- ),
100
+ heading=title.strip(),
101
+ **fallback_kwargs(data or {}, elaboration="", expected_word_count=word_count(body), aims=[]),
77
102
  **kwargs,
78
103
  )
79
104
 
@@ -83,7 +108,7 @@ class ToTypstCode(ArticleMetaData):
83
108
 
84
109
  def to_typst_code(self) -> str:
85
110
  """Converts the component into a Typst code snippet for rendering."""
86
- return f"{self.title}\n{self.typst_metadata_comment}\n"
111
+ return f"{self.title}\n{self.typst_metadata_comment}\n\n{self._unstructured_body}"
87
112
 
88
113
 
89
114
  class ArticleOutlineBase(
@@ -151,12 +176,16 @@ class SectionBase[T: SubSectionBase](ArticleOutlineBase):
151
176
  @classmethod
152
177
  def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
153
178
  """Creates an Article object from the given Typst code."""
154
- return super().from_typst_code(
155
- title,
156
- body,
157
- subsections=[
158
- cls.child_type.from_typst_code(*pack) for pack in extract_sections(body, level=3, section_char="=")
159
- ],
179
+ raw = extract_sections(body, level=3, section_char="=")
180
+
181
+ return (
182
+ super()
183
+ .from_typst_code(
184
+ title,
185
+ body,
186
+ subsections=[cls.child_type.from_typst_code(*pack) for pack in raw],
187
+ )
188
+ .update_unstructured_body("" if raw else strip_comment(body))
160
189
  )
161
190
 
162
191
  def resolve_update_conflict(self, other: Self) -> str:
@@ -191,6 +220,11 @@ class SectionBase[T: SubSectionBase](ArticleOutlineBase):
191
220
  return f"Section `{self.title}` contains no subsections, expected at least one, but got 0, you can add one or more as needed."
192
221
  return ""
193
222
 
223
+ @property
224
+ def exact_word_count(self) -> int:
225
+ """Returns the exact word count of the article section outline."""
226
+ return sum(a.exact_word_count for a in self.subsections)
227
+
194
228
 
195
229
  class ChapterBase[T: SectionBase](ArticleOutlineBase):
196
230
  """Base class for article chapters."""
@@ -206,12 +240,16 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
206
240
  @classmethod
207
241
  def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
208
242
  """Creates an Article object from the given Typst code."""
209
- return super().from_typst_code(
210
- title,
211
- body,
212
- sections=[
213
- cls.child_type.from_typst_code(*pack) for pack in extract_sections(body, level=2, section_char="=")
214
- ],
243
+ raw_sec = extract_sections(body, level=2, section_char="=")
244
+
245
+ return (
246
+ super()
247
+ .from_typst_code(
248
+ title,
249
+ body,
250
+ sections=[cls.child_type.from_typst_code(*pack) for pack in raw_sec],
251
+ )
252
+ .update_unstructured_body("" if raw_sec else strip_comment(body))
215
253
  )
216
254
 
217
255
  def resolve_update_conflict(self, other: Self) -> str:
@@ -243,6 +281,15 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
243
281
  return f"Chapter `{self.title}` contains no sections, expected at least one, but got 0, you can add one or more as needed."
244
282
  return ""
245
283
 
284
+ @property
285
+ def exact_word_count(self) -> int:
286
+ """Calculates the total word count across all sections in the chapter.
287
+
288
+ Returns:
289
+ int: The cumulative word count of all sections.
290
+ """
291
+ return sum(a.exact_word_count for a in self.sections)
292
+
246
293
 
247
294
  class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, ToTypstCode, ABC):
248
295
  """Base class for article outlines."""
@@ -263,15 +310,34 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
263
310
 
264
311
  child_type: ClassVar[Type[ChapterBase]]
265
312
 
313
+ @property
314
+ def language(self) -> str:
315
+ """Get the language of the article."""
316
+ if self.title:
317
+ return super().language
318
+ return self.chapters[0].language
319
+
320
+ @property
321
+ def exact_word_count(self) -> int:
322
+ """Calculates the total word count across all chapters in the article.
323
+
324
+ Returns:
325
+ int: The cumulative word count of all chapters.
326
+ """
327
+ return sum(ch.exact_word_count for ch in self.chapters)
328
+
266
329
  @classmethod
267
330
  def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
268
331
  """Generates an article from the given Typst code."""
269
- return super().from_typst_code(
270
- title,
271
- body,
272
- chapters=[
273
- cls.child_type.from_typst_code(*pack) for pack in extract_sections(body, level=1, section_char="=")
274
- ],
332
+ raw = extract_sections(body, level=1, section_char="=")
333
+ return (
334
+ super()
335
+ .from_typst_code(
336
+ title,
337
+ body,
338
+ chapters=[cls.child_type.from_typst_code(*pack) for pack in raw],
339
+ )
340
+ .update_unstructured_body("" if raw else strip_comment(body))
275
341
  )
276
342
 
277
343
  def iter_dfs_rev(
@@ -350,7 +416,7 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
350
416
 
351
417
  def to_typst_code(self) -> str:
352
418
  """Generates the Typst code representation of the article."""
353
- return f"// #{super().to_typst_code()}\n\n" + "\n\n".join(a.to_typst_code() for a in self.chapters)
419
+ return f"// #Title: {super().to_typst_code()}\n" + "\n\n".join(a.to_typst_code() for a in self.chapters)
354
420
 
355
421
  def finalized_dump(self) -> str:
356
422
  """Generates standardized hierarchical markup for academic publishing systems.
@@ -401,11 +467,11 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
401
467
  """Set all chap, sec, subsec have same word count sum up to be `self.expected_word_count`."""
402
468
  return self.avg_chap_wordcount().avg_sec_wordcount().avg_subsec_wordcount()
403
469
 
404
- def update_article_file(self, file: str | Path) -> Self:
470
+ def update_article_file[S: "ArticleBase"](self: S, file: str | Path) -> S:
405
471
  """Update the article file."""
406
472
  file = Path(file)
407
473
  string = safe_text_read(file)
408
- if updated := replace_thesis_body(string, ARTICLE_WRAPPER, self.to_typst_code()):
474
+ if updated := replace_thesis_body(string, ARTICLE_WRAPPER, f"\n\n{self.to_typst_code()}\n\n"):
409
475
  dump_text(file, updated)
410
476
  logger.success(f"Successfully updated {file.as_posix()}.")
411
477
  else:
@@ -413,7 +479,7 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
413
479
  return self
414
480
 
415
481
  @classmethod
416
- def from_article_file[S: "ArticleBase"](cls: Type[S], file: str | Path, title: str) -> S:
482
+ def from_article_file[S: "ArticleBase"](cls: Type[S], file: str | Path, title: str = "") -> S:
417
483
  """Load article from file."""
418
484
  file = Path(file)
419
485
  string = safe_text_read(file)
@@ -2,9 +2,8 @@
2
2
 
3
3
  from typing import List
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.models.extra.rag import MilvusDataBase
7
- from fabricatio.models.generic import SketchedAble
6
+ from fabricatio.models.generic import PersistentAble, SketchedAble
8
7
  from pydantic import BaseModel
9
8
 
10
9
 
@@ -97,5 +96,3 @@ class ArticleEssence(SketchedAble, PersistentAble, MilvusDataBase):
97
96
 
98
97
  def _prepare_vectorization_inner(self) -> str:
99
98
  return self.compact()
100
-
101
-
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import ClassVar, Dict, Generator, List, Self, Tuple, Type, override
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.decorators import precheck_package
7
6
  from fabricatio.journal import logger
8
7
  from fabricatio.models.extra.article_base import (
@@ -17,7 +16,7 @@ from fabricatio.models.extra.article_outline import (
17
16
  ArticleSectionOutline,
18
17
  ArticleSubsectionOutline,
19
18
  )
20
- from fabricatio.models.generic import Described, SequencePatch, SketchedAble, WithRef, WordCount
19
+ from fabricatio.models.generic import Described, PersistentAble, SequencePatch, SketchedAble, WithRef, WordCount
21
20
  from fabricatio.rust import (
22
21
  convert_all_tex_math,
23
22
  fix_misplaced_labels,
@@ -52,7 +51,7 @@ class Paragraph(SketchedAble, WordCount, Described):
52
51
  return cls(elaboration="", aims=[], expected_word_count=word_count(content), content=content.strip())
53
52
 
54
53
  @property
55
- def exact_wordcount(self) -> int:
54
+ def exact_word_count(self) -> int:
56
55
  """Calculates the exact word count of the content."""
57
56
  return word_count(self.content)
58
57
 
@@ -70,6 +69,11 @@ class ArticleSubsection(SubSectionBase):
70
69
  _max_word_count_deviation: float = 0.3
71
70
  """Maximum allowed deviation from the expected word count, as a percentage."""
72
71
 
72
+ @property
73
+ def exact_word_count(self) -> int:
74
+ """Calculates the exact word count of all paragraphs in the subsection."""
75
+ return sum(a.exact_word_count for a in self.paragraphs)
76
+
73
77
  @property
74
78
  def word_count(self) -> int:
75
79
  """Calculates the total word count of all paragraphs in the subsection."""
@@ -273,9 +277,9 @@ class Article(
273
277
  err = []
274
278
  for chap, sec, subsec in self.iter_subsections():
275
279
  for i, p in enumerate(subsec.paragraphs):
276
- if p.exact_wordcount <= threshold:
280
+ if p.exact_word_count <= threshold:
277
281
  err.append(
278
- f"{chap.title}->{sec.title}->{subsec.title}-> Paragraph [{i}] is too short, {p.exact_wordcount} words."
282
+ f"{chap.title}->{sec.title}->{subsec.title}-> Paragraph [{i}] is too short, {p.exact_word_count} words."
279
283
  )
280
284
 
281
285
  return "\n".join(err)
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import ClassVar, Dict, Type
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.models.extra.article_base import (
7
6
  ArticleBase,
8
7
  ChapterBase,
@@ -10,7 +9,7 @@ from fabricatio.models.extra.article_base import (
10
9
  SubSectionBase,
11
10
  )
12
11
  from fabricatio.models.extra.article_proposal import ArticleProposal
13
- from fabricatio.models.generic import WithRef
12
+ from fabricatio.models.generic import PersistentAble, WithRef
14
13
 
15
14
 
16
15
  class ArticleSubsectionOutline(SubSectionBase):
@@ -2,11 +2,11 @@
2
2
 
3
3
  from typing import Dict, List
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.models.generic import (
7
6
  AsPrompt,
8
7
  Described,
9
8
  Language,
9
+ PersistentAble,
10
10
  SketchedAble,
11
11
  Titled,
12
12
  WithRef,
@@ -10,8 +10,7 @@ complex rule management systems.
10
10
 
11
11
  from typing import List, Self, Tuple, Unpack
12
12
 
13
- from fabricatio.capabilities.persist import PersistentAble
14
- from fabricatio.models.generic import Language, SketchedAble, WithBriefing
13
+ from fabricatio.models.generic import Language, PersistentAble, SketchedAble, WithBriefing
15
14
  from more_itertools import flatten
16
15
 
17
16
 
@@ -1,6 +1,7 @@
1
1
  """This module defines generic classes for models in the Fabricatio library, providing a foundation for various model functionalities."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
+ from datetime import datetime
4
5
  from pathlib import Path
5
6
  from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Self, Sequence, Type, Union, final, overload
6
7
 
@@ -114,7 +115,7 @@ class WordCount(Base, ABC):
114
115
  @property
115
116
  def exact_word_count(self) -> int:
116
117
  """Get the exact word count of this research component."""
117
- raise NotImplementedError(f"`expected_word_count` is not implemented for {self.__class__.__name__}")
118
+ raise NotImplementedError(f"`exact_word_count` is not implemented for {self.__class__.__name__}")
118
119
 
119
120
 
120
121
  class FromMapping:
@@ -810,3 +811,94 @@ class SequencePatch[T](ProposedUpdateAble, ABC):
810
811
  Self: A new instance with an empty list of tweaks.
811
812
  """
812
813
  return cls(tweaked=[])
814
+
815
+
816
+ class PersistentAble(Base, ABC):
817
+ """Class providing file persistence capabilities.
818
+
819
+ Enables saving model instances to disk with timestamped filenames and loading from persisted files.
820
+ Implements basic versioning through filename hashing and timestamping.
821
+ """
822
+
823
+ def persist(self, path: str | Path) -> Self:
824
+ """Save model instance to disk with versioned filename.
825
+
826
+ Args:
827
+ path (str | Path): Target directory or file path. If directory, filename is auto-generated.
828
+
829
+ Returns:
830
+ Self: Current instance for method chaining
831
+
832
+ Notes:
833
+ - Filename format: <ClassName>_<YYYYMMDD_HHMMSS>_<6-char_hash>.json
834
+ - Hash generated from JSON content ensures uniqueness
835
+ """
836
+ p = Path(path)
837
+ out = self.model_dump_json(indent=1, by_alias=True)
838
+
839
+ # Generate a timestamp in the format YYYYMMDD_HHMMSS
840
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
841
+
842
+ # Generate the hash
843
+ file_hash = blake3_hash(out.encode())[:6]
844
+
845
+ # Construct the file name with timestamp and hash
846
+ file_name = f"{self.__class__.__name__}_{timestamp}_{file_hash}.json"
847
+
848
+ if p.is_dir():
849
+ p.joinpath(file_name).write_text(out, encoding="utf-8")
850
+ else:
851
+ p.mkdir(exist_ok=True, parents=True)
852
+ p.write_text(out, encoding="utf-8")
853
+
854
+ logger.info(f"Persisted `{self.__class__.__name__}` to {p.as_posix()}")
855
+ return self
856
+
857
+ @classmethod
858
+ def from_latest_persistent(cls, dir_path: str | Path) -> Optional[Self]:
859
+ """Load most recent persisted instance from directory.
860
+
861
+ Args:
862
+ dir_path (str | Path): Directory containing persisted files
863
+
864
+ Returns:
865
+ Self: Most recently modified instance
866
+
867
+ Raises:
868
+ NotADirectoryError: If path is not a valid directory
869
+ FileNotFoundError: If no matching files found
870
+ """
871
+ dir_path = Path(dir_path)
872
+ if not dir_path.is_dir():
873
+ return None
874
+
875
+ pattern = f"{cls.__name__}_*.json"
876
+ files = list(dir_path.glob(pattern))
877
+
878
+ if not files:
879
+ return None
880
+
881
+ def _get_timestamp(file_path: Path) -> datetime:
882
+ stem = file_path.stem
883
+ parts = stem.split("_")
884
+ return datetime.strptime(f"{parts[1]}_{parts[2]}", "%Y%m%d_%H%M%S")
885
+
886
+ files.sort(key=lambda f: _get_timestamp(f), reverse=True)
887
+
888
+ return cls.from_persistent(files.pop(0))
889
+
890
+ @classmethod
891
+ def from_persistent(cls, path: str | Path) -> Self:
892
+ """Load an instance from a specific persisted file.
893
+
894
+ Args:
895
+ path (str | Path): Path to the JSON file.
896
+
897
+ Returns:
898
+ Self: The loaded instance from the file.
899
+
900
+ Raises:
901
+ FileNotFoundError: If the specified file does not exist.
902
+ ValueError: If the file content is invalid for the model.
903
+ """
904
+ return cls.model_validate_json(safe_text_read(path))
fabricatio/models/role.py CHANGED
@@ -30,6 +30,8 @@ class Role(WithBriefing):
30
30
 
31
31
  registry: Dict[Event, WorkFlow] = Field(default_factory=dict)
32
32
  """The registry of events and workflows."""
33
+ dispatch_on_init: bool = True
34
+ """Whether to dispatch registered workflows on initialization."""
33
35
 
34
36
  def model_post_init(self, __context: Any) -> None:
35
37
  """Initialize the role by resolving configurations and registering workflows.
@@ -39,9 +41,20 @@ class Role(WithBriefing):
39
41
  """
40
42
  self.name = self.name or self.__class__.__name__
41
43
 
42
- self.resolve_configuration().register_workflows()
44
+ if self.dispatch_on_init:
45
+ self.resolve_configuration().dispatch()
43
46
 
44
- def register_workflows(self) -> Self:
47
+ def register_workflow(self, event: Event, workflow: WorkFlow) -> Self:
48
+ """Register a workflow to the role's registry."""
49
+ if event in self.registry:
50
+ logger.warning(
51
+ f"Event `{event.collapse()}` is already registered with workflow "
52
+ f"`{self.registry[event].name}`. It will be overwritten by `{workflow.name}`."
53
+ )
54
+ self.registry[event] = workflow
55
+ return self
56
+
57
+ def dispatch(self) -> Self:
45
58
  """Register each workflow in the registry to its corresponding event in the event bus.
46
59
 
47
60
  Returns:
@@ -63,8 +76,8 @@ class Role(WithBriefing):
63
76
  """
64
77
  for workflow in self.registry.values():
65
78
  logger.debug(f"Resolving config for workflow: `{workflow.name}`")
66
- self._configure_scoped_config(workflow)
67
- self._configure_toolbox_usage(workflow)
79
+ self._configure_scoped_config(workflow)._configure_toolbox_usage(workflow)
80
+
68
81
  workflow.inject_personality(self.briefing)
69
82
  return self
70
83
 
@@ -74,26 +87,70 @@ class Role(WithBriefing):
74
87
  has_capability: Callable[[Type], bool],
75
88
  config_method_name: str,
76
89
  capability_description: str,
77
- ) -> None:
78
- """Propagates configuration to workflow and its actions if they have a given capability."""
79
- if not has_capability(self.__class__):
80
- return
90
+ ) -> Self:
91
+ """Propagates configuration from the Role to a Workflow and its Actions.
92
+
93
+ This method checks if the Role, Workflow, or its Actions possess a specific
94
+ capability (e.g., being a ScopedConfig or ToolBoxUsage). If they do,
95
+ a specified configuration method is called on them to apply or inherit
96
+ settings.
97
+
98
+ The configuration flows hierarchically:
99
+ 1. If the Role has the capability, it's the initial source.
100
+ 2. If the Workflow also has the capability, it can inherit from the Role
101
+ and then becomes the source for its Actions.
102
+ 3. Actions with the capability inherit from the determined source (either
103
+ Workflow or Role).
81
104
 
82
- config_source_for_actions = self
105
+ Args:
106
+ workflow: The WorkFlow instance to configure.
107
+ has_capability: A callable that takes a Type and returns True if
108
+ the type possesses the specific capability, False otherwise.
109
+ config_method_name: The name of the method to call on an object
110
+ (Role, Workflow, Action) to apply the configuration.
111
+ For example, "fallback_to" or "supply_tools_from".
112
+ capability_description: A string describing the capability, used for
113
+ logging purposes (e.g., "scoped config", "toolbox usage").
114
+ """
115
+ # This variable will hold the object from which Actions should inherit their configuration.
116
+ # It could be the Role itself or the Workflow, depending on their capabilities.
117
+ config_source_for_actions = None
118
+
119
+ # Check if the Role itself has the capability.
120
+ if has_capability(self.__class__):
121
+ # If the Role has the capability, it becomes the initial source for configuration.
122
+ config_source_for_actions = self
123
+
124
+ # Check if the Workflow has the capability.
83
125
  if has_capability(workflow.__class__):
84
126
  logger.debug(
85
127
  f"Configuring {capability_description} inherited from `{self.name}` for workflow: `{workflow.name}`"
86
128
  )
87
- getattr(workflow, config_method_name)(self)
129
+ # If the Role was already identified as a config source,
130
+ # the Workflow an inherit its configuration directly from the Role.
131
+ if config_source_for_actions is not None:
132
+ # Call the specified configuration method on the workflow, passing the Role (self) as the source.
133
+ getattr(workflow, config_method_name)(config_source_for_actions)
134
+
135
+ # After potentially inheriting from the Role, the Workflow itself becomes
136
+ # the source of configuration for its Actions.
88
137
  config_source_for_actions = workflow
89
138
 
90
- for action in (act for act in workflow.iter_actions() if has_capability(act.__class__)):
91
- getattr(action, config_method_name)(config_source_for_actions)
139
+ # If a configuration source (either Role or Workflow) has been established:
140
+ if config_source_for_actions is not None:
141
+ # Iterate over all actions within the workflow.
142
+ # Filter for actions that possess the specified capability.
143
+ for action in (act for act in workflow.iter_actions() if has_capability(act.__class__)):
144
+ # Call the specified configuration method on the action,
145
+ # passing the determined config_source_for_actions.
146
+ getattr(action, config_method_name)(config_source_for_actions)
147
+
148
+ return self
92
149
 
93
- def _configure_scoped_config(self, workflow: WorkFlow) -> None:
150
+ def _configure_scoped_config(self, workflow: WorkFlow) -> Self:
94
151
  """Configure scoped configuration for workflow and its actions."""
95
- self._propagate_config(workflow, is_scoped_config, "fallback_to", "scoped config")
152
+ return self._propagate_config(workflow, is_scoped_config, "fallback_to", "scoped config")
96
153
 
97
- def _configure_toolbox_usage(self, workflow: WorkFlow) -> None:
154
+ def _configure_toolbox_usage(self, workflow: WorkFlow) -> Self:
98
155
  """Configure toolbox usage for workflow and its actions."""
99
- self._propagate_config(workflow, is_toolbox_usage, "supply_tools_from", "toolbox usage")
156
+ return self._propagate_config(workflow, is_toolbox_usage, "supply_tools_from", "toolbox usage")
fabricatio/rust.pyi CHANGED
@@ -339,6 +339,16 @@ def uncomment(string: str) -> str:
339
339
  The string with comments (lines starting with '// ' or '//') removed.
340
340
  """
341
341
 
342
+ def strip_comment(string: str) -> str:
343
+ """Remove leading and trailing comment lines from a multi-line string.
344
+
345
+ Args:
346
+ string: Input string that may have comment lines at start and/or end
347
+
348
+ Returns:
349
+ str: A new string with leading and trailing comment lines removed
350
+ """
351
+
342
352
  def split_out_metadata(string: str) -> Tuple[Optional[JsonValue], str]:
343
353
  """Split out metadata from a string.
344
354
 
@@ -494,6 +504,9 @@ class TemplateManagerConfig:
494
504
  class TemplateConfig:
495
505
  """Template configuration structure."""
496
506
 
507
+ research_content_summary_template: str
508
+ """The name of the research content summary template which will be used to generate a summary of research content."""
509
+
497
510
  create_json_obj_template: str
498
511
  """The name of the create json object template which will be used to create a json object."""
499
512
 
Binary file
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fabricatio
3
- Version: 0.3.14.dev8
3
+ Version: 0.3.15.dev5
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Programming Language :: Python :: 3.13
7
8
  Classifier: Programming Language :: Python :: Implementation :: CPython
8
9
  Classifier: Framework :: AsyncIO
9
10
  Classifier: Framework :: Pydantic :: 2
@@ -185,4 +186,5 @@ Special thanks to the contributors and maintainers of:
185
186
  - [PyO3](https://github.com/PyO3/pyo3)
186
187
  - [Maturin](https://github.com/PyO3/maturin)
187
188
  - [Handlebars.rs](https://github.com/sunng87/handlebars-rust)
189
+ - [LiteLLM](https://github.com/BerriAI/litellm)
188
190
 
@@ -1,14 +1,14 @@
1
- fabricatio-0.3.14.dev8.data/scripts/tdown,sha256=op_mffGEO0MwNpPoDJVgVwtrHvVHizmgffZG2vc7aXE,4722624
2
- fabricatio-0.3.14.dev8.data/scripts/ttm,sha256=497Iiu2C1OODIRb-pqOJ9WZdSL7wlHqfTDoPNkINJX8,3918760
3
- fabricatio-0.3.14.dev8.dist-info/METADATA,sha256=pPky7D10BatBBFi46gZ4JXl6uVfgtnK_-BS5ydzBMlU,4969
4
- fabricatio-0.3.14.dev8.dist-info/WHEEL,sha256=OZYXF4emuP5o7uCHyen8StXv3k74AF7eDhQe1rxgOqQ,108
5
- fabricatio-0.3.14.dev8.dist-info/licenses/LICENSE,sha256=yDZaTLnOi03bi3Dk6f5IjhLUc5old2yOsihHWU0z-i0,1067
1
+ fabricatio-0.3.15.dev5.data/scripts/tdown,sha256=kOUNC-Fj426KqvkK1MaOhkmJLpFzpmUadLJiOxMZz1g,4731456
2
+ fabricatio-0.3.15.dev5.data/scripts/ttm,sha256=1NDJgExOy6aW2zbrXLxqVnEyTOLyduDy8_HD3a7oLro,3937688
3
+ fabricatio-0.3.15.dev5.dist-info/METADATA,sha256=lBlJJr7OtXs-u0rItYYYIoOJ4GsLLHrAbqvrQdhchhc,5068
4
+ fabricatio-0.3.15.dev5.dist-info/WHEEL,sha256=OZYXF4emuP5o7uCHyen8StXv3k74AF7eDhQe1rxgOqQ,108
5
+ fabricatio-0.3.15.dev5.dist-info/licenses/LICENSE,sha256=yDZaTLnOi03bi3Dk6f5IjhLUc5old2yOsihHWU0z-i0,1067
6
6
  fabricatio/__init__.py,sha256=pSLe6QL4zQGaZXfhF9KW4fa1D8chqCQm_7yInCP6Kt8,732
7
7
  fabricatio/actions/__init__.py,sha256=ZMa1LeM5BNeqp-J-D32W-f5bD53-kdXGyt0zuueJofM,47
8
- fabricatio/actions/article.py,sha256=8ea9QZk7m21j5fw6_CO_znZtik9_o71JmX77Po5gyS4,12188
8
+ fabricatio/actions/article.py,sha256=DtArcKFQM1jp4f3OKPng9ciqY7TmNIUjrjCaZO3OCg8,16695
9
9
  fabricatio/actions/article_rag.py,sha256=2lQogjV_1iZkbYI4C9kGGpQH9TBeIDaQCkyi7ueqFus,17582
10
10
  fabricatio/actions/fs.py,sha256=nlTmk-tYDW158nz_fzlsNfuYJwj7j4BHn_MFY5hxdqs,934
11
- fabricatio/actions/output.py,sha256=3VRwDcvimBPrf4ypxbhJd_ScJ_JYiC0ucr6vGOqs9Fc,9687
11
+ fabricatio/actions/output.py,sha256=cLJqeNOhW2vzZB4Tgzh2t09v0flH_eZhDWTxOXUqI7A,9724
12
12
  fabricatio/actions/rag.py,sha256=GuRU6VJzIxo3V8dvGWNQ0uQbu6nF0g_qgVuC8NPRx2Y,3487
13
13
  fabricatio/actions/rules.py,sha256=07ILsiwR250AUcKLPHTUPpWD_mPhPCfWKSkEAKcPv3A,3557
14
14
  fabricatio/capabilities/__init__.py,sha256=skaJ43CqAQaZMH-mCRzF4Fps3x99P2SwJ8vSM9pInX8,56
@@ -18,7 +18,6 @@ fabricatio/capabilities/censor.py,sha256=m90gGDAkEkkxkUKcZNkyhYsRwAxkcDut_-gZEBK
18
18
  fabricatio/capabilities/check.py,sha256=eiZZaiX78k-Zt7-Ik43Pn5visXHeOJLk8yLWgtqln40,8379
19
19
  fabricatio/capabilities/correct.py,sha256=z7KiMK1KykGXNdLVA0sB28x63LsQ6Hd4wbtYd0bkEKE,10175
20
20
  fabricatio/capabilities/extract.py,sha256=eLQagkRnHVLZ64yPBtLVcPELO7ubJlN3fbwoaNMWT70,2449
21
- fabricatio/capabilities/persist.py,sha256=GAbj93lYLnGVPu74H_ImrINGWNAglIDH9aGSLJKMLkw,3318
22
21
  fabricatio/capabilities/propose.py,sha256=KqeXaUURJ6O-Ve0ijZYg88rgQYCZEFbuWoqIepI-nQ8,1965
23
22
  fabricatio/capabilities/rag.py,sha256=VSk4BKN8Clwi28-8bz-roqHRln9vu6mGnozr6snaPeY,10930
24
23
  fabricatio/capabilities/rating.py,sha256=FSIh3h0E7G1OkBKAkY83VA4w0G6OZ2bXq27b40WRsL8,17411
@@ -35,25 +34,25 @@ fabricatio/models/adv_kwargs_types.py,sha256=nmj1D0GVosZxKcdiw-B5vJB04Whr5zh30ZB
35
34
  fabricatio/models/extra/__init__.py,sha256=0R9eZsCNu6OV-Xtf15H7FrqhfHTFBFf3fBrcd7ChsJ0,53
36
35
  fabricatio/models/extra/advanced_judge.py,sha256=CKPP4Lseb_Ey8Y7i2V9HJfB-mZgCknFdqq7Zo41o6s4,1060
37
36
  fabricatio/models/extra/aricle_rag.py,sha256=KaryVIaMZRV6vpUYwkHDe09tgOihVWGPb1mGs1GXKSw,11723
38
- fabricatio/models/extra/article_base.py,sha256=GmtgMa--sHSP_H4rJ-6fUxj6tYYfd-m8fOOBDXVyMZQ,16357
39
- fabricatio/models/extra/article_essence.py,sha256=lAkfGj4Jqiy3dSmtloVVr2krej76TV1Ky-2Fr6pNE_Q,2692
40
- fabricatio/models/extra/article_main.py,sha256=Lg0cT4SF-0Y9in5LfYU1l9Rq_OnMYH3cCqqEByEnOhE,10817
41
- fabricatio/models/extra/article_outline.py,sha256=71mgx66KRiXBtdYId4WNkAYp9tJ7OhUqmQyOEe7IRxI,1627
42
- fabricatio/models/extra/article_proposal.py,sha256=7OgcsS9ujjSi_06Z1ln4SCDQgrS4xPGrtgc2dv8EzGo,1857
37
+ fabricatio/models/extra/article_base.py,sha256=SD1tQdr20Sah4M8yM9r2KSnTRrx9lkM_3Ya1quiolrc,18394
38
+ fabricatio/models/extra/article_essence.py,sha256=OGci-Z4NRvaJdMuS2YUpY425rSozdush6z2LlBoWOpA,2647
39
+ fabricatio/models/extra/article_main.py,sha256=fxIdHEtlPDHCkaZX5azp18V2shloN_h10Gn54ciRUXk,10978
40
+ fabricatio/models/extra/article_outline.py,sha256=K3Ajb86JQSsjo61briVCkIJkqRwvJ46uNU94NCrW-cY,1584
41
+ fabricatio/models/extra/article_proposal.py,sha256=4G2qLkMxtK54G1ANgPW0G3w4Pahxgk2lhGPU5KMxuzw,1818
43
42
  fabricatio/models/extra/patches.py,sha256=_ghmnlvTZQq7UJyaH77mTZE9abjvxRJ2mgWHUbezUls,977
44
43
  fabricatio/models/extra/problem.py,sha256=1Sd8hsThQK6pXMXhErRhP1ft58z4PvqeB8AV8VcXiaI,7051
45
44
  fabricatio/models/extra/rag.py,sha256=fwyEXOECQNe8LPUKGAxEcp9vp7o5356rna-TzGpkvnE,3869
46
- fabricatio/models/extra/rule.py,sha256=TYtA_aSgunw8wRS3BfdNqBZbbdeS-VXLbVCJhz85Suk,2617
47
- fabricatio/models/generic.py,sha256=dGap-ckYy7ZX_lXDNxv4d3yM45vdoLDYW4cl49BbCAY,27061
45
+ fabricatio/models/extra/rule.py,sha256=b756_XmWeDoJ1qOFEGy6ZfP8O7rBjOZs4XvfZvWKXXI,2574
46
+ fabricatio/models/generic.py,sha256=GTJV5uToM0OFe19B-UgCmM6OFQ7lMg93O7WIcdp_3IA,30092
48
47
  fabricatio/models/kwargs_types.py,sha256=VrzAJaOSlQ-xN5NIIi3k4KpIY0c9beuxcuUnF-mkEEk,3282
49
- fabricatio/models/role.py,sha256=KxiP_hsIP85QtJhOQL_UH0lKul87hqRcd49IdWr05qQ,4154
48
+ fabricatio/models/role.py,sha256=n1vOr6HZH-7-rSYarnJdDKsR1OmGSMXkCmXudAHeOqs,7337
50
49
  fabricatio/models/task.py,sha256=CdR1Zbf-lZN0jODj9iriTn1X2DxLxjXlvZgy3kEd6lI,10723
51
50
  fabricatio/models/tool.py,sha256=jYdN6FWEz6pE-vEh3H78VHDPpSttUQE79nfXOD4FE6U,12091
52
51
  fabricatio/models/usages.py,sha256=bpM-a9i-WpSOh-XL3LiYTa3AxQUd_ckn44lh-uuKM6M,32250
53
52
  fabricatio/parser.py,sha256=3vT5u5SGpzDH4WLJdMwK5CP8RqO4g1MyQUYpiDKDoEo,4528
54
53
  fabricatio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- fabricatio/rust.cpython-313-x86_64-linux-gnu.so,sha256=xzXBPe9ePywwC0nHRFo7mouBTKUerBYu1YotXCT3NJU,7904464
56
- fabricatio/rust.pyi,sha256=czqdl3jgSjmF05T_mUK6wwzpLaPEXQIFGHckRmuYspA,25026
54
+ fabricatio/rust.cpython-313-x86_64-linux-gnu.so,sha256=liZlbr9MJ00X9xQONvi6yGBz-Sy3ZA7ruUkZ4U5LXe4,7921600
55
+ fabricatio/rust.pyi,sha256=D-YxXyjvX6d1Y5pkjcvv-EjQvxpkeA7qgcTaaHOtJpY,25491
57
56
  fabricatio/toolboxes/__init__.py,sha256=dYm_Gd8XolSU_h4wnkA09dlaLDK146eeFz0CUgPZ8_c,380
58
57
  fabricatio/toolboxes/arithmetic.py,sha256=sSTPkKI6-mb278DwQKFO9jKyzc9kCx45xNH7V6bGBpE,1307
59
58
  fabricatio/toolboxes/fs.py,sha256=OQMdeokYxSNVrCZJAweJ0cYiK4k2QuEiNdIbS5IHIV8,705
@@ -61,4 +60,4 @@ fabricatio/utils.py,sha256=qvl4R8ThuNIIoBJuR1DGEuWYZ7jRFT_8SRx4I_FA8pU,5298
61
60
  fabricatio/workflows/__init__.py,sha256=Lq9pFo2cudwFCrQUUNgSTr1CoU0J1Nw-HNEQN7cHLp8,50
62
61
  fabricatio/workflows/articles.py,sha256=ZDV5nqUKRo1GOuuKWeSV7ZI32FYZU7WiTrD4YDuCeEo,945
63
62
  fabricatio/workflows/rag.py,sha256=uOZXprD479fUhLA6sYvEM8RWcVcUZXXtP0xRbTMPdHE,509
64
- fabricatio-0.3.14.dev8.dist-info/RECORD,,
63
+ fabricatio-0.3.15.dev5.dist-info/RECORD,,
@@ -1,103 +0,0 @@
1
- """Persistence capabilities for model instances."""
2
-
3
- from abc import ABC
4
- from datetime import datetime
5
- from pathlib import Path
6
- from typing import Optional, Self
7
-
8
- from loguru import logger
9
-
10
- from fabricatio.fs import safe_text_read
11
- from fabricatio.models.generic import Base
12
- from fabricatio.rust import blake3_hash
13
-
14
-
15
- class PersistentAble(Base, ABC):
16
- """Class providing file persistence capabilities.
17
-
18
- Enables saving model instances to disk with timestamped filenames and loading from persisted files.
19
- Implements basic versioning through filename hashing and timestamping.
20
- """
21
-
22
- def persist(self, path: str | Path) -> Self:
23
- """Save model instance to disk with versioned filename.
24
-
25
- Args:
26
- path (str | Path): Target directory or file path. If directory, filename is auto-generated.
27
-
28
- Returns:
29
- Self: Current instance for method chaining
30
-
31
- Notes:
32
- - Filename format: <ClassName>_<YYYYMMDD_HHMMSS>_<6-char_hash>.json
33
- - Hash generated from JSON content ensures uniqueness
34
- """
35
- p = Path(path)
36
- out = self.model_dump_json(indent=1, by_alias=True)
37
-
38
- # Generate a timestamp in the format YYYYMMDD_HHMMSS
39
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
40
-
41
- # Generate the hash
42
- file_hash = blake3_hash(out.encode())[:6]
43
-
44
- # Construct the file name with timestamp and hash
45
- file_name = f"{self.__class__.__name__}_{timestamp}_{file_hash}.json"
46
-
47
- if p.is_dir():
48
- p.joinpath(file_name).write_text(out, encoding="utf-8")
49
- else:
50
- p.mkdir(exist_ok=True, parents=True)
51
- p.write_text(out, encoding="utf-8")
52
-
53
- logger.info(f"Persisted `{self.__class__.__name__}` to {p.as_posix()}")
54
- return self
55
-
56
- @classmethod
57
- def from_latest_persistent(cls, dir_path: str | Path) -> Optional[Self]:
58
- """Load most recent persisted instance from directory.
59
-
60
- Args:
61
- dir_path (str | Path): Directory containing persisted files
62
-
63
- Returns:
64
- Self: Most recently modified instance
65
-
66
- Raises:
67
- NotADirectoryError: If path is not a valid directory
68
- FileNotFoundError: If no matching files found
69
- """
70
- dir_path = Path(dir_path)
71
- if not dir_path.is_dir():
72
- return None
73
-
74
- pattern = f"{cls.__name__}_*.json"
75
- files = list(dir_path.glob(pattern))
76
-
77
- if not files:
78
- return None
79
-
80
- def _get_timestamp(file_path: Path) -> datetime:
81
- stem = file_path.stem
82
- parts = stem.split("_")
83
- return datetime.strptime(f"{parts[1]}_{parts[2]}", "%Y%m%d_%H%M%S")
84
-
85
- files.sort(key=lambda f: _get_timestamp(f), reverse=True)
86
-
87
- return cls.from_persistent(files.pop(0))
88
-
89
- @classmethod
90
- def from_persistent(cls, path: str | Path) -> Self:
91
- """Load an instance from a specific persisted file.
92
-
93
- Args:
94
- path (str | Path): Path to the JSON file.
95
-
96
- Returns:
97
- Self: The loaded instance from the file.
98
-
99
- Raises:
100
- FileNotFoundError: If the specified file does not exist.
101
- ValueError: If the file content is invalid for the model.
102
- """
103
- return cls.model_validate_json(safe_text_read(path))
Binary file
Binary file