| 1 |
|
defmodule Llm do |
| 2 |
|
import Application, only: [get_env: 2] |
| 3 |
|
import Compose, only: [compose: 1] |
| 4 |
|
import Snippet, only: [snip: 2] |
| 5 |
|
import Tools, only: [tools: 2] |
| 6 |
|
import Enum, only: [map: 2] |
| 7 |
|
import Map, only: [put: 3, delete: 2] |
| 8 |
|
import List, only: [pop_at: 2] |
| 9 |
|
import Req, only: [post: 2] |
| 10 |
|
import Tape, only: [handle: 4] |
| 11 |
|
@cache_key %{type: "ephemeral"} |
| 12 |
189 |
def llm(%{config: config, history: history, name: agent_name} = state) do |
| 13 |
189 |
composed = compose(config) |
| 14 |
189 |
body = build(composed, history, state) |
| 15 |
189 |
result = tape(body, agent_name, state) |
| 16 |
|
{parts(result), state} |
| 17 |
|
end |
| 18 |
|
|
| 19 |
|
defp tape(body, name, state) do |
| 20 |
189 |
payload = [tape: state[:tape]] |
| 21 |
189 |
handle(body, name, fn -> req(body) |> resp end, payload) |
| 22 |
|
end |
| 23 |
|
|
| 24 |
:-( |
defp req(body), do: post(url(), payload(body)) |
| 25 |
|
|
| 26 |
:-( |
defp payload(body), do: [json: body] ++ settings() |
| 27 |
:-( |
defp settings, do: [headers: headers(), connect_options: connect(), receive_timeout: 120_000] |
| 28 |
|
|
| 29 |
|
defp build(composed, history, state) do |
| 30 |
189 |
base(composed, history, state) |> equip(tools(composed, state)) |
| 31 |
|
end |
| 32 |
|
|
| 33 |
|
defp base(composed, history, %{name: agent_name}) do |
| 34 |
189 |
text = snip(composed.content, composed[:import]) <> " Your name is #{agent_name}." |
| 35 |
189 |
decorate(%{model: model(), max_tokens: 4096}, text, history) |
| 36 |
|
end |
| 37 |
|
|
| 38 |
|
defp decorate(base, text, history) do |
| 39 |
|
base |
| 40 |
|
|> put(:system, [%{type: "text", text: text, cache_control: @cache_key}]) |
| 41 |
189 |
|> put(:messages, history) |
| 42 |
|
end |
| 43 |
|
|
| 44 |
|
defp equip(base, [%{function_declarations: defs}]) do |
| 45 |
138 |
tools = map(defs, &schema/1) |
| 46 |
138 |
put(base, :tools, cache(tools)) |
| 47 |
|
end |
| 48 |
|
|
| 49 |
51 |
defp equip(base, _), do: base |
| 50 |
|
|
| 51 |
|
defp cache(tools) do |
| 52 |
138 |
{last, init} = pop_at(tools, -1) |
| 53 |
138 |
mark(last, init, tools) |
| 54 |
|
end |
| 55 |
|
|
| 56 |
:-( |
defp mark(nil, _init, tools), do: tools |
| 57 |
138 |
defp mark(last, init, _tools), do: init ++ [put(last, :cache_control, @cache_key)] |
| 58 |
|
|
| 59 |
|
defp schema(%{parameters: params} = tool), |
| 60 |
398 |
do: tool |> delete(:parameters) |> put(:input_schema, params) |
| 61 |
|
|
| 62 |
|
defp schema(tool), |
| 63 |
8 |
do: put(tool, :input_schema, %{type: "object"}) |
| 64 |
|
|
| 65 |
189 |
defp parts(list) when is_list(list), do: map(list, &part/1) |
| 66 |
:-( |
defp parts({:error, _} = err), do: err |
| 67 |
146 |
defp part(%{"type" => "text", "text" => text}), do: %{"text" => text} |
| 68 |
|
|
| 69 |
|
defp part(%{"type" => "tool_use", "id" => id, "name" => name, "input" => input}), |
| 70 |
129 |
do: %{"tool_use" => %{"id" => id, "name" => name, "input" => input}} |
| 71 |
|
|
| 72 |
:-( |
defp part(other), do: other |
| 73 |
|
|
| 74 |
:-( |
defp url, do: "#{get_env(:elita, :base_url)}/v1/messages" |
| 75 |
189 |
defp model, do: "claude-haiku-4-5" |
| 76 |
|
|
| 77 |
:-( |
defp headers, do: [{"x-api-key", token()}, {"anthropic-version", "2023-06-01"}] |
| 78 |
|
|
| 79 |
:-( |
defp connect, do: ssl(get_env(:elita, :ca_certs)) |
| 80 |
:-( |
defp ssl(nil), do: [] |
| 81 |
:-( |
defp ssl(path), do: [transport_opts: [cacertfile: path]] |
| 82 |
|
|
| 83 |
:-( |
defp token, do: get_env(:elita, :auth_token) |
| 84 |
|
|
| 85 |
:-( |
defp resp({:ok, %{status: 200, body: %{"content" => content}}}), do: content |
| 86 |
:-( |
defp resp({:ok, %{status: code, body: body}}), do: {:error, "HTTP #{code}: #{inspect(body)}"} |
| 87 |
:-( |
defp resp({:error, err}), do: {:error, "request failed: #{inspect(err)}"} |
| 88 |
|
end |