gohumanloop 0.0.5__py3-none-any.whl → 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,27 +2,28 @@ import os
2
2
  import re
3
3
  import asyncio
4
4
  import smtplib
5
- from imapclient import IMAPClient
5
+ from imapclient import IMAPClient # type: ignore
6
6
  import email.mime.multipart
7
7
  import email.mime.text
8
8
  from email.header import decode_header
9
9
  from email import message_from_bytes
10
10
  from email.message import Message
11
11
  import logging
12
- from typing import Dict, Any, Optional, List, Tuple
12
+ from typing import Dict, Any, Optional, Tuple
13
13
  from datetime import datetime
14
14
  from pydantic import SecretStr
15
- from concurrent.futures import ThreadPoolExecutor
15
+ from concurrent.futures import ThreadPoolExecutor, Future
16
16
 
17
- from gohumanloop.core.interface import ( HumanLoopResult, HumanLoopStatus, HumanLoopType
18
- )
17
+ from gohumanloop.core.interface import HumanLoopResult, HumanLoopStatus, HumanLoopType
19
18
  from gohumanloop.providers.base import BaseProvider
20
19
  from gohumanloop.utils import get_secret_from_env
20
+
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
+
23
24
  class EmailProvider(BaseProvider):
24
25
  """Email-based human-in-the-loop provider implementation"""
25
-
26
+
26
27
  def __init__(
27
28
  self,
28
29
  name: str,
@@ -31,14 +32,14 @@ class EmailProvider(BaseProvider):
31
32
  imap_server: str,
32
33
  imap_port: int,
33
34
  username: Optional[str] = None,
34
- password: Optional[SecretStr]=None,
35
+ password: Optional[SecretStr] = None,
35
36
  sender_email: Optional[str] = None,
36
37
  check_interval: int = 60,
37
38
  language: str = "zh",
38
- config: Optional[Dict[str, Any]] = None
39
+ config: Optional[Dict[str, Any]] = None,
39
40
  ):
40
41
  """Initialize Email Provider
41
-
42
+
42
43
  Args:
43
44
  name: Provider name
44
45
  smtp_server: SMTP server address
@@ -59,32 +60,35 @@ class EmailProvider(BaseProvider):
59
60
  self.imap_port = imap_port
60
61
  self.language = language # 保存语言设置
61
62
 
62
- # 优先从参数获取凭证,如果未提供则从环境变量获取
63
+ # 优先从参数获取凭证,如果未提供则从环境变量获取
63
64
  self.username = username or os.environ.get("GOHUMANLOOP_EMAIL_USERNAME")
64
65
  if not self.username:
65
- raise ValueError(f"Email username not provided, please set it via parameter or environment variable GOHUMANLOOP_EMAIL_USERNAME")
66
-
66
+ raise ValueError(
67
+ "Email username not provided, please set it via parameter or environment variable GOHUMANLOOP_EMAIL_USERNAME"
68
+ )
69
+
67
70
  self.password = password or get_secret_from_env("GOHUMANLOOP_EMAIL_PASSWORD")
68
71
  if not self.password:
69
- raise ValueError(f"Email password not provided, please set it via parameter or environment variable GOHUMANLOOP_EMAIL_PASSWORD")
72
+ raise ValueError(
73
+ "Email password not provided, please set it via parameter or environment variable GOHUMANLOOP_EMAIL_PASSWORD"
74
+ )
70
75
 
71
76
  self.sender_email = sender_email or self.username
72
77
  self.check_interval = check_interval
73
-
78
+
74
79
  # 存储邮件主题与请求ID的映射关系
75
- self._subject_to_request = {}
80
+ self._subject_to_request: Dict[str, Tuple[str, str]] = {}
76
81
  # 存储正在运行的邮件检查任务
77
- self._mail_check_tasks = {}
82
+ self._mail_check_tasks: Dict[Tuple[str, str], Future] = {}
78
83
  # 存储邮件会话ID与对话ID的映射
79
- self._thread_to_conversation = {}
84
+ self._thread_to_conversation: Dict[str, str] = {}
80
85
 
81
86
  self._init_language_templates()
82
87
 
83
88
  # Create thread pool for background service execution
84
89
  self._executor = ThreadPoolExecutor(max_workers=10)
85
90
 
86
-
87
- def __del__(self):
91
+ def __del__(self) -> None:
88
92
  """析构函数,确保线程池被正确关闭"""
89
93
  self._executor.shutdown(wait=False)
90
94
 
@@ -93,7 +97,7 @@ class EmailProvider(BaseProvider):
93
97
  future.cancel()
94
98
  self._mail_check_tasks.clear()
95
99
 
96
- def _init_language_templates(self):
100
+ def _init_language_templates(self) -> None:
97
101
  """初始化不同语言的模板和关键词"""
98
102
  if self.language == "zh":
99
103
  # 中文关键词和模板
@@ -106,7 +110,7 @@ class EmailProvider(BaseProvider):
106
110
  "decision_prefix": "决定:",
107
111
  "reason_prefix": "理由:",
108
112
  "information_prefix": "信息:",
109
- "conversation_instruction": "请直接回复您的内容。如需结束对话,请在回复中包含\"[结束对话]\"。",
113
+ "conversation_instruction": '请直接回复您的内容。如需结束对话,请在回复中包含"[结束对话]"。',
110
114
  "conversation_end_mark": "[结束对话]",
111
115
  "input_placeholder": "[请在此处输入您的回复内容]",
112
116
  "approve_template_title": "批准模板:",
@@ -115,10 +119,10 @@ class EmailProvider(BaseProvider):
115
119
  "template_instruction": "请选择一个模板,复制到您的回复中,替换方括号内的内容后发送。请保留标记行,这将帮助系统准确识别您的回复。",
116
120
  "info_template_instruction": "请复制上面的模板到您的回复中,替换方括号内的内容后发送。请保留标记行,这将帮助系统准确识别您的回复。",
117
121
  "footer": "由 GoHumanLoop 提供支持 - Perfecting AI workflows with human intelligence",
118
- "continue_conversation_template":"继续对话模板:",
122
+ "continue_conversation_template": "继续对话模板:",
119
123
  "end_conversation_template": "结束对话模板:",
120
124
  "invalid_decision": "未提供有效的审批决定",
121
- "powered_by": "由 GoHumanLoop 提供支持"
125
+ "powered_by": "由 GoHumanLoop 提供支持",
122
126
  }
123
127
  else:
124
128
  # 英文关键词和模板
@@ -131,7 +135,7 @@ class EmailProvider(BaseProvider):
131
135
  "decision_prefix": "Decision: ",
132
136
  "reason_prefix": "Reason: ",
133
137
  "information_prefix": "Information: ",
134
- "conversation_instruction": "Please reply directly with your content. If you want to end the conversation, include \"[END CONVERSATION]\" in your reply.",
138
+ "conversation_instruction": 'Please reply directly with your content. If you want to end the conversation, include "[END CONVERSATION]" in your reply.',
135
139
  "conversation_end_mark": "[END CONVERSATION]",
136
140
  "input_placeholder": "[Please enter your reply here]",
137
141
  "approve_template_title": "Approval Template:",
@@ -143,63 +147,63 @@ class EmailProvider(BaseProvider):
143
147
  "continue_conversation_template": "Continue Conversation Template:",
144
148
  "end_conversation_template": "End Conversation Template:",
145
149
  "invalid_decision": "No valid approval decision provided",
146
- "powered_by": "Powered by GoHumanLoop"
150
+ "powered_by": "Powered by GoHumanLoop",
147
151
  }
148
-
152
+
149
153
  async def _async_send_email(
150
- self,
151
- to_email: str,
152
- subject: str,
154
+ self,
155
+ to_email: str,
156
+ subject: str,
153
157
  body: str,
154
158
  html_body: Optional[str] = None,
155
- reply_to: Optional[str] = None
159
+ reply_to: Optional[str] = None,
156
160
  ) -> bool:
157
161
  """Send email
