evalrx 0.1.2__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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Langfuse observability & exporter facade for backward compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from evalrx.observability.tracer import (
|
|
8
|
+
DiagnosticTracer,
|
|
9
|
+
backfill_run_to_langfuse,
|
|
10
|
+
export_to_langfuse_bundle,
|
|
11
|
+
sync_to_langfuse_live,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DiagnosticTracer",
|
|
16
|
+
"backfill_run_to_langfuse",
|
|
17
|
+
"export_to_langfuse_bundle",
|
|
18
|
+
"sync_to_langfuse_live",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
import argparse
|
|
24
|
+
ap = argparse.ArgumentParser(description="Export an EvalRX run into a Langfuse ingestion bundle.")
|
|
25
|
+
ap.add_argument("run_dir", nargs="?", default="outputs", help="Run directory.")
|
|
26
|
+
ap.add_argument("--out", "-o", default=None, help="Output JSON path.")
|
|
27
|
+
ap.add_argument("--sync", action="store_true", help="Push live to Langfuse API.")
|
|
28
|
+
args = ap.parse_args()
|
|
29
|
+
|
|
30
|
+
if args.sync:
|
|
31
|
+
sync_to_langfuse_live(args.run_dir)
|
|
32
|
+
else:
|
|
33
|
+
out_p = args.out or (Path(args.run_dir) / "langfuse_trace.json")
|
|
34
|
+
export_to_langfuse_bundle(args.run_dir, out_p)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Langfuse-backed run reader for the EvalRX HTML report.
|
|
2
|
+
|
|
3
|
+
Langfuse's observations API is row-oriented. A run whose durable events were
|
|
4
|
+
delivered through the outbox queue arrives as one EVENT observation per event
|
|
5
|
+
(``input={"event": {...}}``); this adapter recovers the ordered stream from
|
|
6
|
+
those without making the renderer understand Langfuse-specific response
|
|
7
|
+
types, then re-buckets it into a V2-shaped cache (:func:`materialize`) so the
|
|
8
|
+
normal ``run.json``/``M<n>/log.json`` read path can render it.
|
|
9
|
+
|
|
10
|
+
Caveat: :class:`~evalrx.eval_agent.run_logger_v2.RunLoggerV2` does not
|
|
11
|
+
currently push structured events through that outbox (it relies on live
|
|
12
|
+
spans/generations instead), so a V2 run's recovered events presently carry
|
|
13
|
+
no ``stage`` tag and land in ``run.json``'s ``unrouted`` bucket rather than
|
|
14
|
+
a real ``M<n>/log.json`` — full V2 support here is unfinished, not something
|
|
15
|
+
this module claims to solve.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LangfuseRunSource:
|
|
27
|
+
"""Read EvalRX event envelopes from Langfuse Observations API v2."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, client: Any | None = None) -> None:
|
|
30
|
+
if client is None:
|
|
31
|
+
try:
|
|
32
|
+
from langfuse import Langfuse
|
|
33
|
+
except ImportError as exc:
|
|
34
|
+
raise ImportError(
|
|
35
|
+
"Langfuse report input needs `pip install evalrx[observability]`."
|
|
36
|
+
) from exc
|
|
37
|
+
client = Langfuse()
|
|
38
|
+
self.client = client
|
|
39
|
+
|
|
40
|
+
def events(self, trace_id: str) -> list[dict[str, Any]]:
|
|
41
|
+
"""Return the exact EvalRX events for *trace_id*, ordered by sequence."""
|
|
42
|
+
return [_public_event(event) for event in self._raw_events(trace_id)]
|
|
43
|
+
|
|
44
|
+
def _raw_events(self, trace_id: str) -> list[dict[str, Any]]:
|
|
45
|
+
cursor: str | None = None
|
|
46
|
+
recovered: list[tuple[int, dict[str, Any]]] = []
|
|
47
|
+
# The SDK uses the canonical 32-hex Langfuse trace id. Our local UUID
|
|
48
|
+
# carries dashes, so accept either spelling at the CLI boundary.
|
|
49
|
+
langfuse_trace_id = trace_id.replace("-", "")
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
response = self.client.api.observations.get_many(
|
|
53
|
+
trace_id=langfuse_trace_id,
|
|
54
|
+
fields="io,metadata,time",
|
|
55
|
+
expand_metadata="event_id,event_seq,stage,cycle,artifact_refs",
|
|
56
|
+
limit=1000,
|
|
57
|
+
cursor=cursor,
|
|
58
|
+
)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"Could not query Langfuse observations. Set LANGFUSE_PUBLIC_KEY, "
|
|
62
|
+
"LANGFUSE_SECRET_KEY, and LANGFUSE_HOST (if self-hosted)."
|
|
63
|
+
) from exc
|
|
64
|
+
for observation in response.data:
|
|
65
|
+
event = _event_from_observation(observation)
|
|
66
|
+
if event is not None:
|
|
67
|
+
metadata = _as_dict(getattr(observation, "metadata", None))
|
|
68
|
+
recovered.append((int(metadata.get("event_seq") or event.get("event_seq") or 0), event))
|
|
69
|
+
cursor = getattr(getattr(response, "meta", None), "cursor", None)
|
|
70
|
+
if not cursor:
|
|
71
|
+
break
|
|
72
|
+
recovered.sort(key=lambda item: item[0])
|
|
73
|
+
return [event for _, event in recovered]
|
|
74
|
+
|
|
75
|
+
def materialize(self, trace_id: str, destination: str | Path) -> Path:
|
|
76
|
+
"""Write a renderer-compatible cache; it is not a source of truth."""
|
|
77
|
+
from evalrx.reporting.run_events import write_v2_bundle
|
|
78
|
+
|
|
79
|
+
root = Path(destination)
|
|
80
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
root.chmod(0o700)
|
|
82
|
+
events = self._raw_events(trace_id)
|
|
83
|
+
if not events:
|
|
84
|
+
raise LookupError(f"No EvalRX events found for Langfuse trace {trace_id!r}")
|
|
85
|
+
for event in events:
|
|
86
|
+
self._materialize_artifacts(event, root)
|
|
87
|
+
write_v2_bundle(root, [_public_event(event) for event in events])
|
|
88
|
+
return root
|
|
89
|
+
|
|
90
|
+
def _materialize_artifacts(self, event: dict[str, Any], root: Path) -> None:
|
|
91
|
+
attachments = event.pop("_langfuse_artifacts", [])
|
|
92
|
+
refs = event.pop("_langfuse_artifact_refs", [])
|
|
93
|
+
if not attachments or not refs or not hasattr(self.client, "resolve_media_references"):
|
|
94
|
+
return
|
|
95
|
+
resolved = self.client.resolve_media_references(
|
|
96
|
+
obj=attachments, resolve_with="base64_data_uri",
|
|
97
|
+
)
|
|
98
|
+
ref_by_id = {str(ref.get("artifact_id")): ref for ref in refs if isinstance(ref, dict)}
|
|
99
|
+
for attachment in resolved:
|
|
100
|
+
if not isinstance(attachment, dict):
|
|
101
|
+
continue
|
|
102
|
+
ref = ref_by_id.get(str(attachment.get("artifact_id")))
|
|
103
|
+
content = attachment.get("content")
|
|
104
|
+
if ref is None or not isinstance(content, str):
|
|
105
|
+
continue
|
|
106
|
+
_write_data_uri(root, str(ref.get("path") or ""), content)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _event_from_observation(observation: Any) -> dict[str, Any] | None:
|
|
110
|
+
"""Extract our payload from an ObservationV2 or a lightweight test double."""
|
|
111
|
+
input_data = _as_dict(getattr(observation, "input", None))
|
|
112
|
+
event = input_data.get("event")
|
|
113
|
+
if not isinstance(event, dict):
|
|
114
|
+
return None
|
|
115
|
+
result = dict(event)
|
|
116
|
+
attachments = input_data.get("artifacts")
|
|
117
|
+
metadata = _as_dict(getattr(observation, "metadata", None))
|
|
118
|
+
refs = metadata.get("artifact_refs")
|
|
119
|
+
if isinstance(attachments, list):
|
|
120
|
+
result["_langfuse_artifacts"] = attachments
|
|
121
|
+
if isinstance(refs, list):
|
|
122
|
+
result["_langfuse_artifact_refs"] = refs
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
127
|
+
if isinstance(value, dict):
|
|
128
|
+
return value
|
|
129
|
+
if isinstance(value, str):
|
|
130
|
+
try:
|
|
131
|
+
parsed = json.loads(value)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
return {}
|
|
134
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _public_event(event: dict[str, Any]) -> dict[str, Any]:
|
|
139
|
+
return {key: value for key, value in event.items() if not key.startswith("_langfuse_")}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _write_data_uri(root: Path, relative_path: str, data_uri: str) -> None:
|
|
143
|
+
"""Write a resolved Langfuse media value inside the renderer cache only."""
|
|
144
|
+
if not data_uri.startswith("data:") or "," not in data_uri:
|
|
145
|
+
return
|
|
146
|
+
target = (root / relative_path).resolve()
|
|
147
|
+
if root.resolve() not in target.parents:
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
payload = base64.b64decode(data_uri.split(",", 1)[1])
|
|
151
|
+
except ValueError:
|
|
152
|
+
return
|
|
153
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
target.write_bytes(payload)
|
|
155
|
+
target.chmod(0o600)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Structured diagnostic report model for claim-first dashboards.
|
|
2
|
+
|
|
3
|
+
The raw EvalRX artifacts are optimized for auditability: JSONL events,
|
|
4
|
+
candidate signal dicts, CSV tables, and rendered chart files. This module
|
|
5
|
+
defines the semantic layer above those artifacts. The dashboard should render
|
|
6
|
+
this model instead of re-interpreting raw logs ad hoc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
ClaimStatus = Literal["supported", "inconclusive", "refuted", "descriptive"]
|
|
15
|
+
Confidence = Literal["high", "medium", "low", "unknown"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Evidence:
|
|
20
|
+
id: str
|
|
21
|
+
kind: str
|
|
22
|
+
title: str
|
|
23
|
+
summary: str = ""
|
|
24
|
+
artifact: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"id": self.id,
|
|
29
|
+
"kind": self.kind,
|
|
30
|
+
"title": self.title,
|
|
31
|
+
"summary": self.summary,
|
|
32
|
+
"artifact": self.artifact,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Claim:
|
|
38
|
+
id: str
|
|
39
|
+
text: str
|
|
40
|
+
status: ClaimStatus
|
|
41
|
+
evidence_ids: list[str] = field(default_factory=list)
|
|
42
|
+
counter_evidence_ids: list[str] = field(default_factory=list)
|
|
43
|
+
interpretation: str = ""
|
|
44
|
+
do_not_infer: str = ""
|
|
45
|
+
downstream: list[str] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"id": self.id,
|
|
50
|
+
"text": self.text,
|
|
51
|
+
"status": self.status,
|
|
52
|
+
"evidence_ids": self.evidence_ids,
|
|
53
|
+
"counter_evidence_ids": self.counter_evidence_ids,
|
|
54
|
+
"interpretation": self.interpretation,
|
|
55
|
+
"do_not_infer": self.do_not_infer,
|
|
56
|
+
"downstream": self.downstream,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ReportStep:
|
|
62
|
+
stage: str
|
|
63
|
+
title: str
|
|
64
|
+
summary: str
|
|
65
|
+
artifact_ids: list[str] = field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"stage": self.stage,
|
|
70
|
+
"title": self.title,
|
|
71
|
+
"summary": self.summary,
|
|
72
|
+
"artifact_ids": self.artifact_ids,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class DiagnosticReport:
|
|
78
|
+
question: str = ""
|
|
79
|
+
answer: str = ""
|
|
80
|
+
confidence: Confidence = "unknown"
|
|
81
|
+
claims: list[Claim] = field(default_factory=list)
|
|
82
|
+
evidence: list[Evidence] = field(default_factory=list)
|
|
83
|
+
timeline: list[ReportStep] = field(default_factory=list)
|
|
84
|
+
visual_decisions: list[dict[str, Any]] = field(default_factory=list)
|
|
85
|
+
chart_readings: list[dict[str, Any]] = field(default_factory=list)
|
|
86
|
+
dashboard_storyboard: list[dict[str, Any]] = field(default_factory=list)
|
|
87
|
+
critique: list[str] = field(default_factory=list)
|
|
88
|
+
caveats: list[str] = field(default_factory=list)
|
|
89
|
+
next_actions: list[str] = field(default_factory=list)
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"question": self.question,
|
|
94
|
+
"answer": self.answer,
|
|
95
|
+
"confidence": self.confidence,
|
|
96
|
+
"claims": [c.to_dict() for c in self.claims],
|
|
97
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
98
|
+
"timeline": [s.to_dict() for s in self.timeline],
|
|
99
|
+
"visual_decisions": self.visual_decisions,
|
|
100
|
+
"chart_readings": self.chart_readings,
|
|
101
|
+
"dashboard_storyboard": self.dashboard_storyboard,
|
|
102
|
+
"critique": self.critique,
|
|
103
|
+
"caveats": self.caveats,
|
|
104
|
+
"next_actions": self.next_actions,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class ReaderFinding:
|
|
110
|
+
"""One claim phrased for a reader who does not know the pipeline."""
|
|
111
|
+
|
|
112
|
+
title: str
|
|
113
|
+
summary: str
|
|
114
|
+
why_it_matters: str
|
|
115
|
+
evidence_level: str
|
|
116
|
+
limitation: str = ""
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> dict[str, str]:
|
|
119
|
+
return {
|
|
120
|
+
"title": self.title,
|
|
121
|
+
"summary": self.summary,
|
|
122
|
+
"why_it_matters": self.why_it_matters,
|
|
123
|
+
"evidence_level": self.evidence_level,
|
|
124
|
+
"limitation": self.limitation,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class ReaderReport:
|
|
130
|
+
"""Stable, audience-first view model rendered before technical evidence."""
|
|
131
|
+
|
|
132
|
+
headline: str
|
|
133
|
+
question: str
|
|
134
|
+
answer: str
|
|
135
|
+
confidence: str
|
|
136
|
+
what_we_did: list[str] = field(default_factory=list)
|
|
137
|
+
key_findings: list[ReaderFinding] = field(default_factory=list)
|
|
138
|
+
open_questions: list[str] = field(default_factory=list)
|
|
139
|
+
next_steps: list[str] = field(default_factory=list)
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict[str, Any]:
|
|
142
|
+
return {
|
|
143
|
+
"headline": self.headline,
|
|
144
|
+
"question": self.question,
|
|
145
|
+
"answer": self.answer,
|
|
146
|
+
"confidence": self.confidence,
|
|
147
|
+
"what_we_did": self.what_we_did,
|
|
148
|
+
"key_findings": [finding.to_dict() for finding in self.key_findings],
|
|
149
|
+
"open_questions": self.open_questions,
|
|
150
|
+
"next_steps": self.next_steps,
|
|
151
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Read tidy RunLoggerV2 documents through the existing event-view contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_STAGES = ("M1", "M2", "M3", "M4", "M5")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_v2_root(root: str | Path) -> Path | None:
|
|
13
|
+
"""Return the directory containing V2 ``run.json`` and stage folders."""
|
|
14
|
+
root = Path(root).resolve()
|
|
15
|
+
candidates = (root, root / "logs")
|
|
16
|
+
for candidate in candidates:
|
|
17
|
+
if (candidate / "run.json").is_file() and any(
|
|
18
|
+
(candidate / stage / "log.json").is_file() for stage in _STAGES
|
|
19
|
+
):
|
|
20
|
+
return candidate
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_v2_events(root: str | Path) -> list[dict[str, Any]]:
|
|
25
|
+
"""Flatten V2 bucketed documents into the legacy read-only event view.
|
|
26
|
+
|
|
27
|
+
The V2 files remain authoritative and are never rewritten by this adapter.
|
|
28
|
+
It exists so current report/UI code can migrate independently from the
|
|
29
|
+
storage layout.
|
|
30
|
+
"""
|
|
31
|
+
v2_root = resolve_v2_root(root)
|
|
32
|
+
if v2_root is None:
|
|
33
|
+
return []
|
|
34
|
+
try:
|
|
35
|
+
run = json.loads((v2_root / "run.json").read_text(encoding="utf-8"))
|
|
36
|
+
except (OSError, json.JSONDecodeError):
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
trace_id = str((run.get("run_start") or {}).get("trace_id") or run.get("trace_id") or "")
|
|
40
|
+
events: list[dict[str, Any]] = []
|
|
41
|
+
|
|
42
|
+
def append(event: str, payload: Any, *, stage: str = "RUN") -> None:
|
|
43
|
+
if not isinstance(payload, dict):
|
|
44
|
+
return
|
|
45
|
+
record = dict(payload)
|
|
46
|
+
record.setdefault("event", event)
|
|
47
|
+
record.setdefault("stage", stage)
|
|
48
|
+
# Older V2 bundles encode this only in the folder name. Existing
|
|
49
|
+
# case-study/report consumers still use the V1 module discriminator.
|
|
50
|
+
if event == "surgery":
|
|
51
|
+
record.setdefault("module", stage.lower())
|
|
52
|
+
elif event == "fix":
|
|
53
|
+
record.setdefault("module", "fix")
|
|
54
|
+
if trace_id:
|
|
55
|
+
record.setdefault("trace_id", trace_id)
|
|
56
|
+
events.append(record)
|
|
57
|
+
|
|
58
|
+
append("run_start", run.get("run_start"))
|
|
59
|
+
for row in run.get("cases") or []:
|
|
60
|
+
append("case_record", row)
|
|
61
|
+
for key in (
|
|
62
|
+
"report_published", "diagnose_reports", "loop_end",
|
|
63
|
+
"agent_decisions", "agent_tool_calls", "unrouted",
|
|
64
|
+
):
|
|
65
|
+
event_name = {
|
|
66
|
+
"diagnose_reports": "diagnose_report",
|
|
67
|
+
"agent_decisions": "agent_decision",
|
|
68
|
+
"agent_tool_calls": "agent_tool",
|
|
69
|
+
"unrouted": "unrouted",
|
|
70
|
+
}.get(key, key)
|
|
71
|
+
for row in run.get(key) or []:
|
|
72
|
+
append(event_name, row)
|
|
73
|
+
|
|
74
|
+
for stage in _STAGES:
|
|
75
|
+
path = v2_root / stage / "log.json"
|
|
76
|
+
if not path.is_file():
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
doc = json.loads(path.read_text(encoding="utf-8"))
|
|
80
|
+
except (OSError, json.JSONDecodeError):
|
|
81
|
+
continue
|
|
82
|
+
if not isinstance(doc, dict):
|
|
83
|
+
continue
|
|
84
|
+
for event, rows in doc.items():
|
|
85
|
+
if not isinstance(rows, list):
|
|
86
|
+
continue
|
|
87
|
+
for row in rows:
|
|
88
|
+
append("model_call" if event == "model_calls" else event, row, stage=stage)
|
|
89
|
+
|
|
90
|
+
# New bundles persist a global sequence at write time, even when calls
|
|
91
|
+
# finish concurrently or the wall clock moves backwards. Legacy bundles
|
|
92
|
+
# retain timestamp ordering; never replace a persisted event identity.
|
|
93
|
+
if all(isinstance(event.get("event_seq"), int) for event in events):
|
|
94
|
+
events.sort(key=lambda event: event["event_seq"])
|
|
95
|
+
else:
|
|
96
|
+
events.sort(key=lambda event: str(event.get("ts") or ""))
|
|
97
|
+
seq = max((event.get("event_seq", 0) for event in events), default=0)
|
|
98
|
+
for event in events:
|
|
99
|
+
if "event_seq" not in event:
|
|
100
|
+
seq += 1
|
|
101
|
+
event["event_seq"] = seq
|
|
102
|
+
return events
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Inverse of the RUN-stage bucket mapping `read_v2_events` flattens — event
|
|
106
|
+
# name -> run.json key. These names are always RUN-level regardless of any
|
|
107
|
+
# "stage" tag on the recovered event.
|
|
108
|
+
_RUN_EVENT_TO_KEY = {
|
|
109
|
+
"case_record": "cases",
|
|
110
|
+
"diagnose_report": "diagnose_reports",
|
|
111
|
+
"agent_decision": "agent_decisions",
|
|
112
|
+
"agent_tool": "agent_tool_calls",
|
|
113
|
+
"report_published": "report_published",
|
|
114
|
+
"loop_end": "loop_end",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# A source that recovers events out-of-band (e.g. Langfuse observations
|
|
118
|
+
# reconstructed from a durable-outbox delivery, which predates per-event
|
|
119
|
+
# "stage" tagging) may hand back events with no "stage" field at all. Route
|
|
120
|
+
# by the event's own name to its canonical stage rather than dumping it into
|
|
121
|
+
# run.json's "unrouted" bucket, where `resolve_v2_root` would never find it
|
|
122
|
+
# (it requires at least one real M<n>/log.json to recognize a V2 root).
|
|
123
|
+
_NAME_TO_STAGE = {
|
|
124
|
+
"probe": "M1",
|
|
125
|
+
"analysis": "M2", "explore": "M2",
|
|
126
|
+
"diagnosis": "M3",
|
|
127
|
+
"experiment": "M5", # module=... on the row disambiguates M4 vs M5 below
|
|
128
|
+
"fix": "M5",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fallback_stage(name: "str | None", row: dict[str, Any]) -> "str | None":
|
|
133
|
+
"""Best-effort stage for an event with no "stage" tag of its own."""
|
|
134
|
+
if name == "surgery" or name == "tool_codegen" or name == "experiment":
|
|
135
|
+
module = str(row.get("module") or "").lower()
|
|
136
|
+
if module.startswith("m") and module[1:].isdigit():
|
|
137
|
+
return f"M{module[1:]}"
|
|
138
|
+
return "M5" if name == "experiment" else "M4"
|
|
139
|
+
return _NAME_TO_STAGE.get(name or "")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def write_v2_bundle(root: str | Path, events: list[dict[str, Any]]) -> None:
|
|
143
|
+
"""Reconstruct ``run.json`` + ``M<n>/log.json`` from a flat event list.
|
|
144
|
+
|
|
145
|
+
The exact inverse of :func:`read_v2_events`'s bucketing, for callers that
|
|
146
|
+
recover events from an out-of-band source (e.g. Langfuse) and need to
|
|
147
|
+
write a V2-shaped cache directory back for the report/dashboard's
|
|
148
|
+
normal V2-only read path to find.
|
|
149
|
+
"""
|
|
150
|
+
root = Path(root)
|
|
151
|
+
run_doc: dict[str, Any] = {}
|
|
152
|
+
stage_docs: dict[str, dict[str, list[dict[str, Any]]]] = {}
|
|
153
|
+
|
|
154
|
+
for event in events:
|
|
155
|
+
row = {k: v for k, v in event.items() if k not in ("event", "stage")}
|
|
156
|
+
stage = event.get("stage")
|
|
157
|
+
name = event.get("event")
|
|
158
|
+
if name == "run_start":
|
|
159
|
+
run_doc["run_start"] = row
|
|
160
|
+
continue
|
|
161
|
+
if name in _RUN_EVENT_TO_KEY:
|
|
162
|
+
run_doc.setdefault(_RUN_EVENT_TO_KEY[name], []).append(row)
|
|
163
|
+
continue
|
|
164
|
+
if stage not in _STAGES:
|
|
165
|
+
stage = _fallback_stage(name, row) or stage
|
|
166
|
+
if stage not in _STAGES:
|
|
167
|
+
run_doc.setdefault("unrouted", []).append(row)
|
|
168
|
+
continue
|
|
169
|
+
key = "model_calls" if name == "model_call" else (name or "unrouted")
|
|
170
|
+
stage_docs.setdefault(stage, {}).setdefault(key, []).append(row)
|
|
171
|
+
|
|
172
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
(root / "run.json").write_text(
|
|
174
|
+
json.dumps(run_doc, ensure_ascii=False, default=str), encoding="utf-8",
|
|
175
|
+
)
|
|
176
|
+
for stage, doc in stage_docs.items():
|
|
177
|
+
stage_dir = root / stage
|
|
178
|
+
stage_dir.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
(stage_dir / "log.json").write_text(
|
|
180
|
+
json.dumps(doc, ensure_ascii=False, default=str), encoding="utf-8",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
__all__ = ["read_v2_events", "resolve_v2_root", "write_v2_bundle"]
|