mirror of https://github.com/kortix-ai/suna.git
AI: Add to all the related doc : morphllm as a provider; make it optional step when setting up by getting key at this :
https://morphllm.com/api-keys when setting up, user should be able to choose to go to morphllm to get a key; or provide open router apikey (if not already in the previous step)
This commit is contained in:
parent
09751be00b
commit
93724249e7
|
@ -151,6 +151,7 @@ We welcome contributions from the community! Please see our [Contributing Guide]
|
||||||
- [Playwright](https://playwright.dev/) - Browser automation
|
- [Playwright](https://playwright.dev/) - Browser automation
|
||||||
- [OpenAI](https://openai.com/) - LLM provider
|
- [OpenAI](https://openai.com/) - LLM provider
|
||||||
- [Anthropic](https://www.anthropic.com/) - LLM provider
|
- [Anthropic](https://www.anthropic.com/) - LLM provider
|
||||||
|
- [Morph](https://morphllm.com/) - For AI-powered code editing
|
||||||
- [Tavily](https://tavily.com/) - Search capabilities
|
- [Tavily](https://tavily.com/) - Search capabilities
|
||||||
- [Firecrawl](https://firecrawl.dev/) - Web scraping capabilities
|
- [Firecrawl](https://firecrawl.dev/) - Web scraping capabilities
|
||||||
- [QStash](https://upstash.com/qstash) - Background job processing and workflows
|
- [QStash](https://upstash.com/qstash) - Background job processing and workflows
|
||||||
|
|
|
@ -28,6 +28,7 @@ AWS_REGION_NAME=
|
||||||
|
|
||||||
GROQ_API_KEY=
|
GROQ_API_KEY=
|
||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
|
MORPH_API_KEY=
|
||||||
|
|
||||||
# DATA APIS
|
# DATA APIS
|
||||||
RAPID_API_KEY=
|
RAPID_API_KEY=
|
||||||
|
|
|
@ -38,7 +38,7 @@ class LLMRetryError(LLMError):
|
||||||
|
|
||||||
def setup_api_keys() -> None:
|
def setup_api_keys() -> None:
|
||||||
"""Set up API keys from environment variables."""
|
"""Set up API keys from environment variables."""
|
||||||
providers = ['OPENAI', 'ANTHROPIC', 'GROQ', 'OPENROUTER', 'XAI']
|
providers = ['OPENAI', 'ANTHROPIC', 'GROQ', 'OPENROUTER', 'XAI', 'MORPH']
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
key = getattr(config, f'{provider}_API_KEY')
|
key = getattr(config, f'{provider}_API_KEY')
|
||||||
if key:
|
if key:
|
||||||
|
|
|
@ -47,6 +47,9 @@ Obtain the following API keys:
|
||||||
- [OpenRouter](https://openrouter.ai/)
|
- [OpenRouter](https://openrouter.ai/)
|
||||||
- [AWS Bedrock](https://aws.amazon.com/bedrock/)
|
- [AWS Bedrock](https://aws.amazon.com/bedrock/)
|
||||||
|
|
||||||
|
- **AI-Powered Code Editing (Optional but Recommended)**:
|
||||||
|
- [Morph](https://morphllm.com/api-keys) - For intelligent code editing capabilities
|
||||||
|
|
||||||
- **Search and Web Scraping**:
|
- **Search and Web Scraping**:
|
||||||
|
|
||||||
- [Tavily](https://tavily.com/) - For enhanced search capabilities
|
- [Tavily](https://tavily.com/) - For enhanced search capabilities
|
||||||
|
@ -169,6 +172,7 @@ RABBITMQ_PORT=5672
|
||||||
ANTHROPIC_API_KEY=your-anthropic-key
|
ANTHROPIC_API_KEY=your-anthropic-key
|
||||||
OPENAI_API_KEY=your-openai-key
|
OPENAI_API_KEY=your-openai-key
|
||||||
OPENROUTER_API_KEY=your-openrouter-key
|
OPENROUTER_API_KEY=your-openrouter-key
|
||||||
|
MORPH_API_KEY=
|
||||||
MODEL_TO_USE=anthropic/claude-sonnet-4-20250514
|
MODEL_TO_USE=anthropic/claude-sonnet-4-20250514
|
||||||
|
|
||||||
# WEB SEARCH
|
# WEB SEARCH
|
||||||
|
|
111
setup.py
111
setup.py
|
@ -129,6 +129,7 @@ def load_existing_env_vars():
|
||||||
"OPENAI_API_KEY": backend_env.get("OPENAI_API_KEY", ""),
|
"OPENAI_API_KEY": backend_env.get("OPENAI_API_KEY", ""),
|
||||||
"ANTHROPIC_API_KEY": backend_env.get("ANTHROPIC_API_KEY", ""),
|
"ANTHROPIC_API_KEY": backend_env.get("ANTHROPIC_API_KEY", ""),
|
||||||
"OPENROUTER_API_KEY": backend_env.get("OPENROUTER_API_KEY", ""),
|
"OPENROUTER_API_KEY": backend_env.get("OPENROUTER_API_KEY", ""),
|
||||||
|
"MORPH_API_KEY": backend_env.get("MORPH_API_KEY", ""),
|
||||||
"MODEL_TO_USE": backend_env.get("MODEL_TO_USE", ""),
|
"MODEL_TO_USE": backend_env.get("MODEL_TO_USE", ""),
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
@ -275,7 +276,7 @@ class SetupWizard:
|
||||||
else:
|
else:
|
||||||
self.env_vars[key] = value
|
self.env_vars[key] = value
|
||||||
|
|
||||||
self.total_steps = 17
|
self.total_steps = 18
|
||||||
|
|
||||||
def show_current_config(self):
|
def show_current_config(self):
|
||||||
"""Shows the current configuration status."""
|
"""Shows the current configuration status."""
|
||||||
|
@ -297,7 +298,7 @@ class SetupWizard:
|
||||||
llm_keys = [
|
llm_keys = [
|
||||||
k
|
k
|
||||||
for k in self.env_vars["llm"]
|
for k in self.env_vars["llm"]
|
||||||
if k != "MODEL_TO_USE" and self.env_vars["llm"][k]
|
if k != "MODEL_TO_USE" and self.env_vars["llm"][k] and k != "MORPH_API_KEY"
|
||||||
]
|
]
|
||||||
if llm_keys:
|
if llm_keys:
|
||||||
providers = [k.split("_")[0].capitalize() for k in llm_keys]
|
providers = [k.split("_")[0].capitalize() for k in llm_keys]
|
||||||
|
@ -359,6 +360,14 @@ class SetupWizard:
|
||||||
else:
|
else:
|
||||||
config_items.append(f"{Colors.YELLOW}○{Colors.ENDC} Webhook")
|
config_items.append(f"{Colors.YELLOW}○{Colors.ENDC} Webhook")
|
||||||
|
|
||||||
|
# Check Morph (optional but recommended)
|
||||||
|
if self.env_vars["llm"].get("MORPH_API_KEY"):
|
||||||
|
config_items.append(f"{Colors.GREEN}✓{Colors.ENDC} Morph (Code Editing)")
|
||||||
|
elif self.env_vars["llm"].get("OPENROUTER_API_KEY"):
|
||||||
|
config_items.append(f"{Colors.CYAN}○{Colors.ENDC} Morph (fallback to OpenRouter)")
|
||||||
|
else:
|
||||||
|
config_items.append(f"{Colors.YELLOW}○{Colors.ENDC} Morph (recommended)")
|
||||||
|
|
||||||
if any("✓" in item for item in config_items):
|
if any("✓" in item for item in config_items):
|
||||||
print_info("Current configuration status:")
|
print_info("Current configuration status:")
|
||||||
for item in config_items:
|
for item in config_items:
|
||||||
|
@ -381,18 +390,19 @@ class SetupWizard:
|
||||||
self.run_step(3, self.collect_supabase_info)
|
self.run_step(3, self.collect_supabase_info)
|
||||||
self.run_step(4, self.collect_daytona_info)
|
self.run_step(4, self.collect_daytona_info)
|
||||||
self.run_step(5, self.collect_llm_api_keys)
|
self.run_step(5, self.collect_llm_api_keys)
|
||||||
self.run_step(6, self.collect_search_api_keys)
|
self.run_step(6, self.collect_morph_api_key)
|
||||||
self.run_step(7, self.collect_rapidapi_keys)
|
self.run_step(7, self.collect_search_api_keys)
|
||||||
self.run_step(8, self.collect_smithery_keys)
|
self.run_step(8, self.collect_rapidapi_keys)
|
||||||
self.run_step(9, self.collect_qstash_keys)
|
self.run_step(9, self.collect_smithery_keys)
|
||||||
self.run_step(10, self.collect_mcp_keys)
|
self.run_step(10, self.collect_qstash_keys)
|
||||||
self.run_step(11, self.collect_pipedream_keys)
|
self.run_step(11, self.collect_mcp_keys)
|
||||||
self.run_step(12, self.collect_slack_keys)
|
self.run_step(12, self.collect_pipedream_keys)
|
||||||
self.run_step(13, self.collect_webhook_keys)
|
self.run_step(13, self.collect_slack_keys)
|
||||||
self.run_step(14, self.configure_env_files)
|
self.run_step(14, self.collect_webhook_keys)
|
||||||
self.run_step(15, self.setup_supabase_database)
|
self.run_step(15, self.configure_env_files)
|
||||||
self.run_step(16, self.install_dependencies)
|
self.run_step(16, self.setup_supabase_database)
|
||||||
self.run_step(17, self.start_suna)
|
self.run_step(17, self.install_dependencies)
|
||||||
|
self.run_step(18, self.start_suna)
|
||||||
|
|
||||||
self.final_instructions()
|
self.final_instructions()
|
||||||
|
|
||||||
|
@ -747,9 +757,58 @@ class SetupWizard:
|
||||||
f"LLM keys saved. Default model: {self.env_vars['llm'].get('MODEL_TO_USE', 'Not set')}"
|
f"LLM keys saved. Default model: {self.env_vars['llm'].get('MODEL_TO_USE', 'Not set')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def collect_morph_api_key(self):
|
||||||
|
"""Collects the optional MorphLLM API key for code editing."""
|
||||||
|
print_step(6, self.total_steps, "Configure AI-Powered Code Editing (Optional)")
|
||||||
|
|
||||||
|
existing_key = self.env_vars["llm"].get("MORPH_API_KEY", "")
|
||||||
|
openrouter_key = self.env_vars["llm"].get("OPENROUTER_API_KEY", "")
|
||||||
|
|
||||||
|
if existing_key:
|
||||||
|
print_info(f"Found existing Morph API key: {mask_sensitive_value(existing_key)}")
|
||||||
|
print_info("AI-powered code editing is enabled using Morph.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_info("Suna uses Morph for fast, intelligent code editing.")
|
||||||
|
print_info("This is optional but highly recommended for the best experience.")
|
||||||
|
|
||||||
|
if openrouter_key:
|
||||||
|
print_info(
|
||||||
|
f"An OpenRouter API key is already configured. It can be used as a fallback for code editing if you don't provide a Morph key."
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
choice = input("Do you want to add a Morph API key now? (y/n): ").lower().strip()
|
||||||
|
if choice in ['y', 'n', '']:
|
||||||
|
break
|
||||||
|
print_error("Invalid input. Please enter 'y' or 'n'.")
|
||||||
|
|
||||||
|
if choice == 'y':
|
||||||
|
print_info("Great! Please get your API key from: https://morphllm.com/api-keys")
|
||||||
|
morph_api_key = self._get_input(
|
||||||
|
"Enter your Morph API key (or press Enter to skip): ",
|
||||||
|
validate_api_key,
|
||||||
|
"The key seems invalid, but continuing. You can edit it later in backend/.env",
|
||||||
|
allow_empty=True,
|
||||||
|
default_value="",
|
||||||
|
)
|
||||||
|
if morph_api_key:
|
||||||
|
self.env_vars["llm"]["MORPH_API_KEY"] = morph_api_key
|
||||||
|
print_success("Morph API key saved. AI-powered code editing is enabled.")
|
||||||
|
else:
|
||||||
|
if openrouter_key:
|
||||||
|
print_info("Skipping Morph key. OpenRouter will be used for code editing.")
|
||||||
|
else:
|
||||||
|
print_warning("Skipping Morph key. Code editing will use a less capable model.")
|
||||||
|
else:
|
||||||
|
if openrouter_key:
|
||||||
|
print_info("Okay, OpenRouter will be used as a fallback for code editing.")
|
||||||
|
else:
|
||||||
|
print_warning("Okay, code editing will use a less capable model without a Morph or OpenRouter key.")
|
||||||
|
|
||||||
def collect_search_api_keys(self):
|
def collect_search_api_keys(self):
|
||||||
"""Collects API keys for search and web scraping tools."""
|
"""Collects API keys for search and web scraping tools."""
|
||||||
print_step(6, self.total_steps, "Collecting Search and Scraping API Keys")
|
print_step(7, self.total_steps, "Collecting Search and Scraping API Keys")
|
||||||
|
|
||||||
# Check if we already have values configured
|
# Check if we already have values configured
|
||||||
has_existing = any(self.env_vars["search"].values())
|
has_existing = any(self.env_vars["search"].values())
|
||||||
|
@ -811,7 +870,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def collect_rapidapi_keys(self):
|
def collect_rapidapi_keys(self):
|
||||||
"""Collects the optional RapidAPI key."""
|
"""Collects the optional RapidAPI key."""
|
||||||
print_step(7, self.total_steps, "Collecting RapidAPI Key (Optional)")
|
print_step(8, self.total_steps, "Collecting RapidAPI Key (Optional)")
|
||||||
|
|
||||||
# Check if we already have a value configured
|
# Check if we already have a value configured
|
||||||
existing_key = self.env_vars["rapidapi"]["RAPID_API_KEY"]
|
existing_key = self.env_vars["rapidapi"]["RAPID_API_KEY"]
|
||||||
|
@ -841,7 +900,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def collect_smithery_keys(self):
|
def collect_smithery_keys(self):
|
||||||
"""Collects the optional Smithery API key."""
|
"""Collects the optional Smithery API key."""
|
||||||
print_step(8, self.total_steps, "Collecting Smithery API Key (Optional)")
|
print_step(9, self.total_steps, "Collecting Smithery API Key (Optional)")
|
||||||
|
|
||||||
# Check if we already have a value configured
|
# Check if we already have a value configured
|
||||||
existing_key = self.env_vars["smithery"]["SMITHERY_API_KEY"]
|
existing_key = self.env_vars["smithery"]["SMITHERY_API_KEY"]
|
||||||
|
@ -874,7 +933,7 @@ class SetupWizard:
|
||||||
def collect_qstash_keys(self):
|
def collect_qstash_keys(self):
|
||||||
"""Collects the required QStash configuration."""
|
"""Collects the required QStash configuration."""
|
||||||
print_step(
|
print_step(
|
||||||
9,
|
10,
|
||||||
self.total_steps,
|
self.total_steps,
|
||||||
"Collecting QStash Configuration",
|
"Collecting QStash Configuration",
|
||||||
)
|
)
|
||||||
|
@ -929,7 +988,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def collect_mcp_keys(self):
|
def collect_mcp_keys(self):
|
||||||
"""Collects the MCP configuration."""
|
"""Collects the MCP configuration."""
|
||||||
print_step(10, self.total_steps, "Collecting MCP Configuration")
|
print_step(11, self.total_steps, "Collecting MCP Configuration")
|
||||||
|
|
||||||
# Check if we already have an encryption key configured
|
# Check if we already have an encryption key configured
|
||||||
existing_key = self.env_vars["mcp"]["MCP_CREDENTIAL_ENCRYPTION_KEY"]
|
existing_key = self.env_vars["mcp"]["MCP_CREDENTIAL_ENCRYPTION_KEY"]
|
||||||
|
@ -949,7 +1008,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def collect_pipedream_keys(self):
|
def collect_pipedream_keys(self):
|
||||||
"""Collects the optional Pipedream configuration."""
|
"""Collects the optional Pipedream configuration."""
|
||||||
print_step(11, self.total_steps, "Collecting Pipedream Configuration (Optional)")
|
print_step(12, self.total_steps, "Collecting Pipedream Configuration (Optional)")
|
||||||
|
|
||||||
# Check if we already have values configured
|
# Check if we already have values configured
|
||||||
has_existing = any(self.env_vars["pipedream"].values())
|
has_existing = any(self.env_vars["pipedream"].values())
|
||||||
|
@ -1009,7 +1068,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def collect_slack_keys(self):
|
def collect_slack_keys(self):
|
||||||
"""Collects the optional Slack configuration."""
|
"""Collects the optional Slack configuration."""
|
||||||
print_step(12, self.total_steps, "Collecting Slack Configuration (Optional)")
|
print_step(13, self.total_steps, "Collecting Slack Configuration (Optional)")
|
||||||
|
|
||||||
# Check if we already have values configured
|
# Check if we already have values configured
|
||||||
has_existing = any(self.env_vars["slack"].values())
|
has_existing = any(self.env_vars["slack"].values())
|
||||||
|
@ -1062,7 +1121,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def collect_webhook_keys(self):
|
def collect_webhook_keys(self):
|
||||||
"""Collects the webhook configuration."""
|
"""Collects the webhook configuration."""
|
||||||
print_step(13, self.total_steps, "Collecting Webhook Configuration")
|
print_step(14, self.total_steps, "Collecting Webhook Configuration")
|
||||||
|
|
||||||
# Check if we already have values configured
|
# Check if we already have values configured
|
||||||
has_existing = bool(self.env_vars["webhook"]["WEBHOOK_BASE_URL"])
|
has_existing = bool(self.env_vars["webhook"]["WEBHOOK_BASE_URL"])
|
||||||
|
@ -1087,7 +1146,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def configure_env_files(self):
|
def configure_env_files(self):
|
||||||
"""Configures and writes the .env files for frontend and backend."""
|
"""Configures and writes the .env files for frontend and backend."""
|
||||||
print_step(14, self.total_steps, "Configuring Environment Files")
|
print_step(15, self.total_steps, "Configuring Environment Files")
|
||||||
|
|
||||||
# --- Backend .env ---
|
# --- Backend .env ---
|
||||||
is_docker = self.env_vars["setup_method"] == "docker"
|
is_docker = self.env_vars["setup_method"] == "docker"
|
||||||
|
@ -1143,7 +1202,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def setup_supabase_database(self):
|
def setup_supabase_database(self):
|
||||||
"""Links the project to Supabase and pushes database migrations."""
|
"""Links the project to Supabase and pushes database migrations."""
|
||||||
print_step(15, self.total_steps, "Setting up Supabase Database")
|
print_step(16, self.total_steps, "Setting up Supabase Database")
|
||||||
|
|
||||||
print_info(
|
print_info(
|
||||||
"This step will link your project to Supabase and push database migrations."
|
"This step will link your project to Supabase and push database migrations."
|
||||||
|
@ -1242,7 +1301,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def install_dependencies(self):
|
def install_dependencies(self):
|
||||||
"""Installs frontend and backend dependencies for manual setup."""
|
"""Installs frontend and backend dependencies for manual setup."""
|
||||||
print_step(16, self.total_steps, "Installing Dependencies")
|
print_step(17, self.total_steps, "Installing Dependencies")
|
||||||
if self.env_vars["setup_method"] == "docker":
|
if self.env_vars["setup_method"] == "docker":
|
||||||
print_info(
|
print_info(
|
||||||
"Skipping dependency installation for Docker setup (will be handled by Docker Compose)."
|
"Skipping dependency installation for Docker setup (will be handled by Docker Compose)."
|
||||||
|
@ -1284,7 +1343,7 @@ class SetupWizard:
|
||||||
|
|
||||||
def start_suna(self):
|
def start_suna(self):
|
||||||
"""Starts Suna using Docker Compose or shows instructions for manual startup."""
|
"""Starts Suna using Docker Compose or shows instructions for manual startup."""
|
||||||
print_step(17, self.total_steps, "Starting Suna")
|
print_step(18, self.total_steps, "Starting Suna")
|
||||||
if self.env_vars["setup_method"] == "docker":
|
if self.env_vars["setup_method"] == "docker":
|
||||||
print_info("Starting Suna with Docker Compose...")
|
print_info("Starting Suna with Docker Compose...")
|
||||||
try:
|
try:
|
||||||
|
|
Loading…
Reference in New Issue