assistant.ry

module assistant
  purpose: Send one question to a chat model over HTTP and print its answer.

import std.console
import std.environment
import std.http exposing Url, HttpError
import std.json exposing JsonError

type Message
  has role: Text
  has content: Text
  can ToJson
  can FromJson
end

type ChatRequest
  has model: Text
  has messages: List of Message
  can ToJson
end

type Choice
  has message: Message
  can FromJson
end

type ChatResponse
  has choices: List of Choice
  can FromJson
end

public type AssistantError is one of
  purpose: Why no answer could be shown.
  MissingApiKey
  NoAnswer
  Transport(detail: Text)
end

let endpoint: Url be Url("https://api.example.com/v1/chat/completions")

public function ask(question: Text, api_key: Text)
  returns Text
  or fails with HttpError or JsonError or AssistantError
  needs network.http
  purpose: Ask the model one question and return the text of its first answer.
  tags: llm, chat, http

  let messages be [Message(role: "user", content: question)]
  let request be ChatRequest(model: "example-model", messages: messages)
  let headers be {"Authorization": "Bearer {api_key}", "Content-Type": "application/json"}
  let response be http.post(
    url: endpoint,
    body: json.render(request),
    headers: headers
  ) otherwise fail
  let parsed: ChatResponse be json.parse(response.body) otherwise fail
  let first_choice be parsed.choices.first() otherwise fail with NoAnswer
  return first_choice.message.content
end

public function main() or fails with AssistantError needs console, environment, network.http
  purpose: Read the question from the command line and the API key from the environment.

  let api_key be environment.get("CHAT_API_KEY") otherwise fail with MissingApiKey
  let question be environment.arguments().join(" ")
  let answer be
    match ask(question: question, api_key: api_key)
      when success(text) then text
      when failure(error: AssistantError) then fail with error
      when failure(error) then fail with Transport(detail: error.to_text())
    end
  console.print(answer)
end

test "the first answer is returned" needs network.http replays "fixtures/assistant.json"
  let answer be ask(question: "What is Renyi?", api_key: "example-key") otherwise fail
  check answer is "A programming language that reads as English."
end