qbridge-agent 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agent/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Lean CUDA-Q constrained-optimization agent."""
2
+
3
+ from agent.models import Model, ModelResponse, OpenAICompatModel
4
+
5
+ __all__ = ["Model", "ModelResponse", "OpenAICompatModel"]
6
+ __version__ = "0.1.0"
agent/cli.py ADDED
@@ -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)
agent/cudaq_entry.py ADDED
@@ -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
+ }
agent/models.py ADDED
@@ -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,5 @@
1
+ """Intent-to-DSL planning surface (currently constrained optimization)."""
2
+
3
+ from agent.compiler_interface.dsl_prompts import build_translation_messages
4
+
5
+ __all__ = ["build_translation_messages"]
@@ -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,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,19 @@
1
+ agent/__init__.py,sha256=GZWoQijFftkd2POUs1VN0UcJgIOavfvDzcB8548To20,197
2
+ agent/cli.py,sha256=DifhhgXsaRx-bbH24xUMv6isvmbR18Bfg8YQUG4XApM,996
3
+ agent/cudaq_entry.py,sha256=IddVUow4IhapgHoABfS7iAkJhDjEZsMXJK1oFeNkFUo,2535
4
+ agent/models.py,sha256=x5zYAQ6oZvSP_Ahdhf525LAJAf52rFunv8HAw72rRGo,5598
5
+ agent/compiler_interface/__init__.py,sha256=wXWXyc8IluIGL-dWY2q0L0IYxvJmEFCax2S_MYs-MJc,381
6
+ agent/compiler_interface/dsl_extract.py,sha256=ACF7A_5R2AhDL1SzDp-apqwlDxghC6YkVMQrwb9Xnno,2764
7
+ agent/compiler_interface/dsl_lint.py,sha256=BLmqZtRFompKlPHD1TWNz-UkL_SAfRYFa7fS02gIS_U,3003
8
+ agent/compiler_interface/dsl_prompts.py,sha256=mpcAuFvKlEuO2gV3jgAgAdYF8ORUkjDSfO9TIOfwtvs,4102
9
+ agent/compiler_interface/local_cudaq.py,sha256=aXGoLxKlpW2SX2aaTJpgesWtVZrJAwpIJEUnYs--aAE,2764
10
+ agent/compiler_interface/pipeline.py,sha256=tZx1_GtTJ_PV8p7D4pOZxWEMDuSQX-tBTRzcNTqXd_I,4689
11
+ agent/planner/__init__.py,sha256=l5-_mDigSGRkwZpssTbr1KpF6dMYw_qb2QslJvCx2yo,194
12
+ agent/tools/__init__.py,sha256=1njzsHyLDWJ-C2mEt5apK22nvVmulZPTG657gjnwtEg,51
13
+ agent/tools/cudaq_verification.py,sha256=P0xP_6YbN0_kAsYq7ALjtBcXveLRapGSkfbe1CGkfc4,1632
14
+ qbridge_agent-0.2.0.dist-info/licenses/LICENSE,sha256=l7p_HHOXVNXZKCk96iH4gNPEt6N8eT-3zXDffsoc1pg,1971
15
+ qbridge_agent-0.2.0.dist-info/METADATA,sha256=Y2G4VlPwj4YY3EZTpbdwr_jAocIXhnbtu7TOREvUpzg,3186
16
+ qbridge_agent-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
17
+ qbridge_agent-0.2.0.dist-info/entry_points.txt,sha256=gH2SrEqlzKhVxMbVNAGYhdlFNbIP1e0aBWdIQzMIIuQ,49
18
+ qbridge_agent-0.2.0.dist-info/top_level.txt,sha256=0gvCG7PHc22NA63j3bTGi2Zc37ym9t8Pf90ZLzf1kGA,6
19
+ qbridge_agent-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qbridge-agent = agent.cli:main
@@ -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 @@
1
+ agent