Source code for smarter.apps.prompt.views.detailviews.prompt_manifest_view

# pylint: disable=W0613,C0302
"""
LLMClientDetailView is a Django class-based view that renders a detail view of.

a SAM manifest for an llm_client.
"""

import logging
from typing import Optional

import yaml
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from django.shortcuts import render

from smarter.apps.api.v1.manifests.enum import SAMKinds
from smarter.apps.docs.views.base import DocsBaseView
from smarter.apps.llm_client.models import (
    LLMClient,
    LLMClientHelper,
)
from smarter.common.conf import smarter_settings
from smarter.common.helpers.console_helpers import formatted_json
from smarter.lib.django import waffle
from smarter.lib.django.http.shortcuts import (
    SmarterHttpResponseNotFound,
    SmarterHttpResponseServerError,
)
from smarter.lib.django.waffle import SmarterWaffleSwitches
from smarter.lib.logging import WaffleSwitchedLoggerWrapper

MAX_RETURNED_PLUGINS = 10
PROMPT_LIST_CACHE_TIMEOUT = smarter_settings.cache_expiration
WORKBENCH_CACHE_TIMEOUT = 10  # 10 seconds. keeps the workbench snappy while avoiding appearing stale.


[docs] def should_log(level): """Check if logging should be done based on the waffle switch.""" return waffle.switch_is_active(SmarterWaffleSwitches.PROMPT_LOGGING)
base_logger = logging.getLogger(__name__) logger = WaffleSwitchedLoggerWrapper(base_logger, should_log)
[docs] def should_log_verbose(level): """Check if logging should be done based on the waffle switch.""" return smarter_settings.verbose_logging
verbose_logger = WaffleSwitchedLoggerWrapper(base_logger, should_log_verbose)
[docs] class LLMClientDetailView(DocsBaseView): """ Renders the detail view for a Smarter llm_client. This view renders a detailed manifest for a specific llm_client, including its configuration and metadata, in YAML format. It is intended for authenticated users and provides error handling for missing or unsupported llm_client kinds and names. :param request: Django HTTP request object. :type request: ASGIRequest :param args: Additional positional arguments. :type args: tuple :param kwargs: Keyword arguments, must include 'name' (llm_client name) and 'kind' (llm_client type). :type kwargs: dict :returns: Rendered HTML page with llm_client manifest details, or a 404 error page if the llm_client is not found or parameters are invalid. :rtype: HttpResponse .. note:: The llm_client name and kind must be provided and valid. Otherwise, a "not found" response is returned. .. seealso:: :class:`LLMClient` for llm_client retrieval. :class:`ApiV1CliDescribeApiView` for API details. **Example usage**:: GET /llm-client/detail/?name=my_llm_client&kind=custom """ template_path = "prompt/manifest-detail.html" llm_client: Optional[LLMClient] = None llm_client_helper: Optional[LLMClientHelper] = None @property def formatted_class_name(self) -> str: """Returns a formatted string of the class name for logging purposes.""" class_name = f"{__name__}.{LLMClientDetailView.__name__}[{id(self)}]" return self.formatted_text(class_name)
[docs] def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle GET requests to render the llm_client manifest detail view. This method processes the incoming request to retrieve the specified llm_client's manifest details and renders them in a user-friendly format. It performs validation on the provided llm_client name and kind, retrieves the llm_client metadata, and handles any errors that may arise during this process. Process: 1. Extract and validate 'name' and 'kind' from kwargs. 2. Retrieve the llm_client metadata using the provided name and user context. 3. If the llm_client is found, call the API view to get the llm_client details 4. Convert the JSON response to YAML format for better readability. 5. Render the llm_client manifest detail template with the retrieved data. 6. Handle any errors that occur during the process and return appropriate error responses. :param request: Django HTTP request object. :type request: ASGIRequest :param args: Additional positional arguments. :type args: tuple :param kwargs: Keyword arguments, must include 'name' (llm_client name) and 'kind' (llm_client type). :type kwargs: dict :returns: Rendered HTML page with llm_client manifest details, or an error response if the llm_client is not found or parameters are invalid. :rtype: HttpResponse """ hashed_id = kwargs.pop("hashed_id", None) llm_client_id = LLMClient.id_from_hashed_id(hashed_id) if hashed_id else None try: self.llm_client = LLMClient.get_cached_object(pk=llm_client_id) if not isinstance(self.llm_client, LLMClient): raise LLMClient.DoesNotExist( f"LLMClient with id {llm_client_id} does not exist. Received {type(self.llm_client)} {self.llm_client}" ) self.llm_client_helper = LLMClientHelper(request=request, llm_client=self.llm_client) # we'll pass the llm_client name as a kwarge to the APICli # along with ownership info which we'll set below. kwargs["name"] = self.llm_client.name # there are many ways that we could do this, but using the system # const is easiest. self.kind = SAMKinds.LLM_CLIENT except LLMClient.DoesNotExist: return SmarterHttpResponseNotFound( request=request, error_message=f"LLMClient with id {llm_client_id} not found" ) logger.debug( "%s.dispatch() - url=%s, account=%s, user=%s, llm_client=%s", self.formatted_class_name, self.url, self.account, self.user_profile.user, # type: ignore self.llm_client, ) # we need to re-orchestrate the parameters that we'll send to # self.get_brokered_json_response(), which marshals the request # and kwargs to the ApiV1CliDescribeApiView view to get # the json manifest for the llm_client. Since the llm_client has ownership # that is not necessarily the same as the authenticated user, we need # to spoof the request user to be the owner of the llm_client for the # purposes of generating the manifest. # # things we know: # - request.user was validated in the base classes. # - self.llm_client.user_profile was validated in LLMClientHelper # - user_profile always has a valid user. request.user = self.llm_client.user_profile.user logger.debug( "%s.dispatch() - rendering template %s with kwargs: %s", self.formatted_class_name, self.template_path, kwargs, ) # to avoid circular imports at app startup. # pylint: disable=import-outside-toplevel from smarter.apps.api.v1.cli.urls import ApiV1CliReverseViews from smarter.apps.api.v1.cli.views.describe import ApiV1CliDescribeApiView # build the relative url path to the API CLI end point. reverse_name = str(ApiV1CliReverseViews.namespace + ApiV1CliReverseViews.describe).lower() view = ApiV1CliDescribeApiView.as_view() json_response = self.get_brokered_json_response( reverse_name=reverse_name, view=view, request=request, *args, **kwargs, ) try: yaml_response = yaml.dump(json_response, default_flow_style=False) except yaml.YAMLError as e: logger.error( "%s.dispatch() - Error converting JSON response to YAML: %s. JSON response: %s", self.formatted_class_name, str(e), formatted_json(json_response), ) return SmarterHttpResponseServerError(request=request, error_message="Error converting manifest to YAML") context = { "manifest": yaml_response, "page_title": self.llm_client.name, "owner": self.llm_client.user_profile, } try: response = render(request, self.template_path, context=context) # type: ignore # pylint: disable=broad-except except Exception as e: logger.error( "%s.dispatch() - Error rendering template: %s. context: %s", self.formatted_class_name, str(e), formatted_json(context), exec_info=True, ) return SmarterHttpResponseServerError(request=request, error_message="Error rendering manifest page") return response