qbridge-agent 0.2.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.
- qbridge_agent-0.2.0/LICENSE +43 -0
- qbridge_agent-0.2.0/MANIFEST.in +6 -0
- qbridge_agent-0.2.0/PKG-INFO +92 -0
- qbridge_agent-0.2.0/README.md +77 -0
- qbridge_agent-0.2.0/agent/__init__.py +6 -0
- qbridge_agent-0.2.0/agent/cli.py +29 -0
- qbridge_agent-0.2.0/agent/compiler_interface/__init__.py +9 -0
- qbridge_agent-0.2.0/agent/compiler_interface/dsl_extract.py +87 -0
- qbridge_agent-0.2.0/agent/compiler_interface/dsl_lint.py +76 -0
- qbridge_agent-0.2.0/agent/compiler_interface/dsl_prompts.py +99 -0
- qbridge_agent-0.2.0/agent/compiler_interface/local_cudaq.py +61 -0
- qbridge_agent-0.2.0/agent/compiler_interface/pipeline.py +101 -0
- qbridge_agent-0.2.0/agent/cudaq_entry.py +86 -0
- qbridge_agent-0.2.0/agent/models.py +164 -0
- qbridge_agent-0.2.0/agent/planner/__init__.py +5 -0
- qbridge_agent-0.2.0/agent/tools/__init__.py +1 -0
- qbridge_agent-0.2.0/agent/tools/cudaq_verification.py +33 -0
- qbridge_agent-0.2.0/pyproject.toml +39 -0
- qbridge_agent-0.2.0/qbridge_agent.egg-info/PKG-INFO +92 -0
- qbridge_agent-0.2.0/qbridge_agent.egg-info/SOURCES.txt +24 -0
- qbridge_agent-0.2.0/qbridge_agent.egg-info/dependency_links.txt +1 -0
- qbridge_agent-0.2.0/qbridge_agent.egg-info/entry_points.txt +2 -0
- qbridge_agent-0.2.0/qbridge_agent.egg-info/requires.txt +6 -0
- qbridge_agent-0.2.0/qbridge_agent.egg-info/top_level.txt +1 -0
- qbridge_agent-0.2.0/setup.cfg +4 -0
- qbridge_agent-0.2.0/tests/test_agent_contract.py +38 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
5
|
+
|
|
6
|
+
Parameters
|
|
7
|
+
|
|
8
|
+
Licensor: QBridge contributors
|
|
9
|
+
Licensed Work: QBridge Agent, including all source files in this repository.
|
|
10
|
+
Additional Use Grant: You may use, copy, modify, create derivative works of,
|
|
11
|
+
and redistribute the Licensed Work for any purpose, provided that any
|
|
12
|
+
redistribution before the Change Date is under this License and retains this
|
|
13
|
+
notice.
|
|
14
|
+
Change Date: 2030-07-20
|
|
15
|
+
Change License: Apache License, Version 2.0
|
|
16
|
+
|
|
17
|
+
Terms
|
|
18
|
+
|
|
19
|
+
The Licensed Work is made available under the terms of this Business Source
|
|
20
|
+
License. On the Change Date, or the fourth anniversary of the first public
|
|
21
|
+
release of each later version of the Licensed Work, whichever is later, the
|
|
22
|
+
Licensed Work will be made available under the Change License.
|
|
23
|
+
|
|
24
|
+
The Licensor grants you the right to copy, modify, create derivative works of,
|
|
25
|
+
redistribute, and make non-production use of the Licensed Work. The Additional
|
|
26
|
+
Use Grant above specifies any additional permitted production use.
|
|
27
|
+
|
|
28
|
+
The Licensed Work is provided on an "AS IS" basis. The Licensor disclaims all
|
|
29
|
+
warranties, express or implied, including merchantability, fitness for a
|
|
30
|
+
particular purpose, and non-infringement. In no event will the Licensor be
|
|
31
|
+
liable for any damages arising from use of the Licensed Work.
|
|
32
|
+
|
|
33
|
+
You must conspicuously display this License on each copy of the Licensed Work
|
|
34
|
+
that you redistribute. Any use of the Licensed Work in violation of this
|
|
35
|
+
License automatically terminates your rights under this License.
|
|
36
|
+
|
|
37
|
+
This License does not grant any right to use the Licensor's trademarks, except
|
|
38
|
+
as required for reasonable and customary use in describing the origin of the
|
|
39
|
+
Licensed Work.
|
|
40
|
+
|
|
41
|
+
On the Change Date, the Licensed Work is licensed under the Apache License,
|
|
42
|
+
Version 2.0. A copy of that license is available at
|
|
43
|
+
https://www.apache.org/licenses/LICENSE-2.0.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qbridge-agent
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Local, compiler-backed CUDA-Q optimization agent
|
|
5
|
+
License-Expression: BUSL-1.1
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: qbridge[quantum]>=0.1.0
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# QBridge-Agent
|
|
17
|
+
|
|
18
|
+
## Install and run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install "qbridge[quantum]" qbridge-agent
|
|
22
|
+
qbridge-agent my_problem.py --workspace ./qbridge-output
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`my_problem.py` must be a supported QBridge Python DSL optimization model, not
|
|
26
|
+
arbitrary Python. Add `--no-solve` to emit CUDA-Q without running it.
|
|
27
|
+
|
|
28
|
+
QBridge-Agent translates a constrained binary optimization request into the
|
|
29
|
+
QBridge Python DSL, then delegates verified compilation and execution to the
|
|
30
|
+
locally installed QBridge CUDA-Q compiler. It is deliberately not a general
|
|
31
|
+
coding agent and has no cloud compile/solve dependency.
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
QBridge-Agent/
|
|
37
|
+
├── agent/
|
|
38
|
+
│ ├── planner/ # intent-to-DSL prompt policy
|
|
39
|
+
│ ├── tools/ # independent benchmark verification
|
|
40
|
+
│ ├── compiler_interface/ # DSL extraction, local compiler and CUDA-Q execution
|
|
41
|
+
├── examples/
|
|
42
|
+
├── evaluations/
|
|
43
|
+
│ ├── benchmark_tasks.json # frozen T1/T2 task set
|
|
44
|
+
│ ├── results/ # raw reproducible run artifacts
|
|
45
|
+
│ └── README.md
|
|
46
|
+
└── README.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Evaluation results
|
|
50
|
+
|
|
51
|
+
Run this in the Ubuntu environment that has CUDA-Q installed:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
OPENROUTER_API_KEY=... \
|
|
55
|
+
/home/dilip/qbridge-env/bin/python3 evaluations/run_benchmark.py \
|
|
56
|
+
--trials 3 --results evaluations/results/my_run
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The runner forces `cudaq.set_target("nvidia")`, performs a CUDA-Q sample GPU
|
|
60
|
+
probe before work begins, stores the actual target description for each V2
|
|
61
|
+
result, and checks both systems with the same classical feasibility/objective
|
|
62
|
+
verifier. It never converts an execution error into a successful result.
|
|
63
|
+
|
|
64
|
+
`OPENROUTER_API_KEY` is required only for this external-model comparison; the
|
|
65
|
+
normal agent compile/solve path is local. The reproducible 20 July 2026 run
|
|
66
|
+
on the six frozen T1/T2 tasks (three trials each, NVIDIA `cusvsim_fp32`) found:
|
|
67
|
+
|
|
68
|
+
- QBridge-Agent: 16 `VERIFIED`, 1 `INCORRECT`, 1 `UNVERIFIED`.
|
|
69
|
+
- Aider: 0 `VERIFIED`, 17 `UNVERIFIED`, 1 `INVALID_METHOD`.
|
|
70
|
+
|
|
71
|
+
These are strict verifier outcomes, not success-rate estimates. Raw artifacts
|
|
72
|
+
for that run are retained under `evaluations/results/20260720_t1_t2_gpu_fixed`
|
|
73
|
+
in a source checkout and excluded from package distributions.
|
|
74
|
+
|
|
75
|
+
From a source checkout:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install .
|
|
79
|
+
qbridge-agent my_problem.py --workspace ./qbridge-output
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`qbridge-agent` invokes the local `qbridge` package to emit CUDA-Q, then uses
|
|
83
|
+
the configured local CUDA-Q target. No problem DSL is sent to a QBridge cloud
|
|
84
|
+
endpoint.
|
|
85
|
+
|
|
86
|
+
For maintainer release steps, see [RELEASE.md](RELEASE.md).
|
|
87
|
+
|
|
88
|
+
## Scope
|
|
89
|
+
|
|
90
|
+
Supported: small constrained binary optimization problems expressible in the
|
|
91
|
+
current QBridge DSL. Unsupported quantum algorithms and arbitrary Python are
|
|
92
|
+
errors, not guessed translations.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# QBridge-Agent
|
|
2
|
+
|
|
3
|
+
## Install and run
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install "qbridge[quantum]" qbridge-agent
|
|
7
|
+
qbridge-agent my_problem.py --workspace ./qbridge-output
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`my_problem.py` must be a supported QBridge Python DSL optimization model, not
|
|
11
|
+
arbitrary Python. Add `--no-solve` to emit CUDA-Q without running it.
|
|
12
|
+
|
|
13
|
+
QBridge-Agent translates a constrained binary optimization request into the
|
|
14
|
+
QBridge Python DSL, then delegates verified compilation and execution to the
|
|
15
|
+
locally installed QBridge CUDA-Q compiler. It is deliberately not a general
|
|
16
|
+
coding agent and has no cloud compile/solve dependency.
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
QBridge-Agent/
|
|
22
|
+
├── agent/
|
|
23
|
+
│ ├── planner/ # intent-to-DSL prompt policy
|
|
24
|
+
│ ├── tools/ # independent benchmark verification
|
|
25
|
+
│ ├── compiler_interface/ # DSL extraction, local compiler and CUDA-Q execution
|
|
26
|
+
├── examples/
|
|
27
|
+
├── evaluations/
|
|
28
|
+
│ ├── benchmark_tasks.json # frozen T1/T2 task set
|
|
29
|
+
│ ├── results/ # raw reproducible run artifacts
|
|
30
|
+
│ └── README.md
|
|
31
|
+
└── README.md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Evaluation results
|
|
35
|
+
|
|
36
|
+
Run this in the Ubuntu environment that has CUDA-Q installed:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
OPENROUTER_API_KEY=... \
|
|
40
|
+
/home/dilip/qbridge-env/bin/python3 evaluations/run_benchmark.py \
|
|
41
|
+
--trials 3 --results evaluations/results/my_run
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The runner forces `cudaq.set_target("nvidia")`, performs a CUDA-Q sample GPU
|
|
45
|
+
probe before work begins, stores the actual target description for each V2
|
|
46
|
+
result, and checks both systems with the same classical feasibility/objective
|
|
47
|
+
verifier. It never converts an execution error into a successful result.
|
|
48
|
+
|
|
49
|
+
`OPENROUTER_API_KEY` is required only for this external-model comparison; the
|
|
50
|
+
normal agent compile/solve path is local. The reproducible 20 July 2026 run
|
|
51
|
+
on the six frozen T1/T2 tasks (three trials each, NVIDIA `cusvsim_fp32`) found:
|
|
52
|
+
|
|
53
|
+
- QBridge-Agent: 16 `VERIFIED`, 1 `INCORRECT`, 1 `UNVERIFIED`.
|
|
54
|
+
- Aider: 0 `VERIFIED`, 17 `UNVERIFIED`, 1 `INVALID_METHOD`.
|
|
55
|
+
|
|
56
|
+
These are strict verifier outcomes, not success-rate estimates. Raw artifacts
|
|
57
|
+
for that run are retained under `evaluations/results/20260720_t1_t2_gpu_fixed`
|
|
58
|
+
in a source checkout and excluded from package distributions.
|
|
59
|
+
|
|
60
|
+
From a source checkout:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install .
|
|
64
|
+
qbridge-agent my_problem.py --workspace ./qbridge-output
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`qbridge-agent` invokes the local `qbridge` package to emit CUDA-Q, then uses
|
|
68
|
+
the configured local CUDA-Q target. No problem DSL is sent to a QBridge cloud
|
|
69
|
+
endpoint.
|
|
70
|
+
|
|
71
|
+
For maintainer release steps, see [RELEASE.md](RELEASE.md).
|
|
72
|
+
|
|
73
|
+
## Scope
|
|
74
|
+
|
|
75
|
+
Supported: small constrained binary optimization problems expressible in the
|
|
76
|
+
current QBridge DSL. Unsupported quantum algorithms and arbitrary Python are
|
|
77
|
+
errors, not guessed translations.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Public local CLI for the open-source QBridge Agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agent.compiler_interface.local_cudaq import compile_dsl_locally, solve_dsl_on_nvidia
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(prog="qbridge-agent")
|
|
13
|
+
parser.add_argument("dsl", type=Path, help="QBridge DSL source file")
|
|
14
|
+
parser.add_argument("--workspace", type=Path, default=Path.cwd())
|
|
15
|
+
parser.add_argument("--no-solve", action="store_true")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
source = args.dsl.read_text(encoding="utf-8")
|
|
18
|
+
paths, _ = compile_dsl_locally(source, args.workspace)
|
|
19
|
+
print(f"CUDA-Q written: {paths['cudaq']}")
|
|
20
|
+
if not args.no_solve:
|
|
21
|
+
result = solve_dsl_on_nvidia(source)
|
|
22
|
+
print(f"Backend: {result['backend']}")
|
|
23
|
+
print(f"Assignment: {result['assignment']}")
|
|
24
|
+
print(f"Objective: {result['objective_value']}")
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Lean CUDA-Q agent: NL -> QBridge DSL -> local compile/solve -> artifacts.
|
|
2
|
+
|
|
3
|
+
No Aider, no chat UI, no git — only the deterministic product path from v1's
|
|
4
|
+
``qbridge.quantum`` package (~0.8k LOC here).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from agent.compiler_interface.pipeline import PipelineReport, QuantumPipeline, run_quantum_pipeline
|
|
8
|
+
|
|
9
|
+
__all__ = ["PipelineReport", "QuantumPipeline", "run_quantum_pipeline"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Client-safe DSL helpers (no compiler imports)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_FENCE_RE = re.compile(r"```(?:python)?\s*\n?(.*?)```", re.DOTALL | re.IGNORECASE)
|
|
8
|
+
_THINK_RE = re.compile(
|
|
9
|
+
r"<think>.*?</think>|<thinking>.*?</thinking>|◁think▷.*?◁/think▷",
|
|
10
|
+
re.DOTALL | re.IGNORECASE,
|
|
11
|
+
)
|
|
12
|
+
_IMPORT_QB_RE = re.compile(
|
|
13
|
+
r"(?:^|\n)(import\s+qbridge\s+as\s+qb\b.*?)(?:\n```|\Z)",
|
|
14
|
+
re.DOTALL | re.IGNORECASE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def strip_reasoning_text(raw: str) -> str:
|
|
19
|
+
"""Remove common reasoning / think wrappers from model output."""
|
|
20
|
+
text = raw or ""
|
|
21
|
+
text = _THINK_RE.sub("", text)
|
|
22
|
+
# Drop leading "THINKING" / "ANSWER" section headers some UIs inject
|
|
23
|
+
text = re.sub(r"(?im)^\s*(thinking|reasoning)\s*$", "", text)
|
|
24
|
+
return text.strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def extract_python_source(raw: str) -> str:
|
|
28
|
+
"""Strip markdown fences / reasoning and return executable Python source."""
|
|
29
|
+
text = strip_reasoning_text(raw)
|
|
30
|
+
if not text:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
# Prefer fenced python blocks that look like QBridge DSL
|
|
34
|
+
fences = list(_FENCE_RE.finditer(text))
|
|
35
|
+
if fences:
|
|
36
|
+
best = ""
|
|
37
|
+
for m in fences:
|
|
38
|
+
body = m.group(1).strip()
|
|
39
|
+
if "OptimizationProblem" in body or "import qbridge" in body:
|
|
40
|
+
return body
|
|
41
|
+
if len(body) > len(best):
|
|
42
|
+
best = body
|
|
43
|
+
if best:
|
|
44
|
+
return best
|
|
45
|
+
|
|
46
|
+
if text.startswith("```"):
|
|
47
|
+
lines = text.splitlines()
|
|
48
|
+
if lines and lines[0].startswith("```"):
|
|
49
|
+
lines = lines[1:]
|
|
50
|
+
if lines and lines[-1].strip() == "```":
|
|
51
|
+
lines = lines[:-1]
|
|
52
|
+
return "\n".join(lines).strip()
|
|
53
|
+
|
|
54
|
+
# Unfenced: grab from `import qbridge` onward when present
|
|
55
|
+
m = _IMPORT_QB_RE.search("\n" + text)
|
|
56
|
+
if m:
|
|
57
|
+
return m.group(1).strip()
|
|
58
|
+
|
|
59
|
+
return text
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def looks_like_qbridge_dsl(source: str) -> bool:
|
|
63
|
+
code = (source or "").strip()
|
|
64
|
+
if not code:
|
|
65
|
+
return False
|
|
66
|
+
return (
|
|
67
|
+
"import qbridge" in code
|
|
68
|
+
and "OptimizationProblem" in code
|
|
69
|
+
and ("problem" in code)
|
|
70
|
+
and (".minimize(" in code or ".maximize(" in code or "minimize(" in code or "maximize(" in code)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_and_validate_dsl(raw: str) -> tuple[str, str | None]:
|
|
75
|
+
"""
|
|
76
|
+
Extract DSL and return (source, error).
|
|
77
|
+
error is set when the extract is empty or clearly not QBridge DSL.
|
|
78
|
+
"""
|
|
79
|
+
source = extract_python_source(raw)
|
|
80
|
+
if not source.strip():
|
|
81
|
+
return "", "LLM returned empty content after stripping reasoning/fences."
|
|
82
|
+
if not looks_like_qbridge_dsl(source):
|
|
83
|
+
return source, (
|
|
84
|
+
"Extracted text is not valid QBridge DSL "
|
|
85
|
+
"(need import qbridge, OptimizationProblem, problem, minimize/maximize)."
|
|
86
|
+
)
|
|
87
|
+
return source, None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""DSL lint — static checks before exec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agent.compiler_interface.dsl_extract import extract_python_source
|
|
10
|
+
|
|
11
|
+
_FORBIDDEN_IMPORTS = {"cudaq", "subprocess", "os", "sys", "shutil"}
|
|
12
|
+
_MAX_CONSTRAINTS = 64
|
|
13
|
+
_MAX_ESTIMATED_VARS = 128
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lint_dsl(source: str) -> dict[str, Any]:
|
|
17
|
+
"""Return DSLValidationReport dict."""
|
|
18
|
+
code = extract_python_source(source)
|
|
19
|
+
issues: list[dict[str, str]] = []
|
|
20
|
+
|
|
21
|
+
if "problem" not in code:
|
|
22
|
+
issues.append({"code": "DSL_SYNTAX", "message": "Script must define variable `problem`."})
|
|
23
|
+
if "OptimizationProblem" not in code:
|
|
24
|
+
issues.append({"code": "DSL_SYNTAX", "message": "Must use OptimizationProblem."})
|
|
25
|
+
if re.search(r"\b(minimize|maximize)\s*\(", code) is None and ".minimize(" not in code and ".maximize(" not in code:
|
|
26
|
+
issues.append({"code": "MISSING_OBJECTIVE", "message": "Problem must set an objective."})
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
tree = ast.parse(code)
|
|
30
|
+
except SyntaxError as exc:
|
|
31
|
+
issues.append({"code": "DSL_SYNTAX", "message": f"Syntax error: {exc}"})
|
|
32
|
+
return _report(issues)
|
|
33
|
+
|
|
34
|
+
for node in ast.walk(tree):
|
|
35
|
+
if isinstance(node, ast.Import):
|
|
36
|
+
for alias in node.names:
|
|
37
|
+
root = alias.name.split(".")[0]
|
|
38
|
+
if root in _FORBIDDEN_IMPORTS:
|
|
39
|
+
issues.append({"code": "DSL_POLICY", "message": f"Forbidden import: {alias.name}"})
|
|
40
|
+
if isinstance(node, ast.ImportFrom) and node.module:
|
|
41
|
+
root = node.module.split(".")[0]
|
|
42
|
+
if root in _FORBIDDEN_IMPORTS:
|
|
43
|
+
issues.append({"code": "DSL_POLICY", "message": f"Forbidden import from: {node.module}"})
|
|
44
|
+
|
|
45
|
+
n_constraints = len(re.findall(r"add_constraint\s*\(", code))
|
|
46
|
+
if n_constraints > _MAX_CONSTRAINTS:
|
|
47
|
+
issues.append({"code": "DSL_POLICY", "message": f"Too many constraints ({n_constraints})."})
|
|
48
|
+
|
|
49
|
+
n_vars = len(re.findall(r"\b(Binary|Integer|Continuous)\s*\(", code))
|
|
50
|
+
if n_vars > _MAX_ESTIMATED_VARS:
|
|
51
|
+
issues.append({"code": "DSL_POLICY", "message": f"Too many variables ({n_vars})."})
|
|
52
|
+
|
|
53
|
+
if re.search(r"generate_cudaq|import\s+cudaq", code):
|
|
54
|
+
issues.append({"code": "DSL_POLICY", "message": "DSL must not generate CUDA-Q directly."})
|
|
55
|
+
|
|
56
|
+
# Modulo on symbolic ops is not supported by the compiler frontend
|
|
57
|
+
if re.search(r"%\s*\d|\d\s*%|%\s*[a-zA-Z_]", code):
|
|
58
|
+
issues.append(
|
|
59
|
+
{
|
|
60
|
+
"code": "DSL_POLICY",
|
|
61
|
+
"message": (
|
|
62
|
+
"Do not use % modulo on variables. Prefer Binary + cardinality/cover "
|
|
63
|
+
"constraints. Integer aux (s == 2*k) is a last resort — it adds qubits."
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return _report(issues)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _report(issues: list[dict[str, str]]) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"ok": len(issues) == 0,
|
|
74
|
+
"issues": issues,
|
|
75
|
+
"summary": "ok" if not issues else "; ".join(i["message"] for i in issues[:3]),
|
|
76
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""System prompt and few-shot examples for the AI DSL translator."""
|
|
2
|
+
|
|
3
|
+
SYSTEM_PROMPT = """You are a deterministic code translator. Your sole job is to convert optimization problem descriptions into Qbridge Python DSL scripts.
|
|
4
|
+
|
|
5
|
+
RULES (strict):
|
|
6
|
+
1. Output ONLY valid Python code inside one ```python fence. No explanations, no chain-of-thought, no commentary.
|
|
7
|
+
2. Always start with: import qbridge as qb
|
|
8
|
+
3. Always define: problem = qb.OptimizationProblem()
|
|
9
|
+
4. Set objective with problem.minimize(...) or problem.maximize(...)
|
|
10
|
+
5. Add constraints with problem.add_constraint(...)
|
|
11
|
+
6. Use these variable factories:
|
|
12
|
+
- qb.Binary("name") for 0/1 decisions
|
|
13
|
+
- qb.Integer("name", lb=..., ub=...) for bounded integers
|
|
14
|
+
- qb.Continuous("name", lb=..., ub=..., precision_bits=...) for reals
|
|
15
|
+
7. Optional: SOLVE_KWARGS = {"p": 4, "alpha": 10.0, "tight_encoding": True, "n_seeds": 3}
|
|
16
|
+
8. Never import anything except qbridge.
|
|
17
|
+
9. Never use print(), input(), file I/O, or network calls.
|
|
18
|
+
10. Prefer Binary encodings for parity/cardinality when possible; keep qubit count low.
|
|
19
|
+
11. Never use the Python % modulo operator on variables (unsupported). For evenness of a sum S,
|
|
20
|
+
prefer Binary variables and direct cardinality/cover constraints. Only as a last resort
|
|
21
|
+
introduce k = qb.Integer("k", lb=0, ub=...) with add_constraint(S == 2 * k) — Integer aux
|
|
22
|
+
variables add qubits.
|
|
23
|
+
|
|
24
|
+
NAMING CONVENTIONS:
|
|
25
|
+
- MaxCut: x0, x1, x2, ... (Binary)
|
|
26
|
+
- Knapsack: q0, q1, ... (Integer quantities)
|
|
27
|
+
- Graph Coloring: x_{vertex}_{color} (Binary), colors_used (Integer)
|
|
28
|
+
|
|
29
|
+
OUTPUT FORMAT:
|
|
30
|
+
Return exactly one ```python code block containing the complete script with a top-level `problem` variable.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
FEW_SHOT_EXAMPLES = [
|
|
34
|
+
{
|
|
35
|
+
"user": "MaxCut on a 4-node cycle graph. Maximize the number of cut edges.",
|
|
36
|
+
"assistant": '''```python
|
|
37
|
+
import qbridge as qb
|
|
38
|
+
|
|
39
|
+
x = [qb.Binary(f"x{i}") for i in range(4)]
|
|
40
|
+
problem = qb.OptimizationProblem()
|
|
41
|
+
cut_edges = []
|
|
42
|
+
for i in range(4):
|
|
43
|
+
j = (i + 1) % 4
|
|
44
|
+
cut_edges.append(x[i] * (1 - x[j]) + (1 - x[i]) * x[j])
|
|
45
|
+
problem.maximize(sum(cut_edges))
|
|
46
|
+
|
|
47
|
+
SOLVE_KWARGS = {"p": 4, "alpha": 10.0, "tight_encoding": True, "n_seeds": 3}
|
|
48
|
+
```''',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"user": "3-item knapsack: maximize value subject to weight capacity 30. Items have values [40, 35, 25] and weights [10, 15, 20]. Max quantity 5 per item.",
|
|
52
|
+
"assistant": '''```python
|
|
53
|
+
import qbridge as qb
|
|
54
|
+
|
|
55
|
+
values = [40, 35, 25]
|
|
56
|
+
weights = [10, 15, 20]
|
|
57
|
+
capacity = 30
|
|
58
|
+
qty = [qb.Integer(f"q{i}", lb=0, ub=5) for i in range(3)]
|
|
59
|
+
problem = qb.OptimizationProblem()
|
|
60
|
+
problem.maximize(sum(values[i] * qty[i] for i in range(3)))
|
|
61
|
+
problem.add_constraint(sum(weights[i] * qty[i] for i in range(3)) <= capacity)
|
|
62
|
+
|
|
63
|
+
SOLVE_KWARGS = {"p": 4, "alpha": 10.0, "tight_encoding": True, "n_seeds": 3}
|
|
64
|
+
```''',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"user": "Graph coloring: 3 vertices, 2 colors, edges (0,1) and (1,2). Minimize colors used.",
|
|
68
|
+
"assistant": '''```python
|
|
69
|
+
import qbridge as qb
|
|
70
|
+
|
|
71
|
+
n_vertices = 3
|
|
72
|
+
k_colors = 2
|
|
73
|
+
edges = [(0, 1), (1, 2)]
|
|
74
|
+
x = {(v, c): qb.Binary(f"x_{v}_{c}") for v in range(n_vertices) for c in range(k_colors)}
|
|
75
|
+
colors_used = qb.Integer("colors_used", lb=1, ub=k_colors)
|
|
76
|
+
problem = qb.OptimizationProblem()
|
|
77
|
+
problem.minimize(colors_used)
|
|
78
|
+
for v in range(n_vertices):
|
|
79
|
+
problem.add_constraint(sum(x[v, c] for c in range(k_colors)) == 1)
|
|
80
|
+
for u, v in edges:
|
|
81
|
+
for c in range(k_colors):
|
|
82
|
+
problem.add_constraint(x[u, c] + x[v, c] <= 1)
|
|
83
|
+
problem.add_constraint(colors_used >= 2)
|
|
84
|
+
problem.add_constraint(colors_used <= k_colors)
|
|
85
|
+
|
|
86
|
+
SOLVE_KWARGS = {"p": 4, "alpha": 10.0, "tight_encoding": True, "n_seeds": 3}
|
|
87
|
+
```''',
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_translation_messages(user_description: str) -> list[dict[str, str]]:
|
|
93
|
+
"""Build chat messages with system prompt, few-shots, and user request."""
|
|
94
|
+
messages: list[dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
95
|
+
for example in FEW_SHOT_EXAMPLES:
|
|
96
|
+
messages.append({"role": "user", "content": example["user"]})
|
|
97
|
+
messages.append({"role": "assistant", "content": example["assistant"]})
|
|
98
|
+
messages.append({"role": "user", "content": user_description})
|
|
99
|
+
return messages
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Local, auditable QBridge DSL execution for CUDA-Q benchmarks.
|
|
2
|
+
|
|
3
|
+
This is deliberately a narrow adapter: it parses the declared DSL with the
|
|
4
|
+
compiler, forces CUDA-Q's NVIDIA target, and returns only the decoded result
|
|
5
|
+
plus target evidence. It never falls back to a classical simulator.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compile_dsl_locally(dsl_source: str, workspace_root: Path | str) -> tuple[dict[str, str], str]:
|
|
16
|
+
"""Compile with the installed PyPI ``qbridge`` package, never a cloud API."""
|
|
17
|
+
from qbridge.dsl_build import compile_dsl_file
|
|
18
|
+
|
|
19
|
+
root = Path(workspace_root).resolve()
|
|
20
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
dsl_path = root / "problem.py"
|
|
22
|
+
output = root / "compiled_output" / "kernel_execution.py"
|
|
23
|
+
dsl_path.write_text(dsl_source, encoding="utf-8")
|
|
24
|
+
generated = compile_dsl_file(dsl_path, output)
|
|
25
|
+
(output.parent / "compile_meta.json").write_text(
|
|
26
|
+
json.dumps({"backend": "local:qbridge", "artifact": str(generated)}, indent=2) + "\n",
|
|
27
|
+
encoding="utf-8",
|
|
28
|
+
)
|
|
29
|
+
return {"dsl": str(dsl_path), "cudaq": str(generated)}, generated.read_text(encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
def solve_dsl_on_nvidia(dsl_source: str, *, shots: int = 1024) -> dict[str, Any]:
|
|
32
|
+
"""Compile and execute a declared QBridge optimization DSL on CUDA-Q GPU."""
|
|
33
|
+
try:
|
|
34
|
+
import cudaq
|
|
35
|
+
from qbridge.frontend.dsl_parser import parse_dsl_source
|
|
36
|
+
from qbridge.layer5_cudaq_codegen.backend import BackendConfig, BackendKind
|
|
37
|
+
except ImportError as exc: # pragma: no cover - environment contract
|
|
38
|
+
raise RuntimeError("Local CUDA-Q compiler dependencies are unavailable") from exc
|
|
39
|
+
|
|
40
|
+
cudaq.set_target("nvidia")
|
|
41
|
+
target = str(cudaq.get_target())
|
|
42
|
+
if "nvidia" not in target.lower() and "cusv" not in target.lower():
|
|
43
|
+
raise RuntimeError(f"CUDA-Q did not select the NVIDIA target: {target}")
|
|
44
|
+
|
|
45
|
+
problem, solve_kwargs = parse_dsl_source(dsl_source)
|
|
46
|
+
# Benchmark settings are intentionally capped so generated DSL cannot turn
|
|
47
|
+
# an evaluation into an arbitrarily expensive experiment.
|
|
48
|
+
solve_kwargs.update({"shots": shots, "p": min(int(solve_kwargs.get("p", 2)), 2),
|
|
49
|
+
"n_seeds": min(int(solve_kwargs.get("n_seeds", 1)), 2),
|
|
50
|
+
"maxiter": min(int(solve_kwargs.get("maxiter", 30)), 50)})
|
|
51
|
+
result = problem.solve(
|
|
52
|
+
backend=BackendConfig(kind=BackendKind.CUDAQ_MGPU, target="nvidia", shots=shots),
|
|
53
|
+
**solve_kwargs,
|
|
54
|
+
)
|
|
55
|
+
return {
|
|
56
|
+
"assignment": dict(result.assignment),
|
|
57
|
+
"objective_value": result.objective_value,
|
|
58
|
+
"constraints_satisfied": result.constraints_satisfied,
|
|
59
|
+
"cudaq_target": target,
|
|
60
|
+
"backend": "cudaq:nvidia",
|
|
61
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Local-only path: NL -> QBridge DSL -> installed compiler -> CUDA-Q GPU."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from agent.compiler_interface.dsl_extract import extract_and_validate_dsl
|
|
11
|
+
from agent.compiler_interface.dsl_lint import lint_dsl
|
|
12
|
+
from agent.compiler_interface.dsl_prompts import SYSTEM_PROMPT, build_translation_messages
|
|
13
|
+
from agent.compiler_interface.local_cudaq import compile_dsl_locally, solve_dsl_on_nvidia
|
|
14
|
+
|
|
15
|
+
CompleteFn = Callable[[list[dict[str, str]]], str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class PipelineReport:
|
|
20
|
+
ok: bool
|
|
21
|
+
summary: str
|
|
22
|
+
dsl_source: str = ""
|
|
23
|
+
local_files: dict[str, str] = field(default_factory=dict)
|
|
24
|
+
solution: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
error: str = ""
|
|
26
|
+
steps: list[str] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QuantumPipeline:
|
|
30
|
+
"""Safe local compiler-backed agent; it has no cloud compile/solve step."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, complete_fn: CompleteFn, workspace_root: Path | None = None,
|
|
33
|
+
include_solve: bool = True, max_dsl_retries: int = 1, **_: Any) -> None:
|
|
34
|
+
self.complete_fn, self.workspace_root = complete_fn, Path(workspace_root or Path.cwd()).resolve()
|
|
35
|
+
self.include_solve, self.max_dsl_retries = include_solve, max_dsl_retries
|
|
36
|
+
|
|
37
|
+
def _extract(self, raw: str) -> str:
|
|
38
|
+
source, error = extract_and_validate_dsl(raw)
|
|
39
|
+
if error:
|
|
40
|
+
raise RuntimeError(error)
|
|
41
|
+
lint = lint_dsl(source)
|
|
42
|
+
if not lint["ok"]:
|
|
43
|
+
raise RuntimeError(lint["summary"])
|
|
44
|
+
return source
|
|
45
|
+
|
|
46
|
+
def _repair(self, source: str, error: str) -> str:
|
|
47
|
+
raw = self.complete_fn([
|
|
48
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
49
|
+
{"role": "user", "content": "Fix this DSL. Output only one Python fence.\n"
|
|
50
|
+
f"Compiler error: {error}\n```python\n{source}\n```"},
|
|
51
|
+
])
|
|
52
|
+
return self._extract(raw)
|
|
53
|
+
|
|
54
|
+
def run(self, description: str) -> PipelineReport:
|
|
55
|
+
steps: list[str] = ["synthesize_dsl"]
|
|
56
|
+
try:
|
|
57
|
+
dsl = self._extract(self.complete_fn(build_translation_messages(description)))
|
|
58
|
+
except Exception as exc: # noqa: BLE001
|
|
59
|
+
return PipelineReport(False, "", error=str(exc), steps=steps)
|
|
60
|
+
|
|
61
|
+
for attempt in range(self.max_dsl_retries + 1):
|
|
62
|
+
try:
|
|
63
|
+
steps.append("local_compile")
|
|
64
|
+
paths, _ = compile_dsl_locally(dsl, self.workspace_root)
|
|
65
|
+
solution: dict[str, Any] = {}
|
|
66
|
+
if self.include_solve:
|
|
67
|
+
steps.append("local_cudaq_solve")
|
|
68
|
+
result = solve_dsl_on_nvidia(dsl)
|
|
69
|
+
solution = {
|
|
70
|
+
"assignment": result["assignment"],
|
|
71
|
+
"objective_value": result["objective_value"],
|
|
72
|
+
"constraints_satisfied": result["constraints_satisfied"],
|
|
73
|
+
"backend": result["backend"],
|
|
74
|
+
"cudaq_target": result["cudaq_target"],
|
|
75
|
+
}
|
|
76
|
+
sol = self.workspace_root / "compiled_output" / "solution.json"
|
|
77
|
+
sol.write_text(json.dumps(solution, indent=2) + "\n", encoding="utf-8")
|
|
78
|
+
paths["solution"] = str(sol)
|
|
79
|
+
if not solution["constraints_satisfied"]:
|
|
80
|
+
raise RuntimeError("CUDA-Q solve returned an infeasible decoded assignment")
|
|
81
|
+
return PipelineReport(
|
|
82
|
+
True,
|
|
83
|
+
"\n".join(["Compiled locally with installed qbridge package", f"DSL: {paths['dsl']}",
|
|
84
|
+
f"CUDA-Q: {paths['cudaq']}", f"Assignment: {solution.get('assignment')}"]),
|
|
85
|
+
dsl, paths, solution, steps=steps,
|
|
86
|
+
)
|
|
87
|
+
except Exception as exc: # noqa: BLE001
|
|
88
|
+
if attempt >= self.max_dsl_retries:
|
|
89
|
+
return PipelineReport(False, "Compiled locally but execution failed.", dsl, error=str(exc), steps=steps)
|
|
90
|
+
steps.append("repair_dsl")
|
|
91
|
+
try:
|
|
92
|
+
dsl = self._repair(dsl, str(exc))
|
|
93
|
+
except Exception as repair: # noqa: BLE001
|
|
94
|
+
return PipelineReport(False, "", dsl, error=str(repair), steps=steps)
|
|
95
|
+
raise AssertionError("unreachable")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_quantum_pipeline(description: str, *, complete_fn: CompleteFn,
|
|
99
|
+
workspace_root: Path | None = None, include_solve: bool = True) -> PipelineReport:
|
|
100
|
+
return QuantumPipeline(complete_fn=complete_fn, workspace_root=workspace_root,
|
|
101
|
+
include_solve=include_solve).run(description)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Thin entry: NL problem -> DSL -> local compiler -> CUDA-Q artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agent.compiler_interface.pipeline import run_quantum_pipeline
|
|
11
|
+
from agent.models import OpenAICompatModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_cudaq_case(
|
|
15
|
+
*,
|
|
16
|
+
prompt: str,
|
|
17
|
+
workspace: Path,
|
|
18
|
+
model: str,
|
|
19
|
+
api_key: str,
|
|
20
|
+
base_url: str = "https://openrouter.ai/api/v1",
|
|
21
|
+
seed: int | None = None,
|
|
22
|
+
enable_cot: bool = False,
|
|
23
|
+
max_tokens: int = 4096,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Run one lean AgentV2 CUDA-Q case; write paper-suite artifact layout."""
|
|
26
|
+
workspace = Path(workspace)
|
|
27
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
(workspace / "prompt.txt").write_text(prompt, encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
extra: dict[str, Any] = {"usage": {"include": True}}
|
|
31
|
+
if seed is not None:
|
|
32
|
+
extra["seed"] = seed
|
|
33
|
+
if enable_cot:
|
|
34
|
+
extra["reasoning"] = {"effort": "high"}
|
|
35
|
+
extra["chat_template_kwargs"] = {"enable_thinking": True}
|
|
36
|
+
else:
|
|
37
|
+
extra["reasoning"] = {"effort": "none", "exclude": True}
|
|
38
|
+
extra["chat_template_kwargs"] = {"enable_thinking": False}
|
|
39
|
+
|
|
40
|
+
llm = OpenAICompatModel(
|
|
41
|
+
model=model,
|
|
42
|
+
api_key=api_key,
|
|
43
|
+
base_url=base_url,
|
|
44
|
+
temperature=0.0,
|
|
45
|
+
max_tokens=max_tokens,
|
|
46
|
+
timeout=600.0,
|
|
47
|
+
extra_body=extra,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def complete_fn(messages: list[dict[str, str]]) -> str:
|
|
51
|
+
return llm.generate_messages(messages).text
|
|
52
|
+
|
|
53
|
+
t0 = time.perf_counter()
|
|
54
|
+
report = run_quantum_pipeline(
|
|
55
|
+
prompt,
|
|
56
|
+
complete_fn=complete_fn,
|
|
57
|
+
workspace_root=workspace,
|
|
58
|
+
include_solve=True,
|
|
59
|
+
)
|
|
60
|
+
wall = time.perf_counter() - t0
|
|
61
|
+
|
|
62
|
+
usage_rows = list(llm.last_usage_log)
|
|
63
|
+
meta = {
|
|
64
|
+
"usage": usage_rows,
|
|
65
|
+
"ok": report.ok,
|
|
66
|
+
"error": report.error,
|
|
67
|
+
"summary": report.summary,
|
|
68
|
+
"steps": report.steps,
|
|
69
|
+
"solution": report.solution,
|
|
70
|
+
"timings": {"wall_s": wall},
|
|
71
|
+
"native_retry_policy": (
|
|
72
|
+
"max_dsl_retries=1; lean AgentV2 (no Aider); local CUDA-Q; "
|
|
73
|
+
+ ("CoT on" if enable_cot else "CoT off")
|
|
74
|
+
),
|
|
75
|
+
}
|
|
76
|
+
(workspace / "_usage.json").write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
|
77
|
+
return {
|
|
78
|
+
"ok": report.ok,
|
|
79
|
+
"error": report.error,
|
|
80
|
+
"solution": report.solution,
|
|
81
|
+
"usage": usage_rows,
|
|
82
|
+
"wall_s": wall,
|
|
83
|
+
"dsl_source": report.dsl_source,
|
|
84
|
+
"steps": report.steps,
|
|
85
|
+
"summary": report.summary,
|
|
86
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Model abstraction for prompt → text (+ usage)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ModelResponse:
|
|
15
|
+
text: str
|
|
16
|
+
prompt_tokens: int = 0
|
|
17
|
+
completion_tokens: int = 0
|
|
18
|
+
latency_ms: float = 0.0
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def total_tokens(self) -> int:
|
|
22
|
+
return self.prompt_tokens + self.completion_tokens
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Model(ABC):
|
|
26
|
+
"""Provider-agnostic text generation."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def generate(self, prompt: str, *, system: str | None = None) -> ModelResponse:
|
|
30
|
+
"""Return model text and usage metadata."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MockModel(Model):
|
|
34
|
+
"""Deterministic offline model. Keys are case ids or exact prompt prefixes.
|
|
35
|
+
|
|
36
|
+
Lookup order:
|
|
37
|
+
1. ``case_id`` kw via ``set_case`` / active case
|
|
38
|
+
2. Exact full prompt match in ``responses``
|
|
39
|
+
3. Prefix match
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, responses: dict[str, str] | None = None) -> None:
|
|
43
|
+
self.responses = dict(responses or {})
|
|
44
|
+
self._active_case: str | None = None
|
|
45
|
+
|
|
46
|
+
def set_case(self, case_id: str | None) -> None:
|
|
47
|
+
self._active_case = case_id
|
|
48
|
+
|
|
49
|
+
def generate(self, prompt: str, *, system: str | None = None) -> ModelResponse:
|
|
50
|
+
t0 = time.perf_counter()
|
|
51
|
+
text = ""
|
|
52
|
+
if self._active_case and self._active_case in self.responses:
|
|
53
|
+
text = self.responses[self._active_case]
|
|
54
|
+
elif prompt in self.responses:
|
|
55
|
+
text = self.responses[prompt]
|
|
56
|
+
else:
|
|
57
|
+
for key, value in self.responses.items():
|
|
58
|
+
if prompt.startswith(key) or key in prompt:
|
|
59
|
+
text = value
|
|
60
|
+
break
|
|
61
|
+
if not text:
|
|
62
|
+
text = self.responses.get("__default__", "")
|
|
63
|
+
# Crude token estimate for offline runs
|
|
64
|
+
blob = (system or "") + prompt + text
|
|
65
|
+
tokens = max(1, len(blob.split()))
|
|
66
|
+
latency = (time.perf_counter() - t0) * 1000
|
|
67
|
+
return ModelResponse(
|
|
68
|
+
text=text,
|
|
69
|
+
prompt_tokens=max(1, tokens // 2),
|
|
70
|
+
completion_tokens=max(1, len(text.split()) or 1),
|
|
71
|
+
latency_ms=latency,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OpenAICompatModel(Model):
|
|
76
|
+
"""OpenAI Chat Completions-compatible HTTP client (cloud or local)."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
model: str = "gpt-4o-mini",
|
|
82
|
+
api_key: str | None = None,
|
|
83
|
+
base_url: str = "https://api.openai.com/v1",
|
|
84
|
+
temperature: float = 0.0,
|
|
85
|
+
max_tokens: int = 1024,
|
|
86
|
+
timeout: float = 60.0,
|
|
87
|
+
extra_body: dict[str, Any] | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.model = model
|
|
90
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "")
|
|
91
|
+
self.base_url = base_url.rstrip("/")
|
|
92
|
+
self.temperature = temperature
|
|
93
|
+
self.max_tokens = max_tokens
|
|
94
|
+
self.timeout = timeout
|
|
95
|
+
self.extra_body = dict(extra_body or {})
|
|
96
|
+
self.last_usage_log: list[dict[str, Any]] = []
|
|
97
|
+
|
|
98
|
+
def generate(self, prompt: str, *, system: str | None = None) -> ModelResponse:
|
|
99
|
+
messages: list[dict[str, str]] = []
|
|
100
|
+
if system:
|
|
101
|
+
messages.append({"role": "system", "content": system})
|
|
102
|
+
messages.append({"role": "user", "content": prompt})
|
|
103
|
+
return self.generate_messages(messages)
|
|
104
|
+
|
|
105
|
+
def generate_messages(self, messages: list[dict[str, str]]) -> ModelResponse:
|
|
106
|
+
"""Multi-turn chat (used by the lean CUDA-Q quantum pipeline)."""
|
|
107
|
+
import httpx
|
|
108
|
+
|
|
109
|
+
url = f"{self.base_url}/chat/completions"
|
|
110
|
+
headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
|
|
111
|
+
body: dict[str, Any] = {
|
|
112
|
+
"model": self.model,
|
|
113
|
+
"messages": messages,
|
|
114
|
+
"temperature": self.temperature,
|
|
115
|
+
"max_tokens": self.max_tokens,
|
|
116
|
+
**self.extra_body,
|
|
117
|
+
}
|
|
118
|
+
t0 = time.perf_counter()
|
|
119
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
120
|
+
resp = client.post(url, headers=headers, json=body)
|
|
121
|
+
resp.raise_for_status()
|
|
122
|
+
data = resp.json()
|
|
123
|
+
latency = (time.perf_counter() - t0) * 1000
|
|
124
|
+
text = data["choices"][0]["message"]["content"] or ""
|
|
125
|
+
usage = data.get("usage") or {}
|
|
126
|
+
rec = {
|
|
127
|
+
"prompt_tokens": int(usage.get("prompt_tokens") or 0),
|
|
128
|
+
"completion_tokens": int(usage.get("completion_tokens") or 0),
|
|
129
|
+
"total_tokens": int(usage.get("total_tokens") or 0),
|
|
130
|
+
}
|
|
131
|
+
self.last_usage_log.append(rec)
|
|
132
|
+
return ModelResponse(
|
|
133
|
+
text=text,
|
|
134
|
+
prompt_tokens=rec["prompt_tokens"],
|
|
135
|
+
completion_tokens=rec["completion_tokens"],
|
|
136
|
+
latency_ms=latency,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def extract_json_object(text: str) -> dict[str, Any]:
|
|
141
|
+
"""Pull the first JSON object from model output (fences allowed)."""
|
|
142
|
+
raw = text.strip()
|
|
143
|
+
if raw.startswith("```"):
|
|
144
|
+
lines = raw.splitlines()
|
|
145
|
+
if lines and lines[0].startswith("```"):
|
|
146
|
+
lines = lines[1:]
|
|
147
|
+
if lines and lines[-1].strip() == "```":
|
|
148
|
+
lines = lines[:-1]
|
|
149
|
+
raw = "\n".join(lines).strip()
|
|
150
|
+
if raw.lower().startswith("json"):
|
|
151
|
+
raw = raw[4:].lstrip()
|
|
152
|
+
try:
|
|
153
|
+
obj = json.loads(raw)
|
|
154
|
+
if isinstance(obj, dict):
|
|
155
|
+
return obj
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
pass
|
|
158
|
+
start = raw.find("{")
|
|
159
|
+
end = raw.rfind("}")
|
|
160
|
+
if start >= 0 and end > start:
|
|
161
|
+
obj = json.loads(raw[start : end + 1])
|
|
162
|
+
if isinstance(obj, dict):
|
|
163
|
+
return obj
|
|
164
|
+
raise ValueError(f"No JSON object in model output: {text[:200]!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared verification and execution utilities."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Shared, model-agnostic correctness checks for frozen CUDA-Q tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def verify(assignment: dict[str, Any] | None, problem: dict[str, Any]) -> dict[str, Any]:
|
|
9
|
+
if not isinstance(assignment, dict):
|
|
10
|
+
return {"status": "UNVERIFIED", "ok": False, "notes": ["no assignment"]}
|
|
11
|
+
x = [int(assignment.get(f"x{i}", assignment.get(str(i), 0))) for i in range(problem["num_vars"])]
|
|
12
|
+
inst, bad = problem["instance"], []
|
|
13
|
+
objective = (
|
|
14
|
+
sum(v * x[i] for i, v in enumerate(inst["values"]))
|
|
15
|
+
if "values" in inst
|
|
16
|
+
else sum(w * (x[u] ^ x[v]) for u, v, w in inst["edges"])
|
|
17
|
+
)
|
|
18
|
+
if "capacity" in inst and sum(w * x[i] for i, w in enumerate(inst["weights"])) > inst["capacity"]:
|
|
19
|
+
bad.append("capacity")
|
|
20
|
+
if "cardinality" in inst and sum(x) != inst["cardinality"]:
|
|
21
|
+
bad.append("cardinality")
|
|
22
|
+
if "k" in inst and sum(x) != inst["k"]:
|
|
23
|
+
bad.append("k")
|
|
24
|
+
if any(x[a] and x[b] for a, b in inst.get("conflicts", [])):
|
|
25
|
+
bad.append("conflict")
|
|
26
|
+
if any(not any(x[i] for i in group) for group in inst.get("groups_at_least_one", [])):
|
|
27
|
+
bad.append("group_cover")
|
|
28
|
+
if "exactly_one" in inst and sum(x[i] for i in inst["exactly_one"]) != 1:
|
|
29
|
+
bad.append("exactly_one")
|
|
30
|
+
optimum = problem["classical_reference"]["optimal_objective"]
|
|
31
|
+
return {"status": "VERIFIED" if not bad else "INCORRECT", "ok": not bad,
|
|
32
|
+
"assignment": {f"x{i}": bit for i, bit in enumerate(x)}, "objective": objective,
|
|
33
|
+
"objective_gap": (optimum - objective) / optimum if not bad else None, "notes": bad}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qbridge-agent"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Local, compiler-backed CUDA-Q optimization agent"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "BUSL-1.1"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"qbridge[quantum]>=0.1.0",
|
|
15
|
+
"httpx>=0.27",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest>=7.0", "ruff>=0.6.0"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
qbridge-agent = "agent.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["."]
|
|
26
|
+
include = ["agent*"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
addopts = "-q"
|
|
31
|
+
pythonpath = ["."]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
target-version = "py39"
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
39
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qbridge-agent
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Local, compiler-backed CUDA-Q optimization agent
|
|
5
|
+
License-Expression: BUSL-1.1
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: qbridge[quantum]>=0.1.0
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# QBridge-Agent
|
|
17
|
+
|
|
18
|
+
## Install and run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install "qbridge[quantum]" qbridge-agent
|
|
22
|
+
qbridge-agent my_problem.py --workspace ./qbridge-output
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`my_problem.py` must be a supported QBridge Python DSL optimization model, not
|
|
26
|
+
arbitrary Python. Add `--no-solve` to emit CUDA-Q without running it.
|
|
27
|
+
|
|
28
|
+
QBridge-Agent translates a constrained binary optimization request into the
|
|
29
|
+
QBridge Python DSL, then delegates verified compilation and execution to the
|
|
30
|
+
locally installed QBridge CUDA-Q compiler. It is deliberately not a general
|
|
31
|
+
coding agent and has no cloud compile/solve dependency.
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
QBridge-Agent/
|
|
37
|
+
├── agent/
|
|
38
|
+
│ ├── planner/ # intent-to-DSL prompt policy
|
|
39
|
+
│ ├── tools/ # independent benchmark verification
|
|
40
|
+
│ ├── compiler_interface/ # DSL extraction, local compiler and CUDA-Q execution
|
|
41
|
+
├── examples/
|
|
42
|
+
├── evaluations/
|
|
43
|
+
│ ├── benchmark_tasks.json # frozen T1/T2 task set
|
|
44
|
+
│ ├── results/ # raw reproducible run artifacts
|
|
45
|
+
│ └── README.md
|
|
46
|
+
└── README.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Evaluation results
|
|
50
|
+
|
|
51
|
+
Run this in the Ubuntu environment that has CUDA-Q installed:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
OPENROUTER_API_KEY=... \
|
|
55
|
+
/home/dilip/qbridge-env/bin/python3 evaluations/run_benchmark.py \
|
|
56
|
+
--trials 3 --results evaluations/results/my_run
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The runner forces `cudaq.set_target("nvidia")`, performs a CUDA-Q sample GPU
|
|
60
|
+
probe before work begins, stores the actual target description for each V2
|
|
61
|
+
result, and checks both systems with the same classical feasibility/objective
|
|
62
|
+
verifier. It never converts an execution error into a successful result.
|
|
63
|
+
|
|
64
|
+
`OPENROUTER_API_KEY` is required only for this external-model comparison; the
|
|
65
|
+
normal agent compile/solve path is local. The reproducible 20 July 2026 run
|
|
66
|
+
on the six frozen T1/T2 tasks (three trials each, NVIDIA `cusvsim_fp32`) found:
|
|
67
|
+
|
|
68
|
+
- QBridge-Agent: 16 `VERIFIED`, 1 `INCORRECT`, 1 `UNVERIFIED`.
|
|
69
|
+
- Aider: 0 `VERIFIED`, 17 `UNVERIFIED`, 1 `INVALID_METHOD`.
|
|
70
|
+
|
|
71
|
+
These are strict verifier outcomes, not success-rate estimates. Raw artifacts
|
|
72
|
+
for that run are retained under `evaluations/results/20260720_t1_t2_gpu_fixed`
|
|
73
|
+
in a source checkout and excluded from package distributions.
|
|
74
|
+
|
|
75
|
+
From a source checkout:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install .
|
|
79
|
+
qbridge-agent my_problem.py --workspace ./qbridge-output
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`qbridge-agent` invokes the local `qbridge` package to emit CUDA-Q, then uses
|
|
83
|
+
the configured local CUDA-Q target. No problem DSL is sent to a QBridge cloud
|
|
84
|
+
endpoint.
|
|
85
|
+
|
|
86
|
+
For maintainer release steps, see [RELEASE.md](RELEASE.md).
|
|
87
|
+
|
|
88
|
+
## Scope
|
|
89
|
+
|
|
90
|
+
Supported: small constrained binary optimization problems expressible in the
|
|
91
|
+
current QBridge DSL. Unsupported quantum algorithms and arbitrary Python are
|
|
92
|
+
errors, not guessed translations.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
agent/__init__.py
|
|
6
|
+
agent/cli.py
|
|
7
|
+
agent/cudaq_entry.py
|
|
8
|
+
agent/models.py
|
|
9
|
+
agent/compiler_interface/__init__.py
|
|
10
|
+
agent/compiler_interface/dsl_extract.py
|
|
11
|
+
agent/compiler_interface/dsl_lint.py
|
|
12
|
+
agent/compiler_interface/dsl_prompts.py
|
|
13
|
+
agent/compiler_interface/local_cudaq.py
|
|
14
|
+
agent/compiler_interface/pipeline.py
|
|
15
|
+
agent/planner/__init__.py
|
|
16
|
+
agent/tools/__init__.py
|
|
17
|
+
agent/tools/cudaq_verification.py
|
|
18
|
+
qbridge_agent.egg-info/PKG-INFO
|
|
19
|
+
qbridge_agent.egg-info/SOURCES.txt
|
|
20
|
+
qbridge_agent.egg-info/dependency_links.txt
|
|
21
|
+
qbridge_agent.egg-info/entry_points.txt
|
|
22
|
+
qbridge_agent.egg-info/requires.txt
|
|
23
|
+
qbridge_agent.egg-info/top_level.txt
|
|
24
|
+
tests/test_agent_contract.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Lean agent contract tests: DSL helpers and independent task verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agent.compiler_interface.dsl_extract import extract_and_validate_dsl
|
|
6
|
+
from agent.compiler_interface.pipeline import run_quantum_pipeline
|
|
7
|
+
from agent.tools.cudaq_verification import verify
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_extracts_declared_qbridge_dsl() -> None:
|
|
11
|
+
source, error = extract_and_validate_dsl(
|
|
12
|
+
"```python\nimport qbridge as qb\nproblem = qb.OptimizationProblem()\n"
|
|
13
|
+
"x = qb.Binary('x0')\nproblem.maximize(x)\n```"
|
|
14
|
+
)
|
|
15
|
+
assert error is None
|
|
16
|
+
assert "OptimizationProblem" in source
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_verifier_rejects_constraint_violation() -> None:
|
|
20
|
+
problem = {
|
|
21
|
+
"num_vars": 2,
|
|
22
|
+
"instance": {"values": [5, 4], "weights": [2, 2], "capacity": 2},
|
|
23
|
+
"classical_reference": {"optimal_objective": 5.0},
|
|
24
|
+
}
|
|
25
|
+
assert verify({"x0": 1, "x1": 1}, problem)["status"] == "INCORRECT"
|
|
26
|
+
assert verify({"x0": 1, "x1": 0}, problem)["status"] == "VERIFIED"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pipeline_uses_local_compiler_without_cloud(tmp_path) -> None:
|
|
30
|
+
dsl = """import qbridge as qb
|
|
31
|
+
x = qb.Binary("x0")
|
|
32
|
+
problem = qb.OptimizationProblem()
|
|
33
|
+
problem.maximize(x)
|
|
34
|
+
"""
|
|
35
|
+
report = run_quantum_pipeline("ignored", complete_fn=lambda _: dsl,
|
|
36
|
+
workspace_root=tmp_path, include_solve=False)
|
|
37
|
+
assert report.ok, report.error
|
|
38
|
+
assert (tmp_path / "compiled_output" / "kernel_execution.py").is_file()
|