fabricatio 0.2.11.dev2__cp312-cp312-win_amd64.whl → 0.2.12__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/actions/article.py +20 -4
- fabricatio/actions/article_rag.py +176 -73
- fabricatio/actions/output.py +43 -2
- fabricatio/capabilities/advanced_rag.py +56 -0
- fabricatio/capabilities/rag.py +4 -4
- fabricatio/config.py +3 -3
- fabricatio/fs/curd.py +1 -1
- fabricatio/models/action.py +18 -13
- fabricatio/models/extra/aricle_rag.py +42 -19
- fabricatio/models/extra/article_base.py +79 -37
- fabricatio/models/extra/article_main.py +89 -45
- fabricatio/models/extra/article_outline.py +41 -3
- fabricatio/models/generic.py +10 -6
- fabricatio/models/kwargs_types.py +1 -1
- fabricatio/models/role.py +5 -4
- fabricatio/models/task.py +13 -1
- fabricatio/models/usages.py +1 -1
- fabricatio/rust.cp312-win_amd64.pyd +0 -0
- fabricatio/rust.pyi +34 -1
- fabricatio/utils.py +5 -5
- fabricatio-0.2.12.data/scripts/tdown.exe +0 -0
- {fabricatio-0.2.11.dev2.dist-info → fabricatio-0.2.12.dist-info}/METADATA +4 -2
- {fabricatio-0.2.11.dev2.dist-info → fabricatio-0.2.12.dist-info}/RECORD +25 -24
- fabricatio-0.2.11.dev2.data/scripts/tdown.exe +0 -0
- {fabricatio-0.2.11.dev2.dist-info → fabricatio-0.2.12.dist-info}/WHEEL +0 -0
- {fabricatio-0.2.11.dev2.dist-info → fabricatio-0.2.12.dist-info}/licenses/LICENSE +0 -0
fabricatio/models/action.py
CHANGED
@@ -12,12 +12,13 @@ Classes:
|
|
12
12
|
import traceback
|
13
13
|
from abc import abstractmethod
|
14
14
|
from asyncio import Queue, create_task
|
15
|
-
from typing import Any, Dict, Self, Tuple, Type, Union, final
|
15
|
+
from typing import Any, Dict, Self, Sequence, Tuple, Type, Union, final
|
16
16
|
|
17
17
|
from fabricatio.journal import logger
|
18
18
|
from fabricatio.models.generic import WithBriefing
|
19
19
|
from fabricatio.models.task import Task
|
20
|
-
from fabricatio.models.usages import
|
20
|
+
from fabricatio.models.usages import ToolBoxUsage
|
21
|
+
from fabricatio.utils import override_kwargs
|
21
22
|
from pydantic import Field, PrivateAttr
|
22
23
|
|
23
24
|
OUTPUT_KEY = "task_output"
|
@@ -25,7 +26,7 @@ OUTPUT_KEY = "task_output"
|
|
25
26
|
INPUT_KEY = "task_input"
|
26
27
|
|
27
28
|
|
28
|
-
class Action(WithBriefing
|
29
|
+
class Action(WithBriefing):
|
29
30
|
"""Class that represents an action to be executed in a workflow.
|
30
31
|
|
31
32
|
Actions are the atomic units of work in a workflow. Each action performs
|
@@ -55,7 +56,7 @@ class Action(WithBriefing, LLMUsage):
|
|
55
56
|
self.description = self.description or self.__class__.__doc__ or ""
|
56
57
|
|
57
58
|
@abstractmethod
|
58
|
-
async def _execute(self, *_:Any, **cxt) -> Any:
|
59
|
+
async def _execute(self, *_: Any, **cxt) -> Any:
|
59
60
|
"""Implement the core logic of the action.
|
60
61
|
|
61
62
|
Args:
|
@@ -95,11 +96,12 @@ class Action(WithBriefing, LLMUsage):
|
|
95
96
|
return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
|
96
97
|
return f"# The action you are going to perform: \n{super().briefing}"
|
97
98
|
|
98
|
-
def to_task_output(self)->Self:
|
99
|
+
def to_task_output(self, task_output_key: str = OUTPUT_KEY) -> Self:
|
99
100
|
"""Set the output key to OUTPUT_KEY and return the action instance."""
|
100
|
-
self.output_key=
|
101
|
+
self.output_key = task_output_key
|
101
102
|
return self
|
102
103
|
|
104
|
+
|
103
105
|
class WorkFlow(WithBriefing, ToolBoxUsage):
|
104
106
|
"""Manages sequences of actions to fulfill tasks.
|
105
107
|
|
@@ -121,9 +123,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
121
123
|
_instances: Tuple[Action, ...] = PrivateAttr(default_factory=tuple)
|
122
124
|
"""Instantiated action objects to be executed in this workflow."""
|
123
125
|
|
124
|
-
steps:
|
125
|
-
frozen=True,
|
126
|
-
)
|
126
|
+
steps: Sequence[Union[Type[Action], Action]] = Field(frozen=True)
|
127
127
|
"""The sequence of actions to be executed, can be action classes or instances."""
|
128
128
|
|
129
129
|
task_input_key: str = Field(default=INPUT_KEY)
|
@@ -177,7 +177,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
177
177
|
current_action = None
|
178
178
|
try:
|
179
179
|
# Process each action in sequence
|
180
|
-
for i,step in enumerate(self._instances):
|
180
|
+
for i, step in enumerate(self._instances):
|
181
181
|
current_action = step.name
|
182
182
|
logger.info(f"Executing step [{i}] >> {current_action}")
|
183
183
|
|
@@ -227,8 +227,13 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
227
227
|
- Any extra_init_context values
|
228
228
|
"""
|
229
229
|
logger.debug(f"Initializing context for workflow: {self.name}")
|
230
|
-
|
231
|
-
|
230
|
+
ctx = override_kwargs(self.extra_init_context, **task.extra_init_context)
|
231
|
+
if self.task_input_key in ctx:
|
232
|
+
raise ValueError(
|
233
|
+
f"Task input key: `{self.task_input_key}`, which is reserved, is already set in the init context"
|
234
|
+
)
|
235
|
+
|
236
|
+
await self._context.put({self.task_input_key: task, **ctx})
|
232
237
|
|
233
238
|
def steps_fallback_to_self(self) -> Self:
|
234
239
|
"""Configure all steps to use this workflow's configuration as fallback.
|
@@ -245,7 +250,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
|
|
245
250
|
Returns:
|
246
251
|
Self: The workflow instance for method chaining.
|
247
252
|
"""
|
248
|
-
self.provide_tools_to(i for i in self._instances if isinstance(i,ToolBoxUsage))
|
253
|
+
self.provide_tools_to(i for i in self._instances if isinstance(i, ToolBoxUsage))
|
249
254
|
return self
|
250
255
|
|
251
256
|
def update_init_context(self, /, **kwargs) -> Self:
|
@@ -9,7 +9,7 @@ from fabricatio.journal import logger
|
|
9
9
|
from fabricatio.models.extra.rag import MilvusDataBase
|
10
10
|
from fabricatio.models.generic import AsPrompt
|
11
11
|
from fabricatio.models.kwargs_types import ChunkKwargs
|
12
|
-
from fabricatio.rust import BibManager,
|
12
|
+
from fabricatio.rust import BibManager, blake3_hash, split_into_chunks
|
13
13
|
from fabricatio.utils import ok
|
14
14
|
from more_itertools.recipes import flatten, unique
|
15
15
|
from pydantic import Field
|
@@ -53,7 +53,7 @@ class ArticleChunk(MilvusDataBase, AsPrompt):
|
|
53
53
|
|
54
54
|
def _as_prompt_inner(self) -> Dict[str, str]:
|
55
55
|
return {
|
56
|
-
f"[[{ok(self._cite_number, 'You need to update cite number first.')}]] reference `{self.article_title}`": self.chunk
|
56
|
+
f"[[{ok(self._cite_number, 'You need to update cite number first.')}]] reference `{self.article_title}` from {self.as_auther_seq()}": self.chunk
|
57
57
|
}
|
58
58
|
|
59
59
|
@property
|
@@ -139,15 +139,9 @@ class ArticleChunk(MilvusDataBase, AsPrompt):
|
|
139
139
|
return re.sub(r"\[[\d\s,\\~–-]+]", "", string)
|
140
140
|
|
141
141
|
@property
|
142
|
-
def
|
143
|
-
"""Get the
|
144
|
-
|
145
|
-
for n in self.authors:
|
146
|
-
if is_chinese(n):
|
147
|
-
ret.append(n[0])
|
148
|
-
else:
|
149
|
-
ret.append(n.split()[-1])
|
150
|
-
return ret
|
142
|
+
def auther_lastnames(self) -> List[str]:
|
143
|
+
"""Get the last name of the authors."""
|
144
|
+
return [n.split()[-1] for n in self.authors]
|
151
145
|
|
152
146
|
def as_auther_seq(self) -> str:
|
153
147
|
"""Get the auther sequence."""
|
@@ -155,13 +149,13 @@ class ArticleChunk(MilvusDataBase, AsPrompt):
|
|
155
149
|
case 0:
|
156
150
|
raise ValueError("No authors found")
|
157
151
|
case 1:
|
158
|
-
return f"({self.
|
152
|
+
return f"({self.auther_lastnames[0]},{self.year}){self.as_typst_cite()}"
|
159
153
|
case 2:
|
160
|
-
return f"({self.
|
154
|
+
return f"({self.auther_lastnames[0]}{self.and_word}{self.auther_lastnames[1]},{self.year}){self.as_typst_cite()}"
|
161
155
|
case 3:
|
162
|
-
return f"({self.
|
156
|
+
return f"({self.auther_lastnames[0]},{self.auther_lastnames[1]}{self.and_word}{self.auther_lastnames[2]},{self.year}){self.as_typst_cite()}"
|
163
157
|
case _:
|
164
|
-
return f"({self.
|
158
|
+
return f"({self.auther_lastnames[0]},{self.auther_lastnames[1]}{self.and_word}{self.auther_lastnames[2]}{self.etc_word},{self.year}){self.as_typst_cite()}"
|
165
159
|
|
166
160
|
def update_cite_number(self, cite_number: int) -> Self:
|
167
161
|
"""Update the cite number."""
|
@@ -182,20 +176,32 @@ class CitationManager(AsPrompt):
|
|
182
176
|
abbr_sep: str = "-"
|
183
177
|
"""Separator for abbreviated citation numbers."""
|
184
178
|
|
185
|
-
def update_chunks(
|
179
|
+
def update_chunks(
|
180
|
+
self, article_chunks: List[ArticleChunk], set_cite_number: bool = True, dedup: bool = True
|
181
|
+
) -> Self:
|
186
182
|
"""Update article chunks."""
|
187
183
|
self.article_chunks.clear()
|
188
184
|
self.article_chunks.extend(article_chunks)
|
185
|
+
if dedup:
|
186
|
+
self.article_chunks = list(unique(self.article_chunks, lambda c: blake3_hash(c.chunk.encode())))
|
189
187
|
if set_cite_number:
|
190
188
|
self.set_cite_number_all()
|
191
189
|
return self
|
192
190
|
|
193
|
-
def
|
191
|
+
def empty(self) -> Self:
|
192
|
+
"""Empty the article chunks."""
|
193
|
+
self.article_chunks.clear()
|
194
|
+
return self
|
195
|
+
|
196
|
+
def add_chunks(self, article_chunks: List[ArticleChunk], set_cite_number: bool = True, dedup: bool = True) -> Self:
|
194
197
|
"""Add article chunks."""
|
195
198
|
self.article_chunks.extend(article_chunks)
|
199
|
+
if dedup:
|
200
|
+
self.article_chunks = list(unique(self.article_chunks, lambda c: blake3_hash(c.chunk.encode())))
|
196
201
|
if set_cite_number:
|
197
202
|
self.set_cite_number_all()
|
198
203
|
return self
|
204
|
+
|
199
205
|
def set_cite_number_all(self) -> Self:
|
200
206
|
"""Set citation numbers for all article chunks."""
|
201
207
|
for i, a in enumerate(self.article_chunks, 1):
|
@@ -208,7 +214,7 @@ class CitationManager(AsPrompt):
|
|
208
214
|
|
209
215
|
def apply(self, string: str) -> str:
|
210
216
|
"""Apply citation replacements to the input string."""
|
211
|
-
for origin,m in re.findall(self.pat, string):
|
217
|
+
for origin, m in re.findall(self.pat, string):
|
212
218
|
logger.info(f"Matching citation: {m}")
|
213
219
|
notations = self.convert_to_numeric_notations(m)
|
214
220
|
logger.info(f"Citing Notations: {notations}")
|
@@ -216,9 +222,26 @@ class CitationManager(AsPrompt):
|
|
216
222
|
logger.info(f"Citation Number Sequence: {citation_number_seq}")
|
217
223
|
dedup = self.deduplicate_citation(citation_number_seq)
|
218
224
|
logger.info(f"Deduplicated Citation Number Sequence: {dedup}")
|
219
|
-
string=string.replace(origin, self.unpack_cite_seq(dedup))
|
225
|
+
string = string.replace(origin, self.unpack_cite_seq(dedup))
|
220
226
|
return string
|
221
227
|
|
228
|
+
def citation_count(self, string: str) -> int:
|
229
|
+
"""Get the citation count in the string."""
|
230
|
+
count = 0
|
231
|
+
for _, m in re.findall(self.pat, string):
|
232
|
+
logger.info(f"Matching citation: {m}")
|
233
|
+
notations = self.convert_to_numeric_notations(m)
|
234
|
+
logger.info(f"Citing Notations: {notations}")
|
235
|
+
citation_number_seq = list(flatten(self.decode_expr(n) for n in notations))
|
236
|
+
logger.info(f"Citation Number Sequence: {citation_number_seq}")
|
237
|
+
count += len(dedup := self.deduplicate_citation(citation_number_seq))
|
238
|
+
logger.info(f"Deduplicated Citation Number Sequence: {dedup}")
|
239
|
+
return count
|
240
|
+
|
241
|
+
def citation_coverage(self, string: str) -> float:
|
242
|
+
"""Get the citation coverage in the string."""
|
243
|
+
return self.citation_count(string) / len(self.article_chunks)
|
244
|
+
|
222
245
|
def decode_expr(self, string: str) -> List[int]:
|
223
246
|
"""Decode citation expression into a list of integers."""
|
224
247
|
if self.abbr_sep in string:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""A foundation for hierarchical document components with dependency tracking."""
|
2
2
|
|
3
|
-
from abc import ABC
|
3
|
+
from abc import ABC
|
4
4
|
from enum import StrEnum
|
5
5
|
from typing import Generator, List, Optional, Self, Tuple
|
6
6
|
|
@@ -18,7 +18,8 @@ from fabricatio.models.generic import (
|
|
18
18
|
Titled,
|
19
19
|
WordCount,
|
20
20
|
)
|
21
|
-
from fabricatio.rust import
|
21
|
+
from fabricatio.rust import split_out_metadata, to_metadata, word_count
|
22
|
+
from fabricatio.utils import fallback_kwargs
|
22
23
|
from pydantic import Field
|
23
24
|
|
24
25
|
|
@@ -46,14 +47,49 @@ class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
|
|
46
47
|
aims: List[str]
|
47
48
|
"""List of writing aims of the research component in academic style."""
|
48
49
|
|
50
|
+
@property
|
51
|
+
def typst_metadata_comment(self) -> str:
|
52
|
+
"""Generates a comment for the metadata of the article component."""
|
53
|
+
return to_metadata(self.model_dump(include={"description", "aims", "expected_word_count"}, by_alias=True))
|
54
|
+
|
55
|
+
|
56
|
+
class FromTypstCode(ArticleMetaData):
|
57
|
+
"""Base class for article components that can be created from a Typst code snippet."""
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
|
61
|
+
"""Converts a Typst code snippet into an article component."""
|
62
|
+
data, body = split_out_metadata(body)
|
63
|
+
|
64
|
+
return cls(
|
65
|
+
heading=title,
|
66
|
+
**fallback_kwargs(
|
67
|
+
data or {},
|
68
|
+
elaboration="",
|
69
|
+
expected_word_count=word_count(body),
|
70
|
+
aims=[],
|
71
|
+
),
|
72
|
+
**kwargs,
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
class ToTypstCode(ArticleMetaData):
|
77
|
+
"""Base class for article components that can be converted to a Typst code snippet."""
|
78
|
+
|
79
|
+
def to_typst_code(self) -> str:
|
80
|
+
"""Converts the component into a Typst code snippet for rendering."""
|
81
|
+
return f"{self.title}\n{self.typst_metadata_comment}\n"
|
82
|
+
|
49
83
|
|
50
84
|
class ArticleOutlineBase(
|
51
|
-
ArticleMetaData,
|
52
85
|
ResolveUpdateConflict,
|
53
86
|
ProposedUpdateAble,
|
54
87
|
PersistentAble,
|
55
88
|
ModelHash,
|
56
89
|
Introspect,
|
90
|
+
FromTypstCode,
|
91
|
+
ToTypstCode,
|
92
|
+
ABC,
|
57
93
|
):
|
58
94
|
"""Base class for article outlines."""
|
59
95
|
|
@@ -79,23 +115,13 @@ class ArticleOutlineBase(
|
|
79
115
|
"""Updates the current instance with the attributes of another instance."""
|
80
116
|
return self.update_metadata(other)
|
81
117
|
|
82
|
-
@abstractmethod
|
83
|
-
def to_typst_code(self) -> str:
|
84
|
-
"""Converts the component into a Typst code snippet for rendering."""
|
85
|
-
|
86
118
|
|
87
119
|
class SubSectionBase(ArticleOutlineBase):
|
88
120
|
"""Base class for article sections and subsections."""
|
89
121
|
|
90
122
|
def to_typst_code(self) -> str:
|
91
123
|
"""Converts the component into a Typst code snippet for rendering."""
|
92
|
-
return (
|
93
|
-
f"=== {self.title}\n"
|
94
|
-
f"{comment(f'Desc:\n{self.description}\nAims:\n{"\n".join(self.aims)}')}\n"
|
95
|
-
+ f"Expected Word Count:{self.expected_word_count}"
|
96
|
-
if self.expected_word_count
|
97
|
-
else ""
|
98
|
-
)
|
124
|
+
return f"=== {super().to_typst_code()}"
|
99
125
|
|
100
126
|
def introspect(self) -> str:
|
101
127
|
"""Introspects the article subsection outline."""
|
@@ -120,13 +146,7 @@ class SectionBase[T: SubSectionBase](ArticleOutlineBase):
|
|
120
146
|
Returns:
|
121
147
|
str: The formatted Typst code snippet.
|
122
148
|
"""
|
123
|
-
return (
|
124
|
-
f"== {self.title}\n"
|
125
|
-
f"{comment(f'Desc:\n{self.description}\nAims:\n{"\n".join(self.aims)}')}\n"
|
126
|
-
+ f"Expected Word Count:{self.expected_word_count}"
|
127
|
-
if self.expected_word_count
|
128
|
-
else ""
|
129
|
-
) + "\n\n".join(subsec.to_typst_code() for subsec in self.subsections)
|
149
|
+
return f"== {super().to_typst_code()}" + "\n\n".join(subsec.to_typst_code() for subsec in self.subsections)
|
130
150
|
|
131
151
|
def resolve_update_conflict(self, other: Self) -> str:
|
132
152
|
"""Resolve update errors in the article outline."""
|
@@ -169,13 +189,7 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
|
|
169
189
|
|
170
190
|
def to_typst_code(self) -> str:
|
171
191
|
"""Converts the chapter into a Typst formatted code snippet for rendering."""
|
172
|
-
return (
|
173
|
-
f"= {self.title}\n"
|
174
|
-
f"{comment(f'Desc:\n{self.description}\nAims:\n{"\n".join(self.aims)}')}\n"
|
175
|
-
+ f"Expected Word Count:{self.expected_word_count}"
|
176
|
-
if self.expected_word_count
|
177
|
-
else ""
|
178
|
-
) + "\n\n".join(sec.to_typst_code() for sec in self.sections)
|
192
|
+
return f"= {super().to_typst_code()}" + "\n\n".join(sec.to_typst_code() for sec in self.sections)
|
179
193
|
|
180
194
|
def resolve_update_conflict(self, other: Self) -> str:
|
181
195
|
"""Resolve update errors in the article outline."""
|
@@ -207,12 +221,13 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
|
|
207
221
|
return ""
|
208
222
|
|
209
223
|
|
210
|
-
class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt,
|
224
|
+
class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, ToTypstCode, ABC):
|
211
225
|
"""Base class for article outlines."""
|
212
226
|
|
213
|
-
|
214
|
-
|
215
|
-
|
227
|
+
description: str = Field(
|
228
|
+
alias="elaboration",
|
229
|
+
)
|
230
|
+
"""The abstract of this article, which serves as a concise summary of an academic article, encapsulating its core purpose, methodologies, key results,
|
216
231
|
and conclusions while enabling readers to rapidly assess the relevance and significance of the study.
|
217
232
|
Functioning as the article's distilled essence, it succinctly articulates the research problem, objectives,
|
218
233
|
and scope, providing a roadmap for the full text while also facilitating database indexing, literature reviews,
|
@@ -297,6 +312,10 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, WordCount, Descri
|
|
297
312
|
for _, _, subsec in self.iter_subsections():
|
298
313
|
yield subsec.title
|
299
314
|
|
315
|
+
def to_typst_code(self) -> str:
|
316
|
+
"""Generates the Typst code representation of the article."""
|
317
|
+
return f"// #{super().to_typst_code()}\n\n" + "\n\n".join(a.to_typst_code() for a in self.chapters)
|
318
|
+
|
300
319
|
def finalized_dump(self) -> str:
|
301
320
|
"""Generates standardized hierarchical markup for academic publishing systems.
|
302
321
|
|
@@ -317,8 +336,31 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, WordCount, Descri
|
|
317
336
|
=== Implementation Details
|
318
337
|
== Evaluation Protocol
|
319
338
|
"""
|
320
|
-
return
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
339
|
+
return self.to_typst_code()
|
340
|
+
|
341
|
+
def avg_chap_wordcount[S: "ArticleBase"](self: S) -> S:
|
342
|
+
"""Set all chap have same word count sum up to be `self.expected_word_count`."""
|
343
|
+
avg = int(self.expected_word_count / len(self.chapters))
|
344
|
+
for c in self.chapters:
|
345
|
+
c.expected_word_count = avg
|
346
|
+
return self
|
347
|
+
|
348
|
+
def avg_sec_wordcount[S: "ArticleBase"](self: S) -> S:
|
349
|
+
"""Set all sec have same word count sum up to be `self.expected_word_count`."""
|
350
|
+
for c in self.chapters:
|
351
|
+
avg = int(c.expected_word_count / len(c.sections))
|
352
|
+
for s in c.sections:
|
353
|
+
s.expected_word_count = avg
|
354
|
+
return self
|
355
|
+
|
356
|
+
def avg_subsec_wordcount[S: "ArticleBase"](self: S) -> S:
|
357
|
+
"""Set all subsec have same word count sum up to be `self.expected_word_count`."""
|
358
|
+
for _, s in self.iter_sections():
|
359
|
+
avg = int(s.expected_word_count / len(s.subsections))
|
360
|
+
for ss in s.subsections:
|
361
|
+
ss.expected_word_count = avg
|
362
|
+
return self
|
363
|
+
|
364
|
+
def avg_wordcount_recursive[S: "ArticleBase"](self: S) -> S:
|
365
|
+
"""Set all chap, sec, subsec have same word count sum up to be `self.expected_word_count`."""
|
366
|
+
return self.avg_chap_wordcount().avg_sec_wordcount().avg_subsec_wordcount()
|
@@ -12,13 +12,22 @@ from fabricatio.models.extra.article_base import (
|
|
12
12
|
SubSectionBase,
|
13
13
|
)
|
14
14
|
from fabricatio.models.extra.article_outline import (
|
15
|
+
ArticleChapterOutline,
|
15
16
|
ArticleOutline,
|
17
|
+
ArticleSectionOutline,
|
18
|
+
ArticleSubsectionOutline,
|
16
19
|
)
|
17
20
|
from fabricatio.models.generic import Described, PersistentAble, SequencePatch, SketchedAble, WithRef, WordCount
|
18
|
-
from fabricatio.rust import
|
21
|
+
from fabricatio.rust import (
|
22
|
+
convert_all_block_tex,
|
23
|
+
convert_all_inline_tex,
|
24
|
+
fix_misplaced_labels,
|
25
|
+
split_out_metadata,
|
26
|
+
word_count,
|
27
|
+
)
|
19
28
|
from pydantic import Field, NonNegativeInt
|
20
29
|
|
21
|
-
PARAGRAPH_SEP = "// - -
|
30
|
+
PARAGRAPH_SEP = "\n\n// - - -\n\n"
|
22
31
|
|
23
32
|
|
24
33
|
class Paragraph(SketchedAble, WordCount, Described):
|
@@ -89,17 +98,17 @@ class ArticleSubsection(SubSectionBase):
|
|
89
98
|
Returns:
|
90
99
|
str: Typst code snippet for rendering.
|
91
100
|
"""
|
92
|
-
return
|
101
|
+
return super().to_typst_code() + PARAGRAPH_SEP.join(p.content for p in self.paragraphs)
|
93
102
|
|
94
103
|
@classmethod
|
95
|
-
def from_typst_code(cls, title: str, body: str) -> Self:
|
104
|
+
def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
|
96
105
|
"""Creates an Article object from the given Typst code."""
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
106
|
+
_, para_body = split_out_metadata(body)
|
107
|
+
|
108
|
+
return super().from_typst_code(
|
109
|
+
title,
|
110
|
+
body,
|
111
|
+
paragraphs=[Paragraph.from_content(p) for p in para_body.split(PARAGRAPH_SEP)],
|
103
112
|
)
|
104
113
|
|
105
114
|
|
@@ -107,16 +116,14 @@ class ArticleSection(SectionBase[ArticleSubsection]):
|
|
107
116
|
"""Atomic argumentative unit with high-level specificity."""
|
108
117
|
|
109
118
|
@classmethod
|
110
|
-
def from_typst_code(cls, title: str, body: str) -> Self:
|
119
|
+
def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
|
111
120
|
"""Creates an Article object from the given Typst code."""
|
112
|
-
return
|
121
|
+
return super().from_typst_code(
|
122
|
+
title,
|
123
|
+
body,
|
113
124
|
subsections=[
|
114
125
|
ArticleSubsection.from_typst_code(*pack) for pack in extract_sections(body, level=3, section_char="=")
|
115
126
|
],
|
116
|
-
heading=title,
|
117
|
-
elaboration="",
|
118
|
-
expected_word_count=word_count(body),
|
119
|
-
aims=[],
|
120
127
|
)
|
121
128
|
|
122
129
|
|
@@ -124,21 +131,18 @@ class ArticleChapter(ChapterBase[ArticleSection]):
|
|
124
131
|
"""Thematic progression implementing research function."""
|
125
132
|
|
126
133
|
@classmethod
|
127
|
-
def from_typst_code(cls, title: str, body: str) -> Self:
|
134
|
+
def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
|
128
135
|
"""Creates an Article object from the given Typst code."""
|
129
|
-
return
|
136
|
+
return super().from_typst_code(
|
137
|
+
title,
|
138
|
+
body,
|
130
139
|
sections=[
|
131
140
|
ArticleSection.from_typst_code(*pack) for pack in extract_sections(body, level=2, section_char="=")
|
132
141
|
],
|
133
|
-
heading=title,
|
134
|
-
elaboration="",
|
135
|
-
expected_word_count=word_count(body),
|
136
|
-
aims=[],
|
137
142
|
)
|
138
143
|
|
139
144
|
|
140
145
|
class Article(
|
141
|
-
SketchedAble,
|
142
146
|
WithRef[ArticleOutline],
|
143
147
|
PersistentAble,
|
144
148
|
ArticleBase[ArticleChapter],
|
@@ -157,30 +161,70 @@ class Article(
|
|
157
161
|
"Original Article": self.display(),
|
158
162
|
}
|
159
163
|
|
160
|
-
def convert_tex(self) -> Self:
|
164
|
+
def convert_tex(self, paragraphs: bool = True, descriptions: bool = True) -> Self:
|
161
165
|
"""Convert tex to typst code."""
|
162
|
-
|
163
|
-
for
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
p.content
|
174
|
-
.replace(r" \) ", "$")
|
175
|
-
.replace("\\[\n", "$$\n")
|
176
|
-
.replace("\n\\]", "\n$$")
|
177
|
-
)
|
166
|
+
if descriptions:
|
167
|
+
for a in self.iter_dfs():
|
168
|
+
a.description = fix_misplaced_labels(a.description)
|
169
|
+
a.description = convert_all_inline_tex(a.description)
|
170
|
+
a.description = convert_all_block_tex(a.description)
|
171
|
+
|
172
|
+
if paragraphs:
|
173
|
+
for _, _, subsec in self.iter_subsections():
|
174
|
+
for p in subsec.paragraphs:
|
175
|
+
p.content = fix_misplaced_labels(p.content)
|
176
|
+
p.content = convert_all_inline_tex(p.content)
|
177
|
+
p.content = convert_all_block_tex(p.content)
|
178
178
|
return self
|
179
179
|
|
180
180
|
@override
|
181
181
|
def iter_subsections(self) -> Generator[Tuple[ArticleChapter, ArticleSection, ArticleSubsection], None, None]:
|
182
182
|
return super().iter_subsections() # pyright: ignore [reportReturnType]
|
183
183
|
|
184
|
+
def extrac_outline(self) -> ArticleOutline:
|
185
|
+
"""Extract outline from article."""
|
186
|
+
# Create an empty list to hold chapter outlines
|
187
|
+
chapters = []
|
188
|
+
|
189
|
+
# Iterate through each chapter in the article
|
190
|
+
for chapter in self.chapters:
|
191
|
+
# Create an empty list to hold section outlines
|
192
|
+
sections = []
|
193
|
+
|
194
|
+
# Iterate through each section in the chapter
|
195
|
+
for section in chapter.sections:
|
196
|
+
# Create an empty list to hold subsection outlines
|
197
|
+
subsections = []
|
198
|
+
|
199
|
+
# Iterate through each subsection in the section
|
200
|
+
for subsection in section.subsections:
|
201
|
+
# Create a subsection outline and add it to the list
|
202
|
+
subsections.append(
|
203
|
+
ArticleSubsectionOutline(**subsection.model_dump(exclude={"paragraphs"}, by_alias=True))
|
204
|
+
)
|
205
|
+
|
206
|
+
# Create a section outline and add it to the list
|
207
|
+
sections.append(
|
208
|
+
ArticleSectionOutline(
|
209
|
+
**section.model_dump(exclude={"subsections"}, by_alias=True),
|
210
|
+
subsections=subsections,
|
211
|
+
)
|
212
|
+
)
|
213
|
+
|
214
|
+
# Create a chapter outline and add it to the list
|
215
|
+
chapters.append(
|
216
|
+
ArticleChapterOutline(
|
217
|
+
**chapter.model_dump(exclude={"sections"}, by_alias=True),
|
218
|
+
sections=sections,
|
219
|
+
)
|
220
|
+
)
|
221
|
+
|
222
|
+
# Create and return the article outline
|
223
|
+
return ArticleOutline(
|
224
|
+
**self.model_dump(exclude={"chapters"}, by_alias=True),
|
225
|
+
chapters=chapters,
|
226
|
+
)
|
227
|
+
|
184
228
|
@classmethod
|
185
229
|
def from_outline(cls, outline: ArticleOutline) -> "Article":
|
186
230
|
"""Generates an article from the given outline.
|
@@ -218,15 +262,14 @@ class Article(
|
|
218
262
|
return article
|
219
263
|
|
220
264
|
@classmethod
|
221
|
-
def from_typst_code(cls, title: str, body: str) -> Self:
|
265
|
+
def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
|
222
266
|
"""Generates an article from the given Typst code."""
|
223
|
-
return
|
267
|
+
return super().from_typst_code(
|
268
|
+
title,
|
269
|
+
body,
|
224
270
|
chapters=[
|
225
271
|
ArticleChapter.from_typst_code(*pack) for pack in extract_sections(body, level=1, section_char="=")
|
226
272
|
],
|
227
|
-
heading=title,
|
228
|
-
expected_word_count=word_count(body),
|
229
|
-
abstract="",
|
230
273
|
)
|
231
274
|
|
232
275
|
@classmethod
|
@@ -248,3 +291,4 @@ class Article(
|
|
248
291
|
|
249
292
|
for a in self.iter_dfs():
|
250
293
|
a.title = await text(f"Edit `{a.title}`.", default=a.title).ask_async() or a.title
|
294
|
+
return self
|