fabricatio 0.2.11.dev0__cp312-cp312-manylinux_2_34_x86_64.whl → 0.2.11.dev2__cp312-cp312-manylinux_2_34_x86_64.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.
@@ -5,8 +5,11 @@ from pathlib import Path
5
5
  from typing import Callable, List, Optional
6
6
 
7
7
  from more_itertools import filter_map
8
+ from pydantic import Field
9
+ from rich import print as r_print
8
10
 
9
11
  from fabricatio.capabilities.censor import Censor
12
+ from fabricatio.capabilities.extract import Extract
10
13
  from fabricatio.capabilities.propose import Propose
11
14
  from fabricatio.fs import safe_text_read
12
15
  from fabricatio.journal import logger
@@ -16,9 +19,10 @@ from fabricatio.models.extra.article_main import Article
16
19
  from fabricatio.models.extra.article_outline import ArticleOutline
17
20
  from fabricatio.models.extra.article_proposal import ArticleProposal
18
21
  from fabricatio.models.extra.rule import RuleSet
22
+ from fabricatio.models.kwargs_types import ValidateKwargs
19
23
  from fabricatio.models.task import Task
20
24
  from fabricatio.rust import BibManager, detect_language
21
- from fabricatio.utils import ok
25
+ from fabricatio.utils import ok, wrapp_in_block
22
26
 
23
27
 
24
28
  class ExtractArticleEssence(Action, Propose):
@@ -130,30 +134,45 @@ class GenerateArticleProposal(Action, Propose):
130
134
  ).update_ref(briefing)
131
135
 
132
136
 
133
- class GenerateInitialOutline(Action, Propose):
137
+ class GenerateInitialOutline(Action, Extract):
134
138
  """Generate the initial article outline based on the article proposal."""
135
139
 
136
140
  output_key: str = "initial_article_outline"
137
141
  """The key of the output data."""
138
142
 
143
+ supervisor: bool = False
144
+ """Whether to use the supervisor to fix the outline."""
145
+
146
+ extract_kwargs: ValidateKwargs[Optional[ArticleOutline]] = Field(default_factory=ValidateKwargs)
147
+ """The kwargs to extract the outline."""
148
+
139
149
  async def _execute(
140
150
  self,
141
151
  article_proposal: ArticleProposal,
142
152
  **_,
143
153
  ) -> Optional[ArticleOutline]:
144
154
  raw_outline = await self.aask(
145
- f"{(article_proposal.as_prompt())}\n\nNote that you should use `{article_proposal.language}` to write the `ArticleOutline`\n"
155
+ f"{(article_proposal.as_prompt())}\n"
146
156
  f"Design each chapter of a proper and academic and ready for release manner.\n"
147
157
  f"You Must make sure every chapter have sections, and every section have subsections.\n"
148
158
  f"Make the chapter and sections and subsections bing divided into a specific enough article component.\n"
149
- f"Every chapter must have sections, every section must have subsections.",
159
+ f"Every chapter must have sections, every section must have subsections.\n"
160
+ f"Note that you SHALL use `{article_proposal.language}` as written language",
150
161
  )
151
162
 
163
+ if self.supervisor:
164
+ from questionary import confirm, text
165
+
166
+ r_print(raw_outline)
167
+ while not await confirm("Accept this version and continue?", default=True).ask_async():
168
+ imp = await text("Enter the improvement:").ask_async()
169
+ raw_outline = await self.aask(
170
+ f"{article_proposal.as_prompt()}\n{wrapp_in_block(raw_outline, 'Previous ArticleOutline')}\n{imp}"
171
+ )
172
+ r_print(raw_outline)
173
+
152
174
  return ok(
153
- await self.propose(
154
- ArticleOutline,
155
- f"{raw_outline}\n\n\n\noutline provided above is the outline i need to extract to a JSON,",
156
- ),
175
+ await self.extract(ArticleOutline, raw_outline, **self.extract_kwargs),
157
176
  "Could not generate the initial outline.",
158
177
  ).update_ref(article_proposal)
159
178
 
@@ -6,8 +6,9 @@ from typing import List, Optional
6
6
 
