Skip to content

Commit a10ab45

Browse files
authored
feat: Add AWS DynamoDB and SES components (#25)
- Add DynamoDB session store for conversation persistence with TTL - Add SES email sending component with HTML/text support - Support for IAM roles and explicit AWS credentials - Support for temporary session credentials
1 parent b190deb commit a10ab45

File tree

3 files changed

+698
-28
lines changed

3 files changed

+698
-28
lines changed

langbuilder/src/backend/base/langbuilder/components/amazon/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
from langbuilder.components.amazon.s3_bucket_uploader import S3BucketUploaderComponent
1111
from langbuilder.components.amazon.dynamodb_session_store import DynamoDBSessionStoreComponent
1212
from langbuilder.components.amazon.dynamodb_session_retrieve import DynamoDBSessionRetrieveComponent
13+
from langbuilder.components.amazon.ses_send_email import SESSendEmailComponent
1314

1415
_dynamic_imports = {
1516
"AmazonBedrockEmbeddingsComponent": "amazon_bedrock_embedding",
1617
"AmazonBedrockComponent": "amazon_bedrock_model",
1718
"S3BucketUploaderComponent": "s3_bucket_uploader",
1819
"DynamoDBSessionStoreComponent": "dynamodb_session_store",
1920
"DynamoDBSessionRetrieveComponent": "dynamodb_session_retrieve",
21+
"SESSendEmailComponent": "ses_send_email",
2022
}
2123

2224
__all__ = [
@@ -25,6 +27,7 @@
2527
"S3BucketUploaderComponent",
2628
"DynamoDBSessionStoreComponent",
2729
"DynamoDBSessionRetrieveComponent",
30+
"SESSendEmailComponent",
2831
]
2932

3033

langbuilder/src/backend/base/langbuilder/components/amazon/dynamodb_session_store.py

Lines changed: 158 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
import uuid
1212
from datetime import datetime, timedelta
1313

14-
from langbuilder.custom.custom_component.component import Component
14+
from langchain_core.tools import StructuredTool
15+
from pydantic import BaseModel, Field
16+
17+
from langbuilder.base.langchain_utilities.model import LCToolComponent
18+
from langbuilder.field_typing import Tool
1519
from langbuilder.io import BoolInput, DataInput, DropdownInput, IntInput, MessageTextInput, Output, SecretStrInput, StrInput
1620
from langbuilder.schema.data import Data
1721

@@ -25,7 +29,7 @@
2529
]
2630

2731

28-
class DynamoDBSessionStoreComponent(Component):
32+
class DynamoDBSessionStoreComponent(LCToolComponent):
2933
"""
3034
Store session data to AWS DynamoDB table.
3135
@@ -49,6 +53,7 @@ class DynamoDBSessionStoreComponent(Component):
4953
display_name="Structured Data",
5054
info="Data from Structured Output component containing conversation metadata",
5155
required=True,
56+
tool_mode=True, # Agent can pass data when calling as tool
5257
),
5358

5459
# User message input
@@ -57,6 +62,7 @@ class DynamoDBSessionStoreComponent(Component):
5762
display_name="User Message",
5863
info="The user's message (from Chat Input)",
5964
required=True,
65+
tool_mode=True, # Agent can pass context when calling as tool
6066
),
6167

6268
# Session identification
@@ -81,18 +87,24 @@ class DynamoDBSessionStoreComponent(Component):
8187
name="aws_access_key_id",
8288
display_name="AWS Access Key ID",
8389
info="AWS Access Key ID (leave empty to use IAM role or environment credentials)",
84-
value="AWS_ACCESS_KEY_ID",
8590
required=False,
8691
),
8792

8893
SecretStrInput(
8994
name="aws_secret_access_key",
9095
display_name="AWS Secret Access Key",
9196
info="AWS Secret Access Key (leave empty to use IAM role or environment credentials)",
92-
value="AWS_SECRET_ACCESS_KEY",
9397
required=False,
9498
),
9599

100+
SecretStrInput(
101+
name="aws_session_token",
102+
display_name="AWS Session Token",
103+
info="AWS Session Token for temporary credentials (optional)",
104+
required=False,
105+
advanced=True,
106+
),
107+
96108
DropdownInput(
97109
name="region_name",
98110
display_name="AWS Region",
@@ -122,13 +134,7 @@ class DynamoDBSessionStoreComponent(Component):
122134
),
123135
]
124136

125-
outputs = [
126-
Output(
127-
name="stored_session",
128-
display_name="Stored Session",
129-
method="store_session",
130-
),
131-
]
137+
# Uses default outputs from LCToolComponent base class
132138

133139
def _ensure_table_exists(self, dynamodb, table):
134140
"""
@@ -197,7 +203,7 @@ def _ensure_table_exists(self, dynamodb, table):
197203
# Re-raise other errors
198204
raise
199205

