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
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ SAMBNOVA_API_KEY=""
# TODO: Get your Groq API key from https://console.groq.com/keys
GROQ_API_KEY=""

MINIMAX_API_KEY=""

JINA_API_KEY=""

MAX_WEB_RESEARCH_LOOPS=3
Expand All @@ -36,6 +38,9 @@ LLM_MODEL=gemini-2.5-pro
# LLM_PROVIDER=sambnova
# LLM_MODEL=DeepSeek-V3-0324

# LLM_PROVIDER=minimax
# LLM_MODEL=MiniMax-M2.7

## Activity generation configuration ##
ENABLE_ACTIVITY_GENERATION=true
ACTIVITY_VERBOSITY=medium
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,11 @@ cd ai-research-assistant && npm install && npm run build && cd ..
- `TAVILY_API_KEY` - Tavily search API key
- **One LLM provider key:**
- `OPENAI_API_KEY` - OpenAI API key
- `ANTHROPIC_API_KEY` - Anthropic API key
- `ANTHROPIC_API_KEY` - Anthropic API key
- `GROQ_API_KEY` - Groq API key
- `GOOGLE_CLOUD_PROJECT` - Google Cloud project ID
- `SAMBNOVA_API_KEY` - SambaNova API key
- `MINIMAX_API_KEY` - [MiniMax](https://www.minimax.io/) API key

**Optional Settings:**
- `LLM_PROVIDER` - Default provider (default: `openai`)
Expand All @@ -109,6 +110,7 @@ cd ai-research-assistant && npm install && npm run build && cd ..
| **Google** | `gemini-2.5-pro` | `gemini-2.5-pro`, `gemini-1.5-pro-latest`, `gemini-1.5-flash-latest` |
| **Groq** | `deepseek-r1-distill-llama-70b` | `deepseek-r1-distill-llama-70b`, `llama-3.3-70b-versatile`, `llama3-70b-8192` |
| **SambaNova** | `DeepSeek-V3-0324` | `DeepSeek-V3-0324` |
| **MiniMax** | `MiniMax-M2.7` | `MiniMax-M2.7`, `MiniMax-M2.7-highspeed` |

### Running the Application

Expand Down
11 changes: 11 additions & 0 deletions ai-research-assistant/src/components/InitialScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ const MODEL_OPTIONS = [
</svg>
),
},
{
key: "minimax-m2.7",
label: "MiniMax-M2.7",
model: "MiniMax-M2.7",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4l4.5 8L3 20h3l3-5.5L12 20h3l-4.5-8L15 4h-3l-3 5.5L6 4H3z" fill="#6366F1"/>
<path d="M15 4l4.5 8L15 20h3l3-5.5V20h2V4h-2v5.5L18 4h-3z" fill="#6366F1"/>
</svg>
),
},
]

// Suggested questions for different modes
Expand Down
143 changes: 143 additions & 0 deletions llm_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
SFR_GATEWAY_API_KEY = os.getenv("SFR_GATEWAY_API_KEY")
SAMBNOVA_API_KEY = os.getenv("SAMBNOVA_API_KEY")
MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY")
GOOGLE_CLOUD_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT")
GOOGLE_CLOUD_LOCATION = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")

Expand All @@ -45,6 +46,9 @@
# Google Gemini token limits
GOOGLE_MAX_OUTPUT_TOKENS = 30000

# MiniMax token limits
MINIMAX_MAX_TOKENS = 16384

# Get the current date in various formats for the system prompt
CURRENT_DATE = datetime.now().strftime("%Y-%m-%d")
CURRENT_YEAR = datetime.now().year
Expand Down Expand Up @@ -134,6 +138,15 @@ def _resolve_openai_auth():
"default_model": "gemini-2.5-pro",
"requires_api_key": GOOGLE_CLOUD_PROJECT,
},
# MiniMax models (OpenAI-compatible API)
"minimax": {
"available_models": [
"MiniMax-M2.7", # Latest flagship model (1M context)
"MiniMax-M2.7-highspeed", # High-speed variant
],
"default_model": "MiniMax-M2.7",
"requires_api_key": MINIMAX_API_KEY,
},
}

# Base system prompt template - will be formatted with current date information
Expand Down Expand Up @@ -1180,6 +1193,110 @@ def stream(self, messages, config=None):
yield ChatMessage(content=f"Error: {str(e)}", role="ai")


# Custom client for MiniMax API (OpenAI-compatible)
class MiniMaxClient:
"""Client for MiniMax AI API via OpenAI-compatible endpoint.

MiniMax provides an OpenAI-compatible API at https://api.minimax.io/v1.
This client handles:
1. Temperature clamping to (0.0, 1.0] as required by MiniMax
2. Stripping <think>...</think> tags from reasoning model responses
3. Streaming support for long-running operations
"""

