Source code for smarter.apps.llm_client.manifest.brokers.llm_client

# pylint: disable=W0718,C0302
"""Smarter API LLMClient Manifest handler."""

import datetime
from typing import List, Optional, Type

from django.db import transaction
from django.forms.models import model_to_dict
from django.http import HttpRequest
from rest_framework.serializers import ModelSerializer
from taggit.managers import TaggableManager

from smarter.apps.account.utils import (
    smarter_cached_objects,
    valid_resource_owners_for_user,
)
from smarter.apps.llm_client.manifest.models.llm_client.const import MANIFEST_KIND
from smarter.apps.llm_client.manifest.models.llm_client.metadata import (
    SAMLLMClientMetadata,
)
from smarter.apps.llm_client.manifest.models.llm_client.model import SAMLLMClient
from smarter.apps.llm_client.manifest.models.llm_client.spec import (
    SAMLLMClientSpec,
    SAMLLMClientSpecConfig,
)
from smarter.apps.llm_client.manifest.models.llm_client.status import SAMLLMClientStatus
from smarter.apps.llm_client.models import (
    LLMClient,
    LLMClientAPIKey,
    LLMClientFunctions,
    LLMClientPlugin,
)
from smarter.apps.plugin.models import PluginMeta
from smarter.apps.plugin.signals import broker_ready
from smarter.apps.plugin.utils import get_plugin_examples_by_name
from smarter.common.conf import settings_defaults
from smarter.common.utils.decorators import camel_case
from smarter.lib import logging
from smarter.lib.django.waffle import SmarterWaffleSwitches
from smarter.lib.drf.models import SmarterAuthToken
from smarter.lib.journal.enum import SmarterJournalCliCommands
from smarter.lib.journal.http import SmarterJournaledJsonResponse
from smarter.lib.manifest.broker import (
    AbstractBroker,
    SAMBrokerError,
    SAMBrokerErrorNotFound,
    SAMBrokerErrorNotImplemented,
    SAMBrokerErrorNotReady,
)
from smarter.lib.manifest.enum import (
    SAMKeys,
    SAMMetadataKeys,
    SCLIResponseGet,
    SCLIResponseGetData,
)

logger = logging.getSmarterLogger(
    __name__, any_switches=[SmarterWaffleSwitches.LLM_CLIENT_LOGGING, SmarterWaffleSwitches.MANIFEST_LOGGING]
)

MAX_RESULTS = 1000


