Skip to content

Commit 62a8e25

Browse files
authored
feat: Update LangChain runners to implement Runner protocol returning RunnerResult (#150)
1 parent 382e662 commit 62a8e25

6 files changed

Lines changed: 205 additions & 170 deletions

File tree

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

Lines changed: 48 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -38,79 +38,68 @@ pip install langchain-google-genai
3838
import asyncio
3939
from ldclient import LDClient, Config, Context
4040
from ldai import init
41-
from ldai_langchain import LangChainProvider
41+
from ldai.models import AICompletionConfigDefault, ModelConfig, ProviderConfig
4242

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

47-
# Get AI configuration. Pass a default for improved resiliency when the flag is unavailable or
48-
# LaunchDarkly is unreachable; omit for a disabled default. Example:
49-
# from ldai.models import AICompletionConfigDefault, LDMessage, ModelConfig, ProviderConfig
50-
# default = AICompletionConfigDefault(
51-
# enabled=True,
52-
# model=ModelConfig("gpt-4"),
53-
# provider=ProviderConfig("openai"),
54-
# messages=[LDMessage(role="system", content="You are a helpful assistant.")]
55-
# )
56-
# config = ai_client.config("ai-config-key", context, default)
5747
context = Context.builder("user-123").build()
58-
config = ai_client.config("ai-config-key", context)
5948

6049
async def main():
61-
# Create a LangChain provider from the AI configuration
62-
provider = await LangChainProvider.create(config)
63-
64-
# Use the provider to invoke the model
65-
from ldai.models import LDMessage
66-
messages = [
67-
LDMessage(role="system", content="You are a helpful assistant."),
68-
LDMessage(role="user", content="Hello, how are you?"),
69-
]
70-
71-
response = await provider.invoke_model(messages)
72-
print(response.message.content)
50+
# Create a ManagedModel backed by the LangChain provider
51+
model = await ai_client.create_model(
52+
"ai-config-key",
53+
context,
54+
AICompletionConfigDefault(
55+
enabled=True,
56+
model=ModelConfig("gpt-4"),
57+
provider=ProviderConfig("langchain"),
58+
),
59+
)
60+
61+
if model:
62+
result = await model.run("Hello, how are you?")
63+
print(result.content)
7364

7465
asyncio.run(main())
7566
```
7667

7768
## Usage
7869

79-
### Using LangChainProvider with the Create Factory
70+
### Using `create_model` (recommended)
8071

81-
The simplest way to use the LangChain provider is with the static `create` factory method, which automatically creates the appropriate LangChain model based on your LaunchDarkly AI configuration:
72+
The recommended entry point is `LDAIClient.create_model`, which evaluates a
73+
LaunchDarkly AI config flag, selects the LangChain runner automatically, and
74+
returns a `ManagedModel` that wraps the runner:
8275

8376
```python
84-
from ldai_langchain import LangChainProvider
77+
model = await ai_client.create_model("ai-config-key", context)
8578

86-
# Create provider from AI configuration
87-
provider = await LangChainProvider.create(ai_config)
88-
89-
# Invoke the model
90-
response = await provider.invoke_model(messages)
79+
if model:
80+
result = await model.run("What is feature flagging?")
81+
print(result.content)
9182
```
9283

93-
### Using an Existing LangChain Model
84+
### Using the runner directly
9485

95-
If you already have a LangChain model configured, you can use it directly:
86+
If you need to construct a runner manually (e.g. for testing), you can use
87+
`LangChainRunnerFactory` from the `ldai_langchain` package:
9688

9789
```python
9890
from langchain_openai import ChatOpenAI
99-
from ldai_langchain import LangChainProvider
91+
from ldai_langchain import LangChainRunnerFactory
10092

101-
# Create your own LangChain model
10293
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
94+
runner = LangChainModelRunner(llm)
10395

104-
# Wrap it with LangChainProvider
105-
provider = LangChainProvider(llm)
106-
107-
# Use with LaunchDarkly tracking
108-
response = await provider.invoke_model(messages)
96+
result = await runner.run("Hello!")
97+
print(result.content)
10998
```
11099

111100
### Structured Output
112101

113-
The provider supports structured output using LangChain's `with_structured_output`:
102+
Pass a JSON schema dict as `output_type` to request structured output:
114103

115104
```python
116105
response_structure = {
@@ -122,92 +111,62 @@ response_structure = {
122111
"required": ["sentiment", "confidence"],
123112
}
124113

125-
result = await provider.invoke_structured_model(messages, response_structure)
126-
print(result.data) # {"sentiment": "positive", "confidence": 0.95}
114+
result = await runner.run(messages, output_type=response_structure)
115+
print(result.parsed) # {"sentiment": "positive", "confidence": 0.95}
127116
```
128117

129118
### Tracking Metrics
130119

131-
Use the provider with LaunchDarkly's tracking capabilities:
120+
`ManagedModel.run()` automatically tracks metrics via the associated
121+
`LDAIConfigTracker`. For manual tracking, use the tracker directly:
132122

133123
```python
134-
# Get the AI config with tracker
135-
config = ai_client.config("ai-config-key", context)
136-
137-
# Create provider
138-
provider = await LangChainProvider.create(config)
139-
140-
# Track metrics automatically
141-
async def invoke():
142-
return await provider.invoke_model(messages)
124+
model = await ai_client.create_model("ai-config-key", context)
143125

144-
response = await config.tracker.track_metrics_of_async(
145-
invoke,
146-
lambda r: r.metrics
147-
)
126+
if model:
127+
result = await model.run("Explain feature flags.")
128+
# Metrics are tracked automatically; access them via result.metrics
129+
print(result.metrics.usage)
148130
```
149131

150132
### Static Utility Methods
151133

152-
The `LangChainProvider` class provides several utility methods:
134+
The `ldai_langchain` helper module provides several utility functions:
153135

154136
#### Converting Messages
155137

156138
```python
157139
from ldai.models import LDMessage
158-
from ldai_langchain import LangChainProvider
140+
from ldai_langchain.langchain_helper import convert_messages_to_langchain
159141

160142
messages = [
161143
LDMessage(role="system", content="You are helpful."),
162144
LDMessage(role="user", content="Hello!"),
163145
]
164146

165-
# Convert to LangChain messages
166-
langchain_messages = LangChainProvider.convert_messages_to_langchain(messages)
147+
langchain_messages = convert_messages_to_langchain(messages)
167148
```
168149

169150
#### Extracting Metrics
170151

171152
```python
172-
from ldai_langchain import LangChainProvider
153+
from ldai_langchain.langchain_helper import get_ai_metrics_from_response
173154

174155
# After getting a response from LangChain
175-
metrics = LangChainProvider.get_ai_metrics_from_response(ai_message)
156+
metrics = get_ai_metrics_from_response(ai_message)
176157
print(f"Success: {metrics.success}")
177158
print(f"Tokens used: {metrics.usage.total if metrics.usage else 'N/A'}")
178159
```
179160

180161
#### Provider Name Mapping
181162

182163
```python
183-
# Map LaunchDarkly provider names to LangChain provider names
184-
langchain_provider = LangChainProvider.map_provider("gemini") # Returns "google-genai"
185-
```
186-
187-
## API Reference
188-
189-
### LangChainProvider
164+
from ldai_langchain.langchain_helper import map_provider_name
190165

191-
#### Constructor
192-
193-
```python
194-
LangChainProvider(llm: BaseChatModel, logger: Optional[Any] = None)
166+
# Map LaunchDarkly provider names to LangChain provider names
167+
langchain_provider = map_provider_name("gemini") # Returns "google-genai"
195168
```
196169

197-
#### Static Methods
198-
199-
- `create(ai_config: AIConfigKind, logger: Optional[Any] = None) -> LangChainProvider` - Factory method to create a provider from AI configuration
200-
- `convert_messages_to_langchain(messages: List[LDMessage]) -> List[BaseMessage]` - Convert LaunchDarkly messages to LangChain messages
201-
- `get_ai_metrics_from_response(response: AIMessage) -> LDAIMetrics` - Extract metrics from a LangChain response
202-
- `map_provider(ld_provider_name: str) -> str` - Map LaunchDarkly provider names to LangChain names
203-
- `create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel` - Create a LangChain model from AI configuration
204-
205-
#### Instance Methods
206-
207-
- `invoke_model(messages: List[LDMessage]) -> ChatResponse` - Invoke the model with messages
208-
- `invoke_structured_model(messages: List[LDMessage], response_structure: Dict[str, Any]) -> StructuredResponse` - Invoke with structured output
209-
- `get_chat_model() -> BaseChatModel` - Get the underlying LangChain model
210-
211170
## Documentation
212171

213172
For full documentation, please refer to the [LaunchDarkly AI SDK documentation](https://docs.launchdarkly.com/sdk/ai/python).
Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,100 @@
1-
from typing import Any
1+
from typing import Any, Dict, Optional
22

33
from ldai import log
4-
from ldai.providers import AgentResult, AgentRunner
5-
from ldai.providers.types import LDAIMetrics
4+
from ldai.providers.runner import Runner
5+
from ldai.providers.types import LDAIMetrics, RunnerResult
66

77
from ldai_langchain.langchain_helper import (
88
extract_last_message_content,
99
sum_token_usage_from_messages,
1010
)
1111

1212

13-
class LangChainAgentRunner(AgentRunner):
13+
class LangChainAgentRunner(Runner):
1414
"""
1515
CAUTION:
1616
This feature is experimental and should NOT be considered ready for production use.
1717
It may change or be removed without notice and is not subject to backwards
1818
compatibility guarantees.
1919
20-
AgentRunner implementation for LangChain.
20+
Runner implementation for LangChain agents.
2121
2222
Wraps a compiled LangChain agent graph (from ``langchain.agents.create_agent``)
2323
and delegates execution to it. Tool calling and loop management are handled
2424
internally by the graph.
2525
Returned by LangChainRunnerFactory.create_agent(config, tools).
26+
27+
Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
28+
:meth:`run`.
2629
"""
2730

2831
def __init__(self, agent: Any):
2932
self._agent = agent
3033

31-
async def run(self, input: Any) -> AgentResult:
34+
async def run(
35+
self,
36+
input: Any,
37+
output_type: Optional[Dict[str, Any]] = None,
38+
) -> RunnerResult:
3239
"""
3340
Run the agent with the given input string.
3441
3542
Delegates to the compiled LangChain agent, which handles
3643
the tool-calling loop internally.
3744
3845
:param input: The user prompt or input to the agent
39-
:return: AgentResult with output, raw response, and aggregated metrics
46+
:param output_type: Reserved for future structured output support;
47+
currently ignored.
48+
:return: :class:`RunnerResult` with ``content``, ``raw`` response, and
49+
aggregated metrics.
4050
"""
4151
try:
4252
result = await self._agent.ainvoke({
4353
"messages": [{"role": "user", "content": str(input)}]
4454
})
4555
messages = result.get("messages", [])
4656
output = extract_last_message_content(messages)
47-
return AgentResult(
48-
output=output,
49-
raw=result,
57+
tool_calls = _extract_tool_calls(messages)
58+
return RunnerResult(
59+
content=output,
5060
metrics=LDAIMetrics(
5161
success=True,
5262
usage=sum_token_usage_from_messages(messages),
63+
tool_calls=tool_calls if tool_calls else None,
5364
),
65+
raw=result,
5466
)
5567
except Exception as error:
5668
log.warning(f"LangChain agent run failed: {error}")
57-
return AgentResult(
58-
output="",
59-
raw=None,
69+
return RunnerResult(
70+
content="",
6071
metrics=LDAIMetrics(success=False, usage=None),
6172
)
6273

6374
def get_agent(self) -> Any:
6475
"""Return the underlying compiled LangChain agent."""
6576
return self._agent
77+
78+
79+
def _extract_tool_calls(messages: Any) -> list:
80+
"""
81+
Extract tool-call names from a LangChain agent's message list.
82+
83+
LangChain's ``AIMessage`` exposes ``.tool_calls`` as a list of dicts
84+
(``{'name': ..., 'args': ..., 'id': ...}``). Some providers emit
85+
OpenAI-style objects with ``.function.name`` instead; handle both shapes.
86+
"""
87+
tool_calls: list = []
88+
for msg in messages or []:
89+
msg_tool_calls = getattr(msg, 'tool_calls', None)
90+
if not msg_tool_calls:
91+
continue
92+
for tc in msg_tool_calls:
93+
if isinstance(tc, dict) and 'name' in tc:
94+
tool_calls.append(tc['name'])
95+
else:
96+
fn = getattr(tc, 'function', None)
97+
name = getattr(fn, 'name', None) if fn is not None else None
98+
if name:
99+
tool_calls.append(name)
100+
return tool_calls

0 commit comments

Comments
 (0)