fabricatio 0.2.9.dev2__cp312-cp312-win_amd64.whl → 0.2.9.dev3__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,5 +1,6 @@
1
1
  """A module containing the Correct capability for reviewing, validating, and improving objects."""
2
2
 
3
+ from asyncio import gather
3
4
  from typing import Optional, Type, Unpack, cast
4
5
 
5
6
  from fabricatio.capabilities.propose import Propose
@@ -14,7 +15,7 @@ from fabricatio.models.kwargs_types import (
14
15
  ValidateKwargs,
15
16
  )
16
17
  from fabricatio.rust_instances import TEMPLATE_MANAGER
17
- from fabricatio.utils import ok, override_kwargs
18
+ from fabricatio.utils import fallback_kwargs, ok, override_kwargs
18
19
 
19
20
 
20
21
  class Correct(Rating, Propose):
@@ -33,8 +34,9 @@ class Correct(Rating, Propose):
33
34
  ProblemSolutions: The problem solutions with the best solution selected.
34
35
  """
35
36
  if (leng := len(problem_solutions.solutions)) == 0:
36
- logger.error(f"No solutions found in ProblemSolutions, Skip: {problem_solutions.problem}")
37
+ logger.error(f"No solutions found in ProblemSolutions, Skip: `{problem_solutions.problem.name}`")
37
38
  if leng > 1:
39
+ logger.info(f"{leng} solutions found in Problem `{problem_solutions.problem.name}`, select the best.")
38
40
  problem_solutions.solutions = await self.best(problem_solutions.solutions, **kwargs)
39
41
  return problem_solutions
40
42
 
@@ -48,11 +50,25 @@ class Correct(Rating, Propose):
48
50
  Returns:
49
51
  Improvement: The improvement with the best solutions selected for each problem solution.
50
52
  """
51
- if (leng := len(improvement.problem_solutions)) == 0:
53
+ if leng := len(improvement.problem_solutions):
54
+ logger.debug(f"{leng} problem_solutions found in Improvement, decide solution for each of them.")
55
+ await gather(
56
+ *[
57
+ self.decide_solution(
58
+ ps,
59
+ **fallback_kwargs(
60
+ kwargs, topic=f"which solution is better to deal this problem {ps.problem.compact()}\n\n"
61
+ ),
62
+ )
63
+ for ps in improvement.problem_solutions
64
+ ],
65
+ )
66
+ if any(not (violated := ps).decided() for ps in improvement.problem_solutions):
67
+ logger.error(f"Some problem_solutions are not decided: {violated}")
68
+ else:
69
+ logger.success(f"All problem_solutions are decided '{improvement.focused_on}'")
70
+ else:
52
71
  logger.error(f"No problem_solutions found in Improvement, Skip: {improvement}")
53
- if leng > 1:
54
- for ps in improvement.problem_solutions:
55
- ps.solutions = await self.best(ps.solutions, **kwargs)
56
72
  return improvement
57
73
 
58
74
  async def fix_troubled_obj[M: SketchedAble](
@@ -81,7 +97,7 @@ class Correct(Rating, Propose):
81
97
  "problem": problem_solutions.problem.display(),
82
98
  "solution": ok(
83
99
  problem_solutions.final_solution(),
84
- f"No solution found for problem: {problem_solutions.problem}",
100
+ f"{len(problem_solutions.solutions)} solution Found for `{problem_solutions.problem.name}`.",
85
101
  ).display(),
86
102
  "reference": reference,
87
103
  },
@@ -148,13 +164,15 @@ class Correct(Rating, Propose):
148
164
  TypeError: If the provided object doesn't implement Display or WithBriefing interfaces.
