mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +08:00
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 <willem.jiang@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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/`)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
46
backend/tests/test_reflection_resolvers.py
Normal file
46
backend/tests/test_reflection_resolvers.py
Normal file
@@ -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)
|
||||
99
backend/uv.lock
generated
99
backend/uv.lock
generated
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user