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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ LD_SDK_KEY=your-sdk-key-here
OPENAI_API_KEY=your-openai-key-here
ANTHROPIC_API_KEY=your-anthropic-key-here

# Python LiteLLM runs in-process and uses the provider keys above; no
# LITELLM_BASE_URL or proxy API key is required.

# LaunchDarkly endpoints (leave blank to use production defaults)
LD_BASE_URI=
LD_STREAM_URI=
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ on:
- packages/claude-agents
- packages/claude-messages
- packages/openai-agents
- packages/litellm-agents
- packages/openai-messages
- packages/litellm-messages
- packages/langchain-agents
- packages/langchain-messages
- packages/opencode-agents
Expand Down Expand Up @@ -59,8 +61,12 @@ jobs:
package-claude-messages-tag-name: ${{ steps.release.outputs['packages/claude-messages--tag_name'] }}
package-openai-agents-released: ${{ steps.release.outputs['packages/openai-agents--release_created'] }}
package-openai-agents-tag-name: ${{ steps.release.outputs['packages/openai-agents--tag_name'] }}
package-litellm-agents-released: ${{ steps.release.outputs['packages/litellm-agents--release_created'] }}
package-litellm-agents-tag-name: ${{ steps.release.outputs['packages/litellm-agents--tag_name'] }}
package-openai-messages-released: ${{ steps.release.outputs['packages/openai-messages--release_created'] }}
package-openai-messages-tag-name: ${{ steps.release.outputs['packages/openai-messages--tag_name'] }}
package-litellm-messages-released: ${{ steps.release.outputs['packages/litellm-messages--release_created'] }}
package-litellm-messages-tag-name: ${{ steps.release.outputs['packages/litellm-messages--tag_name'] }}
package-langchain-agents-released: ${{ steps.release.outputs['packages/langchain-agents--release_created'] }}
package-langchain-agents-tag-name: ${{ steps.release.outputs['packages/langchain-agents--tag_name'] }}
package-langchain-messages-released: ${{ steps.release.outputs['packages/langchain-messages--release_created'] }}
Expand Down Expand Up @@ -232,6 +238,37 @@ jobs:
password: ${{ env.PYPI_AUTH_TOKEN }}
packages-dir: packages/openai-agents/dist/

release-litellm-agents:
runs-on: ubuntu-latest
needs: ['release-please']
permissions:
contents: read
id-token: write
attestations: write
if: ${{ needs.release-please.outputs.package-litellm-agents-released == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/ci
with:
workspace_path: packages/litellm-agents
- uses: ./.github/actions/build
with:
workspace_path: packages/litellm-agents
- name: Attest build provenance
uses: actions/attest@v4
with:
subject-path: 'packages/litellm-agents/dist/*'
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
name: 'Get PyPI token'
with:
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
ssm_parameter_pairs: '/production/common/releasing/pypi/token = PYPI_AUTH_TOKEN'
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
password: ${{ env.PYPI_AUTH_TOKEN }}
packages-dir: packages/litellm-agents/dist/

release-openai-messages:
runs-on: ubuntu-latest
needs: ['release-please']
Expand Down Expand Up @@ -263,6 +300,37 @@ jobs:
password: ${{ env.PYPI_AUTH_TOKEN }}
packages-dir: packages/openai-messages/dist/

release-litellm-messages:
runs-on: ubuntu-latest
needs: ['release-please']
permissions:
contents: read
id-token: write
attestations: write
if: ${{ needs.release-please.outputs.package-litellm-messages-released == 'true' }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/ci
with:
workspace_path: packages/litellm-messages
- uses: ./.github/actions/build
with:
workspace_path: packages/litellm-messages
- name: Attest build provenance
uses: actions/attest@v4
with:
subject-path: 'packages/litellm-messages/dist/*'
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
name: 'Get PyPI token'
with:
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
ssm_parameter_pairs: '/production/common/releasing/pypi/token = PYPI_AUTH_TOKEN'
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
password: ${{ env.PYPI_AUTH_TOKEN }}
packages-dir: packages/litellm-messages/dist/

release-langchain-agents:
runs-on: ubuntu-latest
needs: ['release-please']
Expand Down
4 changes: 3 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"packages/openai-messages": "0.2.4",
"packages/langchain-agents": "0.2.5",
"packages/langchain-messages": "0.2.4",
"packages/opencode-agents": "0.1.0"
"packages/opencode-agents": "0.1.0",
"packages/litellm-messages": "0.1.0",
"packages/litellm-agents": "0.1.0"
}
24 changes: 24 additions & 0 deletions .sdk_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@
"aiAgentConfig": { "introduced": "0.1" },
"aiTrackMetrics": { "introduced": "0.1" }
}
},
"python-ai-litellm-messages": {
"name": "LiteLLM Messages AI SDK for Python",
"type": "ai-provider",
"path": "packages/litellm-messages",
"languages": ["Python"],
"aiSdkNames": ["launchdarkly-ai-litellm-messages"],
"releases": { "tag-prefix": "launchdarkly-ai-litellm-messages-" },
"features": {
"aiCompletionConfig": { "introduced": "0.1" }
}
},
"python-ai-litellm-agents": {
"name": "LiteLLM Agents AI SDK for Python",
"type": "ai-provider",
"path": "packages/litellm-agents",
"languages": ["Python"],
"aiSdkNames": ["launchdarkly-ai-litellm-agents"],
"releases": { "tag-prefix": "launchdarkly-ai-litellm-agents-" },
"features": {
"aiAgentConfig": { "introduced": "0.1" },
"aiAgentGraph": { "introduced": "0.1" },
"aiTrackMetrics": { "introduced": "0.1" }
}
}
}
}
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ graph TD
openai["launchdarkly-ai-openai-agents"]
langchain["launchdarkly-ai-langchain-agents"]
opencode["launchdarkly-ai-opencode-agents"]
litellm["launchdarkly-ai-litellm-agents"]
newHandler["launchdarkly-ai-new-provider\n(future)"]
end
subgraph tier0 ["Tier 0 — Core"]
Expand All @@ -125,7 +126,7 @@ graph TD

