diff --git a/lib/instructor/adapters/gemini.ex b/lib/instructor/adapters/gemini.ex index 66dc933..ef81f82 100644 --- a/lib/instructor/adapters/gemini.ex +++ b/lib/instructor/adapters/gemini.ex @@ -27,7 +27,7 @@ defmodule Instructor.Adapters.Gemini do alias Instructor.Adapters alias Instructor.JSONSchema - @supported_modes [:json_schema] + @supported_modes [:json, :json_schema] @doc """ Run a completion against Google's Gemini API @@ -98,35 +98,54 @@ defmodule Instructor.Adapters.Gemini do params = Map.put(params, :contents, contents) params = - case params do - %{response_format: %{json_schema: %{schema: schema}}} -> - generation_config = - generation_config - |> Map.put("response_mime_type", "application/json") - |> Map.put("response_schema", normalize_json_schema(schema)) + case mode do + :json -> + generation_config = Map.put(generation_config, :responseMimeType, "application/json") params |> Map.put(:generationConfig, generation_config) - |> Map.delete(:response_format) - - %{tools: tools} -> - tools = [ - %{ - function_declarations: - Enum.map(tools, fn %{function: tool} -> - %{ - name: tool["name"], - description: tool["description"], - parameters: normalize_json_schema(tool["parameters"]) - } - end) - } - ] + |> Map.delete(:response_format) # Explicitly remove response_format + + :json_schema -> + case params do + %{response_format: %{json_schema: %{schema: schema}}} -> + generation_config = + generation_config + |> Map.put("responseMimeType", "application/json") + |> Map.put("responseSchema", normalize_json_schema(schema)) + + params + |> Map.put(:generationConfig, generation_config) + |> Map.delete(:response_format) + + _ -> + params + end - params - |> Map.put(:generationConfig, generation_config) - |> Map.put(:tools, tools) - |> Map.delete(:tool_choice) + :tools -> + case params do + %{tools: tools} -> + tools = [ + %{ + function_declarations: + Enum.map(tools, fn %{function: tool} -> + %{ + name: tool["name"], + description: tool["description"], + parameters: normalize_json_schema(tool["parameters"]) + } + end) + } + ] + + params + |> Map.put(:generationConfig, generation_config) + |> Map.put(:tools, tools) + |> Map.delete(:tool_choice) + + _ -> + params + end _ -> params @@ -218,6 +237,14 @@ defmodule Instructor.Adapters.Gemini do {:ok, args} end + defp parse_response_for_mode(:json, %{ + "candidates" => [ + %{"content" => %{"parts" => [%{"text" => text}]}} + ] + }) do + Jason.decode(text) + end + defp parse_response_for_mode(:json_schema, %{ "candidates" => [ %{"content" => %{"parts" => [%{"text" => text}]}} @@ -241,6 +268,18 @@ defmodule Instructor.Adapters.Gemini do args end + defp parse_stream_chunk_for_mode(:json, %{ + "candidates" => [ + %{ + "content" => %{ + "parts" => [%{"text" => chunk}] + } + } + ] + }) do + chunk + end + defp parse_stream_chunk_for_mode(:json_schema, %{ "candidates" => [ %{ @@ -253,6 +292,14 @@ defmodule Instructor.Adapters.Gemini do chunk end + # NEW: Fallback clause for :json_schema mode to handle non-data chunks + # This will catch chunks that don't match the pattern above (e.g., metadata or STOP events). + defp parse_stream_chunk_for_mode(:json_schema, _chunk) do + # Optionally, log the unhandled chunk structure for debugging: + # Logger.debug("Gemini adapter: received non-data stream chunk for :json_schema, returning empty. Chunk: #{inspect(chunk)}") + "" + end + defp normalize_json_schema(schema) do JSONSchema.traverse_and_update( schema, @@ -267,7 +314,7 @@ defmodule Instructor.Adapters.Gemini do raise """ Invalid JSON Schema: object with no properties at path: #{inspect(path)} - Gemini does not support empty objects. This is likely because have have a naked :map type + Gemini does not support empty objects. This is likely because it uses a naked :map type without any fields at #{inspect(path)}. Try switching to an embedded schema instead. """ diff --git a/test/instructor_test.exs b/test/instructor_test.exs index 8064961..70d0ead 100644 --- a/test/instructor_test.exs +++ b/test/instructor_test.exs @@ -64,7 +64,7 @@ defmodule InstructorTest do {:openai, [mode: :json, model: "gpt-4o-mini"]}, {:openai, [mode: :json_schema, model: "gpt-4o-mini"]}, {:groq, [mode: :tools, model: "llama-3.3-70b-versatile"]}, - {:gemini, [mode: :json_schema, model: "gemini-2.0-flash"]}, + {:gemini, [mode: :json_schema, model: "gemini-2.5-flash-preview-04-17"]}, {:anthropic, [mode: :tools, model: "claude-3-5-haiku-latest", max_tokens: 1024]}, {:xai, [mode: :tools, model: "grok-2-latest"]}, {:xai, [mode: :json_schema, model: "grok-2-latest"]}, @@ -126,6 +126,24 @@ defmodule InstructorTest do assert is_float(score) end + defmodule AllEctoTypes.NestedObject do + use Ecto.Schema + use Instructor # If you want to add @llm_doc + @primary_key false + embedded_schema do + field :key1, :string # Must have at least one defined property + end + end + + defmodule AllEctoTypes.NestedObjectTwo do + use Ecto.Schema + use Instructor + @primary_key false + embedded_schema do + field :item, :string # Example, if values were strings + end + end + defmodule AllEctoTypes do use Ecto.Schema use Instructor @@ -139,8 +157,13 @@ defmodule InstructorTest do field(:string, :string) # field(:binary, :binary) field(:array, {:array, :string}) - field(:nested_object, :map) - field(:nested_object_two, {:map, :string}) + if adapter == :gemini && params[:mode] == :json_schema do + embeds_one :nested_object, AllEctoTypes.NestedObject + embeds_one :nested_object_two, AllEctoTypes.NestedObjectTwo + else + field(:nested_object, :map) + field(:nested_object_two, {:map, :string}) + end field(:decimal, :decimal) field(:date, :date) field(:time, :time)