✨ feat: make evaluations and tracing async - #535
Merged
Nicola Franco (franconicola) merged 2 commits intoJul 28, 2026
Merged
Conversation
5 tasks
| completion_result["tool_calls"] = tool_calls | ||
| try: | ||
| completion_result["finish_reason"] = response.choices[0].finish_reason | ||
| except (AttributeError, IndexError, TypeError): |
| try: | ||
| if response.usage is not None: | ||
| completion_result["usage"] = response.usage.model_dump() | ||
| except AttributeError: |
| pass | ||
| try: | ||
| completion_result["provider_model"] = response.model | ||
| except AttributeError: |
There was a problem hiding this comment.
Pull request overview
This PR migrates judge evaluation and related tracking/router flows from thread-per-request to true asyncio concurrency, so LLM and backend calls can overlap as non-blocking I/O. It also standardizes the config surface by renaming the judge batching knob to judge_concurrency, and updates tests/docs/examples accordingly.
Changes:
- Add an async request-routing path (
route_request_async) and async LiteLLM dispatch (litellm.acompletion) while preserving the existing response envelope semantics. - Convert the judge evaluator hot path to async fan-out with a semaphore (
judge_concurrency) and add a sharedrun_coroutine_blocking()bridge utility. - Parallelize tracking goal Result-record initialization and overlap Attack/Run record creation with config preparation in the orchestrator.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Locks the new Flask dependency addition. |
| pyproject.toml | Adds flask>=3.1.3 to runtime dependencies. |
| hackagent/async_utils.py | Introduces shared sync→async bridge helper (run_coroutine_blocking). |
| hackagent/attacks/evaluator/base.py | Reworks judge row evaluation to async fan-out; introduces async router/request methods. |
| hackagent/router/router.py | Adds route_request_async and async LiteLLM dispatch path; refactors LiteLLM request preparation/finalization. |
| hackagent/router/tracking/coordinator.py | Parallelizes per-goal Result initialization via async fan-out + to_thread. |
| hackagent/attacks/orchestrator.py | Overlaps Attack/Run record creation with independent configuration prep using a background thread. |
| hackagent/attacks/evaluator/evaluation_step.py | Renames config key mapping to judge_concurrency and adjusts judge-parallelism defaults. |
| hackagent/attacks/evaluator/inline_step_judge.py | Updates inline judge base config to emit judge_concurrency. |
| hackagent/attacks/techniques/config.py | Renames batch_size_judge → judge_concurrency in technique config model and exported default constant. |
| hackagent/attacks/techniques/advprefix/config.py | Wires judge_concurrency into AdvPrefix evaluation config models. |
| hackagent/attacks/techniques/advprefix/attack.py | Updates AdvPrefix pipeline config key lists to judge_concurrency. |
| hackagent/attacks/techniques/baseline/attack.py | Updates pipeline config key list to judge_concurrency. |
| hackagent/attacks/techniques/bon/attack.py | Updates pipeline config key lists to judge_concurrency. |
| hackagent/attacks/techniques/cipherchat/attack.py | Updates pipeline config key list to judge_concurrency. |
| hackagent/attacks/techniques/fc/attack.py | Updates pipeline config key lists to judge_concurrency. |
| hackagent/attacks/techniques/flipattack/attack.py | Updates pipeline config key list to judge_concurrency. |
| hackagent/attacks/techniques/h4rm3l/attack.py | Updates pipeline config key list to judge_concurrency. |
| hackagent/attacks/techniques/mml/attack.py | Updates pipeline config key list to judge_concurrency. |
| hackagent/attacks/techniques/pap/attack.py | Updates pipeline config key lists to judge_concurrency. |
| hackagent/attacks/techniques/static_template/attack.py | Updates pipeline config key list to judge_concurrency. |
| hackagent/attacks/techniques/tap/attack.py | Updates pipeline config key lists to judge_concurrency. |
| hackagent/cli/tui/attack_specs.py | Renames the TUI config field to judge_concurrency and updates labels/descriptions. |
| hackagent/examples/vllm/hack.py | Updates example config constant/key to judge_concurrency. |
| hackagent/examples/openai_sdk/quick_evaluation/run_h4rm3l.py | Updates example config key to judge_concurrency. |
| hackagent/examples/openai_sdk/multi_judge/run_flipattack_multi_judge.py | Updates example config key to judge_concurrency. |
| hackagent/examples/litellm_multi_provider/demo.py | Updates demo config key to judge_concurrency. |
| docs/docs/sdk/python-quickstart.md | Updates docs default config key to judge_concurrency. |
| docs/docs/cli/attack.mdx | Updates CLI docs to reference judge_concurrency. |
| docs/docs/attacks/advprefix.md | Updates attack docs to reference judge_concurrency. |
| docs/docs/attacks/bon.md | Updates attack docs to reference judge_concurrency. |
| docs/docs/attacks/cipherchat.md | Updates attack docs to reference judge_concurrency. |
| docs/docs/attacks/flipattack.md | Updates attack docs to reference judge_concurrency and related narrative. |
| docs/docs/attacks/h4rm3l.md | Updates attack docs to reference judge_concurrency. |
| docs/docs/attacks/mml.md | Updates attack docs to reference judge_concurrency. |
| docs/docs/attacks/pap.md | Updates attack docs to reference judge_concurrency. |
| tests/unit/router/test_dispatch.py | Adds coverage for route_request_async (LiteLLM + ADK paths). |
| tests/unit/attacks/test_evaluator_base.py | Adds regression test ensuring async router path preserves input ordering. |
| tests/unit/attacks/test_evaluation_step.py | Updates evaluation-step config expectations for judge_concurrency. |
| tests/unit/attacks/evaluator/test_inline_step_judge.py | Updates inline judge base-config tests to judge_concurrency. |
| tests/unit/attacks/advprefix/test_advprefix_evaluation_extended.py | Updates AdvPrefix evaluation tests to judge_concurrency. |
| tests/unit/attacks/bon/test_config.py | Updates required config key expectations to judge_concurrency. |
| tests/unit/attacks/cipherchat/test_config.py | Updates required config key expectations to judge_concurrency. |
| tests/unit/attacks/flipattack/test_flipattack_config.py | Updates FlipAttack config tests to judge_concurrency. |
| tests/unit/attacks/flipattack/test_flipattack_attack.py | Updates FlipAttack evaluation-step config key expectations. |
| tests/unit/attacks/h4rm3l/test_config.py | Updates required config key expectations to judge_concurrency. |
| tests/unit/attacks/mml/test_attack.py | Updates MML evaluation-step config key expectations. |
| tests/unit/attacks/pap/test_config.py | Updates required config key expectations to judge_concurrency. |
Comments suppressed due to low confidence (1)
hackagent/attacks/evaluator/base.py:536
- If the coroutine fan-out is executed in a bridge thread (when a loop is already running), progress updates are skipped inside
_fan_out_one(); without compensating updates here, the progress bar will stay at 0 until the context exits. Update the progress bar from the caller thread while iterating over the results returned byrun_coroutine_blocking(_fan_out)whenin_running_loopis true.
results_map[idx] = (
original_index,
current_eval,
current_expl,
current_raw_response,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+514
to
+518
| async def _fan_out_one(idx_row: tuple) -> tuple: | ||
| async with semaphore: | ||
| row_result = await _process_row_async(idx_row) | ||
| progress_bar.update(task, advance=1) | ||
| progress_bar.refresh() |
Nicola Franco (franconicola)
temporarily deployed
to
393v2-convert-the-judge-evaluator-hot-path-to-async-v2 - Docs PR #535
July 27, 2026 13:39 — with
Render
Destroyed
Comment on lines
+1784
to
1795
| def _create_and_start_run() -> Tuple[str, str]: | ||
| attack_id = self._create_server_attack_record( | ||
| attack_type=self.attack_type, | ||
| victim_agent_id=victim_agent_id, | ||
| organization_id=organization_id, | ||
| attack_config=attack_config, | ||
| ) | ||
| except Exception as e: | ||
| logger.error( | ||
| f"Failed to update run status to RUNNING: {e}", | ||
| exc_info=True, | ||
| run_id = self._create_server_run_record( | ||
| attack_id=attack_id, | ||
| victim_agent_id=str(victim_agent_id), | ||
| run_config_override=effective_run_config, | ||
| ) |
Comment on lines
+393
to
398
| "judge_concurrency": ( | ||
| cfg.get("judge_concurrency") or tp.get("judge_concurrency", 1) | ||
| ), | ||
| "judge_parallelism": ( | ||
| cfg.get("judge_parallelism") | ||
| or tp.get("judge_parallelism") | ||
| or cfg.get("batch_size_judge") | ||
| or tp.get("judge_batch_size", 1) | ||
| cfg.get("judge_parallelism") or tp.get("judge_parallelism", 1) | ||
| ), |
Comment on lines
88
to
94
| config = FlipAttackConfig() | ||
| assert config.attack_type == "flipattack" | ||
| assert config.batch_size_judge == 1 | ||
| assert config.judge_concurrency == 1 | ||
| assert config.max_tokens_eval == 4096 | ||
| assert config.filter_len == 10 | ||
| assert config.judge_timeout == 120 | ||
| assert config.judge_temperature == 0.0 | ||
| assert config.max_judge_retries == 1 |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Nicola Franco (franconicola)
deleted the
393v2-convert-the-judge-evaluator-hot-path-to-async-v2
branch
July 28, 2026 07:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR converts the judge-evaluation hot path from thread-per-request to a true
asynciofan-out, adds an async dispatch path to the request router so those judge calls are actually non-blocking I/O, overlaps Attack/Run record creation with independent config prep in the orchestrator, parallelizes per-goal Result-record initialization, and cleans up a dead/confusing config parameter (batch_size_judge→judge_concurrency).Changes by area
1.
hackagent/attacks/evaluator/base.py— async judge fan-outThreadPoolExecutor(max_workers=batch_size)+threading.Lock()withasyncio.gather()over anasyncio.Semaphore(judge_concurrency)._process_row_async,_request_with_assertions_async,_route_request_async(async twins of the existing sync methods).run_coroutine_blocking()bridge helper fromhackagent/async_utils.py(see below) instead of keeping a private copy.2.
hackagent/async_utils.py— new shared module (extracted this session)run_coroutine_blocking(coro_factory): runs a coroutine from synchronous code —asyncio.run()directly when no loop is running, or a dedicated bridge thread when called from inside an already-running loop (e.g. notebooks).base.py; now shared so other synchronous call sites (see bump(deps-dev): bump ruff from 0.11.9 to 0.11.10 #4 below) can reuse the same async fan-out pattern instead of re-implementing it.3.
hackagent/router/router.py— async dispatch pathroute_request_async(): an async twin ofroute_request(), preserving identical before/after-guardrail semantics and error envelopes._prepare_litellm_dispatch()/_finalize_litellm_dispatch()helpers, reused by both a new sync_dispatch_via_litellm()(litellm.completion) and the new_dispatch_via_litellm_async()(litellm.acompletion). Old implementation kept as_dispatch_via_litellm_legacyfor reference/fallback.4.
hackagent/router/tracking/coordinator.py— parallel goal-result initialization (new this session)TrackingCoordinator.initialize_goals()used to create oneResultrecord per goal viaTracker.create_goal_result()in a plain sequentialforloop — each call is an independent network round trip (or, for the local backend, a locked SQLite write), so N goals meant N blocking round trips before the attack itself could start (observed ~30s for 9 goals in a live run).asyncio.gather()+asyncio.Semaphore(_GOAL_INIT_CONCURRENCY=8), wrapping each blockingcreate_goal_result()call inasyncio.to_thread(), bridged back to the synchronous caller viarun_coroutine_blocking().GoalCategoryClassifier.classify_goals()instead of one per goal) — this change only parallelizes the remaining per-goal Result-creation network calls.5.
hackagent/attacks/orchestrator.py— overlapped run creationupdate_run(RUNNING)(3 sequential server round-trips) now run on a background thread (ThreadPoolExecutor(max_workers=1)), submitted immediately, overlapping with independent config preparation instead of blocking on it first.run_future.result()is only awaited right before attack execution starts — error propagation semantics are unchanged.6.
judge_concurrencyrename (config layer, all techniqueattack.pyfiles, TUI, docs, tests)batch_size_judge/judge_batch_size, which had become dead: they only fedEvaluatorConfig.batch_size, a field the new async code never reads. Every judge run was silently always using the hardcoded default of 10, regardless of user config.judge_concurrencyfield flows from user config →EvaluatorConfig→ theasyncio.Semaphore.judge_parallelism(controls how many different judges run concurrently with each other, not row-level concurrency within one judge) was left untouched.7. Misc:
flask>=3.1.3added topyproject.toml(unrelated dependency bump, not part of the async work).Functional impact: local vs. remote models
judge_concurrencycreate_resultHTTP round trips overlap instead of running one at a timejudge_concurrency=10vs1In short: this PR doesn't change what the SDK does with local vs. remote-tracked runs (dashboard sync, tracing, result persistence are conceptually untouched). It changes how fast two previously-sequential phases complete — judge evaluation and goal-result initialization — and both speedups are largest when the bottleneck is network latency (remote APIs / remote backend), and negligible-to-nothing when the bottleneck is local compute or a locked local resource.