-
-

-
-
Welcome to Kortix Suna!
-
-
Hi {user_name},
-
-
Welcome to Kortix Suna — we're excited to have you on board!
-
-
To get started, we'd like to get to know you better: fill out this short form!
-
-
To celebrate your arrival, here's a 15% discount to try out the best version of Suna (1 month):
-
-
🎁 Use code WELCOME15 at checkout.
-
-
Let us know if you need help getting started or have questions — we're always here, and join our Discord community.
-
-
For your business: if you want to automate manual and ordinary tasks for your company, book a call with us here
-
-
Thanks again, and welcome to the Suna community 🌞
-
-
— The Suna Team
-
-
Go to the platform
-
-
-"""
-
- def _get_welcome_email_text(self, user_name: str) -> str:
- return f"""Hi {user_name},
-
-Welcome to Suna — we're excited to have you on board!
-
-To get started, we'd like to get to know you better: fill out this short form!
-https://docs.google.com/forms/d/e/1FAIpQLSef1EHuqmIh_iQz-kwhjnzSC3Ml-V_5wIySDpMoMU9W_j24JQ/viewform
-
-To celebrate your arrival, here's a 15% discount to try out the best version of Suna (1 month):
-🎁 Use code WELCOME15 at checkout.
-
-Let us know if you need help getting started or have questions — we're always here, and join our Discord community: https://discord.com/invite/FjD644cfcs
-
-For your business: if you want to automate manual and ordinary tasks for your company, book a call with us here: https://cal.com/team/kortix/enterprise-demo
-
-Thanks again, and welcome to the Suna community 🌞
-
-— The Suna Team
-
-Go to the platform: https://www.suna.so/
-
----
-© 2024 Suna. All rights reserved.
-You received this email because you signed up for a Suna account."""
-
-email_service = EmailService()
diff --git a/app/services/email_api.py b/app/services/email_api.py
deleted file mode 100644
index 9834c7b..0000000
--- a/app/services/email_api.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from fastapi import APIRouter, HTTPException, Depends
-from pydantic import BaseModel, EmailStr
-from typing import Optional
-import asyncio
-from services.email import email_service
-from utils.logger import logger
-
-router = APIRouter()
-
-class SendWelcomeEmailRequest(BaseModel):
- email: EmailStr
- name: Optional[str] = None
-
-class EmailResponse(BaseModel):
- success: bool
- message: str
-
-@router.post("/send-welcome-email", response_model=EmailResponse)
-async def send_welcome_email(request: SendWelcomeEmailRequest):
- try:
- logger.info(f"Sending welcome email to {request.email}")
- success = email_service.send_welcome_email(
- user_email=request.email,
- user_name=request.name
- )
-
- if success:
- return EmailResponse(
- success=True,
- message="Welcome email sent successfully"
- )
- else:
- return EmailResponse(
- success=False,
- message="Failed to send welcome email"
- )
-
- except Exception as e:
- logger.error(f"Error sending welcome email to {request.email}: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail="Internal server error while sending email"
- )
-
-@router.post("/send-welcome-email-background", response_model=EmailResponse)
-async def send_welcome_email_background(request: SendWelcomeEmailRequest):
- try:
- logger.info(f"Queuing welcome email for {request.email}")
-
- def send_email():
- return email_service.send_welcome_email(
- user_email=request.email,
- user_name=request.name
- )
-
- import concurrent.futures
- with concurrent.futures.ThreadPoolExecutor() as executor:
- future = executor.submit(send_email)
-
- return EmailResponse(
- success=True,
- message="Welcome email queued for sending"
- )
-
- except Exception as e:
- logger.error(f"Error queuing welcome email for {request.email}: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail="Internal server error while queuing email"
- )
diff --git a/app/services/langfuse.py b/app/services/langfuse.py
deleted file mode 100644
index cf624bf..0000000
--- a/app/services/langfuse.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import os
-from langfuse import Langfuse
-
-public_key = os.getenv("LANGFUSE_PUBLIC_KEY")
-secret_key = os.getenv("LANGFUSE_SECRET_KEY")
-host = os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com")
-
-enabled = False
-if public_key and secret_key:
- enabled = True
-
-langfuse = Langfuse(enabled=enabled)
diff --git a/app/services/llm.py b/app/services/llm.py
deleted file mode 100644
index f525f24..0000000
--- a/app/services/llm.py
+++ /dev/null
@@ -1,411 +0,0 @@
-"""
-LLM API interface for making calls to various language models.
-
-This module provides a unified interface for making API calls to different LLM providers
-(OpenAI, Anthropic, Groq, etc.) using LiteLLM. It includes support for:
-- Streaming responses
-- Tool calls and function calling
-- Retry logic with exponential backoff
-- Model-specific configurations
-- Comprehensive error handling and logging
-"""
-
-from typing import Union, Dict, Any, Optional, AsyncGenerator, List
-import os
-import json
-import asyncio
-from openai import OpenAIError
-import litellm
-# from utils.logger import logger
-# from utils.config import config
-
-# litellm.set_verbose=True
-litellm.modify_params=True
-
-# Constants
-MAX_RETRIES = 2
-RATE_LIMIT_DELAY = 30
-RETRY_DELAY = 0.1
-
-class LLMError(Exception):
- """Base exception for LLM-related errors."""
- pass
-
-class LLMRetryError(LLMError):
- """Exception raised when retries are exhausted."""
- pass
-
-def setup_api_keys() -> None:
- """Set up API keys from environment variables."""
- providers = ['OPENAI', 'ANTHROPIC', 'GROQ', 'OPENROUTER']
- for provider in providers:
- key = getattr(config, f'{provider}_API_KEY')
- if key:
- logger.debug(f"API key set for provider: {provider}")
- else:
- logger.warning(f"No API key found for provider: {provider}")
-
- # Set up OpenRouter API base if not already set
- if config.OPENROUTER_API_KEY and config.OPENROUTER_API_BASE:
- os.environ['OPENROUTER_API_BASE'] = config.OPENROUTER_API_BASE
- logger.debug(f"Set OPENROUTER_API_BASE to {config.OPENROUTER_API_BASE}")
-
- # Set up AWS Bedrock credentials
- aws_access_key = config.AWS_ACCESS_KEY_ID
- aws_secret_key = config.AWS_SECRET_ACCESS_KEY
- aws_region = config.AWS_REGION_NAME
-
- if aws_access_key and aws_secret_key and aws_region:
- logger.debug(f"AWS credentials set for Bedrock in region: {aws_region}")
- # Configure LiteLLM to use AWS credentials
- os.environ['AWS_ACCESS_KEY_ID'] = aws_access_key
- os.environ['AWS_SECRET_ACCESS_KEY'] = aws_secret_key
- os.environ['AWS_REGION_NAME'] = aws_region
- else:
- logger.warning(f"Missing AWS credentials for Bedrock integration - access_key: {bool(aws_access_key)}, secret_key: {bool(aws_secret_key)}, region: {aws_region}")
-
-async def handle_error(error: Exception, attempt: int, max_attempts: int) -> None:
- """Handle API errors with appropriate delays and logging."""
- delay = RATE_LIMIT_DELAY if isinstance(error, litellm.exceptions.RateLimitError) else RETRY_DELAY
- logger.warning(f"Error on attempt {attempt + 1}/{max_attempts}: {str(error)}")
- logger.debug(f"Waiting {delay} seconds before retry...")
- await asyncio.sleep(delay)
-
-def prepare_params(
- messages: List[Dict[str, Any]],
- model_name: str,
- temperature: float = 0,
- max_tokens: Optional[int] = None,
- response_format: Optional[Any] = None,
- tools: Optional[List[Dict[str, Any]]] = None,
- tool_choice: str = "auto",
- api_key: Optional[str] = None,
- api_base: Optional[str] = None,
- stream: bool = False,
- top_p: Optional[float] = None,
- model_id: Optional[str] = None,
- enable_thinking: Optional[bool] = False,
- reasoning_effort: Optional[str] = 'low'
-) -> Dict[str, Any]:
- """Prepare parameters for the API call."""
- params = {
- "model": model_name,
- "messages": messages,
- "temperature": temperature,
- "response_format": response_format,
- "top_p": top_p,
- "stream": stream,
- }
-
- if api_key:
- params["api_key"] = api_key
- if api_base:
- params["api_base"] = api_base
- if model_id:
- params["model_id"] = model_id
-
- # Handle token limits
- if max_tokens is not None:
- # For Claude 3.7 in Bedrock, do not set max_tokens or max_tokens_to_sample
- # as it causes errors with inference profiles
- if model_name.startswith("bedrock/") and "claude-3-7" in model_name:
- logger.debug(f"Skipping max_tokens for Claude 3.7 model: {model_name}")
- # Do not add any max_tokens parameter for Claude 3.7
- else:
- param_name = "max_completion_tokens" if 'o1' in model_name else "max_tokens"
- params[param_name] = max_tokens
-
- # Add tools if provided
- if tools:
- params.update({
- "tools": tools,
- "tool_choice": tool_choice
- })
- logger.debug(f"Added {len(tools)} tools to API parameters")
-
- # # Add Claude-specific headers
- if "claude" in model_name.lower() or "anthropic" in model_name.lower():
- params["extra_headers"] = {
- # "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"
- "anthropic-beta": "output-128k-2025-02-19"
- }
- params["fallbacks"] = [{
- "model": "openrouter/anthropic/claude-sonnet-4",
- "messages": messages,
- }]
- logger.debug("Added Claude-specific headers")
-
- # Add OpenRouter-specific parameters
- if model_name.startswith("openrouter/"):
- logger.debug(f"Preparing OpenRouter parameters for model: {model_name}")
-
- # Add optional site URL and app name from config
- site_url = config.OR_SITE_URL
- app_name = config.OR_APP_NAME
- if site_url or app_name:
- extra_headers = params.get("extra_headers", {})
- if site_url:
- extra_headers["HTTP-Referer"] = site_url
- if app_name:
- extra_headers["X-Title"] = app_name
- params["extra_headers"] = extra_headers
- logger.debug(f"Added OpenRouter site URL and app name to headers")
-
- # Add Bedrock-specific parameters
- if model_name.startswith("bedrock/"):
- logger.debug(f"Preparing AWS Bedrock parameters for model: {model_name}")
-
- if not model_id and "anthropic.claude-3-7-sonnet" in model_name:
- params["model_id"] = "arn:aws:bedrock:us-west-2:935064898258:inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0"
- logger.debug(f"Auto-set model_id for Claude 3.7 Sonnet: {params['model_id']}")
-
- # Apply Anthropic prompt caching (minimal implementation)
- # Check model name *after* potential modifications (like adding bedrock/ prefix)
- effective_model_name = params.get("model", model_name) # Use model from params if set, else original
- if "claude" in effective_model_name.lower() or "anthropic" in effective_model_name.lower():
- messages = params["messages"] # Direct reference, modification affects params
-
- # Ensure messages is a list
- if not isinstance(messages, list):
- return params # Return early if messages format is unexpected
-
- # 1. Process the first message if it's a system prompt with string content
- if messages and messages[0].get("role") == "system":
- content = messages[0].get("content")
- if isinstance(content, str):
- # Wrap the string content in the required list structure
- messages[0]["content"] = [
- {"type": "text", "text": content, "cache_control": {"type": "ephemeral"}}
- ]
- elif isinstance(content, list):
- # If content is already a list, check if the first text block needs cache_control
- for item in content:
- if isinstance(item, dict) and item.get("type") == "text":
- if "cache_control" not in item:
- item["cache_control"] = {"type": "ephemeral"}
- break # Apply to the first text block only for system prompt
-
- # 2. Find and process relevant user and assistant messages (limit to 4 max)
- last_user_idx = -1
- second_last_user_idx = -1
- last_assistant_idx = -1
-
- for i in range(len(messages) - 1, -1, -1):
- role = messages[i].get("role")
- if role == "user":
- if last_user_idx == -1:
- last_user_idx = i
- elif second_last_user_idx == -1:
- second_last_user_idx = i
- elif role == "assistant":
- if last_assistant_idx == -1:
- last_assistant_idx = i
-
- # Stop searching if we've found all needed messages (system, last user, second last user, last assistant)
- found_count = sum(idx != -1 for idx in [last_user_idx, second_last_user_idx, last_assistant_idx])
- if found_count >= 3:
- break
-
- # Helper function to apply cache control
- def apply_cache_control(message_idx: int, message_role: str):
- if message_idx == -1:
- return
-
- message = messages[message_idx]
- content = message.get("content")
-
- if isinstance(content, str):
- message["content"] = [
- {"type": "text", "text": content, "cache_control": {"type": "ephemeral"}}
- ]
- elif isinstance(content, list):
- for item in content:
- if isinstance(item, dict) and item.get("type") == "text":
- if "cache_control" not in item:
- item["cache_control"] = {"type": "ephemeral"}
-
- # Apply cache control to the identified messages (max 4: system, last user, second last user, last assistant)
- # System message is always at index 0 if present
- apply_cache_control(0, "system")
- apply_cache_control(last_user_idx, "last user")
- apply_cache_control(second_last_user_idx, "second last user")
- apply_cache_control(last_assistant_idx, "last assistant")
-
- # Add reasoning_effort for Anthropic models if enabled
- use_thinking = enable_thinking if enable_thinking is not None else False
- is_anthropic = "anthropic" in effective_model_name.lower() or "claude" in effective_model_name.lower()
-
- if is_anthropic and use_thinking:
- effort_level = reasoning_effort if reasoning_effort else 'low'
- params["reasoning_effort"] = effort_level
- params["temperature"] = 1.0 # Required by Anthropic when reasoning_effort is used
- logger.info(f"Anthropic thinking enabled with reasoning_effort='{effort_level}'")
-
- return params
-
-async def make_llm_api_call(
- messages: List[Dict[str, Any]],
- model_name: str,
- response_format: Optional[Any] = None,
- temperature: float = 0,
- max_tokens: Optional[int] = None,
- tools: Optional[List[Dict[str, Any]]] = None,
- tool_choice: str = "auto",
- api_key: Optional[str] = None,
- api_base: Optional[str] = None,
- stream: bool = False,
- top_p: Optional[float] = None,
- model_id: Optional[str] = None,
- enable_thinking: Optional[bool] = False,
- reasoning_effort: Optional[str] = 'low'
-) -> Union[Dict[str, Any], AsyncGenerator]:
- """
- Make an API call to a language model using LiteLLM.
-
- Args:
- messages: List of message dictionaries for the conversation
- model_name: Name of the model to use (e.g., "gpt-4", "claude-3", "openrouter/openai/gpt-4", "bedrock/anthropic.claude-3-sonnet-20240229-v1:0")
- response_format: Desired format for the response
- temperature: Sampling temperature (0-1)
- max_tokens: Maximum tokens in the response
- tools: List of tool definitions for function calling
- tool_choice: How to select tools ("auto" or "none")
- api_key: Override default API key
- api_base: Override default API base URL
- stream: Whether to stream the response
- top_p: Top-p sampling parameter
- model_id: Optional ARN for Bedrock inference profiles
- enable_thinking: Whether to enable thinking
- reasoning_effort: Level of reasoning effort
-
- Returns:
- Union[Dict[str, Any], AsyncGenerator]: API response or stream
-
- Raises:
- LLMRetryError: If API call fails after retries
- LLMError: For other API-related errors
- """
- # debug