158
-
162
+
159
163
  Args:
160
164
  to_email: Recipient email address
161
165
  subject: Email subject
162
166
  body: Email body (plain text)
163
167
  html_body: Email body (HTML format, optional)
164
168
  reply_to: Reply email ID (optional)
165
-
169
+
166
170
  Returns:
167
171
  bool: Whether sending was successful
168
172
  """
169
173
  try:
170
- msg = email.mime.multipart.MIMEMultipart('alternative')
171
- msg['From'] = self.sender_email
172
- msg['To'] = to_email
173
- msg['Subject'] = subject
174
-
174
+ msg = email.mime.multipart.MIMEMultipart("alternative")
175
+ msg["From"] = self.sender_email
176
+ msg["To"] = to_email
177
+ msg["Subject"] = subject
178
+
175
179
  # 添加纯文本内容
176
- msg.attach(email.mime.text.MIMEText(body, 'plain'))
177
-
180
+ msg.attach(email.mime.text.MIMEText(body, "plain"))
181
+
178
182
  # 如果提供了HTML内容,也添加HTML版本
179
183
  if html_body:
180
- msg.attach(email.mime.text.MIMEText(html_body, 'html'))
181
-
184
+ msg.attach(email.mime.text.MIMEText(html_body, "html"))
185
+
182
186
  # 如果是回复邮件,添加相关邮件头
183
187
  if reply_to:
184
- msg['In-Reply-To'] = reply_to
185
- msg['References'] = reply_to
186
-
188
+ msg["In-Reply-To"] = reply_to
189
+ msg["References"] = reply_to
190
+
187
191
  # 使用异步方式发送邮件
188
192
  loop = asyncio.get_event_loop()
189
- await loop.run_in_executor(
190
- None,
191
- self._send_email_sync,
192
- msg
193
- )
193
+ await loop.run_in_executor(None, self._send_email_sync, msg)
194
194
  return True
195
195
  except Exception as e:
196
196
  logger.error(f"Failed to send email: {str(e)}", exc_info=True)
197
197
  return False
198
-
199
- def _send_email_sync(self, msg):
198
+
199
+ def _send_email_sync(self, msg: email.mime.multipart.MIMEMultipart) -> None:
200
200
  """Synchronously send email (runs in executor)"""
201
201
  try:
202
202
  with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
203
+ if self.username is None:
204
+ raise ValueError("Username is not set")
205
+ if self.password is None:
206
+ raise ValueError("Password is not set")
203
207
  server.login(self.username, self.password.get_secret_value())
204
208
  server.send_message(msg)
205
209
  except smtplib.SMTPException as e:
@@ -208,47 +212,49 @@ class EmailProvider(BaseProvider):
208
212
  except Exception as e:
209
213
  logger.exception(f"Unknown error occurred while sending email: {str(e)}")
210
214
  raise
211
-
212
- async def _async_check_emails(self, conversation_id: str, request_id: str, recipient_email: str, subject: str):
215
+
216
+ async def _async_check_emails(
217
+ self, conversation_id: str, request_id: str, recipient_email: str, subject: str
218
+ ) -> None:
213
219
  """Check email replies
214
-
220
+
215
221
  Args:
216
222
  conversation_id: Conversation ID
217
- request_id: Request ID
223
+ request_id: Request ID
218
224
  recipient_email: Recipient email address
219
225
  subject: Email subject
220
226
  """
221
227
  request_key = (conversation_id, request_id)
222
-
223
- while request_key in self._requests and self._requests[request_key]["status"] in [
224
- HumanLoopStatus.PENDING
225
- ]:
228
+
229
+ while request_key in self._requests and self._requests[request_key][
230
+ "status"
231
+ ] in [HumanLoopStatus.PENDING]:
226
232
  try:
227
233
  # 使用异步方式检查邮件
228
234
  loop = asyncio.get_event_loop()
229
235
  email_msg = await loop.run_in_executor(
230
- None,
231
- self._fetch_emails_sync,
232
- subject,
233
- recipient_email
236
+ None, self._fetch_emails_sync, subject, recipient_email
237
+ )
238
+
239
+ await self._process_email_response(
240
+ conversation_id, request_id, email_msg
234
241
  )
235
-
236
-
237
- await self._process_email_response(conversation_id, request_id, email_msg)
238
-
242
+
239
243
  # 等待一段时间后再次检查
240
244
  await asyncio.sleep(self.check_interval)
241
245
  except Exception as e:
242
246
  logger.error(f"Failed to check emails: {str(e)}", exc_info=True)
243
- self._update_request_status_error(conversation_id, request_id, f"Failed to check emails: {str(e)}")
247
+ self._update_request_status_error(
248
+ conversation_id, request_id, f"Failed to check emails: {str(e)}"
249
+ )
244
250
  break
245
-
251
+
246
252
  def _decode_email_header(self, header_value: str) -> str:
247
253
  """Parse email header information and handle potential encoding issues
248
-
254
+
249
255
  Args:
250
256
  header_value: Raw email header value
251
-
257
+
252
258
  Returns:
253
259
  str: Decoded email header value
254
260
  """
@@ -262,70 +268,83 @@ class EmailProvider(BaseProvider):
262
268
  else:
263
269
  result += str(part)
264
270
  return result
265
-
266
- def _fetch_emails_sync(self, subject: str, sender_email: Optional[str] = None) -> Any:
271
+
272
+ def _fetch_emails_sync(
273
+ self, subject: str, sender_email: Optional[str] = None
274
+ ) -> Any:
267
275
  """Synchronously fetch emails (runs in executor)
268
-
276
+
269
277
  Args:
