Skip to content

Commit 26593dc

Browse files
authored
[2.x] Upgrade to LangChain v0.3 and Pydantic v2 (#1199)
* remove importliner from project * initial upgrade to langchain~=0.3, pydantic~=2.0 * default to `None` for all `Optional` fields explicitly * fix history impl for Pydantic v2, fixes chat * prefer `.model_dump_json()` over `.json()` Addresses a Pydantic v2 deprecation warning, as `BaseModel.json()` is now deprecated in favor of `BaseModel.model_dump_json()`. * replace `.dict()` with `.model_dump()`. `BaseModel.dict()` is deprecated in favor of `BaseModel.model_dump()` in Pydantic v2. * fix BaseProvider.server_settings * fix OpenRouterProvider * fix remaining unit tests * address all Pydantic v1 deprecation warnings * pre-commit * fix mypy
1 parent b2bb5b5 commit 26593dc

File tree

27 files changed

+189
-271
lines changed

27 files changed

+189
-271
lines changed

.github/workflows/lint.yml

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,3 @@ jobs:
1717
run: jlpm
1818
- name: Lint TypeScript source
1919
run: jlpm lerna run lint:check
20-
21-
lint_py_imports:
22-
name: Lint Python imports
23-
runs-on: ubuntu-latest
24-
steps:
25-
- uses: actions/checkout@v4
26-
- name: Echo environment details
27-
run: |
28-
which python
29-
which pip
30-
python --version
31-
pip --version
32-
33-
# see #546 for context on why this is necessary
34-
- name: Create venv
35-
run: |
36-
python -m venv lint_py_imports
37-
38-
- name: Install job dependencies
39-
run: |
40-
source ./lint_py_imports/bin/activate
41-
pip install jupyterlab~=4.0
42-
pip install import-linter~=1.12.1
43-
44-
- name: Install Jupyter AI packages from source
45-
run: |
46-
source ./lint_py_imports/bin/activate
47-
jlpm install
48-
jlpm install-from-src
49-
50-
- name: Lint Python imports
51-
run: |
52-
source ./lint_py_imports/bin/activate
53-
lint-imports

packages/jupyter-ai-magics/jupyter_ai_magics/embedding_providers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
Field,
77
MultiEnvAuthStrategy,
88
)
9-
from langchain.pydantic_v1 import BaseModel, Extra
109
from langchain_community.embeddings import (
1110
GPT4AllEmbeddings,
1211
HuggingFaceHubEmbeddings,
1312
QianfanEmbeddingsEndpoint,
1413
)
14+
from pydantic import BaseModel, ConfigDict
1515

1616

1717
class BaseEmbeddingsProvider(BaseModel):
1818
"""Base class for embedding providers"""
1919

20-
class Config:
21-
extra = Extra.allow
20+
# pydantic v2 model config
21+
# upstream docs: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.extra
22+
model_config = ConfigDict(extra="allow")
2223

2324
id: ClassVar[str] = ...
2425
"""ID for this provider class."""

packages/jupyter-ai-magics/jupyter_ai_magics/magics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ def handle_error(self, args: ErrorArgs):
442442

443443
prompt = f"Explain the following error:\n\n{last_error}"
444444
# Set CellArgs based on ErrorArgs
445-
values = args.dict()
445+
values = args.model_dump()
446446
values["type"] = "root"
447447
cell_args = CellArgs(**values)
448448

packages/jupyter-ai-magics/jupyter_ai_magics/models/completion.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List, Literal, Optional
22

3-
from langchain.pydantic_v1 import BaseModel
3+
from pydantic import BaseModel
44

55

66
class InlineCompletionRequest(BaseModel):
@@ -21,12 +21,12 @@ class InlineCompletionRequest(BaseModel):
2121
# whether to stream the response (if supported by the model)
2222
stream: bool
2323
# path to the notebook of file for which the completions are generated
24-
path: Optional[str]
24+
path: Optional[str] = None
2525
# language inferred from the document mime type (if possible)
26-
language: Optional[str]
26+
language: Optional[str] = None
2727
# identifier of the cell for which the completions are generated if in a notebook
2828
# previous cells and following cells can be used to learn the wider context
29-
cell_id: Optional[str]
29+
cell_id: Optional[str] = None
3030

3131

