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.
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/PKG-INFO +2 -2
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/README.md +1 -1
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/adapters/base_adapter.py +42 -16
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/core/manager.py +21 -58
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/api_provider.py +51 -14
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/base.py +0 -52
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/email_provider.py +53 -16
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/terminal_provider.py +29 -12
- gohumanloop-0.0.11/gohumanloop/utils/__init__.py +5 -0
- gohumanloop-0.0.11/gohumanloop/utils/context_formatter.py +136 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/utils/utils.py +1 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/PKG-INFO +2 -2
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/pyproject.toml +1 -1
- gohumanloop-0.0.9/gohumanloop/utils/__init__.py +0 -7
- gohumanloop-0.0.9/gohumanloop/utils/context_formatter.py +0 -64
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/LICENSE +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/__main__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/adapters/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/adapters/langgraph_adapter.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/cli/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/cli/main.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/core/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/core/interface.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/manager/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/manager/ghl_manager.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/models/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/models/api_model.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/models/glh_model.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/__init__.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/providers/ghl_provider.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop/utils/threadsafedict.py +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/SOURCES.txt +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/dependency_links.txt +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/entry_points.txt +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/requires.txt +0 -0
- {gohumanloop-0.0.9 → gohumanloop-0.0.11}/gohumanloop.egg-info/top_level.txt +0 -0
- {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.
|
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/
|
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/
|
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
|
-
|
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
|
438
|
-
|
439
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
|
433
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
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,
|
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.
|
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,
|
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.
|
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.
|
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,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
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: gohumanloop
|
3
|
-
Version: 0.0.
|
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/
|
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>
|
@@ -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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|