diff --git a/docs/v3/large-language-models.mdx b/docs/v3/large-language-models.mdx index d3f95a659..4b1d8fb26 100644 --- a/docs/v3/large-language-models.mdx +++ b/docs/v3/large-language-models.mdx @@ -90,6 +90,37 @@ llm = AzureOpenAI(api_base="https://.openai.azure.com/", pai.config.set({"llm": llm}) ``` +## MiniMax models + +[MiniMax](https://www.minimaxi.com/) provides powerful large language models with up to 204K context window. + +Install the pandasai-minimax extension: + +```bash +# Using poetry +poetry add pandasai-minimax + +# Using pip +pip install pandasai-minimax +``` + +In order to use MiniMax models, you need to have a MiniMax API key. You can get one from the [MiniMax platform](https://www.minimaxi.com/). +Once you have an API key, you can use it to instantiate a MiniMax object: + +Configure MiniMax: + +```python +import pandasai as pai +from pandasai_minimax import MiniMax + +llm = MiniMax(api_token="my-minimax-api-key") +# Or set MINIMAX_API_KEY environment variable + +pai.config.set({"llm": llm}) +``` + +Supported models: `MiniMax-M3` (default), `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`. + ## How to set up any LLM? LiteLLM provides a unified interface to interact with 100+ LLM models from various providers including OpenAI, Azure, Anthropic, Google, AWS, Hugging Face, and many more. This makes it easy to switch between different LLM providers without changing your code. diff --git a/extensions/llms/minimax/README.md b/extensions/llms/minimax/README.md new file mode 100644 index 000000000..1441af934 --- /dev/null +++ b/extensions/llms/minimax/README.md @@ -0,0 +1,55 @@ +# MiniMax Extension for PandasAI + +This extension integrates [MiniMax](https://www.minimaxi.com/) with PandasAI, providing MiniMax LLM support. + +MiniMax offers powerful large language models with up to 204K context window through an OpenAI-compatible API. + +## Installation + +```bash +# Using pip +pip install pandasai-minimax + +# Using poetry +poetry add pandasai-minimax +``` + +## Usage + +```python +import pandasai as pai +from pandasai_minimax import MiniMax + +llm = MiniMax(api_token="your-minimax-api-key") +# Or set MINIMAX_API_KEY environment variable + +pai.config.set({"llm": llm}) +``` + +## Supported Models + +| Model | Context Window | Description | +|-------|---------------|-------------| +| `MiniMax-M3` (default) | 512K | Latest model with 128K max output, supports image input | +| `MiniMax-M2.7` | 204K | Previous generation, most capable | +| `MiniMax-M2.7-highspeed` | 204K | Previous generation, optimized for speed | + +## Configuration + +You can pass additional parameters: + +```python +llm = MiniMax( + api_token="your-minimax-api-key", + model="MiniMax-M2.7-highspeed", + temperature=0.3, + max_tokens=2000, +) +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MINIMAX_API_KEY` | Your MiniMax API key | +| `MINIMAX_API_BASE` | Custom API base URL (default: `https://api.minimax.io/v1`) | diff --git a/extensions/llms/minimax/pandasai_minimax/__init__.py b/extensions/llms/minimax/pandasai_minimax/__init__.py new file mode 100644 index 000000000..c7d3e77e2 --- /dev/null +++ b/extensions/llms/minimax/pandasai_minimax/__init__.py @@ -0,0 +1,3 @@ +from .minimax import MiniMax + +__all__ = ["MiniMax"] diff --git a/extensions/llms/minimax/pandasai_minimax/minimax.py b/extensions/llms/minimax/pandasai_minimax/minimax.py new file mode 100644 index 000000000..f656e65ef --- /dev/null +++ b/extensions/llms/minimax/pandasai_minimax/minimax.py @@ -0,0 +1,81 @@ +import os +from typing import Any, Dict, Optional + +import openai + +from pandasai.exceptions import APIKeyNotFoundError, UnsupportedModelError +from pandasai.helpers import load_dotenv + +from pandasai_openai.base import BaseOpenAI + +load_dotenv() + + +class MiniMax(BaseOpenAI): + """MiniMax LLM using BaseOpenAI Class. + + MiniMax provides an OpenAI-compatible API. This class connects to MiniMax's + API endpoint and supports MiniMax chat models. + + The default model is **MiniMax-M3** (512K context, 128K max output, supports + image input). Supported models include: MiniMax-M3, MiniMax-M2.7, + MiniMax-M2.7-highspeed. + """ + + _supported_chat_models = [ + "MiniMax-M3", + "MiniMax-M2.7", + "MiniMax-M2.7-highspeed", + ] + + model: str = "MiniMax-M3" + api_base: str = "https://api.minimax.io/v1" + temperature: float = 0.1 + + def __init__( + self, + api_token: Optional[str] = None, + **kwargs, + ): + """ + __init__ method of MiniMax Class. + + Args: + api_token (str): API Token for MiniMax platform. + Can also be set via MINIMAX_API_KEY environment variable. + **kwargs: Extended Parameters inferred from BaseOpenAI class. + """ + self.api_token = api_token or os.getenv("MINIMAX_API_KEY") or None + + if not self.api_token: + raise APIKeyNotFoundError("MiniMax API key is required") + + self.api_base = ( + kwargs.get("api_base") + or os.getenv("MINIMAX_API_BASE") + or self.api_base + ) + + self.openai_proxy = kwargs.get("openai_proxy") or os.getenv("OPENAI_PROXY") + if self.openai_proxy: + openai.proxy = {"http": self.openai_proxy, "https": self.openai_proxy} + + self._set_params(**kwargs) + + if self.model not in self._supported_chat_models: + raise UnsupportedModelError(self.model) + + self._is_chat_model = True + self.client = openai.OpenAI(**self._client_params).chat.completions + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling MiniMax API.""" + return { + **super()._default_params, + "model": self.model, + } + + @property + def type(self) -> str: + return "minimax" diff --git a/extensions/llms/minimax/pyproject.toml b/extensions/llms/minimax/pyproject.toml new file mode 100644 index 000000000..dbc54c844 --- /dev/null +++ b/extensions/llms/minimax/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "pandasai-minimax" +version = "0.1.0" +description = "MiniMax integration for PandasAI" +authors = ["PandasAI"] +license = "MIT" +readme = "README.md" + +[tool.poetry.urls] +"Documentation" = "https://docs.pandas-ai.com/" +"Repository" = "https://github.com/sinaptik-ai/pandas-ai" + +[tool.poetry.dependencies] +python = ">=3.8,<3.12" +pandasai = ">=3.0.0b4" +pandasai-openai = ">=0.1.0" +openai = "^1.3.7" +typing-extensions = "^4.0.0" + +[tool.poetry.group.test] +optional = true + +[tool.poetry.group.test.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/extensions/llms/minimax/tests/__init__.py b/extensions/llms/minimax/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/llms/minimax/tests/test_minimax.py b/extensions/llms/minimax/tests/test_minimax.py new file mode 100644 index 000000000..ddedecdf2 --- /dev/null +++ b/extensions/llms/minimax/tests/test_minimax.py @@ -0,0 +1,160 @@ +"""Unit tests for the MiniMax LLM class""" + +import os +from unittest import mock + +import openai +import pytest + +from extensions.llms.minimax.pandasai_minimax import MiniMax +from pandasai.core.prompts.base import BasePrompt +from pandasai.exceptions import APIKeyNotFoundError, UnsupportedModelError + + +class OpenAIObject: + def __init__(self, dictionary): + self.__dict__.update(dictionary) + + +class TestMiniMaxLLM: + """Unit tests for the MiniMax LLM class""" + + @pytest.fixture + def prompt(self): + class MockBasePrompt(BasePrompt): + template: str = "instruction" + + return MockBasePrompt() + + def test_type_without_token(self): + with mock.patch.dict(os.environ, clear=True): + with pytest.raises(APIKeyNotFoundError): + MiniMax() + + def test_type_with_token(self): + assert MiniMax(api_token="test").type == "minimax" + + def test_default_model(self): + llm = MiniMax(api_token="test") + assert llm.model == "MiniMax-M3" + + def test_default_api_base(self): + llm = MiniMax(api_token="test") + assert llm.api_base == "https://api.minimax.io/v1" + + def test_default_temperature(self): + llm = MiniMax(api_token="test") + assert llm.temperature == 0.1 + + def test_proxy(self): + proxy = "http://proxy.mycompany.com:8080" + client = MiniMax(api_token="test", openai_proxy=proxy) + assert client.openai_proxy == proxy + assert openai.proxy["http"] == proxy + assert openai.proxy["https"] == proxy + + def test_params_setting(self): + llm = MiniMax( + api_token="test", + model="MiniMax-M2.7-highspeed", + temperature=0.5, + max_tokens=50, + top_p=1.0, + frequency_penalty=2.0, + presence_penalty=3.0, + stop=["\n"], + ) + + assert llm.model == "MiniMax-M2.7-highspeed" + assert llm.temperature == 0.5 + assert llm.max_tokens == 50 + assert llm.top_p == 1.0 + assert llm.frequency_penalty == 2.0 + assert llm.presence_penalty == 3.0 + assert llm.stop == ["\n"] + + def test_completion(self, mocker): + expected_text = "This is the generated text." + expected_response = OpenAIObject( + { + "choices": [{"text": expected_text}], + "usage": { + "prompt_tokens": 2, + "completion_tokens": 1, + "total_tokens": 3, + }, + "model": "MiniMax-M3", + } + ) + + minimax = MiniMax(api_token="test") + mocker.patch.object(minimax, "chat_completion", return_value=expected_response) + result = minimax.chat_completion("Some prompt.") + + minimax.chat_completion.assert_called_once_with("Some prompt.") + assert result == expected_response + + def test_chat_completion(self, mocker): + minimax = MiniMax(api_token="test") + expected_response = OpenAIObject( + { + "choices": [ + { + "text": "Hello, how can I help you today?", + "index": 0, + "logprobs": None, + "finish_reason": "stop", + "start_text": "", + } + ] + } + ) + + mocker.patch.object( + minimax, "chat_completion", return_value=expected_response + ) + + result = minimax.chat_completion("Hi") + minimax.chat_completion.assert_called_once_with("Hi") + + assert result == expected_response + + def test_call_with_unsupported_model(self, prompt): + with pytest.raises( + UnsupportedModelError, + ): + MiniMax(api_token="test", model="not-a-model") + + def test_call_m3_model(self, mocker, prompt): + minimax = MiniMax(api_token="test", model="MiniMax-M3") + mocker.patch.object(minimax, "chat_completion", return_value="response") + + result = minimax.call(instruction=prompt) + assert result == "response" + + def test_call_m27_model(self, mocker, prompt): + minimax = MiniMax(api_token="test", model="MiniMax-M2.7") + mocker.patch.object(minimax, "chat_completion", return_value="response") + + result = minimax.call(instruction=prompt) + assert result == "response" + + def test_call_m27_highspeed_model(self, mocker, prompt): + minimax = MiniMax(api_token="test", model="MiniMax-M2.7-highspeed") + mocker.patch.object(minimax, "chat_completion", return_value="response") + + result = minimax.call(instruction=prompt) + assert result == "response" + + def test_is_chat_model(self): + llm = MiniMax(api_token="test") + assert llm._is_chat_model is True + + def test_env_api_key(self): + with mock.patch.dict(os.environ, {"MINIMAX_API_KEY": "env-test-key"}): + llm = MiniMax() + assert llm.api_token == "env-test-key" + + def test_custom_api_base(self): + llm = MiniMax(api_token="test", api_base="https://custom.api.com/v1") + assert llm.api_base == "https://custom.api.com/v1"