149
165
  """
150
166
  if not improvement.decided():
167
+ logger.info(f"Improvement {improvement.focused_on} not decided, start deciding...")
151
168
  improvement = await self.decide_improvement(improvement, **override_kwargs(kwargs, default=None))
152
169
 
153
170
  for ps in improvement.problem_solutions:
171
+ logger.info(f"Fixing troubling obj {obj.__class__.__name__} when deal with problem: {ps.problem.name}")
154
172
  fixed_obj = await self.fix_troubled_obj(obj, ps, reference, **kwargs)
155
173
  if fixed_obj is None:
156
174
  logger.error(
157
- f"Failed to fix troubling obj {obj.__class__.__name__} when deal with problem: {ps.problem}",
175
+ f"Failed to fix troubling obj {obj.__class__.__name__} when deal with problem: {ps.problem.name}",
158
176
  )
159
177
  return None
160
178
  obj = fixed_obj
@@ -178,6 +196,8 @@ class Correct(Rating, Propose):
178
196
  Optional[str]: A corrected version of the input string, or None if correction fails.
179
197
  """
180
198
  if not improvement.decided():
199
+ logger.info(f"Improvement {improvement.focused_on} not decided, start deciding...")
200
+
181
201
  improvement = await self.decide_improvement(improvement, **override_kwargs(kwargs, default=None))
182
202
 
183
203
  for ps in improvement.problem_solutions:
@@ -43,7 +43,7 @@ class Rating(LLMUsage):
43
43
  Dict[str, float]: A dictionary with the ratings for each dimension.
44
44
  """
45
45
 
46
- def _validator(response: str) -> Dict[str, float] | None:
46
+ def _validator(response: str) -> Optional[Dict[str, float]] :
47
47
  if (
48
48
  (json_data := JsonCapture.validate_with(response, dict, str))
49
49
  and json_data.keys() == rating_manual.keys()
@@ -88,7 +88,7 @@ class Rating(LLMUsage):
88
88
  to_rate: str,
89
89
  topic: str,
90
90
  criteria: Set[str],
91
- manual: Optional[Dict[str, str]],
91
+ manual: Optional[Dict[str, str]] = None,
92
92
  score_range: Tuple[float, float] = (0.0, 1.0),
93
93
  **kwargs: Unpack[ValidateKwargs],
94
94
  ) -> Dict[str, float]: ...
@@ -99,7 +99,7 @@ class Rating(LLMUsage):
99
99
  to_rate: List[str],
100
100
  topic: str,
101
101
  criteria: Set[str],
102
- manual: Optional[Dict[str, str]],
102
+ manual: Optional[Dict[str, str]] = None,
103
103
  score_range: Tuple[float, float] = (0.0, 1.0),
104
104
  **kwargs: Unpack[ValidateKwargs],
105
105
  ) -> List[Dict[str, float]]: ...
@@ -109,7 +109,7 @@ class Rating(LLMUsage):
109
109
  to_rate: Union[str, List[str]],
110
110
  topic: str,
111
111
  criteria: Set[str],
112
- manual: Optional[Dict[str, str]],
112
+ manual: Optional[Dict[str, str]] = None,
113
113
  score_range: Tuple[float, float] = (0.0, 1.0),
114
114
  **kwargs: Unpack[ValidateKwargs],
115
115
  ) -> Optional[Dict[str, float] | List[Dict[str, float]]]:
@@ -170,7 +170,7 @@ class Rating(LLMUsage):
170
170
  configs.templates.draft_rating_manual_template,
171
171
  {
172
172
  "topic": topic,
173
- "criteria": criteria,
173
+ "criteria": list(criteria),
174
174
  },
175
175
  )
176
176
  ),
@@ -360,14 +360,14 @@ class Rating(LLMUsage):
360
360
  return [sum(ratings[c] * weights[c] for c in criteria) for ratings in ratings_seq]
361
361
 
362
362
  @overload
363
- async def best(self, candidates: List[str], k: int=1, **kwargs: Unpack[CompositeScoreKwargs]) -> List[str]: ...
363
+ async def best(self, candidates: List[str], k: int = 1, **kwargs: Unpack[CompositeScoreKwargs]) -> List[str]: ...
364
364
  @overload
365
365
  async def best[T: Display](
366
- self, candidates: List[T], k: int=1, **kwargs: Unpack[CompositeScoreKwargs]
366
+ self, candidates: List[T], k: int = 1, **kwargs: Unpack[CompositeScoreKwargs]
367
367
  ) -> List[T]: ...
368
368
 
369
369
  async def best[T: Display](
370
- self, candidates: List[str] | List[T], k: int=1, **kwargs: Unpack[CompositeScoreKwargs]
370
+ self, candidates: List[str] | List[T], k: int = 1, **kwargs: Unpack[CompositeScoreKwargs]
371
371
  ) -> Optional[List[str] | List[T]]:
372
372
  """Choose the best candidates from the list of candidates based on the composite score.
