rfx-protocols-system 0.2.0__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 (42) hide show
  1. rfx_protocols_system-0.2.0/.github/workflows/publish.yml +65 -0
  2. rfx_protocols_system-0.2.0/.gitignore +43 -0
  3. rfx_protocols_system-0.2.0/LICENSE +21 -0
  4. rfx_protocols_system-0.2.0/PKG-INFO +100 -0
  5. rfx_protocols_system-0.2.0/README.md +58 -0
  6. rfx_protocols_system-0.2.0/pyproject.toml +100 -0
  7. rfx_protocols_system-0.2.0/src/protocols_system/__init__.py +28 -0
  8. rfx_protocols_system-0.2.0/src/protocols_system/core/__init__.py +32 -0
  9. rfx_protocols_system-0.2.0/src/protocols_system/core/discovery.py +199 -0
  10. rfx_protocols_system-0.2.0/src/protocols_system/core/llm_support.py +194 -0
  11. rfx_protocols_system-0.2.0/src/protocols_system/core/registry.py +178 -0
  12. rfx_protocols_system-0.2.0/src/protocols_system/core/types.py +42 -0
  13. rfx_protocols_system-0.2.0/src/protocols_system/core/validation.py +13 -0
  14. rfx_protocols_system-0.2.0/src/protocols_system/protocols/__init__.py +40 -0
  15. rfx_protocols_system-0.2.0/src/protocols_system/protocols/cache.py +25 -0
  16. rfx_protocols_system-0.2.0/src/protocols_system/protocols/embedding.py +25 -0
  17. rfx_protocols_system-0.2.0/src/protocols_system/protocols/geo.py +46 -0
  18. rfx_protocols_system-0.2.0/src/protocols_system/protocols/health.py +28 -0
  19. rfx_protocols_system-0.2.0/src/protocols_system/protocols/image_library.py +105 -0
  20. rfx_protocols_system-0.2.0/src/protocols_system/protocols/llm.py +17 -0
  21. rfx_protocols_system-0.2.0/src/protocols_system/protocols/progress.py +20 -0
  22. rfx_protocols_system-0.2.0/src/protocols_system/providers/__init__.py +53 -0
  23. rfx_protocols_system-0.2.0/src/protocols_system/providers/google_places.py +335 -0
  24. rfx_protocols_system-0.2.0/src/protocols_system/providers/huggingface_embedding.py +405 -0
  25. rfx_protocols_system-0.2.0/src/protocols_system/providers/image_embedding.py +281 -0
  26. rfx_protocols_system-0.2.0/src/protocols_system/providers/immich_client.py +540 -0
  27. rfx_protocols_system-0.2.0/src/protocols_system/providers/llamacpp.py +392 -0
  28. rfx_protocols_system-0.2.0/src/protocols_system/providers/local_cache.py +106 -0
  29. rfx_protocols_system-0.2.0/src/protocols_system/providers/local_image.py +179 -0
  30. rfx_protocols_system-0.2.0/src/protocols_system/providers/local_library.py +481 -0
  31. rfx_protocols_system-0.2.0/src/protocols_system/providers/mux_image.py +298 -0
  32. rfx_protocols_system-0.2.0/src/protocols_system/providers/ollama.py +260 -0
  33. rfx_protocols_system-0.2.0/src/protocols_system/providers/ollama_embedding.py +136 -0
  34. rfx_protocols_system-0.2.0/src/protocols_system/providers/openai_llm.py +266 -0
  35. rfx_protocols_system-0.2.0/src/protocols_system/py.typed +0 -0
  36. rfx_protocols_system-0.2.0/src/protocols_system/webauth.py +98 -0
  37. rfx_protocols_system-0.2.0/tests/__init__.py +0 -0
  38. rfx_protocols_system-0.2.0/tests/test_discovery.py +138 -0
  39. rfx_protocols_system-0.2.0/tests/test_mux_image.py +113 -0
  40. rfx_protocols_system-0.2.0/tests/test_registry.py +172 -0
  41. rfx_protocols_system-0.2.0/tests/test_validation.py +94 -0
  42. rfx_protocols_system-0.2.0/tests/test_webauth.py +57 -0
