# `Tayori.AI`
[🔗](https://github.com/hattoriai/tayori/blob/v0.1.0/lib/tayori/ai.ex#L1)

Writing the news with AI, with the key of the app: a draft from what
shipped, a translation into another language, and changes to a
passage (rewrite, shorter, warmer, fix the spelling).

The admin chooses the model and gives the key in its Settings: any model
that [ReqLLM](https://hex.pm/packages/req_llm) knows, such as
`anthropic:claude-sonnet-5-5`, or a model of OpenAI or Google. The key is encrypted
by the app's Cloak vault (`:vault`), else with the `:secret_key_base` of
Tayori's config, kept in the app's database, and never shown again. The app needs the dependency:

    {:req_llm, "~> 1.6"}

The text streams into the editor as a suggestion, which the admin
accepts or rejects (Kotoba's suggestions): AI never publishes, and
never changes an entry by itself.

The admin can also say what the product is and how it speaks
("About your product"), which every request carries.

Outside the editor, `complete/2` waits for the whole text: a digest
writes its entry from the inbox's changes with it (`Tayori.Digest`),
a draft unless the `:digest` config says to publish it.

# `stream`

```elixir
@callback stream(
  model :: String.t(),
  key :: String.t(),
  system :: String.t(),
  prompt :: String.t()
) ::
  {:ok, Enumerable.t()} | {:error, term()}
```

The adapter that calls the model: `stream(model, key, system, prompt)`.

# `actions`

```elixir
@spec actions() :: [{String.t(), String.t()}]
```

The actions on a passage, `[{id, label}]`, for the editor's Assist menu.

# `available?`

```elixir
@spec available?() :: boolean()
```

Whether the app can use AI: it has ReqLLM, and Tayori can keep a key.

# `check`

```elixir
@spec check() :: :ok | {:error, term()}
```

Checks the model and the key with a request of a few words. Returns
`:ok` or `{:error, reason}`.

# `complete`

```elixir
@spec complete(tuple(), keyword()) :: {:ok, String.t()} | {:error, term()}
```

The whole text of a task (see `stream/1`), for a request that no one
watches as it streams. Options: `:timeout`, in milliseconds (two
minutes). Returns `{:ok, text}`, or `{:error, reason}`: `:timeout` when
the model takes longer.

# `configured?`

```elixir
@spec configured?() :: boolean()
```

Whether the admin has given a model and a key.

# `parse_header`

```elixir
@spec parse_header(String.t()) :: {:ok, map(), String.t()} | :more
```

Reads the head of a draft or a translation, `TITLE:` and `SUMMARY:`
lines and a `---` line, from the text streamed so far.

## Examples

    iex> Tayori.AI.parse_header("TITLE: Exports\nSUMMARY: In two clicks.\n---\nEvery project")
    {:ok, %{title: "Exports", summary: "In two clicks."}, "Every project"}

    iex> Tayori.AI.parse_header("TITLE: Exp")
    :more

# `save_settings`

```elixir
@spec save_settings(map(), String.t() | :keep | nil) :: :ok
```

Saves the model and what the product is; the key is a string to keep,
`:keep`, or nil to remove it.

# `settings`

```elixir
@spec settings() :: %{
  model: String.t() | nil,
  about: String.t() | nil,
  key?: boolean()
}
```

The settings, without the key: `%{model: ..., about: ..., key?: bool}`.

# `stream`

```elixir
@spec stream(tuple()) :: {:ok, Enumerable.t()} | {:error, term()}
```

Streams the text of a task, as a stream of strings:

  * `{:draft, notes, locale}` - an entry from notes of what shipped (a
    list of changes, commits, pull requests), with a title and a
    summary first (see `parse_header/1`);
  * `{:digest, changes, locale}` - an entry from changes of the inbox
    (`Tayori.Change`), in the same form;
  * `{:translate, %{title:, summary:, markdown:}, from, to}` - an entry
    in another language, in the same form;
  * `{:assist, action, text, locale}` - a passage changed by an action of
    `actions/0`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