373
373
 
@@ -177,27 +177,27 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
177
177
  current_action = None
178
178
  try:
179
179
  # Process each action in sequence
180
- for step in self._instances:
180
+ for i,step in enumerate(self._instances):
181
181
  current_action = step.name
182
- logger.info(f"Executing step >> {current_action}")
182
+ logger.info(f"Executing step [{i}] >> {current_action}")
183
183
 
184
184
  # Get current context and execute action
185
185
  context = await self._context.get()
186
186
  act_task = create_task(step.act(context))
187
187
  # Handle task cancellation
188
188
  if task.is_cancelled():
189
- logger.warning(f"Task cancelled by task: {task.name}")
189
+ logger.warning(f"Workflow cancelled by task: {task.name}")
190
190
  act_task.cancel(f"Cancelled by task: {task.name}")
191
191
  break
192
192
 
193
193
  # Update context with modified values
194
194
  modified_ctx = await act_task
195
- logger.success(f"Step execution finished: {current_action}")
195
+ logger.success(f"Step [{i}] `{current_action}` execution finished.")
196
196
  if step.output_key:
197
- logger.success(f"Setting output to `{step.output_key}`")
197
+ logger.success(f"Setting action `{current_action}` output to `{step.output_key}`")
198
198
  await self._context.put(modified_ctx)
199
199
 
200
- logger.success(f"Workflow execution finished: {self.name}")
200
+ logger.success(f"Workflow `{self.name}` execution finished.")
201
201
 
202
202
  # Get final context and extract result
203
203
  final_ctx = await self._context.get()
@@ -7,8 +7,10 @@ from typing import Generator, List, Optional, Self, Tuple, overload
7
7
 
8
8
  from fabricatio.models.generic import (
9
9
  AsPrompt,
10
+ Described,
10
11
  FinalizedDumpAble,
11
12
  Introspect,
13
+ Language,
12
14
  ModelHash,
13
15
  PersistentAble,
14
16
  ProposedUpdateAble,
@@ -103,12 +105,9 @@ class ArticleRef(ProposedUpdateAble):
103
105
  return ReferringType.CHAPTER
104
106
 
105
107
 
106
- class ArticleMetaData(SketchedAble):
108
+ class ArticleMetaData(SketchedAble, Described, Language):
107
109
  """Metadata for an article component."""
108
110
 
109
- description: str
110
- """Description of the research component in academic style."""
111
-
112
111
  support_to: List[ArticleRef]
113
112
  """List of references to other component of this articles that this component supports."""
114
113
  depend_on: List[ArticleRef]
@@ -273,12 +272,9 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
273
272
  return ""
274
273
 
275
274
 
276
- class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
275
+ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, Language, ABC):
277
276
  """Base class for article outlines."""
278
277
 
279
- language: str
280
- """Written language of the article. SHALL be aligned to the language of the article proposal provided."""
281
-
282
278
  title: str
283
279
  """Title of the academic paper."""
284
280
 
@@ -378,12 +374,31 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
378
374
  return component, summary
379
375
  return None
380
376
 
377
+ def gather_introspected(self) -> Optional[str]:
378
+ """Gathers all introspected components in the article structure."""
379
+ return "\n".join([i for component in self.chapters if (i := component.introspect())])
380
+
381
381
  @overload
382
382
  def find_illegal_ref(self, gather_identical: bool) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]: ...
383
383
 
384
384
  @overload