- **Tier 0 — Core** (`launchdarkly-ai-server`): The foundation. Owns all LaunchDarkly integration, telemetry orchestration, shared data types, and the primary entry points (`config()`, `graph()`, `resolve_graph()`). Has no dependency on any other `launchdarkly-ai-*` package.
- **Tier 0 — Convenience barrel** (`launchdarkly-ai-python`): A pure re-export package that makes all of `launchdarkly-ai-server` available under a shorter install name. No new logic — intended as the default install for most Python applications.
- **Tier 1 — Handler packages** (`launchdarkly-ai-claude-agents`, `launchdarkly-ai-claude-messages`, `launchdarkly-ai-openai-agents`, `launchdarkly-ai-openai-messages`, `launchdarkly-ai-langchain-agents`, `launchdarkly-ai-langchain-messages`, `launchdarkly-ai-opencode-agents`, …): Each wraps a specific AI provider SDK. Depends on `launchdarkly-ai-server` for shared types and utilities. Must not depend on other Tier 1 packages. The OpenCode handler reduces caller-supplied session events and does not depend on an OpenCode SDK.
- **Tier 1 — Handler packages** (`launchdarkly-ai-claude-agents`, `launchdarkly-ai-claude-messages`, `launchdarkly-ai-openai-agents`, `launchdarkly-ai-openai-messages`, `launchdarkly-ai-langchain-agents`, `launchdarkly-ai-langchain-messages`, `launchdarkly-ai-litellm-agents`, `launchdarkly-ai-litellm-messages`, `launchdarkly-ai-opencode-agents`, …): Each wraps a specific AI provider SDK. Depends on `launchdarkly-ai-server` for shared types and utilities. Must not depend on other Tier 1 packages. The OpenCode handler reduces caller-supplied session events and does not depend on an OpenCode SDK.
- **Tier 2 — Consumer applications** (e.g. `main.py`, downstream projects): Imports from one or more handler packages and either `launchdarkly-ai-python` or `launchdarkly-ai-server`. Owns tool implementations and orchestration logic. No `launchdarkly-ai-*` package should ever depend on Tier 2 code.

### Rules
Expand Down Expand Up @@ -391,7 +392,7 @@ Requires `handlers` (either in `options` or via `options.registry`) to be set.

Resolves an agent graph's topology and node configs without executing it. The returned `GraphDefinition` carries `enabled`; callers should branch on it before traversing.

This is the entry point that framework-native runners (`to_claude_agents`, `to_openai_agents`, `to_lang_graph`) use to build their own execution structure.
This is the entry point that framework-native runners (`to_claude_agents`, `to_openai_agents`, `to_lang_graph`, `to_litellm_agents`) use to build their own execution structure.

### `Registry` / `global_registry` / `compose`

