fabricatio 0.2.9.dev1__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,16 +1,19 @@
1
1
  """A class that provides the capability to check strings and objects against rules and guidelines."""
2
2
 
3
- from typing import Optional, Unpack
3
+ from asyncio import gather
4
+ from typing import List, Optional, Unpack
4
5
 
5
6
  from fabricatio import TEMPLATE_MANAGER
6
7
  from fabricatio.capabilities.advanced_judge import AdvancedJudge
7
8
  from fabricatio.capabilities.propose import Propose
8
9
  from fabricatio.config import configs
10
+ from fabricatio.journal import logger
9
11
  from fabricatio.models.extra.patches import RuleSetBriefingPatch
10
12
  from fabricatio.models.extra.problem import Improvement
11
13
  from fabricatio.models.extra.rule import Rule, RuleSet
12
14
  from fabricatio.models.generic import Display, WithBriefing
13
15
  from fabricatio.models.kwargs_types import ValidateKwargs
16
+ from fabricatio.rust import detect_language
14
17
  from fabricatio.utils import override_kwargs
15
18
 
16
19
 
@@ -50,15 +53,20 @@ class Check(AdvancedJudge, Propose):
50
53
  if rule_reqs is None:
51
54
  return None
52
55
 
53
- rules = await self.propose(Rule, [TEMPLATE_MANAGER.render_template(configs.templates.rule_requirement_template, {"rule_requirement": r}) for r in rule_reqs], **kwargs)
56
+ rules = await self.propose(
57
+ Rule,
58
+ [
59
+ TEMPLATE_MANAGER.render_template(configs.templates.rule_requirement_template, {"rule_requirement": r})
60
+ for r in rule_reqs
61
+ ],
62
+ **kwargs,
63
+ )
54
64
  if any(r for r in rules if r is None):
55
65
  return None
56
66
 
57
67
  ruleset_patch = await self.propose(
58
68
  RuleSetBriefingPatch,
59
- f"# Rules Requirements\n{rule_reqs}\n# Generated Rules\n{Display.seq_display(rules)}\n\n"
60
- f"You need to write a concise and detailed patch for this ruleset that can be applied to the ruleset nicely.\n"
61
- f"Note that all fields in this patch will be directly copied to the ruleset obj, including `name` and `description`, so write when knowing the subject.\n",
69
+ f"{ruleset_requirement}\n\nYou should use `{detect_language(ruleset_requirement)}`!",
62
70
  **override_kwargs(kwargs, default=None),
63
71
  )
64
72
 
@@ -94,11 +102,12 @@ class Check(AdvancedJudge, Propose):
94
102
  f"# Content to exam\n{input_text}\n\n# Rule Must to follow\n{rule.display()}\nDoes `Content to exam` provided above violate the `Rule Must to follow` provided above?",
95
103
  **override_kwargs(kwargs, default=None),
96
104
  ):
105
+ logger.info(f"Rule `{rule.name}` violated: \n{judge.display()}")
97
106
  return await self.propose(
98
107
  Improvement,
99
108
  TEMPLATE_MANAGER.render_template(
100
109
  configs.templates.check_string_template,
101
- {"to_check": input_text, "rule": rule, "judge": judge.display(), "reference": reference},
110
+ {"to_check": input_text, "rule": rule.display(), "judge": judge.display(), "reference": reference},
102
111
  ),
103
112
  **kwargs,
104
113
  )
@@ -142,7 +151,7 @@ class Check(AdvancedJudge, Propose):
142
151
  ruleset: RuleSet,
143
152
  reference: str = "",
144
153
  **kwargs: Unpack[ValidateKwargs[Improvement]],
145
- ) -> Optional[Improvement]:
154
+ ) -> Optional[List[Improvement]]:
146
155
  """Validate text against full ruleset.
147
156
 
148
157
  Args:
@@ -159,12 +168,13 @@ class Check(AdvancedJudge, Propose):
159
168
  - Halts validation after first successful improvement proposal
160
169
  - Maintains rule execution order from ruleset.rules list
161
170
  """
162
- imp_seq = [
163
- await self.check_string_against_rule(input_text, rule, reference, **kwargs) for rule in ruleset.rules
164
- ]
165
- if all(isinstance(i, Improvement) for i in imp_seq):
166
- return Improvement.gather(*imp_seq) # pyright: ignore [reportArgumentType]
167
- return None
171
+ imp_seq = await gather(
172
+ *[self.check_string_against_rule(input_text, rule, reference, **kwargs) for rule in ruleset.rules]
173
+ )
174
+ if imp_seq is None:
175
+ logger.warning(f"Generation failed for string check against `{ruleset.name}`")
176
+ return None
177
+ return [imp for imp in imp_seq if imp]
168
178
 
