Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
194 changes: 156 additions & 38 deletions packages/ai-providers/server-ai-openai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,64 +23,182 @@ pip install launchdarkly-server-sdk-ai-openai

```python
import asyncio
from ldai import AIClient
from ldai_openai import OpenAIProvider
from ldclient import LDClient, Config, Context
from ldai import init
from ldai.models import AICompletionConfigDefault, ModelConfig, ProviderConfig

# Initialize LaunchDarkly client
ld_client = LDClient(Config("your-sdk-key"))
ai_client = init(ld_client)

context = Context.builder("user-123").build()

async def main():
# Initialize the AI client
ai_client = AIClient(ld_client)

# Get AI config. Pass a default for improved resiliency when the flag is unavailable or
# LaunchDarkly is unreachable; omit for a disabled default. Example:
# from ldai.models import AICompletionConfigDefault, LDMessage, ModelConfig, ProviderConfig
# default = AICompletionConfigDefault(
# enabled=True,
# model=ModelConfig("gpt-4"),
# provider=ProviderConfig("openai"),
# messages=[LDMessage(role="system", content="You are a helpful assistant.")]
# )
# ai_config = ai_client.config("my-ai-config-key", context, default)
ai_config = ai_client.config("my-ai-config-key", context)

# Create an OpenAI provider from the config
provider = await OpenAIProvider.create(ai_config)

# Invoke the model
response = await provider.invoke_model(ai_config.messages)
print(response.message.content)
# Create a ManagedModel backed by the OpenAI provider
model = await ai_client.create_model(
"ai-config-key",
context,
AICompletionConfigDefault(
enabled=True,
model=ModelConfig("gpt-4"),
provider=ProviderConfig("openai"),
),
)

if model:
result = await model.run("Hello, how are you?")
print(result.content)

asyncio.run(main())
```

## Features
## Usage

### Using `create_model` (recommended)

The recommended entry point is `LDAIClient.create_model`, which evaluates a
LaunchDarkly AI config flag, selects the OpenAI runner automatically, and
returns a `ManagedModel` that wraps the runner:

```python
model = await ai_client.create_model("ai-config-key", context)

if model:
result = await model.run("What is feature flagging?")
print(result.content)
```

### Using the runner directly

If you need to construct a runner manually (e.g. for testing), you can use
`OpenAIRunnerFactory` from the `ldai_openai` package:

```python
from ldai_openai import OpenAIRunnerFactory

factory = OpenAIRunnerFactory() # uses OPENAI_API_KEY from environment
runner = factory.create_model(ai_config)

result = await runner.run("Hello!")
print(result.content)
```

### Structured Output

Pass a JSON schema dict as `output_type` to request structured output:

```python
response_structure = {
"type": "object",
"properties": {
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
"confidence": {"type": "number"},
},
"required": ["sentiment", "confidence"],
}

result = await runner.run(messages, output_type=response_structure)
print(result.parsed) # {"sentiment": "positive", "confidence": 0.95}
```

### Tracking Metrics

`ManagedModel.run()` automatically tracks metrics via the associated
`LDAIConfigTracker`. For manual tracking, use the tracker directly:

```python
model = await ai_client.create_model("ai-config-key", context)

if model:
result = await model.run("Explain feature flags.")
# Metrics are tracked automatically; access them via result.metrics
print(result.metrics.usage)
```

### Static Utility Methods

The `ldai_openai` helper module provides several utility functions:

#### Converting Messages

```python
from ldai.models import LDMessage
from ldai_openai import convert_messages_to_openai

messages = [
LDMessage(role="system", content="You are helpful."),
LDMessage(role="user", content="Hello!"),
]

openai_messages = convert_messages_to_openai(messages)
```

#### Extracting Metrics

- Full integration with OpenAI's chat completions API
- Automatic token usage tracking
- Support for structured output (JSON schema)
- Static utility methods for custom integrations
```python
from ldai_openai import get_ai_metrics_from_response

# After getting a response from OpenAI
metrics = get_ai_metrics_from_response(response)
print(f"Success: {metrics.success}")
print(f"Tokens used: {metrics.usage.total if metrics.usage else 'N/A'}")
```

## API Reference

### OpenAIProvider
### OpenAIRunnerFactory

`OpenAIRunnerFactory` is an `AIProvider` that creates runners from LaunchDarkly AI configs.

#### Constructor

```python
OpenAIProvider(client: OpenAI, model_name: str, parameters: Dict[str, Any], logger: Optional[Any] = None)
OpenAIRunnerFactory(client: Optional[AsyncOpenAI] = None)
```

#### Static Methods
If `client` is omitted, an `AsyncOpenAI` client is created using `OPENAI_API_KEY` from the environment.

- `create(ai_config: AIConfigKind, logger: Optional[Any] = None) -> OpenAIProvider` - Factory method to create a provider from an AI config
- `get_ai_metrics_from_response(response: Any) -> LDAIMetrics` - Extract metrics from an OpenAI response
#### Methods

#### Instance Methods
- `create_model(config) -> OpenAIModelRunner` — Create a runner for chat completions from an AI config.
- `create_agent(config, tools=None) -> OpenAIAgentRunner` — Create a runner for an OpenAI agent (experimental).
- `get_client() -> AsyncOpenAI` — Return the underlying `AsyncOpenAI` client.

- `invoke_model(messages: List[LDMessage]) -> ChatResponse` - Invoke the model with messages
- `invoke_structured_model(messages: List[LDMessage], response_structure: Dict[str, Any]) -> StructuredResponse` - Invoke the model with structured output
- `get_client() -> OpenAI` - Get the underlying OpenAI client
### OpenAIModelRunner

`OpenAIModelRunner` implements the `Runner` protocol for OpenAI chat completions.