3232
class InlineCompletionItem(BaseModel):
@@ -36,9 +36,9 @@ class InlineCompletionItem(BaseModel):
3636
"""
3737

3838
insertText: str
39-
filterText: Optional[str]
40-
isIncomplete: Optional[bool]
41-
token: Optional[str]
39+
filterText: Optional[str] = None
40+
isIncomplete: Optional[bool] = None
41+
token: Optional[str] = None
4242

4343

4444
class CompletionError(BaseModel):
@@ -59,7 +59,7 @@ class InlineCompletionReply(BaseModel):
5959
list: InlineCompletionList
6060
# number of request for which we are replying
6161
reply_to: int
62-
error: Optional[CompletionError]
62+
error: Optional[CompletionError] = None
6363

6464

6565
class InlineCompletionStreamChunk(BaseModel):
@@ -69,7 +69,7 @@ class InlineCompletionStreamChunk(BaseModel):
6969
response: InlineCompletionItem
7070
reply_to: int
7171
done: bool
72-
error: Optional[CompletionError]
72+
error: Optional[CompletionError] = None
7373

7474

7575
__all__ = [

packages/jupyter-ai-magics/jupyter_ai_magics/models/persona.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from langchain.pydantic_v1 import BaseModel
1+
from pydantic import BaseModel
22

33

44
class Persona(BaseModel):

packages/jupyter-ai-magics/jupyter_ai_magics/parsers.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Literal, Optional, get_args
33

44
import click
5-
from langchain.pydantic_v1 import BaseModel
5+
from pydantic import BaseModel
66

77
FORMAT_CHOICES_TYPE = Literal[
88
"code", "html", "image", "json", "markdown", "math", "md", "text"
@@ -46,23 +46,23 @@ class CellArgs(BaseModel):
4646
type: Literal["root"] = "root"
4747
model_id: str
4848
format: FORMAT_CHOICES_TYPE
49-
model_parameters: Optional[str]
49+
model_parameters: Optional[str] = None
5050
# The following parameters are required only for SageMaker models
51-
region_name: Optional[str]
52-
request_schema: Optional[str]
53-
response_path: Optional[str]
51+
region_name: Optional[str] = None
52+
request_schema: Optional[str] = None
53+
response_path: Optional[str] = None
5454

5555

5656
# Should match CellArgs
5757
class ErrorArgs(BaseModel):
5858
type: Literal["error"] = "error"
5959
model_id: str
6060
format: FORMAT_CHOICES_TYPE
61-
model_parameters: Optional[str]
61+
model_parameters: Optional[str] = None
6262
# The following parameters are required only for SageMaker models
63-
region_name: Optional[str]
64-
request_schema: Optional[str]
65-
response_path: Optional[str]
63+
region_name: Optional[str] = None
64+
request_schema: Optional[str] = None
65+
response_path: Optional[str] = None
6666

6767

6868
class HelpArgs(BaseModel):
@@ -75,7 +75,7 @@ class VersionArgs(BaseModel):
7575

7676
class ListArgs(BaseModel):
7777
type: Literal["list"] = "list"
78-
provider_id: Optional[str]
78+
provider_id: Optional[str] = None
7979

8080

8181
class RegisterArgs(BaseModel):

packages/jupyter-ai-magics/jupyter_ai_magics/partner_providers/openrouter.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
from jupyter_ai_magics import BaseProvider
44
from jupyter_ai_magics.providers import EnvAuthStrategy, TextField
5-
from langchain_core.pydantic_v1 import root_validator
6-
from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env
5+
from langchain_core.utils import get_from_dict_or_env
76
from langchain_openai import ChatOpenAI
87

98

@@ -31,7 +30,9 @@ class OpenRouterProvider(BaseProvider, ChatOpenRouter):
3130
]
3231

3332
def __init__(self, **kwargs):
34-
openrouter_api_key = kwargs.pop("openrouter_api_key", None)
33+
openrouter_api_key = get_from_dict_or_env(
34+
kwargs, key="openrouter_api_key", env_key="OPENROUTER_API_KEY", default=None
35+
)
3536
openrouter_api_base = kwargs.pop(
3637
"openai_api_base", "https://openrouter.ai/api/v1"
3738
)
@@ -42,14 +43,6 @@ def __init__(self, **kwargs):
4243
**kwargs,
4344
)
4445

45-
@root_validator(pre=False, skip_on_failure=True, allow_reuse=True)
46-
def validate_environment(cls, values: Dict) -> Dict:
47-
"""Validate that api key and python package exists in environment."""
48-
values["openai_api_key"] = convert_to_secret_str(
49-
get_from_dict_or_env(values, "openai_api_key", "OPENROUTER_API_KEY")
50-
)
51-
return super().validate_environment(values)
52-
5346
@classmethod
5447
def is_api_key_exc(cls, e: Exception):
5548
import openai

packages/jupyter-ai-magics/jupyter_ai_magics/providers.py

Lines changed: 21 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,14 @@
2424
PromptTemplate,
2525
SystemMessagePromptTemplate,
2626
)
27-
from langchain.pydantic_v1 import BaseModel, Extra
2827
from langchain.schema import LLMResult
2928
from langchain.schema.output_parser import StrOutputParser
3029
from langchain.schema.runnable import Runnable
3130
from langchain_community.chat_models import QianfanChatEndpoint
3231
from langchain_community.llms import AI21, GPT4All, HuggingFaceEndpoint, Together
3332
from langchain_core.language_models.chat_models import BaseChatModel
3433
from langchain_core.language_models.llms import BaseLLM
35-
36-
# this is necessary because `langchain.pydantic_v1.main` does not include
37-
# `ModelMetaclass`, as it is not listed in `__all__` by the `pydantic.main`
38-
# subpackage.
39-
try:
40-
from pydantic.v1.main import ModelMetaclass
41-
except:
42-
from pydantic.main import ModelMetaclass
34+
from pydantic import BaseModel, ConfigDict
4335

4436
from . import completion_utils as completion
4537
from .models.completion import (
@@ -122,7 +114,7 @@ class EnvAuthStrategy(BaseModel):
122114
name: str
123115
"""The name of the environment variable, e.g. `'ANTHROPIC_API_KEY'`."""
124116

125-
keyword_param: Optional[str]
117+
keyword_param: Optional[str] = None
126118
"""
127119
If unset (default), the authentication token is provided as a keyword
128120
argument with the parameter equal to the environment variable name in
@@ -177,51 +169,10 @@ class IntegerField(BaseModel):
177169
Field = Union[TextField, MultilineTextField, IntegerField]
178170