169
179
  async def check_obj[M: (Display, WithBriefing)](
170
180
  self,
@@ -172,7 +182,7 @@ class Check(AdvancedJudge, Propose):
172
182
  ruleset: RuleSet,
173
183
  reference: str = "",
174
184
  **kwargs: Unpack[ValidateKwargs[Improvement]],
175
- ) -> Optional[Improvement]:
185
+ ) -> Optional[List[Improvement]]:
176
186
  """Validate object against full ruleset.
177
187
 
178
188
  Args:
@@ -189,7 +199,9 @@ class Check(AdvancedJudge, Propose):
189
199
  - Maintains same early termination behavior as check_string
190
200
  - Validates object through text conversion mechanism
191
201
  """
192
- imp_seq = [await self.check_obj_against_rule(obj, rule, reference, **kwargs) for rule in ruleset.rules]
193
- if all(isinstance(i, Improvement) for i in imp_seq):
194
- return Improvement.gather(*imp_seq) # pyright: ignore [reportArgumentType]
195
- return None
202
+ imp_seq = await gather(*[self.check_obj_against_rule(obj, rule, reference, **kwargs) for rule in ruleset.rules])
203
+
204
+ if imp_seq is None:
205
+ logger.warning(f"Generation Failed for `{obj.__class__.__name__}` against Ruleset `{ruleset.name}`")
206
+ return None
207
+ return [i for i in imp_seq if i]
@@ -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](
@@ -78,11 +94,11 @@ class Correct(Rating, Propose):
78
94
  TEMPLATE_MANAGER.render_template(
79
95
  configs.templates.fix_troubled_obj_template,
80
96
  {
81
- "problem": problem_solutions.problem,
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}",
85
- ),
100
+ f"{len(problem_solutions.solutions)} solution Found for `{problem_solutions.problem.name}`.",
101
+ ).display(),
86
102
  "reference": reference,
87
103
  },
88
104
  ),
@@ -111,11 +127,11 @@ class Correct(Rating, Propose):
111
127
  TEMPLATE_MANAGER.render_template(
112
128
  configs.templates.fix_troubled_string_template,
113
129
  {
114
- "problem": problem_solutions.problem,
130
+ "problem": problem_solutions.problem.display(),
115
131
  "solution": ok(
116
132
  problem_solutions.final_solution(),
117
133
  f"No solution found for problem: {problem_solutions.problem}",
118
- ),
134
+ ).display(),
119
135
  "reference": reference,
120
136
  "string_to_fix": input_text,
121
137
  },
@@ -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
 
fabricatio/config.py CHANGED
@@ -303,7 +303,7 @@ class CacheConfig(BaseModel):
303
303
 
304
304
  model_config = ConfigDict(use_attribute_docstrings=True)
305
305
 
306
- type: Optional[LiteLLMCacheType] = None
306
+ type: LiteLLMCacheType = LiteLLMCacheType.LOCAL
307
307
  """The type of cache to use. If None, the default cache type will be used."""
308
308
  params: CacheKwargs = Field(default_factory=CacheKwargs)
309
309
  """The parameters for the cache. If type is None, the default parameters will be used."""
@@ -1,7 +1,12 @@
1
- """Module that contains the classes for actions and workflows.
1
+ """Module that contains the classes for defining and executing task workflows.
2
2
 
3
- This module defines the Action and WorkFlow classes, which are used for
4
- creating and executing sequences of actions in a task-based context.
3
+ This module provides the Action and WorkFlow classes for creating structured
4
+ task execution pipelines. Actions represent atomic operations, while WorkFlows
5
+ orchestrate sequences of actions with shared context and error handling.
6
+
7
+ Classes:
8
+ Action: Base class for defining executable actions with context management.
9
+ WorkFlow: Manages action sequences, context propagation, and task lifecycle.
5
10
  """
6
11
 
7
12
  import traceback
@@ -50,28 +55,26 @@ class Action(WithBriefing, LLMUsage):
50
55
  self.description = self.description or self.__class__.__doc__ or ""
51
56
 
52
57
  @abstractmethod
53
- async def _execute(self, *_, **cxt) -> Any: # noqa: ANN002
54
- """Execute the action logic with the provided context arguments.
55
-
56
- This method must be implemented by subclasses to define the actual behavior.
58
+ async def _execute(self, *_:Any, **cxt) -> Any:
59
+ """Implement the core logic of the action.
57
60
 
58
61
  Args:
59
- **cxt: The context dictionary containing input and output data.
62
+ **cxt: Context dictionary containing input/output data.
60
63
 
61
64
  Returns:
62
- Any: The result of the action execution.
65
+ Result of the action execution to be stored in context.
63
66
  """
64
67
  pass
65
68
 
66
69
  @final
67
70
  async def act(self, cxt: Dict[str, Any]) -> Dict[str, Any]:
