Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 74 additions & 27 deletions lib/instructor/adapters/gemini.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}]}}
Expand All @@ -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" => [
%{
Expand All @@ -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,
Expand All @@ -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.
"""

Expand Down
29 changes: 26 additions & 3 deletions test/instructor_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]},
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down