lib/mistral_client.ex

defmodule MistralClient do
  @moduledoc """
  Provides API wrappers for Mistral API
  See https://docs.mistral.ai/api for further info on REST endpoints
  """

  use Application

  alias MistralClient.Config
  alias MistralClient.Models
  alias MistralClient.Chat
  alias MistralClient.Embeddings
  
  def start(_type, _args) do
    children = [Config]
    opts = [strategy: :one_for_one, name: MistralClient.Supervisor]

    Supervisor.start_link(children, opts)
  end  

  @doc """
  Retrieve the list of available models
  ## Example request
  ```elixir
  MistralClient.models()
  ```
  
  ## Example response
  ```elixir
  {:ok,
    %{
      data: [
        %{
          "created" => 1702997889,
          "id" => "mistral-medium-latest",
          "object" => "model",
          "owned_by" => "mistralai",
          "parent" => nil,
          "permission" => [
            %{
              "allow_create_engine" => false,
              "allow_fine_tuning" => false,
              "allow_logprobs" => false,
              ....
             }
          ],
          "root" => nil
        }
      ]
    }
  }
  ```
  
  See: https://docs.mistral.ai/api#operation/listModels
  """

  def models(), do: Models.fetch()

  @doc """
  Creates a completion for the chat message
  
  ## Example request
  ```elixir
  MistralClient.chat(
    "model": "open-mistral-7b",
    "messages": [
      %{
        "role": "user",
        "content": "What is the best French cheese?"
      }
    ]
  )
  ```
  
  ## Example response
  ```elixir
  {:ok,
    %{
      choices: [
        %{
          "finish_reason" => "stop",
	  "index" => 0,
	  "message" => %{
            "content" => "It's subjective to determine the 'best' French cheese as it depends on personal preferences. Here are some popular and highly regarded French cheeses in various categories:\n\n1. Soft and Bloomy Rind: Brie de Meaux or Brie de Melun, Camembert de Normandie\n2. Hard and Cooked: Comté, Gruyère\n3. Hard and Uncooked: Cheddar-like: Comté, Beaufort, Appenzeller-style: Vacherin Fribourgeois, Alpine-style: Reblochon\n4. Blue Cheese: Roquefort, Fourme d'Ambert\n5. Goat Cheese: Chavignol, Crottin de Chavignol, Sainte-Maure de Touraine\n\nHowever, I would recommend trying a variety of French cheeses to discover your favorite. It's an enjoyable and delicious experience!",
            "role" => "assistant"
          }
        }
     ],
     created: 1702997889,
     id: "cmpl-83f575cf654b4a83b99d342f644db292",
     model: "open-mistral-7b",
     object: "chat.completion",
     usage: %{
       "completion_tokens" => 204,
       "prompt_tokens" => 15,
       "total_tokens" => 219
     }
   }
  }
  ```
  
  N.B. to use "stream" mode you must be set http_options as below when you want to treat the chat completion as a stream. You may also pass in the api_key in the same way, or define in the config.exs of your elixir project.
  
  ## Example request (stream)
  ```elixir
  MistralClient.chat(
    [
      model: "open-mistral-7b",
      messages: [
        %{role: "user", content: "What is the best French cheese?"}
      ],
      stream: true
    ],
    MistralClient.config(http_options: %{stream_to: self(), async: :once})
  )
  |> Stream.each(fn res ->
    IO.inspect(res)
  end)
  |> Stream.run()
  ```
  
  ## Example response (stream)
  ```elixir
  %{
    "choices" => [
      %{"delta" => %{"role" => "assistant"}, "finish_reason" => nil, "index" => 0}
    ],
    "id" => "cmpl-9d2c56da16394e009cafbbde9cb5d725",
    "model" => "open-mistral-7b"
  }
  %{
    "choices" => [
      %{
        "delta" => %{
          "content" => "It's subjective to determine the 'best'",
          "role" => nil
        },
        "finish_reason" => nil,
        "index" => 0
      }
    ],
    "created" => 1702999980,
    "id" => "cmpl-9d2c56da16394e009cafbbde9cb5d725",
    "model" => "open-mistral-7b",
    "object" => "chat.completion.chunk"
  }
  %{
    "choices" => [
      %{
        "delta" => %{
          "content" => " French cheese as it largely depends on personal preferences. Here are a",
          "role" => nil
        },
        "finish_reason" => nil,
        "index" => 0
      }
    ],
    "created" => 1702999980,
    "id" => "cmpl-9d2c56da16394e009cafbbde9cb5d725",
    "model" => "open-mistral-7b",
    "object" => "chat.completion.chunk"
  }
  ```
  
  See: https://docs.mistral.ai/api#operation/createChatCompletion for the complete list of parameters you can pass to the chat function
  """

  def chat(params, config \\ Config.config(%{})) do
    Chat.fetch(params, config)
  end