7
7
  from fabricatio import BibManager
8
8
  from fabricatio.capabilities.censor import Censor
9
- from fabricatio.capabilities.propose import Propose
9
+ from fabricatio.capabilities.extract import Extract
10
10
  from fabricatio.capabilities.rag import RAG
11
+ from fabricatio.decorators import precheck_package
11
12
  from fabricatio.journal import logger
12
13
  from fabricatio.models.action import Action
13
14
  from fabricatio.models.extra.aricle_rag import ArticleChunk, CitationManager
@@ -15,18 +16,42 @@ from fabricatio.models.extra.article_essence import ArticleEssence
15
16
  from fabricatio.models.extra.article_main import Article, ArticleChapter, ArticleSection, ArticleSubsection
16
17
  from fabricatio.models.extra.article_outline import ArticleOutline
17
18
  from fabricatio.models.extra.rule import RuleSet
18
- from fabricatio.utils import ok
19
+ from fabricatio.utils import ask_retain, ok
19
20
 
20
21
 
21
- class WriteArticleContentRAG(Action, RAG, Propose):
22
+ class WriteArticleContentRAG(Action, RAG, Extract):
22
23
  """Write an article based on the provided outline."""
23
24
 
24
- ref_limit: int = 100
25
+ ref_limit: int = 35
25
26
  """The limit of references to be retrieved"""
27
+ threshold: float = 0.55
28
+ """The threshold of relevance"""
26
29
  extractor_model: str
27
30
  """The model to use for extracting the content from the retrieved references."""
28
31
  query_model: str
29
32
  """The model to use for querying the database"""
33
+ supervisor: bool = False
34
+ """Whether to use supervisor mode"""
35
+ req: str = (
36
+ "citation number is REQUIRED to cite any reference!\n"
37
+ "Everything is build upon the typst language, which is similar to latex, \n"
38
+ "Legal citing syntax examples(seperated by |): [[1]]|[[1,2]]|[[1-3]]|[[12,13-15]]|[[1-3,5-7]]\n"
39
+ "Illegal citing syntax examples(seperated by |): [[1],[2],[3]]|[[1],[1-2]]\n"
40
+ "Those reference mark shall not be omitted during the extraction\n"
41
+ "It's recommended to cite multiple references that supports your conclusion at a time.\n"
42
+ "Wrapp inline expression using $ $, and wrapp block equation using $$ $$."
43
+ "In addition to that, you can add a label outside the block equation which can be used as a cross reference identifier, the label is a string wrapped in `<` and `>`,"
44
+ "you can refer to that label by using the syntax with prefix of `@eqt:`"
45
+ "Below is a usage example:\n"
46
+ "```typst\n"
47
+ "See @eqt:mass-energy-equation , it's the foundation of physics.\n"
48
+ "$$\n"
49
+ "E = m c^2"
50
+ "$$\n"
51
+ "<mass-energy-equation>\n\n"
52
+ "In @eqt:mass-energy-equation , $m$ stands for mass, $c$ stands for speed of light, and $E$ stands for energy. \n"
53
+ "```"
54
+ )
30
55
 
31
56
  async def _execute(
32
57
  self,
@@ -36,14 +61,57 @@ class WriteArticleContentRAG(Action, RAG, Propose):
36
61
  **cxt,
37
62
  ) -> Article:
38
63
  article = Article.from_outline(article_outline).update_ref(article_outline)
39
- await gather(
40
- *[
41
- self._inner(article, article_outline, chap, sec, subsec)
42
- for chap, sec, subsec in article.iter_subsections()
43
- ]
44
- )
64
+
65
+ if self.supervisor:
66
+ await gather(
67
+ *[
68
+ self._supervisor_inner(article, article_outline, chap, sec, subsec)
69
+ for chap, sec, subsec in article.iter_subsections()
70
+ ]
71
+ )
72
+ else:
73
+ await gather(
74
+ *[
75
+ self._inner(article, article_outline, chap, sec, subsec)
76
+ for chap, sec, subsec in article.iter_subsections()
77
+ ]
78
+ )
45
79
  return article.convert_tex()
46
80
 
