mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 01:15:09 +03:00
feat(core,partners): add package version tracking to tracing metadata (#35295)
Following on the heels of #35293 TODO: - Packages outside of this repo (e.g. LiteLLM, Nvidia, Google, AWS) --- ## Summary Surface partner package versions in `metadata.versions` on LangSmith traces. Mirrors the JS SDK's `_addVersion()` pattern ([langchainjs#10106](https://github.com/langchain-ai/langchainjs/pull/10106)). Each model constructor records its package version via `_add_version()` on `BaseLanguageModel`. The version dict accumulates through the class hierarchy — `langchain-core` is added in `BaseLanguageModel.model_post_init`, `langchain-openai` in `BaseChatOpenAI._set_openai_chat_version`, and each leaf partner in its uniquely-named `model_validator`. Traces end up with: ```json { "metadata": { "versions": { "langchain-core": "1.4.5", "langchain-openai": "1.3.0", "langchain-xai": "1.2.2" } } } ``` ### Changes - `BaseLanguageModel._add_version(pkg, version)` — appends to `self.metadata["versions"]`; accepts any `Mapping` type; emits a warning if a non-mapping value is found and replaced - `BaseLanguageModel.model_post_init` — adds `langchain-core` version; calls `super()` for MRO safety - `_merge_metadata_dicts` — one-level-deep (non-recursive) merge for nested dict metadata keys - `CallbackManager.add_metadata` — uses `_merge_metadata_dicts` instead of flat `dict.update()` so nested metadata dicts (like `versions`) coexist rather than clobber - `merge_configs` — uses `_merge_metadata_dicts` for config merging **Partners:** - Each now calls `self._add_version("langchain-<pkg>", __version__)` ### Design decisions - **Constructor-based, not `_get_ls_params`-based** — versions flow through `self.metadata` (local metadata on traces), not through `LangSmithParams`. This matches JS and makes child-class version inheritance automatic (no merge/clobber issues). - **`versions` is local (non-inheritable) metadata** — `self.metadata` is passed to `CallbackManager.configure` as `local_metadata` (`add_metadata(..., inherit=False)`), so `versions` is attached **once per chat-model run** and is **not** propagated to child runs or duplicated onto every streaming chunk. This is intentionally the opposite of the inheritable-per-chunk metadata that #36588 was reducing for performance — `versions` does not regress that path. - **`add_metadata` deep-merge is a correctness fix, not just for versions** — previously `add_metadata`/`merge_configs` did a flat top-level `dict.update`/spread, so any nested metadata dict baked into a config (e.g. via `.with_config({"metadata": {...}})`) would be wholly replaced when a caller also passed `metadata`. `_merge_metadata_dicts` merges one level deep so user-provided `config.metadata.versions` and model-set `versions` coexist instead of clobbering. The merge runs once per `configure` (not per chunk), so it is off the streaming hot path. - **One level deep only** — `_merge_metadata_dicts` is deliberately *not* a recursive deep merge; values nested more than one level are last-writer-wins. This covers the `versions` case without the ambiguity/cost of arbitrary-depth merging. - **Warn on non-dict `metadata["versions"]`** — if a user sets `metadata={"versions": "some-string"}`, `_add_version` emits a warning and replaces the value with the version dict rather than silently discarding user data or crashing. This is a soft breaking change for anyone who previously stored non-dict values at this key. ### Follow-ups (tracked separately, out of scope here) - JS `mergeConfigs` still flat-spreads nested metadata, so `metadata.versions` can still clobber on the JS side until an equivalent deep-merge lands. --- Made by [Open SWE](https://openswe.vercel.app) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
1 parent
d5f7d33f88
commit
948f6cc58c
90 files changed
+1618
-173
No files matched your search
@@ -1,67 +0,0 @@
|
||||
# Ensures version numbers in pyproject.toml and version.py stay in sync.
|
||||
#
|
||||
# (Prevents releases with mismatched version numbers)
|
||||
|
||||
name: "🔍 Check Version Equality"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "libs/core/pyproject.toml"
|
||||
- "libs/core/langchain_core/version.py"
|
||||
- "libs/partners/anthropic/pyproject.toml"
|
||||
- "libs/partners/anthropic/langchain_anthropic/_version.py"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check_version_equality:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: "✅ Verify pyproject.toml & version.py Match"
|
||||
run: |
|
||||
# Check core versions
|
||||
CORE_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/core/pyproject.toml)
|
||||
CORE_VERSION_PY_VERSION=$(grep -Po '(?<=^VERSION = ")[^"]*' libs/core/langchain_core/version.py)
|
||||
|
||||
# Compare core versions
|
||||
if [ "$CORE_PYPROJECT_VERSION" != "$CORE_VERSION_PY_VERSION" ]; then
|
||||
echo "langchain-core versions in pyproject.toml and version.py do not match!"
|
||||
echo "pyproject.toml version: $CORE_PYPROJECT_VERSION"
|
||||
echo "version.py version: $CORE_VERSION_PY_VERSION"
|
||||
exit 1
|
||||
else
|
||||
echo "Core versions match: $CORE_PYPROJECT_VERSION"
|
||||
fi
|
||||
|
||||
# Check langchain_v1 versions
|
||||
LANGCHAIN_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/langchain_v1/pyproject.toml)
|
||||
LANGCHAIN_INIT_PY_VERSION=$(grep -Po '(?<=^__version__ = ")[^"]*' libs/langchain_v1/langchain/__init__.py)
|
||||
|
||||
# Compare langchain_v1 versions
|
||||
if [ "$LANGCHAIN_PYPROJECT_VERSION" != "$LANGCHAIN_INIT_PY_VERSION" ]; then
|
||||
echo "langchain_v1 versions in pyproject.toml and __init__.py do not match!"
|
||||
echo "pyproject.toml version: $LANGCHAIN_PYPROJECT_VERSION"
|
||||
echo "version.py version: $LANGCHAIN_INIT_PY_VERSION"
|
||||
exit 1
|
||||
else
|
||||
echo "Langchain v1 versions match: $LANGCHAIN_PYPROJECT_VERSION"
|
||||
fi
|
||||
|
||||
# Check langchain-anthropic versions
|
||||
ANTHROPIC_PYPROJECT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' libs/partners/anthropic/pyproject.toml)
|
||||
ANTHROPIC_VERSION_PY_VERSION=$(grep -Po '(?<=^__version__ = ")[^"]*' libs/partners/anthropic/langchain_anthropic/_version.py)
|
||||
|
||||
# Compare langchain-anthropic versions
|
||||
if [ "$ANTHROPIC_PYPROJECT_VERSION" != "$ANTHROPIC_VERSION_PY_VERSION" ]; then
|
||||
echo "langchain-anthropic versions in pyproject.toml and _version.py do not match!"
|
||||
echo "pyproject.toml version: $ANTHROPIC_PYPROJECT_VERSION"
|
||||
echo "_version.py version: $ANTHROPIC_VERSION_PY_VERSION"
|
||||
exit 1
|
||||
else
|
||||
echo "Langchain-anthropic versions match: $ANTHROPIC_PYPROJECT_VERSION"
|
||||
fi
|
||||
@@ -0,0 +1,54 @@
|
||||
# Ensures version numbers in pyproject.toml and _version.py stay in sync.
|
||||
#
|
||||
# (Prevents releases with mismatched version numbers)
|
||||
|
||||
name: "Check Version Equality"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "libs/core/pyproject.toml"
|
||||
- "libs/core/langchain_core/version.py"
|
||||
- "libs/langchain_v1/pyproject.toml"
|
||||
- "libs/langchain_v1/langchain/__init__.py"
|
||||
- "libs/partners/*/pyproject.toml"
|
||||
- "libs/partners/**/_version.py"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check_version_equality:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: astral-sh/setup-uv@0ca8f610542aa7f4acaf39e65cf4eb3c35091883 # v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: "Verify pyproject.toml & version files match"
|
||||
run: |
|
||||
FAILED=0
|
||||
|
||||
for dir in libs/core libs/langchain_v1 libs/partners/*; do
|
||||
[ -f "$dir/Makefile" ] || continue
|
||||
if grep -q '^check_version:' "$dir/Makefile"; then
|
||||
echo "--- $dir ---"
|
||||
make -C "$dir" check_version || FAILED=1
|
||||
elif find "$dir" -maxdepth 2 -name '_version.py' -not -path '*/tests/*' \
|
||||
| grep -q .; then
|
||||
# A package ships a _version.py but has no way to verify it stays
|
||||
# in sync with pyproject.toml. Don't let it pass unchecked.
|
||||
echo "--- $dir ---"
|
||||
echo "Error: $dir has a _version.py but no 'check_version' Makefile target"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more version checks failed!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1205,9 +1205,15 @@ class BaseCallbackManager(CallbackManagerMixin):
|
||||
metadata: The metadata to add.
|
||||
inherit: Whether to inherit the metadata.
|
||||
"""
|
||||
self.metadata.update(metadata)
|
||||
from langchain_core.runnables.config import ( # noqa: PLC0415
|
||||
_merge_metadata_dicts,
|
||||
)
|
||||
|
||||
self.metadata = _merge_metadata_dicts(self.metadata, metadata)
|
||||
if inherit:
|
||||
self.inheritable_metadata.update(metadata)
|
||||
self.inheritable_metadata = _merge_metadata_dicts(
|
||||
self.inheritable_metadata, metadata
|
||||
)
|
||||
|
||||
def remove_metadata(self, keys: list[str]) -> None:
|
||||
"""Remove metadata from the callback manager.
|
||||
|
||||
@@ -138,6 +138,30 @@ def _get_verbosity() -> bool:
|
||||
return get_verbose()
|
||||
|
||||
|
||||
@cache
|
||||
def _get_langchain_version() -> str | None:
|
||||
"""Return the installed `langchain` version, or `None` if not installed.
|
||||
|
||||
Cached because `importlib.metadata.version` performs a filesystem lookup and
|
||||
`model_post_init` runs on every `BaseLanguageModel` instantiation. `langchain`
|
||||
is an optional sibling package, so its absence is expected and not an error.
|
||||
"""
|
||||
from importlib.metadata import PackageNotFoundError # noqa: PLC0415
|
||||
from importlib.metadata import version as pkg_version # noqa: PLC0415
|
||||
|
||||
try:
|
||||
return pkg_version("langchain")
|
||||
except PackageNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
# Warm the cache at import time, while we're guaranteed to be on the synchronous
|
||||
# import path and outside any event loop. Otherwise the first model constructed
|
||||
# inside async code would run the blocking `os.stat` (via `importlib.metadata`)
|
||||
# on the event loop, tripping blocking-I/O detectors like blockbuster.
|
||||
_get_langchain_version()
|
||||
|
||||
|
||||
class BaseLanguageModel(
|
||||
RunnableSerializable[LanguageModelInput, LanguageModelOutputVar], ABC
|
||||
):
|
||||
@@ -179,6 +203,75 @@ class BaseLanguageModel(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def model_post_init(self, _context: Any, /) -> None:
|
||||
"""Pydantic V2 lifecycle hook called automatically after `__init__`.
|
||||
|
||||
Seeds `metadata["versions"]` with the installed `langchain-core`
|
||||
(and `langchain`, if installed) versions so that every LLM trace
|
||||
carries the package versions that produced it.
|
||||
|
||||
Partner packages should **not** override this method. Instead, they
|
||||
should define a `@model_validator(mode="after")` that calls
|
||||
`_add_version` to append their own version to the same dict.
|
||||
|
||||
!!! warning "Validator naming"
|
||||
|
||||
Each subclass's validator **must** have a unique name. Pydantic
|
||||
replaces — rather than chains — same-named `model_validator` methods
|
||||
in child classes. For example, a `BaseChatOpenAI` subclass should
|
||||
use `_set_<partner>_version`, not `_set_version`, to avoid silently
|
||||
dropping the parent's entry.
|
||||
|
||||
Args:
|
||||
_context: Pydantic validation context (typically `None`).
|
||||
"""
|
||||
super().model_post_init(_context)
|
||||
from langchain_core.version import VERSION # noqa: PLC0415
|
||||
|
||||
self._add_version("langchain-core", VERSION)
|
||||
|
||||
langchain_version = _get_langchain_version()
|
||||
if langchain_version is not None:
|
||||
self._add_version("langchain", langchain_version)
|
||||
|
||||
def _add_version(self, pkg: str, version: str) -> None:
|
||||
"""Record a package version in `metadata.versions` for tracing.
|
||||
|
||||
Each layer in the class hierarchy (core -> langchain -> partner)
|
||||
calls this so that the resulting metadata dict accumulates *all*
|
||||
package versions involved in an invocation.
|
||||
|
||||
Example resulting metadata:
|
||||
|
||||
```python
|
||||
{
|
||||
"versions": {
|
||||
"langchain-core": "1.x.x",
|
||||
"langchain": "1.x.x",
|
||||
"langchain-openai": "1.x.x",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
pkg: Package name (e.g., `'langchain-openai'`).
|
||||
version: Installed version string.
|
||||
"""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
existing = self.metadata.get("versions")
|
||||
if existing is not None and not isinstance(existing, Mapping):
|
||||
warnings.warn(
|
||||
f"metadata['versions'] expected a dict, got "
|
||||
f"{type(existing).__name__}; overwriting with package version dict",
|
||||
stacklevel=2,
|
||||
)
|
||||
existing = None
|
||||
self.metadata["versions"] = {
|
||||
**(existing if isinstance(existing, Mapping) else {}),
|
||||
pkg: version,
|
||||
}
|
||||
|
||||
@field_validator("verbose", mode="before")
|
||||
def set_verbose(cls, verbose: bool | None) -> bool: # noqa: FBT001
|
||||
"""If verbose is `None`, set it.
|
||||
|
||||
@@ -7,7 +7,15 @@ import asyncio
|
||||
# Cannot move uuid to TYPE_CHECKING as RunnableConfig is used in Pydantic models
|
||||
import uuid # noqa: TC003
|
||||
import warnings
|
||||
from collections.abc import Awaitable, Callable, Generator, Iterable, Iterator, Sequence
|
||||
from collections.abc import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generator,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
from concurrent.futures import Executor, Future, ThreadPoolExecutor
|
||||
from contextlib import contextmanager
|
||||
from contextvars import Context, ContextVar, Token, copy_context
|
||||
@@ -388,6 +396,50 @@ def patch_config(
|
||||
return config
|
||||
|
||||
|
||||
def _merge_metadata_dicts(
|
||||
base: Mapping[str, Any], incoming: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Merge two metadata dicts one level deep (not a recursive deep merge).
|
||||
|
||||
If both sides have a `Mapping` value for the same key, the inner mappings
|
||||
are merged (last-writer-wins within). Non-mapping values use
|
||||
last-writer-wins at the top level. Only one level of depth is merged; values
|
||||
nested more deeply are not recursively merged.
|
||||
|
||||
Args:
|
||||
base: The base metadata dict.
|
||||
|
||||
Values here are kept unless overridden by `incoming`.
|
||||
incoming: The metadata dict to merge on top.
|
||||
|
||||
Its values take precedence on conflict.
|
||||
|
||||
Returns:
|
||||
A new merged dict.
|
||||
|
||||
Inputs are not mutated. The returned dict performs shallow copies at
|
||||
the top level and one level deep; mutable values nested beyond that
|
||||
depth are shared references with the originals.
|
||||
"""
|
||||
merged = {**base}
|
||||
for key, value in incoming.items():
|
||||
if (
|
||||
key in merged
|
||||
and isinstance(merged[key], Mapping)
|
||||
and isinstance(value, Mapping)
|
||||
):
|
||||
merged[key] = {**merged[key], **value}
|
||||
elif isinstance(value, Mapping):
|
||||
merged[key] = {**value}
|
||||
else:
|
||||
merged[key] = value
|
||||
# Ensure non-overlapping nested mappings are also copies, not shared refs.
|
||||
for key in base:
|
||||
if key not in incoming and isinstance(merged[key], Mapping):
|
||||
merged[key] = {**merged[key]}
|
||||
return merged
|
||||
|
||||
|
||||
def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||
"""Merge multiple configs into one.
|
||||
|
||||
@@ -403,10 +455,10 @@ def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||
for config in (ensure_config(c) for c in configs if c is not None):
|
||||
for key in config:
|
||||
if key == "metadata":
|
||||
base["metadata"] = {
|
||||
**base.get("metadata", {}),
|
||||
**(config.get("metadata") or {}),
|
||||
}
|
||||
base["metadata"] = _merge_metadata_dicts(
|
||||
base.get("metadata", {}),
|
||||
config.get("metadata") or {},
|
||||
)
|
||||
elif key == "tags":
|
||||
base["tags"] = sorted(
|
||||
set(base.get("tags", []) + (config.get("tags") or [])),
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuid
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from contextlib import contextmanager
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from typing import TYPE_CHECKING, Any, Literal, get_type_hints
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -27,6 +28,7 @@ from langchain_core.language_models._utils import (
|
||||
_filter_invocation_params_for_tracing,
|
||||
_normalize_messages,
|
||||
)
|
||||
from langchain_core.language_models.base import _get_langchain_version
|
||||
from langchain_core.language_models.chat_models import (
|
||||
SimpleChatModel,
|
||||
_generate_response_from_error,
|
||||
@@ -52,6 +54,7 @@ from langchain_core.tracers.context import collect_runs
|
||||
from langchain_core.tracers.event_stream import _AstreamEventsCallbackHandler
|
||||
from langchain_core.tracers.langchain import LangChainTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
from langchain_core.version import VERSION
|
||||
from tests.unit_tests.fake.callbacks import (
|
||||
BaseFakeCallbackHandler,
|
||||
FakeAsyncCallbackHandler,
|
||||
@@ -61,6 +64,7 @@ from tests.unit_tests.stubs import _any_id_ai_message, _any_id_ai_message_chunk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.outputs.llm_result import LLMResult
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
|
||||
|
||||
def _content_blocks_equal_ignore_id(
|
||||
@@ -1305,6 +1309,187 @@ def test_get_ls_params() -> None:
|
||||
assert ls_params["ls_stop"] == ["stop"]
|
||||
|
||||
|
||||
class _VersionedFakeModel(FakeListChatModel):
|
||||
"""Fake model that adds a version via `_add_version`."""
|
||||
|
||||
def model_post_init(self, _context: Any, /) -> None:
|
||||
super().model_post_init(_context)
|
||||
self._add_version("langchain-fake", "0.1.0")
|
||||
|
||||
|
||||
def test_user_versions_metadata_survives_merge() -> None:
|
||||
"""User-provided versions metadata should be deep-merged with model versions.
|
||||
|
||||
Regression test: the `add_metadata` deep-merge in `CallbackManager`
|
||||
must preserve both user-provided and model-provided versions dicts.
|
||||
"""
|
||||
llm = _VersionedFakeModel(responses=["hello"])
|
||||
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
|
||||
|
||||
with collect_runs() as cb:
|
||||
llm.invoke([HumanMessage(content="hi")], config=user_config)
|
||||
assert len(cb.traced_runs) == 1
|
||||
run_metadata = cb.traced_runs[0].extra["metadata"]
|
||||
assert "my-app" in run_metadata["versions"]
|
||||
assert run_metadata["versions"]["my-app"] == "2.0"
|
||||
assert "langchain-fake" in run_metadata["versions"]
|
||||
assert "langchain-core" in run_metadata["versions"]
|
||||
|
||||
|
||||
async def test_user_versions_metadata_survives_merge_async() -> None:
|
||||
"""Async variant: user-provided versions metadata deep-merged with model's."""
|
||||
llm = _VersionedFakeModel(responses=["hello"])
|
||||
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
|
||||
|
||||
with collect_runs() as cb:
|
||||
await llm.ainvoke([HumanMessage(content="hi")], config=user_config)
|
||||
assert len(cb.traced_runs) == 1
|
||||
run_metadata = cb.traced_runs[0].extra["metadata"]
|
||||
assert "my-app" in run_metadata["versions"]
|
||||
assert "langchain-fake" in run_metadata["versions"]
|
||||
assert "langchain-core" in run_metadata["versions"]
|
||||
|
||||
|
||||
def test_user_versions_metadata_survives_merge_stream() -> None:
|
||||
"""Stream variant: user-provided versions metadata deep-merged with model's."""
|
||||
llm = _VersionedFakeModel(responses=["hello"])
|
||||
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
|
||||
|
||||
with collect_runs() as cb:
|
||||
for _ in llm.stream([HumanMessage(content="hi")], config=user_config):
|
||||
pass
|
||||
assert len(cb.traced_runs) == 1
|
||||
run_metadata = cb.traced_runs[0].extra["metadata"]
|
||||
assert "my-app" in run_metadata["versions"]
|
||||
assert "langchain-fake" in run_metadata["versions"]
|
||||
assert "langchain-core" in run_metadata["versions"]
|
||||
|
||||
|
||||
async def test_user_versions_metadata_survives_merge_astream() -> None:
|
||||
"""Async stream variant: user-provided versions metadata deep-merged."""
|
||||
llm = _VersionedFakeModel(responses=["hello"])
|
||||
user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}
|
||||
|
||||
with collect_runs() as cb:
|
||||
async for _ in llm.astream([HumanMessage(content="hi")], config=user_config):
|
||||
pass
|
||||
assert len(cb.traced_runs) == 1
|
||||
run_metadata = cb.traced_runs[0].extra["metadata"]
|
||||
assert "my-app" in run_metadata["versions"]
|
||||
assert "langchain-fake" in run_metadata["versions"]
|
||||
assert "langchain-core" in run_metadata["versions"]
|
||||
|
||||
|
||||
def test_add_version_with_none_metadata() -> None:
|
||||
"""Model constructed with metadata=None should still get versions."""
|
||||
llm = FakeListChatModel(responses=["x"], metadata=None)
|
||||
assert llm.metadata is not None
|
||||
assert "langchain-core" in llm.metadata["versions"]
|
||||
|
||||
|
||||
def test_add_version_with_non_dict_versions() -> None:
|
||||
"""Non-dict `versions` value is silently replaced with a warning."""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
llm = FakeListChatModel(responses=["x"], metadata={"versions": "garbage"})
|
||||
assert any("expected a dict" in str(warning.message) for warning in w)
|
||||
assert llm.metadata is not None
|
||||
assert isinstance(llm.metadata["versions"], dict)
|
||||
assert "langchain-core" in llm.metadata["versions"]
|
||||
|
||||
|
||||
def test_langchain_version_in_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""When `langchain` is installed, its version appears in metadata."""
|
||||
|
||||
def _fake_version(pkg: str) -> str:
|
||||
if pkg == "langchain":
|
||||
return "1.2.13"
|
||||
raise PackageNotFoundError(pkg)
|
||||
|
||||
monkeypatch.setattr("importlib.metadata.version", _fake_version)
|
||||
_get_langchain_version.cache_clear()
|
||||
try:
|
||||
llm = FakeListChatModel(responses=["x"])
|
||||
assert llm.metadata is not None
|
||||
assert llm.metadata["versions"]["langchain"] == "1.2.13"
|
||||
assert "langchain-core" in llm.metadata["versions"]
|
||||
finally:
|
||||
_get_langchain_version.cache_clear()
|
||||
|
||||
|
||||
def test_langchain_version_missing_when_not_installed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When `langchain` is not installed, metadata.versions has no entry.
|
||||
|
||||
The lookup raises `PackageNotFoundError` (the real not-installed signal), which
|
||||
`_get_langchain_version` must swallow without dropping the rest of the dict.
|
||||
"""
|
||||
|
||||
def _raise_not_found(pkg: str) -> str:
|
||||
raise PackageNotFoundError(pkg)
|
||||
|
||||
monkeypatch.setattr("importlib.metadata.version", _raise_not_found)
|
||||
_get_langchain_version.cache_clear()
|
||||
try:
|
||||
llm = FakeListChatModel(responses=["x"])
|
||||
assert llm.metadata is not None
|
||||
assert "langchain" not in llm.metadata["versions"]
|
||||
assert "langchain-core" in llm.metadata["versions"]
|
||||
finally:
|
||||
_get_langchain_version.cache_clear()
|
||||
|
||||
|
||||
def test_version_validator_coexists_with_core_seed() -> None:
|
||||
"""A `model_validator(mode="after")` calling `_add_version` keeps the core seed.
|
||||
|
||||
Mirrors the real partner pattern (a validator, not a `model_post_init`
|
||||
override) and confirms the validator-added entry and the core-seeded
|
||||
`langchain-core` entry accumulate rather than clobber one another.
|
||||
"""
|
||||
|
||||
class _ValidatorVersionedModel(FakeListChatModel):
|
||||
@model_validator(mode="after")
|
||||
def _set_fake_version(self) -> Self:
|
||||
self._add_version("langchain-fake", "0.1.0")
|
||||
return self
|
||||
|
||||
llm = _ValidatorVersionedModel(responses=["x"])
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert versions["langchain-core"] == VERSION
|
||||
assert versions["langchain-fake"] == "0.1.0"
|
||||
|
||||
|
||||
def test_subclass_unique_validator_names_accumulate() -> None:
|
||||
"""Parent and child uniquely-named validators must both contribute entries.
|
||||
|
||||
Regression guard for the documented Pydantic footgun: same-named
|
||||
`model_validator` methods *replace* rather than chain across a class
|
||||
hierarchy. As long as each layer uses a unique validator name, a subclass
|
||||
must not silently drop its parent's version entry.
|
||||
"""
|
||||
|
||||
class _ParentModel(FakeListChatModel):
|
||||
@model_validator(mode="after")
|
||||
def _set_parent_version(self) -> Self:
|
||||
self._add_version("langchain-parent", "1.0.0")
|
||||
return self
|
||||
|
||||
class _ChildModel(_ParentModel):
|
||||
@model_validator(mode="after")
|
||||
def _set_child_version(self) -> Self:
|
||||
self._add_version("langchain-child", "2.0.0")
|
||||
return self
|
||||
|
||||
llm = _ChildModel(responses=["x"])
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert versions["langchain-core"] == VERSION
|
||||
assert versions["langchain-parent"] == "1.0.0"
|
||||
assert versions["langchain-child"] == "2.0.0"
|
||||
|
||||
|
||||
def test_model_profiles() -> None:
|
||||
model = GenericFakeChatModel(messages=iter([]))
|
||||
assert model.profile is None
|
||||
@@ -1592,6 +1777,7 @@ def test_invocation_params_passed_to_tracer_metadata() -> None:
|
||||
"ls_temperature": 0.7,
|
||||
"stop": None,
|
||||
"temperature": 0.7,
|
||||
"versions": {"langchain-core": VERSION},
|
||||
},
|
||||
"options": {"stop": None},
|
||||
"runtime": run.extra["runtime"],
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['foo'], i=1)",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'], i=1)",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
},
|
||||
@@ -128,7 +128,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['bar'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bar'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
},
|
||||
@@ -268,7 +268,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['foo'], i=1)",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'], i=1)",
|
||||
"name": "FakeListLLM"
|
||||
},
|
||||
"fallbacks": [
|
||||
@@ -281,7 +281,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['bar'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bar'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
],
|
||||
@@ -322,7 +322,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['foo'], i=1)",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'], i=1)",
|
||||
"name": "FakeListLLM"
|
||||
},
|
||||
"fallbacks": [
|
||||
@@ -335,7 +335,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['baz'], i=1)",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['baz'], i=1)",
|
||||
"name": "FakeListLLM"
|
||||
},
|
||||
{
|
||||
@@ -347,7 +347,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['bar'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bar'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['foo, bar'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
],
|
||||
@@ -227,7 +227,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['baz, qux'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['baz, qux'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
],
|
||||
@@ -346,7 +346,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['foo, bar'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
|
||||
"name": "FakeListChatModel"
|
||||
},
|
||||
{
|
||||
@@ -457,7 +457,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['baz, qux'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['baz, qux'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
],
|
||||
@@ -848,7 +848,7 @@
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"repr": "FakeStreamingListLLM(responses=['first item, second item, third item'])",
|
||||
"repr": "FakeStreamingListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['first item, second item, third item'])",
|
||||
"name": "FakeStreamingListLLM"
|
||||
},
|
||||
{
|
||||
@@ -884,7 +884,7 @@
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"repr": "FakeStreamingListLLM(responses=['this', 'is', 'a', 'test'])",
|
||||
"repr": "FakeStreamingListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['this', 'is', 'a', 'test'])",
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
@@ -1009,7 +1009,7 @@
|
||||
# name: test_prompt_with_chat_model
|
||||
'''
|
||||
ChatPromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, template='{question}'), additional_kwargs={})])
|
||||
| FakeListChatModel(responses=['foo'])
|
||||
| FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])
|
||||
'''
|
||||
# ---
|
||||
# name: test_prompt_with_chat_model.1
|
||||
@@ -1109,7 +1109,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['foo'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
},
|
||||
@@ -1220,7 +1220,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['foo, bar'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
],
|
||||
@@ -1249,7 +1249,7 @@
|
||||
# name: test_prompt_with_chat_model_async
|
||||
'''
|
||||
ChatPromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, template='{question}'), additional_kwargs={})])
|
||||
| FakeListChatModel(responses=['foo'])
|
||||
| FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])
|
||||
'''
|
||||
# ---
|
||||
# name: test_prompt_with_chat_model_async.1
|
||||
@@ -1349,7 +1349,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['foo'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
},
|
||||
@@ -1459,7 +1459,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['foo', 'bar'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo', 'bar'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
},
|
||||
@@ -1576,7 +1576,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['foo', 'bar'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo', 'bar'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
],
|
||||
@@ -1699,7 +1699,7 @@
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"repr": "FakeStreamingListLLM(responses=['bear, dog, cat', 'tomato, lettuce, onion'])",
|
||||
"repr": "FakeStreamingListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['bear, dog, cat', 'tomato, lettuce, onion'])",
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
],
|
||||
@@ -1867,7 +1867,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['4'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['4'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
},
|
||||
@@ -1940,7 +1940,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=['2'])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['2'])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
},
|
||||
@@ -13407,7 +13407,7 @@
|
||||
just_to_test_lambda: RunnableLambda(...)
|
||||
}
|
||||
| ChatPromptTemplate(input_variables=['documents', 'question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['documents', 'question'], input_types={}, partial_variables={}, template='Context:\n{documents}\n\nQuestion:\n{question}'), additional_kwargs={})])
|
||||
| FakeListChatModel(responses=['foo, bar'])
|
||||
| FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])
|
||||
| CommaSeparatedListOutputParser()
|
||||
'''
|
||||
# ---
|
||||
@@ -13610,7 +13610,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=['foo, bar'])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=['foo, bar'])",
|
||||
"name": "FakeListChatModel"
|
||||
}
|
||||
],
|
||||
@@ -13636,8 +13636,8 @@
|
||||
ChatPromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a nice assistant.'), additional_kwargs={}), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], input_types={}, partial_variables={}, template='{question}'), additional_kwargs={})])
|
||||
| RunnableLambda(...)
|
||||
| {
|
||||
chat: FakeListChatModel(responses=["i'm a chatbot"]),
|
||||
llm: FakeListLLM(responses=["i'm a textbot"])
|
||||
chat: FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=["i'm a chatbot"]),
|
||||
llm: FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=["i'm a textbot"])
|
||||
}
|
||||
'''
|
||||
# ---
|
||||
@@ -13762,7 +13762,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=[\"i'm a chatbot\"])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a chatbot\"])",
|
||||
"name": "FakeListChatModel"
|
||||
},
|
||||
"llm": {
|
||||
@@ -13774,7 +13774,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=[\"i'm a textbot\"])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a textbot\"])",
|
||||
"name": "FakeListLLM"
|
||||
}
|
||||
}
|
||||
@@ -13917,7 +13917,7 @@
|
||||
"fake_chat_models",
|
||||
"FakeListChatModel"
|
||||
],
|
||||
"repr": "FakeListChatModel(responses=[\"i'm a chatbot\"])",
|
||||
"repr": "FakeListChatModel(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a chatbot\"])",
|
||||
"name": "FakeListChatModel"
|
||||
},
|
||||
"kwargs": {
|
||||
@@ -13938,7 +13938,7 @@
|
||||
"fake",
|
||||
"FakeListLLM"
|
||||
],
|
||||
"repr": "FakeListLLM(responses=[\"i'm a textbot\"])",
|
||||
"repr": "FakeListLLM(metadata={'versions': {'langchain-core': '1.4.5'}}, responses=[\"i'm a textbot\"])",
|
||||
"name": "FakeListLLM"
|
||||
},
|
||||
"passthrough": {
|
||||
|
||||
@@ -17,6 +17,7 @@ from langchain_core.runnables import RunnableBinding, RunnablePassthrough
|
||||
from langchain_core.runnables.config import (
|
||||
RunnableConfig,
|
||||
_get_langsmith_inheritable_metadata_from_config,
|
||||
_merge_metadata_dicts,
|
||||
_set_config_context,
|
||||
ensure_config,
|
||||
merge_configs,
|
||||
@@ -322,3 +323,90 @@ async def test_run_in_executor() -> None:
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
await run_in_executor(None, raises_stop_iter)
|
||||
|
||||
|
||||
class TestMergeMetadataDicts:
|
||||
"""Tests for _merge_metadata_dicts deep-merge behavior."""
|
||||
|
||||
def test_deep_merge_preserves_both_nested_dicts(self) -> None:
|
||||
base = {"versions": {"langchain-core": "0.3.1"}, "user_id": "abc"}
|
||||
incoming = {"versions": {"langchain-anthropic": "1.3.3"}, "run": "x"}
|
||||
result = _merge_metadata_dicts(base, incoming)
|
||||
assert result == {
|
||||
"versions": {
|
||||
"langchain-core": "0.3.1",
|
||||
"langchain-anthropic": "1.3.3",
|
||||
},
|
||||
"user_id": "abc",
|
||||
"run": "x",
|
||||
}
|
||||
|
||||
def test_last_writer_wins_within_nested_dicts(self) -> None:
|
||||
base = {"versions": {"pkg": "1.0"}}
|
||||
incoming = {"versions": {"pkg": "2.0"}}
|
||||
result = _merge_metadata_dicts(base, incoming)
|
||||
assert result == {"versions": {"pkg": "2.0"}}
|
||||
|
||||
def test_non_dict_overwrites_dict(self) -> None:
|
||||
base = {"key": {"nested": "value"}}
|
||||
incoming = {"key": "flat"}
|
||||
result = _merge_metadata_dicts(base, incoming)
|
||||
assert result == {"key": "flat"}
|
||||
|
||||
def test_dict_overwrites_non_dict(self) -> None:
|
||||
base = {"key": "flat"}
|
||||
incoming = {"key": {"nested": "value"}}
|
||||
result = _merge_metadata_dicts(base, incoming)
|
||||
assert result == {"key": {"nested": "value"}}
|
||||
|
||||
def test_no_mutation_of_inputs(self) -> None:
|
||||
base = {"versions": {"a": "1"}}
|
||||
incoming = {"versions": {"b": "2"}}
|
||||
base_copy = {"versions": {"a": "1"}}
|
||||
incoming_copy = {"versions": {"b": "2"}}
|
||||
result = _merge_metadata_dicts(base, incoming)
|
||||
assert base == base_copy
|
||||
assert incoming == incoming_copy
|
||||
# Returned nested dicts should not share identity with originals.
|
||||
assert result["versions"] is not base["versions"]
|
||||
assert result["versions"] is not incoming["versions"]
|
||||
|
||||
def test_non_overlapping_nested_dict_is_copied(self) -> None:
|
||||
base = {"versions": {"a": "1"}, "extras": {"x": "y"}}
|
||||
incoming = {"versions": {"b": "2"}}
|
||||
result = _merge_metadata_dicts(base, incoming)
|
||||
# "extras" was not in incoming — result should still be a copy.
|
||||
assert result["extras"] is not base["extras"]
|
||||
assert result["extras"] == {"x": "y"}
|
||||
|
||||
def test_both_empty(self) -> None:
|
||||
assert _merge_metadata_dicts({}, {}) == {}
|
||||
|
||||
def test_empty_base(self) -> None:
|
||||
incoming = {"versions": {"pkg": "1.0"}}
|
||||
result = _merge_metadata_dicts({}, incoming)
|
||||
assert result == {"versions": {"pkg": "1.0"}}
|
||||
assert result["versions"] is not incoming["versions"]
|
||||
|
||||
result["versions"]["new"] = "2.0"
|
||||
assert incoming == {"versions": {"pkg": "1.0"}}
|
||||
|
||||
def test_empty_incoming(self) -> None:
|
||||
result = _merge_metadata_dicts({"versions": {"pkg": "1.0"}}, {})
|
||||
assert result == {"versions": {"pkg": "1.0"}}
|
||||
|
||||
def test_merge_configs_with_none_metadata(self) -> None:
|
||||
merged = merge_configs(
|
||||
cast("RunnableConfig", {"metadata": None}),
|
||||
{"metadata": {"versions": {"a": "1"}}},
|
||||
)
|
||||
assert merged["metadata"] == {"versions": {"a": "1"}}
|
||||
|
||||
def test_three_config_merge_accumulates(self) -> None:
|
||||
c1: RunnableConfig = {"metadata": {"versions": {"a": "1"}}}
|
||||
c2: RunnableConfig = {"metadata": {"versions": {"b": "2"}}}
|
||||
c3: RunnableConfig = {"metadata": {"versions": {"c": "3"}}}
|
||||
merged = merge_configs(c1, c2, c3)
|
||||
assert merged["metadata"] == {
|
||||
"versions": {"a": "1", "b": "2", "c": "3"},
|
||||
}
|
||||
@@ -91,6 +91,7 @@ from langchain_core.tracers import (
|
||||
from langchain_core.tracers._compat import pydantic_copy
|
||||
from langchain_core.tracers.context import collect_runs
|
||||
from langchain_core.utils.pydantic import PYDANTIC_VERSION
|
||||
from langchain_core.version import VERSION
|
||||
from tests.unit_tests.pydantic_utils import _normalize_schema, _schema
|
||||
from tests.unit_tests.stubs import AnyStr, _any_id_ai_message, _any_id_ai_message_chunk
|
||||
|
||||
@@ -2147,7 +2148,11 @@ async def test_prompt_with_llm(
|
||||
"value": {
|
||||
"end_time": None,
|
||||
"final_output": None,
|
||||
"metadata": {"ls_model_type": "llm", "ls_provider": "fakelist"},
|
||||
"metadata": {
|
||||
"ls_model_type": "llm",
|
||||
"ls_provider": "fakelist",
|
||||
"versions": {"langchain-core": VERSION},
|
||||
},
|
||||
"name": "FakeListLLM",
|
||||
"start_time": "2023-01-01T00:00:00.000+00:00",
|
||||
"streamed_output": [],
|
||||
@@ -2361,6 +2366,7 @@ async def test_prompt_with_llm_parser(
|
||||
"metadata": {
|
||||
"ls_model_type": "llm",
|
||||
"ls_provider": "fakestreaminglist",
|
||||
"versions": {"langchain-core": VERSION},
|
||||
},
|
||||
"name": "FakeStreamingListLLM",
|
||||
"start_time": "2023-01-01T00:00:00.000+00:00",
|
||||
|
||||
@@ -56,7 +56,7 @@ from langchain_core.utils.function_calling import (
|
||||
from langchain_core.utils.pydantic import is_basemodel_subclass
|
||||
from langchain_core.utils.utils import _build_model_kwargs
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing_extensions import NotRequired, Self, TypedDict
|
||||
|
||||
from langchain_anthropic import __version__
|
||||
from langchain_anthropic._client_utils import (
|
||||
@@ -1168,6 +1168,12 @@ class ChatAnthropic(BaseChatModel):
|
||||
all_required_field_names = get_pydantic_field_names(cls)
|
||||
return _build_model_kwargs(values, all_required_field_names)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_anthropic_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-anthropic", __version__)
|
||||
return self
|
||||
|
||||
def _resolve_model_profile(self) -> ModelProfile | None:
|
||||
profile = _get_default_model_profile(self.model) or None
|
||||
if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_anthropic/_version.py`. Intended for use as a
|
||||
pre-commit hook to prevent version mismatches.
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
@@ -23,6 +23,7 @@ from pydantic import BaseModel, Field, SecretStr, ValidationError
|
||||
from pytest import CaptureFixture, MonkeyPatch
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_anthropic._version import __version__
|
||||
from langchain_anthropic.chat_models import (
|
||||
_TOOL_CALL_ID_PATTERN,
|
||||
_create_usage_metadata,
|
||||
@@ -2131,6 +2132,8 @@ def test_anthropic_model_params() -> None:
|
||||
"ls_max_tokens": 64000,
|
||||
"ls_temperature": None,
|
||||
}
|
||||
assert llm.metadata is not None
|
||||
assert llm.metadata["versions"]["langchain-anthropic"] == __version__
|
||||
|
||||
ls_params = llm._get_ls_params(model=MODEL_NAME)
|
||||
assert ls_params.get("ls_model_name") == MODEL_NAME
|
||||
|
||||
@@ -55,6 +55,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_deepseek -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -62,6 +65,7 @@ check_imports: $(shell find langchain_deepseek -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
"""LangChain DeepSeek integration."""
|
||||
|
||||
from importlib import metadata
|
||||
|
||||
from langchain_deepseek._version import __version__
|
||||
from langchain_deepseek.chat_models import ChatDeepSeek
|
||||
|
||||
try:
|
||||
__version__ = metadata.version(__package__)
|
||||
except metadata.PackageNotFoundError:
|
||||
# Case where package metadata is not available.
|
||||
__version__ = ""
|
||||
del metadata # optional, avoids polluting the results of dir(__package__)
|
||||
|
||||
__all__ = [
|
||||
"ChatDeepSeek",
|
||||
"__version__",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-deepseek`."""
|
||||
|
||||
__version__ = "1.1.0"
|
||||
@@ -27,6 +27,7 @@ from langchain_openai.chat_models.base import BaseChatOpenAI
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_deepseek._version import __version__
|
||||
from langchain_deepseek.data._profiles import _PROFILES
|
||||
|
||||
DEFAULT_API_BASE = "https://api.deepseek.com/v1"
|
||||
@@ -224,6 +225,16 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
ls_params["ls_provider"] = "deepseek"
|
||||
return ls_params
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_deepseek_version(self) -> Self:
|
||||
"""Set package version in metadata.
|
||||
|
||||
Named uniquely to avoid shadowing `BaseChatOpenAI._set_openai_chat_version`;
|
||||
Pydantic replaces same-named validators rather than chaining them.
|
||||
"""
|
||||
self._add_version("langchain-deepseek", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate necessary environment vars and client params."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_deepseek/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_deepseek" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_deepseek/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_deepseek/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -437,3 +437,13 @@ def test_profile() -> None:
|
||||
model = ChatDeepSeek(model="deepseek-reasoner", api_key=SecretStr("test_key"))
|
||||
assert model.profile is not None
|
||||
assert model.profile["reasoning_output"]
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = ChatDeepSeek(model=MODEL_NAME, api_key=SecretStr("test_key"))
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-deepseek" in versions
|
||||
assert "langchain-openai" in versions
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Test `langchain_deepseek` public API surface."""
|
||||
|
||||
from langchain_deepseek import __all__
|
||||
|
||||
EXPECTED_ALL = ["__version__", "ChatDeepSeek"]
|
||||
|
||||
|
||||
def test_all_imports() -> None:
|
||||
"""Verify that `__all__` exports match the expected public API."""
|
||||
assert sorted(EXPECTED_ALL) == sorted(__all__)
|
||||
@@ -53,6 +53,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_fireworks -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -60,6 +63,7 @@ check_imports: $(shell find langchain_fireworks -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Fireworks AI integration for LangChain."""
|
||||
|
||||
from langchain_fireworks._version import __version__
|
||||
from langchain_fireworks.chat_models import ChatFireworks
|
||||
from langchain_fireworks.embeddings import FireworksEmbeddings
|
||||
from langchain_fireworks.llms import Fireworks
|
||||
from langchain_fireworks.version import __version__
|
||||
|
||||
__all__ = [
|
||||
"ChatFireworks",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-fireworks`."""
|
||||
|
||||
__version__ = "1.4.2"
|
||||
@@ -100,6 +100,7 @@ from pydantic import (
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_fireworks._compat import _convert_from_v1_to_chat_completions
|
||||
from langchain_fireworks._version import __version__
|
||||
from langchain_fireworks.data._profiles import _PROFILES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -827,6 +828,12 @@ class ChatFireworks(BaseChatModel):
|
||||
all_required_field_names = get_pydantic_field_names(cls)
|
||||
return _build_model_kwargs(values, all_required_field_names)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_fireworks_chat_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-fireworks", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate that api key and python package exists in environment."""
|
||||
|
||||
@@ -15,8 +15,9 @@ from langchain_core.language_models.llms import LLM
|
||||
from langchain_core.utils import get_pydantic_field_names
|
||||
from langchain_core.utils.utils import _build_model_kwargs, secret_from_env
|
||||
from pydantic import ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_fireworks.version import __version__
|
||||
from langchain_fireworks._version import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -98,6 +99,12 @@ class Fireworks(LLM):
|
||||
all_required_field_names = get_pydantic_field_names(cls)
|
||||
return _build_model_kwargs(values, all_required_field_names)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_fireworks_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-fireworks", __version__)
|
||||
return self
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
"""Return type of model."""
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
"""Main entrypoint into package."""
|
||||
"""Backwards-compatible re-export from `langchain_fireworks._version`."""
|
||||
|
||||
from importlib import metadata
|
||||
|
||||
try:
|
||||
__version__ = metadata.version(__package__)
|
||||
except metadata.PackageNotFoundError:
|
||||
# Case where package metadata is not available.
|
||||
__version__ = ""
|
||||
from langchain_fireworks._version import __version__ # noqa: F401
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_fireworks/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_fireworks" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_fireworks/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_fireworks/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -116,6 +117,16 @@ def test_convert_dict_to_message_without_reasoning_content() -> None:
|
||||
assert "reasoning_content" not in message.additional_kwargs
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
os.environ.setdefault("FIREWORKS_API_KEY", "fake-key")
|
||||
llm = ChatFireworks(model="accounts/fireworks/models/llama-v3-70b-instruct")
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-fireworks" in versions
|
||||
|
||||
|
||||
def test_format_message_content_passthrough_string() -> None:
|
||||
"""Plain string content is returned unchanged."""
|
||||
assert _format_message_content("hello") == "hello"
|
||||
|
||||
@@ -54,6 +54,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_groq -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -61,6 +64,7 @@ check_imports: $(shell find langchain_groq -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Groq integration for LangChain."""
|
||||
|
||||
from langchain_groq._version import __version__
|
||||
from langchain_groq.chat_models import ChatGroq
|
||||
from langchain_groq.version import __version__
|
||||
|
||||
__all__ = ["ChatGroq", "__version__"]
|
||||
__all__ = [
|
||||
"ChatGroq",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-groq`."""
|
||||
|
||||
__version__ = "1.1.3"
|
||||
@@ -71,8 +71,8 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_groq._compat import _convert_from_v1_to_groq
|
||||
from langchain_groq._version import __version__
|
||||
from langchain_groq.data._profiles import _PROFILES
|
||||
from langchain_groq.version import __version__
|
||||
|
||||
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
|
||||
_STRICT_STRUCTURED_OUTPUT_MODELS = frozenset(
|
||||
@@ -543,6 +543,12 @@ class ChatGroq(BaseChatModel):
|
||||
raise ImportError(msg) from exc
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_groq_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-groq", __version__)
|
||||
return self
|
||||
|
||||
def _resolve_model_profile(self) -> ModelProfile | None:
|
||||
return _get_default_model_profile(self.model_name) or None
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
"""Main entrypoint into package."""
|
||||
"""Backwards-compatible re-export from `langchain_groq._version`."""
|
||||
|
||||
from importlib import metadata
|
||||
|
||||
try:
|
||||
__version__ = metadata.version(__package__)
|
||||
except metadata.PackageNotFoundError:
|
||||
# Case where package metadata is not available.
|
||||
__version__ = ""
|
||||
from langchain_groq._version import __version__ # noqa: F401
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_groq/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_groq" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_groq/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_groq/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1095,3 +1095,12 @@ def test_format_message_content_mixed() -> None:
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,<data>"}},
|
||||
]
|
||||
assert expected == _format_message_content(content)
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = ChatGroq(model="foo") # type: ignore[call-arg]
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-groq" in versions
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all format lint type test tests integration_tests help extended_tests
|
||||
.PHONY: all format lint type test tests integration_tests help extended_tests check_version
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
@@ -55,13 +55,17 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_huggingface -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Hugging Face integration for LangChain."""
|
||||
|
||||
from langchain_huggingface._version import __version__
|
||||
from langchain_huggingface.chat_models import (
|
||||
ChatHuggingFace, # type: ignore[import-not-found]
|
||||
)
|
||||
@@ -18,4 +19,5 @@ __all__ = [
|
||||
"HuggingFaceEndpoint",
|
||||
"HuggingFaceEndpointEmbeddings",
|
||||
"HuggingFacePipeline",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-huggingface`."""
|
||||
|
||||
__version__ = "1.2.2"
|
||||
@@ -69,6 +69,7 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_huggingface._version import __version__
|
||||
from langchain_huggingface.data._profiles import _PROFILES
|
||||
from langchain_huggingface.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
from langchain_huggingface.llms.huggingface_pipeline import HuggingFacePipeline
|
||||
@@ -580,6 +581,12 @@ class ChatHuggingFace(BaseChatModel):
|
||||
):
|
||||
self.model_kwargs = self.llm.model_kwargs.copy()
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_huggingface_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-huggingface", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_llm(self) -> Self:
|
||||
if (
|
||||
|
||||
@@ -17,6 +17,8 @@ from langchain_core.utils import from_env, get_pydantic_field_names
|
||||
from pydantic import ConfigDict, Field, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_huggingface._version import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -249,6 +251,12 @@ class HuggingFaceEndpoint(LLM):
|
||||
raise ValueError(msg)
|
||||
return values
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_huggingface_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-huggingface", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate that package is installed and that the API token is valid."""
|
||||
|
||||
@@ -9,7 +9,9 @@ from langchain_core.callbacks import CallbackManagerForLLMRun
|
||||
from langchain_core.language_models.llms import BaseLLM
|
||||
from langchain_core.outputs import Generation, GenerationChunk, LLMResult
|
||||
from pydantic import ConfigDict, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_huggingface._version import __version__
|
||||
from langchain_huggingface.utils.import_utils import (
|
||||
IMPORT_ERROR,
|
||||
is_ipex_available,
|
||||
@@ -91,6 +93,12 @@ class HuggingFacePipeline(BaseLLM):
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_huggingface_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-huggingface", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def pre_init_validator(cls, values: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_huggingface/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_huggingface" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_huggingface/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_huggingface/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -329,6 +329,17 @@ def test_inheritance_with_empty_llm() -> None:
|
||||
assert chat.temperature is None
|
||||
|
||||
|
||||
def test_metadata_versions(chat_hugging_face: Any) -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
from langchain_huggingface._version import __version__
|
||||
|
||||
assert chat_hugging_face.metadata is not None
|
||||
versions = chat_hugging_face.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-huggingface" in versions
|
||||
assert versions["langchain-huggingface"] == __version__
|
||||
|
||||
|
||||
def test_profile() -> None:
|
||||
empty_llm = Mock(spec=HuggingFaceEndpoint)
|
||||
empty_llm.repo_id = "test/model"
|
||||
|
||||
@@ -56,6 +56,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_mistralai -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -63,6 +66,7 @@ check_imports: $(shell find langchain_mistralai -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"""Mistral AI integration for LangChain."""
|
||||
|
||||
from langchain_mistralai._version import __version__
|
||||
from langchain_mistralai.chat_models import ChatMistralAI
|
||||
from langchain_mistralai.embeddings import MistralAIEmbeddings
|
||||
|
||||
__all__ = ["ChatMistralAI", "MistralAIEmbeddings"]
|
||||
__all__ = [
|
||||
"ChatMistralAI",
|
||||
"MistralAIEmbeddings",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-mistralai`."""
|
||||
|
||||
__version__ = "1.1.5"
|
||||
@@ -78,6 +78,7 @@ from pydantic import (
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_mistralai._compat import _convert_from_v1_to_mistral
|
||||
from langchain_mistralai._version import __version__
|
||||
from langchain_mistralai.data._profiles import _PROFILES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -661,6 +662,12 @@ class ChatMistralAI(BaseChatModel):
|
||||
overall_token_usage[k] = v
|
||||
return {"token_usage": overall_token_usage, "model_name": self.model}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_mistralai_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-mistralai", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate api key, python package exists, temperature, and top_p."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_mistralai/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_mistralai" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_mistralai/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_mistralai/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -653,3 +653,12 @@ def test_no_duplicate_tool_calls_when_multiple_tools() -> None:
|
||||
def test_profile() -> None:
|
||||
model = ChatMistralAI(model="mistral-large-latest") # type: ignore[call-arg]
|
||||
assert model.profile
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = ChatMistralAI(model="foo") # type: ignore[call-arg]
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-mistralai" in versions
|
||||
@@ -1,6 +1,6 @@
|
||||
from langchain_mistralai import __all__
|
||||
|
||||
EXPECTED_ALL = ["ChatMistralAI", "MistralAIEmbeddings"]
|
||||
EXPECTED_ALL = ["__version__", "ChatMistralAI", "MistralAIEmbeddings"]
|
||||
|
||||
|
||||
def test_all_imports() -> None:
|
||||
|
||||
@@ -60,6 +60,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_ollama -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -67,6 +70,7 @@ check_imports: $(shell find langchain_ollama -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -2,39 +2,13 @@
|
||||
|
||||
Provides infrastructure for interacting with the [Ollama](https://ollama.com/)
|
||||
service.
|
||||
|
||||
!!! note
|
||||
**Newly added in 0.3.4:** `validate_model_on_init` param on all models.
|
||||
This parameter allows you to validate the model exists in Ollama locally on
|
||||
initialization. If set to `True`, it will raise an error if the model does not
|
||||
exist locally. This is useful for ensuring that the model is available before
|
||||
attempting to use it, especially in environments where models may not be
|
||||
pre-downloaded.
|
||||
|
||||
"""
|
||||
|
||||
from importlib import metadata
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from typing import NoReturn
|
||||
|
||||
from langchain_ollama._version import __version__
|
||||
from langchain_ollama.chat_models import ChatOllama
|
||||
from langchain_ollama.embeddings import OllamaEmbeddings
|
||||
from langchain_ollama.llms import OllamaLLM
|
||||
|
||||
|
||||
def _raise_package_not_found_error() -> NoReturn:
|
||||
raise PackageNotFoundError
|
||||
|
||||
|
||||
try:
|
||||
if __package__ is None:
|
||||
_raise_package_not_found_error()
|
||||
__version__ = metadata.version(__package__)
|
||||
except metadata.PackageNotFoundError:
|
||||
# Case where package metadata is not available.
|
||||
__version__ = ""
|
||||
del metadata # optional, avoids polluting the results of dir(__package__)
|
||||
|
||||
__all__ = [
|
||||
"ChatOllama",
|
||||
"OllamaEmbeddings",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-ollama`."""
|
||||
|
||||
__version__ = "1.1.0"
|
||||
@@ -95,6 +95,7 @@ from langchain_ollama._utils import (
|
||||
parse_url_with_auth,
|
||||
validate_model,
|
||||
)
|
||||
from langchain_ollama._version import __version__
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -921,6 +922,12 @@ class ChatOllama(BaseChatModel):
|
||||
)
|
||||
return schema
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_ollama_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-ollama", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_clients(self) -> Self:
|
||||
"""Set clients to use for ollama."""
|
||||
|
||||
@@ -20,6 +20,7 @@ from langchain_ollama._utils import (
|
||||
parse_url_with_auth,
|
||||
validate_model,
|
||||
)
|
||||
from langchain_ollama._version import __version__
|
||||
|
||||
|
||||
class OllamaLLM(BaseLLM):
|
||||
@@ -319,6 +320,12 @@ class OllamaLLM(BaseLLM):
|
||||
params["ls_max_tokens"] = max_tokens
|
||||
return params
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_ollama_llm_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-ollama", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_clients(self) -> Self:
|
||||
"""Set clients to use for ollama."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_ollama/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_ollama" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_ollama/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_ollama/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -814,6 +814,15 @@ def test_chat_ollama_ignores_strict_arg() -> None:
|
||||
assert "strict" not in call_kwargs
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = ChatOllama(model=MODEL_NAME)
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-ollama" in versions
|
||||
|
||||
|
||||
def test_chat_ollama_supports_response_format_json_schema() -> None:
|
||||
"""Test that ChatOllama correctly maps json_schema response_format to format."""
|
||||
with patch("langchain_ollama.chat_models.Client") as mock_client_class:
|
||||
|
||||
@@ -90,3 +90,12 @@ async def test_acreate_generate_stream_raises_when_client_none() -> None:
|
||||
with pytest.raises(RuntimeError, match="async client is not initialized"):
|
||||
async for _ in llm._acreate_generate_stream("Hello"):
|
||||
pass
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = OllamaLLM(model=MODEL_NAME)
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-ollama" in versions
|
||||
@@ -72,6 +72,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_openai -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -79,6 +82,7 @@ check_imports: $(shell find langchain_openai -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Module for OpenAI integrations."""
|
||||
|
||||
from langchain_openai._version import __version__
|
||||
from langchain_openai.chat_models import AzureChatOpenAI, ChatOpenAI
|
||||
from langchain_openai.chat_models._client_utils import StreamChunkTimeoutError
|
||||
from langchain_openai.embeddings import AzureOpenAIEmbeddings, OpenAIEmbeddings
|
||||
@@ -14,5 +15,6 @@ __all__ = [
|
||||
"OpenAI",
|
||||
"OpenAIEmbeddings",
|
||||
"StreamChunkTimeoutError",
|
||||
"__version__",
|
||||
"custom_tool",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-openai`."""
|
||||
|
||||
__version__ = "1.3.0"
|
||||
@@ -130,6 +130,7 @@ from pydantic import (
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_openai._version import __version__
|
||||
from langchain_openai.chat_models._client_utils import (
|
||||
_astream_with_chunk_timeout,
|
||||
_build_proxied_async_httpx_client,
|
||||
@@ -1136,6 +1137,20 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
|
||||
return values
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_openai_chat_version(self) -> Self:
|
||||
"""Set package version in metadata.
|
||||
|
||||
Note: Subclasses that inherit from `BaseChatOpenAI` (e.g.
|
||||
`ChatDeepSeek`, `ChatXAI`) must use a **unique** validator name
|
||||
(e.g. `_set_deepseek_version`) instead of overriding this one. Pydantic
|
||||
replaces same-named `model_validator` methods rather than chaining them,
|
||||
so reusing `_set_openai_chat_version` would silently drop the parent's
|
||||
`langchain-openai` version entry.
|
||||
"""
|
||||
self._add_version("langchain-openai", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate that api key and python package exists in environment."""
|
||||
|
||||
@@ -12,6 +12,7 @@ from langchain_core.utils import from_env, secret_from_env
|
||||
from pydantic import Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_openai._version import __version__
|
||||
from langchain_openai.llms.base import BaseOpenAI
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -117,6 +118,12 @@ class AzureOpenAI(BaseOpenAI):
|
||||
"""Return whether this model can be serialized by LangChain."""
|
||||
return True
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_azure_openai_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-openai", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate that api key and python package exists in environment."""
|
||||
|
||||
@@ -21,6 +21,7 @@ from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_fro
|
||||
from pydantic import ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_openai._version import __version__
|
||||
from langchain_openai.data._profiles import _PROFILES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -310,6 +311,12 @@ class BaseOpenAI(BaseLLM):
|
||||
all_required_field_names = get_pydantic_field_names(cls)
|
||||
return _build_model_kwargs(values, all_required_field_names)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_openai_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-openai", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate that api key and python package exists in environment."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_openai/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_openai" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_openai/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_openai/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -269,3 +269,16 @@ def test_max_tokens_converted_to_max_completion_tokens() -> None:
|
||||
assert "max_completion_tokens" in payload
|
||||
assert payload["max_completion_tokens"] == 1000
|
||||
assert "max_tokens" not in payload
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = AzureChatOpenAI( # type: ignore[call-arg]
|
||||
azure_deployment="35-turbo-dev",
|
||||
openai_api_version="2023-05-15",
|
||||
azure_endpoint="my-base-url",
|
||||
)
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-openai" in versions
|
||||
@@ -4077,6 +4077,15 @@ def test_context_overflow_error_backwards_compatibility() -> None:
|
||||
assert isinstance(exc_info.value, ContextOverflowError)
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = ChatOpenAI()
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-openai" in versions
|
||||
|
||||
|
||||
def test_get_request_payload_responses_api_input_file_blocks_passthrough() -> None:
|
||||
llm = ChatOpenAI(model="gpt-5", use_responses_api=True)
|
||||
payload = llm._get_request_payload(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Unit tests for `scripts/check_version.py`.
|
||||
|
||||
The version-consistency check is duplicated verbatim across partner packages, so
|
||||
exercising the parsing helpers here covers the shared logic. The script is loaded
|
||||
by path because `scripts/` is not an importable package.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
_SCRIPT_PATH = Path(__file__).parents[2] / "scripts" / "check_version.py"
|
||||
|
||||
|
||||
def _load_script() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("_check_version", _SCRIPT_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
check_version = _load_script()
|
||||
|
||||
|
||||
def test_get_pyproject_version_parses_version(tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text('[project]\nname = "x"\nversion = "1.2.3"\n', encoding="utf-8")
|
||||
assert check_version.get_pyproject_version(pyproject) == "1.2.3"
|
||||
|
||||
|
||||
def test_get_pyproject_version_missing_returns_none(tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text('[project]\nname = "x"\n', encoding="utf-8")
|
||||
assert check_version.get_pyproject_version(pyproject) is None
|
||||
|
||||
|
||||
def test_get_version_py_version_parses_version(tmp_path: Path) -> None:
|
||||
version_py = tmp_path / "_version.py"
|
||||
version_py.write_text('__version__ = "4.5.6"\n', encoding="utf-8")
|
||||
assert check_version.get_version_py_version(version_py) == "4.5.6"
|
||||
|
||||
|
||||
def test_get_version_py_version_missing_returns_none(tmp_path: Path) -> None:
|
||||
version_py = tmp_path / "_version.py"
|
||||
version_py.write_text('"""No version here."""\n', encoding="utf-8")
|
||||
assert check_version.get_version_py_version(version_py) is None
|
||||
|
||||
|
||||
def test_main_passes_for_real_package() -> None:
|
||||
"""The shipped `pyproject.toml` and `_version.py` must already agree."""
|
||||
assert check_version.main() == 0
|
||||
|
||||
|
||||
def test_main_reports_mismatch(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""A version mismatch must fail loudly with a non-zero exit code."""
|
||||
monkeypatch.setattr(check_version, "get_pyproject_version", lambda _: "1.0.0")
|
||||
monkeypatch.setattr(check_version, "get_version_py_version", lambda _: "2.0.0")
|
||||
assert check_version.main() == 1
|
||||
@@ -1,6 +1,7 @@
|
||||
from langchain_openai import __all__
|
||||
|
||||
EXPECTED_ALL = [
|
||||
"__version__",
|
||||
"OpenAI",
|
||||
"ChatOpenAI",
|
||||
"OpenAIEmbeddings",
|
||||
|
||||
@@ -55,6 +55,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_openrouter -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -62,6 +65,7 @@ check_imports: $(shell find langchain_openrouter -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""LangChain OpenRouter integration."""
|
||||
|
||||
from langchain_openrouter._version import __version__
|
||||
from langchain_openrouter.chat_models import ChatOpenRouter
|
||||
|
||||
__all__ = [
|
||||
"ChatOpenRouter",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-openrouter`."""
|
||||
|
||||
__version__ = "0.2.3"
|
||||
@@ -69,6 +69,7 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_openrouter._version import __version__
|
||||
from langchain_openrouter.data._profiles import _PROFILES
|
||||
|
||||
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
|
||||
@@ -410,6 +411,12 @@ class ChatOpenRouter(BaseChatModel):
|
||||
)
|
||||
return openrouter.OpenRouter(**client_kwargs)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_openrouter_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-openrouter", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate configuration and build the SDK client."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_openrouter/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_openrouter" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_openrouter/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_openrouter/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -274,6 +274,14 @@ class TestChatOpenRouterInstantiation:
|
||||
ls_params = model._get_ls_params()
|
||||
assert ls_params["ls_stop"] == ["END", "STOP"]
|
||||
|
||||
def test_metadata_versions(self) -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
model = _make_model()
|
||||
assert model.metadata is not None
|
||||
versions = model.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-openrouter" in versions
|
||||
|
||||
def test_client_created(self) -> None:
|
||||
"""Test that OpenRouter SDK client is created."""
|
||||
model = _make_model()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from langchain_openrouter import __all__
|
||||
|
||||
EXPECTED_ALL = [
|
||||
"__version__",
|
||||
"ChatOpenRouter",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all format lint type test tests integration_tests help extended_tests
|
||||
.PHONY: all format lint type test tests integration_tests help extended_tests check_version
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
@@ -54,13 +54,17 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_perplexity -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Perplexity AI integration for LangChain."""
|
||||
|
||||
from langchain_perplexity._version import __version__
|
||||
from langchain_perplexity.chat_models import ChatPerplexity
|
||||
from langchain_perplexity.embeddings import PerplexityEmbeddings
|
||||
from langchain_perplexity.output_parsers import (
|
||||
@@ -17,6 +18,7 @@ from langchain_perplexity.types import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"ChatPerplexity",
|
||||
"PerplexityEmbeddings",
|
||||
"PerplexitySearchRetriever",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-perplexity`."""
|
||||
|
||||
__version__ = "1.4.0"
|
||||
@@ -56,6 +56,7 @@ from perplexity import AsyncPerplexity, Perplexity
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_perplexity._version import __version__
|
||||
from langchain_perplexity.data._profiles import _PROFILES
|
||||
from langchain_perplexity.output_parsers import (
|
||||
ReasoningJsonOutputParser,
|
||||
@@ -839,6 +840,12 @@ class ChatPerplexity(BaseChatModel):
|
||||
values["model_kwargs"] = extra
|
||||
return values
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_perplexity_version(self) -> Self:
|
||||
"""Set package version in metadata."""
|
||||
self._add_version("langchain-perplexity", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_environment(self) -> Self:
|
||||
"""Validate that api key and python package exists in environment."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_perplexity/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_perplexity" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_perplexity/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_perplexity/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -223,6 +223,18 @@ def test_perplexity_invoke_includes_num_search_queries(mocker: MockerFixture) ->
|
||||
patcher.assert_called_once()
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
from langchain_perplexity._version import __version__
|
||||
|
||||
llm = ChatPerplexity(model="test")
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-perplexity" in versions
|
||||
assert versions["langchain-perplexity"] == __version__
|
||||
|
||||
|
||||
def test_profile() -> None:
|
||||
model = ChatPerplexity(model="sonar")
|
||||
assert model.profile
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from langchain_perplexity import __all__
|
||||
|
||||
EXPECTED_ALL = [
|
||||
"__version__",
|
||||
"ChatPerplexity",
|
||||
"PerplexityEmbeddings",
|
||||
"PerplexitySearchRetriever",
|
||||
|
||||
@@ -53,6 +53,9 @@ format format_diff:
|
||||
check_imports: $(shell find langchain_xai -name '*.py')
|
||||
$(UV_RUN_LINT) python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
@@ -60,6 +63,7 @@ check_imports: $(shell find langchain_xai -name '*.py')
|
||||
help:
|
||||
@echo '----'
|
||||
@echo 'check_imports - check imports'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'type - run type checking'
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""LangChain integration with xAI."""
|
||||
|
||||
from langchain_xai._version import __version__
|
||||
from langchain_xai.chat_models import ChatXAI
|
||||
|
||||
__all__ = ["ChatXAI"]
|
||||
__all__ = [
|
||||
"ChatXAI",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Version information for `langchain-xai`."""
|
||||
|
||||
__version__ = "1.2.2"
|
||||
@@ -12,6 +12,7 @@ from langchain_openai.chat_models.base import BaseChatOpenAI
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_xai._version import __version__
|
||||
from langchain_xai.data._profiles import _PROFILES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -468,6 +469,16 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override]
|
||||
params["ls_provider"] = "xai"
|
||||
return params
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_xai_version(self) -> Self:
|
||||
"""Set package version in metadata.
|
||||
|
||||
Named uniquely to avoid shadowing `BaseChatOpenAI._set_openai_chat_version`;
|
||||
Pydantic replaces same-named validators rather than chaining them.
|
||||
"""
|
||||
self._add_version("langchain-xai", __version__)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _warn_search_parameters_deprecated(self) -> Self:
|
||||
"""Emit deprecation warning if search_parameters (Live Search) is used."""
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Check version consistency between `pyproject.toml` and `_version.py`.
|
||||
|
||||
This script validates that the version defined in pyproject.toml matches the
|
||||
`__version__` variable in `langchain_xai/_version.py`. Intended for use as a
|
||||
CI check to prevent version mismatches.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_pyproject_version(pyproject_path: Path) -> str | None:
|
||||
"""Extract version from `pyproject.toml`."""
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_version_py_version(version_path: Path) -> str | None:
|
||||
"""Extract `__version__` from `_version.py`."""
|
||||
content = version_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^__version__\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Validate version consistency."""
|
||||
script_dir = Path(__file__).parent
|
||||
package_dir = script_dir.parent
|
||||
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
version_path = package_dir / "langchain_xai" / "_version.py"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: {pyproject_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
if not version_path.exists():
|
||||
print(f"Error: {version_path} not found") # noqa: T201
|
||||
return 1
|
||||
|
||||
pyproject_version = get_pyproject_version(pyproject_path)
|
||||
version_py_version = get_version_py_version(version_path)
|
||||
|
||||
if pyproject_version is None:
|
||||
print("Error: Could not find version in pyproject.toml") # noqa: T201
|
||||
return 1
|
||||
|
||||
if version_py_version is None:
|
||||
print("Error: Could not find __version__ in langchain_xai/_version.py") # noqa: T201
|
||||
return 1
|
||||
|
||||
if pyproject_version != version_py_version:
|
||||
print("Error: Version mismatch detected!") # noqa: T201
|
||||
print(f" pyproject.toml: {pyproject_version}") # noqa: T201
|
||||
print(f" langchain_xai/_version.py: {version_py_version}") # noqa: T201
|
||||
return 1
|
||||
|
||||
print(f"Version check passed: {pyproject_version}") # noqa: T201
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -161,3 +161,13 @@ def test_stream_usage_metadata() -> None:
|
||||
|
||||
model = ChatXAI(model=MODEL_NAME, stream_usage=False)
|
||||
assert model.stream_usage is False
|
||||
|
||||
|
||||
def test_metadata_versions() -> None:
|
||||
"""Test that metadata reports the correct version info."""
|
||||
llm = ChatXAI(model=MODEL_NAME)
|
||||
assert llm.metadata is not None
|
||||
versions = llm.metadata["versions"]
|
||||
assert "langchain-core" in versions
|
||||
assert "langchain-xai" in versions
|
||||
assert "langchain-openai" in versions
|
||||
@@ -1,6 +1,6 @@
|
||||
from langchain_xai import __all__
|
||||
|
||||
EXPECTED_ALL = ["ChatXAI"]
|
||||
EXPECTED_ALL = ["__version__", "ChatXAI"]
|
||||
|
||||
|
||||
def test_all_imports() -> None:
|
||||
|
||||
Reference in new issue
Block a user