270
278
  subject: Email subject
271
279
  sender_email: Sender email address (optional filter)
272
-
280
+
273
281
  Returns:
274
282
  List[email.message.Message]: List of matching emails
275
283
  """
276
284
 
277
285
  # 连接到IMAP服务器
278
- with IMAPClient(host=self.imap_server, port=self.imap_port ,ssl=True) as client:
286
+ with IMAPClient(host=self.imap_server, port=self.imap_port, ssl=True) as client:
287
+ if self.username is None:
288
+ raise ValueError("Username is not set")
289
+ if self.password is None:
290
+ raise ValueError("Password is not set")
279
291
  client.login(self.username, self.password.get_secret_value())
280
-
292
+
281
293
  # 发送 ID 命令,解决某些邮箱服务器的安全限制(如网易邮箱)
282
294
  try:
283
- client.id_({
284
- "name": "GoHumanLoop",
285
- "version": "1.0.0",
286
- "vendor": "GoHumanLoop Client",
287
- "contact": "baird0917@163.com"
288
- })
295
+ client.id_(
296
+ {
297
+ "name": "GoHumanLoop",
298
+ "version": "1.0.0",
299
+ "vendor": "GoHumanLoop Client",
300
+ "contact": "baird0917@163.com",
301
+ }
302
+ )
289
303
  logger.debug("IMAP ID command sent")
290
304
  except Exception as e:
291
305
  logger.warning(f"Failed to send IMAP ID command: {str(e)}")
292
-
306
+
293
307
  # 选择收件箱
294
308
  client.select_folder("INBOX")
295
-
309
+
296
310
  # 执行搜索
297
311
  messages = client.search("UNSEEN")
298
-
312
+
299
313
  if not messages:
300
- logger.warning(f"No unread emails found")
314
+ logger.warning("No unread emails found")
301
315
  return None
302
-
316
+
303
317
  # 获取邮件内容
304
318
  for uid, message_data in client.fetch(messages, "RFC822").items():
305
319
  email_message = message_from_bytes(message_data[b"RFC822"])
306
-
320
+
307
321
  # 使用通用方法解析发件人
308
322
  from_header = self._decode_email_header(email_message.get("From", ""))
309
-
323
+
310
324
  # 使用通用方法解析主题
311
- email_subject = self._decode_email_header(email_message.get("Subject", ""))
312
-
325
+ email_subject = self._decode_email_header(
326
+ email_message.get("Subject", "")
327
+ )
328
+
313
329
  # 检查是否匹配发件人和主题条件
314
- if (sender_email and sender_email not in from_header) or \
315
- (subject and subject not in email_subject):
330
+ if (sender_email and sender_email not in from_header) or (
331
+ subject and subject not in email_subject
332
+ ):
316
333
  continue
317
-
334
+
318
335
  # 将邮件标记为已读
319
- client.set_flags(uid, [b'\\Seen'])
336
+ client.set_flags(uid, [b"\\Seen"])
320
337
  return email_message
321
338
  return None
322
-
323
- async def _process_email_response(self, conversation_id: str, request_id: str, email_msg: Message):
339
+
340
+ async def _process_email_response(
341
+ self, conversation_id: str, request_id: str, email_msg: Message
342
+ ) -> None:
324
343
  """Process email response
325
-
344
+
326
345
  Args:
327
346
  conversation_id: Conversation ID
328
- request_id: Request ID
347
+ request_id: Request ID
329
348
  email_msg: Email message object
