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.
@@ -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 async_humanloop_on_error(
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
- # 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
- )
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
- # Wait for async result synchronously
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
- # 如果没有指定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.4
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
@@ -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=7w3ek8Yzr1r0tfJ4A5UNb_pcFRTfNVDc5gWNVq56IH0,34099
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=m7KVdjd5bpxPNm2Sxk4LySzx_Ll4OfpJygxrfFhToDA,22026
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=ooZQfi4Zu27U8kyTJ9rssAVzAVoyga71xT8nUjdQJmE,24517
17
- gohumanloop/providers/base.py,sha256=Eq2y4fqJXdDfbA4xkyHBBH7opGeOg8PAte5vAbx2lwI,24623
18
- gohumanloop/providers/email_provider.py,sha256=G04557UxdyBnmCx4WTXiakElELRv1fHDYTYNbvzDtqw,42562
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=HBR6fLftaDRydueI5wxKtVOrgYF5RcPxzIhmLPrNVzo,11720
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=iwfIAYuuKSyuEpOUv4ftf7zRDqSvJ6ALgqceHaZNeO4,2102
25
- gohumanloop-0.0.3.dist-info/licenses/LICENSE,sha256=-U5tuCcSpndQwSKWtZbFbazb-_AtZcZL2kQgHbSLg-M,1064
26
- gohumanloop-0.0.3.dist-info/METADATA,sha256=e7exfCd8QVCiMbcAjFwgnAbqoziS1WMUnYh7GL7eZPc,1557
27
- gohumanloop-0.0.3.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
28
- gohumanloop-0.0.3.dist-info/entry_points.txt,sha256=wM6jqRRD8bQXkvIduRVCuAJIlbyWg_F5EDXo5OZ_PwY,88
29
- gohumanloop-0.0.3.dist-info/top_level.txt,sha256=LvOXBqS6Mspmcuqp81uz0Vjx_m_YI0w06DOPCiI1BfY,12
30
- gohumanloop-0.0.3.dist-info/RECORD,,
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,,