iints-sdk-python35 1.1.2__py3-none-any.whl → 1.2.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.
- iints/__init__.py +13 -1
- iints/ai/backends/ollama.py +84 -1
- iints/ai/cli.py +16 -0
- iints/ai/prompts.py +16 -15
- iints/analysis/__init__.py +4 -0
- iints/analysis/carelink_workbench.py +733 -0
- iints/analysis/poster.py +289 -0
- iints/cli/cli.py +219 -1
- iints/data/__init__.py +8 -0
- iints/data/importer.py +327 -0
- {iints_sdk_python35-1.1.2.dist-info → iints_sdk_python35-1.2.0.dist-info}/METADATA +83 -2
- {iints_sdk_python35-1.1.2.dist-info → iints_sdk_python35-1.2.0.dist-info}/RECORD +16 -14
- {iints_sdk_python35-1.1.2.dist-info → iints_sdk_python35-1.2.0.dist-info}/WHEEL +0 -0
- {iints_sdk_python35-1.1.2.dist-info → iints_sdk_python35-1.2.0.dist-info}/entry_points.txt +0 -0
- {iints_sdk_python35-1.1.2.dist-info → iints_sdk_python35-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {iints_sdk_python35-1.1.2.dist-info → iints_sdk_python35-1.2.0.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.
|
|
14
|
+
__version__ = "1.2.0"
|
|
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.
|
|
@@ -56,17 +56,23 @@ from .data.importer import (
|
|
|
56
56
|
export_demo_csv,
|
|
57
57
|
export_standard_csv,
|
|
58
58
|
guess_column_mapping,
|
|
59
|
+
import_carelink_csv,
|
|
60
|
+
import_carelink_timeline,
|
|
59
61
|
import_cgm_csv,
|
|
60
62
|
import_cgm_dataframe,
|
|
63
|
+
load_carelink_event_log,
|
|
61
64
|
load_demo_dataframe,
|
|
62
65
|
scenario_from_csv,
|
|
63
66
|
scenario_from_dataframe,
|
|
67
|
+
summarize_carelink_csv,
|
|
64
68
|
)
|
|
65
69
|
from .data.nightscout import NightscoutConfig, import_nightscout
|
|
66
70
|
from .data.tidepool import TidepoolClient, load_openapi_spec
|
|
67
71
|
from .data.guardians import mdmp_gate, MDMPGateError
|
|
68
72
|
from .data.synthetic_mirror import generate_synthetic_mirror, SyntheticMirrorArtifact
|
|
69
73
|
from .analysis.metrics import generate_benchmark_metrics # Added for benchmark
|
|
74
|
+
from .analysis.carelink_workbench import build_carelink_workbench
|
|
75
|
+
from .analysis.poster import generate_results_poster
|
|
70
76
|
from .analysis.reporting import ClinicalReportGenerator
|
|
71
77
|
from .analysis.edge_efficiency import EnergyEstimate, estimate_energy_per_decision
|
|
72
78
|
from .ai import AIResponse, IINTSAssistant, MDMPGuard
|
|
@@ -160,11 +166,15 @@ __all__ = [
|
|
|
160
166
|
"export_demo_csv",
|
|
161
167
|
"export_standard_csv",
|
|
162
168
|
"guess_column_mapping",
|
|
169
|
+
"import_carelink_csv",
|
|
170
|
+
"import_carelink_timeline",
|
|
163
171
|
"import_cgm_csv",
|
|
164
172
|
"import_cgm_dataframe",
|
|
173
|
+
"load_carelink_event_log",
|
|
165
174
|
"load_demo_dataframe",
|
|
166
175
|
"scenario_from_csv",
|
|
167
176
|
"scenario_from_dataframe",
|
|
177
|
+
"summarize_carelink_csv",
|
|
168
178
|
"NightscoutConfig",
|
|
169
179
|
"import_nightscout",
|
|
170
180
|
"TidepoolClient",
|
|
@@ -175,6 +185,7 @@ __all__ = [
|
|
|
175
185
|
"SyntheticMirrorArtifact",
|
|
176
186
|
# Analysis Metrics
|
|
177
187
|
"generate_benchmark_metrics",
|
|
188
|
+
"build_carelink_workbench",
|
|
178
189
|
"ClinicalReportGenerator",
|
|
179
190
|
"EnergyEstimate",
|
|
180
191
|
"estimate_energy_per_decision",
|
|
@@ -185,6 +196,7 @@ __all__ = [
|
|
|
185
196
|
"generate_report",
|
|
186
197
|
"generate_quickstart_report",
|
|
187
198
|
"generate_demo_report",
|
|
199
|
+
"generate_results_poster",
|
|
188
200
|
# High-level API
|
|
189
201
|
"run_simulation",
|
|
190
202
|
"run_full",
|
iints/ai/backends/ollama.py
CHANGED
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
from http.client import IncompleteRead, RemoteDisconnected
|
|
6
|
+
from time import sleep
|
|
5
7
|
from urllib import error, request
|
|
6
8
|
|
|
7
9
|
|
|
@@ -40,6 +42,20 @@ class OllamaBackend:
|
|
|
40
42
|
def _pull_hint(self) -> str:
|
|
41
43
|
return f"ollama pull {self.model_name}"
|
|
42
44
|
|
|
45
|
+
def _generation_failure_hint(self) -> str:
|
|
46
|
+
resolved = self.resolved_model_name or self.model_name
|
|
47
|
+
return (
|
|
48
|
+
"Ollama closed the generation connection before returning a response.\n"
|
|
49
|
+
f"Endpoint: {self.base_url}\n"
|
|
50
|
+
f"Model: {resolved}\n"
|
|
51
|
+
"This usually means the model crashed while loading, the daemon restarted, "
|
|
52
|
+
"or the machine ran out of memory.\n"
|
|
53
|
+
"Try one of these:\n"
|
|
54
|
+
f" 1. Run `ollama run {resolved} \"Reply with OK.\"` to confirm direct inference works.\n"
|
|
55
|
+
" 2. Run `iints ai local-check --smoke-test` to validate a real generation path.\n"
|
|
56
|
+
" 3. Switch to a smaller local model such as `ministral-3:3b` if memory is tight."
|
|
57
|
+
)
|
|
58
|
+
|
|
43
59
|
def _requires_ministral_3_runtime(self) -> bool:
|
|
44
60
|
requested = self.model_name.strip().lower()
|
|
45
61
|
return requested.startswith("ministral-3") or requested == "ministral"
|
|
@@ -68,6 +84,15 @@ class OllamaBackend:
|
|
|
68
84
|
payload: dict[str, object] | None = None,
|
|
69
85
|
*,
|
|
70
86
|
method: str = "POST",
|
|
87
|
+
) -> dict[str, object]:
|
|
88
|
+
return self._request_json_once(path, payload, method=method)
|
|
89
|
+
|
|
90
|
+
def _request_json_once(
|
|
91
|
+
self,
|
|
92
|
+
path: str,
|
|
93
|
+
payload: dict[str, object] | None = None,
|
|
94
|
+
*,
|
|
95
|
+
method: str = "POST",
|
|
71
96
|
) -> dict[str, object]:
|
|
72
97
|
url = f"{self.base_url}{path}"
|
|
73
98
|
body = None
|
|
@@ -92,6 +117,12 @@ class OllamaBackend:
|
|
|
92
117
|
f"Could not reach Ollama at {self.base_url}. "
|
|
93
118
|
"Start Ollama or set OLLAMA_HOST to the correct endpoint."
|
|
94
119
|
) from exc
|
|
120
|
+
except (RemoteDisconnected, ConnectionResetError, IncompleteRead) as exc:
|
|
121
|
+
if path == "/api/generate":
|
|
122
|
+
raise RuntimeError(self._generation_failure_hint()) from exc
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
f"Ollama connection closed unexpectedly while calling {path} at {self.base_url}."
|
|
125
|
+
) from exc
|
|
95
126
|
|
|
96
127
|
try:
|
|
97
128
|
payload_json = json.loads(text)
|
|
@@ -223,6 +254,43 @@ class OllamaBackend:
|
|
|
223
254
|
"version_ok": version_ok,
|
|
224
255
|
}
|
|
225
256
|
|
|
257
|
+
def smoke_test(self) -> dict[str, object]:
|
|
258
|
+
resolved_model = self.ensure_model_ready()
|
|
259
|
+
payload = {
|
|
260
|
+
"model": resolved_model,
|
|
261
|
+
"system": "You are a health check. Reply with exactly: OK",
|
|
262
|
+
"prompt": "Reply with exactly: OK",
|
|
263
|
+
"stream": False,
|
|
264
|
+
"options": {
|
|
265
|
+
"temperature": 0,
|
|
266
|
+
"num_predict": 8,
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
last_error: Exception | None = None
|
|
271
|
+
for attempt in range(2):
|
|
272
|
+
try:
|
|
273
|
+
response = self._request_json_once("/api/generate", payload)
|
|
274
|
+
text = response.get("response")
|
|
275
|
+
if not isinstance(text, str) or not text.strip():
|
|
276
|
+
raise RuntimeError("Ollama returned an empty smoke-test completion.")
|
|
277
|
+
return {
|
|
278
|
+
"ok": True,
|
|
279
|
+
"response": text.strip(),
|
|
280
|
+
"attempts": attempt + 1,
|
|
281
|
+
}
|
|
282
|
+
except (RuntimeError, RemoteDisconnected, ConnectionResetError, IncompleteRead) as exc:
|
|
283
|
+
if not isinstance(exc, RuntimeError):
|
|
284
|
+
exc = RuntimeError(self._generation_failure_hint())
|
|
285
|
+
last_error = exc
|
|
286
|
+
if attempt == 0:
|
|
287
|
+
sleep(1.0)
|
|
288
|
+
continue
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
assert last_error is not None
|
|
292
|
+
raise last_error
|
|
293
|
+
|
|
226
294
|
def complete(self, *, system_prompt: str, user_prompt: str) -> str:
|
|
227
295
|
resolved_model = self.ensure_model_ready()
|
|
228
296
|
payload = {
|
|
@@ -231,7 +299,22 @@ class OllamaBackend:
|
|
|
231
299
|
"prompt": user_prompt,
|
|
232
300
|
"stream": False,
|
|
233
301
|
}
|
|
234
|
-
|
|
302
|
+
last_error: Exception | None = None
|
|
303
|
+
for attempt in range(2):
|
|
304
|
+
try:
|
|
305
|
+
response = self._request_json_once("/api/generate", payload)
|
|
306
|
+
break
|
|
307
|
+
except (RuntimeError, RemoteDisconnected, ConnectionResetError, IncompleteRead) as exc:
|
|
308
|
+
if not isinstance(exc, RuntimeError):
|
|
309
|
+
exc = RuntimeError(self._generation_failure_hint())
|
|
310
|
+
last_error = exc
|
|
311
|
+
if attempt == 0:
|
|
312
|
+
sleep(1.0)
|
|
313
|
+
continue
|
|
314
|
+
raise exc
|
|
315
|
+
else:
|
|
316
|
+
assert last_error is not None
|
|
317
|
+
raise last_error
|
|
235
318
|
text = response.get("response")
|
|
236
319
|
if not isinstance(text, str) or not text.strip():
|
|
237
320
|
raise RuntimeError("Ollama returned an empty completion.")
|
iints/ai/cli.py
CHANGED
|
@@ -111,6 +111,7 @@ def _render_local_check(console: Console, status: dict[str, object]) -> None:
|
|
|
111
111
|
installed_text = ", ".join(str(item) for item in installed) if isinstance(installed, list) and installed else "none"
|
|
112
112
|
ready = bool(status.get("ready"))
|
|
113
113
|
resolved_model = status.get("resolved_model") or "not found"
|
|
114
|
+
smoke_text = status.get("smoke_test") or "not run"
|
|
114
115
|
console.print(
|
|
115
116
|
Panel(
|
|
116
117
|
"\n".join(
|
|
@@ -126,6 +127,7 @@ def _render_local_check(console: Console, status: dict[str, object]) -> None:
|
|
|
126
127
|
if status.get("pull_command")
|
|
127
128
|
else "Pull command: not needed"
|
|
128
129
|
),
|
|
130
|
+
f"Generate smoke-test: {smoke_text}",
|
|
129
131
|
]
|
|
130
132
|
),
|
|
131
133
|
title="IINTS AI Local Check",
|
|
@@ -239,6 +241,13 @@ def local_check(
|
|
|
239
241
|
model: Annotated[str, typer.Option(help="Ollama model name to validate locally.")] = DEFAULT_MINISTRAL_MODEL,
|
|
240
242
|
ollama_host: Annotated[Optional[str], typer.Option(help="Override the Ollama base URL.")] = None,
|
|
241
243
|
timeout_seconds: Annotated[float, typer.Option(help="HTTP timeout for Ollama health checks.")] = 120.0,
|
|
244
|
+
smoke_test: Annotated[
|
|
245
|
+
bool,
|
|
246
|
+
typer.Option(
|
|
247
|
+
"--smoke-test/--no-smoke-test",
|
|
248
|
+
help="Run a tiny generation request after health checks to prove the model can actually answer.",
|
|
249
|
+
),
|
|
250
|
+
] = True,
|
|
242
251
|
) -> None:
|
|
243
252
|
console = Console()
|
|
244
253
|
backend = OllamaBackend(model_name=model, base_url=ollama_host, timeout_seconds=timeout_seconds)
|
|
@@ -250,6 +259,13 @@ def local_check(
|
|
|
250
259
|
)
|
|
251
260
|
raise typer.Exit(code=1)
|
|
252
261
|
status = backend.healthcheck()
|
|
262
|
+
if smoke_test and bool(status.get("ready")):
|
|
263
|
+
smoke = backend.smoke_test()
|
|
264
|
+
status["smoke_test"] = f"OK ({smoke.get('response')})"
|
|
265
|
+
elif smoke_test:
|
|
266
|
+
status["smoke_test"] = "skipped (model not ready)"
|
|
267
|
+
else:
|
|
268
|
+
status["smoke_test"] = "disabled"
|
|
253
269
|
_render_local_check(console, status)
|
|
254
270
|
if not bool(status.get("ready")):
|
|
255
271
|
raise typer.Exit(code=1)
|
iints/ai/prompts.py
CHANGED
|
@@ -8,8 +8,9 @@ TaskName = Literal["explain_decision", "analyze_trends", "detect_anomalies", "ge
|
|
|
8
8
|
MAX_PROMPT_PAYLOAD_CHARS = 12000
|
|
9
9
|
|
|
10
10
|
SYSTEM_PROMPT = (
|
|
11
|
-
"You are the IINTS-AF research assistant for closed-loop insulin delivery simulations
|
|
12
|
-
"
|
|
11
|
+
"You are the IINTS-AF research assistant for closed-loop insulin delivery simulations "
|
|
12
|
+
"and imported glucose datasets. "
|
|
13
|
+
"Explain glycemic behavior clearly, conservatively, and in plain language. "
|
|
13
14
|
"Do not give medical advice, treatment instructions, or patient-specific recommendations. "
|
|
14
15
|
"State uncertainty when the input is incomplete. "
|
|
15
16
|
"For research use only."
|
|
@@ -17,39 +18,39 @@ SYSTEM_PROMPT = (
|
|
|
17
18
|
|
|
18
19
|
TASK_TEMPLATES: dict[TaskName, str] = {
|
|
19
20
|
"explain_decision": (
|
|
20
|
-
"Given this single
|
|
21
|
-
"1. What the
|
|
22
|
-
"2. Whether
|
|
23
|
-
"3.
|
|
21
|
+
"Given this single decision step or noteworthy glucose snapshot, explain:\n"
|
|
22
|
+
"1. What is happening in the data and why it stands out\n"
|
|
23
|
+
"2. Whether there are safety signals, supervision, or notable context clues\n"
|
|
24
|
+
"3. What a research user should pay attention to next\n\n"
|
|
24
25
|
"Respond in 3 short paragraphs.\n\n"
|
|
25
|
-
"
|
|
26
|
+
"Input JSON:\n{data}"
|
|
26
27
|
),
|
|
27
28
|
"analyze_trends": (
|
|
28
|
-
"Review this glucose-oriented
|
|
29
|
+
"Review this glucose-oriented payload and summarize the main glycemic trends.\n"
|
|
29
30
|
"Focus on direction, stability, excursions, and likely triggers.\n"
|
|
30
31
|
"Respond with:\n"
|
|
31
32
|
"- Trend summary\n"
|
|
32
33
|
"- Main risk signals\n"
|
|
33
34
|
"- Short operational takeaway\n\n"
|
|
34
|
-
"
|
|
35
|
+
"Payload JSON:\n{data}"
|
|
35
36
|
),
|
|
36
37
|
"detect_anomalies": (
|
|
37
|
-
"Inspect this run summary and identify unusual patterns, inconsistent values, or clinically relevant anomalies.\n"
|
|
38
|
+
"Inspect this run or imported-data summary and identify unusual patterns, inconsistent values, or clinically relevant anomalies.\n"
|
|
38
39
|
"Respond with:\n"
|
|
39
40
|
"- Detected anomalies\n"
|
|
40
41
|
"- Why each anomaly matters\n"
|
|
41
42
|
"- Whether follow-up validation is recommended\n\n"
|
|
42
|
-
"
|
|
43
|
+
"Summary JSON:\n{data}"
|
|
43
44
|
),
|
|
44
45
|
"generate_report": (
|
|
45
|
-
"Write a concise markdown report for this IINTS-AF simulation run.\n"
|
|
46
|
+
"Write a concise markdown report for this IINTS-AF simulation run or imported personal glucose dataset.\n"
|
|
46
47
|
"Include sections:\n"
|
|
47
48
|
"1. Executive summary\n"
|
|
48
49
|
"2. Glycemic behavior\n"
|
|
49
|
-
"3. Safety
|
|
50
|
-
"4. Notable events or anomalies\n"
|
|
50
|
+
"3. Safety, supervision, or device behavior\n"
|
|
51
|
+
"4. Notable events, patterns, or anomalies\n"
|
|
51
52
|
"5. Research-only conclusion\n\n"
|
|
52
|
-
"
|
|
53
|
+
"Input JSON:\n{data}"
|
|
53
54
|
),
|
|
54
55
|
}
|
|
55
56
|
|
iints/analysis/__init__.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from .clinical_metrics import ClinicalMetricsCalculator, ClinicalMetricsResult
|
|
2
2
|
from .baseline import compute_metrics, run_baseline_comparison, write_baseline_comparison
|
|
3
|
+
from .carelink_workbench import build_carelink_workbench
|
|
4
|
+
from .poster import generate_results_poster
|
|
3
5
|
from .reporting import ClinicalReportGenerator
|
|
4
6
|
|
|
5
7
|
__all__ = [
|
|
8
|
+
"build_carelink_workbench",
|
|
6
9
|
"ClinicalMetricsCalculator",
|
|
7
10
|
"ClinicalMetricsResult",
|
|
8
11
|
"ClinicalReportGenerator",
|
|
9
12
|
"compute_metrics",
|
|
13
|
+
"generate_results_poster",
|
|
10
14
|
"run_baseline_comparison",
|
|
11
15
|
"write_baseline_comparison",
|
|
12
16
|
]
|