Skip to content

Commit 3e4232e

Browse files
authored
Fix missing pipeline implementations. (#1514)
1 parent 59d7781 commit 3e4232e

File tree

6 files changed

+199
-5
lines changed

6 files changed

+199
-5
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Model Pipelines
2+
3+
Ansible AI Connect is becoming feature rich.
4+
5+
It supports API for the following features:
6+
- Code completions
7+
- Content match
8+
- Playbook Generation
9+
- Role Generation
10+
- Playbook Explanation
11+
- Chat Bot
12+
13+
"Model Pipelines" provides a mechanism to support different _pipelines_ and configuration for each of these features for different providers. Different providers require different configuration information.
14+
15+
## Pipelines
16+
17+
A pipeline can exist for each feature for each type of provider.
18+
19+
Types of provider are:
20+
- `grpc`
21+
- `http`
22+
- `dummy`
23+
- `wca`
24+
- `wca-onprem`
25+
- `wca-dummy`
26+
- `ollama`
27+
- `llamacpp`
28+
- `nop`
29+
30+
### Implementing pipelines
31+
32+
Implementations of a pipeline, for a particular provider, for a particular feature should extend the applicable base class; implementing the `invoke(..)` method accordingly:
33+
- `ModelPipelineCompletions`
34+
- `ModelPipelineContentMatch`
35+
- `ModelPipelinePlaybookGeneration`
36+
- `ModelPipelineRoleGeneration`
37+
- `ModelPipelinePlaybookExplanation`
38+
- `ModelPipelineChatBot`
39+
40+
### Registering pipelines
41+
42+
Implementations of pipelines, per provider, per feature are dynamically registered. To register a pipeline the implementing class should be decorated with `@Register(api_type="<type>")`.
43+
44+
In addition to the supported features themselves implementations for the following must also be provided and registered:
45+
- `MetaData`
46+
47+
A class providing basic meta-data for all features for the applicable provider.
48+
49+
For example API Key, Model ID, Timeout etc.
50+
51+
52+
- `PipelineConfiguration`
53+
54+
A class representing the pipelines configuration parameters.
55+
56+
57+
- `Serializer`
58+
59+
A class that can deserialise configuration JSON/YAML into the target `PipelineConfiguration` class.
60+
61+
### Default implementations
62+
63+
A "No Operation" pipeline is registered by default for each provider and each feature where a concrete implementation is not explicitly available.
64+
65+
### Lookup
66+
67+
A registry is constructed at start-up, containing information of configured pipelines for all providers for all features.
68+
```
69+
REGISTRY = {
70+
"http": {
71+
MetaData: <Implementing class>,
72+
ModelPipelineCompletions: <Implementing class>
73+
ModelPipelineContentMatch: <Implementing class>
74+
ModelPipelinePlaybookGeneration: <Implementing class>
75+
ModelPipelineRoleGeneration: <Implementing class>
76+
ModelPipelinePlaybookExplanation: <Implementing class>
77+
ModelPipelineChatBot: <Implementing class>
78+
PipelineConfiguration: <Implementing class>
79+
Serializer: <Implementing class>
80+
}
81+
...
82+
}
83+
```
84+
85+
To invoke a pipeline for a particular feature the instance for the configured provider can be retrieved from the `ai` Django application:
86+
```
87+
pipeline: ModelPipelinePlaybookGeneration =
88+
apps
89+
.get_app_config("ai")
90+
.get_model_pipeline(ModelPipelinePlaybookGeneration)
91+
```
92+
The pipeline can then be invoked:
93+
```
94+
playbook, outline, warnings = pipeline.invoke(
95+
PlaybookGenerationParameters.init(
96+
request=request,
97+
text=self.validated_data["text"],
98+
custom_prompt=self.validated_data["customPrompt"],
99+
create_outline=self.validated_data["createOutline"],
100+
outline=self.validated_data["outline"],
101+
generation_id=self.validated_data["generationId"],
102+
model_id=self.req_model_id,
103+
)
104+
)
105+
```
106+
The code is identical irrespective of which provider is configured.
107+
108+
### Configuration
109+
110+
Refer to the [examples](../../../../docs/config).

ansible_ai_connect/ai/api/model_pipelines/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@
1717
import ansible_ai_connect.ai.api.model_pipelines.wca.pipelines_dummy # noqa
1818
import ansible_ai_connect.ai.api.model_pipelines.wca.pipelines_onprem # noqa
1919
import ansible_ai_connect.ai.api.model_pipelines.wca.pipelines_saas # noqa
20+
from ansible_ai_connect.ai.api.model_pipelines.registry import set_defaults
21+
22+
set_defaults()

ansible_ai_connect/ai/api/model_pipelines/factory.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
PipelineConfiguration,
2222
)
2323
from ansible_ai_connect.ai.api.model_pipelines.config_providers import Configuration
24+
from ansible_ai_connect.ai.api.model_pipelines.nop.pipelines import NopMetaData
2425
from ansible_ai_connect.ai.api.model_pipelines.pipelines import PIPELINE_TYPE
2526
from ansible_ai_connect.ai.api.model_pipelines.registry import REGISTRY, REGISTRY_ENTRY
2627

