Source code for smarter.lib.manifest.record_locator

"""
Record locators, and the web console manifest pages that they lead to.

A record locator identifies one row of a Smarter resource, e.g. ``llmclient-rMTAwMDAxMwx``: the
lower-cased name of its Django model class, a hyphen, and the row's hashed id. A manifest returns it
as ``status.recordLocator``. See
:attr:`smarter.lib.django.models.timestamped_model.TimestampedModel.record_locator`.

Each kind of resource has a page in the web console that shows its manifest, in the manifest editor,
where it can be changed. Most are addressed by the same hashed id, so that a record locator is enough
to build the url of the resource's manifest page, without a database query:

.. code-block:: python

    from smarter.lib.manifest.record_locator import console_url

    console_url("llmclient-rMTAwMDAxMwx")
    # '/workbench/llm-clients/rMTAwMDAxMwx/manifest/'

The exceptions are an api key (:class:`smarter.lib.drf.models.SmarterAuthToken`), whose record
locator ends with its ``key_id``, and an Extension, whose page is addressed by its slug, which is
read from the database.
"""

from typing import Callable, Optional

from django.conf import settings
from django.urls import NoReverseMatch

from smarter.common.helpers.console_helpers import SmarterFormattedText
from smarter.lib import logging
from smarter.lib.django.shortcuts import reverse

logger = logging.getLogger(__name__)
logger_prefix = SmarterFormattedText.text(__name__)

ConsolePath = Callable[[str], Optional[str]]
"""Builds the path of a resource's web console page from the part of its record locator after the prefix."""