81
+ @precheck_package(
82
+ "questionary", "`questionary` is required for supervisor mode, please install it by `fabricatio[qa]`"
83
+ )
84
+ async def _supervisor_inner(
85
+ self,
86
+ article: Article,
87
+ article_outline: ArticleOutline,
88
+ chap: ArticleChapter,
89
+ sec: ArticleSection,
90
+ subsec: ArticleSubsection,
91
+ ) -> ArticleSubsection:
92
+ from questionary import confirm, text
93
+ from rich import print as r_print
94
+
95
+ ret = await self.search_database(article, article_outline, chap, sec, subsec)
96
+
97
+ cm = CitationManager(article_chunks=await ask_retain([r.chunk for r in ret], ret)).set_cite_number_all()
98
+
99
+ raw = await self.write_raw(article, article_outline, chap, sec, subsec, cm)
100
+ r_print(raw)
101
+ while not await confirm("Accept this version and continue?").ask_async():
102
+ if await confirm("Search for more refs?").ask_async():
103
+ new_refs = await self.search_database(article, article_outline, chap, sec, subsec)
104
+ cm.add_chunks(await ask_retain([r.chunk for r in new_refs], new_refs))
105
+
106
+ instruction = await text("Enter the instructions to improve").ask_async()
107
+ raw = await self.write_raw(article, article_outline, chap, sec, subsec, cm, instruction)
108
+ if await confirm("Edit it?").ask_async():
109
+ raw = await text("Edit", default=raw).ask_async() or raw
110
+
111
+ r_print(raw)
112
+
113
+ return await self.extract_new_subsec(subsec, raw, cm)
114
+
47
115
  async def _inner(
48
116
  self,
49
117
  article: Article,
@@ -52,62 +120,92 @@ class WriteArticleContentRAG(Action, RAG, Propose):
52
120
  sec: ArticleSection,
53
121
  subsec: ArticleSubsection,
54
122
  ) -> ArticleSubsection:
55
- ref_q = ok(
56
- await self.arefined_query(
57
- f"{article_outline.display()}\n\nAbove is my article outline, I m writing graduate thesis titled `{article.title}`. "
58
- f"More specifically, i m witting the Chapter `{chap.title}` >> Section `{sec.title}` >> Subsection `{subsec.title}`.\n"
59
- f"I need to search related references to build up the content of the subsec mentioned above, which is `{subsec.title}`.\n"
60
- f"plus, you can search required formulas by using latex equation code.\n"
61
- f"provide 10 queries as possible, to get best result!\n"
62
- f"You should provide both English version and chinese version of the refined queries!\n",
63
- model=self.query_model,
64
- ),
65
- "Failed to refine query.",
66
- )
67
- ret = await self.aretrieve(ref_q, ArticleChunk, final_limit=self.ref_limit, result_per_query=25)
68
- ret.reverse()
69
- cm = CitationManager().update_chunks(ret)
70
-
71
- raw_paras = await self.aask(
72
- f"{cm.as_prompt()}\nAbove is some related reference retrieved for you. When need to cite some of them ,you MUST follow the academic convention,"
73
- f"{article.referenced.display()}\n\nAbove is my article outline, I m writing graduate thesis titled `{article.title}`. "
74
- f"More specifically, i m witting the Chapter `{chap.title}` >> Section `{sec.title}` >> Subsection `{subsec.title}`.\n"
75
- f"Please help me write the paragraphs of the subsec mentioned above, which is `{subsec.title}`\n"
76
- f"You can output the written paragraphs directly, without explanation. you should use `{subsec.language}`, and maintain academic writing style."
77
- f"In addition,you MUST follow the academic convention and use [[1]] to cite the first reference, and use [[9]] to cite the second reference, and so on.\n"
78
- f"It 's greatly recommended to cite multiple references that stands for the same opinion at a single sentences, like [[1,5,9]] for 1th,5th and 9th references,[[1-9,16]] for 1th to 9th and 16th references.\n"
79
- f"citation number is REQUIRED to cite any reference!\n"
80
- f"for paragraphs that need write equation you should also no forget to doing so. wrapp inline equation using $ $, and wrapp block equation using $$ $$.\n"
81
- )
123
+ ret = await self.search_database(article, article_outline, chap, sec, subsec)
124
+ cm = CitationManager(article_chunks=ret).set_cite_number_all()
82
125
 