68
- """Perform the action and update the context with results.
71
+ """Execute action and update context.
69
72
 
70
73
  Args:
71
- cxt: The context dictionary containing input and output data.
74
+ cxt (Dict[str, Any]): Shared context dictionary.
72
75
 
73
76
  Returns:
74
- Dict[str, Any]: The updated context dictionary.
77
+ Updated context dictionary with new/modified entries.
75
78
  """
76
79
  ret = await self._execute(**cxt)
77
80
 
@@ -83,10 +86,10 @@ class Action(WithBriefing, LLMUsage):
83
86
 
84
87
  @property
85
88
  def briefing(self) -> str:
86
- """Return a formatted description of the action including personality context if available.
89
+ """Generate formatted action description with personality context.
87
90
 
88
91
  Returns:
89
- str: Formatted briefing text with personality and action description.
92
+ Briefing text combining personality and action description.
90
93
  """
91
94
  if self.personality:
92
95
  return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
@@ -98,10 +101,15 @@ class Action(WithBriefing, LLMUsage):
98
101
  return self
99
102
 
100
103
  class WorkFlow(WithBriefing, ToolBoxUsage):
101
- """Class that represents a sequence of actions to be executed for a task.
104
+ """Manages sequences of actions to fulfill tasks.
102
105
 
103
- A workflow manages the execution of multiple actions in sequence, passing
104
- a shared context between them and handling task lifecycle events.
106
+ Handles context propagation between actions, error handling, and task lifecycle
107
+ events like cancellation and completion.
108
+
109
+ Attributes:
110
+ steps (Tuple): Sequence of Action instances or classes to execute.
111
+ task_input_key (str): Key for storing task instance in context.
112
+ task_output_key (str): Key to retrieve final result from context.
105
113
  """
106
114
 
107
115
  description: str = ""
@@ -137,26 +145,29 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
137
145
  self._instances = tuple(step if isinstance(step, Action) else step() for step in self.steps)
138
146
 
139
147
  def inject_personality(self, personality: str) -> Self:
140
- """Set the personality for all actions that don't have one defined.
148
+ """Set personality for actions without existing personality.
141
149
 
142
150
  Args:
143
- personality: The personality text to inject.
151
+ personality (str): Shared personality context
144
152
 
145
153
  Returns:
146
- Self: The workflow instance for method chaining.
154
+ Workflow instance with updated actions
147
155
  """
148
156
  for action in filter(lambda a: not a.personality, self._instances):
149
157
  action.personality = personality
150
158
  return self
151
159
 
152
160
  async def serve(self, task: Task) -> None:
153
- """Execute the workflow to fulfill the given task.
154
-
155
- This method manages the complete lifecycle of processing a task through
156
- the workflow's sequence of actions.
161
+ """Execute workflow to complete given task.
157
162
 
158
163
  Args:
159
- task: The task to be processed.
164
+ task (Task): Task instance to be processed.
165
+
166
+ Steps:
167
+ 1. Initialize context with task instance and extra data
168
+ 2. Execute each action sequentially
169
+ 3. Handle task cancellation and exceptions
170
+ 4. Extract final result from context
160
171
  """
161
172
  logger.info(f"Start execute workflow: {self.name}")
162
173
 
@@ -166,27 +177,27 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
166
177
  current_action = None
167
178
  try:
168
179
  # Process each action in sequence
169
- for step in self._instances:
180
+ for i,step in enumerate(self._instances):
170
181
  current_action = step.name
171
- logger.info(f"Executing step >> {current_action}")
182
+ logger.info(f"Executing step [{i}] >> {current_action}")
172
183
 
173
184
  # Get current context and execute action
174
185
  context = await self._context.get()
175
186
  act_task = create_task(step.act(context))
176
187
  # Handle task cancellation
177
188
  if task.is_cancelled():
178
- logger.warning(f"Task cancelled by task: {task.name}")
189
+ logger.warning(f"Workflow cancelled by task: {task.name}")
179
190
  act_task.cancel(f"Cancelled by task: {task.name}")
180
191
  break
181
192
 
182
193
  # Update context with modified values
183
194
  modified_ctx = await act_task
184
- logger.success(f"Step execution finished: {current_action}")
195
+ logger.success(f"Step [{i}] `{current_action}` execution finished.")
185
196
  if step.output_key:
186
- logger.success(f"Setting output to `{step.output_key}`")
197
+ logger.success(f"Setting action `{current_action}` output to `{step.output_key}`")
187
198
  await self._context.put(modified_ctx)
188
199
 
189
- logger.success(f"Workflow execution finished: {self.name}")
200
+ logger.success(f"Workflow `{self.name}` execution finished.")
190
201
 
191
202
  # Get final context and extract result
192
203
  final_ctx = await self._context.get()
@@ -206,10 +217,14 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
206
217
  await task.fail()
207
218
 
208
219
  async def _init_context[T](self, task: Task[T]) -> None:
209
- """Initialize the context dictionary for workflow execution.
220
+ """Initialize workflow execution context.
210
221
 
211
222
  Args:
212
- task: The task being served by this workflow.
223
+ task (Task[T]): Task being processed
224
+
225
+ Context includes:
226
+ - Task instance stored under task_input_key
227
+ - Any extra_init_context values
213
228
  """
