fabricatio 0.2.8.dev4__cp312-cp312-win_amd64.whl → 0.2.9__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.
- fabricatio/__init__.py +4 -11
- fabricatio/actions/__init__.py +1 -0
- fabricatio/actions/article.py +98 -110
- fabricatio/actions/article_rag.py +15 -10
- fabricatio/actions/output.py +60 -4
- fabricatio/actions/rag.py +2 -1
- fabricatio/actions/rules.py +72 -0
- fabricatio/capabilities/__init__.py +1 -0
- fabricatio/capabilities/censor.py +23 -6
- fabricatio/capabilities/check.py +46 -27
- fabricatio/capabilities/correct.py +35 -16
- fabricatio/capabilities/rag.py +5 -4
- fabricatio/capabilities/rating.py +56 -49
- fabricatio/capabilities/review.py +1 -1
- fabricatio/capabilities/task.py +2 -1
- fabricatio/config.py +5 -3
- fabricatio/fs/readers.py +20 -1
- fabricatio/models/action.py +59 -36
- fabricatio/models/extra/__init__.py +1 -0
- fabricatio/models/extra/advanced_judge.py +4 -4
- fabricatio/models/extra/article_base.py +124 -61
- fabricatio/models/extra/article_main.py +100 -17
- fabricatio/models/extra/article_outline.py +2 -3
- fabricatio/models/extra/article_proposal.py +15 -14
- fabricatio/models/extra/patches.py +17 -4
- fabricatio/models/extra/problem.py +31 -23
- fabricatio/models/extra/rule.py +39 -8
- fabricatio/models/generic.py +369 -78
- fabricatio/models/task.py +1 -1
- fabricatio/models/tool.py +149 -14
- fabricatio/models/usages.py +46 -42
- fabricatio/parser.py +5 -5
- fabricatio/rust.cp312-win_amd64.pyd +0 -0
- fabricatio/{_rust.pyi → rust.pyi} +42 -4
- fabricatio/{_rust_instances.py → rust_instances.py} +1 -1
- fabricatio/utils.py +5 -5
- fabricatio/workflows/__init__.py +1 -0
- fabricatio/workflows/articles.py +3 -5
- fabricatio-0.2.9.data/scripts/tdown.exe +0 -0
- {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/METADATA +1 -1
- fabricatio-0.2.9.dist-info/RECORD +61 -0
- fabricatio/_rust.cp312-win_amd64.pyd +0 -0
- fabricatio-0.2.8.dev4.data/scripts/tdown.exe +0 -0
- fabricatio-0.2.8.dev4.dist-info/RECORD +0 -56
- {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/WHEEL +0 -0
- {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/licenses/LICENSE +0 -0
fabricatio/models/action.py
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
-
"""Module that contains the classes for
|
1
|
+
"""Module that contains the classes for defining and executing task workflows.
|
2
2
|
|
3
|
-
This module
|
4
|
-
|
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
|
@@ -15,6 +20,10 @@ from fabricatio.models.task import Task
|
|
15
20
|
from fabricatio.models.usages import LLMUsage, ToolBoxUsage
|
16
21
|
from pydantic import Field, PrivateAttr
|
17
22
|
|
23
|
+
OUTPUT_KEY = "task_output"
|
24
|
+
|
25
|
+
INPUT_KEY = "task_input"
|
26
|
+
|
18
27
|
|
19
28
|
class Action(WithBriefing, LLMUsage):
|
20
29
|
"""Class that represents an action to be executed in a workflow.
|
@@ -46,28 +55,26 @@ class Action(WithBriefing, LLMUsage):
|
|
46
55
|
self.description = self.description or self.__class__.__doc__ or ""
|
47
56
|
|
48
57
|
@abstractmethod
|
49
|
-
async def _execute(self, *_, **cxt) -> Any:
|
50
|
-
"""
|
51
|
-
|
52
|
-
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.
|
53
60
|
|
54
61
|
Args:
|
55
|
-
**cxt:
|
62
|
+
**cxt: Context dictionary containing input/output data.
|
56
63
|
|
57
64
|
Returns:
|
58
|
-
|
65
|
+
Result of the action execution to be stored in context.
|
59
66
|
"""
|
60
67
|
pass
|
61
68
|
|
62
69
|
@final
|
63
70
|
async def act(self, cxt: Dict[str, Any]) -> Dict[str, Any]:
|
64
|
-
"""
|
71
|
+
"""Execute action and update context.
|
65
72
|
|
66
73
|
Args:
|
67
|
-
cxt:
|
74
|
+
cxt (Dict[str, Any]): Shared context dictionary.
|
68
75
|
|
69
76
|
Returns:
|
70
|
-
|
77
|
+
Updated context dictionary with new/modified entries.
|
71
78
|
"""
|
72
79
|
ret = await self._execute(**cxt)
|
73
80
|
|
@@ -79,21 +86,30 @@ class Action(WithBriefing, LLMUsage):
|
|
79
86
|
|
80
87
|
@property
|
81
88
|
def briefing(self) -> str:
|
82
|
-
"""
|
89
|
+
"""Generate formatted action description with personality context.
|
83
90
|
|
84
91
|
Returns:
|
85
|
-
|
92
|
+
Briefing text combining personality and action description.
|
86
93
|
"""
|
87
94
|
if self.personality:
|
88
95
|
return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
|
89
96
|
return f"# The action you are going to perform: \n{super().briefing}"
|
90
97
|
|
98
|
+
def to_task_output(self)->Self:
|
99
|
+
"""Set the output key to OUTPUT_KEY and return the action instance."""
|
100
|
+
self.output_key=OUTPUT_KEY
|
101
|
+
return self
|
91
102
|
|
92
103
|
class WorkFlow(WithBriefing, ToolBoxUsage):
|
93
|
-
"""
|
104
|
+
"""Manages sequences of actions to fulfill tasks.
|
94
105
|
|
95
|
-
|
96
|
-
|
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.
|
97
113
|
"""
|
98
114
|
|
99
115
|
description: str = ""
|
@@ -110,10 +126,10 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
110
126
|
)
|
111
127
|
"""The sequence of actions to be executed, can be action classes or instances."""
|
112
128
|
|
113
|
-
task_input_key: str = Field(default=
|
129
|
+
task_input_key: str = Field(default=INPUT_KEY)
|
114
130
|
"""Key used to store the input task in the context dictionary."""
|
115
131
|
|
116
|
-
task_output_key: str = Field(default=
|
132
|
+
task_output_key: str = Field(default=OUTPUT_KEY)
|
117
133
|
"""Key used to extract the final result from the context dictionary."""
|
118
134
|
|
119
135
|
extra_init_context: Dict[str, Any] = Field(default_factory=dict, frozen=True)
|
@@ -129,26 +145,29 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
129
145
|
self._instances = tuple(step if isinstance(step, Action) else step() for step in self.steps)
|
130
146
|
|
131
147
|
def inject_personality(self, personality: str) -> Self:
|
132
|
-
"""Set
|
148
|
+
"""Set personality for actions without existing personality.
|
133
149
|
|
134
150
|
Args:
|
135
|
-
personality:
|
151
|
+
personality (str): Shared personality context
|
136
152
|
|
137
153
|
Returns:
|
138
|
-
|
154
|
+
Workflow instance with updated actions
|
139
155
|
"""
|
140
156
|
for action in filter(lambda a: not a.personality, self._instances):
|
141
157
|
action.personality = personality
|
142
158
|
return self
|
143
159
|
|
144
160
|
async def serve(self, task: Task) -> None:
|
145
|
-
"""Execute
|
146
|
-
|
147
|
-
This method manages the complete lifecycle of processing a task through
|
148
|
-
the workflow's sequence of actions.
|
161
|
+
"""Execute workflow to complete given task.
|
149
162
|
|
150
163
|
Args:
|
151
|
-
task:
|
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
|
152
171
|
"""
|
153
172
|
logger.info(f"Start execute workflow: {self.name}")
|
154
173
|
|
@@ -158,27 +177,27 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
158
177
|
current_action = None
|
159
178
|
try:
|
160
179
|
# Process each action in sequence
|
161
|
-
for step in self._instances:
|
180
|
+
for i,step in enumerate(self._instances):
|
162
181
|
current_action = step.name
|
163
|
-
logger.info(f"Executing step >> {current_action}")
|
182
|
+
logger.info(f"Executing step [{i}] >> {current_action}")
|
164
183
|
|
165
184
|
# Get current context and execute action
|
166
185
|
context = await self._context.get()
|
167
186
|
act_task = create_task(step.act(context))
|
168
187
|
# Handle task cancellation
|
169
188
|
if task.is_cancelled():
|
170
|
-
logger.warning(f"
|
189
|
+
logger.warning(f"Workflow cancelled by task: {task.name}")
|
171
190
|
act_task.cancel(f"Cancelled by task: {task.name}")
|
172
191
|
break
|
173
192
|
|
174
193
|
# Update context with modified values
|
175
194
|
modified_ctx = await act_task
|
176
|
-
logger.success(f"Step
|
195
|
+
logger.success(f"Step [{i}] `{current_action}` execution finished.")
|
177
196
|
if step.output_key:
|
178
|
-
logger.success(f"Setting output to `{step.output_key}`")
|
197
|
+
logger.success(f"Setting action `{current_action}` output to `{step.output_key}`")
|
179
198
|
await self._context.put(modified_ctx)
|
180
199
|
|
181
|
-
logger.success(f"Workflow
|
200
|
+
logger.success(f"Workflow `{self.name}` execution finished.")
|
182
201
|
|
183
202
|
# Get final context and extract result
|
184
203
|
final_ctx = await self._context.get()
|
@@ -198,10 +217,14 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
198
217
|
await task.fail()
|
199
218
|
|
200
219
|
async def _init_context[T](self, task: Task[T]) -> None:
|
201
|
-
"""Initialize
|
220
|
+
"""Initialize workflow execution context.
|
202
221
|
|
203
222
|
Args:
|
204
|
-
task:
|
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
|
205
228
|
"""
|
206
229
|
logger.debug(f"Initializing context for workflow: {self.name}")
|
207
230
|
initial_context = {self.task_input_key: task, **dict(self.extra_init_context)}
|
@@ -222,7 +245,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
222
245
|
Returns:
|
223
246
|
Self: The workflow instance for method chaining.
|
224
247
|
"""
|
225
|
-
self.provide_tools_to(self._instances)
|
248
|
+
self.provide_tools_to(i for i in self._instances if isinstance(i,ToolBoxUsage))
|
226
249
|
return self
|
227
250
|
|
228
251
|
def update_init_context(self, /, **kwargs) -> Self:
|
@@ -0,0 +1 @@
|
|
1
|
+
"""A module contains extra models for fabricatio."""
|
@@ -2,17 +2,17 @@
|
|
2
2
|
|
3
3
|
from typing import List
|
4
4
|
|
5
|
-
from fabricatio.models.generic import
|
5
|
+
from fabricatio.models.generic import SketchedAble
|
6
6
|
|
7
7
|
|
8
|
-
class JudgeMent(
|
8
|
+
class JudgeMent(SketchedAble):
|
9
9
|
"""Represents a judgment result containing supporting/denying evidence and final verdict.
|
10
10
|
|
11
11
|
The class stores both affirmative and denies evidence, truth and reasons lists along with the final boolean judgment.
|
12
12
|
"""
|
13
13
|
|
14
14
|
issue_to_judge: str
|
15
|
-
"""The issue to be judged"""
|
15
|
+
"""The issue to be judged, including the original question and context"""
|
16
16
|
|
17
17
|
deny_evidence: List[str]
|
18
18
|
"""List of clues supporting the denial."""
|
@@ -21,7 +21,7 @@ class JudgeMent(ProposedAble, Display):
|
|
21
21
|
"""List of clues supporting the affirmation."""
|
22
22
|
|
23
23
|
final_judgement: bool
|
24
|
-
"""The final judgment made according to all extracted clues."""
|
24
|
+
"""The final judgment made according to all extracted clues. true for the `issue_to_judge` is correct and false for incorrect."""
|
25
25
|
|
26
26
|
def __bool__(self) -> bool:
|
27
27
|
"""Return the final judgment value.
|
@@ -7,16 +7,20 @@ from typing import Generator, List, Optional, Self, Tuple, overload
|
|
7
7
|
|
8
8
|
from fabricatio.models.generic import (
|
9
9
|
AsPrompt,
|
10
|
-
|
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,
|
20
|
+
Titled,
|
21
|
+
WordCount,
|
19
22
|
)
|
23
|
+
from pydantic import Field
|
20
24
|
|
21
25
|
|
22
26
|
class ReferringType(StrEnum):
|
@@ -30,51 +34,51 @@ class ReferringType(StrEnum):
|
|
30
34
|
type RefKey = Tuple[str, Optional[str], Optional[str]]
|
31
35
|
|
32
36
|
|
33
|
-
class ArticleRef(
|
37
|
+
class ArticleRef(ProposedUpdateAble):
|
34
38
|
"""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
39
|
|
36
40
|
Examples:
|
37
41
|
- Referring to a chapter titled `Introduction`:
|
38
42
|
Using Python
|
39
43
|
```python
|
40
|
-
ArticleRef(
|
44
|
+
ArticleRef(chap="Introduction")
|
41
45
|
```
|
42
46
|
Using JSON
|
43
47
|
```json
|
44
|
-
{
|
48
|
+
{chap="Introduction"}
|
45
49
|
```
|
46
50
|
- Referring to a section titled `Background` under the `Introduction` chapter:
|
47
51
|
Using Python
|
48
52
|
```python
|
49
|
-
ArticleRef(
|
53
|
+
ArticleRef(chap="Introduction", sec="Background")
|
50
54
|
```
|
51
55
|
Using JSON
|
52
56
|
```json
|
53
|
-
{
|
57
|
+
{chap="Introduction", sec="Background"}
|
54
58
|
```
|
55
59
|
- Referring to a subsection titled `Related Work` under the `Background` section of the `Introduction` chapter:
|
56
60
|
Using Python
|
57
61
|
```python
|
58
|
-
ArticleRef(
|
62
|
+
ArticleRef(chap="Introduction", sec="Background", subsec="Related Work")
|
59
63
|
```
|
60
64
|
Using JSON
|
61
65
|
```json
|
62
|
-
{
|
66
|
+
{chap="Introduction", sec="Background", subsec="Related Work"}
|
63
67
|
```
|
64
68
|
"""
|
65
69
|
|
66
|
-
|
70
|
+
chap: str
|
67
71
|
"""`title` Field of the referenced chapter"""
|
68
|
-
|
72
|
+
sec: Optional[str] = None
|
69
73
|
"""`title` Field of the referenced section."""
|
70
|
-
|
74
|
+
subsec: Optional[str] = None
|
71
75
|
"""`title` Field of the referenced subsection."""
|
72
76
|
|
73
77
|
def update_from_inner(self, other: Self) -> Self:
|
74
78
|
"""Updates the current instance with the attributes of another instance."""
|
75
|
-
self.
|
76
|
-
self.
|
77
|
-
self.
|
79
|
+
self.chap = other.chap
|
80
|
+
self.sec = other.sec
|
81
|
+
self.subsec = other.subsec
|
78
82
|
return self
|
79
83
|
|
80
84
|
def deref(self, article: "ArticleBase") -> Optional["ArticleOutlineBase"]:
|
@@ -86,39 +90,41 @@ class ArticleRef(CensoredAble, ProposedUpdateAble):
|
|
86
90
|
Returns:
|
87
91
|
ArticleMainBase | ArticleOutline | None: The dereferenced section or subsection, or None if not found.
|
88
92
|
"""
|
89
|
-
chap = next((chap for chap in article.chapters if chap.title == self.
|
90
|
-
if self.
|
93
|
+
chap = next((chap for chap in article.chapters if chap.title == self.chap), None)
|
94
|
+
if self.sec is None or chap is None:
|
91
95
|
return chap
|
92
|
-
sec = next((sec for sec in chap.sections if sec.title == self.
|
93
|
-
if self.
|
96
|
+
sec = next((sec for sec in chap.sections if sec.title == self.sec), None)
|
97
|
+
if self.subsec is None or sec is None:
|
94
98
|
return sec
|
95
|
-
return next((subsec for subsec in sec.subsections if subsec.title == self.
|
99
|
+
return next((subsec for subsec in sec.subsections if subsec.title == self.subsec), None)
|
96
100
|
|
97
101
|
@property
|
98
102
|
def referring_type(self) -> ReferringType:
|
99
103
|
"""Determine the type of reference based on the presence of specific attributes."""
|
100
|
-
if self.
|
104
|
+
if self.subsec is not None:
|
101
105
|
return ReferringType.SUBSECTION
|
102
|
-
if self.
|
106
|
+
if self.sec is not None:
|
103
107
|
return ReferringType.SECTION
|
104
108
|
return ReferringType.CHAPTER
|
105
109
|
|
106
110
|
|
107
|
-
class ArticleMetaData(
|
111
|
+
class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
|
108
112
|
"""Metadata for an article component."""
|
109
113
|
|
110
|
-
description: str
|
111
|
-
|
114
|
+
description: str = Field(
|
115
|
+
alias="elaboration",
|
116
|
+
description=Described.model_fields["description"].description,
|
117
|
+
)
|
112
118
|
|
113
|
-
|
114
|
-
"""List of references to other component of this articles that this component supports."""
|
115
|
-
depend_on: List[ArticleRef]
|
116
|
-
"""List of references to other component of this articles that this component depends on."""
|
119
|
+
title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
|
117
120
|
|
118
|
-
|
121
|
+
aims: List[str]
|
119
122
|
"""List of writing aims of the research component in academic style."""
|
120
|
-
|
121
|
-
|
123
|
+
|
124
|
+
support_to: List[ArticleRef]
|
125
|
+
"""List of references to other future components in this article that this component supports to."""
|
126
|
+
depend_on: List[ArticleRef]
|
127
|
+
"""List of references to other previous components in this article that this component depends on."""
|
122
128
|
|
123
129
|
|
124
130
|
class ArticleRefSequencePatch(SequencePatch[ArticleRef]):
|
@@ -146,8 +152,8 @@ class ArticleOutlineBase(
|
|
146
152
|
self.support_to.extend(other.support_to)
|
147
153
|
self.depend_on.clear()
|
148
154
|
self.depend_on.extend(other.depend_on)
|
149
|
-
self.
|
150
|
-
self.
|
155
|
+
self.aims.clear()
|
156
|
+
self.aims.extend(other.aims)
|
151
157
|
self.description = other.description
|
152
158
|
return self
|
153
159
|
|
@@ -271,25 +277,19 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
|
|
271
277
|
return ""
|
272
278
|
|
273
279
|
|
274
|
-
class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
|
280
|
+
class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, WordCount, Described, Titled, Language, ABC):
|
275
281
|
"""Base class for article outlines."""
|
276
282
|
|
277
|
-
|
278
|
-
|
283
|
+
title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
|
284
|
+
description: str = Field(alias="abstract")
|
285
|
+
"""The abstract serves as a concise summary of an academic article, encapsulating its core purpose, methodologies, key results,
|
286
|
+
and conclusions while enabling readers to rapidly assess the relevance and significance of the study.
|
287
|
+
Functioning as the article's distilled essence, it succinctly articulates the research problem, objectives,
|
288
|
+
and scope, providing a roadmap for the full text while also facilitating database indexing, literature reviews,
|
289
|
+
and citation tracking through standardized metadata. Additionally, it acts as an accessibility gateway,
|
290
|
+
allowing scholars to gauge the study's contribution to existing knowledge, its methodological rigor,
|
291
|
+
and its broader implications without engaging with the entire manuscript, thereby optimizing scholarly communication efficiency."""
|
279
292
|
|
280
|
-
title: str
|
281
|
-
"""Title of the academic paper."""
|
282
|
-
|
283
|
-
prospect: str
|
284
|
-
"""Consolidated research statement with four pillars:
|
285
|
-
1. Problem Identification: Current limitations
|
286
|
-
2. Methodological Response: Technical approach
|
287
|
-
3. Empirical Validation: Evaluation strategy
|
288
|
-
4. Scholarly Impact: Field contributions
|
289
|
-
"""
|
290
|
-
|
291
|
-
abstract: str
|
292
|
-
"""The abstract is a concise summary of the academic paper's main findings."""
|
293
293
|
chapters: List[T]
|
294
294
|
"""Chapters of the article. Contains at least one chapter. You can also add more as needed."""
|
295
295
|
|
@@ -376,34 +376,97 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
|
|
376
376
|
return component, summary
|
377
377
|
return None
|
378
378
|
|
379
|
-
|
380
|
-
|
379
|
+
def gather_introspected(self) -> Optional[str]:
|
380
|
+
"""Gathers all introspected components in the article structure."""
|
381
|
+
return "\n".join([i for component in self.chapters if (i := component.introspect())])
|
382
|
+
|
383
|
+
|
384
|
+
def iter_chap_title(self) -> Generator[str, None, None]:
|
385
|
+
"""Iterates through all chapter titles in the article."""
|
386
|
+
for chap in self.chapters:
|
387
|
+
yield chap.title
|
388
|
+
|
389
|
+
def iter_section_title(self) -> Generator[str, None, None]:
|
390
|
+
"""Iterates through all section titles in the article."""
|
391
|
+
for _, sec in self.iter_sections():
|
392
|
+
yield sec.title
|
393
|
+
|
394
|
+
def iter_subsection_title(self) -> Generator[str, None, None]:
|
395
|
+
"""Iterates through all subsection titles in the article."""
|
396
|
+
for _, _, subsec in self.iter_subsections():
|
397
|
+
yield subsec.title
|
381
398
|
|
382
399
|
@overload
|
383
|
-
def find_illegal_ref(self) -> Optional[Tuple[ArticleRef, str]]: ...
|
400
|
+
def find_illegal_ref(self, gather_identical: bool) -> Optional[Tuple[ArticleOutlineBase,ArticleRef | List[ArticleRef], str]]: ...
|
384
401
|
|
385
|
-
|
402
|
+
@overload
|
403
|
+
def find_illegal_ref(self) -> Optional[Tuple[ArticleOutlineBase,ArticleRef, str]]: ...
|
404
|
+
def find_illegal_ref(
|
405
|
+
self, gather_identical: bool = False
|
406
|
+
) -> Optional[Tuple[ArticleOutlineBase, ArticleRef | List[ArticleRef], str]]:
|
386
407
|
"""Finds the first illegal component in the outline.
|
387
408
|
|
388
409
|
Returns:
|
389
410
|
Tuple[ArticleOutlineBase, str]: A tuple containing the illegal component and an error message.
|
390
411
|
"""
|
391
412
|
summary = ""
|
413
|
+
chap_titles_set = set(self.iter_chap_title())
|
414
|
+
sec_titles_set = set(self.iter_section_title())
|
415
|
+
subsec_titles_set = set(self.iter_subsection_title())
|
416
|
+
|
392
417
|
for component in self.iter_dfs_rev():
|
393
418
|
for ref in chain(component.depend_on, component.support_to):
|
394
419
|
if not ref.deref(self):
|
395
420
|
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
|
-
|
397
|
-
|
421
|
+
|
422
|
+
if ref.chap not in (chap_titles_set):
|
423
|
+
summary += f"Chapter titled `{ref.chap}` is not any of {chap_titles_set}\n"
|
424
|
+
if ref.sec and ref.sec not in (sec_titles_set):
|
425
|
+
summary += f"Section Titled `{ref.sec}` is not any of {sec_titles_set}\n"
|
426
|
+
if ref.subsec and ref.subsec not in (subsec_titles_set):
|
427
|
+
summary += f"Subsection Titled `{ref.subsec}` is not any of {subsec_titles_set}"
|
428
|
+
|
398
429
|
if summary and gather_identical:
|
399
|
-
return
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
430
|
+
return (
|
431
|
+
component,
|
432
|
+
[
|
433
|
+
identical_ref
|
434
|
+
for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
|
435
|
+
if identical_ref == ref
|
436
|
+
],
|
437
|
+
summary,
|
438
|
+
)
|
439
|
+
if summary:
|
440
|
+
return component, ref, summary
|
404
441
|
|
405
442
|
return None
|
406
443
|
|
444
|
+
def gather_illegal_ref(self) -> Tuple[List[ArticleRef], str]:
|
445
|
+
"""Gathers all illegal references in the article."""
|
446
|
+
summary = []
|
447
|
+
chap_titles_set = set(self.iter_chap_title())
|
448
|
+
sec_titles_set = set(self.iter_section_title())
|
449
|
+
subsec_titles_set = set(self.iter_subsection_title())
|
450
|
+
res_seq = []
|
451
|
+
|
452
|
+
for component in self.iter_dfs():
|
453
|
+
for ref in (
|
454
|
+
r for r in chain(component.depend_on, component.support_to) if not r.deref(self) and r not in res_seq
|
455
|
+
):
|
456
|
+
res_seq.append(ref)
|
457
|
+
if ref.chap not in chap_titles_set:
|
458
|
+
summary.append(
|
459
|
+
f"Chapter titled `{ref.chap}` is not exist, since it is not any of {chap_titles_set}."
|
460
|
+
)
|
461
|
+
if ref.sec and (ref.sec not in sec_titles_set):
|
462
|
+
summary.append(f"Section Titled `{ref.sec}` is not exist, since it is not any of {sec_titles_set}")
|
463
|
+
if ref.subsec and (ref.subsec not in subsec_titles_set):
|
464
|
+
summary.append(
|
465
|
+
f"Subsection Titled `{ref.subsec}` is not exist, since it is not any of {subsec_titles_set}"
|
466
|
+
)
|
467
|
+
|
468
|
+
return res_seq, "\n".join(summary)
|
469
|
+
|
407
470
|
def finalized_dump(self) -> str:
|
408
471
|
"""Generates standardized hierarchical markup for academic publishing systems.
|
409
472
|
|