83
- raw_paras = (
84
- raw_paras.replace(r" \( ", "$").replace(r" \) ", "$").replace("\\[\n", "$$\n").replace("\n\\]", "\n$$")
85
- )
126
+ raw_paras = await self.write_raw(article, article_outline, chap, sec, subsec, cm)
86
127
 
128
+ return await self.extract_new_subsec(subsec, raw_paras, cm)
129
+
130
+ async def extract_new_subsec(
131
+ self, subsec: ArticleSubsection, raw_paras: str, cm: CitationManager
132
+ ) -> ArticleSubsection:
133
+ """Extract the new subsec."""
87
134
  new_subsec = ok(
88
- await self.propose(
135
+ await self.extract(
89
136
  ArticleSubsection,
90
- f"{raw_paras}\nAbove is the subsection titled `{subsec.title}`.\n"
91
- f"I need you to extract the content to update my subsection obj provided below.\n"
92
- f"Everything is build upon the typst language, which is similar to latex, \n"
93
- f"so reference annotation like `[[1]]` for 1th reference or `[[2,6]]` for 2th and 6th reference or "
94
- f"`[[1,5,9]]` for 1th,5th and 9th references or "
95
- f"`[[1-9,16]]` for 1th to 9th and 16th references\n"
96
- f"Those reference mark shall not be omitted during the extraction\n"
97
- f"Wrapp inline expression using $ $, and wrapp block equation using $$ $$\n\n\n"
98
- f"{subsec.display()}",
99
- model=self.extractor_model,
137
+ raw_paras,
138
+ f"Above is the subsection titled `{subsec.title}`.\n"
139
+ f"I need you to extract the content to update my subsection obj provided below.\n{self.req}"
140
+ f"{subsec.display()}\n",
100
141
  ),
101
142
  "Failed to propose new subsection.",
102
143
  )
103
-
104
144
  for p in new_subsec.paragraphs:
105
- p.content = cm.apply(p.content)
106
-
145
+ p.content = cm.apply(p.content).replace("$$", "\n$$\n")
107
146
  subsec.update_from(new_subsec)
108
147
  logger.debug(f"{subsec.title}:rpl\n{subsec.display()}")
109
148
  return subsec
110
149
 
150
+ async def write_raw(
151
+ self,
152
+ article: Article,
153
+ article_outline: ArticleOutline,
154
+ chap: ArticleChapter,
155
+ sec: ArticleSection,
156
+ subsec: ArticleSubsection,
157
+ cm: CitationManager,
158
+ extra_instruction: str = "",
159
+ ) -> str:
160
+ """Write the raw paragraphs of the subsec."""
161
+ return (
162
+ (
163
+ await self.aask(
164
+ f"{cm.as_prompt()}\nAbove is some related reference retrieved for you."
165
+ f"{article_outline.finalized_dump()}\n\nAbove is my article outline, I m writing graduate thesis titled `{article.title}`. "
166
+ f"More specifically, i m witting the Chapter `{chap.title}` >> Section `{sec.title}` >> Subsection `{subsec.title}`.\n"
167
+ f"Please help me write the paragraphs of the subsec mentioned above, which is `{subsec.title}`.\n"
168
+ f"{self.req}\n"
169
+ f"You SHALL use `{article.language}` as writing language.\n{extra_instruction}"
170
+ )
171
+ )
172
+ .replace(r" \( ", "$")
173
+ .replace(r" \) ", "$")
174
+ .replace(r"\(", "$")
175
+ .replace(r"\)", "$")
176
+ .replace("\\[\n", "$$\n")
177
+ .replace("\n\\]", "\n$$")
178
+ )
179
+
180
+ async def search_database(
181
+ self,
182
+ article: Article,
183
+ article_outline: ArticleOutline,
184
+ chap: ArticleChapter,
185
+ sec: ArticleSection,
186
+ subsec: ArticleSubsection,
187
+ extra_instruction: str = "",
188
+ ) -> List[ArticleChunk]:
189
+ """Search database for related references."""
190
+ ref_q = ok(
191
+ await self.arefined_query(
192
+ f"{article_outline.finalized_dump()}\n\nAbove is my article outline, I m writing graduate thesis titled `{article.title}`. "
193
+ f"More specifically, i m witting the Chapter `{chap.title}` >> Section `{sec.title}` >> Subsection `{subsec.title}`.\n"
194
+ f"I need to search related references to build up the content of the subsec mentioned above, which is `{subsec.title}`.\n"
195
+ f"provide 10~16 queries as possible, to get best result!\n"
196
+ f"You should provide both English version and chinese version of the refined queries!\n{extra_instruction}\n",
197
+ model=self.query_model,
198
+ ),
199
+ "Failed to refine query.",
200
+ )
201
+
202
+ if self.supervisor:
203
+ ref_q = await ask_retain(ref_q)
204
+
205
+ return await self.aretrieve(
206
+ ref_q, ArticleChunk, final_limit=self.ref_limit, result_per_query=3, similarity_threshold=self.threshold
207
+ )
208
+
111
209
 
112
210
  class TweakArticleRAG(Action, RAG, Censor):
113
211
  """Write an article based on the provided outline.
@@ -18,6 +18,7 @@ class Extract(Propose):
18
18
  cls: Type[M],
19
19
  source: str,
20
20
  extract_requirement: Optional[str] = None,
21
+ align_language: bool = True,
21
22
  **kwargs: Unpack[ValidateKwargs[M]],
22
23
  ) -> M: ...
23
24
  @overload
@@ -26,6 +27,7 @@ class Extract(Propose):
26
27
  cls: Type[M],
27
28
  source: str,
28
29
  extract_requirement: Optional[str] = None,
30
+ align_language: bool = True,
29
31
  **kwargs: Unpack[ValidateKwargs[None]],
30
32
  ) -> Optional[M]: ...
31
33
 
@@ -35,6 +37,7 @@ class Extract(Propose):
35
37
  cls: Type[M],
36
38
  source: List[str],
37
39
  extract_requirement: Optional[str] = None,
40
+ align_language: bool = True,
38
41
  **kwargs: Unpack[ValidateKwargs[M]],
39
42
  ) -> List[M]: ...
40
43
  @overload
@@ -43,6 +46,7 @@ class Extract(Propose):
43
46
  cls: Type[M],
44
47
  source: List[str],
45
48
  extract_requirement: Optional[str] = None,
49
+ align_language: bool = True,
46
50
  **kwargs: Unpack[ValidateKwargs[None]],
47
51
  ) -> List[Optional[M]]: ...
48
52
  async def extract[M: ProposedAble](
@@ -50,6 +54,7 @@ class Extract(Propose):
50
54
  cls: Type[M],
51
55
  source: List[str] | str,
52
56
  extract_requirement: Optional[str] = None,
57
+ align_language: bool = True,
53
58
  **kwargs: Unpack[ValidateKwargs[Optional[M]]],
54
59
  ) -> M | List[M] | Optional[M] | List[Optional[M]]:
55
60
  """Extract information from a given source to a model."""
@@ -59,7 +64,7 @@ class Extract(Propose):
59
64
  configs.templates.extract_template,
60
65
  [{"source": s, "extract_requirement": extract_requirement} for s in source]
61
66
  if isinstance(source, list)
62
- else {"source": source, "extract_requirement": extract_requirement},
67
+ else {"source": source, "extract_requirement": extract_requirement, "align_language": align_language},
63
68
  ),
64
69
  **kwargs,
65
70
  )
fabricatio/config.py CHANGED
@@ -255,7 +255,7 @@ class TemplateConfig(BaseModel):
255
255
 
256
256
  extract_template: str = Field(default="extract")
257
257
  """The name of the extract template which will be used to extract model from string."""
258
-
258
+
259
259
  class MagikaConfig(BaseModel):
260
260
  """Magika configuration class."""
261
261
 
fabricatio/decorators.py CHANGED
@@ -6,7 +6,7 @@ from importlib.util import find_spec
6
6
  from inspect import signature
7
7
  from shutil import which
8
8
  from types import ModuleType
9
- from typing import Callable, List, Optional
9
+ from typing import Callable, Coroutine, List, Optional
10
10
 
11
11
  from fabricatio.config import configs
12
12
  from fabricatio.journal import logger
@@ -23,7 +23,20 @@ def precheck_package[**P, R](package_name: str, msg: str) -> Callable[[Callable[
23
23
  bool: True if the package exists, False otherwise.
24
24
  """
25
25
 
26
- def _wrapper(func: Callable[P, R]) -> Callable[P, R]:
26
+ def _wrapper(
27
+ func: Callable[P, R] | Callable[P, Coroutine[None, None, R]],
28
+ ) -> Callable[P, R] | Callable[P, Coroutine[None, None, R]]:
29
+ if iscoroutinefunction(func):
30
+
31
+ @wraps(func)
32
+ async def _async_inner(*args: P.args, **kwargs: P.kwargs) -> R:
33
+ if find_spec(package_name):
34
+ return await func(*args, **kwargs)
35
+ raise RuntimeError(msg)
36
+
37
+ return _async_inner
38
+
39
+ @wraps(func)
27
40
  def _inner(*args: P.args, **kwargs: P.kwargs) -> R:
28
41
  if find_spec(package_name):
29
42
  return func(*args, **kwargs)
@@ -35,7 +48,7 @@ def precheck_package[**P, R](package_name: str, msg: str) -> Callable[[Callable[
35
48
 
36
49
 
37
50
  def depend_on_external_cmd[**P, R](
38
- bin_name: str, install_tip: Optional[str], homepage: Optional[str] = None
51
+ bin_name: str, install_tip: Optional[str], homepage: Optional[str] = None
39
52
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
40
53
  """Decorator to check for the presence of an external command.
41
54
 
@@ -88,8 +101,9 @@ def logging_execution_info[**P, R](func: Callable[P, R]) -> Callable[P, R]:
88
101
  return _wrapper
89
102
 
90
103
 
91
- @precheck_package("questionary",
92
- "'questionary' is required to run this function. Have you installed `fabricatio[qa]`?.")
104
+ @precheck_package(
105
+ "questionary", "'questionary' is required to run this function. Have you installed `fabricatio[qa]`?."
106
+ )
93
107
  def confirm_to_execute[**P, R](func: Callable[P, R]) -> Callable[P, Optional[R]] | Callable[P, R]:
94
108
  """Decorator to confirm before executing a function.
95
109
 
@@ -109,8 +123,8 @@ def confirm_to_execute[**P, R](func: Callable[P, R]) -> Callable[P, Optional[R]]
109
123
  @wraps(func)
110
124
  async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> Optional[R]:
111
125
  if await confirm(
112
- f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n🔑 Kwargs:{kwargs}\n",
113
- instruction="Please input [Yes/No] to proceed (default: Yes):",
126
+ f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n🔑 Kwargs:{kwargs}\n",
127
+ instruction="Please input [Yes/No] to proceed (default: Yes):",
114
128
  ).ask_async():
115
129
  return await func(*args, **kwargs)
116
130
  logger.warning(f"Function: {func.__name__}{signature(func)} canceled by user.")
@@ -121,8 +135,8 @@ def confirm_to_execute[**P, R](func: Callable[P, R]) -> Callable[P, Optional[R]]
121
135
  @wraps(func)
122
136
  def _wrapper(*args: P.args, **kwargs: P.kwargs) -> Optional[R]:
123
137
  if confirm(
124
- f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n��� Kwargs:{kwargs}\n",
125
- instruction="Please input [Yes/No] to proceed (default: Yes):",
138
+ f"Are you sure to execute function: {func.__name__}{signature(func)} \n📦 Args:{args}\n��� Kwargs:{kwargs}\n",
139
+ instruction="Please input [Yes/No] to proceed (default: Yes):",
126
140
  ).ask():
127
141
  return func(*args, **kwargs)
128
142
  logger.warning(f"Function: {func.__name__}{signature(func)} canceled by user.")
@@ -203,7 +217,9 @@ def use_temp_module[**P, R](modules: ModuleType | List[ModuleType]) -> Callable[
203
217
  return _decorator
204
218
 
205
219
 
206
- def logging_exec_time[**P, R](func: Callable[P, R]) -> Callable[P, R]:
220
+ def logging_exec_time[**P, R](
221
+ func: Callable[P, R] | Callable[P, Coroutine[None, None, R]],
222
+ ) -> Callable[P, R] | Callable[P, Coroutine[None, None, R]]:
207
223
  """Decorator to log the execution time of a function.
208
224
 
209
225
  Args:
@@ -215,6 +231,7 @@ def logging_exec_time[**P, R](func: Callable[P, R]) -> Callable[P, R]:
215
231
  from time import time
216
232
 
217
233
  if iscoroutinefunction(func):
234
+
218
235
  @wraps(func)
219
236
  async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
220
237
  start_time = time()
@@ -136,7 +136,7 @@ class ArticleChunk(MilvusDataBase, AsPrompt):
136
136
  """Purge numeric citation."""
137
137
  import re
138
138
 
139
- return re.sub(r"\[[\d\s,\\~–-]+]", "", string) # noqa: RUF001
139
+ return re.sub(r"\[[\d\s,\\~–-]+]", "", string)
140
140
 
141
141
  @property
142
142
  def auther_firstnames(self) -> List[str]:
@@ -175,7 +175,7 @@ class CitationManager(AsPrompt):
175
175
  article_chunks: List[ArticleChunk] = Field(default_factory=list)
176
176
  """Article chunks."""
177
177
 
178
- pat: str = r"\[\[([\d\s,-]*)]]"
178
+ pat: str = r"(\[\[([\d\s,-]*)]])"
179
179
  """Regex pattern to match citations."""
180
180
  sep: str = ","
181
181
  """Separator for citation numbers."""
@@ -190,6 +190,12 @@ class CitationManager(AsPrompt):
190
190
  self.set_cite_number_all()
191
191
  return self
192
192
 
193
+ def add_chunks(self, article_chunks: List[ArticleChunk], set_cite_number: bool = True)-> Self:
194
+ """Add article chunks."""
195
+ self.article_chunks.extend(article_chunks)
196
+ if set_cite_number:
197
+ self.set_cite_number_all()
198
+ return self
193
199
  def set_cite_number_all(self) -> Self:
194
200
  """Set citation numbers for all article chunks."""
195
201
  for i, a in enumerate(self.article_chunks, 1):
@@ -202,14 +208,15 @@ class CitationManager(AsPrompt):
202
208
 
203
209
  def apply(self, string: str) -> str:
204
210
  """Apply citation replacements to the input string."""
205
- matches = re.findall(self.pat, string)
206
-
207
- for m in matches:
211
+ for origin,m in re.findall(self.pat, string):
212
+ logger.info(f"Matching citation: {m}")
208
213
  notations = self.convert_to_numeric_notations(m)
209
-
214
+ logger.info(f"Citing Notations: {notations}")
210
215
  citation_number_seq = list(flatten(self.decode_expr(n) for n in notations))
216
+ logger.info(f"Citation Number Sequence: {citation_number_seq}")
211
217
  dedup = self.deduplicate_citation(citation_number_seq)
212
- string.replace(m, self.unpack_cite_seq(dedup))
218
+ logger.info(f"Deduplicated Citation Number Sequence: {dedup}")
219
+ string=string.replace(origin, self.unpack_cite_seq(dedup))
213
220
  return string
214
221
 
215
222
  def decode_expr(self, string: str) -> List[int]:
@@ -226,7 +233,7 @@ class CitationManager(AsPrompt):
226
233
  def deduplicate_citation(self, citation_seq: List[int]) -> List[int]:
227
234
  """Deduplicate citation sequence."""
228
235
  chunk_seq = [a for a in self.article_chunks if a.cite_number in citation_seq]
229
- deduped = unique(chunk_seq, lambda a: a.cite_number)
236
+ deduped = unique(chunk_seq, lambda a: a.bibtex_cite_key)
230
237
  return [a.cite_number for a in deduped]
231
238
 
232
239
  def unpack_cite_seq(self, citation_seq: List[int]) -> str:
@@ -18,6 +18,7 @@ from fabricatio.models.generic import (
18
18
  Titled,
19
19
  WordCount,
20
20
  )
21
+ from fabricatio.rust import comment
21
22
  from pydantic import Field
22
23
 
23
24
 
@@ -29,11 +30,9 @@ class ReferringType(StrEnum):
29
30
  SUBSECTION = "subsection"
30
31
 
31
32
 
32
-
33
33
  type RefKey = Tuple[str, Optional[str], Optional[str]]
34
34
 
35
35
 
36
-
37
36
  class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
38
37
  """Metadata for an article component."""
39
38
 
@@ -48,8 +47,6 @@ class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
48
47
  """List of writing aims of the research component in academic style."""
49
48
 
50
49
 
51
-
52
-
53
50
  class ArticleOutlineBase(
54
51
  ArticleMetaData,
55
52
  ResolveUpdateConflict,
@@ -92,7 +89,13 @@ class SubSectionBase(ArticleOutlineBase):
92
89
 
93
90
  def to_typst_code(self) -> str:
94
91
  """Converts the component into a Typst code snippet for rendering."""
95
- return f"=== {self.title}\n"
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
+ )
96
99
 
97
100
  def introspect(self) -> str:
98
101
  """Introspects the article subsection outline."""
@@ -117,7 +120,13 @@ class SectionBase[T: SubSectionBase](ArticleOutlineBase):
117
120
  Returns:
118
121
  str: The formatted Typst code snippet.
119
122
  """
120
- return f"== {self.title}\n" + "\n\n".join(subsec.to_typst_code() for subsec in self.subsections)
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)
121
130
 
122
131
  def resolve_update_conflict(self, other: Self) -> str:
123
132
  """Resolve update errors in the article outline."""
@@ -160,7 +169,13 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
160
169
 
161
170
  def to_typst_code(self) -> str:
162
171
  """Converts the chapter into a Typst formatted code snippet for rendering."""
163
- return f"= {self.title}\n" + "\n\n".join(sec.to_typst_code() for sec in self.sections)
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)
164
179
 
165
180
  def resolve_update_conflict(self, other: Self) -> str:
166
181
  """Resolve update errors in the article outline."""
@@ -302,4 +317,8 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, WordCount, Descri
302
317
  === Implementation Details
303
318
  == Evaluation Protocol
304
319
  """
305
- return "\n\n".join(a.to_typst_code() for a in self.chapters)
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)
@@ -16,7 +16,7 @@ from fabricatio.models.extra.article_outline import (
16
16
  )
17
17
  from fabricatio.models.generic import Described, PersistentAble, SequencePatch, SketchedAble, WithRef, WordCount
18
18
  from fabricatio.rust import convert_all_block_tex, convert_all_inline_tex, word_count
19
- from pydantic import Field
19
+ from pydantic import Field, NonNegativeInt
20
20
 
21
21
  PARAGRAPH_SEP = "// - - -"
22
22
 
@@ -24,6 +24,9 @@ PARAGRAPH_SEP = "// - - -"
24
24
  class Paragraph(SketchedAble, WordCount, Described):
25
25
  """Structured academic paragraph blueprint for controlled content generation."""
26
26
 
27
+ expected_word_count: NonNegativeInt = 0
28
+ """The expected word count of this paragraph, 0 means not specified"""
29
+
27
30
  description: str = Field(
28
31
  alias="elaboration",
29
32
  description=Described.model_fields["description"].description,
@@ -239,6 +242,9 @@ class Article(
239
242
  @precheck_package(
240
243
  "questionary", "'questionary' is required to run this function. Have you installed `fabricatio[qa]`?."
241
244
  )
242
- def edit_titles(self) -> Self:
245
+ async def edit_titles(self) -> Self:
246
+ """Edits the titles of the article."""
247
+ from questionary import text
248
+
243
249
  for a in self.iter_dfs():
244
- pass
250
+ a.title = await text(f"Edit `{a.title}`.", default=a.title).ask_async() or a.title