Skip to content

Commit 0f949fb

Browse files
committed
feat: Add Source links, Notes, and See Also parsing
- Add 'View on GitHub' source links with file path and line number - Parse Notes/Warning/Caution sections from docstrings - Parse See Also section for cross-references - Add source_file and source_line tracking to data classes - Add github_repo configuration per SDK package
1 parent 506c566 commit 0f949fb

File tree

1 file changed

+162
-5
lines changed

1 file changed

+162
-5
lines changed

praisonai_tools/docs_generator/generator.py

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class MethodInfo:
4040
params: List[ParamInfo] = field(default_factory=list)
4141
raises: List[Tuple[str, str]] = field(default_factory=list)
4242
examples: List[str] = field(default_factory=list)
43+
notes: str = "" # Notes/More Information section
44+
see_also: List[Tuple[str, str]] = field(default_factory=list) # (name, description)
45+
source_file: str = "" # Relative path to source file
46+
source_line: int = 0 # Line number in source
4347
is_async: bool = False
4448
is_static: bool = False
4549
is_classmethod: bool = False
@@ -55,6 +59,10 @@ class ClassInfo:
5559
class_methods: List[MethodInfo] = field(default_factory=list)
5660
properties: List[ParamInfo] = field(default_factory=list)
5761
examples: List[str] = field(default_factory=list)
62+
notes: str = "" # Notes/More Information section
63+
see_also: List[Tuple[str, str]] = field(default_factory=list) # (name, description)
64+
source_file: str = "" # Relative path to source file
65+
source_line: int = 0 # Line number in source
5866

5967

6068
@dataclass
@@ -67,6 +75,10 @@ class FunctionInfo:
6775
params: List[ParamInfo] = field(default_factory=list)
6876
raises: List[Tuple[str, str]] = field(default_factory=list)
6977
examples: List[str] = field(default_factory=list)
78+
notes: str = "" # Notes/More Information section
79+
see_also: List[Tuple[str, str]] = field(default_factory=list) # (name, description)
80+
source_file: str = "" # Relative path to source file
81+
source_line: int = 0 # Line number in source
7082
is_async: bool = False
7183

7284

