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.
Files changed (35) hide show
  1. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/PKG-INFO +1 -1
  2. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/__init__.py +11 -3
  3. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/adapters/langgraph_adapter.py +38 -19
  4. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/core/manager.py +24 -111
  5. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/manager/ghl_manager.py +2 -2
  6. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/api_provider.py +34 -8
  7. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/base.py +29 -98
  8. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/email_provider.py +53 -9
  9. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/terminal_provider.py +83 -10
  10. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/utils.py +5 -2
  11. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/PKG-INFO +1 -1
  12. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/pyproject.toml +1 -1
  13. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/LICENSE +0 -0
  14. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/README.md +0 -0
  15. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/__main__.py +0 -0
  16. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/adapters/__init__.py +0 -0
  17. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/cli/__init__.py +0 -0
  18. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/cli/main.py +0 -0
  19. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/core/__init__.py +0 -0
  20. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/core/interface.py +0 -0
  21. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/manager/__init__.py +0 -0
  22. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/models/__init__.py +0 -0
  23. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/models/api_model.py +0 -0
  24. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/models/glh_model.py +0 -0
  25. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/__init__.py +0 -0
  26. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/providers/ghl_provider.py +0 -0
  27. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/__init__.py +0 -0
  28. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/context_formatter.py +0 -0
  29. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop/utils/threadsafedict.py +0 -0
  30. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/SOURCES.txt +0 -0
  31. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/dependency_links.txt +0 -0
  32. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/entry_points.txt +0 -0
  33. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/requires.txt +0 -0
  34. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/gohumanloop.egg-info/top_level.txt +0 -0
  35. {gohumanloop-0.0.3 → gohumanloop-0.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gohumanloop
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Perfecting AI workflows with human intelligence
5
5
  Author-email: gohumanloop authors <baird0917@163.com>
6
6
  Project-URL: repository, https://github.com/ptonlix/gohumanloop
@@ -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 async_humanloop_on_error(
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
- import logging
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
- # Get current event loop or create new one
736
- lg_humanloop.manager.request_humanloop(
737
- task_id="lg_interrupt",
738
- conversation_id=default_conversation_id,
739
- loop_type=HumanLoopType.INFORMATION,
740
- context={
741
- "message": f"{value}",
742
- "question": "The execution has been interrupted. Please review the above information and provide your input to continue.",
743
- },
744
- blocking=False,
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
- # Wait for async result synchronously
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
- loop = asyncio.get_event_loop()
154
- if loop.is_running():
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
- finally:
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
- loop = asyncio.get_event_loop()
271
- if loop.is_running():
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
- loop = asyncio.get_event_loop()
338
- if loop.is_running():
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
- loop = asyncio.get_event_loop()
398
- if loop.is_running():
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
- loop = asyncio.get_event_loop()
460
- if loop.is_running():
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
- loop = asyncio.get_event_loop()
551
- if loop.is_running():
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
- loop = asyncio.get_event_loop()
586
- if loop.is_running():
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
- loop = asyncio.get_event_loop()
608
- if loop.is_running():
609
- # 如果事件循环已经在运行,创建一个新的事件循环
610
- new_loop = asyncio.new_event_loop()
611
- asyncio.set_event_loop(new_loop)
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
- loop = asyncio.get_event_loop()
639
- if loop.is_running():
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
- # 如果没有指定provider_id,从存储的映射中获取
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.check_request_status(conversation_id, request_id)
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
- poll_task = asyncio.create_task(
234
- self._async_poll_request_status(conversation_id, request_id, platform)
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
- # Create polling task
520
- poll_task = asyncio.create_task(
521
- self._async_poll_request_status(conversation_id, request_id, platform)
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
- loop = asyncio.get_event_loop()
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
- finally:
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
- loop = asyncio.get_event_loop()
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
- finally:
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
- loop = asyncio.get_event_loop()
349
- if loop.is_running():
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
- finally:
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
- loop = asyncio.get_event_loop()
414
- if loop.is_running():
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
- finally:
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
- loop = asyncio.get_event_loop()
483
- if loop.is_running():
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
- finally:
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
- check_task = asyncio.create_task(
704
- self._async_check_emails(conversation_id, request_id, recipient_email, subject)
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
- self._mail_check_tasks[(conversation_id, request_id)] = check_task
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 email import message
3
- import sys
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
- # Start async task to process user input
76
- asyncio.create_task(self._process_terminal_interaction(conversation_id, request_id))
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
- asyncio.create_task(self._process_terminal_interaction(conversation_id, request_id))
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
- print("Using existing event loop.")
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
- print("Created new event loop.")
36
+ logger.info("Created new event loop.")
34
37
  else:
35
38
  own_loop = False
36
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gohumanloop
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Perfecting AI workflows with human intelligence
5
5
  Author-email: gohumanloop authors <baird0917@163.com>
6
6
  Project-URL: repository, https://github.com/ptonlix/gohumanloop
@@ -3,7 +3,7 @@ authors = [
3
3
  {name = "gohumanloop authors", email = "baird0917@163.com"},
4
4
  ]
5
5
  name = "gohumanloop"
6
- version = "0.0.3"
6
+ version = "0.0.5"
7
7
  description = "Perfecting AI workflows with human intelligence"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
File without changes
File without changes
File without changes