feat(e5): audit runner — providers, tolerant parsing, recorded runs - #11
Open
Ajay03299 wants to merge 1 commit into
Open
feat(e5): audit runner — providers, tolerant parsing, recorded runs#11Ajay03299 wants to merge 1 commit into
Ajay03299 wants to merge 1 commit into
Conversation
Completes the E5 harness: corpus -> provider -> parsed response -> JSONL ->
metrics panel. Runs fully offline; live calls are the exception.
e5/providers.py — one structural Protocol, three implementations.
StubProvider emits JSON so stub runs exercise the same parser a live reply
would. ReplayProvider turns one real audit into a permanent offline
fixture, and raises on a missing key rather than defaulting, so a partial
recording cannot masquerade as a complete one. LiveProvider is the only one
that touches the network; it reads its key from the environment, never
accepts a literal, and refuses the [YOUR_KEY_HERE] placeholder.
e5/parsing.py — parses tolerantly and records what it found rather than
accepting only well-formed output. If model A emits clean JSON 99% of the
time and model B 80%, discarding failures compares A's full distribution
against B's tidiest 80%, understating B's dispersion — the quantity E5
measures. Format compliance plausibly correlates with templated advice, so
silent rejection would bias h upward. ParseStatus records ok /
renormalized / partial / ambiguous / no_weights / empty; the decision to
exclude anything is made at analysis time.
Two prose-binding bugs found and fixed during development, both of which
returned plausible wrong answers rather than raising:
- per-ticker proximity search: in "40% in AAPL, 35% in MSFT" the 40%
falls within the window of MSFT and is claimed twice
- greedy nearest-neighbour with a claim set: 35% is nearer AAPL, finds it
taken, and is dropped — MSFT ends with no weight
Both are regression-tested. The corpus prompts now carry an explicit JSON
output contract, so prose parsing is a fallback whose failures are flagged
AMBIGUOUS rather than silently mis-bound; separating "AAPL returned 30%"
from "AAPL 50%" needs semantics, not regex.
e5/runner.py — append-only JSONL keyed on (query_id, model, repeat), so an
interrupted run resumes and a partial run is still analyzable. Provider
failures are recorded and the run continues. Allocations are padded to the
full universe: items ask about ticker subsets, but metrics need a common
basis to stack into a (models, repeats, assets) panel.
.env.example gains OPENAI_KEY and ANTHROPIC_KEY, which retail_ai.py's
docstring already referenced but the template never defined.
47 new tests (28 parsing, 19 runner); 180 total.
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.
Completes the E5 harness on top of the corpus and metrics merged in #10.
Runs fully offline. Live calls are the exception, not the default.
e5/providers.pyOne structural
Protocol, three implementations — so tests need no mocking library.StubProvideremits JSON rather than returning an array directly, so stub runs exercise the same parser a live reply would and still catch parser regressions.ReplayProviderturns one real audit into a permanent offline fixture. A missing key raises instead of falling back to a default: a silent substitution would make a partial recording look complete.LiveProvideris the only one that touches the network. It reads its key from the environment, never accepts a literal, refuses the[YOUR_KEY_HERE]placeholder, and retries transient failures with backoff..env.examplegainsOPENAI_KEYandANTHROPIC_KEY—retail_ai.py's docstring already told users to setOPENAI_KEY, but the template never defined it.e5/parsing.py— why it parses tolerantlyReal replies are prose with a JSON block, or a markdown table, or weights summing to 0.97. The tempting move is to accept only well-formed output, but that biases the measurement: if model A emits clean JSON 99% of the time and model B 80%, discarding failures compares A's full response distribution against B's tidiest 80%, understating B's dispersion — which is precisely what E5 measures. Format compliance also plausibly correlates with templated advice, so silent rejection would push ĥ upward.
So every parse records
raw_text, the extracted allocation, the pre-normalization sum, and aParseStatus(ok/renormalized/partial/ambiguous/no_weights/empty). Exclusion is an analysis-time parameter, not a hard-coded policy.Two bugs found during development, both of which returned plausible wrong answers rather than raising:
"40% in AAPL, 35% in MSFT"the40%falls within the proximity window of MSFT and gets claimed twice.35%is nearer to AAPL than to MSFT, finds AAPL taken, and is discarded, leaving MSFT with no weight.Both produced valid-looking simplex points. Only comparing against a known answer caught them, and both are now regression-tested.
Corpus prompts now carry an explicit JSON output contract, so prose parsing is a fallback. When number and ticker counts disagree — a distractor like
"AAPL returned 30%"alongside the real allocation — the result is flaggedAMBIGUOUSrather than silently mis-bound. Distinguishing a past return from a recommendation needs semantics, not regex, so the honest move is to record low confidence.e5/runner.pyAppend-only JSONL keyed on
(query_id, model, repeat), so an interrupted run resumes and a partial run stays analyzable. Provider failures are recorded witherrorset and the run continues — losing a whole audit to one timeout is worse than a recorded gap.Allocations are padded to the full universe. Items ask about ticker subsets, but
e5.metricsneeds a common basis to stack into the(models, repeats, assets)panel.What stub mode does and doesn't show
A stub-only run validates the pipeline, not the phenomenon. Several archetypes —
PASSIVE_INDEXmost obviously — are hard-coded to equal-weight regardless of input, so market condition can't move them, and a stub run showsspread = 0anddrift = 0by construction. That's the point of a zero-variance baseline, but any ĥ figure from stub mode is an artifact of the stub, not a measurement. Real numbers needLiveProviderand your keys.Suggested first live run: 1 archetype × 1 ticker pair × 3 conditions × 2 models × 5 repeats = 60 calls, a few cents. Enough to confirm live replies parse, and it produces the fixture
ReplayProviderneeds so the run is reproducible offline forever after.Tests
47 new (28 parsing, 19 runner), 180 total on Python 3.13 / macOS arm64.
LiveProvideris tested only for key handling, never over the network.Still open from #8
Per-query vs pooled ĥ determines how many repeats a real run needs per prompt — worth settling before spending on the full corpus.