Skip to content

Commit 0a4cd3a

Browse files
FEAT: email action plan or referrals backend (#182)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 504565b commit 0a4cd3a

8 files changed

Lines changed: 391 additions & 228 deletions

File tree

app/src/common/components.py

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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]
346375
Here is your personalized report with resources your case manager recommends to support your goals.
347376
You'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

app/src/pipelines/email_full_result/__init__.py

Whitespace-only changes.

app/src/pipelines/email_full_result/pipeline_wrapper.py

Lines changed: 0 additions & 93 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
Email Responses Pipeline
3+
4+
Consolidated pipeline for emailing resources, action plans, or both to users.
5+
Handles three scenarios:
6+
1. Email only resources (resources_result_id provided)
7+
2. Email only action plan (action_plan_result_id provided)
8+
3. Email both resources and action plan (both IDs provided)
9+
"""
10+
11+
from .pipeline_wrapper import PipelineWrapper
12+
13+
__all__ = ["PipelineWrapper"]

0 commit comments

Comments
 (0)