[docs] class SAMLLMClientBrokerError(SAMBrokerError): """Base exception for Smarter API LLMClient Broker handling.""" @property def get_formatted_err_message(self): return "Smarter API LLMClient Manifest Broker Error"
[docs] class LLMClientSerializer(ModelSerializer): """Django ORM model serializer for get().""" # pylint: disable=C0115 class Meta: model = LLMClient fields = ["name", "url", "dns_verification_status", "deployed", "created_at", "updated_at"]
[docs] class SAMLLMClientBroker(AbstractBroker): """ Broker for :py:class:`SAM <smarter.lib.manifest.models.AbstractSAMMetadataBase>` LLMClient manifests. This class provides a high-level abstraction for managing llm_client manifests within the Smarter platform. It acts as the central coordinator for the lifecycle of llm_client manifests, bridging the gap between declarative YAML files and persistent application state. The broker is responsible for: - Managing the lifecycle of llm_client manifests, including loading, validation, and parsing of YAML files. - Initializing Pydantic models from manifest data to ensure robust schema validation and serialization. - Integrating with Django ORM models that represent llm_client manifests, supporting creation, update, deletion, and querying of database records. - Transforming data between Django ORM models and Pydantic models to enable seamless conversion between database and API representations. - Coordinating composite models, such as LLMClient, LLMClientAPIKey, LLMClientPlugin, and LLMClientFunctions, to ensure all components of an llm_client are synchronized according to the manifest specification. - Ensuring atomic and consistent application of changes using Django's transaction management. - Providing detailed logging and error handling integrated with the Smarter platform's diagnostics systems. This broker is a key component in the deployment, configuration, and lifecycle management of llm_clients in the Smarter Framework. """ # override the base abstract manifest model with the LLMClient model _manifest: Optional[SAMLLMClient] = None _pydantic_model: Type[SAMLLMClient] = SAMLLMClient _llm_client: Optional[LLMClient] = None _functions: Optional[List[str]] = None _plugins: Optional[List[str]] = None _llm_client_api_key: Optional[LLMClientAPIKey] = None _name: Optional[str] = None _ready: bool = False
[docs] def __init__(self, *args, **kwargs): """ Initialize the SAMLLMClientBroker instance. This constructor initializes the broker by calling the parent class's constructor, which will attempt to bootstrap the class instance with any combination of raw manifest data (in JSON or YAML format), a manifest loader, or existing Django ORM models. If a manifest loader is provided and its kind matches the expected kind for this broker, the manifest is initialized using the loader's data. This class can bootstrap itself in any of the following ways: - request.body (yaml or json string) - name + account (determined via authentication of the request object) - SAMLoader instance - manifest instance - filepath to a manifest file If raw manifest data is provided, whether as a string or a dictionary, or a SAMLoader instance, the base class constructor will only goes as far as initializing the loader. The actual manifest model initialization is deferred to this constructor, which checks the loader's kind. :param args: Positional arguments passed to the parent constructor. :param kwargs: Keyword arguments passed to the parent constructor. **Example:** .. code-block:: python broker = SAMLLMClientBroker(loader=loader, plugin_meta=plugin_meta) .. seealso:: - `SAMPluginBaseBroker.__init__` """ super().__init__(*args, **kwargs) logger.debug( "%s.__init__() called with args=%s, kwargs=%s", self.formatted_class_name, args, kwargs, ) self._llm_client = kwargs.get("llm_client") if self._llm_client: logger.debug( "%s.__init__() initialized with existing LLMClient instance: %s", self.formatted_class_name, self._llm_client, ) if not self.ready: if not self.loader and not self.manifest and not self.llm_client: logger.warning( "%s.__init__() No loader nor existing LLMClient provided for %s broker. Cannot initialize.", self.formatted_class_name, self.kind, ) return if self.loader and self.loader.manifest_kind != self.kind: raise SAMBrokerErrorNotReady( f"Loader manifest kind {self.loader.manifest_kind} does not match broker kind {self.kind}", thing=self.kind, ) if self.loader: self._manifest = SAMLLMClient( apiVersion=self.loader.manifest_api_version, kind=self.loader.manifest_kind, metadata=SAMLLMClientMetadata(**self.loader.manifest_metadata), spec=SAMLLMClientSpec(**self.loader.manifest_spec), ) if self._manifest: logger.debug( "%s.__init__() initialized manifest from loader for %s %s", self.formatted_class_name, self.kind, self.manifest.metadata.name if self.manifest and self.manifest.metadata else None, ) msg = f"{self.formatted_class_name}.__init__() broker for {self.kind} {self.name} is {self.ready_state}." logger.info(msg)
@property def SerializerClass(self) -> Type[LLMClientSerializer]: """ The Django ORM model serializer class for the LLMClient. :returns: The LLMClient Django ORM model serializer class. :rtype: Type[ModelSerializer] """ return LLMClientSerializer @property def ready(self) -> bool: """ Check if the broker is ready for operations. This property determines whether the broker has been properly initialized and is ready to perform its functions. A broker is considered ready if it has a valid manifest loaded, either from raw data, a loader, or existing Django ORM models. :returns: ``True`` if the broker is ready, ``False`` otherwise. :rtype: bool """ if self._ready: return self._ready retval = super().ready if not retval: logger.debug("%s.ready() AbstractBroker is not ready for %s", self.formatted_class_name, self.kind) return False retval = self.manifest is not None or self.account is not None logger.debug( "%s.ready() manifest presence indicates ready=%s for %s", self.formatted_class_name, retval, self.kind, ) if retval: self._ready = True broker_ready.send(sender=self.__class__, broker=self) return self._ready @property def llm_client(self) -> Optional[LLMClient]: """ Provides access to the Django ORM model instance representing the current Smarter LLMClient. This property retrieves the LLMClient object associated with the broker's account and name. If a matching LLMClient record exists in the database, it is returned and cached for future access. If no such record exists, and a manifest is available, a new LLMClient instance is created using data extracted from the manifest and then persisted to the database. This property ensures that the broker always has access to a valid LLMClient model, either by fetching an existing record or by creating one from the manifest specification. The LLMClient model stores the configuration and runtime state of the llm_client, and is used for all database operations related to the llm_client's lifecycle. :returns: The Django ORM LLMClient instance if found or created, otherwise ``None`` if neither a database record nor a manifest is available. :rtype: Optional[LLMClient] .. note:: The returned LLMClient object is essential for linking related resources such as API keys, plugins, and functions, and for performing updates or queries on the llm_client's state. .. admonition:: FIX NOTE This should be refactored/removed in favor of orm_instance. There is no logic in this property that merits it overriding the parent orm_instance property. .. admonition:: FIX NOTE This is breaking an unwritten rule of Smarter resources in that it is lazily **creating** a database record on a property getter. Creating/updating database records should be handled in apply(). """ if self._llm_client: return self._llm_client try: self._llm_client = LLMClient.get_cached_object( invalidate=True, user_profile=self.user_profile, name=self.name ) logger.debug( "%s.llm_client() retrieved existing LLMClient instance %s owned by %s from database.", self.formatted_class_name, self._llm_client, self.user_profile, ) return self._llm_client except LLMClient.DoesNotExist: self._llm_client = None logger.debug( "%s.llm_client() LLMClient instance not found for user_profile %s. Attempting to create a new instance.", self.formatted_class_name, self.user_profile, ) if self.manifest: data = self.manifest_to_django_orm() data["user_profile"] = self.user_profile logger.debug("%s.llm_client() Creating new LLMClient with data: %s", self.formatted_class_name, data) tags = data.pop("tags", []) self._llm_client = LLMClient.objects.create(**data) if self._llm_client and tags: self._llm_client.tags.set(tags) self._created = True logger.warning( "%s.llm_client() lazily created new LLMClient instance %s owned by %s. This logic should be handled in apply().", self.formatted_class_name, self._llm_client, self.user_profile, ) else: logger.warning( "%s.llm_client() %s not found for user_profile %s", self.formatted_class_name, self._llm_client, self.user_profile, ) return self._llm_client @property def functions(self) -> Optional[List[str]]: """ Provides access to the Django ORM model class representing LLMClient functions. This property retrieves a list of the names of the ``LLMClientFunctions`` Django ORM model objects that are linked to the LLMClient managed by this broker. The functions define the capabilities and operations that the LLMClient can perform, as specified in the manifest. If the functions have already been retrieved and cached, they are returned immediately. Otherwise, the property attempts to fetch the functions from the database using the current LLMClient instance. If no functions are found, ``None`` is returned. :returns: A list of names of ``LLMClientFunctions`` instances associated with the LLMClient, or ``None`` if no functions exist. :rtype: Optional[List[str]] """ if self._functions: return self._functions if not self.llm_client: return None queryset = LLMClientFunctions.objects.filter(llm_client=self.llm_client) self._functions = list(queryset.values_list("name", flat=True)) return self._functions @property def plugins(self) -> Optional[List[str]]: """ Provides access to the Django ORM model class representing LLMClient plugins. This property retrieves a list of the names of the ``LLMClientPlugin`` Django ORM model objects that are linked to the LLMClient managed by this broker. The plugins extend the functionality of the LLMClient, as specified in the manifest. If the plugins have already been retrieved and cached, they are returned immediately. Otherwise, the property attempts to fetch the plugins from the database using the current LLMClient instance. If no plugins are found, ``None`` is returned. :returns: A list of names of ``LLMClientPlugin`` instances associated with the LLMClient, or ``None`` if no plugins exist. :rtype: Optional[List[str]] """ if self._plugins: return self._plugins if not self.llm_client: return None queryset = LLMClientPlugin.objects.filter(llm_client=self.llm_client) self._plugins = list(queryset.values_list("plugin_meta__name", flat=True)) return self._plugins @property def llm_client_api_key(self) -> Optional[LLMClientAPIKey]: """ Provides access to the API key associated with the current LLMClient instance. This property retrieves the ``LLMClientAPIKey`` Django ORM model object that is linked to the LLMClient managed by this broker. The API key is used for authenticating requests made by the LLMClient and is stored securely in the database. If the API key has already been retrieved and cached, it is returned immediately. Otherwise, the property attempts to fetch the API key from the database using the current LLMClient instance. If no API key is found, ``None`` is returned. This property is essential for operations that require authentication or authorization on behalf of the LLMClient, such as invoking external APIs or managing secure resources. :returns: The ``LLMClientAPIKey`` instance associated with the LLMClient, or ``None`` if no API key exists. :rtype: Optional[LLMClientAPIKey] .. important:: If the LLMClientAPIKey is ``None``, it indicates that no API key has been set for the LLMClient, which in turn will enable anonymous unauthenticated access for the LLMClient. """ if self._llm_client_api_key: return self._llm_client_api_key try: self._llm_client_api_key = LLMClientAPIKey.objects.get(llm_client=self.llm_client) except LLMClientAPIKey.DoesNotExist: return None return self._llm_client_api_key
[docs] def manifest_to_django_orm(self) -> dict: """ Convert the Smarter API LLMClient manifest into a dictionary suitable for creating or updating a Django ORM LLMClient model. This method extracts all relevant configuration, metadata, and versioning information from the loaded manifest and transforms it into a dictionary format compatible with Django ORM operations. The manifest's configuration is first dumped and converted from camelCase to snake_case to match Django's field naming conventions. The resulting dictionary includes the account, name, description, and version fields from the manifest metadata, as well as all configuration fields from the manifest specification. This dictionary can be used to instantiate or update a LLMClient ORM model instance in the database. If the manifest is not loaded or is invalid, an exception is raised to indicate that the broker is not ready to perform the transformation. :returns: A dictionary containing all fields required to create or update a Django ORM LLMClient model. :rtype: dict :raises SAMBrokerErrorNotReady: If the manifest is not loaded or cannot be found. :raises SAMLLMClientBrokerError: If the manifest configuration cannot be converted to a dictionary. """ if not self.manifest: raise SAMBrokerErrorNotReady( f"Manifest not loaded for {self.kind} broker. Cannot convert to Django ORM.", thing=self.kind ) metadata = super().manifest_to_django_orm() config_dump = self.manifest.spec.config.model_dump() config_dump = self.to_snake_case(config_dump) if not isinstance(config_dump, dict): raise SAMLLMClientBrokerError( f"Failed to convert {self.kind} {self.manifest.metadata.name} to dict. Got {type(config_dump)}", thing=self.kind, ) retval = { **metadata, **config_dump, } logger.debug( "%s.manifest_to_django_orm() converted manifest to Django ORM dict: %s", self.formatted_class_name, retval, ) return retval
[docs] @camel_case() def django_orm_to_manifest_dict(self) -> Optional[dict]: """ Transform the Django ORM LLMClient model instance into a dictionary compatible with the Smarter API LLMClient manifest format. This method converts the current LLMClient ORM model and its related resources (plugins, functions, API key) into a dictionary structure that matches the expected schema for a Pydantic manifest. The conversion includes renaming fields from snake_case to camelCase, removing internal-only fields, and assembling metadata, spec, and status sections as required by the manifest. The resulting dictionary contains all configuration, metadata, plugin, function, and status information necessary to reconstruct the manifest for the llm_client. This enables seamless round-trip conversion between database state and manifest representation. If the LLMClient model is not available, the method logs a warning and returns ``None``. If the conversion fails, an exception is raised to indicate the error. :returns: A dictionary representing the Smarter API LLMClient manifest, or ``None`` if the LLMClient model is not set. :rtype: Optional[dict] :raises SAMLLMClientBrokerError: If the ORM model cannot be converted to a manifest dictionary. See also: - :py:meth:`smarter.apps.llm_client.manifest.brokers.llm_client.SAMLLMClientBroker.manifest_to_django_orm` - :py:class:`smarter.apps.llm_client.manifest.models.llm_client.SAMLLMClient` - :py:class:`smarter.apps.llm_client.manifest.models.llm_client.metadata.SAMLLMClientMetadata` - :py:class:`smarter.apps.llm_client.manifest.models.llm_client.spec.SAMLLMClientSpec` - :py:class:`smarter.apps.llm_client.manifest.models.llm_client.status.SAMLLMClientStatus` """ if not self.account: raise SAMBrokerErrorNotReady( f"Account not loaded for {self.kind} broker. Cannot convert Django ORM to manifest dict.", thing=self.kind, ) if not self.user_profile: raise SAMBrokerErrorNotReady( f"User profile not loaded for {self.kind} broker. Cannot convert Django ORM to manifest dict.", thing=self.kind, ) if not self.llm_client: logger.warning( "%s.django_orm_to_manifest_dict() called without a LLMClient. This could affect broker operations.", self.formatted_class_name, ) return None llm_client_dict = model_to_dict(self.llm_client) llm_client_dict = self.to_camel_case(llm_client_dict) if not isinstance(llm_client_dict, dict): raise SAMLLMClientBrokerError( f"Failed to convert {self.kind} {self.llm_client.name} to dict", thing=self.kind ) llm_client_dict.pop("id") llm_client_dict.pop("name") llm_client_dict.pop("description") llm_client_dict.pop("version") plugins = LLMClientPlugin.objects.filter(llm_client=self.llm_client) plugin_names = [plugin.plugin_meta.name for plugin in plugins] functions = LLMClientFunctions.objects.filter(llm_client=self.llm_client) function_names = [function.name for function in functions] api_key = self.llm_client_api_key.api_key if self.llm_client_api_key else None meta = SAMLLMClientMetadata( name=self.llm_client.name, description=self.llm_client.description, version=self.llm_client.version, tags=self.llm_client.tags_list, annotations=self.llm_client.annotations if isinstance(self.llm_client.annotations, list) else [], ) spec_config = SAMLLMClientSpecConfig(**llm_client_dict) spec = SAMLLMClientSpec(config=spec_config, plugins=plugin_names, functions=function_names, apiKey=api_key) status = SAMLLMClientStatus( accountNumber=self.account.account_number, username=self.user_profile.user.username, recordLocator=self.llm_client.record_locator, created=self.llm_client.created_at, modified=self.llm_client.updated_at, deployed=self.llm_client.deployed, defaultHost=self.llm_client.default_host, sandboxHost=self.llm_client.sandbox_host, hostname=self.llm_client.hostname, dnsVerificationStatus=self.llm_client.dns_verification_status, customUrl=self.llm_client.custom_url, defaultUrl=self.llm_client.default_url, sandboxUrl=self.llm_client.sandbox_url, url=self.llm_client.url, urlLLMClient=self.llm_client.url_llm_client, urlChatConfig=self.llm_client.url_chat_config, urlChatapp=self.llm_client.url_chatapp, ) model = SAMLLMClient( apiVersion=self.api_version, kind=self.kind, metadata=meta, spec=spec, status=status, ) logger.debug( "%s.django_orm_to_manifest_dict() converted LLMClient %s to manifest dict: %s", self.formatted_class_name, self.llm_client.name, model.model_dump(), ) return model.model_dump()
########################################################################### # Smarter abstract property implementations ########################################################################### @property def formatted_class_name(self) -> str: """ Returns a formatted string representing the class name for logging purposes. This property generates a human-readable class name that is used to improve the clarity and consistency of log messages throughout the broker. The formatted class name includes the parent class name and appends the specific broker class identifier, making it easier to trace log entries back to their source within the codebase. The formatted class name is especially useful in environments where multiple brokers or components are active, as it helps distinguish log messages and aids in debugging and monitoring application behavior. :returns: A string containing the formatted class name, suitable for use in log output. :rtype: str """ class_name = f"{SAMLLMClientBroker.__name__}[{id(self)}]" return self.formatted_text(class_name) @property def kind(self) -> str: """ Returns the manifest kind for the Smarter API LLMClient. This property provides the specific kind identifier used to classify the Smarter API LLMClient manifest within the Smarter platform. The kind is a key component of the manifest schema, allowing the system to recognize and process llm_client manifests appropriately. The kind value is defined as a constant in the llm_client manifest model and is used throughout the broker to ensure consistency when handling llm_client manifests. :returns: The manifest kind string for the Smarter API LLMClient. :rtype: str .. important:: The kind property is essential for manifest validation, routing, and processing within the Smarter platform. """ return MANIFEST_KIND @property def manifest(self) -> Optional[SAMLLMClient]: """ Returns the Smarter API LLMClient manifest as a Pydantic model. This method constructs and returns an instance of the ``SAMLLMClient`` Pydantic model, which represents the full manifest for a Smarter API LLMClient. The manifest contains all configuration, metadata, and specification details required to describe and deploy an llm_client within the Smarter platform. The manifest is initialized using data provided by the manifest loader. The loader supplies the manifest's API version, kind, metadata, and specification, which are passed to the respective fields of the ``SAMLLMClient`` model. The metadata and spec fields are themselves Pydantic models (``SAMLLMClientMetadata`` and ``SAMLLMClientSpec``), and are recursively initialized with their corresponding data. Unlike child models, which are automatically cascade-initialized by Pydantic when constructing the parent model, the top-level manifest model must be explicitly instantiated in this method. This ensures that all manifest data is validated and structured according to the schema defined by the ``SAMLLMClient`` model. If the manifest has already been initialized and cached, this method returns the cached instance. If the loader is present and its manifest kind matches the expected kind, a new manifest instance is created and cached before returning. :returns: An instance of ``SAMLLMClient`` representing the llm_client manifest, or ``None`` if the manifest cannot be initialized. :rtype: Optional[SAMLLMClient] """ if self._manifest: if not isinstance(self._manifest, SAMLLMClient): raise SAMLLMClientBrokerError("Cached manifest is not a SAMLLMClient instance", thing=self.kind) return self._manifest if self.loader and self.loader.manifest_kind == self.kind: logger.debug( "%s.manifest() initializing %s from SAMLoader with name %s", self.formatted_class_name, self.kind, self.loader.manifest_metadata.get(SAMMetadataKeys.NAME.value, "unknown"), ) self._manifest = SAMLLMClient( apiVersion=self.loader.manifest_api_version, kind=self.loader.manifest_kind, metadata=SAMLLMClientMetadata(**self.loader.manifest_metadata), spec=SAMLLMClientSpec(**self.loader.manifest_spec), ) return self._manifest if self._llm_client: self._manifest = self.django_orm_to_manifest_dict() # type: ignore if self._manifest: logger.debug( "%s.manifest() initialized from loader for existing LLMClient %s with name %s", self.formatted_class_name, self._llm_client, self._llm_client.name, ) return self._manifest else: logger.warning( "%s.manifest() could not initialize", self.formatted_class_name, ) return self._manifest ########################################################################### # Smarter manifest abstract method implementations ###########################################################################
[docs] def cache_invalidations(self) -> None: """ Handle broker specific cache invalidation logic. We should invalidate any cached objects that are related to the LLMClient when any mutation occurs. In this case, we need to invalidate the LLMClient cache itself, but also any related objects such as the plugins, functions and api keys. .. returns: None .. rtype: None """ logger.debug("%s.cache_invalidations() called.", self.formatted_class_name_cache_invalidations) # 1.) invalidate the LLMClient cache itself. # ----------------------------- LLMClient.get_cached_object(pk=self.llm_client.id, invalidate=True) # type: ignore # 2.) invalidate anything else in which the llm_client is part of. this could # include listviews, the plugins, functions and api keys. # ----------------------------- LLMClient.get_cached_objects(user_profile=self.user_profile, invalidate=True) # 3.) invalidate all children of LLMClient # ----------------------------- llm_client_functions = LLMClientFunctions.objects.filter(llm_client=self.llm_client) for llm_client_function in llm_client_functions: LLMClientFunctions.get_cached_object(pk=llm_client_function.id, invalidate=True) # type: ignore llm_client_plugins = LLMClientPlugin.objects.filter(llm_client=self.llm_client) for llm_client_plugin in llm_client_plugins: LLMClientPlugin.get_cached_object(pk=llm_client_plugin.id, invalidate=True) # type: ignore llm_client_api_keys = LLMClientAPIKey.objects.filter(llm_client=self.llm_client) for llm_client_api_key in llm_client_api_keys: LLMClientAPIKey.get_cached_object(pk=llm_client_api_key.id, invalidate=True) # type: ignore return super().cache_invalidations()
@property def ORMMetaModelClass(self) -> Type[LLMClient]: """ Return the Django ORM meta model class for the broker. :return: The Django ORM meta model class definition for the broker. :rtype: Type[LLMClient] """ return LLMClient @property def ORMModelClass(self) -> Type[LLMClient]: """ The Django ORM model class for the LLMClient. :returns: The LLMClient Django ORM model class. :rtype: Type[LLMClient] """ return LLMClient
[docs] def example_manifest(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: """ Return an example manifest for the Smarter API LLMClient. :returns: A JSON response containing an example Smarter API LLMClient manifest. :rtype: SmarterJournaledJsonResponse See also: - :py:class:`smarter.apps.llm_client.manifest.models.llm_client.SAMLLMClient` - :py:class:`smarter.lib.manifest.enumSAMKeys` - :py:class:`smarter.apps.llm_client.manifest.enum.SAMMetadataKeys` - :py:class:`smarter.apps.llm_client.manifest.enum.SCLIResponseGet` - :py:class:`smarter.apps.llm_client.manifest.enum.SCLIResponseGetData` - :py:class:`from smarter.common.conf.settings_defaults` """ command = self.example_manifest.__name__ command = SmarterJournalCliCommands(command) meta_data = SAMLLMClientMetadata( name="example_llm_client", description="This is an example llm_client manifest generated by the SAMLLMClientBroker. It serves as a template for creating your own llm_client manifests.", version="1.0.0", tags=["example", "template", "school-project"], annotations=[ {"color": "red"}, {"size": "medium"}, {"hash": "sha256:abc123def456"}, ], ) config = SAMLLMClientSpecConfig( subdomain=None, customDomain=None, deployed=False, provider=settings_defaults.LLM_DEFAULT_PROVIDER, defaultModel=settings_defaults.LLM_DEFAULT_MODEL, defaultSystemRole=settings_defaults.LLM_DEFAULT_SYSTEM_ROLE, defaultTemperature=settings_defaults.LLM_DEFAULT_TEMPERATURE, defaultMaxTokens=settings_defaults.LLM_DEFAULT_MAX_TOKENS, appName="Example LLMClient", appAssistant="Example Assistant", appWelcomeMessage="Welcome to the Example LLMClient! How can I assist you today?", appExamplePrompts=[ "What is the weather like today?", "Can you tell me a joke?", "How do I reset my password?", ], appPlaceholder="Type your message here...", appInfoUrl="https://example.com/info", appBackgroundImageUrl="https://cdn.smarter.sh/prompt-ui/background.png", appLogoUrl="https://cdn.smarter.sh/prompt-ui/logo.png", appFileAttachment=False, ) spec = SAMLLMClientSpec( config=config, plugins=get_plugin_examples_by_name(), functions=["date_calculator", "get_current_weather"], apiKey="snake_case_api_key_name", ) status = SAMLLMClientStatus( accountNumber=smarter_cached_objects.smarter_account.account_number, username=smarter_cached_objects.smarter_admin.username, recordLocator="abc123def456", created=datetime.datetime.now(), modified=datetime.datetime.now(), deployed=False, defaultHost="example-llm_client.smarterapi.com", defaultUrl="https://example-llm_client.smarterapi.com", customUrl=None, sandboxHost="https://example.com", sandboxUrl="https://example.com/api/v1/llm-clients/1/", hostname="example-llm_client.smarterapi.com", url="https://example.com/api/v1/llm-clients/1/", urlLLMClient="https://example-llm_client.smarterapi.com/llm_client", urlChatapp="https://example-llm_client.smarterapi.com/chatapp", urlChatConfig="https://example-llm_client.smarterapi.com/chatconfig", dnsVerificationStatus="verified", ) model = SAMLLMClient(apiVersion=self.api_version, kind=self.kind, metadata=meta_data, spec=spec, status=status) return self.json_response_ok(command=command, data=model.model_dump())
[docs] def get(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.get.__name__ command = SmarterJournalCliCommands(command) data = [] name = kwargs.get(SAMMetadataKeys.NAME.value, None) name = self.clean_cli_param(param=name, param_name="name", url=self.smarter_build_absolute_uri(request)) # generate a QuerySet of PluginMeta objects that match our search criteria if name: llm_clients = LLMClient.objects.filter(user_profile__account=self.account, name=name) else: llm_clients = LLMClient.objects.filter(user_profile__account=self.account) valid_owners = valid_resource_owners_for_user(user_profile=self.user_profile) llm_clients = llm_clients.filter(user_profile__in=valid_owners).order_by("name")[:MAX_RESULTS] logger.debug( "%s.get() found %s LLMClients for account %s", self.formatted_class_name, llm_clients.count(), self.account ) # iterate over the QuerySet and use a serializer to create a model dump for each LLMClient for llm_client in llm_clients: try: model_dump = LLMClientSerializer(llm_client).data if not model_dump: raise SAMLLMClientBrokerError( f"Model dump failed for {self.kind} {llm_client.name}", thing=self.kind, command=command ) camel_cased_model_dump = self.to_camel_case(model_dump) data.append(camel_cased_model_dump) except Exception as e: logger.error( "%s.get() failed to serialize %s %s", self.formatted_class_name, self.kind, llm_client.name, exc_info=True, ) raise SAMLLMClientBrokerError( f"Failed to serialize {self.kind} {llm_client.name}", thing=self.kind, command=command ) from e data = { SAMKeys.APIVERSION.value: self.api_version, SAMKeys.KIND.value: self.kind, SAMMetadataKeys.NAME.value: name, SAMKeys.METADATA.value: {"count": len(data)}, SCLIResponseGet.KWARGS.value: kwargs, SCLIResponseGet.DATA.value: { SCLIResponseGetData.TITLES.value: self.get_model_titles(serializer=LLMClientSerializer()), SCLIResponseGetData.ITEMS.value: data, }, } return self.json_response_ok(command=command, data=data)
# pylint: disable=too-many-branches
[docs] def apply(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: """ Apply the manifest. copy the manifest data to the Django ORM model and save the model to the database. Call super().apply() to ensure that the manifest is loaded and validated before applying the manifest to the Django ORM model. Note that there are fields included in the manifest that are not editable and are therefore removed from the Django ORM model dict prior to attempting the save() command. These fields are defined in the readonly_fields list. LLMClient is a composite model that includes the LLMClient, LLMClientAPIKey, LLMClientPlugin and LLMClientFunctions models. All of these are represented in the manifest spec and are created or updated as needed. .. note:: tags are handled separately because they are of type TaggableManager and require a different method to set them. """ super().apply(request, kwargs) command = self.apply.__name__ command = SmarterJournalCliCommands(command) if not self.ready: raise SAMBrokerErrorNotReady( f"{self.kind} {self.name} broker is not ready", thing=self.kind, command=command ) if not self.manifest: raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command) if not self.manifest.spec: raise SAMBrokerErrorNotReady( f"{self.kind} {self.name} manifest spec not found", thing=self.kind, command=command ) if not isinstance(self.llm_client, LLMClient): raise SAMLLMClientBrokerError(f"LLMClient {self.name} not found", thing=self.kind, command=command) with transaction.atomic(): readonly_fields = ["id", "created_at", "updated_at", "tags"] try: data = self.manifest_to_django_orm() tags = data.get("tags", []) for field in readonly_fields: data.pop(field, None) for key, value in data.items(): setattr(self.llm_client, key, value) if self.llm_client.user_profile != self.user_profile: raise SAMLLMClientBrokerError( f"User profile mismatch for {self.kind} {self.manifest.metadata.name}", thing=self.kind, command=command, ) self.llm_client.save() # Fix note: occasionally seeing AttributeError: \'list\' object has no attribute \'set\ in the logs, # which is why this is wrapped in a try/except block. try: if not isinstance(self.llm_client.tags, TaggableManager): logger.warning( "%s.apply() llm_client.tags is a list instead of a TaggableManager for %s %s owned by %s. This is unexpected and may indicate an issue with the LLMClient model definition or the database state. Tags=%s", self.formatted_class_name, self.kind, self.manifest.metadata.name, self.user_profile, tags, ) else: self.llm_client.tags.set(tags) # pylint: disable=broad-except except Exception as e: logger.error( "%s.apply() failed to set tags for %s %s owned by %s. Tags=%s. Error: %s", self.formatted_class_name, self.kind, self.manifest.metadata.name, self.user_profile, tags, e, exc_info=True, ) self.llm_client.refresh_from_db() except Exception as e: logger.error( "%s.apply() failed to save %s %s owned by %s. Error: %s", self.formatted_class_name, self.kind, self.manifest.metadata.name, self.user_profile, e, exc_info=True, ) raise SAMLLMClientBrokerError( f"Failed to apply {self.kind} {self.manifest.metadata.name}", thing=self.kind, command=command ) from e # LLMClientAPIKey: create or update the API Key # ------------- if self.manifest.spec.apiKey: try: api_key = SmarterAuthToken.objects.get(name=self.manifest.spec.apiKey, user=self.user) except SmarterAuthToken.DoesNotExist as e: logger.error( "%s.apply() failed to find SmarterAuthToken %s", self.formatted_class_name, self.manifest.spec.apiKey, exc_info=True, ) raise SAMBrokerErrorNotFound( f"API Key {self.manifest.spec.apiKey} not found", thing=self.kind, command=command ) from e for key in LLMClientAPIKey.objects.filter(llm_client=self.llm_client): if key.api_key != api_key: key.delete() logger.debug("%s.apply() Detached SmarterAuthToken %s from LLMClient %s", self.formatted_class_name, key.name, self.llm_client.name) # type: ignore[union-attr] _, created = LLMClientAPIKey.objects.get_or_create(llm_client=self.llm_client, api_key=api_key) if created: logger.debug( "%s.apply() SmarterAuthToken %s attached to LLMClient %s", self.formatted_class_name, self.manifest.spec.apiKey, self.llm_client.name, ) # LLMClientPlugin: add what's missing, remove what is in the model but is not in the manifest # ------------- for plugin in LLMClientPlugin.objects.filter(llm_client=self.llm_client): if not self.manifest.spec.plugins or ( self.manifest.spec.plugins and plugin.plugin_meta.name not in self.manifest.spec.plugins ): plugin.delete() logger.debug( "%s.apply() Detached Plugin %s from LLMClient %s", self.formatted_class_name, plugin.plugin_meta.name, self.llm_client.name, ) if self.manifest.spec.plugins: for plugin_name in self.manifest.spec.plugins: plugin_name = str(self.to_snake_case(plugin_name)) try: plugin = PluginMeta.objects.get(name=plugin_name, user_profile=self.user_profile) except PluginMeta.DoesNotExist as e: logger.error( "%s.apply() did not find a Plugin named %s", self.formatted_class_name, plugin_name, exc_info=True, ) raise SAMBrokerErrorNotFound( f"Plugin {plugin_name} not found for account {self.account.account_number if self.account else 'unknown'}", thing=self.kind, command=command, ) from e _, created = LLMClientPlugin.objects.get_or_create(llm_client=self.llm_client, plugin_meta=plugin) if created: logger.debug( "%s.apply() attached Plugin %s to LLMClient %s", self.formatted_class_name, plugin.name, self.llm_client.name, ) # LLMClientFunctions: add what's missing, remove what's in the model but not in the manifest # ------------- for function in LLMClientFunctions.objects.filter(llm_client=self.llm_client): if function.name not in self.manifest.spec.functions: function.delete() logger.debug( "%s.apply() Detached Function %s from LLMClient %s", self.formatted_class_name, function.name, self.llm_client.name, ) if self.manifest.spec.functions: for function in self.manifest.spec.functions: if function not in LLMClientFunctions.choices_list(): return self.json_response_err_notfound( command=command, message=f"Function {function} not found. Valid functions are: {LLMClientFunctions.choices_list()}", ) _, created = LLMClientFunctions.objects.get_or_create(llm_client=self.llm_client, name=function) if created: logger.debug( "%s.apply() attached Function %s to LLMClient %s", self.formatted_class_name, function, self.llm_client.name, ) # done! return the response. Django will take care of committing the transaction self.cache_invalidations() return self.json_response_ok(command=command, data=self.to_json())
[docs] def prompt(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.prompt.__name__ command = SmarterJournalCliCommands(command) raise SAMBrokerErrorNotImplemented(message="Prompt not implemented", thing=self.kind, command=command)
[docs] def describe(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.describe.__name__ command = SmarterJournalCliCommands(command) if self.name is None: raise SAMBrokerErrorNotReady(f"{self.kind} name property is not set.", thing=self.kind, command=command) if self.llm_client: try: data = self.django_orm_to_manifest_dict() return self.json_response_ok(command=command, data=data) except Exception as e: logger.error( "%s.describe() failed to describe %s %s", self.formatted_class_name, self.kind, self.name, exc_info=True, ) raise SAMLLMClientBrokerError( f"Failed to describe {self.kind} {self.name}", thing=self.kind, command=command ) from e raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command)
[docs] def delete(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.delete.__name__ command = SmarterJournalCliCommands(command) if self.name is None: raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command) if self.llm_client: try: self.llm_client.delete() self.cache_invalidations() return self.json_response_ok(command=command, data={}) except Exception as e: logger.error( "%s.delete() failed to delete %s %s", self.formatted_class_name, self.kind, self.name, exc_info=True, ) raise SAMLLMClientBrokerError( f"Failed to delete {self.kind} {self.name}", thing=self.kind, command=command ) from e raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command)
[docs] def deploy(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.deploy.__name__ command = SmarterJournalCliCommands(command) if self.name is None: raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command) if self.llm_client: try: self.llm_client.deployed = True self.llm_client.save() self.llm_client.refresh_from_db() return self.json_response_ok(command=command, data={}) except Exception as e: logger.error( "%s.deploy() failed to deploy %s %s", self.formatted_class_name, self.kind, self.name, exc_info=True, ) raise SAMLLMClientBrokerError( f"Failed to deploy {self.kind} {self.name}", thing=self.kind, command=command ) from e raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command)
[docs] def undeploy(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.deploy.__name__ command = SmarterJournalCliCommands(command) if self.name is None: raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command) if self.llm_client: try: self.llm_client.deployed = False self.llm_client.save() self.llm_client.refresh_from_db() return self.json_response_ok(command=command, data={}) except Exception as e: logger.error( "%s.undeploy() failed to undeploy %s %s", self.formatted_class_name, self.kind, self.name, exc_info=True, ) raise SAMLLMClientBrokerError( f"Failed to undeploy {self.kind} {self.name}", thing=self.kind, command=command ) from e raise SAMBrokerErrorNotReady(f"{self.kind} {self.name} not found", thing=self.kind, command=command)
[docs] def logs(self, request: HttpRequest, *args, **kwargs) -> SmarterJournaledJsonResponse: command = self.logs.__name__ command = SmarterJournalCliCommands(command) data = {} return self.json_response_ok(command=command, data=data)