feat(llm): add DeepSeek provider and HuggingFace embedding fallback (Issue #41)

This commit is contained in:
Andrew Kaszubski 2025-12-26 11:07:48 +11:00
parent 5ea9e905c5
commit edae1ab2cc
4 changed files with 116 additions and 14 deletions

View File

@ -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

View File

@ -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)"""

View File

@ -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",

View File

@ -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"])