gohumanloop 0.0.3__py3-none-any.whl → 0.0.4__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/adapters/langgraph_adapter.py +31 -16
- gohumanloop/manager/ghl_manager.py +2 -2
- gohumanloop/providers/api_provider.py +34 -8
- gohumanloop/providers/base.py +29 -98
- gohumanloop/providers/email_provider.py +53 -9
- gohumanloop/providers/terminal_provider.py +83 -10
- gohumanloop/utils/utils.py +5 -2
- {gohumanloop-0.0.3.dist-info → gohumanloop-0.0.4.dist-info}/METADATA +1 -1
- {gohumanloop-0.0.3.dist-info → gohumanloop-0.0.4.dist-info}/RECORD +13 -13
- {gohumanloop-0.0.3.dist-info → gohumanloop-0.0.4.dist-info}/WHEEL +0 -0
- {gohumanloop-0.0.3.dist-info → gohumanloop-0.0.4.dist-info}/entry_points.txt +0 -0
- {gohumanloop-0.0.3.dist-info → gohumanloop-0.0.4.dist-info}/licenses/LICENSE +0 -0
- {gohumanloop-0.0.3.dist-info → gohumanloop-0.0.4.dist-info}/top_level.txt +0 -0
@@ -633,7 +633,7 @@ class LangGraphHumanLoopCallback(HumanLoopCallback):
|
|
633
633
|
if self.async_on_timeout:
|
634
634
|
await self.async_on_timeout(self.state, provider)
|
635
635
|
|
636
|
-
async def
|
636
|
+
async def async_on_humanloop_error(
|
637
637
|
self,
|
638
638
|
provider: HumanLoopProvider,
|
639
639
|
error: Exception
|
@@ -713,6 +713,8 @@ default_adapter = LangGraphAdapter(manager, default_timeout=60)
|
|
713
713
|
|
714
714
|
default_conversation_id = str(uuid.uuid4())
|
715
715
|
|
716
|
+
_SKIP_NEXT_HUMANLOOP = False
|
717
|
+
|
716
718
|
def interrupt(value: Any, lg_humanloop: LangGraphAdapter = default_adapter) -> Any:
|
717
719
|
"""
|
718
720
|
Wraps LangGraph's interrupt functionality to pause graph execution and wait for human input
|
@@ -726,23 +728,31 @@ def interrupt(value: Any, lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
726
728
|
Returns:
|
727
729
|
Input value provided by human user
|
728
730
|
"""
|
731
|
+
|
732
|
+
global _SKIP_NEXT_HUMANLOOP
|
733
|
+
|
729
734
|
if not _SUPPORTS_INTERRUPT:
|
730
735
|
raise RuntimeError(
|
731
736
|
"LangGraph version too low, interrupt not supported. Please upgrade to version 0.2.57 or higher."
|
732
737
|
"You can use: pip install --upgrade langgraph>=0.2.57"
|
733
738
|
)
|
734
739
|
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
740
|
+
if not _SKIP_NEXT_HUMANLOOP:
|
741
|
+
# Get current event loop or create new one
|
742
|
+
lg_humanloop.manager.request_humanloop(
|
743
|
+
task_id="lg_interrupt",
|
744
|
+
conversation_id=default_conversation_id,
|
745
|
+
loop_type=HumanLoopType.INFORMATION,
|
746
|
+
context={
|
747
|
+
"message": f"{value}",
|
748
|
+
"question": "The execution has been interrupted. Please review the above information and provide your input to continue.",
|
749
|
+
},
|
750
|
+
blocking=False,
|
751
|
+
)
|
752
|
+
else:
|
753
|
+
# Reset flag to allow normal human intervention trigger next time
|
754
|
+
_SKIP_NEXT_HUMANLOOP = False
|
755
|
+
|
746
756
|
|
747
757
|
# Return LangGraph's interrupt
|
748
758
|
return _lg_interrupt(value)
|
@@ -758,6 +768,9 @@ def create_resume_command(lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
758
768
|
Returns:
|
759
769
|
Command object that can be used with graph.stream method
|
760
770
|
"""
|
771
|
+
|
772
|
+
global _SKIP_NEXT_HUMANLOOP
|
773
|
+
|
761
774
|
if not _SUPPORTS_INTERRUPT:
|
762
775
|
raise RuntimeError(
|
763
776
|
"LangGraph version too low, Command feature not supported. Please upgrade to 0.2.57 or higher."
|
@@ -769,15 +782,13 @@ def create_resume_command(lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
769
782
|
poll_interval = 1.0 # Polling interval (seconds)
|
770
783
|
while True:
|
771
784
|
result = lg_humanloop.manager.check_conversation_status(default_conversation_id)
|
772
|
-
print(result)
|
773
785
|
# If status is final state (not PENDING), return result
|
774
786
|
if result.status != HumanLoopStatus.PENDING:
|
775
787
|
return result.response
|
776
788
|
# Wait before polling again
|
777
789
|
time.sleep(poll_interval)
|
778
|
-
|
779
|
-
|
780
|
-
# loop = asyncio.get_event_loop() # In synchronous environment
|
790
|
+
|
791
|
+
_SKIP_NEXT_HUMANLOOP = True
|
781
792
|
|
782
793
|
response = poll_for_result()
|
783
794
|
return _lg_Command(resume=response)
|
@@ -794,6 +805,8 @@ async def acreate_resume_command(lg_humanloop: LangGraphAdapter = default_adapte
|
|
794
805
|
Returns:
|
795
806
|
Command object that can be used with graph.astream method
|
796
807
|
"""
|
808
|
+
global _SKIP_NEXT_HUMANLOOP
|
809
|
+
|
797
810
|
if not _SUPPORTS_INTERRUPT:
|
798
811
|
raise RuntimeError(
|
799
812
|
"LangGraph version too low, Command feature not supported. Please upgrade to 0.2.57 or higher."
|
@@ -811,6 +824,8 @@ async def acreate_resume_command(lg_humanloop: LangGraphAdapter = default_adapte
|
|
811
824
|
# Wait before polling again
|
812
825
|
await asyncio.sleep(poll_interval)
|
813
826
|
|
827
|
+
_SKIP_NEXT_HUMANLOOP = True
|
828
|
+
|
814
829
|
# Wait for async result directly
|
815
830
|
response = await poll_for_result()
|
816
831
|
return _lg_Command(resume=response)
|
@@ -434,7 +434,7 @@ class GoHumanLoopManager(DefaultHumanLoopManager):
|
|
434
434
|
Returns:
|
435
435
|
HumanLoopResult: 请求状态结果
|
436
436
|
"""
|
437
|
-
#
|
437
|
+
# check_request_status
|
438
438
|
if provider_id is None:
|
439
439
|
provider_id = self._conversation_provider.get(conversation_id)
|
440
440
|
|
@@ -442,7 +442,7 @@ class GoHumanLoopManager(DefaultHumanLoopManager):
|
|
442
442
|
raise ValueError(f"Provider '{provider_id}' not found")
|
443
443
|
|
444
444
|
provider = self.providers[provider_id]
|
445
|
-
return await provider.
|
445
|
+
return await provider.async_check_request_status(conversation_id, request_id)
|
446
446
|
|
447
447
|
async def async_check_request_status(
|
448
448
|
self,
|
@@ -4,7 +4,7 @@ from typing import Dict, Any, Optional
|
|
4
4
|
|
5
5
|
import aiohttp
|
6
6
|
from pydantic import SecretStr
|
7
|
-
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
8
8
|
from gohumanloop.core.interface import (
|
9
9
|
HumanLoopResult, HumanLoopStatus, HumanLoopType
|
10
10
|
)
|
@@ -56,6 +56,18 @@ class APIProvider(BaseProvider):
|
|
56
56
|
|
57
57
|
# Store the currently running polling tasks.
|
58
58
|
self._poll_tasks = {}
|
59
|
+
# Create thread pool for background service execution
|
60
|
+
self._executor = ThreadPoolExecutor(max_workers=10)
|
61
|
+
|
62
|
+
|
63
|
+
def __del__(self):
|
64
|
+
"""析构函数,确保线程池被正确关闭"""
|
65
|
+
self._executor.shutdown(wait=False)
|
66
|
+
|
67
|
+
# 取消所有邮件检查任务
|
68
|
+
for task_key, future in list(self._poll_tasks.items()):
|
69
|
+
future.cancel()
|
70
|
+
self._poll_tasks.clear()
|
59
71
|
|
60
72
|
def __str__(self) -> str:
|
61
73
|
"""Returns a string description of this instance"""
|
@@ -230,10 +242,9 @@ class APIProvider(BaseProvider):
|
|
230
242
|
)
|
231
243
|
|
232
244
|
# Create polling task
|
233
|
-
|
234
|
-
|
245
|
+
self._poll_tasks[(conversation_id, request_id)] = self._executor.submit(
|
246
|
+
self._run_async_poll_request_status, conversation_id, request_id, platform
|
235
247
|
)
|
236
|
-
self._poll_tasks[(conversation_id, request_id)] = poll_task
|
237
248
|
|
238
249
|
# Create timeout task if timeout is set
|
239
250
|
if timeout:
|
@@ -258,6 +269,22 @@ class APIProvider(BaseProvider):
|
|
258
269
|
status=HumanLoopStatus.ERROR,
|
259
270
|
error=str(e)
|
260
271
|
)
|
272
|
+
|
273
|
+
def _run_async_poll_request_status(self, conversation_id: str, request_id: str, platform: str):
|
274
|
+
"""Run asynchronous API interaction in a separate thread"""
|
275
|
+
# Create new event loop
|
276
|
+
loop = asyncio.new_event_loop()
|
277
|
+
asyncio.set_event_loop(loop)
|
278
|
+
|
279
|
+
try:
|
280
|
+
# Run interaction processing in the new event loop
|
281
|
+
loop.run_until_complete(self._async_poll_request_status(conversation_id, request_id, platform))
|
282
|
+
finally:
|
283
|
+
loop.close()
|
284
|
+
# Remove from task dictionary
|
285
|
+
if (conversation_id, request_id) in self._poll_tasks:
|
286
|
+
del self._poll_tasks[(conversation_id, request_id)]
|
287
|
+
|
261
288
|
async def async_check_request_status(
|
262
289
|
self,
|
263
290
|
conversation_id: str,
|
@@ -516,11 +543,10 @@ class APIProvider(BaseProvider):
|
|
516
543
|
error=error_msg
|
517
544
|
)
|
518
545
|
|
519
|
-
|
520
|
-
|
521
|
-
|
546
|
+
# Create polling task
|
547
|
+
self._poll_tasks[(conversation_id, request_id)] = self._executor.submit(
|
548
|
+
self._run_async_poll_request_status, conversation_id, request_id, platform
|
522
549
|
)
|
523
|
-
self._poll_tasks[(conversation_id, request_id)] = poll_task
|
524
550
|
|
525
551
|
# Create timeout task if timeout is set
|
526
552
|
if timeout:
|
gohumanloop/providers/base.py
CHANGED
@@ -7,6 +7,7 @@ import uuid
|
|
7
7
|
from datetime import datetime
|
8
8
|
from collections import defaultdict
|
9
9
|
from gohumanloop.utils.threadsafedict import ThreadSafeDict
|
10
|
+
from gohumanloop.utils import run_async_safely
|
10
11
|
|
11
12
|
from gohumanloop.core.interface import (
|
12
13
|
HumanLoopProvider, HumanLoopResult, HumanLoopStatus, HumanLoopType
|
@@ -157,15 +158,7 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
157
158
|
HumanLoopResult: Result object containing request ID and initial status
|
158
159
|
"""
|
159
160
|
|
160
|
-
|
161
|
-
if loop.is_running():
|
162
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
163
|
-
new_loop = asyncio.new_event_loop()
|
164
|
-
asyncio.set_event_loop(new_loop)
|
165
|
-
loop = new_loop
|
166
|
-
|
167
|
-
try:
|
168
|
-
return loop.run_until_complete(
|
161
|
+
return run_async_safely(
|
169
162
|
self.async_request_humanloop(
|
170
163
|
task_id=task_id,
|
171
164
|
conversation_id=conversation_id,
|
@@ -173,10 +166,8 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
173
166
|
context=context,
|
174
167
|
metadata=metadata,
|
175
168
|
timeout=timeout
|
176
|
-
|
177
|
-
|
178
|
-
if loop != asyncio.get_event_loop():
|
179
|
-
loop.close()
|
169
|
+
)
|
170
|
+
)
|
180
171
|
|
181
172
|
|
182
173
|
async def async_check_request_status(
|
@@ -220,24 +211,13 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
220
211
|
Returns:
|
221
212
|
HumanLoopResult: Result containing the status of the latest request in the conversation
|
222
213
|
"""
|
223
|
-
|
224
|
-
|
225
|
-
if loop.is_running():
|
226
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
227
|
-
new_loop = asyncio.new_event_loop()
|
228
|
-
asyncio.set_event_loop(new_loop)
|
229
|
-
loop = new_loop
|
230
|
-
|
231
|
-
try:
|
232
|
-
return loop.run_until_complete(
|
214
|
+
|
215
|
+
return run_async_safely(
|
233
216
|
self.async_check_request_status(
|
234
217
|
conversation_id=conversation_id,
|
235
218
|
request_id=request_id
|
236
|
-
)
|
237
|
-
|
238
|
-
if loop != asyncio.get_event_loop():
|
239
|
-
loop.close()
|
240
|
-
|
219
|
+
)
|
220
|
+
)
|
241
221
|
|
242
222
|
async def async_check_conversation_status(
|
243
223
|
self,
|
@@ -286,23 +266,12 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
286
266
|
Returns:
|
287
267
|
HumanLoopResult: Result containing the status of the latest request in the conversation
|
288
268
|
"""
|
289
|
-
|
290
|
-
loop = asyncio.get_event_loop()
|
291
|
-
if loop.is_running():
|
292
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
293
|
-
new_loop = asyncio.new_event_loop()
|
294
|
-
asyncio.set_event_loop(new_loop)
|
295
|
-
loop = new_loop
|
296
|
-
|
297
|
-
try:
|
298
|
-
return loop.run_until_complete(
|
299
|
-
self.async_check_conversation_status(
|
300
|
-
conversation_id=conversation_id
|
301
|
-
))
|
302
|
-
finally:
|
303
|
-
if loop != asyncio.get_event_loop():
|
304
|
-
loop.close()
|
305
269
|
|
270
|
+
return run_async_safely(
|
271
|
+
self.async_check_conversation_status(
|
272
|
+
conversation_id=conversation_id
|
273
|
+
)
|
274
|
+
)
|
306
275
|
|
307
276
|
async def async_cancel_request(
|
308
277
|
self,
|
@@ -345,22 +314,13 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
345
314
|
Returns:
|
346
315
|
bool: Whether cancellation was successful, True indicates success, False indicates failure
|
347
316
|
"""
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
new_loop = asyncio.new_event_loop()
|
352
|
-
asyncio.set_event_loop(new_loop)
|
353
|
-
loop = new_loop
|
354
|
-
|
355
|
-
try:
|
356
|
-
return loop.run_until_complete(
|
357
|
-
self.async_cancel_request(
|
317
|
+
|
318
|
+
return run_async_safely(
|
319
|
+
self.async_cancel_request(
|
358
320
|
conversation_id=conversation_id,
|
359
321
|
request_id=request_id
|
360
|
-
)
|
361
|
-
|
362
|
-
if loop != asyncio.get_event_loop():
|
363
|
-
loop.close()
|
322
|
+
)
|
323
|
+
)
|
364
324
|
|
365
325
|
async def async_cancel_conversation(
|
366
326
|
self,
|
@@ -410,22 +370,11 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
410
370
|
bool: Whether the cancellation was successful
|
411
371
|
"""
|
412
372
|
|
413
|
-
|
414
|
-
|
415
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
416
|
-
new_loop = asyncio.new_event_loop()
|
417
|
-
asyncio.set_event_loop(new_loop)
|
418
|
-
loop = new_loop
|
419
|
-
|
420
|
-
try:
|
421
|
-
return loop.run_until_complete(
|
422
|
-
self.async_cancel_conversation(
|
373
|
+
return run_async_safely(
|
374
|
+
self.async_cancel_conversation(
|
423
375
|
conversation_id=conversation_id
|
424
|
-
)
|
425
|
-
|
426
|
-
if loop != asyncio.get_event_loop():
|
427
|
-
loop.close()
|
428
|
-
|
376
|
+
)
|
377
|
+
)
|
429
378
|
|
430
379
|
async def async_continue_humanloop(
|
431
380
|
self,
|
@@ -479,24 +428,14 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
479
428
|
HumanLoopResult: Result object containing request ID and status
|
480
429
|
"""
|
481
430
|
|
482
|
-
|
483
|
-
|
484
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
485
|
-
new_loop = asyncio.new_event_loop()
|
486
|
-
asyncio.set_event_loop(new_loop)
|
487
|
-
loop = new_loop
|
488
|
-
|
489
|
-
try:
|
490
|
-
return loop.run_until_complete(
|
491
|
-
self.async_continue_humanloop(
|
431
|
+
return run_async_safely(
|
432
|
+
self.async_continue_humanloop(
|
492
433
|
conversation_id=conversation_id,
|
493
434
|
context=context,
|
494
435
|
metadata=metadata,
|
495
436
|
timeout=timeout
|
496
|
-
)
|
497
|
-
|
498
|
-
if loop != asyncio.get_event_loop():
|
499
|
-
loop.close()
|
437
|
+
)
|
438
|
+
)
|
500
439
|
|
501
440
|
def async_get_conversation_history(self, conversation_id: str) -> List[Dict[str, Any]]:
|
502
441
|
"""Get complete history for the specified conversation
|
@@ -533,18 +472,10 @@ class BaseProvider(HumanLoopProvider, ABC):
|
|
533
472
|
List[Dict[str, Any]]: List of conversation history records, each containing request ID,
|
534
473
|
status, context, response and other information
|
535
474
|
"""
|
536
|
-
loop = asyncio.get_event_loop()
|
537
|
-
if loop.is_running():
|
538
|
-
new_loop = asyncio.new_event_loop()
|
539
|
-
asyncio.set_event_loop(new_loop)
|
540
|
-
loop = new_loop
|
541
|
-
|
542
|
-
try:
|
543
|
-
return loop.run_until_complete(self.async_get_conversation_history(conversation_id))
|
544
|
-
finally:
|
545
|
-
if loop != asyncio.get_event_loop():
|
546
|
-
loop.close()
|
547
475
|
|
476
|
+
return run_async_safely(
|
477
|
+
self.async_get_conversation_history(conversation_id=conversation_id)
|
478
|
+
)
|
548
479
|
|
549
480
|
async def _async_create_timeout_task(
|
550
481
|
self,
|
@@ -12,6 +12,7 @@ import logging
|
|
12
12
|
from typing import Dict, Any, Optional, List, Tuple
|
13
13
|
from datetime import datetime
|
14
14
|
from pydantic import SecretStr
|
15
|
+
from concurrent.futures import ThreadPoolExecutor
|
15
16
|
|
16
17
|
from gohumanloop.core.interface import ( HumanLoopResult, HumanLoopStatus, HumanLoopType
|
17
18
|
)
|
@@ -79,6 +80,19 @@ class EmailProvider(BaseProvider):
|
|
79
80
|
|
80
81
|
self._init_language_templates()
|
81
82
|
|
83
|
+
# Create thread pool for background service execution
|
84
|
+
self._executor = ThreadPoolExecutor(max_workers=10)
|
85
|
+
|
86
|
+
|
87
|
+
def __del__(self):
|
88
|
+
"""析构函数,确保线程池被正确关闭"""
|
89
|
+
self._executor.shutdown(wait=False)
|
90
|
+
|
91
|
+
# 取消所有邮件检查任务
|
92
|
+
for task_key, future in list(self._mail_check_tasks.items()):
|
93
|
+
future.cancel()
|
94
|
+
self._mail_check_tasks.clear()
|
95
|
+
|
82
96
|
def _init_language_templates(self):
|
83
97
|
"""初始化不同语言的模板和关键词"""
|
84
98
|
if self.language == "zh":
|
@@ -700,10 +714,11 @@ class EmailProvider(BaseProvider):
|
|
700
714
|
self._subject_to_request[subject] = (conversation_id, request_id)
|
701
715
|
|
702
716
|
# 创建邮件检查任务
|
703
|
-
|
704
|
-
|
717
|
+
# 使用线程池执行邮件检查任务,而不是使用asyncio.create_task
|
718
|
+
self._mail_check_tasks[(conversation_id, request_id)] = self._executor.submit(
|
719
|
+
self._run_email_check_task, conversation_id, request_id, recipient_email, subject
|
705
720
|
)
|
706
|
-
|
721
|
+
|
707
722
|
|
708
723
|
# 如果设置了超时,创建超时任务
|
709
724
|
if timeout:
|
@@ -715,7 +730,37 @@ class EmailProvider(BaseProvider):
|
|
715
730
|
loop_type=loop_type,
|
716
731
|
status=HumanLoopStatus.PENDING
|
717
732
|
)
|
733
|
+
|
734
|
+
def _run_email_check_task(self, conversation_id: str, request_id: str, recipient_email: str, subject: str):
|
735
|
+
"""Run email check task in thread
|
736
|
+
|
737
|
+
Args:
|
738
|
+
conversation_id: Conversation ID
|
739
|
+
request_id: Request ID
|
740
|
+
recipient_email: Recipient email address
|
741
|
+
subject: Email subject
|
742
|
+
"""
|
743
|
+
# Create new event loop
|
744
|
+
loop = asyncio.new_event_loop()
|
745
|
+
asyncio.set_event_loop(loop)
|
718
746
|
|
747
|
+
try:
|
748
|
+
# Run email check in new event loop
|
749
|
+
loop.run_until_complete(
|
750
|
+
self._async_check_emails(conversation_id, request_id, recipient_email, subject)
|
751
|
+
)
|
752
|
+
except Exception as e:
|
753
|
+
logger.error(f"Email check task error: {str(e)}", exc_info=True)
|
754
|
+
# Update request status to error
|
755
|
+
self._update_request_status_error(conversation_id, request_id, f"Email check task error: {str(e)}")
|
756
|
+
finally:
|
757
|
+
# Close event loop
|
758
|
+
loop.close()
|
759
|
+
# Remove from task dictionary
|
760
|
+
if (conversation_id, request_id) in self._mail_check_tasks:
|
761
|
+
del self._mail_check_tasks[(conversation_id, request_id)]
|
762
|
+
|
763
|
+
|
719
764
|
async def async_check_request_status(
|
720
765
|
self,
|
721
766
|
conversation_id: str,
|
@@ -876,13 +921,12 @@ class EmailProvider(BaseProvider):
|
|
876
921
|
|
877
922
|
# 存储主题与请求的映射关系
|
878
923
|
self._subject_to_request[subject] = (conversation_id, request_id)
|
879
|
-
|
880
|
-
# 创建邮件检查任务
|
881
|
-
check_task = asyncio.create_task(
|
882
|
-
self._async_check_emails(conversation_id, request_id, recipient_email, subject)
|
883
|
-
)
|
884
|
-
self._mail_check_tasks[(conversation_id, request_id)] = check_task
|
885
924
|
|
925
|
+
# 使用线程池执行邮件检查任务,而不是使用asyncio.create_task
|
926
|
+
self._mail_check_tasks[(conversation_id, request_id)] = self._executor.submit(
|
927
|
+
self._run_email_check_task, conversation_id, request_id, recipient_email, subject
|
928
|
+
)
|
929
|
+
|
886
930
|
# 如果设置了超时,创建超时任务
|
887
931
|
if timeout:
|
888
932
|
await self._async_create_timeout_task(conversation_id, request_id, timeout)
|
@@ -1,8 +1,6 @@
|
|
1
1
|
import asyncio
|
2
|
-
from
|
3
|
-
import
|
4
|
-
import json
|
5
|
-
from typing import Dict, Any, Optional, List
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
3
|
+
from typing import Dict, Any, Optional
|
6
4
|
from datetime import datetime
|
7
5
|
|
8
6
|
from gohumanloop.core.interface import (HumanLoopResult, HumanLoopStatus, HumanLoopType)
|
@@ -22,7 +20,21 @@ class TerminalProvider(BaseProvider):
|
|
22
20
|
name: Provider name
|
23
21
|
config: Configuration options, may include:
|
24
22
|
"""
|
25
|
-
super().__init__(name, config)
|
23
|
+
super().__init__(name, config)
|
24
|
+
|
25
|
+
# Store running terminal input tasks
|
26
|
+
self._terminal_input_tasks = {}
|
27
|
+
# Create thread pool for background service execution
|
28
|
+
self._executor = ThreadPoolExecutor(max_workers=10)
|
29
|
+
|
30
|
+
def __del__(self):
|
31
|
+
"""Destructor to ensure thread pool is properly closed"""
|
32
|
+
self._executor.shutdown(wait=False)
|
33
|
+
|
34
|
+
for task_key, future in list(self._terminal_input_tasks.items()):
|
35
|
+
future.cancel()
|
36
|
+
self._terminal_input_tasks.clear()
|
37
|
+
|
26
38
|
def __str__(self) -> str:
|
27
39
|
base_str = super().__str__()
|
28
40
|
terminal_info = f"- Terminal Provider: Terminal-based human-in-the-loop implementation\n"
|
@@ -72,15 +84,31 @@ class TerminalProvider(BaseProvider):
|
|
72
84
|
status=HumanLoopStatus.PENDING
|
73
85
|
)
|
74
86
|
|
75
|
-
|
76
|
-
|
77
|
-
|
87
|
+
|
88
|
+
self._terminal_input_tasks[(conversation_id, request_id)] = self._executor.submit(self._run_async_terminal_interaction, conversation_id, request_id)
|
89
|
+
|
78
90
|
# Create timeout task if timeout is specified
|
79
91
|
if timeout:
|
80
92
|
await self._async_create_timeout_task(conversation_id, request_id, timeout)
|
81
93
|
|
82
94
|
return result
|
83
|
-
|
95
|
+
|
96
|
+
|
97
|
+
def _run_async_terminal_interaction(self, conversation_id: str, request_id: str):
|
98
|
+
"""Run asynchronous terminal interaction in a separate thread"""
|
99
|
+
# Create new event loop
|
100
|
+
loop = asyncio.new_event_loop()
|
101
|
+
asyncio.set_event_loop(loop)
|
102
|
+
|
103
|
+
try:
|
104
|
+
# Run interaction processing in the new event loop
|
105
|
+
loop.run_until_complete(self._process_terminal_interaction(conversation_id, request_id))
|
106
|
+
finally:
|
107
|
+
loop.close()
|
108
|
+
# Remove from task dictionary
|
109
|
+
if (conversation_id, request_id) in self._terminal_input_tasks:
|
110
|
+
del self._terminal_input_tasks[(conversation_id, request_id)]
|
111
|
+
|
84
112
|
async def async_check_request_status(
|
85
113
|
self,
|
86
114
|
conversation_id: str,
|
@@ -175,7 +203,7 @@ class TerminalProvider(BaseProvider):
|
|
175
203
|
)
|
176
204
|
|
177
205
|
# Start async task to process user input
|
178
|
-
|
206
|
+
self._terminal_input_tasks[(conversation_id, request_id)] = self._executor.submit(self._run_async_terminal_interaction, conversation_id, request_id)
|
179
207
|
|
180
208
|
# Create timeout task if timeout is specified
|
181
209
|
if timeout:
|
@@ -299,3 +327,48 @@ class TerminalProvider(BaseProvider):
|
|
299
327
|
request_info["responded_at"] = datetime.now().isoformat()
|
300
328
|
|
301
329
|
print("\nYour response has been recorded")
|
330
|
+
|
331
|
+
async def async_cancel_request(
|
332
|
+
self,
|
333
|
+
conversation_id: str,
|
334
|
+
request_id: str
|
335
|
+
) -> bool:
|
336
|
+
"""Cancel human-in-the-loop request
|
337
|
+
|
338
|
+
Args:
|
339
|
+
conversation_id: Conversation identifier for multi-turn dialogues
|
340
|
+
request_id: Request identifier for specific interaction request
|
341
|
+
|
342
|
+
Return:
|
343
|
+
bool: Whether cancellation was successful, True indicates successful cancellation,
|
344
|
+
False indicates cancellation failed
|
345
|
+
"""
|
346
|
+
request_key = (conversation_id, request_id)
|
347
|
+
if request_key in self._terminal_input_tasks:
|
348
|
+
self._terminal_input_tasks[request_key].cancel()
|
349
|
+
del self._terminal_input_tasks[request_key]
|
350
|
+
|
351
|
+
# 调用父类方法取消请求
|
352
|
+
return await super().async_cancel_request(conversation_id, request_id)
|
353
|
+
|
354
|
+
async def async_cancel_conversation(
|
355
|
+
self,
|
356
|
+
conversation_id: str
|
357
|
+
) -> bool:
|
358
|
+
"""Cancel the entire conversation
|
359
|
+
|
360
|
+
Args:
|
361
|
+
conversation_id: Conversation identifier
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
bool: Whether cancellation was successful
|
365
|
+
"""
|
366
|
+
# 取消所有相关的邮件检查任务
|
367
|
+
for request_id in self._get_conversation_requests(conversation_id):
|
368
|
+
request_key = (conversation_id, request_id)
|
369
|
+
if request_key in self._terminal_input_tasks:
|
370
|
+
self._terminal_input_tasks[request_key].cancel()
|
371
|
+
del self._terminal_input_tasks[request_key]
|
372
|
+
|
373
|
+
# 调用父类方法取消对话
|
374
|
+
return await super().async_cancel_conversation(conversation_id)
|
gohumanloop/utils/utils.py
CHANGED
@@ -3,6 +3,9 @@ import os
|
|
3
3
|
from typing import Optional, Union
|
4
4
|
from pydantic import SecretStr
|
5
5
|
import warnings
|
6
|
+
import logging
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
6
9
|
|
7
10
|
def run_async_safely(coro):
|
8
11
|
"""
|
@@ -25,12 +28,12 @@ def run_async_safely(coro):
|
|
25
28
|
# Handle synchronous environment
|
26
29
|
try:
|
27
30
|
loop = asyncio.get_event_loop()
|
28
|
-
|
31
|
+
logger.info("Using existing event loop.")
|
29
32
|
except RuntimeError:
|
30
33
|
loop = asyncio.new_event_loop()
|
31
34
|
asyncio.set_event_loop(loop)
|
32
35
|
own_loop = True
|
33
|
-
|
36
|
+
logger.info("Created new event loop.")
|
34
37
|
else:
|
35
38
|
own_loop = False
|
36
39
|
|
@@ -1,30 +1,30 @@
|
|
1
1
|
gohumanloop/__init__.py,sha256=7_AkUtiG-_iozObldORElQS9mufxjZx_WfxuX0E5Af0,1845
|
2
2
|
gohumanloop/__main__.py,sha256=zdGKN92H9SgwZfL4xLqPkE1YaiRcHhVg_GqC-H1VurA,75
|
3
3
|
gohumanloop/adapters/__init__.py,sha256=alRiJPahmH5vIbiw7l6o3eFvEADVTkfWYIsXy5uPGSo,391
|
4
|
-
gohumanloop/adapters/langgraph_adapter.py,sha256=
|
4
|
+
gohumanloop/adapters/langgraph_adapter.py,sha256=eJLJ_LgU7ZHwm2zSwlFeDIZxjbr8JgfNZiskv0TDOGg,34355
|
5
5
|
gohumanloop/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
gohumanloop/cli/main.py,sha256=54-0nwjaAeRH2WhbyO6pN-XADPQwk4_EUUvVWDWruLc,744
|
7
7
|
gohumanloop/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
gohumanloop/core/interface.py,sha256=UjeEBKGS_JjwIsT5sBzyq6_IhUkDFrUvBXqpkxkFrAA,22696
|
9
9
|
gohumanloop/core/manager.py,sha256=MAgT5Sx1aLRBIb1mWxp-XJkQxEoibut8-TVtze8bWXQ,33068
|
10
10
|
gohumanloop/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
gohumanloop/manager/ghl_manager.py,sha256=
|
11
|
+
gohumanloop/manager/ghl_manager.py,sha256=Td54EcPg1r9Q5JfzS90QjzxiAhcMBYMigaERImLjp7M,21993
|
12
12
|
gohumanloop/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
gohumanloop/models/api_model.py,sha256=cNTXTlfI7yrTk_87Qf6ms0VtRXO2fYFJFLPTLy2dmQk,2853
|
14
14
|
gohumanloop/models/glh_model.py,sha256=Ht93iCdLfVYz_nW-uW4bE5s0UoyKG3VEx9q-Gg8_tiY,870
|
15
15
|
gohumanloop/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
gohumanloop/providers/api_provider.py,sha256=
|
17
|
-
gohumanloop/providers/base.py,sha256=
|
18
|
-
gohumanloop/providers/email_provider.py,sha256=
|
16
|
+
gohumanloop/providers/api_provider.py,sha256=MPIe_BzXkZmbY8IbLhs04zDG-u2qw0PEy28ZWZxJQAI,25604
|
17
|
+
gohumanloop/providers/base.py,sha256=en6Px3v-tPJz_zA8-cuT69YVoOyO1Av07kpgfeZoejc,22037
|
18
|
+
gohumanloop/providers/email_provider.py,sha256=L_HWkWopJF-8h0e6sOFTZzu2FanBLvCFuXOVZpDahP4,44342
|
19
19
|
gohumanloop/providers/ghl_provider.py,sha256=YdxTpRzitFhTXTbhUcMhQlPUs3kwEBd4wyXEcGK8Svk,2524
|
20
|
-
gohumanloop/providers/terminal_provider.py,sha256=
|
20
|
+
gohumanloop/providers/terminal_provider.py,sha256=L0whpmmMEzI3shV3nOFl4MrEAXEeL06OrUT0GOVzvl0,14663
|
21
21
|
gohumanloop/utils/__init__.py,sha256=idlE5ZNCELVNF9WIiyhtyzG9HJuQQCOlKeTr2aHJ2-Q,56
|
22
22
|
gohumanloop/utils/context_formatter.py,sha256=v4vdgKNJCHjnTtIMq83AkyXwltL14vx-D4KahwcZhIQ,2171
|
23
23
|
gohumanloop/utils/threadsafedict.py,sha256=0-Pmre2-lqHkUPal9wSaqh3fLaEtbo-OnJ3Wbi_knWE,9601
|
24
|
-
gohumanloop/utils/utils.py,sha256=
|
25
|
-
gohumanloop-0.0.
|
26
|
-
gohumanloop-0.0.
|
27
|
-
gohumanloop-0.0.
|
28
|
-
gohumanloop-0.0.
|
29
|
-
gohumanloop-0.0.
|
30
|
-
gohumanloop-0.0.
|
24
|
+
gohumanloop/utils/utils.py,sha256=3f53fHdWLPve-WTn9mGiz3SB0CE7l39caC5Dz0hm85U,2167
|
25
|
+
gohumanloop-0.0.4.dist-info/licenses/LICENSE,sha256=-U5tuCcSpndQwSKWtZbFbazb-_AtZcZL2kQgHbSLg-M,1064
|
26
|
+
gohumanloop-0.0.4.dist-info/METADATA,sha256=mM0-czwhVde-9FdxCAsEgeuXhGxW6f3OxeaTc9sIjNA,1557
|
27
|
+
gohumanloop-0.0.4.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
28
|
+
gohumanloop-0.0.4.dist-info/entry_points.txt,sha256=wM6jqRRD8bQXkvIduRVCuAJIlbyWg_F5EDXo5OZ_PwY,88
|
29
|
+
gohumanloop-0.0.4.dist-info/top_level.txt,sha256=LvOXBqS6Mspmcuqp81uz0Vjx_m_YI0w06DOPCiI1BfY,12
|
30
|
+
gohumanloop-0.0.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|