"""
The OpenAPI 3.1 document of the Smarter api, generated from Pydantic models.
The document covers the apis that the ``smarter`` cli and the SDKs call, family by family:
- the cli api (``/api/v1/cli/``), to apply, get, describe, validate and delete the resources that
SAM manifests declare,
- the chat api (``/api/v1/llm-clients/{id}/config/`` and ``.../prompt/``), to chat with an
LLMClient, as JSON or as a stream of Server-Sent Events,
- the REST reads of the resource families: the LLMClients', Guardrails', Secrets', MCPClients',
vectorstores', LLMHosts', plugins', Providers' and Proxies', whose responses are built from each
app's contract models, e.g.
:mod:`smarter.apps.llmclient.contract.models`. Only the reads are published: clients write
through the cli api's manifests, which the brokers validate.
It is built from:
- the SAM models of every kind (:func:`smarter.apps.api.v1.contract.sam.sam_models`),
- the Smarter Journal api contract (:mod:`smarter.lib.journal.contract.models`), the envelope and
the errors of every response,
- the Smarter Chat api contract (:mod:`smarter.apps.prompt.contract.models`), the requests,
responses and progress events of the chat api, and the chat configuration and completion of
the cli's prompt commands,
- the models of the other commands' data (:mod:`smarter.apps.api.v1.contract.models`),
so a change to any of them changes the document. ``manage.py export_api_contract`` writes it to
``openapi.json``, beside this module, and a test fails if the committed file is out of date, or if a
route of a family in :data:`DOCUMENTED_URLCONFS` is neither in the document nor in
:data:`UNDOCUMENTED_ROUTES`.
The live Swagger and Redoc pages (``/docs/swagger/``) are generated by drf-yasg from the views, and
describe every api of the platform in OpenAPI 2. This document describes fewer apis, but types their
requests and responses, so that clients can be generated from it.
"""
import json
from typing import Optional, get_args
from pydantic import BaseModel
from pydantic.json_schema import models_json_schema
from smarter.apps.account.contract.models import (
ACCOUNT_API_MODELS,
AccountApiAccountDetail,
AccountApiContact,
AccountApiProfile,
AccountApiUserDetail,
)
from smarter.apps.guardrail.contract.models import (
GUARDRAIL_API_MODELS,
GuardrailApiEvaluateRequest,
GuardrailApiEvaluation,
GuardrailApiEventDisposition,
GuardrailApiEvents,
GuardrailApiGuardrail,
)
from smarter.apps.llmclient.contract.models import (
LLMCLIENT_API_MODELS,
LLMClientApiCustomDomain,
LLMClientApiFunction,
LLMClientApiKey,
LLMClientApiLLMClient,
LLMClientApiPlugin,
)
from smarter.apps.llmhost.contract.models import (
LLMHOST_API_MODELS,
LLMHostApiDiscovery,
LLMHostApiDraftManifest,
LLMHostApiLLMHost,
LLMHostApiLogs,
LLMHostApiPlan,
LLMHostApiReport,
LLMHostApiStatus,
LLMHostApiUndeployed,
)
from smarter.apps.mcpclient.contract.models import (
MCPCLIENT_API_MODELS,
MCPClientApiMCPClient,
MCPClientApiStatus,
MCPClientApiStatusError,
MCPClientApiToolResult,
MCPClientApiTools,
)
from smarter.apps.orchestrator.contract.models import (
ORCHESTRATOR_API_MODELS,
OrchestratorApiOrchestrator,
)
from smarter.apps.plugin.contract.models import PluginApiPluginMeta
from smarter.apps.prompt.contract.const import CHAT_CONTRACT_V2_CAPABILITY
from smarter.apps.prompt.contract.history import (
PROMPT_HISTORY_API_MODELS,
PromptHistoryApiPluginUsage,
PromptHistoryApiSession,
PromptHistoryApiToolCall,
)
from smarter.apps.prompt.contract.models import (
PromptChatApiConfigRequest,
PromptChatApiConfigResponse,
PromptChatApiErrorResponse,
PromptChatApiProgressEvent,
PromptChatApiProgressResult,
PromptChatApiPromptRequest,
PromptChatApiPromptResponse,
)
from smarter.apps.provider.contract.models import (
PROVIDER_API_MODELS,
ProviderApiModel,
ProviderApiProvider,
)
from smarter.apps.proxy.contract.models import PROXY_API_MODELS, ProxyApiProxies
from smarter.apps.secret.contract.models import SECRET_API_MODELS, SecretApiSecret
from smarter.apps.vectorsearch.contract.models import (
VECTORSEARCH_API_MODELS,
VectorsearchApiVectorsearch,
)
from smarter.apps.vectorstore.contract.models import (
VECTORSTORE_API_MODELS,
VectorstoreApiAddText,
VectorstoreApiDocumentDeleted,
VectorstoreApiDocuments,
VectorstoreApiDocumentsAdded,
VectorstoreApiRestored,
VectorstoreApiSearch,
VectorstoreApiSearchResults,
VectorstoreApiSnapshot,
VectorstoreApiSnapshots,
VectorstoreApiStatus,
VectorstoreApiVectorstore,
)
from smarter.common.const import (
SMARTER_PROJECT_DOCS_URL,
SMARTER_PROJECT_WEBSITE_URL,
)
from smarter.lib.journal.api.v1.views import SORT_FIELDS as JOURNAL_SORT_FIELDS
from smarter.lib.journal.contract.const import JOURNAL_KEY_PATTERN
from smarter.lib.journal.contract.models import (
JOURNAL_API_MODELS,
SAMJournalApiEntry,
SAMJournalApiEnvelope,
SAMJournalApiErrorResponse,
SAMJournalApiPage,
)
from .const import (
API_VERSION,
DEFAULT_PLATFORM_URL,
JSON_SCHEMA_DIALECT,
LOCAL_PLATFORM_URL,
OPENAPI_PATH,
OPENAPI_VERSION,
)
from .models import (
CLI_API_MODELS,
CliApiApplied,
CliApiGet,
CliApiPrompt,
CliApiPromptBody,
CliApiResource,
CliApiStatus,
CliApiValidation,
CliApiVersion,
CliApiWhoami,
OpenApiComponents,
OpenApiContact,
OpenApiDocument,
OpenApiInfo,
OpenApiLicense,
OpenApiMediaType,
OpenApiOperation,
OpenApiParameter,
OpenApiPathItem,
OpenApiRequestBody,
OpenApiResponse,
OpenApiSchema,
OpenApiSecurityScheme,
OpenApiServer,
OpenApiServerVariable,
OpenApiTag,
RestApiErrorResponse,
)
from .sam import sam_models
REF_TEMPLATE = "#/components/schemas/{model}"
"""Where the document's schemas refer to each other's definitions."""
API_KEY_AUTH = "ApiKeyAuth"
"""The name of the document's security scheme."""
PUBLIC: list[dict[str, list[str]]] = []
"""The ``security`` of an operation that needs no api key."""
TAG_MANIFESTS = "manifests"
TAG_DEPLOYMENT = "deployment"
TAG_PLATFORM = "platform"
TAG_CHAT = "chat"
TAG_LLMCLIENTS = "llmclients"
TAG_GUARDRAILS = "guardrails"
TAG_SECRETS = "secrets"
TAG_MCPCLIENTS = "mcpclients"
TAG_VECTORSTORES = "vectorstores"
TAG_LLMHOSTS = "llmhosts"
TAG_PLUGINS = "plugins"
TAG_PROVIDERS = "providers"
TAG_PROXIES = "proxies"
TAG_ACCOUNTS = "accounts"
TAG_HISTORY = "history"
TAG_JOURNAL = "journal"
TAG_ORCHESTRATORS = "orchestrators"
TAG_VECTORSEARCHES = "vectorsearches"
SUPERUSER_ONLY = (
"Requires an api key of a superuser: the REST api's views refuse other users with a 403. Other users read "
"their resources with the cli api's get and describe."
)
"""The note of the REST reads whose views are SmarterAdminAPIView and SmarterAdminListAPIView."""
READ_PERMISSION = (
"Requires an api key. The response includes only what the user may read: their account's resources, and "
"the platform's built-in ones."
)
"""The note of the REST reads whose views are SmarterAuthenticatedAPIView and SmarterAuthenticatedListAPIView."""
CHAT_MODELS: tuple[type[BaseModel], ...] = (
PromptChatApiConfigRequest,
PromptChatApiConfigResponse,
PromptChatApiPromptRequest,
PromptChatApiPromptResponse,
PromptChatApiErrorResponse,
PromptChatApiProgressEvent,
PromptChatApiProgressResult,
)
"""The Smarter Chat api contract's models that the chat api's operations use."""
DOCUMENTED_URLCONFS: dict[str, str] = {
"/cli/": "smarter.apps.api.v1.cli.urls",
"/llm-clients/": "smarter.apps.llmclient.api.v1.urls",
"/guardrails/": "smarter.apps.guardrail.api.v1.urls",
"/secrets/": "smarter.apps.secret.api.v1.urls",
"/mcpclients/": "smarter.apps.mcpclient.api.v1.urls",
"/vectorstores/": "smarter.apps.vectorstore.api.v1.urls",
"/llmhosts/": "smarter.apps.llmhost.api.v1.urls",
"/plugins/": "smarter.apps.plugin.api.v1.urls",
"/providers/": "smarter.apps.provider.api.v1.urls",
"/proxy/": "smarter.apps.proxy.api.v1.urls",
"/accounts/": "smarter.apps.account.api.v1.urls",
"/prompts/": "smarter.apps.prompt.api.v1.urls",
"/journal/": "smarter.lib.journal.api.v1.urls",
"/orchestrators/": "smarter.apps.orchestrator.api.v1.urls",
"/vectorsearches/": "smarter.apps.vectorsearch.api.v1.urls",
"/connections/": "smarter.apps.connection.api.v1.urls",
}
"""
The families of routes that the document covers: each one's path, relative to ``/api/v1``, and its urlconf.
Every route of these urlconfs must be in the document, or in :data:`UNDOCUMENTED_ROUTES`.
"""
UNDOCUMENTED_URLCONFS: dict[str, str] = {
"/tests/": "Views that only the platform's own unit tests call.",
}
"""
The families of routes under ``/api/v1`` that the document leaves out, and why.
Every family that ``smarter.apps.api.v1.urls`` mounts must be in :data:`DOCUMENTED_URLCONFS` or here.
"""
PATH_PARAMETER_ALIASES: dict[str, str] = {
"llmclient_id": "id",
"guardrail_id": "id",
"secret_id": "id",
"mcpclient_id": "id",
"vectorstore_id": "id",
"llmhost_id": "id",
"orchestrator_id": "id",
"vectorsearch_id": "id",
"hashed_id": "id",
"pk": "id",
}
"""
The document's names of the routes' path parameters, where they differ.
A resource's routes take either its numeric id or its hashed id, which the document describes as one ``{id}``.
"""
NOT_YET_MODELED = "Not modeled yet. The REST reads of each resource family are the next part of the contract."
WEB_CONSOLE = "A web console action, which clients replace with the cli api's manifests."
UNDOCUMENTED_ROUTES: dict[str, str] = {
"/plugins/{plugin_id}/clone/{new_name}": WEB_CONSOLE,
"/plugins/add-example-plugins/{user_id}/": WEB_CONSOLE,
"/plugins/upload/": WEB_CONSOLE,
"/proxy/{name}/": "A Proxy's passthrough, whose requests and responses are its provider's api, not Smarter's.",
"/proxy/{name}/{path}": "A Proxy's passthrough, whose requests and responses are its provider's api, not Smarter's.",
"/llm-clients/{id}/prompt/config/": "A legacy alias of /llm-clients/{id}/config/, for pages that installed an "
"older Smarter Chat.",
"/accounts/batch-create-users/": "Creates users from a CSV file in the web console. Clients apply User "
"manifests with the cli api.",
"/prompts/smarter/{provider_name}/": "The chat api of an LLM provider through Smarter, whose requests and "
"responses are the provider's, not Smarter's.",
"/prompts/passthrough/{provider_name}/": "A passthrough to an LLM provider's api, whose requests and responses "
"are the provider's, not Smarter's.",
"/orchestrators/{id}/orchestrator/": "Running an Orchestrator is not implemented yet: the route has no POST "
"handler, and answers 405.",
"/vectorsearches/{id}/vectorsearch/": "Running a Vectorsearch is not implemented yet: the route has no POST "
"handler, and answers 405.",
"/llm-clients/{id}/functions/{function_id}/plugins": "It returns every plugin of the LLMClient, whatever the "
"function, because it is routed to LLMClientPluginListView. Not published until it is fixed or removed.",
}
"""The paths of the routes of :data:`DOCUMENTED_URLCONFS` that the document leaves out, and why."""
def ref(name: str) -> OpenApiSchema:
"""
A reference to one of the document's schemas.
:param name: The schema's name in ``components.schemas``.
:type name: str
:returns: e.g. ``{"$ref": "#/components/schemas/SAMGuardrail"}``.
:rtype: OpenApiSchema
"""
return {"$ref": REF_TEMPLATE.format(model=name)}
def json_response(description: str, schema_name: str) -> OpenApiResponse:
"""
A JSON response whose body is one of the document's schemas.
:param description: The response's description.
:type description: str
:param schema_name: The body's schema, in ``components.schemas``.
:type schema_name: str
:returns: The response.
:rtype: OpenApiResponse
"""
return OpenApiResponse(
description=description, content={"application/json": OpenApiMediaType(schema_=ref(schema_name))}
)
def envelope(data: Optional[OpenApiSchema], description: str) -> OpenApiSchema:
"""
The schema of a journaled response: the journal's envelope, beside the command's ``data`` and ``message``.
:param data: The schema of the response's ``data``, or None for a response that has only a message.
:type data: Optional[OpenApiSchema]
:param description: The schema's description.
:type description: str
:returns: The schema.
:rtype: OpenApiSchema
"""
properties: dict[str, OpenApiSchema] = {
"message": {"type": "string", "description": "A human-readable summary of the result."}
}
required: list[OpenApiSchema] = []
if data is not None:
properties["data"] = data
required.append("data")
else:
required.append("message")
return {
"description": description,
"allOf": [
ref(SAMJournalApiEnvelope.__name__),
{"type": "object", "properties": properties, "required": required},
],
}
def model_schemas() -> tuple[dict[str, OpenApiSchema], dict[str, str]]:
"""
The JSON Schemas of the SAM models, the journal contract and the cli's data, as the document's components.
:returns: The schemas, by name, and the name of each SAM kind's schema, by kind. A kind's schema is
named after its model, e.g. SAMGuardrail, unless two models share a name.
:rtype: tuple[dict[str, OpenApiSchema], dict[str, str]]
"""
kinds = sam_models()
models: list[type[BaseModel]] = [
*kinds.values(),
SAMJournalApiEnvelope,
SAMJournalApiErrorResponse,
*CHAT_MODELS,
*LLMCLIENT_API_MODELS,
*GUARDRAIL_API_MODELS,
*SECRET_API_MODELS,
*MCPCLIENT_API_MODELS,
*VECTORSTORE_API_MODELS,
*LLMHOST_API_MODELS,
*PROVIDER_API_MODELS,
*PROXY_API_MODELS,
*ACCOUNT_API_MODELS,
*PROMPT_HISTORY_API_MODELS,
*JOURNAL_API_MODELS,
*ORCHESTRATOR_API_MODELS,
*VECTORSEARCH_API_MODELS,
*CLI_API_MODELS,
]
refs, schema = models_json_schema([(model, "validation") for model in models], ref_template=REF_TEMPLATE)
prefix = REF_TEMPLATE.format(model="")
kind_schemas = {kind: str(refs[(model, "validation")]["$ref"])[len(prefix) :] for kind, model in kinds.items()}
return schema.get("$defs", {}), kind_schemas
[docs]
def cli_schemas() -> tuple[dict[str, OpenApiSchema], list[str]]:
"""
Every schema of the document: the models' schemas, a manifest of any kind, and the responses of the commands.
:returns: The schemas, by name, and the SAM kinds.
:rtype: tuple[dict[str, OpenApiSchema], list[str]]
"""
schemas, kind_schemas = model_schemas()
kinds = list(kind_schemas)
# The SAM models declare kind as a string, which every kind's schema would accept, so that a
# manifest would match each schema of SAMManifest's oneOf. Each kind's broker accepts only its
# own kind, so its schema's kind is a const, which a generated client also types as a literal.
for kind, name in kind_schemas.items():
schemas[name]["properties"]["kind"]["const"] = kind # type: ignore[index,call-overload]
schemas["SAMManifest"] = {
"description": "A SAM manifest of any kind. Its kind selects its schema.",
"oneOf": [ref(name) for name in kind_schemas.values()],
"discriminator": {
"propertyName": "kind",
"mapping": {kind: REF_TEMPLATE.format(model=name) for kind, name in kind_schemas.items()},
},
}
schemas["SAMKind"] = {"type": "string", "enum": kinds, "description": "A SAM manifest's kind."}
schemas["JsonSchema"] = {
"type": "object",
"description": f"A JSON Schema ({JSON_SCHEMA_DIALECT}).",
"additionalProperties": True,
}
schemas["CliApiApplyResponse"] = envelope(ref(CliApiApplied.__name__), "The response of apply.")
schemas["CliApiValidateResponse"] = envelope(ref(CliApiValidation.__name__), "The response of validate.")
schemas["CliApiGetResponse"] = envelope(ref(CliApiGet.__name__), "The response of get.")
schemas["CliApiManifestResponse"] = envelope(
ref("SAMManifest"), "A response whose data is a manifest: describe's and example-manifest's."
)
schemas["CliApiJsonSchemaResponse"] = envelope(ref("JsonSchema"), "The response of json-schema.")
schemas["CliApiMessageResponse"] = envelope(None, "A response that has only a message, e.g. delete's.")
schemas["CliApiStatusResponse"] = envelope(ref(CliApiStatus.__name__), "The response of status.")
schemas["CliApiVersionResponse"] = envelope(ref(CliApiVersion.__name__), "The response of version.")
schemas["CliApiWhoamiResponse"] = envelope(ref(CliApiWhoami.__name__), "The response of whoami.")
schemas["CliApiPromptResponse"] = envelope(ref(CliApiPrompt.__name__), "The response of prompt.")
schemas["CliApiResourcesResponse"] = envelope(
{"type": "array", "items": ref(CliApiResource.__name__)}, "The response of resources."
)
schemas["CliApiErrorResponse"] = {
"description": "A journaled error response.",
"allOf": [ref(SAMJournalApiEnvelope.__name__), ref(SAMJournalApiErrorResponse.__name__)],
}
schemas["MCPClientApiErrorResponse"] = {
"description": "An error: a failed connection to the MCP server, with the MCPClient's connection status, "
"or another error, e.g. a 404 for an MCPClient that the user may not read.",
"anyOf": [ref(MCPClientApiStatusError.__name__), ref(RestApiErrorResponse.__name__)],
}
schemas["LLMClientPromptErrorResponse"] = {
"description": "A failed prompt: the prompt's response with its error, whose http status is the LLM "
"provider's, or, for an error before the prompt runs, e.g. an LLMClient that does not exist, the "
"chat api's error response.",
"anyOf": [ref(PromptChatApiPromptResponse.__name__), ref(PromptChatApiErrorResponse.__name__)],
}
return schemas, kinds
KIND_PARAMETER = OpenApiParameter(
name="kind",
in_="path",
required=True,
description="The resource's kind, as it is written in a manifest, e.g. Guardrail. Case-insensitive.",
schema_=ref("SAMKind"),
)
NAME_PARAMETER = OpenApiParameter(
name="name",
in_="query",
required=True,
description="The resource's name, its manifest's metadata.name.",
schema_={"type": "string"},
)
NAME_FILTER_PARAMETER = OpenApiParameter(
name="name",
in_="query",
required=False,
description="Only the resource with this name. Other query parameters are passed to the kind's broker as criteria.",
schema_={"type": "string"},
)
LLMCLIENT_PARAMETER = OpenApiParameter(
name="name",
in_="path",
required=True,
description="The LLMClient's name.",
schema_={"type": "string"},
)
UID_PARAMETER = OpenApiParameter(
name="uid",
in_="query",
required=True,
description="A unique identifier of the client, e.g. a machine id. The platform caches the client's chat "
"session under it, so that a later prompt continues the same chat.",
schema_={"type": "string"},
)
NEW_SESSION_PARAMETER = OpenApiParameter(
name="new_session",
in_="query",
required=False,
description="true starts a new chat session, rather than continuing the client's last one.",
schema_={"type": "string", "enum": ["true", "false"], "default": "false"},
)
PROMPT_BODY = OpenApiRequestBody(
description="The user's message.",
required=True,
content={"application/json": OpenApiMediaType(schema_=ref(CliApiPromptBody.__name__))},
)
LLMCLIENT_ID_PARAMETER = OpenApiParameter(
name="id",
in_="path",
required=True,
description="The LLMClient's numeric id, e.g. 12, or its hashed id, e.g. rMTAwMDAxMgx.",
schema_={"type": "string"},
)
SESSION_KEY_PARAMETER = OpenApiParameter(
name="session_key",
in_="query",
required=False,
description="The chat session's key. The platform looks for it in this parameter, then in the request "
"body, a header and a cookie, in that order.",
schema_={"type": "string", "pattern": "^[a-f0-9]{64}$"},
)
CAPABILITIES_PARAMETER = OpenApiParameter(
name="X-Smarter-Capabilities",
in_="header",
required=False,
description=f"A comma-separated list of the client's capabilities. {CHAT_CONTRACT_V2_CAPABILITY} asks for "
"version 2 of the Smarter Chat api contract, which this document describes, and which is also the "
"default. chat-contract-v1 asks for the deprecated version 1.",
schema_={"type": "string", "default": CHAT_CONTRACT_V2_CAPABILITY},
)
ACCEPT_PARAMETER = OpenApiParameter(
name="Accept",
in_="header",
required=False,
description="text/event-stream streams the prompt's progress as Server-Sent Events. Otherwise, the "
"response is the prompt's JSON.",
schema_={"type": "string", "default": "application/json"},
)
MANIFEST_BODY = OpenApiRequestBody(
description="A SAM manifest, as YAML or as JSON.",
required=True,
content={
"application/x-yaml": OpenApiMediaType(schema_={"type": "string", "description": "A SAM manifest, as YAML."}),
"application/json": OpenApiMediaType(schema_=ref("SAMManifest")),
},
)
def operation(
operation_id: str,
summary: str,
description: str,
tag: str,
response: str,
*,
parameters: Optional[list[OpenApiParameter]] = None,
request_body: Optional[OpenApiRequestBody] = None,
security: Optional[list[dict[str, list[str]]]] = None,
) -> OpenApiOperation:
"""
A cli command: an operation whose successful response is ``200``, and whose other responses are journaled errors.
:param operation_id: The operation's id, e.g. apply.
:type operation_id: str
:param summary: A one-line summary.
:type summary: str
:param description: A description.
:type description: str
:param tag: The operation's tag.
:type tag: str
:param response: The successful response's schema, in ``components.schemas``.
:type response: str
:param parameters: The operation's parameters.
:type parameters: Optional[list[OpenApiParameter]]
:param request_body: The operation's request body.
:type request_body: Optional[OpenApiRequestBody]
:param security: The operation's security, e.g. :data:`PUBLIC`, or None for the document's.
:type security: Optional[list[dict[str, list[str]]]]
:returns: The operation.
:rtype: OpenApiOperation
"""
return OpenApiOperation(
operationId=operation_id,
summary=summary,
description=description,
tags=[tag],
parameters=parameters or [],
requestBody=request_body,
responses={
"200": json_response("Success.", response),
"default": json_response(
"An error, e.g. 400 for an invalid manifest, 401 or 403 without a valid api key, "
"404 for a resource that does not exist.",
"CliApiErrorResponse",
),
},
security=security,
)
[docs]
def cli_paths() -> dict[str, OpenApiPathItem]:
"""
The cli api's paths, relative to the server's ``/api/v1``, by the url names of their routes.
:returns: The paths, e.g. ``{"/cli/apply/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
by_name = [KIND_PARAMETER, NAME_PARAMETER]
commands = {
"/cli/apply/": operation(
"apply",
"Apply a manifest",
"Create the resource that a manifest declares, or update it to match the manifest.",
TAG_MANIFESTS,
"CliApiApplyResponse",
request_body=MANIFEST_BODY,
),
"/cli/validate/": operation(
"validate",
"Validate a manifest",
"Validate a manifest against its kind's model, without saving it. An invalid manifest is a "
"successful response, whose data lists the manifest's errors.",
TAG_MANIFESTS,
"CliApiValidateResponse",
request_body=MANIFEST_BODY,
),
"/cli/get/{kind}/": operation(
"get",
"List resources",
"List the resources of a kind that the api key's user may see.",
TAG_MANIFESTS,
"CliApiGetResponse",
parameters=[KIND_PARAMETER, NAME_FILTER_PARAMETER],
),
"/cli/describe/{kind}/": operation(
"describe",
"Describe a resource",
"A resource's manifest, with its read-only status.",
TAG_MANIFESTS,
"CliApiManifestResponse",
parameters=by_name,
),
"/cli/delete/{kind}/": operation(
"delete",
"Delete a resource",
"Delete a resource.",
TAG_MANIFESTS,
"CliApiMessageResponse",
parameters=by_name,
),
"/cli/example-manifest/{kind}/": operation(
"example_manifest",
"An example manifest",
"An example manifest of a kind.",
TAG_MANIFESTS,
"CliApiManifestResponse",
parameters=[KIND_PARAMETER],
security=PUBLIC,
),
"/cli/json-schema/{kind}/": operation(
"json_schema",
"A kind's JSON Schema",
"The JSON Schema of a kind's manifests. The same schemas are published in the repository, as "
"smarter/smarter/apps/api/v1/contract/sam/<Kind>.schema.json.",
TAG_MANIFESTS,
"CliApiJsonSchemaResponse",
parameters=[KIND_PARAMETER],
security=PUBLIC,
),
"/cli/deploy/{kind}/": operation(
"deploy",
"Deploy a resource",
"Deploy a resource of a deployable kind, e.g. an LLMClient. Deployment may create cloud infrastructure.",
TAG_DEPLOYMENT,
"CliApiMessageResponse",
parameters=by_name,
),
"/cli/undeploy/{kind}/": operation(
"undeploy",
"Undeploy a resource",
"Undeploy a deployed resource.",
TAG_DEPLOYMENT,
"CliApiMessageResponse",
parameters=by_name,
),
"/cli/logs/{kind}/": operation(
"logs",
"A resource's logs",
"A resource's logs.",
TAG_DEPLOYMENT,
"CliApiMessageResponse",
parameters=by_name,
),
"/cli/status/": operation(
"status",
"Platform status",
"The status of the platform's infrastructure and compute.",
TAG_PLATFORM,
"CliApiStatusResponse",
),
"/cli/version/": operation(
"version",
"Versions",
"The versions of the platform and its packages, and the smarter cli's latest version.",
TAG_PLATFORM,
"CliApiVersionResponse",
),
"/cli/whoami/": operation(
"whoami", "Who am I", "The api key's user and account.", TAG_PLATFORM, "CliApiWhoamiResponse"
),
"/cli/prompt/{name}/": operation(
"prompt",
"Prompt an LLMClient",
"Send a message to an LLMClient, and return its chat completion. The chat session is cached under the "
"uid, so that the next prompt continues it. The completion is the Smarter Chat api contract's version 1 "
"completion, decoded from the JSON string of its body.",
TAG_CHAT,
"CliApiPromptResponse",
parameters=[LLMCLIENT_PARAMETER, UID_PARAMETER, NEW_SESSION_PARAMETER],
request_body=PROMPT_BODY,
),
"/cli/prompt/config/{name}/": operation(
"prompt_config",
"An LLMClient's chat configuration",
"The configuration of a chat with an LLMClient, and its session, as the Smarter Chat api contract's "
"version 2 configuration response.",
TAG_CHAT,
PromptChatApiConfigResponse.__name__,
parameters=[LLMCLIENT_PARAMETER, UID_PARAMETER, NEW_SESSION_PARAMETER],
),
"/cli/resources/": operation(
"resources",
"Resource kinds",
"The kinds of resources that the cli api manages.",
TAG_PLATFORM,
"CliApiResourcesResponse",
),
}
return {path: OpenApiPathItem(post=command) for path, command in commands.items()}
[docs]
def chat_paths() -> dict[str, OpenApiPathItem]:
"""
The chat api's paths, relative to the server's ``/api/v1``.
The prompt's ``200`` response is JSON, or, for a client that accepts ``text/event-stream``, a
stream of Server-Sent Events: ``progress`` events, then one ``result`` event, whose ``response``
is the JSON that a non-streaming client receives. OpenAPI 3.1 can't type the events of a stream,
so the stream's media type names their schemas in an ``x-smarter-events`` extension.
:returns: The paths, e.g. ``{"/llm-clients/{id}/prompt/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
parameters = [LLMCLIENT_ID_PARAMETER, SESSION_KEY_PARAMETER, CAPABILITIES_PARAMETER]
config = OpenApiOperation(
operationId="llmclient_config",
summary="An LLMClient's chat configuration",
description="The configuration of a chat with an LLMClient, and a chat session: a new one, or the one "
"whose key is sent. Its session_key identifies the session in the following prompts, and "
"url_llmclient is where to send them.",
tags=[TAG_CHAT],
parameters=parameters,
requestBody=OpenApiRequestBody(
required=False,
content={"application/json": OpenApiMediaType(schema_=ref(PromptChatApiConfigRequest.__name__))},
),
responses={
"200": json_response("The configuration.", PromptChatApiConfigResponse.__name__),
"default": json_response(
"An error, e.g. 404 for an LLMClient that does not exist, or that the user may not read.",
PromptChatApiErrorResponse.__name__,
),
},
)
prompt = OpenApiOperation(
operationId="llmclient_prompt",
summary="Prompt an LLMClient",
description="Run a prompt: the platform selects plugins, calls the LLM, runs the tools that the LLM "
"calls, and applies the Guardrails, then returns the messages that the prompt added to the chat. "
"Prompts are billed by the LLM provider. A streaming client receives progress events while the prompt "
"runs, then the result, whose status is the prompt's http status.",
tags=[TAG_CHAT],
parameters=[*parameters, ACCEPT_PARAMETER],
requestBody=OpenApiRequestBody(
required=True,
content={"application/json": OpenApiMediaType(schema_=ref(PromptChatApiPromptRequest.__name__))},
),
responses={
"200": OpenApiResponse(
description="The prompt's result, as JSON, or as a stream of Server-Sent Events.",
content={
"application/json": OpenApiMediaType(schema_=ref(PromptChatApiPromptResponse.__name__)),
"text/event-stream": OpenApiMediaType(
schema_={
"type": "string",
"description": "Server-Sent Events: retry, then a progress event per step, keepalive "
"comments while the prompt waits, and a result event, last. Each event's data is JSON.",
},
x_smarter_events={
"progress": ref(PromptChatApiProgressEvent.__name__),
"result": ref(PromptChatApiProgressResult.__name__),
},
),
},
),
"default": json_response("An error.", "LLMClientPromptErrorResponse"),
},
)
return {
"/llm-clients/{id}/config/": OpenApiPathItem(post=config),
"/llm-clients/{id}/prompt/": OpenApiPathItem(post=prompt),
}
[docs]
def rest_read(
operation_id: str,
summary: str,
description: str,
tag: str,
item: str,
*,
many: bool,
parameters: list[OpenApiParameter],
note: str = SUPERUSER_ONLY,
) -> OpenApiOperation:
"""
A read of the REST api: a ``GET`` whose response is a row, or a list of rows, of one of the document's schemas.
:param operation_id: The operation's id, e.g. llmclient_list.
:type operation_id: str
:param summary: A one-line summary.
:type summary: str
:param description: A description, to which ``note`` is added.
:type description: str
:param tag: The operation's tag.
:type tag: str
:param item: The schema of a row, in ``components.schemas``.
:type item: str
:param many: Whether the response is a list of rows.
:type many: bool
:param parameters: The operation's parameters.
:type parameters: list[OpenApiParameter]
:param note: Who may read, e.g. :data:`SUPERUSER_ONLY` or :data:`READ_PERMISSION`.
:type note: str
:returns: The operation.
:rtype: OpenApiOperation
"""
schema = {"type": "array", "items": ref(item)} if many else ref(item)
return OpenApiOperation(
operationId=operation_id,
summary=summary,
description=f"{description} {note}",
tags=[tag],
parameters=parameters,
responses={
"200": OpenApiResponse(
description="The list." if many else "The row.",
content={"application/json": OpenApiMediaType(schema_=schema)},
),
"default": json_response(
"An error, e.g. 404 for a row that does not exist, or 403 for a user who is not a superuser.",
RestApiErrorResponse.__name__,
),
},
)
def integer_id(name: str, description: str) -> OpenApiParameter:
"""
A path parameter that is a row's numeric id.
:param name: The parameter's name, e.g. plugin_id.
:type name: str
:param description: Its description.
:type description: str
:returns: The parameter.
:rtype: OpenApiParameter
"""
return OpenApiParameter(name=name, in_="path", required=True, description=description, schema_={"type": "integer"})
[docs]
def llmclient_paths() -> dict[str, OpenApiPathItem]:
"""
The REST reads of the LLMClients, relative to the server's ``/api/v1``.
An LLMClient is read by its numeric id or its hashed id. Its related rows, by its numeric id only:
their routes take no hashed id.
:returns: The paths, e.g. ``{"/llm-clients/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
llmclient_id = integer_id("id", "The LLMClient's numeric id.")
tag = TAG_LLMCLIENTS
reads = {
"/llm-clients/": rest_read(
"llmclient_list",
"List LLMClients",
"The LLMClients that the user may read.",
tag,
LLMClientApiLLMClient.__name__,
many=True,
parameters=[],
),
"/llm-clients/{id}/": rest_read(
"llmclient_retrieve",
"An LLMClient",
"An LLMClient, with its functions, plugins, custom domains, api keys and urls.",
tag,
LLMClientApiLLMClient.__name__,
many=False,
parameters=[LLMCLIENT_ID_PARAMETER],
),
"/llm-clients/{id}/plugins/": rest_read(
"llmclient_plugin_list",
"An LLMClient's plugins",
"The plugins of an LLMClient, each with the plugin's metadata.",
tag,
LLMClientApiPlugin.__name__,
many=True,
parameters=[llmclient_id],
),
"/llm-clients/{id}/plugins/{plugin_id}/": rest_read(
"llmclient_plugin_retrieve",
"A plugin of an LLMClient",
"A plugin of an LLMClient, by the LLMClientPlugin's id.",
tag,
LLMClientApiPlugin.__name__,
many=False,
parameters=[llmclient_id, integer_id("plugin_id", "The LLMClientPlugin's id.")],
),
"/llm-clients/{id}/apikeys/": rest_read(
"llmclient_apikey_list",
"An LLMClient's api keys",
"The api keys of an LLMClient: the ids of their SmarterAuthTokens, never the keys.",
tag,
LLMClientApiKey.__name__,
many=True,
parameters=[llmclient_id],
),
"/llm-clients/{id}/apikeys/{apikey_id}/": rest_read(
"llmclient_apikey_retrieve",
"An api key of an LLMClient",
"An api key of an LLMClient, by the LLMClientAPIKey's id.",
tag,
LLMClientApiKey.__name__,
many=False,
parameters=[llmclient_id, integer_id("apikey_id", "The LLMClientAPIKey's id.")],
),
"/llm-clients/{id}/customdomains": rest_read(
"llmclient_customdomain_list",
"An LLMClient's custom domains",
"The custom domain of an LLMClient, as a list of none or one.",
tag,
LLMClientApiCustomDomain.__name__,
many=True,
parameters=[llmclient_id],
),
"/llm-clients/{id}/customdomains/{customdomain_id}": rest_read(
"llmclient_customdomain_retrieve",
"A custom domain of an LLMClient",
"A custom domain of an LLMClient, by its id.",
tag,
LLMClientApiCustomDomain.__name__,
many=False,
parameters=[llmclient_id, integer_id("customdomain_id", "The LLMClientCustomDomain's id.")],
),
"/llm-clients/{id}/functions": rest_read(
"llmclient_function_list",
"An LLMClient's built-in functions",
"The built-in functions of an LLMClient.",
tag,
LLMClientApiFunction.__name__,
many=True,
parameters=[llmclient_id],
),
"/llm-clients/{id}/functions/{function_id}": rest_read(
"llmclient_function_retrieve",
"A built-in function of an LLMClient",
"A built-in function of an LLMClient, by the LLMClientFunctions' id.",
tag,
LLMClientApiFunction.__name__,
many=False,
parameters=[llmclient_id, integer_id("function_id", "The LLMClientFunctions' id.")],
),
}
return {path: OpenApiPathItem(get=read) for path, read in reads.items()}
[docs]
def id_or_hashed_id(resource: str) -> OpenApiParameter:
"""
The ``{id}`` path parameter of a resource whose routes take its numeric id or its hashed id.
:param resource: The resource's kind, e.g. Guardrail.
:type resource: str
:returns: The parameter.
:rtype: OpenApiParameter
"""
return OpenApiParameter(
name="id",
in_="path",
required=True,
description=f"The {resource}'s numeric id, e.g. 12, or its hashed id, e.g. rMTAwMDAxMgx.",
schema_={"type": "string"},
)
[docs]
def guardrail_paths() -> dict[str, OpenApiPathItem]:
"""
The Guardrails' REST reads, and the dry run of a Guardrail, relative to the server's ``/api/v1``.
:returns: The paths, e.g. ``{"/guardrails/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
guardrail_id = id_or_hashed_id("Guardrail")
events = rest_read(
"guardrail_events",
"A Guardrail's events",
"The latest 100 events of a Guardrail that the user owns, newest first: what it did to prompts.",
TAG_GUARDRAILS,
GuardrailApiEvents.__name__,
many=False,
parameters=[
guardrail_id,
OpenApiParameter(
name="reviewed",
in_="query",
description="Only the events that a person has reviewed (true), or has not (false).",
schema_={"type": "string", "enum": ["true", "false"]},
),
OpenApiParameter(
name="disposition",
in_="query",
description="Only the events with this disposition, e.g. blocked.",
schema_={"type": "string", "enum": list(get_args(GuardrailApiEventDisposition))},
),
],
note=READ_PERMISSION,
)
evaluate = OpenApiOperation(
operationId="guardrail_evaluate",
summary="Dry-run a Guardrail",
description="Run a Guardrail on a text, as the user's message (input) or the LLM's reply (output), and "
f"return what it would do. It records no events. {READ_PERMISSION}",
tags=[TAG_GUARDRAILS],
parameters=[guardrail_id],
requestBody=OpenApiRequestBody(
required=True,
content={"application/json": OpenApiMediaType(schema_=ref(GuardrailApiEvaluateRequest.__name__))},
),
responses={
"200": json_response("The dry run's result.", GuardrailApiEvaluation.__name__),
"default": json_response(
"An error, e.g. 400 for an invalid request, or 404 for a Guardrail that the user may not read.",
RestApiErrorResponse.__name__,
),
},
)
return {
"/guardrails/": OpenApiPathItem(
get=rest_read(
"guardrail_list",
"List Guardrails",
"The Guardrails that the user may read, by priority and name.",
TAG_GUARDRAILS,
GuardrailApiGuardrail.__name__,
many=True,
parameters=[],
note=READ_PERMISSION,
)
),
"/guardrails/{id}/": OpenApiPathItem(
get=rest_read(
"guardrail_retrieve",
"A Guardrail",
"A Guardrail that the user may read.",
TAG_GUARDRAILS,
GuardrailApiGuardrail.__name__,
many=False,
parameters=[guardrail_id],
note=READ_PERMISSION,
)
),
"/guardrails/{id}/evaluate/": OpenApiPathItem(post=evaluate),
"/guardrails/{id}/events/": OpenApiPathItem(get=events),
}
[docs]
def secret_paths() -> dict[str, OpenApiPathItem]:
"""
The Secrets' REST reads, relative to the server's ``/api/v1``.
A Secret's value is never returned.
:returns: The paths, e.g. ``{"/secrets/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
return {
"/secrets/": OpenApiPathItem(
get=rest_read(
"secret_list",
"List Secrets",
"The Secrets of the user's accounts: their metadata, never their values.",
TAG_SECRETS,
SecretApiSecret.__name__,
many=True,
parameters=[],
)
),
"/secrets/{id}/": OpenApiPathItem(
get=rest_read(
"secret_retrieve",
"A Secret",
"A Secret of the user's accounts: its metadata, never its value.",
TAG_SECRETS,
SecretApiSecret.__name__,
many=False,
parameters=[integer_id("id", "The Secret's numeric id.")],
)
),
}
[docs]
def mcpclient_paths() -> dict[str, OpenApiPathItem]:
"""
The MCPClients' REST reads, their reconnection, and the call of a tool, relative to the server's ``/api/v1``.
:returns: The paths, e.g. ``{"/mcpclients/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
mcpclient_id = id_or_hashed_id("MCPClient")
connection_error = json_response(
"An error, e.g. 502 for an MCP server that failed, 403 for a user who does not own the MCPClient, or 404 for "
"an MCPClient that the user may not read.",
"MCPClientApiErrorResponse",
)
tools = OpenApiOperation(
operationId="mcpclient_tools",
summary="An MCPClient's tools",
description="The tools that the MCPClient offers the LLM, from its server's cached catalog, with its "
f"connection status and the server's instructions. {READ_PERMISSION}",
tags=[TAG_MCPCLIENTS],
parameters=[
mcpclient_id,
OpenApiParameter(
name="refresh",
in_="query",
description="true reconnects to the MCP server first, rather than reading the cached catalog.",
schema_={"type": "string", "enum": ["true", "false"], "default": "false"},
),
],
responses={"200": json_response("The tools.", MCPClientApiTools.__name__), "default": connection_error},
)
refresh = OpenApiOperation(
operationId="mcpclient_refresh",
summary="Reconnect an MCPClient",
description="Reconnect to the MCPClient's server, refresh its catalog, and return its connection status. "
"Only the MCPClient's owner may reconnect it.",
tags=[TAG_MCPCLIENTS],
parameters=[mcpclient_id],
responses={
"200": json_response("The connection status.", MCPClientApiStatus.__name__),
"default": connection_error,
},
)
tool_call = OpenApiOperation(
operationId="mcpclient_tool_call",
summary="Call an MCPClient's tool",
description="Call one of the tools that the MCPClient allows, with the request body as its arguments, and "
"return its result as text. The tool runs on the MCP server, and may change what the server manages. Only "
"the MCPClient's owner may call its tools.",
tags=[TAG_MCPCLIENTS],
parameters=[
mcpclient_id,
OpenApiParameter(
name="tool_name", in_="path", required=True, description="The tool's name.", schema_={"type": "string"}
),
],
requestBody=OpenApiRequestBody(
required=False,
content={
"application/json": OpenApiMediaType(
schema_={
"type": "object",
"additionalProperties": True,
"description": "The tool's arguments, as its input schema describes them.",
}
)
},
),
responses={
"200": json_response("The tool's result.", MCPClientApiToolResult.__name__),
"default": json_response(
"An error, e.g. 403 for a tool that the MCPClient does not allow, or 502 for a failed server.",
RestApiErrorResponse.__name__,
),
},
)
return {
"/mcpclients/": OpenApiPathItem(
get=rest_read(
"mcpclient_list",
"List MCPClients",
"The MCPClients that the user may read, by name.",
TAG_MCPCLIENTS,
MCPClientApiMCPClient.__name__,
many=True,
parameters=[],
note=READ_PERMISSION,
)
),
"/mcpclients/{id}/": OpenApiPathItem(
get=rest_read(
"mcpclient_retrieve",
"An MCPClient",
"An MCPClient that the user may read. Its credentials are their Secret's name, never its value.",
TAG_MCPCLIENTS,
MCPClientApiMCPClient.__name__,
many=False,
parameters=[mcpclient_id],
note=READ_PERMISSION,
)
),
"/mcpclients/{id}/tools/": OpenApiPathItem(get=tools),
"/mcpclients/{id}/refresh/": OpenApiPathItem(post=refresh),
"/mcpclients/{id}/tools/{tool_name}/": OpenApiPathItem(post=tool_call),
}
[docs]
def rest_operation( # pylint: disable=too-many-arguments
operation_id: str,
summary: str,
description: str,
tag: str,
response: str,
*,
parameters: list[OpenApiParameter],
status: str = "200",
request_body: Optional[OpenApiRequestBody] = None,
) -> OpenApiOperation:
"""
An operation of the REST api, whose success is one of the document's schemas, and whose errors are DRF's.
:param operation_id: The operation's id, e.g. vectorstore_search.
:type operation_id: str
:param summary: A one-line summary.
:type summary: str
:param description: A description, which says who may call it, and what it changes.
:type description: str
:param tag: The operation's tag.
:type tag: str
:param response: The successful response's schema, in ``components.schemas``.
:type response: str
:param parameters: The operation's parameters.
:type parameters: list[OpenApiParameter]
:param status: The successful response's http status, e.g. 202 for queued work.
:type status: str
:param request_body: The operation's request body.
:type request_body: Optional[OpenApiRequestBody]
:returns: The operation.
:rtype: OpenApiOperation
"""
return OpenApiOperation(
operationId=operation_id,
summary=summary,
description=description,
tags=[tag],
parameters=parameters,
requestBody=request_body,
responses={
status: json_response("Success.", response),
"default": json_response(
"An error, e.g. 400 for an invalid request, or 404 for a row that the user may not read or own.",
RestApiErrorResponse.__name__,
),
},
)
[docs]
def vectorstore_paths() -> dict[str, OpenApiPathItem]:
"""
The vectorstores' REST reads and maintenance, relative to the server's ``/api/v1``.
Reading and searching need read permission. Everything else needs ownership.
:returns: The paths, e.g. ``{"/vectorstores/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
vectorstore_id = id_or_hashed_id("vectorstore")
tag = TAG_VECTORSTORES
owner = "Only the vectorstore's owner may call it."
files = {
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {"type": "string", "contentMediaType": "application/octet-stream"},
"description": "The files, e.g. PDFs and Word documents, of at most 50 MiB each.",
},
"metadata": {"type": "string", "description": "Metadata to store with each chunk, as a JSON object."},
},
"required": ["files"],
}
return {
"/vectorstores/": OpenApiPathItem(
get=rest_read(
"vectorstore_list",
"List vectorstores",
"The vectorstores that the user may read, by name.",
tag,
VectorstoreApiVectorstore.__name__,
many=True,
parameters=[],
note=READ_PERMISSION,
)
),
"/vectorstores/{id}/": OpenApiPathItem(
get=rest_read(
"vectorstore_retrieve",
"A vectorstore",
"A vectorstore that the user may read.",
tag,
VectorstoreApiVectorstore.__name__,
many=False,
parameters=[vectorstore_id],
note=READ_PERMISSION,
)
),
"/vectorstores/{id}/status/": OpenApiPathItem(
get=rest_read(
"vectorstore_status",
"A vectorstore's status",
"Check a vectorstore's status and statistics now, with its database.",
tag,
VectorstoreApiStatus.__name__,
many=False,
parameters=[vectorstore_id],
note=READ_PERMISSION,
)
),
"/vectorstores/{id}/deploy/": OpenApiPathItem(
post=rest_operation(
"vectorstore_deploy",
"Deploy a vectorstore",
"Create the vectorstore's database: a Qdrant server on the platform's Kubernetes cluster, or a "
f"Pinecone index. It creates billable infrastructure, and returns before it is ready. {owner}",
tag,
VectorstoreApiVectorstore.__name__,
parameters=[vectorstore_id],
status="202",
)
),
"/vectorstores/{id}/undeploy/": OpenApiPathItem(
post=rest_operation(
"vectorstore_undeploy",
"Undeploy a vectorstore",
f"Stop serving the vectorstore. A self-hosted server's data is kept. {owner}",
tag,
VectorstoreApiVectorstore.__name__,
parameters=[vectorstore_id],
)
),
"/vectorstores/{id}/documents/": OpenApiPathItem(
get=rest_read(
"vectorstore_documents",
"A vectorstore's documents",
"The vectorstore's documents, newest first, without their content.",
tag,
VectorstoreApiDocuments.__name__,
many=False,
parameters=[vectorstore_id],
note=READ_PERMISSION,
),
post=rest_operation(
"vectorstore_add_documents",
"Add documents to a vectorstore",
"Add documents, and queue them to be loaded if the vectorstore is ready: a text, the document at a "
"url, or files as multipart/form-data, whose text is extracted now. A document whose content the "
f"vectorstore already has is a duplicate, and is not added again. {owner}",
tag,
VectorstoreApiDocumentsAdded.__name__,
parameters=[vectorstore_id],
status="202",
request_body=OpenApiRequestBody(
required=True,
content={
"application/json": OpenApiMediaType(schema_=ref(VectorstoreApiAddText.__name__)),
"multipart/form-data": OpenApiMediaType(schema_=files),
},
),
),
),
"/vectorstores/{id}/documents/{document_id}/": OpenApiPathItem(
delete=rest_operation(
"vectorstore_delete_document",
"Remove a document from a vectorstore",
f"Remove a document and its chunks. They are removed in the background. {owner}",
tag,
VectorstoreApiDocumentDeleted.__name__,
parameters=[vectorstore_id, integer_id("document_id", "The document's id.")],
status="202",
)
),
"/vectorstores/{id}/search/": OpenApiPathItem(
post=rest_operation(
"vectorstore_search",
"Search a vectorstore",
f"Find the chunks most similar to a query. {READ_PERMISSION}",
tag,
VectorstoreApiSearchResults.__name__,
parameters=[vectorstore_id],
request_body=OpenApiRequestBody(
required=True,
description="The query, and how to search.",
content={"application/json": OpenApiMediaType(schema_=ref(VectorstoreApiSearch.__name__))},
),
)
),
"/vectorstores/{id}/snapshots/": OpenApiPathItem(
get=rest_read(
"vectorstore_snapshots",
"A vectorstore's snapshots",
"The vectorstore's snapshots: Qdrant snapshots, or Pinecone backups.",
tag,
VectorstoreApiSnapshots.__name__,
many=False,
parameters=[vectorstore_id],
note=READ_PERMISSION,
),
post=rest_operation(
"vectorstore_create_snapshot",
"Snapshot a vectorstore",
f"Take a snapshot of the vectorstore's data now. {owner}",
tag,
VectorstoreApiSnapshot.__name__,
parameters=[vectorstore_id],
status="201",
),
),
"/vectorstores/{id}/snapshots/{snapshot_id}/restore/": OpenApiPathItem(
post=rest_operation(
"vectorstore_restore",
"Restore a vectorstore",
f"Replace the vectorstore's data with one of its snapshots. {owner}",
tag,
VectorstoreApiRestored.__name__,
parameters=[vectorstore_id, integer_id("snapshot_id", "The snapshot's id.")],
)
),
}
[docs]
def query(name: str, description: str, schema: OpenApiSchema, required: bool = False) -> OpenApiParameter:
"""
A query parameter.
:param name: The parameter's name.
:type name: str
:param description: Its description.
:type description: str
:param schema: Its JSON Schema.
:type schema: OpenApiSchema
:param required: Whether it is required.
:type required: bool
:returns: The parameter.
:rtype: OpenApiParameter
"""
return OpenApiParameter(name=name, in_="query", required=required, description=description, schema_=schema)
[docs]
def llmhost_paths() -> dict[str, OpenApiPathItem]:
"""
The LLMHosts' REST reads, discovery and deployment, relative to the server's ``/api/v1``.
:returns: The paths, e.g. ``{"/llmhosts/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
llmhost_id = id_or_hashed_id("LLMHost")
tag = TAG_LLMHOSTS
owner = "Only the LLMHost's owner may call it."
def read(operation_id, summary, description, item, parameters, note=READ_PERMISSION, many=False):
return rest_read(operation_id, summary, description, tag, item, many=many, parameters=parameters, note=note)
return {
"/llmhosts/": OpenApiPathItem(
get=read(
"llmhost_list",
"List LLMHosts",
"The LLMHosts that the user may read, by name.",
LLMHostApiLLMHost.__name__,
[],
many=True,
)
),
"/llmhosts/report/": OpenApiPathItem(
get=read(
"llmhost_report",
"A report of LLMHosts",
"A summary of the LLMHosts that the user owns: their status, GPUs and cost.",
LLMHostApiReport.__name__,
[],
)
),
"/llmhosts/discover/": OpenApiPathItem(
get=read(
"llmhost_discover",
"Search a model catalog",
"Search a model catalog, most popular first, to choose a model to host.",
LLMHostApiDiscovery.__name__,
[
query("q", "The text to search for.", {"type": "string"}),
query(
"task",
"Only models of this task.",
{"type": "string", "enum": ["text-generation", "embedding"]},
),
query(
"catalog",
"The catalog.",
{"type": "string", "enum": ["huggingface", "builtin"], "default": "huggingface"},
),
query("limit", "The number of models, at most 50.", {"type": "integer", "default": 20}),
],
)
),
"/llmhosts/discover/manifest/": OpenApiPathItem(
get=read(
"llmhost_discover_manifest",
"Draft an LLMHost manifest",
"Draft an LLMHost manifest for a model of a catalog, to review and apply.",
LLMHostApiDraftManifest.__name__,
[
query("repository", "The model, e.g. meta-llama/Llama-3.1-8B-Instruct.", {"type": "string"}, True),
query("catalog", "The catalog.", {"type": "string", "default": "huggingface"}),
query("engine", "The inference engine, e.g. vllm.", {"type": "string"}),
query("name", "The LLMHost's name.", {"type": "string"}),
query("tokenSecret", "The name of the Secret of a token for gated models.", {"type": "string"}),
],
)
),
"/llmhosts/{id}/": OpenApiPathItem(
get=read(
"llmhost_retrieve",
"An LLMHost",
"An LLMHost that the user may read.",
LLMHostApiLLMHost.__name__,
[llmhost_id],
)
),
"/llmhosts/{id}/status/": OpenApiPathItem(
get=read(
"llmhost_status",
"An LLMHost's status",
"Check an LLMHost's status now, with its pods and health check.",
LLMHostApiStatus.__name__,
[llmhost_id],
)
),
"/llmhosts/{id}/logs/": OpenApiPathItem(
get=read(
"llmhost_logs",
"An LLMHost's logs",
"The inference server's recent logs.",
LLMHostApiLogs.__name__,
[llmhost_id, query("tail", "The number of lines.", {"type": "integer", "default": 200})],
note=owner,
)
),
"/llmhosts/{id}/plan/": OpenApiPathItem(
get=read(
"llmhost_plan",
"An LLMHost's deployment plan",
"The Kubernetes resources that a deployment would apply, with secrets redacted.",
LLMHostApiPlan.__name__,
[llmhost_id],
note=owner,
)
),
"/llmhosts/{id}/deploy/": OpenApiPathItem(
post=rest_operation(
"llmhost_deploy",
"Deploy an LLMHost",
"Launch the LLMHost's inference server on the platform's Kubernetes cluster. It creates billable "
f"infrastructure, e.g. GPU nodes, and returns before the server is ready. {owner}",
tag,
LLMHostApiStatus.__name__,
parameters=[llmhost_id],
status="202",
)
),
"/llmhosts/{id}/undeploy/": OpenApiPathItem(
post=rest_operation(
"llmhost_undeploy",
"Undeploy an LLMHost",
f"Destroy the LLMHost's Kubernetes resources. The LLMHost remains. {owner}",
tag,
LLMHostApiUndeployed.__name__,
parameters=[
llmhost_id,
query(
"purge",
"true also deletes the model volume, and the weights cached on it.",
{"type": "string", "enum": ["true", "false"], "default": "false"},
),
],
)
),
}
[docs]
def plugin_paths() -> dict[str, OpenApiPathItem]:
"""
The plugins' REST reads, relative to the server's ``/api/v1``.
:returns: The paths, e.g. ``{"/plugins/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
return {
"/plugins/": OpenApiPathItem(
get=rest_read(
"plugin_list",
"List plugins",
"The plugins that the user owns, newest first: their metadata.",
TAG_PLUGINS,
PluginApiPluginMeta.__name__,
many=True,
parameters=[],
note="Requires an api key.",
)
),
"/plugins/{plugin_id}/": OpenApiPathItem(
get=rest_read(
"plugin_retrieve",
"A plugin",
"A plugin's manifest, if the user may read the plugin. It is served only while the platform's "
"ALLOW_API_GET switch is active, and answers 405 otherwise.",
TAG_PLUGINS,
"SAMManifest",
many=False,
parameters=[integer_id("plugin_id", "The plugin's id.")],
note=READ_PERMISSION,
)
),
}
[docs]
def provider_paths() -> dict[str, OpenApiPathItem]:
"""
The Providers' REST reads, relative to the server's ``/api/v1``.
A Provider is read by its name.
:returns: The paths, e.g. ``{"/providers/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
name = OpenApiParameter(
name="name",
in_="path",
required=True,
description="The Provider's name, e.g. openai.",
schema_={"type": "string"},
)
model_name = OpenApiParameter(
name="model_name",
in_="path",
required=True,
description="The model's name, e.g. gpt-4o.",
schema_={"type": "string"},
)
tag = TAG_PROVIDERS
return {
"/providers/": OpenApiPathItem(
get=rest_read(
"provider_list",
"List Providers",
"The active Providers that the user may read, by name.",
tag,
ProviderApiProvider.__name__,
many=True,
parameters=[],
note=READ_PERMISSION,
)
),
"/providers/{name}/": OpenApiPathItem(
get=rest_read(
"provider_retrieve",
"A Provider",
"An active Provider that the user may read. The user's own account's wins over another of the same name.",
tag,
ProviderApiProvider.__name__,
many=False,
parameters=[name],
note=READ_PERMISSION,
)
),
"/providers/{name}/models/": OpenApiPathItem(
get=rest_read(
"provider_models",
"A Provider's models",
"The active models of a Provider, by name.",
tag,
ProviderApiModel.__name__,
many=True,
parameters=[name],
note=READ_PERMISSION,
)
),
"/providers/{name}/models/{model_name}/": OpenApiPathItem(
get=rest_read(
"provider_model",
"A Provider's model",
"An active model of a Provider.",
tag,
ProviderApiModel.__name__,
many=False,
parameters=[name, model_name],
note=READ_PERMISSION,
)
),
}
[docs]
def proxy_paths() -> dict[str, OpenApiPathItem]:
"""
The Proxies' list, relative to the server's ``/api/v1``.
Their passthroughs are the providers' apis.
:returns: The paths, e.g. ``{"/proxy/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
return {
"/proxy/": OpenApiPathItem(
get=rest_read(
"proxy_list",
"List Proxies",
"The Proxies that the user may use, with their urls. Send a provider's requests to a Proxy's url, "
"with a Smarter api key, and the Proxy forwards them with the provider's.",
TAG_PROXIES,
ProxyApiProxies.__name__,
many=False,
parameters=[],
note="Requires an api key.",
)
),
}
def account_paths() -> dict[str, OpenApiPathItem]:
"""
The accounts' REST reads, relative to the server's ``/api/v1``.
Only superusers may call them.
:returns: The paths, e.g. ``{"/accounts/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
tag = TAG_ACCOUNTS
def read(operation_id, summary, description, item, parameters, many=False):
return rest_read(operation_id, summary, description, tag, item, many=many, parameters=parameters)
return {
"/accounts/": OpenApiPathItem(
get=read("account_list", "List accounts", "Every account.", AccountApiAccountDetail.__name__, [], True)
),
"/accounts/{account_id}/": OpenApiPathItem(
get=read(
"account_retrieve",
"An account",
"An account, by its id.",
AccountApiAccountDetail.__name__,
[integer_id("account_id", "The account's id.")],
)
),
"/accounts/users/": OpenApiPathItem(
get=read("user_list", "List users", "Every user.", AccountApiUserDetail.__name__, [], True)
),
"/accounts/users/{user_id}/": OpenApiPathItem(
get=read(
"user_retrieve",
"A user",
"A user, by its id.",
AccountApiUserDetail.__name__,
[integer_id("user_id", "The user's id.")],
)
),
"/accounts/user-profiles/": OpenApiPathItem(
get=read(
"user_profile_list",
"List user profiles",
"Every user profile: each user's membership of an account.",
AccountApiProfile.__name__,
[],
True,
)
),
"/accounts/user-profiles/{user_profile_id}/": OpenApiPathItem(
get=read(
"user_profile_retrieve",
"A user profile",
"A user profile, by its id.",
AccountApiProfile.__name__,
[integer_id("user_profile_id", "The user profile's id.")],
)
),
"/accounts/contacts/": OpenApiPathItem(
get=read(
"contact_list",
"List account contacts",
"Every account's contacts, who receive its notices.",
AccountApiContact.__name__,
[],
True,
)
),
"/accounts/contacts/{account_contact_id}/": OpenApiPathItem(
get=read(
"contact_retrieve",
"An account contact",
"An account contact, by its id.",
AccountApiContact.__name__,
[integer_id("account_contact_id", "The account contact's id.")],
)
),
}
HISTORY_PERMISSION = (
"Requires an api key. The response includes only the chat sessions whose user the user may manage: their "
"own, their account's users' for an account admin, and every one for a superuser."
)
"""The note of the prompt history's reads."""
def history_paths() -> dict[str, OpenApiPathItem]:
"""
The prompt history's REST reads, relative to the server's ``/api/v1``, newest first.
:returns: The paths, e.g. ``{"/prompts/history/prompt/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
tag = TAG_HISTORY
row_id = integer_id("id", "The row's id.")
def read(operation_id, summary, description, item, parameters, many=False):
return rest_read(
operation_id, summary, description, tag, item, many=many, parameters=parameters, note=HISTORY_PERMISSION
)
return {
"/prompts/history/prompt/": OpenApiPathItem(
get=read(
"history_session_list",
"List chat sessions",
"The chat sessions: each conversation with an LLMClient.",
PromptHistoryApiSession.__name__,
[],
True,
)
),
"/prompts/history/prompt/{id}/": OpenApiPathItem(
get=read(
"history_session_retrieve",
"A chat session",
"A chat session.",
PromptHistoryApiSession.__name__,
[row_id],
)
),
"/prompts/history/tool-calls/": OpenApiPathItem(
get=read(
"history_tool_call_list",
"List tool calls",
"The tool calls that the LLMs made in the chat sessions, with their input and result.",
PromptHistoryApiToolCall.__name__,
[],
True,
)
),
"/prompts/history/tool-calls/{id}/": OpenApiPathItem(
get=read(
"history_tool_call_retrieve", "A tool call", "A tool call.", PromptHistoryApiToolCall.__name__, [row_id]
)
),
"/prompts/history/plugin-usage/": OpenApiPathItem(
get=read(
"history_plugin_usage_list",
"List plugin usage",
"The plugins that the chat sessions used.",
PromptHistoryApiPluginUsage.__name__,
[],
True,
)
),
"/prompts/history/plugin-usage/{id}/": OpenApiPathItem(
get=read(
"history_plugin_usage_retrieve",
"A plugin usage",
"A use of a plugin in a chat session.",
PromptHistoryApiPluginUsage.__name__,
[row_id],
)
),
}
def journal_paths() -> dict[str, OpenApiPathItem]:
"""
The Smarter Journal's REST reads, relative to the server's ``/api/v1``.
Only superusers may call them.
:returns: The paths, e.g. ``{"/journal/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
tag = TAG_JOURNAL
return {
"/journal/": OpenApiPathItem(
get=rest_read(
"journal_list",
"List journal entries",
"One page of the journal's entries, newest first unless sorted, without their request and response "
"documents.",
tag,
SAMJournalApiPage.__name__,
many=False,
parameters=[
query("page", "The page, from 1.", {"type": "integer", "default": 1}),
query("page_size", "The entries of a page, at most 100.", {"type": "integer", "default": 25}),
query("search", "Text that an entry's key, thing or command contains.", {"type": "string"}),
query(
"ordering",
"The column to sort by, e.g. createdAt, or -createdAt to sort it in descending order.",
{
"type": "string",
"enum": [f"{sign}{column}" for column in JOURNAL_SORT_FIELDS for sign in ("", "-")],
},
),
],
)
),
"/journal/{key}/": OpenApiPathItem(
get=rest_read(
"journal_retrieve",
"A journal entry",
"A journal entry, by its key, with its request and response documents. The request document holds "
"the caller's headers, including its api key.",
tag,
SAMJournalApiEntry.__name__,
many=False,
parameters=[
OpenApiParameter(
name="key",
in_="path",
required=True,
description="The entry's key, 64 hexadecimal characters.",
schema_={"type": "string", "pattern": JOURNAL_KEY_PATTERN},
)
],
)
),
}
def superuser_list_and_retrieve(resource: str, path: str, tag: str, item: str) -> dict[str, OpenApiPathItem]:
"""
A family's REST list and retrieve, whose views only superusers may call.
:param resource: The resource's name, e.g. Orchestrator.
:type resource: str
:param path: The family's path, e.g. /orchestrators/.
:type path: str
:param tag: The operations' tag.
:type tag: str
:param item: The schema of a row, in ``components.schemas``.
:type item: str
:returns: The paths.
:rtype: dict[str, OpenApiPathItem]
"""
plural = tag.lower()
singular = resource.lower()
return {
path: OpenApiPathItem(
get=rest_read(
f"{singular}_list",
f"List {plural}",
f"The {resource}s that the user may read, by name.",
tag,
item,
many=True,
parameters=[],
)
),
f"{path}{{id}}/": OpenApiPathItem(
get=rest_read(
f"{singular}_retrieve",
f"{'An' if resource[0] in 'AEIOU' else 'A'} {resource}",
f"{'An' if resource[0] in 'AEIOU' else 'A'} {resource}, by its id or hashed id.",
tag,
item,
many=False,
parameters=[id_or_hashed_id(resource)],
)
),
}
def orchestrator_paths() -> dict[str, OpenApiPathItem]:
"""
The Orchestrators' REST reads, relative to the server's ``/api/v1``.
Only superusers may call them.
:returns: The paths, e.g. ``{"/orchestrators/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
return superuser_list_and_retrieve(
"Orchestrator", "/orchestrators/", TAG_ORCHESTRATORS, OrchestratorApiOrchestrator.__name__
)
def vectorsearch_paths() -> dict[str, OpenApiPathItem]:
"""
The Vectorsearches' REST reads, relative to the server's ``/api/v1``.
Only superusers may call them.
:returns: The paths, e.g. ``{"/vectorsearches/": ...}``.
:rtype: dict[str, OpenApiPathItem]
"""
return superuser_list_and_retrieve(
"Vectorsearch", "/vectorsearches/", TAG_VECTORSEARCHES, VectorsearchApiVectorsearch.__name__
)
[docs]
def merge_paths(*families: dict[str, OpenApiPathItem]) -> dict[str, OpenApiPathItem]:
"""
The paths of several families, with the operations of a path that two families share in one path item.
:param families: Each family's paths.
:type families: dict[str, OpenApiPathItem]
:returns: The paths.
:rtype: dict[str, OpenApiPathItem]
:raises ValueError: If two families define the same method of a path.
"""
paths: dict[str, OpenApiPathItem] = {}
for family in families:
for path, item in family.items():
merged = paths.get(path, OpenApiPathItem())
for method in ("get", "post", "delete"):
operation = getattr(item, method)
if operation is not None:
if getattr(merged, method) is not None:
raise ValueError(f"{method.upper()} {path} is defined twice.")
setattr(merged, method, operation)
paths[path] = merged
return paths
[docs]
def openapi_document() -> OpenApiDocument:
"""
The Smarter api's OpenAPI document.
:returns: The document.
:rtype: OpenApiDocument
"""
schemas, _ = cli_schemas()
return OpenApiDocument(
openapi=OPENAPI_VERSION,
info=OpenApiInfo(
title="Smarter api",
version=API_VERSION,
summary="Apply, get, describe, validate and delete the resources that SAM manifests declare, and chat "
"with LLMClients.",
description=(
"The api of the smarter cli and of the Smarter SDKs. Every response is journaled: its body "
"has the journal's api and metadata, beside the command's data or message, or an error. "
"The chat api's requests and responses are the Smarter Chat api contract's, version 2. "
"Generated from the platform's Pydantic models by smarter.apps.api.v1.contract.openapi; "
f"do not edit. See {SMARTER_PROJECT_DOCS_URL}."
),
contact=OpenApiContact(name="The Smarter Project", url=SMARTER_PROJECT_WEBSITE_URL),
license=OpenApiLicense(name="AGPL-3.0", url="https://www.gnu.org/licenses/agpl-3.0.html"),
),
jsonSchemaDialect=JSON_SCHEMA_DIALECT,
servers=[
OpenApiServer(
url="{platform}/api/v1",
description="A Smarter platform.",
variables={
"platform": OpenApiServerVariable(
default=DEFAULT_PLATFORM_URL, description="The platform's url, with its scheme."
)
},
),
OpenApiServer(url=f"{LOCAL_PLATFORM_URL}/api/v1", description="A local development platform."),
],
security=[{API_KEY_AUTH: []}],
tags=[
OpenApiTag(name=TAG_MANIFESTS, description="Resources, and the manifests that declare them."),
OpenApiTag(name=TAG_DEPLOYMENT, description="Deployable resources, e.g. LLMClients."),
OpenApiTag(name=TAG_PLATFORM, description="The platform, and the api key's user."),
OpenApiTag(name=TAG_CHAT, description="Chats with LLMClients."),
OpenApiTag(name=TAG_LLMCLIENTS, description="The LLMClients' REST reads."),
OpenApiTag(name=TAG_GUARDRAILS, description="The Guardrails' REST reads, and their dry run."),
OpenApiTag(name=TAG_SECRETS, description="The Secrets' REST reads: their metadata, never their values."),
OpenApiTag(
name=TAG_MCPCLIENTS, description="The MCPClients' REST reads, their reconnection, and tool calls."
),
OpenApiTag(
name=TAG_VECTORSTORES,
description="The vectorstores' REST reads, search, documents and maintenance. Served only when the "
"platform enables vectorstores (SMARTER_ENABLE_VECTORSTORE).",
),
OpenApiTag(name=TAG_LLMHOSTS, description="The LLMHosts' REST reads, model discovery, and deployment."),
OpenApiTag(name=TAG_PLUGINS, description="The plugins' REST reads."),
OpenApiTag(name=TAG_PROVIDERS, description="The LLM Providers' REST reads, and their models."),
OpenApiTag(
name=TAG_PROXIES,
description="The Proxies that the user may use. Served only when the platform enables Proxies "
"(SMARTER_ENABLE_PROXY).",
),
OpenApiTag(name=TAG_ACCOUNTS, description="The accounts, their users and contacts. For superusers."),
OpenApiTag(name=TAG_HISTORY, description="The chat sessions, and their tool calls and plugin usage."),
OpenApiTag(
name=TAG_JOURNAL,
description="The Smarter Journal: a record of every request to the cli api. For superusers.",
),
OpenApiTag(name=TAG_ORCHESTRATORS, description="The Orchestrators' REST reads. For superusers."),
OpenApiTag(name=TAG_VECTORSEARCHES, description="The Vectorsearches' REST reads. For superusers."),
],
paths=merge_paths(
cli_paths(),
chat_paths(),
llmclient_paths(),
guardrail_paths(),
secret_paths(),
mcpclient_paths(),
vectorstore_paths(),
llmhost_paths(),
plugin_paths(),
provider_paths(),
proxy_paths(),
account_paths(),
history_paths(),
journal_paths(),
orchestrator_paths(),
vectorsearch_paths(),
),
components=OpenApiComponents(
schemas=schemas,
securitySchemes={
API_KEY_AUTH: OpenApiSecurityScheme(
name="Authorization", description="An api key, as: Token <api key>."
)
},
),
)
[docs]
def openapi_json() -> str:
"""
The Smarter api's OpenAPI document, as the text of ``openapi.json``.
:returns: The document, as indented JSON, with a final newline.
:rtype: str
"""
document = openapi_document().model_dump(mode="json")
return json.dumps(document, indent=2, sort_keys=True) + "\n"
[docs]
def write_openapi(path: str = OPENAPI_PATH) -> str:
"""
Write the Smarter api's OpenAPI document.
:param path: The file to write. Defaults to ``openapi.json``, beside this module.
:type path: str
:returns: The path that was written.
:rtype: str
"""
with open(path, "w", encoding="utf-8") as file:
file.write(openapi_json())
return path
__all__ = [
"CHAT_MODELS",
"DOCUMENTED_URLCONFS",
"PATH_PARAMETER_ALIASES",
"UNDOCUMENTED_ROUTES",
"UNDOCUMENTED_URLCONFS",
"chat_paths",
"guardrail_paths",
"id_or_hashed_id",
"llmclient_paths",
"llmhost_paths",
"mcpclient_paths",
"plugin_paths",
"provider_paths",
"proxy_paths",
"query",
"rest_operation",
"vectorstore_paths",
"secret_paths",
"merge_paths",
"rest_read",
"cli_paths",
"cli_schemas",
"openapi_document",
"openapi_json",
"write_openapi",
]