Skip to content

Commit b52a1db

Browse files
authored
Prompt refactoring V2 additional (#19)
* moved handle processing more modular and scaleable to custom handles * poetry updates * minor sub fixes to the overall prompt refactoring changes * ruff linting changes * did some redundant prompt removals and updated schedule handle prompts as suggested * added custom exception for handle resolving issues * fixed annottations issue in email agent
1 parent 46cf952 commit b52a1db

File tree

14 files changed

+349
-286
lines changed

14 files changed

+349
-286
lines changed

mxtoai/agents/email_agent.py

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import re
44
from datetime import datetime
5-
from typing import Any
5+
from typing import Any, List, Dict
66

77
from dotenv import load_dotenv
88

@@ -18,9 +18,15 @@
1818
)
1919

2020
from mxtoai._logging import get_logger
21-
from mxtoai.prompts.base_prompts import create_attachment_processing_task, create_email_context, create_task_template
21+
from mxtoai.models import ProcessingInstructions
22+
from mxtoai.prompts.base_prompts import (
23+
LIST_FORMATTING_REQUIREMENTS,
24+
MARKDOWN_STYLE_GUIDE,
25+
RESEARCH_GUIDELINES,
26+
RESPONSE_GUIDELINES,
27+
)
2228
from mxtoai.routed_litellm_model import RoutedLiteLLMModel
23-
from mxtoai.schemas import EmailRequest
29+
from mxtoai.schemas import EmailRequest, EmailAttachment
2430
from mxtoai.scripts.report_formatter import ReportFormatter
2531
from mxtoai.scripts.visual_qa import azure_visualizer
2632
from mxtoai.tools.attachment_processing_tool import AttachmentProcessingTool
@@ -131,7 +137,7 @@ def __init__(
131137
)
132138

