Skip to content

8. Artificial intelligence

Yes, your module can call a language model. No, it cannot call a provider directly.

from kewos.ai import AiMessage, AiRequest, complete

The distinction is not a formality: everything that makes an AI call acceptable in a business lives in the facade, and you inherit it only by going through it.

What the facade does for you

ActivationIf the customer has not switched AI on, the call is refused. No way around it.
The allowlistOnly the providers the customer authorised are reachable.
Data residencyA provider outside the European Union requires the customer’s explicit consent. Without it, the call is refused.
The quotaA budget of calls per time window, per customer.
The auditEvery call is traced.
The cacheAsking the same question twice is not paid for twice.
Model choiceThe customer picks their provider; your code names none.

A module calling a provider’s API directly would lose all seven. That is why it is forbidden, not because we like rules.

A concrete case: suggesting a genre

The librarian pastes the publisher’s blurb. The module proposes a genre and a catalogue sentence; they accept or refuse. Nothing is written first.

_SYSTEM = (
"You classify library books. Answer with a JSON object holding exactly two "
"keys: 'genre', which MUST be one of the allowed values given to you, and "
"'summary', a single sentence of at most 25 words. No other key, no prose."
)
def suggest_for_title(engine, *, title, author, blurb, allowed_genres, secrets):
"""Propose a genre and a one-line summary, or None."""
request = AiRequest(
messages=(AiMessage(role="user", content=_prompt(title, author, blurb, allowed_genres)),),
system=_SYSTEM,
response_format="json", # constrain the reply to a JSON object
max_tokens=300,
)
try:
response = complete(engine, request, secrets=secrets)
except (AiDisabledError, AiQuotaExceededError, AiResidencyError):
return None # legitimate refusals: degrade, never break
return parse_suggestion(response.text, allowed=allowed_genres)

The validation is the real work

Constraining the reply is not enough: it has to be checked against the model’s own list.

def parse_suggestion(text: str, *, allowed: Sequence[str]) -> Suggestion | None:
"""Read the model's answer, or return None when it is unusable."""
try:
payload = json.loads(text)
except (ValueError, TypeError):
return None
if not isinstance(payload, dict):
return None
genre, summary = payload.get("genre"), payload.get("summary")
if not isinstance(genre, str) or not isinstance(summary, str):
return None
# The list comes from the model's own selection field, so a value outside it
# could never be stored anyway. Refused HERE, where it can still be explained.
if genre not in allowed:
return None
if len(summary.split()) > MAX_SUMMARY_WORDS:
return None
return Suggestion(genre=genre, summary=summary.strip())

What it gives, case by case

Run with a stand-in model that answers whatever it is told:

a valid answer -> Suggestion(genre='fiction', summary='A desert planet, a rare
spice, and the fall and rise of a noble house.')
an invented genre -> None
a chatty answer -> None
half an answer -> None
AI switched off -> None

Four None out of five, and that is the point. A model cannot invent a category: a genre outside the list is dropped, not corrected, because a plausible wrong value is worse than an absent one. And half an answer is refused whole, because half a suggestion shown to somebody is a suggestion they accept without seeing the missing half.

Where to make this call

Not inside an HTTP request. A model call takes seconds and goes out over the network. Put it in a background job.

from kewos.jobs import job
@job(queue="default")
def summarise_title(title_id: int) -> None:
"""Fill in the summary of one catalogue entry, out of band."""
...

The natural trigger is a module event, or a scheduled job processing a batch.

A model that sees

For an image, a dedicated block replaces the text:

from kewos.ai.provider import AiImageBlock, AiTextBlock
message = AiMessage(
role="user",
content=(
AiImageBlock(media_type="image/png", data=base64_image),
AiTextBlock(text="Which book cover is this? Give the title and author."),
),
)

To transcribe a document rather than comment on it, do not do it yourself: go through kewos/ocr, which handles engine choice and fallback.

Having a model write data

A language model must never write straight into your database. The shape is: propose, then confirm.

  1. The model produces a structured proposal, constrained by a schema.
  2. You validate it like any user input.
  3. You show it to the user.
  4. The user confirms, and your code does the writing.

An unconfirmed proposal has had no side effect. That is what makes the feature recoverable the day the model is wrong, and it will be.

What does not exist yet

Declaring a prompt in a view or a YAML action. The declarative format knows only two operations today: setting a value and emitting an event. A YAML module therefore cannot call a model without going through code.

It is a known limit, and an architecture decision is being written to lift it. In the meantime, a YAML module needing AI comes with a small code module exposing the call.

Next

Reading a document.