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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,21 @@ jobs:
echo "Building $pkg..."
(cd "$pkg" && uv build --out-dir dist)
done

# ─── Packaging ──────────────────────────────────────────────────────────────
# Installs each wheel into an empty virtualenv with its documented install
# command, then imports its public modules and every module its source
# imports, lazy ones included. The workspace test run cannot catch an
# undeclared dependency, because it installs every package's at once.
packaging:
name: Packaging check
needs: [lint, typecheck, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- run: uv lock
- run: uv sync --all-packages
- run: make check-packaging
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: start test typecheck lint format format-check lint-fix build clean api-report api-report-check
.PHONY: start test typecheck lint format format-check lint-fix build check-packaging clean api-report api-report-check

# Run main.py (uv run python main.py)
start:
Expand Down Expand Up @@ -49,6 +49,14 @@ build:
(cd $$pkg && uv build --out-dir dist); \
done

# Install each built wheel into an empty virtualenv and check its imports resolve there
check-packaging:
rm -rf dist
@for pkg in packages/*/; do \
(cd $$pkg && uv build --quiet --wheel --out-dir ../../dist); \
done
uv run python scripts/check_packaging.py dist

# Clean build artifacts
clean:
rm -rf packages/*/dist
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,26 @@ Tier 0 — Core Client (launchdarkly-ai-server)
| [`launchdarkly-ai-vercel-agents`](packages/vercel-agents/README.md) | `*` (any) | `agent` | Native Vercel `ai.Agent` loop and graph adapter |
| [`launchdarkly-ai-typesafe`](packages/typesafe/README.md) | TypeSafe | `messages` | TypeSafe Jev judge handler |

### Versioning

From 1.0.0 every package in this SDK is released at the same version, so install them at matching versions. Each package depends on `launchdarkly-ai-server` within the same major (`>=1.0,<2`), and a breaking change in any package is a new major for all of them. Names exported from `launchdarkly_ai_server.experimental` are the exception: they may change in a minor release.

Every third-party floor sits on the release line CI tests: the same major as the tested version for a 1.x or later library, the same minor for a 0.x one, where a minor release may break. A floor that was below that line was raised to the tested version; a floor already on it stayed where it was.

## Quick Start

### 1. Install

```bash
pip install launchdarkly-ai-python launchdarkly-server-sdk launchdarkly-ai-openai-messages
pip install "launchdarkly-ai-python[ld]" launchdarkly-ai-openai-messages
```

`launchdarkly-ai-python` is a thin barrel that re-exports all of `launchdarkly-ai-server`. The base LaunchDarkly Python SDK is a peer dependency rather than a bundled one, so install `launchdarkly-server-sdk` alongside it. `init_client()` discovers it at runtime, or you can skip it entirely by passing a pre-initialized client with `init_client(client=my_client)`.
`launchdarkly-ai-python` is a thin barrel that re-exports all of `launchdarkly-ai-server`. The base LaunchDarkly Python SDK is an optional dependency rather than a bundled one; the `[ld]` extra installs `launchdarkly-server-sdk`. `init_client()` discovers it at runtime, or you can skip it entirely by passing a pre-initialized client with `init_client(client=my_client)`.

**With telemetry** (recommended for production) — traces export to the LaunchDarkly Observability dashboard:

```bash
pip install "launchdarkly-ai-python[otel]" launchdarkly-server-sdk launchdarkly-ai-openai-messages
pip install "launchdarkly-ai-python[ld,otel]" launchdarkly-ai-openai-messages
```

No code changes are needed — `init_client()` detects whether the OTel packages are present at runtime and configures the tracer provider automatically. If they are absent, the SDK logs a one-time warning and continues normally.
Expand Down Expand Up @@ -574,7 +580,7 @@ When running inside `graph()`, every node's events carry the graph key, tool inv

### Optional dependencies

The OpenTelemetry SDK packages are **optional** — detected at runtime via `importlib`. The LaunchDarkly server SDK (`launchdarkly-server-sdk`) is also an optional dependency; pass a pre-initialized client to `init_client(client=...)` if you bring your own.
The OpenTelemetry SDK packages are **optional** — detected at runtime via `importlib`. The LaunchDarkly server SDK (`launchdarkly-server-sdk`) is also an optional dependency, installed by the `[ld]` extra; pass a pre-initialized client to `init_client(client=...)` if you bring your own.

**OTel packages** (installed via `pip install "launchdarkly-ai-python[otel]"` or `pip install "launchdarkly-ai-server[otel]"`):
- **If installed:** `init_client()` sets up a `TracerProvider` with a GZIP-compressed OTLP HTTP exporter and W3C trace-context/baggage propagators — no code changes needed.
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Convenience barrel package for the LaunchDarkly AI Python SDK. Re-exports the co
## Installation

```bash
pip install launchdarkly-ai-python launchdarkly-ai-openai-messages
pip install "launchdarkly-ai-python[ld]" launchdarkly-ai-openai-messages
```

To enable trace export to the LaunchDarkly Observability dashboard, install the `otel` extras group:
Expand Down
7 changes: 4 additions & 3 deletions packages/ai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
name = "launchdarkly-ai-python"
version = "0.4.0"
requires-python = ">=3.12"
dependencies = ["launchdarkly-ai-server"]
dependencies = ["launchdarkly-ai-server>=0.6,<2"]
description = "LaunchDarkly AI SDK for Python — convenience barrel re-export"
readme = "README.md"
license = "Apache-2.0"
authors = [{name = "LaunchDarkly", email = "team@launchdarkly.com"}]
keywords = ["launchdarkly", "ai", "feature-flags", "sdk"]
classifiers = [
"Development Status :: 3 - Alpha",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
Expand All @@ -23,7 +23,8 @@ Repository = "https://github.com/launchdarkly/python-ai-sdk"
"Bug Tracker" = "https://github.com/launchdarkly/python-ai-sdk/issues"

[project.optional-dependencies]
otel = ["launchdarkly-ai-server[otel]"]
otel = ["launchdarkly-ai-server[otel]>=0.6,<2"]
ld = ["launchdarkly-ai-server[ld]>=0.6,<2"]

[build-system]
requires = ["hatchling>=1.32,<2"]
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Anthropic Claude handler for `launchdarkly-ai-server` using the **Claude Agent S
## Installation

```bash
pip install launchdarkly-ai-server launchdarkly-ai-claude-agents
pip install "launchdarkly-ai-server[ld]" launchdarkly-ai-claude-agents
```

Set `ANTHROPIC_API_KEY` in your environment (the Anthropic SDK reads it automatically).
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-agents/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def claude_graph(key: str, *, cwd=None, setting_sources=None, **options): ...

# Native graph adapter — builds a Claude code-agents graph from a resolved GraphDefinition.
# opts: context, tool_handlers, and the host options cwd and setting_sources
def to_claude_agents(def_promise: Awaitable[dict], opts: dict | None = None): ...
def to_claude_agents(def_promise: Awaitable[GraphDefinition], opts: ClaudeNativeGraphOptions | None = None) -> NativeGraphRunner: ...

# NativeTool sentinels for Claude Code built-in capabilities
ClaudeBash: NativeTool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ ClaudeBash = NativeTool('Bash')
ClaudeEdit = NativeTool('Edit')
ClaudeGlob = NativeTool('Glob')
ClaudeGrep = NativeTool('Grep')
ClaudeNativeGraphOptions(TypedDict, total=False) [class]
context: LDContext
cwd: str | os.PathLike[str]
setting_sources: list[SettingSource]
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool]
ClaudeNotebookEdit = NativeTool('NotebookEdit')
ClaudeRead = NativeTool('Read')
ClaudeTodoWrite = NativeTool('TodoWrite')
Expand All @@ -13,4 +18,4 @@ ClaudeWebSearch = NativeTool('WebSearch')
ClaudeWrite = NativeTool('Write')
create_claude_agents_handler(*, capture_content: bool | None = None, cwd: str | os.PathLike[str] | None = None, setting_sources: list[SettingSource] | None = None) -> ProviderHandler
create_claude_agents_skills_handler(*, capture_content: bool | None = None, cwd: str | os.PathLike[str] | None = None, setting_sources: list[SettingSource] | None = None, skills_temp_dir: str | os.PathLike[str] | None = None) -> ProviderHandler
to_claude_agents(def_promise: Any, opts: dict[str, Any] | None = None) -> Any
to_claude_agents(def_promise: Awaitable[GraphDefinition], opts: ClaudeNativeGraphOptions | None = None) -> NativeGraphRunner
6 changes: 3 additions & 3 deletions packages/claude-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name = "launchdarkly-ai-claude-agents"
version = "0.3.0"
requires-python = ">=3.12"
dependencies = [
"launchdarkly-ai-server>=0.4.1",
"launchdarkly-ai-server>=0.6,<2",
"opentelemetry-api>=1.25",
"anthropic>=0.40",
"anthropic>=0.113",
"claude-agent-sdk>=0.2.121",
]
description = "Anthropic Claude agent handler for LaunchDarkly AI SDK"
Expand All @@ -14,7 +14,7 @@ license = "Apache-2.0"
authors = [{name = "LaunchDarkly", email = "team@launchdarkly.com"}]
keywords = ["launchdarkly", "ai", "anthropic", "claude", "agents"]
classifiers = [
"Development Status :: 3 - Alpha",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
claude_agents,
create_claude_agents_handler,
)
from .native_graph import to_claude_agents
from .native_graph import ClaudeNativeGraphOptions, to_claude_agents
from .skills import create_claude_agents_skills_handler

__all__ = [
"ClaudeBash",
"ClaudeEdit",
"ClaudeGlob",
"ClaudeGrep",
"ClaudeNativeGraphOptions",
"ClaudeNotebookEdit",
"ClaudeRead",
"ClaudeTodoWrite",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@

async def _build_tool_mcp(
config_tools: dict[str, Any],
handlers: dict[str, Any],
handlers: Mapping[str, Any],
) -> Any:
"""Build an in-process SDK MCP server from LD config tools + handler functions.

Expand Down Expand Up @@ -215,7 +215,7 @@ async def _execute(args: Any, _name: str = tool_name) -> dict[str, Any]:

def _partition_tools(
config_tools: dict[str, Any] | None,
tool_handlers: dict[str, Any],
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool],
) -> tuple[dict[str, Any], dict[str, Any], list[str]]:
"""
Returns (native_tool_map, user_config_tools, native_tool_names).
Expand All @@ -241,7 +241,9 @@ def _partition_tools(
return native_tool_map, user_config_tools, list(native_tool_map.keys())


def _native_tool_aliases(tool_handlers: dict[str, Any]) -> dict[str, str]:
def _native_tool_aliases(
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool],
) -> dict[str, str]:
"""AI Config key → provider tool name, for the config's native tools only.

``_partition_tools``'s ``native_tool_map`` is keyed the other way round, by provider name, which
Expand Down Expand Up @@ -810,7 +812,7 @@ def _create_claude_agents_handler(
async def _call_impl(
config: AiConfigRep,
user_input: str = "",
tool_handlers: dict[str, Any] | None = None,
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool] | None = None,
variables: dict[str, Any] | None = None,
history: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
Expand Down Expand Up @@ -996,7 +998,7 @@ async def _call_impl(
def _stream_impl(
config: AiConfigRep,
user_input: str = "",
tool_handlers: dict[str, Any] | None = None,
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool] | None = None,
variables: dict[str, Any] | None = None,
history: list[dict[str, Any]] | None = None,
) -> AsyncGenerator[dict[str, Any], None]:
Expand Down Expand Up @@ -1067,7 +1069,7 @@ def _map_claude_stream_event(
async def _stream_gen(
config: AiConfigRep,
user_input: str,
tool_handlers: dict[str, Any],
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool],
variables: dict[str, Any],
history: list[dict[str, Any]] | None = None,
*,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
from __future__ import annotations

import logging
import os
import re
import time
import types
import uuid
from typing import Any
from collections.abc import Awaitable, Callable, Mapping
from typing import Any, TypedDict

from claude_agent_sdk import SettingSource

from launchdarkly_ai_server import (
NATIVE_TOOL_KEY,
GraphDefinition,
GraphNode,
LDContext,
NativeGraphRunner,
NativeTool,
get_client,
make_track_data,
Expand Down Expand Up @@ -59,7 +64,7 @@ def _sanitize_name(key: str) -> str:


def _wrap_native_tools(
tool_handlers: dict[str, Any],
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool],
ld_context: Any,
track_data: dict[str, Any],
) -> dict[str, Any]:
Expand Down Expand Up @@ -110,7 +115,7 @@ async def _run_query(
node: GraphNode,
input_text: str,
variables: dict[str, Any],
tool_handlers: dict[str, Any],
tool_handlers: Mapping[str, Callable[..., Any] | NativeTool],
ld_context: Any,
graph_key: str,
run_id: str,
Expand Down Expand Up @@ -219,10 +224,20 @@ async def _run_query(
}


class ClaudeNativeGraphOptions(TypedDict, total=False):
"""Options for :func:`to_claude_agents`."""

tool_handlers: Mapping[str, Callable[..., Any] | NativeTool]
context: LDContext
"""LaunchDarkly context used for tracking events. Required for LD telemetry."""
cwd: str | os.PathLike[str]
setting_sources: list[SettingSource]


def to_claude_agents(
def_promise: Any,
opts: dict[str, Any] | None = None,
) -> Any:
def_promise: Awaitable[GraphDefinition],
opts: ClaudeNativeGraphOptions | None = None,
) -> NativeGraphRunner:
"""
Converts a resolved ``GraphDefinition`` into a nested query() multi-agent
execution where each child node becomes a sub-agent tool.
Expand Down Expand Up @@ -263,7 +278,7 @@ async def invoke(
if raw_ld_context is not None
else None
)
raw_handlers: dict[str, Any] = _opts.get("tool_handlers") or {}
raw_handlers: Mapping[str, Any] = _opts.get("tool_handlers") or {}

start_time = time.monotonic()
run_id = str(uuid.uuid4())
Expand Down Expand Up @@ -474,4 +489,10 @@ async def _subagent_execute(
if span:
span.end()

return types.SimpleNamespace(invoke=invoke)
# A class, not a SimpleNamespace, so mypy checks invoke against NativeGraphRunner.
_invoke = invoke

class _Runner:
invoke = staticmethod(_invoke)

return _Runner()
2 changes: 1 addition & 1 deletion packages/claude-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Anthropic Claude handler for `launchdarkly-ai-server` using the **Anthropic Mess
## Installation

```bash
pip install launchdarkly-ai-server launchdarkly-ai-claude-messages
pip install "launchdarkly-ai-server[ld]" launchdarkly-ai-claude-messages
```

Set `ANTHROPIC_API_KEY` in your environment (the Anthropic SDK reads it automatically).
Expand Down
6 changes: 3 additions & 3 deletions packages/claude-messages/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ name = "launchdarkly-ai-claude-messages"
version = "0.3.0"
requires-python = ">=3.12"
dependencies = [
"launchdarkly-ai-server>=0.4.1",
"launchdarkly-ai-server>=0.6,<2",
"opentelemetry-api>=1.25",
"anthropic>=0.40",
"anthropic>=0.113",
]
description = "Anthropic Claude messages handler for LaunchDarkly AI SDK"
readme = "README.md"
license = "Apache-2.0"
authors = [{name = "LaunchDarkly", email = "team@launchdarkly.com"}]
keywords = ["launchdarkly", "ai", "anthropic", "claude"]
classifiers = [
"Development Status :: 3 - Alpha",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
Expand Down
Loading
Loading