gohumanloop 0.0.9__tar.gz → 0.0.11__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 (38) hide show
  1. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/PKG-INFO +2 -2
  2. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/README.md +1 -1
  3. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/adapters/base_adapter.py +42 -16
  4. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/core/manager.py +21 -58
  5. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/api_provider.py +51 -14
  6. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/base.py +0 -52
  7. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/email_provider.py +53 -16
  8. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/terminal_provider.py +29 -12
  9. gohumanloop-0.0.11/gohumanloop/utils/__init__.py +5 -0
  10. gohumanloop-0.0.11/gohumanloop/utils/context_formatter.py +136 -0
  11. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/utils/utils.py +1 -0
  12. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/PKG-INFO +2 -2
  13. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/pyproject.toml +1 -1
  14. gohumanloop-0.0.9/gohumanloop/utils/__init__.py +0 -7
  15. gohumanloop-0.0.9/gohumanloop/utils/context_formatter.py +0 -64
  16. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/LICENSE +0 -0
  17. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/__init__.py +0 -0
  18. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/__main__.py +0 -0
  19. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/adapters/__init__.py +0 -0
  20. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/adapters/langgraph_adapter.py +0 -0
  21. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/cli/__init__.py +0 -0
  22. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/cli/main.py +0 -0
  23. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/core/__init__.py +0 -0
  24. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/core/interface.py +0 -0
  25. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/manager/__init__.py +0 -0
  26. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/manager/ghl_manager.py +0 -0
  27. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/models/__init__.py +0 -0
  28. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/models/api_model.py +0 -0
  29. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/models/glh_model.py +0 -0
  30. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/__init__.py +0 -0
  31. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/ghl_provider.py +0 -0
  32. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/utils/threadsafedict.py +0 -0
  33. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/SOURCES.txt +0 -0
  34. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/dependency_links.txt +0 -0
  35. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/entry_points.txt +0 -0
  36. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/requires.txt +0 -0
  37. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/top_level.txt +0 -0
  38. {gohumanloop-0.0.9 → gohumanloop-0.0.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gohumanloop
3
- Version: 0.0.9
3
+ Version: 0.0.11
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
@@ -255,7 +255,7 @@ Through `GoHumanloop`'s encapsulation, you can implement secure and efficient `H
255
255
  ## 📚 Key Features
256
256
 
257
257
  <div align="center">
258
- <img height=360 src="http://cdn.oyster-iot.cloud/202505211030197.png"><br>
258
+ <img height=360 src="http://cdn.oyster-iot.cloud/202505291027894.png"><br>
259
259
  <b face="雅黑"> GoHumanLoop Architecture</b>
260
260
  </div>
261
261
  <br>
@@ -230,7 +230,7 @@ Through `GoHumanloop`'s encapsulation, you can implement secure and efficient `H
230
230
  ## 📚 Key Features
231
231
 
232
232
  <div align="center">
233
- <img height=360 src="http://cdn.oyster-iot.cloud/202505211030197.png"><br>
233
+ <img height=360 src="http://cdn.oyster-iot.cloud/202505291027894.png"><br>
234
234
  <b face="雅黑"> GoHumanLoop Architecture</b>
235
235
  </div>
236
236
  <br>
@@ -19,7 +19,7 @@ from inspect import iscoroutinefunction
19
19
  from contextlib import asynccontextmanager, contextmanager
20
20
  import logging
21
21
 
22
- from gohumanloop.utils import run_async_safely
22
+ from gohumanloop.utils import run_async_safely, generate_function_summary
23
23
  from gohumanloop.core.interface import (
24
24
  HumanLoopRequest,
25
25
  HumanLoopResult,
@@ -241,6 +241,12 @@ class HumanloopAdapter:
241
241
 
242
242
  @wraps(fn)
243
243
  async def async_wrapper(*args: Any, **kwargs: Any) -> R:
244
+ # Check if ret_key exists in function parameters
245
+ if ret_key not in fn.__code__.co_varnames:
246
+ raise ValueError(
247
+ f"Function {fn.__name__} must have parameter named {ret_key}"
248
+ )
249
+
244
250
  # Determine if callback is instance or factory function
245
251
  cb = None
246
252
  if callable(callback) and not isinstance(callback, HumanLoopCallback):
@@ -255,7 +261,8 @@ class HumanloopAdapter:
255
261
  conversation_id=conversation_id,
256
262
  loop_type=HumanLoopType.APPROVAL,
257
263
  context={
258
- "message": {
264
+ "message": generate_function_summary(fn, args, kwargs),
265
+ "function": {
259
266
  "function_name": fn.__name__,
260
267
  "function_signature": str(fn.__code__.co_varnames),
261
268
  "arguments": str(args),
@@ -289,7 +296,9 @@ class HumanloopAdapter:
289
296
  "error": result.error,
290
297
  }
291
298
 
292
- kwargs[ret_key] = approval_info
299
+ # Inject approval info into kwargs
300
+ kwargs[ret_key] = approval_info
301
+
293
302
  # Check approval result
294
303
  if isinstance(result, HumanLoopResult):
295
304
  # Handle based on approval status
@@ -426,6 +435,12 @@ class HumanloopAdapter:
426
435
 
427
436
  @wraps(fn)
428
437
  async def async_wrapper(*args: Any, **kwargs: Any) -> R:
438
+ # Check if ret_key exists in function parameters
439
+ if ret_key not in fn.__code__.co_varnames:
440
+ raise ValueError(
441
+ f"Function {fn.__name__} must have parameter named {ret_key}"
442
+ )
443
+
429
444
  # Determine if callback is instance or factory function
430
445
  cb = None
431
446
  state = args[0] if args else None
@@ -434,11 +449,17 @@ class HumanloopAdapter:
434
449
  else:
435
450
  cb = callback
436
451
 
437
- node_input = None
438
- if state:
439
- # Get input information from key fields in State
452
+ # First try to get node_input from kwargs using state_key
453
+ node_input = kwargs.get(state_key)
454
+
455
+ # If not found in kwargs, try to get from first argument (state)
456
+ if node_input is None and state and isinstance(state, dict):
440
457
  node_input = state.get(state_key, {})
441
458
 
459
+ # If still not found, use empty dict as default
460
+ if node_input is None:
461
+ node_input = {}
462
+
442
463
  # Compose question content
443
464
  question_content = (
444
465
  f"Please respond to the following information:\n{node_input}"
@@ -510,8 +531,8 @@ class HumanloopAdapter:
510
531
  "responded_at": result.responded_at,
511
532
  "error": result.error,
512
533
  }
513
-
514
- kwargs[ret_key] = conversation_info
534
+ # Inject conversation info into kwargs
535
+ kwargs[ret_key] = conversation_info
515
536
 
516
537
  if isinstance(result, HumanLoopResult):
517
538
  if iscoroutinefunction(fn):
@@ -625,6 +646,12 @@ class HumanloopAdapter:
625
646
 
626
647
  @wraps(fn)
627
648
  async def async_wrapper(*args: Any, **kwargs: Any) -> R:
649
+ # Check if ret_key exists in function parameters
650
+ if ret_key not in fn.__code__.co_varnames:
651
+ raise ValueError(
652
+ f"Function {fn.__name__} must have parameter named {ret_key}"
653
+ )
654
+
628
655
  # Determine if callback is an instance or factory function
629
656
  # callback: can be HumanLoopCallback instance or factory function
630
657
  # - If factory function: accepts state parameter and returns HumanLoopCallback instance
@@ -659,12 +686,11 @@ class HumanloopAdapter:
659
686
  provider_id=provider_id,
660
687
  blocking=True,
661
688
  )
662
-
663
- # 初始化审批结果对象为None
689
+ # Initialize response info object as None
664
690
  resp_info = None
665
691
 
666
692
  if isinstance(result, HumanLoopResult):
667
- # 如果结果是HumanLoopResult类型,则构建完整的审批信息
693
+ # If result is HumanLoopResult type, build complete response info
668
694
  resp_info = {
669
695
  "conversation_id": result.conversation_id,
670
696
  "request_id": result.request_id,
@@ -676,12 +702,12 @@ class HumanloopAdapter:
676
702
  "responded_at": result.responded_at,
677
703
  "error": result.error,
678
704
  }
705
+ # Inject approval info into kwargs
706
+ kwargs[ret_key] = resp_info
679
707
 
680
- kwargs[ret_key] = resp_info
681
-
682
- # 检查结果是否有效
708
+ # Check if result is valid
683
709
  if isinstance(result, HumanLoopResult):
684
- # 返回获取信息结果,由用户去判断是否使用
710
+ # Return the information result, let user decide whether to use it
685
711
  if iscoroutinefunction(fn):
686
712
  ret = await fn(*args, **kwargs)
687
713
  else:
@@ -695,7 +721,7 @@ class HumanloopAdapter:
695
721
  ret = run_async_safely(async_wrapper(*args, **kwargs))
696
722
  return cast(R, ret)
697
723
 
698
- # 根据被装饰函数类型返回对应的wrapper
724
+ # Return corresponding wrapper based on decorated function type
699
725
  if iscoroutinefunction(fn):
700
726
  return async_wrapper
701
727
  return sync_wrapper
@@ -170,12 +170,6 @@ class DefaultHumanLoopManager(HumanLoopManager):
170
170
  pass
171
171
  self._callbacks[(conversation_id, request_id)] = callback
172
172
 
173
- # 如果设置了超时,创建超时任务
174
- if timeout:
175
- await self._async_create_timeout_task(
176
- conversation_id, request_id, timeout, provider, callback
177
- )
178
-
179
173
  # 如果是阻塞模式,等待结果
180
174
  if blocking:
181
175
  return await self._async_wait_for_result(
@@ -308,12 +302,6 @@ class DefaultHumanLoopManager(HumanLoopManager):
308
302
  pass
309
303
  self._callbacks[(conversation_id, request_id)] = callback
310
304
 
311
- # 如果设置了超时,创建超时任务
312
- if timeout:
313
- await self._async_create_timeout_task(
314
- conversation_id, request_id, timeout, provider, callback
315
- )
316
-
317
305
  # 如果是阻塞模式,等待结果
318
306
  if blocking:
319
307
  return await self._async_wait_for_result(
@@ -429,22 +417,31 @@ class DefaultHumanLoopManager(HumanLoopManager):
429
417
 
430
418
  try:
431
419
  # 检查对话指定provider_id或默认provider_id最后一次请求的状态
432
- return await provider.async_check_conversation_status(conversation_id)
433
- except Exception as e:
434
- # 处理检查对话状态过程中的异常
435
- # 尝试找到与此对话关联的最后一个请求的回调
420
+ result = await provider.async_check_conversation_status(conversation_id)
421
+
436
422
  if (
437
423
  conversation_id in self._conversation_requests
438
424
  and self._conversation_requests[conversation_id]
439
425
  ):
440
426
  last_request_id = self._conversation_requests[conversation_id][-1]
441
427
  callback = self._callbacks.get((conversation_id, last_request_id))
442
- if callback:
443
- try:
444
- await callback.async_on_humanloop_error(provider, e)
445
- except Exception:
446
- # 如果错误回调也失败,只能忽略
447
- pass
428
+
429
+ if callback and result.status not in [HumanLoopStatus.PENDING]:
430
+ # 如果有回调且状态不是等待或进行中,触发状态更新回调
431
+ await self._async_trigger_update_callback(
432
+ conversation_id, last_request_id, provider, result
433
+ )
434
+
435
+ return result
436
+ except Exception as e:
437
+ # 处理检查对话状态过程中的异常
438
+ # 尝试找到与此对话关联的最后一个请求的回调
439
+ if callback:
440
+ try:
441
+ await callback.async_on_humanloop_error(provider, e)
442
+ except Exception:
443
+ # 如果错误回调也失败,只能忽略
444
+ pass
448
445
  raise # 重新抛出异常,让调用者知道发生了错误
449
446
 
450
447
  def check_conversation_status(
@@ -632,42 +629,6 @@ class DefaultHumanLoopManager(HumanLoopManager):
632
629
 
633
630
  return result
634
631
 
635
- async def _async_create_timeout_task(
636
- self,
637
- conversation_id: str,
638
- request_id: str,
639
- timeout: int,
640
- provider: HumanLoopProvider,
641
- callback: Optional[HumanLoopCallback],
642
- ) -> None:
643
- """创建超时任务"""
644
-
645
- async def timeout_task() -> None:
646
- await asyncio.sleep(timeout)
647
- # 检查当前状态
648
- result = await self.async_check_request_status(
649
- conversation_id, request_id, provider.name
650
- )
651
-
652
- # 只有当状态为PENDING时才触发超时回调
653
- # INPROGRESS状态表示对话正在进行中,不应视为超时
654
- if result.status == HumanLoopStatus.PENDING:
655
- if callback:
656
- await callback.async_on_humanloop_timeout(
657
- provider=provider, result=result
658
- )
659
- # 如果状态是INPROGRESS,重置超时任务
660
- elif result.status == HumanLoopStatus.INPROGRESS:
661
- # 对于进行中的对话,我们可以选择延长超时时间
662
- # 这里我们简单地重新创建一个超时任务,使用相同的超时时间
663
- if (conversation_id, request_id) in self._timeout_tasks:
664
- self._timeout_tasks[(conversation_id, request_id)].cancel()
665
- new_task = asyncio.create_task(timeout_task())
666
- self._timeout_tasks[(conversation_id, request_id)] = new_task
667
-
668
- task = asyncio.create_task(timeout_task())
669
- self._timeout_tasks[(conversation_id, request_id)] = task
670
-
671
632
  async def _async_wait_for_result(
672
633
  self,
673
634
  conversation_id: str,
@@ -704,6 +665,8 @@ class DefaultHumanLoopManager(HumanLoopManager):
704
665
  if callback:
705
666
  try:
706
667
  await callback.async_on_humanloop_update(provider, result)
668
+ if result.status == HumanLoopStatus.EXPIRED:
669
+ await callback.async_on_humanloop_timeout(provider, result)
707
670
  # 如果状态是最终状态,可以考虑移除回调
708
671
  if result.status not in [
709
672
  HumanLoopStatus.PENDING,
@@ -267,14 +267,9 @@ class APIProvider(BaseProvider):
267
267
  conversation_id,
268
268
  request_id,
269
269
  platform,
270
+ timeout,
270
271
  )
271
272
 
272
- # Create timeout task if timeout is set
273
- if timeout:
274
- await self._async_create_timeout_task(
275
- conversation_id, request_id, timeout
276
- )
277
-
278
273
  return HumanLoopResult(
279
274
  conversation_id=conversation_id,
280
275
  request_id=request_id,
@@ -296,7 +291,11 @@ class APIProvider(BaseProvider):
296
291
  )
297
292
 
298
293
  def _run_async_poll_request_status(
299
- self, conversation_id: str, request_id: str, platform: str
294
+ self,
295
+ conversation_id: str,
296
+ request_id: str,
297
+ platform: str,
298
+ timeout: Optional[int],
300
299
  ) -> None:
301
300
  """Run asynchronous API interaction in a separate thread"""
302
301
  # Create new event loop
@@ -306,7 +305,9 @@ class APIProvider(BaseProvider):
306
305
  try:
307
306
  # Run interaction processing in the new event loop
308
307
  loop.run_until_complete(
309
- self._async_poll_request_status(conversation_id, request_id, platform)
308
+ self._async_poll_request_status_with_timeout(
309
+ conversation_id, request_id, platform, timeout
310
+ )
310
311
  )
311
312
  finally:
312
313
  loop.close()
@@ -579,14 +580,9 @@ class APIProvider(BaseProvider):
579
580
  conversation_id,
580
581
  request_id,
581
582
  platform,
583
+ timeout,
582
584
  )
583
585
 
584
- # Create timeout task if timeout is set
585
- if timeout:
586
- await self._async_create_timeout_task(
587
- conversation_id, request_id, timeout
588
- )
589
-
590
586
  return HumanLoopResult(
591
587
  conversation_id=conversation_id,
592
588
  request_id=request_id,
@@ -606,6 +602,47 @@ class APIProvider(BaseProvider):
606
602
  error=str(e),
607
603
  )
608
604
 
605
+ async def _async_poll_request_status_with_timeout(
606
+ self,
607
+ conversation_id: str,
608
+ request_id: str,
609
+ platform: str,
610
+ timeout: Optional[int],
611
+ ) -> None:
612
+ """Poll request status with optional timeout
613
+
614
+ Args:
615
+ conversation_id: Conversation identifier
616
+ request_id: Request identifier
617
+ platform: Platform identifier
618
+ timeout: Optional timeout in seconds. If specified, polling will stop after timeout period
619
+ """
620
+
621
+ try:
622
+ if timeout:
623
+ # 使用 wait_for 设置超时
624
+ await asyncio.wait_for(
625
+ self._async_poll_request_status(
626
+ conversation_id, request_id, platform
627
+ ),
628
+ timeout=timeout,
629
+ )
630
+ else:
631
+ # 无超时限制
632
+ await self._async_poll_request_status(
633
+ conversation_id, request_id, platform
634
+ )
635
+
636
+ except asyncio.TimeoutError:
637
+ # 超时处理
638
+ request_info = self._get_request(conversation_id, request_id)
639
+ if request_info and request_info.get("status") == HumanLoopStatus.PENDING:
640
+ request_info["status"] = HumanLoopStatus.EXPIRED
641
+ request_info["error"] = "Request timed out"
642
+ logger.info(
643
+ f"\nRequest {request_id} has timed out after {timeout} seconds"
644
+ )
645
+
609
646
  async def _async_poll_request_status(
610
647
  self, conversation_id: str, request_id: str, platform: str
611
648
  ) -> None:
@@ -1,6 +1,5 @@
1
1
  from abc import ABC
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
- import asyncio
4
3
  import json
5
4
  import uuid
6
5
  from datetime import datetime
@@ -33,8 +32,6 @@ class BaseProvider(HumanLoopProvider, ABC):
33
32
  self._conversations: Dict[str, Dict[str, Any]] = {}
34
33
  # For quick lookup of requests in conversations
35
34
  self._conversation_requests: defaultdict[str, List[str]] = defaultdict(list)
36
- # Store timeout tasks
37
- self._timeout_tasks: Dict[Tuple[str, str], asyncio.Task] = {}
38
35
 
39
36
  self.prompt_template = self.config.get("prompt_template", "{context}")
40
37
 
@@ -291,11 +288,6 @@ class BaseProvider(HumanLoopProvider, ABC):
291
288
  bool: Whether cancellation was successful, True indicates success, False indicates failure
292
289
  """
293
290
 
294
- # Cancel timeout task
295
- if (conversation_id, request_id) in self._timeout_tasks:
296
- self._timeout_tasks[(conversation_id, request_id)].cancel()
297
- del self._timeout_tasks[(conversation_id, request_id)]
298
-
299
291
  request_key = (conversation_id, request_id)
300
292
  if request_key in self._requests:
301
293
  # Update request status to cancelled
@@ -346,11 +338,6 @@ class BaseProvider(HumanLoopProvider, ABC):
346
338
  HumanLoopStatus.INPROGRESS,
347
339
  ]:
348
340
  self._requests[request_key]["status"] = HumanLoopStatus.CANCELLED
349
-
350
- # Cancel the timeout task for this request
351
- if request_key in self._timeout_tasks:
352
- self._timeout_tasks[request_key].cancel()
353
- del self._timeout_tasks[request_key]
354
341
  else:
355
342
  success = False
356
343
 
@@ -482,45 +469,6 @@ class BaseProvider(HumanLoopProvider, ABC):
482
469
 
483
470
  return result
484
471
 
485
- async def _async_create_timeout_task(
486
- self, conversation_id: str, request_id: str, timeout: int
487
- ) -> None:
488
- """Create timeout task
489
-
490
- Args:
491
- conversation_id: Conversation ID
492
- request_id: Request ID
493
- timeout: Timeout duration in seconds
494
- """
495
-
496
- async def timeout_task() -> None:
497
- await asyncio.sleep(timeout)
498
-
499
- # Check current status
500
- request_info = self._get_request(conversation_id, request_id)
501
- if not request_info:
502
- return
503
-
504
- current_status = request_info.get("status", HumanLoopStatus.PENDING)
505
-
506
- # Only trigger timeout when status is PENDING
507
- # INPROGRESS status means conversation is ongoing, should not be considered as timeout
508
- if current_status == HumanLoopStatus.PENDING:
509
- # Update request status to expired
510
- request_info["status"] = HumanLoopStatus.EXPIRED
511
- request_info["error"] = "Request timed out"
512
- # If status is INPROGRESS, reset timeout task
513
- elif current_status == HumanLoopStatus.INPROGRESS:
514
- # For ongoing conversations, we can choose to extend the timeout
515
- # Here we simply create a new timeout task with the same timeout duration
516
- if (conversation_id, request_id) in self._timeout_tasks:
517
- self._timeout_tasks[(conversation_id, request_id)].cancel()
518
- new_task = asyncio.create_task(timeout_task())
519
- self._timeout_tasks[(conversation_id, request_id)] = new_task
520
-
521
- task = asyncio.create_task(timeout_task())
522
- self._timeout_tasks[(conversation_id, request_id)] = task
523
-
524
472
  def build_prompt(
525
473
  self,
526
474
  task_id: str,
@@ -213,6 +213,49 @@ class EmailProvider(BaseProvider):
213
213
  logger.exception(f"Unknown error occurred while sending email: {str(e)}")
214
214
  raise
215
215
 
216
+ async def _async_check_emails_with_timeout(
217
+ self,
218
+ conversation_id: str,
219
+ request_id: str,
220
+ recipient_email: str,
221
+ subject: str,
222
+ timeout: Optional[int],
223
+ ) -> None:
224
+ """Check email replies with timeout functionality
225
+
226
+ Args:
227
+ conversation_id: Conversation ID to identify a complete dialogue session
228
+ request_id: Request ID to identify a specific request
229
+ recipient_email: Recipient's email address
230
+ subject: Email subject line
231
+ timeout: Timeout duration in seconds, no timeout if None
232
+ """
233
+
234
+ try:
235
+ if timeout:
236
+ # 使用 wait_for 设置超时
237
+ await asyncio.wait_for(
238
+ self._async_check_emails(
239
+ conversation_id, request_id, recipient_email, subject
240
+ ),
241
+ timeout=timeout,
242
+ )
243
+ else:
244
+ # 无超时限制
245
+ await self._async_check_emails(
246
+ conversation_id, request_id, recipient_email, subject
247
+ )
248
+
249
+ except asyncio.TimeoutError:
250
+ # 超时处理
251
+ request_info = self._get_request(conversation_id, request_id)
252
+ if request_info and request_info.get("status") == HumanLoopStatus.PENDING:
253
+ request_info["status"] = HumanLoopStatus.EXPIRED
254
+ request_info["error"] = "Request timed out"
255
+ logger.info(
256
+ f"\nRequest {request_id} has timed out after {timeout} seconds"
257
+ )
258
+
216
259
  async def _async_check_emails(
217
260
  self, conversation_id: str, request_id: str, recipient_email: str, subject: str
218
261
  ) -> None:
@@ -488,11 +531,6 @@ class EmailProvider(BaseProvider):
488
531
  }
489
532
  )
490
533
 
491
- # 取消超时任务
492
- if request_key in self._timeout_tasks:
493
- self._timeout_tasks[request_key].cancel()
494
- del self._timeout_tasks[request_key]
495
-
496
534
  def _format_email_body(
497
535
  self, body: str, loop_type: HumanLoopType, subject: str
498
536
  ) -> Tuple[str, str]:
@@ -772,12 +810,9 @@ class EmailProvider(BaseProvider):
772
810
  request_id,
773
811
  recipient_email,
774
812
  subject,
813
+ timeout,
775
814
  )
776
815
 
777
- # 如果设置了超时,创建超时任务
778
- if timeout:
779
- await self._async_create_timeout_task(conversation_id, request_id, timeout)
780
-
781
816
  return HumanLoopResult(
782
817
  conversation_id=conversation_id,
783
818
  request_id=request_id,
@@ -786,7 +821,12 @@ class EmailProvider(BaseProvider):
786
821
  )
787
822
 
788
823
  def _run_email_check_task(
789
- self, conversation_id: str, request_id: str, recipient_email: str, subject: str
824
+ self,
825
+ conversation_id: str,
826
+ request_id: str,
827
+ recipient_email: str,
828
+ subject: str,
829
+ timeout: Optional[int],
790
830
  ) -> None:
791
831
  """Run email check task in thread
792
832
 
@@ -803,8 +843,8 @@ class EmailProvider(BaseProvider):
803
843
  try:
804
844
  # Run email check in new event loop
805
845
  loop.run_until_complete(
806
- self._async_check_emails(
807
- conversation_id, request_id, recipient_email, subject
846
+ self._async_check_emails_with_timeout(
847
+ conversation_id, request_id, recipient_email, subject, timeout
808
848
  )
809
849
  )
810
850
  except Exception as e:
@@ -987,12 +1027,9 @@ class EmailProvider(BaseProvider):
987
1027
  request_id,
988
1028
  recipient_email,
989
1029
  subject,
1030
+ timeout,
990
1031
  )
991
1032
 
992
- # 如果设置了超时,创建超时任务
993
- if timeout:
994
- await self._async_create_timeout_task(conversation_id, request_id, timeout)
995
-
996
1033
  return HumanLoopResult(
997
1034
  conversation_id=conversation_id,
998
1035
  request_id=request_id,
@@ -90,17 +90,36 @@ class TerminalProvider(BaseProvider):
90
90
  self._terminal_input_tasks[
91
91
  (conversation_id, request_id)
92
92
  ] = self._executor.submit(
93
- self._run_async_terminal_interaction, conversation_id, request_id
93
+ self._run_async_terminal_interaction, conversation_id, request_id, timeout
94
94
  )
95
95
 
96
- # Create timeout task if timeout is specified
97
- if timeout:
98
- await self._async_create_timeout_task(conversation_id, request_id, timeout)
99
-
100
96
  return result
101
97
 
98
+ async def _process_terminal_interaction_with_timeout(
99
+ self, conversation_id: str, request_id: str, timeout: Optional[int]
100
+ ) -> None:
101
+ """Process terminal interaction with timeout functionality"""
102
+ try:
103
+ if timeout:
104
+ # Set timeout using wait_for
105
+ await asyncio.wait_for(
106
+ self._process_terminal_interaction(conversation_id, request_id),
107
+ timeout=timeout,
108
+ )
109
+ else:
110
+ # No timeout limit
111
+ await self._process_terminal_interaction(conversation_id, request_id)
112
+
113
+ except asyncio.TimeoutError:
114
+ # Handle timeout
115
+ request_info = self._get_request(conversation_id, request_id)
116
+ if request_info and request_info.get("status") == HumanLoopStatus.PENDING:
117
+ request_info["status"] = HumanLoopStatus.EXPIRED
118
+ request_info["error"] = "Request timed out"
119
+ print(f"\nRequest {request_id} has timed out after {timeout} seconds")
120
+
102
121
  def _run_async_terminal_interaction(
103
- self, conversation_id: str, request_id: str
122
+ self, conversation_id: str, request_id: str, timeout: int | None
104
123
  ) -> None:
105
124
  """Run asynchronous terminal interaction in a separate thread"""
106
125
  # Create new event loop
@@ -110,7 +129,9 @@ class TerminalProvider(BaseProvider):
110
129
  try:
111
130
  # Run interaction processing in the new event loop
112
131
  loop.run_until_complete(
113
- self._process_terminal_interaction(conversation_id, request_id)
132
+ self._process_terminal_interaction_with_timeout(
133
+ conversation_id, request_id, timeout
134
+ )
114
135
  )
115
136
  finally:
116
137
  loop.close()
@@ -213,13 +234,9 @@ class TerminalProvider(BaseProvider):
213
234
  self._terminal_input_tasks[
214
235
  (conversation_id, request_id)
215
236
  ] = self._executor.submit(
216
- self._run_async_terminal_interaction, conversation_id, request_id
237
+ self._run_async_terminal_interaction, conversation_id, request_id, timeout
217
238
  )
218
239
 
219
- # Create timeout task if timeout is specified
220
- if timeout:
221
- await self._async_create_timeout_task(conversation_id, request_id, timeout)
222
-
223
240
  return result
224
241
 
225
242
  async def _process_terminal_interaction(
@@ -0,0 +1,5 @@
1
+ from .utils import run_async_safely, get_secret_from_env
2
+ from .context_formatter import generate_function_summary
3
+
4
+
5
+ __all__ = ["run_async_safely", "get_secret_from_env", "generate_function_summary"]
@@ -0,0 +1,136 @@
1
+ import inspect
2
+ from typing import Callable, Any, Dict, Union, cast
3
+
4
+
5
+ def _param_detail_zh(p: inspect.Parameter) -> str:
6
+ return (
7
+ f" {p.name}: "
8
+ + (f"类型[{p.annotation}] " if p.annotation != p.empty else "")
9
+ + (f"(默认值={p.default})" if p.default != p.empty else "")
10
+ )
11
+
12
+
13
+ def _param_detail_en(p: inspect.Parameter) -> str:
14
+ return (
15
+ f" {p.name}: "
16
+ + (f"Type[{p.annotation}] " if p.annotation != p.empty else "")
17
+ + (f"(default={p.default})" if p.default != p.empty else "")
18
+ )
19
+
20
+
21
+ def generate_function_summary(
22
+ fn: Callable,
23
+ *args: Any,
24
+ language: str = "zh",
25
+ **kwargs: Any,
26
+ ) -> str:
27
+ """生成支持中英文切换的函数说明模板
28
+
29
+ Args:
30
+ fn: 目标函数
31
+ *args: 位置参数示例
32
+ **kwargs: 关键字参数示例
33
+ language: 输出语言 ('zh'/'en')
34
+
35
+ Returns:
36
+ 纯文本格式的函数说明
37
+ """
38
+
39
+ # 定义翻译类型
40
+
41
+ # 语言配置
42
+ translations: Dict[
43
+ str, Dict[str, Union[str, Callable[[inspect.Parameter], str]]]
44
+ ] = {
45
+ "zh": {
46
+ "title": "函数说明",
47
+ "name": "函数名称",
48
+ "module": "所属模块",
49
+ "description": "功能描述",
50
+ "params": "参数列表",
51
+ "param_detail": _param_detail_zh,
52
+ "return": "返回值类型",
53
+ "current_input": "当前输入",
54
+ "positional_args": "位置参数",
55
+ "keyword_args": "关键字参数",
56
+ "usage": "调用方式",
57
+ "approval": "审批状态",
58
+ "no_doc": "无文档说明",
59
+ },
60
+ "en": {
61
+ "title": "Function Documentation",
62
+ "name": "Function Name",
63
+ "module": "Module",
64
+ "description": "Description",
65
+ "params": "Parameters",
66
+ "param_detail": _param_detail_en,
67
+ "return": "Return Type",
68
+ "current_input": "Current Input",
69
+ "positional_args": "Positional Args",
70
+ "keyword_args": "Keyword Args",
71
+ "usage": "Usage",
72
+ "approval": "Approval Status",
73
+ "no_doc": "No documentation",
74
+ },
75
+ }
76
+
77
+ lang = translations.get(language, translations["zh"])
78
+
79
+ # 明确告诉类型检查器这是一个可调用对象
80
+ param_detail_func = cast(Callable[[inspect.Parameter], str], lang["param_detail"])
81
+
82
+ # 获取函数信息
83
+ func_name = fn.__name__
84
+ module_obj = inspect.getmodule(fn)
85
+ module = module_obj.__name__ if module_obj is not None else str(lang["no_doc"])
86
+ doc = (fn.__doc__ or str(lang["no_doc"])).strip()
87
+ sig = inspect.signature(fn)
88
+
89
+ # 构建模板
90
+ template = (
91
+ f"""
92
+ - {lang['title']}: {func_name}
93
+ - {lang['name']}: {func_name}
94
+ - {lang['module']}: {module}
95
+
96
+ - {lang['description']}:
97
+ {doc}
98
+
99
+ - {lang['params']}:
100
+ """
101
+ + "\n".join(param_detail_func(p) for p in sig.parameters.values())
102
+ + f"""
103
+
104
+ - {lang['return']}: {sig.return_annotation if sig.return_annotation != sig.empty else lang['no_doc']}
105
+
106
+ - {lang['current_input']}:
107
+ {lang['positional_args']}: {args}
108
+ {lang['keyword_args']}: {kwargs}
109
+
110
+ - {lang['usage']}: {func_name}(*{args}, **{kwargs})
111
+
112
+ """
113
+ )
114
+ return template.strip()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ # 示例函数
119
+ def calculate(a: int, b: float = 1.0) -> float:
120
+ """计算两个数的乘积/Calculate the product of two numbers"""
121
+ return a * b
122
+
123
+ # 中文输出
124
+ print("==== 中文版 ====")
125
+ print(generate_function_summary(calculate, 3, b=2.5))
126
+
127
+ # 英文输出
128
+ print("\n==== English Version ====")
129
+ print(
130
+ generate_function_summary(
131
+ calculate,
132
+ 3,
133
+ language="en",
134
+ b=2.5,
135
+ )
136
+ )
@@ -42,6 +42,7 @@ def run_async_safely(coro: Awaitable[Any]) -> Any:
42
42
  finally:
43
43
  if own_loop and not loop.is_closed():
44
44
  loop.close()
45
+ asyncio.set_event_loop(None)
45
46
 
46
47
 
47
48
  def get_secret_from_env(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gohumanloop
3
- Version: 0.0.9
3
+ Version: 0.0.11
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
@@ -255,7 +255,7 @@ Through `GoHumanloop`'s encapsulation, you can implement secure and efficient `H
255
255
  ## 📚 Key Features
256
256
 
257
257
  <div align="center">
258
- <img height=360 src="http://cdn.oyster-iot.cloud/202505211030197.png"><br>
258
+ <img height=360 src="http://cdn.oyster-iot.cloud/202505291027894.png"><br>
259
259
  <b face="雅黑"> GoHumanLoop Architecture</b>
260
260
  </div>
261
261
  <br>
@@ -3,7 +3,7 @@ authors = [
3
3
  {name = "gohumanloop authors", email = "baird0917@163.com"},
4
4
  ]
5
5
  name = "gohumanloop"
6
- version = "0.0.9"
6
+ version = "0.0.11"
7
7
  description = "Perfecting AI workflows with human intelligence"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
@@ -1,7 +0,0 @@
1
- from .utils import run_async_safely, get_secret_from_env
2
-
3
-
4
- __all__ = [
5
- "run_async_safely",
6
- "get_secret_from_env",
7
- ]
@@ -1,64 +0,0 @@
1
- from typing import Dict, Any
2
- import json
3
-
4
-
5
- class ContextFormatter:
6
- """上下文格式化工具"""
7
-
8
- @staticmethod
9
- def format_for_human(context: Dict[str, Any]) -> str:
10
- """将上下文格式化为人类可读的文本"""
11
- result = []
12
-
13
- # 添加标题(如果有)
14
- if "title" in context:
15
- result.append(f"# {context['title']}\n")
16
-
17
- # 添加描述(如果有)
18
- if "description" in context:
19
- result.append(f"{context['description']}\n")
20
-
21
- # 添加任务信息
22
- if "task" in context:
23
- result.append(f"## 任务\n{context['task']}\n")
24
-
25
- # 添加代理信息
26
- if "agent" in context:
27
- result.append(f"## 代理\n{context['agent']}\n")
28
-
29
- # 添加操作信息
30
- if "action" in context:
31
- result.append(f"## 请求的操作\n{context['action']}\n")
32
-
33
- # 添加原因
34
- if "reason" in context:
35
- result.append(f"## 原因\n{context['reason']}\n")
36
-
37
- # 添加其他键值对
38
- other_keys = [
39
- k
40
- for k in context.keys()
41
- if k not in ["title", "description", "task", "agent", "action", "reason"]
42
- ]
43
- if other_keys:
44
- result.append("## 附加信息\n")
45
- for key in other_keys:
46
- value = context[key]
47
- if isinstance(value, (dict, list)):
48
- value = json.dumps(value, ensure_ascii=False, indent=2)
49
- result.append(f"### {key}\n```\n{value}\n```\n")
50
-
51
- return "\n".join(result)
52
-
53
- @staticmethod
54
- def format_for_api(context: Dict[str, Any]) -> Dict[str, Any]:
55
- """将上下文格式化为API友好的格式"""
56
- # 复制上下文以避免修改原始数据
57
- formatted = context.copy()
58
-
59
- # 确保所有值都是可序列化的
60
- for key, value in formatted.items():
61
- if not isinstance(value, (str, int, float, bool, list, dict, type(None))):
62
- formatted[key] = str(value)
63
-
64
- return formatted
File without changes
File without changes