diff --git a/frontend/src/assets/png/index.ts b/frontend/src/assets/png/index.ts index f305cb590..4aaea61e5 100644 --- a/frontend/src/assets/png/index.ts +++ b/frontend/src/assets/png/index.ts @@ -37,6 +37,7 @@ export { default as AzurePng } from "./model-providers/azure.png"; export { default as DashScopePng } from "./model-providers/dashscope.png"; export { default as DeepSeekPng } from "./model-providers/deepseek.png"; export { default as GooglePng } from "./model-providers/google.png"; +export { default as OllamaPng } from "./model-providers/ollama.png"; export { default as OpenAiPng } from "./model-providers/openai.png"; export { default as OpenAiCompatiblePng } from "./model-providers/openai-compatible.png"; export { default as OpenRouterPng } from "./model-providers/openrouter.png"; diff --git a/frontend/src/assets/png/model-providers/ollama.png b/frontend/src/assets/png/model-providers/ollama.png new file mode 100644 index 000000000..ee2681e75 Binary files /dev/null and b/frontend/src/assets/png/model-providers/ollama.png differ diff --git a/frontend/src/constants/icons.ts b/frontend/src/constants/icons.ts index 668977c85..25fe155bb 100644 --- a/frontend/src/constants/icons.ts +++ b/frontend/src/constants/icons.ts @@ -10,6 +10,7 @@ import { HyperliquidPng, MexcPng, OkxPng, + OllamaPng, OpenAiCompatiblePng, OpenAiPng, OpenRouterPng, @@ -26,6 +27,7 @@ export const MODEL_PROVIDER_ICONS = { google: GooglePng, azure: AzurePng, dashscope: DashScopePng, + ollama: OllamaPng, }; export const EXCHANGE_ICONS = { diff --git a/python/configs/config.yaml b/python/configs/config.yaml index f081770bb..504961762 100644 --- a/python/configs/config.yaml +++ b/python/configs/config.yaml @@ -50,6 +50,9 @@ models: dashscope: config_file: "providers/dashscope.yaml" api_key_env: "DASHSCOPE_API_KEY" + + ollama: + config_file: "providers/ollama.yaml" # Agent Configuration agents: diff --git a/python/configs/providers/ollama.yaml b/python/configs/providers/ollama.yaml new file mode 100644 index 000000000..9325e8b38 --- /dev/null +++ b/python/configs/providers/ollama.yaml @@ -0,0 +1,7 @@ +name: Ollama +provider_type: Ollama +enabled: true +default_model: qwen3:4b +models: +- id: qwen3:4b + name: qwen3:4b diff --git a/python/pyproject.toml b/python/pyproject.toml index c7631cea3..0e6955b58 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "yfinance>=0.2.65", "requests>=2.32.5", "akshare>=1.17.87", - "agno[openai, google, lancedb]>=2.0,<3.0", + "agno[openai, google, lancedb, ollama]>=2.0,<3.0", "edgartools>=4.12.2", "sqlalchemy>=2.0.43", "aiosqlite>=0.19.0", diff --git a/python/uv.lock b/python/uv.lock index 34ae8119c..b20206cbe 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -62,6 +62,9 @@ lancedb = [ { name = "lancedb" }, { name = "tantivy" }, ] +ollama = [ + { name = "ollama" }, +] openai = [ { name = "openai" }, ] @@ -1938,6 +1941,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, ] +[[package]] +name = "ollama" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, +] + [[package]] name = "openai" version = "1.107.0" @@ -3642,7 +3658,7 @@ version = "0.1.19" source = { editable = "." } dependencies = [ { name = "a2a-sdk", extra = ["http-server"] }, - { name = "agno", extra = ["google", "lancedb", "openai"] }, + { name = "agno", extra = ["google", "lancedb", "ollama", "openai"] }, { name = "aiofiles" }, { name = "aiosqlite" }, { name = "akshare" }, @@ -3700,7 +3716,7 @@ test = [ [package.metadata] requires-dist = [ { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.4" }, - { name = "agno", extras = ["openai", "google", "lancedb"], specifier = ">=2.0,<3.0" }, + { name = "agno", extras = ["openai", "google", "lancedb", "ollama"], specifier = ">=2.0,<3.0" }, { name = "aiofiles", specifier = ">=24.1.0" }, { name = "aiosqlite", specifier = ">=0.19.0" }, { name = "akshare", specifier = ">=1.17.87" }, diff --git a/python/valuecell/adapters/models/__init__.py b/python/valuecell/adapters/models/__init__.py index eca956049..0bdbd6334 100644 --- a/python/valuecell/adapters/models/__init__.py +++ b/python/valuecell/adapters/models/__init__.py @@ -26,6 +26,7 @@ GoogleProvider, ModelFactory, ModelProvider, + OllamaProvider, OpenAICompatibleProvider, OpenAIProvider, OpenRouterProvider, @@ -49,6 +50,7 @@ "SiliconFlowProvider", "DeepSeekProvider", "DashScopeProvider", + "OllamaProvider", # Convenience functions "create_model", "create_model_for_agent", diff --git a/python/valuecell/adapters/models/factory.py b/python/valuecell/adapters/models/factory.py index 2c8e6f924..341b90525 100644 --- a/python/valuecell/adapters/models/factory.py +++ b/python/valuecell/adapters/models/factory.py @@ -564,6 +564,29 @@ def create_embedder(self, model_id: Optional[str] = None, **kwargs): ) +class OllamaProvider(ModelProvider): + """Ollama model provider""" + + def create_model(self, model_id: Optional[str] = None, **kwargs): + """Create Ollama model via agno""" + try: + from agno.models.ollama import Ollama + except ImportError: + raise ImportError( + "agno package not installed, install with: pip install agno" + ) + + model_id = model_id or self.config.default_model + + logger.info(f"Creating Ollama model: {model_id}") + + return Ollama(id=model_id) + + def is_available(self) -> bool: + """Ollama doesn't require API key, just needs host configured""" + return bool(self.config.parameters.get("host")) + + class ModelFactory: """ Factory for creating model instances with provider abstraction @@ -585,6 +608,7 @@ class ModelFactory: "openai-compatible": OpenAICompatibleProvider, "deepseek": DeepSeekProvider, "dashscope": DashScopeProvider, + "ollama": OllamaProvider, } def __init__(self, config_manager: Optional[ConfigManager] = None): diff --git a/python/valuecell/config/manager.py b/python/valuecell/config/manager.py index e12830266..5da57fd19 100644 --- a/python/valuecell/config/manager.py +++ b/python/valuecell/config/manager.py @@ -135,6 +135,7 @@ def primary_provider(self) -> str: "openai", "openai-compatible", "azure", + "ollama", ] for preferred in preferred_order: diff --git a/python/valuecell/server/api/routers/models.py b/python/valuecell/server/api/routers/models.py index 6fec260a1..ec7797f6b 100644 --- a/python/valuecell/server/api/routers/models.py +++ b/python/valuecell/server/api/routers/models.py @@ -134,6 +134,7 @@ def _api_key_url_for(provider: str) -> str | None: "siliconflow": "https://cloud.siliconflow.cn/account/ak", "deepseek": "https://platform.deepseek.com/api_keys", "dashscope": "https://bailian.console.aliyun.com/#/home", + "ollama": None, } return mapping.get(provider)