def __init__(
self,
model_name: str,
api_key: str,
max_tokens: int = MINIMAX_MAX_TOKENS,
):
"""Initialize the MiniMax client.

Args:
model_name: The name of the model to use (e.g., 'MiniMax-M2.7')
api_key: The MiniMax API key
max_tokens: The maximum number of tokens to generate
"""
self.model = model_name
self.model_name = model_name
self._api_key = api_key
self._max_tokens = max_tokens
self._client = openai.OpenAI(
api_key=api_key,
base_url="https://api.minimax.io/v1",
)

@staticmethod
def _clamp_temperature(temperature: float) -> float:
"""Clamp temperature to MiniMax's valid range (0.0, 1.0]."""
if temperature <= 0.0:
return 0.01
if temperature > 1.0:
return 1.0
return temperature

@staticmethod
def _strip_think_tags(text: str) -> str:
"""Strip <think>...</think> tags from reasoning model responses."""
import re
return re.sub(r"<think>.*?</think>\s*", "", text, flags=re.DOTALL)

@traceable
def invoke(self, messages, config=None):
"""Invoke the MiniMax model with the given messages.

Args:
messages: List of LangChain message objects
config: Optional RunConfig for tracing

Returns:
A string with the model's response
"""
try:
# Convert LangChain messages to OpenAI format
openai_messages = []
for msg in messages:
role = msg.type
if role == "human":
role = "user"
elif role == "system":
role = "system"
elif role == "ai":
role = "assistant"
openai_messages.append({"role": role, "content": msg.content})

temperature = self._clamp_temperature(0.5)

print(f"Calling MiniMax API with model {self.model_name} (temperature={temperature})")
response = self._client.chat.completions.create(
model=self.model_name,
messages=openai_messages,
max_tokens=self._max_tokens,
temperature=temperature,
stream=True,
)

# Collect content from the stream
content_chunks = []
for chunk in response:
if hasattr(chunk, "choices") and chunk.choices:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
content_chunks.append(delta.content)
print(".", end="", flush=True)

print("\nMiniMax streaming complete")
response_text = "".join(content_chunks)

# Strip think tags if present
response_text = self._strip_think_tags(response_text)

return response_text
except Exception as e:
print(f"[MiniMaxClient ERROR] {str(e)}")
raise


def get_available_providers():
"""Returns a list of available providers based on configured API keys."""
available_providers = []
Expand Down Expand Up @@ -1347,6 +1464,17 @@ def get_llm_client(provider, model_name=None):
convert_system_message_to_human=True, # Recommended for Gemini
max_output_tokens=GOOGLE_MAX_OUTPUT_TOKENS, # Using variable instead of hardcoded value
)
elif provider == "minimax":
if not MINIMAX_API_KEY:
raise ValueError("MINIMAX_API_KEY is not set in environment")
if not model_name:
model_name = MODEL_CONFIGS["minimax"]["default_model"]
print(f"Using MiniMaxClient for {model_name}")
return MiniMaxClient(
model_name=model_name,
api_key=MINIMAX_API_KEY,
max_tokens=MINIMAX_MAX_TOKENS,
)
else:
raise ValueError(f"Unsupported provider: {provider}")

Expand Down Expand Up @@ -1481,13 +1609,28 @@ async def get_async_llm_client(provider, model_name=None):
convert_system_message_to_human=True, # Recommended for Gemini
max_output_tokens=GOOGLE_MAX_OUTPUT_TOKENS, # Using variable instead of hardcoded value
)
elif provider == "minimax":
if not MINIMAX_API_KEY:
raise ValueError("MINIMAX_API_KEY is not set in environment")
if not model_name:
model_name = MODEL_CONFIGS["minimax"]["default_model"]
logger.info(
f"[get_async_llm_client] Creating async MiniMaxClient with model {model_name}"
)
return MiniMaxClient(
model_name=model_name,
api_key=MINIMAX_API_KEY,
max_tokens=MINIMAX_MAX_TOKENS,
)
else:
# For providers that don't have standard async clients via Langchain yet
supported_providers = ["openai", "anthropic"]
if GROQ_API_KEY:
supported_providers.append("groq")
if GOOGLE_CLOUD_PROJECT: # Add google to supported list
supported_providers.append("google")
if MINIMAX_API_KEY:
supported_providers.append("minimax")

error_msg = f"Async client not supported or API key missing for provider: {provider}. Supported with keys: {', '.join(supported_providers)}."
logger.error(f"[get_async_llm_client] {error_msg}")
Expand Down
11 changes: 8 additions & 3 deletions src/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class LLMProvider(Enum):
ANTHROPIC = "anthropic"
GROQ = "groq"
GOOGLE = "google"
MINIMAX = "minimax"


class ActivityVerbosity(Enum):
Expand Down Expand Up @@ -115,9 +116,13 @@ def llm_model(self) -> str:
"llama-3.3-70b-versatile"
if provider_str == "groq"
else (
"gemini-2.5-pro"
if provider_str == "google"
else "gemini-2.5-pro"
"MiniMax-M2.7"
if provider_str == "minimax"
else (
"gemini-2.5-pro"
if provider_str == "google"
else "gemini-2.5-pro"
)
)
)
)
Expand Down
Empty file added tests/__init__.py
Empty file.
Loading