kgmodule-utils 0.4.2__tar.gz → 0.4.4__tar.gz

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.
Files changed (27) hide show
  1. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/PKG-INFO +1 -1
  2. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/pyproject.toml +1 -1
  3. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/__init__.py +1 -1
  4. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/embedder.py +29 -11
  5. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/_image.py +43 -4
  6. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/worker/client.py +18 -10
  7. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/worker/ops.py +22 -7
  8. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/LICENSE +0 -0
  9. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/README.md +0 -0
  10. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/embed.py +0 -0
  11. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/extractor.py +0 -0
  12. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/module.py +0 -0
  13. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/pipeline.py +0 -0
  14. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/py.typed +0 -0
  15. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/retrieval/__init__.py +0 -0
  16. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/retrieval/hits.py +0 -0
  17. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/semantic.py +0 -0
  18. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/snapshots/__init__.py +0 -0
  19. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/snapshots/manager.py +0 -0
  20. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/snapshots/models.py +0 -0
  21. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/specs.py +0 -0
  22. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/store.py +0 -0
  23. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/__init__.py +0 -0
  24. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/_config.py +0 -0
  25. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/_text.py +0 -0
  26. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/factory.py +0 -0
  27. {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/worker/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kgmodule-utils
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Shared types, graph store, semantic index, and pipeline base for the KGModule SDK
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -10,7 +10,7 @@ build-backend = "poetry.core.masonry.api"
10
10
 
11
11
  [project]
12
12
  name = "kgmodule-utils"
13
- version = "0.4.2"
13
+ version = "0.4.4"
14
14
  description = "Shared types, graph store, semantic index, and pipeline base for the KGModule SDK"
15
15
  readme = "README.md"
16
16
  license = { text = "Elastic-2.0" }
@@ -24,4 +24,4 @@ Optional extras
24
24
  pip install 'kgmodule-utils[synthesis-mflux]' # + mflux (Apple Silicon local gen)
25
25
  """
26
26
 
27
- __version__ = "0.4.2"
27
+ __version__ = "0.4.4"
@@ -34,12 +34,12 @@ License: Elastic 2.0
34
34
 
35
35
  from __future__ import annotations
36
36
 
37
+ import importlib
37
38
  import os
38
39
  from typing import Any
39
40
 
40
41
  from kg_utils.embed import DEFAULT_MODEL, KNOWN_MODELS, resolve_model_path
41
42
 
42
-
43
43
  # ---------------------------------------------------------------------------
44
44
  # Abstract base
45
45
  # ---------------------------------------------------------------------------
@@ -75,7 +75,7 @@ class Embedder:
75
75
  # ---------------------------------------------------------------------------
76
76
 
77
77
 
78
- def load_sentence_transformer(model_name: str = DEFAULT_MODEL) -> Any:
78
+ def load_sentence_transformer(model_name: str = DEFAULT_MODEL, device: str | None = None) -> Any:
79
79
  """Load a ``SentenceTransformer`` with the canonical safe-load sequence.
80
80
 
81
81
  Resolution order:
@@ -90,13 +90,22 @@ def load_sentence_transformer(model_name: str = DEFAULT_MODEL) -> Any:
90
90
  retry loops leave stale thread state that causes SIGBUS on the first
91
91
  ``encode()`` call.
92
92
 
93
+ Device precedence: explicit *device* arg > ``KG_EMBED_DEVICE`` env >
94
+ auto-detect. The env var exists because ``spawn``-based embedding workers
95
+ inherit ``os.environ`` but can't easily receive a Python arg — without a way
96
+ to pin the device, each worker auto-selects MPS and N parallel workers stack
97
+ N GPU allocations into an OOM. So CPU multiprocessing embedding on Apple
98
+ Silicon is only safe with this knob.
99
+
93
100
  :param model_name: HuggingFace model ID or KNOWN_MODELS alias.
101
+ :param device: Explicit device (``"cpu"``/``"mps"``/``"cuda"``). ``None``
102
+ falls back to ``KG_EMBED_DEVICE`` then CUDA→MPS→CPU auto-detect.
94
103
  :return: Loaded ``SentenceTransformer`` instance.
95
104
  """
96
- from sentence_transformers import SentenceTransformer # pylint: disable=import-outside-toplevel
105
+ SentenceTransformer = importlib.import_module("sentence_transformers").SentenceTransformer
97
106
 
98
107
  try:
99
- from transformers import logging as hf_logging # pylint: disable=import-outside-toplevel
108
+ hf_logging = importlib.import_module("transformers.logging")
100
109
 
101
110
  hf_logging.set_verbosity_error()
102
111
  # TQDM_DISABLE alone misses transformers' _tqdm_active gate
@@ -106,9 +115,18 @@ def load_sentence_transformer(model_name: str = DEFAULT_MODEL) -> Any:
106
115
 
107
116
  os.environ["TQDM_DISABLE"] = "1"
108
117
 
109
- import torch # pylint: disable=import-outside-toplevel
110
-
111
- if torch.cuda.is_available():
118
+ torch = importlib.import_module("torch")
119
+
120
+ # Device precedence: explicit arg > KG_EMBED_DEVICE env > auto-detect.
121
+ # The env var lets spawn-based embedding workers (which inherit os.environ
122
+ # but can't easily receive a Python arg) pin to e.g. CPU — without it each
123
+ # worker auto-selects MPS and N parallel workers stack N GPU allocations →
124
+ # MPS OOM. The override is why CPU multiprocessing embedding is safe on
125
+ # Apple Silicon.
126
+ sel = (device or os.environ.get("KG_EMBED_DEVICE", "")).strip().lower()
127
+ if sel:
128
+ device = sel
129
+ elif torch.cuda.is_available():
112
130
  device = "cuda"
113
131
  else:
114
132
  try:
@@ -158,7 +176,7 @@ class SentenceTransformerEmbedder(Embedder):
158
176
 
159
177
  def __init__(self, model_name: str = DEFAULT_MODEL) -> None:
160
178
  try:
161
- from transformers import logging as hf_logging # pylint: disable=import-outside-toplevel
179
+ hf_logging = importlib.import_module("transformers.logging")
162
180
 
163
181
  hf_logging.set_verbosity_error()
164
182
  hf_logging.disable_progress_bar()
@@ -188,7 +206,7 @@ class SentenceTransformerEmbedder(Embedder):
188
206
  :param texts: Input strings.
189
207
  :param encode_batch_size: Passed to ``model.encode()`` — tune down if OOM on MPS.
190
208
  """
191
- import numpy as np # pylint: disable=import-outside-toplevel
209
+ np = importlib.import_module("numpy")
192
210
 
193
211
  vecs = self.model.encode(
194
212
  texts,
@@ -200,7 +218,7 @@ class SentenceTransformerEmbedder(Embedder):
200
218
 
201
219
  def embed_query(self, query: str) -> list[float]:
202
220
  """Embed a single query string into a float32 vector."""
203
- import numpy as np # pylint: disable=import-outside-toplevel
221
+ np = importlib.import_module("numpy")
204
222
 
205
223
  vec = self.model.encode([query], normalize_embeddings=True)[0]
206
224
  return list(np.asarray(vec, dtype="float32").tolist())
@@ -233,7 +251,7 @@ def wrap_embedder(st_model: Any, model_name: str = DEFAULT_MODEL) -> Embedder:
233
251
  :param model_name: Model name stored as metadata on the wrapper.
234
252
  :return: An :class:`Embedder` that delegates all calls to *st_model*.
235
253
  """
236
- import numpy as np # pylint: disable=import-outside-toplevel
254
+ np = importlib.import_module("numpy")
237
255
 
238
256
  resolved = KNOWN_MODELS.get(model_name, model_name)
239
257
  _dim_fn = getattr(st_model, "get_embedding_dimension", None) or getattr(
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import base64
7
+ import importlib
7
8
  import random
8
9
  from io import BytesIO
9
10
  from typing import TYPE_CHECKING, Any
@@ -51,6 +52,29 @@ _GPT_IMAGE_SIZES: dict[str, str] = {
51
52
  }
52
53
 
53
54
 
55
+ # ---------------------------------------------------------------------------
56
+ # Size parsing
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ def _parse_size(size: str | None) -> tuple[int, int] | None:
61
+ """Parse an explicit ``"WIDTHxHEIGHT"`` string into an ``(width, height)`` pair.
62
+
63
+ :param size: Size string such as ``"768x512"`` (case-insensitive ``x``), or None.
64
+ :returns: ``(width, height)`` when *size* parses to two positive ints, else None.
65
+ """
66
+ if not size:
67
+ return None
68
+ try:
69
+ w_str, h_str = size.lower().split("x", 1)
70
+ width, height = int(w_str), int(h_str)
71
+ except (ValueError, AttributeError):
72
+ return None
73
+ if width <= 0 or height <= 0:
74
+ return None
75
+ return width, height
76
+
77
+
54
78
  # ---------------------------------------------------------------------------
55
79
  # Synthesizer
56
80
  # ---------------------------------------------------------------------------
@@ -79,7 +103,10 @@ class ImageSynthesizer:
79
103
  def _load_mflux(self, model_name: str) -> Any:
80
104
  if self._mflux_model is not None and self._mflux_model_name == model_name:
81
105
  return self._mflux_model
82
- from mflux.models.flux2.variants.txt2img.flux2_klein import Flux2Klein # type: ignore
106
+
107
+ Flux2Klein = importlib.import_module(
108
+ "mflux.models.flux2.variants.txt2img.flux2_klein"
109
+ ).Flux2Klein
83
110
 
84
111
  self._mflux_model = Flux2Klein(model_path=model_name)
85
112
  self._mflux_model_name = model_name
@@ -97,6 +124,7 @@ class ImageSynthesizer:
97
124
  seed: int | None = None,
98
125
  steps: int | None = None,
99
126
  model: str | None = None,
127
+ size: str | None = None,
100
128
  ) -> PILImage:
101
129
  """Generate an image and return a PIL Image.
102
130
 
@@ -105,6 +133,9 @@ class ImageSynthesizer:
105
133
  :param seed: Random seed for reproducibility (random int if omitted).
106
134
  :param steps: Override inference steps (mflux backends only; ignored for OpenAI).
107
135
  :param model: Override the configured model for this single call.
136
+ :param size: Explicit ``"WIDTHxHEIGHT"`` override (mflux backends only). When given
137
+ and valid, it takes priority over the *aspect_ratio* size table. OpenAI
138
+ backends ignore it because they accept only a fixed set of sizes.
108
139
  :returns: PIL Image.
109
140
  """
110
141
  cfg = self._cfg
@@ -118,6 +149,7 @@ class ImageSynthesizer:
118
149
  seed=seed,
119
150
  steps=effective_steps,
120
151
  aspect_ratio=aspect_ratio,
152
+ size=size,
121
153
  )
122
154
  elif cfg.backend == ImageBackend.MFLUX_SERVE:
123
155
  return self._generate_via_server(
@@ -126,6 +158,7 @@ class ImageSynthesizer:
126
158
  seed=seed,
127
159
  steps=effective_steps,
128
160
  aspect_ratio=aspect_ratio,
161
+ size=size,
129
162
  )
130
163
  else:
131
164
  return self._generate_openai(
@@ -142,6 +175,7 @@ class ImageSynthesizer:
142
175
  seed: int | None = None,
143
176
  steps: int | None = None,
144
177
  model: str | None = None,
178
+ size: str | None = None,
145
179
  ) -> str:
146
180
  """Generate an image and return it as a base64-encoded PNG string.
147
181
 
@@ -152,9 +186,12 @@ class ImageSynthesizer:
152
186
  :param seed: Random seed for reproducibility.
153
187
  :param steps: Override inference steps (mflux backends only).
154
188
  :param model: Override the configured model for this single call.
189
+ :param size: Explicit ``"WIDTHxHEIGHT"`` override (mflux backends only).
155
190
  :returns: Base64-encoded PNG string.
156
191
  """
157
- img = self.generate(prompt, aspect_ratio=aspect_ratio, seed=seed, steps=steps, model=model)
192
+ img = self.generate(
193
+ prompt, aspect_ratio=aspect_ratio, seed=seed, steps=steps, model=model, size=size
194
+ )
158
195
  buf = BytesIO()
159
196
  img.save(buf, format="PNG")
160
197
  return base64.b64encode(buf.getvalue()).decode()
@@ -171,8 +208,9 @@ class ImageSynthesizer:
171
208
  seed: int | None,
172
209
  steps: int,
173
210
  aspect_ratio: str,
211
+ size: str | None = None,
174
212
  ) -> PILImage:
175
- width, height = _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
213
+ width, height = _parse_size(size) or _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
176
214
  effective_seed = seed if seed is not None else random.randint(0, 2**31 - 1)
177
215
  flux = self._load_mflux(model)
178
216
  result = flux.generate_image(
@@ -194,11 +232,12 @@ class ImageSynthesizer:
194
232
  seed: int | None,
195
233
  steps: int,
196
234
  aspect_ratio: str,
235
+ size: str | None = None,
197
236
  ) -> PILImage:
198
237
  import httpx
199
238
  from PIL import Image # type: ignore[import-unresolved]
200
239
 
201
- width, height = _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
240
+ width, height = _parse_size(size) or _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
202
241
  payload: dict[str, Any] = {
203
242
  "prompt": prompt,
204
243
  "n": 1,
@@ -8,6 +8,7 @@ This module centralizes payload construction and response/error decoding for
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ from typing import Any, cast
11
12
 
12
13
  import httpx
13
14
 
@@ -29,8 +30,9 @@ def _format_error_data(error_data: object) -> str:
29
30
  return str(decoded)
30
31
 
31
32
  if isinstance(error_data, dict):
32
- err_type = error_data.get("error_type", "Unknown")
33
- err_msg = error_data.get("error_message", str(error_data))
33
+ d = cast("dict[str, Any]", error_data)
34
+ err_type = d.get("error_type", "Unknown")
35
+ err_msg = d.get("error_message", str(error_data))
34
36
  return f"{err_type}: {err_msg}"
35
37
 
36
38
  return str(error_data)
@@ -41,10 +43,11 @@ def extract_worker_error(data: object) -> str | None:
41
43
  if not isinstance(data, dict):
42
44
  return str(data)
43
45
 
44
- if data.get("status") == "FAILED" or "error_type" in data:
45
- return _format_error_data(data.get("error", data))
46
+ d = cast("dict[str, Any]", data)
47
+ if d.get("status") == "FAILED" or "error_type" in d:
48
+ return _format_error_data(d.get("error", d))
46
49
 
47
- out = data.get("output")
50
+ out = d.get("output")
48
51
  if isinstance(out, dict) and isinstance(out.get("error"), str):
49
52
  return out["error"]
50
53
 
@@ -60,7 +63,8 @@ def decode_worker_response(data: object) -> dict:
60
63
  if not isinstance(data, dict):
61
64
  raise WorkerError(f"unexpected worker response type: {type(data).__name__}")
62
65
 
63
- out = data.get("output", data)
66
+ d2 = cast("dict[str, Any]", data)
67
+ out = d2.get("output", d2)
64
68
  if not isinstance(out, dict):
65
69
  raise WorkerError(f"unexpected worker output type: {type(out).__name__}")
66
70
  return out
@@ -133,14 +137,18 @@ class WorkerClient:
133
137
  image_backend: str = "",
134
138
  aspect_ratio: str = "3:2",
135
139
  steps: int | None = None,
140
+ size: str | None = None,
136
141
  ) -> tuple[str | None, str | None, str | None, str | None]:
137
- payload: dict = {"input": {"op": "imagine", "prompt": prompt, "aspect_ratio": aspect_ratio}}
142
+ inp: dict[str, Any] = {"op": "imagine", "prompt": prompt, "aspect_ratio": aspect_ratio}
138
143
  if image_backend:
139
- payload["input"]["image_backend"] = image_backend
144
+ inp["image_backend"] = image_backend
140
145
  if steps is not None:
141
- payload["input"]["steps"] = steps
146
+ inp["steps"] = steps
147
+ if size:
148
+ inp["size"] = size
142
149
  if self._secret:
143
- payload["input"]["secret"] = self._secret
150
+ inp["secret"] = self._secret
151
+ payload: dict[str, Any] = {"input": inp}
144
152
 
145
153
  try:
146
154
  data = self._post(
@@ -48,24 +48,39 @@ def handle_aux_ops(
48
48
  return {"error": "imagine requires a non-empty 'prompt'"}
49
49
 
50
50
  aspect = inp.get("aspect_ratio", "3:2")
51
+ size = inp.get("size")
51
52
  seed = inp.get("seed")
52
53
  steps = inp.get("steps")
53
54
  img_synth = image_synth_factory(inp.get("image_backend", ""))
55
+ seed_int = int(seed) if seed is not None else None
56
+ steps_int = int(steps) if steps is not None else None
54
57
 
55
58
  try:
56
- b64 = img_synth.generate_b64(
57
- prompt,
58
- aspect_ratio=aspect,
59
- seed=int(seed) if seed is not None else None,
60
- steps=int(steps) if steps is not None else None,
61
- )
62
- return {
59
+ if size is not None:
60
+ b64 = img_synth.generate_b64(
61
+ prompt,
62
+ aspect_ratio=aspect,
63
+ seed=seed_int,
64
+ steps=steps_int,
65
+ size=size,
66
+ )
67
+ else:
68
+ b64 = img_synth.generate_b64(
69
+ prompt,
70
+ aspect_ratio=aspect,
71
+ seed=seed_int,
72
+ steps=steps_int,
73
+ )
74
+ result = {
63
75
  "image_b64": b64,
64
76
  "prompt": prompt,
65
77
  "aspect_ratio": aspect,
66
78
  "image_model": img_synth._cfg.resolved_model(), # pylint: disable=protected-access
67
79
  "image_backend": img_synth._cfg.backend.value, # pylint: disable=protected-access
68
80
  }
81
+ if size is not None:
82
+ result["size"] = size
83
+ return result
69
84
  except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught
70
85
  return {"error": f"image generation failed: {exc}"}
71
86
 
File without changes
File without changes