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.
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/PKG-INFO +1 -1
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/pyproject.toml +1 -1
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/__init__.py +3 -1
- kgmodule_utils-0.4.3/src/kg_utils/retrieval/__init__.py +5 -0
- kgmodule_utils-0.4.3/src/kg_utils/retrieval/hits.py +75 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/__init__.py +8 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/_image.py +38 -3
- kgmodule_utils-0.4.3/src/kg_utils/synthesis/factory.py +97 -0
- kgmodule_utils-0.4.3/src/kg_utils/worker/__init__.py +17 -0
- kgmodule_utils-0.4.3/src/kg_utils/worker/client.py +204 -0
- kgmodule_utils-0.4.3/src/kg_utils/worker/ops.py +87 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/LICENSE +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/README.md +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/embed.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/embedder.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/extractor.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/module.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/pipeline.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/py.typed +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/semantic.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/snapshots/__init__.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/snapshots/manager.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/snapshots/models.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/specs.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/store.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/_config.py +0 -0
- {kgmodule_utils-0.4.1 → kgmodule_utils-0.4.3}/src/kg_utils/synthesis/_text.py +0 -0
|
@@ -10,7 +10,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "kgmodule-utils"
|
|
13
|
-
version = "0.4.
|
|
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.
|
|
27
|
+
__version__ = "0.4.3"
|
|
@@ -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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|