iints-sdk-python35 1.1.2__py3-none-any.whl → 1.1.3__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 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.1.2"
14
+ __version__ = "1.1.3"
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.
@@ -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
- response = self._request_json("/api/generate", payload)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iints-sdk-python35
3
- Version: 1.1.2
3
+ Version: 1.1.3
4
4
  Summary: A pre-clinical Edge-AI SDK for diabetes management validation.
5
5
  Author-email: Rune Bobbaers <rune.bobbaers@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/python35/IINTS-SDK
@@ -110,6 +110,8 @@ ollama pull ministral-3:8b
110
110
  iints ai local-check --model ministral-3:8b
111
111
  ```
112
112
 
113
+ `local-check` now performs a tiny generation smoke-test by default, so it verifies both model presence and real inference readiness.
114
+
113
115
  Recommended flow:
114
116
 
115
117
  ```bash
@@ -134,6 +136,7 @@ Notes:
134
136
  - Users can choose a larger or smaller local Mistral-family model with `--model ...`.
135
137
  - Large JSON payloads are clipped automatically before prompt generation to keep local inference stable.
136
138
  - `iints ai prepare <run_dir>` now creates AI-ready JSON payloads and, when MDMP is installed, a local development certificate plus keypair in `<run_dir>/ai/`.
139
+ - If Ollama closes the connection during generation, the SDK now surfaces an explicit recovery hint and points users toward `ministral-3:3b` for lower-memory systems.
137
140
  - After `iints ai prepare`, you can point `iints ai explain|trends|anomalies|report` directly at the run directory.
138
141
  - Output is research-only and not medical advice.
139
142
 
@@ -144,7 +147,7 @@ Troubleshooting:
144
147
 
145
148
  ```bash
146
149
  python -m pip uninstall -y iints iints-sdk-python35
147
- python -m pip install -U "iints-sdk-python35[mdmp]==1.1.2"
150
+ python -m pip install -U "iints-sdk-python35[mdmp]==1.1.3"
148
151
  hash -r
149
152
  ```
150
153
 
@@ -1,9 +1,9 @@
1
- iints/__init__.py,sha256=wfAcfS7htgnV4JD-R8_WyKZHOwR8Z98vy-oLYxu0-rE,6391
1
+ iints/__init__.py,sha256=F_rnQgeZZn3bQpyYAy4JwHARfQfq8fUSLvbs07Ycybw,6391
2
2
  iints/highlevel.py,sha256=DX12LRmL6YaYY99P0c_P93xfHe4mZjqyLhTYuS6L6hI,20491
3
3
  iints/metrics.py,sha256=O9hqOqJpUhUJDqsbfuqRMS9dkV97gzcgh3Y2jYUqHzg,907
4
4
  iints/ai/__init__.py,sha256=nyRDcFfSHI4a3NbTvySipFc3_inqRMEsr6xIEipWuyo,575
5
5
  iints/ai/assistant.py,sha256=0Ye1IaWEYg2rZnk3ny8f0GMoYqOWIa7U_GsV-sWrxtU,4346
6
- iints/ai/cli.py,sha256=_1ogEAb36BAt7sZ2CQSRKIJSpdn5xrlM7nNTtBIqfRo,18345
6
+ iints/ai/cli.py,sha256=jR9cgLkYGQRx_AfiGGWK4oBFTQDO2fHas_3UJGq78X8,19009
7
7
  iints/ai/mdmp_guard.py,sha256=BpFQX0oyP9WMCUZbFhhoBzomNeVKuI1HY1EFH9cG8EE,4249
8
8
  iints/ai/model_catalog.py,sha256=gRW-i4eaXkrjX3mIKJlGzHqzU75lpIulEFKQsCX11CI,1804
9
9
  iints/ai/prepare.py,sha256=z3y5elCAMv0p_aNq4gQfZA1uIT7_cX3FGRdzmoZoKho,12967