Expand Down Expand Up @@ -676,7 +677,7 @@ def my_provider(

For example, `claude_agents(config_key, user_input, context)` is equivalent to `config(key=config_key, handler=create_claude_agents_handler()).invoke(user_input, context)`.

The naming convention matches the package suffix: `claude_agents`, `claude_messages`, `openai_agents`, `openai_messages`, `langchain_agents`, `langchain_messages`.
The naming convention matches the package suffix: `claude_agents`, `claude_messages`, `openai_agents`, `openai_messages`, `langchain_agents`, `langchain_messages`, `litellm_agents`, `litellm_messages`.

### Graph Export (optional)

Expand All @@ -687,7 +688,7 @@ def claude_graph(key: str, **options) -> GraphInstance:
return graph(key, handlers=[create_claude_agents_handler()], **options)
```

Naming convention: `claude_graph`, `openai_graph`, `langchain_graph`.
Naming convention: `claude_graph`, `openai_graph`, `langchain_graph`, `litellm_graph`.

### Native Graph Adapter (optional)

Expand All @@ -697,6 +698,7 @@ Current adapters:
- `to_claude_agents(def_coro, opts)` — exported from `launchdarkly_ai_claude_agents`
- `to_openai_agents(def_coro, opts)` — exported from `launchdarkly_ai_openai_agents`
- `to_lang_graph(def_coro, opts)` — exported from `launchdarkly_ai_langchain_agents`
- `to_litellm_agents(def_coro, opts)` — exported from `launchdarkly_ai_litellm_agents`

---

Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ That call is the whole integration. Everything it does is configured in LaunchDa
- Run agents and multi-step graphs, where each step can use a different provider
- Score output quality with judges, including scoring that stays off the request path
- See cost, latency, token usage, errors, and full conversations with no instrumentation code
- Keep the providers and frameworks you already run: OpenAI, Anthropic, LangChain, or your own handler
- Keep the providers and frameworks you already run: OpenAI, Anthropic, LangChain, LiteLLM, or your own handler

- [What you get](#what-you-get)
- [How It Works](#how-it-works)
Expand Down Expand Up @@ -85,6 +85,8 @@ Tier 0 — Core Client (launchdarkly-ai-server)
| [`launchdarkly-ai-claude-agents`](packages/claude-agents/README.md) | Anthropic | `agent` | Claude Agent SDK — agentic loop with MCP tool support |
| [`launchdarkly-ai-langchain-messages`](packages/langchain-messages/README.md) | `*` (any) | `messages` | Any `BaseChatModel` via LangChain `bind_tools` loop |
| [`launchdarkly-ai-langchain-agents`](packages/langchain-agents/README.md) | `*` (any) | `agent` | LangGraph `StateGraph` — managed ReAct loop |
| [`launchdarkly-ai-litellm-messages`](packages/litellm-messages/README.md) | `*` (any) | `messages` | In-process LiteLLM `acompletion` tool loop |
| [`launchdarkly-ai-litellm-agents`](packages/litellm-agents/README.md) | `*` (any) | `agent` | OpenAI Agents SDK with LiteLLM models |

## Quick Start

Expand All @@ -111,6 +113,9 @@ cp .env.example .env
# Fill in LD_SDK_KEY and the API key for your provider
```

LiteLLM runs in-process in Python, so no proxy endpoint is required. Set the
provider credentials required by the model names in your AI Config.

### 3. Call a model

#### 3a. Convenience functions
Expand Down Expand Up @@ -149,6 +154,8 @@ asyncio.run(main())
| `claude_agents` | `launchdarkly-ai-claude-agents` | `claude-agent-sdk` | Claude Agent SDK (MCP) |
| `langchain_messages` | `launchdarkly-ai-langchain-messages` | `langchain-core` | LangChain `bind_tools` loop |
| `langchain_agents` | `launchdarkly-ai-langchain-agents` | `langgraph` | LangGraph `StateGraph` |
| `litellm_messages` | `launchdarkly-ai-litellm-messages` | `litellm` | LiteLLM `acompletion` |
| `litellm_agents` | `launchdarkly-ai-litellm-agents` | `openai-agents[litellm]` | OpenAI Agents via LiteLLM |

---

Expand Down Expand Up @@ -250,7 +257,7 @@ async def main():
asyncio.run(main())
```

Provider packages also export single-provider conveniences (`claude_graph`, `openai_graph`, `langchain_graph`) that pre-bind their handler.
Provider packages also export conveniences (`claude_graph`, `openai_graph`, `langchain_graph`, `litellm_graph`) that pre-bind their handler.

---

Expand Down Expand Up @@ -311,6 +318,22 @@ result = await to_openai_agents(
).invoke("I was double charged")
```

##### `to_litellm_agents` — OpenAI Agents SDK over LiteLLM

Builds the same native Agents SDK handoff tree while creating each evaluated
node model through LiteLLM in-process.

```python
from launchdarkly_ai_server import resolve_graph
from launchdarkly_ai_litellm_agents import to_litellm_agents

ctx = {"kind": "user", "key": "user-123"}
result = await to_litellm_agents(
resolve_graph("support-graph", context=ctx),
{"tool_handlers": registry.tools, "context": ctx},
).invoke("I was double charged")
```

##### `to_lang_graph` — LangGraph `StateGraph`

Uses a pre-order traversal (root → leaves) to build a compiled `StateGraph`. Single-child edges become direct edges; multi-child edges use `Command`-returning handoff tools so the model picks exactly one target.
Expand Down Expand Up @@ -597,6 +620,9 @@ uv run python main.py [example] [flag-key] [user-input]
| `graph-history` | `uv run python main.py graph-history` | `graph().invoke()` with multimodal `history` forwarded to the root node |
| `openai-only` | `uv run python main.py openai-only` | `config()` with a custom `Registry` restricted to OpenAI handlers |
| `streaming` | `uv run python main.py streaming` | `config().stream()` — token-by-token output |
| `litellm` | `uv run python main.py litellm` | `config()` routing both messages and agents through LiteLLM |
| `litellm-agents` | `uv run python main.py litellm-agents` | `litellm_agents()` with in-process LiteLLM routing |
| `litellm-messages` | `uv run python main.py litellm-messages` | `litellm_messages()` with in-process LiteLLM routing |

**Examples:**

Expand Down Expand Up @@ -636,7 +662,9 @@ python-ai-sdk/
│ ├── openai-agents/ # launchdarkly-ai-openai-agents
│ ├── openai-messages/ # launchdarkly-ai-openai-messages
│ ├── langchain-agents/ # launchdarkly-ai-langchain-agents
│ └── langchain-messages/ # launchdarkly-ai-langchain-messages
│ ├── langchain-messages/ # launchdarkly-ai-langchain-messages
│ ├── litellm-agents/ # launchdarkly-ai-litellm-agents
│ └── litellm-messages/ # launchdarkly-ai-litellm-messages
├── .env.example # Template — copy to .env and fill in your values
└── agents.md # Architecture reference for AI agents and contributors
```
Expand Down
37 changes: 37 additions & 0 deletions examples/litellm_agents_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Example: litellm_agents() — LiteLLM agents handler (wildcard provider).

Usage (via main.py):
python main.py litellm-agents <flag-key> "<user input>"
"""

from __future__ import annotations

import json

from examples.tools import (
fetch_launchdarkly_documentation,
get_preferences,
search_ld_documentation,
web_search,
)
from examples.utils import new_context, write_output
from launchdarkly_ai_litellm_agents import litellm_agents


async def run(key: str, user_input: str) -> None:
response = await litellm_agents(
key,
user_input,
new_context(),
tool_handlers={
"get-user-preferences": get_preferences,
"search-ld-documentation": search_ld_documentation,
"fetch-ld-documentation": fetch_launchdarkly_documentation,
"fetch-launchdarkly-documentation": fetch_launchdarkly_documentation,
"web-search": web_search,
},
)

print(json.dumps(response, indent=2, default=str))
write_output(response)
41 changes: 41 additions & 0 deletions examples/litellm_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Example: config() with LiteLLM handlers for messages and agents.

Python calls LiteLLM in-process, so provider-qualified models and aliases from
the evaluated AI Config are routed using the installed LiteLLM configuration.

Usage (via main.py):
python main.py litellm <flag-key> "<user input>"
"""

from __future__ import annotations

from examples.tools import (
fetch_launchdarkly_documentation,
get_preferences,
search_ld_documentation,
web_search,
)
from examples.utils import new_context, write_output
from launchdarkly_ai_litellm_agents import create_litellm_agents_handler
from launchdarkly_ai_litellm_messages import create_litellm_messages_handler
from launchdarkly_ai_server import config


async def run(key: str, user_input: str) -> None:
response = await config(
key=key,
handler=[
create_litellm_messages_handler(),
create_litellm_agents_handler(),
],
tool_handlers={
"get-user-preferences": get_preferences,
"search-ld-documentation": search_ld_documentation,
"fetch-ld-documentation": fetch_launchdarkly_documentation,
"fetch-launchdarkly-documentation": fetch_launchdarkly_documentation,
"web-search": web_search,
},
).invoke(user_input, new_context(), variables={"user_input": user_input})

write_output(response)
Loading
Loading