"""
Pydantic models of the LLMClients' REST api responses (``/api/v1/llm-clients/``).
The REST api's views build their responses from these models, and the Smarter api's OpenAPI
document (:mod:`smarter.apps.api.v1.contract.openapi`) publishes their JSON Schemas. Each model
is built from its row by ``from_model()``.
They describe the JSON that :mod:`smarter.apps.llmclient.serializers` produced before the api
was modeled, which clients already read: camelCase keys, an LLMClient's owner nested rather than
its id, and an LLMClient's related rows inline. Two values changed: ``canDelete`` is computed for
the requesting user, where the serializers, called without a request, always said false, and an api
key's ``apiKey`` is its token's public ``key_id``, where it was the token's digest.
The models forbid extra fields: the api returns exactly what they declare. Adding a field is
backward compatible. Removing or renaming one is not.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Literal, Optional
from uuid import UUID
from pydantic import Field, JsonValue
from smarter.apps.account.contract.models import (
AccountApiModel,
AccountApiUserProfile,
MetaDataApiAnnotations,
)
from smarter.apps.account.serializers import user_can_delete
from smarter.apps.plugin.contract.models import PluginApiPluginMeta
if TYPE_CHECKING:
from smarter.apps.llmclient.models import (
LLMClient,
LLMClientAPIKey,
LLMClientCustomDomain,
LLMClientFunctions,
LLMClientPlugin,
)
LLMClientApiFunctionName = Literal["get_current_weather", "date_calculator", "calculator"]
"""A built-in function's name: the choices of ``LLMClientFunctions.name``.
A test fails if the two differ.
"""
LLMClientApiDnsVerificationStatus = Literal["Verifying", "Not Verified", "Verified", "Failed"]
"""The choices of ``LLMClient.dns_verification_status``.
A test fails if the two differ.
"""
LLMClientApiTlsCertificateIssuanceStatus = Literal["No Certificate", "Requested", "Issued", "Failed"]
"""The choices of ``LLMClient.tls_certificate_issuance_status``.
A test fails if the two differ.
"""
LLMClientApiVerificationStatus = Literal["Not Verified", "Verifying", "Verified", "Failed"]
"""The choices of ``LLMClientCustomDomain.verification_status``.
A test fails if the two differ.
"""
[docs]
class LLMClientApiOrmModel(AccountApiModel):
"""Base class of the models of LLMClient rows: their primary key and timestamps."""
id: int = Field(..., description="The row's primary key.")
created_at: Optional[datetime] = Field(None, description="TimestampedModel.created_at.")
updated_at: Optional[datetime] = Field(None, description="TimestampedModel.updated_at.")
[docs]
class LLMClientApiFunction(LLMClientApiOrmModel):
"""A built-in function of an LLMClient: a :class:`smarter.apps.llmclient.models.LLMClientFunctions`."""
name: Optional[LLMClientApiFunctionName] = Field(None, description="LLMClientFunctions.name.")
llmclient: int = Field(..., description="LLMClientFunctions.llmclient: the LLMClient's id.")
[docs]
@classmethod
def from_model(cls, function: "LLMClientFunctions") -> "LLMClientApiFunction":
"""
The contract's model of a built-in function of an LLMClient.
:param function: The row.
:type function: smarter.apps.llmclient.models.LLMClientFunctions
:returns: The model.
:rtype: LLMClientApiFunction
"""
return cls(
id=function.pk,
created_at=function.created_at,
updated_at=function.updated_at,
name=function.name, # type: ignore[arg-type]
llmclient=function.llmclient_id, # type: ignore[attr-defined]
)
[docs]
class LLMClientApiPlugin(LLMClientApiOrmModel):
"""A plugin of an LLMClient: a :class:`smarter.apps.llmclient.models.LLMClientPlugin`, with the plugin's metadata."""
plugin_meta: PluginApiPluginMeta = Field(..., description="LLMClientPlugin.plugin_meta: the plugin.")
llmclient: int = Field(..., description="LLMClientPlugin.llmclient: the LLMClient's id.")
[docs]
@classmethod
def from_model(cls, plugin: "LLMClientPlugin", user: object = None) -> "LLMClientApiPlugin":
"""
The contract's model of a plugin of an LLMClient.
:param plugin: The row, whose ``plugin_meta`` should be fetched with it.
:type plugin: smarter.apps.llmclient.models.LLMClientPlugin
:param user: The requesting user, for the plugin's ``canDelete``.
:type user: object
:returns: The model.
:rtype: LLMClientApiPlugin
"""
return cls(
id=plugin.pk,
created_at=plugin.created_at,
updated_at=plugin.updated_at,
plugin_meta=PluginApiPluginMeta.from_model(plugin.plugin_meta, user),
llmclient=plugin.llmclient_id, # type: ignore[attr-defined]
)
[docs]
class LLMClientApiPluginRef(AccountApiModel):
"""A plugin of an LLMClient, in the LLMClient's own response: the LLMClientPlugin's id, and the plugin's name."""
id: int = Field(..., description="LLMClientPlugin.id.")
name: str = Field(..., description="The plugin's name, PluginMeta.name.")
[docs]
@classmethod
def from_model(cls, plugin: "LLMClientPlugin") -> "LLMClientApiPluginRef":
"""
The contract's reference to a plugin of an LLMClient.
:param plugin: The row, whose ``plugin_meta`` should be fetched with it.
:type plugin: smarter.apps.llmclient.models.LLMClientPlugin
:returns: The model.
:rtype: LLMClientApiPluginRef
"""
return cls(id=plugin.pk, name=plugin.plugin_meta.name)
[docs]
class LLMClientApiKey(LLMClientApiOrmModel):
"""An api key of an LLMClient: a :class:`smarter.apps.llmclient.models.LLMClientAPIKey`."""
llmclient: int = Field(..., description="LLMClientAPIKey.llmclient: the LLMClient's id.")
api_key: UUID = Field(
...,
description="LLMClientAPIKey.api_key: the SmarterAuthToken's public key_id. Never the key, nor its digest.",
)
[docs]
@classmethod
def from_model(cls, api_key: "LLMClientAPIKey") -> "LLMClientApiKey":
"""
The contract's model of an api key of an LLMClient.
:param api_key: The row, whose ``api_key`` should be fetched with it.
:type api_key: smarter.apps.llmclient.models.LLMClientAPIKey
:returns: The model.
:rtype: LLMClientApiKey
"""
return cls(
id=api_key.pk,
created_at=api_key.created_at,
updated_at=api_key.updated_at,
llmclient=api_key.llmclient_id, # type: ignore[attr-defined]
api_key=api_key.api_key.key_id,
)
[docs]
class LLMClientApiCustomDomain(LLMClientApiOrmModel):
"""A custom domain of an LLMClient: a :class:`smarter.apps.llmclient.models.LLMClientCustomDomain`."""
name: str = Field(..., description="MetaDataModel.name, in snake_case.")
description: Optional[str] = Field(None, description="MetaDataModel.description.")
version: Optional[str] = Field(None, description="MetaDataModel.version, e.g. 1.0.0.")
annotations: MetaDataApiAnnotations = Field(None, description="MetaDataModel.annotations.")
user_profile: int = Field(..., description="The custom domain's owner: the UserProfile's id.")
aws_hosted_zone_id: str = Field(..., description="The id of the domain's DNS zone, empty until it has one.")
domain_name: str = Field(..., description="The domain, e.g. example.com.")
verification_status: LLMClientApiVerificationStatus = Field(
..., description="Whether the platform has verified that it controls the domain's DNS."
)
verified_at: Optional[datetime] = Field(None, description="When the domain was verified.")
verification_message: str = Field(..., description="The result of the last verification, for a person.")
[docs]
@classmethod
def from_model(cls, custom_domain: "LLMClientCustomDomain") -> "LLMClientApiCustomDomain":
"""
The contract's model of a custom domain.
:param custom_domain: The row.
:type custom_domain: smarter.apps.llmclient.models.LLMClientCustomDomain
:returns: The model.
:rtype: LLMClientApiCustomDomain
"""
return cls(
id=custom_domain.pk,
created_at=custom_domain.created_at,
updated_at=custom_domain.updated_at,
name=custom_domain.name,
description=custom_domain.description,
version=custom_domain.version,
annotations=custom_domain.annotations,
user_profile=custom_domain.user_profile_id, # type: ignore[attr-defined]
aws_hosted_zone_id=custom_domain.aws_hosted_zone_id,
domain_name=custom_domain.domain_name,
verification_status=custom_domain.verification_status, # type: ignore[arg-type]
verified_at=custom_domain.verified_at,
verification_message=custom_domain.verification_message,
)
[docs]
class LLMClientApiLLMClient(LLMClientApiOrmModel):
"""
An LLMClient: a :class:`smarter.apps.llmclient.models.LLMClient`, with its related rows and its urls.
Its ``functions``, ``plugins``, ``custom_domains`` and ``api_keys`` are inline, and its urls and
hosts are computed, as the LLMClient's properties compute them for the platform that serves the
request.
"""
hashed_id: str = Field(..., description="The LLMClient's hashed id, e.g. rMTAwMDAwNgx, which its urls use.")
name: str = Field(..., description="MetaDataModel.name, in snake_case.")
description: Optional[str] = Field(None, description="MetaDataModel.description.")
version: Optional[str] = Field(None, description="MetaDataModel.version, e.g. 1.0.0.")
tags: list[str] = Field(default_factory=list, description="MetaDataModel.tags, by name.")
annotations: MetaDataApiAnnotations = Field(None, description="MetaDataModel.annotations.")
user_profile: AccountApiUserProfile = Field(..., description="The LLMClient's owner.")
can_delete: bool = Field(
...,
description="Whether the requesting user may delete the LLMClient now: they own it, and nothing depends on it.",
)
functions: list[LLMClientApiFunction] = Field(..., description="The LLMClient's built-in functions.")
plugins: list[LLMClientApiPluginRef] = Field(..., description="The LLMClient's plugins.")
custom_domains: list[LLMClientApiCustomDomain] = Field(
..., description="The LLMClient's custom domain, as a list of none or one."
)
api_keys: list[LLMClientApiKey] = Field(..., description="The LLMClient's api keys.")
rfc1034_compliant_name: Optional[str] = Field(None, description="The name, as a DNS label.")
default_system_role: Optional[str] = Field(
None, description="The system prompt of a new chat session, with the current date and time."
)
base_api_domain: Optional[str] = Field(None, description="The platform's api domain, e.g. api.example.com.")
base_default_host: Optional[str] = Field(None, description="The account's host under the api domain.")
default_host: Optional[str] = Field(None, description="The LLMClient's host, once deployed.")
default_url: Optional[str] = Field(None, description="The LLMClient's url, once deployed.")
custom_host: Optional[str] = Field(None, description="The LLMClient's host on its custom domain, if any.")
custom_url: Optional[str] = Field(None, description="The LLMClient's url on its custom domain, if any.")
sandbox_host: Optional[str] = Field(None, description="The platform's host, which serves undeployed LLMClients.")
sandbox_url: Optional[str] = Field(None, description="The LLMClient's page in the web console's workbench.")
manifest_url: Optional[str] = Field(None, description="The url of the LLMClient's manifest in the web console.")
hostname: Optional[str] = Field(None, description="The host that serves the LLMClient now.")
url: Optional[str] = Field(None, description="The url that serves the LLMClient now.")
url_llmclient: Optional[str] = Field(None, description="Where to send prompts: the chat api's prompt url.")
url_chat_config: Optional[str] = Field(None, description="The chat api's configuration url.")
url_chatapp: Optional[str] = Field(None, description="The LLMClient's chat page.")
ready: bool = Field(..., description="Whether the LLMClient can be prompted.")
is_authentication_required: bool = Field(..., description="Whether prompting the LLMClient requires an api key.")
deployed: Optional[bool] = Field(None, description="LLMClient.deployed: whether it is served on its own host.")
provider: Optional[str] = Field(None, description="LLMClient.provider: the LLM provider, e.g. openai.")
default_model: Optional[str] = Field(None, description="LLMClient.default_model.")
default_temperature: Optional[float] = Field(None, description="LLMClient.default_temperature.")
default_max_tokens: Optional[int] = Field(None, description="LLMClient.default_max_tokens.")
app_name: Optional[str] = Field(None, description="LLMClient.app_name.")
app_assistant: Optional[str] = Field(None, description="LLMClient.app_assistant: the assistant's name.")
app_welcome_message: Optional[str] = Field(None, description="LLMClient.app_welcome_message.")
app_example_prompts: Optional[list[str]] = Field(None, description="LLMClient.app_example_prompts.")
app_placeholder: Optional[str] = Field(None, description="LLMClient.app_placeholder.")
app_info_url: Optional[str] = Field(None, description="LLMClient.app_info_url.")
app_file_attachment: Optional[bool] = Field(None, description="LLMClient.app_file_attachment.")
styles: Optional[dict[str, JsonValue]] = Field(None, description="LLMClient.styles: the chat's CSS styles.")
allowed_origins: list[str] = Field(..., description="LLMClient.allowed_origins: the CORS origins of its pages.")
dns_verification_status: Optional[LLMClientApiDnsVerificationStatus] = Field(
None, description="LLMClient.dns_verification_status."
)
tls_certificate_issuance_status: Optional[LLMClientApiTlsCertificateIssuanceStatus] = Field(
None, description="LLMClient.tls_certificate_issuance_status."
)
subdomain: Optional[int] = Field(None, description="LLMClient.subdomain: the Route53 record's id, if any.")
custom_domain: Optional[int] = Field(None, description="LLMClient.custom_domain: the custom domain's id, if any.")
[docs]
@classmethod
def from_model(cls, llmclient: "LLMClient", user: object = None) -> "LLMClientApiLLMClient":
"""
The contract's model of an LLMClient.
:param llmclient: The row, whose ``user_profile`` should be fetched with it.
:type llmclient: smarter.apps.llmclient.models.LLMClient
:param user: The requesting user, for ``can_delete``. None may delete nothing.
:type user: object
:returns: The model.
:rtype: LLMClientApiLLMClient
"""
# pylint: disable=import-outside-toplevel
from smarter.apps.llmclient.manifest.models.llmclient.const import (
MANIFEST_KIND as LLMCLIENT_KIND,
)
from smarter.apps.llmclient.models import (
LLMClientAPIKey,
LLMClientCustomDomain,
LLMClientFunctions,
LLMClientPlugin,
)
custom_domains = (
LLMClientCustomDomain.objects.filter(id=llmclient.custom_domain_id) # type: ignore[attr-defined]
if llmclient.custom_domain_id # type: ignore[attr-defined]
else LLMClientCustomDomain.objects.none()
)
return cls(
id=llmclient.pk,
hashed_id=llmclient.hashed_id,
created_at=llmclient.created_at,
updated_at=llmclient.updated_at,
name=llmclient.name,
description=llmclient.description,
version=llmclient.version,
tags=[tag.name for tag in llmclient.tags.all()],
annotations=llmclient.annotations,
user_profile=AccountApiUserProfile.from_model(llmclient.user_profile),
can_delete=user_can_delete(llmclient, LLMCLIENT_KIND, user),
functions=[
LLMClientApiFunction.from_model(row) for row in LLMClientFunctions.objects.filter(llmclient=llmclient)
],
plugins=[
LLMClientApiPluginRef.from_model(row)
for row in LLMClientPlugin.objects.filter(llmclient=llmclient).select_related("plugin_meta")
],
custom_domains=[LLMClientApiCustomDomain.from_model(row) for row in custom_domains],
api_keys=[
LLMClientApiKey.from_model(row)
for row in LLMClientAPIKey.objects.filter(llmclient=llmclient).select_related("api_key")
],
rfc1034_compliant_name=llmclient.rfc1034_compliant_name,
default_system_role=llmclient.default_system_role_enhanced,
base_api_domain=llmclient.base_api_domain,
base_default_host=llmclient.base_default_host,
default_host=llmclient.default_host,
default_url=llmclient.default_url,
custom_host=llmclient.custom_host,
custom_url=llmclient.custom_url,
sandbox_host=llmclient.sandbox_host,
sandbox_url=llmclient.sandbox_url,
manifest_url=llmclient.manifest_url,
hostname=llmclient.hostname,
url=llmclient.url,
url_llmclient=llmclient.url_llmclient,
url_chat_config=llmclient.url_chat_config,
url_chatapp=llmclient.url_chatapp,
ready=llmclient.ready,
is_authentication_required=llmclient.is_authentication_required,
deployed=llmclient.deployed,
provider=llmclient.provider,
default_model=llmclient.default_model,
default_temperature=llmclient.default_temperature,
default_max_tokens=llmclient.default_max_tokens,
app_name=llmclient.app_name,
app_assistant=llmclient.app_assistant,
app_welcome_message=llmclient.app_welcome_message,
app_example_prompts=llmclient.app_example_prompts,
app_placeholder=llmclient.app_placeholder,
app_info_url=llmclient.app_info_url,
app_file_attachment=llmclient.app_file_attachment,
styles=llmclient.styles,
allowed_origins=llmclient.allowed_origins,
dns_verification_status=llmclient.dns_verification_status, # type: ignore[arg-type]
tls_certificate_issuance_status=llmclient.tls_certificate_issuance_status, # type: ignore[arg-type]
subdomain=llmclient.subdomain_id, # type: ignore[attr-defined]
custom_domain=llmclient.custom_domain_id, # type: ignore[attr-defined]
)
LLMCLIENT_API_MODELS = (
LLMClientApiLLMClient,
LLMClientApiPlugin,
LLMClientApiFunction,
LLMClientApiKey,
LLMClientApiCustomDomain,
)
"""The models whose JSON Schemas the Smarter api's OpenAPI document includes.
Their dependencies are included with them.
"""
__all__ = [
"LLMCLIENT_API_MODELS",
"LLMClientApiCustomDomain",
"LLMClientApiDnsVerificationStatus",
"LLMClientApiFunction",
"LLMClientApiFunctionName",
"LLMClientApiKey",
"LLMClientApiLLMClient",
"LLMClientApiOrmModel",
"LLMClientApiPlugin",
"LLMClientApiPluginRef",
"LLMClientApiTlsCertificateIssuanceStatus",
"LLMClientApiVerificationStatus",
]