diff --git a/libs/partners/groq/langchain_groq/chat_models.py b/libs/partners/groq/langchain_groq/chat_models.py index 443692dbe6c..ac23d93ae5c 100644 --- a/libs/partners/groq/langchain_groq/chat_models.py +++ b/libs/partners/groq/langchain_groq/chat_models.py @@ -51,6 +51,7 @@ from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough from langchain_core.tools import BaseTool from langchain_core.utils import from_env, get_pydantic_field_names, secret_from_env from langchain_core.utils.function_calling import ( + convert_to_json_schema, convert_to_openai_function, convert_to_openai_tool, ) @@ -503,8 +504,13 @@ class ChatGroq(BaseChatModel): async_api=async_api, run_manager=run_manager, **kwargs ) if base_should_stream and ("response_format" in kwargs): - # Streaming not supported in JSON mode. - return kwargs["response_format"] != {"type": "json_object"} + # Streaming not supported in JSON mode or structured outputs. + 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 def _generate( @@ -850,7 +856,9 @@ class ChatGroq(BaseChatModel): self, 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, **kwargs: Any, ) -> Runnable[LanguageModelInput, dict | BaseModel]: @@ -875,12 +883,34 @@ class ChatGroq(BaseChatModel): 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 `__ + - ``'json_schema'``: + Uses Groq's `Structured Output API `__. + Supported for a subset of models, including ``openai/gpt-oss``, + ``moonshotai/kimi-k2-instruct``, and some ``meta-llama/llama-4`` + models. See `docs `__ + for details. + - ``'json_mode'``: + Uses Groq's `JSON 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 `__. + method: The method for steering model generation, either ``'function_calling'`` 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 - function-calling API. If ``'json_mode'`` then OpenAI's JSON mode will be - used. + function-calling API. If ``'json_mode'`` then JSON mode will be used. .. note:: 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.invoke( @@ -964,7 +994,7 @@ class ChatGroq(BaseChatModel): 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( 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.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.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.' # } + 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: .. code-block:: @@ -1047,7 +1112,7 @@ class ChatGroq(BaseChatModel): answer: 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( AnswerWithJustification, method="json_mode", @@ -1065,35 +1130,12 @@ class ChatGroq(BaseChatModel): # '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 _ = kwargs.pop("strict", None) if kwargs: msg = f"Received unsupported arguments {kwargs}" raise ValueError(msg) 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 schema is None: msg = ( @@ -1120,6 +1162,35 @@ class ChatGroq(BaseChatModel): output_parser = JsonOutputKeyToolsParser( 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": llm = self.bind( response_format={"type": "json_object"}, diff --git a/libs/partners/groq/tests/integration_tests/test_standard.py b/libs/partners/groq/tests/integration_tests/test_standard.py index 4d3eb6b0d7a..2e449441171 100644 --- a/libs/partners/groq/tests/integration_tests/test_standard.py +++ b/libs/partners/groq/tests/integration_tests/test_standard.py @@ -1,5 +1,7 @@ """Standard LangChain interface tests.""" +from typing import Literal + import pytest from langchain_core.language_models import BaseChatModel from langchain_core.rate_limiters import InMemoryRateLimiter @@ -13,11 +15,15 @@ from langchain_groq import ChatGroq rate_limiter = InMemoryRateLimiter(requests_per_second=0.2) -class BaseTestGroq(ChatModelIntegrationTests): +class TestGroq(ChatModelIntegrationTests): @property def chat_model_class(self) -> type[BaseChatModel]: 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.") def test_tool_message_histories_list_content( self, model: BaseChatModel, my_adder_tool: BaseTool @@ -29,11 +35,23 @@ class BaseTestGroq(ChatModelIntegrationTests): return True -class TestGroqGemma(BaseTestGroq): - @property - def chat_model_params(self) -> dict: - return {"model": "gemma2-9b-it", "rate_limiter": rate_limiter} +@pytest.mark.parametrize("schema_type", ["pydantic", "typeddict", "json_schema"]) +def test_json_schema( + schema_type: Literal["pydantic", "typeddict", "json_schema"], +) -> None: + class JsonSchemaTests(ChatModelIntegrationTests): + @property + def chat_model_class(self) -> type[ChatGroq]: + return ChatGroq - @property - def supports_json_mode(self) -> bool: - return True + @property + def chat_model_params(self) -> dict: + 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)