Add MiniMax as alternative LLM provider in full-featured agent recipe - #134
Add MiniMax as alternative LLM provider in full-featured agent recipe#134octo-patch wants to merge 1 commit into
Conversation
- Extend _get_tool_model and _get_response_model in 02_full_featured_agent.ipynb to support model_name=minimax using ChatOpenAI with MiniMax OpenAI-compatible endpoint - Temperature clamped to 0.1 (MiniMax requires temperature > 0) - Strip think reasoning tags via RunnableLambda before structured output parsing - Add MiniMax to Integrations table in README.md - Add test_minimax_provider.py: 11 unit + 3 integration tests, all 14 pass
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f14131dbe5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" |
There was a problem hiding this comment.
Use escaped word boundaries in MiniMax choice regex
In _parse_minimax_choice the regex is serialized as r"\b([ABCD])\b" inside the notebook JSON string, but here \b is being interpreted by JSON as a backspace escape, so Python receives \x08([ABCD])\x08 instead of a word-boundary regex. That means normal MiniMax outputs like "A" or "Answer: B" do not match and this path raises ValueError, which breaks multi-choice responses whenever model_name="minimax" is used.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR adds MiniMax as an alternative LLM provider option for the full-featured LangGraph agent recipe, alongside a README integration entry and a new MiniMax-focused test module.
Changes:
- Extend the agent notebook’s model factory functions to support
model_name="minimax"via MiniMax’s OpenAI-compatible endpoint, including a custom post-processor for multiple-choice output. - Update
GraphConfig.model_nameLiteral to include"minimax"and add an optionalMINIMAX_API_KEYsetup hint. - Add MiniMax to the README integrations list and introduce a new MiniMax provider test file.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
python-recipes/agents/02_full_featured_agent.ipynb |
Adds MiniMax provider dispatch + MiniMax-specific multiple-choice parsing and updates GraphConfig to allow "minimax". |
python-recipes/agents/test_minimax_provider.py |
New unit + optional integration tests for MiniMax provider behavior and structured output parsing. |
README.md |
Adds MiniMax to the integrations table with a link to the notebook recipe. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" |
There was a problem hiding this comment.
In this notebook JSON, the regex pattern uses \b as \b (word boundary) inside a string literal (re.search(r"\b([ABCD])\b", ...)). In .ipynb JSON, \b is a backspace escape, so this will turn into an actual backspace character at runtime and the word-boundary regex won’t work. Re-save the notebook with the pattern written normally in the code cell (so it is persisted as \\b in JSON), or otherwise ensure the JSON contains escaped backslashes for the regex boundaries.
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" | |
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\\b([ABCD])\\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" |
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" |
There was a problem hiding this comment.
For the MiniMax path, api_key=os.environ.get("MINIMAX_API_KEY") will pass None if the env var isn’t set, which typically fails later with a less actionable error. Consider validating MINIMAX_API_KEY up front (and raising a clear ValueError telling the user to set it) when model_name == "minimax".
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" | |
| "from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\ndef _get_minimax_api_key() -> str:\n api_key = os.environ.get(\"MINIMAX_API_KEY\")\n if not api_key:\n raise ValueError(\n \"MINIMAX_API_KEY is not set. Please set the MINIMAX_API_KEY environment variable to use the 'minimax' model.\"\n )\n return api_key\n\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=_get_minimax_api_key(),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=_get_minimax_api_key(),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n" |
| """Unit and integration tests for MiniMax provider in the full-featured agent notebook.""" | ||
| import os | ||
| import unittest | ||
| from unittest.mock import MagicMock, patch | ||
|
|
There was a problem hiding this comment.
This test suite is added under python-recipes/agents/, but the repo’s GitHub Actions workflows only run notebooks via pytest --nbval-lax and don’t execute standalone test_*.py files. If these tests are meant to guard the MiniMax provider, wire them into CI (e.g., add a pytest job for python files) or move the assertions into a notebook cell that nbval will execute.
| """Unit and integration tests for MiniMax provider in the full-featured agent notebook.""" | ||
| import os | ||
| import unittest | ||
| from unittest.mock import MagicMock, patch |
There was a problem hiding this comment.
patch is imported but never used. Please remove the unused import to keep the test module clean.
| from unittest.mock import MagicMock, patch | |
| from unittest.mock import MagicMock |
| import os | ||
| import unittest | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
|
|
||
| # ─── helpers mirrored from 02_full_featured_agent.ipynb ────────────────────── | ||
|
|
||
| MINIMAX_BASE_URL = "https://api.minimax.io/v1" | ||
| MINIMAX_MODEL = "MiniMax-M2.5" | ||
| MINIMAX_TEMPERATURE = 0.1 # must be in (0, 1] | ||
|
|
||
|
|
||
| def _make_minimax_chat(**overrides): | ||
| """Return a ChatOpenAI-style config dict for MiniMax (used in unit tests).""" | ||
| cfg = { | ||
| "api_key": os.environ.get("MINIMAX_API_KEY", "dummy"), | ||
| "base_url": MINIMAX_BASE_URL, | ||
| "model_name": MINIMAX_MODEL, | ||
| "temperature": MINIMAX_TEMPERATURE, | ||
| } | ||
| cfg.update(overrides) | ||
| return cfg | ||
|
|
||
|
|
||
| # ─── Unit tests ─────────────────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| class TestMiniMaxConfig(unittest.TestCase): | ||
| """Validate MiniMax provider configuration constants.""" | ||
|
|
||
| def test_base_url(self): | ||
| self.assertEqual(MINIMAX_BASE_URL, "https://api.minimax.io/v1") | ||
|
|
||
| def test_model_name(self): | ||
| self.assertIn(MINIMAX_MODEL, ("MiniMax-M2.5", "MiniMax-M2.5-highspeed", | ||
| "MiniMax-M2.7", "MiniMax-M2.7-highspeed")) | ||
|
|
||
| def test_temperature_gt_zero(self): | ||
| """MiniMax rejects temperature == 0; must be in (0, 1].""" | ||
| self.assertGreater(MINIMAX_TEMPERATURE, 0.0) | ||
|
|
||
| def test_temperature_lte_one(self): | ||
| self.assertLessEqual(MINIMAX_TEMPERATURE, 1.0) | ||
|
|
||
| def test_config_keys(self): | ||
| cfg = _make_minimax_chat() | ||
| for key in ("api_key", "base_url", "model_name", "temperature"): | ||
| self.assertIn(key, cfg) | ||
|
|
||
|
|
||
| class TestProviderDispatch(unittest.TestCase): | ||
| """Ensure provider dispatch logic mirrors the notebook.""" | ||
|
|
||
| def _get_tool_model(self, model_name: str, mock_openai_cls): | ||
| """Simulate the notebook _get_tool_model function.""" | ||
| if model_name == "openai": | ||
| model = mock_openai_cls(temperature=0, model_name="gpt-4o") | ||
| elif model_name == "minimax": | ||
| model = mock_openai_cls( | ||
| api_key=os.environ.get("MINIMAX_API_KEY", "dummy"), | ||
| base_url=MINIMAX_BASE_URL, | ||
| model_name=MINIMAX_MODEL, | ||
| temperature=MINIMAX_TEMPERATURE, | ||
| ) | ||
| else: | ||
| raise ValueError(f"Unsupported model type: {model_name}") | ||
| return model | ||
|
|
||
| def test_openai_dispatch(self): | ||
| mock_cls = MagicMock(return_value=MagicMock()) | ||
| self._get_tool_model("openai", mock_cls) | ||
| mock_cls.assert_called_once_with(temperature=0, model_name="gpt-4o") | ||
|
|
||
| def test_minimax_dispatch(self): | ||
| mock_cls = MagicMock(return_value=MagicMock()) | ||
| self._get_tool_model("minimax", mock_cls) | ||
| mock_cls.assert_called_once_with( | ||
| api_key=os.environ.get("MINIMAX_API_KEY", "dummy"), | ||
| base_url=MINIMAX_BASE_URL, | ||
| model_name=MINIMAX_MODEL, | ||
| temperature=MINIMAX_TEMPERATURE, | ||
| ) | ||
|
|
||
| def test_invalid_provider_raises(self): | ||
| mock_cls = MagicMock() | ||
| with self.assertRaises(ValueError): | ||
| self._get_tool_model("unknown_provider", mock_cls) | ||
|
|
||
| def test_minimax_temperature_not_zero(self): | ||
| """Calling with minimax must never pass temperature=0.""" | ||
| calls = [] | ||
|
|
||
| def recording_cls(**kwargs): | ||
| calls.append(kwargs) | ||
| return MagicMock() | ||
|
|
||
| self._get_tool_model("minimax", recording_cls) | ||
| self.assertGreater(calls[0]["temperature"], 0.0, | ||
| "MiniMax requires temperature > 0") | ||
|
|
||
| def test_minimax_base_url_correct(self): | ||
| calls = [] | ||
|
|
||
| def recording_cls(**kwargs): | ||
| calls.append(kwargs) | ||
| return MagicMock() | ||
|
|
||
| self._get_tool_model("minimax", recording_cls) | ||
| self.assertEqual(calls[0]["base_url"], "https://api.minimax.io/v1") | ||
|
|
||
|
|
There was a problem hiding this comment.
The unit tests re-implement _get_tool_model/GraphConfig locally, so they don’t actually validate that the notebook’s code stays in sync (the notebook could change and these tests would still pass). If possible, consider parsing 02_full_featured_agent.ipynb as JSON and asserting the provider dispatch snippets/constants exist there, or refactor shared provider logic into an importable .py module that both the notebook and tests use.
| import os | |
| import unittest | |
| from unittest.mock import MagicMock, patch | |
| # ─── helpers mirrored from 02_full_featured_agent.ipynb ────────────────────── | |
| MINIMAX_BASE_URL = "https://api.minimax.io/v1" | |
| MINIMAX_MODEL = "MiniMax-M2.5" | |
| MINIMAX_TEMPERATURE = 0.1 # must be in (0, 1] | |
| def _make_minimax_chat(**overrides): | |
| """Return a ChatOpenAI-style config dict for MiniMax (used in unit tests).""" | |
| cfg = { | |
| "api_key": os.environ.get("MINIMAX_API_KEY", "dummy"), | |
| "base_url": MINIMAX_BASE_URL, | |
| "model_name": MINIMAX_MODEL, | |
| "temperature": MINIMAX_TEMPERATURE, | |
| } | |
| cfg.update(overrides) | |
| return cfg | |
| # ─── Unit tests ─────────────────────────────────────────────────────────────── | |
| class TestMiniMaxConfig(unittest.TestCase): | |
| """Validate MiniMax provider configuration constants.""" | |
| def test_base_url(self): | |
| self.assertEqual(MINIMAX_BASE_URL, "https://api.minimax.io/v1") | |
| def test_model_name(self): | |
| self.assertIn(MINIMAX_MODEL, ("MiniMax-M2.5", "MiniMax-M2.5-highspeed", | |
| "MiniMax-M2.7", "MiniMax-M2.7-highspeed")) | |
| def test_temperature_gt_zero(self): | |
| """MiniMax rejects temperature == 0; must be in (0, 1].""" | |
| self.assertGreater(MINIMAX_TEMPERATURE, 0.0) | |
| def test_temperature_lte_one(self): | |
| self.assertLessEqual(MINIMAX_TEMPERATURE, 1.0) | |
| def test_config_keys(self): | |
| cfg = _make_minimax_chat() | |
| for key in ("api_key", "base_url", "model_name", "temperature"): | |
| self.assertIn(key, cfg) | |
| class TestProviderDispatch(unittest.TestCase): | |
| """Ensure provider dispatch logic mirrors the notebook.""" | |
| def _get_tool_model(self, model_name: str, mock_openai_cls): | |
| """Simulate the notebook _get_tool_model function.""" | |
| if model_name == "openai": | |
| model = mock_openai_cls(temperature=0, model_name="gpt-4o") | |
| elif model_name == "minimax": | |
| model = mock_openai_cls( | |
| api_key=os.environ.get("MINIMAX_API_KEY", "dummy"), | |
| base_url=MINIMAX_BASE_URL, | |
| model_name=MINIMAX_MODEL, | |
| temperature=MINIMAX_TEMPERATURE, | |
| ) | |
| else: | |
| raise ValueError(f"Unsupported model type: {model_name}") | |
| return model | |
| def test_openai_dispatch(self): | |
| mock_cls = MagicMock(return_value=MagicMock()) | |
| self._get_tool_model("openai", mock_cls) | |
| mock_cls.assert_called_once_with(temperature=0, model_name="gpt-4o") | |
| def test_minimax_dispatch(self): | |
| mock_cls = MagicMock(return_value=MagicMock()) | |
| self._get_tool_model("minimax", mock_cls) | |
| mock_cls.assert_called_once_with( | |
| api_key=os.environ.get("MINIMAX_API_KEY", "dummy"), | |
| base_url=MINIMAX_BASE_URL, | |
| model_name=MINIMAX_MODEL, | |
| temperature=MINIMAX_TEMPERATURE, | |
| ) | |
| def test_invalid_provider_raises(self): | |
| mock_cls = MagicMock() | |
| with self.assertRaises(ValueError): | |
| self._get_tool_model("unknown_provider", mock_cls) | |
| def test_minimax_temperature_not_zero(self): | |
| """Calling with minimax must never pass temperature=0.""" | |
| calls = [] | |
| def recording_cls(**kwargs): | |
| calls.append(kwargs) | |
| return MagicMock() | |
| self._get_tool_model("minimax", recording_cls) | |
| self.assertGreater(calls[0]["temperature"], 0.0, | |
| "MiniMax requires temperature > 0") | |
| def test_minimax_base_url_correct(self): | |
| calls = [] | |
| def recording_cls(**kwargs): | |
| calls.append(kwargs) | |
| return MagicMock() | |
| self._get_tool_model("minimax", recording_cls) | |
| self.assertEqual(calls[0]["base_url"], "https://api.minimax.io/v1") | |
| import json | |
| import os | |
| import unittest | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch | |
| NOTEBOOK_PATH = Path(__file__).with_name("02_full_featured_agent.ipynb") | |
| MINIMAX_ALLOWED_MODELS = ( | |
| "MiniMax-M2.5", | |
| "MiniMax-M2.5-highspeed", | |
| "MiniMax-M2.7", | |
| "MiniMax-M2.7-highspeed", | |
| ) | |
| def _load_notebook_source(): | |
| """Load and flatten source from code cells in 02_full_featured_agent.ipynb.""" | |
| with NOTEBOOK_PATH.open("r", encoding="utf-8") as notebook_file: | |
| notebook = json.load(notebook_file) | |
| code_chunks = [] | |
| for cell in notebook.get("cells", []): | |
| if cell.get("cell_type") != "code": | |
| continue | |
| source = cell.get("source", []) | |
| if isinstance(source, list): | |
| code_chunks.append("".join(source)) | |
| else: | |
| code_chunks.append(source) | |
| return "\n".join(code_chunks) | |
| def _assert_contains_any(test_case, source, snippets, message): | |
| """Assert that at least one of the given snippets appears in notebook source.""" | |
| test_case.assertTrue( | |
| any(snippet in source for snippet in snippets), | |
| message, | |
| ) | |
| # ─── Unit tests ─────────────────────────────────────────────────────────────── | |
| class TestMiniMaxConfig(unittest.TestCase): | |
| """Validate MiniMax provider configuration constants from the notebook.""" | |
| @classmethod | |
| def setUpClass(cls): | |
| cls.notebook_source = _load_notebook_source() | |
| def test_base_url(self): | |
| self.assertIn( | |
| 'MINIMAX_BASE_URL = "https://api.minimax.io/v1"', | |
| self.notebook_source, | |
| ) | |
| def test_model_name(self): | |
| _assert_contains_any( | |
| self, | |
| self.notebook_source, | |
| [f'MINIMAX_MODEL = "{model}"' for model in MINIMAX_ALLOWED_MODELS], | |
| "Notebook must define MINIMAX_MODEL to a supported MiniMax model.", | |
| ) | |
| def test_temperature_gt_zero(self): | |
| """MiniMax rejects temperature == 0; must be in (0, 1].""" | |
| _assert_contains_any( | |
| self, | |
| self.notebook_source, | |
| [ | |
| "MINIMAX_TEMPERATURE = 0.1", | |
| "MINIMAX_TEMPERATURE = 0.2", | |
| "MINIMAX_TEMPERATURE = 0.3", | |
| "MINIMAX_TEMPERATURE = 0.5", | |
| "MINIMAX_TEMPERATURE = 1.0", | |
| ], | |
| "Notebook must define MINIMAX_TEMPERATURE to a value greater than 0.", | |
| ) | |
| def test_temperature_lte_one(self): | |
| self.assertNotIn( | |
| "MINIMAX_TEMPERATURE = 0", | |
| self.notebook_source, | |
| ) | |
| def test_config_keys(self): | |
| for key_snippet in ( | |
| 'api_key=os.environ.get("MINIMAX_API_KEY"', | |
| "base_url=MINIMAX_BASE_URL", | |
| "model_name=MINIMAX_MODEL", | |
| "temperature=MINIMAX_TEMPERATURE", | |
| ): | |
| self.assertIn(key_snippet, self.notebook_source) | |
| class TestProviderDispatch(unittest.TestCase): | |
| """Ensure provider dispatch logic is asserted against the notebook itself.""" | |
| @classmethod | |
| def setUpClass(cls): | |
| cls.notebook_source = _load_notebook_source() | |
| def test_openai_dispatch(self): | |
| self.assertIn('model_name == "openai"', self.notebook_source) | |
| self.assertIn('temperature=0', self.notebook_source) | |
| self.assertIn('model_name="gpt-4o"', self.notebook_source) | |
| def test_minimax_dispatch(self): | |
| self.assertIn('model_name == "minimax"', self.notebook_source) | |
| for snippet in ( | |
| 'api_key=os.environ.get("MINIMAX_API_KEY"', | |
| "base_url=MINIMAX_BASE_URL", | |
| "model_name=MINIMAX_MODEL", | |
| "temperature=MINIMAX_TEMPERATURE", | |
| ): | |
| self.assertIn(snippet, self.notebook_source) | |
| def test_invalid_provider_raises(self): | |
| self.assertIn( | |
| 'raise ValueError(f"Unsupported model type: {model_name}")', | |
| self.notebook_source, | |
| ) | |
| def test_minimax_temperature_not_zero(self): | |
| """Calling with minimax must never pass temperature=0.""" | |
| self.assertIn("temperature=MINIMAX_TEMPERATURE", self.notebook_source) | |
| self.assertNotIn( | |
| 'model_name == "minimax":\n model = mock_openai_cls(\n api_key=os.environ.get("MINIMAX_API_KEY", "dummy"),\n base_url=MINIMAX_BASE_URL,\n model_name=MINIMAX_MODEL,\n temperature=0', | |
| self.notebook_source, | |
| ) | |
| def test_minimax_base_url_correct(self): | |
| self.assertIn( | |
| 'MINIMAX_BASE_URL = "https://api.minimax.io/v1"', | |
| self.notebook_source, | |
| ) | |
| self.assertIn("base_url=MINIMAX_BASE_URL", self.notebook_source) |
| if "langchain" not in sys.modules: | ||
| langchain_stub = types.ModuleType("langchain") | ||
| langchain_stub.verbose = False | ||
| sys.modules["langchain"] = langchain_stub | ||
| else: | ||
| lc = sys.modules["langchain"] | ||
| if not hasattr(lc, "verbose"): | ||
| lc.verbose = False | ||
|
|
There was a problem hiding this comment.
setUpClass mutates sys.modules["langchain"] (inserting a stub or adding verbose) but never restores the previous state. If these integration tests are run alongside other tests in the same process, this global state can leak and affect unrelated tests. Consider saving the prior module/attribute state and restoring it in tearDownClass.
| if "langchain" not in sys.modules: | |
| langchain_stub = types.ModuleType("langchain") | |
| langchain_stub.verbose = False | |
| sys.modules["langchain"] = langchain_stub | |
| else: | |
| lc = sys.modules["langchain"] | |
| if not hasattr(lc, "verbose"): | |
| lc.verbose = False | |
| cls._had_langchain_module = "langchain" in sys.modules | |
| cls._original_langchain_module = sys.modules.get("langchain") | |
| cls._had_langchain_verbose = ( | |
| cls._had_langchain_module | |
| and hasattr(cls._original_langchain_module, "verbose") | |
| ) | |
| cls._original_langchain_verbose = ( | |
| getattr(cls._original_langchain_module, "verbose") | |
| if cls._had_langchain_verbose | |
| else None | |
| ) | |
| if not cls._had_langchain_module: | |
| langchain_stub = types.ModuleType("langchain") | |
| langchain_stub.verbose = False | |
| sys.modules["langchain"] = langchain_stub | |
| else: | |
| lc = cls._original_langchain_module | |
| if not hasattr(lc, "verbose"): | |
| lc.verbose = False | |
| @classmethod | |
| def tearDownClass(cls): | |
| import sys | |
| if not getattr(cls, "_had_langchain_module", False): | |
| sys.modules.pop("langchain", None) | |
| else: | |
| sys.modules["langchain"] = cls._original_langchain_module | |
| if cls._had_langchain_verbose: | |
| cls._original_langchain_module.verbose = cls._original_langchain_verbose | |
| elif hasattr(cls._original_langchain_module, "verbose"): | |
| delattr(cls._original_langchain_module, "verbose") |
nkanu17
left a comment
There was a problem hiding this comment.
@octo-patch thank you for your contribution! Please take a look at the suggested bot changes!
Summary
This PR adds MiniMax as an alternative LLM provider in the full-featured agent recipe notebook (
python-recipes/agents/02_full_featured_agent.ipynb).Changes
02_full_featured_agent.ipynb: Extend_get_tool_modeland_get_response_modelto acceptmodel_name="minimax"alongside the existing"openai"option. Useslangchain-openaiChatOpenAIpointed at MiniMax's OpenAI-compatible endpoint (https://api.minimax.io/v1, modelMiniMax-M2.5).0.1(MiniMax requirestemperature ∈ (0, 1]).<think>...</think>reasoning tokens; these are stripped via aRunnableLambdapost-processor before the multiple-choice letter is regex-extracted — keeping structured output working withoutwith_structured_output.GraphConfig.model_nameLiteral updated to include"minimax".MINIMAX_API_KEYenv-var hint added to the setup cell.README.md: MiniMax added to the Integrations table.python-recipes/agents/test_minimax_provider.py: New test file with 11 unit tests (config validation, provider dispatch, Literal check) + 3 integration tests (basic invoke, tool binding, structured output). Integration tests are automatically skipped whenMINIMAX_API_KEYis not set. All 14 tests pass locally.Usage
To run the agent with MiniMax instead of OpenAI:
MiniMax M2.5 provides a 204K context window and is available at https://www.minimaxi.com/en.