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.
@@ -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 LLMUsage, ToolBoxUsage
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, LLMUsage):
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=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: Tuple[Union[Type[Action], Action], ...] = Field(
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
- initial_context = {self.task_input_key: task, **dict(self.extra_init_context)}
231
- await self._context.put(initial_context)
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, is_chinese, split_into_chunks
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 auther_firstnames(self) -> List[str]:
143
- """Get the first name of the authors."""
144
- ret = []
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.auther_firstnames[0]},{self.year}){self.as_typst_cite()}"
152
+ return f"({self.auther_lastnames[0]},{self.year}){self.as_typst_cite()}"
159
153
  case 2:
160
- return f"({self.auther_firstnames[0]}{self.and_word}{self.auther_firstnames[1]},{self.year}){self.as_typst_cite()}"
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.auther_firstnames[0]},{self.auther_firstnames[1]}{self.and_word}{self.auther_firstnames[2]},{self.year}){self.as_typst_cite()}"
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.auther_firstnames[0]},{self.auther_firstnames[1]}{self.and_word}{self.auther_firstnames[2]}{self.etc_word},{self.year}){self.as_typst_cite()}"
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(self, article_chunks: List[ArticleChunk], set_cite_number: bool = True) -> Self:
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 add_chunks(self, article_chunks: List[ArticleChunk], set_cite_number: bool = True)-> Self:
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, abstractmethod
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 comment
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, WordCount, Described, Titled, Language, ABC):
224
+ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, ToTypstCode, ABC):
211
225
  """Base class for article outlines."""
212
226
 
213
- title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
214
- description: str = Field(alias="abstract")
215
- """The abstract serves as a concise summary of an academic article, encapsulating its core purpose, methodologies, key results,
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 comment(
321
- f"Title:{self.title}\nDesc:\n{self.description}\n" + f"Word Count:{self.expected_word_count}"
322
- if self.expected_word_count
323
- else ""
324
- ) + "\n\n".join(a.to_typst_code() for a in self.chapters)
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 convert_all_block_tex, convert_all_inline_tex, word_count
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 f"=== {self.title}\n" + f"\n{PARAGRAPH_SEP}\n".join(p.content for p in self.paragraphs)
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
- return cls(
98
- heading=title,
99
- elaboration="",
100
- paragraphs=[Paragraph.from_content(p) for p in body.split(PARAGRAPH_SEP)],
101
- expected_word_count=word_count(body),
102
- aims=[],
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 cls(
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 cls(
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
- for _, _, subsec in self.iter_subsections():
163
- for p in subsec.paragraphs:
164
- p.content = convert_all_inline_tex(p.content)
165
- p.content = convert_all_block_tex(p.content)
166
- return self
167
-
168
- def fix_wrapper(self) -> Self:
169
- """Fix wrapper."""
170
- for _, _, subsec in self.iter_subsections():
171
- for p in subsec.paragraphs:
172
- p.content = (
173
- p.content.replace(r" \( ", "$")
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 cls(
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