Skip to content

Commit 95cdf22

Browse files
Merge pull request #112 from aws-samples/rxu/deep-research
Modify to support Bedrock
2 parents a6836c4 + 9897d67 commit 95cdf22

File tree

4 files changed

+116
-55
lines changed

4 files changed

+116
-55
lines changed

langchain/deep-research/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
OPENAI_API_KEY=
2+
BEDROCK_AWS_ACCESS_KEY_ID=
3+
BEDROCK_AWS_SECRET_ACCESS_KEY=
4+
BEDROCK_AWS_REGION=
25
ANTHROPIC_API_KEY=
36
GOOGLE_API_KEY=
47
TAVILY_API_KEY=

langchain/deep-research/src/agent/configuration.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Configuration(BaseModel):
4040

4141
# General Configuration
4242
max_structured_output_retries: int = Field(
43-
default=3,
43+
default=2,
4444
metadata={
4545
"x_oap_ui_config": {
4646
"type": "number",
@@ -92,11 +92,11 @@ class Configuration(BaseModel):
9292
}
9393
)
9494
max_researcher_iterations: int = Field(
95-
default=6,
95+
default=3,
9696
metadata={
9797
"x_oap_ui_config": {
9898
"type": "slider",
99-
"default": 4,
99+
"default": 3,
100100
"min": 1,
101101
"max": 10,
102102
"step": 1,
@@ -105,11 +105,11 @@ class Configuration(BaseModel):
105105
}
106106
)
107107
max_react_tool_calls: int = Field(
108-
default=10,
108+
default=4,
109109
metadata={
110110
"x_oap_ui_config": {
111111
"type": "slider",
112-
"default": 10,
112+
"default": 4,
113113
"min": 1,
114114
"max": 30,
115115
"step": 1,
@@ -119,11 +119,11 @@ class Configuration(BaseModel):
119119
)
120120
# Model Configuration
121121
summarization_model: str = Field(
122-
default="openai:gpt-4.1-nano",
122+
default="bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
123123
metadata={
124124
"x_oap_ui_config": {
125125
"type": "text",
126-
"default": "openai:gpt-4.1-nano",
126+
"default": "bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
127127
"description": "Model for summarizing research results from Tavily search results"
128128
}
129129
}
@@ -151,11 +151,11 @@ class Configuration(BaseModel):
151151
}
152152
)
153153
research_model: str = Field(
154-
default="openai:gpt-4.1-nano",
154+
default="bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
155155
metadata={
156156
"x_oap_ui_config": {
157157
"type": "text",
158-
"default": "openai:gpt-4.1-nano",
158+
"default": "bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
159159
"description": "Model for conducting research. NOTE: Make sure your Researcher Model supports the selected search API."
160160
}
161161
}
@@ -171,11 +171,11 @@ class Configuration(BaseModel):
171171
}
172172
)
173173
compression_model: str = Field(
174-
default="openai:gpt-4.1-nano",
174+
default="bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
175175
metadata={
176176
"x_oap_ui_config": {
177177
"type": "text",
178-
"default": "openai:gpt-4.1-nano",
178+
"default": "bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
179179
"description": "Model for compressing research findings from sub-agents. NOTE: Make sure your Compression Model supports the selected search API."
180180
}
181181
}
@@ -191,11 +191,11 @@ class Configuration(BaseModel):
191191
}
192192
)
193193
final_report_model: str = Field(
194-
default="openai:gpt-4.1-nano",
194+
default="bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
195195
metadata={
196196
"x_oap_ui_config": {
197197
"type": "text",
198-
"default": "openai:gpt-4.1-nano",
198+
"default": "bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0",
199199
"description": "Model for writing the final report from all research findings"
200200
}
201201
}

langchain/deep-research/src/agent/graph.py

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242
from agent.utils import (
4343
anthropic_websearch_called,
44+
build_model_config,
4445
get_all_tools,
4546
get_api_key_for_model,
4647
get_model_token_limit,
@@ -78,12 +79,12 @@ async def clarify_with_user(state: AgentState, config: RunnableConfig) -> Comman
7879

7980
# Step 2: Prepare the model for structured clarification analysis
8081
messages = state["messages"]
81-
model_config = {
82-
"model": configurable.research_model,
83-
"max_tokens": configurable.research_model_max_tokens,
84-
"api_key": get_api_key_for_model(configurable.research_model, config),
85-
"tags": ["langsmith:nostream"]
86-
}
82+
model_config = build_model_config(
83+
configurable.research_model,
84+
configurable.research_model_max_tokens,
85+
config,
86+
tags=["langsmith:nostream"]
87+
)
8788

8889
# Configure model with structured output and retry logic
8990
clarification_model = (
@@ -131,12 +132,12 @@ async def write_research_brief(state: AgentState, config: RunnableConfig) -> Com
131132
"""
132133
# Step 1: Set up the research model for structured output
133134
configurable = Configuration.from_runnable_config(config)
134-
research_model_config = {
135-
"model": configurable.research_model,
136-
"max_tokens": configurable.research_model_max_tokens,
137-
"api_key": get_api_key_for_model(configurable.research_model, config),
138-
"tags": ["langsmith:nostream"]
139-
}
135+
research_model_config = build_model_config(
136+
configurable.research_model,
137+
configurable.research_model_max_tokens,
138+
config,
139+
tags=["langsmith:nostream"]
140+
)
140141

141142
# Configure model for structured research question generation
142143
research_model = (
@@ -191,12 +192,12 @@ async def supervisor(state: SupervisorState, config: RunnableConfig) -> Command[
191192
"""
192193
# Step 1: Configure the supervisor model with available tools
193194
configurable = Configuration.from_runnable_config(config)
194-
research_model_config = {
195-
"model": configurable.research_model,
196-
"max_tokens": configurable.research_model_max_tokens,
197-
"api_key": get_api_key_for_model(configurable.research_model, config),
198-
"tags": ["langsmith:nostream"]
199-
}
195+
research_model_config = build_model_config(
196+
configurable.research_model,
197+
configurable.research_model_max_tokens,
198+
config,
199+
tags=["langsmith:nostream"]
200+
)
200201

201202
# Available tools: research delegation, completion signaling, and strategic thinking
202203
lead_researcher_tools = [ConductResearch, ResearchComplete, think_tool]
@@ -389,12 +390,12 @@ async def researcher(state: ResearcherState, config: RunnableConfig) -> Command[
389390
)
390391

391392
# Step 2: Configure the researcher model with tools
392-
research_model_config = {
393-
"model": configurable.research_model,
394-
"max_tokens": configurable.research_model_max_tokens,
395-
"api_key": get_api_key_for_model(configurable.research_model, config),
396-
"tags": ["langsmith:nostream"]
397-
}
393+
research_model_config = build_model_config(
394+
configurable.research_model,
395+
configurable.research_model_max_tokens,
396+
config,
397+
tags=["langsmith:nostream"]
398+
)
398399

399400
# Prepare system prompt with MCP context if available
400401
researcher_prompt = research_system_prompt.format(
@@ -524,12 +525,13 @@ async def compress_research(state: ResearcherState, config: RunnableConfig):
524525
"""
525526
# Step 1: Configure the compression model
526527
configurable = Configuration.from_runnable_config(config)
527-
synthesizer_model = configurable_model.with_config({
528-
"model": configurable.compression_model,
529-
"max_tokens": configurable.compression_model_max_tokens,
530-
"api_key": get_api_key_for_model(configurable.compression_model, config),
531-
"tags": ["langsmith:nostream"]
532-
})
528+
compression_model_config = build_model_config(
529+
configurable.compression_model,
530+
configurable.compression_model_max_tokens,
531+
config,
532+
tags=["langsmith:nostream"]
533+
)
534+
synthesizer_model = configurable_model.with_config(compression_model_config)
533535

534536
# Step 2: Prepare messages for compression
535537
researcher_messages = state.get("researcher_messages", [])
@@ -624,12 +626,12 @@ async def final_report_generation(state: AgentState, config: RunnableConfig):
624626

625627
# Step 2: Configure the final report generation model
626628
configurable = Configuration.from_runnable_config(config)
627-
writer_model_config = {
628-
"model": configurable.final_report_model,
629-
"max_tokens": configurable.final_report_model_max_tokens,
630-
"api_key": get_api_key_for_model(configurable.final_report_model, config),
631-
"tags": ["langsmith:nostream"]
632-
}
629+
writer_model_config = build_model_config(
630+
configurable.final_report_model,
631+
configurable.final_report_model_max_tokens,
632+
config,
633+
tags=["langsmith:nostream"]
634+
)
633635

634636
# Step 3: Attempt report generation with token limit retry logic
635637
max_retries = 3

langchain/deep-research/src/agent/utils.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@
3333
from agent.prompts import summarize_webpage_prompt
3434
from agent.state import ResearchComplete, Summary
3535

36+
##########################
37+
# AWS Credentials Setup
38+
##########################
39+
def setup_bedrock_credentials():
40+
"""Set up AWS credentials from BEDROCK_* environment variables if standard AWS_* vars are not set."""
41+
# Only set if standard AWS credentials are not already set
42+
if not os.getenv("AWS_ACCESS_KEY_ID") and os.getenv("BEDROCK_AWS_ACCESS_KEY_ID"):
43+
os.environ["AWS_ACCESS_KEY_ID"] = os.getenv("BEDROCK_AWS_ACCESS_KEY_ID")
44+
if not os.getenv("AWS_SECRET_ACCESS_KEY") and os.getenv("BEDROCK_AWS_SECRET_ACCESS_KEY"):
45+
os.environ["AWS_SECRET_ACCESS_KEY"] = os.getenv("BEDROCK_AWS_SECRET_ACCESS_KEY")
46+
if not os.getenv("AWS_DEFAULT_REGION") and os.getenv("BEDROCK_AWS_REGION"):
47+
os.environ["AWS_DEFAULT_REGION"] = os.getenv("BEDROCK_AWS_REGION")
48+
# Also set AWS_REGION if not set
49+
if not os.getenv("AWS_REGION") and os.getenv("BEDROCK_AWS_REGION"):
50+
os.environ["AWS_REGION"] = os.getenv("BEDROCK_AWS_REGION")
51+
3652
##########################
3753
# Tavily Search Tool Utils
3854
##########################
@@ -82,12 +98,14 @@ async def tavily_search(
8298
max_char_to_include = configurable.max_content_length
8399

84100
# Initialize summarization model with retry logic
85-
model_api_key = get_api_key_for_model(configurable.summarization_model, config)
86-
summarization_model = init_chat_model(
87-
model=configurable.summarization_model,
88-
max_tokens=configurable.summarization_model_max_tokens,
89-
api_key=model_api_key,
101+
summarization_model_config = build_model_config(
102+
configurable.summarization_model,
103+
configurable.summarization_model_max_tokens,
104+
config,
90105
tags=["langsmith:nostream"]
106+
)
107+
summarization_model = init_chat_model(
108+
**summarization_model_config
91109
).with_structured_output(Summary).with_retry(
92110
stop_after_attempt=configurable.max_structured_output_retries
93111
)
@@ -890,9 +908,20 @@ def get_config_value(value):
890908
return value.value
891909

892910
def get_api_key_for_model(model_name: str, config: RunnableConfig):
893-
"""Get API key for a specific model from environment or config."""
911+
"""Get API key for a specific model from environment or config.
912+
913+
For Bedrock models, this sets up AWS credentials from BEDROCK_* env vars and returns None
914+
(Bedrock uses AWS credentials instead of API keys).
915+
"""
894916
should_get_from_config = os.getenv("GET_API_KEYS_FROM_CONFIG", "false")
895917
model_name = model_name.lower()
918+
919+
# Bedrock models don't use API keys, they use AWS credentials
920+
# Set up AWS credentials from BEDROCK_* env vars if this is a Bedrock model
921+
if model_name.startswith("bedrock:"):
922+
setup_bedrock_credentials()
923+
return None
924+
896925
if should_get_from_config.lower() == "true":
897926
api_keys = config.get("configurable", {}).get("apiKeys", {})
898927
if not api_keys:
@@ -913,6 +942,33 @@ def get_api_key_for_model(model_name: str, config: RunnableConfig):
913942
return os.getenv("GOOGLE_API_KEY")
914943
return None
915944

945+
def build_model_config(model_name: str, max_tokens: int, config: RunnableConfig, tags: Optional[List[str]] = None):
946+
"""Build a model configuration dictionary, excluding api_key for Bedrock models.
947+
948+
Args:
949+
model_name: The model identifier
950+
max_tokens: Maximum tokens for the model
951+
config: Runtime configuration
952+
tags: Optional list of tags to include
953+
954+
Returns:
955+
Dictionary with model configuration, excluding api_key for Bedrock models
956+
"""
957+
api_key = get_api_key_for_model(model_name, config)
958+
model_config = {
959+
"model": model_name,
960+
"max_tokens": max_tokens,
961+
}
962+
963+
# Only include api_key if it's not None (i.e., not a Bedrock model)
964+
if api_key is not None:
965+
model_config["api_key"] = api_key
966+
967+
if tags:
968+
model_config["tags"] = tags
969+
970+
return model_config
971+
916972
def get_tavily_api_key(config: RunnableConfig):
917973
"""Get Tavily API key from environment or config."""
918974
should_get_from_config = os.getenv("GET_API_KEYS_FROM_CONFIG", "false")

0 commit comments

Comments
 (0)