From a9ac4bf517e8f3869207860f89597ffd4558f9b2 Mon Sep 17 00:00:00 2001 From: martosaur Date: Sun, 20 Oct 2024 20:25:21 -0700 Subject: [PATCH] Add docs for Gemini adapter --- CHANGELOG.md | 2 + README.md | 20 +++++++ config/config.example.exs | 3 +- lib/instructor_lite.ex | 25 ++++++++ lib/instructor_lite/adapters/gemini.ex | 57 ++++++++++++++++++- mix.exs | 3 +- test/instructor_lite/adapters/gemini_test.exs | 2 +- 7 files changed, 108 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 395806f..936a605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased + * Add Gemini adapter + ## v0.2.0 * [OpenAI] Do not overwrite `response_format` params key if provided by user diff --git a/README.md b/README.md index 2ca201e..0be6eac 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,26 @@ iex> InstructorLite.instruct(%{ {:ok, %UserInfo{name: "John Doe", age: 42}} ``` +### Gemini + +```elixir +iex> InstructorLite.instruct(%{ + contents: [ + %{ + role: "user", + parts: [%{text: "John Doe is fourty two years old"}] + } + ] + }, + response_model: UserInfo, + adapter: InstructorLite.Adapters.Gemini, + adapter_context: [ + api_key: Application.fetch_env!(:instructor_lite, :gemini_key) + ] +) +{:ok, %UserInfo{name: "John Doe", age: 42}} +``` + ## Configuration diff --git a/config/config.example.exs b/config/config.example.exs index fb5d18f..6f17040 100644 --- a/config/config.example.exs +++ b/config/config.example.exs @@ -5,4 +5,5 @@ import Config config :instructor_lite, openai_key: "api_key", anthropic_key: "api_key", - llamacpp_url: "http://localhost:8000/completion" + llamacpp_url: "http://localhost:8000/completion", + gemini_key: "api_key" diff --git a/lib/instructor_lite.ex b/lib/instructor_lite.ex index e6e5f62..cd79a45 100644 --- a/lib/instructor_lite.ex +++ b/lib/instructor_lite.ex @@ -125,6 +125,31 @@ defmodule InstructorLite do {:ok, %{name: "John Doe", age: 42}} ``` + ### Gemini + + ```elixir + iex> InstructorLite.instruct(%{ + contents: [ + %{ + role: "user", + parts: [%{text: "John Doe is fourty two years old"}] + } + ] + }, + response_model: %{name: :string, age: :integer}, + json_schema: %{ + type: "object", + required: [:age, :name], + properties: %{name: %{type: "string"}, age: %{type: "integer"}} + }, + adapter: InstructorLite.Adapters.Gemini, + adapter_context: [ + api_key: Application.fetch_env!(:instructor_lite, :gemini_key) + ] + ) + {:ok, %{name: "John Doe", age: 42}} + ``` + ### Using `max_retries` diff --git a/lib/instructor_lite/adapters/gemini.ex b/lib/instructor_lite/adapters/gemini.ex index c2bfd95..2908c86 100644 --- a/lib/instructor_lite/adapters/gemini.ex +++ b/lib/instructor_lite/adapters/gemini.ex @@ -1,5 +1,39 @@ defmodule InstructorLite.Adapters.Gemini do @moduledoc """ + [Gemini](https://ai.google.dev/gemini-api) adapter. + + This adapter is implemented using [Text generation](https://ai.google.dev/gemini-api/docs/text-generation) endpoint configured for [structured output](https://ai.google.dev/gemini-api/docs/structured-output?lang=rest#supply-schema-in-config) + + ## Params + `params` argument should be shaped as a [`models.GenerateBody` request body](https://ai.google.dev/api/generate-content#request-body) + + ## Example + + ``` + InstructorLite.instruct( + %{contents: [%{role: "user", parts: [%{text: "John is 25yo"}]}]}, + response_model: %{name: :string, age: :integer}, + json_schema: %{ + type: "object", + required: [:age, :name], + properties: %{name: %{type: "string"}, age: %{type: "integer"}} + }, + adapter: InstructorLite.Adapters.Gemini, + adapter_context: [ + model: "gemini-1.5-flash-8b", + api_key: Application.fetch_env!(:instructor_lite, :gemini_key) + ] + ) + {:ok, %{name: "John", age: 25}} + ``` + + > #### Specifying model {: .tip} + > + > Note how, unlike other adapters, the Gemini adapter expects `model` under `adapter_context`. + + > #### JSON Schema {: .warning} + > + > Gemini's idea of JSON Schema is [quite different](https://ai.google.dev/api/generate-content#generationconfig) from other major models, so `InstructorLite.JSONSchema` won't help you even for simple cases. Luckily, the Gemini API provides detailed errors for invalid schemas. """ @behaviour InstructorLite.Adapter @@ -34,6 +68,13 @@ defmodule InstructorLite.Adapters.Gemini do ] ) + @doc """ + Make request to Gemini API + + ## Options + + #{NimbleOptions.docs(@send_request_schema)} + """ @impl InstructorLite.Adapter def send_request(params, opts) do context = @@ -59,6 +100,9 @@ defmodule InstructorLite.Adapters.Gemini do end end + @doc """ + Puts `systemInstruction` and updates `generationConfig` in `params` with prompt based on `json_schema` and `notes`. + """ @impl InstructorLite.Adapter def initial_prompt(params, opts) do mandatory_part = """ @@ -93,6 +137,9 @@ defmodule InstructorLite.Adapters.Gemini do end) end + @doc """ + Updates `params` with prompt for retrying a request. + """ @impl InstructorLite.Adapter def retry_prompt(params, resp_params, errors, _response, _opts) do do_better = [ @@ -114,13 +161,21 @@ defmodule InstructorLite.Adapters.Gemini do Map.update(params, :contents, do_better, fn contents -> contents ++ do_better end) end + @doc """ + Parse text generation endpoint response. + + Can return: + * `{:ok, parsed_json}` on success. + * `{:error, :refusal, prompt_feedback}` if [request was blocked](https://ai.google.dev/api/generate-content#generatecontentresponse). + * `{:error, :unexpected_response, response}` if response is of unexpected shape. + """ @impl InstructorLite.Adapter def parse_response(response, _opts) do case response do %{"candidates" => [%{"content" => %{"parts" => [%{"text" => text}]}}]} -> Jason.decode(text) - %{"promptFeedback" => %{"blockReason" => reason}} -> + %{"promptFeedback" => %{"blockReason" => _} = reason} -> {:error, :refusal, reason} other -> diff --git a/mix.exs b/mix.exs index 73ebbbf..846026b 100644 --- a/mix.exs +++ b/mix.exs @@ -61,7 +61,8 @@ defmodule InstructorLite.MixProject do Adapters: [ InstructorLite.Adapters.Anthropic, InstructorLite.Adapters.OpenAI, - InstructorLite.Adapters.Llamacpp + InstructorLite.Adapters.Llamacpp, + InstructorLite.Adapters.Gemini ] ], groups_for_extras: [ diff --git a/test/instructor_lite/adapters/gemini_test.exs b/test/instructor_lite/adapters/gemini_test.exs index bd34c23..0dc3418 100644 --- a/test/instructor_lite/adapters/gemini_test.exs +++ b/test/instructor_lite/adapters/gemini_test.exs @@ -160,7 +160,7 @@ defmodule InstructorLite.Adapters.GeminiTest do } } - assert {:error, :refusal, "OTHER"} = + assert {:error, :refusal, %{"blockReason" => "OTHER"}} = Gemini.parse_response(response, []) end