Skip to content

Commit cfdb4a3

Browse files
authored
Merge pull request Pipelex#131 from Pipelex/release/v0.4.9
### Highlights **Plugin System Refactoring** - Complete overhaul of the plugin architecture to support external LLM providers. ### Added - **External Plugin Support**: New `LLMWorkerAbstract` base class for integrating custom LLM providers, and we don't mean only an OpenAI-SDK-based LLM with a custom endpoint, now the implementation can be anything, as long as it implements the `LLMWorkerAbstract` interface. - **Plugin SDK Registry**: Better management of SDK instances with proper teardown handling - **Enhanced Error Formatting**: Improved Pydantic validation error messages for enums ### Changed - **Plugin Architecture**: Moved plugin system to dedicated `pipelex.plugins` package - **LLM Workers**: Split into `LLMWorkerInternalAbstract` (for built-in providers) and `LLMWorkerAbstract` (for external plugins) - **Configuration**: Plugin configs moved from main `pipelex.toml` to separate `pipelex_libraries/plugins/plugin_config.toml` (⚠️ breaking change) - **Error Handling**: Standardized credential errors with new `CredentialsError` base class
2 parents dbcca5f + 4d47611 commit cfdb4a3

58 files changed

Lines changed: 717 additions & 376 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish-pypi.yml

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ jobs:
5555

5656
github-release:
5757
name: >-
58-
Sign the Python 🐍 distribution 📦 with Sigstore
59-
and upload them to GitHub Release
58+
Test GitHub Release Creation with Changelog
6059
needs:
60+
- build
6161
- publish-to-pypi
6262
runs-on: ubuntu-latest
6363

