Skip to content

Commit 7c5d067

Browse files
jsonbaileyclaude
andcommitted
chore: merge main into branch and resolve conflicts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents 842bd34 + a48b364 commit 7c5d067

29 files changed

Lines changed: 1097 additions & 391 deletions

.github/actions/build/action.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,10 @@ inputs:
44
workspace_path:
55
description: 'Path to the package to build.'
66
required: true
7-
outputs:
8-
package-hashes:
9-
description: "base64-encoded sha256 hashes of distribution files"
10-
value: ${{ steps.package-hashes.outputs.package-hashes }}
117

128
runs:
139
using: composite
1410
steps:
1511
- name: Build distribution files
1612
shell: bash
1713
run: make -C ${{ inputs.workspace_path }} build
18-
19-
- name: Hash build files for provenance
20-
id: package-hashes
21-
shell: bash
22-
working-directory: ${{ inputs.workspace_path }}/dist
23-
run: |
24-
echo "package-hashes=$(sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT"

.github/workflows/release-please.yml

Lines changed: 26 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,10 @@ jobs:
5858
needs: ['release-please']
5959
permissions:
6060
id-token: write # Needed for OIDC to get release secrets from AWS.
61+
attestations: write # Needed for actions/attest.
6162
if: ${{ needs.release-please.outputs.package-server-ai-released == 'true' }}
62-
outputs:
63-
package-hashes: ${{ steps.build.outputs.package-hashes }}
6463
steps:
6564
- uses: actions/checkout@v4
66-
with:
67-
fetch-depth: 0
6865

6966
- uses: ./.github/actions/ci
7067
with:
@@ -75,6 +72,11 @@ jobs:
7572
with:
7673
workspace_path: packages/sdk/server-ai
7774

75+
- name: Attest build provenance
76+
uses: actions/attest@v4
77+
with:
78+
subject-path: 'packages/sdk/server-ai/dist/*'
79+
7880
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
7981
name: 'Get PyPI token'
8082
with:
@@ -92,13 +94,10 @@ jobs:
9294
needs: ['release-please']
9395
permissions:
9496
id-token: write # Needed for OIDC to get release secrets from AWS.
97+
attestations: write # Needed for actions/attest.
9598
if: ${{ needs.release-please.outputs.package-server-ai-langchain-released == 'true' }}
96-
outputs:
97-
package-hashes: ${{ steps.build.outputs.package-hashes }}
9899
steps:
99100
- uses: actions/checkout@v4
100-
with:
101-
fetch-depth: 0
102101

103102
- uses: ./.github/actions/ci
104103
with:
@@ -109,6 +108,11 @@ jobs:
109108
with:
110109
workspace_path: packages/ai-providers/server-ai-langchain
111110

111+
- name: Attest build provenance
112+
uses: actions/attest@v4
113+
with:
114+
subject-path: 'packages/ai-providers/server-ai-langchain/dist/*'
115+
112116
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
113117
name: 'Get PyPI token'
114118
with:
@@ -140,57 +144,28 @@ jobs:
140144
workspace_path: ${{ inputs.workspace_path }}
141145

142146
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
143-
if: ${{ inputs.dry_run != true }}
147+
if: ${{ format('{0}', inputs.dry_run) != 'true' }}
144148
name: 'Get PyPI token'
145149
with:
146150
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
147151
ssm_parameter_pairs: '/production/common/releasing/pypi/token = PYPI_AUTH_TOKEN'
148152

149153
- name: Publish to PyPI
150-
if: ${{ inputs.dry_run != true }}
154+
if: ${{ format('{0}', inputs.dry_run) != 'true' }}
151155
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
152156
with:
153157
password: ${{ env.PYPI_AUTH_TOKEN }}
154158
packages-dir: ${{ inputs.workspace_path }}/dist/
155159

