mirror of
https://github.com/langchain-ai/langsmith-java.git
synced 2026-07-25 21:25:45 -04:00
Compare commits
405 Commits
v0.1.0-alpha.11
...
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 | |||
| ded153ef51 | |||
| 5572d09902 | |||
| 2f56b7d053 | |||
| fe3381c85e | |||
| 4518614715 | |||
| b8f700dbc3 | |||
| 83aa1fe5b7 | |||
| 7e0b329e90 | |||
| 070629d380 | |||
| 6207c8bf2d | |||
| fbba779511 | |||
| b50bd7233c | |||
| 364329ec65 | |||
| 87d0bfb614 | |||
| 7863108ae3 | |||
| 1755b4ae62 | |||
| 034daaf52e | |||
| d267892e0d | |||
| 6bc095cc83 | |||
| 249b9c66c8 | |||
| 737f8d2cf2 | |||
| 4220932794 | |||
| d4cab9eef6 | |||
| 0a5f8a76be | |||
| 32eabcac91 | |||
| 7a644105f6 | |||
| 9de590ad96 | |||
| 2685825993 | |||
| cb8962fb25 | |||
| 4c0258d01f | |||
| ded1673fa6 | |||
| 5597676151 | |||
| dd880b1002 | |||
| 6eba79b3c5 | |||
| 299a93a87b | |||
| 5790fe6d94 | |||
| cda4be05e5 | |||
| 4e90a43364 | |||
| e81435a82f | |||
| dff44aec06 | |||
| b3499192a6 | |||
| e2b33d224a | |||
| c13b6cd619 |
@@ -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,12 @@
|
||||
name: "CodeQL config"
|
||||
|
||||
queries:
|
||||
- uses: security-extended
|
||||
|
||||
paths-ignore:
|
||||
- '**/test/**'
|
||||
- '**/tests/**'
|
||||
- '**/*Test.java'
|
||||
- '**/*Test.kt'
|
||||
- '**/build/**'
|
||||
- '.gradle/**'
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, next]
|
||||
pull_request:
|
||||
branches: [main, next]
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Weekly scan on Sunday at midnight
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: |
|
||||
8
|
||||
21
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: "java-kotlin"
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Build SDK
|
||||
run: ./gradlew build testClasses -x test --no-daemon --no-build-cache clean build
|
||||
@@ -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
|
||||
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.11"
|
||||
".": "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-1fcc324268478b6855afad6c2be9f95d44ecbfb80692c59c3aa766b0ee73c7ad.yml
|
||||
openapi_spec_hash: fea408ff4197ce234ee92f3be98213fa
|
||||
config_hash: 0169251649802409946a3235323cdb8d
|
||||
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.
|
||||
+739
@@ -1,5 +1,744 @@
|
||||
# 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)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([4b197f0](https://github.com/langchain-ai/langsmith-java/commit/4b197f03e8a2d7658d2e268d854114398429f7a9))
|
||||
* **api:** api update ([5703f0f](https://github.com/langchain-ai/langsmith-java/commit/5703f0f838d16543a4c518dc108065d4e72e58b6))
|
||||
* **api:** api update ([e2e98b7](https://github.com/langchain-ai/langsmith-java/commit/e2e98b7e9ab23ea0298e79cfc38a49797db0e978))
|
||||
* **api:** api update ([96a6948](https://github.com/langchain-ai/langsmith-java/commit/96a69483bf3e39b21a65dfb1af1eea6914939dfa))
|
||||
* **api:** api update ([8895f35](https://github.com/langchain-ai/langsmith-java/commit/8895f3571aafdffce373dac1052a4f26fd38347d))
|
||||
* **api:** api update ([9c66288](https://github.com/langchain-ai/langsmith-java/commit/9c66288568aceb6d7b77b8742dc34e132c122b66))
|
||||
* **api:** api update ([f73c866](https://github.com/langchain-ai/langsmith-java/commit/f73c86620f221064be73d86d9ce540f4e8aeb5c6))
|
||||
* **client:** add `HttpRequest#url()` method ([7c51e6a](https://github.com/langchain-ai/langsmith-java/commit/7c51e6a7139f20c181bc4349c247da8aaa151a89))
|
||||
* **client:** allow configuring dispatcher executor service ([4a50a22](https://github.com/langchain-ai/langsmith-java/commit/4a50a22f62527d90e5584b9c805b34cfa8c0a7c8))
|
||||
|
||||
## 0.1.0-alpha.19 (2025-12-10)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.18...v0.1.0-alpha.19](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.18...v0.1.0-alpha.19)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** api update ([c0d8bec](https://github.com/langchain-ai/langsmith-java/commit/c0d8bec69c14c9300a39737092b4a29210d4df59))
|
||||
* **api:** api update ([9a9d362](https://github.com/langchain-ai/langsmith-java/commit/9a9d362b6e509abc8257c4ff8d5f11911f3ca4c0))
|
||||
* **api:** api update ([e27bc6f](https://github.com/langchain-ai/langsmith-java/commit/e27bc6fd053032d0597a5dc4f4d636bada559748))
|
||||
* **api:** api update ([ae353d7](https://github.com/langchain-ai/langsmith-java/commit/ae353d775aa03a2f820c9d065f79528401963c2c))
|
||||
* **api:** api update ([fdb6021](https://github.com/langchain-ai/langsmith-java/commit/fdb6021eb3de21d7e3930dfe9f99a943100118b2))
|
||||
* **api:** api update ([da999c0](https://github.com/langchain-ai/langsmith-java/commit/da999c02267725a5d5c4fdbf17f9482ad5df638e))
|
||||
* **api:** api update ([daa7b07](https://github.com/langchain-ai/langsmith-java/commit/daa7b07fa755ad0d1feb6f6b3370c3bb5ff3c049))
|
||||
* **api:** api update ([4985a65](https://github.com/langchain-ai/langsmith-java/commit/4985a651acb592b38f4d94afda202a4ecddb6be2))
|
||||
* **api:** manual updates ([9ff3976](https://github.com/langchain-ai/langsmith-java/commit/9ff39760388f6cf4e68945ce536a0dd92d4f84bf))
|
||||
* **api:** manual updates ([aa705c4](https://github.com/langchain-ai/langsmith-java/commit/aa705c489b4a64d2815c5ceacde5161c7518afac))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** cancel okhttp call when future cancelled ([89ab779](https://github.com/langchain-ai/langsmith-java/commit/89ab7791a8e27347610bbecea44867c5b991073b))
|
||||
* fix trace input and output ([#83](https://github.com/langchain-ai/langsmith-java/issues/83)) ([7a64410](https://github.com/langchain-ai/langsmith-java/commit/7a644105f69db9d10064ba38c18ee294fc62f360))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **api:** delete index methods ([7e90f5f](https://github.com/langchain-ai/langsmith-java/commit/7e90f5f70e263458f594ce92ae9a7b1a29e81f85))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* remove `$` for better copy-pasteabality ([4163726](https://github.com/langchain-ai/langsmith-java/commit/4163726d6d496289f025006b9a03363caeb879c7))
|
||||
|
||||
## 0.1.0-alpha.18 (2025-12-09)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.17...v0.1.0-alpha.18](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.17...v0.1.0-alpha.18)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** add go target ([ff3556a](https://github.com/langchain-ai/langsmith-java/commit/ff3556a80ff2fc128c17d06cc78b366e27865239))
|
||||
* **api:** api update ([9a1ad24](https://github.com/langchain-ai/langsmith-java/commit/9a1ad247305770017095b760fa59c49883f6d204))
|
||||
* **api:** change paginated retrieve methods to list ([229ed3e](https://github.com/langchain-ai/langsmith-java/commit/229ed3e0214e1d73cd81e05e118f5f6c7be670a7))
|
||||
* **api:** pagination ([bd22dc6](https://github.com/langchain-ai/langsmith-java/commit/bd22dc6238f748edb0f0f70214f1a60b9087b525))
|
||||
* **api:** pagination read me ([eccc183](https://github.com/langchain-ai/langsmith-java/commit/eccc183d0ec53b8b06122598daadafd707e28be3))
|
||||
* **api:** readme example - change REPLACE_ME session_id to uuid ([a12609a](https://github.com/langchain-ai/langsmith-java/commit/a12609a69ae1d7d45a75f3032c74dc8a16c49242))
|
||||
* method counter in ci ([8bb864e](https://github.com/langchain-ai/langsmith-java/commit/8bb864eaeccd46cb8153836e7b25ccfb1ba79838))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* formatting error in record experiment example ([#81](https://github.com/langchain-ai/langsmith-java/issues/81)) ([243e318](https://github.com/langchain-ai/langsmith-java/commit/243e318f8defffda4b4116a8e2728c75f341dea7))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **internal:** version bump ([dcfaf78](https://github.com/langchain-ai/langsmith-java/commit/dcfaf785e20ada8e5c967a324aa23c9aa008372c))
|
||||
|
||||
## 0.1.0-alpha.17 (2025-11-20)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.16...v0.1.0-alpha.17](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.16...v0.1.0-alpha.17)
|
||||
|
||||
### Chores
|
||||
|
||||
* Configure workflow for CodeQL ([f0b024d](https://github.com/langchain-ai/langsmith-java/commit/f0b024dde59c5aab9c1da151061342d2586fe882))
|
||||
|
||||
## 0.1.0-alpha.16 (2025-11-20)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.15...v0.1.0-alpha.16](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.15...v0.1.0-alpha.16)
|
||||
|
||||
### Chores
|
||||
|
||||
* Update runner to ubuntu-latest-l-oss for Sonatype Publish ([5597676](https://github.com/langchain-ai/langsmith-java/commit/5597676151b5bd30087dcca93ae13f0e69714a16))
|
||||
|
||||
## 0.1.0-alpha.15 (2025-11-20)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.14...v0.1.0-alpha.15)
|
||||
|
||||
## 0.1.0-alpha.14 (2025-11-20)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.13...v0.1.0-alpha.14)
|
||||
|
||||
### Chores
|
||||
|
||||
* Update support portal link ([6a36fa7](https://github.com/langchain-ai/langsmith-java/commit/6a36fa7802d64d0fa0e3dc8f7f4958c2d9763f6c))
|
||||
|
||||
## 0.1.0-alpha.13 (2025-11-20)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.12...v0.1.0-alpha.13](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.12...v0.1.0-alpha.13)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** add rate limiting ([51fa6d7](https://github.com/langchain-ai/langsmith-java/commit/51fa6d7ec24088d1f5e4f7d64afa8b84ec0f32e1))
|
||||
* **api:** Update contact email ([20a193d](https://github.com/langchain-ai/langsmith-java/commit/20a193d472fb6ba20d990852e7d116b7d688e9cb))
|
||||
* **api:** Update docs to https://docs.smith.langchain.com/ ([f741f44](https://github.com/langchain-ai/langsmith-java/commit/f741f4487be9e79a4062bcb4e25f8605c2b6159c))
|
||||
|
||||
## 0.1.0-alpha.12 (2025-11-19)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.11...v0.1.0-alpha.12)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** Set targets.java.publish.docs: true ([aec9261](https://github.com/langchain-ai/langsmith-java/commit/aec9261033ddb4a438e0bd409e96c2cb0c056471))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* Move runner to ubuntu-latest-l to allow dokka to complete javadoc building ([05a9c63](https://github.com/langchain-ai/langsmith-java/commit/05a9c630d30918370524825769f9779a5925e9a3))
|
||||
|
||||
## 0.1.0-alpha.11 (2025-11-19)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.10...v0.1.0-alpha.11)
|
||||
|
||||
+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+.
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2025 langChain
|
||||
Copyright 2026 langChain
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -2,12 +2,21 @@
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
[](https://central.sonatype.com/artifact/com.langchain.smith/langsmith-java/0.1.0-alpha.11)
|
||||
[](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 -->
|
||||
|
||||
The [LangSmith](https://www.langchain.com/langsmith/observability) Java SDK provides convenient access to the LangSmith [REST API](https://api.smith.langchain.com/docs) from applications written in Java.
|
||||
|
||||
To learn more about LangSmith, check out the [docs](https://docs.smith.langchain.com/).
|
||||
|
||||
<!-- 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-beta.19).
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
## Installation
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
@@ -15,7 +24,7 @@ The [LangSmith](https://www.langchain.com/langsmith/observability) Java SDK prov
|
||||
### Gradle
|
||||
|
||||
```kotlin
|
||||
implementation("com.langchain.smith:langsmith-java:0.1.0-alpha.11")
|
||||
implementation("com.langchain.smith:langsmith-java:0.1.0-beta.19")
|
||||
```
|
||||
|
||||
### Maven
|
||||
@@ -24,7 +33,7 @@ implementation("com.langchain.smith:langsmith-java:0.1.0-alpha.11")
|
||||
<dependency>
|
||||
<groupId>com.langchain.smith</groupId>
|
||||
<artifactId>langsmith-java</artifactId>
|
||||
<version>0.1.0-alpha.11</version>
|
||||
<version>0.1.0-beta.19</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
@@ -39,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("REPLACE_ME")
|
||||
.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
|
||||
@@ -94,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();
|
||||
```
|
||||
|
||||
@@ -108,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();
|
||||
```
|
||||
|
||||
@@ -119,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();
|
||||
@@ -128,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.
|
||||
|
||||
@@ -161,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
|
||||
|
||||
@@ -178,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("REPLACE_ME")
|
||||
.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:
|
||||
@@ -199,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("REPLACE_ME")
|
||||
.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.
|
||||
@@ -294,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("REPLACE_ME")
|
||||
.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
|
||||
@@ -341,20 +333,131 @@ The SDK throws custom unchecked exception types:
|
||||
|
||||
- [`LangChainException`](langsmith-java-core/src/main/kotlin/com/langchain/smith/errors/LangChainException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
|
||||
|
||||
## Logging
|
||||
## Pagination
|
||||
|
||||
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
|
||||
The SDK defines methods that return a paginated lists of results. It provides convenient ways to access the results either one page at a time or item-by-item across all pages.
|
||||
|
||||
### Auto-pagination
|
||||
|
||||
To iterate through all results across all pages, use the `autoPager()` method, which automatically fetches more pages as needed.
|
||||
|
||||
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.runs.Run;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
RunQueryV2Page page = client.runs().queryV2();
|
||||
|
||||
// Process as an Iterable
|
||||
for (Run run : page.autoPager()) {
|
||||
System.out.println(run);
|
||||
}
|
||||
|
||||
// Process as a Stream
|
||||
page.autoPager()
|
||||
.stream()
|
||||
.limit(50)
|
||||
.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.runs.Run;
|
||||
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
CompletableFuture<RunQueryV2PageAsync> pageFuture = client.async().runs().queryV2();
|
||||
|
||||
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(Run run) {
|
||||
System.out.println(run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(Optional<Throwable> error) {
|
||||
if (error.isPresent()) {
|
||||
System.out.println("Something went wrong!");
|
||||
throw new RuntimeException(error.get());
|
||||
} else {
|
||||
System.out.println("No more!");
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Or use futures
|
||||
pageFuture.thenRun(page -> page.autoPager()
|
||||
.subscribe(run -> {
|
||||
System.out.println(run);
|
||||
})
|
||||
.onCompleteFuture()
|
||||
.whenComplete((unused, error) -> {
|
||||
if (error != null) {
|
||||
System.out.println("Something went wrong!");
|
||||
throw new RuntimeException(error);
|
||||
} else {
|
||||
System.out.println("No more!");
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### Manual pagination
|
||||
|
||||
To access individual page items and manually request the next page, use the `items()`,
|
||||
`hasNextPage()`, and `nextPage()` methods:
|
||||
|
||||
```java
|
||||
import com.langchain.smith.models.runs.Run;
|
||||
import com.langchain.smith.models.runs.RunQueryV2Page;
|
||||
|
||||
RunQueryV2Page page = client.runs().queryV2();
|
||||
while (true) {
|
||||
for (Run run : page.items()) {
|
||||
System.out.println(run);
|
||||
}
|
||||
|
||||
if (!page.hasNextPage()) {
|
||||
break;
|
||||
}
|
||||
|
||||
page = page.nextPage();
|
||||
}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
Enable logging by setting the `LANGCHAIN_LOG` environment variable to `info`:
|
||||
|
||||
```sh
|
||||
$ export LANGCHAIN_LOG=info
|
||||
export LANGCHAIN_LOG=info
|
||||
```
|
||||
|
||||
Or to `debug` for more verbose logging:
|
||||
|
||||
```sh
|
||||
$ export LANGCHAIN_LOG=debug
|
||||
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
|
||||
@@ -365,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).
|
||||
|
||||
@@ -374,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
|
||||
@@ -404,16 +509,14 @@ LangsmithClient client = LangsmithOkHttpClient.builder()
|
||||
|
||||
### Timeouts
|
||||
|
||||
Requests time out after 1 minute by default.
|
||||
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:
|
||||
@@ -449,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]
|
||||
@@ -516,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"))
|
||||
@@ -530,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();
|
||||
```
|
||||
|
||||
@@ -583,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();
|
||||
```
|
||||
@@ -600,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<>() {
|
||||
@@ -630,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);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -652,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:
|
||||
|
||||
@@ -18,6 +18,10 @@ before making any information public.
|
||||
If you encounter security issues that are not directly related to SDKs but pertain to the services
|
||||
or products provided by LangChain, please follow the respective company's security reporting guidelines.
|
||||
|
||||
### LangChain Terms and Policies
|
||||
|
||||
Please contact [support.langchain.com](http://support.langchain.com/) for any questions or concerns regarding the security of our services.
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping us keep the SDKs and systems they interact with secure.
|
||||
|
||||
@@ -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
|
||||
|
||||
+46
-1
@@ -1,6 +1,39 @@
|
||||
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.2.0"
|
||||
id("org.jetbrains.dokka-javadoc") version "2.2.0"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "com.langchain.smith"
|
||||
version = "0.1.0-alpha.11" // 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 {
|
||||
@@ -14,3 +47,15 @@ subprojects {
|
||||
description = "Verifies all source files are formatted."
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.dokka")
|
||||
apply(plugin = "org.jetbrains.dokka-javadoc")
|
||||
}
|
||||
|
||||
// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
|
||||
tasks.named("dokkaJavadocCollector").configure {
|
||||
subprojects.flatMap { it.tasks }
|
||||
.filter { it.project.name != "langsmith-java" && it.name == "dokkaJavadocJar" }
|
||||
.forEach { mustRunAfter(it) }
|
||||
}
|
||||
|
||||
@@ -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,28 +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("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 {
|
||||
@@ -44,3 +61,7 @@ configure<MavenPublishBaseExtension> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Zip>().configureEach {
|
||||
isZip64 = true
|
||||
}
|
||||
|
||||
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,12 +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("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")
|
||||
}
|
||||
|
||||
+119
-17
@@ -6,16 +6,21 @@ 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
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
@@ -44,16 +49,93 @@ class LangsmithOkHttpClient private constructor() {
|
||||
class Builder internal 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
|
||||
|
||||
/**
|
||||
* The executor service to use for running HTTP requests.
|
||||
*
|
||||
* Defaults to OkHttp's
|
||||
* [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
|
||||
*
|
||||
* This class takes ownership of the executor service and shuts it down when closed.
|
||||
*/
|
||||
fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
|
||||
this.dispatcherExecutorService = dispatcherExecutorService
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for calling [Builder.dispatcherExecutorService] with
|
||||
* `dispatcherExecutorService.orElse(null)`.
|
||||
*/
|
||||
fun dispatcherExecutorService(dispatcherExecutorService: Optional<ExecutorService>) =
|
||||
dispatcherExecutorService(dispatcherExecutorService.getOrNull())
|
||||
|
||||
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
|
||||
|
||||
/** 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.
|
||||
*
|
||||
@@ -121,6 +203,17 @@ class LangsmithOkHttpClient private constructor() {
|
||||
*/
|
||||
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
|
||||
|
||||
/**
|
||||
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
|
||||
*
|
||||
* Defaults to a dedicated cached thread pool.
|
||||
*
|
||||
* This class takes ownership of the executor and shuts it down, if possible, when closed.
|
||||
*/
|
||||
fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
|
||||
clientOptions.streamHandlerExecutor(streamHandlerExecutor)
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface to use for delaying execution, like during retries.
|
||||
*
|
||||
@@ -154,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.
|
||||
*/
|
||||
@@ -195,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)`. */
|
||||
@@ -205,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 {
|
||||
@@ -321,6 +419,10 @@ class LangsmithOkHttpClient private constructor() {
|
||||
OkHttpClient.builder()
|
||||
.timeout(clientOptions.timeout())
|
||||
.proxy(proxy)
|
||||
.proxyAuthenticator(proxyAuthenticator)
|
||||
.maxIdleConnections(maxIdleConnections)
|
||||
.keepAliveDuration(keepAliveDuration)
|
||||
.dispatcherExecutorService(dispatcherExecutorService)
|
||||
.sslSocketFactory(sslSocketFactory)
|
||||
.trustManager(trustManager)
|
||||
.hostnameVerifier(hostnameVerifier)
|
||||
|
||||
+119
-17
@@ -6,16 +6,21 @@ 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
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
@@ -44,16 +49,93 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
class Builder internal 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
|
||||
|
||||
/**
|
||||
* The executor service to use for running HTTP requests.
|
||||
*
|
||||
* Defaults to OkHttp's
|
||||
* [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
|
||||
*
|
||||
* This class takes ownership of the executor service and shuts it down when closed.
|
||||
*/
|
||||
fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
|
||||
this.dispatcherExecutorService = dispatcherExecutorService
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for calling [Builder.dispatcherExecutorService] with
|
||||
* `dispatcherExecutorService.orElse(null)`.
|
||||
*/
|
||||
fun dispatcherExecutorService(dispatcherExecutorService: Optional<ExecutorService>) =
|
||||
dispatcherExecutorService(dispatcherExecutorService.getOrNull())
|
||||
|
||||
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
|
||||
|
||||
/** 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.
|
||||
*
|
||||
@@ -121,6 +203,17 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
*/
|
||||
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
|
||||
|
||||
/**
|
||||
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
|
||||
*
|
||||
* Defaults to a dedicated cached thread pool.
|
||||
*
|
||||
* This class takes ownership of the executor and shuts it down, if possible, when closed.
|
||||
*/
|
||||
fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
|
||||
clientOptions.streamHandlerExecutor(streamHandlerExecutor)
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface to use for delaying execution, like during retries.
|
||||
*
|
||||
@@ -154,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.
|
||||
*/
|
||||
@@ -195,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)`. */
|
||||
@@ -205,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 {
|
||||
@@ -321,6 +419,10 @@ class LangsmithOkHttpClientAsync private constructor() {
|
||||
OkHttpClient.builder()
|
||||
.timeout(clientOptions.timeout())
|
||||
.proxy(proxy)
|
||||
.proxyAuthenticator(proxyAuthenticator)
|
||||
.maxIdleConnections(maxIdleConnections)
|
||||
.keepAliveDuration(keepAliveDuration)
|
||||
.dispatcherExecutorService(dispatcherExecutorService)
|
||||
.sslSocketFactory(sslSocketFactory)
|
||||
.trustManager(trustManager)
|
||||
.hostnameVerifier(hostnameVerifier)
|
||||
|
||||
+222
-117
@@ -8,17 +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
|
||||
@@ -26,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(private val okHttpClient: okhttp3.OkHttpClient) :
|
||||
HttpClient {
|
||||
class OkHttpClient
|
||||
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 {
|
||||
@@ -50,20 +60,25 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
): CompletableFuture<HttpResponse> {
|
||||
val future = CompletableFuture<HttpResponse>()
|
||||
|
||||
request.body?.run { future.whenComplete { _, _ -> close() } }
|
||||
|
||||
newCall(request, requestOptions)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
future.complete(response.toResponse())
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
future.completeExceptionally(LangChainIoException("Request failed", e))
|
||||
}
|
||||
val call = newCall(request, requestOptions)
|
||||
call.enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
future.complete(response.toHttpResponse())
|
||||
}
|
||||
)
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
future.completeExceptionally(LangChainIoException("Request failed", e))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
future.whenComplete { _, e ->
|
||||
if (e is CancellationException) {
|
||||
call.cancel()
|
||||
}
|
||||
request.body?.close()
|
||||
}
|
||||
|
||||
return future
|
||||
}
|
||||
@@ -77,23 +92,6 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
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-Tenant-Id")
|
||||
redactHeader("Authorization")
|
||||
redactHeader("X-Organization-Id")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
requestOptions.timeout?.let {
|
||||
clientBuilder
|
||||
.connectTimeout(it.connect())
|
||||
@@ -106,89 +104,6 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
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()
|
||||
}
|
||||
@@ -197,6 +112,10 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
|
||||
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
|
||||
private var hostnameVerifier: HostnameVerifier? = null
|
||||
@@ -207,6 +126,36 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
|
||||
this.sslSocketFactory = sslSocketFactory
|
||||
}
|
||||
@@ -222,12 +171,45 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
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) {
|
||||
@@ -249,3 +231,126 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.langchain.smith.client.okhttp
|
||||
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.*
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest
|
||||
import com.langchain.smith.core.http.HttpMethod
|
||||
import com.langchain.smith.core.http.HttpRequest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.ResourceLock
|
||||
|
||||
@WireMockTest
|
||||
@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
|
||||
internal class OkHttpClientTest {
|
||||
|
||||
private lateinit var baseUrl: String
|
||||
private lateinit var httpClient: OkHttpClient
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) {
|
||||
baseUrl = wmRuntimeInfo.httpBaseUrl
|
||||
httpClient = OkHttpClient.builder().build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() {
|
||||
stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
|
||||
val responseFuture =
|
||||
httpClient.executeAsync(
|
||||
HttpRequest.builder()
|
||||
.method(HttpMethod.POST)
|
||||
.baseUrl(baseUrl)
|
||||
.addPathSegment("something")
|
||||
.build()
|
||||
)
|
||||
val call = httpClient.okHttpClient.dispatcher.runningCalls().single()
|
||||
|
||||
responseFuture.cancel(false)
|
||||
|
||||
// Should have cancelled the underlying call
|
||||
assertThat(call.isCanceled()).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -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,143 +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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-539
@@ -1,539 +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");
|
||||
logger.error(" This usually indicates a network error or authentication problem");
|
||||
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");
|
||||
logger.error(" This usually indicates a network error or authentication problem");
|
||||
logger.error(" Check your LANGSMITH_API_KEY and network connectivity");
|
||||
}
|
||||
});
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
-899
@@ -1,899 +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.chat.completions.ChatCompletion;
|
||||
import com.openai.models.chat.completions.ChatCompletionChunk;
|
||||
import com.openai.models.chat.completions.ChatCompletionCreateParams;
|
||||
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
|
||||
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
|
||||
import com.openai.models.chat.completions.StructuredChatCompletion;
|
||||
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
|
||||
import com.openai.services.blocking.ChatService;
|
||||
import com.openai.services.blocking.chat.ChatCompletionService;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import java.util.function.Consumer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Wrapped ChatService that adds OpenTelemetry tracing to chat completion
|
||||
* operations.
|
||||
*/
|
||||
class WrappedChatService implements ChatService {
|
||||
|
||||
private final ChatService delegate;
|
||||
|
||||
WrappedChatService(ChatService delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatService.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatService withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return new WrappedChatService(delegate.withOptions(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionService completions() {
|
||||
return new WrappedChatCompletionService(delegate.completions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapped ChatCompletionService that adds tracing to create operations.
|
||||
*/
|
||||
private static class WrappedChatCompletionService implements ChatCompletionService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WrappedChatCompletionService.class);
|
||||
|
||||
private final ChatCompletionService delegate;
|
||||
|
||||
WrappedChatCompletionService(ChatCompletionService delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionService.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionService withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return new WrappedChatCompletionService(delegate.withOptions(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.chat.completions.MessageService messages() {
|
||||
return delegate.messages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion create(ChatCompletionCreateParams params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion create(ChatCompletionCreateParams params, RequestOptions requestOptions) {
|
||||
// Extract model from params for span naming
|
||||
String model = params.model() != null ? params.model().toString() : null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "chat").startSpan();
|
||||
|
||||
// Debug: Check if span is recording (not a noop span)
|
||||
if (logger.isDebugEnabled()) {
|
||||
boolean isRecording = span.isRecording();
|
||||
logger.debug(
|
||||
"[WrappedChatService] Created span: {}, isRecording: {}, traceId: {}",
|
||||
span.getSpanContext().getSpanId(),
|
||||
isRecording,
|
||||
span.getSpanContext().getTraceId());
|
||||
}
|
||||
|
||||
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)
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
Long maxTokens = params.maxCompletionTokens().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
|
||||
|
||||
// Capture input messages in JSON format
|
||||
String inputMessagesJson = formatInputMessages(params);
|
||||
TracingUtils.setInputMessages(span, inputMessagesJson);
|
||||
|
||||
ChatCompletion 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 and finish reason
|
||||
String responseModel = result.model();
|
||||
String finishReason = !result.choices().isEmpty()
|
||||
? result.choices().get(0).finishReason().toString()
|
||||
: "stop";
|
||||
TracingUtils.setResponseMetadata(span, responseModel, finishReason);
|
||||
|
||||
// Capture output messages in JSON format
|
||||
String outputMessagesJson = formatOutputMessages(result);
|
||||
TracingUtils.setOutputMessages(span, outputMessagesJson);
|
||||
|
||||
// Extract usage information from result
|
||||
result.usage().ifPresent(usage -> {
|
||||
TracingUtils.setResponseAttributes(
|
||||
span, (long) usage.promptTokens(), (long) usage.completionTokens(), (long)
|
||||
usage.totalTokens());
|
||||
});
|
||||
|
||||
// Create tool call spans for any tool calls in the response
|
||||
createToolCallSpans(result, span);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[WrappedChatService] Ending span: {}",
|
||||
span.getSpanContext().getSpanId());
|
||||
}
|
||||
span.end();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[WrappedChatService] Span ended: {}",
|
||||
span.getSpanContext().getSpanId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input messages from ChatCompletionCreateParams as a JSON array
|
||||
* string.
|
||||
*
|
||||
* <p>
|
||||
* Properly formats messages as JSON with role and content, following
|
||||
* LangSmith conventions.
|
||||
*/
|
||||
private String formatInputMessages(ChatCompletionCreateParams params) {
|
||||
if (params.messages().isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] Processing {} message(s)",
|
||||
params.messages().size());
|
||||
}
|
||||
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
boolean first = true;
|
||||
for (com.openai.models.chat.completions.ChatCompletionMessageParam messageParam : params.messages()) {
|
||||
if (!first) {
|
||||
json.append(",");
|
||||
}
|
||||
first = false;
|
||||
|
||||
json.append("{");
|
||||
|
||||
String role = null;
|
||||
String content = null;
|
||||
|
||||
// Try to extract role and content based on message type
|
||||
// The OpenAI SDK uses different message types (UserMessage, SystemMessage,
|
||||
// etc.)
|
||||
String className = messageParam.getClass().getSimpleName();
|
||||
String fullClassName = messageParam.getClass().getName();
|
||||
|
||||
// Debug logging
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[formatInputMessages] Processing message: {}", fullClassName);
|
||||
java.lang.reflect.Method[] allMethods =
|
||||
messageParam.getClass().getMethods();
|
||||
logger.debug(
|
||||
"[formatInputMessages] Available methods: {}",
|
||||
java.util.Arrays.stream(allMethods)
|
||||
.map(m -> m.getName() + "(" + m.getParameterCount() + ")")
|
||||
.collect(java.util.stream.Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
// ChatCompletionMessageParam is a union type - check type FIRST before calling
|
||||
// as*() methods
|
||||
// This prevents InvocationTargetException when calling asUser() on non-user
|
||||
// messages
|
||||
Object actualMessage = null;
|
||||
|
||||
try {
|
||||
// Use type checking methods first to avoid exceptions
|
||||
java.lang.reflect.Method isUserMethod =
|
||||
messageParam.getClass().getMethod("isUser");
|
||||
java.lang.reflect.Method isSystemMethod =
|
||||
messageParam.getClass().getMethod("isSystem");
|
||||
java.lang.reflect.Method isAssistantMethod =
|
||||
messageParam.getClass().getMethod("isAssistant");
|
||||
java.lang.reflect.Method isToolMethod =
|
||||
messageParam.getClass().getMethod("isTool");
|
||||
|
||||
boolean isUser = (Boolean) isUserMethod.invoke(messageParam);
|
||||
boolean isSystem = (Boolean) isSystemMethod.invoke(messageParam);
|
||||
boolean isAssistant = (Boolean) isAssistantMethod.invoke(messageParam);
|
||||
boolean isTool = (Boolean) isToolMethod.invoke(messageParam);
|
||||
|
||||
if (isUser) {
|
||||
java.lang.reflect.Method asUserMethod =
|
||||
messageParam.getClass().getMethod("asUser");
|
||||
actualMessage = asUserMethod.invoke(messageParam);
|
||||
role = "user";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] Found user message: {}",
|
||||
actualMessage.getClass().getName());
|
||||
}
|
||||
} else if (isSystem) {
|
||||
java.lang.reflect.Method asSystemMethod =
|
||||
messageParam.getClass().getMethod("asSystem");
|
||||
actualMessage = asSystemMethod.invoke(messageParam);
|
||||
role = "system";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] Found system message: {}",
|
||||
actualMessage.getClass().getName());
|
||||
}
|
||||
} else if (isAssistant) {
|
||||
java.lang.reflect.Method asAssistantMethod =
|
||||
messageParam.getClass().getMethod("asAssistant");
|
||||
actualMessage = asAssistantMethod.invoke(messageParam);
|
||||
role = "assistant";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] Found assistant message: {}",
|
||||
actualMessage.getClass().getName());
|
||||
}
|
||||
} else if (isTool) {
|
||||
java.lang.reflect.Method asToolMethod =
|
||||
messageParam.getClass().getMethod("asTool");
|
||||
actualMessage = asToolMethod.invoke(messageParam);
|
||||
role = "tool";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] Found tool message: {}",
|
||||
actualMessage.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Now get content from the actual message object
|
||||
if (actualMessage != null) {
|
||||
try {
|
||||
java.lang.reflect.Method contentMethod =
|
||||
actualMessage.getClass().getMethod("content");
|
||||
Object contentResult = contentMethod.invoke(actualMessage);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] content() returned: {}",
|
||||
contentResult != null
|
||||
? contentResult.getClass().getName()
|
||||
: "null");
|
||||
}
|
||||
|
||||
// Content might be a Content object with text() method
|
||||
if (contentResult != null) {
|
||||
try {
|
||||
java.lang.reflect.Method textMethod =
|
||||
contentResult.getClass().getMethod("text");
|
||||
Object textResult = textMethod.invoke(contentResult);
|
||||
if (textResult instanceof java.util.Optional) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Optional<String> textOpt = (java.util.Optional<String>) textResult;
|
||||
if (textOpt.isPresent()) {
|
||||
content = textOpt.get();
|
||||
}
|
||||
} else if (textResult instanceof String) {
|
||||
content = (String) textResult;
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Content might be a String directly
|
||||
if (contentResult instanceof String) {
|
||||
content = (String) contentResult;
|
||||
} else if (contentResult instanceof java.util.Optional) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Optional<String> contentOpt =
|
||||
(java.util.Optional<String>) contentResult;
|
||||
if (contentOpt.isPresent()) {
|
||||
content = contentOpt.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[formatInputMessages] No content() method on actual message");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[formatInputMessages] Error accessing message", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Role should already be set from asUser/asSystem/asAssistant above
|
||||
// If not, try to get it from the message
|
||||
if (role == null) {
|
||||
try {
|
||||
java.lang.reflect.Method roleMethod =
|
||||
messageParam.getClass().getMethod("role");
|
||||
Object roleResult = roleMethod.invoke(messageParam);
|
||||
if (roleResult != null) {
|
||||
role = roleResult.toString().toLowerCase();
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Fallback: determine role from class name
|
||||
if (className.contains("User") || fullClassName.contains("User")) {
|
||||
role = "user";
|
||||
} else if (className.contains("System") || fullClassName.contains("System")) {
|
||||
role = "system";
|
||||
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
|
||||
role = "assistant";
|
||||
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
|
||||
role = "tool";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[formatInputMessages] Error calling role()", e);
|
||||
}
|
||||
// Fallback to class name
|
||||
if (className.contains("User") || fullClassName.contains("User")) {
|
||||
role = "user";
|
||||
} else if (className.contains("System") || fullClassName.contains("System")) {
|
||||
role = "system";
|
||||
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
|
||||
role = "assistant";
|
||||
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
|
||||
role = "tool";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: try toString parsing
|
||||
if (content == null) {
|
||||
String messageStr = messageParam.toString();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[formatInputMessages] toString(): {}", messageStr);
|
||||
}
|
||||
|
||||
// For tool messages, look for content=Content{text={...}} pattern
|
||||
if (role != null && role.equals("tool")) {
|
||||
// Extract JSON content from tool messages:
|
||||
// content=Content{text={"key":"value"}}
|
||||
java.util.regex.Pattern toolContentPattern =
|
||||
java.util.regex.Pattern.compile("text=\\{([^}]+)\\}");
|
||||
java.util.regex.Matcher toolMatcher = toolContentPattern.matcher(messageStr);
|
||||
if (toolMatcher.find()) {
|
||||
content = "{" + toolMatcher.group(1) + "}";
|
||||
} else {
|
||||
// Try simpler pattern: text="..."
|
||||
java.util.regex.Pattern simplePattern =
|
||||
java.util.regex.Pattern.compile("text=[\"']([^\"']+)[\"']");
|
||||
java.util.regex.Matcher simpleMatcher = simplePattern.matcher(messageStr);
|
||||
if (simpleMatcher.find()) {
|
||||
content = simpleMatcher.group(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Improved extraction: try multiple patterns
|
||||
if (content == null) {
|
||||
// Pattern 1: content="..."
|
||||
int contentIdx = messageStr.indexOf("content=");
|
||||
if (contentIdx >= 0) {
|
||||
// Look for Content{text="..."} pattern
|
||||
int textIdx = messageStr.indexOf("text=", contentIdx);
|
||||
if (textIdx > contentIdx) {
|
||||
int start = messageStr.indexOf("\"", textIdx);
|
||||
if (start >= 0) {
|
||||
// Find matching closing quote, handling escaped quotes
|
||||
int end = start + 1;
|
||||
while (end < messageStr.length() && messageStr.charAt(end) != '"') {
|
||||
if (messageStr.charAt(end) == '\\') {
|
||||
end += 2; // Skip escaped character
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
if (end < messageStr.length()) {
|
||||
content = messageStr.substring(start + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simple quote matching
|
||||
if (content == null) {
|
||||
int start = messageStr.indexOf("\"", contentIdx);
|
||||
if (start >= 0) {
|
||||
int end = messageStr.indexOf("\"", start + 1);
|
||||
if (end > start) {
|
||||
content = messageStr.substring(start + 1, end);
|
||||
}
|
||||
} else {
|
||||
// Try single quotes
|
||||
start = messageStr.indexOf("'", contentIdx);
|
||||
if (start >= 0) {
|
||||
int end = messageStr.indexOf("'", start + 1);
|
||||
if (end > start) {
|
||||
content = messageStr.substring(start + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Look for content in JSON-like format
|
||||
if (content == null) {
|
||||
java.util.regex.Pattern pattern =
|
||||
java.util.regex.Pattern.compile("content[=:]\\s*[\"']([^\"']+)[\"']");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
|
||||
if (matcher.find()) {
|
||||
content = matcher.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: If still null, try to extract from the end of the string
|
||||
if (content == null && messageStr.length() > 0) {
|
||||
// Last resort: try to find any quoted string that might be content
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[\"']([^\"']+)[\"']");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
|
||||
if (matcher.find()) {
|
||||
String potentialContent = matcher.group(1);
|
||||
// Only use if it looks like actual content (not a class name or role)
|
||||
if (!potentialContent.contains("com.openai")
|
||||
&& !potentialContent.equals("user")
|
||||
&& !potentialContent.equals("system")
|
||||
&& !potentialContent.equals("assistant")
|
||||
&& !potentialContent.equals("tool")
|
||||
&& potentialContent.length() > 0) {
|
||||
content = potentialContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build JSON
|
||||
if (role != null) {
|
||||
json.append("\"role\":\"").append(role).append("\"");
|
||||
}
|
||||
if (content != null) {
|
||||
if (role != null) {
|
||||
json.append(",");
|
||||
}
|
||||
json.append("\"content\":\"")
|
||||
.append(TracingUtils.escapeJsonString(content))
|
||||
.append("\"");
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[formatInputMessages] Final: role={}, content={}",
|
||||
role,
|
||||
content != null ? content.substring(0, Math.min(50, content.length())) : "null");
|
||||
}
|
||||
|
||||
json.append("}");
|
||||
}
|
||||
json.append("]");
|
||||
|
||||
String result = json.toString();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[formatInputMessages] Final JSON: {}", result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates tool call spans for any tool calls detected in the chat completion
|
||||
* response.
|
||||
*
|
||||
* @param completion the chat completion response
|
||||
* @param parentSpan the parent span (the chat completion span)
|
||||
*/
|
||||
private void createToolCallSpans(ChatCompletion completion, Span parentSpan) {
|
||||
if (completion.choices() == null || completion.choices().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
|
||||
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
|
||||
|
||||
// Check if message has tool calls
|
||||
java.util.Optional<java.util.List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
|
||||
if (toolCallsOpt.isPresent()) {
|
||||
java.util.List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
|
||||
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
|
||||
// Check if it's a function tool call
|
||||
if (toolCall.isFunction()) {
|
||||
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
|
||||
createToolCallSpan(functionToolCall, parentSpan);
|
||||
}
|
||||
// Note: Custom tool calls are not yet supported
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single tool call span from a function tool call object.
|
||||
*
|
||||
* @param functionToolCall the function tool call object from the OpenAI SDK
|
||||
* @param parentSpan the parent span (the chat completion span)
|
||||
*/
|
||||
private void createToolCallSpan(ChatCompletionMessageFunctionToolCall functionToolCall, Span parentSpan) {
|
||||
try {
|
||||
io.opentelemetry.api.OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
io.opentelemetry.api.trace.Tracer tracer = openTelemetry.getTracer("langsmith-java-otel-wrappers");
|
||||
|
||||
// Extract tool call information directly from the SDK objects
|
||||
String toolCallId = functionToolCall.id();
|
||||
com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall.Function function =
|
||||
functionToolCall.function();
|
||||
String toolName = function.name();
|
||||
String toolArguments = function.arguments();
|
||||
|
||||
// Create span name
|
||||
String spanName = toolName != null ? "tool_call " + toolName : "tool_call";
|
||||
|
||||
// Create tool call span as a child of the parent span
|
||||
io.opentelemetry.api.trace.Span toolCallSpan = tracer.spanBuilder(spanName)
|
||||
.setSpanKind(io.opentelemetry.api.trace.SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.operation.name", "tool_call")
|
||||
.setAttribute("langsmith.span.kind", "tool")
|
||||
.setAttribute("gen_ai.system", "openai")
|
||||
.setAttribute("gen_ai.provider.name", "openai")
|
||||
.startSpan();
|
||||
|
||||
try (io.opentelemetry.context.Scope toolScope = toolCallSpan.makeCurrent()) {
|
||||
// Set tool call attributes
|
||||
if (toolCallId != null) {
|
||||
toolCallSpan.setAttribute("gen_ai.tool.call.id", toolCallId);
|
||||
}
|
||||
if (toolName != null) {
|
||||
toolCallSpan.setAttribute("gen_ai.tool.name", toolName);
|
||||
toolCallSpan.setAttribute("langsmith.trace.name", "Tool Call: " + toolName);
|
||||
}
|
||||
if (toolArguments != null && !toolArguments.isEmpty()) {
|
||||
toolCallSpan.setAttribute("gen_ai.tool.arguments", toolArguments);
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"[WrappedChatService] Created tool call span: {}, tool={}, arguments={}, parent={}",
|
||||
toolCallSpan.getSpanContext().getSpanId(),
|
||||
toolName,
|
||||
toolArguments,
|
||||
parentSpan.getSpanContext().getSpanId());
|
||||
}
|
||||
|
||||
// Note: Tool call result would be set when the tool is actually executed
|
||||
// This span represents the tool call request, not the execution
|
||||
} finally {
|
||||
toolCallSpan.end();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("[WrappedChatService] Error creating tool call span", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats output messages from ChatCompletion as a JSON array string.
|
||||
*/
|
||||
private String formatOutputMessages(ChatCompletion completion) {
|
||||
if (completion.choices() == null || completion.choices().isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
boolean first = true;
|
||||
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
|
||||
if (!first) {
|
||||
json.append(",");
|
||||
}
|
||||
first = false;
|
||||
|
||||
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
|
||||
json.append("{");
|
||||
|
||||
// Add role
|
||||
json.append("\"role\":\"assistant\"");
|
||||
|
||||
// Add content if present
|
||||
message.content().ifPresent(content -> {
|
||||
json.append(",\"content\":\"")
|
||||
.append(TracingUtils.escapeJsonString(content))
|
||||
.append("\"");
|
||||
});
|
||||
|
||||
json.append("}");
|
||||
}
|
||||
json.append("]");
|
||||
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredChatCompletion<T> create(StructuredChatCompletionCreateParams<T> params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredChatCompletion<T> create(
|
||||
StructuredChatCompletionCreateParams<T> params, RequestOptions requestOptions) {
|
||||
// Get model from the underlying params - StructuredChatCompletionCreateParams
|
||||
// wraps ChatCompletionCreateParams
|
||||
String model = params != null
|
||||
&& params.rawParams() != null
|
||||
&& params.rawParams().model() != null
|
||||
? params.rawParams().model().toString()
|
||||
: null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "chat").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
|
||||
if (params.rawParams() != null) {
|
||||
Double temperature = params.rawParams().temperature().orElse(null);
|
||||
Double topP = params.rawParams().topP().orElse(null);
|
||||
Long maxTokens = params.rawParams().maxCompletionTokens().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
|
||||
|
||||
// Capture input messages in JSON format
|
||||
String inputMessagesJson = formatInputMessages(params.rawParams());
|
||||
TracingUtils.setInputMessages(span, inputMessagesJson);
|
||||
}
|
||||
|
||||
StructuredChatCompletion<T> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// For structured completions, we'll just set basic model info
|
||||
// The actual structured output is in the parsed result
|
||||
TracingUtils.setResponseMetadata(span, model, null);
|
||||
|
||||
result.usage().ifPresent(usage -> {
|
||||
TracingUtils.setResponseAttributes(
|
||||
span, (long) usage.promptTokens(), (long) usage.completionTokens(), (long)
|
||||
usage.totalTokens());
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ChatCompletionChunk> createStreaming(ChatCompletionCreateParams params) {
|
||||
return createStreaming(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ChatCompletionChunk> createStreaming(
|
||||
ChatCompletionCreateParams params, RequestOptions requestOptions) {
|
||||
String model = params.model() != null ? params.model().toString() : null;
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "chat").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)
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
Long maxTokens = params.maxCompletionTokens().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
|
||||
|
||||
// Capture input messages in JSON format
|
||||
String inputMessagesJson = formatInputMessages(params);
|
||||
TracingUtils.setInputMessages(span, inputMessagesJson);
|
||||
|
||||
// For streaming, we can't easily extract usage info, so we'll just mark it as
|
||||
// streaming
|
||||
StreamResponse<ChatCompletionChunk> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.createStreaming(params);
|
||||
} else {
|
||||
result = delegate.createStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
// Note: For streaming, the span will end immediately
|
||||
// This is a simplified implementation - in production you might want to
|
||||
// wrap the stream to collect the full response
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate other methods without tracing (for now)
|
||||
@Override
|
||||
public ChatCompletion retrieve(String completionId) {
|
||||
return delegate.retrieve(completionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(
|
||||
String completionId, com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
|
||||
return delegate.retrieve(completionId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(String completionId, RequestOptions requestOptions) {
|
||||
return delegate.retrieve(completionId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(
|
||||
String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieve(completionId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
|
||||
return delegate.retrieve(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(
|
||||
com.openai.models.chat.completions.ChatCompletionRetrieveParams params, RequestOptions requestOptions) {
|
||||
return delegate.retrieve(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(
|
||||
String completionId, com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
|
||||
return delegate.update(completionId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(
|
||||
String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionUpdateParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.update(completionId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
|
||||
return delegate.update(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(
|
||||
com.openai.models.chat.completions.ChatCompletionUpdateParams params, RequestOptions requestOptions) {
|
||||
return delegate.update(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list() {
|
||||
return delegate.list();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list(RequestOptions requestOptions) {
|
||||
return delegate.list(requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list(
|
||||
com.openai.models.chat.completions.ChatCompletionListParams params) {
|
||||
return delegate.list(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list(
|
||||
com.openai.models.chat.completions.ChatCompletionListParams params, RequestOptions requestOptions) {
|
||||
return delegate.list(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId) {
|
||||
return delegate.delete(completionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
String completionId, RequestOptions requestOptions) {
|
||||
return delegate.delete(completionId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
String completionId, com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
|
||||
return delegate.delete(completionId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.delete(completionId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
|
||||
return delegate.delete(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params, RequestOptions requestOptions) {
|
||||
return delegate.delete(params, requestOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
-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")
|
||||
@@ -0,0 +1,21 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core
|
||||
|
||||
import java.util.stream.Stream
|
||||
import java.util.stream.StreamSupport
|
||||
|
||||
class AutoPager<T> private constructor(private val firstPage: Page<T>) : Iterable<T> {
|
||||
|
||||
companion object {
|
||||
|
||||
fun <T> from(firstPage: Page<T>): AutoPager<T> = AutoPager(firstPage)
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> =
|
||||
generateSequence(firstPage) { if (it.hasNextPage()) it.nextPage() else null }
|
||||
.flatMap { it.items() }
|
||||
.iterator()
|
||||
|
||||
fun stream(): Stream<T> = StreamSupport.stream(spliterator(), false)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core
|
||||
|
||||
import com.langchain.smith.core.http.AsyncStreamResponse
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CompletionException
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class AutoPagerAsync<T>
|
||||
private constructor(private val firstPage: PageAsync<T>, private val defaultExecutor: Executor) :
|
||||
AsyncStreamResponse<T> {
|
||||
|
||||
companion object {
|
||||
|
||||
fun <T> from(firstPage: PageAsync<T>, defaultExecutor: Executor): AutoPagerAsync<T> =
|
||||
AutoPagerAsync(firstPage, defaultExecutor)
|
||||
}
|
||||
|
||||
private val onCompleteFuture = CompletableFuture<Void?>()
|
||||
private val state = AtomicReference(State.NEW)
|
||||
|
||||
override fun subscribe(handler: AsyncStreamResponse.Handler<T>): AsyncStreamResponse<T> =
|
||||
subscribe(handler, defaultExecutor)
|
||||
|
||||
override fun subscribe(
|
||||
handler: AsyncStreamResponse.Handler<T>,
|
||||
executor: Executor,
|
||||
): AsyncStreamResponse<T> = apply {
|
||||
// TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
|
||||
check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
|
||||
if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
|
||||
else "Cannot subscribe after the response is closed"
|
||||
}
|
||||
|
||||
fun PageAsync<T>.handle(): CompletableFuture<Void?> {
|
||||
if (state.get() == State.CLOSED) {
|
||||
return CompletableFuture.completedFuture(null)
|
||||
}
|
||||
|
||||
items().forEach { handler.onNext(it) }
|
||||
return if (hasNextPage()) nextPage().thenCompose { it.handle() }
|
||||
else CompletableFuture.completedFuture(null)
|
||||
}
|
||||
|
||||
executor.execute {
|
||||
firstPage.handle().whenComplete { _, error ->
|
||||
val actualError =
|
||||
if (error is CompletionException && error.cause != null) error.cause else error
|
||||
try {
|
||||
handler.onComplete(Optional.ofNullable(actualError))
|
||||
} finally {
|
||||
try {
|
||||
if (actualError == null) {
|
||||
onCompleteFuture.complete(null)
|
||||
} else {
|
||||
onCompleteFuture.completeExceptionally(actualError)
|
||||
}
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompleteFuture(): CompletableFuture<Void?> = onCompleteFuture
|
||||
|
||||
override fun close() {
|
||||
val previousState = state.getAndSet(State.CLOSED)
|
||||
if (previousState == State.CLOSED) {
|
||||
return
|
||||
}
|
||||
|
||||
// When the stream is closed, we should always consider it closed. If it closed due
|
||||
// to an error, then we will have already completed the future earlier, and this
|
||||
// will be a no-op.
|
||||
onCompleteFuture.complete(null)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class State {
|
||||
NEW,
|
||||
SUBSCRIBED,
|
||||
CLOSED,
|
||||
}
|
||||
@@ -3,14 +3,21 @@
|
||||
package com.langchain.smith.core
|
||||
|
||||
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
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
/** A class representing the SDK client configuration. */
|
||||
@@ -40,6 +47,14 @@ private constructor(
|
||||
* rarely needs to be overridden.
|
||||
*/
|
||||
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
|
||||
/**
|
||||
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
|
||||
*
|
||||
* Defaults to a dedicated cached thread pool.
|
||||
*
|
||||
* This class takes ownership of the executor and shuts it down, if possible, when closed.
|
||||
*/
|
||||
@get:JvmName("streamHandlerExecutor") val streamHandlerExecutor: Executor,
|
||||
/**
|
||||
* The interface to use for delaying execution, like during retries.
|
||||
*
|
||||
@@ -66,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.
|
||||
*/
|
||||
@@ -93,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 {
|
||||
@@ -116,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 {
|
||||
@@ -154,6 +174,7 @@ private constructor(
|
||||
private var httpClient: HttpClient? = null
|
||||
private var checkJacksonVersionCompatibility: Boolean = true
|
||||
private var jsonMapper: JsonMapper = jsonMapper()
|
||||
private var streamHandlerExecutor: Executor? = null
|
||||
private var sleeper: Sleeper? = null
|
||||
private var clock: Clock = Clock.systemUTC()
|
||||
private var baseUrl: String? = null
|
||||
@@ -162,16 +183,19 @@ 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 {
|
||||
httpClient = clientOptions.originalHttpClient
|
||||
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
|
||||
jsonMapper = clientOptions.jsonMapper
|
||||
streamHandlerExecutor = clientOptions.streamHandlerExecutor
|
||||
sleeper = clientOptions.sleeper
|
||||
clock = clientOptions.clock
|
||||
baseUrl = clientOptions.baseUrl
|
||||
@@ -180,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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,6 +242,20 @@ private constructor(
|
||||
*/
|
||||
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
|
||||
|
||||
/**
|
||||
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
|
||||
*
|
||||
* Defaults to a dedicated cached thread pool.
|
||||
*
|
||||
* This class takes ownership of the executor and shuts it down, if possible, when closed.
|
||||
*/
|
||||
fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
|
||||
this.streamHandlerExecutor =
|
||||
if (streamHandlerExecutor is ExecutorService)
|
||||
PhantomReachableExecutorService(streamHandlerExecutor)
|
||||
else streamHandlerExecutor
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface to use for delaying execution, like during retries.
|
||||
*
|
||||
@@ -249,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.
|
||||
*/
|
||||
@@ -290,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)`. */
|
||||
@@ -300,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)
|
||||
@@ -402,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) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,6 +489,24 @@ private constructor(
|
||||
*/
|
||||
fun build(): ClientOptions {
|
||||
val httpClient = checkRequired("httpClient", httpClient)
|
||||
val streamHandlerExecutor =
|
||||
streamHandlerExecutor
|
||||
?: PhantomReachableExecutorService(
|
||||
Executors.newCachedThreadPool(
|
||||
object : ThreadFactory {
|
||||
|
||||
private val threadFactory: ThreadFactory =
|
||||
Executors.defaultThreadFactory()
|
||||
private val count = AtomicLong(0)
|
||||
|
||||
override fun newThread(runnable: Runnable): Thread =
|
||||
threadFactory.newThread(runnable).also {
|
||||
it.name =
|
||||
"langchain-stream-handler-thread-${count.getAndIncrement()}"
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
val sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper())
|
||||
|
||||
val headers = Headers.builder()
|
||||
@@ -454,39 +518,52 @@ 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)
|
||||
}
|
||||
}
|
||||
tenantId?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("X-Tenant-Id", it)
|
||||
}
|
||||
}
|
||||
bearerToken?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("Authorization", "Bearer $it")
|
||||
}
|
||||
}
|
||||
organizationId?.let {
|
||||
if (!it.isEmpty()) {
|
||||
headers.put("X-Organization-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,
|
||||
sleeper,
|
||||
clock,
|
||||
baseUrl,
|
||||
@@ -495,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")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -515,6 +636,7 @@ private constructor(
|
||||
*/
|
||||
fun close() {
|
||||
httpClient.close()
|
||||
(streamHandlerExecutor as? ExecutorService)?.shutdown()
|
||||
sleeper.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,33 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core
|
||||
|
||||
/**
|
||||
* An interface representing a single page, with items of type [T], from a paginated endpoint
|
||||
* response.
|
||||
*
|
||||
* Implementations of this interface are expected to request additional pages synchronously. For
|
||||
* asynchronous pagination, see the [PageAsync] interface.
|
||||
*/
|
||||
interface Page<T> {
|
||||
|
||||
/**
|
||||
* Returns whether there's another page after this one.
|
||||
*
|
||||
* The method generally doesn't make requests so the result depends entirely on the data in this
|
||||
* page. If a significant amount of time has passed between requesting this page and calling
|
||||
* this method, then the result could be stale.
|
||||
*/
|
||||
fun hasNextPage(): Boolean
|
||||
|
||||
/**
|
||||
* Returns the page after this one by making another request.
|
||||
*
|
||||
* @throws IllegalStateException if it's impossible to get the next page. This exception is
|
||||
* avoidable by calling [hasNextPage] first.
|
||||
*/
|
||||
fun nextPage(): Page<T>
|
||||
|
||||
/** Returns the items in this page. */
|
||||
fun items(): List<T>
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
package com.langchain.smith.core
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* An interface representing a single page, with items of type [T], from a paginated endpoint
|
||||
* response.
|
||||
*
|
||||
* Implementations of this interface are expected to request additional pages asynchronously. For
|
||||
* synchronous pagination, see the [Page] interface.
|
||||
*/
|
||||
interface PageAsync<T> {
|
||||
|
||||
/**
|
||||
* Returns whether there's another page after this one.
|
||||
*
|
||||
* The method generally doesn't make requests so the result depends entirely on the data in this
|
||||
* page. If a significant amount of time has passed between requesting this page and calling
|
||||
* this method, then the result could be stale.
|
||||
*/
|
||||
fun hasNextPage(): Boolean
|
||||
|
||||
/**
|
||||
* Returns the page after this one by making another request.
|
||||
*
|
||||
* @throws IllegalStateException if it's impossible to get the next page. This exception is
|
||||
* avoidable by calling [hasNextPage] first.
|
||||
*/
|
||||
fun nextPage(): CompletableFuture<out PageAsync<T>>
|
||||
|
||||
/** Returns the items in this page. */
|
||||
fun items(): List<T>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ private constructor(
|
||||
*
|
||||
* A value of [Duration.ZERO] means there's no timeout.
|
||||
*
|
||||
* Defaults to `Duration.ofMinutes(1)`.
|
||||
* Defaults to `Duration.ofSeconds(90)`.
|
||||
*/
|
||||
fun request(): Duration = request ?: Duration.ofMinutes(1)
|
||||
fun request(): Duration = request ?: Duration.ofSeconds(90)
|
||||
|
||||
fun toBuilder() = Builder().from(this)
|
||||
|
||||
@@ -125,7 +125,7 @@ private constructor(
|
||||
*
|
||||
* A value of [Duration.ZERO] means there's no timeout.
|
||||
*
|
||||
* Defaults to `Duration.ofMinutes(1)`.
|
||||
* Defaults to `Duration.ofSeconds(90)`.
|
||||
*/
|
||||
fun request(request: Duration?) = apply { this.request = request }
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -589,7 +589,7 @@ private constructor(
|
||||
|
||||
override fun hashCode() = values.hashCode()
|
||||
|
||||
override fun toString() = values.toString()
|
||||
override fun toString() = "[object Object]"
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@file:JvmName("EmptyHandler")
|
||||
|
||||
package com.langchain.smith.core.handlers
|
||||
|
||||
import com.langchain.smith.core.http.HttpResponse
|
||||
import com.langchain.smith.core.http.HttpResponse.Handler
|
||||
|
||||
@JvmSynthetic internal fun emptyHandler(): Handler<Void?> = EmptyHandlerInternal
|
||||
|
||||
private object EmptyHandlerInternal : Handler<Void?> {
|
||||
override fun handle(response: HttpResponse): Void? = null
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.langchain.smith.core.http
|
||||
|
||||
import com.langchain.smith.core.checkRequired
|
||||
import com.langchain.smith.core.toImmutable
|
||||
import java.net.URLEncoder
|
||||
|
||||
class HttpRequest
|
||||
private constructor(
|
||||
@@ -13,6 +14,35 @@ private constructor(
|
||||
@get:JvmName("body") val body: HttpRequestBody?,
|
||||
) {
|
||||
|
||||
fun url(): String = buildString {
|
||||
append(baseUrl)
|
||||
|
||||
pathSegments.forEach { segment ->
|
||||
if (!endsWith("/")) {
|
||||
append("/")
|
||||
}
|
||||
append(URLEncoder.encode(segment, "UTF-8"))
|
||||
}
|
||||
|
||||
if (queryParams.isEmpty()) {
|
||||
return@buildString
|
||||
}
|
||||
|
||||
append("?")
|
||||
var isFirst = true
|
||||
queryParams.keys().forEach { key ->
|
||||
queryParams.values(key).forEach { value ->
|
||||
if (!isFirst) {
|
||||
append("&")
|
||||
}
|
||||
append(URLEncoder.encode(key, "UTF-8"))
|
||||
append("=")
|
||||
append(URLEncoder.encode(value, "UTF-8"))
|
||||
isFirst = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toBuilder(): Builder = Builder().from(this)
|
||||
|
||||
override fun toString(): String =
|
||||
|
||||
+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) }
|
||||
}
|
||||
|
||||
+16
-19
@@ -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,17 +214,12 @@ 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.
|
||||
val backoffSeconds = min(0.5 * 2.0.pow(retries - 1), 8.0)
|
||||
val backoffSeconds = min(0.5 * 2.0.pow(retries - 1), 16.0)
|
||||
|
||||
// Apply some jitter
|
||||
val jitter = 1.0 - 0.25 * ThreadLocalRandom.current().nextDouble()
|
||||
|
||||
+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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user