@@ -74,6 +74,45 @@ jobs:
7474
run: |
7575
VERSION=$(grep -m 1 'version = ' pyproject.toml | cut -d '"' -f 2)
7676
echo "VERSION=$VERSION" >> $GITHUB_ENV
77+
- name: Extract changelog notes for current version
78+
id: get_changelog
79+
run: |
80+
VERSION=${{ env.VERSION }}
81+
echo "Extracting changelog for version v$VERSION"
82+
83+
# Find the start of the current version section
84+
START_LINE=$(grep -n "## \[v$VERSION\] - " CHANGELOG.md | cut -d: -f1)
85+
86+
if [ -z "$START_LINE" ]; then
87+
echo "Warning: No changelog entry found for version v$VERSION"
88+
echo "CHANGELOG_NOTES=" >> $GITHUB_ENV
89+
exit 0
90+
fi
91+
92+
# Find the start of the next version section (previous version)
93+
NEXT_VERSION_LINE=$(tail -n +$((START_LINE + 1)) CHANGELOG.md | grep -n "^## \[v.*\] - " | head -1 | cut -d: -f1)
94+
95+
if [ -z "$NEXT_VERSION_LINE" ]; then
96+
# No next version found, extract from current version till end of file
97+
CHANGELOG_CONTENT=$(tail -n +$START_LINE CHANGELOG.md)
98+
else
99+
# Extract content from current version header to before next version
100+
END_LINE=$((START_LINE + NEXT_VERSION_LINE - 1))
101+
CHANGELOG_CONTENT=$(sed -n "$START_LINE,$((END_LINE - 1))p" CHANGELOG.md)
102+
fi
103+
104+
# Clean up the content but preserve the blank line after the header
105+
# First, get the header line and add a blank line after it
106+
HEADER_LINE=$(echo "$CHANGELOG_CONTENT" | head -1)
107+
CONTENT_LINES=$(echo "$CHANGELOG_CONTENT" | tail -n +2 | sed '/^$/d' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
108+
109+
# Combine header + blank line + content
110+
CHANGELOG_CONTENT=$(printf "%s\n\n%s" "$HEADER_LINE" "$CONTENT_LINES")
111+
112+
# Escape for GitHub Actions
113+
echo "CHANGELOG_NOTES<<EOF" >> $GITHUB_ENV
114+
echo "$CHANGELOG_CONTENT" >> $GITHUB_ENV
115+
echo "EOF" >> $GITHUB_ENV
77116
- name: Download all the dists
78117
uses: actions/download-artifact@v4
79118
with:
@@ -88,12 +127,18 @@ jobs:
88127
- name: Create GitHub Release
89128
env:
90129
GITHUB_TOKEN: ${{ github.token }}
91-
run: >-
92-
gh release create
93-
"v$VERSION"
94-
--repo "$GITHUB_REPOSITORY"
95-
--title "v$VERSION"
96-
--notes ""
130+
run: |
131+
if [ -n "$CHANGELOG_NOTES" ]; then
132+
gh release create "v$VERSION" \
133+
--repo "$GITHUB_REPOSITORY" \
134+
--title "v$VERSION" \
135+
--notes "$CHANGELOG_NOTES"
136+
else
137+
gh release create "v$VERSION" \
138+
--repo "$GITHUB_REPOSITORY" \
139+
--title "v$VERSION" \
140+
--notes "Release v$VERSION"
141+
fi
97142
- name: Upload artifact signatures to GitHub Release
98143
env:
99144
GITHUB_TOKEN: ${{ github.token }}

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## [v0.4.9] - 2025-06-30
4+
5+
### Highlights
6+
7+
**Plugin System Refactoring** - Complete overhaul of the plugin architecture to support external LLM providers.
8+
9+
### Added
10+
11+
- **External Plugin Support**: New `LLMWorkerAbstract` base class for integrating custom LLM providers, and we don't mean only an OpenAI-SDK-based LLM with a custom endpoint, now the implementation can be anything, as long as it implements the `LLMWorkerAbstract` interface.
12+
- **Plugin SDK Registry**: Better management of SDK instances with proper teardown handling
13+
- **Enhanced Error Formatting**: Improved Pydantic validation error messages for enums
14+
15+
### Changed
16+
17+
- **Plugin Architecture**: Moved plugin system to dedicated `pipelex.plugins` package
18+
- **LLM Workers**: Split into `LLMWorkerInternalAbstract` (for built-in providers) and `LLMWorkerAbstract` (for external plugins)
19+
- **Configuration**: Plugin configs moved from main `pipelex.toml` to separate `pipelex_libraries/plugins/plugin_config.toml` (⚠️ breaking change)
20+
- **Error Handling**: Standardized credential errors with new `CredentialsError` base class
21+
322
## [v0.4.8] - 2025-06-26
423

524
- Added `StorageProviderAbstract`

docs/pages/advanced-customization/index.md

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ from pipelex import Pipelex
1616
pipelex = Pipelex(
1717
template_provider=MyTemplateProvider(),
1818
llm_model_provider=MyLLMProvider(),
19-
plugin_manager=MyPluginManager(),
2019
inference_manager=MyInferenceManager(),
2120
pipeline_tracker=MyPipelineTracker(),
2221
activity_manager=MyActivityManager(),
@@ -38,7 +37,6 @@ from pipelex.hub import PipelexHub
3837
hub = PipelexHub()
3938
hub.set_template_provider(MyTemplateProvider())
4039
hub.set_llm_models_provider(MyLLMProvider())
41-
hub.set_plugin_manager(MyPluginManager())
4240
# ... and so on for other components
4341
```
4442

@@ -88,54 +86,45 @@ Pipelex supports injection of the following components:
8886
- Default: `LLMModelLibrary`
8987
- [Details](llm-model-provider-injection.md)
9088

91-
3. **Plugin Manager** (`PluginManager`)
92-
93-
- Protocol: `PluginManagerProtocol`
94-
- Default: `PluginManager`
95-
- [Details](plugin-manager-injection.md)
96-
97-
4. **Inference Manager** (`InferenceManager`)
89+
3. **Inference Manager** (`InferenceManager`)
9890

9991
- Protocol: `InferenceManagerProtocol`
10092
- Default: `InferenceManager`
10193
- [Details](inference-manager-injection.md)
10294

103-
5. **Reporting Delegate** (`ReportingManager`)
95+
4. **Reporting Delegate** (`ReportingManager`)
10496

10597
- Protocol: `ReportingProtocol`
10698
- Default: `ReportingManager` or `ReportingNoOp` if disabled
10799
- [Details](reporting-delegate-injection.md)
108100

109-
6. **Pipeline Tracker** (`PipelineTracker`)
101+
5. **Pipeline Tracker** (`PipelineTracker`)
110102

111103
- Protocol: `PipelineTrackerProtocol`
112104
- Default: `PipelineTracker` or `PipelineTrackerNoOp` if disabled
113105
- [Details](pipeline-tracker-injection.md)
114106

115-
7. **Activity Manager** (`ActivityManager`)
107+
6. **Activity Manager** (`ActivityManager`)
116108

117109
- Protocol: `ActivityManagerProtocol`
118110
- Default: `ActivityManager` or `ActivityManagerNoOp` if disabled
119111
- [Details](activity-manager-injection.md)
120112

121-
8. **Secrets Provider** (`EnvSecretsProvider`)
113+
7. **Secrets Provider** (`EnvSecretsProvider`)
122114

123115
- Protocol: `SecretsProviderProtocol`
124116
- Default: `EnvSecretsProvider`
125117
- [Details](secrets-provider-injection.md)
126118

127-
9. **Content Generator** (`ContentGenerator`)
119+
8. **Content Generator** (`ContentGenerator`)
128120

129121
- Protocol: `ContentGeneratorProtocol`
130122
- Default: `ContentGenerator`
131123
- [Details](content-generator-injection.md)
132124

133-
10. **Pipe Router** (`PipeRouter`)
125+
9. **Pipe Router** (`PipeRouter`)
134126

135127
- Protocol: `PipeRouterProtocol`
136128
- Default: `PipeRouter`
137129
- [Details](pipe-router-injection.md)
138130

139-
## Best Practices
140-
141-
⚠️ Under construction

pipelex/cogt/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class SdkTypeError(CogtError):
2323
pass
2424

2525

26+
class SdkRegistryError(CogtError):
27+
pass
28+
29+
2630
class LLMWorkerError(CogtError):
2731
pass
2832

@@ -128,5 +132,9 @@ def __init__(self, dependency_name: str, extra_name: str, message: Optional[str]
128132
super().__init__(error_msg)
129133

130134

135+
class MissingPluginError(CogtError):
136+
pass
137+
138+
131139
class OcrCapabilityError(CogtError):
132140
pass

pipelex/cogt/imgg/imgg_worker_factory.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from pipelex.cogt.imgg.imgg_platform import ImggPlatform
66
from pipelex.cogt.imgg.imgg_worker_abstract import ImggWorkerAbstract
77
from pipelex.cogt.llm.llm_models.llm_platform import LLMPlatform
8-
from pipelex.cogt.plugin_manager import PluginHandle
98
from pipelex.hub import get_plugin_manager, get_secret
109
from pipelex.plugins.openai.openai_imgg_worker import OpenAIImggWorker
10+
from pipelex.plugins.plugin_sdk_registry import PluginSdkHandle
1111
from pipelex.reporting.reporting_protocol import ReportingProtocol
1212
from pipelex.tools.secrets.secrets_errors import SecretNotFoundError
1313

@@ -22,8 +22,8 @@ def make_imgg_worker(
2222
imgg_engine: ImggEngine,
2323
reporting_delegate: Optional[ReportingProtocol] = None,
2424
) -> ImggWorkerAbstract:
25-
imgg_sdk_handle = PluginHandle.get_for_imgg_engine(imgg_platform=imgg_engine.imgg_platform)
26-
plugin_manager = get_plugin_manager()
25+
imgg_sdk_handle = PluginSdkHandle.get_for_imgg_engine(imgg_platform=imgg_engine.imgg_platform)
26+
plugin_sdk_registry = get_plugin_manager().plugin_sdk_registry
2727
imgg_worker: ImggWorkerAbstract
2828
match imgg_engine.imgg_platform:
2929
case ImggPlatform.FAL_AI:
@@ -41,7 +41,9 @@ def make_imgg_worker(
4141

4242
from pipelex.plugins.fal.fal_imgg_worker import FalImggWorker
4343

44-
imgg_sdk_instance = plugin_manager.get_imgg_sdk_instance(imgg_sdk_handle=imgg_sdk_handle) or plugin_manager.set_imgg_sdk_instance(
44+
imgg_sdk_instance = plugin_sdk_registry.get_imgg_sdk_instance(
45+
imgg_sdk_handle=imgg_sdk_handle
46+
) or plugin_sdk_registry.set_imgg_sdk_instance(
4547
imgg_sdk_handle=imgg_sdk_handle,
4648
imgg_sdk_instance=FalAsyncClient(key=fal_api_key),
4749
)
@@ -54,7 +56,9 @@ def make_imgg_worker(
5456
case ImggPlatform.OPENAI:
5557
from pipelex.plugins.openai.openai_factory import OpenAIFactory
5658

57-
imgg_sdk_instance = plugin_manager.get_llm_sdk_instance(llm_sdk_handle=imgg_sdk_handle) or plugin_manager.set_llm_sdk_instance(
59+
imgg_sdk_instance = plugin_sdk_registry.get_llm_sdk_instance(
60+
llm_sdk_handle=imgg_sdk_handle
61+
) or plugin_sdk_registry.set_llm_sdk_instance(
5862
llm_sdk_handle=imgg_sdk_handle,
5963
llm_sdk_instance=OpenAIFactory.make_openai_client(llm_platform=LLMPlatform.OPENAI),
6064
)

pipelex/cogt/inference/inference_manager.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Optional
1+
from typing import Dict, Type
22

33
from typing_extensions import override
44

@@ -12,6 +12,7 @@
1212
from pipelex.cogt.llm.llm_models.llm_engine_factory import LLMEngineFactory
1313
from pipelex.cogt.llm.llm_worker_abstract import LLMWorkerAbstract
1414
from pipelex.cogt.llm.llm_worker_factory import LLMWorkerFactory
15+
from pipelex.cogt.llm.llm_worker_internal_abstract import LLMWorkerInternalAbstract
1516
from pipelex.cogt.ocr.ocr_engine_factory import OcrEngineFactory
1617
from pipelex.cogt.ocr.ocr_worker_abstract import OcrWorkerAbstract
1718
from pipelex.cogt.ocr.ocr_worker_factory import OcrWorkerFactory
@@ -31,10 +32,16 @@ def __init__(self):
3132
def teardown(self):
3233
self.imgg_worker_factory = ImggWorkerFactory()
3334
self.ocr_worker_factory = OcrWorkerFactory()
34-
self.llm_workers.clear()
35-
self.imgg_workers.clear()
36-
self.ocr_workers.clear()
37-
log.verbose("InferenceManagerAsync reset")
35+
for llm_worker in self.llm_workers.values():
36+
llm_worker.teardown()
37+
self.llm_workers = {}
38+
for imgg_worker in self.imgg_workers.values():
39+
imgg_worker.teardown()
40+
self.imgg_workers = {}
41+
for ocr_worker in self.ocr_workers.values():
42+
ocr_worker.teardown()
43+
self.ocr_workers = {}
44+
log.verbose("InferenceManager teardown done")
3845

3946
def print_workers(self):
4047
log.debug("LLM Workers:")
@@ -60,15 +67,15 @@ def setup_llm_workers(self):
6067
llm_handle_to_llm_engine_blueprint = get_llm_deck().llm_handles
6168
log.verbose(f"{len(llm_handle_to_llm_engine_blueprint)} LLM engine_cards found")
6269
for llm_handle, llm_engine_blueprint in llm_handle_to_llm_engine_blueprint.items():
63-
self._setup_one_llm_worker(llm_engine_blueprint=llm_engine_blueprint, llm_handle=llm_handle)
70+
self._setup_one_internal_llm_worker(llm_engine_blueprint=llm_engine_blueprint, llm_handle=llm_handle)
6471
log.verbose(f"Setup LLM worker for '{llm_handle}' on {llm_engine_blueprint.llm_platform_choice}")
6572
log.debug("Done setting up LLM Workers (async)")
6673

67-
def _setup_one_llm_worker(
74+
def _setup_one_internal_llm_worker(
6875
self,
6976
llm_engine_blueprint: LLMEngineBlueprint,
7077
llm_handle: str,
71-
) -> LLMWorkerAbstract:
78+
) -> LLMWorkerInternalAbstract:
7279
llm_engine = LLMEngineFactory.make_llm_engine(llm_engine_blueprint=llm_engine_blueprint)
7380
llm_worker = LLMWorkerFactory.make_llm_worker(
7481
llm_engine=llm_engine,
@@ -78,27 +85,34 @@ def _setup_one_llm_worker(
7885
return llm_worker
7986

8087
@override
81-
def get_llm_worker(
82-
self,
83-
llm_handle: str,
84-
specific_llm_engine_blueprint: Optional[LLMEngineBlueprint] = None,
85-
) -> LLMWorkerAbstract:
88+
def get_llm_worker(self, llm_handle: str) -> LLMWorkerAbstract:
8689
if llm_worker := self.llm_workers.get(llm_handle):
8790
return llm_worker
8891
if not get_config().cogt.inference_manager_config.is_auto_setup_preset_llm:
8992
raise InferenceManagerWorkerSetupError(
9093
f"No LLM worker for '{llm_handle}', set it up or enable cogt.inference_manager_config.is_auto_setup_preset_llm"
9194
)
9295

93-
if not specific_llm_engine_blueprint:
94-
specific_llm_engine_blueprint = get_llm_deck().get_llm_engine_blueprint(llm_handle=llm_handle)
95-
llm_worker = self._setup_one_llm_worker(
96-
llm_engine_blueprint=specific_llm_engine_blueprint,
96+
llm_engine_blueprint = get_llm_deck().get_llm_engine_blueprint(llm_handle=llm_handle)
97+
llm_worker = self._setup_one_internal_llm_worker(
98+
llm_engine_blueprint=llm_engine_blueprint,
9799
llm_handle=llm_handle,
98100
)
99101

100102
return llm_worker
101103

104+
@override
105+
def set_llm_worker_from_external_plugin(
106+
self,
107+
llm_handle: str,
108+
llm_worker_class: Type[LLMWorkerAbstract],
109+
should_warn_if_already_registered: bool = True,
110+
):
111+
if llm_handle in self.llm_workers:
112+
if should_warn_if_already_registered:
113+
log.warning(f"LLM worker for '{llm_handle}' already registered, skipping")
114+
self.llm_workers[llm_handle] = llm_worker_class(reporting_delegate=get_report_delegate())
115+
102116
####################################################################################################
103117
# Manage IMGG Workers
104118
####################################################################################################

pipelex/cogt/inference/inference_manager_protocol.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from typing import Optional, Protocol
1+
from typing import Protocol, Type
22

33
from pipelex.cogt.imgg.imgg_worker_abstract import ImggWorkerAbstract
4-
from pipelex.cogt.llm.llm_models.llm_engine_blueprint import LLMEngineBlueprint
54
from pipelex.cogt.llm.llm_worker_abstract import LLMWorkerAbstract
65
from pipelex.cogt.ocr.ocr_worker_abstract import OcrWorkerAbstract
76

@@ -20,11 +19,14 @@ def teardown(self): ...
2019

2120
def setup_llm_workers(self): ...
2221

23-
def get_llm_worker(
22+
def get_llm_worker(self, llm_handle: str) -> LLMWorkerAbstract: ...
23+
24+
def set_llm_worker_from_external_plugin(
2425
self,
2526
llm_handle: str,
26-
specific_llm_engine_blueprint: Optional[LLMEngineBlueprint] = None,
27-
) -> LLMWorkerAbstract: ...
27+
llm_worker_class: Type[LLMWorkerAbstract],
28+
should_warn_if_already_registered: bool = True,
29+
): ...
2830

2931
####################################################################################################
3032
# IMG Generation Workers

pipelex/cogt/inference/inference_worker_abstract.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ def __init__(
1111
):
1212
self.reporting_delegate = reporting_delegate
1313

14+
def setup(self):
15+
pass
16+
17+
def teardown(self):
18+
pass
19+
1420
@property
1521
@abstractmethod
1622
def desc(self) -> str:

0 commit comments

Comments
 (0)