gohumanloop 0.0.5__py3-none-any.whl → 0.0.6__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.
- gohumanloop/__init__.py +6 -8
- gohumanloop/adapters/__init__.py +4 -4
- gohumanloop/adapters/langgraph_adapter.py +348 -207
- gohumanloop/cli/main.py +4 -1
- gohumanloop/core/interface.py +181 -215
- gohumanloop/core/manager.py +332 -265
- gohumanloop/manager/ghl_manager.py +223 -185
- gohumanloop/models/api_model.py +32 -7
- gohumanloop/models/glh_model.py +15 -11
- gohumanloop/providers/api_provider.py +233 -189
- gohumanloop/providers/base.py +179 -172
- gohumanloop/providers/email_provider.py +386 -325
- gohumanloop/providers/ghl_provider.py +19 -17
- gohumanloop/providers/terminal_provider.py +111 -92
- gohumanloop/utils/__init__.py +7 -1
- gohumanloop/utils/context_formatter.py +20 -15
- gohumanloop/utils/threadsafedict.py +64 -56
- gohumanloop/utils/utils.py +28 -28
- gohumanloop-0.0.6.dist-info/METADATA +259 -0
- gohumanloop-0.0.6.dist-info/RECORD +30 -0
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/WHEEL +1 -1
- gohumanloop-0.0.5.dist-info/METADATA +0 -35
- gohumanloop-0.0.5.dist-info/RECORD +0 -30
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/entry_points.txt +0 -0
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/top_level.txt +0 -0
@@ -3,8 +3,9 @@ import asyncio
|
|
3
3
|
import threading
|
4
4
|
|
5
5
|
|
6
|
-
K = TypeVar(
|
7
|
-
V = TypeVar(
|
6
|
+
K = TypeVar("K")
|
7
|
+
V = TypeVar("V")
|
8
|
+
|
8
9
|
|
9
10
|
class ThreadSafeDict(Generic[K, V]):
|
10
11
|
"""
|
@@ -27,71 +28,71 @@ class ThreadSafeDict(Generic[K, V]):
|
|
27
28
|
- 为每个键创建独立的锁,减少不同键之间的锁竞争
|
28
29
|
- 使用全局锁 self._global_lock 保护键级别锁的创建和删除
|
29
30
|
"""
|
30
|
-
|
31
|
-
def __init__(self):
|
32
|
-
self._dict = {}
|
31
|
+
|
32
|
+
def __init__(self) -> None:
|
33
|
+
self._dict: dict[Any, Any] = {}
|
33
34
|
# 使用 threading.RLock 支持同步操作的线程安全
|
34
35
|
self._sync_lock = threading.RLock()
|
35
36
|
# 使用 asyncio.Lock 支持异步操作的线程安全
|
36
37
|
self._async_lock = asyncio.Lock()
|
37
38
|
# 键级别锁字典
|
38
|
-
self._key_locks = {}
|
39
|
+
self._key_locks: dict[Any, asyncio.Lock] = {}
|
39
40
|
# 键级别锁的全局锁
|
40
41
|
self._global_lock = asyncio.Lock()
|
41
|
-
|
42
|
-
async def _get_key_lock(self, key):
|
42
|
+
|
43
|
+
async def _get_key_lock(self, key: K) -> asyncio.Lock:
|
43
44
|
"""获取指定键的锁,如果不存在则创建"""
|
44
45
|
async with self._global_lock:
|
45
46
|
if key not in self._key_locks:
|
46
47
|
self._key_locks[key] = asyncio.Lock()
|
47
48
|
return self._key_locks[key]
|
48
|
-
|
49
|
+
|
49
50
|
# 同步方法 - 使用 threading.RLock 保证线程安全
|
50
|
-
def __getitem__(self, key: K) ->
|
51
|
+
def __getitem__(self, key: K) -> Any:
|
51
52
|
"""获取值 - 同步方法,用于 dict[key] 语法"""
|
52
53
|
with self._sync_lock:
|
53
54
|
return self._dict[key]
|
54
|
-
|
55
|
+
|
55
56
|
def __setitem__(self, key: K, value: V) -> None:
|
56
57
|
"""设置值 - 同步方法,用于 dict[key] = value 语法"""
|
57
58
|
with self._sync_lock:
|
58
59
|
self._dict[key] = value
|
59
|
-
|
60
|
+
|
60
61
|
def __delitem__(self, key: K) -> None:
|
61
62
|
"""删除键 - 同步方法,用于 del dict[key] 语法"""
|
62
63
|
with self._sync_lock:
|
63
64
|
del self._dict[key]
|
64
|
-
|
65
|
+
|
65
66
|
def __contains__(self, key: K) -> bool:
|
66
67
|
"""检查键是否存在 - 同步方法,用于 key in dict 语法"""
|
67
68
|
with self._sync_lock:
|
68
69
|
return key in self._dict
|
69
|
-
|
70
|
-
def get(self, key: K, default: Optional[V] = None) ->
|
70
|
+
|
71
|
+
def get(self, key: K, default: Optional[V] = None) -> Any:
|
71
72
|
"""获取值,如果不存在则返回默认值 - 同步方法"""
|
72
73
|
with self._sync_lock:
|
73
74
|
return self._dict.get(key, default)
|
74
|
-
|
75
|
+
|
75
76
|
def __len__(self) -> int:
|
76
77
|
"""获取字典长度 - 同步方法,用于 len(dict) 语法"""
|
77
78
|
with self._sync_lock:
|
78
79
|
return len(self._dict)
|
79
|
-
|
80
|
-
def keys(self):
|
80
|
+
|
81
|
+
def keys(self) -> list:
|
81
82
|
"""获取所有键 - 同步方法"""
|
82
83
|
with self._sync_lock:
|
83
84
|
return list(self._dict.keys())
|
84
|
-
|
85
|
-
def values(self):
|
85
|
+
|
86
|
+
def values(self) -> list:
|
86
87
|
"""获取所有值 - 同步方法"""
|
87
88
|
with self._sync_lock:
|
88
89
|
return list(self._dict.values())
|
89
|
-
|
90
|
-
def items(self):
|
90
|
+
|
91
|
+
def items(self) -> list:
|
91
92
|
"""获取所有键值对 - 同步方法"""
|
92
93
|
with self._sync_lock:
|
93
94
|
return list(self._dict.items())
|
94
|
-
|
95
|
+
|
95
96
|
def update(self, key: K, updates: Dict[str, Any]) -> bool:
|
96
97
|
"""更新字典中的值 - 同步方法"""
|
97
98
|
with self._sync_lock:
|
@@ -99,7 +100,7 @@ class ThreadSafeDict(Generic[K, V]):
|
|
99
100
|
self._dict[key].update(updates)
|
100
101
|
return True
|
101
102
|
return False
|
102
|
-
|
103
|
+
|
103
104
|
def update_item(self, key: K, item_key: Any, item_value: Any) -> bool:
|
104
105
|
"""更新字典中的单个项 - 同步方法"""
|
105
106
|
with self._sync_lock:
|
@@ -107,20 +108,20 @@ class ThreadSafeDict(Generic[K, V]):
|
|
107
108
|
self._dict[key][item_key] = item_value
|
108
109
|
return True
|
109
110
|
return False
|
110
|
-
|
111
|
+
|
111
112
|
# 异步方法 - 使用 asyncio.Lock 保证线程安全
|
112
|
-
async def aget(self, key: K, default: Optional[V] = None) ->
|
113
|
+
async def aget(self, key: K, default: Optional[V] = None) -> Any:
|
113
114
|
"""安全地获取值 - 异步方法"""
|
114
115
|
async with self._async_lock:
|
115
116
|
return self._dict.get(key, default)
|
116
|
-
|
117
|
+
|
117
118
|
async def aset(self, key: K, value: V) -> None:
|
118
119
|
"""安全地设置值 - 异步方法"""
|
119
120
|
key_lock = await self._get_key_lock(key)
|
120
121
|
async with key_lock:
|
121
122
|
async with self._async_lock:
|
122
123
|
self._dict[key] = value
|
123
|
-
|
124
|
+
|
124
125
|
async def adelete(self, key: K) -> bool:
|
125
126
|
"""安全地删除键 - 异步方法"""
|
126
127
|
key_lock = await self._get_key_lock(key)
|
@@ -134,7 +135,7 @@ class ThreadSafeDict(Generic[K, V]):
|
|
134
135
|
del self._key_locks[key]
|
135
136
|
return True
|
136
137
|
return False
|
137
|
-
|
138
|
+
|
138
139
|
async def aupdate(self, key: K, updates: Dict[str, Any]) -> bool:
|
139
140
|
"""安全地更新值 - 异步方法"""
|
140
141
|
key_lock = await self._get_key_lock(key)
|
@@ -144,7 +145,7 @@ class ThreadSafeDict(Generic[K, V]):
|
|
144
145
|
self._dict[key].update(updates)
|
145
146
|
return True
|
146
147
|
return False
|
147
|
-
|
148
|
+
|
148
149
|
async def aupdate_item(self, key: K, item_key: Any, item_value: Any) -> bool:
|
149
150
|
"""安全地更新字典中的单个项 - 异步方法"""
|
150
151
|
key_lock = await self._get_key_lock(key)
|
@@ -154,90 +155,97 @@ class ThreadSafeDict(Generic[K, V]):
|
|
154
155
|
self._dict[key][item_key] = item_value
|
155
156
|
return True
|
156
157
|
return False
|
157
|
-
|
158
|
+
|
158
159
|
async def acontains(self, key: K) -> bool:
|
159
160
|
"""安全地检查键是否存在 - 异步方法"""
|
160
161
|
async with self._async_lock:
|
161
162
|
return key in self._dict
|
162
|
-
|
163
|
+
|
163
164
|
async def alen(self) -> int:
|
164
165
|
"""安全地获取字典长度 - 异步方法"""
|
165
166
|
async with self._async_lock:
|
166
167
|
return len(self._dict)
|
167
|
-
|
168
|
-
async def akeys(self):
|
168
|
+
|
169
|
+
async def akeys(self) -> list:
|
169
170
|
"""安全地获取所有键 - 异步方法"""
|
170
171
|
async with self._async_lock:
|
171
172
|
return list(self._dict.keys())
|
172
|
-
|
173
|
-
async def avalues(self):
|
173
|
+
|
174
|
+
async def avalues(self) -> list:
|
174
175
|
"""安全地获取所有值 - 异步方法"""
|
175
176
|
async with self._async_lock:
|
176
177
|
return list(self._dict.values())
|
177
|
-
|
178
|
-
async def aitems(self):
|
178
|
+
|
179
|
+
async def aitems(self) -> list:
|
179
180
|
"""安全地获取所有键值对 - 异步方法"""
|
180
181
|
async with self._async_lock:
|
181
182
|
return list(self._dict.items())
|
182
183
|
|
184
|
+
|
183
185
|
if __name__ == "__main__":
|
184
186
|
# 测试同步方法
|
185
|
-
def test_sync_methods():
|
187
|
+
def test_sync_methods() -> None:
|
186
188
|
print("\n=== 测试同步方法 ===")
|
187
|
-
sync_dict = ThreadSafeDict()
|
188
|
-
|
189
|
+
sync_dict: ThreadSafeDict = ThreadSafeDict()
|
190
|
+
|
189
191
|
# 测试基本的增删改查操作
|
190
192
|
sync_dict["key1"] = "value1"
|
191
193
|
print("设置并获取:", sync_dict["key1"]) # value1
|
192
194
|
print("键存在性检查:", "key1" in sync_dict) # True
|
193
195
|
print("获取默认值:", sync_dict.get("not_exist", "default")) # default
|
194
|
-
|
196
|
+
|
195
197
|
# 测试字典长度
|
196
198
|
print("字典长度:", len(sync_dict)) # 1
|
197
|
-
|
199
|
+
|
198
200
|
# 测试字典方法
|
199
201
|
sync_dict["key2"] = "value2"
|
200
202
|
print("所有键:", sync_dict.keys()) # ['key1', 'key2']
|
201
203
|
print("所有值:", sync_dict.values()) # ['value1', 'value2']
|
202
|
-
print(
|
203
|
-
|
204
|
+
print(
|
205
|
+
"所有键值对:", sync_dict.items()
|
206
|
+
) # [('key1', 'value1'), ('key2', 'value2')]
|
207
|
+
|
204
208
|
# 测试嵌套字典更新
|
205
209
|
sync_dict["nested"] = {"a": 1}
|
206
210
|
sync_dict.update("nested", {"b": 2})
|
207
211
|
sync_dict.update_item("nested", "c", 3)
|
208
212
|
print("嵌套字典:", sync_dict["nested"]) # {'a': 1, 'b': 2, 'c': 3}
|
209
|
-
|
213
|
+
|
210
214
|
# 测试删除操作
|
211
215
|
del sync_dict["key1"]
|
212
216
|
print("删除后检查:", "key1" in sync_dict) # False
|
213
217
|
|
214
218
|
# 测试异步方法
|
215
|
-
async def test_async_methods():
|
219
|
+
async def test_async_methods() -> None:
|
216
220
|
print("\n=== 测试异步方法 ===")
|
217
|
-
async_dict = ThreadSafeDict()
|
218
|
-
|
221
|
+
async_dict: ThreadSafeDict = ThreadSafeDict()
|
222
|
+
|
219
223
|
# 测试基本的异步增删改查
|
220
224
|
await async_dict.aset("key1", "value1")
|
221
225
|
print("异步获取:", await async_dict.aget("key1")) # value1
|
222
226
|
print("异步键检查:", await async_dict.acontains("key1")) # True
|
223
|
-
|
227
|
+
|
224
228
|
# 测试异步字典操作
|
225
229
|
await async_dict.aset("key2", "value2")
|
226
230
|
print("异步长度:", await async_dict.alen()) # 2
|
227
231
|
print("异步所有键:", await async_dict.akeys()) # ['key1', 'key2']
|
228
232
|
print("异步所有值:", await async_dict.avalues()) # ['value1', 'value2']
|
229
|
-
print(
|
230
|
-
|
233
|
+
print(
|
234
|
+
"异步所有键值对:", await async_dict.aitems()
|
235
|
+
) # [('key1', 'value1'), ('key2', 'value2')]
|
236
|
+
|
231
237
|
# 测试异步嵌套字典更新
|
232
238
|
await async_dict.aset("nested", {"x": 1})
|
233
239
|
await async_dict.aupdate("nested", {"y": 2})
|
234
240
|
await async_dict.aupdate_item("nested", "z", 3)
|
235
|
-
print(
|
236
|
-
|
241
|
+
print(
|
242
|
+
"异步嵌套字典:", await async_dict.aget("nested")
|
243
|
+
) # {'x': 1, 'y': 2, 'z': 3}
|
244
|
+
|
237
245
|
# 测试异步删除
|
238
246
|
await async_dict.adelete("key1")
|
239
247
|
print("异步删除后检查:", await async_dict.acontains("key1")) # False
|
240
|
-
|
248
|
+
|
241
249
|
# 运行测试
|
242
250
|
test_sync_methods()
|
243
|
-
asyncio.run(test_async_methods())
|
251
|
+
asyncio.run(test_async_methods())
|
gohumanloop/utils/utils.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
import asyncio
|
2
2
|
import os
|
3
|
-
from typing import Optional, Union
|
3
|
+
from typing import Awaitable, Any, Optional, Union
|
4
4
|
from pydantic import SecretStr
|
5
|
-
import warnings
|
6
5
|
import logging
|
7
6
|
|
8
7
|
logger = logging.getLogger(__name__)
|
9
8
|
|
10
|
-
|
9
|
+
|
10
|
+
def run_async_safely(coro: Awaitable[Any]) -> Any:
|
11
11
|
"""
|
12
12
|
Safely run async coroutines in synchronous environment
|
13
13
|
Will raise RuntimeError if called in async environment
|
@@ -16,7 +16,7 @@ def run_async_safely(coro):
|
|
16
16
|
loop = asyncio.get_running_loop()
|
17
17
|
except RuntimeError: # No running event loop
|
18
18
|
loop = None
|
19
|
-
|
19
|
+
|
20
20
|
if loop is not None:
|
21
21
|
raise RuntimeError(
|
22
22
|
"Detected running event loop! "
|
@@ -24,19 +24,19 @@ def run_async_safely(coro):
|
|
24
24
|
"If you really need to call sync code from async context, "
|
25
25
|
"consider using asyncio.to_thread() or other proper methods."
|
26
26
|
)
|
27
|
-
|
27
|
+
|
28
28
|
# Handle synchronous environment
|
29
29
|
try:
|
30
30
|
loop = asyncio.get_event_loop()
|
31
|
-
logger.
|
31
|
+
logger.debug("Using existing event loop.")
|
32
32
|
except RuntimeError:
|
33
33
|
loop = asyncio.new_event_loop()
|
34
34
|
asyncio.set_event_loop(loop)
|
35
35
|
own_loop = True
|
36
|
-
logger.
|
36
|
+
logger.debug("Created new event loop.")
|
37
37
|
else:
|
38
38
|
own_loop = False
|
39
|
-
|
39
|
+
|
40
40
|
try:
|
41
41
|
return loop.run_until_complete(coro)
|
42
42
|
finally:
|
@@ -47,24 +47,24 @@ def run_async_safely(coro):
|
|
47
47
|
def get_secret_from_env(
|
48
48
|
key: Union[str, list, tuple],
|
49
49
|
default: Optional[str] = None,
|
50
|
-
error_message: Optional[str] = None
|
50
|
+
error_message: Optional[str] = None,
|
51
51
|
) -> Optional[SecretStr]:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
52
|
+
"""Get a value from an environment variable."""
|
53
|
+
if isinstance(key, (list, tuple)):
|
54
|
+
for k in key:
|
55
|
+
if k in os.environ:
|
56
|
+
return SecretStr(os.environ[k])
|
57
|
+
if isinstance(key, str) and key in os.environ:
|
58
|
+
return SecretStr(os.environ[key])
|
59
|
+
if isinstance(default, str):
|
60
|
+
return SecretStr(default)
|
61
|
+
if default is None:
|
62
|
+
return None
|
63
|
+
if error_message:
|
64
|
+
raise ValueError(error_message)
|
65
|
+
msg = (
|
66
|
+
f"Did not find {key}, please add an environment variable"
|
67
|
+
f" `{key}` which contains it, or pass"
|
68
|
+
f" `{key}` as a named parameter."
|
69
|
+
)
|
70
|
+
raise ValueError(msg)
|
@@ -0,0 +1,259 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: gohumanloop
|
3
|
+
Version: 0.0.6
|
4
|
+
Summary: Perfecting AI workflows with human intelligence
|
5
|
+
Author-email: gohumanloop authors <baird0917@163.com>
|
6
|
+
Project-URL: repository, https://github.com/ptonlix/gohumanloop
|
7
|
+
Requires-Python: >=3.10
|
8
|
+
Description-Content-Type: text/markdown
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Dist: aiohttp>=3.11.16
|
11
|
+
Requires-Dist: click>=8.1.8
|
12
|
+
Requires-Dist: dotenv>=0.9.9
|
13
|
+
Requires-Dist: pydantic>=2.11.3
|
14
|
+
Requires-Dist: tomli>=2.2.1
|
15
|
+
Provides-Extra: email
|
16
|
+
Requires-Dist: imapclient>=3.0.1; extra == "email"
|
17
|
+
Provides-Extra: langgraph
|
18
|
+
Requires-Dist: langgraph>=0.3.30; extra == "langgraph"
|
19
|
+
Provides-Extra: apiservices
|
20
|
+
Requires-Dist: fastapi>=0.115.12; extra == "apiservices"
|
21
|
+
Requires-Dist: uvicorn>=0.34.2; extra == "apiservices"
|
22
|
+
Dynamic: license-file
|
23
|
+
|
24
|
+
<div align="center">
|
25
|
+
|
26
|
+

|
27
|
+
<b face="雅黑">Perfecting AI workflows with human intelligence</b>
|
28
|
+
|
29
|
+
</div>
|
30
|
+
|
31
|
+
**GoHumanLoop**: A Python library empowering AI agents to dynamically request human input (approval/feedback/conversation) at critical stages. Core features:
|
32
|
+
|
33
|
+
- `Human-in-the-loop control`: Lets AI agent systems pause and escalate decisions, enhancing safety and trust.
|
34
|
+
- `Multi-channel integration`: Supports Terminal, Email, API, and frameworks like LangGraph/CrewAI (soon).
|
35
|
+
- `Flexible workflows`: Combines automated reasoning with human oversight for reliable AI operations.
|
36
|
+
|
37
|
+
Ensures responsible AI deployment by bridging autonomous agents and human judgment.
|
38
|
+
|
39
|
+
<div align="center">
|
40
|
+
<img alt="Repostart" src="https://img.shields.io/github/stars/ptonlix/gohumanloop"/>
|
41
|
+
<img alt=" Python" src="https://img.shields.io/badge/Python-3.10%2B-blue"/>
|
42
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-green"/>
|
43
|
+
|
44
|
+
[简体中文](README-zh.md) | English
|
45
|
+
|
46
|
+
</div>
|
47
|
+
|
48
|
+
## Table of contents
|
49
|
+
|
50
|
+
- [Getting Started](#getting-started)
|
51
|
+
- [Why GoHumanloop?](#why-humanlayer)
|
52
|
+
- [Key Features](#key-features)
|
53
|
+
- [Examples](#examples)
|
54
|
+
- [Roadmap](#roadmap)
|
55
|
+
- [Contributing](#contributing)
|
56
|
+
- [License](#license)
|
57
|
+
|
58
|
+
## 🎹 Getting Started
|
59
|
+
|
60
|
+
To get started, check out the following example or jump straight into one of the [Examples](./examples/):
|
61
|
+
|
62
|
+
- 🦜⛓️ [LangGraph](./examples/langgraph/)
|
63
|
+
|
64
|
+
### Example
|
65
|
+
|
66
|
+
**GoHumanLoop** currently supports `Python`.
|
67
|
+
|
68
|
+
- Installation
|
69
|
+
|
70
|
+
```shell
|
71
|
+
pip install gohumanloop
|
72
|
+
```
|
73
|
+
|
74
|
+
- Example
|
75
|
+
|
76
|
+
The following example enhances [the official LangGraph example](https://langchain-ai.github.io/langgraph/tutorials/get-started/4-human-in-the-loop/#5-resume-execution) with `human-in-the-loop` functionality.
|
77
|
+
|
78
|
+
> 💡 By default, it uses `Terminal` as the `langgraph_adapter` for human interaction.
|
79
|
+
|
80
|
+
```python
|
81
|
+
import os
|
82
|
+
from langchain.chat_models import init_chat_model
|
83
|
+
from typing import Annotated
|
84
|
+
|
85
|
+
from langchain_tavily import TavilySearch
|
86
|
+
from langchain_core.tools import tool
|
87
|
+
from typing_extensions import TypedDict
|
88
|
+
|
89
|
+
from langgraph.checkpoint.memory import MemorySaver
|
90
|
+
from langgraph.graph import StateGraph, START, END
|
91
|
+
from langgraph.graph.message import add_messages
|
92
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
93
|
+
|
94
|
+
# from langgraph.types import Command, interrupt # Don't use langgraph, use gohumanloop instead
|
95
|
+
|
96
|
+
from gohumanloop.adapters.langgraph_adapter import interrupt, create_resume_command
|
97
|
+
|
98
|
+
os.environ["DEEPSEEK_API_KEY"] = "sk-xxx"
|
99
|
+
os.environ["TAVILY_API_KEY"] = "tvly-xxx"
|
100
|
+
|
101
|
+
llm = init_chat_model("deepseek:deepseek-chat")
|
102
|
+
|
103
|
+
class State(TypedDict):
|
104
|
+
messages: Annotated[list, add_messages]
|
105
|
+
|
106
|
+
graph_builder = StateGraph(State)
|
107
|
+
|
108
|
+
@tool
|
109
|
+
def human_assistance(query: str) -> str:
|
110
|
+
"""Request assistance from a human."""
|
111
|
+
human_response = interrupt({"query": query})
|
112
|
+
return human_response
|
113
|
+
|
114
|
+
tool = TavilySearch(max_results=2)
|
115
|
+
tools = [tool, human_assistance]
|
116
|
+
llm_with_tools = llm.bind_tools(tools)
|
117
|
+
|
118
|
+
def chatbot(state: State):
|
119
|
+
message = llm_with_tools.invoke(state["messages"])
|
120
|
+
# Because we will be interrupting during tool execution,
|
121
|
+
# we disable parallel tool calling to avoid repeating any
|
122
|
+
# tool invocations when we resume.
|
123
|
+
assert len(message.tool_calls) <= 1
|
124
|
+
return {"messages": [message]}
|
125
|
+
|
126
|
+
graph_builder.add_node("chatbot", chatbot)
|
127
|
+
|
128
|
+
tool_node = ToolNode(tools=tools)
|
129
|
+
graph_builder.add_node("tools", tool_node)
|
130
|
+
|
131
|
+
graph_builder.add_conditional_edges(
|
132
|
+
"chatbot",
|
133
|
+
tools_condition,
|
134
|
+
)
|
135
|
+
graph_builder.add_edge("tools", "chatbot")
|
136
|
+
graph_builder.add_edge(START, "chatbot")
|
137
|
+
|
138
|
+
memory = MemorySaver()
|
139
|
+
|
140
|
+
graph = graph_builder.compile(checkpointer=memory)
|
141
|
+
|
142
|
+
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
|
143
|
+
config = {"configurable": {"thread_id": "1"}}
|
144
|
+
|
145
|
+
events = graph.stream(
|
146
|
+
{"messages": [{"role": "user", "content": user_input}]},
|
147
|
+
config,
|
148
|
+
stream_mode="values",
|
149
|
+
)
|
150
|
+
for event in events:
|
151
|
+
if "messages" in event:
|
152
|
+
event["messages"][-1].pretty_print()
|
153
|
+
|
154
|
+
# LangGraph code:
|
155
|
+
# human_response = (
|
156
|
+
# "We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
|
157
|
+
# "It's much more reliable and extensible than simple autonomous agents."
|
158
|
+
# )
|
159
|
+
|
160
|
+
# human_command = Command(resume={"data": human_response})
|
161
|
+
|
162
|
+
# GoHumanLoop code:
|
163
|
+
human_command = create_resume_command() # Use this command to resume the execution,instead of using the command above
|
164
|
+
|
165
|
+
events = graph.stream(human_command, config, stream_mode="values")
|
166
|
+
for event in events:
|
167
|
+
if "messages" in event:
|
168
|
+
event["messages"][-1].pretty_print()
|
169
|
+
|
170
|
+
```
|
171
|
+
|
172
|
+
## 🎵 Why GoHumanloop?
|
173
|
+
|
174
|
+
### Human-in-the-loop
|
175
|
+
|
176
|
+
<div align="center">
|
177
|
+
<img height=240 src="http://cdn.oyster-iot.cloud/202505210851404.png"><br>
|
178
|
+
<b face="雅黑">Even with state-of-the-art agentic reasoning and prompt routing, LLMs are not sufficiently reliable to be given access to high-stakes functions without human oversight</b>
|
179
|
+
</div>
|
180
|
+
<br>
|
181
|
+
|
182
|
+
`Human-in-the-loop` is an AI system design philosophy that integrates human judgment and supervision into AI decision-making processes. This concept is particularly important in AI Agent systems:
|
183
|
+
|
184
|
+
- **Safety Assurance**: Allows human intervention and review at critical decision points to prevent potentially harmful AI decisions
|
185
|
+
- **Quality Control**: Improves accuracy and reliability of AI outputs through expert feedback
|
186
|
+
- **Continuous Learning**: AI systems can learn and improve from human feedback, creating a virtuous cycle
|
187
|
+
- **Clear Accountability**: Maintains ultimate human control over important decisions with clear responsibility
|
188
|
+
|
189
|
+
In practice, Human-in-the-loop can take various forms - from simple decision confirmation to deep human-AI collaborative dialogues - ensuring optimal balance between autonomy and human oversight to maximize the potential of AI Agent systems.
|
190
|
+
|
191
|
+
#### Typical Use Cases
|
192
|
+
|
193
|
+
<div align="center">
|
194
|
+
<img height=120 src="http://cdn.oyster-iot.cloud/tool-call-review.png"><br>
|
195
|
+
<b face="雅黑"> A human can review and edit the output from the agent before proceeding. This is particularly critical in applications where the tool calls requested may be sensitive or require human oversight.</b>
|
196
|
+
</div>
|
197
|
+
<br>
|
198
|
+
|
199
|
+
- 🛠️ Tool Call Review: Humans can review, edit or approve tool call requests initiated by LLMs before execution
|
200
|
+
- ✅ Model Output Verification: Humans can review, edit or approve content generated by LLMs (text, decisions, etc.)
|
201
|
+
- 💡 Context Provision: Allows LLMs to actively request human input for clarification, additional details or multi-turn conversation context
|
202
|
+
|
203
|
+
### Secure and Efficient Go➡Humanloop
|
204
|
+
|
205
|
+
`GoHumanloop` provides a set of tools deeply integrated within AI Agents to ensure constant `Human-in-the-loop` oversight. It deterministically ensures high-risk function calls must undergo human review while also enabling human expert feedback, thereby improving AI system reliability and safety while reducing risks from LLM hallucinations.
|
206
|
+
|
207
|
+
<div align="center">
|
208
|
+
<img height=420 src="http://cdn.oyster-iot.cloud/202505210943862.png"><br>
|
209
|
+
<b face="雅黑"> The Outer-Loop and Inversion of Control</b>
|
210
|
+
</div>
|
211
|
+
<br>
|
212
|
+
|
213
|
+
Through `GoHumanloop`'s encapsulation, you can implement secure and efficient `Human-in-the-loop` when requesting tools, Agent nodes, MCP services and other Agents.
|
214
|
+
|
215
|
+
## 📚 Key Features
|
216
|
+
|
217
|
+
<div align="center">
|
218
|
+
<img height=360 src="http://cdn.oyster-iot.cloud/202505211030197.png"><br>
|
219
|
+
<b face="雅黑"> GoHumanLoop Architecture</b>
|
220
|
+
</div>
|
221
|
+
<br>
|
222
|
+
|
223
|
+
`GoHumanloop` offers the following core capabilities:
|
224
|
+
|
225
|
+
- **Approval:** Requests human review or approval when executing specific tool calls or Agent nodes
|
226
|
+
- **Information:** Obtains critical human input during task execution to reduce LLM hallucination risks
|
227
|
+
- **Conversation:** Enables multi-turn interactions with humans through dialogue to acquire richer contextual information
|
228
|
+
- **Framework-specific Integration:** Provides specialized integration methods for specific Agent frameworks, such as `interrupt` and `resume` for `LangGraph`
|
229
|
+
|
230
|
+
## 📅 Roadmap
|
231
|
+
|
232
|
+
| Feature | Status |
|
233
|
+
| ----------------- | ---------- |
|
234
|
+
| Approval | ⚙️ Beta |
|
235
|
+
| Information | ⚙️ Beta |
|
236
|
+
| Conversation | ⚙️ Beta |
|
237
|
+
| Email Provider | ⚙️ Beta |
|
238
|
+
| Terminal Provider | ⚙️ Beta |
|
239
|
+
| API Provider | ⚙️ Beta |
|
240
|
+
| Default Manager | ⚙️ Beta |
|
241
|
+
| GLH Manager | 🗓️ Planned |
|
242
|
+
| Langchain Support | ⚙️ Beta |
|
243
|
+
| CrewAI Support | 🗓️ Planned |
|
244
|
+
|
245
|
+
- 💡 GLH Manager - GoHumanLoop Manager will integrate with the upcoming GoHumanLoop Hub platform to provide users with more flexible management options.
|
246
|
+
|
247
|
+
## 🤝 Contributing
|
248
|
+
|
249
|
+
The GoHumanLoop SDK and documentation are open source. We welcome contributions in the form of issues, documentation and PRs. For more details, please see [CONTRIBUTING.md](./CONTRIBUTING.md)
|
250
|
+
|
251
|
+
## 📱 Contact
|
252
|
+
|
253
|
+
<img height=300 src="http://cdn.oyster-iot.cloud/202505231802103.png"/>
|
254
|
+
|
255
|
+
🎉 If you're interested in this project, feel free to scan the QR code to contact the author.
|
256
|
+
|
257
|
+
## 🌟 Star History
|
258
|
+
|
259
|
+
[](https://www.star-history.com/#gohumanloop/gohumanloop&Date)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
gohumanloop/__init__.py,sha256=oJqVOtRRDHNxwxZ2Blw7ycBRwcTBMTURUCx-XcL2YJ0,1969
|
2
|
+
gohumanloop/__main__.py,sha256=zdGKN92H9SgwZfL4xLqPkE1YaiRcHhVg_GqC-H1VurA,75
|
3
|
+
gohumanloop/adapters/__init__.py,sha256=cFXd9qzyR0oZg8mR912PShj2b_3BAM_RO_cIVMXVdWQ,390
|
4
|
+
gohumanloop/adapters/langgraph_adapter.py,sha256=il_zsQMdgbJrwIn_ucQiH1ku3JhozxIQ7RH82KqTLwA,36750
|
5
|
+
gohumanloop/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
gohumanloop/cli/main.py,sha256=Txjk31MlkiX9zeHZfFEXEu0s2IBESY8sxrxBEzZKk2U,767
|
7
|
+
gohumanloop/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
gohumanloop/core/interface.py,sha256=D59FR4Bj-7gCFXw_55pPcFBjG2bY-EI5sYZl46tBaB0,21476
|
9
|
+
gohumanloop/core/manager.py,sha256=xyJJItJvt5ZPudAa3kWy282jn-I5yCsLJYNPaJyIQLI,29906
|
10
|
+
gohumanloop/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
gohumanloop/manager/ghl_manager.py,sha256=rmbfaS5mAb9c8RPr9Id24vO5DJT9ZC4D282dHZ26SJE,22695
|
12
|
+
gohumanloop/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
gohumanloop/models/api_model.py,sha256=1LtSQdx2ZVMzTdPrOVGe1JE1Cn_B0CnNQDp1-LlnqLw,2950
|
14
|
+
gohumanloop/models/glh_model.py,sha256=gix4f_57G4URkwXhCspWhU08Sf1MKu9FtUCx424Vf3A,940
|
15
|
+
gohumanloop/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
gohumanloop/providers/api_provider.py,sha256=kgduU2udtBUaaMHOjDic7v3bb5kTJeedmlLRpcaOgJM,25771
|
17
|
+
gohumanloop/providers/base.py,sha256=JHvkInxLIWkUvypR3fphRGgisSMOkutpl-fRnYSHdOw,21893
|
18
|
+
gohumanloop/providers/email_provider.py,sha256=n4-k4m3oQ8TocT-yxGLTwUMTXfgddFiGxEKMq9W6FGA,44704
|
19
|
+
gohumanloop/providers/ghl_provider.py,sha256=NmOac2LR0d87pc91sMuYtvTYsKdp4wZrhfmSYuZL2ko,2350
|
20
|
+
gohumanloop/providers/terminal_provider.py,sha256=6YiATcbc6TIYrmzT2BSd9U6gNnE6o8TX4HO1M5Ejxgc,14613
|
21
|
+
gohumanloop/utils/__init__.py,sha256=l0e3lnOVrt6HwaxidNGFXORX4awCKemhJZ_t3k-5u-s,124
|
22
|
+
gohumanloop/utils/context_formatter.py,sha256=sAWrKJpxmsHga6gsyBwFBlAsHW8bjR1hkaGhk1PoE1M,2064
|
23
|
+
gohumanloop/utils/threadsafedict.py,sha256=9uyewnwmvS3u1fCx3SK0YWFxHMcyIwlye1Ev7WW7WHA,9588
|
24
|
+
gohumanloop/utils/utils.py,sha256=-bti65OaVh5dlvc5BIgiTw6dqxmqdKnOQHE0V-LKjek,2107
|
25
|
+
gohumanloop-0.0.6.dist-info/licenses/LICENSE,sha256=-U5tuCcSpndQwSKWtZbFbazb-_AtZcZL2kQgHbSLg-M,1064
|
26
|
+
gohumanloop-0.0.6.dist-info/METADATA,sha256=waWXbFIFOXC9loVTl4OJUTB-UbjSae_TZcXLIrMek8k,10057
|
27
|
+
gohumanloop-0.0.6.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
28
|
+
gohumanloop-0.0.6.dist-info/entry_points.txt,sha256=wM6jqRRD8bQXkvIduRVCuAJIlbyWg_F5EDXo5OZ_PwY,88
|
29
|
+
gohumanloop-0.0.6.dist-info/top_level.txt,sha256=LvOXBqS6Mspmcuqp81uz0Vjx_m_YI0w06DOPCiI1BfY,12
|
30
|
+
gohumanloop-0.0.6.dist-info/RECORD,,
|