179171

180-
class ProviderMetaclass(ModelMetaclass):
181-
"""
182-
A metaclass that ensures all class attributes defined inline within the
183-
class definition are accessible and included in `Class.__dict__`.
184-
185-
This is necessary because Pydantic drops any ClassVars that are defined as
186-
an instance field by a parent class, even if they are defined inline within
187-
the class definition. We encountered this case when `langchain` added a
188-
`name` attribute to a parent class shared by all `Provider`s, which caused
189-
`Provider.name` to be inaccessible. See #558 for more info.
190-
"""
191-
192-
def __new__(mcs, name, bases, namespace, **kwargs):
193-
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
194-
for key in namespace:
195-
# skip private class attributes
196-
if key.startswith("_"):
197-
continue
198-
# skip class attributes already listed in `cls.__dict__`
199-
if key in cls.__dict__:
200-
continue
201-
202-
setattr(cls, key, namespace[key])
203-
204-
return cls
205-
206-
@property
207-
def server_settings(cls):
208-
return cls._server_settings
209-
210-
@server_settings.setter
211-
def server_settings(cls, value):
212-
if cls._server_settings is not None:
213-
raise AttributeError("'server_settings' attribute was already set")
214-
cls._server_settings = value
215-
216-
_server_settings = None
217-
218-
219-
class BaseProvider(BaseModel, metaclass=ProviderMetaclass):
220-
#
221-
# pydantic config
222-
#
223-
class Config:
224-
extra = Extra.allow
172+
class BaseProvider(BaseModel):
173+
# pydantic v2 model config
174+
# upstream docs: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.extra
175+
model_config = ConfigDict(extra="allow")
225176

226177
#
227178
# class attrs
@@ -236,15 +187,25 @@ class Config:
236187
"""List of supported models by their IDs. For registry providers, this will
237188
be just ["*"]."""
238189

239-
help: ClassVar[str] = None
190+
help: ClassVar[Optional[str]] = None
240191
"""Text to display in lieu of a model list for a registry provider that does
241192
not provide a list of models."""
242193

243-
model_id_key: ClassVar[str] = ...
244-
"""Kwarg expected by the upstream LangChain provider."""
194+
model_id_key: ClassVar[Optional[str]] = None
195+
"""
196+
Optional field which specifies the key under which `model_id` is passed to
197+
the parent LangChain class.
198+
199+
If unset, this defaults to "model_id".
200+
"""
245201

246-
model_id_label: ClassVar[str] = ""
247-
"""Human-readable label of the model ID."""
202+
model_id_label: ClassVar[Optional[str]] = None
203+
"""
204+
Optional field which sets the label shown in the UI allowing users to
205+
select/type a model ID.
206+
207+
If unset, the label shown in the UI defaults to "Model ID".
208+
"""
248209

249210
pypi_package_deps: ClassVar[List[str]] = []
250211
"""List of PyPi package dependencies."""
@@ -586,7 +547,6 @@ def __init__(self, **kwargs):
586547

587548
id = "gpt4all"
588549
name = "GPT4All"
589-
docs = "https://docs.gpt4all.io/gpt4all_python.html"
590550
models = [
591551
"ggml-gpt4all-j-v1.2-jazzy",
592552
"ggml-gpt4all-j-v1.3-groovy",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import ClassVar, Optional
2+
3+
from pydantic import BaseModel
4+
5+
from ..providers import BaseProvider
6+
7+
8+
def test_provider_classvars():
9+
"""
10+
Asserts that class attributes are not omitted due to parent classes defining
11+
an instance field of the same name. This was a bug present in Pydantic v1,
12+
which led to an issue documented in #558.
13+
14+
This bug is fixed as of `pydantic==2.10.2`, but we will keep this test in
15+
case this behavior changes in future releases.
16+
"""
17+
18+
class Parent(BaseModel):
19+
test: Optional[str] = None
20+
21+
class Base(BaseModel):
22+
test: ClassVar[str]
23+
24+
class Child(Base, Parent):
25+
test: ClassVar[str] = "expected"
26+
27+
assert Child.test == "expected"

0 commit comments

Comments
 (0)