Skip to content

Commit

Permalink
Fix missing pipeline implementations.
Browse files Browse the repository at this point in the history
  • Loading branch information
manstis committed Jan 30, 2025
1 parent 73d2a07 commit 2e1700d
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 5 deletions.
110 changes: 110 additions & 0 deletions ansible_ai_connect/ai/api/model_pipelines/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Model Pipelines

Ansible AI Connect is becoming feature rich.

It supports API for the following features:
- Code completions
- Content match
- Playbook Generation
- Role Generation
- Playbook Explanation
- Chat Bot

"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.

## Pipelines

A pipeline can exist for each feature for each type of provider.

Types of provider are:
- `grpc`
- `http`
- `dummy`
- `wca`
- `wca-onprem`
- `wca-dummy`
- `ollama`
- `llamacpp`
- `nop`

### Implementing pipelines

Implementations of a pipeline, for a particular provider, for a particular feature should extend the applicable base class; implementing the `invoke(..)` method accordingly:
- `ModelPipelineCompletions`
- `ModelPipelineContentMatch`
- `ModelPipelinePlaybookGeneration`
- `ModelPipelineRoleGeneration`
- `ModelPipelinePlaybookExplanation`
- `ModelPipelineChatBot`

### Registering pipelines

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>")`.

In addition to the supported features themselves implementations for the following must also be provided and registered:
- `MetaData`

A class providing basic meta-data for all features for the applicable provider.

For example API Key, Model ID, Timeout etc.


- `PipelineConfiguration`

A class representing the pipelines configuration parameters.


- `Serializer`

A class that can deserialise configuration JSON/YAML into the target `PipelineConfiguration` class.

### Default implementations

A "No Operation" pipeline is registered by default for each provider and each feature where a concrete implementation is not explicitly available.

### Lookup

A registry is constructed at start-up, containing information of configured pipelines for all providers for all features.
```
REGISTRY = {
"http": {
MetaData: <Implementing class>,
ModelPipelineCompletions: <Implementing class>
ModelPipelineContentMatch: <Implementing class>
ModelPipelinePlaybookGeneration: <Implementing class>
ModelPipelineRoleGeneration: <Implementing class>
ModelPipelinePlaybookExplanation: <Implementing class>
ModelPipelineChatBot: <Implementing class>
PipelineConfiguration: <Implementing class>
Serializer: <Implementing class>
}
...
}
```

To invoke a pipeline for a particular feature the instance for the configured provider can be retrieved from the `ai` Django application:
```
pipeline: ModelPipelinePlaybookGeneration =
apps
.get_app_config("ai")
.get_model_pipeline(ModelPipelinePlaybookGeneration)
```
The pipeline can then be invoked:
```
playbook, outline, warnings = pipeline.invoke(
PlaybookGenerationParameters.init(
request=request,
text=self.validated_data["text"],
custom_prompt=self.validated_data["customPrompt"],
create_outline=self.validated_data["createOutline"],
outline=self.validated_data["outline"],
generation_id=self.validated_data["generationId"],
model_id=self.req_model_id,
)
)
```
The code is identical irrespective of which provider is configured.

### Configuration

Refer to the [examples](../../../../docs/config).
3 changes: 3 additions & 0 deletions ansible_ai_connect/ai/api/model_pipelines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@
import ansible_ai_connect.ai.api.model_pipelines.wca.pipelines_dummy # noqa
import ansible_ai_connect.ai.api.model_pipelines.wca.pipelines_onprem # noqa
import ansible_ai_connect.ai.api.model_pipelines.wca.pipelines_saas # noqa
from ansible_ai_connect.ai.api.model_pipelines.registry import set_defaults

set_defaults()
12 changes: 8 additions & 4 deletions ansible_ai_connect/ai/api/model_pipelines/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
PipelineConfiguration,
)
from ansible_ai_connect.ai.api.model_pipelines.config_providers import Configuration
from ansible_ai_connect.ai.api.model_pipelines.nop.pipelines import NopMetaData
from ansible_ai_connect.ai.api.model_pipelines.pipelines import PIPELINE_TYPE
from ansible_ai_connect.ai.api.model_pipelines.registry import REGISTRY, REGISTRY_ENTRY