385
385
  def find_illegal_ref(self) -> Optional[Tuple[ArticleRef, str]]: ...
386
386
 
387
+ def iter_chap_title(self) -> Generator[str, None, None]:
388
+ """Iterates through all chapter titles in the article."""
389
+ for chap in self.chapters:
390
+ yield chap.title
391
+
392
+ def iter_section_title(self) -> Generator[str, None, None]:
393
+ """Iterates through all section titles in the article."""
394
+ for _, sec in self.iter_sections():
395
+ yield sec.title
396
+
397
+ def iter_subsection_title(self) -> Generator[str, None, None]:
398
+ """Iterates through all subsection titles in the article."""
399
+ for _, _, subsec in self.iter_subsections():
400
+ yield subsec.title
401
+
387
402
  def find_illegal_ref(self, gather_identical: bool = False) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]:
388
403
  """Finds the first illegal component in the outline.
389
404
 
@@ -391,21 +406,68 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
391
406
  Tuple[ArticleOutlineBase, str]: A tuple containing the illegal component and an error message.
392
407
  """
393
408
  summary = ""
409
+ chap_titles_set = set(self.iter_chap_title())
410
+ sec_titles_set = set(self.iter_section_title())
411
+ subsec_titles_set = set(self.iter_subsection_title())
412
+
394
413
  for component in self.iter_dfs_rev():
395
414
  for ref in chain(component.depend_on, component.support_to):
396
415
  if not ref.deref(self):
397
416
  summary += f"Invalid internal reference in `{component.__class__.__name__}` titled `{component.title}`, because the referred {ref.referring_type} is not exists within the article, see the original obj dump: {ref.model_dump()}\n"
398
- if summary and not gather_identical:
399
- return ref, summary
400
- if summary and gather_identical:
401
- return [
402
- identical_ref
403
- for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
404
- if identical_ref == ref
405
- ], summary
417
+
418
+ if ref.referred_chapter_title not in (chap_titles_set):
419
+ summary += f"Chapter titled `{ref.referred_chapter_title}` is not any of {chap_titles_set}\n"
420
+ if ref.referred_section_title and ref.referred_section_title not in (sec_titles_set):
421
+ summary += f"Section Titled `{ref.referred_section_title}` is not any of {sec_titles_set}\n"
422
+ if ref.referred_subsection_title and ref.referred_subsection_title not in (subsec_titles_set):
423
+ summary += (
424
+ f"Subsection Titled `{ref.referred_subsection_title}` is not any of {subsec_titles_set}"
425
+ )
426
+
427
+ if summary:
428
+ return (
429
+ (
430
+ [
431
+ identical_ref
432
+ for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
433
+ if identical_ref == ref
434
+ ],
435
+ summary,
436
+ )
437
+ if gather_identical
438
+ else (ref, summary)
439
+ )
406
440
 
407
441
  return None
408
442
 
443
+ def gather_illegal_ref(self) -> Tuple[List[ArticleRef], str]:
444
+ """Gathers all illegal references in the article."""
445
+ summary = []
446
+ chap_titles_set = set(self.iter_chap_title())
447
+ sec_titles_set = set(self.iter_section_title())
448
+ subsec_titles_set = set(self.iter_subsection_title())
449
+ res_seq = []
450
+
451
+ for component in self.iter_dfs():
452
+ for ref in (
453
+ r for r in chain(component.depend_on, component.support_to) if not r.deref(self) and r not in res_seq
454
+ ):
455
+ res_seq.append(ref)
456
+ if ref.referred_chapter_title not in chap_titles_set:
457
+ summary.append(
458
+ f"Chapter titled `{ref.referred_chapter_title}` is not exist, since it is not any of {chap_titles_set}."
459
+ )
460
+ if ref.referred_section_title and (ref.referred_section_title not in sec_titles_set):
461
+ summary.append(
462
+ f"Section Titled `{ref.referred_section_title}` is not exist, since it is not any of {sec_titles_set}"
463
+ )
464
+ if ref.referred_subsection_title and (ref.referred_subsection_title not in subsec_titles_set):
465
+ summary.append(
466
+ f"Subsection Titled `{ref.referred_subsection_title}` is not exist, since it is not any of {subsec_titles_set}"
467
+ )
468
+
469
+ return res_seq, "\n".join(summary)
470
+
409
471
  def finalized_dump(self) -> str:
410
472
  """Generates standardized hierarchical markup for academic publishing systems.
411
473
 
@@ -15,6 +15,7 @@ from fabricatio.models.extra.article_outline import (
15
15
  ArticleOutline,
16
16
  )
17
17
  from fabricatio.models.generic import PersistentAble, SequencePatch, SketchedAble, WithRef
18
+ from fabricatio.rust import word_count
18
19
  from fabricatio.utils import ok
19
20
 
20
21
 
@@ -27,6 +28,9 @@ class Paragraph(SketchedAble):
27
28
  writing_aim: List[str]
28
29
  """Specific communicative objectives for this paragraph's content."""
29
30
 
31
+ expected_word_count: int
32
+ """Expected word count for the paragraph."""
33
+
30
34
  content: str
31
35
  """The actual content of the paragraph, represented as a string."""
32
36
 
@@ -41,11 +45,26 @@ class ArticleSubsection(SubSectionBase):
41
45
  paragraphs: List[Paragraph]
42
46
  """List of Paragraph objects containing the content of the subsection."""
43
47
 
48
+ _max_word_count_deviation: float = 0.3
49
+ """Maximum allowed deviation from the expected word count, as a percentage."""
50
+
51
+ @property
52
+ def word_count(self) -> int:
53
+ """Calculates the total word count of all paragraphs in the subsection."""
54
+ return sum(word_count(p.content) for p in self.paragraphs)
55
+
44
56
  def introspect(self) -> str:
45
- """Introspects the subsection and returns a message if it has no paragraphs."""
57
+ """Introspects the subsection and returns a summary of its state."""
58
+ summary = ""
46
59
  if len(self.paragraphs) == 0:
47
- return f"`{self.__class__.__name__}` titled `{self.title}` have no paragraphs, to achieve the goal of `{self.writing_aim}`."
48
- return ""
60
+ summary += f"`{self.__class__.__name__}` titled `{self.title}` have no paragraphs!\n"
61
+ if (
62
+ abs((wc := self.word_count) - self.expected_word_count) / self.expected_word_count
63
+ > self._max_word_count_deviation
64
+ ):
65
+ summary += f"`{self.__class__.__name__}` titled `{self.title}` have {wc} words, expected {self.expected_word_count} words!"
66
+
67
+ return summary
49
68
 
50
69
  def update_from_inner(self, other: Self) -> Self:
51
70
  """Updates the current instance with the attributes of another instance."""
