kgmodule-utils 0.4.1__tar.gz → 0.4.3__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.1 → kgmodule_utils-0.4.3}/PKG-INFO +1 -1
  2. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/pyproject.toml +1 -1
  3. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/__init__.py +3 -1
  4. kgmodule_utils-0.4.3/src/kg_utils/retrieval/__init__.py +5 -0
  5. kgmodule_utils-0.4.3/src/kg_utils/retrieval/hits.py +75 -0
  6. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/__init__.py +8 -0
  7. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/_image.py +38 -3
  8. kgmodule_utils-0.4.3/src/kg_utils/synthesis/factory.py +97 -0
  9. kgmodule_utils-0.4.3/src/kg_utils/worker/__init__.py +17 -0
  10. kgmodule_utils-0.4.3/src/kg_utils/worker/client.py +204 -0
  11. kgmodule_utils-0.4.3/src/kg_utils/worker/ops.py +87 -0
  12. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/LICENSE +0 -0
  13. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/README.md +0 -0
  14. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/embed.py +0 -0
  15. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/embedder.py +0 -0
  16. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/extractor.py +0 -0
  17. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/module.py +0 -0
  18. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/pipeline.py +0 -0
  19. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/py.typed +0 -0
  20. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/semantic.py +0 -0
  21. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/snapshots/__init__.py +0 -0
  22. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/snapshots/manager.py +0 -0
  23. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/snapshots/models.py +0 -0
  24. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/specs.py +0 -0
  25. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/store.py +0 -0
  26. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/_config.py +0 -0
  27. {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/_text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kgmodule-utils
3
- Version: 0.4.1
3
+ Version: 0.4.3
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.1"
13
+ version = "0.4.3"
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" }
@@ -14,6 +14,8 @@ Sub-packages / modules:
14
14
  kg_utils.synthesis — Unified text + image synthesis: TextSynthesizer, ImageSynthesizer.
15
15
  Backends: omlx | ollama | openai (text);
16
16
  mflux-local | mflux-serve | openai (image).
17
+ kg_utils.worker — RunPod worker protocol helpers and WorkerClient for /runsync calls.
18
+ kg_utils.retrieval — Shared retrieval helpers: hit_to_dict, attach_content_by_sqlite.
17
19
 
18
20
  Optional extras
19
21
  ---------------
@@ -22,4 +24,4 @@ Optional extras
22
24
  pip install 'kgmodule-utils[synthesis-mflux]' # + mflux (Apple Silicon local gen)
23
25
  """
24
26
 
25
- __version__ = "0.4.1"
27
+ __version__ = "0.4.3"
@@ -0,0 +1,5 @@
1
+ """Shared retrieval helpers for serializing and enriching KG hits."""
2
+
3
+ from kg_utils.retrieval.hits import attach_content_by_sqlite, hit_to_dict
4
+
5
+ __all__ = ["hit_to_dict", "attach_content_by_sqlite"]
@@ -0,0 +1,75 @@
1
+ # © 2026 Eric G. Suchanek, PhD — Flux-Frontiers · SPDX-License-Identifier: Elastic-2.0
2
+ """Hit serialization and content hydration helpers for KG retrieval responses."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sqlite3
7
+ from collections import defaultdict
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ __all__ = ["hit_to_dict", "attach_content_by_sqlite"]
12
+
13
+
14
+ def _is_diary_kind(kind_value: Any) -> bool:
15
+ kind_str = str(kind_value)
16
+ return kind_str == "KGKind.DIARY" or kind_str.lower().endswith("diary")
17
+
18
+
19
+ def hit_to_dict(hit: Any, include_diary_timestamp: bool = False) -> dict:
20
+ """Serialize a KGRAG hit object into a plain dictionary.
21
+
22
+ :param hit: Hit-like object with standard retrieval attributes.
23
+ :param include_diary_timestamp: Include ``timestamp`` field for diary hits.
24
+ :returns: Serialized hit dictionary.
25
+ """
26
+ out = {
27
+ "kg_name": hit.kg_name,
28
+ "kg_kind": str(hit.kg_kind),
29
+ "node_id": hit.node_id,
30
+ "name": hit.name,
31
+ "kind": hit.kind,
32
+ "score": round(float(hit.score), 4),
33
+ "summary": hit.summary,
34
+ "source_path": hit.source_path,
35
+ }
36
+ if include_diary_timestamp:
37
+ out["timestamp"] = hit.name if _is_diary_kind(hit.kg_kind) else None
38
+ return out
39
+
40
+
41
+ def attach_content_by_sqlite(hits: list[dict], kg_sqlite_map: dict[str, Path]) -> None:
42
+ """Attach full node text under ``content`` via batched SQLite lookups.
43
+
44
+ Missing or unreadable databases are ignored to preserve permissive behavior.
45
+
46
+ :param hits: Mutable hit dictionaries. Each hit should include ``kg_name`` and ``node_id``.
47
+ :param kg_sqlite_map: Mapping of KG name to sqlite database path.
48
+ """
49
+ by_kg: dict[str, list[dict]] = defaultdict(list)
50
+ for hit in hits:
51
+ by_kg[hit.get("kg_name", "")].append(hit)
52
+
53
+ for kg_name, kg_hits in by_kg.items():
54
+ db_path = kg_sqlite_map.get(kg_name)
55
+ if not db_path or not Path(db_path).exists():
56
+ continue
57
+
58
+ ids = [h.get("node_id") for h in kg_hits if h.get("node_id")]
59
+ if not ids:
60
+ continue
61
+
62
+ text_by_id: dict[str, str] = {}
63
+ try:
64
+ with sqlite3.connect(str(db_path)) as con:
65
+ placeholders = ",".join("?" * len(ids))
66
+ query = f"SELECT id, text FROM nodes WHERE id IN ({placeholders})"
67
+ for node_id, text in con.execute(query, ids):
68
+ text_by_id[node_id] = text or ""
69
+ except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught
70
+ continue
71
+
72
+ for hit in kg_hits:
73
+ node_id = hit.get("node_id")
74
+ if node_id:
75
+ hit["content"] = text_by_id.get(node_id, "")
@@ -53,6 +53,11 @@ from kg_utils.synthesis._config import (
53
53
  )
54
54
  from kg_utils.synthesis._image import ImageSynthesizer
55
55
  from kg_utils.synthesis._text import TextSynthesizer
56
+ from kg_utils.synthesis.factory import (
57
+ image_synth_for_backend,
58
+ normalize_openai_base_url,
59
+ text_synth_for_backend,
60
+ )
56
61
 
57
62
 
58
63
  def text_synthesizer_from_env() -> TextSynthesizer:
@@ -76,4 +81,7 @@ __all__ = [
76
81
  "image_synthesizer_from_env",
77
82
  "text_config_from_env",
78
83
  "image_config_from_env",
84
+ "normalize_openai_base_url",
85
+ "text_synth_for_backend",
86
+ "image_synth_for_backend",
79
87
  ]
@@ -51,6 +51,29 @@ _GPT_IMAGE_SIZES: dict[str, str] = {
51
51
  }
52
52
 
53
53
 
54
+ # ---------------------------------------------------------------------------
55
+ # Size parsing
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ def _parse_size(size: str | None) -> tuple[int, int] | None:
60
+ """Parse an explicit ``"WIDTHxHEIGHT"`` string into an ``(width, height)`` pair.
61
+
62
+ :param size: Size string such as ``"768x512"`` (case-insensitive ``x``), or None.
63
+ :returns: ``(width, height)`` when *size* parses to two positive ints, else None.
64
+ """
65
+ if not size:
66
+ return None
67
+ try:
68
+ w_str, h_str = size.lower().split("x", 1)
69
+ width, height = int(w_str), int(h_str)
70
+ except (ValueError, AttributeError):
71
+ return None
72
+ if width <= 0 or height <= 0:
73
+ return None
74
+ return width, height
75
+
76
+
54
77
  # ---------------------------------------------------------------------------
55
78
  # Synthesizer
56
79
  # ---------------------------------------------------------------------------
@@ -97,6 +120,7 @@ class ImageSynthesizer:
97
120
  seed: int | None = None,
98
121
  steps: int | None = None,
99
122
  model: str | None = None,
123
+ size: str | None = None,
100
124
  ) -> PILImage:
