Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Play ChatGPT and other LLM with Xiaomi AI Speaker
- [ChatGLM](http://open.bigmodel.cn/)
- [Gemini](https://makersuite.google.com/app/apikey)
- [Doubao](https://console.volcengine.com/iam/keymanage/)
- [MiniMax](https://platform.minimaxi.com/)
- [Moonshot](https://platform.moonshot.cn/docs/api/chat#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
- [01](https://platform.lingyiwanwu.com/apikeys)
- [Llama3](https://console.groq.com/docs/quickstart)
Expand Down Expand Up @@ -85,6 +86,8 @@ xiaogpt --hardware LX06 --mute_xiaoai --use_gemini --gemini_key ${gemini_key}
python3 xiaogpt.py --hardware LX06 --mute_xiaoai --use_gemini --gemini_key ${gemini_key} --gemini_api_domain ${gemini_api_domain}
# 如果你想使用阿里的通义千问
xiaogpt --hardware LX06 --mute_xiaoai --use_qwen --qwen_key ${qwen_key}
# 如果你想使用 MiniMax
xiaogpt --hardware LX06 --mute_xiaoai --use_minimax --minimax_api_key ${minimax_api_key}
# 如果你想使用 kimi
xiaogpt --hardware LX06 --mute_xiaoai --use_moonshot_api --moonshot_api_key ${moonshot_api_key}
# 如果你想使用 llama3
Expand Down Expand Up @@ -118,6 +121,8 @@ python3 xiaogpt.py --hardware LX06 --mute_xiaoai --use_gemini --gemini_key ${ge
python3 xiaogpt.py --hardware LX06 --mute_xiaoai --use_gemini --gemini_key ${gemini_key} --gemini_api_domain ${gemini_api_domain}
# 如果你想使用阿里的通义千问
python3 xiaogpt.py --hardware LX06 --mute_xiaoai --use_qwen --qwen_key ${qwen_key}
# 如果你想使用 MiniMax
xiaogpt --hardware LX06 --mute_xiaoai --use_minimax --minimax_api_key ${minimax_api_key}
# 如果你想使用 kimi
xiaogpt --hardware LX06 --mute_xiaoai --use_moonshot_api --moonshot_api_key ${moonshot_api_key}
# 如果你想使用 01
Expand Down Expand Up @@ -171,6 +176,7 @@ ChatGLM [文档](http://open.bigmodel.cn/doc/api#chatglm_130b)
| account | 小爱账户 | | |
| password | 小爱账户密码 | | |
| openai_key | openai 的 apikey | | |
| minimax_api_key | MiniMax 的 [apikey](https://platform.minimaxi.com/) | | |
| moonshot_api_key | moonshot kimi 的 [apikey](https://platform.moonshot.cn/docs/api/chat#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) | | |
| yi_api_key | 01 wanwu 的 [apikey](https://platform.lingyiwanwu.com/apikeys) | | |
| llama_api_key | groq 的 llama3 [apikey](https://console.groq.com/docs/quickstart) | | |
Expand Down
Empty file added tests/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Mock heavy dependencies for testing."""

import sys
from unittest.mock import MagicMock

# langchain 1.x restructured submodules. Mock all submodules used by xiaogpt.
_mock_modules = [
"langchain.memory",
"langchain.agents",
"langchain.tools",
"langchain.callbacks",
"langchain.callbacks.base",
"langchain.chains",
"langchain.schema",
"langchain.schema.memory",
"langchain.chat_models",
"langchain.llms",
"langchain.utilities",
"langchain_community",
"langchain_community.chat_models",
"langchain_community.llms",
"langchain_community.utilities",
"miservice_fork",
"zhipuai",
"dashscope",
"google.generativeai",
"google.generativeai.types",
"groq",
"tetos",
"tetos.base",
]

for mod_name in _mock_modules:
sys.modules[mod_name] = MagicMock()
234 changes: 234 additions & 0 deletions tests/test_minimax_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
"""Unit tests for MiniMax bot."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from xiaogpt.bot.minimax_bot import MiniMaxBot


class TestMiniMaxBotInit:
"""Test MiniMaxBot initialization."""

def test_name(self):
assert MiniMaxBot.name == "MiniMax"

def test_default_options(self):
assert MiniMaxBot.default_options == {"model": "MiniMax-M1"}

def test_init_defaults(self):
bot = MiniMaxBot(minimax_api_key="test-key")
assert bot.minimax_api_key == "test-key"
assert bot.api_base == "https://api.minimax.io/v1"
assert bot.history == []

def test_init_custom_api_base(self):
bot = MiniMaxBot(
minimax_api_key="test-key",
api_base="https://custom.api.com/v1",
)
assert bot.api_base == "https://custom.api.com/v1"


class TestMiniMaxBotFromConfig:
"""Test MiniMaxBot.from_config class method."""

def test_from_config(self):
config = MagicMock()
config.minimax_api_key = "my-minimax-key"
bot = MiniMaxBot.from_config(config)
assert bot.minimax_api_key == "my-minimax-key"
assert bot.api_base == "https://api.minimax.io/v1"

def test_from_config_empty_key(self):
config = MagicMock()
config.minimax_api_key = ""
bot = MiniMaxBot.from_config(config)
assert bot.minimax_api_key == ""


class TestMiniMaxBotClient:
"""Test MiniMaxBot OpenAI client creation."""

def test_make_openai_client(self):
bot = MiniMaxBot(minimax_api_key="test-key")
import httpx

sess = httpx.AsyncClient()
client = bot._make_openai_client(sess)
assert client.api_key == "test-key"
assert str(client.base_url) == "https://api.minimax.io/v1/"

def test_make_openai_client_custom_base(self):
bot = MiniMaxBot(
minimax_api_key="test-key",
api_base="https://custom.api.com/v1",
)
import httpx

sess = httpx.AsyncClient()
client = bot._make_openai_client(sess)
assert str(client.base_url) == "https://custom.api.com/v1/"


class TestMiniMaxBotHistory:
"""Test MiniMaxBot history management."""

def test_has_history_empty(self):
bot = MiniMaxBot(minimax_api_key="test-key")
assert bot.has_history() is False

def test_has_history_with_data(self):
bot = MiniMaxBot(minimax_api_key="test-key")
bot.history = [("hello", "hi")]
assert bot.has_history() is True

def test_get_messages(self):
bot = MiniMaxBot(minimax_api_key="test-key")
bot.history = [["hello", "hi"], ["how are you", "fine"]]
messages = bot.get_messages()
assert len(messages) == 4
assert messages[0] == {"role": "user", "content": "hello"}
assert messages[1] == {"role": "assistant", "content": "hi"}

def test_change_prompt(self):
bot = MiniMaxBot(minimax_api_key="test-key")
bot.history = [["old prompt", "response"]]
bot.change_prompt("new prompt")
assert bot.history[0][0] == "new prompt"


class TestMiniMaxBotAsk:
"""Test MiniMaxBot ask methods."""

@pytest.mark.asyncio
async def test_ask(self):
bot = MiniMaxBot(minimax_api_key="test-key")
mock_completion = MagicMock()
mock_completion.choices = [
MagicMock(message=MagicMock(content="Hello from MiniMax!"))
]

mock_client = AsyncMock()
mock_client.chat.completions.create = AsyncMock(return_value=mock_completion)

with patch.object(bot, "_make_openai_client", return_value=mock_client):
result = await bot.ask("Hello")
assert result == "Hello from MiniMax!"
assert len(bot.history) == 1

@pytest.mark.asyncio
async def test_ask_error_returns_empty(self):
bot = MiniMaxBot(minimax_api_key="test-key")
mock_client = AsyncMock()
mock_client.chat.completions.create = AsyncMock(
side_effect=Exception("API Error")
)

with patch.object(bot, "_make_openai_client", return_value=mock_client):
result = await bot.ask("Hello")
assert result == ""

@pytest.mark.asyncio
async def test_ask_stream(self):
bot = MiniMaxBot(minimax_api_key="test-key")

chunk1 = MagicMock()
chunk1.choices = [MagicMock(delta=MagicMock(content="Hello"))]
chunk2 = MagicMock()
chunk2.choices = [MagicMock(delta=MagicMock(content=" world"))]
chunk3 = MagicMock()
chunk3.choices = [MagicMock(delta=MagicMock(content="。"))]

async def mock_stream():
for chunk in [chunk1, chunk2, chunk3]:
yield chunk

mock_client = AsyncMock()
mock_client.chat.completions.create = AsyncMock(return_value=mock_stream())

with patch.object(bot, "_make_openai_client", return_value=mock_client):
sentences = []
async for sentence in bot.ask_stream("Hello"):
sentences.append(sentence)
assert len(sentences) > 0


class TestMiniMaxBotRegistration:
"""Test MiniMaxBot registration in bot registry."""

def test_bot_in_registry(self):
from xiaogpt.bot import BOTS

assert "minimax" in BOTS
assert BOTS["minimax"] is MiniMaxBot

def test_minimax_in_all_export(self):
import xiaogpt.bot as bot_module

assert "MiniMaxBot" in bot_module.__all__

def test_get_bot_minimax(self):
from xiaogpt.bot import get_bot

config = MagicMock()
config.bot = "minimax"
config.minimax_api_key = "test-key"
bot = get_bot(config)
assert isinstance(bot, MiniMaxBot)
assert bot.minimax_api_key == "test-key"


class TestMiniMaxConfig:
"""Test MiniMax config integration."""

def test_minimax_api_key_field_exists(self):
from xiaogpt.config import Config

assert "minimax_api_key" in Config.__dataclass_fields__

def test_minimax_api_key_env_var(self):
import os
from xiaogpt.config import Config

# Config reads MINIMAX_API_KEY from env
field = Config.__dataclass_fields__["minimax_api_key"]
assert field.default == os.getenv("MINIMAX_API_KEY", "")

def test_minimax_validation_exists(self):
with open("xiaogpt/config.py") as f:
content = f.read()
assert 'self.bot == "minimax"' in content
assert "MINIMAX_API_KEY" in content

def test_minimax_validation_raises(self):
from xiaogpt.config import Config

with pytest.raises(Exception, match="MINIMAX_API_KEY"):
Config(bot="minimax", minimax_api_key="", account="a", password="p")

def test_use_minimax_config_parsing(self):
with open("xiaogpt/config.py") as f:
content = f.read()
assert '"use_minimax"' in content


class TestMiniMaxCLI:
"""Test MiniMax CLI integration."""

def test_minimax_api_key_arg(self):
with open("xiaogpt/cli.py") as f:
content = f.read()
assert "--minimax_api_key" in content

def test_use_minimax_arg(self):
with open("xiaogpt/cli.py") as f:
content = f.read()
assert "--use_minimax" in content

def test_minimax_in_bot_choices(self):
with open("xiaogpt/cli.py") as f:
content = f.read()
assert '"minimax"' in content
55 changes: 55 additions & 0 deletions tests/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Integration tests for MiniMax bot with the MiniMax API.

These tests require a valid MINIMAX_API_KEY environment variable.
Skip with: pytest -m "not integration"
"""

from __future__ import annotations

import os

import pytest

from xiaogpt.bot.minimax_bot import MiniMaxBot

MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY", "")

pytestmark = pytest.mark.skipif(
not MINIMAX_API_KEY, reason="MINIMAX_API_KEY not set"
)


@pytest.fixture
def minimax_bot():
return MiniMaxBot(minimax_api_key=MINIMAX_API_KEY)


class TestMiniMaxIntegration:
"""Integration tests that hit the real MiniMax API."""

@pytest.mark.asyncio
async def test_ask_real_api(self, minimax_bot):
"""Test a real API call to MiniMax."""
response = await minimax_bot.ask("Say hello in one word.")
assert response # non-empty response
assert isinstance(response, str)
assert len(response) > 0

@pytest.mark.asyncio
async def test_ask_stream_real_api(self, minimax_bot):
"""Test streaming from the real MiniMax API."""
sentences = []
async for sentence in minimax_bot.ask_stream("Say hi in one word."):
sentences.append(sentence)
full_response = "".join(sentences)
assert full_response # non-empty response
assert len(full_response) > 0

@pytest.mark.asyncio
async def test_conversation_history(self, minimax_bot):
"""Test that conversation history is maintained across calls."""
await minimax_bot.ask("My name is TestBot.")
assert minimax_bot.has_history()
response = await minimax_bot.ask("What is my name?")
assert response
assert len(minimax_bot.history) == 2
6 changes: 5 additions & 1 deletion xiao_config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ verbose: false
proxy: ""

# ===== 对话 AI 设置 =====
# 使用的 bot 类型,目前支持 chatgptapi, glm, gemini, langchain, qwen, doubao, moonshot, yi, llama, ppio, jiekou
# 使用的 bot 类型,目前支持 chatgptapi, glm, gemini, langchain, minimax, qwen, doubao, moonshot, yi, llama, ppio, jiekou
bot: chatgptapi
# 自定义 prompt
prompt: "请用300字以内回答"
Expand Down Expand Up @@ -52,6 +52,10 @@ openai_key: ""
#deployment_id: ""


# ----- MiniMax -----
# MiniMax API key: https://platform.minimaxi.com/
minimax_api_key: ""

# ----- Moonshot(Kimi) -----
# Kimi 的 API key: https://platform.moonshot.cn/docs/api/chat#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B
moonshot_api_key: ""
Expand Down
Loading
Loading