#### Constructor

```python
OpenAIModelRunner(client: AsyncOpenAI, model_name: str, parameters: Dict[str, Any])
```

#### Methods

- `run(input, output_type=None) -> RunnerResult` — Run the model with a string prompt or list of `LDMessage` objects. Pass `output_type` (JSON schema dict) for structured output.

### OpenAIAgentRunner

> [!CAUTION]
> This feature is experimental and should NOT be considered ready for production use.
> It may change or be removed without notice.

`OpenAIAgentRunner` implements the `Runner` protocol using the OpenAI Agents SDK
(`openai-agents`). Requires `pip install openai-agents`.

#### Methods

- `run(input, output_type=None) -> RunnerResult` — Run the agent with the given input. The tool-calling loop is handled internally. Returns `RunnerResult` with `content`, `metrics` (including `tool_calls`), and `raw`.

## Documentation

For full documentation, please refer to the [LaunchDarkly AI SDK documentation](https://docs.launchdarkly.com/sdk/ai/python).

## Contributing

See [CONTRIBUTING.md](../../../CONTRIBUTING.md) in the repository root.

## License

Apache-2.0

Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

from ldai import log
from ldai.providers import AgentResult, AgentRunner, ToolRegistry
from ldai.providers import RunnerResult, ToolRegistry
from ldai.providers.runner import Runner
from ldai.providers.types import LDAIMetrics

from ldai_openai.openai_helper import (
get_ai_usage_from_response,
get_tool_calls_from_run_items,
is_agent_tool_instance,
registry_value_to_agent_tool,
)


class OpenAIAgentRunner(AgentRunner):
class OpenAIAgentRunner(Runner):
"""
CAUTION:
This feature is experimental and should NOT be considered ready for production use.
It may change or be removed without notice and is not subject to backwards
compatibility guarantees.

AgentRunner implementation for OpenAI.
Runner implementation for a single OpenAI agent.

Executes a single agent using the OpenAI Agents SDK (``openai-agents``).
Tool calling and the agentic loop are handled internally by ``Runner.run``.
Returned by OpenAIRunnerFactory.create_agent(config, tools).
Returned by ``OpenAIRunnerFactory.create_agent(config, tools)``.

Implements the unified :class:`~ldai.providers.runner.Runner` protocol.
Requires ``openai-agents`` to be installed.
"""

Expand All @@ -39,16 +43,24 @@ def __init__(
self._instructions = instructions
self._tool_definitions = tool_definitions
self._tools = tools
self._tool_name_map: Dict[str, str] = {}

async def run(self, input: Any) -> AgentResult:
async def run(
self,
input: Any,
output_type: Optional[Dict[str, Any]] = None,
) -> RunnerResult:
"""
Run the agent with the given input string.
Run the agent with the given input.

Delegates to the OpenAI Agents SDK ``Runner.run``, which handles the
tool-calling loop internally.

:param input: The user prompt or input to the agent
:return: AgentResult with output, raw response, and aggregated metrics
:param output_type: Reserved for future structured output support;
currently ignored.
:return: :class:`RunnerResult` with ``content``, ``raw`` response, and
metrics including aggregated token usage and observed ``tool_calls``.
Comment thread
cursor[bot] marked this conversation as resolved.
"""
try:
from agents import Agent, Runner
Expand All @@ -57,7 +69,10 @@ async def run(self, input: Any) -> AgentResult:
"openai-agents is required for OpenAIAgentRunner. "
"Install it with: pip install openai-agents"
)
return AgentResult(output="", raw=None, metrics=LDAIMetrics(success=False, usage=None))
return RunnerResult(
content="",
metrics=LDAIMetrics(success=False, usage=None),
)

try:
agent_tools = self._build_agent_tools()
Expand All @@ -73,21 +88,38 @@ async def run(self, input: Any) -> AgentResult:

result = await Runner.run(agent, str(input), max_turns=25)

return AgentResult(
output=str(result.final_output),
raw=result,
tool_calls = [
ld_name
for _agent_name, tool_fn_name in get_tool_calls_from_run_items(result.new_items)
for ld_name in [self._tool_name_map.get(tool_fn_name)]
if ld_name is not None
]
Comment thread
cursor[bot] marked this conversation as resolved.

return RunnerResult(
content=str(result.final_output),
metrics=LDAIMetrics(
success=True,
usage=get_ai_usage_from_response(result),
tool_calls=tool_calls if tool_calls else None,
),
raw=result,
)
except Exception as error:
log.warning(f"OpenAI agent run failed: {error}")
return AgentResult(output="", raw=None, metrics=LDAIMetrics(success=False, usage=None))
return RunnerResult(
content="",
metrics=LDAIMetrics(success=False, usage=None),
)

def _build_agent_tools(self) -> List[Any]:
"""Build tool instances from LD tool definitions and registry."""
"""Build tool instances from LD tool definitions and registry.

Also populates ``self._tool_name_map`` so observed tool-call names
from the runtime can be translated back to their LD config keys for
metric reporting.
"""
tools = []
self._tool_name_map = {}
for td in self._tool_definitions:
if not isinstance(td, dict):
continue
Expand All @@ -97,6 +129,14 @@ def _build_agent_tools(self) -> List[Any]:

tool_fn = self._tools.get(name)
if tool_fn:
# Map runtime tool name → LD config key for metrics (function __name__
# for callables; identity for native tool instances — see get_tool_calls_from_run_items).
if is_agent_tool_instance(tool_fn):
self._tool_name_map[tool_fn.name] = name
else:
fn_name = getattr(tool_fn, '__name__', None)
if fn_name:
self._tool_name_map[fn_name] = name
tools.append(registry_value_to_agent_tool(tool_fn))
continue

Expand Down
Loading
Loading