Packages
langchain
0.9.4
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.4.0-rc.3
0.4.0-rc.2
0.4.0-rc.1
0.4.0-rc.0
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-rc.2
0.3.0-rc.1
0.3.0-rc.0
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir implementation of a LangChain style framework that lets Elixir projects integrate with and leverage LLMs.
Current section
Files
Jump to
Current section
Files
lib/utils/google_schema.ex
defmodule LangChain.Utils.GoogleSchema do
@moduledoc """
Removes JSON Schema keywords that Google's Gemini APIs reject.
Google's function declarations and response schemas accept a
[select subset](https://ai.google.dev/api/caching#Schema) of an OpenAPI 3.0
schema object rather than full JSON Schema. The `Schema` type is a protobuf
message, so a keyword it has no field for is rejected by the parser before
any semantic validation runs:
Invalid JSON payload received. Unknown name "additionalProperties" at
'tools[0].function_declarations[0].parameters': Cannot find field.
That fails the entire request, not the single tool, and the message names a
field path rather than the tool, so the cause is not obvious.
`additionalProperties` is the keyword that matters most in practice, because
`LangChain.FunctionParam.to_parameters_schema/1` adds it to every schema it
generates. Without this sanitizing step, any tool declared the ordinary way —
with a `parameters:` list of `LangChain.FunctionParam` structs — is rejected
by Gemini.
## Semantic loss
One removal changes what the API enforces. Dropping `additionalProperties:
false` means Gemini does not reject unexpected properties, so a tool call's
arguments may contain fields the schema did not declare. Where that matters,
validate the arguments inside the tool's own function.
Schemas built around `$ref` are also flattened, since neither `$ref` nor
`$defs` has a Google equivalent. Inline such schemas before sending them.
"""
# Keywords that are valid JSON Schema but are not fields of Google's `Schema`
# type. Supported keywords are deliberately not enumerated: a denylist lets a
# field Google adds later keep working instead of being silently dropped.
@unsupported_keywords ~w(
additionalProperties unevaluatedProperties patternProperties propertyNames
$schema $ref $defs $comment $id $anchor $dynamicRef $dynamicAnchor $vocabulary
definitions
additionalItems prefixItems unevaluatedItems uniqueItems
contains minContains maxContains
dependencies dependentRequired dependentSchemas
oneOf allOf not if then else
const examples exclusiveMinimum exclusiveMaximum multipleOf
readOnly writeOnly deprecated
contentEncoding contentMediaType contentSchema
)
# Keys whose values hold nested schemas. Recursion is limited to these
# because every other value is instance data — `enum` members, a `default`,
# an `example` — and filtering those would corrupt a value that happens to
# use a keyword as one of its own field names.
@schema_bearing_keys ~w(items anyOf)
@doc """
Remove the schema keywords Google does not support, recursing through nested
schemas.
Non-map input is returned unchanged, so this is safe to apply to a `nil`
response schema or a schema fragment that is a bare boolean.
## Example
iex> LangChain.Utils.GoogleSchema.sanitize(%{
...> "type" => "object",
...> "additionalProperties" => false,
...> "properties" => %{"city" => %{"type" => "string"}}
...> })
%{"type" => "object", "properties" => %{"city" => %{"type" => "string"}}}
"""
@spec sanitize(map()) :: map()
@spec sanitize(any()) :: any()
def sanitize(%{} = schema) do
schema
|> Enum.reject(fn {key, _value} -> to_string(key) in @unsupported_keywords end)
|> Map.new(fn {key, value} -> {key, sanitize_value(to_string(key), value)} end)
end
def sanitize(schema), do: schema
# `properties` maps caller-chosen names to schemas. Its keys are property
# names rather than schema keywords, so only the values are sanitized. A
# parameter named `const` or `examples` has to survive.
defp sanitize_value("properties", %{} = properties) do
Map.new(properties, fn {name, sub_schema} -> {name, sanitize(sub_schema)} end)
end
# `items` holds a schema; `anyOf` holds a list of them.
defp sanitize_value(key, value) when key in @schema_bearing_keys do
sanitize_nested(value)
end
defp sanitize_value(_key, value), do: value
defp sanitize_nested(value) when is_list(value), do: Enum.map(value, &sanitize_nested/1)
defp sanitize_nested(%{} = value), do: sanitize(value)
defp sanitize_nested(value), do: value
@doc """
Return true when the schema describes an object with no properties.
Google rejects such a schema with "should be non-empty for OBJECT type", so a
function declaration that produces one has to omit its parameters entirely
rather than send an empty object.
## Example
iex> LangChain.Utils.GoogleSchema.empty_object?(%{"type" => "object", "properties" => %{}})
true
iex> LangChain.Utils.GoogleSchema.empty_object?(%{"type" => "object", "properties" => %{"a" => %{}}})
false
"""
@spec empty_object?(any()) :: boolean()
def empty_object?(%{"type" => "object", "properties" => properties})
when map_size(properties) == 0,
do: true
def empty_object?(_schema), do: false
end