From 2e1700ded64a1b4b3bca4fd7f31ca20e7d10e11c Mon Sep 17 00:00:00 2001 From: Michael Anstis Date: Fri, 24 Jan 2025 09:39:55 +0000 Subject: [PATCH] Fix missing pipeline implementations. --- .../ai/api/model_pipelines/README.md | 110 ++++++++++++++++++ .../ai/api/model_pipelines/__init__.py | 3 + .../ai/api/model_pipelines/factory.py | 12 +- .../api/model_pipelines/nop/configuration.py | 2 +- .../ai/api/model_pipelines/registry.py | 20 ++++ .../tests/test_default_pipelines.py | 65 +++++++++++ 6 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 ansible_ai_connect/ai/api/model_pipelines/README.md create mode 100644 ansible_ai_connect/ai/api/model_pipelines/tests/test_default_pipelines.py diff --git a/ansible_ai_connect/ai/api/model_pipelines/README.md b/ansible_ai_connect/ai/api/model_pipelines/README.md new file mode 100644 index 000000000..c38891636 --- /dev/null +++ b/ansible_ai_connect/ai/api/model_pipelines/README.md @@ -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="")`. + +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: , + ModelPipelineCompletions: + ModelPipelineContentMatch: + ModelPipelinePlaybookGeneration: + ModelPipelineRoleGeneration: + ModelPipelinePlaybookExplanation: + ModelPipelineChatBot: + PipelineConfiguration: + Serializer: + } + ... +} +``` + +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). diff --git a/ansible_ai_connect/ai/api/model_pipelines/__init__.py b/ansible_ai_connect/ai/api/model_pipelines/__init__.py index 5cf8e245c..7376f1b39 100644 --- a/ansible_ai_connect/ai/api/model_pipelines/__init__.py +++ b/ansible_ai_connect/ai/api/model_pipelines/__init__.py @@ -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() diff --git a/ansible_ai_connect/ai/api/model_pipelines/factory.py b/ansible_ai_connect/ai/api/model_pipelines/factory.py index 2a65bccba..d4c3cd972 100644 --- a/ansible_ai_connect/ai/api/model_pipelines/factory.py +++ b/ansible_ai_connect/ai/api/model_pipelines/factory.py @@ -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 @@ -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 diff --git a/ansible_ai_connect/ai/api/model_pipelines/nop/configuration.py b/ansible_ai_connect/ai/api/model_pipelines/nop/configuration.py index e594c77f0..a9e10b7e5 100644 --- a/ansible_ai_connect/ai/api/model_pipelines/nop/configuration.py +++ b/ansible_ai_connect/ai/api/model_pipelines/nop/configuration.py @@ -40,5 +40,5 @@ def __init__(self, **kwargs): @Register(api_type="nop") -class LlamaCppConfigurationSerializer(serializers.Serializer): +class NopConfigurationSerializer(serializers.Serializer): pass diff --git a/ansible_ai_connect/ai/api/model_pipelines/registry.py b/ansible_ai_connect/ai/api/model_pipelines/registry.py index b4f168d41..2fc99b03e 100644 --- a/ansible_ai_connect/ai/api/model_pipelines/registry.py +++ b/ansible_ai_connect/ai/api/model_pipelines/registry.py @@ -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) diff --git a/ansible_ai_connect/ai/api/model_pipelines/tests/test_default_pipelines.py b/ansible_ai_connect/ai/api/model_pipelines/tests/test_default_pipelines.py new file mode 100644 index 000000000..5a8445d3a --- /dev/null +++ b/ansible_ai_connect/ai/api/model_pipelines/tests/test_default_pipelines.py @@ -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, + )