* feat: add MiniMax AI as LLM platform adaptor Original implementation by octo-patch in PR #318. This commit includes comprehensive improvements and documentation. Code Improvements: - Fix API key validation to properly check JWT format (eyJ prefix) - Add specific exception handling for timeout and connection errors - Remove unused variable in upload method Dependencies: - Add MiniMax to [all-llms] extra group in pyproject.toml Tests: - Remove duplicate setUp method in integration test class - Add 4 new test methods: * test_package_excludes_backup_files * test_upload_success_mocked (with OpenAI mocking) * test_upload_network_error * test_upload_connection_error * test_validate_api_key_jwt_format - Update test_validate_api_key_valid to use JWT format keys - Fix test assertions for error message matching Documentation: - Create comprehensive MINIMAX_INTEGRATION.md guide (380+ lines) - Update MULTI_LLM_SUPPORT.md with MiniMax platform entry - Update 01-installation.md extras table - Update INTEGRATIONS.md AI platforms table - Update AGENTS.md adaptor import pattern example - Fix README.md platform count from 4 to 5 All tests pass (33 passed, 3 skipped) Lint checks pass Co-authored-by: octo-patch <octo-patch@users.noreply.github.com> * fix: improve MiniMax adaptor — typed exceptions, key validation, tests, docs - Remove invalid "minimax" self-reference from all-llms dependency group - Use typed OpenAI exceptions (APITimeoutError, APIConnectionError) instead of string-matching on generic Exception - Replace incorrect JWT assumption in validate_api_key with length check - Use DEFAULT_API_ENDPOINT constant instead of hardcoded URLs (3 sites) - Add Path() cast for output_path before .is_dir() call - Add sys.modules mock to test_enhance_missing_library - Add mocked test_enhance_success with backup/content verification - Update test assertions for new exception types and key validation - Add MiniMax to __init__.py docstrings (module, get_adaptor, list_platforms) - Add MiniMax sections to MULTI_LLM_SUPPORT.md (install, format, API key, workflow example, export-to-all) Follows up on PR #318 by @octo-patch (feat: add MiniMax AI as LLM platform adaptor). Co-Authored-By: Octopus <octo-patch@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: octo-patch <octo-patch@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
Multi-LLM Adaptor Registry
|
||||
|
||||
Provides factory function to get platform-specific adaptors for skill generation.
|
||||
Supports Claude AI, Google Gemini, OpenAI ChatGPT, and generic Markdown export.
|
||||
Supports Claude AI, Google Gemini, OpenAI ChatGPT, MiniMax AI, and generic Markdown export.
|
||||
"""
|
||||
|
||||
from .base import SkillAdaptor, SkillMetadata
|
||||
@@ -69,6 +69,11 @@ try:
|
||||
except ImportError:
|
||||
PineconeAdaptor = None
|
||||
|
||||
try:
|
||||
from .minimax import MiniMaxAdaptor
|
||||
except ImportError:
|
||||
MiniMaxAdaptor = None
|
||||
|
||||
|
||||
# Registry of available adaptors
|
||||
ADAPTORS: dict[str, type[SkillAdaptor]] = {}
|
||||
@@ -98,6 +103,8 @@ if HaystackAdaptor:
|
||||
ADAPTORS["haystack"] = HaystackAdaptor
|
||||
if PineconeAdaptor:
|
||||
ADAPTORS["pinecone"] = PineconeAdaptor
|
||||
if MiniMaxAdaptor:
|
||||
ADAPTORS["minimax"] = MiniMaxAdaptor
|
||||
|
||||
|
||||
def get_adaptor(platform: str, config: dict = None) -> SkillAdaptor:
|
||||
@@ -105,7 +112,7 @@ def get_adaptor(platform: str, config: dict = None) -> SkillAdaptor:
|
||||
Factory function to get platform-specific adaptor instance.
|
||||
|
||||
Args:
|
||||
platform: Platform identifier ('claude', 'gemini', 'openai', 'markdown')
|
||||
platform: Platform identifier ('claude', 'gemini', 'openai', 'minimax', 'markdown')
|
||||
config: Optional platform-specific configuration
|
||||
|
||||
Returns:
|
||||
@@ -116,6 +123,7 @@ def get_adaptor(platform: str, config: dict = None) -> SkillAdaptor:
|
||||
|
||||
Examples:
|
||||
>>> adaptor = get_adaptor('claude')
|
||||
>>> adaptor = get_adaptor('minimax')
|
||||
>>> adaptor = get_adaptor('gemini', {'api_version': 'v1beta'})
|
||||
"""
|
||||
if platform not in ADAPTORS:
|
||||
@@ -141,7 +149,7 @@ def list_platforms() -> list[str]:
|
||||
|
||||
Examples:
|
||||
>>> list_platforms()
|
||||
['claude', 'gemini', 'openai', 'markdown']
|
||||
['claude', 'gemini', 'openai', 'minimax', 'markdown']
|
||||
"""
|
||||
return list(ADAPTORS.keys())
|
||||
|
||||
|
||||
503
src/skill_seekers/cli/adaptors/minimax.py
Normal file
503
src/skill_seekers/cli/adaptors/minimax.py
Normal file
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MiniMax AI Adaptor
|
||||
|
||||
Implements platform-specific handling for MiniMax AI skills.
|
||||
Uses MiniMax's OpenAI-compatible API for AI enhancement with M2.7 model.
|
||||
"""
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .base import SkillAdaptor, SkillMetadata
|
||||
from skill_seekers.cli.arguments.common import DEFAULT_CHUNK_TOKENS, DEFAULT_CHUNK_OVERLAP_TOKENS
|
||||
|
||||
|
||||
class MiniMaxAdaptor(SkillAdaptor):
|
||||
"""
|
||||
MiniMax AI platform adaptor.
|
||||
|
||||
Handles:
|
||||
- System instructions format (plain text, no YAML frontmatter)
|
||||
- ZIP packaging with knowledge files
|
||||
- AI enhancement using MiniMax-M2.7
|
||||
"""
|
||||
|
||||
PLATFORM = "minimax"
|
||||
PLATFORM_NAME = "MiniMax AI"
|
||||
DEFAULT_API_ENDPOINT = "https://api.minimax.io/v1"
|
||||
|
||||
def format_skill_md(self, skill_dir: Path, metadata: SkillMetadata) -> str:
|
||||
"""
|
||||
Format SKILL.md as system instructions for MiniMax AI.
|
||||
|
||||
MiniMax uses OpenAI-compatible chat completions, so instructions
|
||||
are formatted as clear system prompts without YAML frontmatter.
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
metadata: Skill metadata
|
||||
|
||||
Returns:
|
||||
Formatted instructions for MiniMax AI
|
||||
"""
|
||||
existing_content = self._read_existing_content(skill_dir)
|
||||
|
||||
if existing_content and len(existing_content) > 100:
|
||||
content_body = f"""You are an expert assistant for {metadata.name}.
|
||||
|
||||
{metadata.description}
|
||||
|
||||
Use the attached knowledge files to provide accurate, detailed answers about {metadata.name}.
|
||||
|
||||
{existing_content}
|
||||
|
||||
## How to Assist Users
|
||||
|
||||
When users ask questions:
|
||||
1. Search the knowledge files for relevant information
|
||||
2. Provide clear, practical answers with code examples
|
||||
3. Reference specific documentation sections when helpful
|
||||
4. Be concise but thorough
|
||||
|
||||
Always prioritize accuracy by consulting the knowledge base before responding."""
|
||||
else:
|
||||
content_body = f"""You are an expert assistant for {metadata.name}.
|
||||
|
||||
{metadata.description}
|
||||
|
||||
## Your Knowledge Base
|
||||
|
||||
You have access to comprehensive documentation files about {metadata.name}. Use these files to provide accurate answers to user questions.
|
||||
|
||||
{self._generate_toc(skill_dir)}
|
||||
|
||||
## Quick Reference
|
||||
|
||||
{self._extract_quick_reference(skill_dir)}
|
||||
|
||||
## How to Assist Users
|
||||
|
||||
When users ask questions about {metadata.name}:
|
||||
|
||||
1. **Search the knowledge files** - Find relevant information in the documentation
|
||||
2. **Provide code examples** - Include practical, working code snippets
|
||||
3. **Reference documentation** - Cite specific sections when helpful
|
||||
4. **Be practical** - Focus on real-world usage and best practices
|
||||
5. **Stay accurate** - Always verify information against the knowledge base
|
||||
|
||||
## Response Guidelines
|
||||
|
||||
- Keep answers clear and concise
|
||||
- Use proper code formatting with language tags
|
||||
- Provide both simple and detailed explanations as needed
|
||||
- Suggest related topics when relevant
|
||||
- Admit when information isn't in the knowledge base
|
||||
|
||||
Always prioritize accuracy by consulting the attached documentation files before responding."""
|
||||
|
||||
return content_body
|
||||
|
||||
def package(
|
||||
self,
|
||||
skill_dir: Path,
|
||||
output_path: Path,
|
||||
enable_chunking: bool = False,
|
||||
chunk_max_tokens: int = DEFAULT_CHUNK_TOKENS,
|
||||
preserve_code_blocks: bool = True,
|
||||
chunk_overlap_tokens: int = DEFAULT_CHUNK_OVERLAP_TOKENS,
|
||||
) -> Path:
|
||||
"""
|
||||
Package skill into ZIP file for MiniMax AI.
|
||||
|
||||
Creates MiniMax-compatible structure:
|
||||
- system_instructions.txt (main instructions)
|
||||
- knowledge_files/*.md (reference files)
|
||||
- minimax_metadata.json (skill metadata)
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
output_path: Output path/filename for ZIP
|
||||
|
||||
Returns:
|
||||
Path to created ZIP file
|
||||
"""
|
||||
skill_dir = Path(skill_dir)
|
||||
output_path = Path(output_path)
|
||||
|
||||
if output_path.is_dir() or str(output_path).endswith("/"):
|
||||
output_path = Path(output_path) / f"{skill_dir.name}-minimax.zip"
|
||||
elif not str(output_path).endswith(".zip") and not str(output_path).endswith(
|
||||
"-minimax.zip"
|
||||
):
|
||||
output_str = str(output_path).replace(".zip", "-minimax.zip")
|
||||
if not output_str.endswith(".zip"):
|
||||
output_str += ".zip"
|
||||
output_path = Path(output_str)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
instructions = skill_md.read_text(encoding="utf-8")
|
||||
zf.writestr("system_instructions.txt", instructions)
|
||||
|
||||
refs_dir = skill_dir / "references"
|
||||
if refs_dir.exists():
|
||||
for ref_file in refs_dir.rglob("*.md"):
|
||||
if ref_file.is_file() and not ref_file.name.startswith("."):
|
||||
arcname = f"knowledge_files/{ref_file.name}"
|
||||
zf.write(ref_file, arcname)
|
||||
|
||||
metadata = {
|
||||
"platform": "minimax",
|
||||
"name": skill_dir.name,
|
||||
"version": "1.0.0",
|
||||
"created_with": "skill-seekers",
|
||||
"model": "MiniMax-M2.7",
|
||||
"api_base": self.DEFAULT_API_ENDPOINT,
|
||||
}
|
||||
|
||||
zf.writestr("minimax_metadata.json", json.dumps(metadata, indent=2))
|
||||
|
||||
return output_path
|
||||
|
||||
def upload(self, package_path: Path, api_key: str, **kwargs) -> dict[str, Any]:
|
||||
"""
|
||||
Upload packaged skill to MiniMax AI.
|
||||
|
||||
MiniMax uses an OpenAI-compatible chat completion API.
|
||||
This method validates the package and prepares it for use
|
||||
with the MiniMax API.
|
||||
|
||||
Args:
|
||||
package_path: Path to skill ZIP file
|
||||
api_key: MiniMax API key
|
||||
**kwargs: Additional arguments (model, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with upload result
|
||||
"""
|
||||
package_path = Path(package_path)
|
||||
if not package_path.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"File not found: {package_path}",
|
||||
}
|
||||
|
||||
if package_path.suffix != ".zip":
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"Not a ZIP file: {package_path}",
|
||||
}
|
||||
|
||||
try:
|
||||
from openai import OpenAI, APITimeoutError, APIConnectionError
|
||||
except ImportError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "openai library not installed. Run: pip install openai",
|
||||
}
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with zipfile.ZipFile(package_path, "r") as zf:
|
||||
zf.extractall(temp_dir)
|
||||
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
instructions_file = temp_path / "system_instructions.txt"
|
||||
if not instructions_file.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Invalid package: system_instructions.txt not found",
|
||||
}
|
||||
|
||||
instructions = instructions_file.read_text(encoding="utf-8")
|
||||
|
||||
metadata_file = temp_path / "minimax_metadata.json"
|
||||
skill_name = package_path.stem
|
||||
model = kwargs.get("model", "MiniMax-M2.7")
|
||||
|
||||
if metadata_file.exists():
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
skill_name = metadata.get("name", skill_name)
|
||||
model = metadata.get("model", model)
|
||||
|
||||
knowledge_dir = temp_path / "knowledge_files"
|
||||
knowledge_count = 0
|
||||
if knowledge_dir.exists():
|
||||
knowledge_count = len(list(knowledge_dir.glob("*.md")))
|
||||
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=self.DEFAULT_API_ENDPOINT,
|
||||
)
|
||||
|
||||
client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": instructions},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Confirm you are ready to assist with {skill_name}. Reply briefly.",
|
||||
},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=100,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"skill_id": None,
|
||||
"url": "https://platform.minimaxi.com/",
|
||||
"message": f"Skill '{skill_name}' validated with MiniMax {model} ({knowledge_count} knowledge files)",
|
||||
}
|
||||
|
||||
except APITimeoutError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Upload timed out. Try again.",
|
||||
}
|
||||
except APIConnectionError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Connection error. Check your internet connection.",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"Upload failed: {str(e)}",
|
||||
}
|
||||
|
||||
def validate_api_key(self, api_key: str) -> bool:
|
||||
"""
|
||||
Validate MiniMax API key format.
|
||||
|
||||
MiniMax API keys are opaque strings. We only check for
|
||||
a non-empty key with a reasonable minimum length.
|
||||
|
||||
Args:
|
||||
api_key: API key to validate
|
||||
|
||||
Returns:
|
||||
True if key format appears valid
|
||||
"""
|
||||
key = api_key.strip()
|
||||
return len(key) > 10
|
||||
|
||||
def get_env_var_name(self) -> str:
|
||||
"""
|
||||
Get environment variable name for MiniMax API key.
|
||||
|
||||
Returns:
|
||||
'MINIMAX_API_KEY'
|
||||
"""
|
||||
return "MINIMAX_API_KEY"
|
||||
|
||||
def supports_enhancement(self) -> bool:
|
||||
"""
|
||||
MiniMax supports AI enhancement via MiniMax-M2.7.
|
||||
|
||||
Returns:
|
||||
True
|
||||
"""
|
||||
return True
|
||||
|
||||
def enhance(self, skill_dir: Path, api_key: str) -> bool:
|
||||
"""
|
||||
Enhance SKILL.md using MiniMax-M2.7 API.
|
||||
|
||||
Uses MiniMax's OpenAI-compatible API endpoint for enhancement.
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
api_key: MiniMax API key
|
||||
|
||||
Returns:
|
||||
True if enhancement succeeded
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError:
|
||||
print("❌ Error: openai package not installed")
|
||||
print("Install with: pip install openai")
|
||||
return False
|
||||
|
||||
skill_dir = Path(skill_dir)
|
||||
references_dir = skill_dir / "references"
|
||||
skill_md_path = skill_dir / "SKILL.md"
|
||||
|
||||
print("📖 Reading reference documentation...")
|
||||
references = self._read_reference_files(references_dir)
|
||||
|
||||
if not references:
|
||||
print("❌ No reference files found to analyze")
|
||||
return False
|
||||
|
||||
print(f" ✓ Read {len(references)} reference files")
|
||||
total_size = sum(len(c) for c in references.values())
|
||||
print(f" ✓ Total size: {total_size:,} characters\n")
|
||||
|
||||
current_skill_md = None
|
||||
if skill_md_path.exists():
|
||||
current_skill_md = skill_md_path.read_text(encoding="utf-8")
|
||||
print(f" ℹ Found existing SKILL.md ({len(current_skill_md)} chars)")
|
||||
else:
|
||||
print(" ℹ No existing SKILL.md, will create new one")
|
||||
|
||||
prompt = self._build_enhancement_prompt(skill_dir.name, references, current_skill_md)
|
||||
|
||||
print("\n🤖 Asking MiniMax-M2.7 to enhance SKILL.md...")
|
||||
print(f" Input: {len(prompt):,} characters")
|
||||
|
||||
try:
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url="https://api.minimax.io/v1",
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="MiniMax-M2.7",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an expert technical writer creating system instructions for MiniMax AI.",
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=4096,
|
||||
)
|
||||
|
||||
enhanced_content = response.choices[0].message.content
|
||||
print(f" ✓ Generated enhanced SKILL.md ({len(enhanced_content)} chars)\n")
|
||||
|
||||
if skill_md_path.exists():
|
||||
backup_path = skill_md_path.with_suffix(".md.backup")
|
||||
skill_md_path.rename(backup_path)
|
||||
print(f" 💾 Backed up original to: {backup_path.name}")
|
||||
|
||||
skill_md_path.write_text(enhanced_content, encoding="utf-8")
|
||||
print(" ✅ Saved enhanced SKILL.md")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error calling MiniMax API: {e}")
|
||||
return False
|
||||
|
||||
def _read_reference_files(
|
||||
self, references_dir: Path, max_chars: int = 200000
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Read reference markdown files from skill directory.
|
||||
|
||||
Args:
|
||||
references_dir: Path to references directory
|
||||
max_chars: Maximum total characters to read
|
||||
|
||||
Returns:
|
||||
Dictionary mapping filename to content
|
||||
"""
|
||||
if not references_dir.exists():
|
||||
return {}
|
||||
|
||||
references = {}
|
||||
total_chars = 0
|
||||
|
||||
for ref_file in sorted(references_dir.glob("*.md")):
|
||||
if total_chars >= max_chars:
|
||||
break
|
||||
|
||||
try:
|
||||
content = ref_file.read_text(encoding="utf-8")
|
||||
if len(content) > 30000:
|
||||
content = content[:30000] + "\n\n...(truncated)"
|
||||
|
||||
references[ref_file.name] = content
|
||||
total_chars += len(content)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not read {ref_file.name}: {e}")
|
||||
|
||||
return references
|
||||
|
||||
def _build_enhancement_prompt(
|
||||
self, skill_name: str, references: dict[str, str], current_skill_md: str = None
|
||||
) -> str:
|
||||
"""
|
||||
Build MiniMax API prompt for enhancement.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
references: Dictionary of reference content
|
||||
current_skill_md: Existing SKILL.md content (optional)
|
||||
|
||||
Returns:
|
||||
Enhancement prompt for MiniMax-M2.7
|
||||
"""
|
||||
prompt = f"""You are creating system instructions for a MiniMax AI assistant about: {skill_name}
|
||||
|
||||
I've scraped documentation and organized it into reference files. Your job is to create EXCELLENT system instructions that will help the assistant use this documentation effectively.
|
||||
|
||||
CURRENT INSTRUCTIONS:
|
||||
{"```" if current_skill_md else "(none - create from scratch)"}
|
||||
{current_skill_md or "No existing instructions"}
|
||||
{"```" if current_skill_md else ""}
|
||||
|
||||
REFERENCE DOCUMENTATION:
|
||||
"""
|
||||
|
||||
for filename, content in references.items():
|
||||
prompt += f"\n\n## {filename}\n```markdown\n{content[:30000]}\n```\n"
|
||||
|
||||
prompt += """
|
||||
|
||||
YOUR TASK:
|
||||
Create enhanced system instructions that include:
|
||||
|
||||
1. **Clear role definition** - "You are an expert assistant for [topic]"
|
||||
2. **Knowledge base description** - What documentation is attached
|
||||
3. **Excellent Quick Reference** - Extract 5-10 of the BEST, most practical code examples from the reference docs
|
||||
- Choose SHORT, clear examples that demonstrate common tasks
|
||||
- Include both simple and intermediate examples
|
||||
- Annotate examples with clear descriptions
|
||||
- Use proper language tags (cpp, python, javascript, json, etc.)
|
||||
4. **Response guidelines** - How the assistant should help users
|
||||
5. **Search strategy** - How to find information in the knowledge base
|
||||
6. **DO NOT use YAML frontmatter** - This is plain text instructions
|
||||
|
||||
IMPORTANT:
|
||||
- Extract REAL examples from the reference docs, don't make them up
|
||||
- Prioritize SHORT, clear examples (5-20 lines max)
|
||||
- Make it actionable and practical
|
||||
- Write clear, direct instructions
|
||||
- Focus on how the assistant should behave and respond
|
||||
- NO YAML frontmatter (no --- blocks)
|
||||
|
||||
OUTPUT:
|
||||
Return ONLY the complete system instructions as plain text.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
Reference in New Issue
Block a user