artifactr.ui¶
Forms, dashboards and mini apps in the chat, behind the forms extra: uv add "artifactr-ai[forms]" (ADR-0074, the guide). relayr bridges what people send into reflexr in relayr.ui (relayr's reference).
Forms, dashboards and mini apps in the chat (lattice RFC-0005), behind the forms extra.
An agent asks a form with ask_form, which pauses its run until a person answers, or makes a
form artifact that takes submissions without a run waiting; it shows figures as a dashboard.
Whatever a person sends is one artifactr command, as that person, checked here before it
commits. Install artifactr-ai[forms] and import it: nothing else in artifactr imports it, so an
application that shows no forms registers none of its types (ADR-0074). The names most
applications need are here; the rest are in the modules:
artifactr.ui.schema: the form schema, a JSON Schema subset, its hints and its validator.artifactr.ui.types: the artifact types andask_form's request.artifactr.ui.ids: a submission's derived id.artifactr.ui.integrations: which integrations are on in a workspace.artifactr.ui.checks: the checks for artifactr'sWorkspaces(checks=...).artifactr.ui.agent: the agent's toolset.artifactr.ui.wire: the JSON Schema clients generate their types from.
relayr bridges what people send into reflexr as ui:form_submitted, in relayr.ui.
artifactr.ui.schema¶
The form schema: a JSON Schema 2020-12 subset, its hints and its validator (ADR-0076, ADR-0077).
The form schema: a JSON Schema 2020-12 subset, its UI hints, and its validator.
A form is a FormSchema, which models write as JSON Schema and Pydantic emits, beside
UiHints, which say how each property is shown and nothing else. The subset is an
allow-list: a keyword it doesn't hold is refused, never ignored, and the refusal names it.
- The root:
type: "object",title,description,propertiesandrequired;additionalPropertiesis false whether or not it is written. A property's name is a letter or_and up to 63 letters, digits,_or-, and never__proto__,constructororprototype. - A string:
minLength,maxLength(at mostMAX_LENGTH), aformatofdate,date-time,emailoruri(http,httpsormailto), and adefault. - Choices: an
enumof strings, or aoneOfof{const, title}for labelled choices, and adefault. - A number or an integer:
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOfand adefault. - A boolean: a
default. - An array, of choices only:
itemswith anenumor aoneOf,uniqueItems,minItems,maxItemsand adefault. - Every property:
titleanddescription.
Bounds that no value meets, such as a minLength over the maxLength, are refused, and
every number is within MAX_NUMBER either way.
A schema compiles to a Pydantic model once, kept by its JSON for the 256 schemas used last, and
FormSchema.validate_values checks a submission's values with it. No third-party validator
is involved: the formats and multipleOf are checked by this module's own small checks, which
the TypeScript side repeats (ADR-0077).
MAX_NUMBER
module-attribute
¶
MAX_NUMBER: Final = 2 ** 53 - 1
The largest number, either way, a form or a dashboard holds: JavaScript's
Number.MAX_SAFE_INTEGER, the largest past which two integers read alike, so portalr reads
each as it was written.
MAX_LENGTH
module-attribute
¶
MAX_LENGTH: Final = 10000
The longest string a form takes, in characters, whatever its maxLength says.
MAX_SCHEMA_BYTES
module-attribute
¶
MAX_SCHEMA_BYTES: Final = 32 * 1024
The largest form, its title, schema and hints as they are stored, in the UTF-8 bytes of their compact JSON (ADR-0077).
MAX_VALUES_BYTES
module-attribute
¶
MAX_VALUES_BYTES: Final = 64 * 1024
The largest submitted values, in the UTF-8 bytes of their compact JSON.
RESERVED_NAMES
module-attribute
¶
Property names refused although they fit the pattern: JavaScript objects treat them apart.
REFUSED_KEYWORDS
module-attribute
¶
REFUSED_KEYWORDS: Final = frozenset(
{
"$ref",
"$defs",
"pattern",
"if",
"then",
"else",
"allOf",
"anyOf",
"not",
}
)
The JSON Schema keywords a model is most likely to write that the subset refuses by name.
Every other keyword outside the subset is refused too, as an extra input at its place; a nested
object and format: "password" are refused with reasons of their own.
Format
module-attribute
¶
Format = Literal['date', 'date-time', 'email', 'uri']
The string formats the subset checks.
Widget
module-attribute
¶
Widget = Literal['textarea', 'select', 'checkbox']
How a property may ask to be shown in place of its default: a multi-line text field for a string, a select for choices that would be segments, and a checkbox for a boolean's switch.
Property
module-attribute
¶
Property = Annotated[
Annotated[StringProperty, Tag("string")]
| Annotated[ChoiceProperty, Tag("choice")]
| Annotated[NumberProperty, Tag("number")]
| Annotated[BooleanProperty, Tag("boolean")]
| Annotated[ArrayProperty, Tag("array")],
Discriminator(
_kind,
custom_error_type="property_type",
custom_error_message='Each property is a "string", "number", "integer", "boolean" or "array", or choices with an enum or a oneOf',
),
BeforeValidator(_not_nested),
]
One property of a form, told apart by its type, or as choices by enum or oneOf.
StringProperty
pydantic-model
¶
Bases: _Property
A string, on one line unless its hint asks for a text area.
Fields:
-
title(str | None) -
description(str | None) -
type(Literal['string']) -
min_length(Annotated[int, Field(ge=0)] | None) -
max_length(Annotated[int, Field(ge=0, le=MAX_LENGTH)] | None) -
format(Format | None) -
default(str | None)
Validators:
-
_refuse -
_default_is_a_value -
_met
value_type
¶
value_type() -> Any
A string of its lengths, at most MAX_LENGTH, in its format if it has one.
Choice
pydantic-model
¶
Choices
pydantic-model
¶
Bases: _Property
A choice among strings: an enum, or oneOf labelled choices; type is implied.
Fields:
-
title(str | None) -
description(str | None) -
type(Literal['string']) -
enum(Annotated[list[str], Field(min_length=1, max_length=MAX_CHOICES)] | None) -
one_of(Annotated[list[Choice], Field(min_length=1, max_length=MAX_CHOICES)] | None)
Validators:
-
_refuse -
_default_is_a_value -
_one_way
ChoiceProperty
pydantic-model
¶
Bases: Choices
A property that takes one of its choices: segments up to five, else a select.
Fields:
-
title(str | None) -
description(str | None) -
type(Literal['string']) -
enum(Annotated[list[str], Field(min_length=1, max_length=MAX_CHOICES)] | None) -
one_of(Annotated[list[Choice], Field(min_length=1, max_length=MAX_CHOICES)] | None) -
default(str | None)
Validators:
-
_refuse -
_default_is_a_value -
_one_way
NumberProperty
pydantic-model
¶
Bases: _Property
A number, or with type: "integer" a whole one.
Fields:
-
title(str | None) -
description(str | None) -
type(Literal['number', 'integer']) -
minimum(Number | None) -
maximum(Number | None) -
exclusive_minimum(Number | None) -
exclusive_maximum(Number | None) -
multiple_of(Annotated[Number, AfterValidator(_positive)] | None) -
default(Number | None)
Validators:
-
_refuse -
_default_is_a_value -
_met
BooleanProperty
pydantic-model
¶
ArrayProperty
pydantic-model
¶
Bases: _Property
Some of its items' choices, as checkboxes.
Fields:
-
title(str | None) -
description(str | None) -
type(Literal['array']) -
items(Choices) -
unique_items(bool | None) -
min_items(Annotated[int, Field(ge=0)] | None) -
max_items(Annotated[int, Field(ge=0)] | None) -
default(list[str] | None)
Validators:
-
_refuse -
_default_is_a_value -
_met
FormSchema
pydantic-model
¶
Bases: _Subset
A form: a JSON Schema 2020-12 object in the subset, compiled once to check values.
Validating one refuses, by name, every keyword outside the subset, a property the subset
doesn't hold, more than MAX_PROPERTIES properties, a name outside the pattern, a
required name that isn't a property, a default its property doesn't take, and bounds
no value meets. additionalProperties is false, written or not. A form with no
properties is a button. Its size counts towards its form's, MAX_SCHEMA_BYTES.
Fields:
-
type(Literal['object']) -
title(str | None) -
description(str | None) -
properties(dict[Annotated[str, AfterValidator(_named)], Property]) -
required(list[str]) -
additional_properties(Literal[False])
Validators:
-
_refuse -
_checked
validate_values
¶
Check a submission's values against the form, and return them as they were given.
Raises:
| Type | Description |
|---|---|
ValidationFailed
|
If they don't fit: each error's |
FieldHints
pydantic-model
¶
Section
pydantic-model
¶
UiHints
pydantic-model
¶
Bases: _Written
How a form is shown, and nothing else.
A hint names a component of the kit, or carries plain text: none carries a colour, a
class, HTML or a URL. fields hints each property by name, order and sections
arrange them, and submit_label names the form's button.
Fields:
-
fields(dict[str, FieldHints]) -
order(list[str] | None) -
sections(list[Section] | None) -
submit_label(str | None)
check
¶
check(schema: FormSchema) -> None
Check that every name is the schema's and every widget fits its property.
Raises:
| Type | Description |
|---|---|
ValueError
|
If a hint names a property the schema hasn't, or asks for a widget that doesn't fit its property. |
artifactr.ui.types¶
The form, submission, dashboard and app kinds, and ask_form's request.
The artifact types of forms and dashboards, and ask_form's request.
Register UI_TYPES with the application's own types, in Workspaces(types=...) and in
ArtifactWorkspace(types=...):
Form, kindform: aFormSchemawith its hints, and how many submissions it takes.Submission, kindsubmission: the values a person sent to a form, or to an app, against the version they saw. Its id is derived (submission_id).Dashboard, kinddashboard: a snapshot of figures in panels, trueas_ofa moment, which the agent writes.App, kindapp: a mini app's HTML and its state, which an agent writes through its own tools, and which runs sandboxed, with no network, where mini apps are on.
Their data's JSON Schema, with ask_form's request's, is artifactr.ui.wire's.
MAX_SERIES
module-attribute
¶
MAX_SERIES: Final = 3
The most series a bar or line chart holds: as many as portalr's charts have inks for.
MAX_DASHBOARD_BYTES
module-attribute
¶
MAX_DASHBOARD_BYTES: Final = 256 * 1024
The largest dashboard, in the UTF-8 bytes of its compact JSON: every version is in the log, and relayr bridges each.
MAX_APP_HTML_BYTES
module-attribute
¶
MAX_APP_HTML_BYTES: Final = 256 * 1024
The largest app's HTML, in UTF-8 bytes.
MAX_APP_STATE_BYTES
module-attribute
¶
MAX_APP_STATE_BYTES: Final = 64 * 1024
The largest app's state, in the UTF-8 bytes of its compact JSON.
MAX_TITLE
module-attribute
¶
MAX_TITLE: Final = 200
The longest title of a form, a dashboard or an app, in characters.
MAX_SUMMARY
module-attribute
¶
MAX_SUMMARY: Final = 2000
The longest summary of an app, its text in place of what it runs, in characters.
ASK_FORM
module-attribute
¶
ASK_FORM: Final = 'ask_form'
The tool name of a paused request that asks a form, which portalr renders as one.
Submissions
module-attribute
¶
Submissions = Literal['once', 'per_person', 'many']
How many submissions a form takes: one in all, one a person, or any number.
Panel
module-attribute
¶
Panel = Annotated[
StatPanel
| ChartPanel
| DonutPanel
| TablePanel
| TextPanel
| FormPanel,
Field(discriminator="type"),
]
One panel of a dashboard, told apart by its type.
UI_TYPES
module-attribute
¶
The forms layer's artifact types, for Workspaces(types=...).
AGENT_TYPES
module-attribute
¶
The types an agent makes and changes with ArtifactWorkspace's generic tools, for its
types=...: not a submission, which is a person's, nor an app, which create_app and
update_app make and change, while mini apps are on.
FormRequest
pydantic-model
¶
Bases: BaseModel
What ask_form asks: the arguments its paused request carries, as portalr reads them.
The request's args hold these as the model called the tool; the tool checked them before
the run paused, and the answer is checked against schema.
Config:
frozen:Trueextra:forbidserialize_by_alias:True
Fields:
-
title(_Title) -
form_schema(FormSchema) -
ui(UiHints | None)
Validators:
-
_fits
Form
pydantic-model
¶
Bases: Artifact
A form that takes submissions without a run waiting.
It is for a poll, a sign-up, or a form that outlives its run. A submission names the version
it was made against; an edit of the form makes a new one, so a submission made against an
earlier one is refused as changed. Only its author, or whom the application's is_admin
allows, changes its schema or closed.
Config:
serialize_by_alias:True
Fields:
-
title(_Title) -
form_schema(FormSchema) -
ui(UiHints | None) -
submissions(Submissions) -
closed(bool)
Validators:
-
_fits
Submission
pydantic-model
¶
Bases: Artifact
The values a person sent to a form, or to an app, against the version they saw.
It answers one of the two, form_id or app_id. A many form's and an app's carry
the nonce their id is derived from. People never edit one.
Fields:
-
form_id(ArtifactId | None) -
app_id(ArtifactId | None) -
version(int) -
values(dict[str, JsonValue]) -
nonce(Annotated[str, Field(min_length=1)] | None)
Validators:
-
_answers_one
Key
pydantic-model
¶
Part
pydantic-model
¶
StatPanel
pydantic-model
¶
ChartPanel
pydantic-model
¶
DonutPanel
pydantic-model
¶
TablePanel
pydantic-model
¶
TextPanel
pydantic-model
¶
FormPanel
pydantic-model
¶
Bases: _Panel
A form artifact, by its id, shown to submit in place: a form with no fields is a button.
Fields:
-
type(Literal['form']) -
form_id(ArtifactId)
Dashboard
pydantic-model
¶
Bases: Artifact
A snapshot of figures in panels, which the agent writes, true as_of a moment.
Each update is a new version; a dashboard is at most MAX_DASHBOARD_BYTES.
Fields:
Validators:
-
_fits
App
pydantic-model
¶
Bases: Artifact
A mini app: one HTML file an agent wrote, its text fallback, and its state.
portalr runs it sandboxed, on an origin of its own, with no network, while mini apps are on
in the workspace; its summary is shown beside it, and in its place where it doesn't run.
An agent writes it, and its HTML names nothing to load, which the agent's tools and the
checks hold; a person changes only its state, through the app.
Fields:
Validators:
-
_fits
artifactr.ui.ids¶
A submission's derived id.
A submission's id, derived from what it answers, so a form's policy is artifactr's refusal.
artifactr refuses to create an artifact whose id exists, so a derived id makes a retried submit
one artifact, and a form that takes one submission, or one a person, takes no second
(ADR-0045's pattern, and relayr ADR-0019's). The check in
artifactr.ui.checks recomputes it and refuses a submission whose id isn't its derivation.
portalr derives the same ids, and its tests read the vectors in
tests/vectors/submission_ids.json.
submission_id
¶
Derive a submission's id: sub_ and 32 hex digits of a SHA-256 digest.
The digest is of the parts joined by a NUL, which no id holds, since artifactr refuses one:
the policy and the form's id, once, for a form that takes one submission; with the
person's principal, acting_for(actor).participant, per_person, for one a person; or
with the nonce the client makes once per submit and keeps in the submission, many. The
policy is a part, so a form whose policy changes takes submissions afresh, and no nonce
passes for a principal. A submission to an app is derived as one to a many form, from
the app's id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
form_id
|
str
|
The form's id, or the app's. |
required |
principal
|
str | None
|
The submitter's principal, for a |
None
|
nonce
|
str | None
|
The submit's nonce, for a |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If both a principal and a nonce are given. |
artifactr.ui.integrations¶
Which integrations are on in a workspace (ADR-0080).
Which of the forms' integrations are on in a workspace, on lattice's shared port (ADR-0080).
Forms, dashboards and mini apps are lattice's integrations ui:forms, ui:dashboards and
ui:mini-apps, declared in lattice-core's KNOWN, the last
off by default. An application asks lattice-core's Integrations port about them, as it asks
about every integration (lattice ADR-0027): grantr's store, or Defaults without one.
- The artifacts are gated by artifactr's gate (ADR-0081): give
Workspaces(gate=integration_gate(integrations, ui_key_of)), and making or changing a form, a submission, a dashboard or an app while its integration is off is refused asnot_enabled. - The agent's tools are offered only while their integration is on:
ui_tools'sintegrations. - An answer to
ask_form, which changes no artifact the gate could see, is refused byui_checks'sintegrationswhile forms are off, asnot_enabledtoo.
FORMS
module-attribute
¶
FORMS: Final = 'ui:forms'
Forms: ask_form, create_form, and the submissions and answers people send.
DASHBOARDS
module-attribute
¶
DASHBOARDS: Final = 'ui:dashboards'
Dashboards: show_dashboard and update_dashboard, and the dashboards they write.
MINI_APPS
module-attribute
¶
MINI_APPS: Final = 'ui:mini-apps'
Mini apps, RFC-0005's second phase, off by default: apps, and the submissions sent to them.
IntegrationKey
module-attribute
¶
IntegrationKey = Literal[
"ui:forms", "ui:dashboards", "ui:mini-apps"
]
The keys of forms, dashboards and mini apps.
ui_key_of
¶
The integration that governs an artifact, for artifactr's integration_gate.
A form and a submission to one are forms', a dashboard is dashboards', and an app and a submission to one are mini apps'. Any other kind is none of these.
artifactr.ui.checks¶
The checks for Workspaces(checks=...) (ADR-0075).
The checks that refuse what doesn't fit a form, before it commits (ADR-0075).
Give ui_checks to artifactr's Workspaces(checks=...). artifactr records an answer
or a new artifact as given, and may not import a schema validator; its check hook runs these
before each command of their type commits, with the committing handle, so each reads the
workspace as the person who sent the command:
answer_deferredto anask_formrequest: the answer is the form's values, valid against the schema in the request'sargs.create_artifactof asubmission: its form exists and the submitter may read it, it isn't closed or archived (FormClosed), the version it was made against is the form's head (FormChanged), its values are valid, and its id is its derivation. A submission to an app is checked the same way, but an app has no schema to check values against. Anappis made by an agent alone, and its HTML loads nothing.edit_artifact: a submission is never edited; a form'sschema,submissionsandclosedare changed only by its author or whomis_adminallows; and a person changes only an app'sstate, so the HTML that runs is always an agent's, and loads nothing.archive_artifact: a form, or a submission, is archived only by its author or whomis_adminallows.propose_change: the change it proposes is checked as the change itself, and a submission is never proposed: a person sends it.respond_to_proposal: an accepted change is checked again, as its proposer, against what is current, since artifactr applies it to the head; a reviewer's ownchangesaren't taken on a form, an app or a submission.
While forms are off in the workspace, an answer to ask_form is refused as not_enabled.
Making or changing the forms' artifacts while their integration is off is artifactr's gate's to
refuse (artifactr.ui.integrations).
A check reads before the command commits and holds nothing: a form closed in between takes one late submission. A check leaves to artifactr what artifactr refuses itself, such as a second answer, a stale edit or an id in use.
RefusalReason
module-attribute
¶
RefusalReason = Literal['changed', 'closed']
The reason in the payload of artifactr.ui's invalid_state refusals: FormChanged
and FormClosed. An invalid_state with none, to an answer, is artifactr's: the request
is answered already.
IsAdmin
module-attribute
¶
Whether the workspace's actor may govern any form, and archive any submission, as their authors may. The application answers it, from grantr's roles or its own: artifactr can't import them (ADR-0075).
FormChanged
¶
Bases: InvalidState
A submission was made against a version of its form, or its app, that is not its head.
Its payload's reason is "changed", with the target's artifact_id, the version
the submission named and the head, so a client shows "This form changed" without reading
the message.
FormClosed
¶
Bases: InvalidState
A submission was sent to a form that is closed, or to a form or an app that is archived.
Its payload's reason is "closed", with the target's artifact_id.
ui_checks
¶
ui_checks(
*,
is_admin: IsAdmin | None = None,
integrations: Integrations | None = None,
) -> Mapping[str, Check]
Return artifactr.ui's checks, by command type, for Workspaces(checks=...).
Merge them with the application's own, if it has any for the same command types, by calling both in one check of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
is_admin
|
IsAdmin | None
|
Whether an actor may govern any form and archive any submission; only their authors may if omitted. |
None
|
integrations
|
Integrations | None
|
Whether forms are on in a workspace, asked before an answer to
|
None
|
artifactr.ui.agent¶
The agent's toolset (ADR-0078, ADR-0082).
The agent's toolset: asking forms, making them, showing dashboards, and making mini apps.
Register ui_tools on the agent as an application toolset (ADR-0017), beside
the ArtifactWorkspace capability with AGENT_TYPES among its
types, the kinds an agent makes and changes itself::
agent = Agent(
model,
deps_type=Session[None],
output_type=[str, DeferredToolRequests],
toolsets=[ui_tools()],
capabilities=[ArtifactWorkspace(types=[*app_types, *AGENT_TYPES])],
)
Each tool checks what the model wrote before anything is recorded, and gives a refusal back to the model as a retry that names every field and keyword it refuses. Each is offered only while its integration is on in the run's workspace: mini apps', off by default, among them (ADR-0082).
ui_tools
¶
ui_tools(
*,
forms: bool = True,
dashboards: bool = True,
apps: bool = True,
integrations: Integrations | None = None,
) -> FunctionToolset[Session[Any]]
Build the toolset: forms' tools, dashboards' tools, mini apps' tools, or any of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forms
|
bool
|
Include |
True
|
dashboards
|
bool
|
Include |
True
|
apps
|
bool
|
Include |
True
|
integrations
|
Integrations | None
|
Whether an integration is on in the run's workspace, asked before each
model request: one that is off has its tools left out of the request, descriptions
and all. Each integration's default, lattice-core's |
None
|
artifactr.ui.wire¶
The wire schema, committed as schemas/artifactr_ui.v1.json.
The wire schema: the JSON Schema of artifactr.ui's kinds, ask_form's request and refusals.
The schema is checked in at schemas/artifactr_ui.v1.json; a test fails if it drifts from the
models. portalr generates its TypeScript types from it. Regenerate it with
moon run artifactr:schema, or::
python -m artifactr.ui.wire > schemas/artifactr_ui.v1.json
No other module imports this one, so running it registers the artifact types once.