diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index fe970cef..348be7a2 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -104,6 +104,7 @@ async def run_analysis( deep_think_api_key=request.deep_think_api_key or "", embedding_base_url=request.embedding_base_url, embedding_api_key=request.embedding_api_key or "", + embedding_model=request.embedding_model or "all-MiniLM-L6-v2", alpha_vantage_api_key=request.alpha_vantage_api_key or "", finmind_api_key=request.finmind_api_key or "", )) @@ -166,7 +167,7 @@ async def cleanup_task(task_id: str): after the user has saved the results locally or to cloud storage. This helps keep Redis storage clean and reduces memory usage. - Note: Tasks are also automatically cleaned up 10 minutes after + Note: Tasks are also automatically cleaned up 1 hour after completion/failure, so calling this endpoint is optional but recommended. Args: diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py index dca2b490..050f0115 100644 --- a/backend/app/models/schemas.py +++ b/backend/app/models/schemas.py @@ -46,9 +46,13 @@ class AnalysisRequest(BaseModel): deep_think_api_key: Optional[str] = Field(None, description="API Key for Deep Thinking Model", min_length=0) embedding_base_url: Optional[str] = Field( default="https://api.openai.com/v1", - description="Base URL for Embedding Model" + description="Base URL for Embedding Model (only used for OpenAI embeddings)" + ) + embedding_api_key: Optional[str] = Field(None, description="API Key for Embedding Model (only used for OpenAI embeddings)", min_length=0) + embedding_model: Optional[str] = Field( + default="all-MiniLM-L6-v2", + description="Embedding model: 'all-MiniLM-L6-v2' (local, no API key), 'text-embedding-3-small' (OpenAI), etc." ) - embedding_api_key: Optional[str] = Field(None, description="API Key for Embedding Model", min_length=0) alpha_vantage_api_key: Optional[str] = Field( None, description="Alpha Vantage API Key (optional, for US stock fundamental data)", diff --git a/backend/app/services/task_manager.py b/backend/app/services/task_manager.py index a3762a4a..1eca9f69 100644 --- a/backend/app/services/task_manager.py +++ b/backend/app/services/task_manager.py @@ -40,7 +40,7 @@ class HybridTaskManager: self._lock = threading.RLock() self._cleanup_interval = 3600 # 1 hour self._task_expiry = 86400 # 24 hours for pending/running tasks - self._completed_task_expiry = 600 # 10 minutes for completed/failed tasks (auto cleanup) + self._completed_task_expiry = 3600 # 1 hour for completed/failed tasks (auto cleanup) # Check Redis availability on startup if is_redis_available(): diff --git a/backend/app/services/trading_service.py b/backend/app/services/trading_service.py index ffea02ea..98bc0c69 100644 --- a/backend/app/services/trading_service.py +++ b/backend/app/services/trading_service.py @@ -50,6 +50,7 @@ class TradingService: deep_think_api_key: Optional[str] = None, embedding_base_url: str = "https://api.openai.com/v1", embedding_api_key: Optional[str] = None, + embedding_model: str = "all-MiniLM-L6-v2", # Default to local model alpha_vantage_api_key: Optional[str] = None, finmind_api_key: Optional[str] = None, # 台灣股市資料 API market_type: str = "us", # 市場類型:us (美股) 或 tw (台股) @@ -132,8 +133,23 @@ class TradingService: # Note: For non-OpenAI providers, the user MUST provide the specific key if it differs from the shared one. config["quick_think_api_key"] = quick_think_api_key if quick_think_api_key else openai_api_key config["deep_think_api_key"] = deep_think_api_key if deep_think_api_key else openai_api_key - config["embedding_base_url"] = normalize_base_url(embedding_base_url) - config["embedding_api_key"] = embedding_api_key if embedding_api_key else openai_api_key + + # Embedding configuration: determine provider based on model name + local_embedding_models = ["all-MiniLM-L6-v2", "all-mpnet-base-v2"] + is_local_embedding = embedding_model in local_embedding_models + + if is_local_embedding: + # Local embedding: use sentence-transformers (no API key needed) + config["embedding_provider"] = "local" + config["embedding_model"] = embedding_model + logger.info(f"Using local embedding model: {embedding_model}") + else: + # OpenAI embedding: requires API key + config["embedding_provider"] = "openai" + config["embedding_model"] = embedding_model + config["embedding_base_url"] = normalize_base_url(embedding_base_url) + config["embedding_api_key"] = embedding_api_key if embedding_api_key else openai_api_key + logger.info(f"Using OpenAI embedding model: {embedding_model}") # 根據 market_type 設定資料供應商 if market_type in ["twse", "tpex"]: diff --git a/backend/requirements.txt b/backend/requirements.txt index 5869f601..18e014ed 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -38,6 +38,7 @@ stockstats eodhd langgraph chromadb +sentence-transformers setuptools backtrader akshare diff --git a/frontend/components/analysis/AnalysisForm.tsx b/frontend/components/analysis/AnalysisForm.tsx index 6c3bda85..f8da8079 100644 --- a/frontend/components/analysis/AnalysisForm.tsx +++ b/frontend/components/analysis/AnalysisForm.tsx @@ -52,6 +52,7 @@ const formSchema = z.object({ research_depth: z.number().int().min(1).max(5), quick_think_llm: z.string().min(1, "請選擇快速思維模型"), deep_think_llm: z.string().min(1, "請選擇深層思維模型"), + embedding_model: z.string().min(1, "請選擇嵌入式模型"), // Market type selection: us=美股, twse=上市, tpex=上櫃/興櫃 market_type: z.enum(["us", "twse", "tpex"]), @@ -71,14 +72,14 @@ const formSchema = z.object({ .url("請輸入有效的 URL") .optional() .or(z.literal("")), - quick_think_api_key: z.string().min(1, "請輸入快速思維模型 API Key"), - deep_think_api_key: z.string().min(1, "請輸入深層思維模型 API Key"), + quick_think_api_key: z.string().optional().or(z.literal("")), + deep_think_api_key: z.string().optional().or(z.literal("")), embedding_base_url: z .string() .url("請輸入有效的 URL") .optional() .or(z.literal("")), - embedding_api_key: z.string().min(1, "請輸入嵌入模型 API Key"), + embedding_api_key: z.string().optional().or(z.literal("")), // 本地模型不需要 API Key alpha_vantage_api_key: z.string().optional().or(z.literal("")), // 選填 finmind_api_key: z.string().optional().or(z.literal("")), // 選填 }); @@ -106,6 +107,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { market_type: "us", // 預設美股 quick_think_llm: "gpt-5-mini", deep_think_llm: "gpt-5-mini", + embedding_model: "all-MiniLM-L6-v2", // 預設使用本地開源模型 custom_quick_think_model: "", custom_deep_think_model: "", quick_think_base_url: "https://api.openai.com/v1", @@ -122,9 +124,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { // Load API settings from localStorage and update when models change const quickThinkLlm = form.watch("quick_think_llm"); const deepThinkLlm = form.watch("deep_think_llm"); + const embeddingModel = form.watch("embedding_model"); const marketType = form.watch("market_type"); const isQuickThinkCustom = quickThinkLlm === "custom"; const isDeepThinkCustom = deepThinkLlm === "custom"; + const isLocalEmbedding = ["all-MiniLM-L6-v2", "all-mpnet-base-v2"].includes(embeddingModel); useEffect(() => { // Use async version to get decrypted API keys @@ -169,14 +173,20 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { ); } - form.setValue( - "embedding_base_url", - savedSettings.custom_base_url || "https://api.openai.com/v1" - ); - form.setValue( - "embedding_api_key", - savedSettings.custom_api_key || savedSettings.openai_api_key - ); + // 本地模型不需要設定 API Key 和 Base URL + if (!isLocalEmbedding) { + form.setValue( + "embedding_base_url", + savedSettings.custom_base_url || "https://api.openai.com/v1" + ); + form.setValue( + "embedding_api_key", + savedSettings.custom_api_key || savedSettings.openai_api_key + ); + } else { + form.setValue("embedding_base_url", ""); + form.setValue("embedding_api_key", ""); + } form.setValue( "alpha_vantage_api_key", savedSettings.alpha_vantage_api_key || "" @@ -186,7 +196,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { loadSettings(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [quickThinkLlm, deepThinkLlm, isQuickThinkCustom, isDeepThinkCustom]); + }, [quickThinkLlm, deepThinkLlm, embeddingModel, isQuickThinkCustom, isDeepThinkCustom, isLocalEmbedding]); // 當市場類型改變時,更新預設股票代碼和提示 useEffect(() => { @@ -253,6 +263,17 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { return; } + // Validate API keys are set (they come from localStorage/settings) + if (!values.quick_think_api_key) { + alert("請先在右上角「設定」中設定您的 API Key。\n\n快速思維模型需要對應的 API Key 才能運作。"); + return; + } + + if (!values.deep_think_api_key) { + alert("請先在右上角「設定」中設定您的 API Key。\n\n深層思維模型需要對應的 API Key 才能運作。"); + return; + } + const request: AnalysisRequest = { ...values, quick_think_llm: finalQuickThinkLlm, @@ -444,8 +465,8 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { /> - {/* 第二行:研究深度、快速思維模型、深層思維模型(3列) */} -