File size: 6,745 Bytes
ab54eb4 204d3c5 ab54eb4 204d3c5 ab54eb4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | """
Streaming wrapper around AMCPipeline for the web chat front-end.
Runs the pipeline in a background thread and yields JSON-serialisable progress
events as they happen so the chat UI can paint a live timeline.
Web-demo defaults differ from the CLI on purpose:
- dynamic validation off (Stage 3 needs a writable build dir on every host)
- realism check off (extra LLM calls; the demo is already slow)
- short refinement loop and CBMC timeout, since visitors won't wait minutes
"""
from __future__ import annotations
import logging
import queue
import shutil
import tempfile
import threading
from pathlib import Path
from typing import Iterator
import bmc_agent.logger as _bmc_log_mod
from bmc_agent.config import Config
from bmc_agent.pipeline import AMCPipeline
_MAX_SOURCE_BYTES = 64 * 1024 # 64KB cap for pasted source
_WALL_TIMEOUT_SEC = 300 # hard ceiling on a single web run
# bmc_agent loggers set propagate=False, so a handler attached to the
# package root does not receive child records. We attach the queue handler
# to each "bmc_agent.*" logger directly, plus monkey-patch get_logger so
# lazily-created component loggers also pick up the handler.
#
# This mutates global logging state, so runs are serialised through a lock.
_RUN_LOCK = threading.Lock()
def run_aprover_streaming(
source_code: str,
function: str | None = None,
domain_knowledge: str = "",
api_key: str = "",
) -> Iterator[dict]:
"""Run AMCPipeline on a snippet, yielding progress events.
Each yielded value is a dict with a ``type`` field. Types:
- ``started``: pipeline has begun
- ``log``: a log line from bmc_agent.* loggers
- ``error``: fatal error before/while running
- ``result``: terminal event with the bug summary
"""
if not source_code.strip():
yield {"type": "error", "message": "No source code provided."}
return
if len(source_code.encode("utf-8")) > _MAX_SOURCE_BYTES:
yield {
"type": "error",
"message": f"Source too large ({len(source_code)} bytes); web demo cap is {_MAX_SOURCE_BYTES}B.",
}
return
work_dir = Path(tempfile.mkdtemp(prefix="aprover_web_"))
src_path = work_dir / "input.c"
src_path.write_text(source_code, encoding="utf-8")
config = Config.from_env()
# Visitor-supplied key takes precedence over any server-side env key.
# resolved_api_key() checks llm_api_key first, so this routes the whole
# pipeline (spec gen + refinement) through the caller's own key.
if api_key:
config.llm_api_key = api_key
config.artifact_dir = str(work_dir / "artifacts")
config.enable_dynamic_validation = False
config.enable_realism_check = False
config.enable_realism_thinking = False
config.cbmc_timeout = 60
config.cbmc_unwind = 4
config.max_refinement_iters = 2
config.max_spec_retries = 2
if not config.resolved_api_key():
yield {
"type": "error",
"message": "Server is missing ANTHROPIC_API_KEY — verification cannot run.",
}
return
events: queue.Queue = queue.Queue()
sentinel = object()
class _QueueHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
try:
msg = self.format(record)
except Exception:
msg = record.getMessage()
events.put({"type": "log", "level": record.levelname.lower(), "message": msg})
handler = _QueueHandler(level=logging.INFO)
handler.setFormatter(logging.Formatter("%(message)s"))
def _attach_to_existing() -> list[logging.Logger]:
attached: list[logging.Logger] = []
for name, lg in list(logging.Logger.manager.loggerDict.items()):
if name.startswith("bmc_agent.") and isinstance(lg, logging.Logger):
if handler not in lg.handlers:
lg.addHandler(handler)
attached.append(lg)
return attached
holder: dict = {}
with _RUN_LOCK:
attached = _attach_to_existing()
original_get_logger = _bmc_log_mod.get_logger
def _wrapped_get_logger(component: str, *a, **kw): # type: ignore[no-untyped-def]
lg = original_get_logger(component, *a, **kw)
if handler not in lg.handlers:
lg.addHandler(handler)
attached.append(lg)
return lg
_bmc_log_mod.get_logger = _wrapped_get_logger # type: ignore[assignment]
def worker() -> None:
try:
pipeline = AMCPipeline(config)
holder["reports"] = pipeline.run(
source_file=str(src_path),
driver_name="webdemo",
domain_knowledge=domain_knowledge,
)
except Exception as exc: # pragma: no cover - surfaced to user
holder["error"] = f"{type(exc).__name__}: {exc}"
finally:
events.put(sentinel)
t = threading.Thread(target=worker, daemon=True)
t.start()
yield {"type": "started", "function": function or ""}
timed_out = False
while True:
try:
ev = events.get(timeout=_WALL_TIMEOUT_SEC)
except queue.Empty:
timed_out = True
break
if ev is sentinel:
break
yield ev
for lg in attached:
try:
lg.removeHandler(handler)
except ValueError:
pass
_bmc_log_mod.get_logger = original_get_logger # type: ignore[assignment]
t.join(timeout=5)
shutil.rmtree(work_dir, ignore_errors=True)
if timed_out:
yield {
"type": "result",
"result": {"ok": False, "error": f"Pipeline exceeded {_WALL_TIMEOUT_SEC}s wall timeout."},
}
return
if "error" in holder:
yield {"type": "result", "result": {"ok": False, "error": holder["error"]}}
return
reports = holder.get("reports", []) or []
if function:
reports = [r for r in reports if r.function_name == function]
summary = {
"ok": True,
"function_filter": function or "",
"n_bugs": len(reports),
"bugs": [
{
"function": r.function_name,
"bug_type": r.bug_type,
"violated_property": r.violated_property,
"confidence": r.confidence,
"call_chain": r.call_chain or [],
"reasoning": (r.reasoning_trail or "")[:600],
}
for r in reports
],
}
yield {"type": "result", "result": summary}
|