@@ -11,7 +11,7 @@ iints/ai/prompts.py,sha256=pGp9tC1wBZXGG5duxfktaJEF4p_cvmR0zEIxmMTEAyE,2812
11
11
  iints/ai/backends/__init__.py,sha256=EAJRZS8G0DK7fffw_LHio9DkyYHwtzvz2Jo7AXk7pk4,303
12
12
  iints/ai/backends/base.py,sha256=BLgP03X-jebYkF9D5n5crawoPBmy3RSh4q3jaT8a9XM,274
13
13
  iints/ai/backends/mistral_api.py,sha256=dousHnzgzuik49822H8nCclYv5NoxHpTMLwtZPVj_TM,507
14
- iints/ai/backends/ollama.py,sha256=pH_UkrCmm6jI4_Hl-NM_7W4tXOwHZdZ0heeKph72NUk,8739
14
+ iints/ai/backends/ollama.py,sha256=3VWi37ueh7oeNK6tPrq3Ks8gbfQhzgNlMHHnV_6BxAY,12189
15
15
  iints/analysis/__init__.py,sha256=Qx49KDy0deoxSnVORJB10_BsdezZXKsuoXTR0KZRcqg,411
16
16
  iints/analysis/algorithm_xray.py,sha256=-AtXkZsgnsiFQ_K-IozjIDWkq-dDn0i0zmqWVMhINP4,15952
17
17
  iints/analysis/baseline.py,sha256=PCFVb5vX0lYKChZvVk-8I_B5NLQQwGyx7Y6M3XjpIEY,3458
@@ -142,9 +142,9 @@ iints/validation/schemas.py,sha256=uXhiPxyfyvOgCA83ZPBIzlITOu663fWctYxOMXUyf1I,4
142
142
  iints/visualization/__init__.py,sha256=OdxVHDpY-9bDt8DTWWd-dspn1p0O9T908Cck-IGFaiM,640
143
143
  iints/visualization/cockpit.py,sha256=Y7hoJXcTEWQ8yLiU5X5abT58uqGGsQllftXJwqerG1E,25057
144
144
  iints/visualization/uncertainty_cloud.py,sha256=I5nNzSitgai21rkul31YNtJriSEmCeTsW0GWW2HUskY,19848
145
- iints_sdk_python35-1.1.2.dist-info/licenses/LICENSE,sha256=b1luljj2mWWDW10t_qFIqd9Z6euXAcDBmIXowWuUlm4,1417
146
- iints_sdk_python35-1.1.2.dist-info/METADATA,sha256=F-4k2b--KpKAGQpS6Kw8LP_AW3A1zzqATS7lgs_Mx8I,8887
147
- iints_sdk_python35-1.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
148
- iints_sdk_python35-1.1.2.dist-info/entry_points.txt,sha256=aVioeLytTHG7WM7L3LIZ6XDJCKiSfqG-nVUQDVHPpQk,578
149
- iints_sdk_python35-1.1.2.dist-info/top_level.txt,sha256=7Usr6NQKiC9SpNFyCis81MmgXy71lDCr5unR8BNXZ0E,6
150
- iints_sdk_python35-1.1.2.dist-info/RECORD,,
145
+ iints_sdk_python35-1.1.3.dist-info/licenses/LICENSE,sha256=b1luljj2mWWDW10t_qFIqd9Z6euXAcDBmIXowWuUlm4,1417
146
+ iints_sdk_python35-1.1.3.dist-info/METADATA,sha256=5gDxBH6JpAOqvlisyv6aOye2tS4YRLBuybhz2-Oq7H0,9188
147
+ iints_sdk_python35-1.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
148
+ iints_sdk_python35-1.1.3.dist-info/entry_points.txt,sha256=aVioeLytTHG7WM7L3LIZ6XDJCKiSfqG-nVUQDVHPpQk,578
149
+ iints_sdk_python35-1.1.3.dist-info/top_level.txt,sha256=7Usr6NQKiC9SpNFyCis81MmgXy71lDCr5unR8BNXZ0E,6
150
+ iints_sdk_python35-1.1.3.dist-info/RECORD,,