diff --git a/.env.example b/.env.example index 5e9f6b1..7c6e146 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,10 @@ MODEL_BASE_URL= MODEL_KEY= +#MINIMAX (https://platform.minimax.io) +#Models: MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed (204K context) +MINIMAX_API_KEY= + #CUSTOM MODEL QWEN_BASE_URL= QWEN_KEY= diff --git a/README.md b/README.md index 7a4effa..f00e011 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,37 @@ MODEL_BASE_URL= MODEL_KEY= ``` +## Supported LLM Providers + +Aser works with any OpenAI-compatible API. Use the built-in `providers` module for quick setup: + +| Provider | Models | Context | +|----------|--------|---------| +| OpenAI | gpt-4o, gpt-4.1-mini, … | up to 128K | +| [MiniMax](https://platform.minimax.io) | MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed | **204K** | +| Any OpenAI-compat API | set `MODEL_BASE_URL` + `MODEL_KEY` | depends on model | + +### Using MiniMax + +```bash +export MINIMAX_API_KEY=your_api_key_here +``` + +```python +from aser.agent import Agent +from aser.providers import MINIMAX, MINIMAX_MODELS + +agent = Agent( + name="minimax_agent", + model=MINIMAX_MODELS["standard"], # "MiniMax-M2.7" + model_config=MINIMAX, +) +response = agent.chat("What is MiniMax AI?") +print(response) +``` + +See the full example at [examples/agent_minimax.py](./examples/agent_minimax.py). + ## Usage ```python @@ -66,6 +97,7 @@ If you clone the project source code, before running the examples, please run `p - [Aser Agent](./examples/agent.py): Your First AI Agent - [Model Config](./examples/agent_model.py): Customize the LLM configuration +- [MiniMax](./examples/agent_minimax.py): Use MiniMax M2.7 / M2.5 models (204K context) - [Character](./examples/agent_character.py): Build an agent with character - [Memory](./examples/agent_memory.py): Build an agent with memory storage - [RAG](./examples/agent_knowledge.py): Build an agent with knowledge retrieval diff --git a/README_CN.md b/README_CN.md index 9094361..ae5f023 100644 --- a/README_CN.md +++ b/README_CN.md @@ -34,6 +34,37 @@ MODEL_BASE_URL=<您的模型基础URL> MODEL_KEY=<您的模型密钥> ``` +## 支持的 LLM 提供商 + +Aser 兼容任何 OpenAI 兼容 API。通过内置 `providers` 模块可快速配置: + +| 提供商 | 模型 | 上下文长度 | +|--------|------|-----------| +| OpenAI | gpt-4o, gpt-4.1-mini, … | 最高 128K | +| [MiniMax](https://platform.minimax.io) | MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed | **204K** | +| 任意 OpenAI 兼容 API | 设置 `MODEL_BASE_URL` + `MODEL_KEY` | 取决于模型 | + +### 使用 MiniMax + +```bash +export MINIMAX_API_KEY=your_api_key_here +``` + +```python +from aser.agent import Agent +from aser.providers import MINIMAX, MINIMAX_MODELS + +agent = Agent( + name="minimax_agent", + model=MINIMAX_MODELS["standard"], # "MiniMax-M2.7" + model_config=MINIMAX, +) +response = agent.chat("你好,请介绍一下 MiniMax。") +print(response) +``` + +完整示例参见 [examples/agent_minimax.py](./examples/agent_minimax.py)。 + ## 使用 ```python @@ -66,6 +97,7 @@ aser = Agent( - [Aser代理](./examples/agent.py): 您的第一个AI代理 - [模型配置](./examples/agent_model.py): 自定义LLM配置 +- [MiniMax](./examples/agent_minimax.py): 使用 MiniMax M2.7/M2.5 模型(204K 上下文) - [角色](./examples/agent_character.py): 构建具有角色的代理 - [记忆](./examples/agent_memory.py): 构建具有记忆存储的代理 - [RAG](./examples/agent_knowledge.py): 构建具有知识检索的代理 diff --git a/aser/__init__.py b/aser/__init__.py index d559379..b173266 100644 --- a/aser/__init__.py +++ b/aser/__init__.py @@ -16,6 +16,7 @@ from .text2sql import Text2SQL from .evolution import SelfCodingTool from . import social,storage,utils +from . import providers diff --git a/aser/providers.py b/aser/providers.py new file mode 100644 index 0000000..2602eed --- /dev/null +++ b/aser/providers.py @@ -0,0 +1,63 @@ +""" +Provider presets for popular LLM APIs. + +Usage: + from aser.providers import MINIMAX, OPENAI + from aser.agent import Agent + + agent = Agent(name="my_agent", model="MiniMax-M2.7", model_config=MINIMAX) +""" + +import os + + +def _make_config(base_url: str, api_key_env: str, api_key: str | None = None) -> dict: + return { + "base_url": base_url, + "api_key": api_key or os.getenv(api_key_env) or "MISSING_API_KEY", + } + + +# --------------------------------------------------------------------------- +# MiniMax — OpenAI-compatible endpoint +# Models: MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed +# Context: 204K tokens for all models +# Docs: https://docs.minimax.io +# Note: temperature must be in (0.0, 1.0]; do NOT use temperature=0 +# --------------------------------------------------------------------------- +MINIMAX = _make_config( + base_url="https://api.minimax.io/v1", + api_key_env="MINIMAX_API_KEY", +) + +# Recommended models +MINIMAX_MODELS = { + "standard": "MiniMax-M2.7", + "fast": "MiniMax-M2.7-highspeed", + "standard_v25": "MiniMax-M2.5", + "fast_v25": "MiniMax-M2.5-highspeed", +} + +# --------------------------------------------------------------------------- +# OpenAI +# --------------------------------------------------------------------------- +OPENAI = _make_config( + base_url="https://api.openai.com/v1", + api_key_env="OPENAI_API_KEY", +) + +# --------------------------------------------------------------------------- +# Anthropic (OpenAI-compatible proxy via third-party gateways) +# Use your gateway's base_url; Anthropic's native API is not OpenAI-compatible. +# --------------------------------------------------------------------------- +ANTHROPIC_COMPAT = _make_config( + base_url=os.getenv("ANTHROPIC_COMPAT_BASE_URL", "https://api.anthropic.com/v1"), + api_key_env="ANTHROPIC_API_KEY", +) + +# --------------------------------------------------------------------------- +# Generic helper: build a custom provider config on the fly +# --------------------------------------------------------------------------- +def custom_provider(base_url: str, api_key: str) -> dict: + """Return a model_config dict for any OpenAI-compatible provider.""" + return {"base_url": base_url, "api_key": api_key} diff --git a/examples/agent_minimax.py b/examples/agent_minimax.py new file mode 100644 index 0000000..ca00678 --- /dev/null +++ b/examples/agent_minimax.py @@ -0,0 +1,72 @@ +""" +Example: Using Aser with MiniMax models. + +MiniMax offers OpenAI-compatible API access to its M2.7 and M2.5 model families, +all featuring 204K-token context windows. + +Setup: + 1. Get your API key from https://platform.minimax.io + 2. Set the environment variable: + export MINIMAX_API_KEY=your_api_key_here + 3. Run this example: + python examples/agent_minimax.py +""" + +import os +from dotenv import load_dotenv + +load_dotenv() + +from aser.agent import Agent +from aser.providers import MINIMAX, MINIMAX_MODELS + +# --------------------------------------------------------------------------- +# Basic usage: use the default MODEL_BASE_URL / MODEL_KEY env vars +# --------------------------------------------------------------------------- +# You can also point MODEL_BASE_URL=https://api.minimax.io/v1 and +# MODEL_KEY= in your .env file and use Agent without +# explicitly passing model_config: +# +# agent = Agent(name="aser agent", model="MiniMax-M2.7") + +# --------------------------------------------------------------------------- +# Explicit usage: pass model_config=MINIMAX +# --------------------------------------------------------------------------- +print("=== MiniMax M2.7 (standard) ===") +agent = Agent( + name="minimax_agent", + description="You are a helpful AI assistant powered by MiniMax.", + model=MINIMAX_MODELS["standard"], # "MiniMax-M2.7" + model_config=MINIMAX, # base_url + MINIMAX_API_KEY +) +response = agent.chat("What is MiniMax AI? Introduce yourself briefly.") +print(response) + +# --------------------------------------------------------------------------- +# High-speed variant — same capability, optimised for latency +# --------------------------------------------------------------------------- +print("\n=== MiniMax M2.7-highspeed (fast) ===") +fast_agent = Agent( + name="minimax_fast_agent", + description="You are a concise assistant. Always answer in one sentence.", + model=MINIMAX_MODELS["fast"], # "MiniMax-M2.7-highspeed" + model_config=MINIMAX, +) +response = fast_agent.chat("What is the capital of France?") +print(response) + +# --------------------------------------------------------------------------- +# Tip: you can also build the config inline without importing MINIMAX +# --------------------------------------------------------------------------- +print("\n=== Inline model_config ===") +inline_agent = Agent( + name="inline_minimax_agent", + description="Helpful assistant", + model="MiniMax-M2.5", + model_config={ + "base_url": "https://api.minimax.io/v1", + "api_key": os.getenv("MINIMAX_API_KEY"), + }, +) +response = inline_agent.chat("Say hello in three different languages.") +print(response) diff --git a/tests/test_providers.py b/tests/test_providers.py new file mode 100644 index 0000000..f11d2db --- /dev/null +++ b/tests/test_providers.py @@ -0,0 +1,305 @@ +""" +Unit tests for aser/providers.py — MiniMax provider presets. + +Imports providers.py directly (not through aser.__init__) so we don't need +all the heavy optional dependencies (discord, telegram, web3, etc.). + +Run with: pytest tests/test_providers.py -v +""" + +import os +import sys +import importlib +import importlib.util +import types +import pytest +from unittest.mock import patch, MagicMock + + +# --------------------------------------------------------------------------- +# Helper: load providers.py in isolation (no __init__ side effects) +# --------------------------------------------------------------------------- + +PROVIDERS_PATH = os.path.join( + os.path.dirname(__file__), "..", "aser", "providers.py" +) + + +def _load_providers(extra_env: dict | None = None): + """Load aser/providers.py with an optionally patched environment.""" + env_patch = extra_env or {} + with patch.dict(os.environ, env_patch, clear=False): + spec = importlib.util.spec_from_file_location("aser.providers", PROVIDERS_PATH) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +# --------------------------------------------------------------------------- +# Helper: load agent.py with OpenAI stubbed out +# --------------------------------------------------------------------------- + +AGENT_PATH = os.path.join( + os.path.dirname(__file__), "..", "aser", "agent.py" +) + + +def _load_agent_with_mock_openai(): + """Import aser/agent.py with all its dependencies mocked.""" + # Stub the modules Agent imports + stubs = { + "openai": types.ModuleType("openai"), + "aser.utils": types.ModuleType("aser.utils"), + "aser.tools": types.ModuleType("aser.tools"), + } + # openai.OpenAI mock + mock_openai_cls = MagicMock() + stubs["openai"].OpenAI = mock_openai_cls + + # aser.utils stubs + stubs["aser.utils"].knowledge_to_prompt = MagicMock(return_value="") + stubs["aser.utils"].handle_tool_function = MagicMock(return_value="") + stubs["aser.utils"].safe_serialize = MagicMock(return_value=None) + + # aser.tools.Tools stub + mock_tools_cls = MagicMock() + stubs["aser.tools"].Tools = mock_tools_cls + + with patch.dict(sys.modules, stubs): + spec = importlib.util.spec_from_file_location("aser.agent", AGENT_PATH) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod, mock_openai_cls + + +# --------------------------------------------------------------------------- +# MINIMAX preset — base_url +# --------------------------------------------------------------------------- + +class TestMinimaxBaseUrl: + def test_base_url_is_openai_compat(self): + p = _load_providers() + assert p.MINIMAX["base_url"] == "https://api.minimax.io/v1" + + def test_base_url_uses_https(self): + p = _load_providers() + assert p.MINIMAX["base_url"].startswith("https://") + + def test_base_url_ends_with_v1(self): + p = _load_providers() + assert p.MINIMAX["base_url"].endswith("/v1") + + +# --------------------------------------------------------------------------- +# MINIMAX preset — api_key +# --------------------------------------------------------------------------- + +class TestMinimaxApiKey: + def test_api_key_from_env(self): + p = _load_providers({"MINIMAX_API_KEY": "mk-test-key"}) + assert p.MINIMAX["api_key"] == "mk-test-key" + + def test_api_key_different_value(self): + p = _load_providers({"MINIMAX_API_KEY": "mk-another-key-456"}) + assert p.MINIMAX["api_key"] == "mk-another-key-456" + + def test_api_key_missing_returns_placeholder(self): + clean_env = {k: v for k, v in os.environ.items() if k != "MINIMAX_API_KEY"} + with patch.dict(os.environ, clean_env, clear=True): + p = _load_providers() + assert p.MINIMAX["api_key"] == "MISSING_API_KEY" + + def test_minimax_dict_keys(self): + p = _load_providers({"MINIMAX_API_KEY": "k"}) + assert set(p.MINIMAX.keys()) == {"base_url", "api_key"} + + +# --------------------------------------------------------------------------- +# MINIMAX_MODELS +# --------------------------------------------------------------------------- + +class TestMinimaxModels: + def test_standard_model_name(self): + p = _load_providers() + assert p.MINIMAX_MODELS["standard"] == "MiniMax-M2.7" + + def test_fast_model_name(self): + p = _load_providers() + assert p.MINIMAX_MODELS["fast"] == "MiniMax-M2.7-highspeed" + + def test_standard_v25_model_name(self): + p = _load_providers() + assert p.MINIMAX_MODELS["standard_v25"] == "MiniMax-M2.5" + + def test_fast_v25_model_name(self): + p = _load_providers() + assert p.MINIMAX_MODELS["fast_v25"] == "MiniMax-M2.5-highspeed" + + def test_all_model_keys_present(self): + p = _load_providers() + assert set(p.MINIMAX_MODELS.keys()) == {"standard", "fast", "standard_v25", "fast_v25"} + + def test_m27_in_standard_name(self): + p = _load_providers() + assert "M2.7" in p.MINIMAX_MODELS["standard"] + + def test_m25_in_standard_v25_name(self): + p = _load_providers() + assert "M2.5" in p.MINIMAX_MODELS["standard_v25"] + + def test_highspeed_suffix_in_fast_model(self): + p = _load_providers() + assert "highspeed" in p.MINIMAX_MODELS["fast"] + + +# --------------------------------------------------------------------------- +# OPENAI preset +# --------------------------------------------------------------------------- + +class TestOpenAIPreset: + def test_openai_base_url(self): + p = _load_providers({"OPENAI_API_KEY": "sk-test"}) + assert p.OPENAI["base_url"] == "https://api.openai.com/v1" + + def test_openai_api_key_from_env(self): + p = _load_providers({"OPENAI_API_KEY": "sk-hello"}) + assert p.OPENAI["api_key"] == "sk-hello" + + def test_openai_dict_has_correct_keys(self): + p = _load_providers({"OPENAI_API_KEY": "sk-x"}) + assert "base_url" in p.OPENAI + assert "api_key" in p.OPENAI + + +# --------------------------------------------------------------------------- +# custom_provider helper +# --------------------------------------------------------------------------- + +class TestCustomProvider: + def test_returns_correct_dict(self): + p = _load_providers() + cfg = p.custom_provider("https://example.com/v1", "my-key") + assert cfg == {"base_url": "https://example.com/v1", "api_key": "my-key"} + + def test_base_url_preserved(self): + p = _load_providers() + url = "https://api.my-llm.io/v2" + cfg = p.custom_provider(url, "k") + assert cfg["base_url"] == url + + def test_api_key_preserved(self): + p = _load_providers() + key = "supersecretkey" + cfg = p.custom_provider("https://x.com", key) + assert cfg["api_key"] == key + + def test_returns_dict_type(self): + p = _load_providers() + cfg = p.custom_provider("https://x.com", "k") + assert isinstance(cfg, dict) + + +# --------------------------------------------------------------------------- +# Agent integration: model_config is forwarded to OpenAI constructor +# --------------------------------------------------------------------------- + +class TestAgentWithMiniMaxConfig: + def test_openai_called_with_minimax_config(self): + agent_mod, mock_openai_cls = _load_agent_with_mock_openai() + minimax_cfg = { + "base_url": "https://api.minimax.io/v1", + "api_key": "test-key", + } + agent_mod.Agent(name="t", model="MiniMax-M2.7", model_config=minimax_cfg) + mock_openai_cls.assert_called_once_with(**minimax_cfg) + + def test_agent_model_attribute(self): + agent_mod, _ = _load_agent_with_mock_openai() + agent = agent_mod.Agent( + name="t", + model="MiniMax-M2.7-highspeed", + model_config={"base_url": "https://api.minimax.io/v1", "api_key": "k"}, + ) + assert agent.model == "MiniMax-M2.7-highspeed" + + def test_agent_uses_m25_model(self): + agent_mod, mock_openai_cls = _load_agent_with_mock_openai() + cfg = {"base_url": "https://api.minimax.io/v1", "api_key": "k"} + agent = agent_mod.Agent(name="t", model="MiniMax-M2.5", model_config=cfg) + assert agent.model == "MiniMax-M2.5" + mock_openai_cls.assert_called_once_with(**cfg) + + def test_agent_name_stored(self): + agent_mod, _ = _load_agent_with_mock_openai() + agent = agent_mod.Agent( + name="minimax_agent", + model="MiniMax-M2.7", + model_config={"base_url": "https://api.minimax.io/v1", "api_key": "k"}, + ) + assert agent.name == "minimax_agent" + + +# --------------------------------------------------------------------------- +# Integration smoke tests (only run if MINIMAX_API_KEY is set) +# --------------------------------------------------------------------------- + +@pytest.mark.skipif( + not os.getenv("MINIMAX_API_KEY"), + reason="MINIMAX_API_KEY not set — skipping live API test", +) +class TestMinimaxLiveIntegration: + """Live API tests — require a valid MINIMAX_API_KEY.""" + + def _make_agent(self, model_key: str): + """Build an Agent using MINIMAX provider without importing through __init__.""" + p = _load_providers() + agent_mod, _ = _load_agent_with_mock_openai() + + # For live tests, use the real OpenAI client + from openai import OpenAI + import aser.agent as real_agent_mod + return real_agent_mod, p + + def test_minimax_m27_chat(self): + import importlib.util, sys, types + spec = importlib.util.spec_from_file_location("aser.agent_real", AGENT_PATH) + # Can't easily do live test without deps; skip if openai not available + try: + from openai import OpenAI + except ImportError: + pytest.skip("openai package not installed") + + p = _load_providers() + from openai import OpenAI + + client = OpenAI(**p.MINIMAX) + resp = client.chat.completions.create( + model=p.MINIMAX_MODELS["fast"], + messages=[{"role": "user", "content": "Reply with: OK"}], + max_tokens=10, + ) + assert resp.choices[0].message.content is not None + + def test_minimax_m25_chat(self): + try: + from openai import OpenAI + except ImportError: + pytest.skip("openai package not installed") + + p = _load_providers() + from openai import OpenAI + + client = OpenAI(**p.MINIMAX) + resp = client.chat.completions.create( + model=p.MINIMAX_MODELS["fast_v25"], + messages=[{"role": "user", "content": "Say hello."}], + max_tokens=20, + ) + assert isinstance(resp.choices[0].message.content, str) + assert len(resp.choices[0].message.content) > 0 + + def test_minimax_204k_context_available(self): + """Model names contain version markers indicating 204K context support.""" + p = _load_providers() + for key, model_name in p.MINIMAX_MODELS.items(): + assert "MiniMax" in model_name, f"Unexpected model name for key {key!r}: {model_name!r}"