diff --git a/mem0/llms/groq.py b/mem0/llms/groq.py index 4fba88f4b..033577711 100644 --- a/mem0/llms/groq.py +++ b/mem0/llms/groq.py @@ -1,6 +1,7 @@ import json +import logging import os -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union try: from groq import Groq @@ -11,6 +12,8 @@ from mem0.configs.llms.base import BaseLlmConfig from mem0.llms.base import LLMBase from mem0.memory.utils import extract_json +logger = logging.getLogger(__name__) + class GroqLLM(LLMBase): def __init__(self, config: Optional[BaseLlmConfig] = None): @@ -22,6 +25,24 @@ class GroqLLM(LLMBase): api_key = self.config.api_key or os.getenv("GROQ_API_KEY") self.client = Groq(api_key=api_key) + @staticmethod + def _supports_json_mode(model: Optional[Union[str, Dict]]) -> bool: + """ + Groq's compound agentic systems (e.g. ``groq/compound``, ``groq/compound-mini``) + do not support the JSON ``response_format`` and return empty or non-JSON content + when it is requested. See https://console.groq.com/docs/structured-outputs. + + Non-string models (the config allows a dict) are assumed to support JSON mode, + preserving prior behavior. + """ + if not isinstance(model, str): + return True + # Strip provider prefixes (e.g. "groq/compound-mini" -> "compound-mini"), + # mirroring the _is_reasoning_model heuristic in LLMBase, so the match + # targets the compound family rather than any name containing the substring. + base_model = model.lower().rsplit("/", 1)[-1] + return not base_model.startswith("compound") + def _parse_response(self, response, tools): """ Process the response based on whether tools are used or not. @@ -79,7 +100,17 @@ class GroqLLM(LLMBase): "top_p": self.config.top_p, } if response_format: - params["response_format"] = response_format + requests_json = isinstance(response_format, dict) and response_format.get("type") in ( + "json_object", + "json_schema", + ) + if requests_json and not self._supports_json_mode(self.config.model): + logger.debug( + f"Model '{self.config.model}' does not support JSON response_format; " + "sending the request without it." + ) + else: + params["response_format"] = response_format if tools: params["tools"] = tools params["tool_choice"] = tool_choice diff --git a/tests/llms/test_groq.py b/tests/llms/test_groq.py index 288b37f80..ddf403263 100644 --- a/tests/llms/test_groq.py +++ b/tests/llms/test_groq.py @@ -84,3 +84,101 @@ def test_generate_response_with_tools(mock_groq_client): assert len(response["tool_calls"]) == 1 assert response["tool_calls"][0]["name"] == "add_memory" assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."} + + +@pytest.mark.parametrize("model", ["groq/compound", "groq/compound-mini"]) +def test_generate_response_skips_json_mode_for_compound_models(mock_groq_client, model): + config = BaseLlmConfig(model=model, temperature=0.7, max_tokens=100, top_p=1.0) + llm = GroqLLM(config) + messages = [{"role": "user", "content": "Hi, I'm Alice and I love hiking."}] + + # Compound models answer JSON-mode requests with empty or non-JSON content; + # the mock mirrors that plain-text reply. These tests pin request + # construction (response_format omitted), not end-to-end extraction. + mock_response = Mock() + mock_response.choices = [Mock(message=Mock(content="Alice introduced herself and mentioned she loves hiking."))] + mock_groq_client.chat.completions.create.return_value = mock_response + + llm.generate_response(messages, response_format={"type": "json_object"}) + + _, kwargs = mock_groq_client.chat.completions.create.call_args + assert "response_format" not in kwargs + + +def test_generate_response_keeps_json_mode_for_standard_model(mock_groq_client): + config = BaseLlmConfig(model="llama-3.3-70b-versatile", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GroqLLM(config) + messages = [{"role": "user", "content": "Hi, I'm Alice and I love hiking."}] + + mock_response = Mock() + mock_response.choices = [Mock(message=Mock(content='{"memory": ["Name is Alice", "Loves hiking"]}'))] + mock_groq_client.chat.completions.create.return_value = mock_response + + llm.generate_response(messages, response_format={"type": "json_object"}) + + _, kwargs = mock_groq_client.chat.completions.create.call_args + assert kwargs["response_format"] == {"type": "json_object"} + + +def test_generate_response_keeps_non_json_response_format_for_compound_model(mock_groq_client): + config = BaseLlmConfig(model="groq/compound", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GroqLLM(config) + messages = [{"role": "user", "content": "Hi, I'm Alice and I love hiking."}] + + mock_response = Mock() + mock_response.choices = [Mock(message=Mock(content="Alice loves hiking."))] + mock_groq_client.chat.completions.create.return_value = mock_response + + llm.generate_response(messages, response_format={"type": "text"}) + + _, kwargs = mock_groq_client.chat.completions.create.call_args + assert kwargs["response_format"] == {"type": "text"} + + +def test_generate_response_keeps_tools_when_skipping_json_mode(mock_groq_client): + config = BaseLlmConfig(model="groq/compound", temperature=0.7, max_tokens=100, top_p=1.0) + llm = GroqLLM(config) + messages = [{"role": "user", "content": "Add a new memory: Today is a sunny day."}] + tools = [ + { + "type": "function", + "function": { + "name": "add_memory", + "description": "Add a memory", + "parameters": { + "type": "object", + "properties": {"data": {"type": "string", "description": "Data to add to memory"}}, + "required": ["data"], + }, + }, + } + ] + + mock_response = Mock() + mock_message = Mock() + mock_message.content = "Done." + mock_message.tool_calls = None + mock_response.choices = [Mock(message=mock_message)] + mock_groq_client.chat.completions.create.return_value = mock_response + + llm.generate_response(messages, response_format={"type": "json_object"}, tools=tools) + + _, kwargs = mock_groq_client.chat.completions.create.call_args + assert "response_format" not in kwargs + assert kwargs["tools"] == tools + assert kwargs["tool_choice"] == "auto" + + +def test_generate_response_handles_non_string_model(mock_groq_client): + config = BaseLlmConfig(model={"name": "custom-model"}, temperature=0.7, max_tokens=100, top_p=1.0) + llm = GroqLLM(config) + messages = [{"role": "user", "content": "Hi, I'm Alice and I love hiking."}] + + mock_response = Mock() + mock_response.choices = [Mock(message=Mock(content='{"memory": []}'))] + mock_groq_client.chat.completions.create.return_value = mock_response + + llm.generate_response(messages, response_format={"type": "json_object"}) + + _, kwargs = mock_groq_client.chat.completions.create.call_args + assert kwargs["response_format"] == {"type": "json_object"}