From 2e087fb47e0bb54135b552e9732f2822b7502712 Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Wed, 29 Apr 2026 22:20:46 -0400 Subject: [PATCH] 0.4.1: align reference-doc surface with canonical public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation discoverability fixes only — no API changes for end users. The reference docs at reference.langchain.com/python/langchain-parallel were rendering pages under legacy class names (`ChatParallelWeb`, `ParallelWebSearchTool`) and at private module paths (`_types`, `_client`). The user-facing integration docs already promote the canonical names (`ChatParallel`, `ParallelSearchTool`), so the two sources drifted. This release reorganizes the source so the auto-generated reference picks up the canonical names. Changes: - `ChatParallel` is now the actual class definition; `ChatParallelWeb` is the back-compat alias (`ChatParallelWeb = ChatParallel`). Same flip for `ParallelSearchTool` / `ParallelWebSearchTool`. Both names continue to import and `isinstance(x, ChatParallelWeb)` continues to work — they remain the same class object. - `langchain_parallel._types` → `langchain_parallel.types`. The settings classes (`ExcerptSettings`, `FetchPolicy`, `FullContentSettings`, `SourcePolicy`) now live at a public module path. `_types` remains as a back-compat re-export shim. - `langchain_parallel._client` declares `__all__: list[str] = []` and a "private module" docstring so doc tooling treats it as internal. All 87 unit tests pass. ruff + mypy clean. --- CHANGELOG.md | 18 ++++ langchain_parallel/__init__.py | 12 +-- langchain_parallel/_client.py | 8 +- langchain_parallel/_types.py | 137 +++++--------------------- langchain_parallel/chat_models.py | 22 ++--- langchain_parallel/extract_tool.py | 2 +- langchain_parallel/retrievers.py | 2 +- langchain_parallel/search_tool.py | 18 ++-- langchain_parallel/types.py | 115 +++++++++++++++++++++ pyproject.toml | 2 +- tests/unit_tests/test_extract_tool.py | 2 +- tests/unit_tests/test_search_tool.py | 2 +- 12 files changed, 193 insertions(+), 147 deletions(-) create mode 100644 langchain_parallel/types.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 386d2c7..5a079a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.1] - 2026-04-29 + +Documentation discoverability fixes — no API changes for end users. The reference docs at `reference.langchain.com/python/langchain-parallel` were rendering pages under the legacy class names (`ChatParallelWeb`, `ParallelWebSearchTool`) and at private module paths (`_types`, `_client`); this release reorganizes the source so the auto-generated reference matches the canonical names used in user-facing docs. + +### Changed + +- **Canonical class definitions flipped.** `ChatParallel` is now the actual class definition; `ChatParallelWeb` is a back-compat alias (`ChatParallelWeb = ChatParallel`). Same flip for `ParallelSearchTool` / `ParallelWebSearchTool`. Both old and new names continue to import and `isinstance(x, ChatParallelWeb)` continues to work — they're the same class object. +- **`langchain_parallel._types` → `langchain_parallel.types`.** The settings classes (`ExcerptSettings`, `FetchPolicy`, `FullContentSettings`, `SourcePolicy`) now live at the public `types` module path; `_types` remains as a re-export shim so any code importing from the old path keeps working. +- **`langchain_parallel._client`** declares `__all__: list[str] = []` and a "private module" docstring so reference-doc tooling treats it as internal. No public API change — these helpers were never exported from the package root. + +### Migration + +No code changes required. The two flipped classes and the renamed settings module are fully backward compatible: + +- `from langchain_parallel import ChatParallelWeb` and `from langchain_parallel import ChatParallel` both work and resolve to the same class. +- `from langchain_parallel import ParallelWebSearchTool` and `from langchain_parallel import ParallelSearchTool` both work and resolve to the same class. +- `from langchain_parallel._types import ExcerptSettings` continues to work via the back-compat shim; new code should prefer `from langchain_parallel.types import ExcerptSettings` (or the package root: `from langchain_parallel import ExcerptSettings`). + ## [0.4.0] - 2026-04-28 This is a feature release covering Phase 2 of the modernization roadmap. Adds five new public surfaces (retriever, Task API, FindAll, Monitor, MCP toolkit) and removes the deprecation paths that 0.3.0 introduced. diff --git a/langchain_parallel/__init__.py b/langchain_parallel/__init__.py index 0c194d0..f360df3 100644 --- a/langchain_parallel/__init__.py +++ b/langchain_parallel/__init__.py @@ -1,11 +1,5 @@ from importlib import metadata -from langchain_parallel._types import ( - ExcerptSettings, - FetchPolicy, - FullContentSettings, - SourcePolicy, -) from langchain_parallel.chat_models import ChatParallel, ChatParallelWeb from langchain_parallel.extract_tool import ParallelExtractTool from langchain_parallel.findall import ( @@ -26,6 +20,12 @@ parse_basis, verify_webhook, ) +from langchain_parallel.types import ( + ExcerptSettings, + FetchPolicy, + FullContentSettings, + SourcePolicy, +) try: __version__ = metadata.version(__package__ or __name__) diff --git a/langchain_parallel/_client.py b/langchain_parallel/_client.py index 691e461..ab85b54 100644 --- a/langchain_parallel/_client.py +++ b/langchain_parallel/_client.py @@ -1,4 +1,8 @@ -"""Client utilities for Parallel integration.""" +"""Internal client utilities for Parallel integration. + +Private module. Helpers in here are not part of the public API and may +change without notice; import from :mod:`langchain_parallel` instead. +""" from __future__ import annotations @@ -8,6 +12,8 @@ import openai from parallel import AsyncParallel, Parallel +__all__: list[str] = [] + def get_api_key(api_key: Optional[str] = None) -> str: """Retrieve the Parallel API key from argument or environment variables. diff --git a/langchain_parallel/_types.py b/langchain_parallel/_types.py index c5c863a..87e3ccd 100644 --- a/langchain_parallel/_types.py +++ b/langchain_parallel/_types.py @@ -1,115 +1,22 @@ -"""Common types for Parallel API.""" - -from __future__ import annotations - -import datetime as _dt -from typing import Optional, Union - -from pydantic import BaseModel, Field, field_validator, model_validator - - -class ExcerptSettings(BaseModel): - """Settings for excerpt extraction.""" - - max_chars_per_result: Optional[int] = Field( - default=None, - description=( - "Optional upper bound on the total number of characters to include " - "per url. Excerpts may contain fewer characters than this limit to " - "maximize relevance and token efficiency." - ), - ) - - -class FullContentSettings(BaseModel): - """Settings for full content extraction.""" - - max_chars_per_result: Optional[int] = Field( - default=None, - description=( - "Optional limit on the number of characters to include in the full " - "content for each url. Full content always starts at the beginning " - "of the page and is truncated at the limit if necessary." - ), - ) - - -class FetchPolicy(BaseModel): - """Fetch policy for cache vs live content.""" - - max_age_seconds: Optional[int] = Field( - default=None, - ge=600, - description=( - "If cached content is older than this, fetch fresh content from the " - "source. Minimum 600 seconds (10 minutes); if not provided, the API " - "uses a dynamic age policy." - ), - ) - timeout_seconds: Optional[float] = Field( - default=None, - description=( - "Timeout in seconds for fetching live content if unavailable in cache. " - "If unspecified, dynamic timeout will be used (15-60 seconds)." - ), - ) - disable_cache_fallback: bool = Field( - default=False, - description=( - "If false, fallback to cached content older than max-age if live " - "fetch fails or times out. If true, returns an error instead." - ), - ) - - -class SourcePolicy(BaseModel): - """Domain allow/deny lists and freshness floor for web research. - - Apex-domain semantics: include `nature.com`, not `https://www.nature.com`. - Wildcards permitted (e.g. `.org`). - """ - - include_domains: Optional[list[str]] = Field( - default=None, - max_length=200, - description=( - "If provided, only sources from these apex domains are returned. " - "Combined include + exclude lists are capped at 200 domains." - ), - ) - exclude_domains: Optional[list[str]] = Field( - default=None, - max_length=200, - description="If provided, sources from these apex domains are excluded.", - ) - after_date: Optional[Union[_dt.date, str]] = Field( - default=None, - description=( - "ISO date (YYYY-MM-DD). Only return sources published on or after " - "this date." - ), - ) - - @field_validator("after_date", mode="before") - @classmethod - def _parse_after_date(cls, v: object) -> object: - if v is None or isinstance(v, _dt.date): - return v - if isinstance(v, str): - try: - return _dt.date.fromisoformat(v) - except ValueError as e: - msg = f"after_date must be ISO YYYY-MM-DD; got {v!r} ({e!s})." - raise ValueError(msg) from e - return v - - @model_validator(mode="after") - def _check_domain_total(self) -> SourcePolicy: - total = len(self.include_domains or []) + len(self.exclude_domains or []) - if total > 200: - msg = ( - f"Combined include_domains + exclude_domains has {total} entries; " - f"the API caps the total at 200." - ) - raise ValueError(msg) - return self +"""Back-compat shim for the renamed :mod:`langchain_parallel.types` module. + +The settings classes (``ExcerptSettings``, ``FetchPolicy``, ``FullContentSettings``, +``SourcePolicy``) live in :mod:`langchain_parallel.types` as of 0.4.1. This shim +re-exports them so any code that imported from ``langchain_parallel._types`` +keeps working; new code should import from ``langchain_parallel.types`` (or +the package root) directly. +""" + +from langchain_parallel.types import ( + ExcerptSettings, + FetchPolicy, + FullContentSettings, + SourcePolicy, +) + +__all__ = [ + "ExcerptSettings", + "FetchPolicy", + "FullContentSettings", + "SourcePolicy", +] diff --git a/langchain_parallel/chat_models.py b/langchain_parallel/chat_models.py index 8441554..22dc2fc 100644 --- a/langchain_parallel/chat_models.py +++ b/langchain_parallel/chat_models.py @@ -1,6 +1,6 @@ -"""Parallel Web chat model integration. +"""ChatParallel chat model integration. -This module provides the ChatParallelWeb class for interacting with Parallel's +This module provides the ChatParallel class for interacting with Parallel's Chat API through an OpenAI-compatible interface. """ @@ -165,8 +165,8 @@ def _merge_consecutive_messages(messages: list[BaseMessage]) -> list[BaseMessage return merged -class ChatParallelWeb(BaseChatModel): - """Parallel Web chat model integration. +class ChatParallel(BaseChatModel): + """ChatParallel chat model integration. This integration connects to Parallel's Chat API, which provides real-time web research capabilities through an OpenAI-compatible interface. @@ -201,9 +201,9 @@ class ChatParallelWeb(BaseChatModel): Instantiate: ```python - from langchain_parallel import ChatParallelWeb + from langchain_parallel import ChatParallel - llm = ChatParallelWeb( + llm = ChatParallel( model="speed", temperature=0.7, max_tokens=None, @@ -664,7 +664,7 @@ def with_structured_output( f"Structured output requires one of the research models " f"({sorted(_STRUCTURED_OUTPUT_MODELS)}); the '{self.model}' " f"model silently ignores response_format. Re-instantiate with " - f"`ChatParallelWeb(model='lite' | 'base' | 'core')`." + f"`ChatParallel(model='lite' | 'base' | 'core')`." ) raise ValueError(msg) if method == "function_calling": @@ -734,8 +734,8 @@ def _parse_with_capture(raw: AIMessage) -> dict[str, Any]: return bound | output_parser -#: Forward-compat alias for :class:`ChatParallelWeb`. +#: Back-compat alias for :class:`ChatParallel`. #: -#: Prefer ChatParallel in new code; ChatParallelWeb will continue to -#: work indefinitely as an alias for this class. -ChatParallel = ChatParallelWeb +#: ``ChatParallelWeb`` is the legacy class name and continues to work +#: indefinitely; new code should prefer ``ChatParallel``. +ChatParallelWeb = ChatParallel diff --git a/langchain_parallel/extract_tool.py b/langchain_parallel/extract_tool.py index a407986..a8d86f4 100644 --- a/langchain_parallel/extract_tool.py +++ b/langchain_parallel/extract_tool.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, Field, SecretStr, model_validator from ._client import get_api_key, get_async_parallel_client, get_parallel_client -from ._types import ExcerptSettings, FetchPolicy, FullContentSettings +from .types import ExcerptSettings, FetchPolicy, FullContentSettings def _coerce_full_content( diff --git a/langchain_parallel/retrievers.py b/langchain_parallel/retrievers.py index 7833027..73ec1b8 100644 --- a/langchain_parallel/retrievers.py +++ b/langchain_parallel/retrievers.py @@ -14,7 +14,7 @@ from pydantic import Field, SecretStr, model_validator from ._client import get_api_key, get_async_parallel_client, get_parallel_client -from ._types import ExcerptSettings, FetchPolicy, SourcePolicy +from .types import ExcerptSettings, FetchPolicy, SourcePolicy def _join_excerpts(excerpts: Optional[list[str]]) -> str: diff --git a/langchain_parallel/search_tool.py b/langchain_parallel/search_tool.py index e7cb2d9..47ee4b0 100644 --- a/langchain_parallel/search_tool.py +++ b/langchain_parallel/search_tool.py @@ -14,7 +14,7 @@ from pydantic import BaseModel, Field, SecretStr, field_validator, model_validator from ._client import get_api_key, get_async_parallel_client, get_parallel_client -from ._types import ExcerptSettings, FetchPolicy, SourcePolicy +from .types import ExcerptSettings, FetchPolicy, SourcePolicy _VALID_MODES = {"basic", "advanced"} @@ -180,7 +180,7 @@ def _check_query_length(cls, qs: list[str]) -> list[str]: ) -class ParallelWebSearchTool(BaseTool): +class ParallelSearchTool(BaseTool): """Parallel Search tool with web research capabilities. This tool calls Parallel's Search API, which streamlines the traditional @@ -206,9 +206,9 @@ class ParallelWebSearchTool(BaseTool): Instantiation: ```python - from langchain_parallel import ParallelWebSearchTool + from langchain_parallel import ParallelSearchTool - tool = ParallelWebSearchTool() + tool = ParallelSearchTool() ``` Invocation: @@ -292,7 +292,7 @@ class ParallelWebSearchTool(BaseTool): """Asynchronous Parallel SDK client (initialized after validation).""" @model_validator(mode="after") - def validate_environment(self) -> ParallelWebSearchTool: + def validate_environment(self) -> ParallelSearchTool: """Validate the environment and initialize SDK clients.""" api_key_str = get_api_key( self.api_key.get_secret_value() if self.api_key else None, @@ -534,8 +534,8 @@ async def _arun( return response -#: Forward-compat alias for :class:`ParallelWebSearchTool`. +#: Back-compat alias for :class:`ParallelSearchTool`. #: -#: Prefer ParallelSearchTool in new code; ParallelWebSearchTool will -#: continue to work indefinitely as an alias for this class. -ParallelSearchTool = ParallelWebSearchTool +#: ``ParallelWebSearchTool`` is the legacy class name and continues to +#: work indefinitely; new code should prefer ``ParallelSearchTool``. +ParallelWebSearchTool = ParallelSearchTool diff --git a/langchain_parallel/types.py b/langchain_parallel/types.py new file mode 100644 index 0000000..c5c863a --- /dev/null +++ b/langchain_parallel/types.py @@ -0,0 +1,115 @@ +"""Common types for Parallel API.""" + +from __future__ import annotations + +import datetime as _dt +from typing import Optional, Union + +from pydantic import BaseModel, Field, field_validator, model_validator + + +class ExcerptSettings(BaseModel): + """Settings for excerpt extraction.""" + + max_chars_per_result: Optional[int] = Field( + default=None, + description=( + "Optional upper bound on the total number of characters to include " + "per url. Excerpts may contain fewer characters than this limit to " + "maximize relevance and token efficiency." + ), + ) + + +class FullContentSettings(BaseModel): + """Settings for full content extraction.""" + + max_chars_per_result: Optional[int] = Field( + default=None, + description=( + "Optional limit on the number of characters to include in the full " + "content for each url. Full content always starts at the beginning " + "of the page and is truncated at the limit if necessary." + ), + ) + + +class FetchPolicy(BaseModel): + """Fetch policy for cache vs live content.""" + + max_age_seconds: Optional[int] = Field( + default=None, + ge=600, + description=( + "If cached content is older than this, fetch fresh content from the " + "source. Minimum 600 seconds (10 minutes); if not provided, the API " + "uses a dynamic age policy." + ), + ) + timeout_seconds: Optional[float] = Field( + default=None, + description=( + "Timeout in seconds for fetching live content if unavailable in cache. " + "If unspecified, dynamic timeout will be used (15-60 seconds)." + ), + ) + disable_cache_fallback: bool = Field( + default=False, + description=( + "If false, fallback to cached content older than max-age if live " + "fetch fails or times out. If true, returns an error instead." + ), + ) + + +class SourcePolicy(BaseModel): + """Domain allow/deny lists and freshness floor for web research. + + Apex-domain semantics: include `nature.com`, not `https://www.nature.com`. + Wildcards permitted (e.g. `.org`). + """ + + include_domains: Optional[list[str]] = Field( + default=None, + max_length=200, + description=( + "If provided, only sources from these apex domains are returned. " + "Combined include + exclude lists are capped at 200 domains." + ), + ) + exclude_domains: Optional[list[str]] = Field( + default=None, + max_length=200, + description="If provided, sources from these apex domains are excluded.", + ) + after_date: Optional[Union[_dt.date, str]] = Field( + default=None, + description=( + "ISO date (YYYY-MM-DD). Only return sources published on or after " + "this date." + ), + ) + + @field_validator("after_date", mode="before") + @classmethod + def _parse_after_date(cls, v: object) -> object: + if v is None or isinstance(v, _dt.date): + return v + if isinstance(v, str): + try: + return _dt.date.fromisoformat(v) + except ValueError as e: + msg = f"after_date must be ISO YYYY-MM-DD; got {v!r} ({e!s})." + raise ValueError(msg) from e + return v + + @model_validator(mode="after") + def _check_domain_total(self) -> SourcePolicy: + total = len(self.include_domains or []) + len(self.exclude_domains or []) + if total > 200: + msg = ( + f"Combined include_domains + exclude_domains has {total} entries; " + f"the API caps the total at 200." + ) + raise ValueError(msg) + return self diff --git a/pyproject.toml b/pyproject.toml index f293bc8..aad706a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "langchain-parallel" -version = "0.4.0" +version = "0.4.1" description = "A LangChain integration for Parallel Web AI services, including Chat and Search." authors = ["Parallel Team "] readme = "README.md" diff --git a/tests/unit_tests/test_extract_tool.py b/tests/unit_tests/test_extract_tool.py index 571305f..5cb5936 100644 --- a/tests/unit_tests/test_extract_tool.py +++ b/tests/unit_tests/test_extract_tool.py @@ -7,8 +7,8 @@ import pytest -from langchain_parallel._types import ExcerptSettings, FetchPolicy, FullContentSettings from langchain_parallel.extract_tool import ParallelExtractTool +from langchain_parallel.types import ExcerptSettings, FetchPolicy, FullContentSettings def _make_response(payload: dict) -> SimpleNamespace: diff --git a/tests/unit_tests/test_search_tool.py b/tests/unit_tests/test_search_tool.py index dd44991..60bfc70 100644 --- a/tests/unit_tests/test_search_tool.py +++ b/tests/unit_tests/test_search_tool.py @@ -7,8 +7,8 @@ import pytest -from langchain_parallel._types import ExcerptSettings, FetchPolicy, SourcePolicy from langchain_parallel.search_tool import ParallelWebSearchTool, _validate_mode +from langchain_parallel.types import ExcerptSettings, FetchPolicy, SourcePolicy def _make_response(payload: dict) -> SimpleNamespace: