gohumanloop 0.0.3__tar.gz → 0.0.5__tar.gz
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-0.0.3 → gohumanloop-0.0.5}/PKG-INFO +1 -1
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/__init__.py +11 -3
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/adapters/langgraph_adapter.py +38 -19
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/core/manager.py +24 -111
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/manager/ghl_manager.py +2 -2
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/api_provider.py +34 -8
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/base.py +29 -98
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/email_provider.py +53 -9
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/terminal_provider.py +83 -10
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/utils.py +5 -2
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/PKG-INFO +1 -1
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/pyproject.toml +1 -1
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/LICENSE +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/README.md +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/__main__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/adapters/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/cli/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/cli/main.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/core/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/core/interface.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/manager/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/models/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/models/api_model.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/models/glh_model.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/ghl_provider.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/__init__.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/context_formatter.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/threadsafedict.py +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/SOURCES.txt +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/dependency_links.txt +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/entry_points.txt +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/requires.txt +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/top_level.txt +0 -0
- {gohumanloop-0.0.3 → gohumanloop-0.0.5}/setup.cfg +0 -0
@@ -12,12 +12,18 @@ from gohumanloop.manager.ghl_manager import GoHumanLoopManager
|
|
12
12
|
|
13
13
|
from gohumanloop.providers.ghl_provider import GoHumanLoopProvider
|
14
14
|
from gohumanloop.providers.api_provider import APIProvider
|
15
|
-
from gohumanloop.providers.email_provider import EmailProvider
|
16
15
|
from gohumanloop.providers.base import BaseProvider
|
17
16
|
from gohumanloop.providers.terminal_provider import TerminalProvider
|
18
17
|
|
19
18
|
from gohumanloop.utils import run_async_safely, get_secret_from_env
|
20
19
|
|
20
|
+
# Conditionally import EmailProvider
|
21
|
+
try:
|
22
|
+
from gohumanloop.providers.email_provider import EmailProvider
|
23
|
+
_has_email = True
|
24
|
+
except ImportError:
|
25
|
+
_has_email = False
|
26
|
+
|
21
27
|
# Dynamically get version number
|
22
28
|
try:
|
23
29
|
from importlib.metadata import version, PackageNotFoundError
|
@@ -53,7 +59,6 @@ __all__ = [
|
|
53
59
|
"BaseProvider",
|
54
60
|
"APIProvider",
|
55
61
|
"GoHumanLoopProvider",
|
56
|
-
"EmailProvider",
|
57
62
|
"TerminalProvider",
|
58
63
|
|
59
64
|
# Utility Functions
|
@@ -62,4 +67,7 @@ __all__ = [
|
|
62
67
|
|
63
68
|
# Version Information
|
64
69
|
"__version__",
|
65
|
-
]
|
70
|
+
]
|
71
|
+
|
72
|
+
if _has_email:
|
73
|
+
__all__.append("EmailProvider")
|
@@ -5,12 +5,15 @@ import uuid
|
|
5
5
|
import time
|
6
6
|
from inspect import iscoroutinefunction
|
7
7
|
from contextlib import asynccontextmanager, contextmanager
|
8
|
+
import logging
|
8
9
|
|
9
10
|
from gohumanloop.utils import run_async_safely
|
10
11
|
from gohumanloop.core.interface import (
|
11
12
|
HumanLoopManager, HumanLoopResult, HumanLoopStatus, HumanLoopType, HumanLoopCallback, HumanLoopProvider
|
12
13
|
)
|
13
14
|
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
14
17
|
# Define TypeVars for input and output types
|
15
18
|
T = TypeVar("T")
|
16
19
|
R = TypeVar('R')
|
@@ -633,7 +636,7 @@ class LangGraphHumanLoopCallback(HumanLoopCallback):
|
|
633
636
|
if self.async_on_timeout:
|
634
637
|
await self.async_on_timeout(self.state, provider)
|
635
638
|
|
636
|
-
async def
|
639
|
+
async def async_on_humanloop_error(
|
637
640
|
self,
|
638
641
|
provider: HumanLoopProvider,
|
639
642
|
error: Exception
|
@@ -658,9 +661,7 @@ def default_langgraph_callback_factory(state: Any) -> LangGraphHumanLoopCallback
|
|
658
661
|
Returns:
|
659
662
|
Configured LangGraphHumanLoopCallback instance
|
660
663
|
"""
|
661
|
-
|
662
|
-
|
663
|
-
logger = logging.getLogger("gohumanloop.langgraph")
|
664
|
+
|
664
665
|
|
665
666
|
async def async_on_update(state, provider: HumanLoopProvider, result: HumanLoopResult):
|
666
667
|
"""Log human interaction update events"""
|
@@ -713,6 +714,8 @@ default_adapter = LangGraphAdapter(manager, default_timeout=60)
|
|
713
714
|
|
714
715
|
default_conversation_id = str(uuid.uuid4())
|
715
716
|
|
717
|
+
_SKIP_NEXT_HUMANLOOP = False
|
718
|
+
|
716
719
|
def interrupt(value: Any, lg_humanloop: LangGraphAdapter = default_adapter) -> Any:
|
717
720
|
"""
|
718
721
|
Wraps LangGraph's interrupt functionality to pause graph execution and wait for human input
|
@@ -726,23 +729,34 @@ def interrupt(value: Any, lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
726
729
|
Returns:
|
727
730
|
Input value provided by human user
|
728
731
|
"""
|
732
|
+
|
733
|
+
global _SKIP_NEXT_HUMANLOOP
|
734
|
+
|
729
735
|
if not _SUPPORTS_INTERRUPT:
|
730
736
|
raise RuntimeError(
|
731
737
|
"LangGraph version too low, interrupt not supported. Please upgrade to version 0.2.57 or higher."
|
732
738
|
"You can use: pip install --upgrade langgraph>=0.2.57"
|
733
739
|
)
|
734
740
|
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
741
|
+
if not _SKIP_NEXT_HUMANLOOP:
|
742
|
+
# Get current event loop or create new one
|
743
|
+
try:
|
744
|
+
lg_humanloop.manager.request_humanloop(
|
745
|
+
task_id="lg_interrupt",
|
746
|
+
conversation_id=default_conversation_id,
|
747
|
+
loop_type=HumanLoopType.INFORMATION,
|
748
|
+
context={
|
749
|
+
"message": f"{value}",
|
750
|
+
"question": "The execution has been interrupted. Please review the above information and provide your input to continue.",
|
751
|
+
},
|
752
|
+
blocking=False,
|
753
|
+
)
|
754
|
+
except Exception as e:
|
755
|
+
logger.exception(f"Error in interrupt: {e}")
|
756
|
+
else:
|
757
|
+
# Reset flag to allow normal human intervention trigger next time
|
758
|
+
_SKIP_NEXT_HUMANLOOP = False
|
759
|
+
|
746
760
|
|
747
761
|
# Return LangGraph's interrupt
|
748
762
|
return _lg_interrupt(value)
|
@@ -758,6 +772,9 @@ def create_resume_command(lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
758
772
|
Returns:
|
759
773
|
Command object that can be used with graph.stream method
|
760
774
|
"""
|
775
|
+
|
776
|
+
global _SKIP_NEXT_HUMANLOOP
|
777
|
+
|
761
778
|
if not _SUPPORTS_INTERRUPT:
|
762
779
|
raise RuntimeError(
|
763
780
|
"LangGraph version too low, Command feature not supported. Please upgrade to 0.2.57 or higher."
|
@@ -769,15 +786,13 @@ def create_resume_command(lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
769
786
|
poll_interval = 1.0 # Polling interval (seconds)
|
770
787
|
while True:
|
771
788
|
result = lg_humanloop.manager.check_conversation_status(default_conversation_id)
|
772
|
-
print(result)
|
773
789
|
# If status is final state (not PENDING), return result
|
774
790
|
if result.status != HumanLoopStatus.PENDING:
|
775
791
|
return result.response
|
776
792
|
# Wait before polling again
|
777
793
|
time.sleep(poll_interval)
|
778
|
-
|
779
|
-
|
780
|
-
# loop = asyncio.get_event_loop() # In synchronous environment
|
794
|
+
|
795
|
+
_SKIP_NEXT_HUMANLOOP = True
|
781
796
|
|
782
797
|
response = poll_for_result()
|
783
798
|
return _lg_Command(resume=response)
|
@@ -794,6 +809,8 @@ async def acreate_resume_command(lg_humanloop: LangGraphAdapter = default_adapte
|
|
794
809
|
Returns:
|
795
810
|
Command object that can be used with graph.astream method
|
796
811
|
"""
|
812
|
+
global _SKIP_NEXT_HUMANLOOP
|
813
|
+
|
797
814
|
if not _SUPPORTS_INTERRUPT:
|
798
815
|
raise RuntimeError(
|
799
816
|
"LangGraph version too low, Command feature not supported. Please upgrade to 0.2.57 or higher."
|
@@ -811,6 +828,8 @@ async def acreate_resume_command(lg_humanloop: LangGraphAdapter = default_adapte
|
|
811
828
|
# Wait before polling again
|
812
829
|
await asyncio.sleep(poll_interval)
|
813
830
|
|
831
|
+
_SKIP_NEXT_HUMANLOOP = True
|
832
|
+
|
814
833
|
# Wait for async result directly
|
815
834
|
response = await poll_for_result()
|
816
835
|
return _lg_Command(resume=response)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import Dict, Any, Optional, List, Union, Set
|
2
2
|
import asyncio
|
3
3
|
import time
|
4
|
+
from gohumanloop.utils import run_async_safely
|
4
5
|
|
5
6
|
from gohumanloop.core.interface import (
|
6
7
|
HumanLoopManager, HumanLoopProvider, HumanLoopCallback,
|
@@ -150,15 +151,8 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
150
151
|
blocking: bool = False,
|
151
152
|
) -> Union[str, HumanLoopResult]:
|
152
153
|
"""请求人机循环(同步版本)"""
|
153
|
-
|
154
|
-
|
155
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
156
|
-
new_loop = asyncio.new_event_loop()
|
157
|
-
asyncio.set_event_loop(new_loop)
|
158
|
-
loop = new_loop
|
159
|
-
|
160
|
-
try:
|
161
|
-
return loop.run_until_complete(
|
154
|
+
|
155
|
+
return run_async_safely(
|
162
156
|
self.async_request_humanloop(
|
163
157
|
task_id=task_id,
|
164
158
|
conversation_id=conversation_id,
|
@@ -171,9 +165,7 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
171
165
|
blocking=blocking
|
172
166
|
)
|
173
167
|
)
|
174
|
-
|
175
|
-
if loop != asyncio.get_event_loop():
|
176
|
-
loop.close()
|
168
|
+
|
177
169
|
|
178
170
|
async def async_continue_humanloop(
|
179
171
|
self,
|
@@ -267,15 +259,8 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
267
259
|
blocking: bool = False,
|
268
260
|
) -> Union[str, HumanLoopResult]:
|
269
261
|
"""继续人机循环(同步版本)"""
|
270
|
-
|
271
|
-
|
272
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
273
|
-
new_loop = asyncio.new_event_loop()
|
274
|
-
asyncio.set_event_loop(new_loop)
|
275
|
-
loop = new_loop
|
276
|
-
|
277
|
-
try:
|
278
|
-
return loop.run_until_complete(
|
262
|
+
|
263
|
+
return run_async_safely(
|
279
264
|
self.async_continue_humanloop(
|
280
265
|
conversation_id=conversation_id,
|
281
266
|
context=context,
|
@@ -286,9 +271,6 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
286
271
|
blocking=blocking
|
287
272
|
)
|
288
273
|
)
|
289
|
-
finally:
|
290
|
-
if loop != asyncio.get_event_loop():
|
291
|
-
loop.close()
|
292
274
|
|
293
275
|
async def async_check_request_status(
|
294
276
|
self,
|
@@ -334,24 +316,14 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
334
316
|
provider_id: Optional[str] = None
|
335
317
|
) -> HumanLoopResult:
|
336
318
|
"""检查请求状态(同步版本)"""
|
337
|
-
|
338
|
-
|
339
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
340
|
-
new_loop = asyncio.new_event_loop()
|
341
|
-
asyncio.set_event_loop(new_loop)
|
342
|
-
loop = new_loop
|
343
|
-
|
344
|
-
try:
|
345
|
-
return loop.run_until_complete(
|
319
|
+
|
320
|
+
return run_async_safely(
|
346
321
|
self.async_check_request_status(
|
347
322
|
conversation_id=conversation_id,
|
348
323
|
request_id=request_id,
|
349
324
|
provider_id=provider_id
|
350
325
|
)
|
351
326
|
)
|
352
|
-
finally:
|
353
|
-
if loop != asyncio.get_event_loop():
|
354
|
-
loop.close()
|
355
327
|
|
356
328
|
|
357
329
|
async def async_check_conversation_status(
|
@@ -394,23 +366,13 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
394
366
|
provider_id: Optional[str] = None
|
395
367
|
) -> HumanLoopResult:
|
396
368
|
"""检查对话状态(同步版本)"""
|
397
|
-
|
398
|
-
|
399
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
400
|
-
new_loop = asyncio.new_event_loop()
|
401
|
-
asyncio.set_event_loop(new_loop)
|
402
|
-
loop = new_loop
|
403
|
-
|
404
|
-
try:
|
405
|
-
return loop.run_until_complete(
|
369
|
+
|
370
|
+
return run_async_safely(
|
406
371
|
self.async_check_conversation_status(
|
407
372
|
conversation_id=conversation_id,
|
408
373
|
provider_id=provider_id
|
409
374
|
)
|
410
375
|
)
|
411
|
-
finally:
|
412
|
-
if loop != asyncio.get_event_loop():
|
413
|
-
loop.close()
|
414
376
|
|
415
377
|
async def async_cancel_request(
|
416
378
|
self,
|
@@ -456,25 +418,14 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
456
418
|
provider_id: Optional[str] = None
|
457
419
|
) -> bool:
|
458
420
|
"""取消特定请求(同步版本)"""
|
459
|
-
|
460
|
-
|
461
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
462
|
-
new_loop = asyncio.new_event_loop()
|
463
|
-
asyncio.set_event_loop(new_loop)
|
464
|
-
loop = new_loop
|
465
|
-
|
466
|
-
try:
|
467
|
-
return loop.run_until_complete(
|
421
|
+
|
422
|
+
return run_async_safely(
|
468
423
|
self.async_cancel_request(
|
469
424
|
conversation_id=conversation_id,
|
470
425
|
request_id=request_id,
|
471
426
|
provider_id=provider_id
|
472
427
|
)
|
473
428
|
)
|
474
|
-
finally:
|
475
|
-
if loop != asyncio.get_event_loop():
|
476
|
-
loop.close()
|
477
|
-
|
478
429
|
|
479
430
|
async def async_cancel_conversation(
|
480
431
|
self,
|
@@ -547,25 +498,14 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
547
498
|
provider_id: Optional[str] = None
|
548
499
|
) -> bool:
|
549
500
|
"""取消整个对话(同步版本)"""
|
550
|
-
|
551
|
-
|
552
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
553
|
-
new_loop = asyncio.new_event_loop()
|
554
|
-
asyncio.set_event_loop(new_loop)
|
555
|
-
loop = new_loop
|
556
|
-
|
557
|
-
try:
|
558
|
-
return loop.run_until_complete(
|
501
|
+
|
502
|
+
return run_async_safely(
|
559
503
|
self.async_cancel_conversation(
|
560
504
|
conversation_id=conversation_id,
|
561
505
|
provider_id=provider_id
|
562
506
|
)
|
563
507
|
)
|
564
|
-
finally:
|
565
|
-
if loop != asyncio.get_event_loop():
|
566
|
-
loop.close()
|
567
508
|
|
568
|
-
|
569
509
|
async def async_get_provider(
|
570
510
|
self,
|
571
511
|
provider_id: Optional[str] = None
|
@@ -582,20 +522,10 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
582
522
|
provider_id: Optional[str] = None
|
583
523
|
) -> HumanLoopProvider:
|
584
524
|
"""获取指定的提供者实例(同步版本)"""
|
585
|
-
|
586
|
-
|
587
|
-
# 如果事件循环已经在运行,创建一个新的事件循环
|
588
|
-
new_loop = asyncio.new_event_loop()
|
589
|
-
asyncio.set_event_loop(new_loop)
|
590
|
-
loop = new_loop
|
591
|
-
|
592
|
-
try:
|
593
|
-
return loop.run_until_complete(
|
525
|
+
|
526
|
+
return run_async_safely(
|
594
527
|
self.async_get_provider(provider_id=provider_id)
|
595
528
|
)
|
596
|
-
finally:
|
597
|
-
if loop != asyncio.get_event_loop():
|
598
|
-
loop.close()
|
599
529
|
|
600
530
|
async def async_list_providers(self) -> Dict[str, HumanLoopProvider]:
|
601
531
|
"""列出所有注册的提供者"""
|
@@ -604,18 +534,11 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
604
534
|
|
605
535
|
def list_providers(self) -> Dict[str, HumanLoopProvider]:
|
606
536
|
"""列出所有注册的提供者(同步版本)"""
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
loop = new_loop
|
613
|
-
|
614
|
-
try:
|
615
|
-
return loop.run_until_complete(self.async_list_providers())
|
616
|
-
finally:
|
617
|
-
if loop != asyncio.get_event_loop():
|
618
|
-
loop.close()
|
537
|
+
|
538
|
+
return run_async_safely(
|
539
|
+
self.async_list_providers()
|
540
|
+
)
|
541
|
+
|
619
542
|
|
620
543
|
|
621
544
|
async def async_set_default_provider(
|
@@ -635,21 +558,11 @@ class DefaultHumanLoopManager(HumanLoopManager):
|
|
635
558
|
provider_id: str
|
636
559
|
) -> bool:
|
637
560
|
"""设置默认提供者(同步版本)"""
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
new_loop = asyncio.new_event_loop()
|
642
|
-
asyncio.set_event_loop(new_loop)
|
643
|
-
loop = new_loop
|
644
|
-
|
645
|
-
try:
|
646
|
-
return loop.run_until_complete(
|
561
|
+
|
562
|
+
|
563
|
+
return run_async_safely(
|
647
564
|
self.async_set_default_provider(provider_id=provider_id)
|
648
565
|
)
|
649
|
-
finally:
|
650
|
-
if loop != asyncio.get_event_loop():
|
651
|
-
loop.close()
|
652
|
-
|
653
566
|
|
654
567
|
async def _async_create_timeout_task(
|
655
568
|
self,
|
@@ -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:
|
@@ -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)
|
@@ -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
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|