fabricatio 0.2.9.dev2__cp312-cp312-win_amd64.whl → 0.2.9.dev4__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 +84 -107
- fabricatio/actions/article_rag.py +15 -10
- fabricatio/actions/output.py +20 -4
- fabricatio/actions/rules.py +37 -4
- fabricatio/capabilities/censor.py +21 -5
- fabricatio/capabilities/check.py +40 -22
- fabricatio/capabilities/correct.py +30 -11
- fabricatio/capabilities/rating.py +53 -47
- fabricatio/config.py +2 -2
- fabricatio/fs/readers.py +20 -1
- fabricatio/models/action.py +6 -6
- fabricatio/models/extra/advanced_judge.py +3 -3
- fabricatio/models/extra/article_base.py +117 -57
- fabricatio/models/extra/article_main.py +102 -14
- fabricatio/models/extra/article_proposal.py +15 -14
- fabricatio/models/extra/patches.py +6 -6
- fabricatio/models/extra/problem.py +20 -7
- fabricatio/models/extra/rule.py +16 -4
- fabricatio/models/generic.py +23 -6
- fabricatio/models/usages.py +7 -16
- fabricatio/parser.py +5 -5
- fabricatio/rust.cp312-win_amd64.pyd +0 -0
- fabricatio/rust.pyi +33 -0
- fabricatio/utils.py +5 -5
- fabricatio/workflows/articles.py +3 -5
- fabricatio-0.2.9.dev4.data/scripts/tdown.exe +0 -0
- {fabricatio-0.2.9.dev2.dist-info → fabricatio-0.2.9.dev4.dist-info}/METADATA +1 -1
- {fabricatio-0.2.9.dev2.dist-info → fabricatio-0.2.9.dev4.dist-info}/RECORD +30 -30
- fabricatio-0.2.9.dev2.data/scripts/tdown.exe +0 -0
- {fabricatio-0.2.9.dev2.dist-info → fabricatio-0.2.9.dev4.dist-info}/WHEEL +0 -0
- {fabricatio-0.2.9.dev2.dist-info → fabricatio-0.2.9.dev4.dist-info}/licenses/LICENSE +0 -0
@@ -7,15 +7,20 @@ from typing import Generator, List, Optional, Self, Tuple, overload
|
|
7
7
|
|
8
8
|
from fabricatio.models.generic import (
|
9
9
|
AsPrompt,
|
10
|
+
Described,
|
10
11
|
FinalizedDumpAble,
|
11
12
|
Introspect,
|
13
|
+
Language,
|
12
14
|
ModelHash,
|
13
15
|
PersistentAble,
|
14
16
|
ProposedUpdateAble,
|
15
17
|
ResolveUpdateConflict,
|
16
18
|
SequencePatch,
|
17
19
|
SketchedAble,
|
20
|
+
Titled,
|
21
|
+
WordCount,
|
18
22
|
)
|
23
|
+
from pydantic import Field
|
19
24
|
|
20
25
|
|
21
26
|
class ReferringType(StrEnum):
|
@@ -36,44 +41,44 @@ class ArticleRef(ProposedUpdateAble):
|
|
36
41
|
- Referring to a chapter titled `Introduction`:
|
37
42
|
Using Python
|
38
43
|
```python
|
39
|
-
ArticleRef(
|
44
|
+
ArticleRef(chap="Introduction")
|
40
45
|
```
|
41
46
|
Using JSON
|
42
47
|
```json
|
43
|
-
{
|
48
|
+
{chap="Introduction"}
|
44
49
|
```
|
45
50
|
- Referring to a section titled `Background` under the `Introduction` chapter:
|
46
51
|
Using Python
|
47
52
|
```python
|
48
|
-
ArticleRef(
|
53
|
+
ArticleRef(chap="Introduction", sec="Background")
|
49
54
|
```
|
50
55
|
Using JSON
|
51
56
|
```json
|
52
|
-
{
|
57
|
+
{chap="Introduction", sec="Background"}
|
53
58
|
```
|
54
59
|
- Referring to a subsection titled `Related Work` under the `Background` section of the `Introduction` chapter:
|
55
60
|
Using Python
|
56
61
|
```python
|
57
|
-
ArticleRef(
|
62
|
+
ArticleRef(chap="Introduction", sec="Background", subsec="Related Work")
|
58
63
|
```
|
59
64
|
Using JSON
|
60
65
|
```json
|
61
|
-
{
|
66
|
+
{chap="Introduction", sec="Background", subsec="Related Work"}
|
62
67
|
```
|
63
68
|
"""
|
64
69
|
|
65
|
-
|
70
|
+
chap: str
|
66
71
|
"""`title` Field of the referenced chapter"""
|
67
|
-
|
72
|
+
sec: Optional[str] = None
|
68
73
|
"""`title` Field of the referenced section."""
|
69
|
-
|
74
|
+
subsec: Optional[str] = None
|
70
75
|
"""`title` Field of the referenced subsection."""
|
71
76
|
|
72
77
|
def update_from_inner(self, other: Self) -> Self:
|
73
78
|
"""Updates the current instance with the attributes of another instance."""
|
74
|
-
self.
|
75
|
-
self.
|
76
|
-
self.
|
79
|
+
self.chap = other.chap
|
80
|
+
self.sec = other.sec
|
81
|
+
self.subsec = other.subsec
|
77
82
|
return self
|
78
83
|
|
79
84
|
def deref(self, article: "ArticleBase") -> Optional["ArticleOutlineBase"]:
|
@@ -85,42 +90,41 @@ class ArticleRef(ProposedUpdateAble):
|
|
85
90
|
Returns:
|
86
91
|
ArticleMainBase | ArticleOutline | None: The dereferenced section or subsection, or None if not found.
|
87
92
|
"""
|
88
|
-
chap = next((chap for chap in article.chapters if chap.title == self.
|
89
|
-
if self.
|
93
|
+
chap = next((chap for chap in article.chapters if chap.title == self.chap), None)
|
94
|
+
if self.sec is None or chap is None:
|
90
95
|
return chap
|
91
|
-
sec = next((sec for sec in chap.sections if sec.title == self.
|
92
|
-
if self.
|
96
|
+
sec = next((sec for sec in chap.sections if sec.title == self.sec), None)
|
97
|
+
if self.subsec is None or sec is None:
|
93
98
|
return sec
|
94
|
-
return next((subsec for subsec in sec.subsections if subsec.title == self.
|
99
|
+
return next((subsec for subsec in sec.subsections if subsec.title == self.subsec), None)
|
95
100
|
|
96
101
|
@property
|
97
102
|
def referring_type(self) -> ReferringType:
|
98
103
|
"""Determine the type of reference based on the presence of specific attributes."""
|
99
|
-
if self.
|
104
|
+
if self.subsec is not None:
|
100
105
|
return ReferringType.SUBSECTION
|
101
|
-
if self.
|
106
|
+
if self.sec is not None:
|
102
107
|
return ReferringType.SECTION
|
103
108
|
return ReferringType.CHAPTER
|
104
109
|
|
105
110
|
|
106
|
-
class ArticleMetaData(SketchedAble):
|
111
|
+
class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
|
107
112
|
"""Metadata for an article component."""
|
108
113
|
|
109
|
-
description: str
|
110
|
-
|
114
|
+
description: str = Field(
|
115
|
+
alias="elaboration",
|
116
|
+
description=Described.model_fields["description"].description,
|
117
|
+
)
|
111
118
|
|
112
|
-
|
113
|
-
"""List of references to other component of this articles that this component supports."""
|
114
|
-
depend_on: List[ArticleRef]
|
115
|
-
"""List of references to other component of this articles that this component depends on."""
|
119
|
+
title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
|
116
120
|
|
117
|
-
|
121
|
+
aims: List[str]
|
118
122
|
"""List of writing aims of the research component in academic style."""
|
119
|
-
title: str
|
120
|
-
"""Do not add any prefix or suffix to the title. should not contain special characters."""
|
121
123
|
|
122
|
-
|
123
|
-
"""
|
124
|
+
support_to: List[ArticleRef]
|
125
|
+
"""List of references to other future components in this article that this component supports to."""
|
126
|
+
depend_on: List[ArticleRef]
|
127
|
+
"""List of references to other previous components in this article that this component depends on."""
|
124
128
|
|
125
129
|
|
126
130
|
class ArticleRefSequencePatch(SequencePatch[ArticleRef]):
|
@@ -148,8 +152,8 @@ class ArticleOutlineBase(
|
|
148
152
|
self.support_to.extend(other.support_to)
|
149
153
|
self.depend_on.clear()
|
150
154
|
self.depend_on.extend(other.depend_on)
|
151
|
-
self.
|
152
|
-
self.
|
155
|
+
self.aims.clear()
|
156
|
+
self.aims.extend(other.aims)
|
153
157
|
self.description = other.description
|
154
158
|
return self
|
155
159
|
|
@@ -273,25 +277,19 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
|
|
273
277
|
return ""
|
274
278
|
|
275
279
|
|
276
|
-
class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
|
280
|
+
class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, WordCount, Described, Titled, Language, ABC):
|
277
281
|
"""Base class for article outlines."""
|
278
282
|
|
279
|
-
|
280
|
-
|
283
|
+
title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
|
284
|
+
description: str = Field(alias="abstract")
|
285
|
+
"""The abstract serves as a concise summary of an academic article, encapsulating its core purpose, methodologies, key results,
|
286
|
+
and conclusions while enabling readers to rapidly assess the relevance and significance of the study.
|
287
|
+
Functioning as the article's distilled essence, it succinctly articulates the research problem, objectives,
|
288
|
+
and scope, providing a roadmap for the full text while also facilitating database indexing, literature reviews,
|
289
|
+
and citation tracking through standardized metadata. Additionally, it acts as an accessibility gateway,
|
290
|
+
allowing scholars to gauge the study's contribution to existing knowledge, its methodological rigor,
|
291
|
+
and its broader implications without engaging with the entire manuscript, thereby optimizing scholarly communication efficiency."""
|
281
292
|
|
282
|
-
title: str
|
283
|
-
"""Title of the academic paper."""
|
284
|
-
|
285
|
-
prospect: str
|
286
|
-
"""Consolidated research statement with four pillars:
|
287
|
-
1. Problem Identification: Current limitations
|
288
|
-
2. Methodological Response: Technical approach
|
289
|
-
3. Empirical Validation: Evaluation strategy
|
290
|
-
4. Scholarly Impact: Field contributions
|
291
|
-
"""
|
292
|
-
|
293
|
-
abstract: str
|
294
|
-
"""The abstract is a concise summary of the academic paper's main findings."""
|
295
293
|
chapters: List[T]
|
296
294
|
"""Chapters of the article. Contains at least one chapter. You can also add more as needed."""
|
297
295
|
|
@@ -378,12 +376,31 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
|
|
378
376
|
return component, summary
|
379
377
|
return None
|
380
378
|
|
379
|
+
def gather_introspected(self) -> Optional[str]:
|
380
|
+
"""Gathers all introspected components in the article structure."""
|
381
|
+
return "\n".join([i for component in self.chapters if (i := component.introspect())])
|
382
|
+
|
381
383
|
@overload
|
382
384
|
def find_illegal_ref(self, gather_identical: bool) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]: ...
|
383
385
|
|
384
386
|
@overload
|
385
387
|
def find_illegal_ref(self) -> Optional[Tuple[ArticleRef, str]]: ...
|
386
388
|
|
389
|
+
def iter_chap_title(self) -> Generator[str, None, None]:
|
390
|
+
"""Iterates through all chapter titles in the article."""
|
391
|
+
for chap in self.chapters:
|
392
|
+
yield chap.title
|
393
|
+
|
394
|
+
def iter_section_title(self) -> Generator[str, None, None]:
|
395
|
+
"""Iterates through all section titles in the article."""
|
396
|
+
for _, sec in self.iter_sections():
|
397
|
+
yield sec.title
|
398
|
+
|
399
|
+
def iter_subsection_title(self) -> Generator[str, None, None]:
|
400
|
+
"""Iterates through all subsection titles in the article."""
|
401
|
+
for _, _, subsec in self.iter_subsections():
|
402
|
+
yield subsec.title
|
403
|
+
|
387
404
|
def find_illegal_ref(self, gather_identical: bool = False) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]:
|
388
405
|
"""Finds the first illegal component in the outline.
|
389
406
|
|
@@ -391,21 +408,64 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
|
|
391
408
|
Tuple[ArticleOutlineBase, str]: A tuple containing the illegal component and an error message.
|
392
409
|
"""
|
393
410
|
summary = ""
|
411
|
+
chap_titles_set = set(self.iter_chap_title())
|
412
|
+
sec_titles_set = set(self.iter_section_title())
|
413
|
+
subsec_titles_set = set(self.iter_subsection_title())
|
414
|
+
|
394
415
|
for component in self.iter_dfs_rev():
|
395
416
|
for ref in chain(component.depend_on, component.support_to):
|
396
417
|
if not ref.deref(self):
|
397
418
|
summary += f"Invalid internal reference in `{component.__class__.__name__}` titled `{component.title}`, because the referred {ref.referring_type} is not exists within the article, see the original obj dump: {ref.model_dump()}\n"
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
419
|
+
|
420
|
+
if ref.chap not in (chap_titles_set):
|
421
|
+
summary += f"Chapter titled `{ref.chap}` is not any of {chap_titles_set}\n"
|
422
|
+
if ref.sec and ref.sec not in (sec_titles_set):
|
423
|
+
summary += f"Section Titled `{ref.sec}` is not any of {sec_titles_set}\n"
|
424
|
+
if ref.subsec and ref.subsec not in (subsec_titles_set):
|
425
|
+
summary += f"Subsection Titled `{ref.subsec}` is not any of {subsec_titles_set}"
|
426
|
+
|
427
|
+
if summary:
|
428
|
+
return (
|
429
|
+
(
|
430
|
+
[
|
431
|
+
identical_ref
|
432
|
+
for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
|
433
|
+
if identical_ref == ref
|
434
|
+
],
|
435
|
+
summary,
|
436
|
+
)
|
437
|
+
if gather_identical
|
438
|
+
else (ref, summary)
|
439
|
+
)
|
406
440
|
|
407
441
|
return None
|
408
442
|
|
443
|
+
def gather_illegal_ref(self) -> Tuple[List[ArticleRef], str]:
|
444
|
+
"""Gathers all illegal references in the article."""
|
445
|
+
summary = []
|
446
|
+
chap_titles_set = set(self.iter_chap_title())
|
447
|
+
sec_titles_set = set(self.iter_section_title())
|
448
|
+
subsec_titles_set = set(self.iter_subsection_title())
|
449
|
+
res_seq = []
|
450
|
+
|
451
|
+
for component in self.iter_dfs():
|
452
|
+
for ref in (
|
453
|
+
r for r in chain(component.depend_on, component.support_to) if not r.deref(self) and r not in res_seq
|
454
|
+
):
|
455
|
+
res_seq.append(ref)
|
456
|
+
if ref.chap not in chap_titles_set:
|
457
|
+
summary.append(
|
458
|
+
f"Chapter titled `{ref.chap}` is not exist, since it is not any of {chap_titles_set}."
|
459
|
+
)
|
460
|
+
if ref.sec and (ref.sec not in sec_titles_set):
|
461
|
+
summary.append(f"Section Titled `{ref.sec}` is not exist, since it is not any of {sec_titles_set}")
|
462
|
+
if ref.subsec and (ref.subsec not in subsec_titles_set):
|
463
|
+
summary.append(
|
464
|
+
f"Subsection Titled `{ref.subsec}` is not exist, since it is not any of {subsec_titles_set}"
|
465
|
+
)
|
466
|
+
|
467
|
+
return res_seq, "\n".join(summary)
|
468
|
+
|
409
469
|
def finalized_dump(self) -> str:
|
410
470
|
"""Generates standardized hierarchical markup for academic publishing systems.
|
411
471
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
from itertools import chain
|
4
4
|
from typing import Dict, Generator, List, Self, Tuple, override
|
5
5
|
|
6
|
+
from fabricatio.fs.readers import extract_sections
|
6
7
|
from fabricatio.journal import logger
|
7
8
|
from fabricatio.models.extra.article_base import (
|
8
9
|
ArticleBase,
|
@@ -14,22 +15,33 @@ from fabricatio.models.extra.article_base import (
|
|
14
15
|
from fabricatio.models.extra.article_outline import (
|
15
16
|
ArticleOutline,
|
16
17
|
)
|
17
|
-
from fabricatio.models.generic import PersistentAble, SequencePatch, SketchedAble, WithRef
|
18
|
+
from fabricatio.models.generic import Described, PersistentAble, SequencePatch, SketchedAble, WithRef, WordCount
|
19
|
+
from fabricatio.rust import detect_language, word_count
|
18
20
|
from fabricatio.utils import ok
|
21
|
+
from pydantic import Field
|
19
22
|
|
23
|
+
PARAGRAPH_SEP = "// - - -"
|
20
24
|
|
21
|
-
|
25
|
+
|
26
|
+
class Paragraph(SketchedAble, WordCount, Described):
|
22
27
|
"""Structured academic paragraph blueprint for controlled content generation."""
|
23
28
|
|
24
|
-
description: str
|
25
|
-
|
29
|
+
description: str = Field(
|
30
|
+
alias="elaboration",
|
31
|
+
description=Described.model_fields["description"].description,
|
32
|
+
)
|
26
33
|
|
27
|
-
|
34
|
+
aims: List[str]
|
28
35
|
"""Specific communicative objectives for this paragraph's content."""
|
29
36
|
|
30
37
|
content: str
|
31
38
|
"""The actual content of the paragraph, represented as a string."""
|
32
39
|
|
40
|
+
@classmethod
|
41
|
+
def from_content(cls, content: str) -> Self:
|
42
|
+
"""Create a Paragraph object from the given content."""
|
43
|
+
return cls(elaboration="", aims=[], expected_word_count=word_count(content), content=content)
|
44
|
+
|
33
45
|
|
34
46
|
class ArticleParagraphSequencePatch(SequencePatch[Paragraph]):
|
35
47
|
"""Patch for `Paragraph` list of `ArticleSubsection`."""
|
@@ -41,11 +53,25 @@ class ArticleSubsection(SubSectionBase):
|
|
41
53
|
paragraphs: List[Paragraph]
|
42
54
|
"""List of Paragraph objects containing the content of the subsection."""
|
43
55
|
|
56
|
+
_max_word_count_deviation: float = 0.3
|
57
|
+
"""Maximum allowed deviation from the expected word count, as a percentage."""
|
58
|
+
|
59
|
+
@property
|
60
|
+
def word_count(self) -> int:
|
61
|
+
"""Calculates the total word count of all paragraphs in the subsection."""
|
62
|
+
return sum(word_count(p.content) for p in self.paragraphs)
|
63
|
+
|
44
64
|
def introspect(self) -> str:
|
45
|
-
"""Introspects the subsection and returns a
|
65
|
+
"""Introspects the subsection and returns a summary of its state."""
|
66
|
+
summary = ""
|
46
67
|
if len(self.paragraphs) == 0:
|
47
|
-
|
48
|
-
|
68
|
+
summary += f"`{self.__class__.__name__}` titled `{self.title}` have no paragraphs, You should add some!\n"
|
69
|
+
if abs((wc := self.word_count) - self.expected_word_count) / self.expected_word_count > self._max_word_count_deviation:
|
70
|
+
summary += (
|
71
|
+
f"`{self.__class__.__name__}` titled `{self.title}` have {wc} words, expected {self.word_count} words!"
|
72
|
+
)
|
73
|
+
|
74
|
+
return summary
|
49
75
|
|
50
76
|
def update_from_inner(self, other: Self) -> Self:
|
51
77
|
"""Updates the current instance with the attributes of another instance."""
|
@@ -61,16 +87,64 @@ class ArticleSubsection(SubSectionBase):
|
|
61
87
|
Returns:
|
62
88
|
str: Typst code snippet for rendering.
|
63
89
|
"""
|
64
|
-
return f"=== {self.title}\n" + "\n\n".join(p.content for p in self.paragraphs)
|
90
|
+
return f"=== {self.title}\n" + f"\n{PARAGRAPH_SEP}\n".join(p.content for p in self.paragraphs)
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def from_typst_code(cls, title: str, body: str, language: str) -> Self:
|
94
|
+
"""Creates an Article object from the given Typst code."""
|
95
|
+
return cls(
|
96
|
+
heading=title,
|
97
|
+
elaboration="",
|
98
|
+
paragraphs=[Paragraph.from_content(p) for p in body.split(PARAGRAPH_SEP)],
|
99
|
+
expected_word_count=word_count(body),
|
100
|
+
language=language,
|
101
|
+
aims=[],
|
102
|
+
support_to=[],
|
103
|
+
depend_on=[],
|
104
|
+
)
|
65
105
|
|
66
106
|
|
67
107
|
class ArticleSection(SectionBase[ArticleSubsection]):
|
68
108
|
"""Atomic argumentative unit with high-level specificity."""
|
69
109
|
|
110
|
+
@classmethod
|
111
|
+
def from_typst_code(cls, title: str, body: str, language: str) -> Self:
|
112
|
+
"""Creates an Article object from the given Typst code."""
|
113
|
+
return cls(
|
114
|
+
subsections=[
|
115
|
+
ArticleSubsection.from_typst_code(*pack, language=language)
|
116
|
+
for pack in extract_sections(body, level=3, section_char="=")
|
117
|
+
],
|
118
|
+
heading=title,
|
119
|
+
elaboration="",
|
120
|
+
expected_word_count=word_count(body),
|
121
|
+
language=language,
|
122
|
+
aims=[],
|
123
|
+
support_to=[],
|
124
|
+
depend_on=[],
|
125
|
+
)
|
126
|
+
|
70
127
|
|
71
128
|
class ArticleChapter(ChapterBase[ArticleSection]):
|
72
129
|
"""Thematic progression implementing research function."""
|
73
130
|
|
131
|
+
@classmethod
|
132
|
+
def from_typst_code(cls, title: str, body: str, language: str) -> Self:
|
133
|
+
"""Creates an Article object from the given Typst code."""
|
134
|
+
return cls(
|
135
|
+
sections=[
|
136
|
+
ArticleSection.from_typst_code(*pack, language=language)
|
137
|
+
for pack in extract_sections(body, level=2, section_char="=")
|
138
|
+
],
|
139
|
+
heading=title,
|
140
|
+
elaboration="",
|
141
|
+
expected_word_count=word_count(body),
|
142
|
+
language=language,
|
143
|
+
aims=[],
|
144
|
+
support_to=[],
|
145
|
+
depend_on=[],
|
146
|
+
)
|
147
|
+
|
74
148
|
|
75
149
|
class Article(
|
76
150
|
SketchedAble,
|
@@ -94,7 +168,7 @@ class Article(
|
|
94
168
|
|
95
169
|
@override
|
96
170
|
def iter_subsections(self) -> Generator[Tuple[ArticleChapter, ArticleSection, ArticleSubsection], None, None]:
|
97
|
-
return super().iter_subsections()
|
171
|
+
return super().iter_subsections() # pyright: ignore [reportReturnType]
|
98
172
|
|
99
173
|
@classmethod
|
100
174
|
def from_outline(cls, outline: ArticleOutline) -> "Article":
|
@@ -107,31 +181,45 @@ class Article(
|
|
107
181
|
Article: The generated article.
|
108
182
|
"""
|
109
183
|
# Set the title from the outline
|
110
|
-
article = Article(**outline.model_dump(exclude={"chapters"}), chapters=[])
|
184
|
+
article = Article(**outline.model_dump(exclude={"chapters"}, by_alias=True), chapters=[])
|
111
185
|
|
112
186
|
for chapter in outline.chapters:
|
113
187
|
# Create a new chapter
|
114
188
|
article_chapter = ArticleChapter(
|
115
189
|
sections=[],
|
116
|
-
**chapter.model_dump(exclude={"sections"}),
|
190
|
+
**chapter.model_dump(exclude={"sections"}, by_alias=True),
|
117
191
|
)
|
118
192
|
for section in chapter.sections:
|
119
193
|
# Create a new section
|
120
194
|
article_section = ArticleSection(
|
121
195
|
subsections=[],
|
122
|
-
**section.model_dump(exclude={"subsections"}),
|
196
|
+
**section.model_dump(exclude={"subsections"}, by_alias=True),
|
123
197
|
)
|
124
198
|
for subsection in section.subsections:
|
125
199
|
# Create a new subsection
|
126
200
|
article_subsection = ArticleSubsection(
|
127
201
|
paragraphs=[],
|
128
|
-
**subsection.model_dump(),
|
202
|
+
**subsection.model_dump(by_alias=True),
|
129
203
|
)
|
130
204
|
article_section.subsections.append(article_subsection)
|
131
205
|
article_chapter.sections.append(article_section)
|
132
206
|
article.chapters.append(article_chapter)
|
133
207
|
return article
|
134
208
|
|
209
|
+
@classmethod
|
210
|
+
def from_typst_code(cls, title: str, body: str) -> Self:
|
211
|
+
"""Generates an article from the given Typst code."""
|
212
|
+
return cls(
|
213
|
+
language=(lang := detect_language(body)),
|
214
|
+
chapters=[
|
215
|
+
ArticleChapter.from_typst_code(*pack, language=lang)
|
216
|
+
for pack in extract_sections(body, level=1, section_char="=")
|
217
|
+
],
|
218
|
+
heading=title,
|
219
|
+
expected_word_count=word_count(body),
|
220
|
+
abstract="",
|
221
|
+
)
|
222
|
+
|
135
223
|
def gather_dependencies(self, article: ArticleOutlineBase) -> List[ArticleOutlineBase]:
|
136
224
|
"""Gathers dependencies for all sections and subsections in the article.
|
137
225
|
|
@@ -2,21 +2,25 @@
|
|
2
2
|
|
3
3
|
from typing import Dict, List
|
4
4
|
|
5
|
-
from fabricatio.models.generic import
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
from fabricatio.models.generic import (
|
6
|
+
AsPrompt,
|
7
|
+
Described,
|
8
|
+
Language,
|
9
|
+
PersistentAble,
|
10
|
+
SketchedAble,
|
11
|
+
Titled,
|
12
|
+
WithRef,
|
13
|
+
WordCount,
|
14
|
+
)
|
15
|
+
from pydantic import Field
|
16
|
+
|
17
|
+
|
18
|
+
class ArticleProposal(SketchedAble, WithRef[str], AsPrompt, PersistentAble, WordCount, Described, Titled, Language):
|
9
19
|
"""Structured proposal for academic paper development with core research elements.
|
10
20
|
|
11
21
|
Guides LLM in generating comprehensive research proposals with clearly defined components.
|
12
22
|
"""
|
13
23
|
|
14
|
-
language: str
|
15
|
-
"""The language in which the article is written. This should align with the language specified in the article briefing."""
|
16
|
-
|
17
|
-
title: str
|
18
|
-
"""The title of the academic paper, formatted in Title Case."""
|
19
|
-
|
20
24
|
focused_problem: List[str]
|
21
25
|
"""A list of specific research problems or questions that the paper aims to address."""
|
22
26
|
|
@@ -38,12 +42,9 @@ class ArticleProposal(SketchedAble, WithRef[str], AsPrompt, PersistentAble):
|
|
38
42
|
keywords: List[str]
|
39
43
|
"""A list of keywords that represent the main topics and focus areas of the research."""
|
40
44
|
|
41
|
-
|
45
|
+
description: str = Field(alias="abstract")
|
42
46
|
"""A concise summary of the research proposal, outlining the main points and objectives."""
|
43
47
|
|
44
|
-
expected_word_count: int
|
45
|
-
"""The estimated word count of the final academic paper."""
|
46
|
-
|
47
48
|
def _as_prompt_inner(self) -> Dict[str, str]:
|
48
49
|
return {
|
49
50
|
"ArticleBriefing": self.referenced,
|
@@ -3,17 +3,17 @@
|
|
3
3
|
from typing import Optional, Type
|
4
4
|
|
5
5
|
from fabricatio.models.extra.rule import RuleSet
|
6
|
-
from fabricatio.models.generic import Patch, WithBriefing
|
6
|
+
from fabricatio.models.generic import Language, Patch, WithBriefing
|
7
7
|
from pydantic import BaseModel
|
8
8
|
|
9
9
|
|
10
|
-
class
|
11
|
-
"""
|
10
|
+
class BriefingMetadata[T: WithBriefing](Patch[T], WithBriefing):
|
11
|
+
"""A patch class for updating the description and name of a `WithBriefing` object, all fields within this instance will be directly copied onto the target model's field."""
|
12
12
|
|
13
13
|
|
14
|
-
class
|
15
|
-
"""
|
16
|
-
|
14
|
+
class RuleSetMetadata(BriefingMetadata[RuleSet], Language):
|
15
|
+
"""A patch class for updating the description and name of a `RuleSet` object, all fields within this instance will be directly copied onto the target model's field."""
|
16
|
+
|
17
17
|
@staticmethod
|
18
18
|
def ref_cls() -> Optional[Type[BaseModel]]:
|
19
19
|
"""Get the reference class of the model."""
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""A class representing a problem-solution pair identified during a review process."""
|
2
2
|
|
3
3
|
from itertools import chain
|
4
|
-
from typing import List, Literal, Optional, Self
|
4
|
+
from typing import Any, List, Literal, Optional, Self, Tuple, Unpack
|
5
5
|
|
6
6
|
from fabricatio.journal import logger
|
7
7
|
from fabricatio.models.generic import SketchedAble, WithBriefing
|
@@ -51,7 +51,12 @@ class ProblemSolutions(SketchedAble):
|
|
51
51
|
problem: Problem
|
52
52
|
"""The problem identified in the review."""
|
53
53
|
solutions: List[Solution]
|
54
|
-
"""A collection of potential solutions."""
|
54
|
+
"""A collection of potential solutions, spread the thought, add more solution as possible.Do not leave this as blank"""
|
55
|
+
|
56
|
+
def model_post_init(self, context: Any, /) -> None:
|
57
|
+
"""Initialize the problem-solution pair with a problem and a list of solutions."""
|
58
|
+
if len(self.solutions) == 0:
|
59
|
+
logger.warning(f"No solution found for problem {self.problem.name}, please add more solutions manually.")
|
55
60
|
|
56
61
|
def update_from_inner(self, other: Self) -> Self:
|
57
62
|
"""Update the current instance with another instance's attributes."""
|
@@ -69,6 +74,10 @@ class ProblemSolutions(SketchedAble):
|
|
69
74
|
self.solutions = solutions
|
70
75
|
return self
|
71
76
|
|
77
|
+
def has_solutions(self) -> bool:
|
78
|
+
"""Check if the problem-solution pair has any solutions."""
|
79
|
+
return len(self.solutions) > 0
|
80
|
+
|
72
81
|
async def edit_problem(self) -> Self:
|
73
82
|
"""Interactively edit the problem description."""
|
74
83
|
self.problem = Problem.model_validate_strings(
|
@@ -87,11 +96,11 @@ class ProblemSolutions(SketchedAble):
|
|
87
96
|
"""Check if the improvement is decided."""
|
88
97
|
return len(self.solutions) == 1
|
89
98
|
|
90
|
-
def final_solution(self) -> Optional[Solution]:
|
99
|
+
def final_solution(self, always_use_first: bool = False) -> Optional[Solution]:
|
91
100
|
"""Get the final solution."""
|
92
|
-
if not self.decided():
|
101
|
+
if not always_use_first and not self.decided():
|
93
102
|
logger.error(
|
94
|
-
f"There is
|
103
|
+
f"There is {len(self.solutions)} solutions for problem {self.problem.name}, please decide which solution is eventually adopted."
|
95
104
|
)
|
96
105
|
return None
|
97
106
|
return self.solutions[0]
|
@@ -106,6 +115,10 @@ class Improvement(SketchedAble):
|
|
106
115
|
problem_solutions: List[ProblemSolutions]
|
107
116
|
"""Collection of problems identified during review along with their potential solutions."""
|
108
117
|
|
118
|
+
def all_problems_have_solutions(self) -> bool:
|
119
|
+
"""Check if all problems have solutions."""
|
120
|
+
return all(ps.has_solutions() for ps in self.problem_solutions)
|
121
|
+
|
109
122
|
async def supervisor_check(self, check_solutions: bool = True) -> Self:
|
110
123
|
"""Perform an interactive review session to filter problems and solutions.
|
111
124
|
|
@@ -145,9 +158,9 @@ class Improvement(SketchedAble):
|
|
145
158
|
return all(ps.decided() for ps in self.problem_solutions)
|
146
159
|
|
147
160
|
@classmethod
|
148
|
-
def gather(cls, *improvements:
|
161
|
+
def gather(cls, *improvements: Unpack[Tuple["Improvement", ...]]) -> Self:
|
149
162
|
"""Gather multiple improvements into a single instance."""
|
150
163
|
return cls(
|
151
|
-
focused_on="
|
164
|
+
focused_on=";".join(imp.focused_on for imp in improvements),
|
152
165
|
problem_solutions=list(chain(*(imp.problem_solutions for imp in improvements))),
|
153
166
|
)
|