Navegador

navegador / navegador / intelligence / docgen.py
Blame History Raw 310 lines
1
"""
2
DocGenerator — markdown documentation generated from the navegador graph.
3
4
Supports two modes:
5
* **Template mode** (``provider=None``): uses graph data to fill a
6
structured markdown template — zero external dependencies.
7
* **LLM mode** (``provider=`` an :class:`~navegador.llm.LLMProvider`):
8
delegates to :class:`~navegador.intelligence.nlp.NLPEngine` for richer,
9
narrative documentation.
10
11
Usage::
12
13
from navegador.graph import GraphStore
14
from navegador.intelligence.docgen import DocGenerator
15
16
store = GraphStore.sqlite(".navegador/graph.db")
17
18
# Template mode (no LLM required)
19
gen = DocGenerator(store)
20
print(gen.generate_file_docs("navegador/graph/store.py"))
21
print(gen.generate_module_docs("navegador.graph"))
22
print(gen.generate_project_docs())
23
24
# LLM mode
25
from navegador.llm import get_provider
26
provider = get_provider("openai")
27
gen = DocGenerator(store, provider=provider)
28
print(gen.generate_file_docs("navegador/graph/store.py"))
29
"""
30
31
from __future__ import annotations
32
33
from typing import TYPE_CHECKING
34
35
if TYPE_CHECKING:
36
from navegador.graph.store import GraphStore
37
from navegador.llm import LLMProvider
38
39
40
# ── Cypher helpers ────────────────────────────────────────────────────────────
41
42
_FILE_SYMBOLS = """
43
MATCH (f:File {path: $path})-[:CONTAINS]->(n)
44
RETURN labels(n)[0] AS type, n.name AS name,
45
coalesce(n.docstring, '') AS docstring,
46
coalesce(n.signature, '') AS signature,
47
n.line_start AS line
48
ORDER BY n.line_start
49
"""
50
51
_FILE_BY_PATH = """
52
MATCH (f:File)
53
WHERE f.path CONTAINS $path
54
RETURN f.path AS path
55
LIMIT 20
56
"""
57
58
_MODULE_SYMBOLS = """
59
MATCH (n)
60
WHERE (n:Function OR n:Class OR n:Method)
61
AND n.file_path CONTAINS $module_path
62
RETURN labels(n)[0] AS type, n.name AS name,
63
coalesce(n.file_path, '') AS file_path,
64
coalesce(n.docstring, '') AS docstring,
65
coalesce(n.signature, '') AS signature
66
ORDER BY n.file_path, n.name
67
"""
68
69
_ALL_SYMBOLS_SUMMARY = """
70
MATCH (n)
71
WHERE n:Function OR n:Class OR n:Method
72
RETURN labels(n)[0] AS type, count(n) AS count
73
"""
74
75
_TOP_SYMBOLS = """
76
MATCH (n)
77
WHERE n:Function OR n:Class OR n:Method
78
RETURN labels(n)[0] AS type, n.name AS name,
79
coalesce(n.file_path, '') AS file_path,
80
coalesce(n.docstring, '') AS docstring
81
ORDER BY n.file_path, n.name
82
LIMIT 200
83
"""
84
85
_PROJECT_FILES = """
86
MATCH (f:File)
87
RETURN f.path AS path
88
ORDER BY f.path
89
"""
90
91
_STATS = """
92
MATCH (n)
93
RETURN labels(n)[0] AS type, count(n) AS count
94
ORDER BY count DESC
95
"""
96
97
98
class DocGenerator:
99
"""
100
Generate markdown documentation from graph context.
101
102
Args:
103
store: A :class:`~navegador.graph.GraphStore` instance.
104
provider: Optional :class:`~navegador.llm.LLMProvider`. When
105
``None`` (default) template-based generation is used.
106
"""
107
108
def __init__(self, store: "GraphStore", provider: "LLMProvider | None" = None) -> None:
109
self._store = store
110
self._provider = provider
111
112
# ── Public API ────────────────────────────────────────────────────────────
113
114
def generate_file_docs(self, file_path: str) -> str:
115
"""
116
Generate markdown documentation for all symbols in a file.
117
118
Args:
119
file_path: Path to the source file (as stored in the graph).
120
121
Returns:
122
Markdown string describing the file's symbols.
123
"""
124
if self._provider:
125
return self._llm_file_docs(file_path)
126
return self._template_file_docs(file_path)
127
128
def generate_module_docs(self, module_name: str) -> str:
129
"""
130
Generate markdown documentation for a Python module (by dotted name
131
or partial path).
132
133
Args:
134
module_name: Dotted module name (``"navegador.graph"``) or partial
135
file path substring.
136
137
Returns:
138
Markdown string describing all symbols in the module.
139
"""
140
# Convert dotted module name to a path fragment
141
module_path = module_name.replace(".", "/")
142
if self._provider:
143
return self._llm_module_docs(module_name, module_path)
144
return self._template_module_docs(module_name, module_path)
145
146
def generate_project_docs(self) -> str:
147
"""
148
Generate a full project documentation overview.
149
150
Returns:
151
Markdown string with a project overview, file listing, and
152
symbol summary.
153
"""
154
if self._provider:
155
return self._llm_project_docs()
156
return self._template_project_docs()
157
158
# ── Template-based generation ─────────────────────────────────────────
159
160
def _template_file_docs(self, file_path: str) -> str:
161
result = self._store.query(_FILE_SYMBOLS, {"path": file_path})
162
rows = result.result_set or []
163
164
lines = [f"# File: `{file_path}`", ""]
165
if not rows:
166
lines.append("_No symbols found in the graph for this file._")
167
return "\n".join(lines)
168
169
for row in rows:
170
sym_type, name, docstring, signature, line = (row[0], row[1], row[2], row[3], row[4])
171
lines.append(f"## {sym_type}: `{name}`")
172
if line is not None:
173
lines.append(f"_Line {line}_")
174
if signature:
175
lines += ["", f"```python\n{signature}\n```"]
176
if docstring:
177
lines += ["", docstring]
178
lines.append("")
179
180
return "\n".join(lines)
181
182
def _template_module_docs(self, module_name: str, module_path: str) -> str:
183
result = self._store.query(_MODULE_SYMBOLS, {"module_path": module_path})
184
rows = result.result_set or []
185
186
lines = [f"# Module: `{module_name}`", ""]
187
if not rows:
188
lines.append("_No symbols found in the graph for this module._")
189
return "\n".join(lines)
190
191
# Group by file
192
files: dict[str, list[tuple]] = {}
193
for row in rows:
194
fp = row[2] or ""
195
files.setdefault(fp, []).append(row)
196
197
for fp, file_rows in sorted(files.items()):
198
lines.append(f"## `{fp}`")
199
lines.append("")
200
for row in file_rows:
201
sym_type, name, _, docstring, signature = (row[0], row[1], row[2], row[3], row[4])
202
lines.append(f"### {sym_type}: `{name}`")
203
if signature:
204
lines += ["", f"```python\n{signature}\n```"]
205
if docstring:
206
lines += ["", docstring]
207
lines.append("")
208
209
return "\n".join(lines)
210
211
def _template_project_docs(self) -> str:
212
# Stats
213
stats_result = self._store.query(_STATS, {})
214
stats_rows = stats_result.result_set or []
215
216
# Files
217
files_result = self._store.query(_PROJECT_FILES, {})
218
files = [row[0] for row in (files_result.result_set or []) if row[0]]
219
220
# Symbols (capped)
221
syms_result = self._store.query(_TOP_SYMBOLS, {})
222
sym_rows = syms_result.result_set or []
223
224
lines = ["# Project Documentation", ""]
225
lines += ["## Overview", ""]
226
227
if stats_rows:
228
lines.append("| Node type | Count |")
229
lines.append("|-----------|-------|")
230
for row in stats_rows:
231
if row[0]:
232
lines.append(f"| {row[0]} | {row[1]} |")
233
lines.append("")
234
235
if files:
236
lines += ["## Files", ""]
237
for fp in files[:50]:
238
lines.append(f"- `{fp}`")
239
if len(files) > 50:
240
lines.append(f"- _…and {len(files) - 50} more_")
241
lines.append("")
242
243
if sym_rows:
244
lines += ["## Symbols", ""]
245
for row in sym_rows:
246
sym_type, name, fp, docstring = row[0], row[1], row[2], row[3]
247
summary = docstring.split("\n")[0][:80] if docstring else ""
248
loc = f" — `{fp}`" if fp else ""
249
lines.append(f"- **[{sym_type}]** `{name}`{loc}")
250
if summary:
251
lines.append(f" > {summary}")
252
lines.append("")
253
254
return "\n".join(lines)
255
256
# ── LLM-based generation ──────────────────────────────────────────────
257
258
def _llm_file_docs(self, file_path: str) -> str:
259
from navegador.intelligence.nlp import NLPEngine
260
261
engine = NLPEngine(self._store, self._provider) # type: ignore[arg-type]
262
# Use the NLPEngine generate_docs per symbol, then stitch together
263
result = self._store.query(_FILE_SYMBOLS, {"path": file_path})
264
rows = result.result_set or []
265
266
lines = [f"# File: `{file_path}`", ""]
267
if not rows:
268
lines.append("_No symbols found in the graph for this file._")
269
return "\n".join(lines)
270
271
for row in rows[:10]: # cap to avoid excessive API calls
272
name = row[1]
273
lines.append(engine.generate_docs(name, file_path=file_path))
274
lines.append("")
275
276
return "\n".join(lines)
277
278
def _llm_module_docs(self, module_name: str, module_path: str) -> str:
279
from navegador.intelligence.nlp import NLPEngine
280
281
engine = NLPEngine(self._store, self._provider) # type: ignore[arg-type]
282
283
result = self._store.query(_MODULE_SYMBOLS, {"module_path": module_path})
284
rows = result.result_set or []
285
286
lines = [f"# Module: `{module_name}`", ""]
287
if not rows:
288
lines.append("_No symbols found in the graph for this module._")
289
return "\n".join(lines)
290
291
for row in rows[:10]:
292
name, fp = row[1], row[2]
293
lines.append(engine.generate_docs(name, file_path=fp or ""))
294
lines.append("")
295
296
return "\n".join(lines)
297
298
def _llm_project_docs(self) -> str:
299
assert self._provider is not None
300
# Build a compact summary then ask the LLM to produce an overview
301
template_summary = self._template_project_docs()
302
303
prompt = (
304
"You are a technical documentation writer. "
305
"Based on the following auto-generated project summary, write a "
306
"polished, human-readable project README in markdown.\n\n"
307
f"{template_summary}"
308
)
309
return self._provider.complete(prompt)
310

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button