101
125
  """Generate an image and return a PIL Image.
102
126
 
@@ -105,6 +129,9 @@ class ImageSynthesizer:
105
129
  :param seed: Random seed for reproducibility (random int if omitted).
106
130
  :param steps: Override inference steps (mflux backends only; ignored for OpenAI).
107
131
  :param model: Override the configured model for this single call.
132
+ :param size: Explicit ``"WIDTHxHEIGHT"`` override (mflux backends only). When given
133
+ and valid, it takes priority over the *aspect_ratio* size table. OpenAI
134
+ backends ignore it because they accept only a fixed set of sizes.
108
135
  :returns: PIL Image.
109
136
  """
110
137
  cfg = self._cfg
@@ -118,6 +145,7 @@ class ImageSynthesizer:
118
145
  seed=seed,
119
146
  steps=effective_steps,
120
147
  aspect_ratio=aspect_ratio,
148
+ size=size,
121
149
  )
122
150
  elif cfg.backend == ImageBackend.MFLUX_SERVE:
123
151
  return self._generate_via_server(
@@ -126,6 +154,7 @@ class ImageSynthesizer:
126
154
  seed=seed,
127
155
  steps=effective_steps,
128
156
  aspect_ratio=aspect_ratio,
157
+ size=size,
129
158
  )
