diff --git a/PROJECT.md b/PROJECT.md index b4712fad..bb3cce29 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -443,6 +443,30 @@ config = { - Model names use the format: `provider/model-name` (e.g., `anthropic/claude-sonnet-4.5`, `openai/gpt-4o`) - See [OpenRouter models list](https://openrouter.ai/docs/models) for available models +### DeepSeek Configuration Example +DeepSeek provides cost-effective reasoning models with strong performance on quantitative analysis tasks. To use DeepSeek: + +```python +config = { + "llm_provider": "deepseek", + "deep_think_llm": "deepseek-reasoner", # Extended reasoning model + "quick_think_llm": "deepseek-chat", # Fast responses for simple queries + "backend_url": "https://api.deepseek.com/v1", +} +``` + +**Requirements:** +- DEEPSEEK_API_KEY environment variable must be set +- Get your API key from [DeepSeek Platform](https://platform.deepseek.com/) +- For embeddings, either set OPENAI_API_KEY (preferred) or install sentence-transformers package +- Model options: `deepseek-chat` (fast) and `deepseek-reasoner` (extended thinking) +- DeepSeek uses OpenAI API format (ChatOpenAI compatible) + +**Embedding Fallback Chain:** +- Tries OPENAI_API_KEY for OpenAI embeddings (recommended for best quality) +- Falls back to HuggingFace sentence-transformers (all-MiniLM-L6-v2) if available and no OpenAI key +- Disables memory features with warning if no embedding backend available + --- ## DEVELOPMENT NOTES diff --git a/tradingagents/agents/utils/memory.py b/tradingagents/agents/utils/memory.py index 7c8e74a9..41758d33 100644 --- a/tradingagents/agents/utils/memory.py +++ b/tradingagents/agents/utils/memory.py @@ -3,40 +3,77 @@ from chromadb.config import Settings from openai import OpenAI import os +# Try to import HuggingFace sentence-transformers (optional dependency) +# This needs to be at module level for test mocking to work +try: + from sentence_transformers import SentenceTransformer +except ImportError: + SentenceTransformer = None + class FinancialSituationMemory: def __init__(self, name, config): - # Handle embeddings based on provider + self.embedding_backend = None # Track which backend is used + + # Handle embeddings based on provider with fallback chain if config["backend_url"] == "http://localhost:11434/v1": # Ollama local embeddings self.embedding = "nomic-embed-text" self.client = OpenAI(base_url=config["backend_url"]) - elif config.get("llm_provider", "").lower() == "openrouter": - # OpenRouter doesn't have native embeddings, use OpenAI embeddings as fallback + self.embedding_backend = "ollama" + elif config.get("llm_provider", "").lower() in ("openrouter", "deepseek"): + # OpenRouter and DeepSeek don't have native embeddings + # Fallback chain: OpenAI -> HuggingFace -> disable memory openai_key = os.getenv("OPENAI_API_KEY") - if not openai_key: - print("Warning: OPENAI_API_KEY not found. Memory features disabled for OpenRouter.") - self.client = None - else: + if openai_key: + # Use OpenAI embeddings as first fallback self.embedding = "text-embedding-3-small" - self.client = OpenAI(api_key=openai_key) # Use OpenAI directly for embeddings + self.client = OpenAI(api_key=openai_key) + self.embedding_backend = "openai" + elif SentenceTransformer is not None: + # Use HuggingFace sentence-transformers as second fallback + try: + self.client = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") + self.embedding = "all-MiniLM-L6-v2" + self.embedding_backend = "huggingface" + print(f"Info: Using HuggingFace embeddings (all-MiniLM-L6-v2) for memory with {config.get('llm_provider', 'unknown')} provider.") + except Exception as e: + print(f"Warning: Failed to initialize HuggingFace embeddings: {e}. Memory features disabled.") + self.client = None + self.embedding_backend = None + else: + # No embedding backend available - disable memory + print(f"Warning: No embedding backend available for {config.get('llm_provider', 'unknown')} provider. " + "Install sentence-transformers or set OPENAI_API_KEY to enable memory features.") + self.client = None + self.embedding_backend = None else: # Default to text-embedding-3-small for OpenAI and others self.embedding = "text-embedding-3-small" self.client = OpenAI(base_url=config["backend_url"]) + self.embedding_backend = "openai" self.chroma_client = chromadb.Client(Settings(allow_reset=True)) self.situation_collection = self.chroma_client.get_or_create_collection(name=name) def get_embedding(self, text): - """Get OpenAI embedding for a text""" + """Get embedding for a text using the configured backend.""" if self.client is None: raise RuntimeError("Embedding client not initialized. Check API key configuration.") - response = self.client.embeddings.create( - model=self.embedding, input=text - ) - return response.data[0].embedding + if self.embedding_backend == "huggingface": + # HuggingFace SentenceTransformer - returns numpy array or list + embedding = self.client.encode(text) + # Convert to list if needed + if hasattr(embedding, 'tolist'): + return embedding.tolist() + return list(embedding) + else: + # OpenAI or Ollama - use OpenAI API format + response = self.client.embeddings.create( + model=self.embedding, input=text + ) + return response.data[0].embedding def add_situations(self, situations_and_advice): """Add financial situations and their corresponding advice. Parameter is a list of tuples (situation, rec)""" diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index d1ca3d16..b6eb072b 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -9,7 +9,7 @@ DEFAULT_CONFIG = { "dataflows/data_cache", ), # LLM settings - "llm_provider": "openai", + "llm_provider": "openai", # Options: openai, ollama, openrouter, deepseek, anthropic, google "deep_think_llm": "o4-mini", "quick_think_llm": "gpt-4o-mini", "backend_url": "https://api.openai.com/v1", diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 5729d1c8..8b334c7b 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -102,6 +102,47 @@ class TradingAgentsGraph: api_key=openrouter_key, default_headers=default_headers ) + elif self.config["llm_provider"].lower() == "deepseek": + # DeepSeek requires explicit API key handling + deepseek_key = os.getenv("DEEPSEEK_API_KEY") + if not deepseek_key: + raise ValueError( + "DEEPSEEK_API_KEY environment variable is required for DeepSeek provider. " + "Get your API key from https://platform.deepseek.com/" + ) + + # DeepSeek requires specific headers for attribution + default_headers = { + "HTTP-Referer": "https://github.com/TauricResearch/TradingAgents", + "X-Title": "TradingAgents" + } + + # Use backend_url from config, with fallback to default DeepSeek API only if not set at all + base_url = self.config.get("backend_url") + if base_url is None and "backend_url" not in self.config: + base_url = "https://api.deepseek.com/v1" + elif base_url == "": + # Keep empty string if explicitly set + pass + elif base_url is None: + # Keep None if explicitly set + pass + else: + # Use the provided value + pass + + self.deep_thinking_llm = ChatOpenAI( + model=self.config["deep_think_llm"], + base_url=base_url, + api_key=deepseek_key, + default_headers=default_headers + ) + self.quick_thinking_llm = ChatOpenAI( + model=self.config["quick_think_llm"], + base_url=base_url, + api_key=deepseek_key, + default_headers=default_headers + ) elif self.config["llm_provider"].lower() == "anthropic": self.deep_thinking_llm = ChatAnthropic(model=self.config["deep_think_llm"], base_url=self.config["backend_url"]) self.quick_thinking_llm = ChatAnthropic(model=self.config["quick_think_llm"], base_url=self.config["backend_url"])