[docs] def record_locator_prefix(model_class: type) -> str: """ The prefix of a model's record locators: the lower-cased name of its class, e.g. ``llmclient``. :param model_class: The Django model class, e.g. :class:`smarter.apps.llmclient.models.LLMClient`. :type model_class: type :returns: The prefix, without the hyphen. :rtype: str """ return model_class.__name__.lower()
[docs] def parse_record_locator(record_locator: str) -> tuple[str, str]: """ Split a record locator into its prefix and the rest, usually a hashed id. .. code-block:: python parse_record_locator("llmclient-rMTAwMDAxMwx") # ('llmclient', 'rMTAwMDAxMwx') The prefix is everything before the first hyphen, because a model's class name has none, and the rest may have some, e.g. the ``key_id`` of an api key. :param record_locator: The record locator. :type record_locator: str :returns: The prefix and the rest. :rtype: tuple[str, str] :raises ValueError: If the record locator has no prefix, or nothing after it. """ prefix, _, rest = (record_locator or "").partition("-") if not prefix or not rest: raise ValueError(f"Not a record locator: {record_locator!r}") return prefix, rest
[docs] def by_hashed_id(namespace: str, name: str) -> ConsolePath: """ A :data:`ConsolePath` for a page whose url has the resource's ``hashed_id``. :param namespace: The url namespace, e.g. ``GuardrailReverseNames.namespace``. :type namespace: str :param name: The url name, e.g. ``GuardrailReverseNames.detailview``. :type name: str :returns: The function. :rtype: ConsolePath """ def path(hashed_id: str) -> Optional[str]: return reverse(f"{namespace}:{name}", kwargs={"hashed_id": hashed_id}, urlconf=settings.ROOT_URLCONF) return path
[docs] def console_paths() -> dict[str, ConsolePath]: """ The web console manifest page of each kind of resource that has one, by record locator prefix. An LLMClient's is its manifest page, not the Prompt Engineer's Workbench, so that every kind's page is one where its manifest can be edited. An Extension, which has no manifest, has its own page. The views are imported here, not at module level, because the url modules import the apps' views, which import the manifest brokers, which import this module. :returns: A :data:`ConsolePath` for each record locator prefix. :rtype: dict[str, ConsolePath] """ # pylint: disable=import-outside-toplevel from smarter.apps.account.models import Budget from smarter.apps.account.views.budget.urls import BudgetReverseNames from smarter.apps.connection.models import ApiConnection, SqlConnection from smarter.apps.connection.urls import ConnectionReverseNames from smarter.apps.guardrail.models import Guardrail from smarter.apps.guardrail.urls import GuardrailReverseNames from smarter.apps.llmclient.models import LLMClient, LLMClientCustomDomain from smarter.apps.llmclient.urls import LLMClientReverseNames from smarter.apps.llmhost.models import LLMHost, LLMHostCompute from smarter.apps.llmhost.urls import LLMHostReverseNames from smarter.apps.llmtool.extension.views import detail_url as extension_detail_url from smarter.apps.llmtool.models import Extension, PluginMeta from smarter.apps.llmtool.urls import PluginReverseNames from smarter.apps.mcpclient.models import MCPClient from smarter.apps.mcpclient.urls import MCPClientReverseNames from smarter.apps.orchestrator.models import Orchestrator from smarter.apps.orchestrator.urls import OrchestratorReverseNames from smarter.apps.prompt.urls import PromptReverseNames from smarter.apps.provider.models import Provider from smarter.apps.provider.urls import ProviderReverseNames from smarter.apps.proxy.models import Proxy from smarter.apps.proxy.urls import ProxyReverseNames from smarter.apps.secret.models import Secret from smarter.apps.secret.urls import SecretReverseNames from smarter.apps.vectorsearch.models import Vectorsearch from smarter.apps.vectorsearch.urls import VectorsearchReverseNames from smarter.apps.vectorstore.models import VectorstoreMeta from smarter.apps.vectorstore.urls import VectorstoreReverseNames from smarter.lib.drf.models import SmarterAuthToken from smarter.lib.drf.urls import AuthTokenReverseNames def auth_token_path(key_id: str) -> Optional[str]: return reverse( f"{AuthTokenReverseNames.namespace}:{AuthTokenReverseNames.detailview}", kwargs={"authtoken_id": key_id}, urlconf=settings.ROOT_URLCONF, ) def extension_path(hashed_id: str) -> Optional[str]: extension_id = Extension.id_from_hashed_id(hashed_id) slug = Extension.objects.filter(pk=extension_id).values_list("slug", flat=True).first() return extension_detail_url(slug) if slug else None pages: dict[type, ConsolePath] = { ApiConnection: by_hashed_id(ConnectionReverseNames.namespace, ConnectionReverseNames.api_detailview), Budget: by_hashed_id(BudgetReverseNames.namespace, BudgetReverseNames.detailview), Extension: extension_path, Guardrail: by_hashed_id(GuardrailReverseNames.namespace, GuardrailReverseNames.detailview), LLMClient: by_hashed_id(PromptReverseNames.namespace, PromptReverseNames.manifest_by_hashed_id), LLMClientCustomDomain: by_hashed_id( LLMClientReverseNames.namespace, LLMClientReverseNames.custom_domain_detailview ), LLMHost: by_hashed_id(LLMHostReverseNames.namespace, LLMHostReverseNames.detailview), LLMHostCompute: by_hashed_id(LLMHostReverseNames.namespace, LLMHostReverseNames.compute_detailview), MCPClient: by_hashed_id(MCPClientReverseNames.namespace, MCPClientReverseNames.detailview), Orchestrator: by_hashed_id(OrchestratorReverseNames.namespace, OrchestratorReverseNames.detailview), PluginMeta: by_hashed_id(PluginReverseNames.namespace, PluginReverseNames.detailview), Provider: by_hashed_id(ProviderReverseNames.namespace, ProviderReverseNames.detailview), Proxy: by_hashed_id(ProxyReverseNames.namespace, ProxyReverseNames.detailview), Secret: by_hashed_id(SecretReverseNames.namespace, SecretReverseNames.detailview), SmarterAuthToken: auth_token_path, SqlConnection: by_hashed_id(ConnectionReverseNames.namespace, ConnectionReverseNames.sql_detailview), Vectorsearch: by_hashed_id(VectorsearchReverseNames.namespace, VectorsearchReverseNames.detailview), VectorstoreMeta: by_hashed_id(VectorstoreReverseNames.namespace, VectorstoreReverseNames.detailview), } return {record_locator_prefix(model_class): path for model_class, path in pages.items()}
[docs] def console_url(record_locator: Optional[str]) -> Optional[str]: """ The path of a resource's manifest page in the web console, from its record locator. It is a path, not an absolute url, because the web console's own pages use it. A kind of resource that has no page, e.g. a Prompt, or a page that this platform does not serve, e.g. the Proxy pages while proxies are turned off, has none. .. code-block:: python console_url("guardrail-rNQx") # '/guardrail/guardrails/rNQx/' :param record_locator: The resource's record locator, e.g. ``llmclient-rMTAwMDAxMwx``. :type record_locator: Optional[str] :returns: The path, or None. :rtype: Optional[str] """ if not record_locator: return None try: prefix, rest = parse_record_locator(record_locator) except ValueError as e: logger.warning("%s.console_url() %s", logger_prefix, e) return None path = console_paths().get(prefix) if path is None: return None try: return path(rest) except NoReverseMatch as e: logger.debug("%s.console_url() no web console page for %s: %s", logger_prefix, record_locator, e) return None
__all__ = [ "ConsolePath", "by_hashed_id", "console_paths", "console_url", "parse_record_locator", "record_locator_prefix", ]