156-
release-server-ai-provenance:
157-
needs: ['release-please', 'release-server-ai']
158-
if: ${{ needs.release-please.outputs.package-server-ai-released == 'true' }}
159-
permissions:
160-
actions: read # Needed for detecting the GitHub Actions environment.
161-
id-token: write # Needed for provenance signing.
162-
contents: write # Needed for uploading assets to the release.
163-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
164-
with:
165-
base64-subjects: "${{ needs.release-server-ai.outputs.package-hashes }}"
166-
upload-assets: true
167-
upload-tag-name: ${{ needs.release-please.outputs.package-server-ai-tag-name }}
168-
169-
release-server-ai-langchain-provenance:
170-
needs: ['release-please', 'release-server-ai-langchain']
171-
if: ${{ needs.release-please.outputs.package-server-ai-langchain-released == 'true' }}
172-
permissions:
173-
actions: read # Needed for detecting the GitHub Actions environment.
174-
id-token: write # Needed for provenance signing.
175-
contents: write # Needed for uploading assets to the release.
176-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@5a775b367a56d5bd118a224a811bba288150a563 # v2.0.0
177-
with:
178-
base64-subjects: "${{ needs.release-server-ai-langchain.outputs.package-hashes }}"
179-
upload-assets: true
180-
upload-tag-name: ${{ needs.release-please.outputs.package-server-ai-langchain-tag-name }}
181-
182160
release-server-ai-openai:
183161
runs-on: ubuntu-latest
184162
needs: ['release-please']
185163
permissions:
186164
id-token: write # Needed for OIDC to get release secrets from AWS.
165+
attestations: write # Needed for actions/attest.
187166
if: ${{ needs.release-please.outputs.package-server-ai-openai-released == 'true' }}
188-
outputs:
189-
package-hashes: ${{ steps.build.outputs.package-hashes }}
190167
steps:
191168
- uses: actions/checkout@v4
192-
with:
193-
fetch-depth: 0
194169

195170
- uses: ./.github/actions/ci
196171
with:
@@ -201,6 +176,11 @@ jobs:
201176
with:
202177
workspace_path: packages/ai-providers/server-ai-openai
203178

179+
- name: Attest build provenance
180+
uses: actions/attest@v4
181+
with:
182+
subject-path: 'packages/ai-providers/server-ai-openai/dist/*'
183+
204184
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
205185
name: 'Get PyPI token'
206186
with:
@@ -213,31 +193,15 @@ jobs:
213193
password: ${{ env.PYPI_AUTH_TOKEN }}
214194
packages-dir: packages/ai-providers/server-ai-openai/dist/
215195

216-
release-server-ai-openai-provenance:
217-
needs: ['release-please', 'release-server-ai-openai']
218-
if: ${{ needs.release-please.outputs.package-server-ai-openai-released == 'true' }}
219-
permissions:
220-
actions: read # Needed for detecting the GitHub Actions environment.
221-
id-token: write # Needed for provenance signing.
222-
contents: write # Needed for uploading assets to the release.
223-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
224-
with:
225-
base64-subjects: "${{ needs.release-server-ai-openai.outputs.package-hashes }}"
226-
upload-assets: true
227-
upload-tag-name: ${{ needs.release-please.outputs.package-server-ai-openai-tag-name }}
228-
229196
release-server-ai-optimization:
230197
runs-on: ubuntu-latest
231198
needs: ['release-please']
232199
permissions:
233200
id-token: write # Needed for OIDC to get release secrets from AWS.
201+
attestations: write # Needed for actions/attest.
234202
if: ${{ needs.release-please.outputs.package-server-ai-optimization-released == 'true' }}
235-
outputs:
236-
package-hashes: ${{ steps.build.outputs.package-hashes }}
237203
steps:
238204
- uses: actions/checkout@v4
239-
with:
240-
fetch-depth: 0
241205

242206
- uses: ./.github/actions/ci
243207
with:
@@ -248,6 +212,11 @@ jobs:
248212
with:
249213
workspace_path: packages/optimization
250214

