cyphersmith 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.
- cyphersmith-0.1.0/LICENSE +21 -0
- cyphersmith-0.1.0/PKG-INFO +135 -0
- cyphersmith-0.1.0/README.md +102 -0
- cyphersmith-0.1.0/pyproject.toml +55 -0
- cyphersmith-0.1.0/setup.cfg +4 -0
- cyphersmith-0.1.0/src/cyphersmith/__init__.py +16 -0
- cyphersmith-0.1.0/src/cyphersmith/__main__.py +4 -0
- cyphersmith-0.1.0/src/cyphersmith/cli.py +303 -0
- cyphersmith-0.1.0/src/cyphersmith/context.py +56 -0
- cyphersmith-0.1.0/src/cyphersmith/generator.py +373 -0
- cyphersmith-0.1.0/src/cyphersmith/llm.py +162 -0
- cyphersmith-0.1.0/src/cyphersmith/models.py +108 -0
- cyphersmith-0.1.0/src/cyphersmith/neo4j_client.py +414 -0
- cyphersmith-0.1.0/src/cyphersmith/progress.py +66 -0
- cyphersmith-0.1.0/src/cyphersmith/prompts.py +129 -0
- cyphersmith-0.1.0/src/cyphersmith/safety.py +54 -0
- cyphersmith-0.1.0/src/cyphersmith/validation.py +167 -0
- cyphersmith-0.1.0/src/cyphersmith.egg-info/PKG-INFO +135 -0
- cyphersmith-0.1.0/src/cyphersmith.egg-info/SOURCES.txt +26 -0
- cyphersmith-0.1.0/src/cyphersmith.egg-info/dependency_links.txt +1 -0
- cyphersmith-0.1.0/src/cyphersmith.egg-info/entry_points.txt +2 -0
- cyphersmith-0.1.0/src/cyphersmith.egg-info/requires.txt +9 -0
- cyphersmith-0.1.0/src/cyphersmith.egg-info/top_level.txt +1 -0
- cyphersmith-0.1.0/tests/test_cli.py +87 -0
- cyphersmith-0.1.0/tests/test_context.py +36 -0
- cyphersmith-0.1.0/tests/test_generator.py +177 -0
- cyphersmith-0.1.0/tests/test_progress.py +28 -0
- cyphersmith-0.1.0/tests/test_prompt_llm_safety.py +60 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aswin K V
|
|
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.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyphersmith
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn natural language into read-only Cypher, validate it with CyVer, and execute it against Neo4j — powered by LiteLLM.
|
|
5
|
+
Author: Aswin K V
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Aswin-K-V/Neo4j-Cypher-generator
|
|
8
|
+
Project-URL: Repository, https://github.com/Aswin-K-V/Neo4j-Cypher-generator
|
|
9
|
+
Project-URL: Issues, https://github.com/Aswin-K-V/Neo4j-Cypher-generator/issues
|
|
10
|
+
Keywords: neo4j,cypher,llm,natural-language,graph-database,litellm,text-to-cypher
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: neo4j>=5.20
|
|
25
|
+
Requires-Dist: litellm>=1.0
|
|
26
|
+
Requires-Dist: CyVer>=2.0.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Requires-Dist: PyYAML>=6.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# cyphersmith
|
|
35
|
+
|
|
36
|
+
`cyphersmith` turns natural language into read-only Cypher, validates it with CyVer, executes it against Neo4j, and returns the query plus records.
|
|
37
|
+
|
|
38
|
+
It uses LiteLLM for provider-neutral model access, so the same package works with OpenAI, Azure OpenAI, Anthropic, Gemini, and all other LiteLLM-supported providers.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install cyphersmith
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Python API
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from cyphersmith import CypherGenerator, LLMConfig, Neo4jCredentials
|
|
50
|
+
|
|
51
|
+
generator = CypherGenerator(
|
|
52
|
+
neo4j=Neo4jCredentials(
|
|
53
|
+
uri="bolt://localhost:7687",
|
|
54
|
+
username="neo4j",
|
|
55
|
+
password="password",
|
|
56
|
+
database="neo4j",
|
|
57
|
+
),
|
|
58
|
+
llm=LLMConfig(
|
|
59
|
+
model="openai/gpt-4o-mini",
|
|
60
|
+
temperature=0,
|
|
61
|
+
),
|
|
62
|
+
business_context_path="business_context.yaml",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
result = generator.ask("show top 10 items by a chosen metric")
|
|
66
|
+
print(result.cypher)
|
|
67
|
+
print(result.records)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## CLI
|
|
71
|
+
|
|
72
|
+
Interactive setup and question loop:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cyphersmith chat --pretty
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The interactive command prompts for:
|
|
79
|
+
|
|
80
|
+
- LLM provider and model
|
|
81
|
+
- API key and any provider-specific endpoint values
|
|
82
|
+
- Neo4j URI, username, password, and database
|
|
83
|
+
- optional business context file path
|
|
84
|
+
|
|
85
|
+
After setup, keep asking questions at `Question>`. Type `/exit` to stop. Each answer prints the generated Cypher query, records, validation details, attempts, and any error.
|
|
86
|
+
|
|
87
|
+
One-shot command:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cyphersmith ask \
|
|
91
|
+
--neo4j-uri bolt://localhost:7687 \
|
|
92
|
+
--neo4j-user neo4j \
|
|
93
|
+
--neo4j-password password \
|
|
94
|
+
--neo4j-database neo4j \
|
|
95
|
+
--model openai/gpt-4o-mini \
|
|
96
|
+
--business-context business_context.yaml \
|
|
97
|
+
"show top 10 items by a chosen metric"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The CLI prints live intermediate progress steps to `stderr` (context load, schema fetch, generation attempts, safety checks, validation, and execution) with terminal colors for readability. Final structured output stays on `stdout`.
|
|
101
|
+
|
|
102
|
+
Use these flags if needed:
|
|
103
|
+
|
|
104
|
+
- `--no-progress`: disable intermediate step logs
|
|
105
|
+
- `--no-color`: keep progress logs but disable ANSI colors
|
|
106
|
+
|
|
107
|
+
The final CLI payload includes `cypher`, `records`, `validation`, `attempts`, and `error`.
|
|
108
|
+
|
|
109
|
+
## Environment Variables
|
|
110
|
+
|
|
111
|
+
Neo4j values can be passed explicitly or read from:
|
|
112
|
+
|
|
113
|
+
- `NEO4J_URI`
|
|
114
|
+
- `NEO4J_USER`
|
|
115
|
+
- `NEO4J_PASSWORD`
|
|
116
|
+
- `NEO4J_DATABASE`
|
|
117
|
+
|
|
118
|
+
LiteLLM credentials should use the environment variables expected by the provider, such as `OPENAI_API_KEY`, `AZURE_API_KEY`, `AZURE_API_BASE`, or provider-specific equivalents.
|
|
119
|
+
|
|
120
|
+
## Business Context
|
|
121
|
+
|
|
122
|
+
Pass a `.txt`, `.md`, `.yaml`, `.yml`, or `.json` file through `business_context_path` or `--business-context`. The content is added to the Cypher generation prompt as business context only; it is not treated as a schema source.
|
|
123
|
+
|
|
124
|
+
## Safety
|
|
125
|
+
|
|
126
|
+
Generated Cypher is blocked unless it passes both:
|
|
127
|
+
|
|
128
|
+
- a local read-only keyword/procedure check
|
|
129
|
+
- CyVer syntax, schema, and property validation
|
|
130
|
+
|
|
131
|
+
The package will not execute generated queries containing obvious write operations such as `CREATE`, `MERGE`, `DELETE`, `SET`, `REMOVE`, or procedures like `CALL db` and `CALL apoc`.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# cyphersmith
|
|
2
|
+
|
|
3
|
+
`cyphersmith` turns natural language into read-only Cypher, validates it with CyVer, executes it against Neo4j, and returns the query plus records.
|
|
4
|
+
|
|
5
|
+
It uses LiteLLM for provider-neutral model access, so the same package works with OpenAI, Azure OpenAI, Anthropic, Gemini, and all other LiteLLM-supported providers.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install cyphersmith
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Python API
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from cyphersmith import CypherGenerator, LLMConfig, Neo4jCredentials
|
|
17
|
+
|
|
18
|
+
generator = CypherGenerator(
|
|
19
|
+
neo4j=Neo4jCredentials(
|
|
20
|
+
uri="bolt://localhost:7687",
|
|
21
|
+
username="neo4j",
|
|
22
|
+
password="password",
|
|
23
|
+
database="neo4j",
|
|
24
|
+
),
|
|
25
|
+
llm=LLMConfig(
|
|
26
|
+
model="openai/gpt-4o-mini",
|
|
27
|
+
temperature=0,
|
|
28
|
+
),
|
|
29
|
+
business_context_path="business_context.yaml",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
result = generator.ask("show top 10 items by a chosen metric")
|
|
33
|
+
print(result.cypher)
|
|
34
|
+
print(result.records)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## CLI
|
|
38
|
+
|
|
39
|
+
Interactive setup and question loop:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cyphersmith chat --pretty
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The interactive command prompts for:
|
|
46
|
+
|
|
47
|
+
- LLM provider and model
|
|
48
|
+
- API key and any provider-specific endpoint values
|
|
49
|
+
- Neo4j URI, username, password, and database
|
|
50
|
+
- optional business context file path
|
|
51
|
+
|
|
52
|
+
After setup, keep asking questions at `Question>`. Type `/exit` to stop. Each answer prints the generated Cypher query, records, validation details, attempts, and any error.
|
|
53
|
+
|
|
54
|
+
One-shot command:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cyphersmith ask \
|
|
58
|
+
--neo4j-uri bolt://localhost:7687 \
|
|
59
|
+
--neo4j-user neo4j \
|
|
60
|
+
--neo4j-password password \
|
|
61
|
+
--neo4j-database neo4j \
|
|
62
|
+
--model openai/gpt-4o-mini \
|
|
63
|
+
--business-context business_context.yaml \
|
|
64
|
+
"show top 10 items by a chosen metric"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The CLI prints live intermediate progress steps to `stderr` (context load, schema fetch, generation attempts, safety checks, validation, and execution) with terminal colors for readability. Final structured output stays on `stdout`.
|
|
68
|
+
|
|
69
|
+
Use these flags if needed:
|
|
70
|
+
|
|
71
|
+
- `--no-progress`: disable intermediate step logs
|
|
72
|
+
- `--no-color`: keep progress logs but disable ANSI colors
|
|
73
|
+
|
|
74
|
+
The final CLI payload includes `cypher`, `records`, `validation`, `attempts`, and `error`.
|
|
75
|
+
|
|
76
|
+
## Environment Variables
|
|
77
|
+
|
|
78
|
+
Neo4j values can be passed explicitly or read from:
|
|
79
|
+
|
|
80
|
+
- `NEO4J_URI`
|
|
81
|
+
- `NEO4J_USER`
|
|
82
|
+
- `NEO4J_PASSWORD`
|
|
83
|
+
- `NEO4J_DATABASE`
|
|
84
|
+
|
|
85
|
+
LiteLLM credentials should use the environment variables expected by the provider, such as `OPENAI_API_KEY`, `AZURE_API_KEY`, `AZURE_API_BASE`, or provider-specific equivalents.
|
|
86
|
+
|
|
87
|
+
## Business Context
|
|
88
|
+
|
|
89
|
+
Pass a `.txt`, `.md`, `.yaml`, `.yml`, or `.json` file through `business_context_path` or `--business-context`. The content is added to the Cypher generation prompt as business context only; it is not treated as a schema source.
|
|
90
|
+
|
|
91
|
+
## Safety
|
|
92
|
+
|
|
93
|
+
Generated Cypher is blocked unless it passes both:
|
|
94
|
+
|
|
95
|
+
- a local read-only keyword/procedure check
|
|
96
|
+
- CyVer syntax, schema, and property validation
|
|
97
|
+
|
|
98
|
+
The package will not execute generated queries containing obvious write operations such as `CREATE`, `MERGE`, `DELETE`, `SET`, `REMOVE`, or procedures like `CALL db` and `CALL apoc`.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cyphersmith"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Turn natural language into read-only Cypher, validate it with CyVer, and execute it against Neo4j — powered by LiteLLM."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Aswin K V" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["neo4j", "cypher", "llm", "natural-language", "graph-database", "litellm", "text-to-cypher"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Database",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"neo4j>=5.20",
|
|
30
|
+
"litellm>=1.0",
|
|
31
|
+
"CyVer>=2.0.0",
|
|
32
|
+
"pydantic>=2.0",
|
|
33
|
+
"PyYAML>=6.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Aswin-K-V/Neo4j-Cypher-generator"
|
|
38
|
+
Repository = "https://github.com/Aswin-K-V/Neo4j-Cypher-generator"
|
|
39
|
+
Issues = "https://github.com/Aswin-K-V/Neo4j-Cypher-generator/issues"
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"build>=1.2",
|
|
44
|
+
"pytest>=8.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
cyphersmith = "cyphersmith.cli:main"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Public API for the cyphersmith package."""
|
|
2
|
+
|
|
3
|
+
from .generator import CypherGenerator
|
|
4
|
+
from .models import CypherGenerationResult, LLMConfig, Neo4jCredentials
|
|
5
|
+
from .neo4j_client import Neo4jClient, get_neo4j_client
|
|
6
|
+
from .progress import TerminalProgressReporter
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"CypherGenerationResult",
|
|
10
|
+
"CypherGenerator",
|
|
11
|
+
"LLMConfig",
|
|
12
|
+
"Neo4jClient",
|
|
13
|
+
"Neo4jCredentials",
|
|
14
|
+
"TerminalProgressReporter",
|
|
15
|
+
"get_neo4j_client",
|
|
16
|
+
]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
|
|
9
|
+
from .generator import CypherGenerator
|
|
10
|
+
from .models import LLMConfig, Neo4jCredentials
|
|
11
|
+
from .progress import TerminalProgressReporter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
15
|
+
parser = _build_parser()
|
|
16
|
+
args = parser.parse_args(argv)
|
|
17
|
+
|
|
18
|
+
if args.command == "ask":
|
|
19
|
+
return _run_ask(args)
|
|
20
|
+
if args.command in {"chat", "setup", "interactive"}:
|
|
21
|
+
return _run_chat(args)
|
|
22
|
+
|
|
23
|
+
parser.print_help()
|
|
24
|
+
return 2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
prog="cypher-generator",
|
|
30
|
+
description="Generate, validate, and execute read-only Cypher against Neo4j.",
|
|
31
|
+
)
|
|
32
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
33
|
+
|
|
34
|
+
ask = subparsers.add_parser("ask", help="Ask a natural-language graph question.")
|
|
35
|
+
ask.add_argument("query", help="Natural-language graph question.")
|
|
36
|
+
ask.add_argument("--neo4j-uri", dest="neo4j_uri")
|
|
37
|
+
ask.add_argument("--neo4j-user", dest="neo4j_user")
|
|
38
|
+
ask.add_argument("--neo4j-password", dest="neo4j_password")
|
|
39
|
+
ask.add_argument("--neo4j-database", dest="neo4j_database", default=None)
|
|
40
|
+
ask.add_argument("--model", required=True, help="LiteLLM model name.")
|
|
41
|
+
ask.add_argument("--temperature", type=float, default=0)
|
|
42
|
+
ask.add_argument("--timeout", type=float, default=None)
|
|
43
|
+
ask.add_argument("--api-key", dest="api_key", default=None)
|
|
44
|
+
ask.add_argument("--api-base", dest="api_base", default=None)
|
|
45
|
+
ask.add_argument("--api-version", dest="api_version", default=None)
|
|
46
|
+
ask.add_argument("--business-context", dest="business_context", default=None)
|
|
47
|
+
ask.add_argument("--max-validation-retries", type=int, default=2)
|
|
48
|
+
ask.add_argument(
|
|
49
|
+
"--no-progress",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Disable intermediate progress output.",
|
|
52
|
+
)
|
|
53
|
+
ask.add_argument(
|
|
54
|
+
"--no-color",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Disable ANSI colors in progress output.",
|
|
57
|
+
)
|
|
58
|
+
ask.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.")
|
|
59
|
+
ask.set_defaults(command="ask")
|
|
60
|
+
|
|
61
|
+
chat = subparsers.add_parser(
|
|
62
|
+
"chat",
|
|
63
|
+
aliases=["setup", "interactive"],
|
|
64
|
+
help="Prompt for setup once, then ask questions in a loop.",
|
|
65
|
+
)
|
|
66
|
+
chat.add_argument("--pretty", action="store_true", help="Pretty-print result JSON.")
|
|
67
|
+
chat.add_argument("--max-validation-retries", type=int, default=2)
|
|
68
|
+
chat.add_argument(
|
|
69
|
+
"--no-progress",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Disable intermediate progress output.",
|
|
72
|
+
)
|
|
73
|
+
chat.add_argument(
|
|
74
|
+
"--no-color",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Disable ANSI colors in progress output.",
|
|
77
|
+
)
|
|
78
|
+
chat.set_defaults(command="chat")
|
|
79
|
+
return parser
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _run_ask(args: argparse.Namespace) -> int:
|
|
83
|
+
progress_reporter = _build_progress_reporter(args)
|
|
84
|
+
generator = CypherGenerator(
|
|
85
|
+
neo4j=Neo4jCredentials(
|
|
86
|
+
uri=args.neo4j_uri,
|
|
87
|
+
username=args.neo4j_user,
|
|
88
|
+
password=args.neo4j_password,
|
|
89
|
+
database=args.neo4j_database,
|
|
90
|
+
),
|
|
91
|
+
llm=LLMConfig(
|
|
92
|
+
model=args.model,
|
|
93
|
+
temperature=args.temperature,
|
|
94
|
+
timeout=args.timeout,
|
|
95
|
+
api_key=args.api_key,
|
|
96
|
+
api_base=args.api_base,
|
|
97
|
+
api_version=args.api_version,
|
|
98
|
+
),
|
|
99
|
+
business_context_path=args.business_context,
|
|
100
|
+
max_validation_retries=args.max_validation_retries,
|
|
101
|
+
progress_reporter=progress_reporter,
|
|
102
|
+
)
|
|
103
|
+
result = generator.ask(args.query)
|
|
104
|
+
payload = result.model_dump(mode="json")
|
|
105
|
+
print(
|
|
106
|
+
json.dumps(
|
|
107
|
+
payload,
|
|
108
|
+
indent=2 if args.pretty else None,
|
|
109
|
+
ensure_ascii=False,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return 1 if result.error else 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _run_chat(args: argparse.Namespace) -> int:
|
|
116
|
+
print("Cypher Generator interactive setup")
|
|
117
|
+
print("Press Enter to accept defaults. Type /exit in the question loop to stop.")
|
|
118
|
+
|
|
119
|
+
llm_config = _prompt_llm_config()
|
|
120
|
+
neo4j_credentials = _prompt_neo4j_credentials()
|
|
121
|
+
business_context = _prompt_optional(
|
|
122
|
+
"Business context file path (.txt/.md/.yaml/.json), blank for none",
|
|
123
|
+
default="",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
generator = CypherGenerator(
|
|
127
|
+
neo4j=neo4j_credentials,
|
|
128
|
+
llm=llm_config,
|
|
129
|
+
business_context_path=business_context or None,
|
|
130
|
+
max_validation_retries=args.max_validation_retries,
|
|
131
|
+
progress_reporter=_build_progress_reporter(args),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
print("\nSetup complete. Ask a question, or type /exit to quit.")
|
|
135
|
+
while True:
|
|
136
|
+
try:
|
|
137
|
+
question = input("\nQuestion> ").strip()
|
|
138
|
+
except (EOFError, KeyboardInterrupt):
|
|
139
|
+
print()
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
if question.lower() in {"/exit", "/quit", "exit", "quit", "q"}:
|
|
143
|
+
return 0
|
|
144
|
+
if not question:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
result = generator.ask(question)
|
|
148
|
+
_print_interactive_result(result, pretty=args.pretty)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _prompt_llm_config() -> LLMConfig:
|
|
152
|
+
providers = [
|
|
153
|
+
("OpenAI", "openai"),
|
|
154
|
+
("Azure OpenAI", "azure"),
|
|
155
|
+
("Anthropic", "anthropic"),
|
|
156
|
+
("Google Gemini", "gemini"),
|
|
157
|
+
("Groq", "groq"),
|
|
158
|
+
("Custom LiteLLM model string", "custom"),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
print("\nLLM provider")
|
|
162
|
+
for index, (label, _) in enumerate(providers, start=1):
|
|
163
|
+
print(f" {index}. {label}")
|
|
164
|
+
|
|
165
|
+
provider_index = _prompt_choice("Select provider", default=1, maximum=len(providers))
|
|
166
|
+
provider = providers[provider_index - 1][1]
|
|
167
|
+
|
|
168
|
+
if provider == "openai":
|
|
169
|
+
model = _prompt_optional("Model", default="openai/gpt-5")
|
|
170
|
+
api_key = _prompt_secret("OpenAI API key", required=True)
|
|
171
|
+
return LLMConfig(model=model, api_key=api_key, temperature=0)
|
|
172
|
+
|
|
173
|
+
if provider == "azure":
|
|
174
|
+
deployment = _prompt_required(
|
|
175
|
+
"Azure deployment name, without azure/ prefix"
|
|
176
|
+
)
|
|
177
|
+
api_key = _prompt_secret("Azure OpenAI API key", required=True)
|
|
178
|
+
api_base = _prompt_required(
|
|
179
|
+
"Azure API base, e.g. https://your-resource.openai.azure.com"
|
|
180
|
+
)
|
|
181
|
+
api_version = _prompt_optional("Azure API version", default="2024-10-21")
|
|
182
|
+
return LLMConfig(
|
|
183
|
+
model=f"azure/{deployment}",
|
|
184
|
+
api_key=api_key,
|
|
185
|
+
api_base=api_base,
|
|
186
|
+
api_version=api_version,
|
|
187
|
+
temperature=0,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if provider == "anthropic":
|
|
191
|
+
model = _prompt_optional(
|
|
192
|
+
"Model",
|
|
193
|
+
default="anthropic/claude-3-5-sonnet-latest",
|
|
194
|
+
)
|
|
195
|
+
api_key = _prompt_secret("Anthropic API key", required=True)
|
|
196
|
+
return LLMConfig(model=model, api_key=api_key, temperature=0)
|
|
197
|
+
|
|
198
|
+
if provider == "gemini":
|
|
199
|
+
model = _prompt_optional("Model", default="gemini/gemini-2.5-pro")
|
|
200
|
+
api_key = _prompt_secret("Gemini API key", required=True)
|
|
201
|
+
return LLMConfig(model=model, api_key=api_key, temperature=0)
|
|
202
|
+
|
|
203
|
+
if provider == "groq":
|
|
204
|
+
model = _prompt_optional("Model", default="groq/llama-3.3-70b-versatile")
|
|
205
|
+
api_key = _prompt_secret("Groq API key", required=True)
|
|
206
|
+
return LLMConfig(model=model, api_key=api_key, temperature=0)
|
|
207
|
+
|
|
208
|
+
model = _prompt_required(
|
|
209
|
+
"LiteLLM model string, e.g. openai/gpt-5 or azure/my-deployment"
|
|
210
|
+
)
|
|
211
|
+
api_key = _prompt_secret("API key, blank to use provider environment variables")
|
|
212
|
+
api_base = _prompt_optional("API base, blank unless provider needs it", default="")
|
|
213
|
+
api_version = _prompt_optional(
|
|
214
|
+
"API version, blank unless provider needs it",
|
|
215
|
+
default="",
|
|
216
|
+
)
|
|
217
|
+
return LLMConfig(
|
|
218
|
+
model=model,
|
|
219
|
+
api_key=api_key or None,
|
|
220
|
+
api_base=api_base or None,
|
|
221
|
+
api_version=api_version or None,
|
|
222
|
+
temperature=0,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _prompt_neo4j_credentials() -> Neo4jCredentials:
|
|
227
|
+
print("\nNeo4j connection")
|
|
228
|
+
uri = _prompt_optional("Neo4j URI", default="bolt://localhost:7687")
|
|
229
|
+
username = _prompt_optional("Neo4j username", default="neo4j")
|
|
230
|
+
password = _prompt_secret("Neo4j password", required=True)
|
|
231
|
+
database = _prompt_optional("Neo4j database", default="neo4j")
|
|
232
|
+
return Neo4jCredentials(
|
|
233
|
+
uri=uri,
|
|
234
|
+
username=username,
|
|
235
|
+
password=password,
|
|
236
|
+
database=database,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _prompt_choice(prompt: str, *, default: int, maximum: int) -> int:
|
|
241
|
+
while True:
|
|
242
|
+
raw = input(f"{prompt} [{default}]: ").strip()
|
|
243
|
+
if not raw:
|
|
244
|
+
return default
|
|
245
|
+
try:
|
|
246
|
+
value = int(raw)
|
|
247
|
+
except ValueError:
|
|
248
|
+
print(f"Enter a number from 1 to {maximum}.", file=sys.stderr)
|
|
249
|
+
continue
|
|
250
|
+
if 1 <= value <= maximum:
|
|
251
|
+
return value
|
|
252
|
+
print(f"Enter a number from 1 to {maximum}.", file=sys.stderr)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _prompt_optional(prompt: str, *, default: str = "") -> str:
|
|
256
|
+
suffix = f" [{default}]" if default else ""
|
|
257
|
+
value = input(f"{prompt}{suffix}: ").strip()
|
|
258
|
+
return value or default
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _prompt_required(prompt: str) -> str:
|
|
262
|
+
while True:
|
|
263
|
+
value = input(f"{prompt}: ").strip()
|
|
264
|
+
if value:
|
|
265
|
+
return value
|
|
266
|
+
print("This value is required.", file=sys.stderr)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _prompt_secret(prompt: str, *, required: bool = False) -> str:
|
|
270
|
+
while True:
|
|
271
|
+
value = getpass.getpass(f"{prompt}: ").strip()
|
|
272
|
+
if value or not required:
|
|
273
|
+
return value
|
|
274
|
+
print("This value is required.", file=sys.stderr)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _print_interactive_result(result: object, *, pretty: bool) -> None:
|
|
278
|
+
cypher = getattr(result, "cypher", "") or ""
|
|
279
|
+
records = getattr(result, "records", []) or []
|
|
280
|
+
validation = getattr(result, "validation", {}) or {}
|
|
281
|
+
error = getattr(result, "error", None)
|
|
282
|
+
attempts = getattr(result, "attempts", 0)
|
|
283
|
+
|
|
284
|
+
print("\nCypher:")
|
|
285
|
+
print(cypher or "(none)")
|
|
286
|
+
|
|
287
|
+
print("\nResults:")
|
|
288
|
+
print(json.dumps(records, indent=2 if pretty else None, ensure_ascii=False))
|
|
289
|
+
|
|
290
|
+
print("\nValidation:")
|
|
291
|
+
print(json.dumps(validation, indent=2 if pretty else None, ensure_ascii=False))
|
|
292
|
+
|
|
293
|
+
print(f"\nAttempts: {attempts}")
|
|
294
|
+
if error:
|
|
295
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _build_progress_reporter(
|
|
299
|
+
args: argparse.Namespace,
|
|
300
|
+
) -> TerminalProgressReporter | None:
|
|
301
|
+
if getattr(args, "no_progress", False):
|
|
302
|
+
return None
|
|
303
|
+
return TerminalProgressReporter(use_color=not getattr(args, "no_color", False))
|