gohumanloop 0.0.4__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.
- gohumanloop/__init__.py +15 -9
- gohumanloop/adapters/__init__.py +4 -4
- gohumanloop/adapters/langgraph_adapter.py +365 -220
- gohumanloop/cli/main.py +4 -1
- gohumanloop/core/interface.py +181 -215
- gohumanloop/core/manager.py +341 -361
- gohumanloop/manager/ghl_manager.py +223 -185
- gohumanloop/models/api_model.py +32 -7
- gohumanloop/models/glh_model.py +15 -11
- gohumanloop/providers/api_provider.py +233 -189
- gohumanloop/providers/base.py +179 -172
- gohumanloop/providers/email_provider.py +386 -325
- gohumanloop/providers/ghl_provider.py +19 -17
- gohumanloop/providers/terminal_provider.py +111 -92
- gohumanloop/utils/__init__.py +7 -1
- gohumanloop/utils/context_formatter.py +20 -15
- gohumanloop/utils/threadsafedict.py +64 -56
- gohumanloop/utils/utils.py +28 -28
- gohumanloop-0.0.6.dist-info/METADATA +259 -0
- gohumanloop-0.0.6.dist-info/RECORD +30 -0
- {gohumanloop-0.0.4.dist-info → gohumanloop-0.0.6.dist-info}/WHEEL +1 -1
- gohumanloop-0.0.4.dist-info/METADATA +0 -35
- gohumanloop-0.0.4.dist-info/RECORD +0 -30
- {gohumanloop-0.0.4.dist-info → gohumanloop-0.0.6.dist-info}/entry_points.txt +0 -0
- {gohumanloop-0.0.4.dist-info → gohumanloop-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {gohumanloop-0.0.4.dist-info → gohumanloop-0.0.6.dist-info}/top_level.txt +0 -0
@@ -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,
|
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
|
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(
|
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(
|
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":
|
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(
|
171
|
-
msg[
|
172
|
-
msg[
|
173
|
-
msg[
|
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,
|
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,
|
181
|
-
|
184
|
+
msg.attach(email.mime.text.MIMEText(html_body, "html"))
|
185
|
+
|
182
186
|
# 如果是回复邮件,添加相关邮件头
|
183
187
|
if reply_to:
|
184
|
-
msg[
|
185
|
-
msg[
|
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(
|
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][
|
224
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
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(
|
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(
|
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
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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(
|
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(
|
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
|
-
|
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
|
336
|
+
client.set_flags(uid, [b"\\Seen"])
|
320
337
|
return email_message
|
321
338
|
return None
|
322
|
-
|
323
|
-
async def _process_email_response(
|
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
|
358
|
-
|
359
|
-
|
360
|
-
|
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 =
|
390
|
-
reason =
|
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
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
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 =
|
455
|
+
information = ""
|
433
456
|
information_prefix = self.templates["information_prefix"]
|
434
|
-
|
435
|
-
|
436
|
-
line
|
437
|
-
|
438
|
-
|
439
|
-
|
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
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
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(
|
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(
|
546
|
+
lines = text_body.split("\n")
|
519
547
|
instruction_start = -1
|
520
|
-
|
548
|
+
|
521
549
|
for i, line in enumerate(lines):
|
522
|
-
if
|
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(
|
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(
|
576
|
+
html_body.append(
|
577
|
+
f"<p><strong>{templates['approval_instruction']}</strong></p>"
|
578
|
+
)
|
544
579
|
# 添加预格式化回复模板
|
545
|
-
html_body.append(
|
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(
|
569
|
-
|
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(
|
586
|
-
|
625
|
+
# 使用模板变量替换硬编码的中文
|
626
|
+
html_body.append(
|
627
|
+
f"<p><strong>{templates['conversation_instruction']}</strong></p>"
|
628
|
+
)
|
629
|
+
|
587
630
|
# 添加预格式化回复模板
|
588
|
-
html_body.append(
|
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(
|
614
|
-
|
615
|
-
|
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={
|
665
|
-
|
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(
|
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(
|
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,
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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,
|
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[
|
998
|
-
end_marker =
|
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:",
|
1010
|
-
r"From: .*\n?Sent: .*\n?To: .*",
|
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(
|
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(
|
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 =
|
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(
|
1052
|
-
|
1109
|
+
user_content = user_content.replace("\r\n", "\n")
|
1110
|
+
|
1053
1111
|
# 2. 将连续的多个换行符替换为最多两个换行符
|
1054
|
-
user_content = re.sub(r
|
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
|