330
349
  """
331
350
  if email_msg is None:
@@ -334,42 +353,44 @@ class EmailProvider(BaseProvider):
334
353
  request_key = (conversation_id, request_id)
335
354
  if request_key not in self._requests:
336
355
  return
337
-
356
+
338
357
  # 提取邮件内容
339
358
  body = ""
340
359
  html_body = ""
341
-
360
+
342
361
  if email_msg.is_multipart():
343
362
  for part in email_msg.walk():
344
363
  content_type = part.get_content_type()
345
- charse_type = part.get_content_charset()
364
+ charse_type = part.get_content_charset() or "utf-8"
346
365
  content_disposition = str(part.get("Content-Disposition"))
347
-
366
+
348
367
  # 跳过附件
349
368
  if "attachment" in content_disposition:
350
369
  continue
351
-
370
+
352
371
  # 获取邮件内容
353
372
  payload = part.get_payload(decode=True)
354
373
  if payload is None:
355
374
  continue
356
-
357
- if content_type == "text/plain":
358
- body = payload.decode(encoding=charse_type)
359
- elif content_type == "text/html":
360
- html_body = payload.decode(encoding=charse_type)
375
+
376
+ if isinstance(payload, bytes):
377
+ if content_type == "text/plain":
378
+ # 如果字符集未指定,使用默认的utf-8
379
+ body = payload.decode(encoding=charse_type)
380
+ elif content_type == "text/html":
381
+ html_body = payload.decode(encoding=charse_type)
361
382
  else:
362
383
  # 非多部分邮件
363
- charse_type = email_msg.get_content_charset()
384
+ charse_type = email_msg.get_content_charset() or "utf-8"
364
385
  payload = email_msg.get_payload(decode=True)
365
- if payload:
386
+ if isinstance(payload, bytes):
366
387
  body = payload.decode(encoding=charse_type)
367
-
388
+
368
389
  # 获取请求信息
369
390
  request_info = self._requests[request_key]
370
391
  loop_type = request_info.get("loop_type", HumanLoopType.CONVERSATION)
371
392
  responded_by = self._decode_email_header(email_msg.get("From", ""))
372
-
393
+
373
394
  # 解析响应内容
374
395
  parsed_response = {
375
396
  "text": body,
@@ -377,105 +398,112 @@ class EmailProvider(BaseProvider):
377
398
  "subject": self._decode_email_header(email_msg.get("Subject", "")),
378
399
  "from": responded_by,
379
400
  "date": self._decode_email_header(email_msg.get("Date", "")),
380
- "message_id": email_msg.get("Message-ID", "")
401
+ "message_id": email_msg.get("Message-ID", ""),
381
402
  }
382
-
403
+
383
404
  # 提取用户的实际回复内容
384
405
  user_content = self._extract_user_reply_content(body)
385
406
  logger.debug(f"user_content:\n {user_content}")
386
407
  # 根据不同的循环类型解析回复
387
408
  if loop_type == HumanLoopType.APPROVAL:
388
409
  # 解析审批决定和理由
389
- decision = None
390
- reason = None
391
-
410
+ decision = ""
411
+ reason = ""
412
+
392
413
  # 使用语言相关的关键词
393
414
  approve_keywords = self.approve_keywords
394
415
  reject_keywords = self.reject_keywords
395
416
  decision_prefix = self.templates["decision_prefix"]
396
-
417
+
397
418
  # 遍历每行内容寻找决定信息
398
- for line in map(str.strip, user_content.split('\n')):
399
- if not line.startswith(decision_prefix):
400
- continue
401
-
402
- decision_text = line[len(decision_prefix):].strip().lower()
403
-
404
- # 判断决定类型
405
- if any(keyword in decision_text for keyword in approve_keywords):
406
- decision = "approved"
407
- elif any(keyword in decision_text for keyword in reject_keywords):
408
- decision = "rejected"
409
- else:
410
- decision = "rejected"
411
- reason = self.templates["invalid_decision"]
419
+ if user_content:
420
+ for line in map(str.strip, user_content.split("\n")):
421
+ if not line.startswith(decision_prefix):
422
+ continue
423
+
424
+ decision_text = line[len(decision_prefix) :].strip().lower()
425
+
426
+ # 判断决定类型
427
+ if any(keyword in decision_text for keyword in approve_keywords):
428
+ decision = "approved"
429
+ elif any(keyword in decision_text for keyword in reject_keywords):
430
+ decision = "rejected"
431
+ else:
432
+ decision = "rejected"
433
+ reason = self.templates["invalid_decision"]
434
+ break
435
+
436
+ # 提取理由
437
+ reason_prefix = self.templates["reason_prefix"]
438
+ if reason_prefix in line:
439
+ reason = line[
440
+ line.find(reason_prefix) + len(reason_prefix) :
441
+ ].strip()
412
442
  break
413
-
414
- # 提取理由
415
- reason_prefix = self.templates["reason_prefix"]
416
- if reason_prefix in line:
417
- reason = line[line.find(reason_prefix) + len(reason_prefix):].strip()
418
- break
443
+
419
444
  parsed_response["decision"] = decision
420
445
  parsed_response["reason"] = reason
421
-
422
-
446
+
423
447
  # 设置状态
424
448
  if decision == "approved":
425
449
  status = HumanLoopStatus.APPROVED
426
450
  elif decision == "rejected":
427
451
  status = HumanLoopStatus.REJECTED
428
-
429
-
452
+
430
453
  elif loop_type == HumanLoopType.INFORMATION:
431
454
  # 解析提供的信息和备注
432
- information = None
455
+ information = ""
433
456
  information_prefix = self.templates["information_prefix"]
434
-
435
- for line in user_content.split('\n'):
436
- line = line.strip()
437
- if line.startswith(information_prefix):
438
- information = line[len(information_prefix):].strip()
439
- break
440
-
457
+
458
+ if user_content:
459
+ for line in user_content.split("\n"):
460
+ line = line.strip()
461
+ if line.startswith(information_prefix):
462
+ information = line[len(information_prefix) :].strip()
463
+ break
464
+
441
465
  parsed_response["information"] = information
442
466
  status = HumanLoopStatus.COMPLETED
443
-
467
+
444
468
  elif loop_type == HumanLoopType.CONVERSATION:
445
469
  conversation_end_mark = self.templates["conversation_end_mark"]
446
-
470
+
447
471
  # 检查用户的实际回复内容中是否包含结束对话的标记
448
472
  if user_content and conversation_end_mark in user_content:
449
473
  parsed_response["user_content"] = user_content
450
474
  status = HumanLoopStatus.COMPLETED
451
475
  else:
452
- parsed_response["user_content"] = user_content
476
+ parsed_response["user_content"] = user_content or ""
453
477
  status = HumanLoopStatus.INPROGRESS
454
478
  else:
455
479
  status = HumanLoopStatus.COMPLETED
456
-
480
+
457
481
  # 更新请求状态
458
- self._requests[request_key].update({
459
- "status": status,
460
- "response": parsed_response,
461
- "responded_by": responded_by,
462
- "responded_at": datetime.now().isoformat()
463
- })
464
-
482
+ self._requests[request_key].update(
483
+ {
484
+ "status": status,
485
+ "response": parsed_response,
486
+ "responded_by": responded_by,
487
+ "responded_at": datetime.now().isoformat(),
488
+ }
489
+ )
490
+
465
491
  # 取消超时任务
466
492
  if request_key in self._timeout_tasks:
467
493
  self._timeout_tasks[request_key].cancel()
468
494
  del self._timeout_tasks[request_key]
469
-
470
- def _format_email_body(self, body: str, loop_type: HumanLoopType, subject: str) -> Tuple[str, str]:
495
+
496
+ def _format_email_body(
497
+ self, body: str, loop_type: HumanLoopType, subject: str
498
+ ) -> Tuple[str, str]:
471
499
  """Format email body
472
-
500
+
473
501
  Args:
474
502
  body: Email body content
475
503
  loop_type: Loop type
476
504
  conversation_id: Conversation ID (optional)
477
505
  request_id: Request ID (optional)
478
-
506
+
479
507
  Returns:
480
508
  Tuple[str, str]: (Plain text body, HTML body)