@@ -0,0 +1,65 @@
1
+ name: Build and Publish Wheel
2
+
3
+ # Builds the rfx-protocols-system wheel and publishes it so the consuming apps
4
+ # (PhotoStudio, Smart-Albums) can install a versioned artifact
5
+ # (rfx-protocols-system==0.2.x) instead of resolving a git SHA — which removes the
6
+ # need for `git` in every consumer Docker image and keeps both consumers
7
+ # locked to the same published version (SIMPLIFICATION 2.2 / 4.2).
8
+ #
9
+ # Triggers:
10
+ # * version tags (v*) -> release build, publishes to PyPI + attaches
11
+ # the wheel/sdist to a GitHub Release;
12
+ # * manual (workflow_dispatch) -> build + upload artifacts only (no publish).
13
+ on:
14
+ workflow_dispatch:
15
+ push:
16
+ tags:
17
+ - "v*"
18
+
19
+ jobs:
20
+ build:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Install build tooling
31
+ run: python -m pip install --upgrade build
32
+
33
+ - name: Build wheel and sdist
34
+ run: python -m build --outdir dist
35
+
36
+ - name: Upload build artifacts
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/*
41
+
42
+ publish-pypi:
43
+ # Only publish from a version tag, and only after a successful build.
44
+ needs: build
45
+ if: startsWith(github.ref, 'refs/tags/v')
46
+ runs-on: ubuntu-latest
47
+ environment: pypi
48
+ permissions:
49
+ # Required for PyPI Trusted Publishing (OIDC) — no stored API token.
50
+ id-token: write
51
+ contents: write
52
+ steps:
53
+ - name: Download build artifacts
54
+ uses: actions/download-artifact@v4
55
+ with:
56
+ name: dist
57
+ path: dist
58
+
59
+ - name: Publish to PyPI
60
+ uses: pypa/gh-action-pypi-publish@release/v1
61
+
62
+ - name: Attach artifacts to the GitHub Release
63
+ uses: softprops/action-gh-release@v2
64
+ with:
65
+ files: dist/*
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+
7
+ # Virtual environments
8
+ .venv/
9
+ .venv-verify/
10
+ venv/
11
+ env/
12
+
13
+ # Distribution / packaging
14
+ dist/
15
+ build/
16
+ *.egg-info/
17
+ *.egg
18
+
19
+ # IDE
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+ *.swo
24
+ *~
25
+
26
+ # mypy
27
+ .mypy_cache/
28
+
29
+ # pytest
30
+ .pytest_cache/
31
+ htmlcov/
32
+ .coverage
33
+ coverage.xml
34
+
35
+ # ruff
36
+ .ruff_cache/
37
+
38
+ # OS
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Project-specific
43
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rfloresx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.5
2
+ Name: rfx-protocols-system
3
+ Version: 0.2.0
4
+ Summary: A self-describing, pluggable service locator: decorator-based provider registration, singleton lifecycle management, and introspection-driven GUI discovery.
5
+ Project-URL: Homepage, https://github.com/rfloresx/ProtocolsSystem
6
+ Project-URL: Repository, https://github.com/rfloresx/ProtocolsSystem
7
+ Project-URL: Issues, https://github.com/rfloresx/ProtocolsSystem/issues
8
+ Author: Roberto Flores
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: discovery,plugin,protocols,registry,service-locator
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Provides-Extra: dev
20
+ Requires-Dist: mypy>=1.10; extra == 'dev'
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.5; extra == 'dev'
25
+ Provides-Extra: heif
26
+ Requires-Dist: pillow-heif<1,>=0.16.0; extra == 'heif'
27
+ Provides-Extra: huggingface
28
+ Requires-Dist: torch>=2.0.0; extra == 'huggingface'
29
+ Requires-Dist: transformers<5,>=4.38.0; extra == 'huggingface'
30
+ Provides-Extra: openai
31
+ Requires-Dist: openai<3,>=1.0.0; extra == 'openai'
32
+ Provides-Extra: providers
33
+ Requires-Dist: googlemaps>=4.10; extra == 'providers'
34
+ Requires-Dist: httpx>=0.27; extra == 'providers'
35
+ Requires-Dist: immichpy<9,>=8.0; extra == 'providers'
36
+ Requires-Dist: ollama>=0.4; extra == 'providers'
37
+ Requires-Dist: pillow>=10.0; extra == 'providers'
38
+ Requires-Dist: tenacity<10,>=9.0; extra == 'providers'
39
+ Provides-Extra: webauth
40
+ Requires-Dist: bcrypt>=4.0; extra == 'webauth'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Protocols System
44
+
45
+ A self-describing, pluggable service locator for Python.
46
+
47
+ Protocols System provides decorator-based provider registration, singleton
48
+ lifecycle management, and introspection-driven GUI discovery. Protocol and
49
+ provider additions automatically surface in any UI that consumes the discovery
50
+ layer.
51
+
52
+ It is shared across projects (PhotoStudio, Smart-Albums) as a standalone
53
+ package so provider implementations and the registry live in one place.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ # Core only (registry, discovery, protocols). The importable package is
59
+ # `protocols_system`; the distribution name is `rfx-protocols-system`.
60
+ pip install rfx-protocols-system
61
+
62
+ # With built-in provider implementations (Immich, Ollama, llama.cpp, Google Places, ...)
63
+ pip install "rfx-protocols-system[providers]"
64
+ ```
65
+
66
+ For local development against a checkout:
67
+
68
+ ```bash
69
+ pip install -e "../ProtocolsSystem[providers]"
70
+ ```
71
+
72
+ ## Layout
73
+
74
+ - `protocols_system.core` — registry, discovery, types, validation
75
+ - `protocols_system.protocols` — protocol (interface) definitions: cache,
76
+ embedding, geo, health, image_library, llm, progress
77
+ - `protocols_system.providers` — concrete provider implementations that
78
+ self-register via the `@ProtocolsRegistry.register` decorator
79
+
80
+ ## Usage
81
+
82
+ ```python
83
+ from protocols_system import ProtocolsRegistry, get_protocol_slots
84
+ from protocols_system.providers import register_builtin_providers
85
+
86
+ # Import provider modules so they register themselves. Providers whose
87
+ # optional dependencies are missing are skipped silently.
88
+ register_builtin_providers()
89
+
90
+ registry = ProtocolsRegistry()
91
+ ```
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ pip install -e ".[dev,providers]"
97
+ pytest
98
+ mypy
99
+ ruff check
100
+ ```
@@ -0,0 +1,58 @@
1
+ # Protocols System
2
+
3
+ A self-describing, pluggable service locator for Python.
4
+
5
+ Protocols System provides decorator-based provider registration, singleton
6
+ lifecycle management, and introspection-driven GUI discovery. Protocol and
7
+ provider additions automatically surface in any UI that consumes the discovery
8
+ layer.
9
+
10
+ It is shared across projects (PhotoStudio, Smart-Albums) as a standalone
11
+ package so provider implementations and the registry live in one place.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ # Core only (registry, discovery, protocols). The importable package is
17
+ # `protocols_system`; the distribution name is `rfx-protocols-system`.
18
+ pip install rfx-protocols-system
19
+
20
+ # With built-in provider implementations (Immich, Ollama, llama.cpp, Google Places, ...)
21
+ pip install "rfx-protocols-system[providers]"
22
+ ```
23
+
24
+ For local development against a checkout:
25
+
26
+ ```bash
27
+ pip install -e "../ProtocolsSystem[providers]"
28
+ ```
29
+
30
+ ## Layout
31
+
32
+ - `protocols_system.core` — registry, discovery, types, validation
33
+ - `protocols_system.protocols` — protocol (interface) definitions: cache,
34
+ embedding, geo, health, image_library, llm, progress
35
+ - `protocols_system.providers` — concrete provider implementations that
36
+ self-register via the `@ProtocolsRegistry.register` decorator
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from protocols_system import ProtocolsRegistry, get_protocol_slots
42
+ from protocols_system.providers import register_builtin_providers
43
+
44
+ # Import provider modules so they register themselves. Providers whose
45
+ # optional dependencies are missing are skipped silently.
46
+ register_builtin_providers()
47
+
48
+ registry = ProtocolsRegistry()
49
+ ```
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ pip install -e ".[dev,providers]"
55
+ pytest
56
+ mypy
57
+ ruff check
58
+ ```
@@ -0,0 +1,100 @@
1
+ [project]
2
+ name = "rfx-protocols-system"
3
+ version = "0.2.0"
4
+ description = "A self-describing, pluggable service locator: decorator-based provider registration, singleton lifecycle management, and introspection-driven GUI discovery."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Roberto Flores" },
10
+ ]
11
+ keywords = ["service-locator", "registry", "protocols", "plugin", "discovery"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.optional-dependencies]
23
+ # Built-in provider implementations pull optional third-party clients.
24
+ # The registry skips providers whose dependencies are missing, so these
25
+ # are opt-in per consumer.
26
+ providers = [
27
+ "httpx>=0.27",
28
+ "immichpy>=8.0,<9",
29
+ "tenacity>=9.0,<10",
30
+ "ollama>=0.4",
31
+ "googlemaps>=4.10",
32
+ "Pillow>=10.0",
33
+ ]
34
+ # OpenAI-compatible LLM provider (openai_llm).
35
+ openai = [
36
+ "openai>=1.0.0,<3",
37
+ ]
38
+ # HEIC/HEIF decoding for the vision image-preparation path (pillow-heif).
39
+ # Without it, HEIC input reports a clear error rather than sending
40
+ # undecodable bytes to a vision API.
41
+ heif = [
42
+ "pillow-heif>=0.16.0,<1",
43
+ ]
44
+ # Local CLIP-family image embeddings (huggingface_embedding). Heavy — pulls
45
+ # torch — so kept separate from the core providers extra.
46
+ huggingface = [
47
+ "transformers>=4.38.0,<5",
48
+ "torch>=2.0.0",
49
+ ]
50
+ # Shared web-auth primitives (protocols_system.webauth): bcrypt hashing and
51
+ # timing-safe verification used by consumer web GUIs.
52
+ webauth = [
53
+ "bcrypt>=4.0",
54
+ ]
55
+ dev = [
56
+ "pytest>=8.0",
57
+ "pytest-asyncio>=0.23",
58
+ "pytest-cov>=5.0",
59
+ "ruff>=0.5",
60
+ "mypy>=1.10",
61
+ ]
62
+
63
+ [project.urls]
64
+ Homepage = "https://github.com/rfloresx/ProtocolsSystem"
65
+ Repository = "https://github.com/rfloresx/ProtocolsSystem"
66
+ Issues = "https://github.com/rfloresx/ProtocolsSystem/issues"
67
+
68
+ [build-system]
69
+ requires = ["hatchling"]
70
+ build-backend = "hatchling.build"
71
+
72
+ [tool.hatch.build.targets.wheel]
73
+ packages = ["src/protocols_system"]
74
+
75
+ [tool.ruff]
76
+ target-version = "py311"
77
+ src = ["src"]
78
+ line-length = 99
79
+
80
+ [tool.ruff.lint]
81
+ select = ["E", "F", "I", "N", "W", "UP"]
82
+
83
+ [tool.mypy]
84
+ python_version = "3.11"
85
+ strict = true
86
+ packages = ["protocols_system"]
87
+ mypy_path = ["src"]
88
+
89
+ # immichpy has no py.typed / stubs; ollama and googlemaps are suppressed
90
+ # inline via `type: ignore` at their import sites.
91
+ [[tool.mypy.overrides]]
92
+ module = [
93
+ "immichpy.*",
94
+ ]
95
+ ignore_missing_imports = true
96
+
97
+ [tool.pytest.ini_options]
98
+ testpaths = ["tests"]
99
+ pythonpath = ["src"]
100
+ asyncio_mode = "auto"
@@ -0,0 +1,28 @@
1
+ """Protocols System — A self-describing, pluggable service locator.
2
+
3
+ Provides decorator-based provider registration, singleton lifecycle management,
4
+ and introspection-driven GUI discovery. Protocol and provider additions
5
+ automatically surface in any UI that consumes the discovery layer.
6
+ """
7
+
8
+ from .core import (
9
+ ConfigParam,
10
+ ProtocolSlot,
11
+ ProtocolsRegistry,
12
+ classify_widget,
13
+ get_all_provider_schemas,
14
+ get_protocol_slots,
15
+ get_provider_schema,
16
+ set_excluded_protocols,
17
+ )
18
+
19
+ __all__ = [
20
+ "ProtocolsRegistry",
21
+ "get_protocol_slots",
22
+ "get_provider_schema",
23
+ "get_all_provider_schemas",
24
+ "classify_widget",
25
+ "set_excluded_protocols",
26
+ "ProtocolSlot",
27
+ "ConfigParam",
28
+ ]
@@ -0,0 +1,32 @@
1
+ """Protocols System core — registry, discovery, and types."""
2
+
3
+ from .discovery import (
4
+ classify_widget,
5
+ get_all_provider_schemas,
6
+ get_protocol_slots,
7
+ get_provider_schema,
8
+ set_excluded_protocols,
9
+ )
10
+ from .llm_support import (
11
+ ImageConversionError,
12
+ detect_mime,
13
+ prepare_image_for_vision,
14
+ validate_against_schema,
15
+ )
16
+ from .registry import ProtocolsRegistry
17
+ from .types import ConfigParam, ProtocolSlot
18
+
19
+ __all__ = [
20
+ "ProtocolsRegistry",
21
+ "get_protocol_slots",
22
+ "get_provider_schema",
23
+ "get_all_provider_schemas",
24
+ "classify_widget",
25
+ "set_excluded_protocols",
26
+ "ProtocolSlot",
27
+ "ConfigParam",
28
+ "validate_against_schema",
29
+ "prepare_image_for_vision",
30
+ "detect_mime",
31
+ "ImageConversionError",
32
+ ]
@@ -0,0 +1,199 @@
1
+ """Introspection-driven discovery for dynamic GUI rendering.
2
+
3
+ This module provides the bridge between the ProtocolsRegistry catalog and a UI
4
+ framework. It introspects protocols (via docstrings) and providers (via __init__
5
+ signatures) so that settings forms can be rendered without any per-provider UI code.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from inspect import Parameter, signature
12
+ from typing import Any
13
+
14
+ from .registry import ProtocolsRegistry
15
+ from .types import ConfigParam, ProtocolSlot
16
+
17
+ # Protocols in this set won't appear in user-facing settings UI.
18
+ _EXCLUDED_PROTOCOLS: set[type] = set()
19
+
20
+ # Hints that indicate a parameter should be rendered as a password field.
21
+ _SECRET_HINTS = ("api_key", "password", "secret", "token", "credentials")
22
+
23
+
24
+ def set_excluded_protocols(protocols: set[type]) -> None:
25
+ """Configure which protocols are excluded from the settings UI.
26
+
27
+ Call this once at app startup after defining your infrastructure protocols.
28
+ """
29
+ global _EXCLUDED_PROTOCOLS
30
+ _EXCLUDED_PROTOCOLS = protocols
31
+
32
+
33
+ def get_protocol_slots() -> list[ProtocolSlot]:
34
+ """Discover all configurable protocol slots from the registry.
35
+
36
+ Returns a list of ProtocolSlot objects suitable for rendering a settings page.
37
+ Infrastructure protocols in the exclusion set are filtered out.
38
+ """
39
+ slots: list[ProtocolSlot] = []
40
+ for protocol_cls in ProtocolsRegistry.get_protocols():
41
+ if protocol_cls in _EXCLUDED_PROTOCOLS:
42
+ continue
43
+ name = _derive_slot_name(protocol_cls)
44
+ label, description = _parse_protocol_doc(protocol_cls)
45
+ slots.append(ProtocolSlot(name, protocol_cls, label, description))
46
+ return slots
47
+
48
+
49
+ def get_provider_schema(protocol: type, provider_name: str) -> list[ConfigParam]:
50
+ """Introspect a provider's __init__ to produce a list of config parameters.
51
+
52
+ Args:
53
+ protocol: The protocol type.
54
+ provider_name: The registered provider name.
55
+
56
+ Returns:
57
+ List of ConfigParam objects describing each constructor parameter.
58
+
59
+ Raises:
60
+ ValueError: If the protocol or provider is not registered.
61
+ """
62
+ providers = ProtocolsRegistry.get_providers(protocol)
63
+ if provider_name not in providers:
64
+ raise ValueError(
65
+ f"No provider named '{provider_name}' for protocol {protocol.__name__}"
66
+ )
67
+ cls = providers[provider_name]
68
+ return _introspect_init(cls)
69
+
70
+
71
+ def get_all_provider_schemas(protocol: type) -> dict[str, list[ConfigParam]]:
72
+ """Return schemas for all providers registered under a protocol.
73
+
74
+ Returns:
75
+ Dict mapping provider_name -> list[ConfigParam].
76
+ """
77
+ providers = ProtocolsRegistry.get_providers(protocol)
78
+ return {name: _introspect_init(cls) for name, cls in providers.items()}
79
+
80
+
81
+ def classify_widget(key: str, annotation: str, default: Any) -> str:
82
+ """Classify a parameter into a UI widget kind.
83
+
84
+ Args:
85
+ key: Parameter name.
86
+ annotation: Type annotation as string.
87
+ default: Default value (or Parameter.empty).
88
+
89
+ Returns:
90
+ One of: "bool", "password", "float", "int", "str".
91
+ """
92
+ # Bool detection
93
+ if "bool" in annotation.lower() or isinstance(default, bool):
94
+ return "bool"
95
+
96
+ # Secret-looking parameter names
97
+ if any(hint in key.lower() for hint in _SECRET_HINTS):
98
+ return "password"
99
+
100
+ # Numeric types
101
+ if "float" in annotation.lower() or isinstance(default, float):
102
+ return "float"
103
+ if "int" in annotation.lower() or isinstance(default, int):
104
+ return "int"
105
+
106
+ return "str"
107
+
108
+
109
+ # ------------------------------------------------------------------
110
+ # Internal helpers
111
+ # ------------------------------------------------------------------
112
+
113
+
114
+ def _derive_slot_name(protocol_cls: type) -> str:
115
+ """Derive a config-friendly slot name from a protocol class name.
116
+
117
+ IImageGenerationClient -> "image_generation"
118
+ ILLMClient -> "llm"
119
+ IStorageClient -> "storage"
120
+ ICacheManager -> "cache_manager"
121
+ """
122
+ name = protocol_cls.__name__
123
+
124
+ # Strip leading "I"
125
+ if name.startswith("I") and len(name) > 1 and name[1].isupper():
126
+ name = name[1:]
127
+
128
+ # Strip trailing "Client" or "Manager" or "Provider"
129
+ for suffix in ("Client", "Manager", "Provider", "Service"):
130
+ if name.endswith(suffix) and len(name) > len(suffix):
131
+ name = name[: -len(suffix)]
132
+ break
133
+
134
+ # CamelCase to snake_case
135
+ name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
136
+ name = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", name)
137
+ return name.lower()
138
+
139
+
140
+ def _parse_protocol_doc(protocol_cls: type) -> tuple[str, str]:
141
+ """Parse protocol docstring into (label, description).
142
+
143
+ Expected format: "Label:Description text here."
144
+ Falls back to class name if docstring is missing or malformed.
145
+ """
146
+ doc = protocol_cls.__doc__ or ""
147
+ first_line = doc.strip().split("\n")[0].strip()
148
+
149
+ if ":" in first_line:
150
+ label, description = first_line.split(":", 1)
151
+ return label.strip(), description.strip()
152
+
153
+ # Fallback: derive label from class name
154
+ label = _derive_slot_name(protocol_cls).replace("_", " ").title()
155
+ return label, first_line or f"Provider for {label}."
156
+
157
+
158
+ def _introspect_init(cls: type) -> list[ConfigParam]:
159
+ """Extract ConfigParam list from a class's __init__ signature."""
160
+ try:
161
+ sig = signature(cls.__init__) # type: ignore[misc]
162
+ except (ValueError, TypeError):
163
+ return []
164
+
165
+ params: list[ConfigParam] = []
166
+ for name, param in sig.parameters.items():
167
+ if name == "self":
168
+ continue
169
+ if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
170
+ continue
171
+
172
+ # Resolve annotation
173
+ if param.annotation is not Parameter.empty:
174
+ annotation = (
175
+ param.annotation.__name__
176
+ if hasattr(param.annotation, "__name__")
177
+ else str(param.annotation)
178
+ )
179
+ else:
180
+ annotation = "str"
181
+
182
+ # Resolve default
183
+ has_default = param.default is not Parameter.empty
184
+ default = param.default if has_default else None
185
+ required = not has_default
186
+
187
+ # Classify widget
188
+ widget_kind = classify_widget(name, annotation, default)
189
+
190
+ params.append(
191
+ ConfigParam(
192
+ key=name,
193
+ annotation=annotation,
194
+ default=default,
195
+ required=required,
196
+ widget_kind=widget_kind,
197
+ )
198
+ )
199
+ return params