feat(llm): add DeepSeek provider and HuggingFace embedding fallback (Issue #41)
This commit is contained in:
parent
5ea9e905c5
commit
edae1ab2cc
24
PROJECT.md
24
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`)
|
- 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
|
- 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
|
## DEVELOPMENT NOTES
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,77 @@ from chromadb.config import Settings
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
import os
|
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:
|
class FinancialSituationMemory:
|
||||||
def __init__(self, name, config):
|
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":
|
if config["backend_url"] == "http://localhost:11434/v1":
|
||||||
# Ollama local embeddings
|
# Ollama local embeddings
|
||||||
self.embedding = "nomic-embed-text"
|
self.embedding = "nomic-embed-text"
|
||||||
self.client = OpenAI(base_url=config["backend_url"])
|
self.client = OpenAI(base_url=config["backend_url"])
|
||||||
elif config.get("llm_provider", "").lower() == "openrouter":
|
self.embedding_backend = "ollama"
|
||||||
# OpenRouter doesn't have native embeddings, use OpenAI embeddings as fallback
|
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")
|
openai_key = os.getenv("OPENAI_API_KEY")
|
||||||
if not openai_key:
|
if openai_key:
|
||||||
print("Warning: OPENAI_API_KEY not found. Memory features disabled for OpenRouter.")
|
# Use OpenAI embeddings as first fallback
|
||||||
self.client = None
|
|
||||||
else:
|
|
||||||
self.embedding = "text-embedding-3-small"
|
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:
|
else:
|
||||||
# Default to text-embedding-3-small for OpenAI and others
|
# Default to text-embedding-3-small for OpenAI and others
|
||||||
self.embedding = "text-embedding-3-small"
|
self.embedding = "text-embedding-3-small"
|
||||||
self.client = OpenAI(base_url=config["backend_url"])
|
self.client = OpenAI(base_url=config["backend_url"])
|
||||||
|
self.embedding_backend = "openai"
|
||||||
|
|
||||||
self.chroma_client = chromadb.Client(Settings(allow_reset=True))
|
self.chroma_client = chromadb.Client(Settings(allow_reset=True))
|
||||||
self.situation_collection = self.chroma_client.get_or_create_collection(name=name)
|
self.situation_collection = self.chroma_client.get_or_create_collection(name=name)
|
||||||
|
|
||||||
def get_embedding(self, text):
|
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:
|
if self.client is None:
|
||||||
raise RuntimeError("Embedding client not initialized. Check API key configuration.")
|
raise RuntimeError("Embedding client not initialized. Check API key configuration.")
|
||||||
|
|
||||||
response = self.client.embeddings.create(
|
if self.embedding_backend == "huggingface":
|
||||||
model=self.embedding, input=text
|
# HuggingFace SentenceTransformer - returns numpy array or list
|
||||||
)
|
embedding = self.client.encode(text)
|
||||||
return response.data[0].embedding
|
# 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):
|
def add_situations(self, situations_and_advice):
|
||||||
"""Add financial situations and their corresponding advice. Parameter is a list of tuples (situation, rec)"""
|
"""Add financial situations and their corresponding advice. Parameter is a list of tuples (situation, rec)"""
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ DEFAULT_CONFIG = {
|
||||||
"dataflows/data_cache",
|
"dataflows/data_cache",
|
||||||
),
|
),
|
||||||
# LLM settings
|
# LLM settings
|
||||||
"llm_provider": "openai",
|
"llm_provider": "openai", # Options: openai, ollama, openrouter, deepseek, anthropic, google
|
||||||
"deep_think_llm": "o4-mini",
|
"deep_think_llm": "o4-mini",
|
||||||
"quick_think_llm": "gpt-4o-mini",
|
"quick_think_llm": "gpt-4o-mini",
|
||||||
"backend_url": "https://api.openai.com/v1",
|
"backend_url": "https://api.openai.com/v1",
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,47 @@ class TradingAgentsGraph:
|
||||||
api_key=openrouter_key,
|
api_key=openrouter_key,
|
||||||
default_headers=default_headers
|
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":
|
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.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"])
|
self.quick_thinking_llm = ChatAnthropic(model=self.config["quick_think_llm"], base_url=self.config["backend_url"])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue