From 8342e88534b78390221c0ff5bfc9b37759fc26f5 Mon Sep 17 00:00:00 2001 From: Xinmin Zeng <135568692+fancyboi999@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:56:54 +0800 Subject: [PATCH] fix(models): handle google provider import errors and add dependency (#952) * fix(models): improve provider import guidance and add google provider dep * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(reflection): prefer provider install hint on transitive import errors --------- Co-authored-by: Willem Jiang Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/CLAUDE.md | 1 + backend/README.md | 4 + backend/pyproject.toml | 1 + backend/src/reflection/resolvers.py | 32 ++++++- backend/tests/test_reflection_resolvers.py | 46 ++++++++++ backend/uv.lock | 99 ++++++++++++++++++++++ config.example.yaml | 9 ++ 7 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_reflection_resolvers.py diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 32b2bce..b64918e 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -241,6 +241,7 @@ Proxied through nginx: `/api/langgraph/*` → LangGraph, all other `/api/*` → - Supports `thinking_enabled` flag with per-model `when_thinking_enabled` overrides - Supports `supports_vision` flag for image understanding models - Config values starting with `$` resolved as environment variables +- Missing provider modules surface actionable install hints from reflection resolvers (for example `uv add langchain-google-genai`) ### Memory System (`src/agents/memory/`) diff --git a/backend/README.md b/backend/README.md index 3b59625..f1b4b9e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -252,6 +252,10 @@ Key sections: - `subagents` - Subagent system (enabled/disabled) - `memory` - Memory system settings (enabled, storage, debounce, facts limits) +Provider note: +- `models[*].use` references provider classes by module path (for example `langchain_openai:ChatOpenAI`). +- If a provider module is missing, DeerFlow now returns an actionable error with install guidance (for example `uv add langchain-google-genai`). + ### Extensions Configuration (`extensions_config.json`) MCP servers and skill states in a single file: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 01379bd..8f255b7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "uvicorn[standard]>=0.34.0", "ddgs>=9.10.0", "duckdb>=1.4.4", + "langchain-google-genai>=4.2.1", ] [dependency-groups] diff --git a/backend/src/reflection/resolvers.py b/backend/src/reflection/resolvers.py index e940c6c..89cc2f3 100644 --- a/backend/src/reflection/resolvers.py +++ b/backend/src/reflection/resolvers.py @@ -1,5 +1,29 @@ from importlib import import_module +MODULE_TO_PACKAGE_HINTS = { + "langchain_google_genai": "langchain-google-genai", + "langchain_anthropic": "langchain-anthropic", + "langchain_openai": "langchain-openai", + "langchain_deepseek": "langchain-deepseek", +} + + +def _build_missing_dependency_hint(module_path: str, err: ImportError) -> str: + """Build an actionable hint when module import fails.""" + module_root = module_path.split(".", 1)[0] + missing_module = getattr(err, "name", None) or module_root + + # Prefer provider package hints for known integrations, even when the import + # error is triggered by a transitive dependency (e.g. `google`). + package_name = MODULE_TO_PACKAGE_HINTS.get(module_root) + if package_name is None: + package_name = MODULE_TO_PACKAGE_HINTS.get(missing_module, missing_module.replace("_", "-")) + + return ( + f"Missing dependency '{missing_module}'. " + f"Install it with `uv add {package_name}` (or `pip install {package_name}`), then restart DeerFlow." + ) + def resolve_variable[T]( variable_path: str, @@ -27,7 +51,13 @@ def resolve_variable[T]( try: module = import_module(module_path) except ImportError as err: - raise ImportError(f"Could not import module {module_path}") from err + module_root = module_path.split(".", 1)[0] + err_name = getattr(err, "name", None) + if isinstance(err, ModuleNotFoundError) or err_name == module_root: + hint = _build_missing_dependency_hint(module_path, err) + raise ImportError(f"Could not import module {module_path}. {hint}") from err + # Preserve the original ImportError message for non-missing-module failures. + raise ImportError(f"Error importing module {module_path}: {err}") from err try: variable = getattr(module, variable_name) diff --git a/backend/tests/test_reflection_resolvers.py b/backend/tests/test_reflection_resolvers.py new file mode 100644 index 0000000..154ce52 --- /dev/null +++ b/backend/tests/test_reflection_resolvers.py @@ -0,0 +1,46 @@ +"""Tests for reflection resolvers.""" + +import pytest + +from src.reflection import resolvers +from src.reflection.resolvers import resolve_variable + + +def test_resolve_variable_reports_install_hint_for_missing_google_provider(monkeypatch: pytest.MonkeyPatch): + """Missing google provider should return actionable install guidance.""" + def fake_import_module(module_path: str): + raise ModuleNotFoundError(f"No module named '{module_path}'", name=module_path) + + monkeypatch.setattr(resolvers, "import_module", fake_import_module) + + with pytest.raises(ImportError) as exc_info: + resolve_variable("langchain_google_genai:ChatGoogleGenerativeAI") + + message = str(exc_info.value) + assert "Could not import module langchain_google_genai" in message + assert "uv add langchain-google-genai" in message + + +def test_resolve_variable_reports_install_hint_for_missing_google_transitive_dependency( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Missing transitive dependency should still return actionable install guidance.""" + + def fake_import_module(module_path: str): + # Simulate provider module existing but a transitive dependency (e.g. `google`) missing. + raise ModuleNotFoundError("No module named 'google'", name="google") + + monkeypatch.setattr(resolvers, "import_module", fake_import_module) + + with pytest.raises(ImportError) as exc_info: + resolve_variable("langchain_google_genai:ChatGoogleGenerativeAI") + + message = str(exc_info.value) + # Even when a transitive dependency is missing, the hint should still point to the provider package. + assert "uv add langchain-google-genai" in message +def test_resolve_variable_invalid_path_format(): + """Invalid variable path should fail with format guidance.""" + with pytest.raises(ImportError) as exc_info: + resolve_variable("invalid.variable.path") + + assert "doesn't look like a variable path" in str(exc_info.value) diff --git a/backend/uv.lock b/backend/uv.lock index f455279..fbf69b4 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -607,6 +607,7 @@ dependencies = [ { name = "kubernetes" }, { name = "langchain" }, { name = "langchain-deepseek" }, + { name = "langchain-google-genai" }, { name = "langchain-mcp-adapters" }, { name = "langchain-openai" }, { name = "langgraph" }, @@ -641,6 +642,7 @@ requires-dist = [ { name = "kubernetes", specifier = ">=30.0.0" }, { name = "langchain", specifier = ">=1.2.3" }, { name = "langchain-deepseek", specifier = ">=1.0.1" }, + { name = "langchain-google-genai", specifier = ">=4.2.1" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.0" }, { name = "langchain-openai", specifier = ">=1.1.7" }, { name = "langgraph", specifier = ">=1.0.6" }, @@ -763,6 +765,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, ] +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + [[package]] name = "firecrawl-py" version = "4.13.4" @@ -884,6 +895,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -1379,6 +1430,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/dd/a803dfbf64273232f3fc82f859487331abb717671bbcdf266fd80de6ef78/langchain_deepseek-1.0.1-py3-none-any.whl", hash = "sha256:0a9862f335f1873370bb0fe1928ac19b8b9292b014ef5412da462ded8bb82c5a", size = 8325, upload-time = "2025-11-13T16:29:12.385Z" }, ] +[[package]] +name = "langchain-google-genai" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/63/e7d148f903cebfef50109da71378f411166f068d66f79b9e16a62dbacf41/langchain_google_genai-4.2.1.tar.gz", hash = "sha256:7f44487a0337535897e3bba9a1d6605d722629e034f757ffa8755af0aa85daa8", size = 278288, upload-time = "2026-02-19T19:29:19.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/7e/46c5973bd8b10a5c4c8a77136cf536e658796380a17c740246074901b038/langchain_google_genai-4.2.1-py3-none-any.whl", hash = "sha256:a7735289cf94ca3a684d830e09196aac8f6e75e647e3a0a1c3c9dc534ceb985e", size = 66500, upload-time = "2026-02-19T19:29:18.002Z" }, +] + [[package]] name = "langchain-mcp-adapters" version = "0.2.1" @@ -2485,6 +2551,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -3015,6 +3102,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + [[package]] name = "ruff" version = "0.14.11" diff --git a/config.example.yaml b/config.example.yaml index c64d37e..0738f5f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -50,6 +50,15 @@ models: # max_tokens: 8192 # supports_vision: true # Enable vision support for view_image tool + # Example: Google Gemini model + # - name: gemini-2.5-pro + # display_name: Gemini 2.5 Pro + # use: langchain_google_genai:ChatGoogleGenerativeAI + # model: gemini-2.5-pro + # google_api_key: $GOOGLE_API_KEY + # max_tokens: 8192 + # supports_vision: true + # Example: DeepSeek model (with thinking support) # - name: deepseek-v3 # display_name: DeepSeek V3 (Thinking)