130
159
  else:
131
160
  return self._generate_openai(
@@ -142,6 +171,7 @@ class ImageSynthesizer:
142
171
  seed: int | None = None,
143
172
  steps: int | None = None,
144
173
  model: str | None = None,
174
+ size: str | None = None,
145
175
  ) -> str:
146
176
  """Generate an image and return it as a base64-encoded PNG string.
147
177
 
@@ -152,9 +182,12 @@ class ImageSynthesizer:
152
182
  :param seed: Random seed for reproducibility.
153
183
  :param steps: Override inference steps (mflux backends only).
154
184
  :param model: Override the configured model for this single call.
185
+ :param size: Explicit ``"WIDTHxHEIGHT"`` override (mflux backends only).
155
186
  :returns: Base64-encoded PNG string.
156
187
  """
157
- img = self.generate(prompt, aspect_ratio=aspect_ratio, seed=seed, steps=steps, model=model)
188
+ img = self.generate(
189
+ prompt, aspect_ratio=aspect_ratio, seed=seed, steps=steps, model=model, size=size
190
+ )
158
191
  buf = BytesIO()
159
192
  img.save(buf, format="PNG")
160
193
  return base64.b64encode(buf.getvalue()).decode()
@@ -171,8 +204,9 @@ class ImageSynthesizer:
171
204
  seed: int | None,
172
205
  steps: int,
173
206
  aspect_ratio: str,
207
+ size: str | None = None,
174
208
  ) -> PILImage:
175
- width, height = _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
209
+ width, height = _parse_size(size) or _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
176
210
  effective_seed = seed if seed is not None else random.randint(0, 2**31 - 1)
177
211
  flux = self._load_mflux(model)
178
212
  result = flux.generate_image(
@@ -194,11 +228,12 @@ class ImageSynthesizer:
194
228
  seed: int | None,
195
229
  steps: int,
196
230
  aspect_ratio: str,
231
+ size: str | None = None,
197
232
  ) -> PILImage:
198
233
  import httpx
199
234
  from PIL import Image # type: ignore[import-unresolved]
200
235
 
