mirror of
https://github.com/hwchase17/langchain.git
synced 2025-09-13 13:36:15 +00:00
feat(groq): add support for json_schema
(#32396)
This commit is contained in:
@@ -51,6 +51,7 @@ from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough
|
|||||||
from langchain_core.tools import BaseTool
|
from langchain_core.tools import BaseTool
|
||||||
from langchain_core.utils import from_env, get_pydantic_field_names, secret_from_env
|
from langchain_core.utils import from_env, get_pydantic_field_names, secret_from_env
|
||||||
from langchain_core.utils.function_calling import (
|
from langchain_core.utils.function_calling import (
|
||||||
|
convert_to_json_schema,
|
||||||
convert_to_openai_function,
|
convert_to_openai_function,
|
||||||
convert_to_openai_tool,
|
convert_to_openai_tool,
|
||||||
)
|
)
|
||||||
@@ -503,8 +504,13 @@ class ChatGroq(BaseChatModel):
|
|||||||
async_api=async_api, run_manager=run_manager, **kwargs
|
async_api=async_api, run_manager=run_manager, **kwargs
|
||||||
)
|
)
|
||||||
if base_should_stream and ("response_format" in kwargs):
|
if base_should_stream and ("response_format" in kwargs):
|
||||||
# Streaming not supported in JSON mode.
|
# Streaming not supported in JSON mode or structured outputs.
|
||||||
return kwargs["response_format"] != {"type": "json_object"}
|
response_format = kwargs["response_format"]
|
||||||
|
if isinstance(response_format, dict) and response_format.get("type") in {
|
||||||
|
"json_schema",
|
||||||
|
"json_object",
|
||||||
|
}:
|
||||||
|
return False
|
||||||
return base_should_stream
|
return base_should_stream
|
||||||
|
|
||||||
def _generate(
|
def _generate(
|
||||||
@@ -850,7 +856,9 @@ class ChatGroq(BaseChatModel):
|
|||||||
self,
|
self,
|
||||||
schema: Optional[Union[dict, type[BaseModel]]] = None,
|
schema: Optional[Union[dict, type[BaseModel]]] = None,
|
||||||
*,
|
*,
|
||||||
method: Literal["function_calling", "json_mode"] = "function_calling",
|
method: Literal[
|
||||||
|
"function_calling", "json_mode", "json_schema"
|
||||||
|
] = "function_calling",
|
||||||
include_raw: bool = False,
|
include_raw: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Runnable[LanguageModelInput, dict | BaseModel]:
|
) -> Runnable[LanguageModelInput, dict | BaseModel]:
|
||||||
@@ -875,12 +883,34 @@ class ChatGroq(BaseChatModel):
|
|||||||
|
|
||||||
Added support for TypedDict class.
|
Added support for TypedDict class.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.3.8
|
||||||
|
|
||||||
|
Added support for Groq's dedicated structured output feature via
|
||||||
|
``method="json_schema"``.
|
||||||
|
|
||||||
|
method: The method for steering model generation, one of:
|
||||||
|
|
||||||
|
- ``'function_calling'``:
|
||||||
|
Uses Groq's tool-calling `API <https://console.groq.com/docs/tool-use>`__
|
||||||
|
- ``'json_schema'``:
|
||||||
|
Uses Groq's `Structured Output API <https://console.groq.com/docs/structured-outputs>`__.
|
||||||
|
Supported for a subset of models, including ``openai/gpt-oss``,
|
||||||
|
``moonshotai/kimi-k2-instruct``, and some ``meta-llama/llama-4``
|
||||||
|
models. See `docs <https://console.groq.com/docs/structured-outputs>`__
|
||||||
|
for details.
|
||||||
|
- ``'json_mode'``:
|
||||||
|
Uses Groq's `JSON mode <https://console.groq.com/docs/structured-outputs#json-object-mode>`__.
|
||||||
|
Note that if using JSON mode then you must include instructions for
|
||||||
|
formatting the output into the desired schema into the model call
|
||||||
|
|
||||||
|
Learn more about the differences between the methods and which models
|
||||||
|
support which methods `here <https://console.groq.com/docs/structured-outputs>`__.
|
||||||
|
|
||||||
method:
|
method:
|
||||||
The method for steering model generation, either ``'function_calling'``
|
The method for steering model generation, either ``'function_calling'``
|
||||||
or ``'json_mode'``. If ``'function_calling'`` then the schema will be converted
|
or ``'json_mode'``. If ``'function_calling'`` then the schema will be converted
|
||||||
to an OpenAI function and the returned model will make use of the
|
to an OpenAI function and the returned model will make use of the
|
||||||
function-calling API. If ``'json_mode'`` then OpenAI's JSON mode will be
|
function-calling API. If ``'json_mode'`` then JSON mode will be used.
|
||||||
used.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
If using ``'json_mode'`` then you must include instructions for formatting
|
If using ``'json_mode'`` then you must include instructions for formatting
|
||||||
@@ -938,7 +968,7 @@ class ChatGroq(BaseChatModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
|
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
|
||||||
structured_llm = llm.with_structured_output(AnswerWithJustification)
|
structured_llm = llm.with_structured_output(AnswerWithJustification)
|
||||||
|
|
||||||
structured_llm.invoke(
|
structured_llm.invoke(
|
||||||
@@ -964,7 +994,7 @@ class ChatGroq(BaseChatModel):
|
|||||||
justification: str
|
justification: str
|
||||||
|
|
||||||
|
|
||||||
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
|
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
|
||||||
structured_llm = llm.with_structured_output(
|
structured_llm = llm.with_structured_output(
|
||||||
AnswerWithJustification, include_raw=True
|
AnswerWithJustification, include_raw=True
|
||||||
)
|
)
|
||||||
@@ -997,7 +1027,7 @@ class ChatGroq(BaseChatModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
|
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
|
||||||
structured_llm = llm.with_structured_output(AnswerWithJustification)
|
structured_llm = llm.with_structured_output(AnswerWithJustification)
|
||||||
|
|
||||||
structured_llm.invoke(
|
structured_llm.invoke(
|
||||||
@@ -1026,7 +1056,7 @@ class ChatGroq(BaseChatModel):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
|
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
|
||||||
structured_llm = llm.with_structured_output(oai_schema)
|
structured_llm = llm.with_structured_output(oai_schema)
|
||||||
|
|
||||||
structured_llm.invoke(
|
structured_llm.invoke(
|
||||||
@@ -1037,6 +1067,41 @@ class ChatGroq(BaseChatModel):
|
|||||||
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
|
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
Example: schema=Pydantic class, method="json_schema", include_raw=False:
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from langchain_groq import ChatGroq
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerWithJustification(BaseModel):
|
||||||
|
'''An answer to the user question along with justification for the answer.'''
|
||||||
|
|
||||||
|
answer: str
|
||||||
|
# If we provide default values and/or descriptions for fields, these will be passed
|
||||||
|
# to the model. This is an important part of improving a model's ability to
|
||||||
|
# correctly return structured outputs.
|
||||||
|
justification: Optional[str] = Field(
|
||||||
|
default=None, description="A justification for the answer."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
|
||||||
|
structured_llm = llm.with_structured_output(
|
||||||
|
AnswerWithJustification, method="json_schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
structured_llm.invoke(
|
||||||
|
"What weighs more a pound of bricks or a pound of feathers"
|
||||||
|
)
|
||||||
|
|
||||||
|
# -> AnswerWithJustification(
|
||||||
|
# answer='They weigh the same',
|
||||||
|
# justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
|
||||||
|
# )
|
||||||
|
|
||||||
Example: schema=Pydantic class, method="json_mode", include_raw=True:
|
Example: schema=Pydantic class, method="json_mode", include_raw=True:
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
@@ -1047,7 +1112,7 @@ class ChatGroq(BaseChatModel):
|
|||||||
answer: str
|
answer: str
|
||||||
justification: str
|
justification: str
|
||||||
|
|
||||||
llm = ChatGroq(model="llama-3.1-405b-reasoning", temperature=0)
|
llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
|
||||||
structured_llm = llm.with_structured_output(
|
structured_llm = llm.with_structured_output(
|
||||||
AnswerWithJustification,
|
AnswerWithJustification,
|
||||||
method="json_mode",
|
method="json_mode",
|
||||||
@@ -1065,35 +1130,12 @@ class ChatGroq(BaseChatModel):
|
|||||||
# 'parsing_error': None
|
# 'parsing_error': None
|
||||||
# }
|
# }
|
||||||
|
|
||||||
Example: schema=None, method="json_mode", include_raw=True:
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
structured_llm = llm.with_structured_output(method="json_mode", include_raw=True)
|
|
||||||
|
|
||||||
structured_llm.invoke(
|
|
||||||
"Answer the following question. "
|
|
||||||
"Make sure to return a JSON blob with keys 'answer' and 'justification'.\n\n"
|
|
||||||
"What's heavier a pound of bricks or a pound of feathers?"
|
|
||||||
)
|
|
||||||
# -> {
|
|
||||||
# 'raw': AIMessage(content='{\n "answer": "They are both the same weight.",\n "justification": "Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight." \n}'),
|
|
||||||
# 'parsed': {
|
|
||||||
# 'answer': 'They are both the same weight.',
|
|
||||||
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The difference lies in the volume and density of the materials, not the weight.'
|
|
||||||
# },
|
|
||||||
# 'parsing_error': None
|
|
||||||
# }
|
|
||||||
|
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
_ = kwargs.pop("strict", None)
|
_ = kwargs.pop("strict", None)
|
||||||
if kwargs:
|
if kwargs:
|
||||||
msg = f"Received unsupported arguments {kwargs}"
|
msg = f"Received unsupported arguments {kwargs}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
is_pydantic_schema = _is_pydantic_class(schema)
|
is_pydantic_schema = _is_pydantic_class(schema)
|
||||||
if method == "json_schema":
|
|
||||||
# Some applications require that incompatible parameters (e.g., unsupported
|
|
||||||
# methods) be handled.
|
|
||||||
method = "function_calling"
|
|
||||||
if method == "function_calling":
|
if method == "function_calling":
|
||||||
if schema is None:
|
if schema is None:
|
||||||
msg = (
|
msg = (
|
||||||
@@ -1120,6 +1162,35 @@ class ChatGroq(BaseChatModel):
|
|||||||
output_parser = JsonOutputKeyToolsParser(
|
output_parser = JsonOutputKeyToolsParser(
|
||||||
key_name=tool_name, first_tool_only=True
|
key_name=tool_name, first_tool_only=True
|
||||||
)
|
)
|
||||||
|
elif method == "json_schema":
|
||||||
|
# Use structured outputs (json_schema) for models that support it
|
||||||
|
# Convert schema to JSON Schema format for structured outputs
|
||||||
|
if schema is None:
|
||||||
|
msg = (
|
||||||
|
"schema must be specified when method is 'json_schema'. "
|
||||||
|
"Received None."
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
json_schema = convert_to_json_schema(schema)
|
||||||
|
schema_name = json_schema.get("title", "")
|
||||||
|
response_format = {
|
||||||
|
"type": "json_schema",
|
||||||
|
"json_schema": {"name": schema_name, "schema": json_schema},
|
||||||
|
}
|
||||||
|
ls_format_info = {
|
||||||
|
"kwargs": {"method": "json_schema"},
|
||||||
|
"schema": json_schema,
|
||||||
|
}
|
||||||
|
llm = self.bind(
|
||||||
|
response_format=response_format,
|
||||||
|
ls_structured_output_format=ls_format_info,
|
||||||
|
)
|
||||||
|
output_parser = (
|
||||||
|
PydanticOutputParser(pydantic_object=schema) # type: ignore[type-var, arg-type]
|
||||||
|
if is_pydantic_schema
|
||||||
|
else JsonOutputParser()
|
||||||
|
)
|
||||||
|
|
||||||
elif method == "json_mode":
|
elif method == "json_mode":
|
||||||
llm = self.bind(
|
llm = self.bind(
|
||||||
response_format={"type": "json_object"},
|
response_format={"type": "json_object"},
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
"""Standard LangChain interface tests."""
|
"""Standard LangChain interface tests."""
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.language_models import BaseChatModel
|
from langchain_core.language_models import BaseChatModel
|
||||||
from langchain_core.rate_limiters import InMemoryRateLimiter
|
from langchain_core.rate_limiters import InMemoryRateLimiter
|
||||||
@@ -13,11 +15,15 @@ from langchain_groq import ChatGroq
|
|||||||
rate_limiter = InMemoryRateLimiter(requests_per_second=0.2)
|
rate_limiter = InMemoryRateLimiter(requests_per_second=0.2)
|
||||||
|
|
||||||
|
|
||||||
class BaseTestGroq(ChatModelIntegrationTests):
|
class TestGroq(ChatModelIntegrationTests):
|
||||||
@property
|
@property
|
||||||
def chat_model_class(self) -> type[BaseChatModel]:
|
def chat_model_class(self) -> type[BaseChatModel]:
|
||||||
return ChatGroq
|
return ChatGroq
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat_model_params(self) -> dict:
|
||||||
|
return {"model": "llama-3.3-70b-versatile", "rate_limiter": rate_limiter}
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="Not yet implemented.")
|
@pytest.mark.xfail(reason="Not yet implemented.")
|
||||||
def test_tool_message_histories_list_content(
|
def test_tool_message_histories_list_content(
|
||||||
self, model: BaseChatModel, my_adder_tool: BaseTool
|
self, model: BaseChatModel, my_adder_tool: BaseTool
|
||||||
@@ -29,11 +35,23 @@ class BaseTestGroq(ChatModelIntegrationTests):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TestGroqGemma(BaseTestGroq):
|
@pytest.mark.parametrize("schema_type", ["pydantic", "typeddict", "json_schema"])
|
||||||
@property
|
def test_json_schema(
|
||||||
def chat_model_params(self) -> dict:
|
schema_type: Literal["pydantic", "typeddict", "json_schema"],
|
||||||
return {"model": "gemma2-9b-it", "rate_limiter": rate_limiter}
|
) -> None:
|
||||||
|
class JsonSchemaTests(ChatModelIntegrationTests):
|
||||||
|
@property
|
||||||
|
def chat_model_class(self) -> type[ChatGroq]:
|
||||||
|
return ChatGroq
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_json_mode(self) -> bool:
|
def chat_model_params(self) -> dict:
|
||||||
return True
|
return {"model": "openai/gpt-oss-120b", "rate_limiter": rate_limiter}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def structured_output_kwargs(self) -> dict:
|
||||||
|
return {"method": "json_schema"}
|
||||||
|
|
||||||
|
test_instance = JsonSchemaTests()
|
||||||
|
model = test_instance.chat_model_class(**test_instance.chat_model_params)
|
||||||
|
JsonSchemaTests().test_structured_output(model, schema_type)
|
||||||
|
Reference in New Issue
Block a user