1111import uuid
1212from 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
1519from langbuilder .io import BoolInput , DataInput , DropdownInput , IntInput , MessageTextInput , Output , SecretStrInput , StrInput
1620from langbuilder .schema .data import Data
1721
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