@@ -46,16 +47,19 @@ def get_pipeline(self, pipeline_type: Type[PIPELINE_TYPE]) -> PIPELINE_TYPE:
4647
try:
4748
# Get the configuration for the requested pipeline
4849
pipeline_config: PipelineConfiguration = self.pipelines_config[pipeline_type.__name__]
50+
4951
# Get the pipeline class for the configured provider
5052
pipelines = REGISTRY[pipeline_config.provider]
5153
pipeline = pipelines[pipeline_type]
52-
config = pipeline_config.config
53-
# No explicit implementation defined; fallback to NOP
54-
if pipeline_config.provider == "nop":
54+
55+
# Ensure NOP instances are created with NOP configuration
56+
if issubclass(pipeline, NopMetaData):
5557
logger.info(f"Using NOP implementation for '{pipeline_type.__name__}'.")
58+
pipelines = REGISTRY["nop"]
59+
pipeline_config = pipelines[PipelineConfiguration]()
5660

5761
# Construct an instance of the pipeline class with the applicable configuration
58-
self.cache[pipeline_type] = pipeline(config)
62+
self.cache[pipeline_type] = pipeline(pipeline_config.config)
5963

6064
except KeyError:
6165
pass

ansible_ai_connect/ai/api/model_pipelines/nop/configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ def __init__(self, **kwargs):
4040

4141

4242
@Register(api_type="nop")
43-
class LlamaCppConfigurationSerializer(serializers.Serializer):
43+
class NopConfigurationSerializer(serializers.Serializer):
4444
pass

ansible_ai_connect/ai/api/model_pipelines/registry.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,23 @@ def __call__(self, cls):
7070
elif issubclass(cls, Serializer):
7171
REGISTRY[self.api_type][Serializer] = cls
7272
return cls
73+
74+
75+
def set_defaults():
76+
77+
def set_defaults_for_api_type(pipeline_provider):
78+
79+
def v_or_default(k, v):
80+
defaults = REGISTRY["nop"]
81+
if v is None:
82+
logger.warning(
83+
f"'{k.alias()}' is not available for provider '{pipeline_provider}',"
84+
" failing back to 'nop'"
85+
)
86+
return defaults[k]
87+
return v
88+
89+
return {k: v_or_default(k, v) for k, v in REGISTRY[pipeline_provider].items()}
90+
91+
for model_mesh_api_type in get_args(t_model_mesh_api_type):
92+
REGISTRY[model_mesh_api_type] = set_defaults_for_api_type(model_mesh_api_type)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright Red Hat
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from django.test import override_settings
15+
16+
from ansible_ai_connect.ai.api.model_pipelines.factory import ModelPipelineFactory
17+
from ansible_ai_connect.ai.api.model_pipelines.nop.pipelines import NopChatBotPipeline
18+
from ansible_ai_connect.ai.api.model_pipelines.pipelines import (
19+
MetaData,
20+
ModelPipelineChatBot,
21+
)
22+
from ansible_ai_connect.ai.api.model_pipelines.registry import REGISTRY, REGISTRY_ENTRY
23+
from ansible_ai_connect.test_utils import WisdomServiceAPITestCaseBaseOIDC
24+
25+
26+
class TestDefaultModelPipelines(WisdomServiceAPITestCaseBaseOIDC):
27+
28+
@override_settings(ANSIBLE_AI_MODEL_MESH_CONFIG="{}")
29+
def test_default_pipeline_when_not_defined(self):
30+
factory = ModelPipelineFactory()
31+
32+
# The configuration is empty. All pipelines should fall back to the NOP variety
33+
pipelines = list(filter(lambda p: issubclass(p, MetaData), REGISTRY_ENTRY.keys()))
34+
for pipeline in pipelines:
35+
nop = REGISTRY["nop"][pipeline]
36+
with self.assertLogs(logger="root", level="INFO") as log:
37+
implementation = factory.get_pipeline(pipeline)
38+
self.assertIsNotNone(implementation)
39+
self.assertIsInstance(implementation, nop)
40+
self.assertInLog(
41+
f"Using NOP implementation for '{pipeline.__name__}'.",
42+
log,
43+
)
44+
45+
@override_settings(ANSIBLE_AI_MODEL_MESH_CONFIG="ModelPipelineChatBot:\n provider: dummy")
46+
def test_default_pipeline_when_not_implemented(self):
47+
factory = ModelPipelineFactory()
48+
49+
# ChatBot is configured to use "dummy" however there is no "dummy" ChatBot implementation
50+
with self.assertLogs(logger="root", level="INFO") as log:
51+
pipeline = factory.get_pipeline(ModelPipelineChatBot)
52+
self.assertIsNotNone(pipeline)
53+
self.assertIsInstance(pipeline, NopChatBotPipeline)
54+
self.assertInLog(
55+
"Using NOP implementation for 'ModelPipelineChatBot'.",
56+
log,
57+
)

0 commit comments

Comments
 (0)