133139
# Define the list of tools available to the agent
134-
self.available_tools: list[Tool] = [
140+
self.available_tools: List[Tool] = [
135141
self.attachment_tool,
136142
self.schedule_tool,
137143
self.visit_webpage_tool,
@@ -168,7 +174,7 @@ def _init_agent(self):
168174

169175
logger.debug("Agent initialized with routed model configuration")
170176

171-
def _get_required_actions(self, mode: str) -> list[str]:
177+
def _get_required_actions(self, mode: str) -> List[str]:
172178
"""Get list of required actions based on mode."""
173179
actions = []
174180
if mode in ["summary", "full"]:
@@ -202,7 +208,7 @@ def determine_mode_from_email(to_email: str) -> str:
202208

203209
return mode_mapping.get(email_prefix, "full")
204210

205-
def _get_attachment_types(self, attachments: list[dict[str, Any]]) -> list[str]:
211+
def _get_attachment_types(self, attachments: List[Dict[str, Any]]) -> List[str]:
206212
"""Get list of attachment types."""
207213
types = []
208214
for att in attachments:
@@ -211,36 +217,74 @@ def _get_attachment_types(self, attachments: list[dict[str, Any]]) -> list[str]:
211217
types.append(content_type)
212218
return types
213219

214-
def _create_task(self, email_request: EmailRequest, email_instructions: "EmailHandleInstructions") -> str:
220+
def _create_task(self, email_request: EmailRequest, email_instructions: ProcessingInstructions) -> str:
215221
"""Create a task description for the agent based on email handle instructions."""
216-
# Create attachment details with explicit paths if needed
217-
attachment_details = []
218-
if email_instructions.process_attachments and email_request.attachments:
219-
for att in email_request.attachments:
220-
# Quote the file path to handle spaces correctly
221-
quoted_path = f'"{att.path}"'
222-
attachment_details.append(
223-
f"- {att.filename} (Type: {att.contentType}, Size: {att.size} bytes)\n"
224-
f" EXACT FILE PATH: {quoted_path}"
225-
)
226-
227-
# Create the email context section
228-
email_context = create_email_context(email_request, attachment_details)
229-
230-
# Create attachment processing task if needed
231-
attachment_task = create_attachment_processing_task(attachment_details) if attachment_details else ""
222+
attachments = self._format_attachments(email_request.attachments) \
223+
if email_instructions.process_attachments and email_request.attachments else []
232224

233-
# Create the complete task template
234-
return create_task_template(
225+
return self._create_task_template(
235226
handle=email_instructions.handle,
236-
email_context=email_context,
227+
email_context=self._create_email_context(email_request, attachments),
237228
handle_specific_template=email_instructions.task_template,
238-
attachment_task=attachment_task,
229+
attachment_task=self._create_attachment_task(attachments),
239230
deep_research_mandatory=email_instructions.deep_research_mandatory,
240231
output_template=email_instructions.output_template
241232
)
242233

243-
def _process_agent_result(self, final_answer_obj: Any, agent_steps: list) -> dict[str, Any]:
234+
def _format_attachments(self, attachments: List[EmailAttachment]) -> List[str]:
235+
"""Format attachment details for inclusion in the task."""
236+
return [
237+
f"- {att.filename} (Type: {att.contentType}, Size: {att.size} bytes)\n"
238+
f' EXACT FILE PATH: "{att.path}"'
239+
for att in attachments
240+
]
241+
def _create_email_context(self, email_request: EmailRequest, attachment_details=None) -> str:
242+
"""Generate context information from the email request."""
243+
recipients = ", ".join(email_request.recipients) if email_request.recipients else "N/A"
244+
attachments_info = f"Available Attachments:\n{chr(10).join(attachment_details)}" if attachment_details else "No attachments provided."
245+
246+
return f"""Email Content:
247+
Subject: {email_request.subject}
248+
From: {email_request.from_email}
249+
Email Date: {email_request.date}
250+
Recipients: {recipients}
251+
CC: {email_request.cc or "N/A"}
252+
BCC: {email_request.bcc or "N/A"}
253+
Body: {email_request.textContent or email_request.htmlContent or ""}
254+
255+
{attachments_info}
256+
"""
257+
258+
def _create_attachment_task(self, attachment_details: List[str]) -> str:
259+
"""Return instructions for processing attachments, if any."""
260+
return f"Process these attachments:\n{chr(10).join(attachment_details)}" if attachment_details else ""
261+
262+
def _create_task_template(
263+
self,
264+
handle: str,
265+
email_context: str,
266+
handle_specific_template: str = "",
267+
attachment_task: str = "",
268+
deep_research_mandatory: bool = False,
269+
output_template: str = "",
270+
) -> str:
271+
"""Combine all task components into the final task description."""
272+
sections = [
273+
f"Process this email according to the '{handle}' instruction type.\n",
274+
email_context,
275+
RESEARCH_GUIDELINES["mandatory"] if deep_research_mandatory else RESEARCH_GUIDELINES["optional"],
276+
attachment_task,
277+
handle_specific_template,
278+
output_template,
279+
RESPONSE_GUIDELINES,
280+
MARKDOWN_STYLE_GUIDE,
281+
LIST_FORMATTING_REQUIREMENTS
282+
]
283+
284+
return "\n\n".join(filter(None, sections)) # Filter out any empty strings
285+
286+
287+
def _process_agent_result(self, final_answer_obj: Any, agent_steps: List) -> Dict[str, Any]:
244288
"""
245289
Process the agent's result into our expected format, using the agent steps.
246290
Prioritizes direct output from the 'deep_research' tool if available.
@@ -604,7 +648,7 @@ def _process_agent_result(self, final_answer_obj: Any, agent_steps: list) -> dic
604648
def process_email(
605649
self,
606650
email_request: EmailRequest,
607-
email_instructions: "EmailHandleInstructions", # Type hint as string to avoid circular import
651+
email_instructions: ProcessingInstructions, # Type hint as string to avoid circular import
608652
) -> dict[str, Any]:
609653
"""
610654
Process an email using the agent based on the provided email handle instructions.

mxtoai/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
from mxtoai._logging import get_logger
1515
from mxtoai.agents.email_agent import EmailAgent
1616
from mxtoai.config import ATTACHMENTS_DIR, SKIP_EMAIL_DELIVERY
17+
from mxtoai.dependencies import processing_instructions_resolver
1718
from mxtoai.email_sender import (
1819
generate_email_id,
1920
send_email_reply,
2021
)
21-
from mxtoai.handle_configuration import HANDLE_MAP
2222
from mxtoai.schemas import EmailAttachment, EmailRequest
2323
from mxtoai.tasks import process_email_task
2424
from mxtoai.validators import validate_api_key, validate_attachments, validate_email_handle, validate_email_whitelist
@@ -386,7 +386,7 @@ async def process_email(
386386
await file.seek(0) # Reset file pointer for later use
387387

388388
# Get handle configuration
389-
email_instructions = HANDLE_MAP[handle] # Safe to use direct access now
389+
email_instructions = processing_instructions_resolver(handle) # Safe to use direct access now
390390

391391
# Log initial email details
392392
logger.info("Received new email request:")

mxtoai/dependencies.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from mxtoai.email_handles import DEFAULT_EMAIL_HANDLES
2+
from mxtoai.instruction_resolver import ProcessingInstructionsResolver
3+
4+
processing_instructions_resolver = ProcessingInstructionsResolver(DEFAULT_EMAIL_HANDLES)
Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,8 @@
1-
from typing import Optional
2-
3-
from pydantic import BaseModel
4-
1+
from mxtoai.models import ProcessingInstructions
52
from mxtoai.prompts import output_prompts, template_prompts
63

7-
8-
class EmailHandleInstructions(BaseModel):
9-
handle: str
10-
aliases: list[str]
11-
process_attachments: bool
12-
deep_research_mandatory: bool
13-
rejection_message: Optional[str] = (
14-
"This email handle is not supported. Please visit https://mxtoai.com/docs/email-handles to learn about supported email handles."
15-
)
16-
task_template: Optional[str] = None
17-
requires_language_detection: bool = False # Specifically for translate handle
18-
requires_schedule_extraction: bool = False # Specifically for schedule handle
19-
target_model: Optional[str] = "gpt-4" # Default to gpt-4, can be overridden per handle
20-
output_template: Optional[str] = None # Template for structuring the output
21-
22-
23-
# Define all email handle configurations
24-
EMAIL_HANDLES = [
25-
EmailHandleInstructions(
4+
DEFAULT_EMAIL_HANDLES = [
5+
ProcessingInstructions(
266
handle="summarize",
277
aliases=["summarise", "summary"],
288
process_attachments=True,
@@ -31,7 +11,7 @@ class EmailHandleInstructions(BaseModel):
3111
task_template=template_prompts.SUMMARIZE_TEMPLATE,
3212
output_template=output_prompts.SUMMARIZE_OUTPUT_GUIDELINES,
3313
),
34-
EmailHandleInstructions(
14+
ProcessingInstructions(
3515
handle="research",
3616
aliases=["deep-research"],
3717
process_attachments=True,
@@ -41,7 +21,7 @@ class EmailHandleInstructions(BaseModel):
4121
task_template=template_prompts.RESEARCH_TEMPLATE,
4222
output_template=output_prompts.RESEARCH_OUTPUT_GUIDELINES,
4323
),
44-
EmailHandleInstructions(
24+
ProcessingInstructions(
4525
handle="simplify",
4626
aliases=["eli5", "explain"],
4727
process_attachments=True,
@@ -50,7 +30,7 @@ class EmailHandleInstructions(BaseModel):
5030
task_template=template_prompts.SIMPLIFY_TEMPLATE,
5131
output_template=output_prompts.SIMPLIFY_OUTPUT_GUIDELINES,
5232
),
53-
EmailHandleInstructions(
33+
ProcessingInstructions(
5434
handle="ask",
5535
aliases=["custom", "agent", "assist", "assistant", "hi", "hello", "question"],
5636
process_attachments=True,
@@ -59,7 +39,7 @@ class EmailHandleInstructions(BaseModel):
5939
task_template=template_prompts.ASK_TEMPLATE,
6040
output_template=output_prompts.ASK_OUTPUT_GUIDELINES,
6141
),
62-
EmailHandleInstructions(
42+
ProcessingInstructions(
6343
handle="fact-check",
6444
aliases=["factcheck", "verify"],
6545
process_attachments=True,
@@ -68,7 +48,7 @@ class EmailHandleInstructions(BaseModel):
6848
task_template=template_prompts.FACT_TEMPLATE,
6949
output_template=output_prompts.FACT_CHECK_OUTPUT_GUIDELINES,
7050
),
71-
EmailHandleInstructions(
51+
ProcessingInstructions(
7252
handle="background-research",
7353
aliases=["background-check", "background"],
7454
process_attachments=True,
@@ -77,7 +57,7 @@ class EmailHandleInstructions(BaseModel):
7757
task_template=template_prompts.BACKGROUND_RESEARCH_TEMPLATE,
7858
output_template=output_prompts.BACKGROUND_OUTPUT_GUIDELINES,
7959
),
80-
EmailHandleInstructions(
60+
ProcessingInstructions(
8161
handle="translate",
8262
aliases=["translation"],
8363
process_attachments=True,
@@ -86,7 +66,7 @@ class EmailHandleInstructions(BaseModel):
8666
task_template=template_prompts.TRANSLATE_TEMPLATE,
8767
output_template=output_prompts.TRANSLATION_OUTPUT_GUIDELINES,
8868
),
89-
EmailHandleInstructions(
69+
ProcessingInstructions(
9070
handle="schedule",
9171
aliases=["schedule-action"],
9272
process_attachments=True,
@@ -97,10 +77,3 @@ class EmailHandleInstructions(BaseModel):
9777
output_template=output_prompts.SCHEDULE_OUTPUT_GUIDELINES,
9878
),
9979
]
100-
101-
# Create a mapping of handles (including aliases) to their configurations
102-
HANDLE_MAP = {}
103-
for config in EMAIL_HANDLES:
104-
HANDLE_MAP[config.handle] = config
105-
for alias in config.aliases:
106-
HANDLE_MAP[alias] = config

mxtoai/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class UnspportedHandleException(Exception):
2+
def __init__(self, message):
3+
super().__init__(message)
4+
5+
class HandleAlreadyExistsException(Exception):
6+
def __init__(self, message):
7+
super().__init__(message)

mxtoai/instruction_resolver.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import mxtoai.exceptions as exceptions
2+
from mxtoai._logging import get_logger
3+
4+
from .models import ProcessingInstructions
5+
6+
logger = get_logger(__name__)
7+
8+
class ProcessingInstructionsResolver:
9+
"""
10+
Resolves processing instructions based on email handle names or aliases.
11+
12+
This class is responsible for:
13+
- Registering predefined and custom email handle instructions.
14+
- Resolving a given handle (or its alias) to its corresponding instructions.
15+
- Preventing alias/handle conflicts unless explicitly allowed.
16+
- Supporting runtime extensibility via dynamic handle registration.
17+
18+
Attributes:
19+
handle_map (Dict[str, EmailHandleInstructions]): Maps handles and aliases to their instruction objects.
20+
21+
"""
22+
23+
def __init__(self, default_instructions: list[ProcessingInstructions]):
24+
"""
25+
Initialize the resolver with a list of default handle instructions.
26+
27+
Args:
28+
default_instructions (List[EmailHandleInstructions]): List of predefined handle configurations.
29+
30+
"""
31+
self.handle_map: dict[str, ProcessingInstructions] = {}
32+
self.register_handles(default_instructions)
33+
34+
def register_handles(self, instructions_list: list[ProcessingInstructions], overwrite: bool = False):
35+
"""
36+
Registers a list of handle instructions (including aliases).
37+
38+
Args:
39+
instructions_list (List[EmailHandleInstructions]): Handles and aliases to register.
40+
overwrite (bool): If True, allows existing handles/aliases to be overwritten.
41+
42+
Raises:
43+
ValueError: If a handle or alias is already registered and overwrite is False.
44+
45+
"""
46+
for instructions in instructions_list:
47+
all_names = [instructions.handle, *instructions.aliases]
48+
for name in all_names:
49+
if name in self.handle_map and not overwrite:
50+
msg = f"Handle or alias '{name}' already registered. Use `overwrite=True` to replace."
51+
raise exceptions.HandleAlreadyExistsException(msg)
52+
self.handle_map[name] = instructions
53+
54+
def add_custom_handle(self, custom_instruction: ProcessingInstructions, overwrite: bool = False):
55+
"""
56+
Adds a single custom handle instruction.
57+
58+
Args:
59+
custom_instruction (EmailHandleInstructions): The custom handle config to add.
60+
overwrite (bool): If True, allows overwriting existing handles/aliases.
61+
62+
"""
63+
self.register_handles([custom_instruction], overwrite=overwrite)
64+
65+
def __call__(self, handle: str) -> ProcessingInstructions:
66+
"""
67+
Resolves a handle or alias to its corresponding EmailHandleInstructions.
68+
69+
Args:
70+
handle (str): The handle or alias name.
71+
72+
Returns:
73+
EmailHandleInstructions: The matched instruction object.
74+
75+
Raises:
76+
ValueError: If the handle is not registered.
77+
78+
"""
79+
if handle not in self.handle_map:
80+
logger.debug("This email handle is not supported!")
81+
raise exceptions.UnspportedHandleException(
82+
"This email handle is not supported. Please visit https://mxtoai.com/docs/email-handles to learn about supported email handles."
83+
)
84+
return self.handle_map[handle]
85+
86+
def list_available_handles(self) -> list[str]:
87+
"""
88+
Lists all unique handle names currently registered (excluding aliases).
89+
90+
Returns:
91+
List[str]: A list of unique registered handle names.
92+
93+
"""
94+
return list({instr.handle for instr in self.handle_map.values()})

0 commit comments

Comments
 (0)