graph-n4j 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- graph_n4j-0.1.0/PKG-INFO +126 -0
- graph_n4j-0.1.0/README.md +88 -0
- graph_n4j-0.1.0/graph_n4j/__init__.py +36 -0
- graph_n4j-0.1.0/graph_n4j/agents/__init__.py +1 -0
- graph_n4j-0.1.0/graph_n4j/agents/pipeline.py +336 -0
- graph_n4j-0.1.0/graph_n4j/agents/primary_agent.py +185 -0
- graph_n4j-0.1.0/graph_n4j/agents/translation_agent.py +98 -0
- graph_n4j-0.1.0/graph_n4j/apps/__init__.py +1 -0
- graph_n4j-0.1.0/graph_n4j/apps/code_chat.py +61 -0
- graph_n4j-0.1.0/graph_n4j/apps/code_commenter.py +84 -0
- graph_n4j-0.1.0/graph_n4j/apps/code_debugger.py +80 -0
- graph_n4j-0.1.0/graph_n4j/apps/code_generator.py +76 -0
- graph_n4j-0.1.0/graph_n4j/apps/code_unittester.py +81 -0
- graph_n4j-0.1.0/graph_n4j/cache.py +434 -0
- graph_n4j-0.1.0/graph_n4j/cli.py +233 -0
- graph_n4j-0.1.0/graph_n4j/core.py +292 -0
- graph_n4j-0.1.0/graph_n4j/graph_db.py +366 -0
- graph_n4j-0.1.0/graph_n4j/indexer/__init__.py +1 -0
- graph_n4j-0.1.0/graph_n4j/indexer/edge_completion.py +495 -0
- graph_n4j-0.1.0/graph_n4j/indexer/shallow_index.py +468 -0
- graph_n4j-0.1.0/graph_n4j/llm/__init__.py +1 -0
- graph_n4j-0.1.0/graph_n4j/llm/groq_client.py +253 -0
- graph_n4j-0.1.0/graph_n4j/py.typed +1 -0
- graph_n4j-0.1.0/graph_n4j/repo_manager.py +386 -0
- graph_n4j-0.1.0/graph_n4j/schema.py +240 -0
- graph_n4j-0.1.0/graph_n4j.egg-info/PKG-INFO +126 -0
- graph_n4j-0.1.0/graph_n4j.egg-info/SOURCES.txt +32 -0
- graph_n4j-0.1.0/graph_n4j.egg-info/dependency_links.txt +1 -0
- graph_n4j-0.1.0/graph_n4j.egg-info/entry_points.txt +2 -0
- graph_n4j-0.1.0/graph_n4j.egg-info/requires.txt +11 -0
- graph_n4j-0.1.0/graph_n4j.egg-info/top_level.txt +1 -0
- graph_n4j-0.1.0/pyproject.toml +75 -0
- graph_n4j-0.1.0/setup.cfg +4 -0
graph_n4j-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 CodexGraph Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
graph_n4j-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graph-n4j
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bridge LLM agents with code repositories via Neo4j graph databases. Based on the CodexGraph NAACL 2025 paper.
|
|
5
|
+
Author: Shubham Kumar Sahu
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ShubhamKumarSahu-svg/graph-n4j
|
|
8
|
+
Project-URL: Documentation, https://github.com/ShubhamKumarSahu-svg/graph-n4j#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/ShubhamKumarSahu-svg/graph-n4j
|
|
10
|
+
Project-URL: Issues, https://github.com/ShubhamKumarSahu-svg/graph-n4j/issues
|
|
11
|
+
Keywords: neo4j,graph-database,code-analysis,llm,code-graph,static-analysis,code-intelligence,groq,cypher
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: groq>=0.11.0
|
|
28
|
+
Requires-Dist: neo4j>=5.20.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
30
|
+
Requires-Dist: gitpython>=3.1.40
|
|
31
|
+
Requires-Dist: diskcache>=5.6.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# graph-n4j
|
|
40
|
+
|
|
41
|
+
Bridge LLM agents with code repositories via Neo4j graph databases. Based on the CodexGraph NAACL 2025 paper.
|
|
42
|
+
|
|
43
|
+
`graph-n4j` builds a highly structured code graph (AST, inheritance, composition, calls) into Neo4j and uses a dual-agent LLM pipeline to let you chat with your entire codebase with perfect accuracy and drastically reduced token consumption.
|
|
44
|
+
|
|
45
|
+
## Prerequisites
|
|
46
|
+
|
|
47
|
+
To use `graph-n4j`, you need two things running/available:
|
|
48
|
+
|
|
49
|
+
1. **Neo4j Database**: You need a running Neo4j instance.
|
|
50
|
+
- *Quickest way (Docker)*: `docker run -d -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j:latest`
|
|
51
|
+
- *Cloud option*: You can also use a free Neo4j AuraDB instance.
|
|
52
|
+
|
|
53
|
+
2. **Groq API Key**: You need an API key from [Groq](https://console.groq.com/) to power the LLM agents.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install graph-n4j
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Setup & Configuration
|
|
62
|
+
|
|
63
|
+
Configure the package using environment variables. You can set them in your terminal or create a `.env` file in the directory where you run your commands:
|
|
64
|
+
|
|
65
|
+
```env
|
|
66
|
+
# Required: Groq API Key
|
|
67
|
+
GROQ_API_KEY=gsk_your_api_key_here
|
|
68
|
+
|
|
69
|
+
# Optional: Set the model (Defaults to llama-3.3-70b-versatile)
|
|
70
|
+
GROQ_MODEL=llama-3.3-70b-versatile
|
|
71
|
+
# B5 optimization: Use a cheaper/faster model for translation to save costs
|
|
72
|
+
GROQ_MODEL_TRANSLATION=llama-3.1-8b-instant
|
|
73
|
+
|
|
74
|
+
# Neo4j connection (These are the defaults)
|
|
75
|
+
NEO4J_URI=bolt://localhost:7687
|
|
76
|
+
NEO4J_USER=neo4j
|
|
77
|
+
NEO4J_PASSWORD=password
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## CLI Usage
|
|
81
|
+
|
|
82
|
+
The package installs a handy command-line tool `graph-n4j`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 1. Index a local Python repository
|
|
86
|
+
graph-n4j index /path/to/your/python/repo
|
|
87
|
+
|
|
88
|
+
# OR Clone and index a GitHub repository directly
|
|
89
|
+
graph-n4j index-github pallets/click
|
|
90
|
+
|
|
91
|
+
# 2. Chat with your indexed codebase!
|
|
92
|
+
graph-n4j ask "How does the command line parsing work in this repo?"
|
|
93
|
+
|
|
94
|
+
# Check graph statistics
|
|
95
|
+
graph-n4j stats
|
|
96
|
+
|
|
97
|
+
# List all indexed repos
|
|
98
|
+
graph-n4j repos
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Python API Usage
|
|
102
|
+
|
|
103
|
+
You can also use it programmatically in your own apps:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import os
|
|
107
|
+
from graph_n4j import GraphN4J
|
|
108
|
+
|
|
109
|
+
# Set up environment or let it load from .env
|
|
110
|
+
os.environ["GROQ_API_KEY"] = "gsk_your_api_key_here"
|
|
111
|
+
|
|
112
|
+
# Initialize the API
|
|
113
|
+
g = GraphN4J.from_env()
|
|
114
|
+
|
|
115
|
+
# Index a repository
|
|
116
|
+
g.index("./my-project")
|
|
117
|
+
|
|
118
|
+
# Chat with the codebase
|
|
119
|
+
result = g.ask("What does the UserManager class do and what does it inherit from?")
|
|
120
|
+
print(result.answer)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Features
|
|
124
|
+
- **B1-B7 Token Efficiency**: Implements all 7 token optimizations from the CodexGraph paper (Skeleton caching, Context pruning, Early-stopping, etc.)
|
|
125
|
+
- **Multi-repo support**: Seamlessly index and query across multiple isolated repositories.
|
|
126
|
+
- **AST Parsing**: Highly accurate edge extraction for classes, functions, globals, fields, and inheritance.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# graph-n4j
|
|
2
|
+
|
|
3
|
+
Bridge LLM agents with code repositories via Neo4j graph databases. Based on the CodexGraph NAACL 2025 paper.
|
|
4
|
+
|
|
5
|
+
`graph-n4j` builds a highly structured code graph (AST, inheritance, composition, calls) into Neo4j and uses a dual-agent LLM pipeline to let you chat with your entire codebase with perfect accuracy and drastically reduced token consumption.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
To use `graph-n4j`, you need two things running/available:
|
|
10
|
+
|
|
11
|
+
1. **Neo4j Database**: You need a running Neo4j instance.
|
|
12
|
+
- *Quickest way (Docker)*: `docker run -d -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j:latest`
|
|
13
|
+
- *Cloud option*: You can also use a free Neo4j AuraDB instance.
|
|
14
|
+
|
|
15
|
+
2. **Groq API Key**: You need an API key from [Groq](https://console.groq.com/) to power the LLM agents.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install graph-n4j
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Setup & Configuration
|
|
24
|
+
|
|
25
|
+
Configure the package using environment variables. You can set them in your terminal or create a `.env` file in the directory where you run your commands:
|
|
26
|
+
|
|
27
|
+
```env
|
|
28
|
+
# Required: Groq API Key
|
|
29
|
+
GROQ_API_KEY=gsk_your_api_key_here
|
|
30
|
+
|
|
31
|
+
# Optional: Set the model (Defaults to llama-3.3-70b-versatile)
|
|
32
|
+
GROQ_MODEL=llama-3.3-70b-versatile
|
|
33
|
+
# B5 optimization: Use a cheaper/faster model for translation to save costs
|
|
34
|
+
GROQ_MODEL_TRANSLATION=llama-3.1-8b-instant
|
|
35
|
+
|
|
36
|
+
# Neo4j connection (These are the defaults)
|
|
37
|
+
NEO4J_URI=bolt://localhost:7687
|
|
38
|
+
NEO4J_USER=neo4j
|
|
39
|
+
NEO4J_PASSWORD=password
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## CLI Usage
|
|
43
|
+
|
|
44
|
+
The package installs a handy command-line tool `graph-n4j`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1. Index a local Python repository
|
|
48
|
+
graph-n4j index /path/to/your/python/repo
|
|
49
|
+
|
|
50
|
+
# OR Clone and index a GitHub repository directly
|
|
51
|
+
graph-n4j index-github pallets/click
|
|
52
|
+
|
|
53
|
+
# 2. Chat with your indexed codebase!
|
|
54
|
+
graph-n4j ask "How does the command line parsing work in this repo?"
|
|
55
|
+
|
|
56
|
+
# Check graph statistics
|
|
57
|
+
graph-n4j stats
|
|
58
|
+
|
|
59
|
+
# List all indexed repos
|
|
60
|
+
graph-n4j repos
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Python API Usage
|
|
64
|
+
|
|
65
|
+
You can also use it programmatically in your own apps:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import os
|
|
69
|
+
from graph_n4j import GraphN4J
|
|
70
|
+
|
|
71
|
+
# Set up environment or let it load from .env
|
|
72
|
+
os.environ["GROQ_API_KEY"] = "gsk_your_api_key_here"
|
|
73
|
+
|
|
74
|
+
# Initialize the API
|
|
75
|
+
g = GraphN4J.from_env()
|
|
76
|
+
|
|
77
|
+
# Index a repository
|
|
78
|
+
g.index("./my-project")
|
|
79
|
+
|
|
80
|
+
# Chat with the codebase
|
|
81
|
+
result = g.ask("What does the UserManager class do and what does it inherit from?")
|
|
82
|
+
print(result.answer)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Features
|
|
86
|
+
- **B1-B7 Token Efficiency**: Implements all 7 token optimizations from the CodexGraph paper (Skeleton caching, Context pruning, Early-stopping, etc.)
|
|
87
|
+
- **Multi-repo support**: Seamlessly index and query across multiple isolated repositories.
|
|
88
|
+
- **AST Parsing**: Highly accurate edge extraction for classes, functions, globals, fields, and inheritance.
|
|
@@ -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
|
+
}
|