@doc """
  Creates an embedding vector representing the input text.
  
  ## Example request
  ```elixir
  MistralClient.embeddings(
    model: "mistral-embed",
    input: [
      "Embed this sentence.",
      "As well as this one."
    ]
  )
  ```
  
  ## Example response
  ```elixir
  {:ok,
   %{
    data: [
     %{
       "embedding" => [-0.0165863037109375, 0.07012939453125, 0.031494140625,
        0.013092041015625, 0.020416259765625, 0.00977325439453125,
        0.0256195068359375, 0.0021114349365234375, -0.00867462158203125,
        -0.00876617431640625, -0.039520263671875, 0.058441162109375,
        -0.025390625, 0.00748443603515625, -0.0290679931640625,
        0.040557861328125, 0.05474853515625, 0.0258636474609375,
        0.031890869140625, 0.0230255126953125, -0.056427001953125,
        -0.01617431640625, -0.061248779296875, 0.012115478515625,
        -0.045745849609375, -0.0269622802734375, -0.0079498291015625,
        -0.03778076171875, -0.040008544921875, 8.23974609375e-4,
        0.0242767333984375, -0.02996826171875, 0.0305023193359375,
        -0.0022830963134765625, -0.012237548828125, -0.036163330078125,
        -0.033172607421875, -0.044891357421875, 0.01326751708984375,
        0.0021228790283203125, 0.00978851318359375, -2.1147727966308594e-4,
        -0.0305633544921875, -0.0230865478515625, -0.024932861328125, ...],
       "index" => 0,
       "object" => "embedding"
     },
     %{
       "embedding" => [-0.0234222412109375, 0.039337158203125,
        0.052398681640625, -0.0183868408203125, 0.03399658203125,
        0.003879547119140625, 0.024688720703125, -5.402565002441406e-4,
        -0.0119171142578125, -0.006988525390625, -0.0136260986328125,
        0.041839599609375, -0.0274810791015625, -0.015411376953125,
        -0.041412353515625, 0.0305328369140625, 0.006023406982421875,
        0.001140594482421875, -0.007167816162109375, 0.01085662841796875,
        -0.03668212890625, -0.033111572265625, -0.044586181640625,
        0.020538330078125, -0.0423583984375, -0.03131103515625,
        -0.0119781494140625, -0.048736572265625, -0.0850830078125,
        0.0203857421875, -0.0023899078369140625, -0.0249176025390625,
        0.019500732421875, 0.007068634033203125, 0.0301055908203125,
        -0.041534423828125, -0.0255584716796875, -0.0246429443359375,
        0.022674560546875, -2.760887145996094e-4, -0.015045166015625,
        -0.01788330078125, 0.0146484375, -0.005573272705078125, ...],
       "index" => 1,
       "object" => "embedding"
     }
    ],
    id: "embd-7d921a4410e249b9960195ab6705b255",
    model: "mistral-embed",
    object: "list",
    usage: %{
      "completion_tokens" => 0,
      "prompt_tokens" => 15,
      "total_tokens" => 15
    }
  }}
  ```
  
  See: https://docs.mistral.ai/api#operation/createEmbedding
  """

  def embeddings(params, config \\ Config.config(%{})) do
    Embeddings.fetch(params, config)
  end  

  @doc """
  Generates the config settings from the given params, using the defaults defined in the application's config.exs if not passed in params.
  
  ## Example request
  ```elixir
  MistralClient.config(
    api_key: "YOUR_MISTRAL_KEY",
    http_options: %{
      stream_to: self(),
      async: :once
    }
  )
  ```
  """

  def config(params) do
    opts = case params do
	     params_list when is_list(params_list) ->
	       Enum.into(params_list, %{})
	     _ -> params
	   end
    Config.config(opts)
  end
  
end