200-
def store_session(self) -> Data:
206+
def run_model(self) -> Data:
201207
"""
202208
Store session data to DynamoDB.
203209
@@ -232,13 +238,18 @@ def store_session(self) -> Data:
232238
try:
233239
# Use credentials if provided, otherwise use IAM role or environment
234240
if self.aws_access_key_id and self.aws_secret_access_key:
235-
dynamodb = boto3.resource(
236-
"dynamodb",
237-
region_name=self.region_name,
238-
aws_access_key_id=self.aws_access_key_id,
239-
aws_secret_access_key=self.aws_secret_access_key,
240-
)
241-
self.log("Using provided AWS credentials")
241+
client_kwargs = {
242+
"region_name": self.region_name,
243+
"aws_access_key_id": self.aws_access_key_id,
244+
"aws_secret_access_key": self.aws_secret_access_key,
245+
}
246+
# Add session token if provided (for temporary credentials)
247+
if self.aws_session_token:
248+
client_kwargs["aws_session_token"] = self.aws_session_token
249+
self.log("Using temporary session credentials")
250+
else:
251+
self.log("Using provided AWS credentials")
252+
dynamodb = boto3.resource("dynamodb", **client_kwargs)
242253
else:
243254
dynamodb = boto3.resource("dynamodb", region_name=self.region_name)
244255
self.log("Using IAM role or environment credentials")
@@ -283,24 +294,23 @@ def store_session(self) -> Data:
283294
session_id = str(uuid.uuid4())
284295
self.log("WARNING: Auto-generated session_id - conversation continuity will be lost!")
285296

286-
# Prepare item to save - extract fields from structured output
297+
# Prepare item to save - base required fields
287298
item = {
288299
"session_id": session_id, # Primary key
289300
"timestamp": now.isoformat(),
290301
"ttl": ttl_timestamp, # DynamoDB TTL attribute
291302
"user_message": str(self.user_message),
292-
"ai_response": output_data.get("response", ""),
293-
"phase": output_data.get("phase", ""),
294-
"current_question": output_data.get("current_question", ""),
295-
"questions_answered": output_data.get("questions_answered", []),
296-
"answers": output_data.get("answers", {}),
297-
"persona_hint": output_data.get("persona_hint", ""),
298-
"email": output_data.get("email", ""),
299303
}
300304

305+
# Dynamically include ALL fields from structured data
306+
# This allows storing any data shape (contact info, conversation state, etc.)
307+
for key, value in output_data.items():
308+
if key not in item: # Don't overwrite mandatory fields
309+
item[key] = value
310+
301311
# Log operation
302312
self.log(f"Storing session {session_id} to table {self.table_name}")
303-
self.log(f"Phase: {item['phase']}, Question: {item['current_question']}")
313+
self.log(f"Fields: {list(output_data.keys())}")
304314
self.log(f"TTL: {ttl_timestamp} ({self.ttl_days} days from now)")
305315

306316
# Put item to DynamoDB
@@ -325,3 +335,123 @@ def store_session(self) -> Data:
325335

326336
# Return Data object with stored information
327337
return Data(data=item)
338+
339+
async def _get_tools(self):
340+
"""Override to return the named tool from build_tool() instead of generic outputs."""
341+
tool = self.build_tool()
342+
# Ensure tool has tags for framework compatibility
343+
if tool and not tool.tags:
344+
tool.tags = [tool.name]
345+
return [tool] if tool else []
346+
347+
def build_tool(self) -> Tool:
348+
"""
349+
Build a LangChain tool for storing session data to DynamoDB.
350+
351+
Returns:
352+
StructuredTool: A tool that can be used by an Agent to store data.
353+
"""
354+
# Define flexible schema for conversation state
355+
class StoreConversationStateInput(BaseModel):
356+
answers: str = Field(
357+
default="",
358+
description="JSON string of question answers, e.g. '{\"q1\": \"B\", \"q2\": \"A\"}'"
359+
)
360+
phase: str = Field(
361+
default="discovery",
362+
description="Current conversation phase: 'opening', 'discovery', 'closing', 'complete'"
363+
)
364+
current_question: str = Field(
365+
default="",
366+
description="Current question being asked, e.g. 'q3' or 'email'"
367+
)
368+
persona_hint: str = Field(
369+
default="",
370+
description="Detected persona type, e.g. 'Exec-leaning', 'Technical', 'General'"
371+
)
372+
blueprint: str = Field(
373+
default="",
374+
description="Generated blueprint or proposal content"
375+
)
376+
name: str = Field(
377+
default="",
378+
description="Contact's name (if collected)"
379+
)
380+
email: str = Field(
381+
default="",
382+
description="Contact's email (if collected)"
383+
)
384+
company: str = Field(
385+
default="",
386+
description="Contact's company (if collected)"
387+
)
388+
389+
def _store_conversation_state(
390+
answers: str = "",
391+
phase: str = "discovery",
392+
current_question: str = "",
393+
persona_hint: str = "",
394+
blueprint: str = "",
395+
name: str = "",
396+
email: str = "",
397+
company: str = ""
398+
) -> str:
399+
"""Tool function that stores full conversation state."""
400+
import json as json_module
401+
402+
# Parse answers if it's a JSON string
403+
answers_dict = {}
404+
if answers:
405+
try:
406+
answers_dict = json_module.loads(answers)
407+
except:
408+
answers_dict = {"raw": answers}
409+
410+
# Build the full state object
411+
state_data = {
412+
"phase": phase,
413+
"current_question": current_question,
414+
"persona_hint": persona_hint,
415+
"answers": answers_dict,
416+
"questions_answered": list(answers_dict.keys()) if answers_dict else [],
417+
}
418+
419+
# Add optional fields if provided
420+
if blueprint:
421+
state_data["blueprint"] = blueprint
422+
if name:
423+
state_data["name"] = name
424+
if email:
425+
state_data["email"] = email
426+
if company:
427+
state_data["company"] = company
428+
429+
# Set the structured_data attribute
430+
self.structured_data = Data(data=state_data)
431+
432+
# Set user_message
433+
if not self.user_message:
434+
self.user_message = f"Conversation state saved at phase: {phase}"
435+
436+
# Call run_model to store
437+
result = self.run_model()
438+
if hasattr(result, 'data'):
439+
session_id = result.data.get('session_id', 'unknown')
440+
return f"Conversation state saved. Phase: {phase}, Questions: {list(answers_dict.keys())}, Session: {session_id}"
441+
return f"Conversation state saved. Phase: {phase}"
442+
443+
tool = StructuredTool.from_function(
444+
name="store_conversation_state",
445+
description=(
446+
"Store full conversation state to DynamoDB. Use this after each question is answered "
447+
"to persist the conversation progress. Pass answers as a JSON string like "
448+
"'{\"q1\": \"B\", \"q2\": \"A\"}'. Also use this to save the blueprint/proposal content."
449+
),
450+
args_schema=StoreConversationStateInput,
451+
func=_store_conversation_state,
452+
return_direct=False,
453+
tags=["store_conversation_state"],
454+
)
455+
456+
self.status = "DynamoDB Conversation State Tool created"
457+
return tool

0 commit comments

Comments
 (0)