Skip to content

Commit bbfa20f

Browse files
committed
♻️ refactor: upgrade to langchain 1.0.0a12 and migrate to v1alpha ecosystem
**Breaking Changes:** - Upgraded langchain to 1.0.0a12 which forces langchain-core to v1alpha - Migrated from langchain-qwq and langchain-siliconflow to langchain-openai - langchain-qwq and langchain-siliconflow are not yet v1alpha compatible - Consolidated all OpenAI-compatible providers to use langchain-openai - Standardized InjectedState imports to `langchain.tools.tool_node.InjectedState` - Removed fallback import logic from search.py - Updated handoff.py to use canonical import location **API Changes:** - Fixed AgentStatePydantic deprecation warnings in tests - Updated import paths for v1alpha compatibility - Removed redundant provider registration for deprecated packages **Test Updates:** - Updated all unit and integration tests for langchain v1alpha - Fixed API key fixture handling in test suites - All 171 unit tests passing across libs and apps **Dependencies:** - libs/langgraph-up-devkits/pyproject.toml: Updated to langchain-openai - apps/sample-deep-agent/pyproject.toml: Aligned with v1alpha ecosystem - uv.lock: Regenerated with new dependency tree
1 parent 68ba0b0 commit bbfa20f

File tree

16 files changed

+569
-564
lines changed

16 files changed

+569
-564
lines changed

.github/workflows/unit-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,9 @@ jobs:
4141
ignore_words_file: .codespellignore
4242
path: "**/README.md"
4343
- name: Run tests with make
44+
env:
45+
SILICONFLOW_API_KEY: sk-test-fake-key-for-ci
46+
OPENROUTER_API_KEY: sk-test-fake-key-for-ci
47+
OPENAI_API_KEY: sk-test-fake-key-for-ci
4448
run: |
4549
make test