481
509
  """
@@ -485,7 +513,7 @@ class EmailProvider(BaseProvider):
485
513
 
486
514
  # 构建纯文本正文
487
515
  text_body = body
488
-
516
+
489
517
  # 根据不同的循环类型添加回复指导
490
518
  if loop_type == HumanLoopType.APPROVAL:
491
519
  text_body += f"\n\n{templates['approval_instruction']}\n"
@@ -506,43 +534,51 @@ class EmailProvider(BaseProvider):
506
534
 
507
535
  # 添加纯文本版本的宣传标识
508
536
  text_body += f"\n\n---\n{templates['footer']}\n"
509
-
537
+
510
538
  # 构建HTML正文
511
539
  html_body = ["<html><body>"]
512
-
540
+
513
541
  # 将纯文本内容按行分割并转换为HTML段落
514
542
  content_lines = []
515
543
  instruction_lines = []
516
-
544
+
517
545
  # 分离内容和指导说明
518
- lines = text_body.split('\n')
546
+ lines = text_body.split("\n")
519
547
  instruction_start = -1
520
-
548
+
521
549
  for i, line in enumerate(lines):
522
- if line.strip() == templates['approval_instruction'] or line.strip() == templates['conversation_instruction']:
550
+ if (
551
+ line.strip() == templates["approval_instruction"]
552
+ or line.strip() == templates["conversation_instruction"]
553
+ ):
523
554
  instruction_start = i
524
555
  break
525
-
556
+
526
557
  if instruction_start > -1:
527
558
  content_lines = lines[:instruction_start]
528
559
  instruction_lines = lines[instruction_start:]
529
560
  else:
530
561
  content_lines = lines
531
-
562
+
532
563
  # 添加主要内容
533
564
  for line in content_lines:
534
565
  if line.strip():
535
566
  html_body.append(f"<p>{line}</p>")
536
-
567
+
537
568
  # 添加回复指导(使用不同的样式)
538
569
  if instruction_lines:
539
570
  html_body.append("<hr>")
540
- html_body.append("<div style='background-color: #f5f5f5; padding: 10px; border-left: 4px solid #007bff;'>")
541
-
571
+ html_body.append(
572
+ "<div style='background-color: #f5f5f5; padding: 10px; border-left: 4px solid #007bff;'>"
573
+ )
574
+
542
575
  if loop_type == HumanLoopType.APPROVAL:
543
- html_body.append(f"<p><strong>{templates['approval_instruction']}</strong></p>")
576
+ html_body.append(
577
+ f"<p><strong>{templates['approval_instruction']}</strong></p>"
578
+ )
544
579
  # 添加预格式化回复模板
545
- html_body.append(f"""
580
+ html_body.append(
581
+ f"""
546
582
  <div style="margin-top: 15px;">
547
583
  <div style="background-color: #f8f9fa; padding: 15px; border: 1px solid #ddd; border-radius: 5px;">
548
584
  <p style="margin: 0 0 10px 0; font-weight: bold;">{templates['approve_template_title']}</p>
@@ -551,7 +587,7 @@ class EmailProvider(BaseProvider):
551
587
  {templates['decision_prefix']}{list(self.approve_keywords)[0]}
552
588
  {templates['reason_prefix']}[Your reason]
553
589
  {templates['content_end_mark']}</pre>
554
-
590
+
555
591
  <p style="margin: 0 0 10px 0; font-weight: bold;">{templates['reject_template_title']}</p>
556
592
  <pre style="background-color: #ffffff; padding: 10px; border: 1px solid #eee; margin: 0;">
557
593
  {templates['content_start_mark']}
@@ -563,10 +599,14 @@ class EmailProvider(BaseProvider):
563
599
  {templates['template_instruction']}
564
600
  </p>
565
601
  </div>
566
- """)
602
+ """
603
+ )
567
604
  elif loop_type == HumanLoopType.INFORMATION:
568
- html_body.append(f"<p><strong>{templates['approval_instruction']}</strong></p>")
569
- html_body.append(f"""
605
+ html_body.append(
606
+ f"<p><strong>{templates['approval_instruction']}</strong></p>"
607
+ )
608
+ html_body.append(
609
+ f"""
570
610
  <div style="margin-top: 15px;">
571
611
  <div style="background-color: #f8f9fa; padding: 15px; border: 1px solid #ddd; border-radius: 5px;">
572
612
  <p style="margin: 0 0 10px 0; font-weight: bold;">{templates['info_template_title']}</p>
@@ -579,13 +619,17 @@ class EmailProvider(BaseProvider):
579
619
  {templates['info_template_instruction']}
580
620
  </p>
581
621
  </div>
582
- """)
622
+ """
623
+ )
583
624
  elif loop_type == HumanLoopType.CONVERSATION:
584
- # 使用模板变量替换硬编码的中文
585
- html_body.append(f"<p><strong>{templates['conversation_instruction']}</strong></p>")
586
-
625
+ # 使用模板变量替换硬编码的中文
626
+ html_body.append(
627
+ f"<p><strong>{templates['conversation_instruction']}</strong></p>"
628
+ )
629
+
587
630
  # 添加预格式化回复模板
588
- html_body.append(f"""
631
+ html_body.append(
632
+ f"""
589
633
  <div style="margin-top: 15px;">
590
634
  <div style="background-color: #f8f9fa; padding: 15px; border: 1px solid #ddd; border-radius: 5px;">
591
635
  <p style="margin: 0 0 10px 0; font-weight: bold;">{templates['continue_conversation_template']}</p>
@@ -604,19 +648,26 @@ class EmailProvider(BaseProvider):
604
648
  {templates['template_instruction']}
605
649
  </p>
606
650
  </div>
607
- """)
608
-
651
+ """
652
+ )
653
+
609
654
  html_body.append("</div>")
610
-
655
+
611
656
  # 添加 GoHumanLoop 宣传标识
612
657
  html_body.append("<hr style='margin-top: 20px; margin-bottom: 10px;'>")
613
- html_body.append("<div style='text-align: center; color: #666; font-size: 12px; margin-bottom: 10px;'>")
614
- html_body.append(f"<p>{'由 ' if self.language == 'zh' else 'Powered by '}<strong style='color: #007bff;'><a href=\"https://github.com/ptonlix/gohumanloop\" style=\"color: #007bff; text-decoration: none;\">GoHumanLoop</a></strong>{' 提供支持' if self.language == 'zh' else ''}</p>")
615
- html_body.append(f"<p style='font-style: italic;'>{templates['footer'].split(' - ')[1]}</p>")
658
+ html_body.append(
659
+ "<div style='text-align: center; color: #666; font-size: 12px; margin-bottom: 10px;'>"
660
+ )
661
+ html_body.append(
662
+ f"<p>{'由 ' if self.language == 'zh' else 'Powered by '}<strong style='color: #007bff;'><a href=\"https://github.com/ptonlix/gohumanloop\" style=\"color: #007bff; text-decoration: none;\">GoHumanLoop</a></strong>{' 提供支持' if self.language == 'zh' else ''}</p>"
663
+ )
664
+ html_body.append(
665
+ f"<p style='font-style: italic;'>{templates['footer'].split(' - ')[1]}</p>"
666
+ )
616
667
  html_body.append("</div>")
617
-
668
+
618
669
  return text_body, "\n".join(html_body)
619
-
670
+
620
671
  async def async_request_humanloop(
621
672
  self,
622
673
  task_id: str,
@@ -624,10 +675,10 @@ class EmailProvider(BaseProvider):
624
675
  loop_type: HumanLoopType,
625
676
  context: Dict[str, Any],
626
677
  metadata: Optional[Dict[str, Any]] = None,
627
- timeout: Optional[int] = None
678
+ timeout: Optional[int] = None,
628
679
  ) -> HumanLoopResult:
629
680
  """Request human-in-the-loop interaction
630
-
681
+
631
682
  Args:
632
683
  task_id: Task identifier
633
684
  conversation_id: Conversation ID for multi-turn dialogues
@@ -635,18 +686,15 @@ class EmailProvider(BaseProvider):
635
686
  context: Context information provided to human
636
687
  metadata: Additional metadata
637
688
  timeout: Request timeout in seconds
638
-
689
+
639
690
  Returns:
640
691
  HumanLoopResult: Result object containing request ID and initial status
