Skip to content

Commit 6a60940

Browse files
committed
MLFlow eval
1 parent 56b6ac4 commit 6a60940

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""
2+
MLflow evaluation functions for RCA-Annotator.
3+
4+
Provides functions to download annotation files from jumpbox and log
5+
evaluation results to MLflow.
6+
"""
7+
8+
import argparse
9+
import json
10+
import os
11+
import sys
12+
from pathlib import Path
13+
from typing import Any
14+
15+
import mlflow # type: ignore
16+
from jumpbox_io import download_from_jumpbox
17+
18+
19+
def load_annotation(job_id: str) -> dict[str, Any] | None:
20+
"""
21+
Load annotation_draft.json for a given job_id.
22+
23+
Args:
24+
job_id: Job ID to load annotation for
25+
26+
Returns:
27+
Annotation dict if successful, None on failure
28+
"""
29+
annotation_file = Path(".analysis") / job_id / "annotation_draft.json"
30+
31+
if not annotation_file.exists():
32+
print(f" Error: Annotation file not found: {annotation_file}")
33+
return None
34+
35+
try:
36+
with open(annotation_file) as f:
37+
annotation = json.load(f)
38+
return annotation
39+
except json.JSONDecodeError as e:
40+
print(f" Error: Invalid JSON in {annotation_file}: {e}")
41+
return None
42+
except Exception as e:
43+
print(f" Error: Failed to read {annotation_file}: {e}")
44+
return None
45+
46+
47+
def download_annotations_for_eval(
48+
job_ids: list[str], jumpbox_uri: str | None = None
49+
) -> dict[str, dict[str, Any]]:
50+
"""
51+
Download and load annotations for multiple jobs.
52+
53+
Args:
54+
job_ids: List of job IDs to download
55+
jumpbox_uri: JUMPBOX_URI connection string (defaults to env var)
56+
57+
Returns:
58+
Dict mapping job_id to annotation data
59+
"""
60+
annotations = {}
61+
62+
print(f"Downloading annotations for {len(job_ids)} jobs...")
63+
64+
for i, job_id in enumerate(job_ids, 1):
65+
print(f"\n[{i}/{len(job_ids)}] Job {job_id}")
66+
67+
if download_from_jumpbox(job_id, jumpbox_uri):
68+
annotation = load_annotation(job_id)
69+
if annotation:
70+
annotations[job_id] = annotation
71+
print(" ✓ Loaded annotation")
72+
else:
73+
print(" ✗ Failed to load annotation")
74+
else:
75+
print(" ✗ Download failed")
76+
77+
print(f"\n{'=' * 60}")
78+
print(f"Downloaded {len(annotations)}/{len(job_ids)} annotations")
79+
print(f"{'=' * 60}")
80+
81+
return annotations
82+
83+
84+
def log_annotation_feedback(trace_id: str, annotations: dict[str, Any]) -> None:
85+
"""Log annotation details as MLflow feedback."""
86+
root_cause = annotations.get("root_cause", {})
87+
if root_cause:
88+
mlflow.log_feedback(
89+
trace_id=trace_id,
90+
name="Root Cause",
91+
value=f"Category: {root_cause.get('category')} \n Confidence: {root_cause.get('confidence')}",
92+
rationale=root_cause.get("summary"),
93+
)
94+
95+
for evidence_item in annotations.get("evidence", []):
96+
mlflow.log_feedback(
97+
trace_id=trace_id,
98+
name="Evidence",
99+
value=f"{evidence_item.get('source')}: {evidence_item.get('message')} \n Confidence {evidence_item.get('confidence')}",
100+
)
101+
102+
for recommendation in annotations.get("recommendations", []):
103+
mlflow.log_feedback(
104+
trace_id=trace_id,
105+
name="Recommendation",
106+
value=f"Priority: {recommendation.get('priority')} \n Action: {recommendation.get('action')}",
107+
rationale=f"File: {recommendation.get('file')}",
108+
)
109+
110+
for alt_diagnosis in annotations.get("alternative_diagnoses", []):
111+
mlflow.log_feedback(
112+
trace_id=trace_id,
113+
name="Alternative Diagnosis",
114+
value=f"Category: {alt_diagnosis.get('category')} \n Summary: {alt_diagnosis.get('summary')}",
115+
rationale=alt_diagnosis.get("why_wrong"),
116+
)
117+
118+
for factor in annotations.get("contributing_factors", []):
119+
mlflow.log_feedback(trace_id=trace_id, name="Contributing Factor", value=factor)
120+
121+
for key, value in annotations.get("consistency_check", {}).items():
122+
mlflow.log_feedback(
123+
trace_id=trace_id, name=f"Consistency Check: {key}", value=f"{key}: {value}"
124+
)
125+
126+
127+
def log_expectation(trace_id: str, annotations: dict[str, Any]) -> None:
128+
"""Log expectations for the given annotations."""
129+
human_review = annotations.get("human_review", {})
130+
if human_review:
131+
# Category review
132+
mlflow.log_expectation(
133+
trace_id=trace_id,
134+
name="Human Review",
135+
value=f"Summary accurate: {human_review.get('summary_accurate')} \n Summary comment: {human_review.get('summary_comment')}",
136+
)
137+
138+
# Summary review
139+
mlflow.log_expectation(
140+
trace_id=trace_id,
141+
name="Summary (Human Review)",
142+
value=f"Summary accurate: {human_review.get('summary_accurate')}",
143+
)
144+
145+
# Evidence review
146+
mlflow.log_expectation(
147+
trace_id=trace_id, name="Evidence ", value=human_review.get("evidence_feedback")
148+
)
149+
150+
# Difficulty review
151+
mlflow.log_expectation(
152+
trace_id=trace_id,
153+
name="Difficulty",
154+
value=f"Difficulty appropriate: {human_review.get('difficulty_appropriate')}",
155+
)
156+
157+
# Alternative diagnoses added by human reviewer
158+
for alt_diagnosis in human_review.get("alternative_diagnoses_added", []):
159+
mlflow.log_expectation(
160+
trace_id=trace_id,
161+
name="Alternative Diagnosis Added",
162+
value=f"Category: {alt_diagnosis.get('category')} | Plausibility: {alt_diagnosis.get('plausibility')} \nSummary: {alt_diagnosis.get('summary')}",
163+
)
164+
165+
166+
def evaluate_jobs(job_ids: list[str]) -> None:
167+
"""Run evaluation for the given job IDs."""
168+
tracking_uri = os.environ.get("MLFLOW_TRACKING_URI", "http://localhost:5000")
169+
mlflow.set_tracking_uri(tracking_uri)
170+
experiment_name = os.environ.get("MLFLOW_EXPERIMENT_NAME", "Default")
171+
mlflow.set_experiment(experiment_name)
172+
173+
with mlflow.start_run(run_name="ANNOTATOR_EVALUATION"):
174+
# Create a traced span within the run to ensure linkage
175+
with mlflow.start_span(name="download_annotations") as span:
176+
data = download_annotations_for_eval(job_ids)
177+
trace_id = span.request_id
178+
179+
for job_id in job_ids:
180+
if job_id not in data:
181+
print(f"Warning: No data for job {job_id}")
182+
continue
183+
184+
annotations = data[job_id]
185+
186+
# Log feedback for annotation quality metrics
187+
log_annotation_feedback(trace_id, annotations)
188+
189+
# Log ground truth annotation as expectation
190+
log_expectation(trace_id, annotations)
191+
192+
# Log run params
193+
mlflow.log_param("job_ids", job_ids)
194+
if job_ids and job_ids[0] in data:
195+
mlflow.log_param("annotator", data[job_ids[0]].get("annotator"))
196+
197+
print(f"Traces are {trace_id}")
198+
199+
200+
def main():
201+
parser = argparse.ArgumentParser(description="Evaluate RCA annotations using MLflow.")
202+
parser.add_argument("job_ids", nargs="+", help="List of job IDs to evaluate")
203+
args = parser.parse_args()
204+
evaluate_jobs(args.job_ids)
205+
206+
207+
if __name__ == "__main__":
208+
sys.exit(main())

0 commit comments

Comments
 (0)