diff --git a/.formatter.exs b/.formatter.exs index dfaff38..76c6572 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,10 @@ # Used by "mix format" [ - import_deps: [:ecto, :phoenix, :phoenix_live_view], + import_deps: [:ecto, :flint, :phoenix, :phoenix_live_view], plugins: [Phoenix.LiveView.HTMLFormatter], - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}", "pages/cookbook/**/*.{ex,exs}"] + inputs: [ + "{mix,.formatter}.exs", + "{config,lib,test}/**/*.{ex,exs}", + "pages/cookbook/**/*.{ex,exs}" + ] ] diff --git a/lib/instructor.ex b/lib/instructor.ex index 50e99bd..975d912 100644 --- a/lib/instructor.ex +++ b/lib/instructor.ex @@ -589,12 +589,12 @@ defmodule Instructor do not is_ecto_schema(response_model) -> changeset - function_exported?(response_model, :validate_changeset, 1) -> - response_model.validate_changeset(changeset) - function_exported?(response_model, :validate_changeset, 2) -> response_model.validate_changeset(changeset, context) + function_exported?(response_model, :validate_changeset, 1) -> + response_model.validate_changeset(changeset) + true -> changeset end diff --git a/lib/instructor/adapters/anthropic.ex b/lib/instructor/adapters/anthropic.ex index e21d809..725a8b2 100644 --- a/lib/instructor/adapters/anthropic.ex +++ b/lib/instructor/adapters/anthropic.ex @@ -3,6 +3,10 @@ defmodule Instructor.Adapters.Anthropic do Anthropic adapter for Instructor. """ @behaviour Instructor.Adapter + @default_config [ + api_url: "https://api.anthropic.com/", + http_options: [receive_timeout: 60_000] + ] alias Instructor.SSEStreamParser @@ -131,14 +135,9 @@ defmodule Instructor.Adapters.Anthropic do defp api_key(config), do: Keyword.fetch!(config, :api_key) defp http_options(config), do: Keyword.fetch!(config, :http_options) - defp config(nil), do: config(Application.get_env(:instructor, :anthropic, [])) - - defp config(base_config) do - default_config = [ - api_url: "https://api.anthropic.com/", - http_options: [receive_timeout: 60_000] - ] - - Keyword.merge(default_config, base_config) + defp config(base_config \\ nil) do + @default_config + |> Keyword.merge(Application.get_env(:anthropic, :openai, [])) + |> Keyword.merge(base_config) end end diff --git a/lib/instructor/adapters/openai.ex b/lib/instructor/adapters/openai.ex index 6133074..54f4b27 100644 --- a/lib/instructor/adapters/openai.ex +++ b/lib/instructor/adapters/openai.ex @@ -8,6 +8,13 @@ defmodule Instructor.Adapters.OpenAI do alias Instructor.JSONSchema alias Instructor.SSEStreamParser + @default_config [ + api_url: "https://api.openai.com", + api_path: "/v1/chat/completions", + auth_mode: :bearer, + http_options: [receive_timeout: 60_000] + ] + @impl true def chat_completion(params, user_config \\ nil) do config = config(user_config) @@ -213,16 +220,9 @@ defmodule Instructor.Adapters.OpenAI do defp http_options(config), do: Keyword.fetch!(config, :http_options) - defp config(nil), do: config(Application.get_env(:instructor, :openai, [])) - - defp config(base_config) do - default_config = [ - api_url: "https://api.openai.com", - api_path: "/v1/chat/completions", - auth_mode: :bearer, - http_options: [receive_timeout: 60_000] - ] - - Keyword.merge(default_config, base_config) + defp config(base_config \\ nil) do + @default_config + |> Keyword.merge(Application.get_env(:instructor, :openai, [])) + |> Keyword.merge(base_config) end end diff --git a/lib/instructor/ecto_type.ex b/lib/instructor/ecto_type.ex index 070b200..273ec13 100644 --- a/lib/instructor/ecto_type.ex +++ b/lib/instructor/ecto_type.ex @@ -4,7 +4,7 @@ defmodule Instructor.EctoType do that works natively with Instructor. ## Example - + ```elixir defmodule MyCustomType do use Ecto.Type @@ -22,10 +22,177 @@ defmodule Instructor.EctoType do ``` """ @callback to_json_schema() :: map() + @callback to_json_schema(tuple()) :: map() + + @optional_callbacks to_json_schema: 0, to_json_schema: 1 + + defguard is_ecto_schema(mod) when is_atom(mod) + defguard is_ecto_types(types) when is_map(types) + + def title_for(ecto_schema) when is_ecto_schema(ecto_schema) do + to_string(ecto_schema) |> String.trim_leading("Elixir.") + end + + def for_type(:any), do: %{} + def for_type(:id), do: %{type: "integer", description: "Integer, e.g. 1"} + def for_type(:binary_id), do: %{type: "string"} + def for_type(:integer), do: %{type: "integer", description: "Integer, e.g. 1"} + def for_type(:float), do: %{type: "number", description: "Float, e.g. 1.27", format: "float"} + def for_type(:boolean), do: %{type: "boolean", description: "Boolean, e.g. true"} + def for_type(:string), do: %{type: "string", description: "String, e.g. 'hello'"} + # def for_type(:binary), do: %{type: "unsupported"} + def for_type({:array, type}), do: %{type: "array", items: for_type(type)} + + def for_type(:map), + do: %{ + type: "object", + properties: %{}, + additionalProperties: false, + description: "An object with arbitrary keys and values, e.g. { key: value }" + } + + def for_type({:map, type}), + do: %{ + type: "object", + properties: %{}, + additionalProperties: for_type(type), + description: "An object with values of a type #{inspect(type)}, e.g. { key: value }" + } + + def for_type(:decimal), do: %{type: "number", format: "float"} + + def for_type(:date), + do: %{type: "string", description: "ISO8601 Date, e.g. \"2024-07-20\"", format: "date"} + + def for_type(:time), + do: %{ + type: "string", + description: "ISO8601 Time, e.g. \"12:00:00\"", + pattern: "^[0-9]{2}:?[0-9]{2}:?[0-9]{2}$" + } + + def for_type(:time_usec), + do: %{ + type: "string", + description: "ISO8601 Time with microseconds, e.g. \"12:00:00.000000\"", + pattern: "^[0-9]{2}:?[0-9]{2}:?[0-9]{2}.[0-9]{6}$" + } + + def for_type(:naive_datetime), + do: %{ + type: "string", + description: "ISO8601 DateTime, e.g. \"2024-07-20T12:00:00\"", + format: "date-time" + } + + def for_type(:naive_datetime_usec), + do: %{ + type: "string", + description: "ISO8601 DateTime with microseconds, e.g. \"2024-07-20T12:00:00.000000\"", + format: "date-time" + } + + def for_type(:utc_datetime), + do: %{ + type: "string", + description: "ISO8601 DateTime, e.g. \"2024-07-20T12:00:00Z\"", + format: "date-time" + } + + def for_type(:utc_datetime_usec), + do: %{ + type: "string", + description: "ISO8601 DateTime with microseconds, e.g. \"2024-07-20T12:00:00.000000Z\"", + format: "date-time" + } + + def for_type( + {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :many, related: related}}} + ) + when is_ecto_schema(related) do + title = title_for(related) + + %{ + items: %{"$ref": "#/$defs/#{title}"}, + title: title, + type: "array" + } + end + + def for_type( + {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :many, related: related}}} + ) + when is_ecto_types(related) do + properties = + for {field, type} <- related, into: %{} do + {field, for_type(type)} + end + + required = Map.keys(properties) |> Enum.sort() + + %{ + items: %{ + type: "object", + required: required, + properties: properties + }, + type: "array" + } + end + + def for_type( + {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :one, related: related}}} + ) + when is_ecto_schema(related) do + %{"$ref": "#/$defs/#{title_for(related)}"} + end + + def for_type( + {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :one, related: related}}} + ) + when is_ecto_types(related) do + properties = + for {field, type} <- related, into: %{} do + {field, for_type(type)} + end + + required = Map.keys(properties) |> Enum.sort() + + %{ + type: "object", + required: required, + properties: properties, + additionalProperties: false + } + end + + def for_type({:parameterized, {Ecto.Enum, %{mappings: mappings}}}) do + %{ + type: "string", + enum: Keyword.keys(mappings) + } + end + + def for_type({:parameterized, {mod, params}}) do + if function_exported?(mod, :to_json_schema, 1) do + mod.to_json_schema(params) + else + raise "Unsupported type: #{inspect(mod)}, please implement `to_json_schema/1` via `use Instructor.EctoType`" + end + end + + def for_type(mod) do + if function_exported?(mod, :to_json_schema, 0) do + mod.to_json_schema() + else + raise "Unsupported type: #{inspect(mod)}, please implement `to_json_schema/0` via `use Instructor.EctoType`" + end + end def __using__(_) do quote do @behaviour Instructor.EctoType + import Instructor.EctoType end end end diff --git a/lib/instructor/gbnf.ex b/lib/instructor/gbnf.ex index d5e6aa9..9b073ec 100644 --- a/lib/instructor/gbnf.ex +++ b/lib/instructor/gbnf.ex @@ -39,6 +39,8 @@ defmodule Instructor.GBNF do ws01 ::= ([ \\t\\n])? """ + import Instructor.EctoType + @doc """ Convert a JSONSchema to a GBNF grammar to be used with llama.cpp diff --git a/lib/instructor/instruction.ex b/lib/instructor/instruction.ex new file mode 100644 index 0000000..3fedbd7 --- /dev/null +++ b/lib/instructor/instruction.ex @@ -0,0 +1,108 @@ +if Code.ensure_loaded?(Flint.Schema) do + defmodule Instructor.Union do + use Flint.Type, extends: Flint.Types.Union + @behaviour Instructor.EctoType + + @impl true + def to_json_schema(%{types: types}) when is_list(types) do + %{ + "oneOf" => Enum.map(types, &Instructor.EctoType.for_type/1) + } + end + end + + defmodule Instructor.Instruction do + use Flint.Extension + + attribute(:stream, default: false, validator: &is_boolean/1) + attribute(:validation_context, default: %{}, validator: &is_map/1) + attribute(:mode, default: :tools, validator: &Kernel.in(&1, [:tools, :json, :md_json])) + attribute(:max_retries, default: 0, validator: &is_integer/1) + attribute(:system_prompt, validator: &is_binary/1) + attribute(:model, validator: &is_binary/1) + attribute(:array, default: false, validator: &is_boolean/1) + attribute(:template) + + option(:doc, default: "", validator: &is_binary/1, required: false) + + defmacro __using__(_opts) do + quote do + use Instructor.Validator + alias Instructor.Union + + def render_template(assigns) do + EEx.eval_string(__MODULE__.__schema__(:template), assigns: assigns) + end + + def chat_completion(messages, opts \\ []) do + {stream, opts} = Keyword.pop(opts, :stream, __MODULE__.__schema__(:stream)) + + {validation_context, opts} = + Keyword.pop(opts, :validation_context, __MODULE__.__schema__(:validation_context)) + + {mode, opts} = Keyword.pop(opts, :mode, __MODULE__.__schema__(:mode)) + + {max_retries, opts} = + Keyword.pop(opts, :max_retries, __MODULE__.__schema__(:max_retries)) + + {model, opts} = Keyword.pop(opts, :model, __MODULE__.__schema__(:model)) + + {config, opts} = Keyword.split(opts, [:api_key, :api_url, :http_options]) + + settings = + [ + stream: stream, + validation_context: validation_context, + mode: mode, + max_retries: max_retries, + model: model + ] + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + + messages = if Keyword.keyword?(messages), do: [messages], else: messages + + messages = + for message <- messages do + case message do + %{role: _role, content: _content} -> + message + + _ -> + %{ + role: "user", + content: + if(__MODULE__.__schema__(:template), + do: render_template(message), + else: message + ) + } + end + end + + messages = + if __MODULE__.__schema__(:system_prompt) do + [%{role: "system", content: __MODULE__.__schema__(:system_prompt)} | messages] + else + messages + end + + response_model = + if __MODULE__.__schema__(:array), do: {:array, __MODULE__}, else: __MODULE__ + + opts = [messages: messages, response_model: response_model] ++ settings ++ opts + + Instructor.chat_completion(opts, config) + end + + @impl true + def validate_changeset(changeset, context \\ %{}) do + __MODULE__ + |> struct!() + |> changeset(changeset, Enum.into(context, [])) + end + + defoverridable validate_changeset: 1, validate_changeset: 2 + end + end + end +end diff --git a/lib/instructor/json_schema.ex b/lib/instructor/json_schema.ex index dbe3126..c63382e 100644 --- a/lib/instructor/json_schema.ex +++ b/lib/instructor/json_schema.ex @@ -1,6 +1,5 @@ defmodule Instructor.JSONSchema do - defguardp is_ecto_schema(mod) when is_atom(mod) - defguardp is_ecto_types(types) when is_map(types) + import Instructor.EctoType @doc """ Generates a JSON Schema from an Ecto schema. @@ -51,6 +50,9 @@ defmodule Instructor.JSONSchema do ecto_schema_struct_literal = "%#{title_for(ecto_schema)}{}" case Code.fetch_docs(ecto_schema) do + {:docs_v1, _, :elixir, _, %{"en" => module_doc}, _, _} -> + module_doc + {_, _, _, _, _, _, docs} -> docs |> Enum.find_value(fn @@ -72,12 +74,41 @@ defmodule Instructor.JSONSchema do when is_ecto_schema(ecto_schema) do seen_schemas = MapSet.put(seen_schemas, ecto_schema) + field_docs = + try do + ecto_schema.__schema__(:extra_options) + |> Enum.map(fn {field, opts} -> + {field, Keyword.get(opts, :doc, "")} + end) + rescue + _ -> + [] + end + properties = ecto_schema.__schema__(:fields) |> Enum.map(fn field -> type = ecto_schema.__schema__(:type, field) + field_doc = Keyword.get(field_docs, field, "") |> String.trim() value = for_type(type) - value = Map.merge(%{title: Atom.to_string(field)}, value) + + value = + Map.merge(%{title: Atom.to_string(field)}, value) + |> Map.update(:description, field_doc, fn desc -> + field_doc = + cond do + field_doc == "" -> + "" + + String.ends_with?(field_doc, ".") -> + field_doc <> " " + + true -> + field_doc <> ". " + end + + field_doc <> desc + end) {field, value} end) @@ -173,9 +204,6 @@ defmodule Instructor.JSONSchema do [schema | bfs_from_ecto_schema(rest, seen_schemas)] end - defp title_for(ecto_schema) when is_ecto_schema(ecto_schema) do - to_string(ecto_schema) |> String.trim_leading("Elixir.") - end # Find all values in a map or list that match a predicate defp find_all_values(map, pred) when is_map(map) do @@ -202,153 +230,6 @@ defmodule Instructor.JSONSchema do defp find_all_values(_, _pred), do: [] - defp for_type(:id), do: %{type: "integer", description: "Integer, e.g. 1"} - defp for_type(:binary_id), do: %{type: "string"} - defp for_type(:integer), do: %{type: "integer", description: "Integer, e.g. 1"} - defp for_type(:float), do: %{type: "number", description: "Float, e.g. 1.27", format: "float"} - defp for_type(:boolean), do: %{type: "boolean", description: "Boolean, e.g. true"} - defp for_type(:string), do: %{type: "string", description: "String, e.g. 'hello'"} - # defp for_type(:binary), do: %{type: "unsupported"} - defp for_type({:array, type}), do: %{type: "array", items: for_type(type)} - - defp for_type(:map), - do: %{ - type: "object", - properties: %{}, - additionalProperties: false, - description: "An object with arbitrary keys and values, e.g. { key: value }" - } - - defp for_type({:map, type}), - do: %{ - type: "object", - properties: %{}, - additionalProperties: for_type(type), - description: "An object with values of a type #{inspect(type)}, e.g. { key: value }" - } - - defp for_type(:decimal), do: %{type: "number", format: "float"} - - defp for_type(:date), - do: %{type: "string", description: "ISO8601 Date, e.g. \"2024-07-20\"", format: "date"} - - defp for_type(:time), - do: %{ - type: "string", - description: "ISO8601 Time, e.g. \"12:00:00\"", - pattern: "^[0-9]{2}:?[0-9]{2}:?[0-9]{2}$" - } - - defp for_type(:time_usec), - do: %{ - type: "string", - description: "ISO8601 Time with microseconds, e.g. \"12:00:00.000000\"", - pattern: "^[0-9]{2}:?[0-9]{2}:?[0-9]{2}.[0-9]{6}$" - } - - defp for_type(:naive_datetime), - do: %{ - type: "string", - description: "ISO8601 DateTime, e.g. \"2024-07-20T12:00:00\"", - format: "date-time" - } - - defp for_type(:naive_datetime_usec), - do: %{ - type: "string", - description: "ISO8601 DateTime with microseconds, e.g. \"2024-07-20T12:00:00.000000\"", - format: "date-time" - } - - defp for_type(:utc_datetime), - do: %{ - type: "string", - description: "ISO8601 DateTime, e.g. \"2024-07-20T12:00:00Z\"", - format: "date-time" - } - - defp for_type(:utc_datetime_usec), - do: %{ - type: "string", - description: "ISO8601 DateTime with microseconds, e.g. \"2024-07-20T12:00:00.000000Z\"", - format: "date-time" - } - - defp for_type( - {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :many, related: related}}} - ) - when is_ecto_schema(related) do - title = title_for(related) - - %{ - items: %{"$ref": "#/$defs/#{title}"}, - title: title, - type: "array" - } - end - - defp for_type( - {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :many, related: related}}} - ) - when is_ecto_types(related) do - properties = - for {field, type} <- related, into: %{} do - {field, for_type(type)} - end - - required = Map.keys(properties) |> Enum.sort() - - %{ - items: %{ - type: "object", - required: required, - properties: properties - }, - type: "array" - } - end - - defp for_type( - {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :one, related: related}}} - ) - when is_ecto_schema(related) do - %{"$ref": "#/$defs/#{title_for(related)}"} - end - - defp for_type( - {:parameterized, {Ecto.Embedded, %Ecto.Embedded{cardinality: :one, related: related}}} - ) - when is_ecto_types(related) do - properties = - for {field, type} <- related, into: %{} do - {field, for_type(type)} - end - - required = Map.keys(properties) |> Enum.sort() - - %{ - type: "object", - required: required, - properties: properties, - additionalProperties: false - } - end - - defp for_type({:parameterized, {Ecto.Enum, %{mappings: mappings}}}) do - %{ - type: "string", - enum: Keyword.keys(mappings) - } - end - - defp for_type(mod) do - if function_exported?(mod, :to_json_schema, 0) do - mod.to_json_schema() - else - raise "Unsupported type: #{inspect(mod)}, please implement `to_json_schema/0` via `use Instructor.EctoType`" - end - end - @doc """ Traverses a tree structure of maps and lists, allowing the user to update or remove elements. diff --git a/lib/instructor/sse_stream_parser.ex b/lib/instructor/sse_stream_parser.ex index a695ecb..a88fb34 100644 --- a/lib/instructor/sse_stream_parser.ex +++ b/lib/instructor/sse_stream_parser.ex @@ -1,4 +1,5 @@ defmodule Instructor.SSEStreamParser do + require Logger @moduledoc false def parse(stream) do @@ -16,6 +17,5 @@ defmodule Instructor.SSEStreamParser do Jason.decode!(json_string) end) end) - # |> Stream.each(&IO.inspect/1) end end diff --git a/mix.exs b/mix.exs index b0b1d27..c042f9c 100644 --- a/mix.exs +++ b/mix.exs @@ -115,6 +115,7 @@ defmodule Instructor.MixProject do {:jason, "~> 1.4.0"}, {:req, "~> 0.5 or ~> 1.0"}, {:jaxon, "~> 2.0"}, + {:flint, github: "acalejos/flint"}, {:ex_doc, "~> 0.31", only: :dev, runtime: false}, {:mox, "~> 1.1.0", only: :test}, {:phoenix, "~> 1.7", only: :test}, diff --git a/mix.lock b/mix.lock index a4cdf56..917caf4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,15 @@ %{ "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ecto": {:hex, :ecto, "3.12.1", "626765f7066589de6fa09e0876a253ff60c3d00870dd3a1cd696e2ba67bfceea", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df0045ab9d87be947228e05a8d153f3e06e0d05ab10c3b3cc557d2f7243d1940"}, + "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, + "flint": {:git, "https://github.com/acalejos/flint.git", "8c0a01ee1aa17e4f4f0cafbcccbd8d911915b59b", []}, + "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, + "igniter": {:hex, :igniter, "0.4.7", "a91ab006e400f82ae93c1ada7d112cc7e7c1684455428c0de98d6b2e66025b43", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rewrite, "~> 1.0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "89bc7a093d60c5c70e46014ffd83d993593c1151c8301e3a16f2846695ff22ec"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jaxon": {:hex, :jaxon, "2.0.8", "00951a79d354260e28d7e36f956c3de94818124768a4b22e0fc55559d1b3bfe7", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "74532853b1126609615ea98f0ceb5009e70465ca98027afbbd8ed314d887e82d"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, @@ -26,7 +29,12 @@ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"}, + "rewrite": {:hex, :rewrite, "1.1.1", "0e6674eb5f8cb11aabe5ad6207151b4156bf173aa9b43133a68f8cc882364570", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "fcd688b3ca543c3a1f1f4615ccc054ec37cfcde91133a27a683ec09b35ae1496"}, + "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, + "spark": {:hex, :spark, "2.2.35", "1c0bb30f340151eca24164885935de39e6ada4010555f444c813d0488990f8f3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "f242d6385c287389034a0e146d8f025b5c9ab777f1ae5cf0fdfc9209db6ae748"}, + "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, }