diff --git a/.gitignore b/.gitignore index c5c10538d..8cb98906b 100644 --- a/.gitignore +++ b/.gitignore @@ -212,4 +212,9 @@ __marimo__/ *.db-wal *.db-shm *.sqlite -*.sqlite3 \ No newline at end of file +*.sqlite3 + +# Downloads +*csv +*xlsx +*xls \ No newline at end of file diff --git a/python/configs/agent_cards/trading_agents.json b/python/configs/agent_cards/trading_agents.json index dd4b21c64..2e115af9b 100644 --- a/python/configs/agent_cards/trading_agents.json +++ b/python/configs/agent_cards/trading_agents.json @@ -6,12 +6,19 @@ "streaming": true, "push_notifications": false }, - "metadata": { - "version": "1.0.0", - "author": "ValueCell Team", - "tags": ["trading", "analysis", "multi-agent", "stocks", "finance"], - "supported_tickers": ["AAPL", "GOOGL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "NFLX", "SPY"], - "supported_analysts": ["market", "social", "news", "fundamentals"], - "supported_llm_providers": ["openai", "anthropic", "google", "ollama", "openrouter"] + "skills": [{ + "id": "trading_agents", + "name": "Trading Agents", + "description": "Trading Agents - Multi-agent trading analysis system with market, sentiment, news and fundamentals analysis", + "examples": [ + "Analyze APPL using all analysts", + "Analyze NVDA using market and fundamentals analysts", + "Analyze TSLA using all analysts, date 2024-01-15, enable debug mode" + ], + "tags": ["analysis", "multi-agent", "stocks", "US market"] + }], + "provider": { + "organization": "Tauric", + "url": "https://arxiv.org/abs/2412.20138" } } \ No newline at end of file diff --git a/python/locales/en-GB.json b/python/configs/locales/en-GB.json similarity index 100% rename from python/locales/en-GB.json rename to python/configs/locales/en-GB.json diff --git a/python/locales/en-US.json b/python/configs/locales/en-US.json similarity index 100% rename from python/locales/en-US.json rename to python/configs/locales/en-US.json diff --git a/python/locales/zh-Hans.json b/python/configs/locales/zh-Hans.json similarity index 100% rename from python/locales/zh-Hans.json rename to python/configs/locales/zh-Hans.json diff --git a/python/locales/zh-Hant.json b/python/configs/locales/zh-Hant.json similarity index 100% rename from python/locales/zh-Hant.json rename to python/configs/locales/zh-Hant.json diff --git a/python/third_party/TradingAgents/.env.example b/python/third_party/TradingAgents/.env.example index b4a30d8a3..e921f45b1 100644 --- a/python/third_party/TradingAgents/.env.example +++ b/python/third_party/TradingAgents/.env.example @@ -9,16 +9,22 @@ # Get your key from: https://platform.openai.com/api-keys OPENAI_API_KEY=your_openai_api_key_here -# OpenAI Embeddings API Key - IMPORTANT for Memory Functionality +# Finnhub API Key - Required for financial news and insider trading data +# Get your free key from: https://finnhub.io/register +FINNHUB_API_KEY=your_finnhub_api_key_here + + +# Embeddings API Key - IMPORTANT for Memory Functionality # This is needed when using non-OpenAI providers (OpenRouter, Anthropic, Google) # because they don't support OpenAI's embeddings API # If not set, the system will try to use OPENAI_API_KEY for embeddings # Get your key from: https://platform.openai.com/api-keys -OPENAI_EMBEDDINGS_API_KEY=your_openai_embeddings_key_here +EMBEDDINGS_API_KEY=your_openai_embeddings_key_here +# Default embedding URL is OpenAI +EMBEDDINGS_BACKEND_URL=your_embedding_model_backend_url_here +# Default embedding model +EMBEDDINGS_MODEL=text-embedding-3-small -# Finnhub API Key - Required for financial news and insider trading data -# Get your free key from: https://finnhub.io/register -FINNHUB_API_KEY=your_finnhub_api_key_here # ============================================================================= # OPTIONAL API KEYS (Only needed if using specific providers) @@ -112,7 +118,7 @@ TRADINGAGENTS_ONLINE_TOOLS=true # OpenRouter does not support OpenAI's embeddings API, which is required for # the memory functionality in TradingAgents. You MUST set both: # OPENAI_API_KEY=sk-or-v1-your-openrouter-key-here (for chat models) -# OPENAI_EMBEDDINGS_API_KEY=sk-your-real-openai-key-here (for embeddings) +# EMBEDDINGS_API_KEY=sk-your-real-openai-key-here (for embeddings) # For Azure OpenAI # TRADINGAGENTS_LLM_PROVIDER=azure @@ -130,7 +136,7 @@ TRADINGAGENTS_ONLINE_TOOLS=true # Solution 1 - Use dedicated OpenAI key for embeddings (RECOMMENDED): # OPENAI_API_KEY=sk-or-v1-your-openrouter-key-here -# OPENAI_EMBEDDINGS_API_KEY=sk-your-real-openai-key-here +# EMBEDDINGS_API_KEY=sk-your-real-openai-key-here # Solution 2 - Switch to Ollama for fully local processing: # TRADINGAGENTS_LLM_PROVIDER=ollama @@ -138,7 +144,7 @@ TRADINGAGENTS_ONLINE_TOOLS=true # Then run: ollama pull llama3.1 && ollama pull nomic-embed-text # Solution 3 - Accept limited memory functionality: -# If you don't set OPENAI_EMBEDDINGS_API_KEY, the system will use dummy +# If you don't set EMBEDDINGS_API_KEY, the system will use dummy # embeddings and continue running, but memory features will be limited. # Affected providers that need separate embeddings key: @@ -156,6 +162,6 @@ TRADINGAGENTS_ONLINE_TOOLS=true # 1. Copy this file to .env: cp .env.example .env # 2. Fill in your actual API keys and configuration values # 3. Make sure .env is in your .gitignore to keep your keys private -# 4. For OpenRouter users: Set both OPENAI_API_KEY and OPENAI_EMBEDDINGS_API_KEY +# 4. For OpenRouter users: Set both OPENAI_API_KEY and EMBEDDINGS_API_KEY # 5. For Ollama users: Run 'ollama pull nomic-embed-text' for embeddings support # 6. Run the application: python main.py or python -m cli.main diff --git a/python/third_party/TradingAgents/.gitignore b/python/third_party/TradingAgents/.gitignore index 9316947db..5ac4ebdfd 100644 --- a/python/third_party/TradingAgents/.gitignore +++ b/python/third_party/TradingAgents/.gitignore @@ -1,5 +1,6 @@ # Environment variables .env +.env.backup .env.local .env.*.local diff --git a/python/third_party/TradingAgents/adapter/__main__.py b/python/third_party/TradingAgents/adapter/__main__.py index 776d8e0c6..dc564d787 100644 --- a/python/third_party/TradingAgents/adapter/__main__.py +++ b/python/third_party/TradingAgents/adapter/__main__.py @@ -3,7 +3,6 @@ from datetime import datetime, date from typing import List, Dict, Any, Optional import re -import json from langchain_core.messages import HumanMessage from langchain_openai import ChatOpenAI diff --git a/python/third_party/TradingAgents/tradingagents/agents/utils/memory.py b/python/third_party/TradingAgents/tradingagents/agents/utils/memory.py index c97c5a478..c5ab19be4 100644 --- a/python/third_party/TradingAgents/tradingagents/agents/utils/memory.py +++ b/python/third_party/TradingAgents/tradingagents/agents/utils/memory.py @@ -6,27 +6,12 @@ class FinancialSituationMemory: def __init__(self, name, config): - if config["backend_url"] == "http://localhost:11434/v1": - self.embedding = "nomic-embed-text" - self.client = OpenAI(base_url=config["backend_url"]) - else: - self.embedding = "text-embedding-3-small" - # For embeddings, handle different providers appropriately - # Many providers like OpenRouter, Anthropic, Google don't support embeddings API - if "openrouter.ai" in config["backend_url"] or "anthropic.com" in config["backend_url"] or "generativelanguage.googleapis.com" in config["backend_url"]: - # Use a dedicated OpenAI API key for embeddings, or fall back to the main key - embeddings_api_key = os.getenv("OPENAI_EMBEDDINGS_API_KEY") or os.getenv("OPENAI_API_KEY") - - # Check if the API key is from OpenRouter (starts with sk-or-v1-) - if embeddings_api_key and embeddings_api_key.startswith("sk-or-v1-"): - print("โš ๏ธ Warning: OpenRouter API key detected for embeddings.") - print("๐Ÿ’ก OpenRouter doesn't support embeddings API. Please set OPENAI_EMBEDDINGS_API_KEY") - print(" with a real OpenAI API key, or the memory functionality will be disabled.") - # Try to use it anyway, but it will likely fail and trigger fallback - - self.client = OpenAI(api_key=embeddings_api_key) - else: - self.client = OpenAI(base_url=config["backend_url"]) + self.embedding = config["embeddings_model"] + self.client = OpenAI(base_url=config["embeddings_backend_url"]) + if "localhost" not in config["embeddings_backend_url"]: + embeddings_api_key = os.getenv("EMBEDDINGS_API_KEY") or os.getenv("OPENAI_API_KEY") + self.client = OpenAI(api_key=embeddings_api_key, base_url=config["embeddings_backend_url"]) + self.chroma_client = chromadb.Client(Settings(allow_reset=True)) try: self.situation_collection = self.chroma_client.create_collection(name=name) @@ -49,20 +34,8 @@ def get_embedding(self, text): # try alternative approaches print(f"โš ๏ธ Embedding request failed with current provider: {str(e)}") - # Try with a dedicated embeddings API key - embeddings_key = os.getenv("OPENAI_EMBEDDINGS_API_KEY") - if embeddings_key and not embeddings_key.startswith("sk-or-v1-"): - print("๐Ÿ”„ Trying with dedicated OPENAI_EMBEDDINGS_API_KEY...") - try: - fallback_client = OpenAI(api_key=embeddings_key) - response = fallback_client.embeddings.create( - model="text-embedding-3-small", input=text - ) - return response.data[0].embedding - except Exception as fallback_error: - print(f"โŒ Dedicated embeddings key also failed: {str(fallback_error)}") - # Return a dummy embedding vector of the expected dimension (1536 for text-embedding-3-small) + print("Using dummy embeddings as fallback...") import random return [random.random() for _ in range(1536)] diff --git a/python/third_party/TradingAgents/tradingagents/default_config.py b/python/third_party/TradingAgents/tradingagents/default_config.py index 4097b0332..287a744a2 100644 --- a/python/third_party/TradingAgents/tradingagents/default_config.py +++ b/python/third_party/TradingAgents/tradingagents/default_config.py @@ -37,6 +37,9 @@ def str_to_bool(value): "deep_think_llm": os.getenv("TRADINGAGENTS_DEEP_THINK_LLM", "o4-mini"), "quick_think_llm": os.getenv("TRADINGAGENTS_QUICK_THINK_LLM", "gpt-4o-mini"), "backend_url": os.getenv("TRADINGAGENTS_BACKEND_URL", "https://api.openai.com/v1"), + # Embeddings settings + "embeddings_backend_url": os.getenv("EMBEDDINGS_BACKEND_URL", "https://api.openai.com/v1"), + "embeddings_model": os.getenv("EMBEDDINGS_MODEL", "text-embedding-3-small"), # Debate and discussion settings "max_debate_rounds": int(os.getenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "1")), "max_risk_discuss_rounds": int(os.getenv("TRADINGAGENTS_MAX_RISK_DISCUSS_ROUNDS", "1")), diff --git a/python/third_party/TradingAgents/uv.lock b/python/third_party/TradingAgents/uv.lock index f63113785..bc2aebd29 100644 --- a/python/third_party/TradingAgents/uv.lock +++ b/python/third_party/TradingAgents/uv.lock @@ -818,6 +818,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, ] +[[package]] +name = "edgartools" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "httpx" }, + { name = "httpxthrottlecache" }, + { name = "humanize" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "nest-asyncio" }, + { name = "orjson" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "rank-bm25" }, + { name = "rapidfuzz" }, + { name = "rich" }, + { name = "stamina" }, + { name = "tabulate" }, + { name = "textdistance" }, + { name = "tqdm" }, + { name = "unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3b/1a84f814629abb265edcc4a1be6faa6c88194f7a6047fb22b6c0390f370f/edgartools-4.13.0.tar.gz", hash = "sha256:49be653a5fa305da2dbf05c0b888c48ae9ce7ac868ce0fdbf4b7fa7cbb7e0e8a", size = 1503059, upload-time = "2025-09-18T13:42:56.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/03/b4e7f37c56d648c7142f72effc98b6cda6c7005e8ed5fc6347d6eeae863c/edgartools-4.13.0-py3-none-any.whl", hash = "sha256:aed9a97e5e7d4d5f9951a2a68923967c452e96cb64bec8ec04d68a0ce75e4f2d", size = 1611558, upload-time = "2025-09-18T13:42:54.621Z" }, +] + [[package]] name = "eodhd" version = "1.0.32" @@ -1217,6 +1247,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691, upload-time = "2025-09-12T20:10:28.433Z" }, ] +[[package]] +name = "hishel" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/b5/c063cd3eab8154ddd61deb07b50497cf24010727eaeec4d78ed1a6262986/hishel-0.1.3.tar.gz", hash = "sha256:db3e07429cb739dcda851ff9b35b0f3e7589e21b90ee167df54336ac608b6ec3", size = 36649, upload-time = "2025-07-06T14:19:23.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/a5/bf3553b44a36e1c5d2aa0cd15478e02b466dcaecdc2983b07068999d2675/hishel-0.1.3-py3-none-any.whl", hash = "sha256:bae3ba9970ffc56f90014aea2b3019158fb0a5b0b635a56f414ba6b96651966e", size = 42518, upload-time = "2025-07-06T14:19:22.336Z" }, +] + [[package]] name = "html5lib" version = "1.1" @@ -1289,6 +1331,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, ] +[[package]] +name = "httpxthrottlecache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "filelock" }, + { name = "hishel" }, + { name = "pyrate-limiter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/0e/4d9b3b695dd17d3701fba0a054adf6eb48db96ab0518af26a0a2c842421b/httpxthrottlecache-0.2.1.tar.gz", hash = "sha256:10b1754648673a17ec39a7c34a511961364c69f9e700d37ac0e7f7695f3a6806", size = 16831, upload-time = "2025-08-19T02:09:17.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/28923efa54c23b2cad1de0c574ba8553f95553f2919c1afa9383b6b6a985/httpxthrottlecache-0.2.1-py3-none-any.whl", hash = "sha256:986ba39215743f63dbd414728704efed7ececfa95139a2ff07009b3bc7bd936b", size = 15995, upload-time = "2025-08-19T02:09:16.557Z" }, +] + [[package]] name = "huggingface-hub" version = "0.34.4" @@ -1320,6 +1377,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] +[[package]] +name = "humanize" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/1d/3062fcc89ee05a715c0b9bfe6490c00c576314f27ffee3a704122c6fd259/humanize-4.13.0.tar.gz", hash = "sha256:78f79e68f76f0b04d711c4e55d32bebef5be387148862cb1ef83d2b58e7935a0", size = 81884, upload-time = "2025-08-25T09:39:20.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/c7/316e7ca04d26695ef0635dc81683d628350810eb8e9b2299fc08ba49f366/humanize-4.13.0-py3-none-any.whl", hash = "sha256:b810820b31891813b1673e8fec7f1ed3312061eab2f26e3fa192c393d11ed25f", size = 128869, upload-time = "2025-08-25T09:39:18.54Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -3375,6 +3441,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/a9/8ce0ca222ef04d602924a1e099be93f5435ca6f3294182a30574d4159ca2/py_mini_racer-0.6.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:42896c24968481dd953eeeb11de331f6870917811961c9b26ba09071e07180e2", size = 5416149, upload-time = "2021-04-22T07:58:25.615Z" }, ] +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -3629,6 +3724,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] +[[package]] +name = "pyrate-limiter" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/da/f682c5c5f9f0a5414363eb4397e6b07d84a02cde69c4ceadcbf32c85537c/pyrate_limiter-3.9.0.tar.gz", hash = "sha256:6b882e2c77cda07a241d3730975daea4258344b39c878f1dd8849df73f70b0ce", size = 289308, upload-time = "2025-07-30T14:36:58.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/af/d8bf0959ece9bc4679bd203908c31019556a421d76d8143b0c6871c7f614/pyrate_limiter-3.9.0-py3-none-any.whl", hash = "sha256:77357840c8cf97a36d67005d4e090787043f54000c12c2b414ff65657653e378", size = 33628, upload-time = "2025-07-30T14:36:57.71Z" }, +] + [[package]] name = "pyreadline3" version = "3.5.4" @@ -3756,6 +3860,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, ] +[[package]] +name = "rank-bm25" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347, upload-time = "2022-02-16T12:10:52.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/21/f691fb2613100a62b3fa91e9988c991e9ca5b89ea31c0d3152a3210344f9/rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae", size = 8584, upload-time = "2022-02-16T12:10:50.626Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/fc/a98b616db9a42dcdda7c78c76bdfdf6fe290ac4c5ffbb186f73ec981ad5b/rapidfuzz-3.14.1.tar.gz", hash = "sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c", size = 57869570, upload-time = "2025-09-08T21:08:15.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/77/2f4887c9b786f203e50b816c1cde71f96642f194e6fa752acfa042cf53fd/rapidfuzz-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:809515194f628004aac1b1b280c3734c5ea0ccbd45938c9c9656a23ae8b8f553", size = 1932216, upload-time = "2025-09-08T21:06:09.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/bd/b5e445d156cb1c2a87d36d8da53daf4d2a1d1729b4851660017898b49aa0/rapidfuzz-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0afcf2d6cb633d0d4260d8df6a40de2d9c93e9546e2c6b317ab03f89aa120ad7", size = 1393414, upload-time = "2025-09-08T21:06:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/de/bd/98d065dd0a4479a635df855616980eaae1a1a07a876db9400d421b5b6371/rapidfuzz-3.14.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1c3d07d53dcafee10599da8988d2b1f39df236aee501ecbd617bd883454fcd", size = 1377194, upload-time = "2025-09-08T21:06:12.471Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/1265547b771128b686f3c431377ff1db2fa073397ed082a25998a7b06d4e/rapidfuzz-3.14.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e9ee3e1eb0a027717ee72fe34dc9ac5b3e58119f1bd8dd15bc19ed54ae3e62b", size = 1669573, upload-time = "2025-09-08T21:06:14.016Z" }, + { url = "https://files.pythonhosted.org/packages/a8/57/e73755c52fb451f2054196404ccc468577f8da023b3a48c80bce29ee5d4a/rapidfuzz-3.14.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:70c845b64a033a20c44ed26bc890eeb851215148cc3e696499f5f65529afb6cb", size = 2217833, upload-time = "2025-09-08T21:06:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/20/14/7399c18c460e72d1b754e80dafc9f65cb42a46cc8f29cd57d11c0c4acc94/rapidfuzz-3.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26db0e815213d04234298dea0d884d92b9cb8d4ba954cab7cf67a35853128a33", size = 3159012, upload-time = "2025-09-08T21:06:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/24f0226ddb5440cabd88605d2491f99ae3748a6b27b0bc9703772892ced7/rapidfuzz-3.14.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:6ad3395a416f8b126ff11c788531f157c7debeb626f9d897c153ff8980da10fb", size = 1227032, upload-time = "2025-09-08T21:06:21.06Z" }, + { url = "https://files.pythonhosted.org/packages/40/43/1d54a4ad1a5fac2394d5f28a3108e2bf73c26f4f23663535e3139cfede9b/rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61c5b9ab6f730e6478aa2def566223712d121c6f69a94c7cc002044799442afd", size = 2395054, upload-time = "2025-09-08T21:06:23.482Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/e9864cd5b0f086c4a03791f5dfe0155a1b132f789fe19b0c76fbabd20513/rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13e0ea3d0c533969158727d1bb7a08c2cc9a816ab83f8f0dcfde7e38938ce3e6", size = 2524741, upload-time = "2025-09-08T21:06:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/53f88286b912faf4a3b2619a60df4f4a67bd0edcf5970d7b0c1143501f0c/rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6325ca435b99f4001aac919ab8922ac464999b100173317defb83eae34e82139", size = 2785311, upload-time = "2025-09-08T21:06:29.471Z" }, + { url = "https://files.pythonhosted.org/packages/53/9a/229c26dc4f91bad323f07304ee5ccbc28f0d21c76047a1e4f813187d0bad/rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:07a9fad3247e68798424bdc116c1094e88ecfabc17b29edf42a777520347648e", size = 3303630, upload-time = "2025-09-08T21:06:31.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/de/20e330d6d58cbf83da914accd9e303048b7abae2f198886f65a344b69695/rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8ff5dbe78db0a10c1f916368e21d328935896240f71f721e073cf6c4c8cdedd", size = 4262364, upload-time = "2025-09-08T21:06:32.877Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/2327f83fad3534a8d69fe9cd718f645ec1fe828b60c0e0e97efc03bf12f8/rapidfuzz-3.14.1-cp312-cp312-win32.whl", hash = "sha256:9c83270e44a6ae7a39fc1d7e72a27486bccc1fa5f34e01572b1b90b019e6b566", size = 1711927, upload-time = "2025-09-08T21:06:34.669Z" }, + { url = "https://files.pythonhosted.org/packages/78/8d/199df0370133fe9f35bc72f3c037b53c93c5c1fc1e8d915cf7c1f6bb8557/rapidfuzz-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:e06664c7fdb51c708e082df08a6888fce4c5c416d7e3cc2fa66dd80eb76a149d", size = 1542045, upload-time = "2025-09-08T21:06:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c6/cc5d4bd1b16ea2657c80b745d8b1c788041a31fad52e7681496197b41562/rapidfuzz-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:6c7c26025f7934a169a23dafea6807cfc3fb556f1dd49229faf2171e5d8101cc", size = 813170, upload-time = "2025-09-08T21:06:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/0024cc8eead108c4c29337abe133d72ddf3406ce9bbfbcfc110414a7ea07/rapidfuzz-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260", size = 1926515, upload-time = "2025-09-08T21:06:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/ae/6cb211f8930bea20fa989b23f31ee7f92940caaf24e3e510d242a1b28de4/rapidfuzz-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4", size = 1388431, upload-time = "2025-09-08T21:06:41.73Z" }, + { url = "https://files.pythonhosted.org/packages/39/88/bfec24da0607c39e5841ced5594ea1b907d20f83adf0e3ee87fa454a425b/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a", size = 1375664, upload-time = "2025-09-08T21:06:43.737Z" }, + { url = "https://files.pythonhosted.org/packages/f4/43/9f282ba539e404bdd7052c7371d3aaaa1a9417979d2a1d8332670c7f385a/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f", size = 1668113, upload-time = "2025-09-08T21:06:45.758Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/0b3153053b1acca90969eb0867922ac8515b1a8a48706a3215c2db60e87c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b", size = 2212875, upload-time = "2025-09-08T21:06:47.447Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/623001dddc518afaa08ed1fbbfc4005c8692b7a32b0f08b20c506f17a770/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f", size = 3161181, upload-time = "2025-09-08T21:06:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b7/d8404ed5ad56eb74463e5ebf0a14f0019d7eb0e65e0323f709fe72e0884c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9", size = 1225495, upload-time = "2025-09-08T21:06:51.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6c/b96af62bc7615d821e3f6b47563c265fd7379d7236dfbc1cbbcce8beb1d2/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1", size = 2396294, upload-time = "2025-09-08T21:06:53.063Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b7/c60c9d22a7debed8b8b751f506a4cece5c22c0b05e47a819d6b47bc8c14e/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25", size = 2529629, upload-time = "2025-09-08T21:06:55.188Z" }, + { url = "https://files.pythonhosted.org/packages/25/94/a9ec7ccb28381f14de696ffd51c321974762f137679df986f5375d35264f/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502", size = 2782960, upload-time = "2025-09-08T21:06:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/04e5276d223060eca45250dbf79ea39940c0be8b3083661d58d57572c2c5/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f", size = 3298427, upload-time = "2025-09-08T21:06:59.319Z" }, + { url = "https://files.pythonhosted.org/packages/4a/63/24759b2a751562630b244e68ccaaf7a7525c720588fcc77c964146355aee/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e", size = 4267736, upload-time = "2025-09-08T21:07:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/18/a4/73f1b1f7f44d55f40ffbffe85e529eb9d7e7f7b2ffc0931760eadd163995/rapidfuzz-3.14.1-cp313-cp313-win32.whl", hash = "sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc", size = 1710515, upload-time = "2025-09-08T21:07:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8b/a8fe5a6ee4d06fd413aaa9a7e0a23a8630c4b18501509d053646d18c2aa7/rapidfuzz-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d", size = 1540081, upload-time = "2025-09-08T21:07:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/4b0ac16c118a2367d85450b45251ee5362661e9118a1cef88aae1765ffff/rapidfuzz-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4", size = 812725, upload-time = "2025-09-08T21:07:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cb/1ad9a76d974d153783f8e0be8dbe60ec46488fac6e519db804e299e0da06/rapidfuzz-3.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6", size = 1945173, upload-time = "2025-09-08T21:07:08.893Z" }, + { url = "https://files.pythonhosted.org/packages/d9/61/959ed7460941d8a81cbf6552b9c45564778a36cf5e5aa872558b30fc02b2/rapidfuzz-3.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd", size = 1413949, upload-time = "2025-09-08T21:07:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/f46fca44457ca1f25f23cc1f06867454fc3c3be118cd10b552b0ab3e58a2/rapidfuzz-3.14.1-cp313-cp313t-win32.whl", hash = "sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef", size = 1760666, upload-time = "2025-09-08T21:07:12.884Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d0/7a5d9c04446f8b66882b0fae45b36a838cf4d31439b5d1ab48a9d17c8e57/rapidfuzz-3.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8", size = 1579760, upload-time = "2025-09-08T21:07:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/4e/aa/2c03ae112320d0746f2c869cae68c413f3fe3b6403358556f2b747559723/rapidfuzz-3.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c", size = 832088, upload-time = "2025-09-08T21:07:17.03Z" }, + { url = "https://files.pythonhosted.org/packages/d6/36/53debca45fbe693bd6181fb05b6a2fd561c87669edb82ec0d7c1961a43f0/rapidfuzz-3.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44", size = 1926336, upload-time = "2025-09-08T21:07:18.809Z" }, + { url = "https://files.pythonhosted.org/packages/ae/32/b874f48609665fcfeaf16cbaeb2bbc210deef2b88e996c51cfc36c3eb7c3/rapidfuzz-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27", size = 1389653, upload-time = "2025-09-08T21:07:20.667Z" }, + { url = "https://files.pythonhosted.org/packages/97/25/f6c5a1ff4ec11edadacb270e70b8415f51fa2f0d5730c2c552b81651fbe3/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6", size = 1380911, upload-time = "2025-09-08T21:07:22.584Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/d322202ef8fab463759b51ebfaa33228100510c82e6153bd7a922e150270/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548", size = 1673515, upload-time = "2025-09-08T21:07:24.417Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b9/6b2a97f4c6be96cac3749f32301b8cdf751ce5617b1c8934c96586a0662b/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9", size = 2219394, upload-time = "2025-09-08T21:07:26.402Z" }, + { url = "https://files.pythonhosted.org/packages/11/bf/afb76adffe4406e6250f14ce48e60a7eb05d4624945bd3c044cfda575fbc/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6", size = 3163582, upload-time = "2025-09-08T21:07:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/e6405227560f61e956cb4c5de653b0f874751c5ada658d3532d6c1df328e/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9", size = 1221116, upload-time = "2025-09-08T21:07:30.8Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/5b757e2e18de384b11d1daf59608453f0baf5d5d8d1c43e1a964af4dc19a/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404", size = 2402670, upload-time = "2025-09-08T21:07:32.702Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/d753a415fe54531aa882e288db5ed77daaa72e05c1a39e1cbac00d23024f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816", size = 2521659, upload-time = "2025-09-08T21:07:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/d4e7fe1515430db98f42deb794c7586a026d302fe70f0216b638d89cf10f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f", size = 2788552, upload-time = "2025-09-08T21:07:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/eab05473af7a2cafb4f3994bc6bf408126b8eec99a569aac6254ac757db4/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a", size = 3306261, upload-time = "2025-09-08T21:07:39.624Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/2feb8dfcfcff6508230cd2ccfdde7a8bf988c6fda142fe9ce5d3eb15704d/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb", size = 4269522, upload-time = "2025-09-08T21:07:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/250538d73c8fbab60597c3d131a11ef2a634d38b44296ca11922794491ac/rapidfuzz-3.14.1-cp314-cp314-win32.whl", hash = "sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598", size = 1745018, upload-time = "2025-09-08T21:07:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/c5/15/d50839d20ad0743aded25b08a98ffb872f4bfda4e310bac6c111fcf6ea1f/rapidfuzz-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440", size = 1587666, upload-time = "2025-09-08T21:07:46.917Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ff/d73fec989213fb6f0b6f15ee4bbdf2d88b0686197951a06b036111cd1c7d/rapidfuzz-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a", size = 835780, upload-time = "2025-09-08T21:07:49.256Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e7/f0a242687143cebd33a1fb165226b73bd9496d47c5acfad93de820a18fa8/rapidfuzz-3.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669", size = 1945182, upload-time = "2025-09-08T21:07:51.84Z" }, + { url = "https://files.pythonhosted.org/packages/96/29/ca8a3f8525e3d0e7ab49cb927b5fb4a54855f794c9ecd0a0b60a6c96a05f/rapidfuzz-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702", size = 1413946, upload-time = "2025-09-08T21:07:53.702Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ef/6fd10aa028db19c05b4ac7fe77f5613e4719377f630c709d89d7a538eea2/rapidfuzz-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7", size = 1795851, upload-time = "2025-09-08T21:07:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/acd29ebd906a50f9e0f27d5f82a48cf5e8854637b21489bd81a2459985cf/rapidfuzz-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000", size = 1626748, upload-time = "2025-09-08T21:07:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f4/dfc7b8c46b1044a47f7ca55deceb5965985cff3193906cb32913121e6652/rapidfuzz-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b", size = 853771, upload-time = "2025-09-08T21:08:00.816Z" }, +] + [[package]] name = "redis" version = "6.4.0" @@ -4123,6 +4302,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, ] +[[package]] +name = "stamina" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/c4/d242d76ffc88aa1fd14214d3143b542857b32276db4a20f8d99669054a5e/stamina-25.1.0.tar.gz", hash = "sha256:ad674809796ae40512b3b6296cfade826efd63863ff2ca2f59f806342e91e94a", size = 561127, upload-time = "2025-03-12T09:37:08.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl", hash = "sha256:c08291da540e6f4243c20f7ee98f0ed0ac9101d639803c481a029b56d7e9b45d", size = 17323, upload-time = "2025-03-12T09:37:06.886Z" }, +] + [[package]] name = "starlette" version = "0.47.3" @@ -4185,6 +4376,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "textdistance" +version = "4.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/68/97ac72dd781301d6a52140066c68400c96f1a91f69737959e414844749b0/textdistance-4.6.3.tar.gz", hash = "sha256:d6dabc50b4ea832cdcf0e1e6021bd0c7fcd9ade155888d79bb6a3c31fce2dc6f", size = 32710, upload-time = "2024-07-16T09:34:54.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c2/c62601c858010b0513a6434b9be19bd740533a6e861eddfd30b7258d92a0/textdistance-4.6.3-py3-none-any.whl", hash = "sha256:0cb1b2cc8e3339ddc3e0f8c870e49fb49de6ecc42a718917308b3c971f34aa56", size = 31263, upload-time = "2024-07-16T09:34:51.082Z" }, +] + [[package]] name = "tiktoken" version = "0.11.0" @@ -4483,6 +4683,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + [[package]] name = "update-checker" version = "0.18.0" @@ -4556,11 +4765,13 @@ dependencies = [ { name = "a2a-sdk", extra = ["http-server"] }, { name = "agno", extra = ["openai"] }, { name = "akshare" }, + { name = "edgartools" }, { name = "fastapi" }, { name = "pydantic" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "requests" }, + { name = "sqlalchemy" }, { name = "tushare" }, { name = "uvicorn" }, { name = "yfinance" }, @@ -4571,6 +4782,7 @@ requires-dist = [ { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.4" }, { name = "agno", extras = ["openai"], specifier = ">=1.8.2,<2.0" }, { name = "akshare", specifier = ">=1.17.44" }, + { name = "edgartools", specifier = ">=4.12.2" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, @@ -4580,12 +4792,27 @@ requires-dist = [ { name = "pytz", specifier = ">=2023.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'" }, + { name = "sqlalchemy", specifier = ">=2.0.43" }, { name = "tushare", specifier = ">=1.4.24" }, { name = "uvicorn", specifier = ">=0.24.0" }, { name = "yfinance", specifier = ">=0.2.65" }, ] provides-extras = ["dev"] +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "ruff" }, +] +lint = [{ name = "ruff" }] +test = [ + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, +] + [[package]] name = "w3lib" version = "2.3.1" diff --git a/python/valuecell/core/agent/connect.py b/python/valuecell/core/agent/connect.py index af3517705..3882dadcf 100644 --- a/python/valuecell/core/agent/connect.py +++ b/python/valuecell/core/agent/connect.py @@ -31,6 +31,8 @@ def __init__(self): self._remote_agent_configs: Dict[str, dict] = {} # Per-agent locks for concurrent start_agent calls self._agent_locks: Dict[str, asyncio.Lock] = {} + # Load remote agent configs on initialization + self._load_remote_agent_configs() def _get_agent_lock(self, agent_name: str) -> asyncio.Lock: """Get or create a lock for a specific agent (thread-safe)""" diff --git a/python/valuecell/core/agent/tests/test_connect.py b/python/valuecell/core/agent/tests/test_connect.py deleted file mode 100644 index 22359d701..000000000 --- a/python/valuecell/core/agent/tests/test_connect.py +++ /dev/null @@ -1,497 +0,0 @@ -""" -Additional comprehensive tests for RemoteConnections to improve coverage. -""" - -import asyncio -import json -import tempfile -from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from valuecell.core.agent.connect import RemoteConnections - - -class TestRemoteConnectionsComprehensive: - """Comprehensive tests to improve coverage of RemoteConnections.""" - - def setup_method(self): - """Setup before each test method.""" - self.instance = RemoteConnections() - - def test_init_creates_all_required_attributes(self): - """Test that __init__ properly initializes all attributes.""" - instance = RemoteConnections() - - assert isinstance(instance._connections, dict) - assert isinstance(instance._running_agents, dict) - assert isinstance(instance._agent_instances, dict) - assert isinstance(instance._listeners, dict) - assert isinstance(instance._listener_urls, dict) - assert isinstance(instance._remote_agent_cards, dict) - assert isinstance(instance._remote_agent_configs, dict) - assert isinstance(instance._agent_locks, dict) - - # All should be empty initially - assert len(instance._connections) == 0 - assert len(instance._running_agents) == 0 - assert len(instance._agent_instances) == 0 - assert len(instance._listeners) == 0 - assert len(instance._listener_urls) == 0 - assert len(instance._remote_agent_cards) == 0 - assert len(instance._remote_agent_configs) == 0 - assert len(instance._agent_locks) == 0 - - def test_load_remote_agent_configs_with_invalid_json(self): - """Test loading remote agent configs with invalid JSON.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Create file with invalid JSON - invalid_file = Path(temp_dir) / "invalid.json" - with open(invalid_file, "w") as f: - f.write("{ invalid json content") - - # Should not raise exception - self.instance._load_remote_agent_configs(temp_dir) - - # Should not load any configs - assert len(self.instance._remote_agent_configs) == 0 - - def test_load_remote_agent_configs_with_missing_name(self): - """Test loading remote agent configs with missing name field.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Create file without name field - no_name_file = Path(temp_dir) / "no_name.json" - config_data = { - "url": "http://localhost:8000", - "description": "Test agent without name", - } - with open(no_name_file, "w") as f: - json.dump(config_data, f) - - self.instance._load_remote_agent_configs(temp_dir) - - # Should not load config without name - assert len(self.instance._remote_agent_configs) == 0 - - def test_load_remote_agent_configs_with_missing_url(self): - """Test loading remote agent configs with missing URL field.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Create file without URL field - no_url_file = Path(temp_dir) / "no_url.json" - config_data = { - "name": "test_agent", - "description": "Test agent without URL", - } - with open(no_url_file, "w") as f: - json.dump(config_data, f) - - self.instance._load_remote_agent_configs(temp_dir) - - # Should not load config without URL - assert len(self.instance._remote_agent_configs) == 0 - - @pytest.mark.asyncio - async def test_load_remote_agents_with_nonexistent_directory(self): - """Test load_remote_agents with non-existent directory.""" - with patch( - "valuecell.core.agent.connect.get_agent_card_path", - return_value=Path("/nonexistent"), - ): - # Should not raise exception - await self.instance.load_remote_agents() - - # Should not load any agents - assert len(self.instance._remote_agent_cards) == 0 - - @pytest.mark.asyncio - async def test_load_remote_agents_with_http_error(self): - """Test load_remote_agents when HTTP client fails.""" - # This test is challenging because the actual implementation doesn't - # have proper exception handling in the load_remote_agents method. - # We'll skip this for now and focus on other coverage improvements. - pytest.skip( - "Skipping test due to missing exception handling in load_remote_agents" - ) - - @pytest.mark.asyncio - async def test_connect_remote_agent_not_found(self): - """Test connect_remote_agent with non-existent agent.""" - with pytest.raises(ValueError, match="Remote agent 'nonexistent' not found"): - await self.instance.connect_remote_agent("nonexistent") - - @pytest.mark.asyncio - async def test_connect_remote_agent_success(self): - """Test successful remote agent connection.""" - # Set up remote agent config - self.instance._remote_agent_configs["test_agent"] = { - "name": "test_agent", - "url": "http://localhost:8000", - } - - with patch("valuecell.core.agent.connect.AgentClient") as mock_client: - result = await self.instance.connect_remote_agent("test_agent") - - assert result == "http://localhost:8000" - assert "test_agent" in self.instance._connections - mock_client.assert_called_once_with("http://localhost:8000") - - @pytest.mark.asyncio - async def test_start_agent_remote_agent_flow(self): - """Test start_agent with remote agent.""" - # Set up remote agent config - self.instance._remote_agent_configs["remote_agent"] = { - "name": "remote_agent", - "url": "http://localhost:8000", - } - - mock_card = MagicMock() - mock_card.capabilities.push_notifications = False - - with patch.object( - self.instance, "_handle_remote_agent", return_value=mock_card - ) as mock_handle: - result = await self.instance.start_agent("remote_agent") - - assert result == mock_card - mock_handle.assert_called_once() - - @pytest.mark.asyncio - async def test_start_agent_local_agent_not_found(self): - """Test start_agent with non-existent local agent.""" - with patch( - "valuecell.core.agent.registry.get_agent_class_by_name", return_value=None - ): - with pytest.raises( - ValueError, match="Agent 'nonexistent' not found in registry" - ): - await self.instance.start_agent("nonexistent") - - @pytest.mark.asyncio - async def test_start_agent_already_running(self): - """Test start_agent with already running agent.""" - # Mock agent instance - mock_instance = MagicMock() - mock_card = MagicMock() - mock_instance.agent_card = mock_card - - self.instance._agent_instances["test_agent"] = mock_instance - self.instance._running_agents["test_agent"] = MagicMock() - - result = await self.instance.start_agent("test_agent") - assert result == mock_card - - @pytest.mark.asyncio - async def test_start_agent_with_listener_setup_failure(self): - """Test start_agent when listener setup fails.""" - mock_agent_class = MagicMock() - mock_instance = MagicMock() - mock_card = MagicMock() - mock_card.capabilities.push_notifications = True - mock_instance.agent_card = mock_card - mock_agent_class.return_value = mock_instance - - with patch( - "valuecell.core.agent.registry.get_agent_class_by_name", - return_value=mock_agent_class, - ): - with patch.object( - self.instance, - "_setup_listener_if_needed", - side_effect=Exception("Listener failed"), - ): - with patch.object(self.instance, "_cleanup_agent") as mock_cleanup: - with pytest.raises(Exception, match="Listener failed"): - await self.instance.start_agent( - "test_agent", with_listener=True - ) - - mock_cleanup.assert_called_once_with("test_agent") - - @pytest.mark.asyncio - async def test_start_agent_service_failure(self): - """Test start_agent when agent service start fails.""" - mock_agent_class = MagicMock() - mock_instance = MagicMock() - mock_card = MagicMock() - mock_card.capabilities.push_notifications = False - mock_instance.agent_card = mock_card - mock_agent_class.return_value = mock_instance - - with patch( - "valuecell.core.agent.registry.get_agent_class_by_name", - return_value=mock_agent_class, - ): - with patch.object( - self.instance, - "_start_agent_service", - side_effect=Exception("Service failed"), - ): - with patch.object(self.instance, "_cleanup_agent") as mock_cleanup: - with pytest.raises( - RuntimeError, match="Failed to start agent 'test_agent'" - ): - await self.instance.start_agent("test_agent") - - mock_cleanup.assert_called_once_with("test_agent") - - @pytest.mark.asyncio - async def test_setup_listener_if_needed_no_listener(self): - """Test _setup_listener_if_needed when listener is not needed.""" - mock_card = MagicMock() - mock_card.capabilities.push_notifications = True - - result = await self.instance._setup_listener_if_needed( - "test_agent", - mock_card, - with_listener=False, - listener_host="localhost", - listener_port=5000, - notification_callback=None, - ) - - assert result is None - - @pytest.mark.asyncio - async def test_setup_listener_if_needed_no_push_notifications(self): - """Test _setup_listener_if_needed when agent doesn't support push notifications.""" - mock_card = MagicMock() - mock_card.capabilities.push_notifications = False - - result = await self.instance._setup_listener_if_needed( - "test_agent", - mock_card, - with_listener=True, - listener_host="localhost", - listener_port=5000, - notification_callback=None, - ) - - assert result is None - - @pytest.mark.asyncio - async def test_setup_listener_if_needed_failure(self): - """Test _setup_listener_if_needed when listener start fails.""" - mock_card = MagicMock() - mock_card.capabilities.push_notifications = True - - with patch.object( - self.instance, - "_start_listener_for_agent", - side_effect=Exception("Listener failed"), - ): - with pytest.raises( - RuntimeError, match="Failed to start listener for 'test_agent'" - ): - await self.instance._setup_listener_if_needed( - "test_agent", - mock_card, - with_listener=True, - listener_host="localhost", - listener_port=5000, - notification_callback=None, - ) - - @pytest.mark.asyncio - async def test_handle_remote_agent_already_connected(self): - """Test _handle_remote_agent when agent is already connected.""" - mock_card = MagicMock() - self.instance._connections["remote_agent"] = MagicMock() - self.instance._remote_agent_cards["remote_agent"] = mock_card - - result = await self.instance._handle_remote_agent("remote_agent") - assert result == mock_card - - @pytest.mark.asyncio - async def test_handle_remote_agent_card_loading_failure(self): - """Test _handle_remote_agent when card loading fails.""" - self.instance._remote_agent_configs["remote_agent"] = { - "name": "remote_agent", - "url": "http://localhost:8000", - } - - with patch("httpx.AsyncClient"): - with patch("valuecell.core.agent.connect.A2ACardResolver") as mock_resolver: - mock_resolver.return_value.get_agent_card.side_effect = Exception( - "Card loading failed" - ) - - await self.instance._handle_remote_agent("remote_agent") - # Should handle error gracefully and still create connection - assert "remote_agent" in self.instance._connections - - @pytest.mark.asyncio - async def test_start_listener_for_agent_with_auto_port(self): - """Test _start_listener_for_agent with automatic port assignment.""" - with patch( - "valuecell.core.agent.connect.get_next_available_port", return_value=5555 - ): - with patch("valuecell.core.agent.connect.NotificationListener"): - with patch("asyncio.create_task"): - with patch("asyncio.sleep"): - result = await self.instance._start_listener_for_agent( - "test_agent", "localhost" - ) - - assert result == "http://localhost:5555/notify" - assert "test_agent" in self.instance._listeners - assert ( - self.instance._listener_urls["test_agent"] - == "http://localhost:5555/notify" - ) - - @pytest.mark.asyncio - async def test_start_agent_service(self): - """Test _start_agent_service method.""" - mock_agent = MagicMock() - mock_agent.serve = AsyncMock() - - with patch("asyncio.create_task") as mock_task: - with patch("asyncio.sleep"): - await self.instance._start_agent_service("test_agent", mock_agent) - - mock_task.assert_called_once() - assert "test_agent" in self.instance._running_agents - - def test_create_client_for_agent(self): - """Test _create_client_for_agent method.""" - with patch("valuecell.core.agent.connect.AgentClient") as mock_client: - self.instance._create_client_for_agent( - "test_agent", "http://localhost:8000", "http://localhost:5000/notify" - ) - - mock_client.assert_called_once_with( - "http://localhost:8000", - push_notification_url="http://localhost:5000/notify", - ) - assert "test_agent" in self.instance._connections - - @pytest.mark.asyncio - async def test_cleanup_agent_complete(self): - """Test _cleanup_agent with all resources present.""" - # Set up mock resources - mock_client = AsyncMock() - - # Create proper task mocks that can be awaited - mock_listener_task = asyncio.create_task(asyncio.sleep(0)) - mock_agent_task = asyncio.create_task(asyncio.sleep(0)) - - # Cancel them immediately to simulate cleanup - mock_listener_task.cancel() - mock_agent_task.cancel() - - self.instance._connections["test_agent"] = mock_client - self.instance._listeners["test_agent"] = mock_listener_task - self.instance._running_agents["test_agent"] = mock_agent_task - self.instance._agent_instances["test_agent"] = MagicMock() - self.instance._listener_urls["test_agent"] = "http://localhost:5000/notify" - - await self.instance._cleanup_agent("test_agent") - - # Verify cleanup - mock_client.close.assert_called_once() - - assert "test_agent" not in self.instance._connections - assert "test_agent" not in self.instance._listeners - assert "test_agent" not in self.instance._running_agents - assert "test_agent" not in self.instance._agent_instances - assert "test_agent" not in self.instance._listener_urls - - @pytest.mark.asyncio - async def test_get_client_starts_agent_if_not_exists(self): - """Test get_client starts agent if connection doesn't exist.""" - mock_client = MagicMock() - - with patch.object(self.instance, "start_agent") as mock_start: - # Mock start_agent to add the connection - async def side_effect(agent_name): - self.instance._connections[agent_name] = mock_client - return MagicMock() - - mock_start.side_effect = side_effect - - result = await self.instance.get_client("test_agent") - - mock_start.assert_called_once_with("test_agent") - assert result == mock_client - - def test_get_agent_info_remote_agent(self): - """Test get_agent_info for remote agent.""" - self.instance._remote_agent_configs["remote_agent"] = { - "name": "remote_agent", - "url": "http://localhost:8000", - } - - result = self.instance.get_agent_info("remote_agent") - - assert result["name"] == "remote_agent" - assert result["type"] == "remote" - assert result["url"] == "http://localhost:8000" - assert result["connected"] is False - assert result["running"] is False - - def test_get_agent_info_remote_agent_with_card(self): - """Test get_agent_info for remote agent with loaded card.""" - mock_card = MagicMock() - mock_card.model_dump.return_value = {"name": "remote_agent", "capabilities": {}} - - self.instance._remote_agent_configs["remote_agent"] = { - "name": "remote_agent", - "url": "http://localhost:8000", - } - self.instance._remote_agent_cards["remote_agent"] = mock_card - - result = self.instance.get_agent_info("remote_agent") - - assert result["card"] == {"name": "remote_agent", "capabilities": {}} - - def test_get_agent_info_local_agent(self): - """Test get_agent_info for local agent.""" - mock_instance = MagicMock() - mock_card = MagicMock() - mock_card.url = "http://localhost:8001" - mock_card.model_dump.return_value = {"name": "local_agent"} - mock_instance.agent_card = mock_card - - self.instance._agent_instances["local_agent"] = mock_instance - self.instance._running_agents["local_agent"] = MagicMock() - self.instance._listeners["local_agent"] = MagicMock() - self.instance._listener_urls["local_agent"] = "http://localhost:5000/notify" - - result = self.instance.get_agent_info("local_agent") - - assert result["name"] == "local_agent" - assert result["type"] == "local" - assert result["url"] == "http://localhost:8001" - assert result["running"] is True - assert result["has_listener"] is True - assert result["listener_url"] == "http://localhost:5000/notify" - - def test_get_agent_info_nonexistent(self): - """Test get_agent_info for non-existent agent.""" - result = self.instance.get_agent_info("nonexistent") - assert result is None - - def test_get_remote_agent_card_with_card(self): - """Test get_remote_agent_card when card is available.""" - mock_card = {"name": "test_agent", "capabilities": {}} - self.instance._remote_agent_cards["test_agent"] = mock_card - - result = self.instance.get_remote_agent_card("test_agent") - assert result == mock_card - - def test_get_remote_agent_card_config_only(self): - """Test get_remote_agent_card when only config is available.""" - config_data = {"name": "test_agent", "url": "http://localhost:8000"} - self.instance._remote_agent_configs["test_agent"] = config_data - - result = self.instance.get_remote_agent_card("test_agent") - assert result == config_data - - def test_get_remote_agent_card_none(self): - """Test get_remote_agent_card when neither card nor config is available.""" - result = self.instance.get_remote_agent_card("nonexistent") - assert result is None - - -if __name__ == "__main__": - pytest.main([__file__]) diff --git a/python/valuecell/examples/agent_i18n_example.py b/python/valuecell/examples/agent_i18n_example.py deleted file mode 100644 index d4ac6e1ba..000000000 --- a/python/valuecell/examples/agent_i18n_example.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Example usage of i18n for Agent communication in ValueCell.""" - -# TODO: This file is a temporary file, it will be removed in the future. -import os -import sys -from datetime import datetime -from pathlib import Path - -# Add the parent directory to Python path to enable imports -current_dir = Path(__file__).parent -project_root = current_dir.parent.parent -sys.path.insert(0, str(project_root)) - -# Set environment for example -os.environ["LANG"] = "zh-Hans" -os.environ["TIMEZONE"] = "Asia/Shanghai" - -try: - from valuecell.services.agent_context import ( - get_agent_context, - get_current_user_id, - get_i18n_context, - t, - ) - from valuecell.api.i18n_api import get_i18n_api - from valuecell.config.settings import get_settings -except ImportError as e: - print(f"Import error: {e}") - print("Please make sure you're running this from the correct directory.") - sys.exit(1) - - -class ExampleAgent: - """Example agent that uses i18n context.""" - - def __init__(self, name: str): - """Initialize agent.""" - self.name = name - self.agent_context = get_agent_context() - - def process_user_request(self, user_id: str, request: str) -> str: - """Process user request with user's i18n context.""" - # Set user context for this agent - self.agent_context.set_user_context(user_id) - - # Get user's i18n context - context = self.agent_context.get_i18n_context() - - # Use user's language for responses - welcome = self.agent_context.translate("messages.welcome") - processing = self.agent_context.translate("common.loading") - - # Format current time in user's timezone - now = datetime.now() - formatted_time = self.agent_context.format_datetime(now) - - # Format some numbers - sample_amount = 1234.56 - formatted_currency = self.agent_context.format_currency(sample_amount) - - response = f""" -{self.name} Agent Response: -{welcome} -{processing} - -Request: {request} -Current time: {formatted_time} -Sample amount: {formatted_currency} -User language: {context.language} -User timezone: {context.timezone} -""" - return response.strip() - - def batch_process_users(self, user_requests: dict) -> dict: - """Process requests for multiple users with their respective contexts.""" - results = {} - - for user_id, request in user_requests.items(): - # Use context manager for temporary user context - with self.agent_context.user_context(user_id): - # All operations within this block use the specific user's i18n settings - welcome = t("messages.welcome") - success = t("messages.data_saved") - - # Format data for this user - now = datetime.now() - formatted_time = self.agent_context.format_datetime(now, "time") - - results[user_id] = { - "agent": self.name, - "welcome": welcome, - "status": success, - "time": formatted_time, - "language": self.agent_context.get_current_language(), - "timezone": self.agent_context.get_current_timezone(), - "response": f"Processed: {request}", - } - - return results - - -def setup_test_users(): - """Setup test users with different i18n preferences.""" - i18n_api = get_i18n_api() - - # User 1: English (US) - i18n_api.set_user_context( - "user1", {"language": "en-US", "timezone": "America/New_York"} - ) - - # User 2: Chinese (Simplified) - i18n_api.set_user_context( - "user2", {"language": "zh-Hans", "timezone": "Asia/Shanghai"} - ) - - # User 3: Chinese (Traditional - Hong Kong) - i18n_api.set_user_context( - "user3", {"language": "zh-Hant", "timezone": "Asia/Hong_Kong"} - ) - - # User 4: English (UK) - i18n_api.set_user_context( - "user4", {"language": "en-GB", "timezone": "Europe/London"} - ) - - -def main(): - """Main example function.""" - print("=== ValueCell Agent i18n Example ===\n") - - # Setup test users - setup_test_users() - - # Create example agents - financial_agent = ExampleAgent("Financial") - portfolio_agent = ExampleAgent("Portfolio") - - print("1. Single User Processing:") - print("-" * 50) - - # Process request for each user - users = ["user1", "user2", "user3", "user4"] - for user_id in users: - response = financial_agent.process_user_request( - user_id, "Show me my portfolio performance" - ) - print(f"\n{user_id.upper()}:") - print(response) - - print("\n" + "=" * 60) - print("2. Batch Processing with Context Management:") - print("-" * 50) - - # Batch process multiple users - user_requests = { - "user1": "Calculate my returns", - "user2": "ๅˆ†ๆžๆˆ‘็š„ๆŠ•่ต„็ป„ๅˆ", - "user3": "้กฏ็คบๆˆ‘็š„่ณ‡็”ข้…็ฝฎ", - "user4": "Update my risk profile", - } - - results = portfolio_agent.batch_process_users(user_requests) - - for user_id, result in results.items(): - print(f"\n{user_id.upper()} Results:") - for key, value in result.items(): - print(f" {key}: {value}") - - print("\n" + "=" * 60) - print("3. Agent Context Information:") - print("-" * 50) - - # Show how agents can get user context - agent_context = get_agent_context() - - for user_id in users: - agent_context.set_user_context(user_id) - context = get_i18n_context() - - print(f"\n{user_id.upper()} Context:") - print(f" Language: {context.language}") - print(f" Timezone: {context.timezone}") - print(f" Currency: {context.currency_symbol}") - print(f" Date Format: {context.date_format}") - print(f" Current User ID: {get_current_user_id()}") - - print("\n" + "=" * 60) - print("4. API Integration Example:") - print("-" * 50) - - # Show how to get configuration from API - settings = get_settings() - api_config = settings.get_api_config() - i18n_config = settings.get_i18n_config() - - print("API Configuration:") - for key, value in api_config.items(): - print(f" {key}: {value}") - - print("\nI18n Configuration:") - for key, value in i18n_config.items(): - if isinstance(value, dict): - print(f" {key}:") - for k, v in value.items(): - print(f" {k}: {v}") - else: - print(f" {key}: {value}") - - print("\n=== Example Complete ===") - - -if __name__ == "__main__": - main() diff --git a/python/valuecell/server/api/i18n_api.py b/python/valuecell/server/api/i18n_api.py index 2f932ca52..c670c8294 100644 --- a/python/valuecell/server/api/i18n_api.py +++ b/python/valuecell/server/api/i18n_api.py @@ -20,7 +20,6 @@ SupportedLanguagesData, TimezonesData, UserI18nSettingsData, - AgentI18nContextData, LanguageDetectionData, TranslationData, DateTimeFormatData, @@ -165,16 +164,6 @@ def _create_router(self) -> APIRouter: description="Update internationalization settings for specified user", ) - # Agent context - router.add_api_route( - "/agent/context", - self.get_agent_context, - methods=["GET"], - response_model=SuccessResponse[AgentI18nContextData], - summary="Get Agent i18n context", - description="Get i18n context information for inter-agent communication", - ) - return router def _get_user_context(self, user_id: Optional[str]) -> Dict[str, Any]: @@ -487,30 +476,6 @@ async def update_user_settings( }, ) - async def get_agent_context( - self, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - session_id: Optional[str] = Header(None, alias="X-Session-ID"), - ) -> SuccessResponse[AgentI18nContextData]: - """Get i18n context for agent communication.""" - # Load user-specific settings - self._get_user_context(user_id) - - context = AgentI18nContextData( - language=self.i18n_service.get_current_language(), - timezone=self.i18n_service.get_current_timezone(), - currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), - date_format=self.i18n_service._i18n_config.get_date_format(), - time_format=self.i18n_service._i18n_config.get_time_format(), - number_format=self.i18n_service._i18n_config.get_number_format(), - user_id=user_id, - session_id=session_id, - ) - - return SuccessResponse.create( - data=context, msg="Agent i18n context retrieved successfully" - ) - def get_user_context(self, user_id: str) -> Dict[str, Any]: """Get user context for agents.""" return self._user_contexts.get( diff --git a/python/valuecell/server/config/settings.py b/python/valuecell/server/config/settings.py index 3244f5345..705ce693e 100644 --- a/python/valuecell/server/config/settings.py +++ b/python/valuecell/server/config/settings.py @@ -47,7 +47,7 @@ def __init__(self): self.LOGS_DIR.mkdir(exist_ok=True) # I18n Configuration - self.LOCALE_DIR = self.BASE_DIR / "locales" + self.LOCALE_DIR = self.BASE_DIR / "configs/locales" # Agent Configuration self.AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "300")) # 5 minutes diff --git a/python/valuecell/server/services/__init__.py b/python/valuecell/server/services/__init__.py index b5f1f42cd..1d5f3a925 100644 --- a/python/valuecell/server/services/__init__.py +++ b/python/valuecell/server/services/__init__.py @@ -9,15 +9,9 @@ # I18n service from .i18n_service import I18nService, get_i18n_service -# Agent context service -from .agent_context import AgentContextManager, get_agent_context - __all__ = [ # I18n services "I18nService", "get_i18n_service", - # Agent context services - "AgentContextManager", - "get_agent_context", # Note: For asset services, import directly from valuecell.services.assets ] diff --git a/python/valuecell/server/services/agent_context.py b/python/valuecell/server/services/agent_context.py deleted file mode 100644 index 0908c66b2..000000000 --- a/python/valuecell/server/services/agent_context.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Agent context management for ValueCell application.""" - -from typing import Optional -from datetime import datetime -import threading -from contextlib import contextmanager - -from ..services.i18n_service import get_i18n_service -from ..api.schemas.i18n import AgentI18nContextData - - -class AgentContextManager: - """Manages context for agents to access user i18n settings.""" - - def __init__(self): - """Initialize agent context manager.""" - self.i18n_service = get_i18n_service() - self._local = threading.local() - self._user_contexts = {} # Store user contexts locally - - def set_user_context( - self, - user_id: str, - session_id: Optional[str] = None, - language: str = "en-US", - timezone: str = "UTC", - ): - """Set current user context for the agent.""" - # Store in thread local storage - self._local.user_id = user_id - self._local.session_id = session_id - self._local.language = language - self._local.timezone = timezone - - # Update i18n service - self.i18n_service.set_language(self._local.language) - self.i18n_service.set_timezone(self._local.timezone) - - def get_current_user_id(self) -> Optional[str]: - """Get current user ID.""" - return getattr(self._local, "user_id", None) - - def get_current_session_id(self) -> Optional[str]: - """Get current session ID.""" - return getattr(self._local, "session_id", None) - - def get_current_language(self) -> str: - """Get current user's language.""" - return getattr(self._local, "language", "en-US") - - def get_current_timezone(self) -> str: - """Get current user's timezone.""" - return getattr(self._local, "timezone", "UTC") - - def get_i18n_context(self) -> AgentI18nContextData: - """Get complete i18n context for agent.""" - return AgentI18nContextData( - language=self.get_current_language(), - timezone=self.get_current_timezone(), - currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), - date_format=self.i18n_service._i18n_config.get_date_format(), - time_format=self.i18n_service._i18n_config.get_time_format(), - number_format=self.i18n_service._i18n_config.get_number_format(), - user_id=self.get_current_user_id(), - session_id=self.get_current_session_id(), - ) - - def translate(self, key: str, **variables) -> str: - """Translate using current user's language.""" - return self.i18n_service.translate( - key, self.get_current_language(), **variables - ) - - def format_datetime(self, dt: datetime, format_type: str = "datetime") -> str: - """Format datetime using current user's settings.""" - return self.i18n_service.format_datetime(dt, format_type) - - def format_number(self, number: float, decimal_places: int = 2) -> str: - """Format number using current user's settings.""" - return self.i18n_service.format_number(number, decimal_places) - - def format_currency(self, amount: float, decimal_places: int = 2) -> str: - """Format currency using current user's settings.""" - return self.i18n_service.format_currency(amount, decimal_places) - - @contextmanager - def user_context(self, user_id: str, session_id: Optional[str] = None): - """Context manager for temporary user context.""" - # Save current context - old_user_id = getattr(self._local, "user_id", None) - old_session_id = getattr(self._local, "session_id", None) - old_language = getattr(self._local, "language", "en-US") - old_timezone = getattr(self._local, "timezone", "UTC") - - try: - # Set new context - self.set_user_context(user_id, session_id) - yield self - finally: - # Restore old context - if old_user_id: - self._local.user_id = old_user_id - self._local.session_id = old_session_id - self._local.language = old_language - self._local.timezone = old_timezone - self.i18n_service.set_language(old_language) - self.i18n_service.set_timezone(old_timezone) - else: - # Clear context - if hasattr(self._local, "user_id"): - delattr(self._local, "user_id") - if hasattr(self._local, "session_id"): - delattr(self._local, "session_id") - if hasattr(self._local, "language"): - delattr(self._local, "language") - if hasattr(self._local, "timezone"): - delattr(self._local, "timezone") - - def clear_context(self): - """Clear current user context.""" - if hasattr(self._local, "user_id"): - delattr(self._local, "user_id") - if hasattr(self._local, "session_id"): - delattr(self._local, "session_id") - if hasattr(self._local, "language"): - delattr(self._local, "language") - if hasattr(self._local, "timezone"): - delattr(self._local, "timezone") - - -# Global agent context manager -_agent_context: Optional[AgentContextManager] = None - - -def get_agent_context() -> AgentContextManager: - """Get global agent context manager.""" - global _agent_context - if _agent_context is None: - _agent_context = AgentContextManager() - return _agent_context - - -def reset_agent_context(): - """Reset global agent context manager.""" - global _agent_context - _agent_context = None - - -# Convenience functions for agents -def set_user_context(user_id: str, session_id: Optional[str] = None): - """Set user context for current agent (convenience function).""" - return get_agent_context().set_user_context(user_id, session_id) - - -def get_current_user_id() -> Optional[str]: - """Get current user ID (convenience function).""" - return get_agent_context().get_current_user_id() - - -def get_i18n_context() -> AgentI18nContextData: - """Get i18n context (convenience function).""" - return get_agent_context().get_i18n_context() - - -def t(key: str, **variables) -> str: - """Translate using current user context (convenience function).""" - return get_agent_context().translate(key, **variables) - - -def user_context(user_id: str, session_id: Optional[str] = None): - """Context manager for user context (convenience function).""" - return get_agent_context().user_context(user_id, session_id)