cover/Elixir.Llm.html

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
Line Hits Source