214
229
  logger.debug(f"Initializing context for workflow: {self.name}")
215
230
  initial_context = {self.task_input_key: task, **dict(self.extra_init_context)}
@@ -230,7 +245,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
230
245
  Returns:
231
246
  Self: The workflow instance for method chaining.
232
247
  """
233
- self.provide_tools_to(self._instances)
248
+ self.provide_tools_to(i for i in self._instances if isinstance(i,ToolBoxUsage))
234
249
  return self
235
250
 
236
251
  def update_init_context(self, /, **kwargs) -> Self:
@@ -7,15 +7,16 @@ from typing import Generator, List, Optional, Self, Tuple, overload
7
7
 
8
8
  from fabricatio.models.generic import (
9
9
  AsPrompt,
10
- CensoredAble,
11
- Display,
10
+ Described,
12
11
  FinalizedDumpAble,
13
12
  Introspect,
13
+ Language,
14
14
  ModelHash,
15
15
  PersistentAble,
16
16
  ProposedUpdateAble,
17
17
  ResolveUpdateConflict,
18
18
  SequencePatch,
19
+ SketchedAble,
19
20
  )
20
21
 
21
22
 
@@ -30,7 +31,7 @@ class ReferringType(StrEnum):
30
31
  type RefKey = Tuple[str, Optional[str], Optional[str]]
31
32
 
32
33
 
33
- class ArticleRef(CensoredAble, ProposedUpdateAble):
34
+ class ArticleRef(ProposedUpdateAble):
34
35
  """Reference to a specific chapter, section or subsection within the article. You SHALL not refer to an article component that is external and not present within our own article.
35
36
 
36
37
  Examples:
@@ -104,12 +105,9 @@ class ArticleRef(CensoredAble, ProposedUpdateAble):
104
105
  return ReferringType.CHAPTER
105
106
 
106
107
 
107
- class ArticleMetaData(CensoredAble, Display):
108
+ class ArticleMetaData(SketchedAble, Described, Language):
108
109
  """Metadata for an article component."""
109
110
 
110
- description: str
111
- """Description of the research component in academic style."""
112
-
113
111
  support_to: List[ArticleRef]
114
112
  """List of references to other component of this articles that this component supports."""
115
113
  depend_on: List[ArticleRef]
@@ -120,6 +118,9 @@ class ArticleMetaData(CensoredAble, Display):
120
118
  title: str
121
119
  """Do not add any prefix or suffix to the title. should not contain special characters."""
122
120
 
121
+ expected_word_count: int
122
+ """Expected word count of this research component."""
123
+
123
124
 
124
125
  class ArticleRefSequencePatch(SequencePatch[ArticleRef]):
125
126
  """Patch for article refs."""
@@ -271,12 +272,9 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
271
272
  return ""
272
273
 
273
274
 
274
- class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
275
+ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, Language, ABC):
275
276
  """Base class for article outlines."""
276
277
 
277
- language: str
278
- """Written language of the article. SHALL be aligned to the language of the article proposal provided."""
279
-
280
278
  title: str
281
279
  """Title of the academic paper."""
282
280
 
@@ -376,12 +374,31 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
376
374
  return component, summary
377
375
  return None
378
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
+
379
381
  @overload
380
382
  def find_illegal_ref(self, gather_identical: bool) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]: ...
381
383
 
382
384
  @overload
383
385
  def find_illegal_ref(self) -> Optional[Tuple[ArticleRef, str]]: ...
384
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
+
385
402
  def find_illegal_ref(self, gather_identical: bool = False) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]:
386
403
  """Finds the first illegal component in the outline.
387
404
 
@@ -389,21 +406,68 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
389
406
  Tuple[ArticleOutlineBase, str]: A tuple containing the illegal component and an error message.
390
407
  """
391
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
+
392
413
  for component in self.iter_dfs_rev():
393
414
  for ref in chain(component.depend_on, component.support_to):
394
415
  if not ref.deref(self):
395
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"
396
- if summary and not gather_identical:
397
- return ref, summary
398
- if summary and gather_identical:
399
- return [
400
- identical_ref
401
- for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
402
- if identical_ref == ref
403
- ], 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
+ )
404
440
 
405
441
  return None
406
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
+
407
471
  def finalized_dump(self) -> str:
408
472
  """Generates standardized hierarchical markup for academic publishing systems.
409
473