641
692
  """
642
693
  metadata = metadata or {}
643
-
694
+
644
695
  # 生成请求ID
645
696
  request_id = self._generate_request_id()
646
697
 
647
-
648
-
649
-
650
698
  # 获取收件人邮箱
651
699
  recipient_email = metadata.get("recipient_email")
652
700
 
@@ -654,27 +702,33 @@ class EmailProvider(BaseProvider):
654
702
  subject_prefix = metadata.get("subject_prefix", f"[{self.name}]")
655
703
  subject = metadata.get("subject", f"{subject_prefix} Task {task_id}")
656
704
 
657
- # 存储请求信息
705
+ # 存储请求信息
658
706
  self._store_request(
659
707
  conversation_id=conversation_id,
660
708
  request_id=request_id,
661
709
  task_id=task_id,
662
710
  loop_type=loop_type,
663
711
  context=context,
664
- metadata={**metadata, "subject": subject, "recipient_email": recipient_email},
665
- timeout=timeout
712
+ metadata={
713
+ **metadata,
714
+ "subject": subject,
715
+ "recipient_email": recipient_email,
716
+ },
717
+ timeout=timeout,
666
718
  )
667
719
 
668
720
  if not recipient_email:
669
- self._update_request_status_error(conversation_id, request_id, "Recipient email address is missing")
721
+ self._update_request_status_error(
722
+ conversation_id, request_id, "Recipient email address is missing"
723
+ )
670
724
  return HumanLoopResult(
671
725
  conversation_id=conversation_id,
672
726
  request_id=request_id,
673
727
  loop_type=loop_type,
674
728
  status=HumanLoopStatus.ERROR,
675
- error="Recipient email address is missing"
729
+ error="Recipient email address is missing",
676
730
  )
677
-
731
+
678
732
  # 构建邮件内容
679
733
  prompt = self.build_prompt(
680
734
  task_id=task_id,
@@ -684,56 +738,58 @@ class EmailProvider(BaseProvider):
684
738
  created_at=datetime.now().isoformat(),
685
739
  context=context,
686
740
  metadata=metadata,
687
- color=False
741
+ color=False,
688
742
  )
689
743
 
690
744
  body, html_body = self._format_email_body(prompt, loop_type, subject)
691
-
692
-
693
-
745
+
694
746
  # 发送邮件
695
747
  success = await self._async_send_email(
696
- to_email=recipient_email,
697
- subject=subject,
698
- body=body,
699
- html_body=html_body
748
+ to_email=recipient_email, subject=subject, body=body, html_body=html_body
700
749
  )
701
-
750
+
702
751
  if not success:
703
752
  # Update request status to error
704
- self._update_request_status_error(conversation_id, request_id, "Failed to send email")
705
-
753
+ self._update_request_status_error(
754
+ conversation_id, request_id, "Failed to send email"
755
+ )
756
+
706
757
  return HumanLoopResult(
707
758
  conversation_id=conversation_id,
708
759
  request_id=request_id,
709
760
  loop_type=loop_type,
710
761
  status=HumanLoopStatus.ERROR,
711
- error="Failed to send email"
762
+ error="Failed to send email",
712
763
  )
713
764
  # 存储主题与请求的映射关系
714
765
  self._subject_to_request[subject] = (conversation_id, request_id)
715
-
766
+
716
767
  # 创建邮件检查任务
717
768
  # 使用线程池执行邮件检查任务,而不是使用asyncio.create_task
718
769
  self._mail_check_tasks[(conversation_id, request_id)] = self._executor.submit(
719
- self._run_email_check_task, conversation_id, request_id, recipient_email, subject
770
+ self._run_email_check_task,
771
+ conversation_id,
772
+ request_id,
773
+ recipient_email,
774
+ subject,
720
775
  )
721
-
722
-
776
+
723
777
  # 如果设置了超时,创建超时任务
724
778
  if timeout:
725
779
  await self._async_create_timeout_task(conversation_id, request_id, timeout)
726
-
780
+
727
781
  return HumanLoopResult(
728
782
  conversation_id=conversation_id,
729
783
  request_id=request_id,
730
784
  loop_type=loop_type,
731
- status=HumanLoopStatus.PENDING
785
+ status=HumanLoopStatus.PENDING,
732
786
  )
733
787
 
734
- def _run_email_check_task(self, conversation_id: str, request_id: str, recipient_email: str, subject: str):
788
+ def _run_email_check_task(
789
+ self, conversation_id: str, request_id: str, recipient_email: str, subject: str
790
+ ) -> None:
735
791
  """Run email check task in thread
736
-
792
+
737
793
  Args:
738
794
  conversation_id: Conversation ID
739
795
  request_id: Request ID
@@ -743,16 +799,20 @@ class EmailProvider(BaseProvider):
743
799
  # Create new event loop
744
800
  loop = asyncio.new_event_loop()
745
801
  asyncio.set_event_loop(loop)
746
-
802
+
747
803
  try:
748
804
  # Run email check in new event loop
749
805
  loop.run_until_complete(
750
- self._async_check_emails(conversation_id, request_id, recipient_email, subject)
806
+ self._async_check_emails(
807
+ conversation_id, request_id, recipient_email, subject
808
+ )
751
809
  )
752
810
  except Exception as e:
753
811
  logger.error(f"Email check task error: {str(e)}", exc_info=True)
754
812
  # Update request status to error
755
- self._update_request_status_error(conversation_id, request_id, f"Email check task error: {str(e)}")
813
+ self._update_request_status_error(
814
+ conversation_id, request_id, f"Email check task error: {str(e)}"
815
+ )
756
816
  finally:
757
817
  # Close event loop
758
818
  loop.close()
@@ -760,18 +820,15 @@ class EmailProvider(BaseProvider):
760
820
  if (conversation_id, request_id) in self._mail_check_tasks:
761
821
  del self._mail_check_tasks[(conversation_id, request_id)]
762
822
 
763
-
764
823
  async def async_check_request_status(
765
- self,
766
- conversation_id: str,
767
- request_id: str
824
+ self, conversation_id: str, request_id: str
768
825
  ) -> HumanLoopResult:
769
826
  """Check request status
770
-
827
+
771
828
  Args:
772
829
  conversation_id: Conversation identifier
773
830
  request_id: Request identifier
774
-
831
+
775
832
  Returns:
776
833
  HumanLoopResult: Result object containing current status
777
834
  """
@@ -782,9 +839,9 @@ class EmailProvider(BaseProvider):
782
839
  request_id=request_id,
783
840
  loop_type=HumanLoopType.CONVERSATION,
784
841
  status=HumanLoopStatus.ERROR,
785
- error=f"Request '{request_id}' not found in conversation '{conversation_id}'"
842
+ error=f"Request '{request_id}' not found in conversation '{conversation_id}'",
786
843
  )
787
-
844
+
788
845
  # 构建结果对象
789
846
  result = HumanLoopResult(
790
847
  conversation_id=conversation_id,
@@ -795,11 +852,11 @@ class EmailProvider(BaseProvider):
795
852
  feedback=request_info.get("feedback", {}),
796
853
  responded_by=request_info.get("responded_by", None),
797
854
  responded_at=request_info.get("responded_at", None),
798
- error=request_info.get("error", None)
855
+ error=request_info.get("error", None),
799
856
  )
