@@ -147,13 +147,31 @@ def run(self, messages: List[ChatMessage]) -> dict:
147147
148148
149149@component
150- class LoadResult :
150+ class LoadResultOptional :
151151 """
152- Loads result from database.
152+ Loads result from database with optional result_id.
153+
154+ If result_id is None or empty string, returns an empty dict instead of raising an error.
155+ This allows pipelines to gracefully handle optional inputs.
156+
157+ Returns:
158+ dict: {"result_json": dict} where dict is either the loaded JSON or empty dict
153159 """
154160
155- @component .output_types (result_json = dict )
156- def run (self , result_id : str ) -> dict :
161+ @staticmethod
162+ def _load_and_parse_result (result_id : str ) -> dict :
163+ """
164+ Helper function to load result from database and parse JSON.
165+
166+ Args:
167+ result_id: The UUID of the result to load
168+
169+ Returns:
170+ dict: The parsed JSON result
171+
172+ Raises:
173+ ValueError: If result not found or JSON parsing fails
174+ """
157175 with config .db_session () as db_session , db_session .begin ():
158176 db_record = (
159177 db_session .query (LlmResponse ).filter (LlmResponse .id == result_id ).one_or_none ()
@@ -172,6 +190,17 @@ def run(self, result_id: str) -> dict:
172190 raise ValueError (f"Invalid JSON format in result with id={ result_id } : { text !r} " )
173191
174192 json_dict = json .loads (text [start : end + 1 ])
193+ return json_dict
194+
195+ @component .output_types (result_json = dict )
196+ def run (self , result_id : Optional [str ]) -> dict :
197+ # Return empty dict if no result_id provided
198+ if not result_id :
199+ logger .debug ("No result_id provided, returning empty dict" )
200+ return {"result_json" : {}}
201+
202+ # Otherwise, use shared helper to load and parse result
203+ json_dict = self ._load_and_parse_result (result_id )
175204 return {"result_json" : json_dict }
176205
177206
@@ -346,54 +375,94 @@ def _add_child_spans(self, web_search_responses: list[ResponseFunctionWebSearch]
346375Here is your personalized report with resources your case manager recommends to support your goals.
347376You've already taken a great first step by exploring these options.
348377
349- **Your next step**: Look over the resources to see contact info and details about how to get started.
378+ **Your next step**: Look over the resources to see contact info and details about how to get started.\
350379 """
351380
352381
353382@component
354- class EmailFullResult :
383+ class EmailResponses :
355384 """
356- Formats JSON object (representing a list of resources and action plan) and sends it to email address.
385+ Unified email component that handles sending resources, action plans, or both.
386+
387+ This component consolidates the functionality of EmailResult, EmailActionPlan, and
388+ EmailFullResult into a single component that dynamically formats and sends emails
389+ based on what content is provided.
390+
391+ Scenarios handled:
392+ 1. Resources only: resources_dict provided, action_plan_dict empty
393+ 2. Action plan only: action_plan_dict provided, resources_dict empty
394+ 3. Both: both dicts provided
395+
396+ Args:
397+ email: Recipient email address
398+ resources_dict: Dict containing resources data (empty dict if not provided)
399+ action_plan_dict: Dict containing action plan data (empty dict if not provided)
400+
401+ Returns:
402+ dict: Contains status ("success" or "failed"), email, and message content
403+
404+ Raises:
405+ ValueError: If neither resources_dict nor action_plan_dict has content
357406 """
358407
359408 @component .output_types (status = str , email = str , message = str )
360409 def run (self , email : str , resources_dict : dict , action_plan_dict : dict ) -> dict :
361- logger .info ("Emailing result to %s" , email )
362- logger .debug ("Resources JSON content:\n %s" , json .dumps (resources_dict , indent = 2 ))
363- if action_plan_dict :
364- logger .debug ("Action plan JSON content:\n %s" , json .dumps (action_plan_dict , indent = 2 ))
410+ # Determine what content we have
411+ # Check if resources dict has a non-empty "resources" list
412+ has_resources = bool (resources_dict and resources_dict .get ("resources" ))
413+ # Check if action plan dict has content (title, summary, or content fields)
414+ has_action_plan = bool (
415+ action_plan_dict
416+ and (
417+ action_plan_dict .get ("title" )
418+ or action_plan_dict .get ("summary" )
419+ or action_plan_dict .get ("content" )
420+ )
421+ )
365422
366- formatted_resources = format_resources (resources_dict .get ("resources" , []))
367- formatted_action_plan = format_action_plan (action_plan_dict )
423+ # Validate that at least one type of content is provided
424+ if not has_resources and not has_action_plan :
425+ raise ValueError (
426+ "At least one of resources_dict or action_plan_dict must contain valid data. "
427+ f"Received resources_dict={ bool (resources_dict )} , action_plan_dict={ bool (action_plan_dict )} "
428+ )
368429
369- message = f"{ EMAIL_INTRO } \n { formatted_resources } \n \n { formatted_action_plan } "
430+ logger .info (
431+ "Emailing to %s (resources=%s, action_plan=%s)" , email , has_resources , has_action_plan
432+ )
370433
371- # Send email via AWS SES
372- subject = "Your Requested Resources and Action Plan"
373- success = send_email (recipient = email , subject = subject , body = message )
374- status = "success" if success else "failed"
434+ # Log the content we're working with
435+ if has_resources :
436+ logger .debug ("Resources JSON content:\n %s" , json .dumps (resources_dict , indent = 2 ))
437+ if has_action_plan :
438+ logger .debug ("Action plan JSON content:\n %s" , json .dumps (action_plan_dict , indent = 2 ))
375439
376- return {"status" : status , "email" : email , "message" : message }
440+ # Format content based on what's available
441+ message_parts = [EMAIL_INTRO ]
377442
443+ if has_resources :
444+ formatted_resources = format_resources (resources_dict .get ("resources" , []))
445+ message_parts .append (formatted_resources )
378446
379- @component
380- class EmailResult :
381- """
382- Formats JSON object (representing a list of resources) and sends it to email address.
383- """
447+ if has_action_plan :
448+ formatted_action_plan = format_action_plan (action_plan_dict )
449+ message_parts .append (formatted_action_plan )
384450
385- @component .output_types (status = str , email = str , message = str )
386- def run (self , email : str , json_dict : dict ) -> dict :
387- logger .info ("Emailing result to %s" , email )
388- logger .debug ("JSON content:\n %s" , json .dumps (json_dict , indent = 2 ))
389- formatted_resources = format_resources (json_dict .get ("resources" , []))
390- message = f"{ EMAIL_INTRO } \n { formatted_resources } "
451+ message = "\n \n " .join (message_parts )
452+
453+ # Set subject based on what's included
454+ if has_resources and has_action_plan :
455+ subject = "Your Requested Resources and Action Plan"
456+ elif has_resources :
457+ subject = "Your Requested Resources"
458+ else : # has_action_plan only
459+ subject = "Your Personalized Action Plan"
391460
392461 # Send email via AWS SES
393- subject = "Your Requested Resources"
394462 success = send_email (recipient = email , subject = subject , body = message )
395463 status = "success" if success else "failed"
396464
465+ logger .info ("Email send status: %s" , status )
397466 return {"status" : status , "email" : email , "message" : message }
398467
399468
0 commit comments