215+
- name: Attest build provenance
216+
uses: actions/attest@v4
217+
with:
218+
subject-path: 'packages/optimization/dist/*'
219+
251220
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
252221
name: 'Get PyPI token'
253222
with:
@@ -259,16 +228,3 @@ jobs:
259228
with:
260229
password: ${{ env.PYPI_AUTH_TOKEN }}
261230
packages-dir: packages/optimization/dist/
262-
263-
release-server-ai-optimization-provenance:
264-
needs: ['release-please', 'release-server-ai-optimization']
265-
if: ${{ needs.release-please.outputs.package-server-ai-optimization-released == 'true' }}
266-
permissions:
267-
actions: read # Needed for detecting the GitHub Actions environment.
268-
id-token: write # Needed for provenance signing.
269-
contents: write # Needed for uploading assets to the release.
270-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
271-
with:
272-
base64-subjects: "${{ needs.release-server-ai-optimization.outputs.package-hashes }}"
273-
upload-assets: true
274-
upload-tag-name: ${{ needs.release-please.outputs.package-server-ai-optimization-tag-name }}

.release-please-manifest.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"packages/sdk/server-ai": "0.16.1",
3-
"packages/ai-providers/server-ai-langchain": "0.3.2",
4-
"packages/ai-providers/server-ai-openai": "0.2.1"
2+
"packages/sdk/server-ai": "0.17.0",
3+
"packages/ai-providers/server-ai-langchain": "0.4.0",
4+
"packages/ai-providers/server-ai-openai": "0.3.0",
5+
"packages/optimization": "0.1.0"
56
}