apps/sample-agent/src/sample_agent/subagents/math.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ def make_graph(config: RunnableConfig | None = None) -> CompiledStateGraph[Any,
3434
model = load_chat_model(context.model_name)
3535

3636
# Create and return the math agent directly
37-
agent = create_agent(
37+
return create_agent(
3838
model=model,
3939
tools=[add, multiply],
4040
name="math_expert",
41-
prompt=MATH_EXPERT_PROMPT,
41+
system_prompt=MATH_EXPERT_PROMPT,
4242
)
43-
return agent.compile() if hasattr(agent, "compile") else agent

apps/sample-agent/src/sample_agent/subagents/research.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ def make_graph(config: RunnableConfig | None = None) -> CompiledStateGraph[Any,
3434
model = load_chat_model(context.model_name)
3535

3636
# Create and return the research agent directly
37-
agent = create_agent(
37+
return create_agent(
3838
model=model,
3939
tools=[web_search],
4040
name="research_expert",
41-
prompt=RESEARCH_EXPERT_PROMPT,
41+
system_prompt=RESEARCH_EXPERT_PROMPT,
4242
)
43-
return agent.compile() if hasattr(agent, "compile") else agent

apps/sample-agent/tests/unit/test_graph.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ def test_math_agent_creation(self, mock_load_model, mock_create_agent):
8989

9090
mock_model = Mock()
9191
mock_load_model.return_value = mock_model
92-
mock_agent = Mock()
93-
mock_agent.compile.return_value = "compiled_agent"
94-
mock_create_agent.return_value = mock_agent
92+
mock_compiled_graph = Mock()
93+
mock_create_agent.return_value = mock_compiled_graph
9594

9695
config = {"configurable": {"model_name": "test_model"}}
9796
result = make_graph(config)
@@ -102,8 +101,8 @@ def test_math_agent_creation(self, mock_load_model, mock_create_agent):
102101
assert call_args[1]['model'] == mock_model
103102
assert call_args[1]['name'] == "math_expert"
104103
assert len(call_args[1]['tools']) == 2 # add and multiply
105-
# Result should be compiled agent since agent has compile method
106-
assert result == "compiled_agent"
104+
# Result should be the compiled graph returned by create_agent
105+
assert result == mock_compiled_graph
107106

108107
@patch('sample_agent.subagents.research.create_agent')
109108
@patch('sample_agent.subagents.research.load_chat_model')
@@ -113,9 +112,8 @@ def test_research_agent_creation(self, mock_load_model, mock_create_agent):
113112

114113
mock_model = Mock()
115114
mock_load_model.return_value = mock_model
116-
mock_agent = Mock()
117-
mock_agent.compile.return_value = "compiled_agent"
118-
mock_create_agent.return_value = mock_agent
115+
mock_compiled_graph = Mock()
116+
mock_create_agent.return_value = mock_compiled_graph
119117

120118
config = {"configurable": {"model_name": "test_model"}}
121119
result = make_graph(config)
@@ -126,8 +124,8 @@ def test_research_agent_creation(self, mock_load_model, mock_create_agent):
126124
assert call_args[1]['model'] == mock_model
127125
assert call_args[1]['name'] == "research_expert"
128126
assert len(call_args[1]['tools']) == 1 # web_search
129-
# Result should be compiled agent since agent has compile method
130-
assert result == "compiled_agent"
127+
# Result should be the compiled graph returned by create_agent
128+
assert result == mock_compiled_graph
131129

132130
@patch('sample_agent.graph.load_chat_model')
133131
@patch('sample_agent.graph.make_math_graph')

apps/sample-deep-agent/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ readme = "README.md"
99
license = { text = "MIT" }
1010
requires-python = ">=3.11,<4.0"
1111
dependencies = [
12-
"deepagents>=0.0.9",
12+
"deepagents>=0.0.11",
1313
"langgraph-up-devkits",
1414
]
1515

apps/sample-deep-agent/tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"""Test configuration and fixtures."""
22

3-
import os
43
from unittest.mock import Mock, patch
54

65
import pytest
76

87

98
@pytest.fixture(autouse=True)
109
def mock_openai_key(monkeypatch):
11-
"""Mock OPENAI_API_KEY to prevent API key errors in unit tests."""
10+
"""Mock API keys to prevent API key errors in unit tests."""
1211
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-fake-key-for-unit-tests")
1312
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test-fake-key-for-unit-tests")
13+
monkeypatch.setenv("SILICONFLOW_API_KEY", "sk-test-fake-key-for-unit-tests")
1414

1515

1616
@pytest.fixture

apps/sample-deep-agent/tests/unit/test_graph.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Unit tests for sample deep agent graph components."""
22

3-
import pytest
43
from unittest.mock import Mock, patch
54

65
from sample_deep_agent.context import DeepAgentContext
@@ -225,9 +224,9 @@ def test_tool_integration_in_graph(self):
225224
assert deep_web_search is not None
226225
assert think_tool is not None
227226

228-
# Should have required methods
229-
assert callable(deep_web_search)
230-
assert callable(think_tool)
227+
# Should have required methods (StructuredTool has invoke, not directly callable)
228+
assert hasattr(deep_web_search, "invoke") or callable(deep_web_search)
229+
assert hasattr(think_tool, "invoke") or callable(think_tool)
231230

232231
@patch('sample_deep_agent.graph.async_create_deep_agent')
233232
def test_tools_passed_to_deep_agent(self, mock_create_deep_agent):

libs/langgraph-up-devkits/pyproject.toml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "langgraph-up-devkits"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
description = "Development toolkit for LangGraph agents with middleware, context schemas, and provider integrations"
55
authors = [
66
{ name = "Haili Zhang", email = "[email protected]" },
@@ -20,12 +20,11 @@ classifiers = [
2020
"Topic :: Scientific/Engineering :: Artificial Intelligence",
2121
]
2222
dependencies = [
23-
"langchain>=1.0.0a10",
24-
"langchain-dev-utils>=0.1.15",
23+
"langchain>=1.0.0a12",
24+
"langchain-dev-utils>=0.1.17",
2525
"tavily-python>=0.7.12",
26-
"langchain-siliconflow>=0.1.3",
27-
"langchain-qwq>=0.2.1",
28-
"langchain-mcp-adapters>=0.1.10",
26+
"langchain-mcp-adapters>=0.1.11",
27+
"langchain-openai>=1.0.0a4",
2928
]
3029

3130
[project.urls]

libs/langgraph-up-devkits/src/langgraph_up_devkits/tools/search.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,12 @@
77
from datetime import datetime
88
from typing import Annotated, Any, cast
99

10+
from langchain.tools.tool_node import InjectedState
1011
from langchain_core.messages import ToolMessage
11-
from langchain_core.tools import tool
12+
from langchain_core.tools import InjectedToolCallId, tool
1213
from langgraph.runtime import get_runtime
1314
from langgraph.types import Command
1415

15-
try:
16-
# Try new location first (LangGraph 0.2+)
17-
from langgraph.prebuilt.tool_node import InjectedState
18-
except ImportError:
19-
try:
20-
# Fallback to old location for compatibility
21-
from langchain.agents.tool_node import InjectedState # type: ignore[import-not-found,no-redef]
22-
except ImportError as e:
23-
raise ImportError(
24-
"InjectedState is required for deep_web_search tool. "
25-
"Please update LangChain and LangGraph to compatible versions."
26-
) from e
27-
28-
try:
29-
from langchain_core.tools import InjectedToolCallId
30-
except ImportError as e:
31-
raise ImportError(
32-
"InjectedToolCallId is required for deep_web_search tool. Please update LangChain to a compatible version."
33-
) from e
34-
3516

3617
def _get_tavily_client() -> Any:
3718
"""Dynamically import Tavily client to avoid hard dependency."""

libs/langgraph-up-devkits/src/langgraph_up_devkits/utils/providers.py

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -57,80 +57,78 @@ def normalize_region(region: str) -> str | None:
5757
return None
5858

5959

60+
def _get_qwen_base_url() -> str:
61+
"""Get Qwen base URL based on region."""
62+
region = normalize_region(os.getenv("REGION", ""))
63+
return (
64+
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
65+
if region == "international"
66+
else "https://dashscope.aliyuncs.com/compatible-mode/v1"
67+
)
68+
69+
70+
def _get_siliconflow_base_url() -> str:
71+
"""Get SiliconFlow base URL based on region."""
72+
region = normalize_region(os.getenv("REGION", ""))
73+
return "https://api.siliconflow.com/v1" if region == "international" else "https://api.siliconflow.cn/v1"
74+
75+
6076
def _register_qwen_provider() -> bool:
61-
"""Register Qwen provider using langchain-dev-utils."""
77+
"""Register Qwen provider using langchain-dev-utils, fallback to OpenAI."""
6278
if not DEV_UTILS_AVAILABLE:
6379
return False
6480

81+
base_url = _get_qwen_base_url()
82+
6583
try:
66-
# Dynamic imports to avoid hard dependencies
84+
# Try to use native langchain_qwq package
6785
import importlib
6886

6987
qwq_module = importlib.import_module("langchain_qwq")
7088
ChatQwen = getattr(qwq_module, "ChatQwen", None)
7189
ChatQwQ = getattr(qwq_module, "ChatQwQ", None)
7290

73-
if not ChatQwen or not ChatQwQ:
74-
return False
75-
76-
# Register both ChatQwen and ChatQwQ
77-
register_model_provider("qwen", ChatQwen) # For regular Qwen models
78-
register_model_provider("qwq", ChatQwQ) # For QwQ models
79-
80-
# Handle region-based URLs
81-
region = normalize_region(os.getenv("REGION", ""))
82-
if region == "prc":
83-
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
84-
elif region == "international":
85-
base_url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
86-
else:
87-
base_url = None
88-
89-
# Register with base URL if needed
90-
if base_url:
91+
if ChatQwen and ChatQwQ:
9192
register_model_provider("qwen", ChatQwen, base_url=base_url)
9293
register_model_provider("qwq", ChatQwQ, base_url=base_url)
93-
94-
return True
94+
return True
9595
except (ImportError, AttributeError, ModuleNotFoundError):
96-
return False
96+
pass
97+
98+
# Fallback to OpenAI client
99+
register_model_provider("qwen", "openai", base_url=base_url)
100+
register_model_provider("qwq", "openai", base_url=base_url)
101+
return True
97102

98103

99104
def _register_siliconflow_provider() -> bool:
100-
"""Register SiliconFlow provider using langchain-dev-utils with proper base URL."""
105+
"""Register SiliconFlow provider using langchain-dev-utils, fallback to OpenAI."""
101106
if not DEV_UTILS_AVAILABLE:
102107
return False
103108

109+
base_url = _get_siliconflow_base_url()
110+
104111
try:
105-
# Dynamic import to avoid hard dependency
112+
# Try to use native langchain_siliconflow package
106113
import importlib
107114

108115
siliconflow_module = importlib.import_module("langchain_siliconflow")
109116
ChatSiliconFlow = getattr(siliconflow_module, "ChatSiliconFlow", None)
110-
if not ChatSiliconFlow:
111-
return False
112-
113-
# Handle region-based URLs during registration
114-
region = normalize_region(os.getenv("REGION", ""))
115-
if region == "prc":
116-
base_url = "https://api.siliconflow.cn/v1"
117-
elif region == "international":
118-
base_url = "https://api.siliconflow.com/v1"
119-
else:
120-
base_url = "https://api.siliconflow.cn/v1" # Default to PRC
121-
122-
# Create a factory function that returns ChatSiliconFlow with correct base URL
123-
def siliconflow_factory(**kwargs: Any) -> Any:
124-
# Always use our region-specific base URL
125-
kwargs["base_url"] = base_url
126-
return ChatSiliconFlow(**kwargs)
127-
128-
# Register the factory function instead of the raw class
129-
register_model_provider("siliconflow", siliconflow_factory)
130-
131-
return True
117+
118+
if ChatSiliconFlow:
119+
# Create factory to inject base_url
120+
def siliconflow_factory(**kwargs: Any) -> Any:
121+
kwargs["base_url"] = base_url
122+
return ChatSiliconFlow(**kwargs)
123+
124+
register_model_provider("siliconflow", siliconflow_factory)
125+
return True
132126
except (ImportError, AttributeError, ModuleNotFoundError):
133-
return False
127+
pass
128+
129+
# Fallback to OpenAI client
130+
register_model_provider("siliconflow", "openai", base_url=base_url)
131+
return True
134132

135133

136134
def _register_openrouter_provider() -> bool:

0 commit comments

Comments
 (0)