fabricatio 0.2.6.dev2__cp312-cp312-win_amd64.whl → 0.2.7__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 +7 -24
- fabricatio/_rust.cp312-win_amd64.pyd +0 -0
- fabricatio/_rust.pyi +22 -0
- fabricatio/actions/article.py +168 -19
- fabricatio/actions/article_rag.py +35 -0
- fabricatio/actions/output.py +21 -6
- fabricatio/actions/rag.py +51 -3
- fabricatio/capabilities/correct.py +36 -4
- fabricatio/capabilities/rag.py +67 -16
- fabricatio/capabilities/rating.py +15 -6
- fabricatio/capabilities/review.py +7 -4
- fabricatio/capabilities/task.py +5 -5
- fabricatio/config.py +29 -21
- fabricatio/decorators.py +32 -0
- fabricatio/models/action.py +117 -43
- fabricatio/models/extra/article_base.py +378 -0
- fabricatio/models/extra/article_essence.py +226 -0
- fabricatio/models/extra/article_main.py +196 -0
- fabricatio/models/extra/article_outline.py +32 -0
- fabricatio/models/extra/article_proposal.py +35 -0
- fabricatio/models/generic.py +164 -14
- fabricatio/models/kwargs_types.py +40 -10
- fabricatio/models/role.py +30 -6
- fabricatio/models/tool.py +6 -2
- fabricatio/models/usages.py +94 -47
- fabricatio/models/utils.py +29 -2
- fabricatio/parser.py +2 -0
- fabricatio/workflows/articles.py +12 -1
- fabricatio-0.2.7.data/scripts/tdown.exe +0 -0
- fabricatio-0.2.7.dist-info/METADATA +181 -0
- fabricatio-0.2.7.dist-info/RECORD +47 -0
- {fabricatio-0.2.6.dev2.dist-info → fabricatio-0.2.7.dist-info}/WHEEL +1 -1
- fabricatio/models/extra.py +0 -171
- fabricatio-0.2.6.dev2.data/scripts/tdown.exe +0 -0
- fabricatio-0.2.6.dev2.dist-info/METADATA +0 -432
- fabricatio-0.2.6.dev2.dist-info/RECORD +0 -42
- {fabricatio-0.2.6.dev2.dist-info → fabricatio-0.2.7.dist-info}/licenses/LICENSE +0 -0
fabricatio/models/action.py
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
-
"""Module that contains the classes for actions and workflows.
|
1
|
+
"""Module that contains the classes for actions and workflows.
|
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.
|
5
|
+
"""
|
2
6
|
|
3
7
|
import traceback
|
4
8
|
from abc import abstractmethod
|
@@ -15,20 +19,27 @@ from pydantic import Field, PrivateAttr
|
|
15
19
|
|
16
20
|
|
17
21
|
class Action(HandleTask, ProposeTask, Correct):
|
18
|
-
"""Class that represents an action to be executed in a workflow.
|
22
|
+
"""Class that represents an action to be executed in a workflow.
|
23
|
+
|
24
|
+
Actions are the atomic units of work in a workflow. Each action performs
|
25
|
+
a specific operation and can modify the shared context data.
|
26
|
+
"""
|
19
27
|
|
20
28
|
name: str = Field(default="")
|
21
29
|
"""The name of the action."""
|
30
|
+
|
22
31
|
description: str = Field(default="")
|
23
32
|
"""The description of the action."""
|
33
|
+
|
24
34
|
personality: str = Field(default="")
|
25
|
-
"""The personality
|
35
|
+
"""The personality traits or context for the action executor."""
|
36
|
+
|
26
37
|
output_key: str = Field(default="")
|
27
|
-
"""The key
|
38
|
+
"""The key used to store this action's output in the context dictionary."""
|
28
39
|
|
29
40
|
@final
|
30
41
|
def model_post_init(self, __context: Any) -> None:
|
31
|
-
"""Initialize the action by setting
|
42
|
+
"""Initialize the action by setting default name and description if not provided.
|
32
43
|
|
33
44
|
Args:
|
34
45
|
__context: The context to be used for initialization.
|
@@ -37,122 +48,185 @@ class Action(HandleTask, ProposeTask, Correct):
|
|
37
48
|
self.description = self.description or self.__class__.__doc__ or ""
|
38
49
|
|
39
50
|
@abstractmethod
|
40
|
-
async def _execute(self, **cxt) -> Any:
|
41
|
-
"""Execute the action with the provided arguments.
|
51
|
+
async def _execute(self, *_, **cxt) -> Any: # noqa: ANN002
|
52
|
+
"""Execute the action logic with the provided context arguments.
|
53
|
+
|
54
|
+
This method must be implemented by subclasses to define the actual behavior.
|
42
55
|
|
43
56
|
Args:
|
44
57
|
**cxt: The context dictionary containing input and output data.
|
45
58
|
|
46
59
|
Returns:
|
47
|
-
The result of the action execution.
|
60
|
+
Any: The result of the action execution.
|
48
61
|
"""
|
49
62
|
pass
|
50
63
|
|
51
64
|
@final
|
52
65
|
async def act(self, cxt: Dict[str, Any]) -> Dict[str, Any]:
|
53
|
-
"""Perform the action
|
66
|
+
"""Perform the action and update the context with results.
|
54
67
|
|
55
68
|
Args:
|
56
69
|
cxt: The context dictionary containing input and output data.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Dict[str, Any]: The updated context dictionary.
|
57
73
|
"""
|
58
74
|
ret = await self._execute(**cxt)
|
75
|
+
|
59
76
|
if self.output_key:
|
60
77
|
logger.debug(f"Setting output: {self.output_key}")
|
61
78
|
cxt[self.output_key] = ret
|
79
|
+
|
62
80
|
return cxt
|
63
81
|
|
64
82
|
@property
|
65
83
|
def briefing(self) -> str:
|
66
|
-
"""Return a
|
84
|
+
"""Return a formatted description of the action including personality context if available.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
str: Formatted briefing text with personality and action description.
|
88
|
+
"""
|
67
89
|
if self.personality:
|
68
90
|
return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
|
69
91
|
return f"# The action you are going to perform: \n{super().briefing}"
|
70
92
|
|
71
93
|
|
72
94
|
class WorkFlow(WithBriefing, ToolBoxUsage):
|
73
|
-
"""Class that represents a
|
95
|
+
"""Class that represents a sequence of actions to be executed for a task.
|
96
|
+
|
97
|
+
A workflow manages the execution of multiple actions in sequence, passing
|
98
|
+
a shared context between them and handling task lifecycle events.
|
99
|
+
"""
|
74
100
|
|
75
101
|
_context: Queue[Dict[str, Any]] = PrivateAttr(default_factory=lambda: Queue(maxsize=1))
|
76
|
-
"""
|
102
|
+
"""Queue for storing the workflow execution context."""
|
77
103
|
|
78
104
|
_instances: Tuple[Action, ...] = PrivateAttr(default_factory=tuple)
|
79
|
-
"""
|
105
|
+
"""Instantiated action objects to be executed in this workflow."""
|
80
106
|
|
81
107
|
steps: Tuple[Union[Type[Action], Action], ...] = Field(...)
|
82
|
-
"""
|
108
|
+
"""The sequence of actions to be executed, can be action classes or instances."""
|
109
|
+
|
83
110
|
task_input_key: str = Field(default="task_input")
|
84
|
-
"""
|
111
|
+
"""Key used to store the input task in the context dictionary."""
|
112
|
+
|
85
113
|
task_output_key: str = Field(default="task_output")
|
86
|
-
"""
|
114
|
+
"""Key used to extract the final result from the context dictionary."""
|
115
|
+
|
87
116
|
extra_init_context: Dict[str, Any] = Field(default_factory=dict, frozen=True)
|
88
|
-
"""
|
117
|
+
"""Additional initial context values to be included at workflow start."""
|
89
118
|
|
90
119
|
def model_post_init(self, __context: Any) -> None:
|
91
|
-
"""Initialize the workflow by
|
120
|
+
"""Initialize the workflow by instantiating any action classes.
|
92
121
|
|
93
122
|
Args:
|
94
123
|
__context: The context to be used for initialization.
|
95
124
|
"""
|
96
|
-
|
97
|
-
for step in self.steps
|
98
|
-
temp.append(step if isinstance(step, Action) else step())
|
99
|
-
self._instances = tuple(temp)
|
125
|
+
# Convert any action classes to instances
|
126
|
+
self._instances = tuple(step if isinstance(step, Action) else step() for step in self.steps)
|
100
127
|
|
101
128
|
def inject_personality(self, personality: str) -> Self:
|
102
|
-
"""
|
129
|
+
"""Set the personality for all actions that don't have one defined.
|
103
130
|
|
104
131
|
Args:
|
105
|
-
personality: The personality to
|
132
|
+
personality: The personality text to inject.
|
106
133
|
|
107
134
|
Returns:
|
108
|
-
Self: The
|
135
|
+
Self: The workflow instance for method chaining.
|
109
136
|
"""
|
110
|
-
for
|
111
|
-
|
137
|
+
for action in filter(lambda a: not a.personality, self._instances):
|
138
|
+
action.personality = personality
|
112
139
|
return self
|
113
140
|
|
114
141
|
async def serve(self, task: Task) -> None:
|
115
|
-
"""
|
142
|
+
"""Execute the workflow to fulfill the given task.
|
143
|
+
|
144
|
+
This method manages the complete lifecycle of processing a task through
|
145
|
+
the workflow's sequence of actions.
|
116
146
|
|
117
147
|
Args:
|
118
|
-
task: The task to be
|
148
|
+
task: The task to be processed.
|
119
149
|
"""
|
150
|
+
logger.info(f"Start execute workflow: {self.name}")
|
151
|
+
|
120
152
|
await task.start()
|
121
153
|
await self._init_context(task)
|
154
|
+
|
122
155
|
current_action = None
|
123
156
|
try:
|
157
|
+
# Process each action in sequence
|
124
158
|
for step in self._instances:
|
125
|
-
|
126
|
-
|
159
|
+
current_action = step.name
|
160
|
+
logger.info(f"Executing step: {current_action}")
|
161
|
+
|
162
|
+
# Get current context and execute action
|
163
|
+
context = await self._context.get()
|
164
|
+
act_task = create_task(step.act(context))
|
165
|
+
# Handle task cancellation
|
127
166
|
if task.is_cancelled():
|
128
167
|
act_task.cancel(f"Cancelled by task: {task.name}")
|
129
168
|
break
|
169
|
+
|
170
|
+
# Update context with modified values
|
130
171
|
modified_ctx = await act_task
|
172
|
+
logger.success(f"Step execution finished: {current_action}")
|
131
173
|
await self._context.put(modified_ctx)
|
132
|
-
logger.info(f"Finished executing workflow: {self.name}")
|
133
174
|
|
134
|
-
|
175
|
+
logger.success(f"Workflow execution finished: {self.name}")
|
176
|
+
|
177
|
+
# Get final context and extract result
|
178
|
+
final_ctx = await self._context.get()
|
179
|
+
result = final_ctx.get(self.task_output_key)
|
180
|
+
|
181
|
+
if self.task_output_key not in final_ctx:
|
135
182
|
logger.warning(
|
136
|
-
f"Task output key: {self.task_output_key} not found in the context, None will be returned.
|
183
|
+
f"Task output key: {self.task_output_key} not found in the context, None will be returned. "
|
184
|
+
f"You can check if `Action.output_key` is set the same as `WorkFlow.task_output_key`."
|
137
185
|
)
|
138
186
|
|
139
|
-
await task.finish(
|
140
|
-
|
141
|
-
|
142
|
-
logger.
|
143
|
-
|
187
|
+
await task.finish(result)
|
188
|
+
|
189
|
+
except Exception as e: # noqa: BLE001
|
190
|
+
logger.critical(f"Error during task: {current_action} execution: {e}")
|
191
|
+
logger.critical(traceback.format_exc())
|
192
|
+
await task.fail()
|
144
193
|
|
145
194
|
async def _init_context[T](self, task: Task[T]) -> None:
|
146
|
-
"""Initialize the context dictionary for workflow execution.
|
195
|
+
"""Initialize the context dictionary for workflow execution.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
task: The task being served by this workflow.
|
199
|
+
"""
|
147
200
|
logger.debug(f"Initializing context for workflow: {self.name}")
|
148
|
-
|
201
|
+
initial_context = {self.task_input_key: task, **dict(self.extra_init_context)}
|
202
|
+
await self._context.put(initial_context)
|
149
203
|
|
150
204
|
def steps_fallback_to_self(self) -> Self:
|
151
|
-
"""
|
205
|
+
"""Configure all steps to use this workflow's configuration as fallback.
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
Self: The workflow instance for method chaining.
|
209
|
+
"""
|
152
210
|
self.hold_to(self._instances)
|
153
211
|
return self
|
154
212
|
|
155
213
|
def steps_supply_tools_from_self(self) -> Self:
|
156
|
-
"""
|
214
|
+
"""Provide this workflow's tools to all steps in the workflow.
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
Self: The workflow instance for method chaining.
|
218
|
+
"""
|
157
219
|
self.provide_tools_to(self._instances)
|
158
220
|
return self
|
221
|
+
|
222
|
+
def update_init_context(self, **kwargs) -> Self:
|
223
|
+
"""Update the initial context with additional key-value pairs.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
**kwargs: Key-value pairs to add to the initial context.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
Self: The workflow instance for method chaining.
|
230
|
+
"""
|
231
|
+
self.extra_init_context.update(kwargs)
|
232
|
+
return self
|
@@ -0,0 +1,378 @@
|
|
1
|
+
"""A foundation for hierarchical document components with dependency tracking."""
|
2
|
+
|
3
|
+
from abc import abstractmethod
|
4
|
+
from enum import StrEnum
|
5
|
+
from typing import Generator, List, Optional, Self, Tuple
|
6
|
+
|
7
|
+
from fabricatio.models.generic import (
|
8
|
+
CensoredAble,
|
9
|
+
Display,
|
10
|
+
FinalizedDumpAble,
|
11
|
+
Introspect,
|
12
|
+
ModelHash,
|
13
|
+
PersistentAble,
|
14
|
+
ProposedAble,
|
15
|
+
ResolveUpdateConflict,
|
16
|
+
UpdateFrom,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class ReferringType(StrEnum):
|
21
|
+
"""Enumeration of different types of references that can be made in an article."""
|
22
|
+
|
23
|
+
CHAPTER = "chapter"
|
24
|
+
SECTION = "section"
|
25
|
+
SUBSECTION = "subsection"
|
26
|
+
|
27
|
+
|
28
|
+
class ArticleRef(CensoredAble, Display):
|
29
|
+
"""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.
|
30
|
+
|
31
|
+
Examples:
|
32
|
+
- Referring to a chapter titled `Introduction`:
|
33
|
+
Using Python
|
34
|
+
```python
|
35
|
+
ArticleRef(referred_chapter_title="Introduction")
|
36
|
+
```
|
37
|
+
Using JSON
|
38
|
+
```json
|
39
|
+
{referred_chapter_title="Introduction"}
|
40
|
+
```
|
41
|
+
- Referring to a section titled `Background` under the `Introduction` chapter:
|
42
|
+
Using Python
|
43
|
+
```python
|
44
|
+
ArticleRef(referred_chapter_title="Introduction", referred_section_title="Background")
|
45
|
+
```
|
46
|
+
Using JSON
|
47
|
+
```json
|
48
|
+
{referred_chapter_title="Introduction", referred_section_title="Background"}
|
49
|
+
```
|
50
|
+
- Referring to a subsection titled `Related Work` under the `Background` section of the `Introduction` chapter:
|
51
|
+
Using Python
|
52
|
+
```python
|
53
|
+
ArticleRef(referred_chapter_title="Introduction", referred_section_title="Background", referred_subsection_title="Related Work")
|
54
|
+
```
|
55
|
+
Using JSON
|
56
|
+
```json
|
57
|
+
{referred_chapter_title="Introduction", referred_section_title="Background", referred_subsection_title="Related Work"}
|
58
|
+
```
|
59
|
+
"""
|
60
|
+
|
61
|
+
referred_chapter_title: str
|
62
|
+
"""`title` Field of the referenced chapter"""
|
63
|
+
referred_section_title: Optional[str] = None
|
64
|
+
"""`title` Field of the referenced section."""
|
65
|
+
referred_subsection_title: Optional[str] = None
|
66
|
+
"""`title` Field of the referenced subsection."""
|
67
|
+
|
68
|
+
def __hash__(self) -> int:
|
69
|
+
"""Overrides the default hash function to ensure consistent hashing across instances."""
|
70
|
+
return hash((self.referred_chapter_title, self.referred_section_title, self.referred_subsection_title))
|
71
|
+
|
72
|
+
def deref(self, article: "ArticleBase") -> Optional["ArticleOutlineBase"]:
|
73
|
+
"""Dereference the reference to the actual section or subsection within the provided article.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
article (ArticleOutline | Article): The article to dereference the reference from.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
ArticleMainBase | ArticleOutline | None: The dereferenced section or subsection, or None if not found.
|
80
|
+
"""
|
81
|
+
chap = next((chap for chap in article.chapters if chap.title == self.referred_chapter_title), None)
|
82
|
+
if self.referred_section_title is None or chap is None:
|
83
|
+
return chap
|
84
|
+
sec = next((sec for sec in chap.sections if sec.title == self.referred_section_title), None)
|
85
|
+
if self.referred_subsection_title is None or sec is None:
|
86
|
+
return sec
|
87
|
+
return next((subsec for subsec in sec.subsections if subsec.title == self.referred_subsection_title), None)
|
88
|
+
|
89
|
+
@property
|
90
|
+
def referring_type(self) -> ReferringType:
|
91
|
+
"""Determine the type of reference based on the presence of specific attributes."""
|
92
|
+
if self.referred_subsection_title is not None:
|
93
|
+
return ReferringType.SUBSECTION
|
94
|
+
if self.referred_section_title is not None:
|
95
|
+
return ReferringType.SECTION
|
96
|
+
return ReferringType.CHAPTER
|
97
|
+
|
98
|
+
|
99
|
+
class ArticleMetaData(CensoredAble, Display):
|
100
|
+
"""Metadata for an article component."""
|
101
|
+
|
102
|
+
description: str
|
103
|
+
"""Description of the research component in academic style."""
|
104
|
+
|
105
|
+
support_to: List[ArticleRef]
|
106
|
+
"""List of references to other component of this articles that this component supports."""
|
107
|
+
depend_on: List[ArticleRef]
|
108
|
+
"""List of references to other component of this articles that this component depends on."""
|
109
|
+
|
110
|
+
writing_aim: List[str]
|
111
|
+
"""List of writing aims of the research component in academic style."""
|
112
|
+
title: str
|
113
|
+
"""Do not add any prefix or suffix to the title. should not contain special characters."""
|
114
|
+
|
115
|
+
|
116
|
+
class ArticleOutlineBase(
|
117
|
+
ArticleMetaData,
|
118
|
+
UpdateFrom,
|
119
|
+
ResolveUpdateConflict,
|
120
|
+
ProposedAble,
|
121
|
+
PersistentAble,
|
122
|
+
ModelHash,
|
123
|
+
Introspect,
|
124
|
+
):
|
125
|
+
"""Base class for article outlines."""
|
126
|
+
|
127
|
+
@property
|
128
|
+
def metadata(self) -> ArticleMetaData:
|
129
|
+
"""Returns the metadata of the article component."""
|
130
|
+
return ArticleMetaData.model_validate(self, from_attributes=True)
|
131
|
+
|
132
|
+
def update_metadata(self, other: ArticleMetaData) -> Self:
|
133
|
+
"""Updates the metadata of the current instance with the attributes of another instance."""
|
134
|
+
self.support_to.clear()
|
135
|
+
self.support_to.extend(other.support_to)
|
136
|
+
self.depend_on.clear()
|
137
|
+
self.depend_on.extend(other.depend_on)
|
138
|
+
self.writing_aim.clear()
|
139
|
+
self.writing_aim.extend(other.writing_aim)
|
140
|
+
self.description = other.description
|
141
|
+
return self
|
142
|
+
|
143
|
+
def display_metadata(self) -> str:
|
144
|
+
"""Displays the metadata of the current instance."""
|
145
|
+
return self.model_dump_json(
|
146
|
+
indent=1, include={"title", "writing_aim", "description", "support_to", "depend_on"}
|
147
|
+
)
|
148
|
+
|
149
|
+
def update_from_inner(self, other: Self) -> Self:
|
150
|
+
"""Updates the current instance with the attributes of another instance."""
|
151
|
+
return self.update_metadata(other)
|
152
|
+
|
153
|
+
@abstractmethod
|
154
|
+
def to_typst_code(self) -> str:
|
155
|
+
"""Converts the component into a Typst code snippet for rendering."""
|
156
|
+
|
157
|
+
|
158
|
+
class SubSectionBase(ArticleOutlineBase):
|
159
|
+
"""Base class for article sections and subsections."""
|
160
|
+
|
161
|
+
def to_typst_code(self) -> str:
|
162
|
+
"""Converts the component into a Typst code snippet for rendering."""
|
163
|
+
return f"=== {self.title}\n"
|
164
|
+
|
165
|
+
def introspect(self) -> str:
|
166
|
+
"""Introspects the article subsection outline."""
|
167
|
+
return ""
|
168
|
+
|
169
|
+
def resolve_update_conflict(self, other: Self) -> str:
|
170
|
+
"""Resolve update errors in the article outline."""
|
171
|
+
if self.title != other.title:
|
172
|
+
return f"Title mismatched, expected `{self.title}`, got `{other.title}`"
|
173
|
+
return ""
|
174
|
+
|
175
|
+
|
176
|
+
class SectionBase[T: SubSectionBase](ArticleOutlineBase):
|
177
|
+
"""Base class for article sections and subsections."""
|
178
|
+
|
179
|
+
subsections: List[T]
|
180
|
+
"""Subsections of the section. Contains at least one subsection. You can also add more as needed."""
|
181
|
+
|
182
|
+
def to_typst_code(self) -> str:
|
183
|
+
"""Converts the section into a Typst formatted code snippet.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
str: The formatted Typst code snippet.
|
187
|
+
"""
|
188
|
+
return f"== {self.title}\n" + "\n\n".join(subsec.to_typst_code() for subsec in self.subsections)
|
189
|
+
|
190
|
+
def resolve_update_conflict(self, other: Self) -> str:
|
191
|
+
"""Resolve update errors in the article outline."""
|
192
|
+
out = ""
|
193
|
+
if self.title != other.title:
|
194
|
+
out += f"Title mismatched, expected `{self.title}`, got `{other.title}`"
|
195
|
+
if len(self.subsections) != len(other.subsections):
|
196
|
+
out += f"Section count mismatched, expected `{len(self.subsections)}`, got `{len(other.subsections)}`"
|
197
|
+
return out or "\n".join(
|
198
|
+
[
|
199
|
+
conf
|
200
|
+
for s, o in zip(self.subsections, other.subsections, strict=True)
|
201
|
+
if (conf := s.resolve_update_conflict(o))
|
202
|
+
]
|
203
|
+
)
|
204
|
+
|
205
|
+
def update_from_inner(self, other: Self) -> Self:
|
206
|
+
"""Updates the current instance with the attributes of another instance."""
|
207
|
+
super().update_from_inner(other)
|
208
|
+
if len(self.subsections) == 0:
|
209
|
+
self.subsections = other.subsections
|
210
|
+
return self
|
211
|
+
|
212
|
+
for self_subsec, other_subsec in zip(self.subsections, other.subsections, strict=True):
|
213
|
+
self_subsec.update_from(other_subsec)
|
214
|
+
return self
|
215
|
+
|
216
|
+
def introspect(self) -> str:
|
217
|
+
"""Introspects the article section outline."""
|
218
|
+
if len(self.subsections) == 0:
|
219
|
+
return f"Section `{self.title}` contains no subsections, expected at least one, but got 0, you can add one or more as needed."
|
220
|
+
return ""
|
221
|
+
|
222
|
+
|
223
|
+
class ChapterBase[T: SectionBase](ArticleOutlineBase):
|
224
|
+
"""Base class for article chapters."""
|
225
|
+
|
226
|
+
sections: List[T]
|
227
|
+
"""Sections of the chapter. Contains at least one section. You can also add more as needed."""
|
228
|
+
|
229
|
+
def to_typst_code(self) -> str:
|
230
|
+
"""Converts the chapter into a Typst formatted code snippet for rendering."""
|
231
|
+
return f"= {self.title}\n" + "\n\n".join(sec.to_typst_code() for sec in self.sections)
|
232
|
+
|
233
|
+
def resolve_update_conflict(self, other: Self) -> str:
|
234
|
+
"""Resolve update errors in the article outline."""
|
235
|
+
out = ""
|
236
|
+
|
237
|
+
if self.title != other.title:
|
238
|
+
out += f"Title mismatched, expected `{self.title}`, got `{other.title}`"
|
239
|
+
if len(self.sections) == len(other.sections):
|
240
|
+
out += f"Chapter count mismatched, expected `{len(self.sections)}`, got `{len(other.sections)}`"
|
241
|
+
|
242
|
+
return out or "\n".join(
|
243
|
+
[conf for s, o in zip(self.sections, other.sections, strict=True) if (conf := s.resolve_update_conflict(o))]
|
244
|
+
)
|
245
|
+
|
246
|
+
def update_from_inner(self, other: Self) -> Self:
|
247
|
+
"""Updates the current instance with the attributes of another instance."""
|
248
|
+
if len(self.sections) == 0:
|
249
|
+
self.sections = other.sections
|
250
|
+
return self
|
251
|
+
|
252
|
+
for self_sec, other_sec in zip(self.sections, other.sections, strict=True):
|
253
|
+
self_sec.update_from(other_sec)
|
254
|
+
return self
|
255
|
+
|
256
|
+
def introspect(self) -> str:
|
257
|
+
"""Introspects the article chapter outline."""
|
258
|
+
if len(self.sections) == 0:
|
259
|
+
return f"Chapter `{self.title}` contains no sections, expected at least one, but got 0, you can add one or more as needed."
|
260
|
+
return ""
|
261
|
+
|
262
|
+
|
263
|
+
class ArticleBase[T: ChapterBase](FinalizedDumpAble):
|
264
|
+
"""Base class for article outlines."""
|
265
|
+
|
266
|
+
language: str
|
267
|
+
"""Written language of the article. SHALL be aligned to the language of the article proposal provided."""
|
268
|
+
|
269
|
+
title: str
|
270
|
+
"""Title of the academic paper."""
|
271
|
+
|
272
|
+
prospect: str
|
273
|
+
"""Consolidated research statement with four pillars:
|
274
|
+
1. Problem Identification: Current limitations
|
275
|
+
2. Methodological Response: Technical approach
|
276
|
+
3. Empirical Validation: Evaluation strategy
|
277
|
+
4. Scholarly Impact: Field contributions
|
278
|
+
"""
|
279
|
+
|
280
|
+
abstract: str
|
281
|
+
"""The abstract is a concise summary of the academic paper's main findings."""
|
282
|
+
chapters: List[T]
|
283
|
+
"""Chapters of the article. Contains at least one chapter. You can also add more as needed."""
|
284
|
+
|
285
|
+
def iter_dfs_rev(
|
286
|
+
self,
|
287
|
+
) -> Generator[ArticleOutlineBase, None, None]:
|
288
|
+
"""Performs a depth-first search (DFS) through the article structure in reverse order.
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
Generator[ArticleMainBase]: Each component in the article structure in reverse order.
|
292
|
+
"""
|
293
|
+
for chap in self.chapters:
|
294
|
+
for sec in chap.sections:
|
295
|
+
yield from sec.subsections
|
296
|
+
yield sec
|
297
|
+
yield chap
|
298
|
+
|
299
|
+
def iter_dfs(self) -> Generator[ArticleOutlineBase, None, None]:
|
300
|
+
"""Performs a depth-first search (DFS) through the article structure.
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
Generator[ArticleMainBase]: Each component in the article structure.
|
304
|
+
"""
|
305
|
+
for chap in self.chapters:
|
306
|
+
yield chap
|
307
|
+
for sec in chap.sections:
|
308
|
+
yield sec
|
309
|
+
yield from sec.subsections
|
310
|
+
|
311
|
+
def iter_sections(self) -> Generator[Tuple[ChapterBase, SectionBase], None, None]:
|
312
|
+
"""Iterates through all sections in the article.
|
313
|
+
|
314
|
+
Returns:
|
315
|
+
Generator[ArticleOutlineBase]: Each section in the article.
|
316
|
+
"""
|
317
|
+
for chap in self.chapters:
|
318
|
+
for sec in chap.sections:
|
319
|
+
yield chap, sec
|
320
|
+
|
321
|
+
def iter_subsections(self) -> Generator[Tuple[ChapterBase, SectionBase, SubSectionBase], None, None]:
|
322
|
+
"""Iterates through all subsections in the article.
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
Generator[ArticleOutlineBase]: Each subsection in the article.
|
326
|
+
"""
|
327
|
+
for chap, sec in self.iter_sections():
|
328
|
+
for subsec in sec.subsections:
|
329
|
+
yield chap, sec, subsec
|
330
|
+
|
331
|
+
def find_introspected(self) -> Optional[Tuple[ArticleOutlineBase, str]]:
|
332
|
+
"""Finds the first introspected component in the article structure."""
|
333
|
+
summary = ""
|
334
|
+
for component in self.iter_dfs_rev():
|
335
|
+
summary += component.introspect()
|
336
|
+
if summary:
|
337
|
+
return component, summary
|
338
|
+
return None
|
339
|
+
|
340
|
+
def find_illegal_ref(self) -> Optional[Tuple[ArticleOutlineBase, str]]:
|
341
|
+
"""Finds the first illegal component in the outline.
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
Tuple[ArticleOutlineBase, str]: A tuple containing the illegal component and an error message.
|
345
|
+
"""
|
346
|
+
summary = ""
|
347
|
+
for component in self.iter_dfs_rev():
|
348
|
+
for ref in component.depend_on:
|
349
|
+
if not ref.deref(self):
|
350
|
+
summary += f"Invalid internal reference in {component.__class__.__name__} titled `{component.title}` at `depend_on` field, because the referred {ref.referring_type} is not exists within the article, see the original obj dump: {ref.model_dump()}\n"
|
351
|
+
for ref in component.support_to:
|
352
|
+
if not ref.deref(self):
|
353
|
+
summary += f"Invalid internal reference in {component.__class__.__name__} titled `{component.title}` at `support_to` field, because the referred {ref.referring_type} is not exists within the article, see the original obj dump: {ref.model_dump()}\n"
|
354
|
+
if summary:
|
355
|
+
return component, summary
|
356
|
+
return None
|
357
|
+
|
358
|
+
def finalized_dump(self) -> str:
|
359
|
+
"""Generates standardized hierarchical markup for academic publishing systems.
|
360
|
+
|
361
|
+
Implements ACL 2024 outline conventions with four-level structure:
|
362
|
+
= Chapter Title (Level 1)
|
363
|
+
== Section Title (Level 2)
|
364
|
+
=== Subsection Title (Level 3)
|
365
|
+
==== Subsubsection Title (Level 4)
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
str: Strictly formatted outline with academic sectioning
|
369
|
+
|
370
|
+
Example:
|
371
|
+
= Methodology
|
372
|
+
== Neural Architecture Search Framework
|
373
|
+
=== Differentiable Search Space
|
374
|
+
==== Constrained Optimization Parameters
|
375
|
+
=== Implementation Details
|
376
|
+
== Evaluation Protocol
|
377
|
+
"""
|
378
|
+
return "\n\n".join(a.to_typst_code() for a in self.chapters)
|