Files
the-island/backend/app/llm.py
empty 8bb78a808d revert: remove auto-prefix, user specifies full provider/model
Support both OpenAI-compatible and Anthropic-compatible endpoints.
User must specify full model name with provider prefix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 14:07:17 +08:00

265 lines
9.1 KiB
Python

"""
LLM Service - Agent Brain Module.
Provides AI-powered responses for agents using LiteLLM (supports multiple providers).
Supported providers (via environment variables):
- OpenAI: OPENAI_API_KEY → model="gpt-3.5-turbo" or "gpt-4"
- Anthropic: ANTHROPIC_API_KEY → model="claude-3-haiku-20240307" or "claude-3-sonnet-20240229"
- Google: GEMINI_API_KEY → model="gemini/gemini-pro"
- Azure OpenAI: AZURE_API_KEY + AZURE_API_BASE → model="azure/<deployment-name>"
- OpenRouter: OPENROUTER_API_KEY → model="openrouter/<model>"
- Ollama (local): OLLAMA_API_BASE → model="ollama/llama2"
- Custom/Self-hosted: LLM_API_KEY + LLM_API_BASE → any OpenAI-compatible endpoint
- And 100+ more providers...
Configuration:
- LLM_MODEL: Model to use (default: gpt-3.5-turbo)
- LLM_API_BASE: Custom API base URL (for self-hosted or proxy services)
- LLM_API_KEY: Generic API key (used with LLM_API_BASE)
- LLM_MOCK_MODE: Set to "true" to force mock mode
"""
import logging
import os
import random
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import Agent
logger = logging.getLogger(__name__)
# Mock responses for development without API key
MOCK_REACTIONS = {
"feed": [
"Oh! Finally some food! Thank you stranger!",
"Mmm, that's delicious! I was starving!",
"You're too kind! My energy is back!",
"Food! Glorious food! I love you!",
],
"idle_sunny": [
"What a beautiful day on this island...",
"The sun feels nice, but I'm getting hungry.",
"I wonder if rescue will ever come...",
"At least the weather is good today.",
],
"idle_rainy": [
"This rain is so depressing...",
"I hope the storm passes soon.",
"Getting wet and cold out here...",
"Rain again? Just my luck.",
],
"idle_starving": [
"I'm so hungry... I can barely stand...",
"Someone please... I need food...",
"My stomach is eating itself...",
"Is this how it ends? Starving on a beach?",
],
}
# Default model configuration
DEFAULT_MODEL = "gpt-3.5-turbo"
class LLMService:
"""
Service for generating AI-powered agent reactions using LiteLLM.
Supports multiple LLM providers through a unified interface.
Falls back to mock responses if no API key is configured.
"""
def __init__(self) -> None:
"""Initialize the LLM service with LiteLLM or mock mode."""
self._model = os.environ.get("LLM_MODEL", DEFAULT_MODEL)
self._api_base = os.environ.get("LLM_API_BASE") # Custom base URL
self._api_key = os.environ.get("LLM_API_KEY") # Generic API key
self._mock_mode = os.environ.get("LLM_MOCK_MODE", "").lower() == "true"
self._acompletion = None
if self._mock_mode:
logger.info("LLMService running in MOCK mode (forced by LLM_MOCK_MODE)")
return
# Check for any supported API key (order matters for provider detection)
api_keys = {
"OPENAI_API_KEY": "OpenAI",
"ANTHROPIC_API_KEY": "Anthropic",
"GEMINI_API_KEY": "Google Gemini",
"AZURE_API_KEY": "Azure OpenAI",
"AZURE_API_BASE": "Azure OpenAI",
"OPENROUTER_API_KEY": "OpenRouter",
"COHERE_API_KEY": "Cohere",
"HUGGINGFACE_API_KEY": "HuggingFace",
"OLLAMA_API_BASE": "Ollama (local)",
"LLM_API_KEY": "Custom (with LLM_API_BASE)",
"LLM_API_BASE": "Custom endpoint",
}
found_provider = None
for key, provider in api_keys.items():
if os.environ.get(key):
found_provider = provider
break
if not found_provider:
logger.warning(
"No LLM API key found in environment. "
"LLMService running in MOCK mode - using predefined responses. "
f"Supported keys: {', '.join(api_keys.keys())}"
)
self._mock_mode = True
return
try:
from litellm import acompletion
self._acompletion = acompletion
# Log configuration details
config_info = f"provider: {found_provider}, model: {self._model}"
if self._api_base:
config_info += f", api_base: {self._api_base}"
logger.info(f"LLMService initialized with LiteLLM ({config_info})")
except ImportError:
logger.error("litellm package not installed. Running in MOCK mode.")
self._mock_mode = True
except Exception as e:
logger.error(f"Failed to initialize LiteLLM: {e}. Running in MOCK mode.")
self._mock_mode = True
@property
def is_mock_mode(self) -> bool:
"""Check if service is running in mock mode."""
return self._mock_mode
@property
def model(self) -> str:
"""Get the current model name."""
return self._model
@property
def api_base(self) -> str | None:
"""Get the custom API base URL if configured."""
return self._api_base
def _get_mock_response(self, event_type: str = "feed") -> str:
"""Get a random mock response for testing without API."""
responses = MOCK_REACTIONS.get(event_type, MOCK_REACTIONS["feed"])
return random.choice(responses)
async def generate_reaction(
self,
agent: "Agent",
event_description: str,
event_type: str = "feed"
) -> str:
"""
Generate an AI reaction for an agent based on an event.
Args:
agent: The Agent model instance
event_description: Description of what happened (e.g., "User X gave you food")
event_type: Type of event for mock mode categorization
Returns:
A first-person verbal response from the agent
"""
if self._mock_mode:
return self._get_mock_response(event_type)
try:
system_prompt = (
f"You are {agent.name}. "
f"Personality: {agent.personality}. "
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
f"You live on a survival island. "
f"React to the following event briefly (under 20 words). "
f"Respond in first person, as if speaking out loud."
)
# Build kwargs for acompletion
kwargs = {
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": event_description}
],
"max_tokens": 50,
"temperature": 0.8,
}
if self._api_base:
kwargs["api_base"] = self._api_base
if self._api_key:
kwargs["api_key"] = self._api_key
response = await self._acompletion(**kwargs)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"LLM API error: {e}")
return self._get_mock_response(event_type)
async def generate_idle_chat(
self,
agent: "Agent",
weather: str = "Sunny"
) -> str:
"""
Generate idle chatter for an agent based on current conditions.
Args:
agent: The Agent model instance
weather: Current weather condition
Returns:
A spontaneous thought or comment from the agent
"""
# Determine event type for mock responses
if agent.energy <= 20:
event_type = "idle_starving"
elif weather.lower() in ["rainy", "stormy"]:
event_type = "idle_rainy"
else:
event_type = "idle_sunny"
if self._mock_mode:
return self._get_mock_response(event_type)
try:
system_prompt = (
f"You are {agent.name}. "
f"Personality: {agent.personality}. "
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
f"You are stranded on a survival island. "
f"The weather is {weather}. "
f"Say something brief (under 15 words) about your situation or thoughts. "
f"Speak naturally, as if talking to yourself or nearby survivors."
)
# Build kwargs for acompletion
kwargs = {
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "What are you thinking right now?"}
],
"max_tokens": 40,
"temperature": 0.9,
}
if self._api_base:
kwargs["api_base"] = self._api_base
if self._api_key:
kwargs["api_key"] = self._api_key
response = await self._acompletion(**kwargs)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"LLM API error for idle chat: {e}")
return self._get_mock_response(event_type)
# Global instance for easy import
llm_service = LLMService()