packages/ai-providers/server-ai-langchain/CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
All notable changes to the LaunchDarkly Python AI LangChain provider package will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
4+
5+
## [0.4.0](https://github.com/launchdarkly/python-server-sdk-ai/compare/launchdarkly-server-sdk-ai-langchain-0.3.2...launchdarkly-server-sdk-ai-langchain-0.4.0) (2026-04-02)
6+
7+
8+
### ⚠ BREAKING CHANGES
9+
10+
* Bump minimum LangChain version to 1.0.0
11+
* Restructure provider factory and support additional create methods ([#102](https://github.com/launchdarkly/python-server-sdk-ai/issues/102))
12+
* Extract shared utilities to langchain_helper
13+
14+
### Features
15+
16+
* Add LangGraphAgentGraphRunner ([56ce0fd](https://github.com/launchdarkly/python-server-sdk-ai/commit/56ce0fd63b4301b58f33c17c55c4ecd47e9f8559))
17+
* Extract shared utilities to langchain_helper ([453c71c](https://github.com/launchdarkly/python-server-sdk-ai/commit/453c71c84adcc6b8a3e316a98907dcb511bc9d41))
18+
* Add get_ai_usage_from_response to langchain_helper ([4fab18f](https://github.com/launchdarkly/python-server-sdk-ai/commit/4fab18fa62375b6c97cb12a89225805c81ca4ee8))
19+
* Add get_tool_calls_from_response and sum_token_usage_from_messages to langchain_helper ([4fab18f](https://github.com/launchdarkly/python-server-sdk-ai/commit/4fab18fa62375b6c97cb12a89225805c81ca4ee8))
20+
* Drop support for python 3.9 ([#114](https://github.com/launchdarkly/python-server-sdk-ai/issues/114)) ([dc592c5](https://github.com/launchdarkly/python-server-sdk-ai/commit/dc592c5a2e2bf3bf679af14a9aa63e81678a69ab))
21+
22+
23+
### Bug Fixes
24+
25+
* Use time.perf_counter_ns() for sub-millisecond precision in duration calculations ([4fab18f](https://github.com/launchdarkly/python-server-sdk-ai/commit/4fab18fa62375b6c97cb12a89225805c81ca4ee8))
26+
327
## [0.3.2](https://github.com/launchdarkly/python-server-sdk-ai/compare/launchdarkly-server-sdk-ai-langchain-0.3.1...launchdarkly-server-sdk-ai-langchain-0.3.2) (2026-03-16)
428

529

packages/ai-providers/server-ai-langchain/pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "launchdarkly-server-sdk-ai-langchain"
3-
version = "0.3.2"
3+
version = "0.4.0"
44
description = "LaunchDarkly AI SDK LangChain Provider"
55
authors = [{name = "LaunchDarkly", email = "dev@launchdarkly.com"}]
66
license = {text = "Apache-2.0"}
@@ -19,11 +19,14 @@ classifiers = [
1919
"Topic :: Software Development :: Libraries",
2020
]
2121
dependencies = [
22-
"launchdarkly-server-sdk-ai>=0.16.0",
22+
"launchdarkly-server-sdk-ai>=0.17.0",
2323
"langchain-core>=1.0.0",
2424
"langchain>=1.0.0",
2525
]
2626

27+
[project.optional-dependencies]
28+
graph = ["langgraph>=1.0.0"]
29+
2730
[project.urls]
2831
Homepage = "https://docs.launchdarkly.com/sdk/ai/python"
2932
Repository = "https://github.com/launchdarkly/python-server-sdk-ai"
@@ -36,6 +39,7 @@ dev = [
3639
"mypy==1.18.2",
3740
"pycodestyle>=2.11.0",
3841
"isort>=5.12.0",
42+
"langgraph>=1.0.0",
3943
]
4044

4145
[build-system]

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_agent_runner.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
"""LangChain agent runner for LaunchDarkly AI SDK."""
2-
31
from typing import Any
42

53
from ldai import log
64
from ldai.providers import AgentResult, AgentRunner
75
from ldai.providers.types import LDAIMetrics
86

9-
from ldai_langchain.langchain_helper import sum_token_usage_from_messages
7+
from ldai_langchain.langchain_helper import (
8+
extract_last_message_content,
9+
sum_token_usage_from_messages,
10+
)
1011

1112

1213
class LangChainAgentRunner(AgentRunner):
1314
"""
15+
CAUTION:
16+
This feature is experimental and should NOT be considered ready for production use.
17+
It may change or be removed without notice and is not subject to backwards
18+
compatibility guarantees.
19+
1420
AgentRunner implementation for LangChain.
1521
1622
Wraps a compiled LangChain agent graph (from ``langchain.agents.create_agent``)
@@ -37,11 +43,7 @@ async def run(self, input: Any) -> AgentResult:
3743
"messages": [{"role": "user", "content": str(input)}]
3844
})
3945
messages = result.get("messages", [])
40-
output = ""
41-
if messages:
42-
last = messages[-1]
43-
if hasattr(last, 'content') and isinstance(last.content, str):
44-
output = last.content
46+
output = extract_last_message_content(messages)
4547
return AgentResult(
4648
output=output,
4749
raw=result,

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_helper.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,38 @@ def _resolve_tools_for_langchain(
153153
]
154154

155155

156+
def build_tools(ai_config: AIConfigKind, tool_registry: ToolRegistry) -> List[Any]:
157+
"""
158+
Return callables from the registry for each tool defined in the AI config.
159+
160+
Tools not found in the registry are skipped with a warning. The returned
161+
callables can be passed directly to bind_tools or langchain.agents.create_agent.
162+
Functions should have type-annotated parameters so LangChain can infer the schema.
163+
164+
:param ai_config: The LaunchDarkly AI configuration
165+
:param tool_registry: Registry mapping tool names to callable implementations
166+
:return: List of callables ready to pass to bind_tools or create_agent
167+
"""
168+
config_dict = ai_config.to_dict()
169+
model_dict = config_dict.get('model') or {}
170+
parameters = dict(model_dict.get('parameters') or {})
171+
tool_definitions = parameters.pop('tools', []) or []
172+
173+
tools = []
174+
for td in tool_definitions:
175+
if not isinstance(td, dict):
176+
continue
177+
name = td.get('name')
178+
if not name:
179+
continue
180+
fn = tool_registry.get(name)
181+
if fn is None:
182+
log.warning(f"Tool '{name}' is defined in the AI config but was not found in the tool registry; skipping.")
183+
continue
184+
tools.append(fn)
185+
return tools
186+
187+
156188
def build_structured_tools(ai_config: AIConfigKind, tool_registry: ToolRegistry) -> List[Any]:
157189
"""
158190
Build a list of LangChain StructuredTool instances from LD tool definitions and a registry.

0 commit comments

Comments
 (0)