Expand All @@ -46,16 +47,19 @@ def get_pipeline(self, pipeline_type: Type[PIPELINE_TYPE]) -> PIPELINE_TYPE:
try:
# Get the configuration for the requested pipeline
pipeline_config: PipelineConfiguration = self.pipelines_config[pipeline_type.__name__]

# Get the pipeline class for the configured provider
pipelines = REGISTRY[pipeline_config.provider]
pipeline = pipelines[pipeline_type]
config = pipeline_config.config
# No explicit implementation defined; fallback to NOP
if pipeline_config.provider == "nop":

# Ensure NOP instances are created with NOP configuration
if issubclass(pipeline, NopMetaData):
logger.info(f"Using NOP implementation for '{pipeline_type.__name__}'.")
pipelines = REGISTRY["nop"]
pipeline_config = pipelines[PipelineConfiguration]()

# Construct an instance of the pipeline class with the applicable configuration
self.cache[pipeline_type] = pipeline(config)
self.cache[pipeline_type] = pipeline(pipeline_config.config)

except KeyError:
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ def __init__(self, **kwargs):


@Register(api_type="nop")
class LlamaCppConfigurationSerializer(serializers.Serializer):
class NopConfigurationSerializer(serializers.Serializer):
pass
20 changes: 20 additions & 0 deletions ansible_ai_connect/ai/api/model_pipelines/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,23 @@ def __call__(self, cls):
elif issubclass(cls, Serializer):
REGISTRY[self.api_type][Serializer] = cls
return cls


def set_defaults():

def set_defaults_for_api_type(pipeline_provider):

def v_or_default(k, v):
defaults = REGISTRY["nop"]
if v is None:
logger.warning(
f"'{k.alias()}' is not available for provider '{pipeline_provider}',"
" failing back to 'nop'"
)
return defaults[k]
return v

return {k: v_or_default(k, v) for k, v in REGISTRY[pipeline_provider].items()}

for model_mesh_api_type in get_args(t_model_mesh_api_type):
REGISTRY[model_mesh_api_type] = set_defaults_for_api_type(model_mesh_api_type)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json

from django.test import override_settings

from ansible_ai_connect.ai.api.model_pipelines.factory import ModelPipelineFactory
from ansible_ai_connect.ai.api.model_pipelines.nop.pipelines import NopChatBotPipeline
from ansible_ai_connect.ai.api.model_pipelines.pipelines import (
MetaData,
ModelPipelineChatBot,
)
from ansible_ai_connect.ai.api.model_pipelines.registry import REGISTRY, REGISTRY_ENTRY
from ansible_ai_connect.test_utils import WisdomServiceAPITestCaseBaseOIDC

CHATBOT = {
"ModelPipelineChatBot": {
"provider": "dummy",
},
}


class TestDefaultModelPipelines(WisdomServiceAPITestCaseBaseOIDC):

@override_settings(ANSIBLE_AI_MODEL_MESH_CONFIG="{}")
def test_default_pipeline_when_not_defined(self):
factory = ModelPipelineFactory()

# The configuration is empty. All pipelines should fall back to the NOP variety
pipelines = list(filter(lambda p: issubclass(p, MetaData), REGISTRY_ENTRY.keys()))
for pipeline in pipelines:
nop = REGISTRY["nop"][pipeline]
with self.assertLogs(logger="root", level="INFO") as log:
implementation = factory.get_pipeline(pipeline)
self.assertIsNotNone(implementation)
self.assertIsInstance(implementation, nop)
self.assertInLog(
f"Using NOP implementation for '{pipeline.__name__}'.",
log,
)

@override_settings(ANSIBLE_AI_MODEL_MESH_CONFIG=json.dumps(CHATBOT))
def test_default_pipeline_when_not_implemented(self):
factory = ModelPipelineFactory()

# ChatBot is configured to use "dummy" however there is no "dummy" ChatBot implementation
with self.assertLogs(logger="root", level="INFO") as log:
pipeline = factory.get_pipeline(ModelPipelineChatBot)
self.assertIsNotNone(pipeline)
self.assertIsInstance(pipeline, NopChatBotPipeline)
self.assertInLog(
"Using NOP implementation for 'ModelPipelineChatBot'.",
log,
)

0 comments on commit 2e1700d

Please sign in to comment.