Skip to content

Commit 958d0b1

Browse files
authored
comments refactoring and added annotations where needed (#22)
1 parent 282ee48 commit 958d0b1

22 files changed

+625
-95
lines changed

mxtoai/_logging.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,18 @@ def get_logger(source: str) -> Any:
8383
def span(
8484
msg_template: str, name: str | None = None, tags: Sequence[str] | None = None, **msg_template_kwargs: Any
8585
) -> Any:
86-
"""Context manager for creating spans in logging."""
86+
"""
87+
Context manager for creating spans in logging.
88+
89+
Args:
90+
msg_template (str): The message template for the span.
91+
name (str | None): Optional name for the span.
92+
tags (Sequence[str] | None): Optional tags for the span.
93+
**msg_template_kwargs: Additional keyword arguments for the message template.
94+
95+
Yields:
96+
Any: The span context manager or a dummy context manager.
97+
"""
8798
# Check if LOGFIRE_TOKEN environment variable is defined
8899
if os.getenv("LOGFIRE_TOKEN"):
89100
if tags:

mxtoai/agents/email_agent.py

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,12 @@ def __init__(
119119
logger.info("Email agent initialized successfully")
120120

121121
def _init_agent(self):
122-
"""Initialize the ToolCallingAgent with Azure OpenAI."""
123-
# Initialize the model with routing capabilities
124-
self.routed_model = RoutedLiteLLMModel() # Store as instance variable to update handle later
122+
"""
123+
Initialize the ToolCallingAgent with Azure OpenAI.
124+
"""
125+
# Initialize the routed model with the default model group
126+
self.routed_model = RoutedLiteLLMModel()
125127

126-
# Initialize the agent
127128
self.agent = ToolCallingAgent(
128129
model=self.routed_model,
129130
tools=self.available_tools,
@@ -138,7 +139,12 @@ def _init_agent(self):
138139
logger.debug("Agent initialized with routed model configuration")
139140

140141
def _initialize_search_tools(self) -> SearchWithFallbackTool:
141-
"""Initializes and configures the search tools, returning the SearchWithFallbackTool."""
142+
"""
143+
Initializes and configures the search tools, returning the SearchWithFallbackTool.
144+
145+
Returns:
146+
SearchWithFallbackTool: The configured search tool with Bing and DuckDuckGo as primary engines and Google as fallback.
147+
"""
142148
bing_search_tool = WebSearchTool(engine="bing", max_results=5)
143149
logger.debug("Initialized WebSearchTool with Bing engine.")
144150

@@ -167,6 +173,25 @@ def _initialize_search_tools(self) -> SearchWithFallbackTool:
167173
logger.info(f"Initialized SearchWithFallbackTool. Primary engines: {primary_names}, Fallback: {fallback_name}")
168174
return search_tool
169175

176+
def _get_required_actions(self, mode: str) -> List[str]:
177+
"""
178+
Get list of required actions based on mode.
179+
180+
Args:
181+
mode: The mode of operation (e.g., "summary", "reply", "research", "full")
182+
183+
Returns:
184+
List[str]: List of actions to be performed by the agent
185+
"""
186+
actions = []
187+
if mode in ["summary", "full"]:
188+
actions.append("Generate summary")
189+
if mode in ["reply", "full"]:
190+
actions.append("Generate reply")
191+
if mode in ["research", "full"]:
192+
actions.append("Conduct research")
193+
return actions
194+
170195
def _initialize_google_search_tool(self) -> Optional[GoogleSearchTool]:
171196
"""
172197
Initialize Google search tool with either SerpAPI or Serper provider.
@@ -195,7 +220,15 @@ def _initialize_google_search_tool(self) -> Optional[GoogleSearchTool]:
195220
return None
196221

197222
def _initialize_deep_research_tool(self, enable_deep_research: bool) -> Optional[DeepResearchTool]:
198-
"""Initializes the DeepResearchTool if API key is available."""
223+
"""
224+
Initializes the DeepResearchTool if API key is available.
225+
226+
Args:
227+
enable_deep_research: Flag to enable deep research functionality
228+
229+
Returns:
230+
Optional[DeepResearchTool]: Initialized DeepResearchTool instance or None if API key is not found
231+
"""
199232
research_tool: Optional[DeepResearchTool] = None
200233
if os.getenv("JINA_API_KEY"):
201234
research_tool = DeepResearchTool()
@@ -210,7 +243,18 @@ def _initialize_deep_research_tool(self, enable_deep_research: bool) -> Optional
210243
return research_tool
211244

212245
def _create_task(self, email_request: EmailRequest, email_instructions: ProcessingInstructions) -> str:
213-
"""Create a task description for the agent based on email handle instructions."""
246+
"""
247+
Create a task description for the agent based on email handle instructions.
248+
249+
Args:
250+
email_request: EmailRequest instance containing email data
251+
email_instructions: EmailHandleInstructions object containing processing configuration
252+
253+
Returns:
254+
str: The task description for the agent
255+
"""
256+
257+
# process attachments if specified
214258
attachments = self._format_attachments(email_request.attachments) \
215259
if email_instructions.process_attachments and email_request.attachments else []
216260

@@ -224,14 +268,31 @@ def _create_task(self, email_request: EmailRequest, email_instructions: Processi
224268
)
225269

226270
def _format_attachments(self, attachments: List[EmailAttachment]) -> List[str]:
227-
"""Format attachment details for inclusion in the task."""
271+
"""
272+
Format attachment details for inclusion in the task.
273+
274+
Args:
275+
attachments: List of EmailAttachment objects
276+
277+
Returns:
278+
List[str]: Formatted attachment details
279+
"""
228280
return [
229281
f"- {att.filename} (Type: {att.contentType}, Size: {att.size} bytes)\n"
230282
f' EXACT FILE PATH: "{att.path}"'
231283
for att in attachments
232284
]
233285
def _create_email_context(self, email_request: EmailRequest, attachment_details=None) -> str:
234-
"""Generate context information from the email request."""
286+
"""
287+
Generate context information from the email request.
288+
289+
Args:
290+
email_request: EmailRequest instance containing email data
291+
attachment_details: List of formatted attachment details
292+
293+
Returns:
294+
str: The context information for the agent
295+
"""
235296
recipients = ", ".join(email_request.recipients) if email_request.recipients else "N/A"
236297
attachments_info = f"Available Attachments:\n{chr(10).join(attachment_details)}" if attachment_details else "No attachments provided."
237298

@@ -248,7 +309,15 @@ def _create_email_context(self, email_request: EmailRequest, attachment_details=
248309
"""
249310

250311
def _create_attachment_task(self, attachment_details: List[str]) -> str:
251-
"""Return instructions for processing attachments, if any."""
312+
"""
313+
Return instructions for processing attachments, if any.
314+
315+
Args:
316+
attachment_details: List of formatted attachment details
317+
318+
Returns:
319+
str: Instructions for processing attachments
320+
"""
252321
return f"Process these attachments:\n{chr(10).join(attachment_details)}" if attachment_details else ""
253322

254323
def _create_task_template(
@@ -260,7 +329,22 @@ def _create_task_template(
260329
deep_research_mandatory: bool = False,
261330
output_template: str = "",
262331
) -> str:
263-
"""Combine all task components into the final task description."""
332+
"""
333+
Combine all task components into the final task description.
334+
335+
Args:
336+
handle: The email handle being processed.
337+
email_context: The context information extracted from the email.
338+
handle_specific_template: Any specific template for the handle.
339+
attachment_task: Instructions for processing attachments.
340+
deep_research_mandatory: Flag indicating if deep research is mandatory.
341+
output_template: The output template to use.
342+
343+
Returns:
344+
str: The complete task description for the agent.
345+
"""
346+
347+
# Merge the task components into a single string by listing the sections
264348
sections = [
265349
f"Process this email according to the '{handle}' instruction type.\n",
266350
email_context,
@@ -330,7 +414,6 @@ def _process_agent_result(self, final_answer_obj: Any, agent_steps: List) -> Dic
330414
)
331415
tool_name = None # Reset tool_name if extraction failed
332416

333-
# Revised Output Extraction
334417
action_out = getattr(step, "action_output", None)
335418
obs_out = getattr(step, "observations", None)
336419

@@ -422,7 +505,6 @@ def _process_agent_result(self, final_answer_obj: Any, agent_steps: List) -> Dic
422505
logger.debug(f"[Memory Step {i+1}] Matched tool: deep_research")
423506
try:
424507
if isinstance(tool_output, dict):
425-
# Store the primary findings content
426508
research_findings_content = tool_output.get("findings", "")
427509
# Store metadata separately
428510
research_metadata = {
@@ -533,7 +615,6 @@ def _process_agent_result(self, final_answer_obj: Any, agent_steps: List) -> Dic
533615

534616
# --- Format the selected content ---
535617
if email_body_content:
536-
# Remove signature remnants before formatting
537618
signature_markers = [
538619
"Best regards,\nMXtoAI Assistant",
539620
"Best regards,",
@@ -552,7 +633,6 @@ def _process_agent_result(self, final_answer_obj: Any, agent_steps: List) -> Dic
552633
).strip()
553634
logger.debug("Removed potential signature remnants from email body content.")
554635

555-
# Format using ReportFormatter
556636
result["email_content"]["text"] = self.report_formatter.format_report(
557637
email_body_content, format_type="text", include_signature=True
558638
)
@@ -637,13 +717,10 @@ def process_email(
637717
638718
"""
639719
try:
640-
# Update the model's current handle
720+
# create task
641721
self.routed_model.current_handle = email_instructions
642-
643-
# Create task with specific instructions
644722
task = self._create_task(email_request, email_instructions)
645723

646-
# Run the agent
647724
try:
648725
logger.info("Starting agent execution...")
649726
final_answer_obj = self.agent.run(task)
@@ -670,7 +747,7 @@ def process_email(
670747
if not processed_result.get("email_content") or not processed_result["email_content"].get("text"):
671748
msg = "No reply text was generated by _process_agent_result"
672749
logger.error(msg)
673-
# Populate errors within the existing structure if possible
750+
674751
if "metadata" not in processed_result:
675752
processed_result["metadata"] = {}
676753
if "errors" not in processed_result["metadata"]:
@@ -680,7 +757,7 @@ def process_email(
680757
processed_result["metadata"]["email_sent"] = {}
681758
processed_result["metadata"]["email_sent"]["status"] = "error"
682759
processed_result["metadata"]["email_sent"]["error"] = msg
683-
# Return the partially processed result with error flags
760+
684761
return processed_result
685762

686763
logger.info(f"Email processed successfully with handle: {email_instructions.handle}")

mxtoai/api.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@
4040

4141

4242
# Function to cleanup attachment files and directory
43-
def cleanup_attachments(directory_path):
44-
"""Delete attachment directory and all its contents"""
43+
def cleanup_attachments(directory_path: str) -> bool:
44+
"""
45+
Delete attachment directory and all its contents
46+
47+
Args:
48+
directory_path (str): Path to the directory to be deleted
49+
50+
Returns:
51+
bool: True if deletion was successful, False otherwise
52+
"""
4553
try:
4654
if os.path.exists(directory_path):
4755
shutil.rmtree(directory_path)
@@ -55,7 +63,17 @@ def cleanup_attachments(directory_path):
5563
def create_success_response(
5664
summary: str, email_response: dict[str, Any], attachment_info: list[dict[str, Any]]
5765
) -> Response:
58-
"""Create a success response with summary and email details"""
66+
"""
67+
Create a success response with summary and email details
68+
69+
Args:
70+
summary (str): Summary of the email processing
71+
email_response (dict): Response from the email sending service
72+
attachment_info (list): List of processed attachments
73+
74+
Returns:
75+
Response: FastAPI Response object with JSON content
76+
"""
5977
return Response(
6078
content=json.dumps(
6179
{
@@ -72,7 +90,17 @@ def create_success_response(
7290

7391

7492
def create_error_response(summary: str, attachment_info: list[dict[str, Any]], error: str) -> Response:
75-
"""Create an error response with summary and error details"""
93+
"""
94+
Create an error response with summary and error details
95+
96+
Args:
97+
summary (str): Summary of the email processing
98+
attachment_info (list): List of processed attachments
99+
error (str): Error message
100+
101+
Returns:
102+
Response: FastAPI Response object with JSON content
103+
"""
76104
return Response(
77105
content=json.dumps(
78106
{
@@ -92,7 +120,17 @@ def create_error_response(summary: str, attachment_info: list[dict[str, Any]], e
92120
async def handle_file_attachments(
93121
attachments: list[EmailAttachment], email_id: str, email_data: EmailRequest
94122
) -> tuple[str, list[dict[str, Any]]]:
95-
"""Process uploaded files and save them as attachments"""
123+
"""
124+
Process uploaded files and save them as attachments
125+
126+
Args:
127+
attachments (list[EmailAttachment]): List of EmailAttachment objects
128+
email_id (str): Unique identifier for the email
129+
email_data (EmailRequest): EmailRequest object containing email details
130+
131+
Returns:
132+
tuple[str, list[dict[str, Any]]]: Tuple containing the directory path and list of processed attachments
133+
"""
96134
email_attachments_dir = ""
97135
attachment_info = []
98136

@@ -202,7 +240,16 @@ async def handle_file_attachments(
202240

203241
# Helper function to send email reply using SES
204242
async def send_agent_email_reply(email_data: EmailRequest, processing_result: dict[str, Any]) -> dict[str, Any]:
205-
"""Send email reply using SES and return the response details"""
243+
"""
244+
Send email reply using SES and return the response details
245+
246+
Args:
247+
email_data (EmailRequest): EmailRequest object containing email details
248+
processing_result (dict): Result of the email processing
249+
250+
Returns:
251+
dict: Response details including status and message ID
252+
"""
206253
if not processing_result or "email_content" not in processing_result:
207254
logger.error("Invalid processing result format")
208255
return {"status": "error", "error": "Invalid processing result format", "timestamp": datetime.now().isoformat()}
@@ -283,7 +330,15 @@ async def send_agent_email_reply(email_data: EmailRequest, processing_result: di
283330

284331
# Helper function to create sanitized response
285332
def sanitize_processing_result(processing_result: dict[str, Any]) -> dict[str, Any]:
286-
"""Create a clean response suitable for API return and database storage"""
333+
"""
334+
Create a clean response suitable for API return and database storage
335+
336+
Args:
337+
processing_result (dict): Result of the email processing
338+
339+
Returns:
340+
dict: Sanitized response with metadata, research, and attachment info
341+
"""
287342
if not isinstance(processing_result, dict):
288343
return {"error": "Invalid processing result format", "timestamp": datetime.now().isoformat()}
289344

@@ -329,7 +384,25 @@ async def process_email(
329384
files: Annotated[list[UploadFile] | None, File()] = None,
330385
api_key: str = Depends(api_auth_scheme),
331386
):
332-
"""Process an incoming email with attachments, analyze content, and send reply"""
387+
"""
388+
Process an incoming email with attachments, analyze content, and send reply
389+
390+
Args:
391+
from_email (str): Sender's email address
392+
to (str): Recipient's email address
393+
subject (str): Subject of the email
394+
textContent (str): Plain text content of the email
395+
htmlContent (str): HTML content of the email
396+
messageId (str): Unique identifier for the email message
397+
date (str): Date when the email was sent
398+
emailId (str): Unique identifier for the email in the system
399+
rawHeaders (str): Raw headers of the email in JSON format
400+
files (list[UploadFile] | None): List of uploaded files as attachments
401+
api_key (str): API key for authentication
402+
403+
Returns:
404+
Response: FastAPI Response object with JSON content
405+
"""
333406
# Validate API key
334407
if response := await validate_api_key(api_key):
335408
return response

mxtoai/dependencies.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from mxtoai.email_handles import DEFAULT_EMAIL_HANDLES
22
from mxtoai.instruction_resolver import ProcessingInstructionsResolver
33

4+
# global resolver for processing instructions
45
processing_instructions_resolver = ProcessingInstructionsResolver(DEFAULT_EMAIL_HANDLES)

0 commit comments

Comments
 (0)