Skip to content

Commit 59a54e1

Browse files
authored
feat: Add LangGraph multi agent example (#12)
1 parent aaaed58 commit 59a54e1

3 files changed

Lines changed: 303 additions & 5 deletions

File tree

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ This repository includes examples for `OpenAI`, `Bedrock`, and `LangChain` for m
1212

1313
### General setup
1414

15-
1. Set the environment variable `LAUNCHDARKLY_SDK_KEY` to your LaunchDarkly SDK key. If there is an existing AI Config in your LaunchDarkly project that you want to evaluate, set `LAUNCHDARKLY_AI_CONFIG_KEY` to the flag key; otherwise, an AI Config of `sample-ai-config` or `sample-ai-agent-config` will be assumed.
15+
1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) using the key specified in each example, or copy the key of existing AI Config in your LaunchDarkly project that you want to evaluate.
16+
1. Set the environment variable `LAUNCHDARKLY_SDK_KEY` to your LaunchDarkly SDK key and `LAUNCHDARKLY_AI_CONFIG_KEY` to the AI Config key; otherwise, an AI Config of `sample-ai-config` or `sample-ai-agent-config` will be assumed for most examples.
1617

1718
```bash
1819
export LAUNCHDARKLY_SDK_KEY="1234567890abcdef"
@@ -46,15 +47,27 @@ This repository includes examples for `OpenAI`, `Bedrock`, and `LangChain` for m
4647
This example uses `OpenAI`, `Bedrock`, and `Gemini` LangChain provider packages. You can add additional LangChain providers using the `poetry add` command.
4748

4849
1. Install all dependencies with `poetry install -E langchain` or `poetry install --all-extras`.
49-
1. Set up API keys for the providers you want to use
50+
1. Set up API keys for the providers you want to use.
5051
1. On the command line, run `poetry run langchain-example`
5152

5253
#### LangGraph setup (multiple providers, single agent)
5354

5455
1. Install all dependencies with `poetry install -E langgraph` or `poetry install --all-extras`.
55-
1. Set up API keys for the providers you want to use
56-
1. Optionally set environment variable for the agent config:
56+
1. Set up API keys for the providers you want to use.
57+
1. Optionally set this environment variable to use a different agent config:
5758
```bash
5859
export LAUNCHDARKLY_AGENT_CONFIG_KEY="sample-ai-agent-config"
5960
```
60-
1. On the command line, run `poetry run langgraph-agent-example`
61+
1. On the command line, run `poetry run langgraph-agent-example`.
62+
63+
#### LangGraph setup (multiple providers, multiple agents)
64+
65+
1. Install all dependencies with `poetry install -E langgraph` or `poetry install --all-extras`.
66+
1. Set up API keys for the providers you want to use.
67+
1. [Create an AI Config (Agent-based)](https://launchdarkly.com/docs/home/ai-configs/agents) using the keys below. Write a goal for each config and enable it with targeting rules.
68+
1. Optionally set these environment variables to use different agent configs:
69+
```bash
70+
export LAUNCHDARKLY_ANALYZER_CONFIG_KEY="code-review-analyzer"
71+
export LAUNCHDARKLY_DOCUMENTATION_CONFIG_KEY="code-review-documentation"
72+
```
73+
1. On the command line, run `poetry run langgraph-multi-agent-example`.
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import os
2+
import ldclient
3+
from ldclient import Context
4+
from ldclient.config import Config
5+
from ldai.client import LDAIClient, LDAIAgentConfig, LDAIAgentDefaults
6+
from ldai.tracker import TokenUsage
7+
from langchain.chat_models import init_chat_model
8+
from langgraph.prebuilt import create_react_agent
9+
from langgraph.graph import StateGraph, END
10+
from langgraph.types import Command
11+
from typing_extensions import TypedDict
12+
13+
# Set sdk_key to your LaunchDarkly SDK key.
14+
sdk_key = os.getenv('LAUNCHDARKLY_SDK_KEY')
15+
16+
# Set config keys for the two agents
17+
analyzer_config_key = os.getenv('LAUNCHDARKLY_ANALYZER_CONFIG_KEY', 'code-review-analyzer')
18+
documentation_config_key = os.getenv('LAUNCHDARKLY_DOCUMENTATION_CONFIG_KEY', 'code-review-documentation')
19+
20+
# Custom state class for the code review workflow
21+
class CodeReviewState(TypedDict):
22+
messages: list
23+
analysis: str
24+
documentation: str
25+
final_report: str
26+
27+
def map_provider_to_langchain(provider_name):
28+
"""Map LaunchDarkly provider names to LangChain provider names."""
29+
provider_mapping = {
30+
'gemini': 'google_genai'
31+
}
32+
lower_provider = provider_name.lower()
33+
return provider_mapping.get(lower_provider, lower_provider)
34+
35+
def track_langgraph_metrics(tracker, func, prev_message_count=0):
36+
"""
37+
Track LangGraph agent operations with LaunchDarkly metrics.
38+
"""
39+
try:
40+
result = tracker.track_duration_of(func)
41+
tracker.track_success()
42+
43+
# For LangGraph agents, usage_metadata is included on all messages that used AI
44+
total_input_tokens = 0
45+
total_output_tokens = 0
46+
total_tokens = 0
47+
48+
if "messages" in result:
49+
# Only look at messages that were added during this function call
50+
new_messages = result['messages'][prev_message_count:]
51+
print(f"New messages: {new_messages}")
52+
for message in new_messages:
53+
# Check for usage_metadata directly on the message
54+
if hasattr(message, "usage_metadata") and message.usage_metadata:
55+
usage_data = message.usage_metadata
56+
total_input_tokens += usage_data.get("input_tokens", 0)
57+
total_output_tokens += usage_data.get("output_tokens", 0)
58+
total_tokens += usage_data.get("total_tokens", 0)
59+
60+
if total_tokens > 0:
61+
token_usage = TokenUsage(
62+
input=total_input_tokens,
63+
output=total_output_tokens,
64+
total=total_tokens
65+
)
66+
tracker.track_tokens(token_usage)
67+
except Exception:
68+
tracker.track_error()
69+
raise
70+
return result
71+
72+
# Note: Agent instructions are now configured through LaunchDarkly AI flags
73+
# The SDK will use the instructions from the flag configuration
74+
75+
def create_agent_with_config(aiclient, config_key, context):
76+
"""Create a LangChain model with LaunchDarkly AI config."""
77+
default_value = LDAIAgentDefaults(
78+
enabled=False, # Disabled by default
79+
)
80+
81+
agent_config = aiclient.agent(
82+
LDAIAgentConfig(
83+
key=config_key,
84+
default_value=default_value,
85+
),
86+
context
87+
)
88+
89+
if not agent_config.enabled:
90+
return None, None, True
91+
92+
langchain_provider = map_provider_to_langchain(agent_config.provider.name)
93+
llm = init_chat_model(
94+
model=agent_config.model.name,
95+
model_provider=langchain_provider,
96+
)
97+
98+
# Create a React agent with the LLM
99+
agent = create_react_agent(llm, [], prompt=agent_config.instructions)
100+
101+
return agent, agent_config.tracker, False
102+
103+
def ai_node(
104+
state: CodeReviewState,
105+
aiclient,
106+
context,
107+
config_key: str,
108+
state_key: str,
109+
next_step: str
110+
) -> Command:
111+
"""Unified function to process code with AI agents (analysis or documentation)."""
112+
print(f"Starting node for {config_key}")
113+
114+
try:
115+
agent, tracker, disabled = create_agent_with_config(
116+
aiclient, config_key, context
117+
)
118+
119+
if disabled:
120+
return Command(
121+
goto=END,
122+
update={
123+
"messages": state["messages"],
124+
state_key: f"AI Config {config_key} is disabled. Node for {config_key} skipped."
125+
}
126+
)
127+
128+
# Track and execute the AI operation
129+
prev_message_count = len(state["messages"])
130+
completion = track_langgraph_metrics(tracker, lambda: agent.invoke({"messages": state["messages"]}), prev_message_count)
131+
132+
# Extract the content from the agent's response
133+
content = ""
134+
if completion["messages"]:
135+
last_message = completion["messages"][-1]
136+
if hasattr(last_message, 'content'):
137+
content = last_message.content
138+
139+
# Return Command to update state and route to next step
140+
return Command(
141+
goto=next_step,
142+
update={
143+
"messages": completion["messages"],
144+
state_key: content
145+
}
146+
)
147+
148+
except Exception as e:
149+
print(f"❌ Error in node for {config_key}: {e}")
150+
return Command(
151+
goto=END,
152+
update={
153+
"messages": [{"role": "system", "content": f"Error: {str(e)}"}],
154+
state_key: f"Error: {str(e)}"
155+
}
156+
)
157+
158+
def create_final_report(state: CodeReviewState) -> Command:
159+
"""Combine analysis and documentation into a final report."""
160+
print("Creating final report")
161+
162+
# Use the stored analysis and documentation from state
163+
analysis = state.get("analysis", "No analysis available")
164+
documentation = state.get("documentation", "No documentation available")
165+
166+
final_report = f"""# Code Review Report
167+
168+
## Code Analysis
169+
{analysis}
170+
171+
## Generated Documentation
172+
{documentation}
173+
174+
---
175+
*This report was generated by the LaunchDarkly Code Review Duo using LangGraph*"""
176+
177+
print("✅ Final report created")
178+
179+
return Command(
180+
goto=END,
181+
update={
182+
"final_report": final_report
183+
}
184+
)
185+
186+
def main():
187+
if not sdk_key:
188+
print("*** Please set the LAUNCHDARKLY_SDK_KEY env first")
189+
exit()
190+
191+
ldclient.set_config(Config(sdk_key))
192+
if not ldclient.get().is_initialized():
193+
print("*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.")
194+
exit()
195+
196+
aiclient = LDAIClient(ldclient.get())
197+
print("*** SDK successfully initialized")
198+
199+
# Set up the evaluation context
200+
context = (
201+
Context
202+
.builder('code-review-user')
203+
.kind('user')
204+
.name('Code Reviewer')
205+
.build()
206+
)
207+
208+
# Sample code for review
209+
sample_code = '''
210+
def process_user_data(user_input):
211+
"""Process user input and return processed data."""
212+
data = user_input.strip()
213+
result = []
214+
215+
for item in data.split(','):
216+
result.append(item.upper())
217+
218+
return result
219+
220+
def calculate_average(numbers):
221+
total = 0
222+
count = 0
223+
224+
for num in numbers:
225+
total += num
226+
count += 1
227+
228+
return total / count
229+
'''
230+
231+
print("🔍 Starting Code Review Duo with LangGraph...")
232+
print(f"📋 Using analyzer config: {analyzer_config_key}")
233+
print(f"📝 Using documentation config: {documentation_config_key}")
234+
print()
235+
236+
# Create the workflow graph with custom state
237+
workflow = StateGraph(CodeReviewState)
238+
239+
# Add nodes with proper function signatures
240+
workflow.add_node("analyze", lambda state: ai_node(state, aiclient, context, analyzer_config_key, "analysis", "document"))
241+
workflow.add_node("document", lambda state: ai_node(state, aiclient, context, documentation_config_key, "documentation", "finalize"))
242+
workflow.add_node("finalize", create_final_report)
243+
244+
# Define the workflow
245+
workflow.set_entry_point("analyze")
246+
247+
# Compile the graph
248+
app = workflow.compile()
249+
250+
# Initialize state with the sample code
251+
initial_state = {
252+
"messages": [
253+
{
254+
"role": "user",
255+
"content": sample_code
256+
}
257+
],
258+
"analysis": "",
259+
"documentation": "",
260+
"final_report": ""
261+
}
262+
263+
# Execute the workflow
264+
try:
265+
result = app.invoke(initial_state)
266+
267+
print("\n" + "="*80)
268+
print("📊 FINAL CODE REVIEW REPORT")
269+
print("="*80)
270+
271+
# Use the final report from state
272+
final_report = result.get("final_report", "No report generated")
273+
print(final_report)
274+
print("="*80)
275+
276+
except Exception as e:
277+
print(f"❌ Error during workflow execution: {e}")
278+
print("Please ensure you have the correct API keys and credentials set up for the detected providers.")
279+
280+
# Close the client to flush events and close the connection.
281+
ldclient.get().close()
282+
283+
if __name__ == "__main__":
284+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ openai-example = 'examples.openai_example:main'
1313
gemini-example = 'examples.gemini_example:main'
1414
langchain-example = 'examples.langchain_example:main'
1515
langgraph-agent-example = 'examples.langgraph_agent_example:main'
16+
langgraph-multi-agent-example = 'examples.langgraph_multi_agent_example:main'
1617

1718
[tool.poetry.dependencies]
1819
python = "^3.9"

0 commit comments

Comments
 (0)