Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/scripts/inject_uv_overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Point a downstream uv project at locally built uipath* wheels.

Appends ``tool.uv.override-dependencies`` entries for every ``uipath*`` wheel
found under ``$WHEELS_DIR`` (default: ``./wheels``) to the ``pyproject.toml``
given as ``argv[1]``. uv overrides bypass version specifiers, so a downstream
cap like ``uipath-langchain-client<1.19.0`` cannot mask the new code, and they
apply to project commands (``uv sync``/``uv add``) where the ``UV_OVERRIDE``
env var is silently ignored.

An override replaces the whole requirement, dropping any extras the downstream
graph asked for (e.g. ``uipath-langchain-client[openai]``), so EXTRAS pins the
union of extras each overridden package must keep providing.
"""

import glob
import os
import pathlib
import re
import sys

EXTRAS: dict[str, str] = {
"uipath-langchain-client": "all",
}


def main() -> None:
pyproject = pathlib.Path(sys.argv[1])
wheels = pathlib.Path(os.environ.get("WHEELS_DIR", "wheels")).resolve()

entries: list[str] = []
for whl in sorted(glob.glob(str(wheels / "**" / "*.whl"), recursive=True)):
# Wheel filename is ``{distribution}-{version}-...whl`` where the
# distribution escapes hyphens to underscores (uipath_llm_client ->
# uipath-llm-client).
dist = pathlib.Path(whl).name.split("-", 1)[0].replace("_", "-")
if not dist.startswith("uipath"):
continue
extra = f"[{EXTRAS[dist]}]" if dist in EXTRAS else ""
entries.append(f' "{dist}{extra} @ {pathlib.Path(whl).as_uri()}",')

if not entries:
raise SystemExit(f"no uipath wheels found under {wheels}")

block = "override-dependencies = [\n" + "\n".join(entries) + "\n]\n"
text = pyproject.read_text()
if re.search(r"^\[tool\.uv\]$", text, flags=re.M):
text = re.sub(r"^\[tool\.uv\]\n", "[tool.uv]\n" + block, text, count=1, flags=re.M)
else:
text = text.rstrip() + "\n\n[tool.uv]\n" + block
pyproject.write_text(text)
print(f"{pyproject}:\n{block}")


if __name__ == "__main__":
main()
148 changes: 148 additions & 0 deletions .github/workflows/test-downstream-langchain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Downstream gate: run uipath-langchain-python's tests against wheels built
# from this PR, so a client change that breaks the downstream repo cannot merge
# (and therefore cannot auto-publish to PyPI via cd.yml / cd-langchain.yml).
#
# Requires repo secrets ALPHA_TEST_CLIENT_ID / ALPHA_TEST_CLIENT_SECRET /
# ALPHA_BASE_URL (same values as uipath-langchain-python's integration tests).
# The `skip:downstream` label bypasses the gate for emergencies (e.g. the
# downstream main is broken for unrelated reasons).
name: Downstream integration

on:
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: downstream-${{ github.ref }}
cancel-in-progress: true

env:
DOWNSTREAM_REPO: UiPath/uipath-langchain-python
DOWNSTREAM_REF: main

jobs:
detect-changes:
runs-on: uipath-ubuntu-latest
outputs:
run_downstream: ${{ steps.detect.outputs.changed }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0

- name: Detect package source changes
id: detect
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
changed=$(git diff --name-only "$BASE_SHA...$HEAD_SHA" \
| grep -cE '^(src/|packages/uipath_langchain_client/(src/|pyproject\.toml)|pyproject\.toml|uv\.lock)' \
|| true)
echo "changed=$([ "$changed" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "Package source files changed: $changed"

build-wheels:
needs: detect-changes
if: needs.detect-changes.outputs.run_downstream == 'true' && !contains(github.event.pull_request.labels.*.name, 'skip:downstream')
runs-on: uipath-ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: "0.9.27"

- name: Build wheels from the PR
run: |
uv build --out-dir wheels
uv build --package uipath-langchain-client --out-dir wheels

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: client-wheels
path: wheels/*.whl

integration-tests:
needs: build-wheels
runs-on: uipath-ubuntu-latest
timeout-minutes: 10
container:
image: ghcr.io/astral-sh/uv:python3.12-bookworm
env:
UIPATH_JOB_KEY: "3a03d5cb-fa21-4021-894d-a8e2eda0afe0"
UIPATH_TRACING_ENABLED: false
strategy:
fail-fast: false
matrix:
# Only the testcases that exercise the LLM client; the downstream repo's
# own CI covers the SDK-scaffolding testcases and the full env matrix.
testcase: [chat-models, multimodal-invoke]
environment: [alpha]

name: "${{ matrix.testcase }} / ${{ matrix.environment }}"

steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
path: client
sparse-checkout: .github/scripts

- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
repository: ${{ env.DOWNSTREAM_REPO }}
ref: ${{ env.DOWNSTREAM_REF }}
path: downstream

- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: client-wheels
path: wheels

- name: Point testcase at the PR wheels
run: |
python3 client/.github/scripts/inject_uv_overrides.py \
"downstream/testcases/${{ matrix.testcase }}/pyproject.toml"

- name: Run testcase
working-directory: downstream/testcases/${{ matrix.testcase }}
env:
CLIENT_ID: ${{ secrets.ALPHA_TEST_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.ALPHA_TEST_CLIENT_SECRET }}
BASE_URL: ${{ secrets.ALPHA_BASE_URL }}
run: |
# With empty credentials `uipath auth` falls back to the interactive
# browser flow and hangs until the job timeout - fail fast instead.
# POSIX sh, not bash: container-job steps run under `sh -e`.
if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ] || [ -z "$BASE_URL" ]; then
echo "::error::ALPHA_TEST_CLIENT_ID / ALPHA_TEST_CLIENT_SECRET / ALPHA_BASE_URL secrets are not configured on this repository. Copy them from uipath-langchain-python's integration setup."
exit 1
fi
echo "Running testcase: ${{ matrix.testcase }} against ${{ matrix.environment }}"
bash run.sh
bash ../common/validate_output.sh

downstream-gate:
needs: [detect-changes, integration-tests]
if: always()
runs-on: uipath-ubuntu-latest
steps:
- name: Evaluate gate
run: |
if [[ "${{ needs.detect-changes.outputs.run_downstream }}" != "true" ]]; then
echo "No package source changed - downstream tests not required."
exit 0
fi
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'skip:downstream') }}" == "true" ]]; then
echo "skip:downstream label present - gate bypassed."
exit 0
fi
if [[ "${{ needs.integration-tests.result }}" == "success" ]]; then
echo "Downstream green."
exit 0
fi
echo "Downstream tests failed - this PR would break uipath-langchain-python."
exit 1
6 changes: 6 additions & 0 deletions packages/uipath_langchain_client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to `uipath_langchain_client` will be documented in this file.

## [1.18.4] - 2026-09-02

### Fixed
- `UiPathChatAnthropic._create`/`._acreate` now return raw responses (`with_raw_response`) to match langchain-anthropic 1.7.0, which calls `.parse()` on the result. Lifts the interim `<1.7.0` pin from 1.18.2: the `langchain-anthropic` bound on the `anthropic` and `bedrock` extras is now `>=1.7.0,<2.0.0`.
- `UiPathChatLiteLLM.stream()`/`.astream()` no longer silently fall back to non-streaming calls on langchain-core >= 1.4, which treats the `streaming=False` that ChatLiteLLM's validator marks as explicitly set as a hard streaming opt-out.

## [1.18.3] - 2026-09-02

### Added
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath_langchain_client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ google = [
"langchain-google-genai>=4.2.2,<5.0.0",
]
anthropic = [
"langchain-anthropic>=1.4.1,<1.7.0",
"langchain-anthropic>=1.7.0,<2.0.0",
"anthropic[bedrock,vertex]>=0.96.0,<1.0.0",
]
bedrock = [
"langchain-aws[anthropic]>=1.4.5,<2.0.0",
"langchain-anthropic>=1.4.1,<1.7.0",
"langchain-anthropic>=1.7.0,<2.0.0",
"anthropic[bedrock]>=0.96.0,<1.0.0",
]
vertexai = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "UiPath LangChain Client"
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
__version__ = "1.18.3"
__version__ = "1.18.4"
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,18 @@ def _async_anthropic_client(
case _:
raise ValueError("Anthropic models are currently not hosted on any other provider")

# langchain-anthropic >= 1.7.0 expects _create/_acreate to return a raw-response
# wrapper (it calls .parse() on the result), so route through with_raw_response.
@override
def _create(self, payload: dict[str, Any]) -> Any:
if "betas" in payload:
return self._anthropic_client.beta.messages.create(**payload)
return self._anthropic_client.messages.create(**payload)
return self._anthropic_client.beta.messages.with_raw_response.create(**payload)
return self._anthropic_client.messages.with_raw_response.create(**payload)

@override
async def _acreate(self, payload: dict[str, Any]) -> Any:
if "betas" in payload:
return await self._async_anthropic_client.beta.messages.create(**payload)
return await self._async_anthropic_client.messages.create(**payload)
return await self._async_anthropic_client.beta.messages.with_raw_response.create(
**payload
)
return await self._async_anthropic_client.messages.with_raw_response.create(**payload)
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ class UiPathChatLiteLLM(UiPathBaseChatModel, ChatLiteLLM): # type: ignore[overr
vendor_type: VendorType | str | None = Field(default=None, exclude=True)
api_flavor: ApiFlavor | str | None = Field(default=None, exclude=True)

def __init__(self, **kwargs: Any) -> None:
streaming_explicitly_set = "streaming" in kwargs
super().__init__(**kwargs)
# ChatLiteLLM's before-validator materializes a value for every field,
# which marks `streaming` as explicitly set. langchain-core >= 1.4
# treats an explicitly-set streaming=False as a hard opt-out, silently
# downgrading .stream()/.astream() to non-streaming invoke calls.
# Un-mark the field unless the caller actually passed it.
if not streaming_explicitly_set:
self.__pydantic_fields_set__.discard("streaming")

# Internal core client — handles discovery, HTTPHandler lifecycle, provider resolution
_core: UiPathLiteLLM | None = None

Expand Down
Loading
Loading