800
-
857
+
801
858
  return result
802
-
859
+
803
860
  async def async_continue_humanloop(
804
861
  self,
805
862
  conversation_id: str,
@@ -808,24 +865,24 @@ class EmailProvider(BaseProvider):
808
865
  timeout: Optional[int] = None,
809
866
  ) -> HumanLoopResult:
810
867
  """Continue human-in-the-loop interaction
811
-
868
+
812
869
  Args:
813
870
  conversation_id: Conversation ID for multi-turn dialogues
814
871
  context: Context information provided to human
815
872
  metadata: Additional metadata
816
873
  timeout: Request timeout in seconds
817
-
874
+
818
875
  Returns:
819
876
  HumanLoopResult: Result object containing request ID and status
820
877
  """
821
878
  """Continue human-in-the-loop interaction for multi-turn dialogues
822
-
879
+
823
880
  Args:
824
881
  conversation_id: Conversation ID
825
882
  context: Context information provided to human
826
- metadata: Additional metadata
883
+ metadata: Additional metadata
827
884
  timeout: Request timeout in seconds
828
-
885
+
829
886
  Returns:
830
887
  HumanLoopResult: Result object containing request ID and status
831
888
  """
@@ -839,12 +896,12 @@ class EmailProvider(BaseProvider):
839
896
  request_id="",
840
897
  loop_type=HumanLoopType.CONVERSATION,
841
898
  status=HumanLoopStatus.ERROR,
842
- error=f"Conversation '{conversation_id}' not found"
899
+ error=f"Conversation '{conversation_id}' not found",
843
900
  )
844
-
901
+
845
902
  # 生成新的请求ID
846
903
  request_id = self._generate_request_id()
847
-
904
+
848
905
  # 获取任务ID
849
906
  task_id = conversation_info.get("task_id", "unknown_task")
850
907
 
@@ -856,34 +913,34 @@ class EmailProvider(BaseProvider):
856
913
  loop_type=HumanLoopType.CONVERSATION, # 继续对话默认为对话类型
857
914
  context=context,
858
915
  metadata=metadata or {},
859
- timeout=timeout
916
+ timeout=timeout,
860
917
  )
861
918
 
862
-
863
- # 获取收件人邮箱
919
+ # 获取收件人邮箱
864
920
  recipient_email = metadata.get("recipient_email")
865
921
  if not recipient_email:
866
- self._update_request_status_error(conversation_id, request_id, "Recipient email address is missing")
922
+ self._update_request_status_error(
923
+ conversation_id, request_id, "Recipient email address is missing"
924
+ )
867
925
  return HumanLoopResult(
868
926
  conversation_id=conversation_id,
869
927
  request_id=request_id,
870
928
  loop_type=HumanLoopType.CONVERSATION,
871
929
  status=HumanLoopStatus.ERROR,
872
- error="Recipient email address is missing"
930
+ error="Recipient email address is missing",
873
931
  )
874
932
 
875
-
876
933
  # 继续对话,使用相同的主题
877
934
  if conversation_id in self._conversations:
878
935
  conversation_info = self._get_conversation(conversation_id)
879
936
  if conversation_info:
880
- latest_request_id = conversation_info.get("latest_request_id")
937
+ latest_request_id = conversation_info.get("latest_request_id") or ""
881
938
  last_request_key = (conversation_id, latest_request_id)
882
939
  if last_request_key in self._requests:
883
940
  last_metadata = self._requests[last_request_key].get("metadata", {})
884
941
  if "subject" in last_metadata:
885
942
  subject = last_metadata["subject"]
886
- metadata["subject"] = subject # 保持相同主题
943
+ metadata["subject"] = subject # 保持相同主题
887
944
 
888
945
  # 构建邮件内容
889
946
  prompt = self.build_prompt(
@@ -894,61 +951,62 @@ class EmailProvider(BaseProvider):
894
951
  created_at=datetime.now().isoformat(),
895
952
  context=context,
896
953
  metadata=metadata,
897
- color=False
954
+ color=False,
898
955
  )
899
956
 
900
- body, html_body = self._format_email_body(prompt, HumanLoopType.CONVERSATION, subject)
957
+ body, html_body = self._format_email_body(
958
+ prompt, HumanLoopType.CONVERSATION, subject
959
+ )
901
960
 
902
961
  # 发送邮件
903
962
  success = await self._async_send_email(
904
- to_email=recipient_email,
905
- subject=subject,
906
- body=body,
907
- html_body=html_body
963
+ to_email=recipient_email, subject=subject, body=body, html_body=html_body
908
964
  )
909
-
965
+
910
966
  if not success:
911
967
  # Update request status to error
912
- self._update_request_status_error(conversation_id, request_id, "Failed to send email")
913
-
968
+ self._update_request_status_error(
969
+ conversation_id, request_id, "Failed to send email"
970
+ )
971
+
914
972
  return HumanLoopResult(
915
973
  conversation_id=conversation_id,
916
974
  request_id=request_id,
917
975
  loop_type=HumanLoopType.CONVERSATION,
918
976
  status=HumanLoopStatus.ERROR,
919
- error="Failed to send email"
977
+ error="Failed to send email",
920
978
  )
921
-
979
+
922
980
  # 存储主题与请求的映射关系
923
981
  self._subject_to_request[subject] = (conversation_id, request_id)
924
982
 
925
983
  # 使用线程池执行邮件检查任务,而不是使用asyncio.create_task
926
984
  self._mail_check_tasks[(conversation_id, request_id)] = self._executor.submit(
927
- self._run_email_check_task, conversation_id, request_id, recipient_email, subject
985
+ self._run_email_check_task,
986
+ conversation_id,
987
+ request_id,
988
+ recipient_email,
989
+ subject,
928
990
  )
929
-
991
+
930
992
  # 如果设置了超时,创建超时任务
931
993
  if timeout:
932
994
  await self._async_create_timeout_task(conversation_id, request_id, timeout)
933
-
995
+
934
996
  return HumanLoopResult(
935
997
  conversation_id=conversation_id,
936
998
  request_id=request_id,
937
999
  loop_type=HumanLoopType.CONVERSATION,
938
- status=HumanLoopStatus.PENDING
1000
+ status=HumanLoopStatus.PENDING,
939
1001
  )
940
1002
 
