Navegador

Blame History Raw 257 lines
1
"""
2
NLPEngine — natural language queries, community naming, and documentation generation.
3
4
Converts plain-English questions into Cypher queries, names communities with
5
descriptive labels, and generates documentation for individual symbols.
6
7
Usage::
8
9
from navegador.graph import GraphStore
10
from navegador.llm import get_provider
11
from navegador.intelligence.nlp import NLPEngine
12
13
store = GraphStore.sqlite(".navegador/graph.db")
14
provider = get_provider("anthropic")
15
engine = NLPEngine(store, provider)
16
17
answer = engine.natural_query("Which functions call authenticate_user?")
18
print(answer)
19
20
docs = engine.generate_docs("authenticate_user", file_path="auth.py")
21
print(docs)
22
"""
23
24
from __future__ import annotations
25
26
import json
27
import re
28
from typing import TYPE_CHECKING, Any
29
30
if TYPE_CHECKING:
31
from navegador.graph.store import GraphStore
32
from navegador.intelligence.community import Community
33
from navegador.llm import LLMProvider
34
35
36
# ── Prompt templates ──────────────────────────────────────────────────────────
37
38
_SCHEMA_SUMMARY = """
39
The navegador knowledge graph contains these node types:
40
Function, Class, Method, File, Decorator — code layer
41
Concept, Rule, Decision, WikiPage, Domain, Person — knowledge layer
42
43
Common relationships:
44
CALLS, INHERITS, DECORATES, CONTAINS, REFERENCES, IMPLEMENTS,
45
BELONGS_TO, GOVERNS, DOCUMENTS, RELATED_TO, ASSIGNED_TO, DECIDED_BY
46
47
Node properties (where present):
48
name, file_path, line_start, docstring, description, signature,
49
status, domain, rationale, alternatives, date, community
50
"""
51
52
_NL_TO_CYPHER_PROMPT = """\
53
You are a FalkorDB Cypher expert. Given the schema below and a user question,
54
write a single Cypher query that answers the question.
55
56
Return ONLY the Cypher query — no markdown fences, no explanation.
57
58
{schema}
59
60
User question: {question}
61
"""
62
63
_FORMAT_RESULT_PROMPT = """\
64
The user asked: "{question}"
65
66
The Cypher query executed was:
67
{cypher}
68
69
The raw result rows are:
70
{rows}
71
72
Please summarise the result in a clear, concise paragraph (2–5 sentences).
73
"""
74
75
_NAME_COMMUNITY_PROMPT = """\
76
You are naming software communities detected via graph analysis.
77
78
Community members (function/class/concept names): {members}
79
80
Based on these names, suggest a short, descriptive community name (3–6 words).
81
Return ONLY the name — no punctuation, no explanation.
82
"""
83
84
_GENERATE_DOCS_PROMPT = """\
85
Generate concise markdown documentation for the symbol described below.
86
87
Symbol name: {name}
88
File: {file_path}
89
Type: {type}
90
Docstring: {docstring}
91
Signature: {signature}
92
Callers: {callers}
93
Callees: {callees}
94
95
Write:
96
1. A one-paragraph description.
97
2. Parameters (if applicable).
98
3. Returns (if applicable).
99
4. Example usage snippet.
100
101
Use markdown.
102
"""
103
104
105
class NLPEngine:
106
"""
107
LLM-powered natural language interface to the navegador graph.
108
109
Args:
110
store: A :class:`~navegador.graph.GraphStore` instance.
111
provider: An :class:`~navegador.llm.LLMProvider` for completions.
112
"""
113
114
def __init__(self, store: "GraphStore", provider: "LLMProvider") -> None:
115
self._store = store
116
self._provider = provider
117
118
# ── Natural language query ─────────────────────────────────────────────
119
120
def natural_query(self, question: str) -> str:
121
"""
122
Convert a natural-language *question* into Cypher, execute it, and
123
return an LLM-formatted answer.
124
125
Args:
126
question: Plain-English question about the codebase or knowledge
127
graph (e.g. ``"Which functions call validate_token?"``).
128
129
Returns:
130
A human-readable answer string.
131
"""
132
# Step 1: translate question → Cypher
133
cypher_prompt = _NL_TO_CYPHER_PROMPT.format(schema=_SCHEMA_SUMMARY, question=question)
134
cypher = self._provider.complete(cypher_prompt).strip()
135
136
# Strip any accidental markdown fences the model may still produce
137
cypher = _strip_fences(cypher)
138
139
# Step 2: execute
140
try:
141
result = self._store.query(cypher, {})
142
rows = result.result_set or []
143
except Exception as exc: # noqa: BLE001
144
return (
145
f"Failed to execute the generated Cypher query.\n\nQuery: {cypher}\n\nError: {exc}"
146
)
147
148
# Step 3: format result
149
rows_text = json.dumps(rows[:50], indent=2, default=str)
150
fmt_prompt = _FORMAT_RESULT_PROMPT.format(question=question, cypher=cypher, rows=rows_text)
151
return self._provider.complete(fmt_prompt)
152
153
# ── Community naming ──────────────────────────────────────────────────
154
155
def name_communities(self, communities: list["Community"]) -> list[dict[str, Any]]:
156
"""
157
Use the LLM to generate a meaningful name for each community.
158
159
Args:
160
communities: List of :class:`~navegador.intelligence.community.Community`
161
objects (as returned by :meth:`~CommunityDetector.detect`).
162
163
Returns:
164
List of dicts with keys ``original_name``, ``suggested_name``,
165
``members``, ``size``.
166
"""
167
named: list[dict[str, Any]] = []
168
for comm in communities:
169
members_str = ", ".join(comm.members[:20]) # cap to avoid huge prompts
170
prompt = _NAME_COMMUNITY_PROMPT.format(members=members_str)
171
try:
172
suggested = self._provider.complete(prompt).strip()
173
except Exception: # noqa: BLE001
174
suggested = comm.name
175
named.append(
176
{
177
"original_name": comm.name,
178
"suggested_name": suggested,
179
"members": comm.members,
180
"size": comm.size,
181
}
182
)
183
return named
184
185
# ── Documentation generation ──────────────────────────────────────────
186
187
def generate_docs(self, name: str, file_path: str = "") -> str:
188
"""
189
Generate markdown documentation for a named symbol.
190
191
Retrieves graph context (type, docstring, signature, callers, callees)
192
and asks the LLM to produce structured markdown.
193
194
Args:
195
name: Symbol name (function, class, etc.).
196
file_path: Optional file path to disambiguate.
197
198
Returns:
199
Markdown documentation string.
200
"""
201
# Look up the node
202
cypher = """
203
MATCH (n)
204
WHERE n.name = $name AND ($file_path = '' OR n.file_path = $file_path)
205
RETURN labels(n)[0] AS type, n.name AS name,
206
coalesce(n.file_path, '') AS file_path,
207
coalesce(n.docstring, n.description, '') AS docstring,
208
coalesce(n.signature, '') AS signature
209
LIMIT 1
210
"""
211
result = self._store.query(cypher, {"name": name, "file_path": file_path})
212
rows = result.result_set or []
213
214
node_type = "Unknown"
215
docstring = ""
216
signature = ""
217
fp = file_path
218
219
if rows:
220
row = rows[0]
221
node_type, _, fp, docstring, signature = (row[0], row[1], row[2], row[3], row[4])
222
223
# Fetch callers
224
callers_result = self._store.query(
225
"MATCH (caller)-[:CALLS]->(n {name: $name}) RETURN caller.name LIMIT 10",
226
{"name": name},
227
)
228
callers = [r[0] for r in (callers_result.result_set or []) if r[0]]
229
230
# Fetch callees
231
callees_result = self._store.query(
232
"MATCH (n {name: $name})-[:CALLS]->(callee) RETURN callee.name LIMIT 10",
233
{"name": name},
234
)
235
callees = [r[0] for r in (callees_result.result_set or []) if r[0]]
236
237
prompt = _GENERATE_DOCS_PROMPT.format(
238
name=name,
239
file_path=fp,
240
type=node_type,
241
docstring=docstring or "(none)",
242
signature=signature or "(none)",
243
callers=", ".join(callers) if callers else "(none)",
244
callees=", ".join(callees) if callees else "(none)",
245
)
246
return self._provider.complete(prompt)
247
248
249
# ── Helpers ───────────────────────────────────────────────────────────────────
250
251
252
def _strip_fences(text: str) -> str:
253
"""Remove markdown code fences (```cypher … ```) from LLM output."""
254
text = re.sub(r"```[a-zA-Z]*\n?", "", text)
255
text = text.replace("```", "")
256
return text.strip()
257

Keyboard Shortcuts

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