@@ -94,7 +113,7 @@ class Article(
94
113
 
95
114
  @override
96
115
  def iter_subsections(self) -> Generator[Tuple[ArticleChapter, ArticleSection, ArticleSubsection], None, None]:
97
- return super().iter_subsections()
116
+ return super().iter_subsections() # pyright: ignore [reportReturnType]
98
117
 
99
118
  @classmethod
100
119
  def from_outline(cls, outline: ArticleOutline) -> "Article":
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Dict, List
4
4
 
5
- from fabricatio.models.generic import AsPrompt, SketchedAble, PersistentAble, WithRef
5
+ from fabricatio.models.generic import AsPrompt, PersistentAble, SketchedAble, WithRef
6
6
 
7
7
 
8
8
  class ArticleProposal(SketchedAble, WithRef[str], AsPrompt, PersistentAble):
@@ -1,7 +1,7 @@
1
1
  """A class representing a problem-solution pair identified during a review process."""
2
2
 
3
3
  from itertools import chain
4
- from typing import List, Literal, Optional, Self
4
+ from typing import Any, List, Literal, Optional, Self, Tuple, Unpack
5
5
 
6
6
  from fabricatio.journal import logger
7
7
  from fabricatio.models.generic import SketchedAble, WithBriefing
@@ -51,7 +51,12 @@ class ProblemSolutions(SketchedAble):
51
51
  problem: Problem
52
52
  """The problem identified in the review."""
53
53
  solutions: List[Solution]
54
- """A collection of potential solutions."""
54
+ """A collection of potential solutions, spread the thought, add more solution as possible.Do not leave this as blank"""
55
+
56
+ def model_post_init(self, context: Any, /) -> None:
57
+ """Initialize the problem-solution pair with a problem and a list of solutions."""
58
+ if len(self.solutions) == 0:
59
+ logger.warning(f"No solution found for problem {self.problem.name}, please add more solutions manually.")
55
60
 
56
61
  def update_from_inner(self, other: Self) -> Self:
57
62
  """Update the current instance with another instance's attributes."""
@@ -69,6 +74,10 @@ class ProblemSolutions(SketchedAble):
69
74
  self.solutions = solutions
70
75
  return self
71
76
 
77
+ def has_solutions(self) -> bool:
78
+ """Check if the problem-solution pair has any solutions."""
79
+ return len(self.solutions) > 0
80
+
72
81
  async def edit_problem(self) -> Self:
73
82
  """Interactively edit the problem description."""
74
83
  self.problem = Problem.model_validate_strings(
@@ -87,11 +96,11 @@ class ProblemSolutions(SketchedAble):
87
96
  """Check if the improvement is decided."""
88
97
  return len(self.solutions) == 1
89
98
 
90
- def final_solution(self) -> Optional[Solution]:
99
+ def final_solution(self, always_use_first: bool = False) -> Optional[Solution]:
91
100
  """Get the final solution."""
92
- if not self.decided():
101
+ if not always_use_first and not self.decided():
93
102
  logger.error(
94
- f"There is more than one solution for problem {self.problem.name}, please decide which solution is eventually adopted."
103
+ f"There is {len(self.solutions)} solutions for problem {self.problem.name}, please decide which solution is eventually adopted."
95
104
  )
96
105
  return None
97
106
  return self.solutions[0]
@@ -106,6 +115,10 @@ class Improvement(SketchedAble):
106
115
  problem_solutions: List[ProblemSolutions]
107
116
  """Collection of problems identified during review along with their potential solutions."""
108
117
 
118
+ def all_problems_have_solutions(self) -> bool:
119
+ """Check if all problems have solutions."""
120
+ return all(ps.has_solutions() for ps in self.problem_solutions)
121
+
109
122
  async def supervisor_check(self, check_solutions: bool = True) -> Self:
110
123
  """Perform an interactive review session to filter problems and solutions.
111
124
 
@@ -145,9 +158,9 @@ class Improvement(SketchedAble):
145
158
  return all(ps.decided() for ps in self.problem_solutions)
146
159
 
147
160
  @classmethod
148
- def gather(cls, *improvements: Self) -> Self:
161
+ def gather(cls, *improvements: Unpack[Tuple["Improvement", ...]]) -> Self:
149
162
  """Gather multiple improvements into a single instance."""
150
163
  return cls(
151
- focused_on="\n".join(imp.focused_on for imp in improvements),
164
+ focused_on=";".join(imp.focused_on for imp in improvements),
152
165
  problem_solutions=list(chain(*(imp.problem_solutions for imp in improvements))),
153
166
  )
@@ -8,15 +8,15 @@ descriptions, examples, and metadata for each rule and rule set, making it suita
8
8
  complex rule management systems.
9
9
  """
10
10
 
11
- from typing import List
11
+ from typing import List, Self, Tuple, Unpack
12
12
 
13
13
  from fabricatio.models.generic import Language, PersistentAble, SketchedAble, WithBriefing
14
+ from more_itertools import flatten
14
15
 
15
16
 
16
- class Rule(WithBriefing,Language, SketchedAble,PersistentAble):
17
+ class Rule(WithBriefing, Language, SketchedAble, PersistentAble):
17
18
  """Represents a rule or guideline for a specific topic."""
18
19
 
19
-
20
20
  violation_examples: List[str]
21
21
  """A list of concrete examples demonstrating violations of this rule. Each example should
22
22
  be a clear scenario or case that illustrates how the rule can be broken, including the
@@ -30,7 +30,7 @@ class Rule(WithBriefing,Language, SketchedAble,PersistentAble):
30
30
  serve as practical guidance for implementing the rule correctly."""
31
31
 
32
32
 
33
- class RuleSet( SketchedAble, PersistentAble, WithBriefing,Language):
33
+ class RuleSet(SketchedAble, PersistentAble, WithBriefing, Language):
34
34
  """Represents a collection of rules and guidelines for a particular topic."""
35
35
 
36
36
  rules: List[Rule]
@@ -38,3 +38,15 @@ class RuleSet( SketchedAble, PersistentAble, WithBriefing,Language):
38
38
  a well-defined, specific guideline that contributes to the overall purpose of the rule set.
39
39
  The rules should be logically organized and consistent with each other, forming a coherent
40
40
  framework for the topic or domain covered by the rule set."""
41
+
42
+ @classmethod
43
+ def gather(cls, *rulesets: Unpack[Tuple["RuleSet",...]]) -> Self:
44
+ """Gathers multiple rule sets into a single rule set."""
45
+ if not rulesets:
46
+ raise ValueError("No rulesets provided")
47
+ return cls(
48
+ language=rulesets[0].language,
49
+ name=";".join(ruleset.name for ruleset in rulesets),
50
+ description=";".join(ruleset.description for ruleset in rulesets),
51
+ rules=list(flatten(r.rules for r in rulesets)),
52
+ )
@@ -229,7 +229,7 @@ class PersistentAble(Base):
229
229
  return self
230
230
 
231
231
  @classmethod
232
- def from_latest_persistent(cls, dir_path: str | Path) -> Self:
232
+ def from_latest_persistent(cls, dir_path: str | Path) -> Optional[Self]:
233
233
  """Load most recent persisted instance from directory.
234
234
 
235
235
  Args:
@@ -244,13 +244,13 @@ class PersistentAble(Base):
244
244
  """
245
245
  dir_path = Path(dir_path)
246
246
  if not dir_path.is_dir():
247
- raise NotADirectoryError(f"{dir_path} is not a valid directory")
247
+ return None
248
248
 
249
249
  pattern = f"{cls.__name__}_*.json"
250
250
  files = list(dir_path.glob(pattern))
251
251
 
252
252
  if not files:
253
- raise FileNotFoundError(f"No persistent files found for {cls.__name__} in {dir_path}")
253
+ return None
254
254
 
255
255
  def _get_timestamp(file_path: Path) -> datetime:
256
256
  stem = file_path.stem
@@ -130,7 +130,6 @@ class LLMUsage(ScopedConfig):
130
130
  question: str,
131
131
  system_message: str = "",
132
132
  n: PositiveInt | None = None,
133
- stream_buffer_size: int = 50,
134
133
  **kwargs: Unpack[LLMKwargs],
135
134
  ) -> Sequence[TextChoices | Choices | StreamingChoices]:
136
135
  """Asynchronously invokes the language model with a question and optional system message.
@@ -139,7 +138,6 @@ class LLMUsage(ScopedConfig):
139
138
  question (str): The question to ask the model.
140
139
  system_message (str): The system message to provide context to the model. Defaults to an empty string.
141
140
  n (PositiveInt | None): The number of responses to generate. Defaults to the instance's `llm_generation_count` or the global configuration.
142
- stream_buffer_size (int): The buffer size for streaming responses. Defaults to 50.
143
141
  **kwargs (Unpack[LLMKwargs]): Additional keyword arguments for the LLM usage.
144
142
 
145
143
  Returns:
@@ -155,16 +153,7 @@ class LLMUsage(ScopedConfig):
155
153
  if isinstance(resp, CustomStreamWrapper):
156
154
  if not configs.debug.streaming_visible and (pack := stream_chunk_builder(await asyncstdlib.list())):
157
155
  return pack.choices
158
- chunks = []
159
- buffer = ""
160
- async for chunk in resp:
161
- chunks.append(chunk)
162
- buffer += chunk.choices[0].delta.content or ""
163
- if len(buffer) > stream_buffer_size:
164
- print(buffer, end="") # noqa: T201
165
- buffer = ""
166
- print(buffer) # noqa: T201
167
- if pack := stream_chunk_builder(chunks):
156
+ if pack := stream_chunk_builder(await asyncstdlib.list(resp)):
168
157
  return pack.choices
169
158
  logger.critical(err := f"Unexpected response type: {type(resp)}")
170
159
  raise ValueError(err)
Binary file
fabricatio/rust.pyi CHANGED
@@ -1,8 +1,24 @@
1
+ """
2
+ Python interface definitions for Rust-based functionality.
3
+
4
+ This module provides type stubs and documentation for Rust-implemented utilities,
5
+ including template rendering, cryptographic hashing, language detection, and
6
+ bibliography management. The actual implementations are provided by Rust modules.
7
+
8
+ Key Features:
9
+ - TemplateManager: Handles Handlebars template rendering and management.
10
+ - BibManager: Manages BibTeX bibliography parsing and querying.
11
+ - Cryptographic utilities: BLAKE3 hashing.
12
+ - Text utilities: Word boundary splitting and word counting.
13
+ """
14
+
15
+
1
16
  from pathlib import Path
2
17
  from typing import List, Optional
3
18
 
4
19
  from pydantic import JsonValue
5
20
 
21
+
6
22
  class TemplateManager:
7
23
  """Template rendering engine using Handlebars templates.
8
24
 
@@ -81,6 +97,28 @@ def blake3_hash(content: bytes) -> str:
81
97
  def detect_language(string: str) -> str:
82
98
  """Detect the language of a given string."""
83
99
 
100
+
101
+ def split_word_bounds(string: str) -> List[str]:
102
+ """Split the string into words based on word boundaries.
103
+
104
+ Args:
105
+ string: The input string to be split.
106
+
107
+ Returns:
108
+ A list of words extracted from the string.
109
+ """
110
+ def word_count(string: str) -> int:
111
+ """Count the number of words in the string.
112
+
113
+ Args:
114
+ string: The input string to count words from.
115
+
116
+ Returns:
117
+ The number of words in the string.
118
+ """
119
+
120
+
121
+
84
122
  class BibManager:
85
123
  """BibTeX bibliography manager for parsing and querying citation data."""
86
124
 
fabricatio/utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """A collection of utility functions for the fabricatio package."""
2
2
 
3
- from typing import Any, Dict, List, Optional
3
+ from typing import Any, Dict, List, Mapping, Optional
4
4
 
5
5
  from questionary import text
6
6
 
@@ -25,16 +25,16 @@ async def ask_edit(
25
25
  return res
26
26
 
27
27
 
28
- def override_kwargs(kwargs: Dict[str,Any], **overrides) -> Dict[str, Any]:
28
+ def override_kwargs(kwargs: Mapping[str,Any], **overrides) -> Dict[str, Any]:
29
29
  """Override the values in kwargs with the provided overrides."""
30
- new_kwargs = kwargs.copy()
30
+ new_kwargs = dict(kwargs.items())
31
31
  new_kwargs.update({k: v for k, v in overrides.items() if v is not None})
32
32
  return new_kwargs
33
33
 
34
34
 
35
- def fallback_kwargs(kwargs: Dict[str, Any], **overrides) -> Dict[str, Any]:
35
+ def fallback_kwargs(kwargs: Mapping[str, Any], **overrides) -> Dict[str, Any]:
36
36
  """Fallback the values in kwargs with the provided overrides."""
37
- new_kwargs = kwargs.copy()
37
+ new_kwargs = dict(kwargs.items())
38
38
  new_kwargs.update({k: v for k, v in overrides.items() if k not in new_kwargs and v is not None})
39
39
  return new_kwargs
40
40