941
- async def async_cancel_request(
942
- self,
943
- conversation_id: str,
944
- request_id: str
945
- ) -> bool:
1003
+ async def async_cancel_request(self, conversation_id: str, request_id: str) -> bool:
946
1004
  """Cancel human-in-the-loop request
947
-
1005
+
948
1006
  Args:
949
1007
  conversation_id: Conversation identifier for multi-turn dialogues
950
1008
  request_id: Request identifier for specific interaction request
951
-
1009
+
952
1010
  Return:
953
1011
  bool: Whether cancellation was successful, True indicates successful cancellation,
954
1012
  False indicates cancellation failed
@@ -958,19 +1016,16 @@ class EmailProvider(BaseProvider):
958
1016
  if request_key in self._mail_check_tasks:
959
1017
  self._mail_check_tasks[request_key].cancel()
960
1018
  del self._mail_check_tasks[request_key]
961
-
1019
+
962
1020
  # 调用父类方法取消请求
963
1021
  return await super().async_cancel_request(conversation_id, request_id)
964
-
965
- async def async_cancel_conversation(
966
- self,
967
- conversation_id: str
968
- ) -> bool:
1022
+
1023
+ async def async_cancel_conversation(self, conversation_id: str) -> bool:
969
1024
  """Cancel the entire conversation
970
-
1025
+
971
1026
  Args:
972
1027
  conversation_id: Conversation identifier
973
-
1028
+
974
1029
  Returns:
975
1030
  bool: Whether cancellation was successful
976
1031
  """
@@ -980,23 +1035,23 @@ class EmailProvider(BaseProvider):
980
1035
  if request_key in self._mail_check_tasks:
981
1036
  self._mail_check_tasks[request_key].cancel()
982
1037
  del self._mail_check_tasks[request_key]
983
-
1038
+
984
1039
  # 调用父类方法取消对话
985
1040
  return await super().async_cancel_conversation(conversation_id)
986
1041
 
987
- def _extract_user_reply_content(self, body: str) -> str:
1042
+ def _extract_user_reply_content(self, body: str) -> str | None:
988
1043
  """Extract the actual reply content from the email, excluding quoted original email content
989
-
1044
+
990
1045
  Args:
991
1046
  body: Email body text
992
-
1047
+
993
1048
  Returns:
994
1049
  str: User's actual reply content
995
1050
  """
996
1051
  # 首先尝试使用标记提取内容
997
- start_marker = self.templates['content_start_mark']
998
- end_marker = self.templates['content_end_mark']
999
-
1052
+ start_marker = self.templates["content_start_mark"]
1053
+ end_marker = self.templates["content_end_mark"]
1054
+
1000
1055
  if start_marker in body and end_marker in body:
1001
1056
  start_index = body.find(start_marker) + len(start_marker)
1002
1057
  end_index = body.find(end_marker)
@@ -1006,58 +1061,64 @@ class EmailProvider(BaseProvider):
1006
1061
  # 常见的邮件回复分隔符模式
1007
1062
  reply_patterns = [
1008
1063
  # 常见的邮件客户端引用标记
1009
- r"On .* wrote:", # Gmail, Apple Mail 等
1010
- r"From: .*\n?Sent: .*\n?To: .*", # Outlook
1011
- r"-{3,}Original Message-{3,}", # 一些邮件客户端
1064
+ r"On .* wrote:", # Gmail, Apple Mail 等
1065
+ r"From: .*\n?Sent: .*\n?To: .*", # Outlook
1066
+ r"-{3,}Original Message-{3,}", # 一些邮件客户端
1012
1067
  r"From: [\w\s<>@.]+(\[mailto:[\w@.]+\])?\s+Sent:", # Outlook 变体
1013
- r"在.*写道:", # 中文邮件客户端
1068
+ r"在.*写道:", # 中文邮件客户端
1014
1069
  r"发件人: .*\n?时间: .*\n?收件人: .*", # 中文 Outlook
1015
- r">{3,}", # 多级引用
1016
- r"_{5,}", # 分隔线
1017
- r"\*{5,}", # 分隔线
1018
- r"={5,}", # 分隔线
1019
- r"原始邮件", # 中文原始邮件
1020
- r"请按以下格式回复:", # 我们自己的指导语
1070
+ r">{3,}", # 多级引用
1071
+ r"_{5,}", # 分隔线
1072
+ r"\*{5,}", # 分隔线
1073
+ r"={5,}", # 分隔线
1074
+ r"原始邮件", # 中文原始邮件
1075
+ r"请按以下格式回复:", # 我们自己的指导语
1021
1076
  r"请直接回复您的内容。如需结束对话,请在回复中包含", # 我们自己的指导语
1022
1077
  ]
1023
-
1078
+
1024
1079
  # 合并所有模式
1025
1080
  combined_pattern = "|".join(reply_patterns)
1026
-
1081
+
1027
1082
  # 尝试根据分隔符分割邮件内容
1028
1083
  parts = re.split(combined_pattern, body, flags=re.IGNORECASE)
1029
-
1084
+
1030
1085
  if parts and len(parts) > 1:
1031
1086
  # 第一部分通常是用户的回复
1032
1087
  user_content = parts[0].strip()
1033
-
1088
+
1034
1089
  # 如果没有找到明确的分隔符,尝试基于行分析
1035
- lines = body.split('\n')
1090
+ lines = body.split("\n")
1036
1091
  user_content_lines = []
1037
-
1092
+
1038
1093
  for line in lines:
1039
1094
  # 跳过引用行(以 > 开头的行)
1040
- if line.strip().startswith('>'):
1095
+ if line.strip().startswith(">"):
1041
1096
  continue
1042
1097
  # 如果遇到可能的分隔符,停止收集
1043
- if any(re.search(pattern, line, re.IGNORECASE) for pattern in reply_patterns):
1098
+ if any(
1099
+ re.search(pattern, line, re.IGNORECASE)
1100
+ for pattern in reply_patterns
1101
+ ):
1044
1102
  break
1045
1103
  user_content_lines.append(line)
1046
-
1047
- user_content = '\n'.join(user_content_lines).strip()
1048
-
1104
+
1105
+ user_content = "\n".join(user_content_lines).strip()
1106
+
1049
1107
  # 清理用户内容中的多余换行符和空白
1050
1108
  # 1. 将所有 \r\n 替换为 \n
1051
- user_content = user_content.replace('\r\n', '\n')
1052
-
1109
+ user_content = user_content.replace("\r\n", "\n")
1110
+
1053
1111
  # 2. 将连续的多个换行符替换为最多两个换行符
1054
- user_content = re.sub(r'\n{3,}', '\n\n', user_content)
1055
-
1112
+ user_content = re.sub(r"\n{3,}", "\n\n", user_content)
1113
+
1056
1114
  # 3. 去除开头和结尾的空白行
1057
1115
  user_content = user_content.strip()
1058
-
1116
+
1059
1117
  # 4. 如果内容为空,返回一个友好的提示
1060
1118
  if not user_content or user_content.isspace():
1061
1119
  return "User didn't provide valid reply content"
1062
-
1120
+
1063
1121
  return user_content
1122
+
1123
+ # 如果没有找到任何有效的用户回复内容,返回 None
1124
+ return None