mirror of
https://github.com/langchain-ai/langsmith-java.git
synced 2026-07-25 21:25:45 -04:00
Compare commits
362 Commits
v0.1.0-alpha.20
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2775de9d25 | |||
| 2ab482c2d3 | |||
| b1649d975e | |||
| aece72d035 | |||
| aa4be05c11 | |||
| 8aded40800 | |||
| 6165434d21 | |||
| 95da1e9b94 | |||
| eace8c3f99 | |||
| 056dbd3673 | |||
| e7fe407f74 | |||
| f6608288d4 | |||
| b072b2e2fc | |||
| 391b75ca3b | |||
| 42326ef486 | |||
| b6d079b991 | |||
| 804022edf1 | |||
| feb140e343 | |||
| 0d0a8f5ffb | |||
| 57da309643 | |||
| 388984c005 | |||
| 283c4c2bcf | |||
| b8f1caebe1 | |||
| b64cfc2ca8 | |||
| 101472f4ce | |||
| df8f95af97 | |||
| 6720c1f0e4 | |||
| 2f1ca870dd | |||
| 7ba0bad56f | |||
| 68707629d2 | |||
| 3880cb324a | |||
| 37d650a362 | |||
| cff262179d | |||
| a19f9aae0b | |||
| 1409dd89d8 | |||
| 839107858c | |||
| 8eb067064e | |||
| 306a079479 | |||
| 8b1dff9a1e | |||
| 50525423cc | |||
| d53da42ffa | |||
| cbc00807ee | |||
| 2cf0ff6e08 | |||
| 8e2a0583bf | |||
| 63ca829e51 | |||
| d35ada22c7 | |||
| 2c9704f3c0 | |||
| b2ed882d0e | |||
| 99fe7dbe33 | |||
| c3764a059c | |||
| 3f086d2302 | |||
| ca210b2c66 | |||
| e639046090 | |||
| 1893f83cf2 | |||
| 63bde3c6e4 | |||
| 9cfd26bef9 | |||
| 17551decf7 | |||
| 0a29041897 | |||
| 12185beb4c | |||
| 18d7867206 | |||
| a80e55d510 | |||
| b8f033e3d9 | |||
| 38646cb1fa | |||
| 59d3e33e71 | |||
| c35c92289b | |||
| c22f69aac3 | |||
| 57f86d2e60 | |||
| 141e3e5603 | |||
| 5d5112753c | |||
| 13b36e3f11 | |||
| fcc7ab733b | |||
| a07babf48b | |||
| fdba8e6123 | |||
| bf2443da84 | |||
| 3bc6173328 | |||
| d71cff9f59 | |||
| e2fc8a84dd | |||
| 8e17124b68 | |||
| 6ab6fa281f | |||
| 585b12dd26 | |||
| 4513b3ffb0 | |||
| b00f843bc8 | |||
| 5b3f2edfce | |||
| f6ea8673f6 | |||
| 539f2893ea | |||
| e3b478437e | |||
| c01f895e57 | |||
| 6b52583c7c | |||
| 384c001825 | |||
| a00b1f18ca | |||
| 48b3b08984 | |||
| 7a1922239b | |||
| 8b752b109b | |||
| 636b67ce06 | |||
| 2d4900b16b | |||
| 4d45358e0e | |||
| 10a1f9bcbb | |||
| 2f0ce40b86 | |||
| bbd15dab2c | |||
| 2c08fe0ecb | |||
| 7d9a2fafd4 | |||
| ab6c153520 | |||
| 34f1c52fc1 | |||
| d1420efd7a | |||
| cdbd36936f | |||
| 49bf68d747 | |||
| 387526fd3a | |||
| e88560861c | |||
| cfed196979 | |||
| 70edc4514c | |||
| 6f1ae304fa | |||
| 84e02f96b4 | |||
| 65b5b9ae40 | |||
| a82b5c88fc | |||
| e097147ff3 | |||
| ba419de13e | |||
| 17bb73a006 | |||
| 48c283c7a7 | |||
| 3c36fa9f58 | |||
| 49039f2770 | |||
| 54d105ef91 | |||
| b963cab6c6 | |||
| 734a8285cd | |||
| 152169f4e1 | |||
| ac8da0f77c | |||
| 7f24878418 | |||
| 3c218e98bc | |||
| 2ddc687610 | |||
| f00fef565e | |||
| 87e3acbb7b | |||
| 280a37489d | |||
| 77f12e3a6e | |||
| b86e854687 | |||
| fd2980ce69 | |||
| e480cd4104 | |||
| 3f3a0c9c30 | |||
| 77d19d3241 | |||
| 5a6bddd241 | |||
| 10f1d65063 | |||
| 79647d1579 | |||
| f3ce273a02 | |||
| bbbcf7bf40 | |||
| f859585592 | |||
| 370b940a1c | |||
| ad6daecac9 | |||
| 6b25ea38a8 | |||
| 8b85e16472 | |||
| 7cb70fc3e3 | |||
| cb519f93d4 | |||
| 6bbe522f0a | |||
| bb9f973f41 | |||
| da0fc7b507 | |||
| 9c85b0cfd1 | |||
| 25b9aacfda | |||
| 3bd596cefa | |||
| 334fe4ef56 | |||
| 744987978d | |||
| c904c2d278 | |||
| 1540e83048 | |||
| 513d207b80 | |||
| 48a0841016 | |||
| 090c06bbaa | |||
| ea5f34e155 | |||
| 14634be733 | |||
| 3220bc56d5 | |||
| 9ccfc6a350 | |||
| 9f90d52312 | |||
| 4e86f4ee9e | |||
| f3d8fdeccf | |||
| 4ba75e7776 | |||
| 35bbb77c96 | |||
| ad80d1105b | |||
| 12a841eeb4 | |||
| 19c9b420ea | |||
| 53f2092dcd | |||
| 81f7359cac | |||
| 5c4f4acb92 | |||
| 80dceff65a | |||
| b5b42f69a3 | |||
| c794cc215d | |||
| d07b7f3f40 | |||
| 305cae3bef | |||
| dd6d82830b | |||
| 9491540375 | |||
| 22ed526472 | |||
| f7f2672957 | |||
| eaf31118a5 | |||
| da3535ded5 | |||
| 2796973131 | |||
| d977e55ba6 | |||
| 7b9a6f2b09 | |||
| 219e966da6 | |||
| d8873a4cce | |||
| 2ce327828b | |||
| b0f3b5aab6 | |||
| a54a91735b | |||
| 959d619ee0 | |||
| 960ee71498 | |||
| 26b3f9cb2f | |||
| d6e265a7df | |||
| 315a4bd074 | |||
| bd1b0e8b59 | |||
| 57116f2104 | |||
| 554e497084 | |||
| c8873ff34e | |||
| 12e572bdd1 | |||
| a91eece7d1 | |||
| d3a153ef43 | |||
| 8705818e81 | |||
| d272832132 | |||
| 0cf53d62a4 | |||
| 8336c6349a | |||
| 382da9a537 | |||
| ad75c9789b | |||
| f6d0b66f60 | |||
| 6666c85031 | |||
| 8f1fc7bf69 | |||
| f09f2c48b3 | |||
| 3517baad85 | |||
| a42751fe92 | |||
| bca5f1858b | |||
| 43023605f0 | |||
| f247ab1bf3 | |||
| 4461ffbf99 | |||
| bb060d0897 | |||
| 3e731df52b | |||
| b9ce5af2d4 | |||
| 3eed0e974f | |||
| c616f1a7af | |||
| c6e950f613 | |||
| 2d81794530 | |||
| 33152dc37b | |||
| a1533b5161 | |||
| c9238e7d8a | |||
| 8c1f5f27ad | |||
| 06771e7b9e | |||
| 9b3cd93e74 | |||
| 0ff2f3175f | |||
| f06475795a | |||
| 81ce4fe1f3 | |||
| 6c35351e46 | |||
| 7256a6ca4a | |||
| feb7baba86 | |||
| 4baa783b9b | |||
| 97f40ac895 | |||
| 7d80a1978c | |||
| 92ce263397 | |||
| 4466c613fc | |||
| 848c0a5751 | |||
| 1d2e391160 | |||
| dab7b73dc9 | |||
| 59ea961218 | |||
| 873396aceb | |||
| f12b234a80 | |||
| a2520117a5 | |||
| 10946e186a | |||
| 9bd1c8c310 | |||
| 44a2308df3 | |||
| 765bad3142 | |||
| 41fcd7cec9 | |||
| f102c9929d | |||
| 6866c4fdfe | |||
| 45dd9a2727 | |||
| 7512302dd4 | |||
| 98e16a283e | |||
| 8cdc61ab86 | |||
| 3a1a72f559 | |||
| 76da93f2c0 | |||
| cf4696d51a | |||
| d99b42f8af | |||
| 19b171e07e | |||
| d02088b5c9 | |||
| 8c1c7d1930 | |||
| ee9eec9529 | |||
| 000f84af61 | |||
| 115ecd9065 | |||
| a5822f60d0 | |||
| ea263dc48f | |||
| 2c88ad1bb9 | |||
| c4b69463ce | |||
| a797755dc6 | |||
| 750cb9727c | |||
| 2e9af6f60c | |||
| ea568e078f | |||
| 793ca57f56 | |||
| f0673a8381 | |||
| a3018d05fc | |||
| e11fa749b7 | |||
| 24bca7acbd | |||
| 3dfde0eac4 | |||
| 66495749d9 | |||
| cac6931b8f | |||
| 929a7d700f | |||
| fb2260d06f | |||
| a40f07edcd | |||
| 7068b7797c | |||
| 379af11f42 | |||
| 974472d764 | |||
| cd7a03e546 | |||
| b528d8b51a | |||
| 9a58e84cda | |||
| e6d4a0e83d | |||
| 0871a9da0f | |||
| 86774475b2 | |||
| 7b84fdac3d | |||
| 71cba9d0e2 | |||
| bd733743ac | |||
| bc76417677 | |||
| 84e2de05ce | |||
| 5b3bba1323 | |||
| 2b6c2ed358 | |||
| 88e87f3eae | |||
| 40409eb155 | |||
| 4825017b86 | |||
| f1fa362930 | |||
| fdb1ee6552 | |||
| 0756d8db46 | |||
| e4a386aa13 | |||
| 3b85a7e71f | |||
| 021a770e89 | |||
| c358fb4281 | |||
| 9c4625d82c | |||
| e9125a7bdf | |||
| 74b82f7625 | |||
| 449caf4826 | |||
| e2583dfe8d | |||
| 52f37011ed | |||
| 4cde1b3db1 | |||
| bb7ebc08af | |||
| 256dfab0e1 | |||
| 03270ff2a5 | |||
| 1902c228dd | |||
| 3c79eea5dd | |||
| cff199b8fa | |||
| 88af6cd36e | |||
| 47f39a40e4 | |||
| 79f436a6a3 | |||
| 60e0ce825e | |||
| 54d56e2915 | |||
| 23597a4e3f | |||
| e55e8c7fe2 | |||
| 92d87eab22 | |||
| d40dd2c26f | |||
| f0f5ad3c41 | |||
| da14cc6869 | |||
| a2648390af | |||
| 43741790f7 | |||
| 10e2af9aea | |||
| 04508f89a2 | |||
| 4c2738351e | |||
| b34611a2d6 | |||
| 0a7b44831a | |||
| 49918bb9ab | |||
| 3c7a62f01e | |||
| 501beab343 | |||
| fff7a4663f | |||
| 398c86771f | |||
| 2d298cd81f | |||
| 7050baf4e1 | |||
| f0b2abc659 | |||
| 7ef6b9f8c7 | |||
| 5c4cf4f0f7 |
@@ -1,5 +0,0 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
@@ -0,0 +1,58 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gradle
|
||||
directory: /
|
||||
target-branch: "next"
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
major:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- major
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /.devcontainer
|
||||
target-branch: "next"
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
major:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- major
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
target-branch: "next"
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
major:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- major
|
||||
open-pull-requests-limit: 5
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Check Stainless File Modification
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
check-stainless-files:
|
||||
name: Check for Stainless-generated file modifications
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'langchain-ai/langsmith-java-staging'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for modifications to Stainless-generated files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
SKIP_LABEL="skip-stainless-generated-file-modification-check"
|
||||
|
||||
# Check if the skip label is present
|
||||
LABELS=$(gh api repos/"$REPO"/pulls/"$PR_NUMBER" --jq '[.labels[].name] | join(",")')
|
||||
if echo "$LABELS" | grep -qF "$SKIP_LABEL"; then
|
||||
echo "Skip label '$SKIP_LABEL' is present — skipping check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get the list of files changed in this PR
|
||||
CHANGED_FILES=$(git diff --name-only "origin/${{ github.base_ref }}"...HEAD)
|
||||
|
||||
STAINLESS_FILES=""
|
||||
while IFS= read -r FILE; do
|
||||
[ -z "$FILE" ] && continue
|
||||
[ ! -f "$FILE" ] && continue
|
||||
if grep -qF "File generated from our OpenAPI spec by Stainless." "$FILE"; then
|
||||
STAINLESS_FILES="$STAINLESS_FILES\n - $FILE"
|
||||
fi
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
if [ -n "$STAINLESS_FILES" ]; then
|
||||
echo "::error::This PR modifies Stainless-generated files, which should not be edited directly."
|
||||
printf "Stainless-generated files modified:%b\n" "$STAINLESS_FILES"
|
||||
echo ""
|
||||
echo "Please choose one of the following options:"
|
||||
echo ""
|
||||
echo " 1. Refactor your change to avoid modifying the generated file directly."
|
||||
echo ""
|
||||
echo " 2. Use Stainless configuration instead:"
|
||||
echo " - OpenAPI extensions: https://www.stainless.com/docs/openapi/extensions/"
|
||||
echo " - Transforms: https://www.stainless.com/docs/openapi/transforms/reference/"
|
||||
echo ""
|
||||
echo " 3. If neither option above is feasible, add the label '$SKIP_LABEL' to this PR to bypass this check."
|
||||
echo " Use this only as a last resort and document why it is necessary."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No Stainless-generated files were modified."
|
||||
+25
-34
@@ -1,29 +1,34 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'generated'
|
||||
- 'codegen/**'
|
||||
- 'integrated/**'
|
||||
- 'stl-preview-head/**'
|
||||
- 'stl-preview-base/**'
|
||||
branches:
|
||||
- '**'
|
||||
- '!integrated/**'
|
||||
- '!stl-preview-head/**'
|
||||
- '!stl-preview-base/**'
|
||||
- '!generated'
|
||||
- '!codegen/**'
|
||||
- 'codegen/stl/**'
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- 'stl-preview-head/**'
|
||||
- 'stl-preview-base/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
timeout-minutes: 15
|
||||
name: lint
|
||||
runs-on: ${{ github.repository == 'stainless-sdks/langsmith-api-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||
runs-on: ubuntu-latest
|
||||
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: |
|
||||
@@ -32,7 +37,7 @@ jobs:
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
||||
|
||||
- name: Run lints
|
||||
run: ./scripts/lint
|
||||
@@ -40,14 +45,17 @@ jobs:
|
||||
build:
|
||||
timeout-minutes: 15
|
||||
name: build
|
||||
runs-on: ${{ github.repository == 'stainless-sdks/langsmith-api-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: |
|
||||
@@ -56,30 +64,13 @@ jobs:
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
||||
|
||||
- name: Build SDK
|
||||
run: ./scripts/build
|
||||
|
||||
test:
|
||||
timeout-minutes: 15
|
||||
name: test
|
||||
runs-on: ${{ github.repository == 'stainless-sdks/langsmith-api-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: |
|
||||
8
|
||||
21
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3
|
||||
|
||||
- name: Run tests
|
||||
run: ./scripts/test
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
branches: [main, next]
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Weekly scan on Sunday at midnight
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -18,17 +22,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: |
|
||||
8
|
||||
21
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Count number of methods in the SDK
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- 'generated'
|
||||
- 'codegen/**'
|
||||
- 'integrated/**'
|
||||
- 'stl-preview-head/**'
|
||||
- 'stl-preview-base/**'
|
||||
|
||||
jobs:
|
||||
count-methods:
|
||||
name: count methods
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Read method count from .stats.yml
|
||||
id: read_method_count
|
||||
run: |
|
||||
if [ ! -f .stats.yml ]; then
|
||||
echo "Error: .stats.yml file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
METHOD_COUNT=$(grep -E '^configured_endpoints:\s*[0-9]+' .stats.yml | awk '{print $2}' || echo "")
|
||||
|
||||
if [ -z "$METHOD_COUNT" ]; then
|
||||
echo "Error: Could not find configured_endpoints in .stats.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Method count from .stats.yml: $METHOD_COUNT"
|
||||
echo "method_count=$METHOD_COUNT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Fail if method count exceeds threshold
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const methodCount = parseInt('${{ steps.read_method_count.outputs.method_count }}');
|
||||
const threshold = 100;
|
||||
|
||||
if (methodCount > threshold) {
|
||||
core.setFailed(
|
||||
` Method count check failed!\n` +
|
||||
` Current method count: ${methodCount}\n` +
|
||||
` Maximum allowed: ${threshold}\n` +
|
||||
` Excess: ${methodCount - threshold} methods\n\n` +
|
||||
`This check ensures the SDK doesn't exceed the configured endpoint limit.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Currently at ${methodCount} methods`);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
# This workflow is triggered when a GitHub release is created.
|
||||
# It can also be run manually to re-publish to Sonatype in case it failed for some reason.
|
||||
# You can run this workflow by navigating to https://www.github.com/langchain-ai/langsmith-java/actions/workflows/publish-sonatype.yml
|
||||
# workflow for re-running publishing to Sonatype in case it fails for some reason
|
||||
# you can run this workflow by navigating to https://www.github.com/langchain-ai/langsmith-java/actions/workflows/publish-sonatype.yml
|
||||
name: Publish Sonatype
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -8,16 +7,19 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: publish
|
||||
runs-on: ubuntu-latest-l-oss
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: |
|
||||
@@ -26,14 +28,18 @@ jobs:
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3
|
||||
|
||||
- name: Publish to Sonatype
|
||||
run: |-
|
||||
export -- GPG_SIGNING_KEY_ID
|
||||
printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
|
||||
GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
|
||||
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
|
||||
export ORG_GRADLE_PROJECT_signingInMemoryKey="$GPG_SIGNING_KEY"
|
||||
export ORG_GRADLE_PROJECT_signingInMemoryKeyId="$GPG_SIGNING_KEY_ID"
|
||||
export ORG_GRADLE_PROJECT_signingInMemoryKeyPassword="$GPG_SIGNING_PASSWORD"
|
||||
# Keep publish execution serial to avoid Gradle/Dokka task graph race conditions.
|
||||
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache --no-parallel
|
||||
env:
|
||||
SONATYPE_USERNAME: ${{ secrets.LANGCHAIN_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
|
||||
SONATYPE_PASSWORD: ${{ secrets.LANGCHAIN_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
name: Release Doctor
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release_doctor:
|
||||
name: release doctor
|
||||
@@ -12,12 +15,13 @@ jobs:
|
||||
if: github.repository == 'langchain-ai/langsmith-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check release environment
|
||||
run: |
|
||||
bash ./bin/check-release-environment
|
||||
env:
|
||||
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
SONATYPE_USERNAME: ${{ secrets.LANGCHAIN_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
|
||||
SONATYPE_PASSWORD: ${{ secrets.LANGCHAIN_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
|
||||
GPG_SIGNING_KEY: ${{ secrets.LANGCHAIN_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Release Please
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
if: github.repository == 'langchain-ai/langsmith-java'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
|
||||
id: release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger publish
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
run: gh workflow run publish-sonatype.yml --ref ${{ steps.release.outputs.tag_name }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
@@ -0,0 +1,47 @@
|
||||
name: "stlc: trigger promote on stlc/from-prod → main approval"
|
||||
|
||||
# Dispatches the ff-only merge workflow in langchainplus whenever the stlc/from-prod → main
|
||||
# PR in this repo receives an approving review.
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
trigger-promote:
|
||||
if: |
|
||||
github.repository == 'langchain-ai/langsmith-java-staging' &&
|
||||
github.event.review.state == 'approved' &&
|
||||
github.event.pull_request.base.ref == 'main' &&
|
||||
github.event.pull_request.head.ref == 'stlc/from-prod'
|
||||
runs-on: self-hosted-ubuntu-small-amd64
|
||||
steps:
|
||||
- name: Load GitHub App key from 1Password
|
||||
id: load-gh-app-secrets
|
||||
uses: 1password/load-secrets-action@92467eb28f72e8255933372f1e0707c567ce2259 # v4.0.0
|
||||
with:
|
||||
export-env: false
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
|
||||
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
|
||||
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: "Iv23liMFOFBcPTKFWi3S"
|
||||
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
|
||||
repositories: langchainplus
|
||||
|
||||
- name: Dispatch ff-only merge workflow in langchainplus
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
gh workflow run git-ff-only-merge.yml \
|
||||
--repo langchain-ai/langchainplus \
|
||||
--field repo=langsmith-java-staging \
|
||||
--field src_branch=stlc/from-prod \
|
||||
--field target_branch=main
|
||||
@@ -0,0 +1,87 @@
|
||||
name: 'stlc: promote to production'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
if: github.repository == 'langchain-ai/langsmith-java-staging'
|
||||
runs-on: self-hosted-ubuntu-small-amd64
|
||||
env:
|
||||
PRODUCTION_REPO: langchain-ai/langsmith-java
|
||||
steps:
|
||||
- name: Check out staging
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Load GitHub App key from 1Password
|
||||
id: load-gh-app-secrets
|
||||
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2
|
||||
with:
|
||||
export-env: false
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
|
||||
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
|
||||
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: "Iv23liMFOFBcPTKFWi3S"
|
||||
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
|
||||
repositories: langsmith-java-staging,langsmith-java
|
||||
|
||||
- name: Fetch production main
|
||||
run: |
|
||||
git remote add production \
|
||||
"https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${PRODUCTION_REPO}.git"
|
||||
git fetch production main
|
||||
|
||||
- name: Check if production is already in sync
|
||||
id: diff
|
||||
run: |
|
||||
STAGING_SHA=$(git rev-parse origin/main)
|
||||
PRODUCTION_SHA=$(git rev-parse production/main)
|
||||
if [ "$STAGING_SHA" = "$PRODUCTION_SHA" ]; then
|
||||
echo "Production is already at $STAGING_SHA. Nothing to release."
|
||||
echo "synced=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "synced=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Push staging main to the release branch on production
|
||||
if: steps.diff.outputs.synced == 'false'
|
||||
run: |
|
||||
git push production origin/main:refs/heads/stainless/release --force
|
||||
|
||||
- name: Open or update the release PR on production
|
||||
if: steps.diff.outputs.synced == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
EXISTING_PR=$(gh pr list \
|
||||
--repo "${PRODUCTION_REPO}" \
|
||||
--head stainless/release \
|
||||
--state open \
|
||||
--json number \
|
||||
--jq '.[0].number')
|
||||
if [ -z "${EXISTING_PR}" ]; then
|
||||
gh pr create \
|
||||
--repo "${PRODUCTION_REPO}" \
|
||||
--base main \
|
||||
--head stainless/release \
|
||||
--title "Release SDK updates" \
|
||||
--body "$(git log --reverse --format="%s%n" production/main..origin/main)"
|
||||
else
|
||||
gh pr edit "${EXISTING_PR}" \
|
||||
--repo "${PRODUCTION_REPO}" \
|
||||
--body "$(git log --reverse --format="%s%n" production/main..origin/main)"
|
||||
echo "Release PR #${EXISTING_PR} already exists. Updated its description."
|
||||
fi
|
||||
@@ -0,0 +1,47 @@
|
||||
name: "stlc: seal custom code"
|
||||
|
||||
# Triggered on every push to the `main` branch of langsmith-java-staging.
|
||||
# Delegates to the stlc_seal_custom_code workflow in langchainplus (where
|
||||
# the stainless workspace and reusable actions live) by dispatching it via
|
||||
# the GitHub API, scoped to the java target only.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
seal:
|
||||
if: github.repository == 'langchain-ai/langsmith-java-staging'
|
||||
runs-on: self-hosted-ubuntu-small-amd64
|
||||
steps:
|
||||
- name: Load GitHub App key from 1Password
|
||||
id: load-gh-app-secrets
|
||||
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2
|
||||
with:
|
||||
export-env: false
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
|
||||
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
|
||||
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: "Iv23liMFOFBcPTKFWi3S"
|
||||
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
|
||||
repositories: langsmith-java-staging,langchainplus
|
||||
|
||||
- name: Trigger stlc seal workflow in langchainplus
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
gh workflow run stlc_seal_custom_code.yml \
|
||||
--repo langchain-ai/langchainplus \
|
||||
--field repositories=langsmith-java-staging
|
||||
@@ -0,0 +1,86 @@
|
||||
name: "stlc: sync from production"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 */6 * * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: stlc-sync-from-production
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: self-hosted-ubuntu-small-amd64
|
||||
if: github.repository == 'langchain-ai/langsmith-java-staging'
|
||||
env:
|
||||
PRODUCTION_REPO: langchain-ai/langsmith-java
|
||||
steps:
|
||||
- name: Load GitHub App key from 1Password
|
||||
id: load-gh-app-secrets
|
||||
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2
|
||||
with:
|
||||
export-env: false
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
|
||||
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
|
||||
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: "Iv23liMFOFBcPTKFWi3S"
|
||||
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
|
||||
repositories: langsmith-java-staging,langsmith-java,stlc,stlc-java,langchainplus
|
||||
|
||||
- name: Check out staging
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Fetch production main
|
||||
env:
|
||||
APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
git remote add production \
|
||||
"https://x-access-token:${APP_TOKEN}@github.com/${PRODUCTION_REPO}.git"
|
||||
git fetch production main
|
||||
|
||||
- name: Check whether production has content staging lacks
|
||||
id: diff
|
||||
run: |
|
||||
MERGED=$(git merge-tree --write-tree origin/main production/main) || MERGED=conflict
|
||||
STAGING_TREE=$(git rev-parse 'origin/main^{tree}')
|
||||
if [ "$MERGED" = "$STAGING_TREE" ]; then
|
||||
echo "Staging already has production's content. Nothing to pull back."
|
||||
echo "behind=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "behind=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Push production main to the back-sync branch on staging
|
||||
if: steps.diff.outputs.behind == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: git push origin production/main:refs/heads/stlc/from-prod --force
|
||||
|
||||
- name: Open or update the back-sync PR on staging
|
||||
if: steps.diff.outputs.behind == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
EXISTING_PR=$(gh pr list --head stlc/from-prod --base main --state open --json number --jq '.[0].number')
|
||||
if [ -z "${EXISTING_PR}" ]; then
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head stlc/from-prod \
|
||||
--title "Sync release changes from production" \
|
||||
--body "Pulls production's release-please commits (and any merged contributions) back onto staging \`main\` so the next \`stlc build\` reseals against the released state. Bot-authored; safe to auto-merge."
|
||||
fi
|
||||
+13
@@ -1,7 +1,20 @@
|
||||
.prism.log
|
||||
.stdy.log
|
||||
.env
|
||||
.env.*
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
credentials.json
|
||||
node_modules/
|
||||
__pycache__/
|
||||
.venv/
|
||||
.DS_Store
|
||||
.gradle
|
||||
.idea
|
||||
.kotlin
|
||||
**/bin/main/
|
||||
**/bin/test/
|
||||
build/
|
||||
codegen.log
|
||||
kls_database.db
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.1.0-alpha.20"
|
||||
".": "0.1.0-beta.19"
|
||||
}
|
||||
+1
-4
@@ -1,4 +1 @@
|
||||
configured_endpoints: 100
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/langsmith%2Flangsmith-api-2bca48917a9e2f6cd8442710cc9170d50db781cc03534a7ea3ddcc13838f05c6.yml
|
||||
openapi_spec_hash: 0f0142e66b6f782304e0f285b38d73dd
|
||||
config_hash: 4c81a93850b2f9805605749a196ed2df
|
||||
configured_endpoints: 149
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
# Agent Guidelines
|
||||
|
||||
Code conventions and patterns for this project, learned from review feedback.
|
||||
|
||||
## Code structure
|
||||
|
||||
### Break up complex functions with helpers
|
||||
|
||||
When a function has deeply nested logic or multiple concerns, extract helpers. Use `flatMap` + small named functions instead of imperative loops with nested `when`/`if`:
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
fun format(variables: Map<String, Any>): PromptMessages {
|
||||
val formatted = messages.flatMap { msg ->
|
||||
if (msg.isPlaceholder()) expandPlaceholder(msg, variables)
|
||||
else listOf(PromptMessage.withTemplate(msg, msg.format(variables)))
|
||||
}
|
||||
return PromptMessages(formatted, inputVariables, outputSchema)
|
||||
}
|
||||
|
||||
private fun expandPlaceholder(msg: PromptMessage, variables: Map<String, Any>): List<PromptMessage> {
|
||||
val items = variables[msg.template] as? List<*> ?: return emptyList()
|
||||
return items.mapNotNull(::toPromptMessage)
|
||||
}
|
||||
|
||||
// Bad — deeply nested imperative loop
|
||||
fun format(variables: Map<String, Any>): PromptMessages {
|
||||
val formatted = mutableListOf<PromptMessage>()
|
||||
for (msg in messages) {
|
||||
if (msg.isPlaceholder()) {
|
||||
val value = variables[msg.template]
|
||||
if (value is List<*>) {
|
||||
for (item in value) {
|
||||
when (item) {
|
||||
is PromptMessage -> formatted.add(item)
|
||||
is Map<*, *> -> { /* 15 more lines */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kotlin idioms
|
||||
|
||||
### Prefer immutable collection transformations
|
||||
|
||||
Avoid mutable accumulators when `map`, `filter`, `partition`, `associate`, `buildMap`, or `buildList` express the same logic clearly. Use mutation only when it materially improves readability, performance, or is required by an API.
|
||||
|
||||
### Use `buildMap` / `buildList` instead of mutable + convert
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
val messages = items.map { msg ->
|
||||
buildMap<String, String> {
|
||||
put("role", msg.role)
|
||||
put("content", msg.content)
|
||||
msg.toolCallId?.let { put("tool_call_id", it) }
|
||||
}
|
||||
}
|
||||
|
||||
// Bad — unnecessary mutable/immutable conversion
|
||||
val messages = items.map { msg ->
|
||||
val base = mutableMapOf<String, String>(
|
||||
"role" to msg.role,
|
||||
"content" to msg.content,
|
||||
)
|
||||
if (msg.toolCallId != null) {
|
||||
base["tool_call_id"] = msg.toolCallId
|
||||
}
|
||||
base.toMap()
|
||||
}
|
||||
```
|
||||
|
||||
### Use `buildList` for conditional `toString()` parts
|
||||
|
||||
```kotlin
|
||||
// Good
|
||||
override fun toString(): String {
|
||||
val parts = buildList {
|
||||
add("messages=[${messages.joinToString(", ")}]")
|
||||
if (inputVariables.isNotEmpty()) add("inputVariables=$inputVariables")
|
||||
commitHash?.let { add("commitHash=$it") }
|
||||
outputSchema?.let { add("outputSchema=${it["title"] ?: "..."}") }
|
||||
}
|
||||
return "Prompt{${parts.joinToString(", ")}}"
|
||||
}
|
||||
|
||||
// Bad — chained ternary string concatenation
|
||||
override fun toString(): String =
|
||||
"Prompt{messages=[...]" +
|
||||
(if (commitHash != null) ", commitHash=$commitHash" else "") +
|
||||
(if (hasOutputSchema()) ", outputSchema=..." else "") +
|
||||
"}"
|
||||
```
|
||||
|
||||
### Prefer extension functions over type casts
|
||||
|
||||
When adding behavior to a type you don't own, use a `private` extension function instead of casting to an implementation type:
|
||||
|
||||
```kotlin
|
||||
// Good — extension function, no cast needed
|
||||
private fun Stream<*>.withErrorTracking(
|
||||
errorRef: AtomicReference<Throwable>,
|
||||
exhaustedRef: AtomicBoolean,
|
||||
): Stream<Any?> { ... }
|
||||
|
||||
val instrumented = result.withErrorTracking(iterationError, streamExhausted)
|
||||
|
||||
// Bad — casting to implementation type
|
||||
val instrumented = wrapStreamWithErrorCapture(result, iterationError, streamExhausted)
|
||||
// or worse:
|
||||
(runs as? RunServiceImpl)?.flush()
|
||||
```
|
||||
|
||||
### Use `partition` instead of double `filter`
|
||||
|
||||
```kotlin
|
||||
// Good — single pass
|
||||
val (systemMessages, nonSystemMessages) =
|
||||
messages.partition { it.role == Role.SYSTEM }
|
||||
|
||||
// Bad — iterates the list twice
|
||||
val systemMessages = messages.filter { it.role == Role.SYSTEM }
|
||||
val nonSystemMessages = messages.filter { it.role != Role.SYSTEM }
|
||||
```
|
||||
|
||||
### Use parameterized tests for table-driven cases
|
||||
|
||||
When multiple tests share the exact same structure (input → assert same fields), use `@ParameterizedTest` with `@MethodSource`:
|
||||
|
||||
```kotlin
|
||||
data class Case(val input: String, val expected: String)
|
||||
|
||||
@ParameterizedTest(name = "{index}: \"{0}\"")
|
||||
@MethodSource("cases")
|
||||
fun myTest(case: Case) {
|
||||
assertThat(transform(case.input)).isEqualTo(case.expected)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun cases(): Stream<Case> = Stream.of(
|
||||
Case("input1", "expected1"),
|
||||
Case("input2", "expected2"),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Only do this when every test has the same assertion shape. If tests have different setup or assertions, keep them as individual `@Test` methods.
|
||||
|
||||
### Extract test assertion helpers to reduce repetition
|
||||
|
||||
When the same assertion pattern appears across many tests, extract a helper:
|
||||
|
||||
```kotlin
|
||||
// Good — readable, DRY
|
||||
private fun assertMessage(msg: Map<String, String>, role: String, content: String) {
|
||||
assertThat(msg["role"]).isEqualTo(role)
|
||||
assertThat(msg["content"]).isEqualTo(content)
|
||||
}
|
||||
|
||||
assertMessage(result.messages[0], "system", "You are helpful.")
|
||||
assertMessage(result.messages[1], "user", "Hello")
|
||||
|
||||
// Bad — verbose, repetitive
|
||||
assertThat(result.messages[0]).isEqualTo(mapOf("role" to "system", "content" to "You are helpful."))
|
||||
assertThat(result.messages[1]).isEqualTo(mapOf("role" to "user", "content" to "Hello"))
|
||||
```
|
||||
|
||||
## Formatting and linting
|
||||
|
||||
```bash
|
||||
./gradlew :langsmith-java-core:formatKotlin
|
||||
./gradlew lintKotlin
|
||||
```
|
||||
|
||||
The project uses ktfmt with `--kotlinlang-style`.
|
||||
|
||||
## Testing
|
||||
|
||||
### Don't add comments that restate the test name
|
||||
|
||||
Test function names should be descriptive enough on their own. Don't add comments that repeat what the name already says:
|
||||
|
||||
```kotlin
|
||||
// Good — name is self-documenting
|
||||
@Test
|
||||
fun parseLegacyPromptTemplateWithTemplateFormat() {
|
||||
val manifest = ...
|
||||
}
|
||||
|
||||
// Bad — comment restates the function name
|
||||
@Test
|
||||
fun parseLegacyPromptTemplateWithTemplateFormat() {
|
||||
// Legacy PromptTemplate format with template_format field
|
||||
val manifest = ...
|
||||
}
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
Try to run targeted tests matching the code you changed:
|
||||
|
||||
```bash
|
||||
# Run tests for the package you changed
|
||||
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.*" --rerun
|
||||
|
||||
# Run a single test class
|
||||
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.ManifestParserTest" --rerun
|
||||
|
||||
# Run a single test method
|
||||
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.ManifestParserTest.parsePromptTemplate" --rerun
|
||||
|
||||
# See println output
|
||||
./gradlew :langsmith-java-core:test --tests "..." --rerun --info
|
||||
```
|
||||
|
||||
Only run the full suite (`./gradlew :langsmith-java-core:test --rerun`) before finalizing a PR or after large cross-cutting changes.
|
||||
|
||||
### Integration tests
|
||||
|
||||
Integration tests require environment variables:
|
||||
|
||||
```bash
|
||||
export LANGSMITH_API_KEY="lsv2_pt_..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
Tests skip gracefully via `assumeTrue` if keys are missing.
|
||||
|
||||
## Code style
|
||||
|
||||
- For cross-method concurrency coordination, prefer an explicit named `ReentrantLock` over `synchronized` when review clarity matters. Use Kotlin's `withLock { ... }` extension instead of manual `lock()` / `try` / `finally { unlock() }` unless explicit lock management is required. Keep the locked section minimal and do slow/blocking work outside the lock.
|
||||
- Choose the simplest concurrency primitive that fits the state being protected:
|
||||
- Use atomic types (`AtomicBoolean`, `AtomicInteger`, etc.) for simple flags, counters, and compare-and-set state.
|
||||
- Use `synchronized` only for small, local critical sections where a named lock would not improve clarity.
|
||||
- Use `ReentrantReadWriteLock` when reads are frequent, writes are infrequent, and concurrent reads materially help.
|
||||
- Use coroutine `Mutex.withLock { ... }` for coroutine-based concurrency instead of blocking thread locks.
|
||||
- `toString()` should be single-line, following the `ClassName{field=value, field=value}` convention used by the rest of the SDK.
|
||||
- Avoid `@Suppress("UNCHECKED_CAST")` — restructure code to use safe patterns (`as? String`, `is Map<*, *>` with `entries.associate`, etc). When unavoidable (e.g. generic type erasure after an `is` check), add a comment explaining why the cast is safe.
|
||||
- Use named arguments for constructor/function calls with 2+ parameters, especially when types could be confused:
|
||||
```kotlin
|
||||
// Good
|
||||
PromptMessage(
|
||||
role = PromptMessage.Role.HUMAN,
|
||||
template = template,
|
||||
templateFormat = templateFormat,
|
||||
)
|
||||
|
||||
// Bad — positional args are ambiguous
|
||||
PromptMessage(PromptMessage.Role.HUMAN, template, templateFormat = templateFormat)
|
||||
```
|
||||
- Name functions from the caller's perspective — describe what the caller gets, not what the function does internally. Prefer `stream.withErrorTracking()` over `wrapStreamWithErrorCapture(stream)`.
|
||||
- When an `Optional` has a fallback default, use `orElse(default)` directly instead of `orElse(null) ?: default`:
|
||||
```kotlin
|
||||
// Good — default goes straight into orElse
|
||||
kwargs["template_format"]?.asString()?.orElse("f-string") ?: "f-string"
|
||||
|
||||
// Bad — creates unnecessary null intermediary
|
||||
kwargs["template_format"]?.asString()?.orElse(null) ?: "f-string"
|
||||
```
|
||||
The `?: "f-string"` is still needed to handle the case where the key is missing from the map (`null` from `kwargs["template_format"]`), but `orElse` should carry the default for when the key exists but isn't a string.
|
||||
- Anthropic SDK is a `compileOnly` dependency — users must add it themselves. Methods that use Anthropic types should catch `NoClassDefFoundError` and throw `IllegalStateException` with a clear message.
|
||||
+615
@@ -1,5 +1,620 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0-beta.19](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.18...v0.1.0-beta.19) (2026-07-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **backend:** Allow storing grid layout to custom dashboards ([8aded40](https://github.com/langchain-ai/langsmith-java/commit/8aded40800f6096880f43d2ef51a07963dec65e5))
|
||||
* **backend:** update CRUD endpoint to allow retrieving / storing chart series metadata ([aa4be05](https://github.com/langchain-ai/langsmith-java/commit/aa4be05c111d702aa801cba4acbda69e0a5da7a6))
|
||||
* **host:** p50/p99 run queue wait-time chart in project monitoring [LSD-1547] ([f660828](https://github.com/langchain-ai/langsmith-java/commit/f6608288d483e0041c6e0fe643f6881f3c1a3a68))
|
||||
* **sandboxes:** free-form labels on sandboxes and snapshots ([6165434](https://github.com/langchain-ai/langsmith-java/commit/6165434d21025dfaeef22b4a63e31a0f0e4977a1))
|
||||
* **sandboxes:** let proxy rules contribute sandbox env vars ([eace8c3](https://github.com/langchain-ai/langsmith-java/commit/eace8c3f9903e180fc7e9cfbaf0a13a7a91a5635))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **evaluators:** persist and wire through playground_settings_id for llm-as-judge oauth models [part of ENT-1402] ([b072b2e](https://github.com/langchain-ai/langsmith-java/commit/b072b2e2fcf6dafd32cb0213e02127a82ab0659a))
|
||||
* **runs:** require session in POST /runs/stats ([056dbd3](https://github.com/langchain-ai/langsmith-java/commit/056dbd3673294fcd1fbc564d4332ba38c151ec6b))
|
||||
* support last_queued_at in v2 runs query ([e7fe407](https://github.com/langchain-ai/langsmith-java/commit/e7fe407f74819ac775fcc092289d9dcfaafc036d))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* **runs:** require session in POST /runs/stats ([95da1e9](https://github.com/langchain-ai/langsmith-java/commit/95da1e9b944058f30b5274c882f4669933b8bbfd))
|
||||
|
||||
|
||||
### Refactors
|
||||
|
||||
* **runs:** remove SmithDB v2 endpoint flag [LSO-3391] ([aece72d](https://github.com/langchain-ai/langsmith-java/commit/aece72d035ca24eb8ee94236d301f7610b18eefc))
|
||||
|
||||
## [0.1.0-beta.18](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.17...v0.1.0-beta.18) (2026-07-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **runs:** add GET /v2/runs/{run_id}/url endpoint and SDK runs.get_url method ([57da309](https://github.com/langchain-ai/langsmith-java/commit/57da3096437fe210865bab3a7560eff50d20feef))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build, lint, and test failures (feat(gateway): rate limit policy CRUD, validation, and materialization [ENT-1138]) ([388984c](https://github.com/langchain-ai/langsmith-java/commit/388984c00503fe2dd39aacb93fe4053ae2030d43))
|
||||
|
||||
## [0.1.0-beta.17](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.16...v0.1.0-beta.17) (2026-07-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **runs/v2:** make start_time optional in GET /v2/runs/:id ([101472f](https://github.com/langchain-ai/langsmith-java/commit/101472f4ce1210028bb37a577a3992a49511af40))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* remove stlc-promote-to-main workflow ([#45](https://github.com/langchain-ai/langsmith-java/issues/45)) ([df8f95a](https://github.com/langchain-ai/langsmith-java/commit/df8f95af97e784a035da210551d5aa17c63608c3))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* minor rewording of List Commits endpoint description ([2f1ca87](https://github.com/langchain-ai/langsmith-java/commit/2f1ca870dd9a60016d2c0bad66b1ece2826d41da))
|
||||
|
||||
## [0.1.0-beta.16](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.15...v0.1.0-beta.16) (2026-07-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add run rule evaluator tracing flag [LSO-3323] ([d53da42](https://github.com/langchain-ai/langsmith-java/commit/d53da42ffaa0d5072409f45d4398a03741013c6a))
|
||||
* **engine:** issue lifecycle backend + webhook status filter (2/8) ([cbc0080](https://github.com/langchain-ai/langsmith-java/commit/cbc00807ee3991638b76430095c521243bcf5675))
|
||||
* **feedback:** support GET /feedback for thread AQs ([5052542](https://github.com/langchain-ai/langsmith-java/commit/50525423cca7d2024be967a074267a97068c33ed))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datasets:** serve public dataset sessions via Go, fixing SmithDB-only 501 ([8eb0670](https://github.com/langchain-ai/langsmith-java/commit/8eb067064eedc528a7e3381e9a84c9bceff0886f))
|
||||
* patch 6 high-severity security alerts ([#42](https://github.com/langchain-ai/langsmith-java/issues/42)) ([8b1dff9](https://github.com/langchain-ai/langsmith-java/commit/8b1dff9a1ed8a7982278f393ca2859a3d96a97a0))
|
||||
* remove July 2026 premier plan ([1409dd8](https://github.com/langchain-ai/langsmith-java/commit/1409dd89d8da75b5650ca15509c6e73d545364df))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* new unshare endpoint that doesn't require lookup ([8391078](https://github.com/langchain-ai/langsmith-java/commit/839107858ca208c7238ef83e4fbd9379e31bf220))
|
||||
* Stainless SDK methods for V2 public-run APIs ([306a079](https://github.com/langchain-ai/langsmith-java/commit/306a079479b914fc67a0d7627f29c8c95b6b5c4c))
|
||||
|
||||
## [0.1.0-beta.15](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.14...v0.1.0-beta.15) (2026-07-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add July 2026 startup and premier plan tiers ([b2ed882](https://github.com/langchain-ai/langsmith-java/commit/b2ed882d0edd038733fcdeda5448519a5f4dfe9b))
|
||||
* **context-hub:** fire commit webhook and deliver async ([2c9704f](https://github.com/langchain-ai/langsmith-java/commit/2c9704f3c0151bd51ff57ecc54edbcfb527e61d7))
|
||||
* **sandboxes:** add preserve_memory_on_stop create attribute ([99fe7db](https://github.com/langchain-ai/langsmith-java/commit/99fe7dbe33f3a0bb8709577bcd08a3f24113d5e4))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* update smithdb proto generated code ([d35ada2](https://github.com/langchain-ai/langsmith-java/commit/d35ada22c7e73d60b6fc44185d7a65a99a4255e1))
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.13...v0.1.0-beta.14) (2026-07-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **dashboards:** convert prebuilts to v2 charts on clone [LSO-3306, LSO-3322] ([0a29041](https://github.com/langchain-ai/langsmith-java/commit/0a2904189719cead7198fcc1765e2c44ab36dec9))
|
||||
* **evaluators:** expose is_managed on Go evaluator reads [LSE-2532] ([17551de](https://github.com/langchain-ai/langsmith-java/commit/17551decf77e4e2141473fe9cdbf33ea4b4648b6))
|
||||
* forward filter param to thread stats API ([12185be](https://github.com/langchain-ai/langsmith-java/commit/12185beb4cf348a4252af15257ae2b0133027a42))
|
||||
* **sdk:** wire annotation-queue runs/by-key into stainless [LSAQ-141] ([18d7867](https://github.com/langchain-ai/langsmith-java/commit/18d7867206e7b66a91a3d4452811636aa173e55c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* patch remaining Java security alerts ([#38](https://github.com/langchain-ai/langsmith-java/issues/38)) ([e639046](https://github.com/langchain-ai/langsmith-java/commit/e639046090177450d005f9fa511d4c03a565a504))
|
||||
* **smith-go:** default time bounds for POST /v2/threads/query ([63bde3c](https://github.com/langchain-ai/langsmith-java/commit/63bde3c6e4ef1c470a82a7814dd5f6083f98c110))
|
||||
* **smith-sdks:** name thread stats response model ThreadStats ([1893f83](https://github.com/langchain-ai/langsmith-java/commit/1893f83cf27ed269a62fba637c829e31c1a94bf8))
|
||||
|
||||
|
||||
### Refactors
|
||||
|
||||
* **sandboxes:** make context hub sync a generic mount ([9cfd26b](https://github.com/langchain-ai/langsmith-java/commit/9cfd26bef9d4efc3d66d25f6a54f2224393d935b))
|
||||
|
||||
## [0.1.0-beta.13](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.12...v0.1.0-beta.13) (2026-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Adds attachment support to Java multipart requests ([#33](https://github.com/langchain-ai/langsmith-java/issues/33)) ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
|
||||
* **backend:** extend_trace_retention field for annotation queue adds ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* re-emit commits missed by release-please due to timestamp ordering ([#181](https://github.com/langchain-ai/langsmith-java/issues/181)) ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
|
||||
|
||||
|
||||
### Refactors
|
||||
|
||||
* **sandboxes:** resolve default sandbox via snapshot tag instead of blueprint ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
|
||||
|
||||
## [0.1.0-beta.12](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.11...v0.1.0-beta.12) (2026-07-08)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **stainless:** skip sessions resource for python and typescript
|
||||
|
||||
### Features
|
||||
|
||||
* **managed-eval:** expose is_managed_evaluator on run-rule read/write paths ([8e17124](https://github.com/langchain-ai/langsmith-java/commit/8e17124b681e504789b3bcb7b03653e327339d1a))
|
||||
* **smith-sdks:** expose threads and traces v2 endpoints in public SDK ([3bc6173](https://github.com/langchain-ai/langsmith-java/commit/3bc61733286b179f5567439dd65cf94e6faff571))
|
||||
* **stainless:** skip sessions resource for python and typescript ([d71cff9](https://github.com/langchain-ai/langsmith-java/commit/d71cff9f592c797a6b7383755ebfde6feea7e7c6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove duplicate threads/traces declarations in client impls ([#35](https://github.com/langchain-ai/langsmith-java/issues/35)) ([fdba8e6](https://github.com/langchain-ai/langsmith-java/commit/fdba8e6123bda467312acd260ef7cf20e21dba9b))
|
||||
* **smith-sdks:** remove dead retrieve_thread_preview SDK mapping ([585b12d](https://github.com/langchain-ai/langsmith-java/commit/585b12dd2672808df97c6221c783342e17dd6755))
|
||||
* **smith-sdks:** rename dataset runs POST methods from create to query ([4513b3f](https://github.com/langchain-ai/langsmith-java/commit/4513b3ffb05b51ec5be3e5f394d312726de1dd13))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **stainless:** skip all datasets endpoints in Python/TS SDKs except experiment-runs ([6ab6fa2](https://github.com/langchain-ai/langsmith-java/commit/6ab6fa281f3f9db790121a94d20a32323e56dc80))
|
||||
|
||||
## [0.1.0-beta.11](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.10...v0.1.0-beta.11) (2026-07-02)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* disallow run_count session sort
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* restore title/additionalProperties on v2 RunResponse JSON fields for Stainless ([e3b4784](https://github.com/langchain-ai/langsmith-java/commit/e3b478437ef307d6895f0256b4945e68f5e32a23))
|
||||
|
||||
|
||||
### Refactors
|
||||
|
||||
* disallow run_count session sort ([a00b1f1](https://github.com/langchain-ai/langsmith-java/commit/a00b1f18ca2c9b6048f4789df0da60704852a79a))
|
||||
|
||||
## [0.1.0-beta.10](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.9...v0.1.0-beta.10) (2026-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **evaluation:** add core evaluate() param parity ([#17](https://github.com/langchain-ai/langsmith-java/issues/17)) ([7d9a2fa](https://github.com/langchain-ai/langsmith-java/commit/7d9a2fafd45c60dbe11852c1c4c74565c4517595))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** add GH_REPO to release-please trigger publish step ([#24](https://github.com/langchain-ai/langsmith-java/issues/24)) ([10a1f9b](https://github.com/langchain-ai/langsmith-java/commit/10a1f9bcbbf662e048b4e4f0a1bb4e9a29f18bc7))
|
||||
* update hand-written code to use RunIngest after stainless rename ([#25](https://github.com/langchain-ai/langsmith-java/issues/25)) ([4d45358](https://github.com/langchain-ai/langsmith-java/commit/4d45358e0ef8c0df8905cc3f927a495af2bcbeca))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **examples:** dual-write source run lookup coordinates in Python ([2d4900b](https://github.com/langchain-ai/langsmith-java/commit/2d4900b16bdda9512fa71368dd03f4729f55620a))
|
||||
* **stainless:** rename run models in runs resource ([2f0ce40](https://github.com/langchain-ai/langsmith-java/commit/2f0ce40b862dc33d52ff8cb0fb8d50fde790ef73))
|
||||
|
||||
## [0.1.0-beta.9](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.8...v0.1.0-beta.9) (2026-06-29)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **api:** document v2 experiment-runs and de-publicize legacy dataset endpoints
|
||||
* **backend:** accept float value for percentile metric [LS-000]
|
||||
|
||||
### Features
|
||||
|
||||
* add GET endpoint to retrieve single workspace by id [closes ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
|
||||
* add GET endpoint to retrieve single workspace by id [closes ENT-508] ([cdbd369](https://github.com/langchain-ai/langsmith-java/commit/cdbd36936fde80c7c3dea773deefd2789d0feecf))
|
||||
* **api:** document v2 experiment-runs and de-publicize legacy dataset endpoints ([387526f](https://github.com/langchain-ai/langsmith-java/commit/387526fd3ac9e892de0786be5b538c53d2f40049))
|
||||
* **evaluation:** add Java evaluate orchestrator ([#8](https://github.com/langchain-ai/langsmith-java/issues/8)) ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
|
||||
* **evaluation:** add Java evaluate orchestrator ([#8](https://github.com/langchain-ai/langsmith-java/issues/8)) ([d1420ef](https://github.com/langchain-ai/langsmith-java/commit/d1420efd7a9c307d8b99d64ac57e3f0f26b1580b))
|
||||
* **sandbox-host:** per-VM cgroups for CPU bursting (2x), fractional ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
|
||||
* **sandbox-host:** per-VM cgroups for CPU bursting (2x), fractional vCPU, and sustained-burst limiting ([49bf68d](https://github.com/langchain-ai/langsmith-java/commit/49bf68d74763138fa7894d0e04eb1b73e72e82b3))
|
||||
* **sandboxes:** expose registry CRUD and generate Python/TypeScript SDK ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
|
||||
* **sandboxes:** expose registry CRUD and generate Python/TypeScript SDK clients ([cfed196](https://github.com/langchain-ai/langsmith-java/commit/cfed196979dce85b747a4171cb1dcacb38f88641))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** accept float value for percentile metric [LS-000] ([e885608](https://github.com/langchain-ai/langsmith-java/commit/e88560861c9f3de50e7448b97d2cde1cfdfbca46))
|
||||
* trigger publish-sonatype workflow after release-please creates a ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
|
||||
* trigger publish-sonatype workflow after release-please creates a release ([#13](https://github.com/langchain-ai/langsmith-java/issues/13)) ([70edc45](https://github.com/langchain-ai/langsmith-java/commit/70edc4514cfc54a98fbf5605795a386b8149300d))
|
||||
|
||||
## [0.1.0-beta.8](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.7...v0.1.0-beta.8) (2026-06-25)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **sandboxes:** default S3 mount endpoint
|
||||
* **sandboxes:** add ArtifactFS git mounts
|
||||
* **sandboxes:** add gcs bucket mounts
|
||||
|
||||
### Features
|
||||
|
||||
* **abac:** add tag_value_ids to dataset creation endpoints ([280a374](https://github.com/langchain-ai/langsmith-java/commit/280a37489da492957d07773692e5ae62140d98d0))
|
||||
* **abac:** add tag_value_ids to prompt creation endpoints [ENT-1176] ([f3ce273](https://github.com/langchain-ai/langsmith-java/commit/f3ce273a028c0de24171835d854edb4c69268d91))
|
||||
* **abac:** add tag_value_ids to tracer session creation ([f859585](https://github.com/langchain-ai/langsmith-java/commit/f859585592c616310f02f91d8f0f6822ab9be17d))
|
||||
* add online evaluator endpoints to stainless config ([77d19d3](https://github.com/langchain-ai/langsmith-java/commit/77d19d324195ca6a93b51ea74aaa4484fb7a2cef))
|
||||
* adding time to first token for experiment metrics ([5a6bddd](https://github.com/langchain-ai/langsmith-java/commit/5a6bddd241bb95b0d176d70ae36c810a8157177d))
|
||||
* **backend:** allow retrieving and storing V2 charts in database [LSO-2799] ([b86e854](https://github.com/langchain-ai/langsmith-java/commit/b86e854687089b26205de336c8c99d994fd57369))
|
||||
* end-to-end OAuth bearer for Playground [part of ENT-760] ([77f12e3](https://github.com/langchain-ai/langsmith-java/commit/77f12e3a6e6d627a0e2309ecd35396abb01448ac))
|
||||
* **evaluation:** Java evaluate() primitives ([#7](https://github.com/langchain-ai/langsmith-java/issues/7)) ([a82b5c8](https://github.com/langchain-ai/langsmith-java/commit/a82b5c88fc08de041e78657d368c2b0359196248))
|
||||
* **fleet:** frontend passes typed fields (closes AB-000) ([bbbcf7b](https://github.com/langchain-ai/langsmith-java/commit/bbbcf7bf4027f95b13f2a0bffe18564da3fcfbe9))
|
||||
* **group-stats:** back thread stats with SmithDB ([7f24878](https://github.com/langchain-ai/langsmith-java/commit/7f24878418347b187fbc2c7064c52725de775c8b))
|
||||
* **hub:** add include_owners to repos list for Fleet [closes AB-2537] ([10f1d65](https://github.com/langchain-ai/langsmith-java/commit/10f1d6506332402672366470117774db374350a6))
|
||||
* make online evaluator retention opt-in (backend) ([e480cd4](https://github.com/langchain-ai/langsmith-java/commit/e480cd4104a22b71607aca0f209b416320fe62e0))
|
||||
* **run-rules:** per-action trace-retention control for automations [LSO-2749] ([3c218e9](https://github.com/langchain-ai/langsmith-java/commit/3c218e98bc255847a60b75499d9230372468adb6))
|
||||
* **runs/v2:** expose runs v2 endpoints publicly with stainless config ([48c283c](https://github.com/langchain-ai/langsmith-java/commit/48c283c7a7448b03b52185053a578027005bbdbd))
|
||||
* **sandboxes:** add ArtifactFS git mounts ([2ddc687](https://github.com/langchain-ai/langsmith-java/commit/2ddc687610df41bf15429b2492fd85cbcff86472))
|
||||
* **sandboxes:** add GCP proxy auth flow ([fd2980c](https://github.com/langchain-ai/langsmith-java/commit/fd2980ce69f3be63a0fd44922f08f72a0a6d9ac2))
|
||||
* **sandboxes:** add gcs bucket mounts ([87e3acb](https://github.com/langchain-ai/langsmith-java/commit/87e3acbb7b1d871a44e82472ff7fbce8669395de))
|
||||
* **sandboxes:** add sandbox env var support ([da0fc7b](https://github.com/langchain-ai/langsmith-java/commit/da0fc7b507828609f0264e90a50f6f8910ce9567))
|
||||
* **sandboxes:** filter lists by creator [INF-1492] ([bb9f973](https://github.com/langchain-ai/langsmith-java/commit/bb9f973f41a930ed23d33ee42deeaaaba4d07c35))
|
||||
* **sandboxes:** move mounts under mount_config ([734a828](https://github.com/langchain-ai/langsmith-java/commit/734a8285cd37d10e9676e67ae8fa98b6420514b0))
|
||||
* **sandboxes:** proxy runtime routes through platform backend [INF-0000] ([7cb70fc](https://github.com/langchain-ai/langsmith-java/commit/7cb70fc3e3dc71788c9ad66360dbcf4d577a098d))
|
||||
* **sandboxes:** resolve AWS proxy rule secrets ([cb519f9](https://github.com/langchain-ai/langsmith-java/commit/cb519f93d44d0a34633cae88c0fa79a4c3db182b))
|
||||
* **sandboxes:** set AWS proxy compatibility env ([6bbe522](https://github.com/langchain-ai/langsmith-java/commit/6bbe522f0a167e6671c5528eec2fa95dde0b680e))
|
||||
* **sandboxes:** snapshot memory from stopped boxes + honor restore_memory in v2 [INF-0000] ([79647d1](https://github.com/langchain-ai/langsmith-java/commit/79647d1579da9cdc6027f7922075ddb489e9601a))
|
||||
* **stats:** add include_details param to /runs/stats for backwards compat ([49039f2](https://github.com/langchain-ai/langsmith-java/commit/49039f277018a60f46af85108a30815609c8482c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **evaluators:** show code evaluator trace counts [LSE-2359] ([3f3a0c9](https://github.com/langchain-ai/langsmith-java/commit/3f3a0c9c30fc61198a19b27c7b3c6b8e5aa444c6))
|
||||
* **sandboxes:** default S3 mount endpoint ([54d105e](https://github.com/langchain-ai/langsmith-java/commit/54d105ef91e0c65de0f2a77ccc7e794570da15cb))
|
||||
* **sandboxes:** use built-in gcp proxy host matching ([ac8da0f](https://github.com/langchain-ai/langsmith-java/commit/ac8da0f77c706c7fc072842d5ec4e95b4f82a2d7))
|
||||
* **tests:** remove duplicate retrieve() method in RunServiceTest ([#10](https://github.com/langchain-ai/langsmith-java/issues/10)) ([ba419de](https://github.com/langchain-ai/langsmith-java/commit/ba419de13e4749f8427a98b1572bc1764404dd20))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* add release-please workflow ([#11](https://github.com/langchain-ai/langsmith-java/issues/11)) ([65b5b9a](https://github.com/langchain-ai/langsmith-java/commit/65b5b9ae402be8b84253d5ca7c255bd188c0e415))
|
||||
* add stlc sync and promotion workflows ([#5](https://github.com/langchain-ai/langsmith-java/issues/5)) ([f00fef5](https://github.com/langchain-ai/langsmith-java/commit/f00fef565e6c5c5edfa651223790e8e9cf90c317))
|
||||
* **fleet:** add param to list threads [closes AB-2522] ([8b85e16](https://github.com/langchain-ai/langsmith-java/commit/8b85e16472982a2821e82a6677c843d0565d17f3))
|
||||
* **smith-sdks:** update Java SDK stainless custom-code tracking file ([17bb73a](https://github.com/langchain-ai/langsmith-java/commit/17bb73a0061627a86d2ed1dba19316dcdfaf9c6d))
|
||||
|
||||
|
||||
### Refactors
|
||||
|
||||
* **playground:** remove legacy experiment endpoints [LSO-2230] ([370b940](https://github.com/langchain-ai/langsmith-java/commit/370b940a1ca252f0198b49d315749dc5d9282714))
|
||||
* rename field to match prev trace retention fields ([b963cab](https://github.com/langchain-ai/langsmith-java/commit/b963cab6c63f10bed9a7a1237b6df12c111be9d4))
|
||||
|
||||
## 0.1.0-beta.7 (2026-05-27)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.6...v0.1.0-beta.7](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.6...v0.1.0-beta.7)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([cad4993](https://github.com/langchain-ai/langsmith-java/commit/cad499356e3a38d660814b7ceed92eafbcf2989d))
|
||||
* **api:** api update ([84e287c](https://github.com/langchain-ai/langsmith-java/commit/84e287ca9be34e45b80fd0d33c3ebc1dd1ca0f32))
|
||||
* **api:** api update ([ce44c3b](https://github.com/langchain-ai/langsmith-java/commit/ce44c3b540624598c9152b8d1d469bb28ec5c67e))
|
||||
* **api:** api update ([81f7b39](https://github.com/langchain-ai/langsmith-java/commit/81f7b3937992f117bc573e968e9d5988c5bb5399))
|
||||
* **api:** api update ([3d0c992](https://github.com/langchain-ai/langsmith-java/commit/3d0c992f4c4772d5a3e294ebeba2b28a641b21ed))
|
||||
* **api:** api update ([b991c00](https://github.com/langchain-ai/langsmith-java/commit/b991c00c14ee68cffd2df17c6d6bdbc6c780cfd1))
|
||||
* **api:** api update ([104d3c6](https://github.com/langchain-ai/langsmith-java/commit/104d3c65623d66f24f90589df3ee04586fa8d913))
|
||||
* **api:** api update ([e01a424](https://github.com/langchain-ai/langsmith-java/commit/e01a42416bef9d17a897900e70cbef70e13ad552))
|
||||
* **api:** api update ([e1443b6](https://github.com/langchain-ai/langsmith-java/commit/e1443b69ab955cbdcee618449f11bc3cdd41add4))
|
||||
* **api:** api update ([9deafad](https://github.com/langchain-ai/langsmith-java/commit/9deafade612c0e7c163bf13fae4c2d27751122be))
|
||||
* **api:** api update ([7e7c479](https://github.com/langchain-ai/langsmith-java/commit/7e7c479912229f27ae96878f2e939971b5fff829))
|
||||
* **api:** api update ([b5a3200](https://github.com/langchain-ai/langsmith-java/commit/b5a32009fc34eb5a04d70205c19bf973414d800f))
|
||||
* **api:** api update ([3ab0c6c](https://github.com/langchain-ai/langsmith-java/commit/3ab0c6c5482c2378fc1d2a2b8657248ec218a501))
|
||||
* **client:** support proxy authentication ([d9e3ab5](https://github.com/langchain-ai/langsmith-java/commit/d9e3ab5b34a8fc2321fb1f9f86910517ec30dbdb))
|
||||
* **sdk:** add hub directories endpoints to Stainless config ([b3dbb81](https://github.com/langchain-ai/langsmith-java/commit/b3dbb81bd63f8b21fce99d345cf9c73345626451))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **go:** avoid panic when http.DefaultTransport is wrapped ([aba9578](https://github.com/langchain-ai/langsmith-java/commit/aba9578496866a723d85540da2d9f9bc6418027a))
|
||||
* **go:** avoid panic when http.DefaultTransport is wrapped ([9ea7653](https://github.com/langchain-ai/langsmith-java/commit/9ea76536a054185cbfdad5ddad7036b2e5d46137))
|
||||
* restore dokka-javadoc plugin application to subprojects ([#157](https://github.com/langchain-ai/langsmith-java/issues/157)) ([1df1c4a](https://github.com/langchain-ai/langsmith-java/commit/1df1c4aacd6c3cc9477576403a1da641bd20302d))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **client:** create one json mapper ([e68229b](https://github.com/langchain-ai/langsmith-java/commit/e68229b7009315bf60149df92e4af5f0b083c012))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **internal:** codegen related update ([5e551de](https://github.com/langchain-ai/langsmith-java/commit/5e551de99e03fb34a737f3fcac3ee6a08fab8863))
|
||||
* **internal:** renormalize gradlew.bat line endings ([4738606](https://github.com/langchain-ai/langsmith-java/commit/4738606db159ecc8ad5088714e3100c0e7972f52))
|
||||
* remove duplicated dokka setup ([b1af8d8](https://github.com/langchain-ai/langsmith-java/commit/b1af8d842d8aa3b1a47cb00ffa22d71134b0d883))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* clarify forwards compat behavior ([d4b4164](https://github.com/langchain-ai/langsmith-java/commit/d4b41649cb18523851c353c6a47b83b492e0c8e5))
|
||||
|
||||
## 0.1.0-beta.6 (2026-05-20)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.5...v0.1.0-beta.6](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.5...v0.1.0-beta.6)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* patch security alerts — OTel, Tomcat, Jetty, Logback, Spring, SnakeYAML ([#153](https://github.com/langchain-ai/langsmith-java/issues/153)) ([dfa5469](https://github.com/langchain-ai/langsmith-java/commit/dfa54697292c14a987320ba776b580ee25c58f94))
|
||||
## 0.1.0-beta.5 (2026-05-13)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.4...v0.1.0-beta.5](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.4...v0.1.0-beta.5)
|
||||
|
||||
### Features
|
||||
|
||||
* **sandbox:** make snapshot optional ([#152](https://github.com/langchain-ai/langsmith-java/issues/152)) ([5849b16](https://github.com/langchain-ai/langsmith-java/commit/5849b1695ec4504460205c80cabf8d443fd8a5f0))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **deps:** bump actions/github-script from 8 to 9 ([#144](https://github.com/langchain-ai/langsmith-java/issues/144)) ([c3f8fbd](https://github.com/langchain-ai/langsmith-java/commit/c3f8fbd5449fb5f52529166fd9b2db5eb8dbe24a))
|
||||
* **deps:** bump gradle/actions ([#143](https://github.com/langchain-ai/langsmith-java/issues/143)) ([fd8a8ae](https://github.com/langchain-ai/langsmith-java/commit/fd8a8aef399ba3784c6d459c758e7a06f6ee5e98))
|
||||
* **deps:** bump the minor-and-patch group across 1 directory with 3 updates ([#145](https://github.com/langchain-ai/langsmith-java/issues/145)) ([c4a883d](https://github.com/langchain-ai/langsmith-java/commit/c4a883dfd2e89373180f88c7149c13ef426996ee))
|
||||
|
||||
## 0.1.0-beta.4 (2026-05-04)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.3...v0.1.0-beta.4](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.3...v0.1.0-beta.4)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([4dc8a79](https://github.com/langchain-ai/langsmith-java/commit/4dc8a7912f8b1b449ad65c24bdf74b667dde30f4))
|
||||
|
||||
## 0.1.0-beta.3 (2026-05-01)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.2...v0.1.0-beta.3](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.2...v0.1.0-beta.3)
|
||||
|
||||
## 0.1.0-beta.2 (2026-04-30)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.1...v0.1.0-beta.2](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.1...v0.1.0-beta.2)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix another build issue ([#141](https://github.com/langchain-ai/langsmith-java/issues/141)) ([46baa8a](https://github.com/langchain-ai/langsmith-java/commit/46baa8a73e2770e190966d237996135b7fb6b0a3))
|
||||
|
||||
## 0.1.0-beta.1 (2026-04-30)
|
||||
|
||||
Full Changelog: [v0.1.0-beta.0...v0.1.0-beta.1](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.0...v0.1.0-beta.1)
|
||||
|
||||
### Chores
|
||||
|
||||
* Fix build ([#139](https://github.com/langchain-ai/langsmith-java/issues/139)) ([cbaa348](https://github.com/langchain-ai/langsmith-java/commit/cbaa3480cd21c2f90a0056a575e977af1bf906b6))
|
||||
|
||||
## 0.1.0-beta.0 (2026-04-30)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.28...v0.1.0-beta.0](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.28...v0.1.0-beta.0)
|
||||
|
||||
### Features
|
||||
|
||||
* Add autobatch tracing mode ([#134](https://github.com/langchain-ai/langsmith-java/issues/134)) ([230959e](https://github.com/langchain-ai/langsmith-java/commit/230959e124ec705648c4cd1e48da915d945154b8))
|
||||
* Add multipart batching ([#138](https://github.com/langchain-ai/langsmith-java/issues/138)) ([288221c](https://github.com/langchain-ai/langsmith-java/commit/288221c44835d472eb9f0ff59d1f22d0ec150ccb))
|
||||
* Add zstd compression to runs service ([#136](https://github.com/langchain-ai/langsmith-java/issues/136)) ([e412191](https://github.com/langchain-ai/langsmith-java/commit/e4121913a5d5a8a6de2a16e272925c947694e7c0))
|
||||
* **api:** add workspaces ([cc88cdf](https://github.com/langchain-ai/langsmith-java/commit/cc88cdf706b9f2ff7a1e66e00273af027c60f203))
|
||||
* **api:** api update ([c3252b8](https://github.com/langchain-ai/langsmith-java/commit/c3252b8becd6ee89c95dc6ae39bf701d76629e18))
|
||||
* **api:** api update ([af1ba52](https://github.com/langchain-ai/langsmith-java/commit/af1ba5242ec894847e0dc007674681858cb37a42))
|
||||
* **api:** api update ([e1cca2b](https://github.com/langchain-ai/langsmith-java/commit/e1cca2b004858451074865be8b9fb4e532f2f5f5))
|
||||
* **api:** api update ([fd1fc58](https://github.com/langchain-ai/langsmith-java/commit/fd1fc5830a739a88c221e682d1520612c0e45e66))
|
||||
* **api:** api update ([b487509](https://github.com/langchain-ai/langsmith-java/commit/b4875093d10d752fcc8c3757e33d6c2bbe334c15))
|
||||
* **api:** api update ([0758583](https://github.com/langchain-ai/langsmith-java/commit/0758583e5cc42b465c662a2897fff92a6fd61394))
|
||||
* **api:** api update ([1310303](https://github.com/langchain-ai/langsmith-java/commit/131030331c0bab2568498a2bff96112f0fe46fee))
|
||||
* **api:** manual updates ([0be4215](https://github.com/langchain-ai/langsmith-java/commit/0be4215e63ce63ee562ce1c48b001a6848cce704))
|
||||
* **api:** manual updates ([f35b312](https://github.com/langchain-ai/langsmith-java/commit/f35b31249d59265e74d6d33f4e3298fe416757ae))
|
||||
* **api:** sandbox apis ([bd21a1d](https://github.com/langchain-ai/langsmith-java/commit/bd21a1df96b708ac04712aaed7ef1a95505cf1f1))
|
||||
* **evaluators:** add list evaluators (GET /api/v1/runs/rules) ([be32489](https://github.com/langchain-ai/langsmith-java/commit/be324893c09e4ab6e2176dda1d2f6a6cf15413e6))
|
||||
* Make autobatch queue denote batches by payload size, add aggregation delay ([#137](https://github.com/langchain-ai/langsmith-java/issues/137)) ([2fb72ae](https://github.com/langchain-ai/langsmith-java/commit/2fb72ae9681964dd2e798642d88ded506c8189b5))
|
||||
* Merge enqueued posts and patches to optimize batching ([#135](https://github.com/langchain-ai/langsmith-java/issues/135)) ([d65e301](https://github.com/langchain-ai/langsmith-java/commit/d65e3019910bb077ea59056aa37c9d0ef30bfb1a))
|
||||
* support setting headers via env ([567af0c](https://github.com/langchain-ai/langsmith-java/commit/567af0c1eba8bf2dc6648529c11260f36e586bd3))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **deps:** bump the minor-and-patch group across 1 directory with 5 updates ([#123](https://github.com/langchain-ai/langsmith-java/issues/123)) ([1f88790](https://github.com/langchain-ai/langsmith-java/commit/1f887908d8d851dc641905b7beb8cf4f7cf935b1))
|
||||
|
||||
## 0.1.0-alpha.28 (2026-04-14)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.27...v0.1.0-alpha.28](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.27...v0.1.0-alpha.28)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([35484cf](https://github.com/langchain-ai/langsmith-java/commit/35484cf9b1f1ed6642ef857d3fb4aefdb37ae369))
|
||||
|
||||
## 0.1.0-alpha.27 (2026-04-14)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.26...v0.1.0-alpha.27](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.26...v0.1.0-alpha.27)
|
||||
|
||||
## 0.1.0-alpha.26 (2026-04-13)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.25...v0.1.0-alpha.26](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.25...v0.1.0-alpha.26)
|
||||
|
||||
### Features
|
||||
|
||||
* Allow manually specifying parent run tree in traceable config ([#126](https://github.com/langchain-ai/langsmith-java/issues/126)) ([a1409fb](https://github.com/langchain-ai/langsmith-java/commit/a1409fb0d7c0319d4129daea5300578e4371a858))
|
||||
* **api:** api update ([8198ce7](https://github.com/langchain-ai/langsmith-java/commit/8198ce7001d0656bcb4807331d385b0eb8b81a75))
|
||||
* **api:** api update ([286a62f](https://github.com/langchain-ai/langsmith-java/commit/286a62fba8b5bac8105c71dc52b4a36a37012eae))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **deps:** bump gradle-wrapper from 8.12 to 9.4.1 ([#124](https://github.com/langchain-ai/langsmith-java/issues/124)) ([78b7db0](https://github.com/langchain-ai/langsmith-java/commit/78b7db0e1f08c6de8c12af504dd098cf7c958faa))
|
||||
* **deps:** bump gradle/actions ([#121](https://github.com/langchain-ai/langsmith-java/issues/121)) ([e3ca5d7](https://github.com/langchain-ai/langsmith-java/commit/e3ca5d7b712f92ce72586bfd7be208c73cec332f))
|
||||
* **deps:** bump gradle/gradle-build-action ([#122](https://github.com/langchain-ai/langsmith-java/issues/122)) ([d91305f](https://github.com/langchain-ai/langsmith-java/commit/d91305f9157b8d73dfaf8a925c7b2b0e5743f4b2))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* improve examples ([1556365](https://github.com/langchain-ai/langsmith-java/commit/1556365dd6c04f3290b9a836b3c28c09056be5ad))
|
||||
|
||||
## 0.1.0-alpha.25 (2026-03-31)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.24...v0.1.0-alpha.25](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.24...v0.1.0-alpha.25)
|
||||
|
||||
### Features
|
||||
|
||||
* Add `traceable` function wrapper for LangSmith tracing ([#101](https://github.com/langchain-ai/langsmith-java/issues/101)) ([f3bf340](https://github.com/langchain-ai/langsmith-java/commit/f3bf3404fe56be9801806ea44f283eb586d96ce8))
|
||||
* Adds package version resource to build ([#100](https://github.com/langchain-ai/langsmith-java/issues/100)) ([31cc003](https://github.com/langchain-ai/langsmith-java/commit/31cc0037be79a336cd86f6d53c7093f6c3fb7034))
|
||||
* Adds processInputs and processOutputs to traceable ([#113](https://github.com/langchain-ai/langsmith-java/issues/113)) ([ee2991f](https://github.com/langchain-ai/langsmith-java/commit/ee2991f830703f29b3d0eddfd621109362a465e5))
|
||||
* Adds streaming support for wrapOpenAI ([#118](https://github.com/langchain-ai/langsmith-java/issues/118)) ([aae9d2c](https://github.com/langchain-ai/langsmith-java/commit/aae9d2c0dc7f7dae40960113154d9ddff3bd0e06))
|
||||
* Adds support for tracing streams with traceable ([#117](https://github.com/langchain-ai/langsmith-java/issues/117)) ([0217521](https://github.com/langchain-ai/langsmith-java/commit/0217521b852c70dba4795b145b3eb7b16fd2d10f))
|
||||
* **api:** api update ([9182ef8](https://github.com/langchain-ai/langsmith-java/commit/9182ef84e304c602899c73b6865473d3ade4953f))
|
||||
* **api:** api update ([0c91488](https://github.com/langchain-ai/langsmith-java/commit/0c914884473e1de2877708cd92db30fdc8fc10aa))
|
||||
* **api:** api update ([efa16fc](https://github.com/langchain-ai/langsmith-java/commit/efa16fcb5974fbd5500714936cb52a6e930a6181))
|
||||
* **api:** api update ([979e8af](https://github.com/langchain-ai/langsmith-java/commit/979e8af2b6f01444bcea33e1e09d1acbc986a6be))
|
||||
* **api:** api update ([cd9f555](https://github.com/langchain-ai/langsmith-java/commit/cd9f555e525172d5ba0bcb0cd033dc045a9026bd))
|
||||
* **api:** api update ([af93e09](https://github.com/langchain-ai/langsmith-java/commit/af93e09dc9dd8597454b2a02b1ed37de4e71fde4))
|
||||
* **api:** manual updates ([493d5e9](https://github.com/langchain-ai/langsmith-java/commit/493d5e9b2844ff160598b87602c9f91733722fe4))
|
||||
* Rework wrapOpenAI ([#114](https://github.com/langchain-ai/langsmith-java/issues/114)) ([63e0bdc](https://github.com/langchain-ai/langsmith-java/commit/63e0bdc5c893185399065c2c602ed0d5c46bbc68))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix format detection when pulling legacy prompts ([#115](https://github.com/langchain-ai/langsmith-java/issues/115)) ([ea6c3d2](https://github.com/langchain-ai/langsmith-java/commit/ea6c3d29d7c726b7b8455e6e93ec3322330ea5e7))
|
||||
* patch security alerts — bump Jackson and constrain vulnerable transitive deps ([#119](https://github.com/langchain-ai/langsmith-java/issues/119)) ([dfe092c](https://github.com/langchain-ai/langsmith-java/commit/dfe092c51ab27c3131b81785321664a75d584764))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **deps:** bump gradle/actions from 5 to 6 in the all-actions group ([#99](https://github.com/langchain-ai/langsmith-java/issues/99)) ([4ba75e7](https://github.com/langchain-ai/langsmith-java/commit/4ba75e7776499833eddb1959b194b71eb847840f))
|
||||
* fix dependabot.yml posture issues ([#105](https://github.com/langchain-ai/langsmith-java/issues/105)) ([a0e5441](https://github.com/langchain-ai/langsmith-java/commit/a0e544170d0c62e340a6250c32b6b521f818eaa4))
|
||||
|
||||
## 0.1.0-alpha.24 (2026-03-25)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.23...v0.1.0-alpha.24](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.23...v0.1.0-alpha.24)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([8e94d9d](https://github.com/langchain-ai/langsmith-java/commit/8e94d9dc65dfacd3f238e88631623a1338edec2e))
|
||||
* **api:** api update ([3288052](https://github.com/langchain-ai/langsmith-java/commit/32880529fe17d465d20ee922894a9e1af3351edd))
|
||||
* **api:** api update ([a4e3e5c](https://github.com/langchain-ai/langsmith-java/commit/a4e3e5cb858e66a9409ac162f04660c514cc24df))
|
||||
* **api:** api update ([ff16250](https://github.com/langchain-ai/langsmith-java/commit/ff162506f81ad1616567c9119ded36c1a344c774))
|
||||
* **api:** api update ([ef7c7a7](https://github.com/langchain-ai/langsmith-java/commit/ef7c7a798786b073e403d994ee2a9367d4db59ee))
|
||||
* **api:** api update ([643c7b1](https://github.com/langchain-ai/langsmith-java/commit/643c7b121acd874a72c36e80fbd6d8d4aa772319))
|
||||
* **api:** api update ([4b9c97d](https://github.com/langchain-ai/langsmith-java/commit/4b9c97d873492f919885fc1e394d1819fbedd09a))
|
||||
* **api:** api update ([ea4e843](https://github.com/langchain-ai/langsmith-java/commit/ea4e843eea1709113fd259659c2cc85fd6317fa5))
|
||||
* **api:** api update ([74d2131](https://github.com/langchain-ai/langsmith-java/commit/74d213181b7a2398fa8852235de5a1123ff0a7fe))
|
||||
* **api:** api update ([2708533](https://github.com/langchain-ai/langsmith-java/commit/27085335b36f0ecb249a3bd079f96d8d9fad6c1f))
|
||||
* **api:** api update ([8f48677](https://github.com/langchain-ai/langsmith-java/commit/8f48677e5ca0dfd9c9e9bc248c204863f233542a))
|
||||
* **api:** api update ([c11821f](https://github.com/langchain-ai/langsmith-java/commit/c11821f56ba08939568332468a1e9376c78e6372))
|
||||
* **api:** api update ([44fe8b3](https://github.com/langchain-ai/langsmith-java/commit/44fe8b31986af7ae7ac489c0a59335afb8021f9f))
|
||||
* **api:** api update ([d11c759](https://github.com/langchain-ai/langsmith-java/commit/d11c759b1cf0d360f07aa0807f554c36f3262b14))
|
||||
* **api:** manual updates ([09f1302](https://github.com/langchain-ai/langsmith-java/commit/09f1302329d0fcd9082bc52b2381271aabd71bab))
|
||||
* **api:** manual updates ([dbed70a](https://github.com/langchain-ai/langsmith-java/commit/dbed70af760f0e807626650a2a7d6ecb8d30f0ad))
|
||||
* **api:** manual updates ([48e2ca2](https://github.com/langchain-ai/langsmith-java/commit/48e2ca294bd412398290777db3ef14655ed47b1c))
|
||||
* **api:** manual updates ([d8ac870](https://github.com/langchain-ai/langsmith-java/commit/d8ac87089db42004a53e880f419dcf6749c60817))
|
||||
* **api:** manual updates ([bdb35f0](https://github.com/langchain-ai/langsmith-java/commit/bdb35f01f6374860fe6fa3c83f6f102f22590b23))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** allow updating header/query affecting fields in `toBuilder()` ([5069881](https://github.com/langchain-ai/langsmith-java/commit/5069881aebcffc8ea2058de667a7af33efc15ec3))
|
||||
* **client:** incorrect `Retry-After` parsing ([b607264](https://github.com/langchain-ai/langsmith-java/commit/b607264e2c7e10374f5d79d922c6850208b6f839))
|
||||
* fix streaming in java sdk ([#95](https://github.com/langchain-ai/langsmith-java/issues/95)) ([76da93f](https://github.com/langchain-ai/langsmith-java/commit/76da93f2c0c3a0466feee196243a214e8642b303))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* align user agent format ([#96](https://github.com/langchain-ai/langsmith-java/issues/96)) ([5cf06d6](https://github.com/langchain-ai/langsmith-java/commit/5cf06d612963f01b49d6f62117226d948e004046))
|
||||
* **ci:** skip lint on metadata-only changes ([4c1197c](https://github.com/langchain-ai/langsmith-java/commit/4c1197ce483f317defed85f81d3ec3f5053f68d3))
|
||||
* **ci:** skip uploading artifacts on stainless-internal branches ([c994122](https://github.com/langchain-ai/langsmith-java/commit/c994122d8c4eea78bb8a942b0ed1db58e6338e1e))
|
||||
* **deps:** bump the all-actions group with 5 updates ([#92](https://github.com/langchain-ai/langsmith-java/issues/92)) ([3a1a72f](https://github.com/langchain-ai/langsmith-java/commit/3a1a72f5595393a437a8ddf1acc69bb44529b0e8))
|
||||
* drop apache dependency ([75ca9d7](https://github.com/langchain-ai/langsmith-java/commit/75ca9d71b49bad02bdbd66141f693204b296a9ec))
|
||||
* **internal:** bump ktfmt ([d9a4a39](https://github.com/langchain-ai/langsmith-java/commit/d9a4a396ad92c9f33c5922dabc00a8ba2451199b))
|
||||
* **internal:** bump palantir-java-format ([576ebd2](https://github.com/langchain-ai/langsmith-java/commit/576ebd2a02613c632dd1e00ba9c606df99de8f37))
|
||||
* **internal:** codegen related update ([29d55b0](https://github.com/langchain-ai/langsmith-java/commit/29d55b0ee9f40c2d327f1c08a714ccd9cad1384b))
|
||||
* **internal:** expand imports ([4c7542f](https://github.com/langchain-ai/langsmith-java/commit/4c7542f4525b43d8fc2adcd7d084a080581b5c48))
|
||||
* **internal:** tweak CI branches ([b7cecc1](https://github.com/langchain-ai/langsmith-java/commit/b7cecc1caa774426df72acd5734a2c29a96a3e60))
|
||||
* **internal:** update gitignore ([d0df3fa](https://github.com/langchain-ai/langsmith-java/commit/d0df3fa13a923514d9d3c66d5d96b4fa1fea4a22))
|
||||
* **internal:** update retry delay tests ([a9c7999](https://github.com/langchain-ai/langsmith-java/commit/a9c7999f55bc12c2d1bac268ce3a2ca5bbd58dc5))
|
||||
* make `Properties` more resilient to `null` ([47c70f9](https://github.com/langchain-ai/langsmith-java/commit/47c70f91d5ec941fafc08a81baabf946602076c9))
|
||||
* remove old test ([#97](https://github.com/langchain-ai/langsmith-java/issues/97)) ([79d708b](https://github.com/langchain-ai/langsmith-java/commit/79d708b935ba234c50d10980a7fef8ad12c58f61))
|
||||
* update placeholder string ([be1baa7](https://github.com/langchain-ai/langsmith-java/commit/be1baa704ad8ebe899fbf8e4003f706fbc3d3f73))
|
||||
|
||||
## 0.1.0-alpha.23 (2026-02-20)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.22...v0.1.0-alpha.23](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.22...v0.1.0-alpha.23)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([9485112](https://github.com/langchain-ai/langsmith-java/commit/948511234707031e0e375ded4ed51cd142d0dae1))
|
||||
* **api:** api update ([786edff](https://github.com/langchain-ai/langsmith-java/commit/786edfff5f8e200c20fb201d83daff8e0fe33b59))
|
||||
* **api:** api update ([3a6c191](https://github.com/langchain-ai/langsmith-java/commit/3a6c1911a5127afe550cc6ff0cbc2182e7e9cce4))
|
||||
* **api:** api update ([cd9f91d](https://github.com/langchain-ai/langsmith-java/commit/cd9f91d07e8e0c1c7d13f7a6a849856a9da19d90))
|
||||
* **api:** api update ([b344828](https://github.com/langchain-ai/langsmith-java/commit/b3448285414b13fe8081c7b7eae023e033ad6ad2))
|
||||
* **api:** api update ([2e0602a](https://github.com/langchain-ai/langsmith-java/commit/2e0602a9e218f3c9c435c7d1e254621d8f97f14b))
|
||||
* **api:** api update ([3b1e4c6](https://github.com/langchain-ai/langsmith-java/commit/3b1e4c61e3b8ff44f0415aa808e6a7f550a7834b))
|
||||
* **api:** api update ([703b77a](https://github.com/langchain-ai/langsmith-java/commit/703b77ac2c34ed600964d0c815b1fa759124756c))
|
||||
* **api:** api update ([7d0f1d4](https://github.com/langchain-ai/langsmith-java/commit/7d0f1d4e07b0bd50a92947124e2bd1b83b840605))
|
||||
* **api:** api update ([debadf0](https://github.com/langchain-ai/langsmith-java/commit/debadf0da6c073a569e1abea1c3b58fc8e33bcf4))
|
||||
* **api:** api update ([9ebd84f](https://github.com/langchain-ai/langsmith-java/commit/9ebd84fe982b0a0304d1fcb7da168df0c97a043f))
|
||||
* **api:** api update ([88bd850](https://github.com/langchain-ai/langsmith-java/commit/88bd85062b8c87a72f96d6fbac738963c1df562e))
|
||||
* **api:** api update ([af7bf93](https://github.com/langchain-ai/langsmith-java/commit/af7bf93c5b285d509484c5f8cb838583dd0ed7e8))
|
||||
* **api:** manual updates ([3f1d46e](https://github.com/langchain-ai/langsmith-java/commit/3f1d46e06a3616cb5fa22f3f89d13a0ff871c6c3))
|
||||
* **api:** manual updates ([2ac51c9](https://github.com/langchain-ai/langsmith-java/commit/2ac51c9075348a4fd8e38dace4911d8a6bee2000))
|
||||
* **api:** manual updates ([187c87f](https://github.com/langchain-ai/langsmith-java/commit/187c87f96a08b4c197b37232081c38797b8d59cc))
|
||||
* **api:** manual updates ([5e0db72](https://github.com/langchain-ai/langsmith-java/commit/5e0db72d55d10c512dc0b0d4997968d2382dc32d))
|
||||
* **api:** manual updates ([6bc9970](https://github.com/langchain-ai/langsmith-java/commit/6bc9970850b2ce9206eefa770c9e4e8164487745))
|
||||
* **client:** add connection pooling option ([9acbf93](https://github.com/langchain-ai/langsmith-java/commit/9acbf9334972d4ff5d00caa346e3907ded7d3a72))
|
||||
* **client:** add more convenience service method overloads ([fba86c8](https://github.com/langchain-ai/langsmith-java/commit/fba86c83fdbe3ad353ec9a5db065464ac0928e55))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **api:** minor updates ([5561ef0](https://github.com/langchain-ai/langsmith-java/commit/5561ef0a24492e78eab34609a48da09aa37496a1))
|
||||
* **ci:** upgrade `actions/github-script` ([f758afb](https://github.com/langchain-ai/langsmith-java/commit/f758afb6d78e7ac01b085403f89ec33dbb0caa6c))
|
||||
* **internal:** allow passing args to `./scripts/test` ([206af30](https://github.com/langchain-ai/langsmith-java/commit/206af3009b966f77ad4aec77a975b38ebafbbbde))
|
||||
* **internal:** make `OkHttp` constructor internal ([b55c1e8](https://github.com/langchain-ai/langsmith-java/commit/b55c1e8e15bbb1c370ab4d8b4713d37965b2f4db))
|
||||
* **internal:** remove mock server code ([f72c72d](https://github.com/langchain-ai/langsmith-java/commit/f72c72d36f8c390bfbcbd14cdf25cf25d8131a7f))
|
||||
* **internal:** update `TestServerExtension` comment ([c4956f5](https://github.com/langchain-ai/langsmith-java/commit/c4956f59bae794a6da689eb891ed329ad6bac082))
|
||||
* **internal:** upgrade AssertJ ([2535a3a](https://github.com/langchain-ai/langsmith-java/commit/2535a3ab58df98b740bee627f7fecd0f88adb05c))
|
||||
* refactor ([#90](https://github.com/langchain-ai/langsmith-java/issues/90)) ([1902c22](https://github.com/langchain-ai/langsmith-java/commit/1902c228ddd2b4ddcc5b741bf7c67f250ae9da06))
|
||||
* update mock server docs ([1704b40](https://github.com/langchain-ai/langsmith-java/commit/1704b408f7fe5af7ee8fa322f037a37749e22d05))
|
||||
|
||||
## 0.1.0-alpha.22 (2026-01-23)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.21...v0.1.0-alpha.22](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.21...v0.1.0-alpha.22)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([5fe36e6](https://github.com/langchain-ai/langsmith-java/commit/5fe36e66be0d8a4118d631794c5f70f443c88dac))
|
||||
* **client:** send `X-Stainless-Kotlin-Version` header ([748e689](https://github.com/langchain-ai/langsmith-java/commit/748e6890d3560b3f373aa07031bc8b2e6fa288f8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** disallow coercion from float to int ([bc01e05](https://github.com/langchain-ai/langsmith-java/commit/bc01e05032fb90f0318bde2107978ccf93eaa330))
|
||||
* **client:** fully respect max retries ([07ba637](https://github.com/langchain-ai/langsmith-java/commit/07ba63741949f3e159314c5556902e7a3fb93def))
|
||||
* **client:** preserve time zone in lenient date-time parsing ([498395e](https://github.com/langchain-ai/langsmith-java/commit/498395ea719057dd6d5bbef7f38fc1206ffc6d04))
|
||||
* **client:** send retry count header for max retries 0 ([07ba637](https://github.com/langchain-ai/langsmith-java/commit/07ba63741949f3e159314c5556902e7a3fb93def))
|
||||
* date time deserialization leniency ([ac7a559](https://github.com/langchain-ai/langsmith-java/commit/ac7a559ad9704bac385a49c724b0d86704d0da4f))
|
||||
* deserialization order ([71f380d](https://github.com/langchain-ai/langsmith-java/commit/71f380d5b09e54f6a0f0d2b703c2e3e2cc4e4766))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **ci:** upgrade `actions/setup-java` ([ee1ed58](https://github.com/langchain-ai/langsmith-java/commit/ee1ed58ada45c071662c61e700b473ee471660a1))
|
||||
* **internal:** clean up maven repo artifact script and add html documentation to repo root ([85d8771](https://github.com/langchain-ai/langsmith-java/commit/85d877129ea7e78b889b4d28f6ea06bcefacfce1))
|
||||
* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([1cd0cf2](https://github.com/langchain-ai/langsmith-java/commit/1cd0cf2d27d3c87c6196d1492a6c6a8b3562ba49))
|
||||
* **internal:** depend on packages directly in example ([07ba637](https://github.com/langchain-ai/langsmith-java/commit/07ba63741949f3e159314c5556902e7a3fb93def))
|
||||
* **internal:** improve maven repo docs ([d928145](https://github.com/langchain-ai/langsmith-java/commit/d92814500d00759eb188ab19bfc036b90022c782))
|
||||
* **internal:** update `actions/checkout` version ([70785ca](https://github.com/langchain-ai/langsmith-java/commit/70785ca2b98445faebb66ef7a54223d5b3ea9e31))
|
||||
* **internal:** update maven repo doc to include authentication ([2900175](https://github.com/langchain-ai/langsmith-java/commit/29001754d9b523a9bdb76a7c9650991284fbb761))
|
||||
* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/langchain-ai/langsmith-java/issues/3240) in tests ([ac7a559](https://github.com/langchain-ai/langsmith-java/commit/ac7a559ad9704bac385a49c724b0d86704d0da4f))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add comment for arbitrary value fields ([642227b](https://github.com/langchain-ai/langsmith-java/commit/642227ba6b07a16d3f3287f585077afbc2684573))
|
||||
|
||||
## 0.1.0-alpha.21 (2026-01-14)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.20...v0.1.0-alpha.21](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.20...v0.1.0-alpha.21)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([d5e92e1](https://github.com/langchain-ai/langsmith-java/commit/d5e92e102d1dd770ae1205dc52d5d7f0a0692dcb))
|
||||
* **api:** api update ([591546c](https://github.com/langchain-ai/langsmith-java/commit/591546c935e7e10e9521310870af83c8bd67ebd4))
|
||||
* **api:** api update ([440ea8a](https://github.com/langchain-ai/langsmith-java/commit/440ea8abf0a22148c636f06f5a4c778e20b947e9))
|
||||
* **api:** manual updates ([ee36c44](https://github.com/langchain-ai/langsmith-java/commit/ee36c44834da0d371168b876b7f9619c281f749e))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **internal:** support uploading Maven repo artifacts to stainless package server ([d1b01b9](https://github.com/langchain-ai/langsmith-java/commit/d1b01b9073fc2db7643fa07de2d421088218f4a3))
|
||||
|
||||
## 0.1.0-alpha.20 (2026-01-12)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.19...v0.1.0-alpha.20](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.19...v0.1.0-alpha.20)
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to the LangSmith Java SDK!
|
||||
|
||||
## Overview
|
||||
|
||||
This SDK is **auto-generated by [Stainless](https://www.stainless.com/)** from our OpenAPI spec. Custom code (prompts, OTel wrappers, etc.) lives alongside generated code — Stainless is configured to [preserve custom code](https://www.stainless.com/docs/sdks/configure/custom-code/) and never overwrite it.
|
||||
|
||||
> **Important:** Most files in the SDK are generated. If you need to change generated code, talk to @edongashi — there may be weird errors in the Stainless config, but they're generally fixable.
|
||||
|
||||
## Branching and workflow
|
||||
|
||||
| Branch | Purpose |
|
||||
| ----------------------------------------------- | ----------------------------------------------------- |
|
||||
| `next` | **Development branch.** All PRs should target `next`. |
|
||||
| `main` | Release branch. Only updated via release-please. |
|
||||
| `release-please--branches--main--changes--next` | Auto-generated release PR. Merges `next` → `main`. |
|
||||
|
||||
**Always work off `next`.** Do not merge directly to `main` — Dependabot or other bots occasionally violate this, which can cause headaches.
|
||||
|
||||
### Release process
|
||||
|
||||
1. Merge your PR into `next`.
|
||||
2. A release-please PR is automatically created/updated (`release-please--branches--main--changes--next`).
|
||||
3. Verify your change appears in the release PR diff and description.
|
||||
4. Approve the release PR — it auto-merges on approval.
|
||||
5. A GitHub Action publishes to Maven Central via `./gradlew publishAndReleaseToMavenCentral`.
|
||||
6. Maven Central can be slow to index — verify on [repo1.maven.org](https://repo1.maven.org/maven2/com/langchain/smith/langsmith-java/) rather than [mvnrepository.com](https://mvnrepository.com/artifact/com.langchain.smith/langsmith-java), which lags behind.
|
||||
|
||||
> **Note:** If lint fixes or other changes are pushed to the release-please branch after a version bump, the publish action can fail. Always ensure CI is green before approving.
|
||||
|
||||
## Development setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 8+ (tested against 8 and 21)
|
||||
- Gradle (uses the included `gradlew` wrapper)
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
./scripts/build
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
The project uses [ktfmt](https://github.com/facebook/ktfmt) (`--kotlinlang-style`) for Kotlin and [Palantir Java Format](https://github.com/palantir/palantir-java-format) for Java.
|
||||
|
||||
```bash
|
||||
# Format all files
|
||||
./scripts/format
|
||||
|
||||
# Format only Kotlin
|
||||
./gradlew formatKotlin
|
||||
|
||||
# Format a single module
|
||||
./gradlew :langsmith-java-core:formatKotlin
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Lint all files
|
||||
./scripts/lint
|
||||
|
||||
# Lint only Kotlin
|
||||
./gradlew lintKotlin
|
||||
```
|
||||
|
||||
CI blocks on lint failures. Always run `./scripts/format` before pushing.
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./scripts/test
|
||||
|
||||
# Run specific tests
|
||||
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.*"
|
||||
|
||||
# Force re-run (skip Gradle cache)
|
||||
./gradlew :langsmith-java-core:test --tests "..." --rerun
|
||||
|
||||
# See stdout output
|
||||
./gradlew :langsmith-java-core:test --tests "..." --rerun --info
|
||||
```
|
||||
|
||||
#### Integration tests
|
||||
|
||||
Integration tests require API keys and skip gracefully via `assumeTrue` if they're missing:
|
||||
|
||||
```bash
|
||||
export LANGSMITH_API_KEY="lsv2_pt_..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
## CI
|
||||
|
||||
CI runs three jobs on every push and PR:
|
||||
|
||||
1. **lint** — `./scripts/lint`
|
||||
2. **build** — `./scripts/build`
|
||||
3. **test** — `./scripts/test`
|
||||
|
||||
All three must pass before merging.
|
||||
|
||||
## Code style
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for detailed Kotlin conventions, including:
|
||||
|
||||
- Prefer `buildMap`/`buildList` over mutable collections
|
||||
- Use `partition` instead of double `filter`
|
||||
- Use parameterized tests for table-driven cases
|
||||
- Avoid `@Suppress("UNCHECKED_CAST")` — restructure with safe casts
|
||||
- `toString()` should follow the `ClassName{field=value}` convention
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Anthropic SDK** is a `compileOnly` dependency — users must add it themselves. Methods using Anthropic types should catch `NoClassDefFoundError` and throw `IllegalStateException` with a clear message.
|
||||
- **SLF4J** (`slf4j-api`) is an `api` dependency for logging. Tests use `slf4j-simple`.
|
||||
- **Jackson** 2.22.0 is the default, but the SDK is compatible with 2.13.4+.
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
[](https://central.sonatype.com/artifact/com.langchain.smith/langsmith-java/0.1.0-alpha.20)
|
||||
[](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-alpha.20)
|
||||
[](https://central.sonatype.com/artifact/com.langchain.smith/langsmith-java/0.1.0-beta.19)
|
||||
[](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-beta.18)
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
@@ -13,7 +13,7 @@ To learn more about LangSmith, check out the [docs](https://docs.smith.langchain
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
The REST API documentation can be found on [docs.smith.langchain.com](https://docs.smith.langchain.com/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-alpha.20).
|
||||
The REST API documentation can be found on [docs.smith.langchain.com](https://docs.smith.langchain.com/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-beta.19).
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
@@ -24,7 +24,7 @@ The REST API documentation can be found on [docs.smith.langchain.com](https://do
|
||||
### Gradle
|
||||
|
||||
```kotlin
|
||||
implementation("com.langchain.smith:langsmith-java:0.1.0-alpha.20")
|
||||
implementation("com.langchain.smith:langsmith-java:0.1.0-beta.19")
|
||||
```
|
||||
|
||||
### Maven
|
||||
@@ -33,7 +33,7 @@ implementation("com.langchain.smith:langsmith-java:0.1.0-alpha.20")
|
||||
<dependency>
|
||||
<groupId>com.langchain.smith</groupId>
|
||||
<artifactId>langsmith-java</artifactId>
|
||||
<version>0.1.0-alpha.20</version>
|
||||
<version>0.1.0-beta.19</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
@@ -48,25 +48,17 @@ This library requires Java 8 or later.
|
||||
```java
|
||||
import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
import com.langchain.smith.models.runs.RunQueryParams;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
LangsmithClient client = LangsmithOkHttpClient.fromEnv();
|
||||
|
||||
RunQueryParams params = RunQueryParams.builder()
|
||||
.addSession("1ffaeba7-541e-469f-bae7-df3208ea3d45")
|
||||
.limit(10L)
|
||||
RunQueryV2Params params = RunQueryV2Params.builder()
|
||||
.addProjectId("00000000-0000-0000-0000-000000000000")
|
||||
.build();
|
||||
var response = client.runs().query(params);
|
||||
|
||||
// Print runs
|
||||
System.out.println("Found " + response.runs().size() + " runs:");
|
||||
for (var run : response.runs()) {
|
||||
System.out.println("Run ID: " + run.id());
|
||||
System.out.println("Run Name: " + run.name());
|
||||
System.out.println("---");
|
||||
}
|
||||
RunQueryV2Page page = client.runs().queryV2(params);
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -103,8 +95,8 @@ Configure the client using system properties or environment variables:
|
||||
import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
LangsmithClient client = LangsmithOkHttpClient.fromEnv();
|
||||
```
|
||||
|
||||
@@ -117,7 +109,6 @@ import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
.apiKey("My API Key")
|
||||
.tenantId("My Tenant ID")
|
||||
.organizationId("My Organization ID")
|
||||
.build();
|
||||
```
|
||||
|
||||
@@ -128,8 +119,8 @@ import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
|
||||
LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
.fromEnv()
|
||||
.apiKey("My API Key")
|
||||
.build();
|
||||
@@ -137,13 +128,11 @@ LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
|
||||
See this table for the available options:
|
||||
|
||||
| Setter | System property | Environment variable | Required | Default value |
|
||||
| ---------------- | ----------------------------------- | --------------------------- | -------- | ------------------------------------ |
|
||||
| `apiKey` | `langchain.langsmithApiKey` | `LANGSMITH_API_KEY` | false | - |
|
||||
| `tenantId` | `langchain.langsmithTenantId` | `LANGSMITH_TENANT_ID` | false | - |
|
||||
| `bearerToken` | `langchain.langsmithBearerToken` | `LANGSMITH_BEARER_TOKEN` | false | - |
|
||||
| `organizationId` | `langchain.langsmithOrganizationId` | `LANGSMITH_ORGANIZATION_ID` | false | - |
|
||||
| `baseUrl` | `langchain.baseUrl` | `LANGSMITH_ENDPOINT` | true | `"https://api.smith.langchain.com/"` |
|
||||
| Setter | System property | Environment variable | Required | Default value |
|
||||
| ---------- | ----------------------------- | --------------------- | -------- | ------------------------------------ |
|
||||
| `apiKey` | `langchain.langsmithApiKey` | `LANGSMITH_API_KEY` | false | - |
|
||||
| `tenantId` | `langchain.langsmithTenantId` | `LANGSMITH_TENANT_ID` | false | - |
|
||||
| `baseUrl` | `langchain.baseUrl` | `LANGSMITH_ENDPOINT` | true | `"https://api.smith.langchain.com/"` |
|
||||
|
||||
System properties take precedence over environment variables.
|
||||
|
||||
@@ -170,7 +159,7 @@ The `withOptions()` method does not affect the original client or service.
|
||||
|
||||
To send a request to the LangChain API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
|
||||
|
||||
For example, `client.sessions().dashboard(...)` should be called with an instance of `SessionDashboardParams`, and it will return an instance of `CustomChartsSection`.
|
||||
For example, `client.runs().queryV2(...)` should be called with an instance of `RunQueryV2Params`, and it will return an instance of `RunQueryV2Page`.
|
||||
|
||||
## Immutability
|
||||
|
||||
@@ -187,20 +176,18 @@ The default client is synchronous. To switch to asynchronous execution, call the
|
||||
```java
|
||||
import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
|
||||
import com.langchain.smith.models.sessions.SessionDashboardParams;
|
||||
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
LangsmithClient client = LangsmithOkHttpClient.fromEnv();
|
||||
|
||||
SessionDashboardParams params = SessionDashboardParams.builder()
|
||||
.sessionId("1ffaeba7-541e-469f-bae7-df3208ea3d45")
|
||||
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
|
||||
RunQueryV2Params params = RunQueryV2Params.builder()
|
||||
.addProjectId("00000000-0000-0000-0000-000000000000")
|
||||
.build();
|
||||
CompletableFuture<CustomChartsSection> customChartsSection = client.async().sessions().dashboard(params);
|
||||
CompletableFuture<RunQueryV2PageAsync> page = client.async().runs().queryV2(params);
|
||||
```
|
||||
|
||||
Or create an asynchronous client from the beginning:
|
||||
@@ -208,20 +195,18 @@ Or create an asynchronous client from the beginning:
|
||||
```java
|
||||
import com.langchain.smith.client.LangsmithClientAsync;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClientAsync;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
|
||||
import com.langchain.smith.models.sessions.SessionDashboardParams;
|
||||
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
|
||||
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
|
||||
LangsmithClientAsync client = LangsmithOkHttpClientAsync.fromEnv();
|
||||
|
||||
SessionDashboardParams params = SessionDashboardParams.builder()
|
||||
.sessionId("1ffaeba7-541e-469f-bae7-df3208ea3d45")
|
||||
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
|
||||
RunQueryV2Params params = RunQueryV2Params.builder()
|
||||
.addProjectId("00000000-0000-0000-0000-000000000000")
|
||||
.build();
|
||||
CompletableFuture<CustomChartsSection> customChartsSection = client.sessions().dashboard(params);
|
||||
CompletableFuture<RunQueryV2PageAsync> page = client.runs().queryV2(params);
|
||||
```
|
||||
|
||||
The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
|
||||
@@ -303,26 +288,24 @@ To access this data, prefix any HTTP method call on a client or service with `wi
|
||||
```java
|
||||
import com.langchain.smith.core.http.Headers;
|
||||
import com.langchain.smith.core.http.HttpResponseFor;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
|
||||
import com.langchain.smith.models.sessions.SessionDashboardParams;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
|
||||
SessionDashboardParams params = SessionDashboardParams.builder()
|
||||
.sessionId("1ffaeba7-541e-469f-bae7-df3208ea3d45")
|
||||
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
|
||||
RunQueryV2Params params = RunQueryV2Params.builder()
|
||||
.addProjectId("00000000-0000-0000-0000-000000000000")
|
||||
.build();
|
||||
HttpResponseFor<CustomChartsSection> customChartsSection = client.sessions().withRawResponse().dashboard(params);
|
||||
HttpResponseFor<RunQueryV2Page> page = client.runs().withRawResponse().queryV2(params);
|
||||
|
||||
int statusCode = customChartsSection.statusCode();
|
||||
Headers headers = customChartsSection.headers();
|
||||
int statusCode = page.statusCode();
|
||||
Headers headers = page.headers();
|
||||
```
|
||||
|
||||
You can still deserialize the response into an instance of a Java class if needed:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
CustomChartsSection parsedCustomChartsSection = customChartsSection.parse();
|
||||
RunQueryV2Page parsedPage = page.parse();
|
||||
```
|
||||
|
||||
## Error handling
|
||||
@@ -361,43 +344,43 @@ To iterate through all results across all pages, use the `autoPager()` method, w
|
||||
When using the synchronous client, the method returns an [`Iterable`](https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html)
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.datasets.Dataset;
|
||||
import com.langchain.smith.models.datasets.DatasetListPage;
|
||||
import com.langchain.smith.models.runs.Run;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
DatasetListPage page = client.datasets().list();
|
||||
RunQueryV2Page page = client.runs().queryV2();
|
||||
|
||||
// Process as an Iterable
|
||||
for (Dataset dataset : page.autoPager()) {
|
||||
System.out.println(dataset);
|
||||
for (Run run : page.autoPager()) {
|
||||
System.out.println(run);
|
||||
}
|
||||
|
||||
// Process as a Stream
|
||||
page.autoPager()
|
||||
.stream()
|
||||
.limit(50)
|
||||
.forEach(dataset -> System.out.println(dataset));
|
||||
.forEach(run -> System.out.println(run));
|
||||
```
|
||||
|
||||
When using the asynchronous client, the method returns an [`AsyncStreamResponse`](langsmith-java-core/src/main/kotlin/com/langchain/smith/core/http/AsyncStreamResponse.kt):
|
||||
|
||||
```java
|
||||
import com.langchain.smith.core.http.AsyncStreamResponse;
|
||||
import com.langchain.smith.models.datasets.Dataset;
|
||||
import com.langchain.smith.models.datasets.DatasetListPageAsync;
|
||||
import com.langchain.smith.models.runs.Run;
|
||||
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
CompletableFuture<DatasetListPageAsync> pageFuture = client.async().datasets().list();
|
||||
CompletableFuture<RunQueryV2PageAsync> pageFuture = client.async().runs().queryV2();
|
||||
|
||||
pageFuture.thenRun(page -> page.autoPager().subscribe(dataset -> {
|
||||
System.out.println(dataset);
|
||||
pageFuture.thenRun(page -> page.autoPager().subscribe(run -> {
|
||||
System.out.println(run);
|
||||
}));
|
||||
|
||||
// If you need to handle errors or completion of the stream
|
||||
pageFuture.thenRun(page -> page.autoPager().subscribe(new AsyncStreamResponse.Handler<>() {
|
||||
@Override
|
||||
public void onNext(Dataset dataset) {
|
||||
System.out.println(dataset);
|
||||
public void onNext(Run run) {
|
||||
System.out.println(run);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -413,8 +396,8 @@ pageFuture.thenRun(page -> page.autoPager().subscribe(new AsyncStreamResponse.Ha
|
||||
|
||||
// Or use futures
|
||||
pageFuture.thenRun(page -> page.autoPager()
|
||||
.subscribe(dataset -> {
|
||||
System.out.println(dataset);
|
||||
.subscribe(run -> {
|
||||
System.out.println(run);
|
||||
})
|
||||
.onCompleteFuture()
|
||||
.whenComplete((unused, error) -> {
|
||||
@@ -433,13 +416,13 @@ To access individual page items and manually request the next page, use the `ite
|
||||
`hasNextPage()`, and `nextPage()` methods:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.datasets.Dataset;
|
||||
import com.langchain.smith.models.datasets.DatasetListPage;
|
||||
import com.langchain.smith.models.runs.Run;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
DatasetListPage page = client.datasets().list();
|
||||
RunQueryV2Page page = client.runs().queryV2();
|
||||
while (true) {
|
||||
for (Dataset dataset : page.items()) {
|
||||
System.out.println(dataset);
|
||||
for (Run run : page.items()) {
|
||||
System.out.println(run);
|
||||
}
|
||||
|
||||
if (!page.hasNextPage()) {
|
||||
@@ -452,8 +435,6 @@ while (true) {
|
||||
|
||||
## Logging
|
||||
|
||||
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
|
||||
|
||||
Enable logging by setting the `LANGCHAIN_LOG` environment variable to `info`:
|
||||
|
||||
```sh
|
||||
@@ -466,6 +447,19 @@ Or to `debug` for more verbose logging:
|
||||
export LANGCHAIN_LOG=debug
|
||||
```
|
||||
|
||||
Or configure the client manually using the `logLevel` method:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
import com.langchain.smith.core.LogLevel;
|
||||
|
||||
LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
.fromEnv()
|
||||
.logLevel(LogLevel.INFO)
|
||||
.build();
|
||||
```
|
||||
|
||||
## ProGuard and R8
|
||||
|
||||
Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `langsmith-java-core` is published with a [configuration file](langsmith-java-core/src/main/resources/META-INF/proguard/langsmith-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
|
||||
@@ -474,7 +468,7 @@ ProGuard and R8 should automatically detect and use the published rules, but you
|
||||
|
||||
## Jackson
|
||||
|
||||
The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
|
||||
The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.22.0 by default.
|
||||
|
||||
The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
|
||||
|
||||
@@ -483,6 +477,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
|
||||
> [!CAUTION]
|
||||
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
|
||||
|
||||
Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead.
|
||||
|
||||
## Network options
|
||||
|
||||
### Retries
|
||||
@@ -518,11 +514,9 @@ Requests time out after 90 seconds by default.
|
||||
To set a custom timeout, configure the method call using the `timeout` method:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
CustomChartsSection customChartsSection = client.sessions().dashboard(
|
||||
params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
|
||||
);
|
||||
RunQueryV2Page page = client.runs().queryV2(RequestOptions.builder().timeout(Duration.ofSeconds(30)).build());
|
||||
```
|
||||
|
||||
Or configure the default for all method calls at the client level:
|
||||
@@ -558,6 +552,40 @@ LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
.build();
|
||||
```
|
||||
|
||||
If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
import com.langchain.smith.core.http.ProxyAuthenticator;
|
||||
|
||||
LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
.fromEnv()
|
||||
.proxy(...)
|
||||
// Or a custom implementation of `ProxyAuthenticator`.
|
||||
.proxyAuthenticator(ProxyAuthenticator.basic("username", "password"))
|
||||
.build();
|
||||
```
|
||||
|
||||
### Connection pooling
|
||||
|
||||
To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.client.LangsmithClient;
|
||||
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
.fromEnv()
|
||||
// If `maxIdleConnections` is set, then `keepAliveDuration` must be set, and vice versa.
|
||||
.maxIdleConnections(10)
|
||||
.keepAliveDuration(Duration.ofMinutes(2))
|
||||
.build();
|
||||
```
|
||||
|
||||
If both options are unset, OkHttp's default connection pool settings are used.
|
||||
|
||||
### HTTPS
|
||||
|
||||
> [!NOTE]
|
||||
@@ -625,9 +653,9 @@ To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQu
|
||||
|
||||
```java
|
||||
import com.langchain.smith.core.JsonValue;
|
||||
import com.langchain.smith.models.sessions.SessionDashboardParams;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
|
||||
SessionDashboardParams params = SessionDashboardParams.builder()
|
||||
RunQueryV2Params params = RunQueryV2Params.builder()
|
||||
.putAdditionalHeader("Secret-Header", "42")
|
||||
.putAdditionalQueryParam("secret_query_param", "42")
|
||||
.putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
|
||||
@@ -639,11 +667,11 @@ These can be accessed on the built object later using the `_additionalHeaders()`
|
||||
To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](langsmith-java-core/src/main/kotlin/com/langchain/smith/core/Values.kt) object to its setter:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
|
||||
import com.langchain.smith.models.sessions.SessionDashboardParams;
|
||||
import com.langchain.smith.core.JsonValue;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
|
||||
SessionDashboardParams params = SessionDashboardParams.builder()
|
||||
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
|
||||
RunQueryV2Params params = RunQueryV2Params.builder()
|
||||
.projectIds(JsonValue.from(42))
|
||||
.build();
|
||||
```
|
||||
|
||||
@@ -692,11 +720,10 @@ To forcibly omit a required parameter or property, pass [`JsonMissing`](langsmit
|
||||
|
||||
```java
|
||||
import com.langchain.smith.core.JsonMissing;
|
||||
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
|
||||
import com.langchain.smith.models.sessions.SessionDashboardParams;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Params;
|
||||
import com.langchain.smith.models.sessions.SessionRetrieveParams;
|
||||
|
||||
SessionDashboardParams params = SessionDashboardParams.builder()
|
||||
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
|
||||
RunQueryV2Params params = SessionRetrieveParams.builder()
|
||||
.sessionId(JsonMissing.of())
|
||||
.build();
|
||||
```
|
||||
@@ -709,7 +736,7 @@ To access undocumented response properties, call the `_additionalProperties()` m
|
||||
import com.langchain.smith.core.JsonValue;
|
||||
import java.util.Map;
|
||||
|
||||
Map<String, JsonValue> additionalProperties = client.sessions().dashboard(params)._additionalProperties();
|
||||
Map<String, JsonValue> additionalProperties = client.sessions().create(params)._additionalProperties();
|
||||
JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
|
||||
|
||||
String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
|
||||
@@ -739,19 +766,19 @@ To access a property's raw JSON value, which may be undocumented, call its `_` p
|
||||
import com.langchain.smith.core.JsonField;
|
||||
import java.util.Optional;
|
||||
|
||||
JsonField<Object> field = client.sessions().dashboard(params)._field();
|
||||
JsonField<String> id = client.sessions().create(params)._id();
|
||||
|
||||
if (field.isMissing()) {
|
||||
if (id.isMissing()) {
|
||||
// The property is absent from the JSON response
|
||||
} else if (field.isNull()) {
|
||||
} else if (id.isNull()) {
|
||||
// The property was set to literal null
|
||||
} else {
|
||||
// Check if value was provided as a string
|
||||
// Other methods include `asNumber()`, `asBoolean()`, etc.
|
||||
Optional<String> jsonString = field.asString();
|
||||
Optional<String> jsonString = id.asString();
|
||||
|
||||
// Try to deserialize into a custom type
|
||||
MyClass myObject = field.asUnknown().orElseThrow().convert(MyClass.class);
|
||||
MyClass myObject = id.asUnknown().orElseThrow().convert(MyClass.class);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -761,22 +788,22 @@ In rare cases, the API may return a response that doesn't match the expected typ
|
||||
|
||||
By default, the SDK will not throw an exception in this case. It will throw [`LangChainInvalidDataException`](langsmith-java-core/src/main/kotlin/com/langchain/smith/errors/LangChainInvalidDataException.kt) only if you directly access the property.
|
||||
|
||||
If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
|
||||
Validating the response is _not_ forwards compatible with new types from the API for existing fields.
|
||||
|
||||
If you would still prefer to check that the response is completely well-typed upfront, then either call `validate()`:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.sessions.TracerSessionWithoutVirtualFields;
|
||||
|
||||
CustomChartsSection customChartsSection = client.sessions().dashboard(params).validate();
|
||||
TracerSessionWithoutVirtualFields tracerSessionWithoutVirtualFields = client.sessions().create(params).validate();
|
||||
```
|
||||
|
||||
Or configure the method call to validate the response using the `responseValidation` method:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.sessions.CustomChartsSection;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
CustomChartsSection customChartsSection = client.sessions().dashboard(
|
||||
params, RequestOptions.builder().responseValidation(true).build()
|
||||
);
|
||||
RunQueryV2Page page = client.runs().queryV2(RequestOptions.builder().responseValidation(true).build());
|
||||
```
|
||||
|
||||
Or configure the default for all method calls at the client level:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Security Fix Plan - langsmith-java
|
||||
|
||||
Validated 2026-07-02 against the open Dependabot alerts for `langsmith-java-staging`.
|
||||
|
||||
## Status
|
||||
|
||||
| Item | Scope | Status |
|
||||
|------|-------|--------|
|
||||
| Jackson 2.13.4/2.14.0/2.18.6 -> 2.22.0 | Published SDK + examples + tests | Done |
|
||||
| Spring Boot 2.7.18 -> 3.5.16 | Example module only, non-published | Done |
|
||||
| Spring Framework 5.x pins removed | Example module only, non-published | Done |
|
||||
| Tomcat 9.x pins removed | Example module only, non-published | Done |
|
||||
| SnakeYAML 1.x pin removed | Example module only, non-published | Done |
|
||||
| WireMock JRE8 2.35.2 -> org.wiremock:wiremock 4.0.0-beta.37 | Test scope only | Done |
|
||||
| Jetty test constraints -> 12.1.10 | Test scope only | Done |
|
||||
| Bouncy Castle test constraints -> 1.84 | Test scope only | Done |
|
||||
| Log4j Core test constraint -> 2.25.4 | Test scope only | Done |
|
||||
| OpenNLP test constraint -> 2.5.9 | Test scope only | Done |
|
||||
| Handlebars test constraint -> 4.5.2 | Test scope only | Done |
|
||||
| Plexus Utils test constraint -> 4.0.3 | Test scope only | Done |
|
||||
| Commons/Guava/json-path/XMLUnit/json-smart test constraints bumped | Test scope only | Done |
|
||||
|
||||
## Notes
|
||||
|
||||
The Spring Boot upgrade is intentionally limited to the example module, which is not published as part
|
||||
of the SDK artifacts. It removes the old Spring Boot 2/Spring Framework 5 workaround pins that kept
|
||||
several alerts open.
|
||||
|
||||
WireMock 4 is currently beta-only, but it is test scoped and moves the test server dependency graph to
|
||||
Jetty 12. Keeping WireMock 3 would still leave Jetty 11 in at least one advisory range.
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
errors=()
|
||||
|
||||
if [ -z "${RELEASE_PLEASE_TOKEN}" ]; then
|
||||
errors+=("The RELEASE_PLEASE_TOKEN secret has not been set. Create a fine-grained GitHub PAT and add it as a repository secret.")
|
||||
fi
|
||||
|
||||
if [ -z "${SONATYPE_USERNAME}" ]; then
|
||||
errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
|
||||
fi
|
||||
|
||||
+28
-3
@@ -1,5 +1,16 @@
|
||||
buildscript {
|
||||
configurations.classpath {
|
||||
resolutionStrategy {
|
||||
// CVE-2026-54512, CVE-2026-54513, and CVE-2026-54515: Dokka 2.2.0 depends on a vulnerable
|
||||
// jackson-databind version. Remove once Dokka resolves jackson-databind >= 2.18.9.
|
||||
force("com.fasterxml.jackson.core:jackson-databind:2.18.9")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.dokka") version "2.0.0"
|
||||
id("org.jetbrains.dokka") version "2.2.0"
|
||||
id("org.jetbrains.dokka-javadoc") version "2.2.0"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -8,7 +19,21 @@ repositories {
|
||||
|
||||
allprojects {
|
||||
group = "com.langchain.smith"
|
||||
version = "0.1.0-alpha.20" // x-release-please-version
|
||||
version = "0.1.0-beta.19" // x-release-please-version
|
||||
|
||||
configurations.matching { it.name.startsWith("dokka") }.configureEach {
|
||||
// Keep Dokka worker classpaths on the same patched version as the buildscript classpath.
|
||||
resolutionStrategy.force("com.fasterxml.jackson.core:jackson-databind:2.18.9")
|
||||
}
|
||||
|
||||
configurations.matching { it.name == "kotlinBouncyCastleConfiguration" }.configureEach {
|
||||
// CVE-2026-3505: remove once Kotlin's publishing validation selects Bouncy Castle >= 1.84.
|
||||
resolutionStrategy.force(
|
||||
"org.bouncycastle:bcpg-jdk18on:1.84",
|
||||
"org.bouncycastle:bcpkix-jdk18on:1.84",
|
||||
"org.bouncycastle:bcprov-jdk18on:1.84",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
@@ -21,11 +46,11 @@ subprojects {
|
||||
group = "Verification"
|
||||
description = "Verifies all source files are formatted."
|
||||
}
|
||||
apply(plugin = "org.jetbrains.dokka")
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.dokka")
|
||||
apply(plugin = "org.jetbrains.dokka-javadoc")
|
||||
}
|
||||
|
||||
// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
kotlin("jvm") version "2.0.21"
|
||||
id("com.vanniktech.maven.publish") version "0.28.0"
|
||||
kotlin("jvm") version "2.3.21"
|
||||
id("com.vanniktech.maven.publish") version "0.36.0"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -16,6 +16,6 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
|
||||
implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21")
|
||||
implementation("com.vanniktech:gradle-maven-publish-plugin:0.36.0")
|
||||
}
|
||||
|
||||
@@ -44,9 +44,14 @@ tasks.withType<Test>().configureEach {
|
||||
}
|
||||
}
|
||||
|
||||
val palantir by configurations.creating
|
||||
val palantir by configurations.creating {
|
||||
resolutionStrategy {
|
||||
// CVE-2026-54515: remove once Palantir resolves jackson-databind >= 2.21.5.
|
||||
force("com.fasterxml.jackson.core:jackson-databind:2.21.5")
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
palantir("com.palantir.javaformat:palantir-java-format:2.73.0")
|
||||
palantir("com.palantir.javaformat:palantir-java-format:2.93.0")
|
||||
}
|
||||
|
||||
fun registerPalantir(
|
||||
|
||||
@@ -24,19 +24,26 @@ kotlin {
|
||||
"-nowarn",
|
||||
)
|
||||
jvmTarget.set(JvmTarget.JVM_1_8)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_2_0)
|
||||
apiVersion.set(KotlinVersion.KOTLIN_2_0)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_2_1)
|
||||
apiVersion.set(KotlinVersion.KOTLIN_2_1)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
systemProperty("junit.jupiter.execution.parallel.enabled", true)
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
|
||||
|
||||
// Suppress JVM warnings about dynamically loaded agents (e.g. byte-buddy used by Mockito).
|
||||
// Without this, agent warnings are written to stderr and break tests that capture stderr output.
|
||||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||||
|
||||
// `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation.
|
||||
inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true)
|
||||
}
|
||||
|
||||
val ktfmt by configurations.creating
|
||||
dependencies {
|
||||
ktfmt("com.facebook:ktfmt:0.56")
|
||||
ktfmt("com.facebook:ktfmt:0.61")
|
||||
}
|
||||
|
||||
fun registerKtfmt(
|
||||
|
||||
@@ -1,36 +1,45 @@
|
||||
import com.vanniktech.maven.publish.JavadocJar
|
||||
import com.vanniktech.maven.publish.KotlinJvm
|
||||
import com.vanniktech.maven.publish.MavenPublishBaseExtension
|
||||
import com.vanniktech.maven.publish.SonatypeHost
|
||||
import com.vanniktech.maven.publish.SourcesJar
|
||||
|
||||
plugins {
|
||||
id("com.vanniktech.maven.publish")
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (project.hasProperty("publishLocal")) {
|
||||
maven {
|
||||
name = "LocalFileSystem"
|
||||
url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
extra["signingInMemoryKey"] = System.getenv("GPG_SIGNING_KEY")
|
||||
extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID")
|
||||
extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD")
|
||||
|
||||
configure<MavenPublishBaseExtension> {
|
||||
signAllPublications()
|
||||
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
|
||||
if (!project.hasProperty("publishLocal")) {
|
||||
signAllPublications()
|
||||
publishToMavenCentral()
|
||||
}
|
||||
|
||||
coordinates(project.group.toString(), project.name, project.version.toString())
|
||||
configure(
|
||||
KotlinJvm(
|
||||
javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
|
||||
sourcesJar = true,
|
||||
javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationJavadoc"),
|
||||
sourcesJar = SourcesJar.Sources(),
|
||||
)
|
||||
)
|
||||
|
||||
pom {
|
||||
name.set("LangSmith")
|
||||
description.set("The LangSmith API is used to programmatically create and manage LangSmith\nresources.\n\n## Host\n\nhttps://dev.api.smith.langchain.com\n\n## Authentication\n\nTo authenticate with the LangSmith API, set the `X-Api-Key` header to a valid\n[LangSmith API key](https://docs.langchain.com/langsmith/create-account-api-key#create-an-api-key).")
|
||||
description.set("The LangSmith API is used to programmatically create and manage LangSmith\nresources.\n\n## Host\n\nhttp://localhost:1984\n\n## Authentication\n\nTo authenticate with the LangSmith API, set the `X-Api-Key` header to a valid\n[LangSmith API key](https://docs.langchain.com/langsmith/create-account-api-key#create-an-api-key).")
|
||||
url.set("https://www.github.com/langchain-ai/langsmith-java")
|
||||
|
||||
licenses {
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -205,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+82
-94
@@ -1,94 +1,82 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
This directory can be used to store custom files to expand the SDK.
|
||||
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
|
||||
@@ -3,13 +3,35 @@ plugins {
|
||||
id("langchain.publish")
|
||||
}
|
||||
|
||||
configurations.matching { it.name in setOf("testCompileClasspath", "testRuntimeClasspath") }.configureEach {
|
||||
// Test-only WireMock 4 requires Java 17+ dependencies. Published artifacts still target Java 8.
|
||||
attributes.attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":langsmith-java-core"))
|
||||
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
|
||||
constraints {
|
||||
testImplementation("org.eclipse.jetty:jetty-server") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty:jetty-http") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty:jetty-servlets") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty:jetty-xml") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty.http2:http2-common") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty.http2:http2-hpack") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty.http2:http2-server") { version { require("12.1.10") } }
|
||||
testImplementation("com.github.jknack:handlebars") { version { require("4.5.2") } }
|
||||
testImplementation("commons-fileupload:commons-fileupload") { version { require("1.6.0") } }
|
||||
testImplementation("commons-io:commons-io") { version { require("2.22.0") } }
|
||||
testImplementation("com.google.guava:guava") { version { require("33.6.0-jre") } }
|
||||
testImplementation("com.jayway.jsonpath:json-path") { version { require("2.10.0") } }
|
||||
testImplementation("org.xmlunit:xmlunit-core") { version { require("2.12.0") } }
|
||||
testImplementation("net.minidev:json-smart") { version { require("2.6.0") } }
|
||||
}
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("org.assertj:assertj-core:3.25.3")
|
||||
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
|
||||
testImplementation("org.assertj:assertj-core:3.27.7")
|
||||
testImplementation("org.wiremock:wiremock:4.0.0-beta.37")
|
||||
testImplementation("org.wiremock:wiremock-junit5:4.0.0-beta.37")
|
||||
}
|
||||
|
||||
+84
-17
@@ -6,11 +6,13 @@ import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.client.LangsmithClientImpl
|
||||
import com.langchain.smith.core.ClientOptions
|
||||
import com.langchain.smith.core.LogLevel
|
||||
import com.langchain.smith.core.Sleeper
|
||||
import com.langchain.smith.core.Timeout
|
||||
import com.langchain.smith.core.http.AsyncStreamResponse
|
||||
import com.langchain.smith.core.http.Headers
|
||||
import com.langchain.smith.core.http.HttpClient
|
||||
import com.langchain.smith.core.http.ProxyAuthenticator
|
||||
import com.langchain.smith.core.http.QueryParams
|
||||
import com.langchain.smith.core.jsonMapper
|
||||
import java.net.Proxy
|
||||
@@ -49,6 +51,9 @@ class LangsmithOkHttpClient private constructor() {
|
||||
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
|
||||
private var dispatcherExecutorService: ExecutorService? = null
|
||||
private var proxy: Proxy? = null
|
||||
private var proxyAuthenticator: ProxyAuthenticator? = null
|
||||
private var maxIdleConnections: Int? = null
|
||||
private var keepAliveDuration: Duration? = null
|
||||
private var sslSocketFactory: SSLSocketFactory? = null
|
||||
private var trustManager: X509TrustManager? = null
|
||||
private var hostnameVerifier: HostnameVerifier? = null
|
||||
@@ -77,6 +82,60 @@ class LangsmithOkHttpClient private constructor() {
|
||||
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
|
||||
fun proxy(proxy: Optional<Proxy>) = proxy(proxy.getOrNull())
|
||||
|
||||
/**
|
||||
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
|
||||
* Required`.
|
||||
*/
|
||||
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
|
||||
this.proxyAuthenticator = proxyAuthenticator
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
|
||||
*/
|
||||
fun proxyAuthenticator(proxyAuthenticator: Optional<ProxyAuthenticator>) =
|
||||
proxyAuthenticator(proxyAuthenticator.getOrNull())
|
||||
|
||||
/**
|
||||
* The maximum number of idle connections kept by the underlying OkHttp connection pool.
|
||||
*
|
||||
* If this is set, then [keepAliveDuration] must also be set.
|
||||
*
|
||||
* If unset, then OkHttp's default is used.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Int?) = apply {
|
||||
this.maxIdleConnections = maxIdleConnections
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for [Builder.maxIdleConnections].
|
||||
*
|
||||
* This unboxed primitive overload exists for backwards compatibility.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Int) =
|
||||
maxIdleConnections(maxIdleConnections as Int?)
|
||||
|
||||
/**
|
||||
* Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Optional<Int>) =
|
||||
maxIdleConnections(maxIdleConnections.getOrNull())
|
||||
|
||||
/**
|
||||
* The keep-alive duration for idle connections in the underlying OkHttp connection pool.
|
||||
*
|
||||
* If this is set, then [maxIdleConnections] must also be set.
|
||||
*
|
||||
* If unset, then OkHttp's default is used.
|
||||
*/
|
||||
fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
|
||||
this.keepAliveDuration = keepAliveDuration
|
||||
}
|
||||
|
||||
/** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
|
||||
fun keepAliveDuration(keepAliveDuration: Optional<Duration>) =
|
||||
keepAliveDuration(keepAliveDuration.getOrNull())
|
||||
|
||||
/**
|
||||
* The socket factory used to secure HTTPS connections.
|
||||
*
|
||||
@@ -188,6 +247,9 @@ class LangsmithOkHttpClient private constructor() {
|
||||
/**
|
||||
* Whether to call `validate` on every response before returning it.
|
||||
*
|
||||
* Setting this to `true` is _not_ forwards compatible with new types from the API for
|
||||
* existing fields.
|
||||
*
|
||||
* Defaults to false, which means the shape of the response will not be validated upfront.
|
||||
* Instead, validation will only occur for the parts of the response that are accessed.
|
||||
*/
|
||||
@@ -229,6 +291,25 @@ class LangsmithOkHttpClient private constructor() {
|
||||
*/
|
||||
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
|
||||
|
||||
/**
|
||||
* Whether run create/update calls should be automatically batched for tracing.
|
||||
*
|
||||
* Defaults to true. Set to false to send run create/update calls synchronously through the
|
||||
* single-run endpoints.
|
||||
*/
|
||||
fun autoBatchTracing(autoBatchTracing: Boolean) = apply {
|
||||
clientOptions.autoBatchTracing(autoBatchTracing)
|
||||
}
|
||||
|
||||
/**
|
||||
* The level at which to log request and response information.
|
||||
*
|
||||
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
|
||||
*
|
||||
* Defaults to [LogLevel.fromEnv].
|
||||
*/
|
||||
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
|
||||
|
||||
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
|
||||
|
||||
/** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
|
||||
@@ -239,23 +320,6 @@ class LangsmithOkHttpClient private constructor() {
|
||||
/** Alias for calling [Builder.tenantId] with `tenantId.orElse(null)`. */
|
||||
fun tenantId(tenantId: Optional<String>) = tenantId(tenantId.getOrNull())
|
||||
|
||||
/**
|
||||
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
|
||||
* x-organization-id (for org scoped apis).
|
||||
*/
|
||||
fun bearerToken(bearerToken: String?) = apply { clientOptions.bearerToken(bearerToken) }
|
||||
|
||||
/** Alias for calling [Builder.bearerToken] with `bearerToken.orElse(null)`. */
|
||||
fun bearerToken(bearerToken: Optional<String>) = bearerToken(bearerToken.getOrNull())
|
||||
|
||||
fun organizationId(organizationId: String?) = apply {
|
||||
clientOptions.organizationId(organizationId)
|
||||
}
|
||||
|
||||
/** Alias for calling [Builder.organizationId] with `organizationId.orElse(null)`. */
|
||||
fun organizationId(organizationId: Optional<String>) =
|
||||
organizationId(organizationId.getOrNull())
|
||||
|
||||
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
|
||||
|
||||
fun headers(headers: Map<String, Iterable<String>>) = apply {
|
||||
@@ -355,6 +419,9 @@ class LangsmithOkHttpClient private constructor() {
|
||||
OkHttpClient.builder()
|
||||
.timeout(clientOptions.timeout())
|
||||
.proxy(proxy)
|
||||
.proxyAuthenticator(proxyAuthenticator)
|
||||
.maxIdleConnections(maxIdleConnections)
|
||||
.keepAliveDuration(keepAliveDuration)
|
||||
.dispatcherExecutorService(dispatcherExecutorService)
|
||||
.sslSocketFactory(sslSocketFactory)
|
||||
.trustManager(trustManager)
|
||||
|
||||
+84
-17
@@ -6,11 +6,13 @@ import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.langchain.smith.client.LangsmithClientAsync
|
||||
import com.langchain.smith.client.LangsmithClientAsyncImpl
|
||||
import com.langchain.smith.core.ClientOptions
|
||||
import com.langchain.smith.core.LogLevel
|
||||
import com.langchain.smith.core.Sleeper
|
||||
import com.langchain.smith.core.Timeout
|
||||
import com.langchain.smith.core.http.AsyncStreamResponse
|
||||
import com.langchain.smith.core.http.Headers
|
||||
import com.langchain.smith.core.http.HttpClient
|
||||
import com.langchain.smith.core.http.ProxyAuthenticator
|
||||
import com.langchain.smith.core.http.QueryParams
|
||||
import com.langchain.smith.core.jsonMapper
|
||||
import java.net.Proxy
|
||||
@@ -49,6 +51,9 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
|
||||
private var dispatcherExecutorService: ExecutorService? = null
|
||||
private var proxy: Proxy? = null
|
||||
private var proxyAuthenticator: ProxyAuthenticator? = null
|
||||
private var maxIdleConnections: Int? = null
|
||||
private var keepAliveDuration: Duration? = null
|
||||
private var sslSocketFactory: SSLSocketFactory? = null
|
||||
private var trustManager: X509TrustManager? = null
|
||||
private var hostnameVerifier: HostnameVerifier? = null
|
||||
@@ -77,6 +82,60 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
|
||||
fun proxy(proxy: Optional<Proxy>) = proxy(proxy.getOrNull())
|
||||
|
||||
/**
|
||||
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
|
||||
* Required`.
|
||||
*/
|
||||
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
|
||||
this.proxyAuthenticator = proxyAuthenticator
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
|
||||
*/
|
||||
fun proxyAuthenticator(proxyAuthenticator: Optional<ProxyAuthenticator>) =
|
||||
proxyAuthenticator(proxyAuthenticator.getOrNull())
|
||||
|
||||
/**
|
||||
* The maximum number of idle connections kept by the underlying OkHttp connection pool.
|
||||
*
|
||||
* If this is set, then [keepAliveDuration] must also be set.
|
||||
*
|
||||
* If unset, then OkHttp's default is used.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Int?) = apply {
|
||||
this.maxIdleConnections = maxIdleConnections
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for [Builder.maxIdleConnections].
|
||||
*
|
||||
* This unboxed primitive overload exists for backwards compatibility.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Int) =
|
||||
maxIdleConnections(maxIdleConnections as Int?)
|
||||
|
||||
/**
|
||||
* Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Optional<Int>) =
|
||||
maxIdleConnections(maxIdleConnections.getOrNull())
|
||||
|
||||
/**
|
||||
* The keep-alive duration for idle connections in the underlying OkHttp connection pool.
|
||||
*
|
||||
* If this is set, then [maxIdleConnections] must also be set.
|
||||
*
|
||||
* If unset, then OkHttp's default is used.
|
||||
*/
|
||||
fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
|
||||
this.keepAliveDuration = keepAliveDuration
|
||||
}
|
||||
|
||||
/** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
|
||||
fun keepAliveDuration(keepAliveDuration: Optional<Duration>) =
|
||||
keepAliveDuration(keepAliveDuration.getOrNull())
|
||||
|
||||
/**
|
||||
* The socket factory used to secure HTTPS connections.
|
||||
*
|
||||
@@ -188,6 +247,9 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
/**
|
||||
* Whether to call `validate` on every response before returning it.
|
||||
*
|
||||
* Setting this to `true` is _not_ forwards compatible with new types from the API for
|
||||
* existing fields.
|
||||
*
|
||||
* Defaults to false, which means the shape of the response will not be validated upfront.
|
||||
* Instead, validation will only occur for the parts of the response that are accessed.
|
||||
*/
|
||||
@@ -229,6 +291,25 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
*/
|
||||
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
|
||||
|
||||
/**
|
||||
* Whether run create/update calls should be automatically batched for tracing.
|
||||
*
|
||||
* Defaults to true. Set to false to send run create/update calls through the single-run
|
||||
* endpoints.
|
||||
*/
|
||||
fun autoBatchTracing(autoBatchTracing: Boolean) = apply {
|
||||
clientOptions.autoBatchTracing(autoBatchTracing)
|
||||
}
|
||||
|
||||
/**
|
||||
* The level at which to log request and response information.
|
||||
*
|
||||
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
|
||||
*
|
||||
* Defaults to [LogLevel.fromEnv].
|
||||
*/
|
||||
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
|
||||
|
||||
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
|
||||
|
||||
/** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
|
||||
@@ -239,23 +320,6 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
/** Alias for calling [Builder.tenantId] with `tenantId.orElse(null)`. */
|
||||
fun tenantId(tenantId: Optional<String>) = tenantId(tenantId.getOrNull())
|
||||
|
||||
/**
|
||||
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
|
||||
* x-organization-id (for org scoped apis).
|
||||
*/
|
||||
fun bearerToken(bearerToken: String?) = apply { clientOptions.bearerToken(bearerToken) }
|
||||
|
||||
/** Alias for calling [Builder.bearerToken] with `bearerToken.orElse(null)`. */
|
||||
fun bearerToken(bearerToken: Optional<String>) = bearerToken(bearerToken.getOrNull())
|
||||
|
||||
fun organizationId(organizationId: String?) = apply {
|
||||
clientOptions.organizationId(organizationId)
|
||||
}
|
||||
|
||||
/** Alias for calling [Builder.organizationId] with `organizationId.orElse(null)`. */
|
||||
fun organizationId(organizationId: Optional<String>) =
|
||||
organizationId(organizationId.getOrNull())
|
||||
|
||||
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
|
||||
|
||||
fun headers(headers: Map<String, Iterable<String>>) = apply {
|
||||
@@ -355,6 +419,9 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
OkHttpClient.builder()
|
||||
.timeout(clientOptions.timeout())
|
||||
.proxy(proxy)
|
||||
.proxyAuthenticator(proxyAuthenticator)
|
||||
.maxIdleConnections(maxIdleConnections)
|
||||
.keepAliveDuration(keepAliveDuration)
|
||||
.dispatcherExecutorService(dispatcherExecutorService)
|
||||
.sslSocketFactory(sslSocketFactory)
|
||||
.trustManager(trustManager)
|
||||
|
||||
+194
-104
@@ -8,20 +8,26 @@ import com.langchain.smith.core.http.HttpMethod
|
||||
import com.langchain.smith.core.http.HttpRequest
|
||||
import com.langchain.smith.core.http.HttpRequestBody
|
||||
import com.langchain.smith.core.http.HttpResponse
|
||||
import com.langchain.smith.core.http.ProxyAuthenticator
|
||||
import com.langchain.smith.errors.LangChainIoException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.Proxy
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -29,17 +35,18 @@ import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import okio.BufferedSink
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
|
||||
class OkHttpClient
|
||||
private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
|
||||
internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
|
||||
|
||||
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
|
||||
val call = newCall(request, requestOptions)
|
||||
|
||||
return try {
|
||||
call.execute().toResponse()
|
||||
call.execute().toHttpResponse()
|
||||
} catch (e: IOException) {
|
||||
throw LangChainIoException("Request failed", e)
|
||||
} finally {
|
||||
@@ -57,7 +64,7 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
call.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
future.complete(response.toResponse())
|
||||
future.complete(response.toHttpResponse())
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
@@ -85,23 +92,6 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
|
||||
val clientBuilder = okHttpClient.newBuilder()
|
||||
|
||||
val logLevel =
|
||||
when (System.getenv("LANGCHAIN_LOG")?.lowercase()) {
|
||||
"info" -> HttpLoggingInterceptor.Level.BASIC
|
||||
"debug" -> HttpLoggingInterceptor.Level.BODY
|
||||
else -> null
|
||||
}
|
||||
if (logLevel != null) {
|
||||
clientBuilder.addNetworkInterceptor(
|
||||
HttpLoggingInterceptor().setLevel(logLevel).apply {
|
||||
redactHeader("X-API-Key")
|
||||
redactHeader("X-Organization-Id")
|
||||
redactHeader("Authorization")
|
||||
redactHeader("X-Tenant-Id")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
requestOptions.timeout?.let {
|
||||
clientBuilder
|
||||
.connectTimeout(it.connect())
|
||||
@@ -114,89 +104,6 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
return client.newCall(request.toRequest(client))
|
||||
}
|
||||
|
||||
private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
|
||||
var body: RequestBody? = body?.toRequestBody()
|
||||
if (body == null && requiresBody(method)) {
|
||||
body = "".toRequestBody()
|
||||
}
|
||||
|
||||
val builder = Request.Builder().url(toUrl()).method(method.name, body)
|
||||
headers.names().forEach { name ->
|
||||
headers.values(name).forEach { builder.addHeader(name, it) }
|
||||
}
|
||||
|
||||
if (
|
||||
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
|
||||
) {
|
||||
builder.addHeader(
|
||||
"X-Stainless-Read-Timeout",
|
||||
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
|
||||
)
|
||||
}
|
||||
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
|
||||
builder.addHeader(
|
||||
"X-Stainless-Timeout",
|
||||
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/** `OkHttpClient` always requires a request body for some methods. */
|
||||
private fun requiresBody(method: HttpMethod): Boolean =
|
||||
when (method) {
|
||||
HttpMethod.POST,
|
||||
HttpMethod.PUT,
|
||||
HttpMethod.PATCH -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun HttpRequest.toUrl(): String {
|
||||
val builder = baseUrl.toHttpUrl().newBuilder()
|
||||
pathSegments.forEach(builder::addPathSegment)
|
||||
queryParams.keys().forEach { key ->
|
||||
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun HttpRequestBody.toRequestBody(): RequestBody {
|
||||
val mediaType = contentType()?.toMediaType()
|
||||
val length = contentLength()
|
||||
|
||||
return object : RequestBody() {
|
||||
override fun contentType(): MediaType? = mediaType
|
||||
|
||||
override fun contentLength(): Long = length
|
||||
|
||||
override fun isOneShot(): Boolean = !repeatable()
|
||||
|
||||
override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.toResponse(): HttpResponse {
|
||||
val headers = headers.toHeaders()
|
||||
|
||||
return object : HttpResponse {
|
||||
override fun statusCode(): Int = code
|
||||
|
||||
override fun headers(): Headers = headers
|
||||
|
||||
override fun body(): InputStream = body!!.byteStream()
|
||||
|
||||
override fun close() = body!!.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun okhttp3.Headers.toHeaders(): Headers {
|
||||
val headersBuilder = Headers.builder()
|
||||
forEach { (name, value) -> headersBuilder.put(name, value) }
|
||||
return headersBuilder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun builder() = Builder()
|
||||
}
|
||||
@@ -205,6 +112,9 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
|
||||
private var timeout: Timeout = Timeout.default()
|
||||
private var proxy: Proxy? = null
|
||||
private var proxyAuthenticator: ProxyAuthenticator? = null
|
||||
private var maxIdleConnections: Int? = null
|
||||
private var keepAliveDuration: Duration? = null
|
||||
private var dispatcherExecutorService: ExecutorService? = null
|
||||
private var sslSocketFactory: SSLSocketFactory? = null
|
||||
private var trustManager: X509TrustManager? = null
|
||||
@@ -216,6 +126,32 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
|
||||
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
|
||||
|
||||
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
|
||||
this.proxyAuthenticator = proxyAuthenticator
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of idle connections kept by the underlying [ConnectionPool].
|
||||
*
|
||||
* If this is set, then [keepAliveDuration] must also be set.
|
||||
*
|
||||
* If unset, then OkHttp's default is used.
|
||||
*/
|
||||
fun maxIdleConnections(maxIdleConnections: Int?) = apply {
|
||||
this.maxIdleConnections = maxIdleConnections
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the keep-alive duration for idle connections in the underlying [ConnectionPool].
|
||||
*
|
||||
* If this is set, then [maxIdleConnections] must also be set.
|
||||
*
|
||||
* If unset, then OkHttp's default is used.
|
||||
*/
|
||||
fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
|
||||
this.keepAliveDuration = keepAliveDuration
|
||||
}
|
||||
|
||||
fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
|
||||
this.dispatcherExecutorService = dispatcherExecutorService
|
||||
}
|
||||
@@ -235,14 +171,45 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
fun build(): OkHttpClient =
|
||||
OkHttpClient(
|
||||
okhttp3.OkHttpClient.Builder()
|
||||
// `RetryingHttpClient` handles retries if the user enabled them.
|
||||
.retryOnConnectionFailure(false)
|
||||
.connectTimeout(timeout.connect())
|
||||
.readTimeout(timeout.read())
|
||||
.writeTimeout(timeout.write())
|
||||
.callTimeout(timeout.request())
|
||||
.proxy(proxy)
|
||||
.apply {
|
||||
proxyAuthenticator?.let { auth ->
|
||||
proxyAuthenticator { route, response ->
|
||||
auth
|
||||
.authenticate(
|
||||
route?.proxy ?: Proxy.NO_PROXY,
|
||||
response.request.toHttpRequest(),
|
||||
response.toHttpResponse(),
|
||||
)
|
||||
.getOrNull()
|
||||
?.toRequest(client = null)
|
||||
}
|
||||
}
|
||||
|
||||
dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
|
||||
|
||||
val maxIdleConnections = maxIdleConnections
|
||||
val keepAliveDuration = keepAliveDuration
|
||||
if (maxIdleConnections != null && keepAliveDuration != null) {
|
||||
connectionPool(
|
||||
ConnectionPool(
|
||||
maxIdleConnections,
|
||||
keepAliveDuration.toNanos(),
|
||||
TimeUnit.NANOSECONDS,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
check((maxIdleConnections != null) == (keepAliveDuration != null)) {
|
||||
"Both or none of `maxIdleConnections` and `keepAliveDuration` must be set, but only one was set"
|
||||
}
|
||||
}
|
||||
|
||||
val sslSocketFactory = sslSocketFactory
|
||||
val trustManager = trustManager
|
||||
if (sslSocketFactory != null && trustManager != null) {
|
||||
@@ -264,3 +231,126 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient?): Request {
|
||||
var body: RequestBody? = body?.toRequestBody()
|
||||
if (body == null && requiresBody(method)) {
|
||||
body = "".toRequestBody()
|
||||
}
|
||||
|
||||
val builder = Request.Builder().url(toUrl()).method(method.name, body)
|
||||
headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } }
|
||||
|
||||
if (client != null) {
|
||||
if (
|
||||
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
|
||||
) {
|
||||
builder.addHeader(
|
||||
"X-Stainless-Read-Timeout",
|
||||
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
|
||||
)
|
||||
}
|
||||
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
|
||||
builder.addHeader(
|
||||
"X-Stainless-Timeout",
|
||||
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/** `OkHttpClient` always requires a request body for some methods. */
|
||||
private fun requiresBody(method: HttpMethod): Boolean =
|
||||
when (method) {
|
||||
HttpMethod.POST,
|
||||
HttpMethod.PUT,
|
||||
HttpMethod.PATCH -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun HttpRequest.toUrl(): String {
|
||||
val builder = baseUrl.toHttpUrl().newBuilder()
|
||||
pathSegments.forEach(builder::addPathSegment)
|
||||
queryParams.keys().forEach { key ->
|
||||
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun HttpRequestBody.toRequestBody(): RequestBody {
|
||||
val mediaType = contentType()?.toMediaType()
|
||||
val length = contentLength()
|
||||
|
||||
return object : RequestBody() {
|
||||
override fun contentType(): MediaType? = mediaType
|
||||
|
||||
override fun contentLength(): Long = length
|
||||
|
||||
override fun isOneShot(): Boolean = !repeatable()
|
||||
|
||||
override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Request.toHttpRequest(): HttpRequest {
|
||||
val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl())
|
||||
url.pathSegments.forEach(builder::addPathSegment)
|
||||
url.queryParameterNames.forEach { name ->
|
||||
url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) }
|
||||
}
|
||||
headers.forEach { (name, value) -> builder.putHeader(name, value) }
|
||||
body?.let { builder.body(it.toHttpRequestBody()) }
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun HttpUrl.toBaseUrl(): String = buildString {
|
||||
append(scheme).append("://").append(host)
|
||||
if (port != HttpUrl.defaultPort(scheme)) {
|
||||
append(":").append(port)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RequestBody.toHttpRequestBody(): HttpRequestBody {
|
||||
val mediaType = contentType()?.toString()
|
||||
val length = contentLength()
|
||||
val isOneShot = isOneShot()
|
||||
val source = this
|
||||
return object : HttpRequestBody {
|
||||
override fun contentType(): String? = mediaType
|
||||
|
||||
override fun contentLength(): Long = length
|
||||
|
||||
override fun repeatable(): Boolean = !isOneShot
|
||||
|
||||
override fun writeTo(outputStream: OutputStream) {
|
||||
val sink = outputStream.sink().buffer()
|
||||
source.writeTo(sink)
|
||||
sink.flush()
|
||||
}
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.toHttpResponse(): HttpResponse {
|
||||
val headers = headers.toHeaders()
|
||||
|
||||
return object : HttpResponse {
|
||||
override fun statusCode(): Int = code
|
||||
|
||||
override fun headers(): Headers = headers
|
||||
|
||||
override fun body(): InputStream = body!!.byteStream()
|
||||
|
||||
override fun close() = body!!.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun okhttp3.Headers.toHeaders(): Headers {
|
||||
val headersBuilder = Headers.builder()
|
||||
forEach { (name, value) -> headersBuilder.put(name, value) }
|
||||
return headersBuilder.build()
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.langchain.smith.client.okhttp
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.tracing.LangsmithClientProvider
|
||||
|
||||
/** [LangsmithClientProvider] backed by [LangsmithOkHttpClient]. */
|
||||
class OkHttpLangsmithClientProvider : LangsmithClientProvider {
|
||||
override fun createClient(): LangsmithClient = LangsmithOkHttpClient.fromEnv()
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.langchain.smith.client.okhttp.OkHttpLangsmithClientProvider
|
||||
@@ -3,53 +3,118 @@ plugins {
|
||||
id("langchain.publish")
|
||||
}
|
||||
|
||||
val generateVersionProperties by tasks.registering {
|
||||
val outputDir = layout.buildDirectory.dir("generated/resources/version")
|
||||
val projectVersion = provider { project.version.toString() }
|
||||
outputs.dir(outputDir)
|
||||
inputs.property("version", projectVersion)
|
||||
doLast {
|
||||
val dir = outputDir.get().asFile.resolve("com/langchain/smith")
|
||||
dir.mkdirs()
|
||||
dir.resolve("version.properties").writeText("version=${projectVersion.get()}\n")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
resources.srcDir(generateVersionProperties)
|
||||
}
|
||||
|
||||
val jacksonVersion = "2.22.1"
|
||||
val jacksonAnnotationsVersion = "2.22"
|
||||
|
||||
configurations.matching { it.name in setOf("testCompileClasspath", "testRuntimeClasspath") }.configureEach {
|
||||
// Test-only WireMock 4 requires Java 17+ dependencies. Published artifacts still target Java 8.
|
||||
attributes.attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
// Compile and test against a lower Jackson version to ensure we're compatible with it.
|
||||
// We publish with a higher version (see below) to ensure users depend on a secure version by default.
|
||||
force("com.fasterxml.jackson.core:jackson-core:2.13.4")
|
||||
force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
|
||||
force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
|
||||
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
|
||||
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
|
||||
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
|
||||
// Keep all Jackson modules aligned so dependency resolution cannot select a vulnerable
|
||||
// transitive version from SDK or test dependencies.
|
||||
force("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
|
||||
force("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||
force("com.fasterxml.jackson.core:jackson-annotations:$jacksonAnnotationsVersion")
|
||||
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion")
|
||||
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
|
||||
force("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("com.fasterxml.jackson.core:jackson-core:2.18.2")
|
||||
api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
|
||||
api("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||
api("com.google.errorprone:error_prone_annotations:2.33.0")
|
||||
|
||||
implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
|
||||
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonAnnotationsVersion")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
|
||||
implementation("com.github.luben:zstd-jni:1.5.7-7")
|
||||
implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
|
||||
implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
|
||||
|
||||
|
||||
// OpenTelemetry dependencies
|
||||
api("io.opentelemetry:opentelemetry-api:1.32.0")
|
||||
api("io.opentelemetry:opentelemetry-sdk:1.32.0")
|
||||
api("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0")
|
||||
api("io.opentelemetry.semconv:opentelemetry-semconv:1.23.1-alpha")
|
||||
|
||||
// OpenAI SDK (for OpenTelemetry wrappers)
|
||||
api("com.openai:openai-java:4.6.1")
|
||||
api("io.opentelemetry:opentelemetry-api:1.62.0")
|
||||
api("io.opentelemetry:opentelemetry-sdk:1.62.0")
|
||||
api("io.opentelemetry:opentelemetry-exporter-otlp:1.62.0")
|
||||
api("io.opentelemetry.semconv:opentelemetry-semconv:1.41.1")
|
||||
|
||||
// OpenAI SDK (for wrapOpenAI tracing wrapper)
|
||||
api("com.openai:openai-java:4.30.0")
|
||||
|
||||
// Mustache template engine (for prompt template formatting)
|
||||
implementation("com.samskivert:jmustache:1.16")
|
||||
|
||||
// SLF4J for logging (API only - consumers choose implementation)
|
||||
api("org.slf4j:slf4j-api:2.0.17")
|
||||
|
||||
// Anthropic SDK — optional peer dependency for prompt conversion.
|
||||
// Users who call AnthropicPayload.toAnthropicParams() must add this to their own dependencies.
|
||||
compileOnly("com.anthropic:anthropic-java:2.18.0")
|
||||
testImplementation("com.anthropic:anthropic-java:2.18.0")
|
||||
|
||||
// Security: constrain vulnerable transitive test dependencies.
|
||||
// These constraints apply to the test scope only and do not affect published artifacts.
|
||||
constraints {
|
||||
testImplementation("org.eclipse.jetty:jetty-server") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty:jetty-http") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty:jetty-servlets") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty:jetty-xml") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty.http2:http2-common") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty.http2:http2-hpack") { version { require("12.1.10") } }
|
||||
testImplementation("org.eclipse.jetty.http2:http2-server") { version { require("12.1.10") } }
|
||||
testImplementation("org.bouncycastle:bcpg-jdk18on:1.84")
|
||||
testImplementation("org.bouncycastle:bcpkix-jdk18on:1.84")
|
||||
testImplementation("org.bouncycastle:bcprov-jdk18on:1.84")
|
||||
testImplementation("org.apache.logging.log4j:log4j-core:2.25.4")
|
||||
testImplementation("org.apache.opennlp:opennlp-tools") { version { require("2.5.9") } }
|
||||
testImplementation("com.github.jknack:handlebars") { version { require("4.5.2") } }
|
||||
testImplementation("org.codehaus.plexus:plexus-utils:4.0.3")
|
||||
testImplementation("org.apache.commons:commons-lang3:3.18.0")
|
||||
testImplementation("org.apache.commons:commons-compress") { version { require("1.28.0") } }
|
||||
testImplementation("commons-fileupload:commons-fileupload") { version { require("1.6.0") } }
|
||||
testImplementation("commons-io:commons-io") { version { require("2.22.0") } }
|
||||
testImplementation("com.google.guava:guava") { version { require("33.6.0-jre") } }
|
||||
testImplementation("com.jayway.jsonpath:json-path") { version { require("2.10.0") } }
|
||||
testImplementation("org.xmlunit:xmlunit-core") { version { require("2.12.0") } }
|
||||
testImplementation("net.minidev:json-smart") { version { require("2.6.0") } }
|
||||
}
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
// Simple logging for tests only
|
||||
testImplementation("org.slf4j:slf4j-simple:2.0.17")
|
||||
testImplementation(project(":langsmith-java-client-okhttp"))
|
||||
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
|
||||
testImplementation("org.assertj:assertj-core:3.25.3")
|
||||
testImplementation("org.wiremock:wiremock:4.0.0-beta.37")
|
||||
testImplementation("org.wiremock:wiremock-junit5:4.0.0-beta.37")
|
||||
testImplementation("org.assertj:assertj-core:3.27.7")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
|
||||
testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
|
||||
testImplementation("org.mockito:mockito-core:5.14.2")
|
||||
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
|
||||
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
||||
|
||||
// LangChain4j for testing parallel tool execution context propagation
|
||||
testImplementation("dev.langchain4j:langchain4j:1.12.2")
|
||||
testImplementation("dev.langchain4j:langchain4j-core:1.12.2")
|
||||
}
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
package com.langchain.smith.otel;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration for OpenTelemetry trace export.
|
||||
*
|
||||
* <p>This class provides configuration options for exporting LangSmith runs as OpenTelemetry traces.
|
||||
* Configuration can be loaded from environment variables or system properties, or set manually via
|
||||
* the builder.
|
||||
*
|
||||
* <p>Environment variables:
|
||||
* <ul>
|
||||
* <li>OTEL_EXPORTER_OTLP_ENDPOINT: The OTEL endpoint URL (default: http://localhost:4318/v1/traces)
|
||||
* <li>OTEL_EXPORTER_OTLP_ENABLED: Whether export is enabled (default: false)
|
||||
* <li>OTEL_EXPORTER_OTLP_TIMEOUT: Timeout in seconds (default: 10)
|
||||
* </ul>
|
||||
*
|
||||
* <p>System properties:
|
||||
* <ul>
|
||||
* <li>langchain.otel.endpoint
|
||||
* <li>langchain.otel.enabled
|
||||
* <li>langchain.otel.timeout
|
||||
* </ul>
|
||||
*/
|
||||
public final class OtelConfig {
|
||||
private final String endpoint;
|
||||
private final boolean enabled;
|
||||
private final Duration timeout;
|
||||
private final Map<String, String> headers;
|
||||
private final String serviceName;
|
||||
|
||||
private OtelConfig(Builder builder) {
|
||||
this.endpoint = builder.endpoint;
|
||||
this.enabled = builder.enabled;
|
||||
this.timeout = builder.timeout;
|
||||
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
|
||||
this.serviceName = builder.serviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenTelemetry endpoint URL.
|
||||
*
|
||||
* @return the endpoint URL
|
||||
*/
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether OpenTelemetry export is enabled.
|
||||
*
|
||||
* @return true if enabled, false otherwise
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timeout for export requests.
|
||||
*
|
||||
* @return the timeout duration
|
||||
*/
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns additional headers to include in export requests.
|
||||
*
|
||||
* @return a map of header names to values
|
||||
*/
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service name for OpenTelemetry traces.
|
||||
*
|
||||
* @return the service name, or null if not set
|
||||
*/
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder for constructing an OtelConfig.
|
||||
*
|
||||
* @return a new builder
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration from environment variables or system properties.
|
||||
*
|
||||
* <p>System properties take precedence over environment variables.
|
||||
*
|
||||
* @return a configuration loaded from environment/system properties
|
||||
*/
|
||||
public static OtelConfig fromEnv() {
|
||||
String endpoint = System.getProperty("langchain.otel.endpoint") != null
|
||||
? System.getProperty("langchain.otel.endpoint")
|
||||
: System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != null
|
||||
? System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
: "http://localhost:4318/v1/traces";
|
||||
|
||||
String enabledStr = System.getProperty("langchain.otel.enabled") != null
|
||||
? System.getProperty("langchain.otel.enabled")
|
||||
: System.getenv("OTEL_EXPORTER_OTLP_ENABLED") != null
|
||||
? System.getenv("OTEL_EXPORTER_OTLP_ENABLED")
|
||||
: "false";
|
||||
boolean enabled = "true".equalsIgnoreCase(enabledStr);
|
||||
|
||||
String timeoutStr = System.getProperty("langchain.otel.timeout") != null
|
||||
? System.getProperty("langchain.otel.timeout")
|
||||
: System.getenv("OTEL_EXPORTER_OTLP_TIMEOUT") != null
|
||||
? System.getenv("OTEL_EXPORTER_OTLP_TIMEOUT")
|
||||
: "10";
|
||||
long timeoutSeconds = 10L;
|
||||
try {
|
||||
timeoutSeconds = Long.parseLong(timeoutStr);
|
||||
} catch (NumberFormatException e) {
|
||||
// Use default
|
||||
}
|
||||
Duration timeout = Duration.ofSeconds(timeoutSeconds);
|
||||
|
||||
String serviceName = System.getProperty("langchain.otel.service.name") != null
|
||||
? System.getProperty("langchain.otel.service.name")
|
||||
: System.getenv("OTEL_SERVICE_NAME") != null ? System.getenv("OTEL_SERVICE_NAME") : null;
|
||||
|
||||
Builder configBuilder = builder().endpoint(endpoint).enabled(enabled).timeout(timeout);
|
||||
if (serviceName != null) {
|
||||
configBuilder.serviceName(serviceName);
|
||||
}
|
||||
return configBuilder.build();
|
||||
}
|
||||
|
||||
/** Builder for OtelConfig. */
|
||||
public static final class Builder {
|
||||
private String endpoint = "http://localhost:4318/v1/traces";
|
||||
private boolean enabled = false;
|
||||
private Duration timeout = Duration.ofSeconds(10);
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
private String serviceName = null;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
/**
|
||||
* Sets the OpenTelemetry endpoint URL.
|
||||
*
|
||||
* @param endpoint the endpoint URL
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder endpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether OpenTelemetry export is enabled.
|
||||
*
|
||||
* @param enabled true to enable export, false to disable
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder enabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout for export requests.
|
||||
*
|
||||
* @param timeout the timeout duration
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder timeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets additional headers to include in export requests.
|
||||
*
|
||||
* @param headers a map of header names to values
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder headers(Map<String, String> headers) {
|
||||
this.headers.clear();
|
||||
this.headers.putAll(headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header to include in export requests.
|
||||
*
|
||||
* @param name the header name
|
||||
* @param value the header value
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder putHeader(String name, String value) {
|
||||
this.headers.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service name for OpenTelemetry traces.
|
||||
*
|
||||
* @param serviceName the service name (e.g., "langsmith-java", "my-application")
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder serviceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the OtelConfig.
|
||||
*
|
||||
* @return a new OtelConfig instance
|
||||
*/
|
||||
public OtelConfig build() {
|
||||
return new OtelConfig(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package com.langchain.smith.otel;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
import io.opentelemetry.context.Context;
|
||||
|
||||
/**
|
||||
* Utility class for creating OpenTelemetry spans with Gen AI semantic conventions.
|
||||
* Provides helper methods to create spans for LLM, tool, retrieval, and chain operations.
|
||||
*/
|
||||
public final class OtelSpanCreator {
|
||||
|
||||
private OtelSpanCreator() {}
|
||||
|
||||
/**
|
||||
* Creates an LLM span with common gen_ai attributes pre-configured.
|
||||
*
|
||||
* @param tracer the OpenTelemetry tracer
|
||||
* @param name the span name
|
||||
* @param system the AI system (e.g., "openai", "anthropic")
|
||||
* @param model the model name (e.g., "gpt-4", "claude-3")
|
||||
* @param serviceName the service name
|
||||
* @param sessionId optional session ID
|
||||
* @return a started Span with gen_ai attributes set
|
||||
*/
|
||||
public static Span createLlmSpan(
|
||||
Tracer tracer, String name, String system, String model, String serviceName, String sessionId) {
|
||||
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "chat");
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.system"), system);
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.request.model"), model);
|
||||
if (serviceName != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the prompt/input on a span.
|
||||
*
|
||||
* @param span the span to set the input on
|
||||
* @param input the input/prompt text
|
||||
*/
|
||||
public static void setInput(Span span, String input) {
|
||||
if (input != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.prompt"), input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion/output on a span.
|
||||
*
|
||||
* @param span the span to set the output on
|
||||
* @param output the output/completion text
|
||||
*/
|
||||
public static void setOutput(Span span, String output) {
|
||||
if (output != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.completion"), output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output messages in JSON format on a span.
|
||||
* This is used to represent the full message structure including tool calls.
|
||||
*
|
||||
* <p>The messages JSON should follow OpenAI's message format:
|
||||
* <pre>
|
||||
* [
|
||||
* {
|
||||
* "role": "assistant",
|
||||
* "content": "Let me check the weather...",
|
||||
* "tool_calls": [
|
||||
* {
|
||||
* "id": "call_123",
|
||||
* "type": "function",
|
||||
* "function": {
|
||||
* "name": "get_weather",
|
||||
* "arguments": "{\"location\":\"San Francisco\"}"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* </pre>
|
||||
*
|
||||
* @param span the span to set the output messages on
|
||||
* @param messagesJson JSON string containing the output messages array
|
||||
*/
|
||||
public static void setOutputMessages(Span span, String messagesJson) {
|
||||
if (messagesJson != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.output.messages"), messagesJson);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input messages in JSON format on a span.
|
||||
*
|
||||
* <p>The messages JSON should follow OpenAI's message format:
|
||||
* <pre>
|
||||
* [
|
||||
* {"role": "user", "content": "What's the weather?"},
|
||||
* {"role": "system", "content": "You are a helpful assistant."}
|
||||
* ]
|
||||
* </pre>
|
||||
*
|
||||
* @param span the span to set the input messages on
|
||||
* @param messagesJson JSON string containing the input messages array
|
||||
*/
|
||||
public static void setInputMessages(Span span, String messagesJson) {
|
||||
if (messagesJson != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.input.messages"), messagesJson);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets token usage information on a span.
|
||||
*
|
||||
* @param span the span to set token usage on
|
||||
* @param inputTokens number of input tokens
|
||||
* @param outputTokens number of output tokens
|
||||
*/
|
||||
public static void setTokenUsage(Span span, int inputTokens, int outputTokens) {
|
||||
span.setAttribute(AttributeKey.longKey("gen_ai.usage.input_tokens"), (long) inputTokens);
|
||||
span.setAttribute(AttributeKey.longKey("gen_ai.usage.output_tokens"), (long) outputTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tool span with common gen_ai attributes pre-configured.
|
||||
*
|
||||
* @param tracer the OpenTelemetry tracer
|
||||
* @param name the span name
|
||||
* @param toolName the tool name
|
||||
* @param serviceName the service name
|
||||
* @param sessionId optional session ID
|
||||
* @return a started Span with gen_ai attributes set
|
||||
*/
|
||||
public static Span createToolSpan(
|
||||
Tracer tracer, String name, String toolName, String serviceName, String sessionId) {
|
||||
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "tool");
|
||||
span.setAttribute(AttributeKey.stringKey("tool.name"), toolName);
|
||||
if (serviceName != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a retrieval span with common gen_ai attributes pre-configured.
|
||||
*
|
||||
* @param tracer the OpenTelemetry tracer
|
||||
* @param name the span name
|
||||
* @param serviceName the service name
|
||||
* @param sessionId optional session ID
|
||||
* @return a started Span with gen_ai attributes set
|
||||
*/
|
||||
public static Span createRetrievalSpan(Tracer tracer, String name, String serviceName, String sessionId) {
|
||||
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "retrieval");
|
||||
if (serviceName != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a chain/workflow span with common gen_ai attributes pre-configured.
|
||||
*
|
||||
* @param tracer the OpenTelemetry tracer
|
||||
* @param name the span name
|
||||
* @param serviceName the service name
|
||||
* @param sessionId optional session ID
|
||||
* @return a started Span with gen_ai attributes set
|
||||
*/
|
||||
public static Span createChainSpan(Tracer tracer, String name, String serviceName, String sessionId) {
|
||||
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
|
||||
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "chat");
|
||||
if (serviceName != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
package com.langchain.smith.otel;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
|
||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
|
||||
import io.opentelemetry.sdk.OpenTelemetrySdk;
|
||||
import io.opentelemetry.sdk.common.CompletableResultCode;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.SdkTracerProvider;
|
||||
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
|
||||
import io.opentelemetry.semconv.ResourceAttributes;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Manages OpenTelemetry SDK for exporting traces to OTLP endpoints.
|
||||
*
|
||||
* <p>This class initializes the OpenTelemetry SDK with OTLP HTTP export capabilities,
|
||||
* providing a Tracer for creating spans with Gen AI semantic conventions.
|
||||
*/
|
||||
public final class OtelTraceExporter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OtelTraceExporter.class);
|
||||
private static final String INSTRUMENTATION_NAME = "langsmith-java";
|
||||
private static final String INSTRUMENTATION_VERSION = "0.1.0";
|
||||
|
||||
private final OtelConfig config;
|
||||
private final OpenTelemetry openTelemetry;
|
||||
private final Tracer tracer;
|
||||
private final SdkTracerProvider tracerProvider;
|
||||
private final String projectName;
|
||||
|
||||
private OtelTraceExporter(
|
||||
OtelConfig config,
|
||||
OpenTelemetry openTelemetry,
|
||||
Tracer tracer,
|
||||
SdkTracerProvider tracerProvider,
|
||||
String projectName) {
|
||||
this.config = config;
|
||||
this.openTelemetry = openTelemetry;
|
||||
this.tracer = tracer;
|
||||
this.tracerProvider = tracerProvider;
|
||||
this.projectName = projectName != null ? projectName : "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OpenTelemetry Tracer for creating spans directly.
|
||||
* @return The OpenTelemetry Tracer instance
|
||||
*/
|
||||
public Tracer getTracer() {
|
||||
return tracer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project name configured for this exporter.
|
||||
* @return The project name
|
||||
*/
|
||||
public String getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the exporter and flushes any pending exports.
|
||||
*
|
||||
* @return a CompletableResultCode indicating success or failure
|
||||
*/
|
||||
public CompletableResultCode shutdown() {
|
||||
return tracerProvider.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes any pending exports.
|
||||
*
|
||||
* @return a CompletableResultCode indicating success or failure
|
||||
*/
|
||||
public CompletableResultCode flush() {
|
||||
return tracerProvider.forceFlush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an OtelTraceExporter from environment variables or system properties.
|
||||
*
|
||||
* @return a new OtelTraceExporter instance
|
||||
* @see OtelConfig#fromEnv()
|
||||
*/
|
||||
public static OtelTraceExporter fromEnv() {
|
||||
return fromConfig(OtelConfig.fromEnv());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an OtelTraceExporter from a configuration.
|
||||
* Sets up the complete OpenTelemetry SDK with TracerProvider and BatchSpanProcessor.
|
||||
*
|
||||
* @param config the configuration to use
|
||||
* @return a new OtelTraceExporter instance
|
||||
*/
|
||||
public static OtelTraceExporter fromConfig(OtelConfig config) {
|
||||
String serviceName = config.getServiceName() != null ? config.getServiceName() : "langsmith-app";
|
||||
|
||||
Resource resource = Resource.getDefault().toBuilder()
|
||||
.put(ResourceAttributes.SERVICE_NAME, serviceName)
|
||||
.put(ResourceAttributes.SERVICE_VERSION, INSTRUMENTATION_VERSION)
|
||||
.build();
|
||||
|
||||
if (!config.isEnabled()) {
|
||||
SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build();
|
||||
OpenTelemetry openTelemetry =
|
||||
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
|
||||
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
|
||||
return new OtelTraceExporter(config, openTelemetry, tracer, tracerProvider, null);
|
||||
}
|
||||
|
||||
// Build the OTLP HTTP exporter
|
||||
OtlpHttpSpanExporterBuilder exporterBuilder =
|
||||
OtlpHttpSpanExporter.builder().setEndpoint(config.getEndpoint()).setTimeout(config.getTimeout());
|
||||
|
||||
// Add custom headers
|
||||
for (Map.Entry<String, String> header : config.getHeaders().entrySet()) {
|
||||
exporterBuilder.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
OtlpHttpSpanExporter exporter = exporterBuilder.build();
|
||||
|
||||
BatchSpanProcessor spanProcessor = BatchSpanProcessor.builder(exporter)
|
||||
.setScheduleDelay(5, TimeUnit.SECONDS) // Batch every 5 seconds
|
||||
.setMaxQueueSize(2048)
|
||||
.setMaxExportBatchSize(512)
|
||||
.build();
|
||||
|
||||
// Create TracerProvider with the processor
|
||||
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
|
||||
.addResource(resource)
|
||||
.addSpanProcessor(spanProcessor)
|
||||
.build();
|
||||
|
||||
// Create OpenTelemetry SDK
|
||||
OpenTelemetry openTelemetry =
|
||||
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
|
||||
|
||||
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
|
||||
|
||||
String projectName = config.getHeaders().get("Langsmith-Project");
|
||||
if (projectName == null) {
|
||||
projectName = "default";
|
||||
}
|
||||
|
||||
// Log configuration for debugging
|
||||
logger.debug(
|
||||
"Created OpenTelemetry SDK with endpoint: {}, timeout: {}", config.getEndpoint(), config.getTimeout());
|
||||
logger.debug("Headers: {}", config.getHeaders());
|
||||
logger.debug("Service name: {}, Project name: {}", serviceName, projectName);
|
||||
|
||||
return new OtelTraceExporter(config, openTelemetry, tracer, tracerProvider, projectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for OtelTraceExporter.
|
||||
*
|
||||
* @return a new builder
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/** Builder for OtelTraceExporter. */
|
||||
public static final class Builder {
|
||||
private OtelConfig config;
|
||||
private String endpoint;
|
||||
private Boolean enabled;
|
||||
private Duration timeout;
|
||||
private Map<String, String> headers = new java.util.HashMap<>();
|
||||
private String serviceName;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
/**
|
||||
* Sets the configuration to use.
|
||||
*
|
||||
* @param config the configuration
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder config(OtelConfig config) {
|
||||
this.config = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the OpenTelemetry endpoint URL.
|
||||
*
|
||||
* @param endpoint the endpoint URL
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder endpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether OpenTelemetry export is enabled.
|
||||
*
|
||||
* @param enabled true to enable export, false to disable
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder enabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout for export requests.
|
||||
*
|
||||
* @param timeout the timeout duration
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder timeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets additional headers to include in export requests.
|
||||
*
|
||||
* @param headers a map of header names to values
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder headers(Map<String, String> headers) {
|
||||
this.headers.clear();
|
||||
this.headers.putAll(headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header to include in export requests.
|
||||
*
|
||||
* @param name the header name
|
||||
* @param value the header value
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder putHeader(String name, String value) {
|
||||
this.headers.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service name for OpenTelemetry traces.
|
||||
*
|
||||
* @param serviceName the service name (e.g., "langsmith-java", "my-application")
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder serviceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the OtelTraceExporter.
|
||||
*
|
||||
* @return a new OtelTraceExporter instance
|
||||
*/
|
||||
public OtelTraceExporter build() {
|
||||
OtelConfig.Builder configBuilder = config != null
|
||||
? null
|
||||
: OtelConfig.builder()
|
||||
.endpoint(endpoint != null ? endpoint : "http://localhost:4318/v1/traces")
|
||||
.enabled(enabled != null ? enabled : false)
|
||||
.timeout(timeout != null ? timeout : Duration.ofSeconds(10))
|
||||
.headers(headers);
|
||||
|
||||
if (configBuilder != null && serviceName != null) {
|
||||
configBuilder.serviceName(serviceName);
|
||||
}
|
||||
|
||||
OtelConfig finalConfig = config != null ? config : configBuilder.build();
|
||||
|
||||
return fromConfig(finalConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
-210
@@ -1,210 +0,0 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.context.ContextKey;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Immutable context for experiment metadata that will be automatically
|
||||
* attached to OpenTelemetry spans.
|
||||
*
|
||||
* <p>
|
||||
* This class provides a convenient way to associate experiment metadata with
|
||||
* LLM calls without requiring manual span creation. When you set experiment
|
||||
* context using this class, the wrapped OpenAI client will automatically
|
||||
* attach it to the spans it creates.
|
||||
*
|
||||
* <p>
|
||||
* This class follows the OpenTelemetry Context pattern with immutable
|
||||
* context objects and explicit scope management.
|
||||
*
|
||||
* <p>
|
||||
* Example usage (recommended - try-with-resources):
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Convenience method (recommended)
|
||||
* try (Scope scope = ExperimentContext.withExperiment("example-123", "session-789")) {
|
||||
* ChatCompletion completion = client.chat().completions().create(params);
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* This is particularly useful for running experiments where you want to link
|
||||
* LLM traces to specific dataset examples in LangSmith.
|
||||
*
|
||||
* <p>
|
||||
* Thread Safety: This class is thread-safe. Each thread maintains its own
|
||||
* context via OpenTelemetry's Context mechanism. Context instances are
|
||||
* immutable and safe to share across threads.
|
||||
*/
|
||||
public final class ExperimentContext {
|
||||
|
||||
private static final ContextKey<ExperimentData> CONTEXT_KEY = ContextKey.named("langsmith-experiment-context");
|
||||
|
||||
private final ExperimentData data;
|
||||
|
||||
private ExperimentContext(ExperimentData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current ExperimentContext from the OpenTelemetry Context.
|
||||
* Returns an empty context if no context has been set.
|
||||
*
|
||||
* @return the current ExperimentContext
|
||||
*/
|
||||
public static ExperimentContext current() {
|
||||
Context otelContext = Context.current();
|
||||
ExperimentData data = otelContext.get(CONTEXT_KEY);
|
||||
return new ExperimentContext(data != null ? data : ExperimentData.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to set all experiment context values and return a Scope.
|
||||
* This is the recommended method for running experiments as it properly links runs to
|
||||
* the experiment session.
|
||||
*
|
||||
* <pre>{@code
|
||||
* try (Scope scope = ExperimentContext.withExperiment(example.id(), session.id())) {
|
||||
* ChatCompletion completion = client.chat().completions().create(params);
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @param exampleId the reference example ID from your LangSmith dataset
|
||||
* @param sessionId the session/experiment UUID
|
||||
* @return a Scope that will restore the previous context when closed
|
||||
* @throws IllegalArgumentException if exampleId or sessionId is null or empty
|
||||
*/
|
||||
public static Scope withExperiment(String exampleId, String sessionId) {
|
||||
if (exampleId == null || exampleId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("exampleId cannot be null or empty");
|
||||
}
|
||||
if (sessionId == null || sessionId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("sessionId cannot be null or empty");
|
||||
}
|
||||
return current()
|
||||
.withReferenceExampleId(exampleId)
|
||||
.withSessionId(sessionId)
|
||||
.makeCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ExperimentContext with the reference example ID set.
|
||||
*
|
||||
* @param exampleId the reference example ID from your LangSmith dataset
|
||||
* @return a new ExperimentContext with the updated value
|
||||
* @throws IllegalArgumentException if exampleId is null or empty
|
||||
*/
|
||||
public ExperimentContext withReferenceExampleId(String exampleId) {
|
||||
if (exampleId == null || exampleId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("exampleId cannot be null or empty");
|
||||
}
|
||||
return new ExperimentContext(data.withReferenceExampleId(exampleId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ExperimentContext with the session ID set.
|
||||
*
|
||||
* @param sessionId the session/experiment UUID from LangSmith
|
||||
* @return a new ExperimentContext with the updated value
|
||||
* @throws IllegalArgumentException if sessionId is null or empty
|
||||
*/
|
||||
public ExperimentContext withSessionId(String sessionId) {
|
||||
if (sessionId == null || sessionId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("sessionId cannot be null or empty");
|
||||
}
|
||||
return new ExperimentContext(data.withSessionId(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new ExperimentContext with custom metadata added.
|
||||
* Metadata will be attached to spans as attributes prefixed with "langsmith.metadata.".
|
||||
*
|
||||
* @param key the metadata key
|
||||
* @param value the metadata value
|
||||
* @return a new ExperimentContext with the updated metadata
|
||||
* @throws IllegalArgumentException if key or value is null or empty
|
||||
*/
|
||||
public ExperimentContext withMetadata(String key, String value) {
|
||||
if (key == null || key.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("metadata key cannot be null or empty");
|
||||
}
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("metadata value cannot be null or empty");
|
||||
}
|
||||
return new ExperimentContext(data.withMetadata(key, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this ExperimentContext the current context in the OpenTelemetry Context.
|
||||
* Returns a Scope that will restore the previous context when closed.
|
||||
*/
|
||||
private Scope makeCurrent() {
|
||||
Context otelContext = Context.current().with(CONTEXT_KEY, this.data);
|
||||
return otelContext.makeCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the reference example ID.
|
||||
*
|
||||
* @return an Optional containing the reference example ID, or empty if not set
|
||||
*/
|
||||
public Optional<String> getReferenceExampleId() {
|
||||
return Optional.ofNullable(data.referenceExampleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the session ID.
|
||||
*
|
||||
* @return an Optional containing the session ID, or empty if not set
|
||||
*/
|
||||
public Optional<String> getSessionId() {
|
||||
return Optional.ofNullable(data.sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all custom metadata as an unmodifiable map.
|
||||
*
|
||||
* @return an unmodifiable map of metadata key-value pairs
|
||||
*/
|
||||
public Map<String, String> getMetadata() {
|
||||
return data.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable data holder for experiment context values.
|
||||
*/
|
||||
private static class ExperimentData {
|
||||
final String referenceExampleId;
|
||||
final String sessionId;
|
||||
final Map<String, String> metadata;
|
||||
|
||||
private ExperimentData(String referenceExampleId, String sessionId, Map<String, String> metadata) {
|
||||
this.referenceExampleId = referenceExampleId;
|
||||
this.sessionId = sessionId;
|
||||
this.metadata = Collections.unmodifiableMap(new HashMap<>(metadata));
|
||||
}
|
||||
|
||||
static ExperimentData empty() {
|
||||
return new ExperimentData(null, null, new HashMap<>());
|
||||
}
|
||||
|
||||
ExperimentData withReferenceExampleId(String exampleId) {
|
||||
return new ExperimentData(exampleId, sessionId, metadata);
|
||||
}
|
||||
|
||||
ExperimentData withSessionId(String sessionId) {
|
||||
return new ExperimentData(referenceExampleId, sessionId, metadata);
|
||||
}
|
||||
|
||||
ExperimentData withMetadata(String key, String value) {
|
||||
Map<String, String> newMetadata = new HashMap<>(metadata);
|
||||
newMetadata.put(key, value);
|
||||
return new ExperimentData(referenceExampleId, sessionId, newMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
-592
@@ -1,592 +0,0 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
|
||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
|
||||
import io.opentelemetry.sdk.OpenTelemetrySdk;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.SdkTracerProvider;
|
||||
import io.opentelemetry.sdk.trace.SpanProcessor;
|
||||
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
|
||||
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
|
||||
import io.opentelemetry.sdk.trace.export.SpanExporter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Configuration utility for setting up OpenTelemetry to export traces to
|
||||
* LangSmith.
|
||||
*
|
||||
* <p>
|
||||
* This class provides a convenient way to configure OpenTelemetry with
|
||||
* LangSmith's OTLP
|
||||
* endpoint using a Builder pattern. Configuration can be done programmatically
|
||||
* or via environment variables.
|
||||
*
|
||||
* <p>
|
||||
* Example usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Configure OpenTelemetry for LangSmith before using the wrapper
|
||||
* OpenTelemetryConfig.builder()
|
||||
* .apiKey("your-langsmith-api-key")
|
||||
* .projectName("your-project-name")
|
||||
* .build();
|
||||
*
|
||||
* // Or using only environment variables (LANGSMITH_API_KEY, LANGSMITH_PROJECT)
|
||||
* OpenTelemetryConfig.builder().build();
|
||||
*
|
||||
* // Now use the wrapped client - traces will be sent to LangSmith
|
||||
* WrappedOpenAIClient client = OpenAIWrappers.wrapFromEnv();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class OpenTelemetryConfig {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryConfig.class);
|
||||
|
||||
private OpenTelemetryConfig() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Default LangSmith base URL.
|
||||
* Can be overridden by setting LANGSMITH_ENDPOINT environment variable or
|
||||
* by passing a custom base URL to the configuration methods.
|
||||
*/
|
||||
public static final String DEFAULT_BASE_URL = "https://api.smith.langchain.com";
|
||||
|
||||
/**
|
||||
* OTLP traces endpoint path.
|
||||
* This path is appended to the base URL to construct the full OTLP endpoint.
|
||||
*/
|
||||
public static final String OTLP_TRACES_PATH = "/otel/v1/traces";
|
||||
|
||||
/**
|
||||
* Span processor type for configuring how spans are exported.
|
||||
*/
|
||||
public enum SpanProcessorType {
|
||||
/**
|
||||
* BatchSpanProcessor - Queues spans and exports them in batches.
|
||||
* Best for production use - efficient and non-blocking.
|
||||
*/
|
||||
BATCH,
|
||||
|
||||
/**
|
||||
* SimpleSpanProcessor - Exports spans synchronously on span.end().
|
||||
* Best for testing - predictable but blocks application thread.
|
||||
* WARNING: Not recommended for production due to performance impact.
|
||||
*/
|
||||
SIMPLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for configuring OpenTelemetry for LangSmith.
|
||||
* Provides a fluent API for setting only the parameters you need.
|
||||
*
|
||||
* <p>
|
||||
* Example usage:
|
||||
* <pre>{@code
|
||||
* OpenTelemetry otel = OpenTelemetryConfig.builder()
|
||||
* .apiKey("your-api-key")
|
||||
* .projectName("MyProject")
|
||||
* .processorType(SpanProcessorType.SIMPLE)
|
||||
* .maxBatchSize(1)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public static class Builder {
|
||||
private String apiKey;
|
||||
private String projectName;
|
||||
private String serviceName;
|
||||
private String baseUrl;
|
||||
private SpanProcessorType processorType = SpanProcessorType.BATCH;
|
||||
private int maxBatchSize = 512;
|
||||
|
||||
private Builder() {
|
||||
// Initialize from environment variables as defaults
|
||||
this.apiKey = System.getenv("LANGSMITH_API_KEY");
|
||||
this.projectName = System.getenv("LANGSMITH_PROJECT");
|
||||
this.serviceName = System.getenv("OTEL_SERVICE_NAME");
|
||||
this.baseUrl = System.getenv("LANGSMITH_ENDPOINT");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the LangSmith API key (optional, defaults to LANGSMITH_API_KEY env var).
|
||||
*
|
||||
* <p>
|
||||
* This is your authentication key for LangSmith. You can find it in your
|
||||
* LangSmith account settings.
|
||||
*
|
||||
* <p>
|
||||
* If not set explicitly, the builder will use the value from the
|
||||
* LANGSMITH_API_KEY environment variable. At least one of these must be provided.
|
||||
*
|
||||
* @param apiKey your LangSmith API key
|
||||
* @return this Builder for method chaining
|
||||
* @throws IllegalStateException if apiKey is null or empty when build() is called
|
||||
*/
|
||||
public Builder apiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the LangSmith project name (optional, defaults to LANGSMITH_PROJECT env var).
|
||||
*
|
||||
* <p>
|
||||
* If set, all traces will be sent to this project in LangSmith. The project
|
||||
* will be created automatically if it doesn't exist.
|
||||
*
|
||||
* <p>
|
||||
* If not set explicitly, the builder will use the value from the
|
||||
* LANGSMITH_PROJECT environment variable.
|
||||
*
|
||||
* <p>
|
||||
* <b>Note:</b> For experiments, you may want to leave this unset and instead
|
||||
* use {@link ExperimentContext#withExperiment(String, String)} to set
|
||||
* the session ID via span attributes. This prevents the project name from
|
||||
* overwriting the session's reference_dataset_id.
|
||||
*
|
||||
* @param projectName your LangSmith project name
|
||||
* @return this Builder for method chaining
|
||||
*/
|
||||
public Builder projectName(String projectName) {
|
||||
this.projectName = projectName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service name for OpenTelemetry (optional, defaults to OTEL_SERVICE_NAME env var).
|
||||
*
|
||||
* <p>
|
||||
* This identifies your application in the traces. If not set explicitly,
|
||||
* the builder will use the value from the OTEL_SERVICE_NAME environment variable,
|
||||
* or fall back to "langsmith-java-otel-wrappers" if not set.
|
||||
*
|
||||
* <p>
|
||||
* The service name appears in the trace metadata and can be used to filter
|
||||
* traces from different services.
|
||||
*
|
||||
* @param serviceName the service name to identify your application
|
||||
* @return this Builder for method chaining
|
||||
*/
|
||||
public Builder serviceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the LangSmith base URL (optional, defaults to LANGSMITH_ENDPOINT env var).
|
||||
*
|
||||
* <p>
|
||||
* The base URL is used to construct the OTLP endpoint by appending "/otel/v1/traces".
|
||||
* If not set explicitly, the builder will use the value from the LANGSMITH_ENDPOINT
|
||||
* environment variable, or default to "https://api.smith.langchain.com".
|
||||
*
|
||||
* <p>
|
||||
* This is useful for:
|
||||
* <ul>
|
||||
* <li>Self-hosted LangSmith instances</li>
|
||||
* <li>Internal/development environments</li>
|
||||
* <li>Testing against different LangSmith deployments</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param baseUrl the LangSmith base URL (e.g., "https://dev.api.smith.langchain.com")
|
||||
* @return this Builder for method chaining
|
||||
*/
|
||||
public Builder baseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the span processor type (optional).
|
||||
*
|
||||
* <p>
|
||||
* Determines how spans are processed and exported:
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>BATCH</b> (default, recommended for production):
|
||||
* <ul>
|
||||
* <li>Queues spans and exports them in batches</li>
|
||||
* <li>Non-blocking - doesn't slow down your application</li>
|
||||
* <li>Efficient - reduces network overhead</li>
|
||||
* <li>May have a slight delay before spans appear in LangSmith</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>SIMPLE</b> (recommended for testing/examples):
|
||||
* <ul>
|
||||
* <li>Exports spans immediately when span.end() is called</li>
|
||||
* <li>Blocking - waits for export to complete</li>
|
||||
* <li>Predictable - spans appear in LangSmith right away</li>
|
||||
* <li><b>Warning:</b> Can impact performance, not recommended for production</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>When to use SIMPLE:</b>
|
||||
* <ul>
|
||||
* <li>Short-lived applications or scripts</li>
|
||||
* <li>Testing and debugging</li>
|
||||
* <li>When you need immediate visibility of traces</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>When to use BATCH:</b>
|
||||
* <ul>
|
||||
* <li>Production applications</li>
|
||||
* <li>High-throughput services</li>
|
||||
* <li>Long-running applications</li>
|
||||
* <li>When performance is critical</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param processorType the span processor type (BATCH or SIMPLE)
|
||||
* @return this Builder for method chaining
|
||||
* @see SpanProcessorType
|
||||
*/
|
||||
public Builder processorType(SpanProcessorType processorType) {
|
||||
this.processorType = processorType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum batch size for span export (optional).
|
||||
*
|
||||
* <p>
|
||||
* <b>Only applies to BATCH processor type.</b> Ignored when using SIMPLE processor.
|
||||
*
|
||||
* <p>
|
||||
* Controls how many spans are batched together before triggering an export:
|
||||
* <ul>
|
||||
* <li><b>Default: 512</b> - Good balance between efficiency and latency</li>
|
||||
* <li><b>1</b> - Exports immediately (still non-blocking unlike SIMPLE)</li>
|
||||
* <li><b>Higher values</b> - More efficient but increased latency</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>Setting to 1:</b> If you want immediate export but don't want to block
|
||||
* your application thread, use BATCH processor with maxBatchSize=1 instead of
|
||||
* SIMPLE processor. This gives you non-blocking immediate export.
|
||||
*
|
||||
* <p>
|
||||
* Example for immediate non-blocking export:
|
||||
* <pre>{@code
|
||||
* OpenTelemetryConfig.builder()
|
||||
* .apiKey(apiKey)
|
||||
* .processorType(SpanProcessorType.BATCH)
|
||||
* .maxBatchSize(1) // Export immediately but don't block
|
||||
* .build();
|
||||
* }</pre>
|
||||
*
|
||||
* @param maxBatchSize the maximum batch size (1-512, default 512)
|
||||
* @return this Builder for method chaining
|
||||
*/
|
||||
public Builder maxBatchSize(int maxBatchSize) {
|
||||
this.maxBatchSize = maxBatchSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and configures the OpenTelemetry instance.
|
||||
*
|
||||
* @return the configured OpenTelemetry instance
|
||||
* @throws IllegalStateException if apiKey is not set
|
||||
*/
|
||||
public OpenTelemetry build() {
|
||||
// Validate required fields
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"LangSmith API key is required. Set it using apiKey() or LANGSMITH_API_KEY environment"
|
||||
+ " variable.");
|
||||
}
|
||||
|
||||
// Build OTLP endpoint from base URL
|
||||
String endpointUrl = buildOtlpEndpoint(baseUrl);
|
||||
|
||||
// Create OTLP HTTP exporter configured for LangSmith
|
||||
OtlpHttpSpanExporterBuilder exporterBuilder =
|
||||
OtlpHttpSpanExporter.builder().setEndpoint(endpointUrl).addHeader("x-api-key", apiKey);
|
||||
|
||||
// Only add project header if projectName is not null and not empty
|
||||
if (projectName != null && !projectName.isEmpty()) {
|
||||
exporterBuilder.addHeader("Langsmith-Project", projectName);
|
||||
}
|
||||
|
||||
OtlpHttpSpanExporter spanExporter = exporterBuilder.build();
|
||||
|
||||
// Wrap exporter to log export errors
|
||||
SpanExporter loggingExporter = new LoggingSpanExporter(spanExporter);
|
||||
|
||||
// Create resource with service name
|
||||
Resource resource = Resource.getDefault()
|
||||
.merge(Resource.builder()
|
||||
.put("service.name", serviceName != null ? serviceName : "langsmith-java-otel-wrappers")
|
||||
.build());
|
||||
|
||||
// Build and configure span processor based on type
|
||||
SpanProcessor spanProcessor;
|
||||
if (processorType == SpanProcessorType.SIMPLE) {
|
||||
// SimpleSpanProcessor - exports synchronously on span.end()
|
||||
// Good for testing, but blocks the application thread
|
||||
spanProcessor = SimpleSpanProcessor.create(loggingExporter);
|
||||
} else {
|
||||
// BatchSpanProcessor - queues and exports in batches
|
||||
// Good for production, non-blocking
|
||||
// If maxBatchSize is 1, spans are exported immediately as they complete
|
||||
spanProcessor = BatchSpanProcessor.builder(loggingExporter)
|
||||
.setScheduleDelay(100, TimeUnit.MILLISECONDS) // Export every 100ms
|
||||
.setMaxExportBatchSize(maxBatchSize) // Trigger export when batch reaches this size
|
||||
.setExporterTimeout(5, TimeUnit.SECONDS) // 5 second timeout
|
||||
.build();
|
||||
}
|
||||
|
||||
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
|
||||
.addSpanProcessor(spanProcessor)
|
||||
.setResource(resource)
|
||||
.build();
|
||||
|
||||
return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Builder for configuring OpenTelemetry.
|
||||
*
|
||||
* @return a new Builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces flushing of all pending spans to ensure they are exported.
|
||||
* This should be called before application shutdown to ensure all spans
|
||||
* are sent to LangSmith.
|
||||
*
|
||||
* @return true if flush completed successfully, false otherwise
|
||||
*/
|
||||
public static boolean flush() {
|
||||
return flush(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces flushing of all pending spans to ensure they are exported.
|
||||
* This should be called before application shutdown to ensure all spans
|
||||
* are sent to LangSmith.
|
||||
*
|
||||
* @param timeout the maximum time to wait for flush to complete
|
||||
* @param unit the time unit of the timeout
|
||||
* @return true if flush completed successfully, false otherwise
|
||||
*/
|
||||
public static boolean flush(long timeout, TimeUnit unit) {
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
if (openTelemetry instanceof OpenTelemetrySdk) {
|
||||
// Don't close the SDK instance - it's the global instance that should remain
|
||||
// alive
|
||||
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
|
||||
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
|
||||
if (tracerProvider != null) {
|
||||
try {
|
||||
io.opentelemetry.sdk.common.CompletableResultCode result = tracerProvider.forceFlush();
|
||||
result.join(timeout, unit);
|
||||
if (!result.isSuccess()) {
|
||||
logger.warn("Flush did not complete successfully");
|
||||
}
|
||||
return result.isSuccess();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to flush spans", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the OTLP endpoint URL from a base URL.
|
||||
*
|
||||
* <p>
|
||||
* The endpoint is constructed as: baseUrl + OTLP_TRACES_PATH
|
||||
*
|
||||
* <p>
|
||||
* If baseUrl is null or empty, defaults to DEFAULT_BASE_URL.
|
||||
*
|
||||
* @param baseUrl the base URL (can be null)
|
||||
* @return the OTLP endpoint URL
|
||||
*/
|
||||
private static String buildOtlpEndpoint(String baseUrl) {
|
||||
// Use provided base URL or default
|
||||
String effectiveBaseUrl = baseUrl;
|
||||
if (effectiveBaseUrl == null || effectiveBaseUrl.isEmpty()) {
|
||||
effectiveBaseUrl = System.getenv("LANGSMITH_ENDPOINT");
|
||||
}
|
||||
if (effectiveBaseUrl == null || effectiveBaseUrl.isEmpty()) {
|
||||
effectiveBaseUrl = DEFAULT_BASE_URL;
|
||||
}
|
||||
|
||||
// Remove trailing slash if present
|
||||
if (effectiveBaseUrl.endsWith("/")) {
|
||||
effectiveBaseUrl = effectiveBaseUrl.substring(0, effectiveBaseUrl.length() - 1);
|
||||
}
|
||||
|
||||
return effectiveBaseUrl + OTLP_TRACES_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the OpenTelemetry SDK and ensures all spans are exported.
|
||||
* This should be called before application shutdown.
|
||||
*/
|
||||
public static void shutdown() {
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
if (openTelemetry instanceof OpenTelemetrySdk) {
|
||||
// Don't close the SDK instance - it's the global instance that should remain
|
||||
// alive
|
||||
@SuppressWarnings("resource")
|
||||
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
|
||||
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
|
||||
if (tracerProvider != null) {
|
||||
try {
|
||||
tracerProvider.shutdown().join(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to shutdown OpenTelemetry", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper SpanExporter that logs export errors to help debug issues.
|
||||
*/
|
||||
private static class LoggingSpanExporter implements SpanExporter {
|
||||
private final SpanExporter delegate;
|
||||
private static final boolean DEBUG =
|
||||
Boolean.getBoolean("langsmith.debug") || "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
|
||||
LoggingSpanExporter(SpanExporter delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public io.opentelemetry.sdk.common.CompletableResultCode export(
|
||||
java.util.Collection<io.opentelemetry.sdk.trace.data.SpanData> spans) {
|
||||
if (DEBUG) {
|
||||
logger.debug("[LangSmith] Exporting " + spans.size() + " span(s):");
|
||||
for (io.opentelemetry.sdk.trace.data.SpanData span : spans) {
|
||||
logger.debug(" - " + span.getName()
|
||||
+ " (kind=" + span.getKind()
|
||||
+ ", attributes=" + span.getAttributes().size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.export(spans);
|
||||
|
||||
// For SimpleSpanProcessor, wait for the result synchronously to get immediate
|
||||
// feedback
|
||||
// For BatchSpanProcessor, this will return immediately but we can still check
|
||||
// status
|
||||
if (DEBUG) {
|
||||
// Wait up to 5 seconds for the result
|
||||
try {
|
||||
result.join(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (!result.isSuccess()) {
|
||||
logger.error("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
|
||||
|
||||
// Try to get more error details
|
||||
try {
|
||||
// Check if there's an exception
|
||||
java.lang.reflect.Method getExceptionMethod =
|
||||
result.getClass().getMethod("getException");
|
||||
Throwable exception = (Throwable) getExceptionMethod.invoke(result);
|
||||
if (exception != null) {
|
||||
logger.error(" Exception: "
|
||||
+ exception.getClass().getName() + ": " + exception.getMessage());
|
||||
if (exception.getCause() != null) {
|
||||
logger.error(" Caused by: "
|
||||
+ exception.getCause().getClass().getName() + ": "
|
||||
+ exception.getCause().getMessage());
|
||||
}
|
||||
// Print full stack trace in debug mode
|
||||
java.io.StringWriter sw = new java.io.StringWriter();
|
||||
exception.printStackTrace(new java.io.PrintWriter(sw));
|
||||
logger.debug(" Stack trace:\n" + sw.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Reflection failed, try to get error message another way
|
||||
logger.debug(" Could not extract exception details: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Log span details for debugging
|
||||
logger.error(" Spans being exported:");
|
||||
for (io.opentelemetry.sdk.trace.data.SpanData span : spans) {
|
||||
logger.error(" - " + span.getName() + " (traceId=" + span.getTraceId() + ", spanId="
|
||||
+ span.getSpanId() + ")");
|
||||
logger.debug(" Attributes: " + span.getAttributes());
|
||||
}
|
||||
|
||||
logger.error(" This usually indicates a network error, authentication problem, or invalid span"
|
||||
+ " data");
|
||||
logger.error(" Check your LANGSMITH_API_KEY and network connectivity");
|
||||
} else {
|
||||
logger.debug("[LangSmith] Successfully exported " + spans.size() + " span(s)");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("[LangSmith ERROR] Exception waiting for export result", e);
|
||||
}
|
||||
} else {
|
||||
// Without DEBUG, still log errors but don't block
|
||||
result.whenComplete(() -> {
|
||||
if (!result.isSuccess()) {
|
||||
logger.error("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
|
||||
|
||||
// Try to get exception details even without DEBUG
|
||||
try {
|
||||
java.lang.reflect.Method getExceptionMethod =
|
||||
result.getClass().getMethod("getException");
|
||||
Throwable exception = (Throwable) getExceptionMethod.invoke(result);
|
||||
if (exception != null) {
|
||||
logger.error(" Error: " + exception.getMessage());
|
||||
if (exception.getCause() != null) {
|
||||
logger.error(" Caused by: "
|
||||
+ exception.getCause().getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore reflection errors
|
||||
}
|
||||
|
||||
logger.error(" This usually indicates a network error, authentication problem, or invalid span"
|
||||
+ " data");
|
||||
logger.error(" Check your LANGSMITH_API_KEY and network connectivity");
|
||||
logger.error(" Set LANGSMITH_DEBUG=true for more details");
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public io.opentelemetry.sdk.common.CompletableResultCode flush() {
|
||||
if (DEBUG) {
|
||||
logger.debug("[LangSmith] Flushing spans...");
|
||||
}
|
||||
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.flush();
|
||||
result.whenComplete(() -> {
|
||||
if (!result.isSuccess()) {
|
||||
logger.error("[LangSmith ERROR] Failed to flush spans");
|
||||
} else if (DEBUG) {
|
||||
logger.debug("[LangSmith] Flush completed successfully");
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public io.opentelemetry.sdk.common.CompletableResultCode shutdown() {
|
||||
if (DEBUG) {
|
||||
logger.debug("[LangSmith] Shutting down span exporter...");
|
||||
}
|
||||
return delegate.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.SpanBuilder;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
|
||||
/** Internal utility for OpenTelemetry span creation and management. */
|
||||
final class TracingUtils {
|
||||
|
||||
private static final String INSTRUMENTATION_NAME = "langsmith-java-otel-wrappers";
|
||||
|
||||
private TracingUtils() {}
|
||||
|
||||
static Tracer getTracer() {
|
||||
try {
|
||||
Tracer tracer = io.opentelemetry.api.GlobalOpenTelemetry.get().getTracer(INSTRUMENTATION_NAME);
|
||||
|
||||
boolean debug =
|
||||
Boolean.getBoolean("langsmith.debug") || "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
if (debug) {
|
||||
io.opentelemetry.api.OpenTelemetry otel = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
boolean isNoop = otel.getClass().getName().contains("Noop");
|
||||
System.out.println("[TracingUtils] Tracer obtained: "
|
||||
+ tracer.getClass().getName() + ", OpenTelemetry isNoop: " + isNoop);
|
||||
}
|
||||
|
||||
return tracer;
|
||||
} catch (Exception e) {
|
||||
return io.opentelemetry.api.GlobalOpenTelemetry.get().getTracer(INSTRUMENTATION_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
static SpanBuilder createSpanBuilder(String model, String operationType, String spanKind) {
|
||||
Tracer tracer = getTracer();
|
||||
String spanName = operationType + " " + (model != null ? model : "unknown");
|
||||
SpanBuilder builder = tracer.spanBuilder(spanName)
|
||||
.setSpanKind(SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.system", "openai")
|
||||
.setAttribute("gen_ai.operation.name", operationType)
|
||||
.setAttribute("gen_ai.provider.name", "openai");
|
||||
|
||||
if (spanKind != null) {
|
||||
builder.setAttribute("langsmith.span.kind", spanKind);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
static SpanBuilder createSpanBuilder(String model, String operationType) {
|
||||
return createSpanBuilder(model, operationType, "llm");
|
||||
}
|
||||
|
||||
static void setRequestAttributes(Span span, String model) {
|
||||
if (model != null) {
|
||||
span.setAttribute("gen_ai.request.model", model);
|
||||
}
|
||||
}
|
||||
|
||||
static void setRequestParameters(Span span, Double temperature, Double topP, Long maxTokens) {
|
||||
if (temperature != null) {
|
||||
span.setAttribute("gen_ai.request.temperature", temperature);
|
||||
}
|
||||
if (topP != null) {
|
||||
span.setAttribute("gen_ai.request.top_p", topP);
|
||||
}
|
||||
if (maxTokens != null) {
|
||||
span.setAttribute("gen_ai.request.max_tokens", maxTokens);
|
||||
}
|
||||
}
|
||||
|
||||
static void setInputMessages(Span span, String messagesJson) {
|
||||
if (messagesJson != null) {
|
||||
span.setAttribute("gen_ai.input.messages", messagesJson);
|
||||
}
|
||||
}
|
||||
|
||||
static void setOutputMessages(Span span, String messagesJson) {
|
||||
if (messagesJson != null) {
|
||||
span.setAttribute("gen_ai.output.messages", messagesJson);
|
||||
}
|
||||
}
|
||||
|
||||
static void setResponseAttributes(Span span, Long inputTokens, Long outputTokens, Long totalTokens) {
|
||||
if (inputTokens != null) {
|
||||
span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
|
||||
}
|
||||
if (outputTokens != null) {
|
||||
span.setAttribute("gen_ai.usage.output_tokens", outputTokens);
|
||||
}
|
||||
if (totalTokens != null) {
|
||||
span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
|
||||
}
|
||||
}
|
||||
|
||||
static void setResponseMetadata(Span span, String responseModel, String finishReason) {
|
||||
if (responseModel != null) {
|
||||
span.setAttribute("gen_ai.response.model", responseModel);
|
||||
}
|
||||
if (finishReason != null) {
|
||||
span.setAttribute("gen_ai.response.finish_reason", finishReason);
|
||||
}
|
||||
}
|
||||
|
||||
static void recordException(Span span, Throwable exception) {
|
||||
span.recordException(exception);
|
||||
span.setAttribute("error", true);
|
||||
}
|
||||
|
||||
static String escapeJsonString(String str) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
}
|
||||
return str.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
-1009
File diff suppressed because it is too large
Load Diff
-290
@@ -1,290 +0,0 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.core.ClientOptions;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Wrapped OpenAI client that maintains the same developer experience as the
|
||||
* original client
|
||||
* while adding LangSmith tracing capabilities.
|
||||
*
|
||||
* <p>
|
||||
* This wrapper delegates all calls to the underlying OpenAI client, allowing
|
||||
* all
|
||||
* configuration options and methods to work exactly as they would with the
|
||||
* original client.
|
||||
*/
|
||||
public class WrappedOpenAIClient implements OpenAIClient {
|
||||
|
||||
private final OpenAIClient delegate;
|
||||
|
||||
/**
|
||||
* Creates a new wrapped client that delegates to the provided client.
|
||||
*
|
||||
* @param delegate the underlying OpenAI client to wrap
|
||||
*/
|
||||
public WrappedOpenAIClient(OpenAIClient delegate) {
|
||||
if (delegate == null) {
|
||||
throw new IllegalArgumentException("Delegate client cannot be null");
|
||||
}
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying delegate client.
|
||||
*
|
||||
* @return the wrapped OpenAI client
|
||||
*/
|
||||
public OpenAIClient getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.client.OpenAIClientAsync async() {
|
||||
return delegate.async();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAIClient.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAIClient withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return delegate.withOptions(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.CompletionService completions() {
|
||||
return delegate.completions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ChatService chat() {
|
||||
return new WrappedChatService(delegate.chat());
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.EmbeddingService embeddings() {
|
||||
return delegate.embeddings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.FileService files() {
|
||||
return delegate.files();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ImageService images() {
|
||||
return delegate.images();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.AudioService audio() {
|
||||
return delegate.audio();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ModerationService moderations() {
|
||||
return delegate.moderations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ModelService models() {
|
||||
return delegate.models();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.FineTuningService fineTuning() {
|
||||
return delegate.fineTuning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.GraderService graders() {
|
||||
return delegate.graders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.VectorStoreService vectorStores() {
|
||||
return delegate.vectorStores();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.WebhookService webhooks() {
|
||||
return delegate.webhooks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.BetaService beta() {
|
||||
return delegate.beta();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.BatchService batches() {
|
||||
return delegate.batches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.UploadService uploads() {
|
||||
return delegate.uploads();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ResponseService responses() {
|
||||
return new WrappedResponseService(delegate.responses());
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.RealtimeService realtime() {
|
||||
return delegate.realtime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ConversationService conversations() {
|
||||
return delegate.conversations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.EvalService evals() {
|
||||
return delegate.evals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ContainerService containers() {
|
||||
return delegate.containers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.VideoService videos() {
|
||||
return delegate.videos();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating wrapped OpenAI clients with the same configuration
|
||||
* options
|
||||
* as the original client builder.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final OpenAIOkHttpClient.Builder delegateBuilder;
|
||||
|
||||
/**
|
||||
* Creates a new builder that wraps the OpenAI client builder.
|
||||
*/
|
||||
public Builder() {
|
||||
this.delegateBuilder = OpenAIOkHttpClient.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder that wraps the OpenAI client builder, starting from
|
||||
* environment variables.
|
||||
*
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder fromEnv() {
|
||||
delegateBuilder.fromEnv();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API key.
|
||||
*
|
||||
* @param apiKey the OpenAI API key
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder apiKey(String apiKey) {
|
||||
delegateBuilder.apiKey(apiKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the organization ID.
|
||||
*
|
||||
* @param organization the organization ID
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder organization(String organization) {
|
||||
delegateBuilder.organization(organization);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the project ID.
|
||||
*
|
||||
* @param project the project ID
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder project(String project) {
|
||||
delegateBuilder.project(project);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the webhook secret.
|
||||
*
|
||||
* @param webhookSecret the webhook secret
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder webhookSecret(String webhookSecret) {
|
||||
delegateBuilder.webhookSecret(webhookSecret);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL.
|
||||
*
|
||||
* @param baseUrl the base URL
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder baseUrl(String baseUrl) {
|
||||
delegateBuilder.baseUrl(baseUrl);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the wrapped OpenAI client.
|
||||
*
|
||||
* @return a new wrapped OpenAI client
|
||||
*/
|
||||
public WrappedOpenAIClient build() {
|
||||
return new WrappedOpenAIClient(delegateBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder for constructing wrapped OpenAI clients.
|
||||
*
|
||||
* @return a new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an existing OpenAI client to add LangSmith tracing capabilities.
|
||||
*
|
||||
* <p>This is a convenience method equivalent to using the constructor directly.
|
||||
*
|
||||
* @param client the OpenAI client to wrap
|
||||
* @return a wrapped client that delegates to the original client
|
||||
* @throws IllegalArgumentException if client is null
|
||||
*/
|
||||
public static WrappedOpenAIClient wrap(OpenAIClient client) {
|
||||
return new WrappedOpenAIClient(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapped OpenAI client from environment variables.
|
||||
*
|
||||
* @return a new wrapped OpenAI client configured from environment variables
|
||||
*/
|
||||
public static WrappedOpenAIClient fromEnv() {
|
||||
return builder().fromEnv().build();
|
||||
}
|
||||
}
|
||||
-433
@@ -1,433 +0,0 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.core.ClientOptions;
|
||||
import com.openai.core.RequestOptions;
|
||||
import com.openai.core.http.StreamResponse;
|
||||
import com.openai.models.responses.Response;
|
||||
import com.openai.models.responses.ResponseCreateParams;
|
||||
import com.openai.models.responses.ResponseStreamEvent;
|
||||
import com.openai.models.responses.StructuredResponse;
|
||||
import com.openai.models.responses.StructuredResponseCreateParams;
|
||||
import com.openai.services.blocking.ResponseService;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Wrapped ResponseService that adds OpenTelemetry tracing to response operations.
|
||||
*/
|
||||
class WrappedResponseService implements ResponseService {
|
||||
|
||||
private final ResponseService delegate;
|
||||
|
||||
WrappedResponseService(ResponseService delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseService.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseService withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return new WrappedResponseService(delegate.withOptions(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.responses.InputItemService inputItems() {
|
||||
return delegate.inputItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.responses.InputTokenService inputTokens() {
|
||||
return delegate.inputTokens();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create() {
|
||||
return create((ResponseCreateParams) null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create(RequestOptions requestOptions) {
|
||||
return create((ResponseCreateParams) null, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create(ResponseCreateParams params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create(ResponseCreateParams params, RequestOptions requestOptions) {
|
||||
// Extract model from params
|
||||
String model =
|
||||
params != null && params.model().isPresent() ? params.model().toString() : null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set experiment context attributes if present
|
||||
setExperimentContextAttributes(span);
|
||||
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null) {
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
Response result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// Extract response model (simplified - just use the request model)
|
||||
TracingUtils.setResponseMetadata(span, model, null);
|
||||
|
||||
// Extract usage information from result
|
||||
if (result.usage().isPresent()) {
|
||||
com.openai.models.responses.ResponseUsage usage = result.usage().get();
|
||||
TracingUtils.setResponseAttributes(
|
||||
span, (long) usage.inputTokens(), (long) usage.outputTokens(), (long) usage.totalTokens());
|
||||
}
|
||||
|
||||
if (result.status() != null) {
|
||||
span.setAttribute("gen_ai.response.status", result.status().toString());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params, RequestOptions requestOptions) {
|
||||
// Get model from rawParams
|
||||
String model = params != null
|
||||
&& params.rawParams() != null
|
||||
&& params.rawParams().model() != null
|
||||
? params.rawParams().model().toString()
|
||||
: null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set experiment context attributes if present
|
||||
setExperimentContextAttributes(span);
|
||||
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null && params.rawParams() != null) {
|
||||
Double temperature = params.rawParams().temperature().orElse(null);
|
||||
Double topP = params.rawParams().topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
StructuredResponse<T> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// Extract response model (simplified - just use the request model)
|
||||
TracingUtils.setResponseMetadata(span, model, null);
|
||||
|
||||
if (result.usage().isPresent()) {
|
||||
com.openai.models.responses.ResponseUsage usage = result.usage().get();
|
||||
TracingUtils.setResponseAttributes(
|
||||
span, (long) usage.inputTokens(), (long) usage.outputTokens(), (long) usage.totalTokens());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming() {
|
||||
return createStreaming((ResponseCreateParams) null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(RequestOptions requestOptions) {
|
||||
return createStreaming((ResponseCreateParams) null, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(ResponseCreateParams params) {
|
||||
return createStreaming(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(
|
||||
ResponseCreateParams params, RequestOptions requestOptions) {
|
||||
String model = params != null && params.model() != null ? params.model().toString() : null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set experiment context attributes if present
|
||||
setExperimentContextAttributes(span);
|
||||
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
span.setAttribute("gen_ai.streaming", true);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null) {
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
StreamResponse<ResponseStreamEvent> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.createStreaming(params);
|
||||
} else {
|
||||
result = delegate.createStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
// Note: For streaming, the span will end immediately
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(StructuredResponseCreateParams<?> params) {
|
||||
return createStreaming(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(
|
||||
StructuredResponseCreateParams<?> params, RequestOptions requestOptions) {
|
||||
// Get model from rawParams
|
||||
String model = params != null
|
||||
&& params.rawParams() != null
|
||||
&& params.rawParams().model() != null
|
||||
? params.rawParams().model().toString()
|
||||
: null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set experiment context attributes if present
|
||||
setExperimentContextAttributes(span);
|
||||
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
span.setAttribute("gen_ai.streaming", true);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null && params.rawParams() != null) {
|
||||
Double temperature = params.rawParams().temperature().orElse(null);
|
||||
Double topP = params.rawParams().topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
StreamResponse<ResponseStreamEvent> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.createStreaming(params);
|
||||
} else {
|
||||
result = delegate.createStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets experiment context attributes on the span if they are present.
|
||||
* This includes reference example ID, session ID, and metadata.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
*/
|
||||
private void setExperimentContextAttributes(Span span) {
|
||||
// Set reference example ID if present
|
||||
ExperimentContext.current()
|
||||
.getReferenceExampleId()
|
||||
.filter(id -> !id.isEmpty())
|
||||
.ifPresent(id -> span.setAttribute("langsmith.reference_example_id", id));
|
||||
|
||||
// Set session ID (experiment ID) if present
|
||||
// This is critical for linking runs to experiments in the dataset's Experiments tab
|
||||
ExperimentContext.current()
|
||||
.getSessionId()
|
||||
.filter(id -> !id.isEmpty())
|
||||
.ifPresent(id -> span.setAttribute("langsmith.trace.session_id", id));
|
||||
|
||||
// Set custom metadata
|
||||
java.util.Map<String, String> metadata = ExperimentContext.current().getMetadata();
|
||||
for (java.util.Map.Entry<String, String> entry : metadata.entrySet()) {
|
||||
span.setAttribute("langsmith.metadata." + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate other methods without tracing (for now)
|
||||
@Override
|
||||
public Response retrieve(String responseId) {
|
||||
return delegate.retrieve(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(String responseId, RequestOptions requestOptions) {
|
||||
return delegate.retrieve(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(String responseId, com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieve(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(
|
||||
String responseId,
|
||||
com.openai.models.responses.ResponseRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieve(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieve(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params, RequestOptions requestOptions) {
|
||||
return delegate.retrieve(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId) {
|
||||
return delegate.retrieveStreaming(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId, RequestOptions requestOptions) {
|
||||
return delegate.retrieveStreaming(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
|
||||
String responseId, com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieveStreaming(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
|
||||
String responseId,
|
||||
com.openai.models.responses.ResponseRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieveStreaming(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
|
||||
com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieveStreaming(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
|
||||
com.openai.models.responses.ResponseRetrieveParams params, RequestOptions requestOptions) {
|
||||
return delegate.retrieveStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId) {
|
||||
delegate.delete(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId, RequestOptions requestOptions) {
|
||||
delegate.delete(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId, com.openai.models.responses.ResponseDeleteParams params) {
|
||||
delegate.delete(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(
|
||||
String responseId, com.openai.models.responses.ResponseDeleteParams params, RequestOptions requestOptions) {
|
||||
delegate.delete(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(com.openai.models.responses.ResponseDeleteParams params) {
|
||||
delegate.delete(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(com.openai.models.responses.ResponseDeleteParams params, RequestOptions requestOptions) {
|
||||
delegate.delete(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId) {
|
||||
return delegate.cancel(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId, RequestOptions requestOptions) {
|
||||
return delegate.cancel(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId, com.openai.models.responses.ResponseCancelParams params) {
|
||||
return delegate.cancel(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(
|
||||
String responseId, com.openai.models.responses.ResponseCancelParams params, RequestOptions requestOptions) {
|
||||
return delegate.cancel(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(com.openai.models.responses.ResponseCancelParams params) {
|
||||
return delegate.cancel(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(com.openai.models.responses.ResponseCancelParams params, RequestOptions requestOptions) {
|
||||
return delegate.cancel(params, requestOptions);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.langchain.smith.client
|
||||
|
||||
import com.langchain.smith.models.info.InfoListResponse.BatchIngestConfig
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
internal data class AutoBatchIngestLimits(
|
||||
val batchSizeLimit: Int = AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT,
|
||||
val batchSizeLimitBytes: Int = AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT_BYTES,
|
||||
val useMultipartEndpoint: Boolean = true,
|
||||
)
|
||||
|
||||
internal fun BatchIngestConfig?.toAutoBatchIngestLimits(): AutoBatchIngestLimits =
|
||||
AutoBatchIngestLimits(
|
||||
batchSizeLimit =
|
||||
this?.sizeLimit()?.getOrNull()?.takeIf { it > 0 && it <= Int.MAX_VALUE }?.toInt()
|
||||
?: AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT,
|
||||
batchSizeLimitBytes =
|
||||
this?.sizeLimitBytes()?.getOrNull()?.takeIf { it > 0 && it <= Int.MAX_VALUE }?.toInt()
|
||||
?: AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT_BYTES,
|
||||
useMultipartEndpoint = this?.useMultipartEndpoint()?.getOrNull() ?: true,
|
||||
)
|
||||
@@ -0,0 +1,519 @@
|
||||
package com.langchain.smith.client
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.langchain.smith.core.RequestOptions
|
||||
import com.langchain.smith.core.Timeout
|
||||
import com.langchain.smith.core.http.Headers
|
||||
import com.langchain.smith.core.http.QueryParams
|
||||
import com.langchain.smith.core.jsonMapper
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import com.langchain.smith.models.runs.RunIngestBatchParams
|
||||
import java.util.concurrent.CompletionException
|
||||
import java.util.concurrent.CompletionStage
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Phaser
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Batches run create/update operations and sends them to LangSmith in a single `ingestBatch`
|
||||
* request, reducing HTTP overhead.
|
||||
*
|
||||
* Operations are buffered and flushed either:
|
||||
* - When the buffer reaches [batchSizeLimit] operations
|
||||
* - After [aggregationDelayMs] milliseconds of inactivity (timer-based drain)
|
||||
* - When [flush] is called explicitly
|
||||
*
|
||||
* @param sendBatch sends a batch and completes when the send has finished
|
||||
* @param batchSizeLimit max operations before auto-flush (default 100)
|
||||
* @param aggregationDelayMs inactivity delay before timer-based flush (default 250ms, matching JS
|
||||
* auto-batch aggregation and Python's initial queue wait)
|
||||
* @param sendParallelism max number of batch requests to send concurrently (default 4)
|
||||
* @param maxAggregationDelayMs max delay before timer-based flush after first item in a burst
|
||||
* (default 500ms, matching Python's background flush interval)
|
||||
* @param batchSizeLimitBytes max serialized JSON body size per batch (default 20 MiB)
|
||||
*/
|
||||
class AutoBatchQueue(
|
||||
private val sendBatch: (RunIngestBatchParams, RequestOptions) -> CompletionStage<Void?>,
|
||||
private val batchSizeLimit: Int = DEFAULT_BATCH_SIZE_LIMIT,
|
||||
private val aggregationDelayMs: Long = DEFAULT_AGGREGATION_DELAY_MS,
|
||||
private val sendParallelism: Int = DEFAULT_SEND_PARALLELISM,
|
||||
private val maxAggregationDelayMs: Long = DEFAULT_MAX_AGGREGATION_DELAY_MS,
|
||||
private val batchSizeLimitBytes: Int,
|
||||
) {
|
||||
private val items = ConcurrentLinkedQueue<BatchItem>()
|
||||
private val queuedCount = AtomicInteger(0)
|
||||
private val shutdown = AtomicBoolean(false)
|
||||
private val enqueueShutdownLock = ReentrantLock()
|
||||
private val delayedFlushLock = ReentrantLock()
|
||||
private var delayedFlushFuture: ScheduledFuture<*>? = null
|
||||
private var firstQueuedAtNanos: Long? = null
|
||||
private val activeSends =
|
||||
object : Phaser(0) {
|
||||
override fun onAdvance(phase: Int, registeredParties: Int): Boolean = false
|
||||
}
|
||||
|
||||
private val coordinator: ScheduledExecutorService =
|
||||
Executors.newSingleThreadScheduledExecutor { r ->
|
||||
Thread(r, "langsmith-batch-coordinator").apply { isDaemon = true }
|
||||
}
|
||||
|
||||
private val sendExecutor: ExecutorService =
|
||||
Executors.newFixedThreadPool(sendParallelism) { r ->
|
||||
Thread(r, "langsmith-batch-sender").apply { isDaemon = true }
|
||||
}
|
||||
|
||||
/** Enqueues a run create operation. */
|
||||
fun post(
|
||||
run: RunIngest,
|
||||
headers: Headers = Headers.builder().build(),
|
||||
queryParams: QueryParams = QueryParams.builder().build(),
|
||||
requestOptions: RequestOptions = RequestOptions.none(),
|
||||
) {
|
||||
enqueue(BatchOp.Post, run, headers, queryParams, requestOptions)
|
||||
}
|
||||
|
||||
/** Enqueues a run update (patch) operation. */
|
||||
fun patch(
|
||||
run: RunIngest,
|
||||
headers: Headers = Headers.builder().build(),
|
||||
queryParams: QueryParams = QueryParams.builder().build(),
|
||||
requestOptions: RequestOptions = RequestOptions.none(),
|
||||
) {
|
||||
enqueue(BatchOp.Patch, run, headers, queryParams, requestOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all queued operations immediately, blocking until batch requests that were queued or
|
||||
* already in-flight have completed.
|
||||
*
|
||||
* Safe to call from any thread. No-op if the queue is empty.
|
||||
*/
|
||||
fun flush() {
|
||||
cancelDelayedFlush()
|
||||
while (true) {
|
||||
if (queuedCount.get() > 0 && !drainOnCoordinator()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!waitForActiveSends()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (queuedCount.get() == 0 && !hasActiveSends()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes remaining operations and shuts down the background executors.
|
||||
*
|
||||
* After calling this, the queue will no longer accept new operations.
|
||||
*/
|
||||
fun shutdown() {
|
||||
val startedShutdown =
|
||||
enqueueShutdownLock.withLock {
|
||||
// Serialize with enqueue's check-and-add so flush cannot miss an item that observed
|
||||
// shutdown=false but has not yet been queued.
|
||||
shutdown.compareAndSet(false, true)
|
||||
}
|
||||
if (!startedShutdown) return
|
||||
|
||||
flush()
|
||||
coordinator.shutdown()
|
||||
sendExecutor.shutdown()
|
||||
|
||||
try {
|
||||
if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
coordinator.shutdownNow()
|
||||
}
|
||||
if (!sendExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
sendExecutor.shutdownNow()
|
||||
}
|
||||
} catch (_: InterruptedException) {
|
||||
coordinator.shutdownNow()
|
||||
sendExecutor.shutdownNow()
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
sendBatch: (RunIngestBatchParams, RequestOptions) -> CompletionStage<Void?>,
|
||||
batchSizeLimit: Int = DEFAULT_BATCH_SIZE_LIMIT,
|
||||
aggregationDelayMs: Long = DEFAULT_AGGREGATION_DELAY_MS,
|
||||
sendParallelism: Int = DEFAULT_SEND_PARALLELISM,
|
||||
) : this(
|
||||
sendBatch = sendBatch,
|
||||
batchSizeLimit = batchSizeLimit,
|
||||
aggregationDelayMs = aggregationDelayMs,
|
||||
sendParallelism = sendParallelism,
|
||||
maxAggregationDelayMs = DEFAULT_MAX_AGGREGATION_DELAY_MS,
|
||||
batchSizeLimitBytes = DEFAULT_BATCH_SIZE_LIMIT_BYTES,
|
||||
)
|
||||
|
||||
/** Returns the number of queued operations (for testing). */
|
||||
internal fun size(): Int = queuedCount.get()
|
||||
|
||||
private fun enqueue(
|
||||
op: BatchOp,
|
||||
run: RunIngest,
|
||||
headers: Headers,
|
||||
queryParams: QueryParams,
|
||||
requestOptions: RequestOptions,
|
||||
) {
|
||||
val count =
|
||||
enqueueShutdownLock.withLock {
|
||||
check(!shutdown.get()) { "AutoBatchQueue is shut down" }
|
||||
items.add(
|
||||
BatchItem(
|
||||
op = op,
|
||||
run = run,
|
||||
headers = headers,
|
||||
queryParams = queryParams,
|
||||
requestOptions = requestOptions,
|
||||
)
|
||||
)
|
||||
queuedCount.incrementAndGet()
|
||||
}
|
||||
|
||||
afterEnqueue(count)
|
||||
}
|
||||
|
||||
private fun afterEnqueue(count: Int) {
|
||||
if (count >= batchSizeLimit) {
|
||||
triggerFlush()
|
||||
} else {
|
||||
scheduleFlush()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleFlush() {
|
||||
delayedFlushLock.withLock {
|
||||
try {
|
||||
val now = System.nanoTime()
|
||||
val firstQueuedAt = firstQueuedAtNanos ?: now.also { firstQueuedAtNanos = it }
|
||||
val elapsedMs = TimeUnit.NANOSECONDS.toMillis(now - firstQueuedAt)
|
||||
val remainingMaxDelayMs = (maxAggregationDelayMs - elapsedMs).coerceAtLeast(0)
|
||||
val delayMs = minOf(aggregationDelayMs, remainingMaxDelayMs)
|
||||
|
||||
delayedFlushFuture?.cancel(false)
|
||||
delayedFlushFuture =
|
||||
coordinator.schedule(
|
||||
{
|
||||
clearDelayedFlushState()
|
||||
drainAndSubmitSends()
|
||||
},
|
||||
delayMs,
|
||||
TimeUnit.MILLISECONDS,
|
||||
)
|
||||
} catch (e: RejectedExecutionException) {
|
||||
delayedFlushFuture = null
|
||||
firstQueuedAtNanos = null
|
||||
logger.warn("Batch queue coordinator rejected delayed flush", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDelayedFlushState() {
|
||||
delayedFlushLock.withLock {
|
||||
delayedFlushFuture = null
|
||||
firstQueuedAtNanos = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerFlush() {
|
||||
cancelDelayedFlush()
|
||||
try {
|
||||
coordinator.execute { drainAndSubmitSends() }
|
||||
} catch (e: RejectedExecutionException) {
|
||||
logger.warn("Batch queue coordinator rejected flush", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelDelayedFlush() {
|
||||
delayedFlushLock.withLock {
|
||||
delayedFlushFuture?.cancel(false)
|
||||
delayedFlushFuture = null
|
||||
firstQueuedAtNanos = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun drainOnCoordinator(): Boolean {
|
||||
val drainFuture =
|
||||
try {
|
||||
coordinator.submit { drainAndSubmitSends() }
|
||||
} catch (e: RejectedExecutionException) {
|
||||
throw IllegalStateException("Batch queue coordinator rejected flush", e)
|
||||
}
|
||||
|
||||
try {
|
||||
drainFuture.get()
|
||||
return true
|
||||
} catch (_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
return false
|
||||
} catch (e: ExecutionException) {
|
||||
throw RuntimeException("Failed to flush batch queue", e.cause)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drainAndSubmitSends() {
|
||||
var remainingToDrain = queuedCount.get()
|
||||
while (remainingToDrain > 0) {
|
||||
val drainResult = drainUpTo(minOf(batchSizeLimit, remainingToDrain))
|
||||
if (drainResult.itemCount == 0) break
|
||||
|
||||
drainResult.batches.forEach(::submitBatch)
|
||||
remainingToDrain -= drainResult.itemCount
|
||||
}
|
||||
|
||||
when {
|
||||
queuedCount.get() >= batchSizeLimit -> triggerFlush()
|
||||
queuedCount.get() > 0 && !shutdown.get() -> scheduleFlush()
|
||||
queuedCount.get() > 0 && shutdown.get() -> triggerFlush()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains up to [maxItems] queued operations and returns batch params grouped by request
|
||||
* options.
|
||||
*
|
||||
* Multipart ingest compression is applied at send time; legacy `/runs/batch` remains
|
||||
* uncompressed.
|
||||
*
|
||||
* TODO: Support attachment parts in multipart ingest.
|
||||
*/
|
||||
private fun drainUpTo(maxItems: Int): DrainResult {
|
||||
val openGroups = linkedMapOf<RequestOptionsKey, BatchGroup>()
|
||||
var drained = 0
|
||||
|
||||
val batches = buildList {
|
||||
while (drained < maxItems) {
|
||||
val item = items.poll() ?: break
|
||||
queuedCount.decrementAndGet()
|
||||
drained++
|
||||
|
||||
val key = item.requestOptions.key()
|
||||
val itemSerializedSize = objectMapper.writeValueAsBytes(item.run).size
|
||||
var group = openGroups.getOrPut(key) { BatchGroup(item.requestOptions) }
|
||||
if (
|
||||
group.isNotEmpty() &&
|
||||
group.serializedSizeWith(item, itemSerializedSize) > batchSizeLimitBytes
|
||||
) {
|
||||
add(group.toBatch())
|
||||
group = BatchGroup(item.requestOptions)
|
||||
openGroups[key] = group
|
||||
}
|
||||
group.add(item, itemSerializedSize)
|
||||
}
|
||||
|
||||
addAll(openGroups.values.map { it.toBatch() })
|
||||
}
|
||||
return DrainResult(batches = batches, itemCount = drained)
|
||||
}
|
||||
|
||||
private fun submitBatch(batch: Batch) {
|
||||
activeSends.register()
|
||||
try {
|
||||
sendExecutor.execute {
|
||||
try {
|
||||
sendBatch(batch.params, batch.requestOptions).toCompletableFuture().join()
|
||||
} catch (e: CompletionException) {
|
||||
logger.warn("Failed to send batch of runs", e.cause ?: e)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Failed to send batch of runs", e)
|
||||
} finally {
|
||||
activeSends.arriveAndDeregister()
|
||||
}
|
||||
}
|
||||
} catch (e: RejectedExecutionException) {
|
||||
activeSends.arriveAndDeregister()
|
||||
logger.warn(
|
||||
"Batch queue sender rejected a batch; dropping {} run operations",
|
||||
operationCount(batch.params),
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForActiveSends(): Boolean {
|
||||
while (hasActiveSends()) {
|
||||
val phase = activeSends.phase
|
||||
try {
|
||||
activeSends.awaitAdvanceInterruptibly(phase)
|
||||
} catch (_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasActiveSends(): Boolean = activeSends.registeredParties > 0
|
||||
|
||||
private fun operationCount(params: RunIngestBatchParams): Int =
|
||||
params.post().orElse(emptyList()).size + params.patch().orElse(emptyList()).size
|
||||
|
||||
private enum class BatchOp {
|
||||
Post,
|
||||
Patch,
|
||||
}
|
||||
|
||||
private data class DrainResult(val batches: List<Batch>, val itemCount: Int)
|
||||
|
||||
private data class Batch(val params: RunIngestBatchParams, val requestOptions: RequestOptions)
|
||||
|
||||
private class BatchGroup(
|
||||
val requestOptions: RequestOptions,
|
||||
val posts: MutableList<RunIngest> = mutableListOf(),
|
||||
val patches: MutableList<RunIngest> = mutableListOf(),
|
||||
val headers: Headers.Builder = Headers.builder(),
|
||||
val queryParams: QueryParams.Builder = QueryParams.builder(),
|
||||
) {
|
||||
private var postSerializedSizeBytes = 0
|
||||
private var patchSerializedSizeBytes = 0
|
||||
|
||||
fun add(item: BatchItem, serializedSizeBytes: Int) {
|
||||
when (item.op) {
|
||||
BatchOp.Post -> {
|
||||
posts.add(item.run)
|
||||
postSerializedSizeBytes += serializedSizeBytes
|
||||
}
|
||||
BatchOp.Patch -> {
|
||||
patches.add(item.run)
|
||||
patchSerializedSizeBytes += serializedSizeBytes
|
||||
}
|
||||
}
|
||||
headers.putAll(item.headers)
|
||||
queryParams.putAll(item.queryParams)
|
||||
}
|
||||
|
||||
fun isNotEmpty(): Boolean = posts.isNotEmpty() || patches.isNotEmpty()
|
||||
|
||||
fun serializedSizeWith(item: BatchItem, itemSerializedSizeBytes: Int): Int =
|
||||
estimatedBodySizeBytes(
|
||||
postCount = posts.size + if (item.op == BatchOp.Post) 1 else 0,
|
||||
postItemsSizeBytes =
|
||||
postSerializedSizeBytes +
|
||||
if (item.op == BatchOp.Post) itemSerializedSizeBytes else 0,
|
||||
patchCount = patches.size + if (item.op == BatchOp.Patch) 1 else 0,
|
||||
patchItemsSizeBytes =
|
||||
patchSerializedSizeBytes +
|
||||
if (item.op == BatchOp.Patch) itemSerializedSizeBytes else 0,
|
||||
)
|
||||
|
||||
private fun estimatedBodySizeBytes(
|
||||
postCount: Int,
|
||||
postItemsSizeBytes: Int,
|
||||
patchCount: Int,
|
||||
patchItemsSizeBytes: Int,
|
||||
): Int {
|
||||
var size = 2 // Opening and closing braces.
|
||||
var fieldCount = 0
|
||||
if (patchCount > 0) {
|
||||
size += fieldSizeBytes("patch", patchCount, patchItemsSizeBytes)
|
||||
fieldCount++
|
||||
}
|
||||
if (postCount > 0) {
|
||||
if (fieldCount > 0) size++ // Comma between top-level fields.
|
||||
size += fieldSizeBytes("post", postCount, postItemsSizeBytes)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
private fun fieldSizeBytes(name: String, count: Int, itemsSizeBytes: Int): Int =
|
||||
name.length + 5 + itemsSizeBytes + (count - 1) // `"name":[]` + item commas.
|
||||
|
||||
fun toBatch(): Batch {
|
||||
val mergeResult = mergePostsAndPatches()
|
||||
val builder = RunIngestBatchParams.builder()
|
||||
if (mergeResult.posts.isNotEmpty()) builder.post(mergeResult.posts)
|
||||
if (mergeResult.patches.isNotEmpty()) builder.patch(mergeResult.patches)
|
||||
builder.additionalHeaders(headers.build())
|
||||
builder.additionalQueryParams(queryParams.build())
|
||||
return Batch(params = builder.build(), requestOptions = requestOptions)
|
||||
}
|
||||
|
||||
private fun mergePostsAndPatches(): MergeResult {
|
||||
if (posts.isEmpty() || patches.isEmpty()) {
|
||||
return MergeResult(posts = posts, patches = patches)
|
||||
}
|
||||
|
||||
val postsById =
|
||||
posts.mapNotNull { post -> post.id().getOrNull()?.let { it to post } }.toMap()
|
||||
val postsWithoutId = posts.filter { it.id().getOrNull() == null }
|
||||
val patchesByPostId =
|
||||
patches.mapNotNull { patch ->
|
||||
patch.id().getOrNull()?.takeIf(postsById::containsKey)?.let { it to patch }
|
||||
}
|
||||
val patchesByPostIdMap = patchesByPostId.toMap()
|
||||
val standalonePatches =
|
||||
patches.filter { patch ->
|
||||
patch.id().getOrNull()?.let(postsById::containsKey) != true
|
||||
}
|
||||
|
||||
return MergeResult(
|
||||
posts =
|
||||
postsWithoutId +
|
||||
postsById.map { (id, post) ->
|
||||
patchesByPostIdMap[id]?.let { mergePostAndPatch(post, it) } ?: post
|
||||
},
|
||||
patches = standalonePatches,
|
||||
mergedRunIds = patchesByPostId.map { it.first },
|
||||
)
|
||||
}
|
||||
|
||||
private fun mergePostAndPatch(post: RunIngest, patch: RunIngest): RunIngest {
|
||||
val merged = objectMapper.valueToTree<ObjectNode>(post)
|
||||
val patchFields = objectMapper.valueToTree<ObjectNode>(patch)
|
||||
patchFields.fields().forEach { (field, value) -> merged.set<ObjectNode>(field, value) }
|
||||
val mergedRun = objectMapper.treeToValue(merged, RunIngest::class.java)
|
||||
return mergedRun
|
||||
.toBuilder()
|
||||
.attachments(post.attachments() + patch.attachments())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private data class MergeResult(
|
||||
val posts: List<RunIngest>,
|
||||
val patches: List<RunIngest>,
|
||||
val mergedRunIds: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
private data class BatchItem(
|
||||
val op: BatchOp,
|
||||
val run: RunIngest,
|
||||
val headers: Headers,
|
||||
val queryParams: QueryParams,
|
||||
val requestOptions: RequestOptions,
|
||||
)
|
||||
|
||||
private data class RequestOptionsKey(val responseValidation: Boolean?, val timeout: Timeout?)
|
||||
|
||||
private fun RequestOptions.key(): RequestOptionsKey =
|
||||
RequestOptionsKey(responseValidation = responseValidation, timeout = timeout)
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(AutoBatchQueue::class.java)
|
||||
private val objectMapper = jsonMapper()
|
||||
|
||||
const val DEFAULT_BATCH_SIZE_LIMIT = 100
|
||||
const val DEFAULT_BATCH_SIZE_LIMIT_BYTES = 20_971_520
|
||||
const val DEFAULT_AGGREGATION_DELAY_MS = 250L
|
||||
const val DEFAULT_MAX_AGGREGATION_DELAY_MS = 500L
|
||||
const val DEFAULT_SEND_PARALLELISM = 4
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,21 @@ import com.langchain.smith.core.ClientOptions
|
||||
import com.langchain.smith.services.blocking.AnnotationQueueService
|
||||
import com.langchain.smith.services.blocking.CommitService
|
||||
import com.langchain.smith.services.blocking.DatasetService
|
||||
import com.langchain.smith.services.blocking.EvaluatorService
|
||||
import com.langchain.smith.services.blocking.ExampleService
|
||||
import com.langchain.smith.services.blocking.FeedbackService
|
||||
import com.langchain.smith.services.blocking.InfoService
|
||||
import com.langchain.smith.services.blocking.IssueService
|
||||
import com.langchain.smith.services.blocking.OnlineEvaluatorService
|
||||
import com.langchain.smith.services.blocking.PublicService
|
||||
import com.langchain.smith.services.blocking.RepoService
|
||||
import com.langchain.smith.services.blocking.RunService
|
||||
import com.langchain.smith.services.blocking.SandboxService
|
||||
import com.langchain.smith.services.blocking.SessionService
|
||||
import com.langchain.smith.services.blocking.SettingService
|
||||
import com.langchain.smith.services.blocking.ThreadService
|
||||
import com.langchain.smith.services.blocking.TraceService
|
||||
import com.langchain.smith.services.blocking.WorkspaceService
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
@@ -59,18 +67,34 @@ interface LangsmithClient {
|
||||
|
||||
fun runs(): RunService
|
||||
|
||||
fun threads(): ThreadService
|
||||
|
||||
fun traces(): TraceService
|
||||
|
||||
fun evaluators(): EvaluatorService
|
||||
|
||||
fun onlineEvaluators(): OnlineEvaluatorService
|
||||
|
||||
fun feedback(): FeedbackService
|
||||
|
||||
fun public_(): PublicService
|
||||
|
||||
fun annotationQueues(): AnnotationQueueService
|
||||
|
||||
fun info(): InfoService
|
||||
|
||||
fun workspaces(): WorkspaceService
|
||||
|
||||
fun repos(): RepoService
|
||||
|
||||
fun commits(): CommitService
|
||||
|
||||
fun settings(): SettingService
|
||||
|
||||
fun issues(): IssueService
|
||||
|
||||
fun sandboxes(): SandboxService
|
||||
|
||||
/**
|
||||
* Closes this client, relinquishing any underlying resources.
|
||||
*
|
||||
@@ -102,16 +126,32 @@ interface LangsmithClient {
|
||||
|
||||
fun runs(): RunService.WithRawResponse
|
||||
|
||||
fun threads(): ThreadService.WithRawResponse
|
||||
|
||||
fun traces(): TraceService.WithRawResponse
|
||||
|
||||
fun evaluators(): EvaluatorService.WithRawResponse
|
||||
|
||||
fun onlineEvaluators(): OnlineEvaluatorService.WithRawResponse
|
||||
|
||||
fun feedback(): FeedbackService.WithRawResponse
|
||||
|
||||
fun public_(): PublicService.WithRawResponse
|
||||
|
||||
fun annotationQueues(): AnnotationQueueService.WithRawResponse
|
||||
|
||||
fun info(): InfoService.WithRawResponse
|
||||
|
||||
fun workspaces(): WorkspaceService.WithRawResponse
|
||||
|
||||
fun repos(): RepoService.WithRawResponse
|
||||
|
||||
fun commits(): CommitService.WithRawResponse
|
||||
|
||||
fun settings(): SettingService.WithRawResponse
|
||||
|
||||
fun issues(): IssueService.WithRawResponse
|
||||
|
||||
fun sandboxes(): SandboxService.WithRawResponse
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -6,13 +6,21 @@ import com.langchain.smith.core.ClientOptions
|
||||
import com.langchain.smith.services.async.AnnotationQueueServiceAsync
|
||||
import com.langchain.smith.services.async.CommitServiceAsync
|
||||
import com.langchain.smith.services.async.DatasetServiceAsync
|
||||
import com.langchain.smith.services.async.EvaluatorServiceAsync
|
||||
import com.langchain.smith.services.async.ExampleServiceAsync
|
||||
import com.langchain.smith.services.async.FeedbackServiceAsync
|
||||
import com.langchain.smith.services.async.InfoServiceAsync
|
||||
import com.langchain.smith.services.async.IssueServiceAsync
|
||||
import com.langchain.smith.services.async.OnlineEvaluatorServiceAsync
|
||||
import com.langchain.smith.services.async.PublicServiceAsync
|
||||
import com.langchain.smith.services.async.RepoServiceAsync
|
||||
import com.langchain.smith.services.async.RunServiceAsync
|
||||
import com.langchain.smith.services.async.SandboxServiceAsync
|
||||
import com.langchain.smith.services.async.SessionServiceAsync
|
||||
import com.langchain.smith.services.async.SettingServiceAsync
|
||||
import com.langchain.smith.services.async.ThreadServiceAsync
|
||||
import com.langchain.smith.services.async.TraceServiceAsync
|
||||
import com.langchain.smith.services.async.WorkspaceServiceAsync
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
@@ -59,18 +67,34 @@ interface LangsmithClientAsync {
|
||||
|
||||
fun runs(): RunServiceAsync
|
||||
|
||||
fun threads(): ThreadServiceAsync
|
||||
|
||||
fun traces(): TraceServiceAsync
|
||||
|
||||
fun evaluators(): EvaluatorServiceAsync
|
||||
|
||||
fun onlineEvaluators(): OnlineEvaluatorServiceAsync
|
||||
|
||||
fun feedback(): FeedbackServiceAsync
|
||||
|
||||
fun public_(): PublicServiceAsync
|
||||
|
||||
fun annotationQueues(): AnnotationQueueServiceAsync
|
||||
|
||||
fun info(): InfoServiceAsync
|
||||
|
||||
fun workspaces(): WorkspaceServiceAsync
|
||||
|
||||
fun repos(): RepoServiceAsync
|
||||
|
||||
fun commits(): CommitServiceAsync
|
||||
|
||||
fun settings(): SettingServiceAsync
|
||||
|
||||
fun issues(): IssueServiceAsync
|
||||
|
||||
fun sandboxes(): SandboxServiceAsync
|
||||
|
||||
/**
|
||||
* Closes this client, relinquishing any underlying resources.
|
||||
*
|
||||
@@ -106,16 +130,32 @@ interface LangsmithClientAsync {
|
||||
|
||||
fun runs(): RunServiceAsync.WithRawResponse
|
||||
|
||||
fun threads(): ThreadServiceAsync.WithRawResponse
|
||||
|
||||
fun traces(): TraceServiceAsync.WithRawResponse
|
||||
|
||||
fun evaluators(): EvaluatorServiceAsync.WithRawResponse
|
||||
|
||||
fun onlineEvaluators(): OnlineEvaluatorServiceAsync.WithRawResponse
|
||||
|
||||
fun feedback(): FeedbackServiceAsync.WithRawResponse
|
||||
|
||||
fun public_(): PublicServiceAsync.WithRawResponse
|
||||
|
||||
fun annotationQueues(): AnnotationQueueServiceAsync.WithRawResponse
|
||||
|
||||
fun info(): InfoServiceAsync.WithRawResponse
|
||||
|
||||
fun workspaces(): WorkspaceServiceAsync.WithRawResponse
|
||||
|
||||
fun repos(): RepoServiceAsync.WithRawResponse
|
||||
|
||||
fun commits(): CommitServiceAsync.WithRawResponse
|
||||
|
||||
fun settings(): SettingServiceAsync.WithRawResponse
|
||||
|
||||
fun issues(): IssueServiceAsync.WithRawResponse
|
||||
|
||||
fun sandboxes(): SandboxServiceAsync.WithRawResponse
|
||||
}
|
||||
}
|
||||
|
||||
+126
-6
@@ -10,20 +10,36 @@ import com.langchain.smith.services.async.CommitServiceAsync
|
||||
import com.langchain.smith.services.async.CommitServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.DatasetServiceAsync
|
||||
import com.langchain.smith.services.async.DatasetServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.EvaluatorServiceAsync
|
||||
import com.langchain.smith.services.async.EvaluatorServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.ExampleServiceAsync
|
||||
import com.langchain.smith.services.async.ExampleServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.FeedbackServiceAsync
|
||||
import com.langchain.smith.services.async.FeedbackServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.InfoServiceAsync
|
||||
import com.langchain.smith.services.async.InfoServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.IssueServiceAsync
|
||||
import com.langchain.smith.services.async.IssueServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.OnlineEvaluatorServiceAsync
|
||||
import com.langchain.smith.services.async.OnlineEvaluatorServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.PublicServiceAsync
|
||||
import com.langchain.smith.services.async.PublicServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.RepoServiceAsync
|
||||
import com.langchain.smith.services.async.RepoServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.RunServiceAsync
|
||||
import com.langchain.smith.services.async.RunServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.SandboxServiceAsync
|
||||
import com.langchain.smith.services.async.SandboxServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.SessionServiceAsync
|
||||
import com.langchain.smith.services.async.SessionServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.SettingServiceAsync
|
||||
import com.langchain.smith.services.async.SettingServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.ThreadServiceAsync
|
||||
import com.langchain.smith.services.async.ThreadServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.TraceServiceAsync
|
||||
import com.langchain.smith.services.async.TraceServiceAsyncImpl
|
||||
import com.langchain.smith.services.async.WorkspaceServiceAsync
|
||||
import com.langchain.smith.services.async.WorkspaceServiceAsyncImpl
|
||||
import java.util.function.Consumer
|
||||
|
||||
class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : LangsmithClientAsync {
|
||||
@@ -33,11 +49,11 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
else
|
||||
clientOptions
|
||||
.toBuilder()
|
||||
.putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
|
||||
.putHeader("User-Agent", "langsmith-java/${getPackageVersion()}")
|
||||
.build()
|
||||
|
||||
// Pass the original clientOptions so that this client sets its own User-Agent.
|
||||
private val sync: LangsmithClient by lazy { LangsmithClientImpl(clientOptions) }
|
||||
private val sync = lazy { LangsmithClientImpl(clientOptions) }
|
||||
|
||||
private val withRawResponse: LangsmithClientAsync.WithRawResponse by lazy {
|
||||
WithRawResponseImpl(clientOptions)
|
||||
@@ -55,7 +71,23 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
DatasetServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val runs: RunServiceAsync by lazy { RunServiceAsyncImpl(clientOptionsWithUserAgent) }
|
||||
private val runs = lazy { RunServiceAsyncImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val threads: ThreadServiceAsync by lazy {
|
||||
ThreadServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val traces: TraceServiceAsync by lazy {
|
||||
TraceServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val evaluators: EvaluatorServiceAsync by lazy {
|
||||
EvaluatorServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val onlineEvaluators: OnlineEvaluatorServiceAsync by lazy {
|
||||
OnlineEvaluatorServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val feedback: FeedbackServiceAsync by lazy {
|
||||
FeedbackServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
@@ -69,6 +101,12 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
AnnotationQueueServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val info: InfoServiceAsync by lazy { InfoServiceAsyncImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val workspaces: WorkspaceServiceAsync by lazy {
|
||||
WorkspaceServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val repos: RepoServiceAsync by lazy { RepoServiceAsyncImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val commits: CommitServiceAsync by lazy {
|
||||
@@ -79,7 +117,15 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
SettingServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
override fun sync(): LangsmithClient = sync
|
||||
private val issues: IssueServiceAsync by lazy {
|
||||
IssueServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val sandboxes: SandboxServiceAsync by lazy {
|
||||
SandboxServiceAsyncImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
override fun sync(): LangsmithClient = sync.value
|
||||
|
||||
override fun withRawResponse(): LangsmithClientAsync.WithRawResponse = withRawResponse
|
||||
|
||||
@@ -92,7 +138,15 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
|
||||
override fun datasets(): DatasetServiceAsync = datasets
|
||||
|
||||
override fun runs(): RunServiceAsync = runs
|
||||
override fun runs(): RunServiceAsync = runs.value
|
||||
|
||||
override fun threads(): ThreadServiceAsync = threads
|
||||
|
||||
override fun traces(): TraceServiceAsync = traces
|
||||
|
||||
override fun evaluators(): EvaluatorServiceAsync = evaluators
|
||||
|
||||
override fun onlineEvaluators(): OnlineEvaluatorServiceAsync = onlineEvaluators
|
||||
|
||||
override fun feedback(): FeedbackServiceAsync = feedback
|
||||
|
||||
@@ -100,13 +154,30 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
|
||||
override fun annotationQueues(): AnnotationQueueServiceAsync = annotationQueues
|
||||
|
||||
override fun info(): InfoServiceAsync = info
|
||||
|
||||
override fun workspaces(): WorkspaceServiceAsync = workspaces
|
||||
|
||||
override fun repos(): RepoServiceAsync = repos
|
||||
|
||||
override fun commits(): CommitServiceAsync = commits
|
||||
|
||||
override fun settings(): SettingServiceAsync = settings
|
||||
|
||||
override fun close() = clientOptions.close()
|
||||
override fun issues(): IssueServiceAsync = issues
|
||||
|
||||
override fun sandboxes(): SandboxServiceAsync = sandboxes
|
||||
|
||||
override fun close() {
|
||||
if (runs.isInitialized()) {
|
||||
runs.value.shutdown()
|
||||
}
|
||||
if (sync.isInitialized()) {
|
||||
sync.value.close()
|
||||
} else {
|
||||
clientOptions.close()
|
||||
}
|
||||
}
|
||||
|
||||
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
|
||||
LangsmithClientAsync.WithRawResponse {
|
||||
@@ -127,6 +198,22 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
RunServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val threads: ThreadServiceAsync.WithRawResponse by lazy {
|
||||
ThreadServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val traces: TraceServiceAsync.WithRawResponse by lazy {
|
||||
TraceServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val evaluators: EvaluatorServiceAsync.WithRawResponse by lazy {
|
||||
EvaluatorServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val onlineEvaluators: OnlineEvaluatorServiceAsync.WithRawResponse by lazy {
|
||||
OnlineEvaluatorServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val feedback: FeedbackServiceAsync.WithRawResponse by lazy {
|
||||
FeedbackServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
@@ -139,6 +226,14 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
AnnotationQueueServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val info: InfoServiceAsync.WithRawResponse by lazy {
|
||||
InfoServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val workspaces: WorkspaceServiceAsync.WithRawResponse by lazy {
|
||||
WorkspaceServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val repos: RepoServiceAsync.WithRawResponse by lazy {
|
||||
RepoServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
@@ -151,6 +246,14 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
SettingServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val issues: IssueServiceAsync.WithRawResponse by lazy {
|
||||
IssueServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val sandboxes: SandboxServiceAsync.WithRawResponse by lazy {
|
||||
SandboxServiceAsyncImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
override fun withOptions(
|
||||
modifier: Consumer<ClientOptions.Builder>
|
||||
): LangsmithClientAsync.WithRawResponse =
|
||||
@@ -166,6 +269,15 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
|
||||
override fun runs(): RunServiceAsync.WithRawResponse = runs
|
||||
|
||||
override fun threads(): ThreadServiceAsync.WithRawResponse = threads
|
||||
|
||||
override fun traces(): TraceServiceAsync.WithRawResponse = traces
|
||||
|
||||
override fun evaluators(): EvaluatorServiceAsync.WithRawResponse = evaluators
|
||||
|
||||
override fun onlineEvaluators(): OnlineEvaluatorServiceAsync.WithRawResponse =
|
||||
onlineEvaluators
|
||||
|
||||
override fun feedback(): FeedbackServiceAsync.WithRawResponse = feedback
|
||||
|
||||
override fun public_(): PublicServiceAsync.WithRawResponse = public_
|
||||
@@ -173,10 +285,18 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
|
||||
override fun annotationQueues(): AnnotationQueueServiceAsync.WithRawResponse =
|
||||
annotationQueues
|
||||
|
||||
override fun info(): InfoServiceAsync.WithRawResponse = info
|
||||
|
||||
override fun workspaces(): WorkspaceServiceAsync.WithRawResponse = workspaces
|
||||
|
||||
override fun repos(): RepoServiceAsync.WithRawResponse = repos
|
||||
|
||||
override fun commits(): CommitServiceAsync.WithRawResponse = commits
|
||||
|
||||
override fun settings(): SettingServiceAsync.WithRawResponse = settings
|
||||
|
||||
override fun issues(): IssueServiceAsync.WithRawResponse = issues
|
||||
|
||||
override fun sandboxes(): SandboxServiceAsync.WithRawResponse = sandboxes
|
||||
}
|
||||
}
|
||||
|
||||
+111
-4
@@ -10,20 +10,36 @@ import com.langchain.smith.services.blocking.CommitService
|
||||
import com.langchain.smith.services.blocking.CommitServiceImpl
|
||||
import com.langchain.smith.services.blocking.DatasetService
|
||||
import com.langchain.smith.services.blocking.DatasetServiceImpl
|
||||
import com.langchain.smith.services.blocking.EvaluatorService
|
||||
import com.langchain.smith.services.blocking.EvaluatorServiceImpl
|
||||
import com.langchain.smith.services.blocking.ExampleService
|
||||
import com.langchain.smith.services.blocking.ExampleServiceImpl
|
||||
import com.langchain.smith.services.blocking.FeedbackService
|
||||
import com.langchain.smith.services.blocking.FeedbackServiceImpl
|
||||
import com.langchain.smith.services.blocking.InfoService
|
||||
import com.langchain.smith.services.blocking.InfoServiceImpl
|
||||
import com.langchain.smith.services.blocking.IssueService
|
||||
import com.langchain.smith.services.blocking.IssueServiceImpl
|
||||
import com.langchain.smith.services.blocking.OnlineEvaluatorService
|
||||
import com.langchain.smith.services.blocking.OnlineEvaluatorServiceImpl
|
||||
import com.langchain.smith.services.blocking.PublicService
|
||||
import com.langchain.smith.services.blocking.PublicServiceImpl
|
||||
import com.langchain.smith.services.blocking.RepoService
|
||||
import com.langchain.smith.services.blocking.RepoServiceImpl
|
||||
import com.langchain.smith.services.blocking.RunService
|
||||
import com.langchain.smith.services.blocking.RunServiceImpl
|
||||
import com.langchain.smith.services.blocking.SandboxService
|
||||
import com.langchain.smith.services.blocking.SandboxServiceImpl
|
||||
import com.langchain.smith.services.blocking.SessionService
|
||||
import com.langchain.smith.services.blocking.SessionServiceImpl
|
||||
import com.langchain.smith.services.blocking.SettingService
|
||||
import com.langchain.smith.services.blocking.SettingServiceImpl
|
||||
import com.langchain.smith.services.blocking.ThreadService
|
||||
import com.langchain.smith.services.blocking.ThreadServiceImpl
|
||||
import com.langchain.smith.services.blocking.TraceService
|
||||
import com.langchain.smith.services.blocking.TraceServiceImpl
|
||||
import com.langchain.smith.services.blocking.WorkspaceService
|
||||
import com.langchain.smith.services.blocking.WorkspaceServiceImpl
|
||||
import java.util.function.Consumer
|
||||
|
||||
class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithClient {
|
||||
@@ -33,7 +49,7 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
else
|
||||
clientOptions
|
||||
.toBuilder()
|
||||
.putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
|
||||
.putHeader("User-Agent", "langsmith-java/${getPackageVersion()}")
|
||||
.build()
|
||||
|
||||
// Pass the original clientOptions so that this client sets its own User-Agent.
|
||||
@@ -49,7 +65,19 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
|
||||
private val datasets: DatasetService by lazy { DatasetServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val runs: RunService by lazy { RunServiceImpl(clientOptionsWithUserAgent) }
|
||||
private val runs = lazy { RunServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val threads: ThreadService by lazy { ThreadServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val traces: TraceService by lazy { TraceServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val evaluators: EvaluatorService by lazy {
|
||||
EvaluatorServiceImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val onlineEvaluators: OnlineEvaluatorService by lazy {
|
||||
OnlineEvaluatorServiceImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val feedback: FeedbackService by lazy {
|
||||
FeedbackServiceImpl(clientOptionsWithUserAgent)
|
||||
@@ -61,12 +89,22 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
AnnotationQueueServiceImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val info: InfoService by lazy { InfoServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val workspaces: WorkspaceService by lazy {
|
||||
WorkspaceServiceImpl(clientOptionsWithUserAgent)
|
||||
}
|
||||
|
||||
private val repos: RepoService by lazy { RepoServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val commits: CommitService by lazy { CommitServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val settings: SettingService by lazy { SettingServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val issues: IssueService by lazy { IssueServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
private val sandboxes: SandboxService by lazy { SandboxServiceImpl(clientOptionsWithUserAgent) }
|
||||
|
||||
override fun async(): LangsmithClientAsync = async
|
||||
|
||||
override fun withRawResponse(): LangsmithClient.WithRawResponse = withRawResponse
|
||||
@@ -80,7 +118,15 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
|
||||
override fun datasets(): DatasetService = datasets
|
||||
|
||||
override fun runs(): RunService = runs
|
||||
override fun runs(): RunService = runs.value
|
||||
|
||||
override fun threads(): ThreadService = threads
|
||||
|
||||
override fun traces(): TraceService = traces
|
||||
|
||||
override fun evaluators(): EvaluatorService = evaluators
|
||||
|
||||
override fun onlineEvaluators(): OnlineEvaluatorService = onlineEvaluators
|
||||
|
||||
override fun feedback(): FeedbackService = feedback
|
||||
|
||||
@@ -88,13 +134,26 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
|
||||
override fun annotationQueues(): AnnotationQueueService = annotationQueues
|
||||
|
||||
override fun info(): InfoService = info
|
||||
|
||||
override fun workspaces(): WorkspaceService = workspaces
|
||||
|
||||
override fun repos(): RepoService = repos
|
||||
|
||||
override fun commits(): CommitService = commits
|
||||
|
||||
override fun settings(): SettingService = settings
|
||||
|
||||
override fun close() = clientOptions.close()
|
||||
override fun issues(): IssueService = issues
|
||||
|
||||
override fun sandboxes(): SandboxService = sandboxes
|
||||
|
||||
override fun close() {
|
||||
if (runs.isInitialized()) {
|
||||
runs.value.shutdown()
|
||||
}
|
||||
clientOptions.close()
|
||||
}
|
||||
|
||||
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
|
||||
LangsmithClient.WithRawResponse {
|
||||
@@ -115,6 +174,22 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
RunServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val threads: ThreadService.WithRawResponse by lazy {
|
||||
ThreadServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val traces: TraceService.WithRawResponse by lazy {
|
||||
TraceServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val evaluators: EvaluatorService.WithRawResponse by lazy {
|
||||
EvaluatorServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val onlineEvaluators: OnlineEvaluatorService.WithRawResponse by lazy {
|
||||
OnlineEvaluatorServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val feedback: FeedbackService.WithRawResponse by lazy {
|
||||
FeedbackServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
@@ -127,6 +202,14 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
AnnotationQueueServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val info: InfoService.WithRawResponse by lazy {
|
||||
InfoServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val workspaces: WorkspaceService.WithRawResponse by lazy {
|
||||
WorkspaceServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val repos: RepoService.WithRawResponse by lazy {
|
||||
RepoServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
@@ -139,6 +222,14 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
SettingServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val issues: IssueService.WithRawResponse by lazy {
|
||||
IssueServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
private val sandboxes: SandboxService.WithRawResponse by lazy {
|
||||
SandboxServiceImpl.WithRawResponseImpl(clientOptions)
|
||||
}
|
||||
|
||||
override fun withOptions(
|
||||
modifier: Consumer<ClientOptions.Builder>
|
||||
): LangsmithClient.WithRawResponse =
|
||||
@@ -154,16 +245,32 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
|
||||
|
||||
override fun runs(): RunService.WithRawResponse = runs
|
||||
|
||||
override fun threads(): ThreadService.WithRawResponse = threads
|
||||
|
||||
override fun traces(): TraceService.WithRawResponse = traces
|
||||
|
||||
override fun evaluators(): EvaluatorService.WithRawResponse = evaluators
|
||||
|
||||
override fun onlineEvaluators(): OnlineEvaluatorService.WithRawResponse = onlineEvaluators
|
||||
|
||||
override fun feedback(): FeedbackService.WithRawResponse = feedback
|
||||
|
||||
override fun public_(): PublicService.WithRawResponse = public_
|
||||
|
||||
override fun annotationQueues(): AnnotationQueueService.WithRawResponse = annotationQueues
|
||||
|
||||
override fun info(): InfoService.WithRawResponse = info
|
||||
|
||||
override fun workspaces(): WorkspaceService.WithRawResponse = workspaces
|
||||
|
||||
override fun repos(): RepoService.WithRawResponse = repos
|
||||
|
||||
override fun commits(): CommitService.WithRawResponse = commits
|
||||
|
||||
override fun settings(): SettingService.WithRawResponse = settings
|
||||
|
||||
override fun issues(): IssueService.WithRawResponse = issues
|
||||
|
||||
override fun sandboxes(): SandboxService.WithRawResponse = sandboxes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.langchain.smith.client
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.langchain.smith.core.MultipartField
|
||||
import com.langchain.smith.core.http.HttpRequestBody
|
||||
import com.langchain.smith.core.http.multipartFormData
|
||||
import com.langchain.smith.models.runs.RunAttachment
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import com.langchain.smith.models.runs.RunIngestBatchParams
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
internal fun RunIngestBatchParams.toRunMultipartFormData(jsonMapper: JsonMapper): HttpRequestBody? {
|
||||
// Multipart requires run id, trace_id, and dotted_order to address each part correctly.
|
||||
// If a queued run is missing those fields, callers fall back to legacy JSON batch ingest
|
||||
// instead of failing the entire auto-batch.
|
||||
val fields =
|
||||
listOf(
|
||||
multipartFieldsForRuns(
|
||||
jsonMapper,
|
||||
operation = "post",
|
||||
runs = post().orElse(emptyList()),
|
||||
) ?: return null,
|
||||
multipartFieldsForRuns(
|
||||
jsonMapper,
|
||||
operation = "patch",
|
||||
runs = patch().orElse(emptyList()),
|
||||
) ?: return null,
|
||||
)
|
||||
.flatten()
|
||||
.toMap()
|
||||
|
||||
return fields.takeIf { it.isNotEmpty() }?.let { multipartFormData(jsonMapper, it) }
|
||||
}
|
||||
|
||||
private fun multipartFieldsForRuns(
|
||||
jsonMapper: JsonMapper,
|
||||
operation: String,
|
||||
runs: List<RunIngest>,
|
||||
): List<Pair<String, MultipartField<*>>>? =
|
||||
runs.flatMap { run -> multipartFieldsForRun(jsonMapper, operation, run) ?: return null }
|
||||
|
||||
private fun multipartFieldsForRun(
|
||||
jsonMapper: JsonMapper,
|
||||
operation: String,
|
||||
run: RunIngest,
|
||||
): List<Pair<String, MultipartField<*>>>? {
|
||||
val runId = run.id().getOrNull() ?: return null
|
||||
run.traceId().getOrNull() ?: return null
|
||||
run.dottedOrder().getOrNull() ?: return null
|
||||
|
||||
val runFields =
|
||||
jsonMapper
|
||||
.valueToTree<ObjectNode>(run)
|
||||
.fields()
|
||||
.asSequence()
|
||||
.map { it.key to it.value }
|
||||
.toList()
|
||||
val main =
|
||||
jsonMapper
|
||||
.createObjectNode()
|
||||
.setAll<ObjectNode>(
|
||||
runFields
|
||||
.filterNot { (fieldName, _) -> fieldName in MULTIPART_EXCLUDED_FIELDS }
|
||||
.toMap()
|
||||
)
|
||||
val separateFields =
|
||||
MULTIPART_SEPARATE_FIELDS.mapNotNull { fieldName ->
|
||||
runFields
|
||||
.firstOrNull { (name, _) -> name == fieldName }
|
||||
?.second
|
||||
?.takeUnless { it.isNull || it.isMissingNode }
|
||||
?.let { fieldName to it }
|
||||
}
|
||||
|
||||
return listOf("$operation.$runId" to jsonField(jsonMapper, main)) +
|
||||
separateFields.map { (fieldName, value) ->
|
||||
"$operation.$runId.$fieldName" to jsonField(jsonMapper, value)
|
||||
} +
|
||||
attachmentFields(runId, run.attachments())
|
||||
}
|
||||
|
||||
private fun jsonField(jsonMapper: JsonMapper, value: JsonNode): MultipartField<ByteArray> {
|
||||
val bytes = jsonMapper.writeValueAsBytes(value)
|
||||
return MultipartField.builder<ByteArray>()
|
||||
.value(bytes)
|
||||
.contentType("application/json; length=${bytes.size}")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun attachmentFields(
|
||||
runId: String,
|
||||
attachments: Map<String, RunAttachment>,
|
||||
): List<Pair<String, MultipartField<*>>> =
|
||||
attachments.mapNotNull { (name, attachment) ->
|
||||
if (name.contains('.')) {
|
||||
logger.warn(
|
||||
"Skipping attachment '{}' for run {}: attachment names must not contain periods ('.')",
|
||||
name,
|
||||
runId,
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val contentType =
|
||||
attachment.length().getOrNull()?.let { "${attachment.contentType()}; length=$it" }
|
||||
?: attachment.contentType()
|
||||
"attachment.$runId.$name" to
|
||||
MultipartField.builder<Any>()
|
||||
.value(attachment.multipartValue())
|
||||
.contentType(contentType)
|
||||
.filename(attachment.filename())
|
||||
.build()
|
||||
}
|
||||
|
||||
private val MULTIPART_SEPARATE_FIELDS =
|
||||
listOf("inputs", "outputs", "events", "extra", "error", "serialized")
|
||||
|
||||
private val MULTIPART_EXCLUDED_FIELDS = MULTIPART_SEPARATE_FIELDS + "attachments"
|
||||
|
||||
private val logger = LoggerFactory.getLogger("com.langchain.smith.client.RunMultipartBatch")
|
||||
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.langchain.smith.core.http.AsyncStreamResponse
|
||||
import com.langchain.smith.core.http.Headers
|
||||
import com.langchain.smith.core.http.HttpClient
|
||||
import com.langchain.smith.core.http.LoggingHttpClient
|
||||
import com.langchain.smith.core.http.PhantomReachableClosingHttpClient
|
||||
import com.langchain.smith.core.http.QueryParams
|
||||
import com.langchain.smith.core.http.RetryingHttpClient
|
||||
@@ -80,6 +81,9 @@ private constructor(
|
||||
/**
|
||||
* Whether to call `validate` on every response before returning it.
|
||||
*
|
||||
* Setting this to `true` is _not_ forwards compatible with new types from the API for existing
|
||||
* fields.
|
||||
*
|
||||
* Defaults to false, which means the shape of the response will not be validated upfront.
|
||||
* Instead, validation will only occur for the parts of the response that are accessed.
|
||||
*/
|
||||
@@ -107,10 +111,20 @@ private constructor(
|
||||
* Defaults to 2.
|
||||
*/
|
||||
@get:JvmName("maxRetries") val maxRetries: Int,
|
||||
/** Whether run create/update calls should be automatically batched for tracing. */
|
||||
@get:JvmName("autoBatchTracing") val autoBatchTracing: Boolean,
|
||||
/**
|
||||
* The level at which to log request and response information.
|
||||
*
|
||||
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
|
||||
*
|
||||
* Defaults to [LogLevel.fromEnv].
|
||||
*/
|
||||
@get:JvmName("logLevel") val logLevel: LogLevel,
|
||||
private val apiKey: String?,
|
||||
private val tenantId: String?,
|
||||
private val bearerToken: String?,
|
||||
private val organizationId: String?,
|
||||
private val oauthAccessToken: String?,
|
||||
private val profileAuth: ProfileAuth?,
|
||||
) {
|
||||
|
||||
init {
|
||||
@@ -130,14 +144,6 @@ private constructor(
|
||||
|
||||
fun tenantId(): Optional<String> = Optional.ofNullable(tenantId)
|
||||
|
||||
/**
|
||||
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
|
||||
* x-organization-id (for org scoped apis).
|
||||
*/
|
||||
fun bearerToken(): Optional<String> = Optional.ofNullable(bearerToken)
|
||||
|
||||
fun organizationId(): Optional<String> = Optional.ofNullable(organizationId)
|
||||
|
||||
fun toBuilder() = Builder().from(this)
|
||||
|
||||
companion object {
|
||||
@@ -177,10 +183,12 @@ private constructor(
|
||||
private var responseValidation: Boolean = false
|
||||
private var timeout: Timeout = Timeout.default()
|
||||
private var maxRetries: Int = 2
|
||||
private var autoBatchTracing: Boolean = true
|
||||
private var logLevel: LogLevel = LogLevel.fromEnv()
|
||||
private var apiKey: String? = null
|
||||
private var tenantId: String? = null
|
||||
private var bearerToken: String? = null
|
||||
private var organizationId: String? = null
|
||||
private var oauthAccessToken: String? = null
|
||||
private var profileAuth: ProfileAuth? = null
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun from(clientOptions: ClientOptions) = apply {
|
||||
@@ -196,10 +204,12 @@ private constructor(
|
||||
responseValidation = clientOptions.responseValidation
|
||||
timeout = clientOptions.timeout
|
||||
maxRetries = clientOptions.maxRetries
|
||||
autoBatchTracing = clientOptions.autoBatchTracing
|
||||
logLevel = clientOptions.logLevel
|
||||
apiKey = clientOptions.apiKey
|
||||
tenantId = clientOptions.tenantId
|
||||
bearerToken = clientOptions.bearerToken
|
||||
organizationId = clientOptions.organizationId
|
||||
oauthAccessToken = clientOptions.oauthAccessToken
|
||||
profileAuth = clientOptions.profileAuth
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +289,9 @@ private constructor(
|
||||
/**
|
||||
* Whether to call `validate` on every response before returning it.
|
||||
*
|
||||
* Setting this to `true` is _not_ forwards compatible with new types from the API for
|
||||
* existing fields.
|
||||
*
|
||||
* Defaults to false, which means the shape of the response will not be validated upfront.
|
||||
* Instead, validation will only occur for the parts of the response that are accessed.
|
||||
*/
|
||||
@@ -320,6 +333,25 @@ private constructor(
|
||||
*/
|
||||
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
|
||||
|
||||
/**
|
||||
* Whether run create/update calls should be automatically batched for tracing.
|
||||
*
|
||||
* Defaults to true. Set to false to send run create/update calls synchronously through the
|
||||
* single-run endpoints.
|
||||
*/
|
||||
fun autoBatchTracing(autoBatchTracing: Boolean) = apply {
|
||||
this.autoBatchTracing = autoBatchTracing
|
||||
}
|
||||
|
||||
/**
|
||||
* The level at which to log request and response information.
|
||||
*
|
||||
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
|
||||
*
|
||||
* Defaults to [LogLevel.fromEnv].
|
||||
*/
|
||||
fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
|
||||
|
||||
fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
|
||||
|
||||
/** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
|
||||
@@ -330,21 +362,6 @@ private constructor(
|
||||
/** Alias for calling [Builder.tenantId] with `tenantId.orElse(null)`. */
|
||||
fun tenantId(tenantId: Optional<String>) = tenantId(tenantId.getOrNull())
|
||||
|
||||
/**
|
||||
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
|
||||
* x-organization-id (for org scoped apis).
|
||||
*/
|
||||
fun bearerToken(bearerToken: String?) = apply { this.bearerToken = bearerToken }
|
||||
|
||||
/** Alias for calling [Builder.bearerToken] with `bearerToken.orElse(null)`. */
|
||||
fun bearerToken(bearerToken: Optional<String>) = bearerToken(bearerToken.getOrNull())
|
||||
|
||||
fun organizationId(organizationId: String?) = apply { this.organizationId = organizationId }
|
||||
|
||||
/** Alias for calling [Builder.organizationId] with `organizationId.orElse(null)`. */
|
||||
fun organizationId(organizationId: Optional<String>) =
|
||||
organizationId(organizationId.getOrNull())
|
||||
|
||||
fun headers(headers: Headers) = apply {
|
||||
this.headers.clear()
|
||||
putAllHeaders(headers)
|
||||
@@ -432,31 +449,30 @@ private constructor(
|
||||
*
|
||||
* See this table for the available options:
|
||||
*
|
||||
* |Setter |System property |Environment variable |Required|Default value |
|
||||
* |----------------|-----------------------------------|---------------------------|--------|------------------------------------|
|
||||
* |`apiKey` |`langchain.langsmithApiKey` |`LANGSMITH_API_KEY` |false |- |
|
||||
* |`tenantId` |`langchain.langsmithTenantId` |`LANGSMITH_TENANT_ID` |false |- |
|
||||
* |`bearerToken` |`langchain.langsmithBearerToken` |`LANGSMITH_BEARER_TOKEN` |false |- |
|
||||
* |`organizationId`|`langchain.langsmithOrganizationId`|`LANGSMITH_ORGANIZATION_ID`|false |- |
|
||||
* |`baseUrl` |`langchain.baseUrl` |`LANGSMITH_ENDPOINT` |true |`"https://api.smith.langchain.com/"`|
|
||||
* |Setter |System property |Environment variable |Required|Default value |
|
||||
* |----------|-----------------------------|---------------------|--------|------------------------------------|
|
||||
* |`apiKey` |`langchain.langsmithApiKey` |`LANGSMITH_API_KEY` |false |- |
|
||||
* |`tenantId`|`langchain.langsmithTenantId`|`LANGSMITH_TENANT_ID`|false |- |
|
||||
* |`baseUrl` |`langchain.baseUrl` |`LANGSMITH_ENDPOINT` |true |`"https://api.smith.langchain.com/"`|
|
||||
*
|
||||
* System properties take precedence over environment variables.
|
||||
*/
|
||||
fun fromEnv() = apply {
|
||||
(System.getProperty("langchain.baseUrl") ?: System.getenv("LANGSMITH_ENDPOINT"))?.let {
|
||||
baseUrl(it)
|
||||
logLevel(LogLevel.fromEnv())
|
||||
langsmithEndpoint()?.let { baseUrl(it) }
|
||||
langsmithApiKey()?.let { apiKey(it) }
|
||||
langsmithTenantId()?.let { tenantId(it) }
|
||||
System.getenv("LANGCHAIN_CUSTOM_HEADERS")?.let { customHeadersEnv ->
|
||||
for (line in customHeadersEnv.split("\n")) {
|
||||
val colon = line.indexOf(':')
|
||||
if (colon >= 0) {
|
||||
putHeader(line.substring(0, colon).trim(), line.substring(colon + 1).trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
loadProfileClientConfig(jsonMapper, clock, profileNameOverride = profileName())?.let {
|
||||
applyProfileConfig(it)
|
||||
}
|
||||
(System.getProperty("langchain.langsmithApiKey") ?: System.getenv("LANGSMITH_API_KEY"))
|
||||
?.let { apiKey(it) }
|
||||
(System.getProperty("langchain.langsmithTenantId")
|
||||
?: System.getenv("LANGSMITH_TENANT_ID"))
|
||||
?.let { tenantId(it) }
|
||||
(System.getProperty("langchain.langsmithBearerToken")
|
||||
?: System.getenv("LANGSMITH_BEARER_TOKEN"))
|
||||
?.let { bearerToken(it) }
|
||||
(System.getProperty("langchain.langsmithOrganizationId")
|
||||
?: System.getenv("LANGSMITH_ORGANIZATION_ID"))
|
||||
?.let { organizationId(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -502,37 +518,49 @@ private constructor(
|
||||
headers.put("X-Stainless-Package-Version", getPackageVersion())
|
||||
headers.put("X-Stainless-Runtime", "JRE")
|
||||
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
|
||||
apiKey?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("X-API-Key", it)
|
||||
}
|
||||
}
|
||||
organizationId?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("X-Organization-Id", it)
|
||||
}
|
||||
}
|
||||
bearerToken?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("Authorization", "Bearer $it")
|
||||
}
|
||||
}
|
||||
tenantId?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("X-Tenant-Id", it)
|
||||
}
|
||||
}
|
||||
headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString())
|
||||
// We replace after all the default headers to allow end-users to overwrite them.
|
||||
headers.replaceAll(this.headers.build())
|
||||
queryParams.replaceAll(this.queryParams.build())
|
||||
apiKey
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let {
|
||||
headers.remove("Authorization")
|
||||
headers.replace("X-API-Key", it)
|
||||
}
|
||||
if (apiKey.isNullOrEmpty()) {
|
||||
oauthAccessToken
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let {
|
||||
val currentHeaders = headers.build()
|
||||
if (
|
||||
currentHeaders.values("X-API-Key").all(String::isBlank) &&
|
||||
currentHeaders.values("Authorization").all(String::isBlank)
|
||||
) {
|
||||
headers.replace("Authorization", "Bearer $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
tenantId?.takeIf { it.isNotEmpty() }?.let { headers.replace("X-Tenant-Id", it) }
|
||||
|
||||
return ClientOptions(
|
||||
httpClient,
|
||||
val retryingHttpClient =
|
||||
RetryingHttpClient.builder()
|
||||
.httpClient(httpClient)
|
||||
.httpClient(
|
||||
LoggingHttpClient.builder()
|
||||
.httpClient(httpClient)
|
||||
.clock(clock)
|
||||
.level(logLevel)
|
||||
.build()
|
||||
)
|
||||
.sleeper(sleeper)
|
||||
.clock(clock)
|
||||
.maxRetries(maxRetries)
|
||||
.build(),
|
||||
.build()
|
||||
|
||||
return ClientOptions(
|
||||
httpClient,
|
||||
profileAuth?.let { ProfileAuthHttpClient(retryingHttpClient, it) }
|
||||
?: retryingHttpClient,
|
||||
checkJacksonVersionCompatibility,
|
||||
jsonMapper,
|
||||
streamHandlerExecutor,
|
||||
@@ -544,12 +572,56 @@ private constructor(
|
||||
responseValidation,
|
||||
timeout,
|
||||
maxRetries,
|
||||
autoBatchTracing,
|
||||
logLevel,
|
||||
apiKey,
|
||||
tenantId,
|
||||
bearerToken,
|
||||
organizationId,
|
||||
oauthAccessToken,
|
||||
profileAuth,
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyProfileConfig(profile: ProfileClientConfig) {
|
||||
if (baseUrl.isNullOrBlank()) {
|
||||
profile.baseUrl?.let { baseUrl(it) }
|
||||
}
|
||||
if (tenantId.isNullOrBlank()) {
|
||||
profile.tenantId?.let { tenantId(it) }
|
||||
}
|
||||
if (!apiKey.isNullOrBlank() || hasAuthHeader()) {
|
||||
return
|
||||
}
|
||||
if (!profile.oauthAccessToken.isNullOrBlank()) {
|
||||
oauthAccessToken = profile.oauthAccessToken
|
||||
profileAuth = profile.profileAuth
|
||||
} else if (profile.profileAuth != null) {
|
||||
profileAuth = profile.profileAuth
|
||||
} else if (!profile.apiKey.isNullOrBlank()) {
|
||||
apiKey(profile.apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAuthHeader(): Boolean {
|
||||
val currentHeaders = headers.build()
|
||||
return currentHeaders.values("X-API-Key").any { it.isNotBlank() } ||
|
||||
currentHeaders.values("Authorization").any { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun langsmithEndpoint(): String? =
|
||||
System.getProperty("langchain.baseUrl")
|
||||
?: System.getenv("LANGSMITH_ENDPOINT")
|
||||
?: System.getenv("LANGCHAIN_ENDPOINT")
|
||||
|
||||
private fun langsmithApiKey(): String? =
|
||||
System.getProperty("langchain.langsmithApiKey")
|
||||
?: System.getenv("LANGSMITH_API_KEY")
|
||||
?: System.getenv("LANGCHAIN_API_KEY")
|
||||
|
||||
private fun langsmithTenantId(): String? =
|
||||
System.getProperty("langchain.langsmithTenantId")
|
||||
?: System.getenv("LANGSMITH_TENANT_ID")
|
||||
?: System.getenv("LANGSMITH_WORKSPACE_ID")
|
||||
?: System.getenv("LANGCHAIN_WORKSPACE_ID")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core
|
||||
|
||||
/** The level at which to log request and response information. */
|
||||
enum class LogLevel {
|
||||
/** No logging. */
|
||||
OFF,
|
||||
/** Minimal request and response summary logs. No headers or bodies are logged. */
|
||||
INFO,
|
||||
/** [INFO] logs plus details about request failures. */
|
||||
ERROR,
|
||||
/**
|
||||
* Full request and response logs. Sensitive headers are redacted, but sensitive data in request
|
||||
* and response bodies may still be visible.
|
||||
*/
|
||||
DEBUG;
|
||||
|
||||
/** Returns whether this level is at or higher than the given [level]. */
|
||||
fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal
|
||||
|
||||
companion object {
|
||||
|
||||
/** Returns a [LogLevel] based on the `LANGCHAIN_LOG` environment variable. */
|
||||
fun fromEnv() =
|
||||
when (System.getenv("LANGCHAIN_LOG")?.lowercase()) {
|
||||
"info" -> INFO
|
||||
"error" -> ERROR
|
||||
"debug" -> DEBUG
|
||||
else -> OFF
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,14 @@ import java.io.InputStream
|
||||
import java.time.DateTimeException
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoField
|
||||
|
||||
fun jsonMapper(): JsonMapper =
|
||||
fun jsonMapper(): JsonMapper = JSON_MAPPER
|
||||
|
||||
private val JSON_MAPPER: JsonMapper =
|
||||
JsonMapper.builder()
|
||||
.addModule(kotlinModule())
|
||||
.addModule(Jdk8Module())
|
||||
@@ -36,7 +39,7 @@ fun jsonMapper(): JsonMapper =
|
||||
.addModule(
|
||||
SimpleModule()
|
||||
.addSerializer(InputStreamSerializer)
|
||||
.addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
|
||||
.addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
|
||||
)
|
||||
.withCoercionConfig(LogicalType.Boolean) {
|
||||
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
|
||||
@@ -47,6 +50,7 @@ fun jsonMapper(): JsonMapper =
|
||||
}
|
||||
.withCoercionConfig(LogicalType.Integer) {
|
||||
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
|
||||
@@ -64,6 +68,12 @@ fun jsonMapper(): JsonMapper =
|
||||
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
|
||||
}
|
||||
.withCoercionConfig(LogicalType.DateTime) {
|
||||
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
|
||||
}
|
||||
.withCoercionConfig(LogicalType.Array) {
|
||||
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
|
||||
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
|
||||
@@ -124,10 +134,10 @@ private object InputStreamSerializer : BaseSerializer<InputStream>(InputStream::
|
||||
}
|
||||
|
||||
/**
|
||||
* A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
|
||||
* A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes.
|
||||
*/
|
||||
private class LenientLocalDateTimeDeserializer :
|
||||
StdDeserializer<LocalDateTime>(LocalDateTime::class.java) {
|
||||
private class LenientOffsetDateTimeDeserializer :
|
||||
StdDeserializer<OffsetDateTime>(OffsetDateTime::class.java) {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -141,7 +151,7 @@ private class LenientLocalDateTimeDeserializer :
|
||||
|
||||
override fun logicalType(): LogicalType = LogicalType.DateTime
|
||||
|
||||
override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
|
||||
override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime {
|
||||
val exceptions = mutableListOf<Exception>()
|
||||
|
||||
for (formatter in DATE_TIME_FORMATTERS) {
|
||||
@@ -150,17 +160,20 @@ private class LenientLocalDateTimeDeserializer :
|
||||
|
||||
return when {
|
||||
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
|
||||
LocalDate.from(temporal).atStartOfDay()
|
||||
LocalDate.from(temporal)
|
||||
.atStartOfDay()
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toOffsetDateTime()
|
||||
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
|
||||
LocalDateTime.from(temporal)
|
||||
else -> ZonedDateTime.from(temporal).toLocalDateTime()
|
||||
LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime()
|
||||
else -> OffsetDateTime.from(temporal)
|
||||
}
|
||||
} catch (e: DateTimeException) {
|
||||
exceptions.add(e)
|
||||
}
|
||||
}
|
||||
|
||||
throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
|
||||
throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
|
||||
exceptions.forEach { addSuppressed(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.langchain.smith.core
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.langchain.smith.core.http.HttpClient
|
||||
import com.langchain.smith.core.http.HttpRequest
|
||||
import com.langchain.smith.core.http.HttpResponse
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
internal data class ProfileClientConfig(
|
||||
val baseUrl: String?,
|
||||
val apiKey: String?,
|
||||
val tenantId: String?,
|
||||
val oauthAccessToken: String?,
|
||||
val profileAuth: ProfileAuth?,
|
||||
)
|
||||
|
||||
private const val OAUTH_CLIENT_ID = "langsmith-cli"
|
||||
private val TOKEN_REFRESH_LEEWAY: Duration = Duration.ofMinutes(1)
|
||||
private val TOKEN_REFRESH_TIMEOUT: Duration = Duration.ofSeconds(10)
|
||||
|
||||
internal fun loadProfileClientConfig(
|
||||
jsonMapper: JsonMapper,
|
||||
clock: Clock,
|
||||
configPathOverride: Path? = null,
|
||||
profileNameOverride: String? = null,
|
||||
): ProfileClientConfig? {
|
||||
val configPath = configPathOverride ?: profileConfigPath()
|
||||
if (!Files.isRegularFile(configPath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val config =
|
||||
runCatching { jsonMapper.readTree(configPath.toFile()) as? ObjectNode }.getOrNull()
|
||||
?: return null
|
||||
val profiles = config["profiles"] as? ObjectNode ?: return null
|
||||
val profileName =
|
||||
profileNameOverride?.takeIf { it.isNotBlank() }
|
||||
?: text(config["current_profile"])
|
||||
?: "default"
|
||||
val profile = profiles[profileName] as? ObjectNode ?: return null
|
||||
|
||||
val oauth = profile["oauth"]
|
||||
val oauthAccessToken = text(oauth?.get("access_token"))
|
||||
val oauthRefreshToken = text(oauth?.get("refresh_token"))
|
||||
|
||||
return ProfileClientConfig(
|
||||
baseUrl = text(profile["api_url"]),
|
||||
apiKey = text(profile["api_key"]),
|
||||
tenantId = text(profile["workspace_id"]),
|
||||
oauthAccessToken = oauthAccessToken,
|
||||
profileAuth =
|
||||
if (oauthAccessToken != null || oauthRefreshToken != null) {
|
||||
ProfileAuth(jsonMapper, clock, configPath, config, profile)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun profileConfigPath(): Path {
|
||||
val configuredPath =
|
||||
System.getProperty("langchain.langsmithConfigFile")
|
||||
?: System.getenv("LANGSMITH_CONFIG_FILE")
|
||||
return configuredPath?.takeIf { it.isNotBlank() }?.let(Paths::get)
|
||||
?: Paths.get(System.getProperty("user.home"), ".langsmith", "config.json")
|
||||
}
|
||||
|
||||
internal fun profileName(): String? =
|
||||
(System.getProperty("langchain.langsmithProfile") ?: System.getenv("LANGSMITH_PROFILE"))
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun shouldRefreshProfileToken(oauth: ObjectNode, clock: Clock): Boolean {
|
||||
if (text(oauth["refresh_token"]) == null) {
|
||||
return false
|
||||
}
|
||||
if (text(oauth["access_token"]) == null) {
|
||||
return true
|
||||
}
|
||||
val expiresAt = expiresAt(oauth["expires_at"]) ?: return false
|
||||
return !expiresAt.isAfter(clock.instant().plus(TOKEN_REFRESH_LEEWAY))
|
||||
}
|
||||
|
||||
private fun refreshProfileOAuthToken(
|
||||
jsonMapper: JsonMapper,
|
||||
baseUrl: String,
|
||||
refreshToken: String,
|
||||
): OAuthTokenResponse? {
|
||||
return runCatching {
|
||||
val connection =
|
||||
(URL("${normalizeConfigUrl(baseUrl)}/oauth/token").openConnection()
|
||||
as HttpURLConnection)
|
||||
.apply {
|
||||
requestMethod = "POST"
|
||||
connectTimeout = TOKEN_REFRESH_TIMEOUT.toMillis().toInt()
|
||||
readTimeout = TOKEN_REFRESH_TIMEOUT.toMillis().toInt()
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
val body =
|
||||
formEncode(
|
||||
mapOf(
|
||||
"grant_type" to "refresh_token",
|
||||
"client_id" to OAUTH_CLIENT_ID,
|
||||
"refresh_token" to refreshToken,
|
||||
)
|
||||
)
|
||||
OutputStreamWriter(connection.outputStream, StandardCharsets.UTF_8).use {
|
||||
it.write(body)
|
||||
}
|
||||
if (connection.responseCode !in 200..299) {
|
||||
return@runCatching null
|
||||
}
|
||||
val response = connection.inputStream.use { jsonMapper.readTree(it) }
|
||||
val accessToken = text(response["access_token"]) ?: return@runCatching null
|
||||
OAuthTokenResponse(
|
||||
accessToken = accessToken,
|
||||
refreshToken = text(response["refresh_token"]),
|
||||
expiresInSeconds = response["expires_in"]?.takeIf { it.isNumber }?.asLong(),
|
||||
)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
private fun normalizeConfigUrl(baseUrl: String): String =
|
||||
baseUrl.trimEnd('/').removeSuffix("/api/v1")
|
||||
|
||||
private fun formEncode(values: Map<String, String>): String =
|
||||
values.entries.joinToString("&") { (key, value) -> "${urlEncode(key)}=${urlEncode(value)}" }
|
||||
|
||||
private fun urlEncode(value: String): String =
|
||||
URLEncoder.encode(value, StandardCharsets.UTF_8.name())
|
||||
|
||||
private fun text(node: JsonNode?): String? =
|
||||
node?.takeIf { it.isTextual }?.asText()?.trimQuotes()?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun expiresAt(node: JsonNode?): Instant? =
|
||||
when {
|
||||
node == null -> null
|
||||
node.isNumber -> Instant.ofEpochSecond(node.asLong())
|
||||
node.isTextual ->
|
||||
runCatching { Instant.parse(node.asText()) }
|
||||
.recoverCatching { OffsetDateTime.parse(node.asText()).toInstant() }
|
||||
.getOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun String.trimQuotes(): String = trim().trim('"', '\'')
|
||||
|
||||
internal class ProfileAuth(
|
||||
private val jsonMapper: JsonMapper,
|
||||
private val clock: Clock,
|
||||
private val configPath: Path,
|
||||
private val config: ObjectNode,
|
||||
private val profile: ObjectNode,
|
||||
) {
|
||||
private val managedAuthorizationValues = mutableSetOf<String>()
|
||||
|
||||
init {
|
||||
rememberProfileAuthHeader(authHeaderFromProfile())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun currentAuthHeader(): Pair<String, String>? =
|
||||
authHeaderFromProfile().also { rememberProfileAuthHeader(it) }
|
||||
|
||||
@Synchronized
|
||||
fun isProfileAuthorizationHeader(value: String): Boolean =
|
||||
managedAuthorizationValues.contains(value)
|
||||
|
||||
@Synchronized
|
||||
fun authHeader(): Pair<String, String>? {
|
||||
val oauth = profile["oauth"] as? ObjectNode
|
||||
if (oauth != null && shouldRefreshProfileToken(oauth, clock)) {
|
||||
val refreshToken = text(oauth["refresh_token"])
|
||||
if (refreshToken != null) {
|
||||
val tokenEndpointBaseUrl = text(profile["api_url"]) ?: ClientOptions.PRODUCTION_URL
|
||||
val tokenResponse =
|
||||
refreshProfileOAuthToken(jsonMapper, tokenEndpointBaseUrl, refreshToken)
|
||||
if (tokenResponse != null) {
|
||||
oauth.put("access_token", tokenResponse.accessToken)
|
||||
tokenResponse.refreshToken?.let { oauth.put("refresh_token", it) }
|
||||
tokenResponse.expiresInSeconds?.let {
|
||||
oauth.put("expires_at", clock.instant().plusSeconds(it).toString())
|
||||
}
|
||||
oauth.remove("token_type")
|
||||
oauth.remove("bearer_token")
|
||||
saveConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
return authHeaderFromProfile().also { rememberProfileAuthHeader(it) }
|
||||
}
|
||||
|
||||
private fun authHeaderFromProfile(): Pair<String, String>? {
|
||||
val oauth = profile["oauth"] as? ObjectNode
|
||||
text(oauth?.get("access_token"))?.let {
|
||||
return "Authorization" to "Bearer $it"
|
||||
}
|
||||
text(profile["api_key"])?.let {
|
||||
return "X-API-Key" to it
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun rememberProfileAuthHeader(header: Pair<String, String>?) {
|
||||
if (header?.first?.equals("Authorization", ignoreCase = true) == true) {
|
||||
managedAuthorizationValues.add(header.second)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveConfig() {
|
||||
runCatching {
|
||||
configPath.parent?.let { Files.createDirectories(it) }
|
||||
jsonMapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ProfileAuthHttpClient(
|
||||
private val delegate: HttpClient,
|
||||
private val profileAuth: ProfileAuth,
|
||||
) : HttpClient {
|
||||
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse =
|
||||
delegate.execute(prepareProfileAuthRequest(request), requestOptions)
|
||||
|
||||
override fun executeAsync(
|
||||
request: HttpRequest,
|
||||
requestOptions: RequestOptions,
|
||||
): CompletableFuture<HttpResponse> =
|
||||
delegate.executeAsync(prepareProfileAuthRequest(request), requestOptions)
|
||||
|
||||
override fun close() = delegate.close()
|
||||
|
||||
private fun prepareProfileAuthRequest(request: HttpRequest): HttpRequest {
|
||||
if (request.headers.values("X-API-Key").any(String::isNotBlank)) {
|
||||
return request.toBuilder().removeHeaders("Authorization").build()
|
||||
}
|
||||
val authorizationValues = request.headers.values("Authorization").filter(String::isNotBlank)
|
||||
if (
|
||||
authorizationValues.isNotEmpty() &&
|
||||
authorizationValues.none { profileAuth.isProfileAuthorizationHeader(it) }
|
||||
) {
|
||||
return request
|
||||
}
|
||||
val (name, value) = profileAuth.authHeader() ?: return request
|
||||
val builder = request.toBuilder()
|
||||
if (name.equals("X-API-Key", ignoreCase = true)) {
|
||||
builder.removeHeaders("Authorization")
|
||||
}
|
||||
return builder.replaceHeaders(name, value).build()
|
||||
}
|
||||
}
|
||||
|
||||
private data class OAuthTokenResponse(
|
||||
val accessToken: String,
|
||||
val refreshToken: String?,
|
||||
val expiresInSeconds: Long?,
|
||||
)
|
||||
@@ -3,6 +3,9 @@
|
||||
package com.langchain.smith.core
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
private val logger = LoggerFactory.getLogger("com.langchain.smith.core.Properties")
|
||||
|
||||
fun getOsArch(): String {
|
||||
val osArch = System.getProperty("os.arch")
|
||||
@@ -12,8 +15,10 @@ fun getOsArch(): String {
|
||||
"i386",
|
||||
"x32",
|
||||
"x86" -> "x32"
|
||||
|
||||
"amd64",
|
||||
"x86_64" -> "x64"
|
||||
|
||||
"arm" -> "arm"
|
||||
"aarch64" -> "arm64"
|
||||
else -> "other:$osArch"
|
||||
@@ -34,9 +39,23 @@ fun getOsName(): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun getOsVersion(): String = System.getProperty("os.version", "unknown")
|
||||
fun getOsVersion(): String = System.getProperty("os.version", "unknown") ?: "unknown"
|
||||
|
||||
fun getPackageVersion(): String =
|
||||
LangsmithClient::class.java.`package`.implementationVersion ?: "unknown"
|
||||
fun getPackageVersion(): String {
|
||||
try {
|
||||
val props = java.util.Properties()
|
||||
LangsmithClient::class
|
||||
.java
|
||||
.classLoader
|
||||
.getResourceAsStream("com/langchain/smith/version.properties")
|
||||
?.use { props.load(it) }
|
||||
props.getProperty("version")?.let {
|
||||
return it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Failed to read version from version.properties", e)
|
||||
}
|
||||
return LangsmithClient::class.java.`package`.implementationVersion ?: "unknown"
|
||||
}
|
||||
|
||||
fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
|
||||
fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown"
|
||||
|
||||
@@ -33,6 +33,15 @@ class RequestOptions private constructor(val responseValidation: Boolean?, val t
|
||||
private var responseValidation: Boolean? = null
|
||||
private var timeout: Timeout? = null
|
||||
|
||||
/**
|
||||
* Whether to call `validate` on the response before returning it.
|
||||
*
|
||||
* Setting this to `true` is _not_ forwards compatible with new types from the API for
|
||||
* existing fields.
|
||||
*
|
||||
* Defaults to false, which means the shape of the response will not be validated upfront.
|
||||
* Instead, validation will only occur for the parts of the response that are accessed.
|
||||
*/
|
||||
fun responseValidation(responseValidation: Boolean) = apply {
|
||||
this.responseValidation = responseValidation
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package com.langchain.smith.core
|
||||
import com.langchain.smith.errors.LangChainInvalidDataException
|
||||
import java.util.Collections
|
||||
import java.util.SortedMap
|
||||
import java.util.SortedSet
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.locks.Lock
|
||||
|
||||
@@ -16,6 +17,11 @@ internal fun <T : Any> T?.getOrThrow(name: String): T =
|
||||
internal fun <T> List<T>.toImmutable(): List<T> =
|
||||
if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun <V : Comparable<V>> SortedSet<V>.toImmutable(): SortedSet<V> =
|
||||
if (isEmpty()) Collections.emptySortedSet()
|
||||
else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder()))
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun <K, V> Map<K, V>.toImmutable(): Map<K, V> =
|
||||
if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
|
||||
|
||||
+231
-86
@@ -7,14 +7,15 @@ package com.langchain.smith.core.http
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeType
|
||||
import com.github.luben.zstd.ZstdOutputStream
|
||||
import com.langchain.smith.core.MultipartField
|
||||
import com.langchain.smith.core.toImmutable
|
||||
import com.langchain.smith.errors.LangChainInvalidDataException
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.UUID
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
|
||||
import org.apache.hc.core5.http.ContentType
|
||||
import org.apache.hc.core5.http.HttpEntity
|
||||
|
||||
@JvmSynthetic
|
||||
internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
|
||||
@@ -32,99 +33,243 @@ internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequ
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun zstd(body: HttpRequestBody): HttpRequestBody =
|
||||
object : HttpRequestBody {
|
||||
|
||||
override fun writeTo(outputStream: OutputStream) {
|
||||
ZstdOutputStream(NonClosingOutputStream(outputStream)).setCloseFrameOnFlush(true).use {
|
||||
body.writeTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentType(): String? = body.contentType()
|
||||
|
||||
override fun contentLength(): Long = -1L
|
||||
|
||||
override fun repeatable(): Boolean = body.repeatable()
|
||||
|
||||
override fun close() = body.close()
|
||||
}
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun multipartFormData(
|
||||
jsonMapper: JsonMapper,
|
||||
fields: Map<String, MultipartField<*>>,
|
||||
): HttpRequestBody =
|
||||
object : HttpRequestBody {
|
||||
private val entity: HttpEntity by lazy {
|
||||
MultipartEntityBuilder.create()
|
||||
.apply {
|
||||
fields.forEach { (name, field) ->
|
||||
val knownValue = field.value.asKnown().getOrNull()
|
||||
val parts =
|
||||
if (knownValue is InputStream) {
|
||||
// Read directly from the `InputStream` instead of reading it all
|
||||
// into memory due to the `jsonMapper` serialization below.
|
||||
sequenceOf(name to knownValue)
|
||||
} else {
|
||||
val node = jsonMapper.valueToTree<JsonNode>(field.value)
|
||||
serializePart(name, node)
|
||||
}
|
||||
|
||||
parts.forEach { (name, bytes) ->
|
||||
addBinaryBody(
|
||||
name,
|
||||
bytes,
|
||||
ContentType.parseLenient(field.contentType),
|
||||
field.filename().getOrNull(),
|
||||
)
|
||||
}
|
||||
MultipartBody.Builder()
|
||||
.apply {
|
||||
fields.forEach { (name, field) ->
|
||||
val knownValue = field.value.asKnown().getOrNull()
|
||||
val parts =
|
||||
if (knownValue is InputStream) {
|
||||
// Read directly from the `InputStream` instead of reading it all
|
||||
// into memory due to the `jsonMapper` serialization below.
|
||||
sequenceOf(name to knownValue)
|
||||
} else {
|
||||
val node = jsonMapper.valueToTree<JsonNode>(field.value)
|
||||
serializePart(name, node)
|
||||
}
|
||||
|
||||
parts.forEach { (name, bytes) ->
|
||||
val partBody =
|
||||
if (bytes is ByteArrayInputStream) {
|
||||
val byteArray = bytes.readBytes()
|
||||
|
||||
object : HttpRequestBody {
|
||||
|
||||
override fun writeTo(outputStream: OutputStream) {
|
||||
outputStream.write(byteArray)
|
||||
}
|
||||
|
||||
override fun contentType(): String = field.contentType
|
||||
|
||||
override fun contentLength(): Long = byteArray.size.toLong()
|
||||
|
||||
override fun repeatable(): Boolean = true
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
} else {
|
||||
object : HttpRequestBody {
|
||||
|
||||
override fun writeTo(outputStream: OutputStream) {
|
||||
bytes.copyTo(outputStream)
|
||||
}
|
||||
|
||||
override fun contentType(): String = field.contentType
|
||||
|
||||
override fun contentLength(): Long = -1L
|
||||
|
||||
override fun repeatable(): Boolean = false
|
||||
|
||||
override fun close() = bytes.close()
|
||||
}
|
||||
}
|
||||
|
||||
addPart(
|
||||
MultipartBody.Part.create(
|
||||
name,
|
||||
field.filename().getOrNull(),
|
||||
field.contentType,
|
||||
partBody,
|
||||
)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
private class NonClosingOutputStream(private val delegate: OutputStream) : OutputStream() {
|
||||
override fun write(b: Int) = delegate.write(b)
|
||||
|
||||
override fun write(b: ByteArray) = delegate.write(b)
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) = delegate.write(b, off, len)
|
||||
|
||||
override fun flush() = delegate.flush()
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
|
||||
private fun serializePart(name: String, node: JsonNode): Sequence<Pair<String, InputStream>> =
|
||||
when (node.nodeType) {
|
||||
JsonNodeType.MISSING,
|
||||
JsonNodeType.NULL -> emptySequence()
|
||||
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
|
||||
JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream())
|
||||
JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream())
|
||||
JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream())
|
||||
JsonNodeType.ARRAY ->
|
||||
node.elements().asSequence().flatMap { element -> serializePart(name, element) }
|
||||
JsonNodeType.OBJECT ->
|
||||
node.fields().asSequence().flatMap { (key, value) ->
|
||||
serializePart("$name[$key]", value)
|
||||
}
|
||||
JsonNodeType.POJO,
|
||||
null -> throw LangChainInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
|
||||
}
|
||||
|
||||
private class MultipartBody
|
||||
private constructor(private val boundary: String, private val parts: List<Part>) : HttpRequestBody {
|
||||
private val boundaryBytes: ByteArray = boundary.toByteArray()
|
||||
private val contentType = "multipart/form-data; boundary=$boundary"
|
||||
|
||||
// This must remain in sync with `contentLength`.
|
||||
override fun writeTo(outputStream: OutputStream) {
|
||||
parts.forEach { part ->
|
||||
outputStream.write(DASHDASH)
|
||||
outputStream.write(boundaryBytes)
|
||||
outputStream.write(CRLF)
|
||||
|
||||
outputStream.write(CONTENT_DISPOSITION)
|
||||
outputStream.write(part.contentDisposition.toByteArray())
|
||||
outputStream.write(CRLF)
|
||||
|
||||
outputStream.write(CONTENT_TYPE)
|
||||
outputStream.write(part.contentType.toByteArray())
|
||||
outputStream.write(CRLF)
|
||||
|
||||
outputStream.write(CRLF)
|
||||
part.body.writeTo(outputStream)
|
||||
outputStream.write(CRLF)
|
||||
}
|
||||
|
||||
private fun serializePart(
|
||||
name: String,
|
||||
node: JsonNode,
|
||||
): Sequence<Pair<String, InputStream>> =
|
||||
when (node.nodeType) {
|
||||
JsonNodeType.MISSING,
|
||||
JsonNodeType.NULL -> emptySequence()
|
||||
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
|
||||
JsonNodeType.STRING -> sequenceOf(name to node.textValue().inputStream())
|
||||
JsonNodeType.BOOLEAN ->
|
||||
sequenceOf(name to node.booleanValue().toString().inputStream())
|
||||
JsonNodeType.NUMBER ->
|
||||
sequenceOf(name to node.numberValue().toString().inputStream())
|
||||
JsonNodeType.ARRAY ->
|
||||
sequenceOf(
|
||||
name to
|
||||
node
|
||||
.elements()
|
||||
.asSequence()
|
||||
.mapNotNull { element ->
|
||||
when (element.nodeType) {
|
||||
JsonNodeType.MISSING,
|
||||
JsonNodeType.NULL -> null
|
||||
JsonNodeType.STRING -> node.textValue()
|
||||
JsonNodeType.BOOLEAN -> node.booleanValue().toString()
|
||||
JsonNodeType.NUMBER -> node.numberValue().toString()
|
||||
null,
|
||||
JsonNodeType.BINARY,
|
||||
JsonNodeType.ARRAY,
|
||||
JsonNodeType.OBJECT,
|
||||
JsonNodeType.POJO ->
|
||||
throw LangChainInvalidDataException(
|
||||
"Unexpected JsonNode type in array: ${node.nodeType}"
|
||||
)
|
||||
}
|
||||
}
|
||||
.joinToString(",")
|
||||
.inputStream()
|
||||
)
|
||||
JsonNodeType.OBJECT ->
|
||||
node.fields().asSequence().flatMap { (key, value) ->
|
||||
serializePart("$name[$key]", value)
|
||||
}
|
||||
JsonNodeType.POJO,
|
||||
null ->
|
||||
throw LangChainInvalidDataException(
|
||||
"Unexpected JsonNode type: ${node.nodeType}"
|
||||
)
|
||||
outputStream.write(DASHDASH)
|
||||
outputStream.write(boundaryBytes)
|
||||
outputStream.write(DASHDASH)
|
||||
outputStream.write(CRLF)
|
||||
}
|
||||
|
||||
override fun contentType(): String = contentType
|
||||
|
||||
// This must remain in sync with `writeTo`.
|
||||
override fun contentLength(): Long {
|
||||
var byteCount = 0L
|
||||
|
||||
parts.forEach { part ->
|
||||
val contentLength = part.body.contentLength()
|
||||
if (contentLength == -1L) {
|
||||
return -1L
|
||||
}
|
||||
|
||||
private fun String.inputStream(): InputStream = toByteArray().inputStream()
|
||||
byteCount +=
|
||||
DASHDASH.size +
|
||||
boundaryBytes.size +
|
||||
CRLF.size +
|
||||
CONTENT_DISPOSITION.size +
|
||||
part.contentDisposition.toByteArray().size +
|
||||
CRLF.size +
|
||||
CONTENT_TYPE.size +
|
||||
part.contentType.toByteArray().size +
|
||||
CRLF.size +
|
||||
CRLF.size +
|
||||
contentLength +
|
||||
CRLF.size
|
||||
}
|
||||
|
||||
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
|
||||
|
||||
override fun contentType(): String = entity.contentType
|
||||
|
||||
override fun contentLength(): Long = entity.contentLength
|
||||
|
||||
override fun repeatable(): Boolean = entity.isRepeatable
|
||||
|
||||
override fun close() = entity.close()
|
||||
byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
|
||||
return byteCount
|
||||
}
|
||||
|
||||
override fun repeatable(): Boolean = parts.all { it.body.repeatable() }
|
||||
|
||||
override fun close() {
|
||||
parts.forEach { it.body.close() }
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private val boundary = UUID.randomUUID().toString()
|
||||
private val parts: MutableList<Part> = mutableListOf()
|
||||
|
||||
fun addPart(part: Part) = apply { parts.add(part) }
|
||||
|
||||
fun build() = MultipartBody(boundary, parts.toImmutable())
|
||||
}
|
||||
|
||||
class Part
|
||||
private constructor(
|
||||
val contentDisposition: String,
|
||||
val contentType: String,
|
||||
val body: HttpRequestBody,
|
||||
) {
|
||||
companion object {
|
||||
fun create(
|
||||
name: String,
|
||||
filename: String?,
|
||||
contentType: String,
|
||||
body: HttpRequestBody,
|
||||
): Part {
|
||||
val disposition = buildString {
|
||||
append("form-data; name=")
|
||||
appendQuotedString(name)
|
||||
if (filename != null) {
|
||||
append("; filename=")
|
||||
appendQuotedString(filename)
|
||||
}
|
||||
}
|
||||
return Part(disposition, contentType, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
|
||||
private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
|
||||
private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray()
|
||||
private val CONTENT_TYPE = "Content-Type: ".toByteArray()
|
||||
|
||||
private fun StringBuilder.appendQuotedString(key: String) {
|
||||
append('"')
|
||||
for (ch in key) {
|
||||
when (ch) {
|
||||
'\n' -> append("%0A")
|
||||
'\r' -> append("%0D")
|
||||
'"' -> append("%22")
|
||||
else -> append(ch)
|
||||
}
|
||||
}
|
||||
append('"')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+630
@@ -0,0 +1,630 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core.http
|
||||
|
||||
import com.langchain.smith.core.LogLevel
|
||||
import com.langchain.smith.core.RequestOptions
|
||||
import com.langchain.smith.core.checkRequired
|
||||
import com.langchain.smith.core.toImmutable
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.CharacterCodingException
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.CharsetDecoder
|
||||
import java.nio.charset.CodingErrorAction
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.SortedSet
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CompletionException
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
/** A wrapper [HttpClient] around [httpClient] that logs request and response information. */
|
||||
class LoggingHttpClient
|
||||
private constructor(
|
||||
/** The underlying [HttpClient] for making requests. */
|
||||
@get:JvmName("httpClient") val httpClient: HttpClient,
|
||||
/**
|
||||
* Sensitive headers to redact from logs.
|
||||
*
|
||||
* Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie",
|
||||
* "x-tenant-id")`.
|
||||
*/
|
||||
@get:JvmName("redactedHeaders") val redactedHeaders: SortedSet<String>,
|
||||
/**
|
||||
* The clock to use for measuring request and response durations.
|
||||
*
|
||||
* This is primarily useful for using a fake clock in tests.
|
||||
*
|
||||
* Defaults to [Clock.systemUTC].
|
||||
*/
|
||||
@get:JvmName("clock") val clock: Clock,
|
||||
/**
|
||||
* The log level to use.
|
||||
*
|
||||
* Pass [LogLevel.fromEnv] to read from environment variables.
|
||||
*/
|
||||
@get:JvmName("level") val level: LogLevel,
|
||||
) : HttpClient {
|
||||
|
||||
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
|
||||
val loggingRequest = logRequest(request)
|
||||
|
||||
val before = OffsetDateTime.now(clock)
|
||||
val response =
|
||||
try {
|
||||
httpClient.execute(loggingRequest, requestOptions)
|
||||
} catch (e: Throwable) {
|
||||
logFailure(e, Duration.between(before, OffsetDateTime.now(clock)))
|
||||
throw e
|
||||
}
|
||||
|
||||
val took = Duration.between(before, OffsetDateTime.now(clock))
|
||||
return logResponse(response, took)
|
||||
}
|
||||
|
||||
override fun executeAsync(
|
||||
request: HttpRequest,
|
||||
requestOptions: RequestOptions,
|
||||
): CompletableFuture<HttpResponse> {
|
||||
val loggingRequest = logRequest(request)
|
||||
|
||||
val before = OffsetDateTime.now(clock)
|
||||
val future =
|
||||
try {
|
||||
httpClient.executeAsync(loggingRequest, requestOptions)
|
||||
} catch (e: Throwable) {
|
||||
logFailure(e, Duration.between(before, OffsetDateTime.now(clock)))
|
||||
throw e
|
||||
}
|
||||
return future.handle { response, error ->
|
||||
val took = Duration.between(before, OffsetDateTime.now(clock))
|
||||
if (error != null) {
|
||||
logFailure(unwrapCompletionException(error), took)
|
||||
throw error
|
||||
}
|
||||
logResponse(response, took)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRequest(request: HttpRequest): HttpRequest {
|
||||
if (!level.shouldLog(LogLevel.INFO)) {
|
||||
return request
|
||||
}
|
||||
|
||||
System.err.println(
|
||||
buildString {
|
||||
append("--> ${request.method} ${request.url()}")
|
||||
request.body?.let {
|
||||
val length = it.contentLength()
|
||||
append(if (length >= 0) " ($length-byte body)" else " (unknown-length body)")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!level.shouldLog(LogLevel.DEBUG)) {
|
||||
return request
|
||||
}
|
||||
|
||||
logHeaders(request.headers)
|
||||
|
||||
if (request.body == null) {
|
||||
System.err.println("--> END ${request.method}")
|
||||
System.err.println()
|
||||
return request
|
||||
}
|
||||
|
||||
return request
|
||||
.toBuilder()
|
||||
.body(LoggingHttpRequestBody(request.method, request.body))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun logResponse(response: HttpResponse, took: Duration): HttpResponse {
|
||||
if (!level.shouldLog(LogLevel.INFO)) {
|
||||
return response
|
||||
}
|
||||
|
||||
val contentLength = response.headers().values("Content-Length").firstOrNull()?.toIntOrNull()
|
||||
System.err.println(
|
||||
"<-- ${response.statusCode()} (${
|
||||
buildString {
|
||||
append(took.format())
|
||||
contentLength?.let { append(", $contentLength-byte body") }
|
||||
}
|
||||
})"
|
||||
)
|
||||
|
||||
if (!level.shouldLog(LogLevel.DEBUG)) {
|
||||
return response
|
||||
}
|
||||
|
||||
logHeaders(response.headers())
|
||||
return LoggingHttpResponse(response)
|
||||
}
|
||||
|
||||
private fun logFailure(error: Throwable, took: Duration) {
|
||||
if (!level.shouldLog(LogLevel.ERROR)) {
|
||||
return
|
||||
}
|
||||
|
||||
System.err.println(
|
||||
buildString {
|
||||
append("<-- !! ${error.javaClass.simpleName}")
|
||||
error.message?.let { append(": $it") }
|
||||
append(" (${took.format()})")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun unwrapCompletionException(error: Throwable): Throwable =
|
||||
if (error is CompletionException && error.cause != null) error.cause!! else error
|
||||
|
||||
private fun logHeaders(headers: Headers) =
|
||||
headers.names().forEach { name ->
|
||||
headers.values(name).forEach { value ->
|
||||
System.err.println("$name: ${if (redactedHeaders.contains(name)) "██" else value}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() = httpClient.close()
|
||||
|
||||
fun toBuilder() = Builder().from(this)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Returns a mutable builder for constructing an instance of [LoggingHttpClient].
|
||||
*
|
||||
* The following fields are required:
|
||||
* ```java
|
||||
* .httpClient()
|
||||
* .level()
|
||||
* ```
|
||||
*/
|
||||
@JvmStatic fun builder() = Builder()
|
||||
}
|
||||
|
||||
/** A builder for [LoggingHttpClient]. */
|
||||
class Builder internal constructor() {
|
||||
|
||||
private var httpClient: HttpClient? = null
|
||||
private var redactedHeaders: Set<String> =
|
||||
setOf("authorization", "api-key", "x-api-key", "cookie", "set-cookie", "x-tenant-id")
|
||||
private var clock: Clock = Clock.systemUTC()
|
||||
private var level: LogLevel? = null
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun from(loggingHttpClient: LoggingHttpClient) = apply {
|
||||
httpClient = loggingHttpClient.httpClient
|
||||
redactedHeaders = loggingHttpClient.redactedHeaders
|
||||
clock = loggingHttpClient.clock
|
||||
level = loggingHttpClient.level
|
||||
}
|
||||
|
||||
/** The underlying [HttpClient] for making requests. */
|
||||
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
|
||||
|
||||
/**
|
||||
* Sensitive headers to redact from logs.
|
||||
*
|
||||
* Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie",
|
||||
* "x-tenant-id")`.
|
||||
*/
|
||||
fun redactedHeaders(redactedHeaders: Set<String>) = apply {
|
||||
this.redactedHeaders = redactedHeaders
|
||||
}
|
||||
|
||||
/**
|
||||
* The clock to use for measuring request and response durations.
|
||||
*
|
||||
* This is primarily useful for using a fake clock in tests.
|
||||
*
|
||||
* Defaults to [Clock.systemUTC].
|
||||
*/
|
||||
fun clock(clock: Clock) = apply { this.clock = clock }
|
||||
|
||||
/**
|
||||
* The log level to use.
|
||||
*
|
||||
* Pass [LogLevel.fromEnv] to read from environment variables.
|
||||
*/
|
||||
fun level(level: LogLevel) = apply { this.level = level }
|
||||
|
||||
/**
|
||||
* Returns an immutable instance of [LoggingHttpClient].
|
||||
*
|
||||
* Further updates to this [Builder] will not mutate the returned instance.
|
||||
*
|
||||
* The following fields are required:
|
||||
* ```java
|
||||
* .httpClient()
|
||||
* .level()
|
||||
* ```
|
||||
*
|
||||
* @throws IllegalStateException if any required field is unset.
|
||||
*/
|
||||
fun build(): LoggingHttpClient =
|
||||
LoggingHttpClient(
|
||||
checkRequired("httpClient", httpClient),
|
||||
redactedHeaders.toSortedSet(String.CASE_INSENSITIVE_ORDER).toImmutable(),
|
||||
clock,
|
||||
checkRequired("level", level),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An [HttpRequestBody] wrapper that delegates to [body] while also logging line by line as it's
|
||||
* written.
|
||||
*
|
||||
* The logging occurs in a streaming manner with minimal buffering.
|
||||
*/
|
||||
private class LoggingHttpRequestBody(
|
||||
private val method: HttpMethod,
|
||||
private val body: HttpRequestBody,
|
||||
) : HttpRequestBody {
|
||||
|
||||
private val charset by lazy { parseCharset(body.contentType()) }
|
||||
|
||||
override fun writeTo(outputStream: OutputStream) {
|
||||
val loggingOutputStream = LoggingOutputStream(outputStream, charset)
|
||||
body.writeTo(loggingOutputStream)
|
||||
|
||||
loggingOutputStream.flush()
|
||||
System.err.println("--> END $method (${loggingOutputStream.writeCount()}-byte body)")
|
||||
System.err.println()
|
||||
}
|
||||
|
||||
override fun contentType(): String? = body.contentType()
|
||||
|
||||
override fun contentLength(): Long = body.contentLength()
|
||||
|
||||
override fun repeatable(): Boolean = body.repeatable()
|
||||
|
||||
override fun close() = body.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* An [OutputStream] wrapper that delegates to [outputStream] while also logging bytes line by line
|
||||
* as it's written to.
|
||||
*
|
||||
* The written content is assumed to be in the given [charset] and the logging occurs in a streaming
|
||||
* manner with minimal buffering.
|
||||
*/
|
||||
private class LoggingOutputStream(private val outputStream: OutputStream, charset: Charset?) :
|
||||
OutputStream() {
|
||||
|
||||
private val buffer = LoggingBuffer(charset)
|
||||
|
||||
fun writeCount() = buffer.writeCount()
|
||||
|
||||
override fun write(b: Int) {
|
||||
outputStream.write(b)
|
||||
buffer.write(b)
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
outputStream.write(b, off, len)
|
||||
for (i in off until off + len) {
|
||||
buffer.write(b[i].toInt() and 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
/** Prints any currently buffered content. */
|
||||
override fun flush() {
|
||||
buffer.flush()
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
override fun close() = outputStream.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* An [HttpResponse] wrapper that delegates to [response] while also logging line-by-line as it's
|
||||
* read.
|
||||
*
|
||||
* The logging occurs in a streaming manner with minimal buffering.
|
||||
*/
|
||||
private class LoggingHttpResponse(private val response: HttpResponse) : HttpResponse {
|
||||
|
||||
private val loggingBody: Lazy<InputStream> = lazy {
|
||||
LoggingInputStream(
|
||||
response.body(),
|
||||
parseCharset(response.headers().values("Content-Type").firstOrNull()),
|
||||
)
|
||||
}
|
||||
|
||||
override fun statusCode(): Int = response.statusCode()
|
||||
|
||||
override fun headers(): Headers = response.headers()
|
||||
|
||||
override fun body(): InputStream = loggingBody.value
|
||||
|
||||
override fun close() {
|
||||
if (loggingBody.isInitialized()) {
|
||||
loggingBody.value.close()
|
||||
}
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An [InputStream] wrapper that delegates to [inputStream] while also logging bytes line by line as
|
||||
* it's read.
|
||||
*
|
||||
* The contents of [inputStream] are assumed to be in the given [charset] and the logging occurs in
|
||||
* a streaming manner with minimal buffering.
|
||||
*/
|
||||
private class LoggingInputStream(private val inputStream: InputStream, charset: Charset?) :
|
||||
InputStream() {
|
||||
|
||||
private var isDone = false
|
||||
private val buffer = LoggingBuffer(charset)
|
||||
|
||||
override fun read(): Int {
|
||||
if (isDone) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val b = inputStream.read()
|
||||
|
||||
if (b == -1) {
|
||||
markDone()
|
||||
return b
|
||||
}
|
||||
|
||||
buffer.write(b)
|
||||
return b
|
||||
}
|
||||
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
if (isDone) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val bytesRead = inputStream.read(b, off, len)
|
||||
|
||||
if (bytesRead == -1) {
|
||||
markDone()
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
for (i in off until off + bytesRead) {
|
||||
buffer.write(b[i].toInt() and 0xFF)
|
||||
}
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!isDone) {
|
||||
markDone(closedEarly = true)
|
||||
}
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
private fun markDone(closedEarly: Boolean = false) {
|
||||
isDone = true
|
||||
buffer.flush()
|
||||
val suffix = if (closedEarly) ", closed early" else ""
|
||||
System.err.println("<-- END HTTP (${buffer.writeCount()}-byte body$suffix)")
|
||||
System.err.println()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A byte buffer that prints line by line, using the given [charset], as bytes are written to it.
|
||||
*
|
||||
* When [charset] is `null`, the buffer performs an upfront check to detect binary content. If
|
||||
* non-whitespace ISO control characters are found in the first [PROBABLY_UTF8_CODE_POINT_LIMIT]
|
||||
* code points, body logging is suppressed entirely.
|
||||
*/
|
||||
private class LoggingBuffer(charset: Charset?) {
|
||||
|
||||
private val charset = charset ?: StandardCharsets.UTF_8
|
||||
|
||||
private val decoder: CharsetDecoder =
|
||||
this.charset
|
||||
.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||
private var writeCount = 0
|
||||
private val buffer = ByteArrayOutputStream(128)
|
||||
|
||||
/**
|
||||
* Whether logging has been suppressed because the content doesn't appear to be readable text.
|
||||
*
|
||||
* This is only set when [charset] is `null` and the content fails the [isProbablyUtf8] check.
|
||||
*/
|
||||
private var suppressed = false
|
||||
|
||||
/**
|
||||
* Bytes accumulated for the [isProbablyUtf8] check before any lines are printed.
|
||||
*
|
||||
* Once the check passes (or [charset] is non-null), this is set to `null` and bytes flow
|
||||
* directly to [buffer].
|
||||
*/
|
||||
private var prefetchBuffer: ByteArrayOutputStream? =
|
||||
if (charset != null) null else ByteArrayOutputStream(128)
|
||||
|
||||
fun writeCount() = writeCount
|
||||
|
||||
fun write(b: Int) {
|
||||
if (writeCount == 0) {
|
||||
// Print a newline before we start printing anything to separate the printed content
|
||||
// from previous content.
|
||||
System.err.println()
|
||||
}
|
||||
|
||||
writeCount++
|
||||
|
||||
if (suppressed) {
|
||||
return
|
||||
}
|
||||
|
||||
val prefetch = prefetchBuffer
|
||||
if (prefetch != null) {
|
||||
prefetch.write(b)
|
||||
// Continue accumulating until we have enough bytes to decide.
|
||||
if (prefetch.size() < PROBABLY_UTF8_BYTE_LIMIT && b != '\n'.code) {
|
||||
return
|
||||
}
|
||||
// We have enough bytes. Check if the content is probably UTF-8.
|
||||
prefetchBuffer = null
|
||||
val bytes = prefetch.toByteArray()
|
||||
if (!isProbablyUtf8(bytes)) {
|
||||
suppressed = true
|
||||
System.err.println("(binary body omitted)")
|
||||
return
|
||||
}
|
||||
// Content looks like UTF-8. Feed the accumulated bytes into the normal buffer.
|
||||
for (byte in bytes) {
|
||||
writeToBuffer(byte.toInt() and 0xFF)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
writeToBuffer(b)
|
||||
}
|
||||
|
||||
private fun writeToBuffer(b: Int) {
|
||||
if (b == '\n'.code) {
|
||||
flush()
|
||||
return
|
||||
}
|
||||
|
||||
buffer.write(b)
|
||||
}
|
||||
|
||||
/** Prints any currently buffered content. */
|
||||
fun flush() {
|
||||
if (suppressed) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we still have a prefetch buffer when flush is called (body was shorter than the
|
||||
// limit), run the check now.
|
||||
val prefetch = prefetchBuffer
|
||||
if (prefetch != null) {
|
||||
prefetchBuffer = null
|
||||
val bytes = prefetch.toByteArray()
|
||||
if (bytes.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (!isProbablyUtf8(bytes)) {
|
||||
suppressed = true
|
||||
System.err.println("(binary body omitted)")
|
||||
return
|
||||
}
|
||||
for (byte in bytes) {
|
||||
writeToBuffer(byte.toInt() and 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.size() == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val line =
|
||||
try {
|
||||
decoder.decode(ByteBuffer.wrap(buffer.toByteArray()))
|
||||
} catch (e: CharacterCodingException) {
|
||||
"(omitted line is not valid $charset)"
|
||||
}
|
||||
buffer.reset()
|
||||
System.err.println(line)
|
||||
}
|
||||
}
|
||||
|
||||
/** The maximum number of code points to sample when checking if content is probably UTF-8. */
|
||||
private const val PROBABLY_UTF8_CODE_POINT_LIMIT = 64
|
||||
|
||||
/**
|
||||
* The maximum number of bytes to accumulate before running the [isProbablyUtf8] check. UTF-8 code
|
||||
* points are at most 4 bytes, so this accommodates [PROBABLY_UTF8_CODE_POINT_LIMIT] code points.
|
||||
*/
|
||||
private const val PROBABLY_UTF8_BYTE_LIMIT = PROBABLY_UTF8_CODE_POINT_LIMIT * 4
|
||||
|
||||
/**
|
||||
* Returns `true` if the given [bytes] probably contain human-readable UTF-8 text.
|
||||
*
|
||||
* Decodes up to [PROBABLY_UTF8_CODE_POINT_LIMIT] code points and returns `false` if any
|
||||
* non-whitespace ISO control characters are found, or if the bytes are not valid UTF-8.
|
||||
*/
|
||||
private fun isProbablyUtf8(bytes: ByteArray): Boolean {
|
||||
try {
|
||||
val decoder =
|
||||
StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||
val charBuffer = decoder.decode(ByteBuffer.wrap(bytes))
|
||||
var codePointCount = 0
|
||||
var i = 0
|
||||
while (i < charBuffer.length && codePointCount < PROBABLY_UTF8_CODE_POINT_LIMIT) {
|
||||
val codePoint = Character.codePointAt(charBuffer, i)
|
||||
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
|
||||
return false
|
||||
}
|
||||
i += Character.charCount(codePoint)
|
||||
codePointCount++
|
||||
}
|
||||
return true
|
||||
} catch (e: CharacterCodingException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the [Charset] in the given [contentType] string, or `null` if unspecified. */
|
||||
private fun parseCharset(contentType: String?): Charset? =
|
||||
contentType
|
||||
?.split(";")
|
||||
?.drop(1)
|
||||
?.map { it.trim() }
|
||||
?.firstOrNull { it.startsWith("charset=", ignoreCase = true) }
|
||||
?.substringAfter("=")
|
||||
?.trim()
|
||||
?.removeSurrounding("\"")
|
||||
?.let { runCatching { charset(it) }.getOrNull() }
|
||||
|
||||
/** Formats the [Duration] into a string like "1m 40s 467ms". */
|
||||
private fun Duration.format(): String =
|
||||
toKotlinDuration().toComponents { days, hours, minutes, seconds, nanoseconds ->
|
||||
buildString {
|
||||
val milliseconds = nanoseconds / 1_000_000
|
||||
if (days > 0) {
|
||||
append("${days}d")
|
||||
}
|
||||
if (hours > 0) {
|
||||
if (isNotEmpty()) {
|
||||
append(" ")
|
||||
}
|
||||
append("${hours}h")
|
||||
}
|
||||
if (minutes > 0) {
|
||||
if (isNotEmpty()) {
|
||||
append(" ")
|
||||
}
|
||||
append("${minutes}m")
|
||||
}
|
||||
if (seconds > 0) {
|
||||
if (isNotEmpty()) {
|
||||
append(" ")
|
||||
}
|
||||
append("${seconds}s")
|
||||
}
|
||||
if (milliseconds > 0) {
|
||||
if (isNotEmpty()) {
|
||||
append(" ")
|
||||
}
|
||||
append("${milliseconds}ms")
|
||||
}
|
||||
|
||||
if (isEmpty()) {
|
||||
append("0s")
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.langchain.smith.core.http
|
||||
|
||||
import java.net.Proxy
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Base64
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication Required`.
|
||||
*
|
||||
* Implementations inspect the 407 [response] (typically its `Proxy-Authenticate` header) and return
|
||||
* the request to retry with a `Proxy-Authorization` header set, or [Optional.empty] to abandon
|
||||
* authentication and surface the 407 to the caller.
|
||||
*
|
||||
* Implementations must be thread-safe; they may be invoked concurrently from multiple HTTP calls.
|
||||
*/
|
||||
fun interface ProxyAuthenticator {
|
||||
|
||||
/**
|
||||
* @param proxy the proxy that produced the challenge, or [Proxy.NO_PROXY] if the route is not
|
||||
* yet established
|
||||
* @param request the request that produced [response]
|
||||
* @param response the 407 challenge response
|
||||
* @return the retry request to send (typically [request] with a `Proxy-Authorization` header
|
||||
* added), or [Optional.empty] to abandon authentication
|
||||
*/
|
||||
fun authenticate(
|
||||
proxy: Proxy,
|
||||
request: HttpRequest,
|
||||
response: HttpResponse,
|
||||
): Optional<HttpRequest>
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the ISO-8859-1
|
||||
* charset.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun basic(username: String, password: String): ProxyAuthenticator =
|
||||
basic(username, password, StandardCharsets.ISO_8859_1)
|
||||
|
||||
/**
|
||||
* A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the given [charset].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun basic(username: String, password: String, charset: Charset): ProxyAuthenticator {
|
||||
val token =
|
||||
Base64.getEncoder().encodeToString("$username:$password".toByteArray(charset))
|
||||
val headerValue = "Basic $token"
|
||||
return ProxyAuthenticator { _, request, _ ->
|
||||
Optional.of(
|
||||
request.toBuilder().putHeader("Proxy-Authorization", headerValue).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,27 +43,7 @@ private constructor(
|
||||
is JsonBoolean -> put(key, value.value.toString())
|
||||
is JsonNumber -> put(key, value.value.toString())
|
||||
is JsonString -> put(key, value.value)
|
||||
is JsonArray ->
|
||||
put(
|
||||
key,
|
||||
value.values
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is JsonMissing,
|
||||
is JsonNull -> null
|
||||
is JsonBoolean -> it.value.toString()
|
||||
is JsonNumber -> it.value.toString()
|
||||
is JsonString -> it.value
|
||||
is JsonArray,
|
||||
is JsonObject ->
|
||||
throw IllegalArgumentException(
|
||||
"Cannot comma separate non-primitives in query params"
|
||||
)
|
||||
}
|
||||
}
|
||||
.joinToString(","),
|
||||
)
|
||||
is JsonArray -> value.values.forEach { put(key, it) }
|
||||
is JsonObject ->
|
||||
value.values.forEach { (nestedKey, value) -> put("$key[$nestedKey]", value) }
|
||||
}
|
||||
|
||||
+15
-18
@@ -1,3 +1,5 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core.http
|
||||
|
||||
import com.langchain.smith.core.DefaultSleeper
|
||||
@@ -31,10 +33,6 @@ private constructor(
|
||||
) : HttpClient {
|
||||
|
||||
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
|
||||
if (!isRetryable(request) || maxRetries <= 0) {
|
||||
return httpClient.execute(request, requestOptions)
|
||||
}
|
||||
|
||||
var modifiedRequest = maybeAddIdempotencyHeader(request)
|
||||
|
||||
// Don't send the current retry count in the headers if the caller set their own value.
|
||||
@@ -48,6 +46,10 @@ private constructor(
|
||||
modifiedRequest = setRetryCountHeader(modifiedRequest, retries)
|
||||
}
|
||||
|
||||
if (!isRetryable(modifiedRequest)) {
|
||||
return httpClient.execute(modifiedRequest, requestOptions)
|
||||
}
|
||||
|
||||
val response =
|
||||
try {
|
||||
val response = httpClient.execute(modifiedRequest, requestOptions)
|
||||
@@ -75,10 +77,6 @@ private constructor(
|
||||
request: HttpRequest,
|
||||
requestOptions: RequestOptions,
|
||||
): CompletableFuture<HttpResponse> {
|
||||
if (!isRetryable(request) || maxRetries <= 0) {
|
||||
return httpClient.executeAsync(request, requestOptions)
|
||||
}
|
||||
|
||||
val modifiedRequest = maybeAddIdempotencyHeader(request)
|
||||
|
||||
// Don't send the current retry count in the headers if the caller set their own value.
|
||||
@@ -94,8 +92,12 @@ private constructor(
|
||||
val requestWithRetryCount =
|
||||
if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request
|
||||
|
||||
return httpClient
|
||||
.executeAsync(requestWithRetryCount, requestOptions)
|
||||
val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions)
|
||||
if (!isRetryable(requestWithRetryCount)) {
|
||||
return responseFuture
|
||||
}
|
||||
|
||||
return responseFuture
|
||||
.handleAsync(
|
||||
fun(
|
||||
response: HttpResponse?,
|
||||
@@ -199,7 +201,7 @@ private constructor(
|
||||
?: headers.values("Retry-After").getOrNull(0)?.let { retryAfter ->
|
||||
retryAfter.toFloatOrNull()?.times(TimeUnit.SECONDS.toNanos(1))
|
||||
?: try {
|
||||
ChronoUnit.MILLIS.between(
|
||||
ChronoUnit.NANOS.between(
|
||||
OffsetDateTime.now(clock),
|
||||
OffsetDateTime.parse(
|
||||
retryAfter,
|
||||
@@ -212,13 +214,8 @@ private constructor(
|
||||
}
|
||||
}
|
||||
?.let { retryAfterNanos ->
|
||||
// If the API asks us to wait a certain amount of time (and it's a reasonable
|
||||
// amount), just
|
||||
// do what it says.
|
||||
val retryAfter = Duration.ofNanos(retryAfterNanos.toLong())
|
||||
if (retryAfter in Duration.ofNanos(0)..Duration.ofMinutes(1)) {
|
||||
return retryAfter
|
||||
}
|
||||
// If the API asks us to wait a certain amount of time, do what it says.
|
||||
return Duration.ofNanos(retryAfterNanos.toLong())
|
||||
}
|
||||
|
||||
// Apply exponential backoff, but not more than the max.
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class BadRequestException
|
||||
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
|
||||
LangChainServiceException("400: $body", cause) {
|
||||
LangChainServiceException(formatMessage(400, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = 400
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ private constructor(
|
||||
private val headers: Headers,
|
||||
private val body: JsonValue,
|
||||
cause: Throwable?,
|
||||
) : LangChainServiceException("$statusCode: $body", cause) {
|
||||
) : LangChainServiceException(formatMessage(statusCode, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = statusCode
|
||||
|
||||
|
||||
+16
@@ -2,6 +2,7 @@
|
||||
|
||||
package com.langchain.smith.errors
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.langchain.smith.core.JsonValue
|
||||
import com.langchain.smith.core.http.Headers
|
||||
|
||||
@@ -14,4 +15,19 @@ protected constructor(message: String, cause: Throwable? = null) :
|
||||
abstract fun headers(): Headers
|
||||
|
||||
abstract fun body(): JsonValue
|
||||
|
||||
companion object {
|
||||
|
||||
/** Formats a status code and [JsonValue] error body into an error message. */
|
||||
@JvmStatic
|
||||
fun formatMessage(statusCode: Int, body: JsonValue): String {
|
||||
val bodyStr =
|
||||
try {
|
||||
ObjectMapper().writeValueAsString(body)
|
||||
} catch (_: Exception) {
|
||||
body.toString()
|
||||
}
|
||||
return "$statusCode: $bodyStr"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class NotFoundException
|
||||
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
|
||||
LangChainServiceException("404: $body", cause) {
|
||||
LangChainServiceException(formatMessage(404, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = 404
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class PermissionDeniedException
|
||||
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
|
||||
LangChainServiceException("403: $body", cause) {
|
||||
LangChainServiceException(formatMessage(403, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = 403
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class RateLimitException
|
||||
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
|
||||
LangChainServiceException("429: $body", cause) {
|
||||
LangChainServiceException(formatMessage(429, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = 429
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class UnauthorizedException
|
||||
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
|
||||
LangChainServiceException("401: $body", cause) {
|
||||
LangChainServiceException(formatMessage(401, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = 401
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ private constructor(
|
||||
private val headers: Headers,
|
||||
private val body: JsonValue,
|
||||
cause: Throwable?,
|
||||
) : LangChainServiceException("$statusCode: $body", cause) {
|
||||
) : LangChainServiceException(formatMessage(statusCode, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = statusCode
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class UnprocessableEntityException
|
||||
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
|
||||
LangChainServiceException("422: $body", cause) {
|
||||
LangChainServiceException(formatMessage(422, body), cause) {
|
||||
|
||||
override fun statusCode(): Int = 422
|
||||
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
|
||||
/** Pairwise score emitted by a [ComparativeEvaluator] for one or more runs. */
|
||||
data class ComparisonEvaluationResult(
|
||||
val key: String,
|
||||
val scores: Map<String, Number>,
|
||||
val comment: String? = null,
|
||||
val sourceRunId: String? = null,
|
||||
)
|
||||
|
||||
/** Compares runs from two or more experiments on the same dataset example. */
|
||||
fun interface ComparativeEvaluator {
|
||||
fun compareRuns(runs: List<RunIngest>, example: Example): ComparisonEvaluationResult
|
||||
}
|
||||
|
||||
/** Creates a [ComparativeEvaluator] from a comparison function. */
|
||||
fun comparativeEvaluator(
|
||||
func: (List<RunIngest>, Example) -> ComparisonEvaluationResult
|
||||
): ComparativeEvaluator = ComparativeEvaluator { runs, example -> func(runs, example) }
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.datasets.comparative.ComparativeCreateResponse
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
|
||||
/** One row from a comparative evaluation for a shared dataset example. */
|
||||
data class ComparativeResultRow(
|
||||
val example: Example,
|
||||
val runs: List<RunIngest>,
|
||||
val evaluationResults: Map<String, ComparisonEvaluationResult>,
|
||||
)
|
||||
|
||||
/** Results returned by [evaluateComparative]. */
|
||||
class ComparativeExperimentResults
|
||||
internal constructor(
|
||||
val rows: List<ComparativeResultRow>,
|
||||
val comparativeExperiment: ComparativeCreateResponse?,
|
||||
val url: String?,
|
||||
) {
|
||||
override fun toString(): String = "ComparativeExperimentResults{rows=${rows.size}, url=$url}"
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
|
||||
/**
|
||||
* Wraps a function as a [RunEvaluator], normalizing common evaluator signatures to `(Run,
|
||||
* Example?)`.
|
||||
*/
|
||||
class DynamicRunEvaluator
|
||||
private constructor(private val evaluator: (RunIngest, Example?) -> EvaluatorOutput) :
|
||||
RunEvaluator {
|
||||
|
||||
override val feedbackKeys: List<String> = emptyList()
|
||||
|
||||
override fun evaluateRun(
|
||||
run: RunIngest,
|
||||
example: Example?,
|
||||
evaluatorRunId: String?,
|
||||
): EvaluationOutput {
|
||||
val sourceRunId = evaluatorRunId ?: newEvaluatorRunId()
|
||||
val raw = evaluator(run, example)
|
||||
return coerceEvaluatorOutput(raw, sourceRunId, defaultKey = "evaluator")
|
||||
}
|
||||
|
||||
override fun toString(): String = "DynamicRunEvaluator{feedbackKeys=$feedbackKeys}"
|
||||
|
||||
companion object {
|
||||
internal fun fromRunExample(
|
||||
func: (RunIngest, Example?) -> EvaluatorOutput
|
||||
): DynamicRunEvaluator = DynamicRunEvaluator(func)
|
||||
|
||||
internal fun fromOutputsReference(
|
||||
func:
|
||||
(outputs: Map<String, Any?>, referenceOutputs: Map<String, Any?>) -> EvaluatorOutput
|
||||
): DynamicRunEvaluator = DynamicRunEvaluator { run, example ->
|
||||
func(runOutputsMap(run), exampleOutputs(example))
|
||||
}
|
||||
|
||||
internal fun fromInputsOutputsReference(
|
||||
func:
|
||||
(
|
||||
inputs: Map<String, Any?>,
|
||||
outputs: Map<String, Any?>,
|
||||
referenceOutputs: Map<String, Any?>,
|
||||
) -> EvaluatorOutput
|
||||
): DynamicRunEvaluator = DynamicRunEvaluator { run, example ->
|
||||
func(exampleInputs(example), runOutputsMap(run), exampleOutputs(example))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a [RunEvaluator] from a function that receives the full [Run] and [Example]. */
|
||||
fun runEvaluator(func: (RunIngest, Example?) -> EvaluatorOutput): RunEvaluator =
|
||||
DynamicRunEvaluator.fromRunExample(func)
|
||||
|
||||
/** Creates a [RunEvaluator] from a function that receives model outputs and reference outputs. */
|
||||
@JvmName("runEvaluatorFromOutputsReference")
|
||||
fun runEvaluator(
|
||||
func: (outputs: Map<String, Any?>, referenceOutputs: Map<String, Any?>) -> EvaluatorOutput
|
||||
): RunEvaluator = DynamicRunEvaluator.fromOutputsReference(func)
|
||||
|
||||
/**
|
||||
* Creates a [RunEvaluator] from a function that receives inputs, outputs, and reference outputs.
|
||||
*/
|
||||
@JvmName("runEvaluatorFromInputsOutputsReference")
|
||||
fun runEvaluator(
|
||||
func:
|
||||
(
|
||||
inputs: Map<String, Any?>,
|
||||
outputs: Map<String, Any?>,
|
||||
referenceOutputs: Map<String, Any?>,
|
||||
) -> EvaluatorOutput
|
||||
): RunEvaluator = DynamicRunEvaluator.fromInputsOutputsReference(func)
|
||||
|
||||
/** We pass the full run and dataset row for evaluators that need trace-level details. */
|
||||
fun interface RunExampleEvaluator {
|
||||
fun evaluate(run: RunIngest, example: Example?): EvaluatorOutput
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a [RunExampleEvaluator] so the evaluate runner can call it like any other [RunEvaluator].
|
||||
*/
|
||||
fun runEvaluator(func: RunExampleEvaluator): RunEvaluator =
|
||||
DynamicRunEvaluator.fromRunExample(func::evaluate)
|
||||
|
||||
/** We pass the app's answer and expected answer for simple match-style evaluators. */
|
||||
fun interface OutputReferenceEvaluator {
|
||||
fun evaluate(outputs: Map<String, Any?>, referenceOutputs: Map<String, Any?>): EvaluatorOutput
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an [OutputReferenceEvaluator] so the evaluate runner can call it like any other
|
||||
* [RunEvaluator].
|
||||
*/
|
||||
@JvmName("runEvaluatorFromOutputReferenceEvaluator")
|
||||
fun runEvaluator(func: OutputReferenceEvaluator): RunEvaluator =
|
||||
DynamicRunEvaluator.fromOutputsReference(func::evaluate)
|
||||
@@ -0,0 +1,165 @@
|
||||
@file:JvmName("Evaluation")
|
||||
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.function.Function
|
||||
|
||||
private val evaluateExecutor =
|
||||
Executors.newCachedThreadPool { runnable ->
|
||||
Thread(runnable, "langsmith-evaluate").apply { isDaemon = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [target] on a dataset, traces each prediction, scores it with [params.evaluators], and
|
||||
* uploads feedback to LangSmith.
|
||||
*
|
||||
* @param blocking When `true` (default), waits for the evaluation to finish before returning. When
|
||||
* `false`, use [evaluateAsync] instead — Java cannot return in-progress [ExperimentResults].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun evaluate(
|
||||
client: LangsmithClient,
|
||||
target: (Map<String, Any?>) -> Any?,
|
||||
params: EvaluateParams,
|
||||
blocking: Boolean = true,
|
||||
): ExperimentResults {
|
||||
if (!blocking) {
|
||||
throw IllegalArgumentException(
|
||||
"Non-blocking evaluation requires evaluateAsync(). " +
|
||||
"Call evaluateAsync(client, target, params) instead."
|
||||
)
|
||||
}
|
||||
return EvaluateRunner(client, params).run(target)
|
||||
}
|
||||
|
||||
/** Java-friendly overload using [Function]. */
|
||||
@JvmOverloads
|
||||
fun evaluate(
|
||||
client: LangsmithClient,
|
||||
target: Function<Map<String, Any?>, Any?>,
|
||||
params: EvaluateParams,
|
||||
blocking: Boolean = true,
|
||||
): ExperimentResults = evaluate(client, { inputs -> target.apply(inputs) }, params, blocking)
|
||||
|
||||
/** Runs an [EvaluateTarget] (Runnable-style) on a dataset. */
|
||||
@JvmOverloads
|
||||
fun evaluate(
|
||||
client: LangsmithClient,
|
||||
target: EvaluateTarget,
|
||||
params: EvaluateParams,
|
||||
blocking: Boolean = true,
|
||||
): ExperimentResults = evaluate(client, target::invoke, params, blocking)
|
||||
|
||||
/**
|
||||
* Runs [target] asynchronously. Equivalent to Python `evaluate(..., blocking=False)` — call
|
||||
* [CompletableFuture.join] or [CompletableFuture.get] when results are needed.
|
||||
*/
|
||||
fun evaluateAsync(
|
||||
client: LangsmithClient,
|
||||
target: (Map<String, Any?>) -> Any?,
|
||||
params: EvaluateParams,
|
||||
): CompletableFuture<ExperimentResults> =
|
||||
CompletableFuture.supplyAsync({ EvaluateRunner(client, params).run(target) }, evaluateExecutor)
|
||||
|
||||
/** Async overload for [EvaluateTarget]. */
|
||||
fun evaluateAsync(
|
||||
client: LangsmithClient,
|
||||
target: EvaluateTarget,
|
||||
params: EvaluateParams,
|
||||
): CompletableFuture<ExperimentResults> = evaluateAsync(client, target::invoke, params)
|
||||
|
||||
/** Async overload for Java [Function]. */
|
||||
fun evaluateAsync(
|
||||
client: LangsmithClient,
|
||||
target: Function<Map<String, Any?>, Any?>,
|
||||
params: EvaluateParams,
|
||||
): CompletableFuture<ExperimentResults> =
|
||||
evaluateAsync(client, { inputs -> target.apply(inputs) }, params)
|
||||
|
||||
/**
|
||||
* Evaluates an existing experiment's runs without re-running the target. Mirrors Python
|
||||
* `evaluate_existing` and `evaluate(existing_experiment_id, ...)`.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun evaluateExisting(
|
||||
client: LangsmithClient,
|
||||
experimentId: String,
|
||||
params: EvaluateExistingParams,
|
||||
blocking: Boolean = true,
|
||||
): ExperimentResults {
|
||||
if (!blocking) {
|
||||
throw IllegalArgumentException(
|
||||
"Non-blocking evaluation requires evaluateExistingAsync(). " +
|
||||
"Call evaluateExistingAsync(client, experimentId, params) instead."
|
||||
)
|
||||
}
|
||||
return EvaluateExistingRunner(client, experimentId, params).run()
|
||||
}
|
||||
|
||||
fun evaluateExistingAsync(
|
||||
client: LangsmithClient,
|
||||
experimentId: String,
|
||||
params: EvaluateExistingParams,
|
||||
): CompletableFuture<ExperimentResults> =
|
||||
CompletableFuture.supplyAsync(
|
||||
{ EvaluateExistingRunner(client, experimentId, params).run() },
|
||||
evaluateExecutor,
|
||||
)
|
||||
|
||||
/**
|
||||
* Compares two existing experiments with pairwise evaluators. Mirrors Python `evaluate_comparative`
|
||||
* and `evaluate((experiment_a, experiment_b), ...)`.
|
||||
*/
|
||||
fun evaluateComparative(
|
||||
client: LangsmithClient,
|
||||
experiments: Pair<String, String>,
|
||||
params: EvaluateComparativeParams,
|
||||
): ComparativeExperimentResults = EvaluateComparativeRunner(client, experiments, params).run()
|
||||
|
||||
/** Convenience overload accepting experiment ids as a list. */
|
||||
fun evaluateComparative(
|
||||
client: LangsmithClient,
|
||||
experiments: List<String>,
|
||||
params: EvaluateComparativeParams,
|
||||
): ComparativeExperimentResults {
|
||||
require(experiments.size == 2) {
|
||||
"Comparative evaluation requires exactly 2 experiments, got ${experiments.size}"
|
||||
}
|
||||
return evaluateComparative(client, experiments[0] to experiments[1], params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-scores an existing experiment using evaluators from [EvaluateParams]. Convenience wrapper
|
||||
* around [evaluateExisting] for callers that already hold [EvaluateParams].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun evaluate(
|
||||
client: LangsmithClient,
|
||||
experimentId: String,
|
||||
params: EvaluateParams,
|
||||
blocking: Boolean = true,
|
||||
): ExperimentResults =
|
||||
evaluateExisting(
|
||||
client,
|
||||
experimentId,
|
||||
EvaluateExistingParams.builder()
|
||||
.evaluators(params.evaluators)
|
||||
.summaryEvaluators(params.summaryEvaluators)
|
||||
.metadata(params.metadata)
|
||||
.maxConcurrency(params.maxConcurrency)
|
||||
.build(),
|
||||
blocking,
|
||||
)
|
||||
|
||||
/**
|
||||
* Compares two existing experiments. Convenience wrapper when callers pass a pair of session ids as
|
||||
* the evaluation target (Python tuple-target style).
|
||||
*/
|
||||
fun evaluate(
|
||||
client: LangsmithClient,
|
||||
experiments: Pair<String, String>,
|
||||
params: EvaluateComparativeParams,
|
||||
): ComparativeExperimentResults = evaluateComparative(client, experiments, params)
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
/** Configuration for [evaluateComparative]. */
|
||||
class EvaluateComparativeParams
|
||||
private constructor(
|
||||
val evaluators: List<ComparativeEvaluator>,
|
||||
val experimentPrefix: String?,
|
||||
val description: String?,
|
||||
val metadata: Map<String, Any>,
|
||||
val maxConcurrency: Int,
|
||||
val loadNested: Boolean,
|
||||
val randomizeOrder: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic fun builder() = Builder()
|
||||
}
|
||||
|
||||
class Builder internal constructor() {
|
||||
private var evaluators: MutableList<ComparativeEvaluator> = mutableListOf()
|
||||
private var experimentPrefix: String? = null
|
||||
private var description: String? = null
|
||||
private var metadata: Map<String, Any> = emptyMap()
|
||||
private var maxConcurrency: Int = 5
|
||||
private var loadNested: Boolean = false
|
||||
private var randomizeOrder: Boolean = false
|
||||
|
||||
fun addEvaluator(evaluator: ComparativeEvaluator) = apply { evaluators.add(evaluator) }
|
||||
|
||||
fun evaluators(evaluators: List<ComparativeEvaluator>) = apply {
|
||||
this.evaluators = evaluators.toMutableList()
|
||||
}
|
||||
|
||||
fun experimentPrefix(experimentPrefix: String) = apply {
|
||||
this.experimentPrefix = experimentPrefix
|
||||
}
|
||||
|
||||
fun description(description: String) = apply { this.description = description }
|
||||
|
||||
fun metadata(metadata: Map<String, Any>) = apply { this.metadata = metadata }
|
||||
|
||||
fun maxConcurrency(maxConcurrency: Int) = apply {
|
||||
require(maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
|
||||
this.maxConcurrency = maxConcurrency
|
||||
}
|
||||
|
||||
fun loadNested(loadNested: Boolean) = apply { this.loadNested = loadNested }
|
||||
|
||||
fun randomizeOrder(randomizeOrder: Boolean) = apply { this.randomizeOrder = randomizeOrder }
|
||||
|
||||
fun build(): EvaluateComparativeParams {
|
||||
require(evaluators.isNotEmpty()) { "at least one comparative evaluator is required" }
|
||||
return EvaluateComparativeParams(
|
||||
evaluators = evaluators.toList(),
|
||||
experimentPrefix = experimentPrefix,
|
||||
description = description,
|
||||
metadata = metadata,
|
||||
maxConcurrency = maxConcurrency,
|
||||
loadNested = loadNested,
|
||||
randomizeOrder = randomizeOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.core.JsonValue
|
||||
import com.langchain.smith.models.datasets.comparative.ComparativeCreateParams
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import com.langchain.smith.models.sessions.TracerSession
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
/** Runs pairwise evaluators over two existing experiments. */
|
||||
internal class EvaluateComparativeRunner(
|
||||
private val client: LangsmithClient,
|
||||
private val experimentIds: Pair<String, String>,
|
||||
private val params: EvaluateComparativeParams,
|
||||
) {
|
||||
private val logger = Logger.getLogger(EvaluateComparativeRunner::class.java.name)
|
||||
|
||||
fun run(): ComparativeExperimentResults {
|
||||
require(params.maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
|
||||
|
||||
val projects =
|
||||
listOf(
|
||||
loadExperiment(client, experimentIds.first),
|
||||
loadExperiment(client, experimentIds.second),
|
||||
)
|
||||
|
||||
val datasetIds =
|
||||
projects.map { project ->
|
||||
project.referenceDatasetId().orElseThrow {
|
||||
IllegalStateException("Experiment ${project.id()} has no reference dataset")
|
||||
}
|
||||
}
|
||||
require(datasetIds.distinct().size == 1) {
|
||||
"All experiments must have the same reference dataset"
|
||||
}
|
||||
val datasetId = datasetIds.first()
|
||||
|
||||
val experimentName = resolveComparativeName(projects)
|
||||
val comparativeExperimentId = UUID.randomUUID().toString()
|
||||
val comparativeExperiment =
|
||||
client
|
||||
.datasets()
|
||||
.comparative()
|
||||
.create(
|
||||
ComparativeCreateParams.builder()
|
||||
.id(comparativeExperimentId)
|
||||
.name(experimentName)
|
||||
.description(params.description)
|
||||
.experimentIds(projects.map { it.id() })
|
||||
.referenceDatasetId(datasetId)
|
||||
.apply {
|
||||
if (params.metadata.isNotEmpty()) {
|
||||
val extraBuilder = ComparativeCreateParams.Extra.builder()
|
||||
params.metadata.forEach { (key, value) ->
|
||||
extraBuilder.putAdditionalProperty(key, JsonValue.from(value))
|
||||
}
|
||||
extra(extraBuilder.build())
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
val runsByExperiment =
|
||||
projects.map { project ->
|
||||
loadRunsForExperiment(client, project.id(), params.loadNested)
|
||||
}
|
||||
|
||||
val exampleIntersection =
|
||||
runsByExperiment
|
||||
.map { runs ->
|
||||
runs.mapNotNull { run -> run.referenceExampleId().getOrNull() }.toSet()
|
||||
}
|
||||
.reduce { acc, ids -> acc.intersect(ids) }
|
||||
|
||||
val examplesById =
|
||||
loadExamplesByIds(
|
||||
client,
|
||||
datasetId,
|
||||
exampleIntersection.toList(),
|
||||
experimentDatasetVersion(projects.first()),
|
||||
)
|
||||
|
||||
val runsByExample = linkedMapOf<String, MutableList<RunIngest>>()
|
||||
runsByExperiment.forEach { runs ->
|
||||
runs.forEach { run ->
|
||||
val exampleId = run.referenceExampleId().getOrNull() ?: return@forEach
|
||||
if (exampleId !in exampleIntersection) {
|
||||
return@forEach
|
||||
}
|
||||
runsByExample.getOrPut(exampleId) { mutableListOf() }.add(run)
|
||||
}
|
||||
}
|
||||
|
||||
val rows = compareExamples(runsByExample, examplesById, comparativeExperiment.id())
|
||||
val url = buildComparativeUrl(projects, comparativeExperiment.id(), datasetId)
|
||||
|
||||
return ComparativeExperimentResults(
|
||||
rows = rows,
|
||||
comparativeExperiment = comparativeExperiment,
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveComparativeName(projects: List<TracerSession>): String {
|
||||
params.experimentPrefix?.let { prefix ->
|
||||
return "$prefix-${UUID.randomUUID().toString().take(8)}"
|
||||
}
|
||||
val names = projects.mapNotNull { it.name().orElse(null) }
|
||||
return if (names.size == projects.size) {
|
||||
names.joinToString(" vs. ") + "-" + UUID.randomUUID().toString().take(4)
|
||||
} else {
|
||||
"comparative-${UUID.randomUUID().toString().take(8)}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareExamples(
|
||||
runsByExample: Map<String, List<RunIngest>>,
|
||||
examplesById: Map<String, Example>,
|
||||
comparativeExperimentId: String,
|
||||
): List<ComparativeResultRow> {
|
||||
if (params.maxConcurrency == 0 || runsByExample.size <= 1) {
|
||||
return runsByExample.mapNotNull { (exampleId, runs) ->
|
||||
val example = examplesById[exampleId] ?: return@mapNotNull null
|
||||
compareExample(example, runs, comparativeExperimentId)
|
||||
}
|
||||
}
|
||||
|
||||
val poolSize = minOf(params.maxConcurrency, runsByExample.size)
|
||||
val executor = Executors.newFixedThreadPool(poolSize)
|
||||
return try {
|
||||
runsByExample
|
||||
.map { (exampleId, runs) ->
|
||||
executor.submit(
|
||||
Callable {
|
||||
val example = examplesById[exampleId] ?: return@Callable null
|
||||
compareExample(example, runs, comparativeExperimentId)
|
||||
}
|
||||
)
|
||||
}
|
||||
.mapNotNull { it.get() }
|
||||
} finally {
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareExample(
|
||||
example: Example,
|
||||
runs: List<RunIngest>,
|
||||
comparativeExperimentId: String,
|
||||
): ComparativeResultRow {
|
||||
val orderedRuns =
|
||||
if (params.randomizeOrder) {
|
||||
runs.shuffled()
|
||||
} else {
|
||||
runs
|
||||
}
|
||||
|
||||
val evaluationResults = linkedMapOf<String, ComparisonEvaluationResult>()
|
||||
for (evaluator in params.evaluators) {
|
||||
try {
|
||||
val result = evaluator.compareRuns(orderedRuns, example)
|
||||
evaluationResults["feedback.${result.key}"] = result
|
||||
val feedbackGroupId = UUID.randomUUID().toString()
|
||||
logComparativeEvaluationFeedback(
|
||||
client,
|
||||
result,
|
||||
orderedRuns,
|
||||
comparativeExperimentId,
|
||||
feedbackGroupId,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.log(
|
||||
Level.SEVERE,
|
||||
"Error running comparative evaluator $evaluator on example ${example.id()}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return ComparativeResultRow(
|
||||
example = example,
|
||||
runs = orderedRuns,
|
||||
evaluationResults = evaluationResults,
|
||||
)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
/** How to handle errors when the target function fails on a dataset row. */
|
||||
enum class EvaluateErrorHandling {
|
||||
/** Log the error and include the run in the experiment (default). */
|
||||
LOG,
|
||||
|
||||
/** Log the error and omit the run from the experiment. */
|
||||
IGNORE,
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
/** Configuration for [evaluateExisting]. */
|
||||
class EvaluateExistingParams
|
||||
private constructor(
|
||||
val evaluators: List<RunEvaluator>,
|
||||
val summaryEvaluators: List<SummaryEvaluator>,
|
||||
val metadata: Map<String, Any>,
|
||||
val maxConcurrency: Int,
|
||||
val loadNested: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic fun builder() = Builder()
|
||||
}
|
||||
|
||||
class Builder internal constructor() {
|
||||
private var evaluators: MutableList<RunEvaluator> = mutableListOf()
|
||||
private var summaryEvaluators: MutableList<SummaryEvaluator> = mutableListOf()
|
||||
private var metadata: Map<String, Any> = emptyMap()
|
||||
private var maxConcurrency: Int = 0
|
||||
private var loadNested: Boolean = false
|
||||
|
||||
fun addEvaluator(evaluator: RunEvaluator) = apply { evaluators.add(evaluator) }
|
||||
|
||||
fun evaluators(evaluators: List<RunEvaluator>) = apply {
|
||||
this.evaluators = evaluators.toMutableList()
|
||||
}
|
||||
|
||||
fun addSummaryEvaluator(evaluator: SummaryEvaluator) = apply {
|
||||
summaryEvaluators.add(evaluator)
|
||||
}
|
||||
|
||||
fun summaryEvaluators(evaluators: List<SummaryEvaluator>) = apply {
|
||||
summaryEvaluators = evaluators.toMutableList()
|
||||
}
|
||||
|
||||
fun metadata(metadata: Map<String, Any>) = apply { this.metadata = metadata }
|
||||
|
||||
fun maxConcurrency(maxConcurrency: Int) = apply {
|
||||
require(maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
|
||||
this.maxConcurrency = maxConcurrency
|
||||
}
|
||||
|
||||
fun loadNested(loadNested: Boolean) = apply { this.loadNested = loadNested }
|
||||
|
||||
fun build(): EvaluateExistingParams {
|
||||
require(evaluators.isNotEmpty() || summaryEvaluators.isNotEmpty()) {
|
||||
"at least one evaluator or summary evaluator is required"
|
||||
}
|
||||
return EvaluateExistingParams(
|
||||
evaluators = evaluators.toList(),
|
||||
summaryEvaluators = summaryEvaluators.toList(),
|
||||
metadata = metadata,
|
||||
maxConcurrency = maxConcurrency,
|
||||
loadNested = loadNested,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
/** Scores existing experiment runs without re-running the target. */
|
||||
internal class EvaluateExistingRunner(
|
||||
private val client: LangsmithClient,
|
||||
private val experimentId: String,
|
||||
private val params: EvaluateExistingParams,
|
||||
) {
|
||||
private val logger = Logger.getLogger(EvaluateExistingRunner::class.java.name)
|
||||
|
||||
fun run(): ExperimentResults {
|
||||
require(params.maxConcurrency >= 0) { "maxConcurrency must be >= 0 (0 = sequential)" }
|
||||
|
||||
val session = loadExperiment(client, experimentId)
|
||||
val experimentName = session.name().orElse(experimentId)
|
||||
val datasetId =
|
||||
session.referenceDatasetId().orElseThrow {
|
||||
IllegalStateException("Experiment $experimentId has no reference dataset")
|
||||
}
|
||||
|
||||
val runs = loadRunsForExperiment(client, experimentId, params.loadNested)
|
||||
require(runs.isNotEmpty()) { "No runs found for experiment $experimentId" }
|
||||
|
||||
val exampleIds = runs.mapNotNull { run -> run.referenceExampleId().getOrNull() }.distinct()
|
||||
val examplesById =
|
||||
loadExamplesByIds(client, datasetId, exampleIds, experimentDatasetVersion(session))
|
||||
|
||||
val rows = scoreRuns(runs, examplesById)
|
||||
val summaryResults = applySummaryEvaluators(rows, experimentId)
|
||||
val url = buildExperimentUrl(session, datasetId)
|
||||
|
||||
return ExperimentResults(
|
||||
experimentName = experimentName,
|
||||
experimentId = experimentId,
|
||||
datasetId = datasetId,
|
||||
rows = rows,
|
||||
summaryResults = summaryResults,
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
|
||||
private fun scoreRuns(
|
||||
runs: List<RunIngest>,
|
||||
examplesById: Map<String, Example>,
|
||||
): List<ExperimentResultRow> {
|
||||
if (params.maxConcurrency == 0 || runs.size <= 1) {
|
||||
return runs.mapNotNull { run -> scoreRun(run, examplesById) }
|
||||
}
|
||||
|
||||
val poolSize = minOf(params.maxConcurrency, runs.size)
|
||||
val executor = Executors.newFixedThreadPool(poolSize)
|
||||
return try {
|
||||
runs
|
||||
.map { run -> executor.submit(Callable { scoreRun(run, examplesById) }) }
|
||||
.mapNotNull { it.get() }
|
||||
} finally {
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scoreRun(run: RunIngest, examplesById: Map<String, Example>): ExperimentResultRow? {
|
||||
val exampleId = run.referenceExampleId().getOrNull() ?: return null
|
||||
val example = examplesById[exampleId] ?: return null
|
||||
|
||||
val evaluationResults =
|
||||
params.evaluators.flatMap { evaluator -> runEvaluator(evaluator, run, example) }
|
||||
|
||||
return ExperimentResultRow(
|
||||
run = run,
|
||||
example = example,
|
||||
evaluationResults = EvaluationResults(evaluationResults),
|
||||
)
|
||||
}
|
||||
|
||||
private fun runEvaluator(
|
||||
evaluator: RunEvaluator,
|
||||
run: RunIngest,
|
||||
example: Example,
|
||||
): List<EvaluationResult> {
|
||||
val evaluatorRunId = newEvaluatorRunId()
|
||||
return try {
|
||||
val response = evaluator.evaluateRun(run, example, evaluatorRunId)
|
||||
logEvaluationFeedback(client, response, run)
|
||||
selectEvaluationResults(response)
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Error running evaluator $evaluator on run ${run.id()}", e)
|
||||
val keys = evaluator.feedbackKeys
|
||||
if (keys.isEmpty()) {
|
||||
throw e
|
||||
}
|
||||
val errorResponse =
|
||||
EvaluationResults(
|
||||
keys.map { key ->
|
||||
EvaluationResult(
|
||||
key = key,
|
||||
sourceRunId = evaluatorRunId,
|
||||
comment = e.toString(),
|
||||
extra = mapOf("error" to true),
|
||||
)
|
||||
}
|
||||
)
|
||||
logEvaluationFeedback(client, errorResponse, run)
|
||||
errorResponse.results
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySummaryEvaluators(
|
||||
rows: List<ExperimentResultRow>,
|
||||
sessionId: String,
|
||||
): EvaluationResults {
|
||||
if (params.summaryEvaluators.isEmpty()) {
|
||||
return EvaluationResults(emptyList())
|
||||
}
|
||||
|
||||
val runs = rows.map { it.run }
|
||||
val examples = rows.map { it.example }
|
||||
val aggregateResults = mutableListOf<EvaluationResult>()
|
||||
|
||||
for (evaluator in params.summaryEvaluators) {
|
||||
try {
|
||||
val response = evaluator.evaluate(runs, examples)
|
||||
aggregateResults.addAll(selectEvaluationResults(response))
|
||||
logSummaryEvaluationFeedback(client, response, sessionId)
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Error running summary evaluator $evaluator", e)
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationResults(aggregateResults)
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.core.JsonValue
|
||||
import com.langchain.smith.models.feedback.FeedbackCreateSchema
|
||||
import com.langchain.smith.models.feedback.ModelFeedbackSource
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
/** Turns an evaluator response into a flat list of [EvaluationResult]. */
|
||||
internal fun selectEvaluationResults(output: EvaluationOutput): List<EvaluationResult> =
|
||||
when (output) {
|
||||
is EvaluationResult -> listOf(output)
|
||||
is EvaluationResults -> output.results
|
||||
}
|
||||
|
||||
/** Posts evaluator feedback for a prediction run. */
|
||||
internal fun logEvaluationFeedback(
|
||||
client: LangsmithClient,
|
||||
evaluatorResponse: EvaluationOutput,
|
||||
run: RunIngest,
|
||||
) {
|
||||
for (result in selectEvaluationResults(evaluatorResponse)) {
|
||||
client.feedback().create(buildFeedbackCreateSchema(result, run))
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFeedbackCreateSchema(
|
||||
result: EvaluationResult,
|
||||
run: RunIngest,
|
||||
): FeedbackCreateSchema {
|
||||
val builder = FeedbackCreateSchema.builder().key(result.key)
|
||||
|
||||
val runId = result.targetRunId ?: run.id().getOrNull()
|
||||
if (runId != null) {
|
||||
builder.runId(runId)
|
||||
}
|
||||
|
||||
builder.feedbackSource(
|
||||
ModelFeedbackSource.builder().type(ModelFeedbackSource.Type.MODEL).build()
|
||||
)
|
||||
|
||||
val score = result.score
|
||||
if (score != null) {
|
||||
builder.score(score.toDouble())
|
||||
}
|
||||
|
||||
applyFeedbackValue(builder, result.value)
|
||||
|
||||
val comment = result.comment
|
||||
if (comment != null) {
|
||||
builder.comment(comment)
|
||||
}
|
||||
|
||||
val feedbackConfig = result.feedbackConfig
|
||||
if (feedbackConfig != null) {
|
||||
builder.feedbackConfig(feedbackConfig)
|
||||
}
|
||||
|
||||
run.traceId().getOrNull()?.let { builder.traceId(it) }
|
||||
run.sessionId().getOrNull()?.let { builder.sessionId(it) }
|
||||
run.startTime().getOrNull()?.let { startTime ->
|
||||
runStartTime(startTime)?.let { builder.startTime(it) }
|
||||
}
|
||||
|
||||
val correction = result.correction
|
||||
if (correction != null) {
|
||||
builder.correction(
|
||||
FeedbackCreateSchema.Correction.ofUnionMember0(
|
||||
FeedbackCreateSchema.Correction.UnionMember0.builder()
|
||||
.putAllAdditionalProperties(
|
||||
correction.mapValues { (_, value) -> JsonValue.from(value) }
|
||||
)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val error = result.extra?.get("error") as? Boolean
|
||||
if (error == true) {
|
||||
builder.error(true)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
internal fun applyFeedbackValue(builder: FeedbackCreateSchema.Builder, value: Any?) {
|
||||
when (value) {
|
||||
null -> Unit
|
||||
is Number ->
|
||||
builder.value(
|
||||
when (value) {
|
||||
is Double -> value
|
||||
is Float -> value.toDouble()
|
||||
else -> value.toDouble()
|
||||
}
|
||||
)
|
||||
is Boolean -> builder.value(value)
|
||||
is String -> builder.value(value)
|
||||
is Map<*, *> -> {
|
||||
@Suppress("UNCHECKED_CAST") val stringMap = value as Map<String, Any?>
|
||||
builder.value(
|
||||
FeedbackCreateSchema.Value.ofUnionMember3(
|
||||
FeedbackCreateSchema.Value.UnionMember3.builder()
|
||||
.putAllAdditionalProperties(
|
||||
stringMap.mapValues { (_, v) -> JsonValue.from(v) }
|
||||
)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> builder.value(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun runStartTime(startTime: String): OffsetDateTime? =
|
||||
try {
|
||||
OffsetDateTime.parse(startTime)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.examples.Example
|
||||
|
||||
/** Configuration for [evaluate]. */
|
||||
class EvaluateParams
|
||||
private constructor(
|
||||
internal val data: EvaluateData,
|
||||
val evaluators: List<RunEvaluator>,
|
||||
val summaryEvaluators: List<SummaryEvaluator>,
|
||||
val experimentPrefix: String,
|
||||
val experimentName: String?,
|
||||
val experimentId: String?,
|
||||
val description: String?,
|
||||
val metadata: Map<String, Any>,
|
||||
val maxConcurrency: Int,
|
||||
val numRepetitions: Int,
|
||||
val uploadResults: Boolean,
|
||||
val errorHandling: EvaluateErrorHandling,
|
||||
) {
|
||||
companion object {
|
||||
/** Creates a new [Builder]. */
|
||||
@JvmStatic fun builder() = Builder()
|
||||
}
|
||||
|
||||
/** Builds [EvaluateParams] with sensible defaults. */
|
||||
class Builder internal constructor() {
|
||||
private var data: EvaluateData? = null
|
||||
private var evaluators: MutableList<RunEvaluator> = mutableListOf()
|
||||
private var summaryEvaluators: MutableList<SummaryEvaluator> = mutableListOf()
|
||||
private var experimentPrefix: String = DEFAULT_EXPERIMENT_PREFIX
|
||||
private var experimentName: String? = null
|
||||
private var experimentId: String? = null
|
||||
private var description: String? = null
|
||||
private var metadata: Map<String, Any> = emptyMap()
|
||||
private var maxConcurrency: Int = 0
|
||||
private var numRepetitions: Int = 1
|
||||
private var uploadResults: Boolean = true
|
||||
private var errorHandling: EvaluateErrorHandling = EvaluateErrorHandling.LOG
|
||||
|
||||
/** Dataset name or ID whose examples will be evaluated. */
|
||||
fun data(datasetNameOrId: String) = apply {
|
||||
require(datasetNameOrId.isNotBlank()) { "datasetNameOrId must not be blank" }
|
||||
data = EvaluateData.Dataset(datasetNameOrId)
|
||||
}
|
||||
|
||||
/** In-memory examples to evaluate (for example after creating a dataset locally). */
|
||||
fun data(examples: List<Example>) = apply {
|
||||
require(examples.isNotEmpty()) { "examples must not be empty" }
|
||||
data = EvaluateData.Examples(examples)
|
||||
}
|
||||
|
||||
fun addEvaluator(evaluator: RunEvaluator) = apply { evaluators.add(evaluator) }
|
||||
|
||||
fun evaluators(evaluators: List<RunEvaluator>) = apply {
|
||||
this.evaluators = evaluators.toMutableList()
|
||||
}
|
||||
|
||||
fun addSummaryEvaluator(evaluator: SummaryEvaluator) = apply {
|
||||
summaryEvaluators.add(evaluator)
|
||||
}
|
||||
|
||||
fun summaryEvaluators(evaluators: List<SummaryEvaluator>) = apply {
|
||||
summaryEvaluators = evaluators.toMutableList()
|
||||
}
|
||||
|
||||
/** Prefix used when generating a unique experiment name. */
|
||||
fun experimentPrefix(experimentPrefix: String) = apply {
|
||||
this.experimentPrefix = experimentPrefix
|
||||
}
|
||||
|
||||
/** Explicit experiment name; overrides [experimentPrefix] when set. */
|
||||
fun experimentName(experimentName: String) = apply { this.experimentName = experimentName }
|
||||
|
||||
/**
|
||||
* Existing experiment session id to extend with additional runs. When set, a new session is
|
||||
* not created.
|
||||
*/
|
||||
fun experimentId(experimentId: String) = apply { this.experimentId = experimentId }
|
||||
|
||||
fun description(description: String) = apply { this.description = description }
|
||||
|
||||
fun metadata(metadata: Map<String, Any>) = apply { this.metadata = metadata }
|
||||
|
||||
/**
|
||||
* Maximum number of examples to evaluate concurrently. `0` runs sequentially. Values
|
||||
* greater than `0` use a fixed thread pool of that size.
|
||||
*/
|
||||
fun maxConcurrency(maxConcurrency: Int) = apply {
|
||||
require(maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
|
||||
this.maxConcurrency = maxConcurrency
|
||||
}
|
||||
|
||||
/** Number of times to evaluate each dataset example. */
|
||||
fun numRepetitions(numRepetitions: Int) = apply {
|
||||
require(numRepetitions >= 1) { "numRepetitions must be >= 1" }
|
||||
this.numRepetitions = numRepetitions
|
||||
}
|
||||
|
||||
/** When false, runs and feedback are kept local and not uploaded to LangSmith. */
|
||||
fun uploadResults(uploadResults: Boolean) = apply { this.uploadResults = uploadResults }
|
||||
|
||||
fun errorHandling(errorHandling: EvaluateErrorHandling) = apply {
|
||||
this.errorHandling = errorHandling
|
||||
}
|
||||
|
||||
fun build(): EvaluateParams {
|
||||
val resolvedData =
|
||||
data
|
||||
?: throw IllegalArgumentException(
|
||||
"data is required (dataset name/id or examples)"
|
||||
)
|
||||
require(evaluators.isNotEmpty() || summaryEvaluators.isNotEmpty()) {
|
||||
"at least one evaluator or summary evaluator is required"
|
||||
}
|
||||
require(experimentId == null || experimentName == null) {
|
||||
"experimentId and experimentName cannot both be set"
|
||||
}
|
||||
return EvaluateParams(
|
||||
data = resolvedData,
|
||||
evaluators = evaluators.toList(),
|
||||
summaryEvaluators = summaryEvaluators.toList(),
|
||||
experimentPrefix = experimentPrefix,
|
||||
experimentName = experimentName,
|
||||
experimentId = experimentId,
|
||||
description = description,
|
||||
metadata = metadata,
|
||||
maxConcurrency = maxConcurrency,
|
||||
numRepetitions = numRepetitions,
|
||||
uploadResults = uploadResults,
|
||||
errorHandling = errorHandling,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class EvaluateData {
|
||||
data class Dataset(val identifier: String) : EvaluateData()
|
||||
|
||||
data class Examples(val examples: List<Example>) : EvaluateData()
|
||||
}
|
||||
|
||||
internal const val DEFAULT_EXPERIMENT_PREFIX = "experiment"
|
||||
@@ -0,0 +1,374 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.core.JsonValue
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import com.langchain.smith.models.sessions.SessionCreateParams
|
||||
import com.langchain.smith.models.sessions.SessionUpdateParams
|
||||
import com.langchain.smith.models.sessions.TracerSessionWithoutVirtualFields
|
||||
import com.langchain.smith.tracing.RunTree
|
||||
import com.langchain.smith.tracing.TraceConfig
|
||||
import com.langchain.smith.tracing.getCurrentRunTree
|
||||
import com.langchain.smith.tracing.traceable
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
/** Runs your app on each dataset row, scores the results, and records everything in LangSmith. */
|
||||
internal class EvaluateRunner(
|
||||
private val client: LangsmithClient,
|
||||
private val params: EvaluateParams,
|
||||
) {
|
||||
private val logger = Logger.getLogger(EvaluateRunner::class.java.name)
|
||||
|
||||
/** End-to-end evaluate flow: load rows, open an experiment, run and score each one. */
|
||||
fun run(target: (Map<String, Any?>) -> Any?): ExperimentResults {
|
||||
require(params.maxConcurrency >= 0) { "maxConcurrency must be >= 0 (0 = sequential)" }
|
||||
|
||||
val examples = expandExamples(loadExamples(), params.numRepetitions)
|
||||
require(examples.isNotEmpty()) { "No examples found to evaluate" }
|
||||
|
||||
val datasetId = examples.first().datasetId()
|
||||
val sessionInfo = resolveExperimentSession(examples.size, datasetId)
|
||||
val experimentName = sessionInfo.first
|
||||
val sessionId = sessionInfo.second
|
||||
|
||||
val rows =
|
||||
evaluateExamples(
|
||||
target = target,
|
||||
examples = examples,
|
||||
experimentName = experimentName,
|
||||
sessionId = sessionId,
|
||||
)
|
||||
|
||||
val summaryResults = applySummaryEvaluators(rows, sessionId)
|
||||
|
||||
if (params.uploadResults && sessionId != null) {
|
||||
updateExperimentMetadata(sessionId, examples)
|
||||
}
|
||||
|
||||
// LangSmith UI URLs are not exposed on the Java session schema yet.
|
||||
val url: String? = null
|
||||
|
||||
return ExperimentResults(
|
||||
experimentName = experimentName,
|
||||
experimentId = sessionId,
|
||||
datasetId = datasetId,
|
||||
rows = rows,
|
||||
summaryResults = summaryResults,
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveExperimentSession(
|
||||
numExamples: Int,
|
||||
datasetId: String,
|
||||
): Pair<String, String?> {
|
||||
params.experimentId?.let { existingId ->
|
||||
val session = loadExperiment(client, existingId)
|
||||
val name = session.name().orElse(existingId)
|
||||
return name to existingId
|
||||
}
|
||||
|
||||
val experimentName =
|
||||
params.experimentName ?: generateExperimentName(params.experimentPrefix)
|
||||
|
||||
if (!params.uploadResults) {
|
||||
return experimentName to null
|
||||
}
|
||||
|
||||
val session = createExperimentSession(experimentName, datasetId, numExamples)
|
||||
return experimentName to session.id()
|
||||
}
|
||||
|
||||
private fun evaluateExamples(
|
||||
target: (Map<String, Any?>) -> Any?,
|
||||
examples: List<Example>,
|
||||
experimentName: String,
|
||||
sessionId: String?,
|
||||
): List<ExperimentResultRow> {
|
||||
if (params.maxConcurrency == 0 || examples.size <= 1) {
|
||||
return examples.mapNotNull { example ->
|
||||
evaluateExample(
|
||||
target = target,
|
||||
example = example,
|
||||
experimentName = experimentName,
|
||||
sessionId = sessionId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val poolSize = minOf(params.maxConcurrency, examples.size)
|
||||
val executor = Executors.newFixedThreadPool(poolSize)
|
||||
return try {
|
||||
val futures =
|
||||
examples.map { example ->
|
||||
executor.submit(
|
||||
Callable {
|
||||
evaluateExample(
|
||||
target = target,
|
||||
example = example,
|
||||
experimentName = experimentName,
|
||||
sessionId = sessionId,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
futures.mapNotNull { it.get() }
|
||||
} finally {
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadExamples(): List<Example> =
|
||||
when (val data = params.data) {
|
||||
is EvaluateData.Dataset -> listExamples(client, data.identifier)
|
||||
is EvaluateData.Examples -> data.examples
|
||||
}
|
||||
|
||||
private fun createExperimentSession(
|
||||
experimentName: String,
|
||||
datasetId: String,
|
||||
numExamples: Int,
|
||||
): TracerSessionWithoutVirtualFields {
|
||||
val sessionMetadata = buildMap {
|
||||
putAll(params.metadata)
|
||||
put("__ls_runner", "java_sdk_evaluate")
|
||||
put("num_repetitions", params.numRepetitions)
|
||||
}
|
||||
|
||||
val extraBuilder = SessionCreateParams.Extra.builder()
|
||||
sessionMetadata.forEach { (key, value) ->
|
||||
extraBuilder.putAdditionalProperty(key, JsonValue.from(value))
|
||||
}
|
||||
|
||||
var candidateName = experimentName
|
||||
repeat(MAX_NAME_COLLISION_RETRIES) {
|
||||
try {
|
||||
return client
|
||||
.sessions()
|
||||
.create(
|
||||
SessionCreateParams.builder()
|
||||
.name(candidateName)
|
||||
.description(params.description)
|
||||
.referenceDatasetId(datasetId)
|
||||
.numExamples(numExamples.toLong())
|
||||
.startTime(OffsetDateTime.now())
|
||||
.extra(extraBuilder.build())
|
||||
.build()
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
candidateName = "$experimentName-${UUID.randomUUID().toString().take(6)}"
|
||||
}
|
||||
}
|
||||
throw IllegalStateException(
|
||||
"Could not create experiment session after $MAX_NAME_COLLISION_RETRIES attempts"
|
||||
)
|
||||
}
|
||||
|
||||
private fun evaluateExample(
|
||||
target: (Map<String, Any?>) -> Any?,
|
||||
example: Example,
|
||||
experimentName: String,
|
||||
sessionId: String?,
|
||||
): ExperimentResultRow? {
|
||||
val inputs = exampleInputs(example)
|
||||
val runMetadata = buildMap {
|
||||
putAll(params.metadata)
|
||||
put("example_version", exampleVersion(example))
|
||||
}
|
||||
|
||||
val run =
|
||||
if (params.uploadResults) {
|
||||
runTracedTarget(target, inputs, example, experimentName, sessionId, runMetadata)
|
||||
} else {
|
||||
runLocalTarget(target, inputs, example)
|
||||
} ?: return null
|
||||
|
||||
val evaluationResults =
|
||||
params.evaluators.flatMap { evaluator -> runEvaluator(evaluator, run, example) }
|
||||
|
||||
return ExperimentResultRow(
|
||||
run = run,
|
||||
example = example,
|
||||
evaluationResults = EvaluationResults(evaluationResults),
|
||||
)
|
||||
}
|
||||
|
||||
private fun runTracedTarget(
|
||||
target: (Map<String, Any?>) -> Any?,
|
||||
inputs: Map<String, Any?>,
|
||||
example: Example,
|
||||
experimentName: String,
|
||||
sessionId: String?,
|
||||
runMetadata: Map<String, Any>,
|
||||
): RunIngest? {
|
||||
var runTree: RunTree? = null
|
||||
val wrappedTarget: (Map<String, Any?>) -> Any? = { exampleInputs ->
|
||||
target(exampleInputs).also { runTree = getCurrentRunTree() }
|
||||
}
|
||||
|
||||
val traced =
|
||||
traceable(
|
||||
wrappedTarget,
|
||||
TraceConfig(
|
||||
name = "Target",
|
||||
client = client,
|
||||
projectName = experimentName,
|
||||
referenceExampleId =
|
||||
if (params.errorHandling == EvaluateErrorHandling.LOG) example.id()
|
||||
else null,
|
||||
sessionId = sessionId,
|
||||
metadata = runMetadata,
|
||||
tracingEnabled = true,
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
traced(inputs)
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Error running target function", e)
|
||||
if (params.errorHandling == EvaluateErrorHandling.IGNORE) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return runTree?.buildRunData()
|
||||
?: if (params.errorHandling == EvaluateErrorHandling.IGNORE) {
|
||||
null
|
||||
} else {
|
||||
throw IllegalStateException("Failed to capture run for example ${example.id()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun runLocalTarget(
|
||||
target: (Map<String, Any?>) -> Any?,
|
||||
inputs: Map<String, Any?>,
|
||||
example: Example,
|
||||
): RunIngest? {
|
||||
return try {
|
||||
val output = target(inputs)
|
||||
val outputsBuilder = RunIngest.Outputs.builder()
|
||||
when (output) {
|
||||
is Map<*, *> ->
|
||||
output.forEach { (key, value) ->
|
||||
outputsBuilder.putAdditionalProperty(key.toString(), JsonValue.from(value))
|
||||
}
|
||||
else -> outputsBuilder.putAdditionalProperty("output", JsonValue.from(output))
|
||||
}
|
||||
RunIngest.builder()
|
||||
.id(UUID.randomUUID().toString())
|
||||
.referenceExampleId(example.id())
|
||||
.outputs(outputsBuilder.build())
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Error running target function", e)
|
||||
if (params.errorHandling == EvaluateErrorHandling.IGNORE) {
|
||||
null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runEvaluator(
|
||||
evaluator: RunEvaluator,
|
||||
run: RunIngest,
|
||||
example: Example,
|
||||
): List<EvaluationResult> {
|
||||
val evaluatorRunId = newEvaluatorRunId()
|
||||
return try {
|
||||
val response = evaluator.evaluateRun(run, example, evaluatorRunId)
|
||||
if (params.uploadResults) {
|
||||
logEvaluationFeedback(client, response, run)
|
||||
}
|
||||
selectEvaluationResults(response)
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Error running evaluator $evaluator on run ${run.id()}", e)
|
||||
val keys = evaluator.feedbackKeys
|
||||
if (keys.isEmpty()) {
|
||||
throw e
|
||||
}
|
||||
val errorResponse =
|
||||
EvaluationResults(
|
||||
keys.map { key ->
|
||||
EvaluationResult(
|
||||
key = key,
|
||||
sourceRunId = evaluatorRunId,
|
||||
comment = e.toString(),
|
||||
extra = mapOf("error" to true),
|
||||
)
|
||||
}
|
||||
)
|
||||
if (params.uploadResults) {
|
||||
logEvaluationFeedback(client, errorResponse, run)
|
||||
}
|
||||
errorResponse.results
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySummaryEvaluators(
|
||||
rows: List<ExperimentResultRow>,
|
||||
sessionId: String?,
|
||||
): EvaluationResults {
|
||||
if (params.summaryEvaluators.isEmpty()) {
|
||||
return EvaluationResults(emptyList())
|
||||
}
|
||||
|
||||
val runs = rows.map { it.run }
|
||||
val examples = rows.map { it.example }
|
||||
val aggregateResults = mutableListOf<EvaluationResult>()
|
||||
|
||||
for (evaluator in params.summaryEvaluators) {
|
||||
try {
|
||||
val response = evaluator.evaluate(runs, examples)
|
||||
aggregateResults.addAll(selectEvaluationResults(response))
|
||||
if (params.uploadResults && sessionId != null) {
|
||||
logSummaryEvaluationFeedback(client, response, sessionId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.SEVERE, "Error running summary evaluator $evaluator", e)
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationResults(aggregateResults)
|
||||
}
|
||||
|
||||
private fun updateExperimentMetadata(sessionId: String, examples: List<Example>) {
|
||||
try {
|
||||
val projectMetadata = buildMap {
|
||||
putAll(params.metadata)
|
||||
put("dataset_version", datasetVersion(examples))
|
||||
}
|
||||
val extraBuilder = SessionUpdateParams.Extra.builder()
|
||||
projectMetadata.forEach { (key, value) ->
|
||||
extraBuilder.putAdditionalProperty(key, JsonValue.from(value))
|
||||
}
|
||||
client
|
||||
.sessions()
|
||||
.update(
|
||||
sessionId,
|
||||
SessionUpdateParams.builder().extra(extraBuilder.build()).build(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.WARNING, "Failed to update experiment metadata for $sessionId", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun datasetVersion(examples: List<Example>): String? {
|
||||
val modifiedAt =
|
||||
examples.mapNotNull { example ->
|
||||
example.modifiedAt().orElseGet { example.createdAt().orElse(null) }
|
||||
}
|
||||
val maxModifiedAt = modifiedAt.maxOrNull() ?: return null
|
||||
return maxModifiedAt.toString()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_NAME_COLLISION_RETRIES = 10
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.client.LangsmithClient
|
||||
import com.langchain.smith.core.JsonValue
|
||||
import com.langchain.smith.models.datasets.DatasetListParams
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.examples.ExampleListParams
|
||||
import com.langchain.smith.models.feedback.FeedbackCreateSchema
|
||||
import com.langchain.smith.models.feedback.ModelFeedbackSource
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import com.langchain.smith.models.runs.RunQueryParams
|
||||
import com.langchain.smith.models.runs.RunSchema
|
||||
import com.langchain.smith.models.sessions.TracerSession
|
||||
import java.util.UUID
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
internal fun expandExamples(examples: List<Example>, numRepetitions: Int): List<Example> =
|
||||
if (numRepetitions <= 1) {
|
||||
examples
|
||||
} else {
|
||||
List(numRepetitions) { examples }.flatten()
|
||||
}
|
||||
|
||||
internal fun resolveDatasetId(client: LangsmithClient, datasetIdentifier: String): String {
|
||||
if (isUuid(datasetIdentifier)) {
|
||||
return datasetIdentifier
|
||||
}
|
||||
val matches =
|
||||
client.datasets().list(DatasetListParams.builder().name(datasetIdentifier).build()).items()
|
||||
require(matches.isNotEmpty()) {
|
||||
"No dataset found with name '$datasetIdentifier'. Create the dataset first or pass its UUID."
|
||||
}
|
||||
require(matches.size == 1) {
|
||||
"Multiple datasets named '$datasetIdentifier'; pass the dataset UUID instead."
|
||||
}
|
||||
return matches.single().id()
|
||||
}
|
||||
|
||||
internal fun listExamples(client: LangsmithClient, datasetIdentifier: String): List<Example> {
|
||||
val datasetId = resolveDatasetId(client, datasetIdentifier)
|
||||
return listDatasetExamples(client, datasetId)
|
||||
}
|
||||
|
||||
internal fun listDatasetExamples(
|
||||
client: LangsmithClient,
|
||||
datasetId: String,
|
||||
asOf: String? = null,
|
||||
): List<Example> {
|
||||
val builder = ExampleListParams.builder().dataset(datasetId)
|
||||
asOf?.let { builder.asOf(it) }
|
||||
return client.examples().list(builder.build()).autoPager().toList()
|
||||
}
|
||||
|
||||
internal fun experimentDatasetVersion(session: TracerSession): String? {
|
||||
val extra = session.extra().orElse(null) ?: return null
|
||||
val props = extra._additionalProperties()
|
||||
props["dataset_version"]?.convert(String::class.java)?.let {
|
||||
return it
|
||||
}
|
||||
return props["metadata"]?.accept(
|
||||
object : JsonValue.Visitor<String?> {
|
||||
override fun visitObject(values: Map<String, JsonValue>): String? =
|
||||
values["dataset_version"]?.convert(String::class.java)
|
||||
|
||||
override fun visitDefault(): String? = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal fun loadExperiment(client: LangsmithClient, experiment: String): TracerSession =
|
||||
client.sessions().retrieve(experiment)
|
||||
|
||||
internal fun loadRunsForExperiment(
|
||||
client: LangsmithClient,
|
||||
sessionId: String,
|
||||
loadNested: Boolean,
|
||||
): List<RunIngest> {
|
||||
val deadlineMs = System.currentTimeMillis() + RUN_QUERY_TIMEOUT_MS
|
||||
var delayMs = RUN_QUERY_INITIAL_DELAY_MS
|
||||
while (true) {
|
||||
client.runs().flush()
|
||||
val runs = queryRunsForExperiment(client, sessionId, loadNested)
|
||||
if (runs.isNotEmpty() || System.currentTimeMillis() >= deadlineMs) {
|
||||
return runs
|
||||
}
|
||||
Thread.sleep(delayMs)
|
||||
delayMs = minOf(delayMs * 2, RUN_QUERY_MAX_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryRunsForExperiment(
|
||||
client: LangsmithClient,
|
||||
sessionId: String,
|
||||
loadNested: Boolean,
|
||||
): List<RunIngest> {
|
||||
val paramsBuilder = RunQueryParams.builder().session(listOf(sessionId)).limit(100L)
|
||||
if (!loadNested) {
|
||||
paramsBuilder.isRoot(true)
|
||||
}
|
||||
return client.runs().query(paramsBuilder.build()).autoPager().toList().map {
|
||||
it.toEvaluateRun()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RunSchema.toEvaluateRun(): RunIngest {
|
||||
val builder = RunIngest.builder().id(id()).sessionId(sessionId()).traceId(traceId())
|
||||
referenceExampleId().getOrNull()?.let { builder.referenceExampleId(it) }
|
||||
startTime().getOrNull()?.let { builder.startTime(it.toString()) }
|
||||
outputs().getOrNull()?.let { schemaOutputs ->
|
||||
val outputsBuilder = RunIngest.Outputs.builder()
|
||||
schemaOutputs._additionalProperties().forEach { (key, value) ->
|
||||
outputsBuilder.putAdditionalProperty(key, value)
|
||||
}
|
||||
builder.outputs(outputsBuilder.build())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
internal fun loadExamplesByIds(
|
||||
client: LangsmithClient,
|
||||
datasetId: String,
|
||||
exampleIds: List<String>,
|
||||
asOf: String? = null,
|
||||
): Map<String, Example> {
|
||||
if (exampleIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
val wanted = exampleIds.toSet()
|
||||
return listDatasetExamples(client, datasetId, asOf)
|
||||
.filter { it.id() in wanted }
|
||||
.associateBy { it.id() }
|
||||
}
|
||||
|
||||
internal fun buildExperimentUrl(
|
||||
session: TracerSession?,
|
||||
@Suppress("UNUSED_PARAMETER") datasetId: String,
|
||||
): String? = null
|
||||
|
||||
internal fun buildComparativeUrl(
|
||||
experiments: List<TracerSession>,
|
||||
comparativeExperimentId: String,
|
||||
datasetId: String,
|
||||
): String? = null
|
||||
|
||||
internal fun isUuid(value: String): Boolean =
|
||||
try {
|
||||
UUID.fromString(value)
|
||||
true
|
||||
} catch (_: IllegalArgumentException) {
|
||||
false
|
||||
}
|
||||
|
||||
internal fun generateExperimentName(prefix: String): String =
|
||||
"$prefix-${UUID.randomUUID().toString().take(8)}"
|
||||
|
||||
internal fun exampleVersion(example: Example): String {
|
||||
val timestamp = example.modifiedAt().orElseGet { example.createdAt().orElse(null) }
|
||||
return timestamp?.toString() ?: java.time.OffsetDateTime.now().toString()
|
||||
}
|
||||
|
||||
internal fun logSummaryEvaluationFeedback(
|
||||
client: LangsmithClient,
|
||||
evaluatorResponse: EvaluationOutput,
|
||||
projectId: String,
|
||||
) {
|
||||
for (result in selectEvaluationResults(evaluatorResponse)) {
|
||||
client.feedback().create(buildProjectFeedbackCreateSchema(result, projectId))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun logComparativeEvaluationFeedback(
|
||||
client: LangsmithClient,
|
||||
result: ComparisonEvaluationResult,
|
||||
runs: List<RunIngest>,
|
||||
comparativeExperimentId: String,
|
||||
feedbackGroupId: String,
|
||||
) {
|
||||
val comments =
|
||||
if (result.comment != null) {
|
||||
result.scores.keys.associateWith { result.comment }
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
val runsById = runs.associateBy { it.id().getOrNull().orEmpty() }
|
||||
for ((runId, score) in result.scores) {
|
||||
val run = runsById[runId]
|
||||
val builder =
|
||||
FeedbackCreateSchema.builder()
|
||||
.key(result.key)
|
||||
.runId(runId)
|
||||
.score(score.toDouble())
|
||||
.comparativeExperimentId(comparativeExperimentId)
|
||||
.feedbackGroupId(feedbackGroupId)
|
||||
.feedbackSource(
|
||||
ModelFeedbackSource.builder().type(ModelFeedbackSource.Type.MODEL).build()
|
||||
)
|
||||
comments[runId]?.let { builder.comment(it) }
|
||||
run?.sessionId()?.getOrNull()?.let { builder.sessionId(it) }
|
||||
run?.startTime()?.getOrNull()?.let { startTime ->
|
||||
runStartTime(startTime)?.let { builder.startTime(it) }
|
||||
}
|
||||
client.feedback().create(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildProjectFeedbackCreateSchema(
|
||||
result: EvaluationResult,
|
||||
projectId: String,
|
||||
): FeedbackCreateSchema {
|
||||
val builder = FeedbackCreateSchema.builder().key(result.key).sessionId(projectId)
|
||||
|
||||
builder.feedbackSource(
|
||||
ModelFeedbackSource.builder().type(ModelFeedbackSource.Type.MODEL).build()
|
||||
)
|
||||
|
||||
val score = result.score
|
||||
if (score != null) {
|
||||
builder.score(score.toDouble())
|
||||
}
|
||||
|
||||
applyFeedbackValue(builder, result.value)
|
||||
|
||||
val comment = result.comment
|
||||
if (comment != null) {
|
||||
builder.comment(comment)
|
||||
}
|
||||
|
||||
val feedbackConfig = result.feedbackConfig
|
||||
if (feedbackConfig != null) {
|
||||
builder.feedbackConfig(feedbackConfig)
|
||||
}
|
||||
|
||||
val correction = result.correction
|
||||
if (correction != null) {
|
||||
builder.correction(
|
||||
FeedbackCreateSchema.Correction.ofUnionMember0(
|
||||
FeedbackCreateSchema.Correction.UnionMember0.builder()
|
||||
.putAllAdditionalProperties(
|
||||
correction.mapValues { (_, value) -> JsonValue.from(value) }
|
||||
)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val error = result.extra?.get("error") as? Boolean
|
||||
if (error == true) {
|
||||
builder.error(true)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private const val RUN_QUERY_TIMEOUT_MS = 30_000L
|
||||
private const val RUN_QUERY_INITIAL_DELAY_MS = 500L
|
||||
private const val RUN_QUERY_MAX_DELAY_MS = 2_000L
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
* Runnable-style evaluation target. Mirrors Python's `Runnable.invoke` / chain target support
|
||||
* without requiring a LangChain dependency.
|
||||
*/
|
||||
fun interface EvaluateTarget {
|
||||
fun invoke(inputs: Map<String, Any?>): Any?
|
||||
}
|
||||
|
||||
/** Wraps a Kotlin lambda as an [EvaluateTarget]. */
|
||||
fun evaluateTarget(func: (Map<String, Any?>) -> Any?): EvaluateTarget = EvaluateTarget(func::invoke)
|
||||
|
||||
/** Wraps a Java [Function] as an [EvaluateTarget]. */
|
||||
fun evaluateTarget(func: Function<Map<String, Any?>, Any?>): EvaluateTarget =
|
||||
EvaluateTarget { inputs ->
|
||||
func.apply(inputs)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.feedback.FeedbackCreateSchema
|
||||
|
||||
/**
|
||||
* Normalized output from [RunEvaluator.evaluateRun], matching Python's `Union[EvaluationResult,
|
||||
* EvaluationResults]`.
|
||||
*/
|
||||
sealed interface EvaluationOutput
|
||||
|
||||
/** A single evaluation feedback metric produced by a [RunEvaluator]. */
|
||||
data class EvaluationResult(
|
||||
val key: String,
|
||||
val score: Number? = null,
|
||||
val value: Any? = null,
|
||||
val metadata: Map<String, Any>? = null,
|
||||
val comment: String? = null,
|
||||
val correction: Map<String, Any>? = null,
|
||||
val evaluatorInfo: Map<String, Any> = emptyMap(),
|
||||
val feedbackConfig: FeedbackCreateSchema.FeedbackConfig? = null,
|
||||
val sourceRunId: String? = null,
|
||||
val targetRunId: String? = null,
|
||||
val extra: Map<String, Any>? = null,
|
||||
) : EvaluationOutput
|
||||
|
||||
/** Batch evaluation results returned when an evaluator emits multiple metrics at once. */
|
||||
data class EvaluationResults(val results: List<EvaluationResult>) : EvaluationOutput
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.langchain.smith.core.JsonValue
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.feedback.FeedbackCreateSchema
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
import java.util.UUID
|
||||
|
||||
private val stringAnyMapType = object : TypeReference<Map<String, Any?>>() {}
|
||||
|
||||
private val feedbackConfigType = object : TypeReference<FeedbackCreateSchema.FeedbackConfig>() {}
|
||||
|
||||
/** Gets the app's answer from a finished run as a simple map. */
|
||||
internal fun runOutputsMap(run: RunIngest): Map<String, Any?> =
|
||||
run.outputs().map { it._additionalProperties().toPlainMap() }.orElse(emptyMap())
|
||||
|
||||
/** Gets the inputs sent to the app for one dataset row as a simple map. */
|
||||
internal fun exampleInputs(example: Example?): Map<String, Any?> =
|
||||
example?.inputs()?.let { it._additionalProperties().toPlainMap() } ?: emptyMap()
|
||||
|
||||
/** Gets the expected outputs stored on a dataset row as a simple map. */
|
||||
internal fun exampleOutputs(example: Example?): Map<String, Any?> =
|
||||
example?.outputs()?.map { it._additionalProperties().toPlainMap() }?.orElse(emptyMap())
|
||||
?: emptyMap()
|
||||
|
||||
/** Unpacks stored fields into plain maps that evaluators can read and compare. */
|
||||
private fun Map<String, JsonValue>.toPlainMap(): Map<String, Any?> =
|
||||
JsonValue.from(this).convert(stringAnyMapType) ?: emptyMap()
|
||||
|
||||
/** Accepts whatever an evaluator returned and shapes it into a standard map first. */
|
||||
internal fun formatEvaluatorResult(result: EvaluatorOutput): Map<String, Any?> =
|
||||
when (result) {
|
||||
is EvaluationResult ->
|
||||
mapOf("key" to result.key, "score" to result.score, "value" to result.value)
|
||||
is EvaluationResults -> mapOf("results" to result.results)
|
||||
is Boolean -> mapOf("score" to if (result) 1 else 0)
|
||||
is Number -> mapOf("score" to result)
|
||||
is String -> mapOf("value" to result)
|
||||
is List<*> -> formatResultList(result)
|
||||
is Map<*, *> -> result.asStringKeyMap()
|
||||
null -> throw IllegalArgumentException("Evaluator returned null")
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Expected EvaluationResult, EvaluationResults, Map, String, Boolean, Number, or List. Got $result"
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatResultList(result: List<*>): Map<String, Any?> {
|
||||
if (result.isEmpty()) {
|
||||
throw IllegalArgumentException(
|
||||
"Expected a non-empty list of evaluation results, got empty list"
|
||||
)
|
||||
}
|
||||
if (result.any { it !is Map<*, *> && it !is EvaluationResult }) {
|
||||
throw IllegalArgumentException(
|
||||
"Expected a list of maps or EvaluationResult objects, got $result"
|
||||
)
|
||||
}
|
||||
return mapOf("results" to result)
|
||||
}
|
||||
|
||||
// Silencing a compile-time warning because we already handle the wrong type case when the code
|
||||
// runs.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun Map<*, *>.asStringKeyMap(): Map<String, Any?> = this as Map<String, Any?>
|
||||
|
||||
private fun Any.requireStringKeyMap(): Map<String, Any?> =
|
||||
(this as? Map<*, *>)?.asStringKeyMap()
|
||||
?: throw IllegalArgumentException(
|
||||
"Expected EvaluationResult or Map with metric fields, got $this"
|
||||
)
|
||||
|
||||
private fun parseFeedbackConfig(value: Any?): FeedbackCreateSchema.FeedbackConfig? =
|
||||
when (value) {
|
||||
null -> null
|
||||
is FeedbackCreateSchema.FeedbackConfig -> value
|
||||
is Map<*, *> ->
|
||||
JsonValue.from(value).convert(feedbackConfigType)
|
||||
?: throw IllegalArgumentException("Invalid feedbackConfig: $value")
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Expected feedbackConfig as FeedbackConfig or Map, got $value"
|
||||
)
|
||||
}
|
||||
|
||||
/** Builds one scored feedback item from an evaluator's return value. */
|
||||
internal fun coerceEvaluationResult(
|
||||
result: Any,
|
||||
sourceRunId: String,
|
||||
defaultKey: String,
|
||||
allowMissingKey: Boolean = false,
|
||||
): EvaluationResult {
|
||||
if (result is EvaluationResult) {
|
||||
return result.copy(sourceRunId = result.sourceRunId ?: sourceRunId)
|
||||
}
|
||||
val map = result.requireStringKeyMap()
|
||||
if (map.isEmpty()) {
|
||||
throw IllegalArgumentException("Expected a non-empty evaluation result map, got $map")
|
||||
}
|
||||
val key =
|
||||
map["key"] as? String
|
||||
?: defaultKey.takeIf { allowMissingKey }
|
||||
?: throw IllegalArgumentException("Evaluation result map must include a 'key' field")
|
||||
if ("score" !in map && "value" !in map && "comment" !in map) {
|
||||
throw IllegalArgumentException(
|
||||
"Evaluation result map must include at least one of score, value, or comment"
|
||||
)
|
||||
}
|
||||
return EvaluationResult(
|
||||
key = key,
|
||||
score = map["score"] as? Number,
|
||||
value = map["value"],
|
||||
metadata = map["metadata"] as? Map<String, Any>,
|
||||
comment = map["comment"] as? String,
|
||||
correction = map["correction"] as? Map<String, Any>,
|
||||
evaluatorInfo = map["evaluatorInfo"] as? Map<String, Any> ?: emptyMap(),
|
||||
feedbackConfig = parseFeedbackConfig(map["feedbackConfig"] ?: map["feedback_config"]),
|
||||
sourceRunId = sourceRunId,
|
||||
targetRunId = map["targetRunId"] as? String,
|
||||
extra = map["extra"] as? Map<String, Any>,
|
||||
)
|
||||
}
|
||||
|
||||
/** Normalizes any evaluator return into one or more scored feedback items. */
|
||||
internal fun coerceEvaluatorOutput(
|
||||
output: EvaluatorOutput,
|
||||
sourceRunId: String,
|
||||
defaultKey: String,
|
||||
): EvaluationOutput =
|
||||
when (output) {
|
||||
is EvaluationResult -> output.copy(sourceRunId = output.sourceRunId ?: sourceRunId)
|
||||
is EvaluationResults ->
|
||||
EvaluationResults(
|
||||
output.results.map { coerceEvaluationResult(it, sourceRunId, defaultKey) }
|
||||
)
|
||||
else -> {
|
||||
val map = formatEvaluatorResult(output)
|
||||
if ("results" in map) {
|
||||
@Suppress("UNCHECKED_CAST") val results = map["results"] as List<Any>
|
||||
EvaluationResults(
|
||||
results.map {
|
||||
coerceEvaluationResult(it, sourceRunId, defaultKey, allowMissingKey = false)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
coerceEvaluationResult(map, sourceRunId, defaultKey, allowMissingKey = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a unique id for this evaluator scoring step. */
|
||||
internal fun newEvaluatorRunId(): String = UUID.randomUUID().toString()
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
|
||||
/** One dataset row evaluated in an experiment. */
|
||||
data class ExperimentResultRow(
|
||||
val run: RunIngest,
|
||||
val example: Example,
|
||||
val evaluationResults: EvaluationResults,
|
||||
) {
|
||||
override fun toString(): String =
|
||||
"ExperimentResultRow{exampleId=${example.id()}, runId=${run.id().orElse(null)}, " +
|
||||
"results=${evaluationResults.results.size}}"
|
||||
}
|
||||
|
||||
/** Results returned by [evaluate]. */
|
||||
class ExperimentResults
|
||||
internal constructor(
|
||||
val experimentName: String,
|
||||
/** LangSmith experiment session id. */
|
||||
val experimentId: String?,
|
||||
val datasetId: String,
|
||||
val rows: List<ExperimentResultRow>,
|
||||
val summaryResults: EvaluationResults = EvaluationResults(emptyList()),
|
||||
val url: String? = null,
|
||||
) {
|
||||
override fun toString(): String =
|
||||
"ExperimentResults{experimentName=$experimentName, experimentId=$experimentId, " +
|
||||
"datasetId=$datasetId, rows=${rows.size}, summaryResults=${summaryResults.results.size}}"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
|
||||
/** Evaluates a single prediction [run] against an optional dataset [example]. */
|
||||
interface RunEvaluator {
|
||||
fun evaluateRun(run: RunIngest, example: Example?, evaluatorRunId: String?): EvaluationOutput
|
||||
|
||||
fun evaluateRun(run: RunIngest, example: Example?): EvaluationOutput =
|
||||
evaluateRun(run, example, null)
|
||||
|
||||
fun evaluateRun(run: RunIngest): EvaluationOutput = evaluateRun(run, null, null)
|
||||
|
||||
/** Feedback keys this evaluator emits, when statically known. */
|
||||
val feedbackKeys: List<String>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
/** Raw return values accepted from custom evaluator lambdas before normalization. */
|
||||
typealias EvaluatorOutput = Any
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.langchain.smith.evaluation
|
||||
|
||||
import com.langchain.smith.models.examples.Example
|
||||
import com.langchain.smith.models.runs.RunIngest
|
||||
|
||||
/** Evaluates aggregate metrics across all runs in an experiment. */
|
||||
fun interface SummaryEvaluator {
|
||||
fun evaluate(runs: List<RunIngest>, examples: List<Example>): EvaluationOutput
|
||||
}
|
||||
|
||||
/** Creates a [SummaryEvaluator] from a function over all runs and examples. */
|
||||
fun summaryEvaluator(func: (List<RunIngest>, List<Example>) -> EvaluatorOutput): SummaryEvaluator =
|
||||
SummaryEvaluator { runs, examples ->
|
||||
coerceSummaryOutput(func(runs, examples), newEvaluatorRunId())
|
||||
}
|
||||
|
||||
internal fun coerceSummaryOutput(output: EvaluatorOutput, sourceRunId: String): EvaluationOutput =
|
||||
coerceEvaluatorOutput(output, sourceRunId, defaultKey = "summary")
|
||||
+91
-1
@@ -85,6 +85,12 @@ private constructor(
|
||||
*/
|
||||
fun reservationMinutes(): Optional<Long> = body.reservationMinutes()
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if the
|
||||
* server responded with an unexpected value).
|
||||
*/
|
||||
fun reviewerAccessMode(): Optional<String> = body.reviewerAccessMode()
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if the
|
||||
* server responded with an unexpected value).
|
||||
@@ -175,6 +181,14 @@ private constructor(
|
||||
*/
|
||||
fun _reservationMinutes(): JsonField<Long> = body._reservationMinutes()
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [reviewerAccessMode].
|
||||
*
|
||||
* Unlike [reviewerAccessMode], this method doesn't throw if the JSON field has an unexpected
|
||||
* type.
|
||||
*/
|
||||
fun _reviewerAccessMode(): JsonField<String> = body._reviewerAccessMode()
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [rubricInstructions].
|
||||
*
|
||||
@@ -422,6 +436,21 @@ private constructor(
|
||||
body.reservationMinutes(reservationMinutes)
|
||||
}
|
||||
|
||||
fun reviewerAccessMode(reviewerAccessMode: String) = apply {
|
||||
body.reviewerAccessMode(reviewerAccessMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets [Builder.reviewerAccessMode] to an arbitrary JSON value.
|
||||
*
|
||||
* You should usually call [Builder.reviewerAccessMode] with a well-typed [String] value
|
||||
* instead. This method is primarily for setting the field to an undocumented or not yet
|
||||
* supported value.
|
||||
*/
|
||||
fun reviewerAccessMode(reviewerAccessMode: JsonField<String>) = apply {
|
||||
body.reviewerAccessMode(reviewerAccessMode)
|
||||
}
|
||||
|
||||
fun rubricInstructions(rubricInstructions: String?) = apply {
|
||||
body.rubricInstructions(rubricInstructions)
|
||||
}
|
||||
@@ -659,6 +688,7 @@ private constructor(
|
||||
private val metadata: JsonField<Metadata>,
|
||||
private val numReviewersPerItem: JsonField<Long>,
|
||||
private val reservationMinutes: JsonField<Long>,
|
||||
private val reviewerAccessMode: JsonField<String>,
|
||||
private val rubricInstructions: JsonField<String>,
|
||||
private val rubricItems: JsonField<List<AnnotationQueueRubricItemSchema>>,
|
||||
private val sessionIds: JsonField<List<String>>,
|
||||
@@ -691,6 +721,9 @@ private constructor(
|
||||
@JsonProperty("reservation_minutes")
|
||||
@ExcludeMissing
|
||||
reservationMinutes: JsonField<Long> = JsonMissing.of(),
|
||||
@JsonProperty("reviewer_access_mode")
|
||||
@ExcludeMissing
|
||||
reviewerAccessMode: JsonField<String> = JsonMissing.of(),
|
||||
@JsonProperty("rubric_instructions")
|
||||
@ExcludeMissing
|
||||
rubricInstructions: JsonField<String> = JsonMissing.of(),
|
||||
@@ -713,6 +746,7 @@ private constructor(
|
||||
metadata,
|
||||
numReviewersPerItem,
|
||||
reservationMinutes,
|
||||
reviewerAccessMode,
|
||||
rubricInstructions,
|
||||
rubricItems,
|
||||
sessionIds,
|
||||
@@ -777,6 +811,13 @@ private constructor(
|
||||
fun reservationMinutes(): Optional<Long> =
|
||||
reservationMinutes.getOptional("reservation_minutes")
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if
|
||||
* the server responded with an unexpected value).
|
||||
*/
|
||||
fun reviewerAccessMode(): Optional<String> =
|
||||
reviewerAccessMode.getOptional("reviewer_access_mode")
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if
|
||||
* the server responded with an unexpected value).
|
||||
@@ -882,6 +923,16 @@ private constructor(
|
||||
@ExcludeMissing
|
||||
fun _reservationMinutes(): JsonField<Long> = reservationMinutes
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [reviewerAccessMode].
|
||||
*
|
||||
* Unlike [reviewerAccessMode], this method doesn't throw if the JSON field has an
|
||||
* unexpected type.
|
||||
*/
|
||||
@JsonProperty("reviewer_access_mode")
|
||||
@ExcludeMissing
|
||||
fun _reviewerAccessMode(): JsonField<String> = reviewerAccessMode
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [rubricInstructions].
|
||||
*
|
||||
@@ -956,6 +1007,7 @@ private constructor(
|
||||
private var metadata: JsonField<Metadata> = JsonMissing.of()
|
||||
private var numReviewersPerItem: JsonField<Long> = JsonMissing.of()
|
||||
private var reservationMinutes: JsonField<Long> = JsonMissing.of()
|
||||
private var reviewerAccessMode: JsonField<String> = JsonMissing.of()
|
||||
private var rubricInstructions: JsonField<String> = JsonMissing.of()
|
||||
private var rubricItems: JsonField<MutableList<AnnotationQueueRubricItemSchema>>? = null
|
||||
private var sessionIds: JsonField<MutableList<String>>? = null
|
||||
@@ -973,6 +1025,7 @@ private constructor(
|
||||
metadata = body.metadata
|
||||
numReviewersPerItem = body.numReviewersPerItem
|
||||
reservationMinutes = body.reservationMinutes
|
||||
reviewerAccessMode = body.reviewerAccessMode
|
||||
rubricInstructions = body.rubricInstructions
|
||||
rubricItems = body.rubricItems.map { it.toMutableList() }
|
||||
sessionIds = body.sessionIds.map { it.toMutableList() }
|
||||
@@ -1150,6 +1203,20 @@ private constructor(
|
||||
this.reservationMinutes = reservationMinutes
|
||||
}
|
||||
|
||||
fun reviewerAccessMode(reviewerAccessMode: String) =
|
||||
reviewerAccessMode(JsonField.of(reviewerAccessMode))
|
||||
|
||||
/**
|
||||
* Sets [Builder.reviewerAccessMode] to an arbitrary JSON value.
|
||||
*
|
||||
* You should usually call [Builder.reviewerAccessMode] with a well-typed [String] value
|
||||
* instead. This method is primarily for setting the field to an undocumented or not yet
|
||||
* supported value.
|
||||
*/
|
||||
fun reviewerAccessMode(reviewerAccessMode: JsonField<String>) = apply {
|
||||
this.reviewerAccessMode = reviewerAccessMode
|
||||
}
|
||||
|
||||
fun rubricInstructions(rubricInstructions: String?) =
|
||||
rubricInstructions(JsonField.ofNullable(rubricInstructions))
|
||||
|
||||
@@ -1284,6 +1351,7 @@ private constructor(
|
||||
metadata,
|
||||
numReviewersPerItem,
|
||||
reservationMinutes,
|
||||
reviewerAccessMode,
|
||||
rubricInstructions,
|
||||
(rubricItems ?: JsonMissing.of()).map { it.toImmutable() },
|
||||
(sessionIds ?: JsonMissing.of()).map { it.toImmutable() },
|
||||
@@ -1294,6 +1362,15 @@ private constructor(
|
||||
|
||||
private var validated: Boolean = false
|
||||
|
||||
/**
|
||||
* Validates that the types of all values in this object match their expected types
|
||||
* recursively.
|
||||
*
|
||||
* This method is _not_ forwards compatible with new types from the API for existing fields.
|
||||
*
|
||||
* @throws LangChainInvalidDataException if any value type in this object doesn't match its
|
||||
* expected type.
|
||||
*/
|
||||
fun validate(): Body = apply {
|
||||
if (validated) {
|
||||
return@apply
|
||||
@@ -1308,6 +1385,7 @@ private constructor(
|
||||
metadata().ifPresent { it.validate() }
|
||||
numReviewersPerItem()
|
||||
reservationMinutes()
|
||||
reviewerAccessMode()
|
||||
rubricInstructions()
|
||||
rubricItems().ifPresent { it.forEach { it.validate() } }
|
||||
sessionIds()
|
||||
@@ -1340,6 +1418,7 @@ private constructor(
|
||||
(metadata.asKnown().getOrNull()?.validity() ?: 0) +
|
||||
(if (numReviewersPerItem.asKnown().isPresent) 1 else 0) +
|
||||
(if (reservationMinutes.asKnown().isPresent) 1 else 0) +
|
||||
(if (reviewerAccessMode.asKnown().isPresent) 1 else 0) +
|
||||
(if (rubricInstructions.asKnown().isPresent) 1 else 0) +
|
||||
(rubricItems.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
|
||||
(sessionIds.asKnown().getOrNull()?.size ?: 0) +
|
||||
@@ -1360,6 +1439,7 @@ private constructor(
|
||||
metadata == other.metadata &&
|
||||
numReviewersPerItem == other.numReviewersPerItem &&
|
||||
reservationMinutes == other.reservationMinutes &&
|
||||
reviewerAccessMode == other.reviewerAccessMode &&
|
||||
rubricInstructions == other.rubricInstructions &&
|
||||
rubricItems == other.rubricItems &&
|
||||
sessionIds == other.sessionIds &&
|
||||
@@ -1378,6 +1458,7 @@ private constructor(
|
||||
metadata,
|
||||
numReviewersPerItem,
|
||||
reservationMinutes,
|
||||
reviewerAccessMode,
|
||||
rubricInstructions,
|
||||
rubricItems,
|
||||
sessionIds,
|
||||
@@ -1389,7 +1470,7 @@ private constructor(
|
||||
override fun hashCode(): Int = hashCode
|
||||
|
||||
override fun toString() =
|
||||
"Body{name=$name, id=$id, createdAt=$createdAt, defaultDataset=$defaultDataset, description=$description, enableReservations=$enableReservations, metadata=$metadata, numReviewersPerItem=$numReviewersPerItem, reservationMinutes=$reservationMinutes, rubricInstructions=$rubricInstructions, rubricItems=$rubricItems, sessionIds=$sessionIds, updatedAt=$updatedAt, additionalProperties=$additionalProperties}"
|
||||
"Body{name=$name, id=$id, createdAt=$createdAt, defaultDataset=$defaultDataset, description=$description, enableReservations=$enableReservations, metadata=$metadata, numReviewersPerItem=$numReviewersPerItem, reservationMinutes=$reservationMinutes, reviewerAccessMode=$reviewerAccessMode, rubricInstructions=$rubricInstructions, rubricItems=$rubricItems, sessionIds=$sessionIds, updatedAt=$updatedAt, additionalProperties=$additionalProperties}"
|
||||
}
|
||||
|
||||
class Metadata
|
||||
@@ -1450,6 +1531,15 @@ private constructor(
|
||||
|
||||
private var validated: Boolean = false
|
||||
|
||||
/**
|
||||
* Validates that the types of all values in this object match their expected types
|
||||
* recursively.
|
||||
*
|
||||
* This method is _not_ forwards compatible with new types from the API for existing fields.
|
||||
*
|
||||
* @throws LangChainInvalidDataException if any value type in this object doesn't match its
|
||||
* expected type.
|
||||
*/
|
||||
fun validate(): Metadata = apply {
|
||||
if (validated) {
|
||||
return@apply
|
||||
|
||||
+9
@@ -433,6 +433,15 @@ private constructor(
|
||||
|
||||
private var validated: Boolean = false
|
||||
|
||||
/**
|
||||
* Validates that the types of all values in this object match their expected types
|
||||
* recursively.
|
||||
*
|
||||
* This method is _not_ forwards compatible with new types from the API for existing fields.
|
||||
*
|
||||
* @throws LangChainInvalidDataException if any value type in this object doesn't match its
|
||||
* expected type.
|
||||
*/
|
||||
fun validate(): Body = apply {
|
||||
if (validated) {
|
||||
return@apply
|
||||
|
||||
+8
@@ -81,6 +81,14 @@ private constructor(private val additionalProperties: MutableMap<String, JsonVal
|
||||
|
||||
private var validated: Boolean = false
|
||||
|
||||
/**
|
||||
* Validates that the types of all values in this object match their expected types recursively.
|
||||
*
|
||||
* This method is _not_ forwards compatible with new types from the API for existing fields.
|
||||
*
|
||||
* @throws LangChainInvalidDataException if any value type in this object doesn't match its
|
||||
* expected type.
|
||||
*/
|
||||
fun validate(): AnnotationQueueCreateRunStatusResponse = apply {
|
||||
if (validated) {
|
||||
return@apply
|
||||
|
||||
+8
@@ -78,6 +78,14 @@ private constructor(private val additionalProperties: MutableMap<String, JsonVal
|
||||
|
||||
private var validated: Boolean = false
|
||||
|
||||
/**
|
||||
* Validates that the types of all values in this object match their expected types recursively.
|
||||
*
|
||||
* This method is _not_ forwards compatible with new types from the API for existing fields.
|
||||
*
|
||||
* @throws LangChainInvalidDataException if any value type in this object doesn't match its
|
||||
* expected type.
|
||||
*/
|
||||
fun validate(): AnnotationQueueDeleteResponse = apply {
|
||||
if (validated) {
|
||||
return@apply
|
||||
|
||||
+91
-4
@@ -37,6 +37,12 @@ private constructor(
|
||||
*/
|
||||
fun endTime(): Optional<OffsetDateTime> = body.endTime()
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if the
|
||||
* server responded with an unexpected value).
|
||||
*/
|
||||
fun includeAnnotatorDetail(): Optional<Boolean> = body.includeAnnotatorDetail()
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if the
|
||||
* server responded with an unexpected value).
|
||||
@@ -50,6 +56,14 @@ private constructor(
|
||||
*/
|
||||
fun _endTime(): JsonField<OffsetDateTime> = body._endTime()
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [includeAnnotatorDetail].
|
||||
*
|
||||
* Unlike [includeAnnotatorDetail], this method doesn't throw if the JSON field has an
|
||||
* unexpected type.
|
||||
*/
|
||||
fun _includeAnnotatorDetail(): JsonField<Boolean> = body._includeAnnotatorDetail()
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [startTime].
|
||||
*
|
||||
@@ -104,6 +118,7 @@ private constructor(
|
||||
* This is generally only useful if you are already constructing the body separately.
|
||||
* Otherwise, it's more convenient to use the top-level setters instead:
|
||||
* - [endTime]
|
||||
* - [includeAnnotatorDetail]
|
||||
* - [startTime]
|
||||
*/
|
||||
fun body(body: Body) = apply { this.body = body.toBuilder() }
|
||||
@@ -122,6 +137,21 @@ private constructor(
|
||||
*/
|
||||
fun endTime(endTime: JsonField<OffsetDateTime>) = apply { body.endTime(endTime) }
|
||||
|
||||
fun includeAnnotatorDetail(includeAnnotatorDetail: Boolean) = apply {
|
||||
body.includeAnnotatorDetail(includeAnnotatorDetail)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets [Builder.includeAnnotatorDetail] to an arbitrary JSON value.
|
||||
*
|
||||
* You should usually call [Builder.includeAnnotatorDetail] with a well-typed [Boolean]
|
||||
* value instead. This method is primarily for setting the field to an undocumented or not
|
||||
* yet supported value.
|
||||
*/
|
||||
fun includeAnnotatorDetail(includeAnnotatorDetail: JsonField<Boolean>) = apply {
|
||||
body.includeAnnotatorDetail(includeAnnotatorDetail)
|
||||
}
|
||||
|
||||
fun startTime(startTime: OffsetDateTime?) = apply { body.startTime(startTime) }
|
||||
|
||||
/** Alias for calling [Builder.startTime] with `startTime.orElse(null)`. */
|
||||
@@ -284,6 +314,7 @@ private constructor(
|
||||
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
|
||||
private constructor(
|
||||
private val endTime: JsonField<OffsetDateTime>,
|
||||
private val includeAnnotatorDetail: JsonField<Boolean>,
|
||||
private val startTime: JsonField<OffsetDateTime>,
|
||||
private val additionalProperties: MutableMap<String, JsonValue>,
|
||||
) {
|
||||
@@ -293,10 +324,13 @@ private constructor(
|
||||
@JsonProperty("end_time")
|
||||
@ExcludeMissing
|
||||
endTime: JsonField<OffsetDateTime> = JsonMissing.of(),
|
||||
@JsonProperty("include_annotator_detail")
|
||||
@ExcludeMissing
|
||||
includeAnnotatorDetail: JsonField<Boolean> = JsonMissing.of(),
|
||||
@JsonProperty("start_time")
|
||||
@ExcludeMissing
|
||||
startTime: JsonField<OffsetDateTime> = JsonMissing.of(),
|
||||
) : this(endTime, startTime, mutableMapOf())
|
||||
) : this(endTime, includeAnnotatorDetail, startTime, mutableMapOf())
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if
|
||||
@@ -304,6 +338,13 @@ private constructor(
|
||||
*/
|
||||
fun endTime(): Optional<OffsetDateTime> = endTime.getOptional("end_time")
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if
|
||||
* the server responded with an unexpected value).
|
||||
*/
|
||||
fun includeAnnotatorDetail(): Optional<Boolean> =
|
||||
includeAnnotatorDetail.getOptional("include_annotator_detail")
|
||||
|
||||
/**
|
||||
* @throws LangChainInvalidDataException if the JSON field has an unexpected type (e.g. if
|
||||
* the server responded with an unexpected value).
|
||||
@@ -319,6 +360,16 @@ private constructor(
|
||||
@ExcludeMissing
|
||||
fun _endTime(): JsonField<OffsetDateTime> = endTime
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [includeAnnotatorDetail].
|
||||
*
|
||||
* Unlike [includeAnnotatorDetail], this method doesn't throw if the JSON field has an
|
||||
* unexpected type.
|
||||
*/
|
||||
@JsonProperty("include_annotator_detail")
|
||||
@ExcludeMissing
|
||||
fun _includeAnnotatorDetail(): JsonField<Boolean> = includeAnnotatorDetail
|
||||
|
||||
/**
|
||||
* Returns the raw JSON value of [startTime].
|
||||
*
|
||||
@@ -350,12 +401,14 @@ private constructor(
|
||||
class Builder internal constructor() {
|
||||
|
||||
private var endTime: JsonField<OffsetDateTime> = JsonMissing.of()
|
||||
private var includeAnnotatorDetail: JsonField<Boolean> = JsonMissing.of()
|
||||
private var startTime: JsonField<OffsetDateTime> = JsonMissing.of()
|
||||
private var additionalProperties: MutableMap<String, JsonValue> = mutableMapOf()
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun from(body: Body) = apply {
|
||||
endTime = body.endTime
|
||||
includeAnnotatorDetail = body.includeAnnotatorDetail
|
||||
startTime = body.startTime
|
||||
additionalProperties = body.additionalProperties.toMutableMap()
|
||||
}
|
||||
@@ -374,6 +427,20 @@ private constructor(
|
||||
*/
|
||||
fun endTime(endTime: JsonField<OffsetDateTime>) = apply { this.endTime = endTime }
|
||||
|
||||
fun includeAnnotatorDetail(includeAnnotatorDetail: Boolean) =
|
||||
includeAnnotatorDetail(JsonField.of(includeAnnotatorDetail))
|
||||
|
||||
/**
|
||||
* Sets [Builder.includeAnnotatorDetail] to an arbitrary JSON value.
|
||||
*
|
||||
* You should usually call [Builder.includeAnnotatorDetail] with a well-typed [Boolean]
|
||||
* value instead. This method is primarily for setting the field to an undocumented or
|
||||
* not yet supported value.
|
||||
*/
|
||||
fun includeAnnotatorDetail(includeAnnotatorDetail: JsonField<Boolean>) = apply {
|
||||
this.includeAnnotatorDetail = includeAnnotatorDetail
|
||||
}
|
||||
|
||||
fun startTime(startTime: OffsetDateTime?) = startTime(JsonField.ofNullable(startTime))
|
||||
|
||||
/** Alias for calling [Builder.startTime] with `startTime.orElse(null)`. */
|
||||
@@ -414,17 +481,33 @@ private constructor(
|
||||
*
|
||||
* Further updates to this [Builder] will not mutate the returned instance.
|
||||
*/
|
||||
fun build(): Body = Body(endTime, startTime, additionalProperties.toMutableMap())
|
||||
fun build(): Body =
|
||||
Body(
|
||||
endTime,
|
||||
includeAnnotatorDetail,
|
||||
startTime,
|
||||
additionalProperties.toMutableMap(),
|
||||
)
|
||||
}
|
||||
|
||||
private var validated: Boolean = false
|
||||
|
||||
/**
|
||||
* Validates that the types of all values in this object match their expected types
|
||||
* recursively.
|
||||
*
|
||||
* This method is _not_ forwards compatible with new types from the API for existing fields.
|
||||
*
|
||||
* @throws LangChainInvalidDataException if any value type in this object doesn't match its
|
||||
* expected type.
|
||||
*/
|
||||
fun validate(): Body = apply {
|
||||
if (validated) {
|
||||
return@apply
|
||||
}
|
||||
|
||||
endTime()
|
||||
includeAnnotatorDetail()
|
||||
startTime()
|
||||
validated = true
|
||||
}
|
||||
@@ -446,6 +529,7 @@ private constructor(
|
||||
@JvmSynthetic
|
||||
internal fun validity(): Int =
|
||||
(if (endTime.asKnown().isPresent) 1 else 0) +
|
||||
(if (includeAnnotatorDetail.asKnown().isPresent) 1 else 0) +
|
||||
(if (startTime.asKnown().isPresent) 1 else 0)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -455,16 +539,19 @@ private constructor(
|
||||
|
||||
return other is Body &&
|
||||
endTime == other.endTime &&
|
||||
includeAnnotatorDetail == other.includeAnnotatorDetail &&
|
||||
startTime == other.startTime &&
|
||||
additionalProperties == other.additionalProperties
|
||||
}
|
||||
|
||||
private val hashCode: Int by lazy { Objects.hash(endTime, startTime, additionalProperties) }
|
||||
private val hashCode: Int by lazy {
|
||||
Objects.hash(endTime, includeAnnotatorDetail, startTime, additionalProperties)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = hashCode
|
||||
|
||||
override fun toString() =
|
||||
"Body{endTime=$endTime, startTime=$startTime, additionalProperties=$additionalProperties}"
|
||||
"Body{endTime=$endTime, includeAnnotatorDetail=$includeAnnotatorDetail, startTime=$startTime, additionalProperties=$additionalProperties}"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user