agentloop-profiler 0.1.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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
agentloop/events.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from agentloop.operations import operation_kind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def utc_now_iso() -> str:
|
|
12
|
+
return datetime.now(timezone.utc).isoformat()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def new_event_id() -> str:
|
|
16
|
+
return "evt_" + uuid4().hex[:16]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def new_run_id() -> str:
|
|
20
|
+
return "run_" + uuid4().hex[:16]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_exception_detail(exc: BaseException) -> str:
|
|
24
|
+
"""Return useful telemetry text even when an exception has no message."""
|
|
25
|
+
|
|
26
|
+
message = str(exc)
|
|
27
|
+
return message if message.strip() else type(exc).__name__
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AgentEvent:
|
|
32
|
+
event_id: str
|
|
33
|
+
run_id: str
|
|
34
|
+
event_type: str
|
|
35
|
+
name: str
|
|
36
|
+
started_at: str
|
|
37
|
+
ended_at: str
|
|
38
|
+
duration_ms: float
|
|
39
|
+
parent_id: str | None = None
|
|
40
|
+
model: str | None = None
|
|
41
|
+
input_tokens: int = 0
|
|
42
|
+
output_tokens: int = 0
|
|
43
|
+
# Where input_tokens/output_tokens came from. ``None`` means the trace was
|
|
44
|
+
# written before this field existed, which reads back as "unspecified" — not
|
|
45
|
+
# as exact. See agentloop.tokens and docs/TRACE_SCHEMA.md.
|
|
46
|
+
token_provenance: str | None = None
|
|
47
|
+
input_text: str | None = None
|
|
48
|
+
output_text: str | None = None
|
|
49
|
+
status: str = "ok"
|
|
50
|
+
error: str | None = None
|
|
51
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def operation_kind(self) -> str:
|
|
55
|
+
return operation_kind(self)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def total_tokens(self) -> int:
|
|
59
|
+
return self.input_tokens + self.output_tokens
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict[str, Any]:
|
|
62
|
+
return self.__dict__.copy()
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, data: dict[str, Any], *, index: int = 0) -> "AgentEvent":
|
|
66
|
+
"""Build an event from a serialized dict, validating the schema contract.
|
|
67
|
+
|
|
68
|
+
Raises :class:`agentloop.schema.TraceValidationError` (a ``ValueError``)
|
|
69
|
+
for missing, wrongly typed, or out-of-range fields; unknown fields are
|
|
70
|
+
ignored per the documented forward-compatibility policy.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
from agentloop.schema import coerce_event_dict
|
|
74
|
+
|
|
75
|
+
return cls(**coerce_event_dict(data, index=index))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Owned local artifacts; exclusive journal lock and atomic immutable writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from agentloop.experiment_types import canonical
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def local_path(root, relative):
|
|
15
|
+
root = Path(root).resolve()
|
|
16
|
+
target = (root / relative).resolve()
|
|
17
|
+
if root not in target.parents:
|
|
18
|
+
raise ValueError("experiment artifact path escapes its root")
|
|
19
|
+
return target
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_json(path):
|
|
23
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_once(root, relative, value):
|
|
27
|
+
return write_bytes_once(root, relative, (canonical(value) + "\n").encode("utf-8"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_bytes_once(root, relative, encoded):
|
|
31
|
+
path = local_path(root, relative)
|
|
32
|
+
if path.exists():
|
|
33
|
+
if path.read_bytes() != encoded:
|
|
34
|
+
raise ValueError("existing experiment artifact has conflicting content")
|
|
35
|
+
return path
|
|
36
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
temporary = local_path(
|
|
38
|
+
root, str(path.relative_to(Path(root).resolve())) + "." + uuid4().hex + ".tmp"
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
with temporary.open("xb") as handle:
|
|
42
|
+
handle.write(encoded)
|
|
43
|
+
handle.flush()
|
|
44
|
+
os.fsync(handle.fileno())
|
|
45
|
+
# All journal writers hold the same exclusive lock. Existing artifacts
|
|
46
|
+
# are compared above; an interrupted temporary is never a completed record.
|
|
47
|
+
os.replace(temporary, path)
|
|
48
|
+
finally:
|
|
49
|
+
temporary.unlink(missing_ok=True)
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def journal_lock(root):
|
|
55
|
+
root = Path(root).resolve()
|
|
56
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
path = local_path(root, "journal.lock")
|
|
58
|
+
nonce = uuid4().hex
|
|
59
|
+
try:
|
|
60
|
+
with path.open("x", encoding="utf-8") as handle:
|
|
61
|
+
handle.write(nonce)
|
|
62
|
+
handle.flush()
|
|
63
|
+
os.fsync(handle.fileno())
|
|
64
|
+
except FileExistsError:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"experiment journal is busy or requires host lock reconciliation"
|
|
67
|
+
) from None
|
|
68
|
+
failed = False
|
|
69
|
+
try:
|
|
70
|
+
yield
|
|
71
|
+
except BaseException:
|
|
72
|
+
failed = True
|
|
73
|
+
raise
|
|
74
|
+
finally:
|
|
75
|
+
try:
|
|
76
|
+
if path.read_text(encoding="utf-8") == nonce:
|
|
77
|
+
path.unlink()
|
|
78
|
+
except BaseException:
|
|
79
|
+
if not failed:
|
|
80
|
+
raise
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Bounded scalar observations attached to native experiment traces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import Counter
|
|
7
|
+
|
|
8
|
+
from agentloop.context_types import identifier
|
|
9
|
+
from agentloop.experiment_types import owned, reference
|
|
10
|
+
from agentloop.studies import summarize_values
|
|
11
|
+
from agentloop.tracer import current_trace
|
|
12
|
+
|
|
13
|
+
OBSERVATIONS_KEY = "agentloop.experiment_observations"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def observation(value, *, kind="observed", unit="count", source_ref):
|
|
17
|
+
if value is not None and not (
|
|
18
|
+
type(value) in {bool, int, float, str} and (not isinstance(value, str) or len(value) <= 256)
|
|
19
|
+
):
|
|
20
|
+
raise ValueError("experiment observations must be bounded scalars")
|
|
21
|
+
if type(value) in {int, float}:
|
|
22
|
+
try:
|
|
23
|
+
if not math.isfinite(value):
|
|
24
|
+
raise ValueError("experiment observations must be finite")
|
|
25
|
+
except OverflowError:
|
|
26
|
+
raise ValueError("experiment observation is outside the numeric range") from None
|
|
27
|
+
if kind not in {"observed", "estimated", "declared"}:
|
|
28
|
+
raise ValueError("unknown observation kind")
|
|
29
|
+
identifier(unit, "observation unit")
|
|
30
|
+
reference(source_ref, "observation source")
|
|
31
|
+
return {"value": value, "kind": kind, "unit": unit, "source_ref": source_ref}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def record_experiment_observations(values):
|
|
35
|
+
trace = current_trace()
|
|
36
|
+
if trace is None or not isinstance(trace.metadata.get("experiment_id"), str):
|
|
37
|
+
raise ValueError("observations require an active experiment trace")
|
|
38
|
+
if not isinstance(values, dict) or len(values) > 64:
|
|
39
|
+
raise ValueError("observations require a bounded mapping")
|
|
40
|
+
copied = {}
|
|
41
|
+
for key, value in values.items():
|
|
42
|
+
identifier(key, "observation name")
|
|
43
|
+
if not isinstance(value, dict) or set(value) != {"value", "kind", "unit", "source_ref"}:
|
|
44
|
+
raise ValueError("invalid observation fields")
|
|
45
|
+
copied[key] = observation(**value)
|
|
46
|
+
envelope = trace.metadata.setdefault(OBSERVATIONS_KEY, {"schema_version": "1.0", "values": {}})
|
|
47
|
+
if (
|
|
48
|
+
not isinstance(envelope, dict)
|
|
49
|
+
or envelope.get("schema_version") != "1.0"
|
|
50
|
+
or not isinstance(envelope.get("values"), dict)
|
|
51
|
+
):
|
|
52
|
+
raise ValueError("invalid experiment observation envelope")
|
|
53
|
+
if len(set(envelope["values"]) | set(copied)) > 64:
|
|
54
|
+
raise ValueError("experiment observation limit exceeded")
|
|
55
|
+
if any(
|
|
56
|
+
key in envelope["values"] and envelope["values"][key] != value
|
|
57
|
+
for key, value in copied.items()
|
|
58
|
+
):
|
|
59
|
+
raise ValueError("experiment observation cannot be rewritten")
|
|
60
|
+
envelope["values"].update(owned(copied))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def summarize_observations(rows):
|
|
64
|
+
sets = []
|
|
65
|
+
for row in rows:
|
|
66
|
+
raw = (row["trace"] or {}).get("metadata", {}).get(OBSERVATIONS_KEY)
|
|
67
|
+
if raw is None:
|
|
68
|
+
sets.append({})
|
|
69
|
+
continue
|
|
70
|
+
if (
|
|
71
|
+
not isinstance(raw, dict)
|
|
72
|
+
or raw.get("schema_version") != "1.0"
|
|
73
|
+
or not isinstance(raw.get("values"), dict)
|
|
74
|
+
or len(raw["values"]) > 64
|
|
75
|
+
):
|
|
76
|
+
raise ValueError("invalid saved experiment observations")
|
|
77
|
+
values = {}
|
|
78
|
+
for name, item in raw["values"].items():
|
|
79
|
+
identifier(name, "observation name")
|
|
80
|
+
values[name] = observation(**item)
|
|
81
|
+
sets.append(values)
|
|
82
|
+
result = {}
|
|
83
|
+
for name in sorted({key for values in sets for key in values}):
|
|
84
|
+
available = [
|
|
85
|
+
values[name] for values in sets if name in values and values[name]["value"] is not None
|
|
86
|
+
]
|
|
87
|
+
kinds = sorted({item["kind"] for item in available})
|
|
88
|
+
units = sorted({item["unit"] for item in available})
|
|
89
|
+
sources = sorted({item["source_ref"] for item in available})
|
|
90
|
+
row_values = [values.get(name, {}).get("value") for values in sets]
|
|
91
|
+
compatible = len(kinds) == len(units) == len(sources) == 1
|
|
92
|
+
summary = {
|
|
93
|
+
"planned_count": len(rows),
|
|
94
|
+
"known_count": len(available),
|
|
95
|
+
"missing_count": len(rows) - len(available),
|
|
96
|
+
"kinds": kinds,
|
|
97
|
+
"units": units,
|
|
98
|
+
"source_refs": sources,
|
|
99
|
+
"aggregation_status": "compatible" if compatible else "unknown_or_mixed_basis",
|
|
100
|
+
}
|
|
101
|
+
if compatible and all(type(value) is bool for value in row_values if value is not None):
|
|
102
|
+
true_count = sum(value is True for value in row_values)
|
|
103
|
+
quality_true = sum(
|
|
104
|
+
value is True and row["receipt"] is not None and row["receipt"]["quality_passed"]
|
|
105
|
+
for value, row in zip(row_values, rows)
|
|
106
|
+
)
|
|
107
|
+
summary.update(
|
|
108
|
+
true_count=true_count,
|
|
109
|
+
false_count=sum(value is False for value in row_values),
|
|
110
|
+
true_fraction_of_planned=true_count / len(rows),
|
|
111
|
+
quality_preserving_true_count=quality_true,
|
|
112
|
+
quality_preserving_true_fraction_of_planned=quality_true / len(rows),
|
|
113
|
+
)
|
|
114
|
+
elif compatible and all(
|
|
115
|
+
type(value) in {int, float} for value in row_values if value is not None
|
|
116
|
+
):
|
|
117
|
+
summary["values"] = summarize_values(row_values)
|
|
118
|
+
summary["quality_preserving_values"] = summarize_values(
|
|
119
|
+
[
|
|
120
|
+
value
|
|
121
|
+
if row["receipt"] is not None and row["receipt"]["quality_passed"]
|
|
122
|
+
else None
|
|
123
|
+
for value, row in zip(row_values, rows)
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
if units == ["completed_items"] and kinds == ["observed"]:
|
|
127
|
+
rates = []
|
|
128
|
+
for value, row in zip(row_values, rows):
|
|
129
|
+
elapsed = (
|
|
130
|
+
None
|
|
131
|
+
if row["intervention"] is None
|
|
132
|
+
else row["intervention"]["measured"]["candidate"]["runtime_ms"]
|
|
133
|
+
)
|
|
134
|
+
rate = (
|
|
135
|
+
value * 1000 / elapsed
|
|
136
|
+
if value is not None and elapsed is not None and elapsed > 0
|
|
137
|
+
else None
|
|
138
|
+
)
|
|
139
|
+
rates.append(rate if rate is not None and math.isfinite(rate) else None)
|
|
140
|
+
summary["per_second_of_end_to_end_candidate_runtime"] = summarize_values(rates)
|
|
141
|
+
summary["quality_preserving_per_second"] = summarize_values(
|
|
142
|
+
[
|
|
143
|
+
value
|
|
144
|
+
if row["receipt"] is not None and row["receipt"]["quality_passed"]
|
|
145
|
+
else None
|
|
146
|
+
for value, row in zip(rates, rows)
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
elif compatible:
|
|
150
|
+
summary["value_counts"] = dict(
|
|
151
|
+
sorted(Counter(str(value) for value in row_values if value is not None).items())
|
|
152
|
+
)
|
|
153
|
+
summary["quality_pass_counts_by_value"] = dict(
|
|
154
|
+
sorted(
|
|
155
|
+
Counter(
|
|
156
|
+
str(value)
|
|
157
|
+
for value, row in zip(row_values, rows)
|
|
158
|
+
if value is not None
|
|
159
|
+
and row["receipt"] is not None
|
|
160
|
+
and row["receipt"]["quality_passed"]
|
|
161
|
+
).items()
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
summary["quality_fraction_by_value"] = {
|
|
165
|
+
key: summary["quality_pass_counts_by_value"].get(key, 0) / count
|
|
166
|
+
for key, count in summary["value_counts"].items()
|
|
167
|
+
}
|
|
168
|
+
result[name] = summary
|
|
169
|
+
return result
|