iints-sdk-python35 1.5.26__py3-none-any.whl → 1.5.30__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.
- iints/__init__.py +1 -1
- iints/ai/backends/ollama.py +8 -9
- iints/analysis/run_quality.py +475 -0
- iints/cli/cli.py +32 -14
- iints/core/formula_registry.py +20 -11
- iints/core/patient/advanced_metabolic_model.py +12 -2
- iints/core/patient/bergman_model.py +8 -2
- iints/core/patient/hovorka_model.py +8 -6
- iints/core/patient/patient_factory.py +48 -9
- iints/core/simulator.py +4 -3
- iints/core/supervisor.py +6 -2
- iints/data/virtual_patients/reference_free_living_t1d.yaml +2 -2
- iints/governance/__init__.py +17 -0
- iints/governance/research_policy.py +199 -0
- iints/research/alphafold_engine.py +23 -13
- iints/research/genomics_engine.py +71 -23
- iints/research/stem_cell_optimizer.py +147 -26
- iints/research/stem_cell_transplant.py +370 -0
- iints/research/tissue_stressor.py +70 -40
- iints/validation/schemas.py +6 -0
- iints/visualization/cockpit.py +8 -1
- iints_desktop/evidence_connectors.py +173 -0
- iints_desktop/local_ai.py +17 -2
- iints_desktop/qt_app.py +155 -67
- iints_desktop/tauri_bridge.py +439 -0
- iints_desktop/terminal_utils.py +51 -25
- iints_desktop/update.py +80 -0
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/METADATA +16 -12
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/RECORD +35 -29
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/WHEEL +1 -1
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/entry_points.txt +1 -0
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/licenses/LICENSE +0 -0
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/licenses/LICENSE-MIT-IINTS-LEGACY +0 -0
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/licenses/NOTICE +0 -0
- {iints_sdk_python35-1.5.26.dist-info → iints_sdk_python35-1.5.30.dist-info}/top_level.txt +0 -0
iints/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ except ImportError: # pragma: no cover - Python < 3.8 fallback
|
|
|
11
11
|
try:
|
|
12
12
|
__version__ = version("iints-sdk-python35")
|
|
13
13
|
except PackageNotFoundError: # pragma: no cover - source tree fallback
|
|
14
|
-
__version__ = "1.5.
|
|
14
|
+
__version__ = "1.5.30"
|
|
15
15
|
|
|
16
16
|
# Note to developers: this SDK is currently maintained by a single author.
|
|
17
17
|
# Please report bugs via GitHub issues and feel free to contribute fixes via PRs.
|
iints/ai/backends/ollama.py
CHANGED
|
@@ -234,15 +234,14 @@ class OllamaBackend:
|
|
|
234
234
|
resolved = installed_lookup.get(alias.lower())
|
|
235
235
|
if resolved is not None:
|
|
236
236
|
return resolved
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return installed_name
|
|
237
|
+
for installed_name in installed:
|
|
238
|
+
lowered = installed_name.lower()
|
|
239
|
+
if "ministral-3" in lowered and "8b" in lowered:
|
|
240
|
+
return installed_name
|
|
241
|
+
for installed_name in installed:
|
|
242
|
+
lowered = installed_name.lower()
|
|
243
|
+
if "ministral" in lowered and "8b" in lowered:
|
|
244
|
+
return installed_name
|
|
246
245
|
|
|
247
246
|
return None
|
|
248
247
|
|
iints/analysis/run_quality.py
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Any, Dict, Optional
|
|
5
7
|
|
|
6
8
|
import pandas as pd
|
|
7
9
|
|
|
10
|
+
from iints.ai.backends.ollama import DEFAULT_MINISTRAL_MODEL, OllamaBackend
|
|
8
11
|
from iints.analysis.safety_visualizer import write_safety_visualizer
|
|
9
12
|
from iints.data.realism_dashboard import write_realism_dashboard
|
|
10
13
|
from iints.data.realism_validator import validate_realism_dataset, write_realism_report
|
|
14
|
+
from iints.governance import RESEARCH_ONLY_NOTICE, guard_ai_output
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CORE_RESULT_COLUMNS = (
|
|
18
|
+
"time_minutes",
|
|
19
|
+
"glucose_actual_mgdl",
|
|
20
|
+
"delivered_insulin_units",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
LOCAL_AI_REVIEW_ENV = "IINTS_LOCAL_AI_REVIEW"
|
|
24
|
+
LOCAL_AI_REVIEW_MODEL_ENV = "IINTS_LOCAL_AI_MODEL"
|
|
25
|
+
LOCAL_AI_REVIEW_TIMEOUT_ENV = "IINTS_LOCAL_AI_REVIEW_TIMEOUT_SECONDS"
|
|
11
26
|
|
|
12
27
|
|
|
13
28
|
def _series_or_default(df: pd.DataFrame, column: str, default: float = 0.0) -> pd.Series:
|
|
@@ -53,6 +68,7 @@ def _write_run_quality_markdown(
|
|
|
53
68
|
realism_report: Any,
|
|
54
69
|
selected_reference: str | None,
|
|
55
70
|
reference_selection: str | None,
|
|
71
|
+
quality_summary: Dict[str, Any],
|
|
56
72
|
) -> Path:
|
|
57
73
|
failed = [check for check in realism_report.checks if check.status == "failed"]
|
|
58
74
|
warnings = [check for check in realism_report.checks if check.status == "warning"]
|
|
@@ -65,6 +81,8 @@ def _write_run_quality_markdown(
|
|
|
65
81
|
"",
|
|
66
82
|
f"- Verdict: `{realism_report.verdict}`",
|
|
67
83
|
f"- Realism score: `{realism_report.realism_score:.2f}`",
|
|
84
|
+
f"- Result quality grade: `{quality_summary['grade']}`",
|
|
85
|
+
f"- Result quality score: `{quality_summary['score']:.1f}/100`",
|
|
68
86
|
f"- Reference selection: `{reference_selection}`",
|
|
69
87
|
f"- Applied reference: `{selected_reference or 'none'}`",
|
|
70
88
|
f"- Mean glucose: `{realism_report.metrics.get('mean_glucose_mgdl')} mg/dL`",
|
|
@@ -77,6 +95,11 @@ def _write_run_quality_markdown(
|
|
|
77
95
|
realism_report.summary,
|
|
78
96
|
"",
|
|
79
97
|
]
|
|
98
|
+
if quality_summary["review_reasons"]:
|
|
99
|
+
lines.extend(["## Result Quality Gate", ""])
|
|
100
|
+
for reason in quality_summary["review_reasons"]:
|
|
101
|
+
lines.append(f"- {reason}")
|
|
102
|
+
lines.append("")
|
|
80
103
|
if failed or warnings:
|
|
81
104
|
lines.extend(["## Review Items", ""])
|
|
82
105
|
for check in failed + warnings:
|
|
@@ -106,6 +129,415 @@ def _write_run_quality_markdown(
|
|
|
106
129
|
return output_path
|
|
107
130
|
|
|
108
131
|
|
|
132
|
+
def _count_safety_interventions(results_df: pd.DataFrame, safety_report: Optional[Dict[str, Any]]) -> int:
|
|
133
|
+
if safety_report:
|
|
134
|
+
for key in (
|
|
135
|
+
"bolus_interventions_count",
|
|
136
|
+
"interventions_count",
|
|
137
|
+
"safety_interventions_count",
|
|
138
|
+
"total_overrides",
|
|
139
|
+
):
|
|
140
|
+
if key in safety_report:
|
|
141
|
+
try:
|
|
142
|
+
return int(safety_report[key])
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
pass
|
|
145
|
+
if "safety_triggered" not in results_df.columns:
|
|
146
|
+
return 0
|
|
147
|
+
return int(results_df["safety_triggered"].fillna(False).astype(bool).sum())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _routine_safety_adjustment_mask(results_df: pd.DataFrame) -> pd.Series:
|
|
151
|
+
"""Identify routine deterministic dose shaping, not hard safety failures."""
|
|
152
|
+
|
|
153
|
+
if "safety_reason" not in results_df.columns:
|
|
154
|
+
return pd.Series([False] * len(results_df), index=results_df.index)
|
|
155
|
+
reasons = results_df["safety_reason"].fillna("").astype(str)
|
|
156
|
+
return (
|
|
157
|
+
reasons.str.startswith("PD_STACKING_PREVENTION")
|
|
158
|
+
| reasons.str.startswith("PD_CLEARANCE_LIMIT")
|
|
159
|
+
| reasons.str.startswith("WARNING: Hyperglycemia detected")
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _count_routine_safety_adjustments(results_df: pd.DataFrame) -> int:
|
|
164
|
+
if "safety_triggered" not in results_df.columns:
|
|
165
|
+
return 0
|
|
166
|
+
triggered = results_df["safety_triggered"].fillna(False).astype(bool)
|
|
167
|
+
return int((triggered & _routine_safety_adjustment_mask(results_df)).sum())
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _count_material_safety_interventions(results_df: pd.DataFrame) -> int:
|
|
171
|
+
if "safety_triggered" not in results_df.columns:
|
|
172
|
+
return 0
|
|
173
|
+
triggered = results_df["safety_triggered"].fillna(False).astype(bool)
|
|
174
|
+
routine = _routine_safety_adjustment_mask(results_df)
|
|
175
|
+
return int((triggered & ~routine).sum())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def build_result_quality_summary(
|
|
179
|
+
results_df: pd.DataFrame,
|
|
180
|
+
*,
|
|
181
|
+
realism_report: Any,
|
|
182
|
+
safety_report: Optional[Dict[str, Any]] = None,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""Build a deterministic reviewer-facing quality gate for one run."""
|
|
185
|
+
review_reasons: list[str] = []
|
|
186
|
+
score = float(realism_report.realism_score) * 100.0
|
|
187
|
+
|
|
188
|
+
missing_columns = [column for column in CORE_RESULT_COLUMNS if column not in results_df.columns]
|
|
189
|
+
if missing_columns:
|
|
190
|
+
review_reasons.append(f"Missing core result columns: {', '.join(missing_columns)}.")
|
|
191
|
+
score -= 25.0
|
|
192
|
+
|
|
193
|
+
glucose = pd.to_numeric(results_df.get("glucose_actual_mgdl", pd.Series(dtype=float)), errors="coerce")
|
|
194
|
+
nan_glucose_rows = int(glucose.isna().sum())
|
|
195
|
+
if nan_glucose_rows:
|
|
196
|
+
review_reasons.append(f"{nan_glucose_rows} glucose row(s) are NaN or non-numeric.")
|
|
197
|
+
score -= min(20.0, nan_glucose_rows * 2.0)
|
|
198
|
+
|
|
199
|
+
timestamps = pd.to_numeric(results_df.get("time_minutes", pd.Series(dtype=float)), errors="coerce")
|
|
200
|
+
duplicate_timestamp_rows = int(timestamps.duplicated().sum()) if not timestamps.empty else 0
|
|
201
|
+
if duplicate_timestamp_rows:
|
|
202
|
+
review_reasons.append(f"{duplicate_timestamp_rows} duplicate timestamp row(s) detected.")
|
|
203
|
+
score -= min(15.0, duplicate_timestamp_rows * 1.5)
|
|
204
|
+
|
|
205
|
+
terminated_early = bool((safety_report or {}).get("terminated_early", False))
|
|
206
|
+
if terminated_early:
|
|
207
|
+
review_reasons.append("Simulation terminated early.")
|
|
208
|
+
score -= 35.0
|
|
209
|
+
|
|
210
|
+
intervention_count = _count_safety_interventions(results_df, safety_report)
|
|
211
|
+
routine_adjustment_count = _count_routine_safety_adjustments(results_df)
|
|
212
|
+
material_intervention_count = _count_material_safety_interventions(results_df)
|
|
213
|
+
if material_intervention_count:
|
|
214
|
+
review_reasons.append(f"{material_intervention_count} material safety intervention(s) occurred.")
|
|
215
|
+
score -= min(20.0, material_intervention_count * 0.75)
|
|
216
|
+
if routine_adjustment_count:
|
|
217
|
+
review_reasons.append(
|
|
218
|
+
f"{routine_adjustment_count} routine pharmacodynamic dose-shaping adjustment(s) occurred."
|
|
219
|
+
)
|
|
220
|
+
score -= min(4.0, routine_adjustment_count * 0.02)
|
|
221
|
+
|
|
222
|
+
fail_soft_count = int((safety_report or {}).get("input_validator_fail_soft_count", 0) or 0)
|
|
223
|
+
if fail_soft_count:
|
|
224
|
+
review_reasons.append(f"{fail_soft_count} input-validator fail-soft event(s) occurred.")
|
|
225
|
+
score -= min(20.0, fail_soft_count * 2.0)
|
|
226
|
+
|
|
227
|
+
if realism_report.verdict == "likely_unrealistic":
|
|
228
|
+
review_reasons.append("Realism verdict is likely_unrealistic.")
|
|
229
|
+
score -= 30.0
|
|
230
|
+
elif realism_report.verdict == "needs_review":
|
|
231
|
+
review_reasons.append("Realism verdict needs_review.")
|
|
232
|
+
score -= 10.0
|
|
233
|
+
|
|
234
|
+
score = max(0.0, min(100.0, score))
|
|
235
|
+
if terminated_early or missing_columns or realism_report.verdict == "likely_unrealistic":
|
|
236
|
+
grade = "do_not_use"
|
|
237
|
+
elif score < 75.0 or realism_report.verdict == "needs_review" or fail_soft_count:
|
|
238
|
+
grade = "review_before_use"
|
|
239
|
+
else:
|
|
240
|
+
grade = "research_ready"
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"grade": grade,
|
|
244
|
+
"score": round(score, 3),
|
|
245
|
+
"review_reasons": review_reasons,
|
|
246
|
+
"row_count": int(len(results_df)),
|
|
247
|
+
"missing_columns": missing_columns,
|
|
248
|
+
"nan_glucose_rows": nan_glucose_rows,
|
|
249
|
+
"duplicate_timestamp_rows": duplicate_timestamp_rows,
|
|
250
|
+
"safety_intervention_count": intervention_count,
|
|
251
|
+
"material_safety_intervention_count": material_intervention_count,
|
|
252
|
+
"routine_dose_shaping_count": routine_adjustment_count,
|
|
253
|
+
"input_validator_fail_soft_count": fail_soft_count,
|
|
254
|
+
"terminated_early": terminated_early,
|
|
255
|
+
"realism_verdict": realism_report.verdict,
|
|
256
|
+
"realism_score": float(realism_report.realism_score),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _json_safe(value: Any) -> Any:
|
|
261
|
+
if isinstance(value, dict):
|
|
262
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
263
|
+
if isinstance(value, (list, tuple, set)):
|
|
264
|
+
return [_json_safe(item) for item in value]
|
|
265
|
+
if isinstance(value, Path):
|
|
266
|
+
return str(value)
|
|
267
|
+
if hasattr(value, "item"):
|
|
268
|
+
try:
|
|
269
|
+
return _json_safe(value.item())
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
try:
|
|
273
|
+
if pd.isna(value):
|
|
274
|
+
return None
|
|
275
|
+
except (TypeError, ValueError):
|
|
276
|
+
pass
|
|
277
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
278
|
+
return value
|
|
279
|
+
return str(value)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _resolve_local_ai_review_mode(local_ai_review: bool | str | None) -> str:
|
|
283
|
+
raw_value: bool | str | None = local_ai_review
|
|
284
|
+
if raw_value is None:
|
|
285
|
+
raw_value = os.getenv(LOCAL_AI_REVIEW_ENV, "auto")
|
|
286
|
+
if isinstance(raw_value, bool):
|
|
287
|
+
return "auto" if raw_value else "off"
|
|
288
|
+
normalized = str(raw_value).strip().lower()
|
|
289
|
+
if normalized in {"1", "true", "yes", "on", "enabled"}:
|
|
290
|
+
return "auto"
|
|
291
|
+
if normalized in {"0", "false", "no", "off", "disabled", "skip"}:
|
|
292
|
+
return "off"
|
|
293
|
+
if normalized in {"required", "force", "strict"}:
|
|
294
|
+
return "required"
|
|
295
|
+
if normalized == "auto":
|
|
296
|
+
return "auto"
|
|
297
|
+
return "auto"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _local_ai_timeout_seconds(default: float) -> float:
|
|
301
|
+
raw_value = os.getenv(LOCAL_AI_REVIEW_TIMEOUT_ENV)
|
|
302
|
+
if not raw_value:
|
|
303
|
+
return default
|
|
304
|
+
try:
|
|
305
|
+
return max(1.0, float(raw_value))
|
|
306
|
+
except ValueError:
|
|
307
|
+
return default
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _sample_results_rows(results_df: pd.DataFrame, *, max_rows: int = 72) -> list[dict[str, Any]]:
|
|
311
|
+
preferred_columns = [
|
|
312
|
+
"time_minutes",
|
|
313
|
+
"glucose_actual_mgdl",
|
|
314
|
+
"glucose_sensor_mgdl",
|
|
315
|
+
"carb_intake_grams",
|
|
316
|
+
"delivered_insulin_units",
|
|
317
|
+
"algo_recommended_insulin_units",
|
|
318
|
+
"active_insulin_units",
|
|
319
|
+
"safety_triggered",
|
|
320
|
+
"safety_reason",
|
|
321
|
+
]
|
|
322
|
+
columns = [column for column in preferred_columns if column in results_df.columns]
|
|
323
|
+
if not columns or results_df.empty:
|
|
324
|
+
return []
|
|
325
|
+
step = max(1, len(results_df) // max_rows)
|
|
326
|
+
sampled = results_df.loc[:, columns].iloc[::step].head(max_rows)
|
|
327
|
+
return _json_safe(sampled.to_dict(orient="records"))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _scalar_safety_summary(safety_report: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
331
|
+
if not safety_report:
|
|
332
|
+
return {}
|
|
333
|
+
summary: Dict[str, Any] = {}
|
|
334
|
+
for key, value in safety_report.items():
|
|
335
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
336
|
+
summary[key] = _json_safe(value)
|
|
337
|
+
return summary
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _build_local_ai_review_payload(
|
|
341
|
+
results_df: pd.DataFrame,
|
|
342
|
+
*,
|
|
343
|
+
run_label: str,
|
|
344
|
+
realism_report: Any,
|
|
345
|
+
selected_reference: str | None,
|
|
346
|
+
reference_selection: str | None,
|
|
347
|
+
quality_summary: Dict[str, Any],
|
|
348
|
+
safety_report: Optional[Dict[str, Any]],
|
|
349
|
+
) -> Dict[str, Any]:
|
|
350
|
+
return {
|
|
351
|
+
"run_label": run_label,
|
|
352
|
+
"research_only": True,
|
|
353
|
+
"medical_device": False,
|
|
354
|
+
"instruction": (
|
|
355
|
+
"Use the deterministic run_quality grade as source of truth. "
|
|
356
|
+
"The local AI verifier may explain risks and suggest research checks, "
|
|
357
|
+
"but must not provide treatment or dosing advice."
|
|
358
|
+
),
|
|
359
|
+
"realism": {
|
|
360
|
+
"verdict": realism_report.verdict,
|
|
361
|
+
"score": float(realism_report.realism_score),
|
|
362
|
+
"summary": realism_report.summary,
|
|
363
|
+
"metrics": _json_safe(realism_report.metrics),
|
|
364
|
+
"reference": selected_reference,
|
|
365
|
+
"reference_selection": reference_selection,
|
|
366
|
+
"checks": [
|
|
367
|
+
{
|
|
368
|
+
"title": check.title,
|
|
369
|
+
"status": check.status,
|
|
370
|
+
"detail": check.detail,
|
|
371
|
+
}
|
|
372
|
+
for check in realism_report.checks
|
|
373
|
+
if check.status in {"failed", "warning"}
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
"deterministic_quality_gate": _json_safe(quality_summary),
|
|
377
|
+
"safety": _scalar_safety_summary(safety_report),
|
|
378
|
+
"sampled_results": _sample_results_rows(results_df),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _write_local_ai_metadata(
|
|
383
|
+
metadata_path: Path,
|
|
384
|
+
*,
|
|
385
|
+
status: str,
|
|
386
|
+
model: str,
|
|
387
|
+
reason: str | None = None,
|
|
388
|
+
markdown_path: Path | None = None,
|
|
389
|
+
policy_violations: tuple[str, ...] = (),
|
|
390
|
+
policy_warnings: tuple[str, ...] = (),
|
|
391
|
+
policy_action: str = "allow",
|
|
392
|
+
) -> Dict[str, Any]:
|
|
393
|
+
metadata = {
|
|
394
|
+
"status": status,
|
|
395
|
+
"model": model,
|
|
396
|
+
"reason": reason,
|
|
397
|
+
"markdown": str(markdown_path) if markdown_path else None,
|
|
398
|
+
"research_only": True,
|
|
399
|
+
"medical_device": False,
|
|
400
|
+
"policy_violations": list(policy_violations),
|
|
401
|
+
"policy_warnings": list(policy_warnings),
|
|
402
|
+
"policy_action": policy_action,
|
|
403
|
+
}
|
|
404
|
+
metadata_path.parent.mkdir(parents=True, exist_ok=True)
|
|
405
|
+
metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
|
|
406
|
+
return metadata
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _write_local_ai_run_verification(
|
|
410
|
+
results_df: pd.DataFrame,
|
|
411
|
+
output_path: Path,
|
|
412
|
+
*,
|
|
413
|
+
run_label: str,
|
|
414
|
+
realism_report: Any,
|
|
415
|
+
selected_reference: str | None,
|
|
416
|
+
reference_selection: str | None,
|
|
417
|
+
quality_summary: Dict[str, Any],
|
|
418
|
+
safety_report: Optional[Dict[str, Any]],
|
|
419
|
+
local_ai_review: bool | str | None,
|
|
420
|
+
local_ai_model: str,
|
|
421
|
+
local_ai_timeout_seconds: float,
|
|
422
|
+
ollama_host: str | None,
|
|
423
|
+
) -> Dict[str, Any]:
|
|
424
|
+
ai_dir = output_path / "ai"
|
|
425
|
+
markdown_path = ai_dir / "local_run_verification.md"
|
|
426
|
+
metadata_path = ai_dir / "local_run_verification.json"
|
|
427
|
+
mode = _resolve_local_ai_review_mode(local_ai_review)
|
|
428
|
+
|
|
429
|
+
if mode == "off":
|
|
430
|
+
metadata = _write_local_ai_metadata(
|
|
431
|
+
metadata_path,
|
|
432
|
+
status="skipped",
|
|
433
|
+
model=local_ai_model,
|
|
434
|
+
reason=f"disabled via {LOCAL_AI_REVIEW_ENV}",
|
|
435
|
+
)
|
|
436
|
+
return {
|
|
437
|
+
"local_ai_review_status": metadata["status"],
|
|
438
|
+
"local_ai_review_json": str(metadata_path),
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if mode == "auto" and os.getenv("CI", "").strip().lower() in {"1", "true", "yes"}:
|
|
442
|
+
metadata = _write_local_ai_metadata(
|
|
443
|
+
metadata_path,
|
|
444
|
+
status="skipped",
|
|
445
|
+
model=local_ai_model,
|
|
446
|
+
reason="disabled during CI; set IINTS_LOCAL_AI_REVIEW=required to force it",
|
|
447
|
+
)
|
|
448
|
+
return {
|
|
449
|
+
"local_ai_review_status": metadata["status"],
|
|
450
|
+
"local_ai_review_json": str(metadata_path),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
backend = OllamaBackend(
|
|
455
|
+
model_name=local_ai_model,
|
|
456
|
+
base_url=ollama_host,
|
|
457
|
+
timeout_seconds=_local_ai_timeout_seconds(local_ai_timeout_seconds),
|
|
458
|
+
temperature=0.0,
|
|
459
|
+
top_p=0.7,
|
|
460
|
+
num_predict=900,
|
|
461
|
+
num_ctx=8192,
|
|
462
|
+
)
|
|
463
|
+
if not backend.available():
|
|
464
|
+
reason = (
|
|
465
|
+
f"local Ollama is not reachable at {backend.base_url}; "
|
|
466
|
+
"start Ollama to enable automatic local AI verification"
|
|
467
|
+
)
|
|
468
|
+
if mode == "required":
|
|
469
|
+
raise RuntimeError(reason)
|
|
470
|
+
metadata = _write_local_ai_metadata(
|
|
471
|
+
metadata_path,
|
|
472
|
+
status="skipped",
|
|
473
|
+
model=local_ai_model,
|
|
474
|
+
reason=reason,
|
|
475
|
+
)
|
|
476
|
+
return {
|
|
477
|
+
"local_ai_review_status": metadata["status"],
|
|
478
|
+
"local_ai_review_json": str(metadata_path),
|
|
479
|
+
}
|
|
480
|
+
resolved_model = backend.ensure_model_ready()
|
|
481
|
+
payload = _build_local_ai_review_payload(
|
|
482
|
+
results_df,
|
|
483
|
+
run_label=run_label,
|
|
484
|
+
realism_report=realism_report,
|
|
485
|
+
selected_reference=selected_reference,
|
|
486
|
+
reference_selection=reference_selection,
|
|
487
|
+
quality_summary=quality_summary,
|
|
488
|
+
safety_report=safety_report,
|
|
489
|
+
)
|
|
490
|
+
system_prompt = (
|
|
491
|
+
"You are the local IINTS-AF run verifier. You review simulation output for "
|
|
492
|
+
"research and education only. You are not a medical device and must not give "
|
|
493
|
+
"insulin, glucagon, diagnosis, or treatment advice. Treat the deterministic "
|
|
494
|
+
"quality gate as authoritative; your role is to explain what looks trustworthy, "
|
|
495
|
+
"what needs review, and what next checks would improve the research result. "
|
|
496
|
+
f"Boundary: {RESEARCH_ONLY_NOTICE}"
|
|
497
|
+
)
|
|
498
|
+
user_prompt = (
|
|
499
|
+
"Review this bounded IINTS run summary and return concise Markdown with exactly "
|
|
500
|
+
"these sections: ## Verdict, ## What Looks Trustworthy, ## What Needs Review, "
|
|
501
|
+
"## Next Checks, ## Research-Only Note. Keep it concrete and cite the provided "
|
|
502
|
+
"metrics. Do not invent clinical validation.\n\n"
|
|
503
|
+
f"```json\n{json.dumps(_json_safe(payload), indent=2)}\n```"
|
|
504
|
+
)
|
|
505
|
+
response_text = backend.complete(system_prompt=system_prompt, user_prompt=user_prompt).strip()
|
|
506
|
+
if not response_text:
|
|
507
|
+
raise RuntimeError("local AI verifier returned an empty response")
|
|
508
|
+
guarded = guard_ai_output(response_text, source="run_quality_local_ai")
|
|
509
|
+
ai_dir.mkdir(parents=True, exist_ok=True)
|
|
510
|
+
markdown_path.write_text(guarded.text + "\n", encoding="utf-8")
|
|
511
|
+
metadata = _write_local_ai_metadata(
|
|
512
|
+
metadata_path,
|
|
513
|
+
status="completed" if guarded.allowed else "blocked_policy",
|
|
514
|
+
model=resolved_model,
|
|
515
|
+
markdown_path=markdown_path,
|
|
516
|
+
policy_violations=guarded.violations,
|
|
517
|
+
policy_warnings=guarded.warnings,
|
|
518
|
+
policy_action=guarded.action,
|
|
519
|
+
)
|
|
520
|
+
return {
|
|
521
|
+
"local_ai_review_status": metadata["status"],
|
|
522
|
+
"local_ai_review_md": str(markdown_path),
|
|
523
|
+
"local_ai_review_json": str(metadata_path),
|
|
524
|
+
"local_ai_review_model": resolved_model,
|
|
525
|
+
}
|
|
526
|
+
except Exception as exc:
|
|
527
|
+
if mode == "required":
|
|
528
|
+
raise
|
|
529
|
+
metadata = _write_local_ai_metadata(
|
|
530
|
+
metadata_path,
|
|
531
|
+
status="failed",
|
|
532
|
+
model=local_ai_model,
|
|
533
|
+
reason=str(exc),
|
|
534
|
+
)
|
|
535
|
+
return {
|
|
536
|
+
"local_ai_review_status": metadata["status"],
|
|
537
|
+
"local_ai_review_json": str(metadata_path),
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
109
541
|
def write_run_quality_artifacts(
|
|
110
542
|
results_df: pd.DataFrame,
|
|
111
543
|
output_dir: str | Path,
|
|
@@ -113,6 +545,10 @@ def write_run_quality_artifacts(
|
|
|
113
545
|
run_label: Optional[str] = None,
|
|
114
546
|
safety_report: Optional[Dict[str, Any]] = None,
|
|
115
547
|
realism_reference: Optional[str] = "auto",
|
|
548
|
+
local_ai_review: bool | str | None = None,
|
|
549
|
+
local_ai_model: str | None = None,
|
|
550
|
+
local_ai_timeout_seconds: float = 90.0,
|
|
551
|
+
ollama_host: str | None = None,
|
|
116
552
|
) -> Dict[str, Any]:
|
|
117
553
|
"""Write reviewer-facing quality artifacts for one run.
|
|
118
554
|
|
|
@@ -132,7 +568,14 @@ def write_run_quality_artifacts(
|
|
|
132
568
|
realism_json = output_path / "realism_report.json"
|
|
133
569
|
realism_html = output_path / "realism_dashboard.html"
|
|
134
570
|
realism_markdown = output_path / "run_quality_review.md"
|
|
571
|
+
quality_json = output_path / "run_quality_summary.json"
|
|
135
572
|
write_realism_report(realism_report, realism_json)
|
|
573
|
+
quality_summary = build_result_quality_summary(
|
|
574
|
+
results_df,
|
|
575
|
+
realism_report=realism_report,
|
|
576
|
+
safety_report=safety_report,
|
|
577
|
+
)
|
|
578
|
+
quality_json.write_text(json.dumps(quality_summary, indent=2), encoding="utf-8")
|
|
136
579
|
write_realism_dashboard(
|
|
137
580
|
realism_report,
|
|
138
581
|
realism_frame,
|
|
@@ -146,6 +589,7 @@ def write_run_quality_artifacts(
|
|
|
146
589
|
realism_report=realism_report,
|
|
147
590
|
selected_reference=selected_reference,
|
|
148
591
|
reference_selection=realism_reference,
|
|
592
|
+
quality_summary=quality_summary,
|
|
149
593
|
)
|
|
150
594
|
realism_summary = {
|
|
151
595
|
"verdict": realism_report.verdict,
|
|
@@ -153,17 +597,48 @@ def write_run_quality_artifacts(
|
|
|
153
597
|
"summary": realism_report.summary,
|
|
154
598
|
"reference": selected_reference,
|
|
155
599
|
"reference_selection": realism_reference,
|
|
600
|
+
"quality_grade": quality_summary["grade"],
|
|
601
|
+
"quality_score": quality_summary["score"],
|
|
156
602
|
}
|
|
157
603
|
if safety_report is not None:
|
|
158
604
|
safety_report["realism_review"] = realism_summary
|
|
605
|
+
safety_report["run_quality"] = quality_summary
|
|
159
606
|
outputs.update(
|
|
160
607
|
{
|
|
161
608
|
"realism_report_json": str(realism_json),
|
|
162
609
|
"realism_dashboard_html": str(realism_html),
|
|
163
610
|
"run_quality_review_md": str(realism_markdown),
|
|
611
|
+
"run_quality_summary_json": str(quality_json),
|
|
164
612
|
"realism_review": realism_summary,
|
|
613
|
+
"run_quality": quality_summary,
|
|
165
614
|
}
|
|
166
615
|
)
|
|
616
|
+
local_ai_outputs = _write_local_ai_run_verification(
|
|
617
|
+
results_df,
|
|
618
|
+
output_path,
|
|
619
|
+
run_label=label,
|
|
620
|
+
realism_report=realism_report,
|
|
621
|
+
selected_reference=selected_reference,
|
|
622
|
+
reference_selection=realism_reference,
|
|
623
|
+
quality_summary=quality_summary,
|
|
624
|
+
safety_report=safety_report,
|
|
625
|
+
local_ai_review=local_ai_review,
|
|
626
|
+
local_ai_model=local_ai_model
|
|
627
|
+
or os.getenv(LOCAL_AI_REVIEW_MODEL_ENV)
|
|
628
|
+
or DEFAULT_MINISTRAL_MODEL,
|
|
629
|
+
local_ai_timeout_seconds=local_ai_timeout_seconds,
|
|
630
|
+
ollama_host=ollama_host,
|
|
631
|
+
)
|
|
632
|
+
outputs.update(local_ai_outputs)
|
|
633
|
+
if local_ai_outputs:
|
|
634
|
+
quality_summary["local_ai_review_status"] = local_ai_outputs.get("local_ai_review_status")
|
|
635
|
+
if local_ai_outputs.get("local_ai_review_md"):
|
|
636
|
+
quality_summary["local_ai_review_md"] = local_ai_outputs["local_ai_review_md"]
|
|
637
|
+
if local_ai_outputs.get("local_ai_review_json"):
|
|
638
|
+
quality_summary["local_ai_review_json"] = local_ai_outputs["local_ai_review_json"]
|
|
639
|
+
quality_json.write_text(json.dumps(quality_summary, indent=2), encoding="utf-8")
|
|
640
|
+
if safety_report is not None:
|
|
641
|
+
safety_report["run_quality"] = quality_summary
|
|
167
642
|
except Exception as exc:
|
|
168
643
|
outputs["realism_warning"] = str(exc)
|
|
169
644
|
|
iints/cli/cli.py
CHANGED
|
@@ -488,6 +488,27 @@ def _maybe_prepare_ai_artifacts(output_dir: Path, console: Console) -> None:
|
|
|
488
488
|
console.print(f"[yellow]AI-ready payload export skipped:[/yellow] {exc}")
|
|
489
489
|
|
|
490
490
|
|
|
491
|
+
def _print_run_quality_review(console: Console, quality_outputs: Dict[str, Any]) -> None:
|
|
492
|
+
realism_review = quality_outputs.get("realism_review")
|
|
493
|
+
if not realism_review:
|
|
494
|
+
return
|
|
495
|
+
verdict = realism_review.get("verdict")
|
|
496
|
+
realism_score = float(realism_review.get("realism_score", 0.0))
|
|
497
|
+
quality_grade = realism_review.get("quality_grade", "unknown")
|
|
498
|
+
quality_score = float(realism_review.get("quality_score", 0.0))
|
|
499
|
+
console.print(
|
|
500
|
+
"[green]Run quality:[/green] "
|
|
501
|
+
f"{quality_grade} ({quality_score:.1f}/100); "
|
|
502
|
+
f"realism {verdict} ({realism_score:.2f})"
|
|
503
|
+
)
|
|
504
|
+
ai_status = quality_outputs.get("local_ai_review_status")
|
|
505
|
+
if ai_status == "completed":
|
|
506
|
+
console.print(f"[green]Local AI verifier:[/green] {quality_outputs.get('local_ai_review_md')}")
|
|
507
|
+
elif ai_status in {"failed", "skipped"}:
|
|
508
|
+
ai_json = quality_outputs.get("local_ai_review_json")
|
|
509
|
+
console.print(f"[yellow]Local AI verifier:[/yellow] {ai_status} ({ai_json})")
|
|
510
|
+
|
|
511
|
+
|
|
491
512
|
def _get_preset(name: str) -> Dict[str, Any]:
|
|
492
513
|
presets = _load_presets()
|
|
493
514
|
for preset in presets:
|
|
@@ -4786,12 +4807,7 @@ def presets_run(
|
|
|
4786
4807
|
run_label=run_id,
|
|
4787
4808
|
safety_report=safety_report,
|
|
4788
4809
|
)
|
|
4789
|
-
|
|
4790
|
-
realism_review = quality_outputs["realism_review"]
|
|
4791
|
-
console.print(
|
|
4792
|
-
"[green]Realism review:[/green] "
|
|
4793
|
-
f"{realism_review.get('verdict')} (score {float(realism_review.get('realism_score', 0.0)):.2f})"
|
|
4794
|
-
)
|
|
4810
|
+
_print_run_quality_review(console, quality_outputs)
|
|
4795
4811
|
console.print(f"Safety visualizer: {quality_outputs.get('safety_visualizer_html')}")
|
|
4796
4812
|
|
|
4797
4813
|
manifest_files = {
|
|
@@ -4811,6 +4827,8 @@ def presets_run(
|
|
|
4811
4827
|
"realism_dashboard": "realism_dashboard_html",
|
|
4812
4828
|
"safety_visualizer": "safety_visualizer_html",
|
|
4813
4829
|
"safety_visualizer_json": "safety_visualizer_json",
|
|
4830
|
+
"local_ai_review": "local_ai_review_md",
|
|
4831
|
+
"local_ai_review_json": "local_ai_review_json",
|
|
4814
4832
|
}.items():
|
|
4815
4833
|
output_value = quality_outputs.get(output_key)
|
|
4816
4834
|
if output_value:
|
|
@@ -5536,6 +5554,8 @@ def run(
|
|
|
5536
5554
|
"realism_dashboard": "realism_dashboard_html",
|
|
5537
5555
|
"safety_visualizer": "safety_visualizer_html",
|
|
5538
5556
|
"safety_visualizer_json": "safety_visualizer_json",
|
|
5557
|
+
"local_ai_review": "local_ai_review_md",
|
|
5558
|
+
"local_ai_review_json": "local_ai_review_json",
|
|
5539
5559
|
}.items():
|
|
5540
5560
|
output_value = quality_outputs.get(output_key)
|
|
5541
5561
|
if output_value:
|
|
@@ -5548,12 +5568,7 @@ def run(
|
|
|
5548
5568
|
console.print(f"Using compute device: [blue]{device}[/blue]")
|
|
5549
5569
|
if baseline_paths is not None:
|
|
5550
5570
|
console.print(f"Baseline comparison saved to: {baseline_paths}")
|
|
5551
|
-
|
|
5552
|
-
realism_review = quality_outputs["realism_review"]
|
|
5553
|
-
console.print(
|
|
5554
|
-
"[green]Realism review:[/green] "
|
|
5555
|
-
f"{realism_review.get('verdict')} (score {float(realism_review.get('realism_score', 0.0)):.2f})"
|
|
5556
|
-
)
|
|
5571
|
+
_print_run_quality_review(console, quality_outputs)
|
|
5557
5572
|
if quality_outputs.get("safety_visualizer_html"):
|
|
5558
5573
|
console.print(f"Safety visualizer: {quality_outputs.get('safety_visualizer_html')}")
|
|
5559
5574
|
console.print(f"Run metadata: {run_metadata_path}")
|
|
@@ -10955,9 +10970,12 @@ def medtronic_live_cmd(
|
|
|
10955
10970
|
start=1,
|
|
10956
10971
|
):
|
|
10957
10972
|
if not timeline.empty:
|
|
10973
|
+
if merged.empty:
|
|
10974
|
+
merged = timeline.copy()
|
|
10975
|
+
else:
|
|
10976
|
+
merged = pd.concat([merged, timeline], ignore_index=True)
|
|
10958
10977
|
merged = (
|
|
10959
|
-
|
|
10960
|
-
.sort_values("timestamp_dt")
|
|
10978
|
+
merged.sort_values("timestamp_dt")
|
|
10961
10979
|
.drop_duplicates("timestamp_dt", keep="last")
|
|
10962
10980
|
.reset_index(drop=True)
|
|
10963
10981
|
)
|