201
- width, height = _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
236
+ width, height = _parse_size(size) or _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
202
237
  payload: dict[str, Any] = {
203
238
  "prompt": prompt,
204
239
  "n": 1,
@@ -0,0 +1,97 @@
1
+ # © 2026 Eric G. Suchanek, PhD — Flux-Frontiers · SPDX-License-Identifier: Elastic-2.0
2
+ """Synthesis backend factory helpers for per-request backend overrides."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+
8
+ from kg_utils.synthesis._config import (
9
+ ImageBackend,
10
+ ImageConfig,
11
+ TextBackend,
12
+ TextConfig,
13
+ )
14
+ from kg_utils.synthesis._image import ImageSynthesizer
15
+ from kg_utils.synthesis._text import TextSynthesizer
16
+
17
+ __all__ = [
18
+ "normalize_openai_base_url",
19
+ "text_synth_for_backend",
20
+ "image_synth_for_backend",
21
+ ]
22
+
23
+
24
+ def normalize_openai_base_url(endpoint: str) -> str:
25
+ """Normalize an OpenAI-wire endpoint so it ends with /v1.
26
+
27
+ Returns an empty string when endpoint is empty.
28
+ """
29
+ ep = (endpoint or "").strip().rstrip("/")
30
+ if not ep:
31
+ return ""
32
+ if ep.endswith("/v1"):
33
+ return ep
34
+ return f"{ep}/v1"
35
+
36
+
37
+ def text_synth_for_backend(backend: str, fallback: TextSynthesizer) -> TextSynthesizer:
38
+ """Return a TextSynthesizer configured for a specific backend override.
39
+
40
+ Unknown or empty backend strings return ``fallback``.
41
+ """
42
+ backend_str = (backend or "").strip().lower()
43
+ if not backend_str:
44
+ return fallback
45
+
46
+ try:
47
+ selected = TextBackend(backend_str)
48
+ except ValueError:
49
+ return fallback
50
+
51
+ if selected == TextBackend.OMLX:
52
+ endpoint = os.environ.get("SYNTH_ENDPOINT") or os.environ.get("VLLM_ENDPOINT_URL") or ""
53
+ endpoint = normalize_openai_base_url(endpoint)
54
+ api_key = os.environ.get("SYNTH_API_KEY") or os.environ.get("VLLM_API_KEY") or ""
55
+ model = os.environ.get("SYNTH_MODEL") or os.environ.get("VLLM_MODEL") or ""
56
+ return TextSynthesizer(
57
+ TextConfig(backend=selected, endpoint=endpoint, api_key=api_key, model=model)
58
+ )
59
+
60
+ if selected == TextBackend.OLLAMA:
61
+ endpoint = os.environ.get("OLLAMA_ENDPOINT") or ""
62
+ return TextSynthesizer(TextConfig(backend=selected, endpoint=endpoint))
63
+
64
+ if selected == TextBackend.OPENAI:
65
+ api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("SYNTH_API_KEY") or ""
66
+ return TextSynthesizer(TextConfig(backend=selected, api_key=api_key))
67
+
68
+ return fallback
69
+
70
+
71
+ def image_synth_for_backend(backend: str, fallback: ImageSynthesizer) -> ImageSynthesizer:
72
+ """Return an ImageSynthesizer configured for a specific backend override.
73
+
74
+ Unknown or empty backend strings return ``fallback``.
75
+ """
76
+ backend_str = (backend or "").strip().lower()
77
+ if not backend_str:
78
+ return fallback
79
+
80
+ try:
81
+ selected = ImageBackend(backend_str)
82
+ except ValueError:
83
+ return fallback
84
+
85
+ if selected == ImageBackend.OPENAI:
86
+ api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("IMAGE_API_KEY") or ""
87
+ return ImageSynthesizer(ImageConfig(backend=selected, api_key=api_key))
88
+
89
+ if selected == ImageBackend.MFLUX_SERVE:
90
+ server_url = os.environ.get("IMAGE_ENDPOINT") or ""
91
+ return ImageSynthesizer(ImageConfig(backend=selected, server_url=server_url))
92
+
93
+ if selected == ImageBackend.MFLUX_LOCAL:
94
+ model = os.environ.get("IMAGE_MODEL") or os.environ.get("GUTENKG_IMAGE_MODEL") or ""
95
+ return ImageSynthesizer(ImageConfig(backend=selected, model=model))
96
+
97
+ return fallback
@@ -0,0 +1,17 @@
1
+ """Worker protocol helpers and client for RunPod ``/runsync`` endpoints."""
2
+
3
+ from kg_utils.worker.client import (
4
+ WorkerClient,
5
+ WorkerError,
6
+ decode_worker_response,
7
+ extract_worker_error,
8
+ )
9
+ from kg_utils.worker.ops import handle_aux_ops
10
+
11
+ __all__ = [
12
+ "WorkerClient",
13
+ "WorkerError",
14
+ "decode_worker_response",
15
+ "extract_worker_error",
16
+ "handle_aux_ops",
17
+ ]
@@ -0,0 +1,204 @@
1
+ # © 2026 Eric G. Suchanek, PhD — Flux-Frontiers · SPDX-License-Identifier: Elastic-2.0
2
+ """RunPod worker client utilities for chat and handler front-ends.
3
+
4
+ This module centralizes payload construction and response/error decoding for
5
+ ``/runsync`` worker calls used by Streamlit clients.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, cast
12
+
13
+ import httpx
14
+
15
+
16
+ class WorkerError(Exception):
17
+ """Raised when a worker response contains a structured application-level error."""
18
+
19
+
20
+ def _format_error_data(error_data: object) -> str:
21
+ if isinstance(error_data, str):
22
+ try:
23
+ decoded = json.loads(error_data)
24
+ except (ValueError, TypeError):
25
+ return error_data
26
+ if isinstance(decoded, dict):
27
+ err_type = decoded.get("error_type", "Unknown")
28
+ err_msg = decoded.get("error_message", str(decoded))
29
+ return f"{err_type}: {err_msg}"
30
+ return str(decoded)
31
+
32
+ if isinstance(error_data, dict):
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))
36
+ return f"{err_type}: {err_msg}"
37
+
38
+ return str(error_data)
39
+
40
+
41
+ def extract_worker_error(data: object) -> str | None:
42
+ """Extract a readable worker error from a raw RunPod response payload."""
43
+ if not isinstance(data, dict):
44
+ return str(data)
45
+
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))
49
+
50
+ out = d.get("output")
51
+ if isinstance(out, dict) and isinstance(out.get("error"), str):
52
+ return out["error"]
53
+
54
+ return None
55
+
56
+
57
+ def decode_worker_response(data: object) -> dict:
58
+ """Decode a worker response payload and raise WorkerError on application errors."""
59
+ error = extract_worker_error(data)
60
+ if error:
61
+ raise WorkerError(error)
62
+
63
+ if not isinstance(data, dict):
64
+ raise WorkerError(f"unexpected worker response type: {type(data).__name__}")
65
+
66
+ d2 = cast("dict[str, Any]", data)
67
+ out = d2.get("output", d2)
68
+ if not isinstance(out, dict):
69
+ raise WorkerError(f"unexpected worker output type: {type(out).__name__}")
70
+ return out
71
+
72
+
73
+ class WorkerClient:
74
+ """Small client for RunPod ``/runsync`` worker endpoints."""
75
+
76
+ def __init__(self, base_url: str, secret: str = "") -> None:
77
+ self._base_url = base_url.rstrip("/")
78
+ self._secret = secret
79
+
80
+ def _post(self, payload: dict, timeout: httpx.Timeout) -> dict:
81
+ resp = httpx.post(f"{self._base_url}/runsync", json=payload, timeout=timeout)
82
+ resp.raise_for_status()
83
+ return resp.json()
84
+
85
+ def list_models(self, backend: str = "") -> tuple[list[str], str]:
86
+ payload: dict = {"input": {"op": "models"}}
87
+ if backend:
88
+ payload["input"]["backend"] = backend
89
+ if self._secret:
90
+ payload["input"]["secret"] = self._secret
91
+
92
+ try:
93
+ data = self._post(
94
+ payload,
95
+ timeout=httpx.Timeout(connect=5.0, read=20.0, write=5.0, pool=5.0),
96
+ )
97
+ out = data.get("output", {}) if isinstance(data, dict) else {}
98
+ if not isinstance(out, dict):
99
+ return [], ""
100
+ return out.get("models", []), out.get("default", "")
101
+ except Exception: # noqa: BLE001
102
+ return [], ""
103
+
104
+ def rewrite(
105
+ self,
106
+ text: str,
107
+ backend: str = "",
108
+ model: str = "",
109
+ ) -> tuple[str, str | None]:
110
+ payload: dict = {"input": {"op": "rewrite", "text": text}}
111
+ if backend:
112
+ payload["input"]["backend"] = backend
113
+ if model:
114
+ payload["input"]["model"] = model
115
+ if self._secret:
116
+ payload["input"]["secret"] = self._secret
117
+
118
+ try:
119
+ data = self._post(
120
+ payload,
121
+ timeout=httpx.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0),
122
+ )
123
+ err = extract_worker_error(data)
124
+ if err:
125
+ return text, err
126
+ out = data.get("output", {}) if isinstance(data, dict) else {}
127
+ if not isinstance(out, dict):
128
+ return text, "unexpected worker output"
129
+ return out.get("prompt", text), out.get("error")
130
+ except Exception as exc: # noqa: BLE001
131
+ return text, str(exc)
132
+
133
+ def imagine(
134
+ self,
135
+ prompt: str,
136
+ *,
137
+ image_backend: str = "",
138
+ aspect_ratio: str = "3:2",
139
+ steps: int | None = None,
140
+ size: str | None = None,
141
+ ) -> tuple[str | None, str | None, str | None, str | None]:
142
+ inp: dict[str, Any] = {"op": "imagine", "prompt": prompt, "aspect_ratio": aspect_ratio}
143
+ if image_backend:
144
+ inp["image_backend"] = image_backend
145
+ if steps is not None:
146
+ inp["steps"] = steps
147
+ if size:
148
+ inp["size"] = size
149
+ if self._secret:
150
+ inp["secret"] = self._secret
151
+ payload: dict[str, Any] = {"input": inp}
152
+
153
+ try:
154
+ data = self._post(
155
+ payload,
156
+ timeout=httpx.Timeout(connect=5.0, read=300.0, write=10.0, pool=5.0),
157
+ )
158
+ err = extract_worker_error(data)
159
+ if err:
160
+ return None, None, None, err
161
+
162
+ out = data.get("output", {}) if isinstance(data, dict) else {}
163
+ if not isinstance(out, dict):
164
+ return None, None, None, "unexpected worker output"
165
+ if "error" in out:
166
+ return None, None, None, str(out["error"])
167
+ return out.get("image_b64"), out.get("image_model"), out.get("image_backend"), None
168
+ except Exception as exc: # noqa: BLE001
169
+ return None, None, None, str(exc)
170
+
171
+ def query(
172
+ self,
173
+ query: str,
174
+ *,
175
+ corpus: str = "all",
176
+ k: int = 8,
177
+ min_score: float = 0.0,
178
+ semantic_floor: float = 0.0,
179
+ synthesize: bool = False,
180
+ model: str = "",
181
+ backend: str = "",
182
+ ) -> dict:
183
+ payload: dict = {
184
+ "input": {
185
+ "query": query,
186
+ "corpus": corpus,
187
+ "k": k,
188
+ "min_score": min_score,
189
+ "semantic_floor": semantic_floor,
190
+ "synthesize": synthesize,
191
+ }
192
+ }
193
+ if model:
194
+ payload["input"]["model"] = model
195
+ if backend:
196
+ payload["input"]["backend"] = backend
197
+ if self._secret:
198
+ payload["input"]["secret"] = self._secret
199
+
200
+ data = self._post(
201
+ payload,
202
+ timeout=httpx.Timeout(connect=5.0, read=600.0, write=30.0, pool=5.0),
203
+ )
204
+ return decode_worker_response(data)
@@ -0,0 +1,87 @@
1
+ # © 2026 Eric G. Suchanek, PhD — Flux-Frontiers · SPDX-License-Identifier: Elastic-2.0
2
+ """Shared handler operation dispatch for models, rewrite, and imagine."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+
8
+ from kg_utils.synthesis._image import ImageSynthesizer
9
+ from kg_utils.synthesis._text import TextSynthesizer
10
+
11
+ __all__ = ["handle_aux_ops"]
12
+
13
+
14
+ def handle_aux_ops(
15
+ inp: dict,
16
+ text_synth_factory: Callable[[str], TextSynthesizer],
17
+ image_synth_factory: Callable[[str], ImageSynthesizer],
18
+ ) -> dict | None:
19
+ """Handle shared non-query worker operations.
20
+
21
+ Returns:
22
+ - operation payload dict when op is recognized
23
+ - ``None`` when input has no recognized operation
24
+ """
25
+ op = inp.get("op")
26
+
27
+ if op == "models":
28
+ synth = text_synth_factory(inp.get("backend", ""))
29
+ # Existing handlers expose the active model via synthesizer config internals.
30
+ return {
31
+ "models": synth.list_models(),
32
+ "default": synth._cfg.resolved_model(), # pylint: disable=protected-access
33
+ }
34
+
35
+ if op == "rewrite":
36
+ text = (inp.get("text") or "").strip()
37
+ if not text:
38
+ return {"error": "rewrite requires a non-empty 'text'"}
39
+
40
+ synth = text_synth_factory(inp.get("backend", ""))
41
+ model_override = (inp.get("model") or "").strip() or None
42
+ prompt, error = synth.rewrite_for_image(text, model=model_override)
43
+ return {"prompt": prompt, "error": error}
44
+
45
+ if op == "imagine":
46
+ prompt = (inp.get("prompt") or "").strip()
47
+ if not prompt:
48
+ return {"error": "imagine requires a non-empty 'prompt'"}
49
+
50
+ aspect = inp.get("aspect_ratio", "3:2")
51
+ size = inp.get("size")
52
+ seed = inp.get("seed")
53
+ steps = inp.get("steps")
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
57
+
58
+ try:
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 = {
75
+ "image_b64": b64,
76
+ "prompt": prompt,
77
+ "aspect_ratio": aspect,
78
+ "image_model": img_synth._cfg.resolved_model(), # pylint: disable=protected-access
79
+ "image_backend": img_synth._cfg.backend.value, # pylint: disable=protected-access
80
+ }
81
+ if size is not None:
82
+ result["size"] = size
83
+ return result
84
+ except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught
85
+ return {"error": f"image generation failed: {exc}"}
86
+
87
+ return None
File without changes
File without changes