graph-n4j 0.1.0__py3-none-any.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.
graph_n4j/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ graph-n4j — Bridge LLM agents with code repositories via Neo4j graph databases.
3
+
4
+ Based on the CodexGraph NAACL 2025 paper:
5
+ "CodexGraph: Bridging Large Language Models and Code Repositories via Code Graph Databases"
6
+
7
+ Quick Start:
8
+ from graph_n4j import GraphN4J
9
+
10
+ g = GraphN4J.from_env() # reads GROQ_API_KEY, NEO4J_* from .env
11
+ g.index("./my-python-project") # index a local repo
12
+ result = g.ask("What does the Foo class do?")
13
+ print(result.answer)
14
+ """
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ from graph_n4j.core import GraphN4J
19
+ from graph_n4j.graph_db import GraphDB
20
+ from graph_n4j.llm.groq_client import GroqClient
21
+ from graph_n4j.schema import SCHEMA_DESCRIPTION
22
+ from graph_n4j.cache import SkeletonCache, QueryCache, ContextTracker
23
+ from graph_n4j.repo_manager import RepoManager, RepoMetadata
24
+
25
+ __all__ = [
26
+ "GraphN4J",
27
+ "GraphDB",
28
+ "GroqClient",
29
+ "SCHEMA_DESCRIPTION",
30
+ "SkeletonCache",
31
+ "QueryCache",
32
+ "ContextTracker",
33
+ "RepoManager",
34
+ "RepoMetadata",
35
+ "__version__",
36
+ ]
@@ -0,0 +1 @@
1
+ # graph_n4j.agents — Primary Agent, Translation Agent, and iterative pipeline
@@ -0,0 +1,336 @@
1
+ """
2
+ pipeline.py -- Iterative dual-agent pipeline with token efficiency optimizations.
3
+
4
+ Per Section 3.2 of the paper, plus B1-B7 token efficiency improvements:
5
+ B1. Repo skeleton injection (--include-skeleton)
6
+ B2. Query/translation caching (--enable-cache)
7
+ B3. Context pruning — seen code replaced with reference tags (--compact-context)
8
+ B4. Result truncation to top-K rows (--truncate-results)
9
+ B5. Cheaper model for Translation Agent (GROQ_MODEL_TRANSLATION)
10
+ B6. Early-stopping on empty/duplicate results (--early-stop)
11
+ B7. All caches keyed by (repo_id, commit_sha)
12
+ """
13
+
14
+ import os
15
+ import logging
16
+ from dataclasses import dataclass, field
17
+ from typing import Optional
18
+
19
+ from graph_n4j.graph_db import GraphDB
20
+ from graph_n4j.llm.groq_client import GroqClient
21
+ from graph_n4j.agents.primary_agent import PrimaryAgent
22
+ from graph_n4j.agents.translation_agent import TranslationAgent
23
+ from graph_n4j.cache import (
24
+ SkeletonCache, QueryCache, ContextTracker,
25
+ truncate_results, should_early_stop,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class PipelineResult:
33
+ """Result from a complete pipeline run."""
34
+ answer: str
35
+ rounds_used: int
36
+ total_queries: int
37
+ neo4j_roundtrips: int = 0 # B2: actual DB hits (excl. cache hits)
38
+ context_history: list[dict] = field(default_factory=list)
39
+ token_usage: dict = field(default_factory=dict)
40
+ cache_stats: dict = field(default_factory=dict) # B2: hit/miss rates
41
+ errors: list[str] = field(default_factory=list)
42
+ early_stopped: bool = False # B6: whether early-stop fired
43
+ early_stop_reason: str = ""
44
+
45
+
46
+ class CodexGraphPipeline:
47
+ """
48
+ Orchestrates the iterative dual-agent loop with token efficiency optimizations.
49
+
50
+ All B-flags default ON except B5 (cheaper translation model), which is opt-in.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ groq_client: GroqClient,
56
+ graph_db: GraphDB,
57
+ max_rounds: int = 5,
58
+ query_mode: str = "single_query",
59
+ task_instruction: str = "",
60
+ no_translation_agent: bool = False,
61
+ no_edges: bool = False,
62
+ # ── B-flag controls ──
63
+ repo_id: str = "",
64
+ commit_sha: str = "",
65
+ include_skeleton: bool = True, # B1
66
+ enable_cache: bool = True, # B2
67
+ compact_context: bool = True, # B3
68
+ truncate_max_rows: int = 15, # B4 (0 = disabled)
69
+ translation_model: Optional[str] = None, # B5
70
+ early_stop: bool = True, # B6
71
+ # Shared cache objects (reuse across pipeline calls in a session)
72
+ skeleton_cache: Optional[SkeletonCache] = None,
73
+ query_cache: Optional[QueryCache] = None,
74
+ ):
75
+ self.client = groq_client
76
+ self.db = graph_db
77
+ self.max_rounds = max_rounds
78
+ self.query_mode = query_mode
79
+ self.no_translation_agent = no_translation_agent
80
+ self.no_edges = no_edges
81
+
82
+ # B-flag settings
83
+ self.repo_id = repo_id
84
+ self.commit_sha = commit_sha
85
+ self.include_skeleton = include_skeleton
86
+ self.enable_cache = enable_cache
87
+ self.compact_context = compact_context
88
+ self.truncate_max_rows = truncate_max_rows
89
+ self.translation_model = translation_model or os.getenv("GROQ_MODEL_TRANSLATION")
90
+ self.early_stop = early_stop
91
+
92
+ # Cache objects — shared across calls for session-level reuse (B7)
93
+ self.skeleton_cache = skeleton_cache or SkeletonCache()
94
+ self.query_cache = query_cache or QueryCache(enabled=enable_cache)
95
+
96
+ # Build task instruction with ablation modifications
97
+ full_instruction = task_instruction
98
+ if no_translation_agent:
99
+ full_instruction += (
100
+ "\n\nIMPORTANT: You must write Cypher queries directly instead of "
101
+ "natural language queries. Output valid Cypher in the 'queries' array."
102
+ )
103
+ if no_edges:
104
+ full_instruction += (
105
+ "\n\nIMPORTANT: You may ONLY query node attributes (properties). "
106
+ "Do NOT traverse any edges/relationships. Only use MATCH on single "
107
+ "node labels with property filters."
108
+ )
109
+
110
+ # B1: Build/load skeleton and inject into system prompt
111
+ skeleton_text = ""
112
+ if include_skeleton:
113
+ skeleton = self.skeleton_cache.get(repo_id, commit_sha)
114
+ if not skeleton:
115
+ try:
116
+ skeleton = self.skeleton_cache.build_from_db(graph_db, repo_id, commit_sha)
117
+ except Exception as e:
118
+ logger.warning("Failed to build skeleton: %s", e)
119
+ if skeleton:
120
+ skeleton_text = (
121
+ "\n\n## Repository Structure (skeleton)\n"
122
+ "Use this overview to plan targeted queries instead of exploring blindly.\n\n"
123
+ + skeleton.to_prompt_text()
124
+ )
125
+ logger.info("B1: Skeleton injected (%d chars)", len(skeleton_text))
126
+
127
+ self.primary_agent = PrimaryAgent(
128
+ groq_client=groq_client,
129
+ query_mode=query_mode,
130
+ task_instruction=full_instruction + skeleton_text,
131
+ )
132
+
133
+ self.translation_agent = TranslationAgent(
134
+ groq_client=groq_client,
135
+ model_override=self.translation_model, # B5
136
+ )
137
+
138
+ def run(self, question: str) -> PipelineResult:
139
+ """Execute the full iterative pipeline for a user question."""
140
+ initial_tokens = self.client.usage.total_tokens
141
+ context_history: list[dict] = []
142
+ total_queries = 0
143
+ neo4j_roundtrips = 0
144
+ errors: list[str] = []
145
+ previous_results: list[str] = []
146
+
147
+ # B3: Context tracker for this session
148
+ ctx_tracker = ContextTracker(enabled=self.compact_context)
149
+
150
+ logger.info("Pipeline started -- question: '%s'", question[:100])
151
+
152
+ for round_num in range(1, self.max_rounds + 1):
153
+ logger.info("--- Round %d/%d ---", round_num, self.max_rounds)
154
+
155
+ # -- Step 1: Primary Agent generates analysis + queries --
156
+ try:
157
+ primary_response = self.primary_agent.run(question, context_history)
158
+ except Exception as e:
159
+ error_msg = f"Round {round_num}: Primary Agent error -- {e}"
160
+ logger.error(error_msg)
161
+ errors.append(error_msg)
162
+ break
163
+
164
+ # Check if sufficient context
165
+ if primary_response.get("sufficient_context", False):
166
+ answer = primary_response.get("final_answer", primary_response.get("analysis", ""))
167
+ logger.info("Primary Agent signals sufficient context at round %d.", round_num)
168
+ return PipelineResult(
169
+ answer=answer,
170
+ rounds_used=round_num,
171
+ total_queries=total_queries,
172
+ neo4j_roundtrips=neo4j_roundtrips,
173
+ context_history=context_history,
174
+ token_usage=self._get_token_delta(initial_tokens),
175
+ cache_stats=self.query_cache.stats() if self.enable_cache else {},
176
+ errors=errors,
177
+ )
178
+
179
+ queries = primary_response.get("queries", [])
180
+ if not queries:
181
+ logger.warning("No queries generated in round %d. Ending pipeline.", round_num)
182
+ break
183
+
184
+ # -- Step 2: Translate + Execute each query --
185
+ round_results: list[str] = []
186
+ for nl_query in queries:
187
+ total_queries += 1
188
+
189
+ # Get Cypher (from cache or Translation Agent)
190
+ try:
191
+ if self.no_translation_agent:
192
+ cypher = nl_query
193
+ else:
194
+ # B2: Check translation cache first
195
+ cached_cypher = self.query_cache.get_translation(nl_query)
196
+ if cached_cypher:
197
+ cypher = cached_cypher
198
+ logger.info("B2: Translation cache hit for '%s'", nl_query[:50])
199
+ else:
200
+ cypher = self.translation_agent.translate(nl_query)
201
+ self.query_cache.put_translation_with_nl(nl_query, cypher)
202
+ except Exception as e:
203
+ error_msg = f"Translation error for '{nl_query[:50]}': {e}"
204
+ logger.error(error_msg)
205
+ errors.append(error_msg)
206
+ round_results.append(f"(Translation error: {e})")
207
+ continue
208
+
209
+ # Execute against Neo4j (with B2 result cache)
210
+ try:
211
+ # B2: Check result cache
212
+ cached_result = self.query_cache.get_cypher_result(
213
+ cypher, self.repo_id, self.commit_sha
214
+ )
215
+ if cached_result is not None:
216
+ records = cached_result
217
+ logger.info("B2: Result cache hit (%d records)", len(records))
218
+ else:
219
+ records = self.db.run_query(cypher, parameters={"repo_id": self.repo_id})
220
+ neo4j_roundtrips += 1
221
+ # Cache the result
222
+ try:
223
+ self.query_cache.put_cypher_result(
224
+ cypher, self.repo_id, self.commit_sha, records
225
+ )
226
+ except Exception:
227
+ pass # Don't fail on cache write errors
228
+
229
+ # B4: Truncate large result sets
230
+ if self.truncate_max_rows > 0:
231
+ records, truncation_note = truncate_results(
232
+ records, max_rows=self.truncate_max_rows
233
+ )
234
+ else:
235
+ truncation_note = None
236
+
237
+ formatted = self.db.format_results(records)
238
+
239
+ # B3: Compact already-seen code
240
+ if self.compact_context:
241
+ formatted = ctx_tracker.compact_results(formatted, round_num)
242
+ for rec in records:
243
+ ctx_tracker.mark_seen(rec, round_num)
244
+
245
+ if truncation_note:
246
+ formatted += f"\n\n{truncation_note}"
247
+
248
+ round_results.append(formatted)
249
+ logger.info("Query returned %d records.", len(records))
250
+
251
+ except Exception as e:
252
+ error_msg = f"Neo4j query error -- Cypher: '{cypher[:80]}' -- {e}"
253
+ logger.error(error_msg)
254
+ errors.append(error_msg)
255
+ round_results.append(f"(Query execution error: {e})")
256
+
257
+ # B6: Early-stopping heuristic
258
+ if self.early_stop:
259
+ stop, reason = should_early_stop(round_results, previous_results)
260
+ if stop:
261
+ logger.info("B6: Early stop triggered -- %s", reason)
262
+ # Store context and break to final synthesis
263
+ context_history.append({
264
+ "round": round_num,
265
+ "queries": queries,
266
+ "results": round_results,
267
+ })
268
+ return PipelineResult(
269
+ answer=self._synthesize_final(question, context_history),
270
+ rounds_used=round_num,
271
+ total_queries=total_queries,
272
+ neo4j_roundtrips=neo4j_roundtrips,
273
+ context_history=context_history,
274
+ token_usage=self._get_token_delta(initial_tokens),
275
+ cache_stats=self.query_cache.stats() if self.enable_cache else {},
276
+ errors=errors,
277
+ early_stopped=True,
278
+ early_stop_reason=reason,
279
+ )
280
+
281
+ previous_results = round_results
282
+
283
+ # -- Step 3: Store round context --
284
+ context_history.append({
285
+ "round": round_num,
286
+ "queries": queries,
287
+ "results": round_results,
288
+ })
289
+
290
+ # -- Fallback: max rounds reached --
291
+ logger.info("Max rounds reached. Requesting final synthesis.")
292
+ answer = self._synthesize_final(question, context_history)
293
+
294
+ return PipelineResult(
295
+ answer=answer,
296
+ rounds_used=self.max_rounds,
297
+ total_queries=total_queries,
298
+ neo4j_roundtrips=neo4j_roundtrips,
299
+ context_history=context_history,
300
+ token_usage=self._get_token_delta(initial_tokens),
301
+ cache_stats=self.query_cache.stats() if self.enable_cache else {},
302
+ errors=errors,
303
+ )
304
+
305
+ def _synthesize_final(self, question: str, context_history: list[dict]) -> str:
306
+ """Ask the LLM for a final answer given all gathered context."""
307
+ try:
308
+ final_messages = [{
309
+ "role": "system",
310
+ "content": (
311
+ "You have reached the maximum number of retrieval rounds. "
312
+ "Based on all the context gathered so far, provide your best "
313
+ "answer to the user's question. Respond with valid JSON: "
314
+ '{"analysis": "...", "final_answer": "..."}'
315
+ ),
316
+ }, {
317
+ "role": "user",
318
+ "content": f"Question: {question}\n\nContext gathered:\n" +
319
+ "\n".join(
320
+ f"Round {e['round']}: {e['results']}"
321
+ for e in context_history
322
+ ),
323
+ }]
324
+ final = self.client.chat_json(final_messages, temperature=0.3)
325
+ return final.get("final_answer", final.get("analysis", "Unable to determine answer."))
326
+ except Exception as e:
327
+ logger.error("Final synthesis failed: %s", e)
328
+ return "Unable to synthesize a final answer. See context history for partial results."
329
+
330
+ def _get_token_delta(self, initial_total: int) -> dict:
331
+ """Calculate tokens used in this pipeline run."""
332
+ delta = self.client.usage.total_tokens - initial_total
333
+ return {
334
+ "pipeline_total_tokens": delta,
335
+ "cumulative": self.client.usage.to_dict(),
336
+ }
@@ -0,0 +1,185 @@
1
+ """
2
+ primary_agent.py — The Primary Agent in CodexGraph's dual-agent architecture.
3
+
4
+ Per Section 3.2 of the paper, the Primary Agent:
5
+ - Receives the user's code question + accumulated context from prior rounds
6
+ - Produces: (a) natural-language analysis, (b) one or more NL queries describing
7
+ what to retrieve from the graph, (c) a decision on whether enough context exists
8
+ - Supports both 'single_query' and 'multiple_queries' modes
9
+ """
10
+
11
+ import logging
12
+ from typing import Optional
13
+
14
+ from graph_n4j.llm.groq_client import GroqClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # ═══════════════════════════════════════════
20
+ # System prompts
21
+ # ═══════════════════════════════════════════
22
+
23
+ PRIMARY_SYSTEM_PROMPT = """You are the Primary Agent in the CodexGraph system. Your job is to help \
24
+ answer questions about a Python code repository by deciding what information to \
25
+ retrieve from a code graph database.
26
+
27
+ The graph database contains the following node and edge types:
28
+
29
+ **Nodes:** MODULE (name, file_path), CLASS (name, file_path, signature, code), \
30
+ FUNCTION (name, file_path, signature, code), METHOD (name, file_path, class_name, \
31
+ signature, code), FIELD (name, file_path, class_name), GLOBAL_VARIABLE (name, \
32
+ file_path, code)
33
+
34
+ **Edges:** CONTAINS (MODULE->CLASS/FUNCTION/GLOBAL_VARIABLE), \
35
+ HAS_METHOD (CLASS->METHOD), HAS_FIELD (CLASS->FIELD), \
36
+ INHERITS (CLASS->CLASS), USES (FUNCTION/METHOD->GLOBAL_VARIABLE/FIELD, \
37
+ with source_association_type and target_association_type)
38
+
39
+ ## Your Process
40
+
41
+ Each round, you will:
42
+ 1. Analyze what information you still need
43
+ 2. Write natural language queries describing what to retrieve from the graph
44
+ 3. Decide if you have gathered enough context to answer the question
45
+
46
+ ## Output Format
47
+
48
+ You MUST respond with valid JSON in this exact format:
49
+ {{
50
+ "analysis": "Your analysis of what information is needed and why",
51
+ "queries": [
52
+ "Natural language description of what to retrieve from the graph database"
53
+ ],
54
+ "sufficient_context": false
55
+ }}
56
+
57
+ When sufficient_context is true, include a "final_answer" field:
58
+ {{
59
+ "analysis": "Final analysis synthesizing all gathered context",
60
+ "queries": [],
61
+ "sufficient_context": true,
62
+ "final_answer": "Your comprehensive answer to the user's question"
63
+ }}
64
+
65
+ ## Rules
66
+ - Each query should describe ONE specific retrieval operation
67
+ - Queries should reference specific node types, names, and relationships from the schema
68
+ - Be precise: "Find the class named X" is better than "find the relevant class"
69
+ - {query_mode_instruction}
70
+ - Use information from previous rounds to refine your queries
71
+ - Do NOT write Cypher — write natural language descriptions of what to retrieve
72
+ """
73
+
74
+ SINGLE_QUERY_INSTRUCTION = "Generate exactly ONE query per round (best for complex reasoning tasks)"
75
+ MULTIPLE_QUERIES_INSTRUCTION = "Generate up to 3 queries per round (best for gathering broad context quickly)"
76
+
77
+
78
+ def get_system_prompt(
79
+ query_mode: str = "single_query",
80
+ task_specific_instruction: str = "",
81
+ ) -> str:
82
+ """
83
+ Build the Primary Agent's system prompt.
84
+
85
+ Args:
86
+ query_mode: "single_query" or "multiple_queries"
87
+ task_specific_instruction: Additional instructions for specific tasks
88
+ (e.g., Code Debugger, Code Generator, etc.)
89
+ """
90
+ instruction = (
91
+ SINGLE_QUERY_INSTRUCTION if query_mode == "single_query"
92
+ else MULTIPLE_QUERIES_INSTRUCTION
93
+ )
94
+
95
+ prompt = PRIMARY_SYSTEM_PROMPT.format(query_mode_instruction=instruction)
96
+
97
+ if task_specific_instruction:
98
+ prompt += f"\n\n## Task-Specific Instructions\n{task_specific_instruction}"
99
+
100
+ return prompt
101
+
102
+
103
+ # ═══════════════════════════════════════════
104
+ # Primary Agent class
105
+ # ═══════════════════════════════════════════
106
+
107
+ class PrimaryAgent:
108
+ """
109
+ The Primary Agent: analyzes the user's question, generates NL graph queries,
110
+ and synthesizes a final answer when sufficient context is gathered.
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ groq_client: GroqClient,
116
+ query_mode: str = "single_query",
117
+ task_instruction: str = "",
118
+ temperature: float = 0.3,
119
+ ):
120
+ self.client = groq_client
121
+ self.query_mode = query_mode
122
+ self.temperature = temperature
123
+ self.system_prompt = get_system_prompt(query_mode, task_instruction)
124
+
125
+ def run(
126
+ self,
127
+ user_question: str,
128
+ context_history: list[dict],
129
+ ) -> dict:
130
+ """
131
+ Execute one round of the Primary Agent.
132
+
133
+ Args:
134
+ user_question: The original user question.
135
+ context_history: List of prior round results:
136
+ [{"round": 1, "queries": [...], "results": [...]}, ...]
137
+
138
+ Returns:
139
+ Parsed JSON dict with keys: analysis, queries, sufficient_context,
140
+ and optionally final_answer.
141
+ """
142
+ messages = [{"role": "system", "content": self.system_prompt}]
143
+
144
+ # Build the user message with accumulated context
145
+ user_content = f"## User Question\n{user_question}\n"
146
+
147
+ if context_history:
148
+ user_content += "\n## Context from Previous Rounds\n"
149
+ for entry in context_history:
150
+ user_content += f"\n### Round {entry['round']}\n"
151
+ for i, (query, result) in enumerate(
152
+ zip(entry["queries"], entry["results"]), 1
153
+ ):
154
+ user_content += f"**Query {i}:** {query}\n"
155
+ user_content += f"**Result:** {result}\n\n"
156
+ else:
157
+ user_content += "\n(This is the first round — no prior context.)\n"
158
+
159
+ messages.append({"role": "user", "content": user_content})
160
+
161
+ # Call Groq with JSON mode
162
+ response = self.client.chat_json(
163
+ messages=messages,
164
+ temperature=self.temperature,
165
+ )
166
+
167
+ # Validate response structure
168
+ if "analysis" not in response:
169
+ response["analysis"] = ""
170
+ if "queries" not in response:
171
+ response["queries"] = []
172
+ if "sufficient_context" not in response:
173
+ response["sufficient_context"] = False
174
+
175
+ # Enforce query mode limits
176
+ if self.query_mode == "single_query" and len(response["queries"]) > 1:
177
+ response["queries"] = response["queries"][:1]
178
+
179
+ logger.info(
180
+ "Primary Agent — round analysis: %s queries, sufficient=%s",
181
+ len(response["queries"]),
182
+ response["sufficient_context"],
183
+ )
184
+
185
+ return response
@@ -0,0 +1,98 @@
1
+ """
2
+ translation_agent.py — The Translation Agent in CodexGraph's dual-agent architecture.
3
+
4
+ Per Section 3.2 ("write then translate" strategy), the Translation Agent:
5
+ - Receives the full graph schema + a natural language query from the Primary Agent
6
+ - Outputs a syntactically valid Cypher query
7
+ - Uses temperature=0.0 for deterministic translations
8
+ - The system prompt embeds the full schema so the model translates reliably
9
+ """
10
+
11
+ import logging
12
+
13
+ from graph_n4j.llm.groq_client import GroqClient
14
+ from graph_n4j.schema import SCHEMA_DESCRIPTION
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # ═══════════════════════════════════════════
20
+ # System prompt
21
+ # ═══════════════════════════════════════════
22
+
23
+ TRANSLATION_SYSTEM_PROMPT = f"""You are the Translation Agent in the CodexGraph system. Your ONLY job \
24
+ is to translate natural language queries about a code repository into valid Cypher \
25
+ queries for Neo4j.
26
+
27
+ {SCHEMA_DESCRIPTION}
28
+
29
+ ## Rules
30
+
31
+ 1. Output ONLY the Cypher query — no explanations, no markdown fences, no comments.
32
+ 2. Use MATCH patterns that align with the schema above.
33
+ 3. Always use parameterized property matching where possible.
34
+ 4. Return useful properties in your RETURN clause (name, file_path, signature, code).
35
+ 5. Use OPTIONAL MATCH when a relationship might not exist.
36
+ 6. Limit results to 20 rows unless the query implies exhaustive retrieval.
37
+ 7. For inheritance chains, use variable-length paths: `-[:INHERITS*]->`.
38
+ 8. When searching by name, use case-sensitive exact match by default.
39
+ 9. If the query mentions "all methods" or "all fields" of a class, include inherited ones via INHERITS edges.
40
+ 10. Always include RETURN — never write a query that only MATCHes without returning.
41
+
42
+ ## Examples
43
+
44
+ Query: "Find the class named Shape and its methods"
45
+ MATCH (c:CLASS {{name: 'Shape'}})-[:HAS_METHOD]->(m:METHOD) RETURN c.name, c.file_path, c.signature, m.name, m.signature, m.code
46
+
47
+ Query: "Find all classes that inherit from Shape"
48
+ MATCH (c:CLASS)-[:INHERITS]->(base:CLASS {{name: 'Shape'}}) RETURN c.name, c.file_path, c.signature
49
+
50
+ Query: "Find the module containing the function calculate_area"
51
+ MATCH (m:MODULE)-[:CONTAINS]->(f:FUNCTION {{name: 'calculate_area'}}) RETURN m.name, m.file_path, f.signature
52
+
53
+ Query: "Find all global variables used by the function process_data"
54
+ MATCH (f:FUNCTION {{name: 'process_data'}})-[:USES]->(gv:GLOBAL_VARIABLE) RETURN gv.name, gv.file_path, gv.code
55
+
56
+ Query: "Find the complete inheritance chain for class Circle"
57
+ MATCH (c:CLASS {{name: 'Circle'}})-[:INHERITS*]->(parent:CLASS) RETURN parent.name, parent.file_path, parent.signature
58
+ """
59
+
60
+
61
+ class TranslationAgent:
62
+ """
63
+ The Translation Agent: converts natural language graph queries into Cypher.
64
+ Uses temperature=0.0 for deterministic output.
65
+
66
+ B5: Supports a separate model_override so you can run a cheaper model
67
+ for translation while keeping the Primary Agent on the strong model.
68
+ Configure via GROQ_MODEL_TRANSLATION env var or pass model_override=.
69
+ """
70
+
71
+ def __init__(self, groq_client: GroqClient, model_override: str | None = None):
72
+ self.client = groq_client
73
+ self.model_override = model_override
74
+
75
+ def translate(self, nl_query: str) -> str:
76
+ """
77
+ Translate a natural language query into a Cypher query.
78
+
79
+ Args:
80
+ nl_query: Natural language description of what to retrieve.
81
+
82
+ Returns:
83
+ A Cypher query string (fences stripped).
84
+ """
85
+ messages = [
86
+ {"role": "system", "content": TRANSLATION_SYSTEM_PROMPT},
87
+ {"role": "user", "content": nl_query},
88
+ ]
89
+
90
+ cypher = self.client.chat_cypher(
91
+ messages=messages,
92
+ temperature=0.0,
93
+ max_tokens=2048,
94
+ model_override=self.model_override,
95
+ )
96
+
97
+ logger.info("Translation Agent -- NL: '%s' -> Cypher: '%s'", nl_query[:80], cypher[:80])
98
+ return cypher
@@ -0,0 +1 @@
1
+ # graph_n4j.apps — 5 real-world applications from Section 6 of the paper