@@ -604,6 +616,77 @@ def render_related_section(name: str, max_items: int = 5, package: str = "python
604616
"""
605617

606618

619+
def render_source_link(source_file: str, source_line: int, github_repo: str) -> str:
620+
"""Render a 'View Source on GitHub' link.
621+
622+
Args:
623+
source_file: Relative path to source file
624+
source_line: Line number in source
625+
github_repo: Base GitHub repo URL
626+
627+
Returns:
628+
MDX string with source link, or empty string if no source info
629+
"""
630+
if not source_file or not github_repo:
631+
return ""
632+
633+
line_anchor = f"#L{source_line}" if source_line > 0 else ""
634+
github_url = f"{github_repo}/{source_file}{line_anchor}"
635+
636+
return f"""
637+
## Source
638+
639+
<Card title="View on GitHub" icon="github" href="{github_url}">
640+
`{source_file}` at line {source_line}
641+
</Card>
642+
"""
643+
644+
645+
def render_notes_section(notes: str) -> str:
646+
"""Render a Notes/More Information section.
647+
648+
Args:
649+
notes: Notes text from docstring
650+
651+
Returns:
652+
MDX string with notes section, or empty string if no notes
653+
"""
654+
if not notes:
655+
return ""
656+
657+
return f"""
658+
## Notes
659+
660+
{notes}
661+
"""
662+
663+
664+
def render_see_also_section(see_also: list) -> str:
665+
"""Render a See Also section with cross-references.
666+
667+
Args:
668+
see_also: List of (name, description) tuples
669+
670+
Returns:
671+
MDX string with see also section, or empty string if none
672+
"""
673+
if not see_also:
674+
return ""
675+
676+
items = []
677+
for name, desc in see_also:
678+
if desc:
679+
items.append(f"- **`{name}`**: {desc}")
680+
else:
681+
items.append(f"- **`{name}`**")
682+
683+
return f"""
684+
## See Also
685+
686+
{chr(10).join(items)}
687+
"""
688+
689+
607690
# =============================================================================
608691
# CONFIGURATION
609692
# =============================================================================
@@ -829,6 +912,9 @@ def parse_module(self, module_path: str) -> Optional[ModuleInfo]:
829912
source = file_path.read_text()
830913
tree = ast.parse(source)
831914

915+
# Calculate relative source file path for GitHub links
916+
source_file_rel = str(file_path.relative_to(self.package_path.parent))
917+
832918
module_short_name = module_path.split(".")[-1]
833919
info = ModuleInfo(
834920
name=module_path,
@@ -840,10 +926,17 @@ def parse_module(self, module_path: str) -> Optional[ModuleInfo]:
840926
for node in tree.body:
841927
if isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
842928
class_info = self._parse_class(node)
843-
if class_info: info.classes.append(class_info)
929+
if class_info:
930+
class_info.source_file = source_file_rel
931+
# Also set source_file on methods
932+
for method in class_info.methods + class_info.class_methods:
933+
method.source_file = source_file_rel
934+
info.classes.append(class_info)
844935
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and not node.name.startswith("_"):
845936
func_info = self._parse_function(node)
846-
if func_info: info.functions.append(func_info)
937+
if func_info:
938+
func_info.source_file = source_file_rel
939+
info.functions.append(func_info)
847940
elif isinstance(node, ast.Assign):
848941
for target in node.targets:
849942
if isinstance(target, ast.Name) and target.id.isupper():
@@ -863,7 +956,10 @@ def _parse_class(self, node: ast.ClassDef) -> Optional[ClassInfo]:
863956
name=node.name,
864957
docstring=parsed_doc["description"],
865958
bases=bases,
866-
examples=parsed_doc["examples"]
959+
examples=parsed_doc["examples"],
960+
notes=parsed_doc["notes"],
961+
see_also=parsed_doc["see_also"],
962+
source_line=node.lineno,
867963
)
868964

869965
for item in node.body:
@@ -911,6 +1007,9 @@ def _parse_method(self, node) -> Optional[MethodInfo]:
9111007
params=params,
9121008
raises=parsed_doc["raises"],
9131009
examples=parsed_doc["examples"],
1010+
notes=parsed_doc["notes"],
1011+
see_also=parsed_doc["see_also"],
1012+
source_line=node.lineno,
9141013
is_async=is_async,
9151014
is_static=is_static,
9161015
is_classmethod=is_classmethod,
@@ -941,6 +1040,9 @@ def _parse_function(self, node) -> Optional[FunctionInfo]:
9411040
params=params,
9421041
raises=parsed_doc["raises"],
9431042
examples=parsed_doc["examples"],
1043+
notes=parsed_doc["notes"],
1044+
see_also=parsed_doc["see_also"],
1045+
source_line=node.lineno,
9441046
is_async=is_async,
9451047
)
9461048
except Exception:
@@ -954,15 +1056,17 @@ def _parse_docstring(self, docstring: str) -> Dict[str, Any]:
9541056
"returns": "",
9551057
"returns_type": "",
9561058
"raises": [],
957-
"examples": []
1059+
"examples": [],
1060+
"notes": "",
1061+
"see_also": []
9581062
}
9591063

9601064
if not docstring:
9611065
return result
9621066

9631067
# More robust splitting: only match sections at the start of original lines
9641068
# This prevents picking up 'Examples:' inside an 'Args' description
965-
section_pattern = r'\n\s*(Args|Parameters|Returns|Raises|Example|Examples|Usage):?\s*\n'
1069+
section_pattern = r'\n\s*(Args|Parameters|Returns|Raises|Example|Examples|Usage|Notes?|See Also|Warning|Caution):?\s*\n'
9661070
sections = re.split(section_pattern, '\n' + docstring)
9671071
result["description"] = sections[0].strip()
9681072

@@ -1002,6 +1106,23 @@ def _parse_docstring(self, docstring: str) -> Dict[str, Any]:
10021106
# Strip existing triple backticks if they wrap the entire example
10031107
content = re.sub(r'^```[a-z]*\n?(.*?)\n?```$', r'\1', content, flags=re.DOTALL)
10041108
result["examples"].append(content.strip())
1109+
1110+
elif section_name in ("note", "notes", "warning", "caution"):
1111+
result["notes"] = section_content.strip()
1112+
1113+
elif section_name == "see also":
1114+
# Parse "See Also" section - format: "name : description" or just "name"
1115+
see_also_lines = section_content.strip().split('\n')
1116+
for line in see_also_lines:
1117+
line = line.strip()
1118+
if not line:
1119+
continue
1120+
# Match "name : description" or "name: description"
1121+
match = re.match(r'^(\w+(?:\.\w+)*)\s*:?\s*(.*)$', line)
1122+
if match:
1123+
name = match.group(1).strip()
1124+
desc = match.group(2).strip() if match.group(2) else ""
1125+
result["see_also"].append((name, desc))
10051126

10061127
return result
10071128

@@ -1513,6 +1634,22 @@ def _render_class_page(self, cls: ClassInfo, module_info: ModuleInfo) -> str:
15131634
else:
15141635
lines.append(f"```{lang}\n{cls.examples[0]}\n```\n")
15151636

1637+
# Add Notes section if available
1638+
notes_section = render_notes_section(cls.notes)
1639+
if notes_section:
1640+
lines.append(notes_section)
1641+
1642+
# Add See Also section if available
1643+
see_also_section = render_see_also_section(cls.see_also)
1644+
if see_also_section:
1645+
lines.append(see_also_section)
1646+
1647+
# Add Source link
1648+
github_repo = self.config.get("github_repo", "")
1649+
source_section = render_source_link(cls.source_file, cls.source_line, github_repo)
1650+
if source_section:
1651+
lines.append(source_section)
1652+
15161653
# Add related documentation section
15171654
related_section = render_related_section(cls.name, max_items=5, package=self.package_name)
15181655
if related_section:
@@ -1623,6 +1760,22 @@ def _render_function_page(self, func: FunctionInfo, module_info: ModuleInfo, cls
16231760
dedented_ex = textwrap.dedent(func.examples[0])
16241761
lines.append(f"```python\n{dedented_ex}\n```\n")
16251762

1763+
# Add Notes section if available
1764+
notes_section = render_notes_section(func.notes)
1765+
if notes_section:
1766+
lines.append(notes_section)
1767+
1768+
# Add See Also section if available
1769+
see_also_section = render_see_also_section(func.see_also)
1770+
if see_also_section:
1771+
lines.append(see_also_section)
1772+
1773+
# Add Source link
1774+
github_repo = self.config.get("github_repo", "")
1775+
source_section = render_source_link(func.source_file, func.source_line, github_repo)
1776+
if source_section:
1777+
lines.append(source_section)
1778+
16261779
# Add related documentation section
16271780
related_section = render_related_section(func.name, max_items=5, package=self.package_name)
16281781
if related_section:
@@ -1743,6 +1896,7 @@ def __init__(
17431896
"badge_color": "blue",
17441897
"badge_text": "AI Agent",
17451898
"title_suffix": " • AI Agent SDK",
1899+
"github_repo": "https://github.com/MervinPraison/PraisonAI/blob/main/src/praisonai-agents",
17461900
},
17471901
"praisonai": {
17481902
"source": base_src / "src/praisonai/praisonai",
@@ -1751,6 +1905,7 @@ def __init__(
17511905
"badge_color": "purple",
17521906
"badge_text": "AI Agents Framework",
17531907
"title_suffix": " • AI Agents Framework",
1908+
"github_repo": "https://github.com/MervinPraison/PraisonAI/blob/main/src/praisonai",
17541909
},
17551910
"typescript": {
17561911
"source": base_src / "src/praisonai-ts/src",
@@ -1759,6 +1914,7 @@ def __init__(
17591914
"badge_color": "green",
17601915
"badge_text": "TypeScript AI Agent",
17611916
"title_suffix": " • TypeScript AI Agent SDK",
1917+
"github_repo": "https://github.com/MervinPraison/PraisonAI/blob/main/src/praisonai-ts/src",
17621918
},
17631919
"rust": {
17641920
"source": base_src / "src/praisonai-rust",
@@ -1768,6 +1924,7 @@ def __init__(
17681924
"badge_text": "Rust AI Agent SDK",
17691925
"title_suffix": " • Rust AI Agent SDK",
17701926
"language": "rust",
1927+
"github_repo": "https://github.com/ARC-Solutions/praisonai-rust/blob/main",
17711928
},
17721929
}
17731930

0 commit comments

Comments
 (0)