gohumanloop 0.0.6__py3-none-any.whl → 0.0.7__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/adapters/__init__.py +2 -2
- gohumanloop/adapters/base_adapter.py +838 -0
- gohumanloop/adapters/langgraph_adapter.py +58 -691
- gohumanloop/core/interface.py +16 -5
- gohumanloop/core/manager.py +43 -1
- {gohumanloop-0.0.6.dist-info → gohumanloop-0.0.7.dist-info}/METADATA +44 -5
- {gohumanloop-0.0.6.dist-info → gohumanloop-0.0.7.dist-info}/RECORD +11 -10
- {gohumanloop-0.0.6.dist-info → gohumanloop-0.0.7.dist-info}/WHEEL +1 -1
- {gohumanloop-0.0.6.dist-info → gohumanloop-0.0.7.dist-info}/entry_points.txt +0 -0
- {gohumanloop-0.0.6.dist-info → gohumanloop-0.0.7.dist-info}/licenses/LICENSE +0 -0
- {gohumanloop-0.0.6.dist-info → gohumanloop-0.0.7.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
|
|
1
1
|
from typing import (
|
2
|
-
cast,
|
3
2
|
Dict,
|
4
3
|
Any,
|
5
4
|
Optional,
|
@@ -7,23 +6,14 @@ from typing import (
|
|
7
6
|
Awaitable,
|
8
7
|
TypeVar,
|
9
8
|
Union,
|
10
|
-
Type,
|
11
|
-
AsyncIterator,
|
12
|
-
Iterator,
|
13
|
-
Coroutine,
|
14
9
|
)
|
15
|
-
from types import TracebackType
|
16
|
-
from functools import wraps
|
17
10
|
import asyncio
|
18
11
|
import uuid
|
19
12
|
import time
|
20
|
-
from inspect import iscoroutinefunction
|
21
|
-
from contextlib import asynccontextmanager, contextmanager
|
22
13
|
import logging
|
23
14
|
|
24
|
-
from gohumanloop.utils import run_async_safely
|
25
15
|
from gohumanloop.core.interface import (
|
26
|
-
|
16
|
+
HumanLoopRequest,
|
27
17
|
HumanLoopResult,
|
28
18
|
HumanLoopStatus,
|
29
19
|
HumanLoopType,
|
@@ -32,6 +22,7 @@ from gohumanloop.core.interface import (
|
|
32
22
|
)
|
33
23
|
from gohumanloop.core.manager import DefaultHumanLoopManager
|
34
24
|
from gohumanloop.providers.terminal_provider import TerminalProvider
|
25
|
+
from gohumanloop.adapters.base_adapter import HumanloopAdapter
|
35
26
|
|
36
27
|
logger = logging.getLogger(__name__)
|
37
28
|
|
@@ -71,707 +62,52 @@ if _SUPPORTS_INTERRUPT:
|
|
71
62
|
_SUPPORTS_INTERRUPT = False
|
72
63
|
|
73
64
|
|
74
|
-
class HumanLoopWrapper:
|
75
|
-
def __init__(
|
76
|
-
self,
|
77
|
-
decorator: Callable[[Any], Callable],
|
78
|
-
) -> None:
|
79
|
-
self.decorator = decorator
|
80
|
-
|
81
|
-
def wrap(self, fn: Callable) -> Callable:
|
82
|
-
return self.decorator(fn)
|
83
|
-
|
84
|
-
def __call__(self, fn: Callable) -> Callable:
|
85
|
-
return self.decorator(fn)
|
86
|
-
|
87
|
-
|
88
|
-
class LangGraphAdapter:
|
89
|
-
"""LangGraph adapter for simplifying human-in-the-loop integration
|
90
|
-
|
91
|
-
Provides decorators for three scenarios:
|
92
|
-
- require_approval: Requires human approval
|
93
|
-
- require_info: Requires human input information
|
94
|
-
- require_conversation: Requires multi-turn conversation
|
95
|
-
"""
|
96
|
-
|
97
|
-
def __init__(
|
98
|
-
self, manager: HumanLoopManager, default_timeout: Optional[int] = None
|
99
|
-
):
|
100
|
-
self.manager = manager
|
101
|
-
self.default_timeout = default_timeout
|
102
|
-
|
103
|
-
async def __aenter__(self) -> "LangGraphAdapter":
|
104
|
-
"""Implements async context manager protocol, automatically manages manager lifecycle"""
|
105
|
-
|
106
|
-
manager = cast(Any, self.manager)
|
107
|
-
if hasattr(manager, "__aenter__"):
|
108
|
-
await manager.__aenter__()
|
109
|
-
return self
|
110
|
-
|
111
|
-
async def __aexit__(
|
112
|
-
self,
|
113
|
-
exc_type: Optional[Type[BaseException]],
|
114
|
-
exc_val: Optional[BaseException],
|
115
|
-
exc_tb: Optional[TracebackType],
|
116
|
-
) -> Optional[bool]:
|
117
|
-
"""Implements async context manager protocol, automatically manages manager lifecycle"""
|
118
|
-
|
119
|
-
manager = cast(Any, self.manager)
|
120
|
-
if hasattr(manager, "__aexit__"):
|
121
|
-
await manager.__aexit__(exc_type, exc_val, exc_tb)
|
122
|
-
|
123
|
-
return None
|
124
|
-
|
125
|
-
def __enter__(self) -> "LangGraphAdapter":
|
126
|
-
"""Implements sync context manager protocol, automatically manages manager lifecycle"""
|
127
|
-
|
128
|
-
manager = cast(Any, self.manager)
|
129
|
-
if hasattr(manager, "__enter__"):
|
130
|
-
manager.__enter__()
|
131
|
-
return self
|
132
|
-
|
133
|
-
def __exit__(
|
134
|
-
self,
|
135
|
-
exc_type: Optional[Type[BaseException]],
|
136
|
-
exc_val: Optional[BaseException],
|
137
|
-
exc_tb: Optional[TracebackType],
|
138
|
-
) -> Optional[bool]:
|
139
|
-
"""Implements sync context manager protocol, automatically manages manager lifecycle"""
|
140
|
-
|
141
|
-
manager = cast(Any, self.manager)
|
142
|
-
if hasattr(manager, "__exit__"):
|
143
|
-
manager.__exit__(exc_type, exc_val, exc_tb)
|
144
|
-
|
145
|
-
return None
|
146
|
-
|
147
|
-
@asynccontextmanager
|
148
|
-
async def asession(self) -> AsyncIterator["LangGraphAdapter"]:
|
149
|
-
"""Provides async context manager for managing session lifecycle
|
150
|
-
|
151
|
-
Example:
|
152
|
-
async with adapter.session():
|
153
|
-
# Use adapter here
|
154
|
-
"""
|
155
|
-
try:
|
156
|
-
manager = cast(Any, self.manager)
|
157
|
-
if hasattr(manager, "__aenter__"):
|
158
|
-
await manager.__aenter__()
|
159
|
-
yield self
|
160
|
-
finally:
|
161
|
-
if hasattr(manager, "__aexit__"):
|
162
|
-
await manager.__aexit__(None, None, None)
|
163
|
-
|
164
|
-
@contextmanager
|
165
|
-
def session(self) -> Iterator["LangGraphAdapter"]:
|
166
|
-
"""Provides a synchronous context manager for managing session lifecycle
|
167
|
-
|
168
|
-
Example:
|
169
|
-
with adapter.sync_session():
|
170
|
-
# Use adapter here
|
171
|
-
"""
|
172
|
-
try:
|
173
|
-
manager = cast(Any, self.manager)
|
174
|
-
if hasattr(manager, "__enter__"):
|
175
|
-
manager.__enter__()
|
176
|
-
yield self
|
177
|
-
finally:
|
178
|
-
if hasattr(manager, "__exit__"):
|
179
|
-
manager.__exit__(None, None, None)
|
180
|
-
|
181
|
-
def require_approval(
|
182
|
-
self,
|
183
|
-
task_id: Optional[str] = None,
|
184
|
-
conversation_id: Optional[str] = None,
|
185
|
-
ret_key: str = "approval_result",
|
186
|
-
additional: Optional[str] = "",
|
187
|
-
metadata: Optional[Dict[str, Any]] = None,
|
188
|
-
provider_id: Optional[str] = None,
|
189
|
-
timeout: Optional[int] = None,
|
190
|
-
execute_on_reject: bool = False,
|
191
|
-
callback: Optional[
|
192
|
-
Union[HumanLoopCallback, Callable[[Any], HumanLoopCallback]]
|
193
|
-
] = None,
|
194
|
-
) -> HumanLoopWrapper:
|
195
|
-
"""Decorator for approval scenario"""
|
196
|
-
if task_id is None:
|
197
|
-
task_id = str(uuid.uuid4())
|
198
|
-
if conversation_id is None:
|
199
|
-
conversation_id = str(uuid.uuid4())
|
200
|
-
|
201
|
-
def decorator(fn: Callable) -> Callable:
|
202
|
-
return self._approve_cli(
|
203
|
-
fn,
|
204
|
-
task_id,
|
205
|
-
conversation_id,
|
206
|
-
ret_key,
|
207
|
-
additional,
|
208
|
-
metadata,
|
209
|
-
provider_id,
|
210
|
-
timeout,
|
211
|
-
execute_on_reject,
|
212
|
-
callback,
|
213
|
-
)
|
214
|
-
|
215
|
-
return HumanLoopWrapper(decorator)
|
216
|
-
|
217
|
-
def _approve_cli(
|
218
|
-
self,
|
219
|
-
fn: Callable[[T], R],
|
220
|
-
task_id: str,
|
221
|
-
conversation_id: str,
|
222
|
-
ret_key: str = "approval_result",
|
223
|
-
additional: Optional[str] = "",
|
224
|
-
metadata: Optional[Dict[str, Any]] = None,
|
225
|
-
provider_id: Optional[str] = None,
|
226
|
-
timeout: Optional[int] = None,
|
227
|
-
execute_on_reject: bool = False,
|
228
|
-
callback: Optional[
|
229
|
-
Union[HumanLoopCallback, Callable[[Any], HumanLoopCallback]]
|
230
|
-
] = None,
|
231
|
-
) -> Union[
|
232
|
-
Callable[[T], Coroutine[Any, Any, R]], # For async functions
|
233
|
-
Callable[[T], R], # For sync functions
|
234
|
-
]:
|
235
|
-
"""
|
236
|
-
Converts function type from Callable[[T], R] to Callable[[T], R]
|
237
|
-
|
238
|
-
Passes approval results through keyword arguments while maintaining original function signature
|
239
|
-
|
240
|
-
Benefits of this approach:
|
241
|
-
1. Maintains original function return type, keeping compatibility with LangGraph workflow
|
242
|
-
2. Decorated function can optionally use approval result information
|
243
|
-
3. Can pass richer approval context information
|
244
|
-
|
245
|
-
Parameters:
|
246
|
-
- fn: Target function to be decorated
|
247
|
-
- task_id: Unique task identifier for tracking approval requests
|
248
|
-
- conversation_id: Unique conversation identifier for tracking approval sessions
|
249
|
-
- ret_key: Parameter name used to inject approval results into function kwargs
|
250
|
-
- additional: Additional context information to show to approvers
|
251
|
-
- metadata: Optional metadata dictionary passed with request
|
252
|
-
- provider_id: Optional provider identifier to route requests
|
253
|
-
- timeout: Timeout in seconds for approval response
|
254
|
-
- execute_on_reject: Whether to execute function on rejection
|
255
|
-
- callback: Optional callback object or factory function for approval events
|
256
|
-
|
257
|
-
Returns:
|
258
|
-
- Decorated function maintaining original signature
|
259
|
-
- Raises ValueError if approval fails or is rejected
|
260
|
-
|
261
|
-
Notes:
|
262
|
-
- Decorated function must accept ret_key parameter to receive approval results
|
263
|
-
- If approval is rejected, execution depends on execute_on_reject parameter
|
264
|
-
- Approval results contain complete context including:
|
265
|
-
- conversation_id: Unique conversation identifier
|
266
|
-
- request_id: Unique request identifier
|
267
|
-
- loop_type: Type of human loop (APPROVAL)
|
268
|
-
- status: Current approval status
|
269
|
-
- response: Approver's response
|
270
|
-
- feedback: Optional approver feedback
|
271
|
-
- responded_by: Approver identity
|
272
|
-
- responded_at: Response timestamp
|
273
|
-
- error: Error information if any
|
274
|
-
"""
|
275
|
-
|
276
|
-
@wraps(fn)
|
277
|
-
async def async_wrapper(*args: Any, **kwargs: Any) -> R:
|
278
|
-
# Determine if callback is instance or factory function
|
279
|
-
cb = None
|
280
|
-
if callable(callback) and not isinstance(callback, HumanLoopCallback):
|
281
|
-
# Factory function, pass state
|
282
|
-
state = args[0] if args else None
|
283
|
-
cb = callback(state)
|
284
|
-
else:
|
285
|
-
cb = callback
|
286
|
-
|
287
|
-
result = await self.manager.async_request_humanloop(
|
288
|
-
task_id=task_id,
|
289
|
-
conversation_id=conversation_id,
|
290
|
-
loop_type=HumanLoopType.APPROVAL,
|
291
|
-
context={
|
292
|
-
"message": {
|
293
|
-
"function_name": fn.__name__,
|
294
|
-
"function_signature": str(fn.__code__.co_varnames),
|
295
|
-
"arguments": str(args),
|
296
|
-
"keyword_arguments": str(kwargs),
|
297
|
-
"documentation": fn.__doc__ or "No documentation available",
|
298
|
-
},
|
299
|
-
"question": "Please review and approve/reject this human loop execution.",
|
300
|
-
"additional": additional,
|
301
|
-
},
|
302
|
-
callback=cb,
|
303
|
-
metadata=metadata,
|
304
|
-
provider_id=provider_id,
|
305
|
-
timeout=timeout or self.default_timeout,
|
306
|
-
blocking=True,
|
307
|
-
)
|
308
|
-
|
309
|
-
# Initialize approval result object as None
|
310
|
-
approval_info = None
|
311
|
-
|
312
|
-
if isinstance(result, HumanLoopResult):
|
313
|
-
# If result is HumanLoopResult type, build complete approval info
|
314
|
-
approval_info = {
|
315
|
-
"conversation_id": result.conversation_id,
|
316
|
-
"request_id": result.request_id,
|
317
|
-
"loop_type": result.loop_type,
|
318
|
-
"status": result.status,
|
319
|
-
"response": result.response,
|
320
|
-
"feedback": result.feedback,
|
321
|
-
"responded_by": result.responded_by,
|
322
|
-
"responded_at": result.responded_at,
|
323
|
-
"error": result.error,
|
324
|
-
}
|
325
|
-
|
326
|
-
kwargs[ret_key] = approval_info
|
327
|
-
# Check approval result
|
328
|
-
if isinstance(result, HumanLoopResult):
|
329
|
-
# Handle based on approval status
|
330
|
-
if result.status == HumanLoopStatus.APPROVED:
|
331
|
-
if iscoroutinefunction(fn):
|
332
|
-
ret = await fn(*args, **kwargs)
|
333
|
-
else:
|
334
|
-
ret = fn(*args, **kwargs)
|
335
|
-
return cast(R, ret)
|
336
|
-
elif result.status == HumanLoopStatus.REJECTED:
|
337
|
-
# If execute on reject is set, run the function
|
338
|
-
if execute_on_reject:
|
339
|
-
if iscoroutinefunction(fn):
|
340
|
-
ret = await fn(*args, **kwargs)
|
341
|
-
else:
|
342
|
-
ret = fn(*args, **kwargs)
|
343
|
-
return cast(R, ret)
|
344
|
-
# Otherwise return rejection info
|
345
|
-
reason = result.response
|
346
|
-
raise ValueError(
|
347
|
-
f"Function {fn.__name__} execution not approved: {reason}"
|
348
|
-
)
|
349
|
-
else:
|
350
|
-
raise ValueError(
|
351
|
-
f"Approval error for {fn.__name__}: approval status: {result.status} and {result.error}"
|
352
|
-
)
|
353
|
-
else:
|
354
|
-
raise ValueError(f"Unknown approval error: {fn.__name__}")
|
355
|
-
|
356
|
-
@wraps(fn)
|
357
|
-
def sync_wrapper(*args: Any, **kwargs: Any) -> R:
|
358
|
-
ret = run_async_safely(async_wrapper(*args, **kwargs))
|
359
|
-
return cast(R, ret)
|
360
|
-
|
361
|
-
# Return corresponding wrapper based on decorated function type
|
362
|
-
if iscoroutinefunction(fn):
|
363
|
-
return async_wrapper
|
364
|
-
return sync_wrapper
|
365
|
-
|
366
|
-
def require_conversation(
|
367
|
-
self,
|
368
|
-
task_id: Optional[str] = None,
|
369
|
-
conversation_id: Optional[str] = None,
|
370
|
-
state_key: str = "conv_info",
|
371
|
-
ret_key: str = "conv_result",
|
372
|
-
additional: Optional[str] = "",
|
373
|
-
provider_id: Optional[str] = None,
|
374
|
-
metadata: Optional[Dict[str, Any]] = None,
|
375
|
-
timeout: Optional[int] = None,
|
376
|
-
callback: Optional[
|
377
|
-
Union[HumanLoopCallback, Callable[[Any], HumanLoopCallback]]
|
378
|
-
] = None,
|
379
|
-
) -> HumanLoopWrapper:
|
380
|
-
"""Decorator for multi-turn conversation scenario"""
|
381
|
-
|
382
|
-
if task_id is None:
|
383
|
-
task_id = str(uuid.uuid4())
|
384
|
-
if conversation_id is None:
|
385
|
-
conversation_id = str(uuid.uuid4())
|
386
|
-
|
387
|
-
def decorator(fn: Callable) -> Callable:
|
388
|
-
return self._conversation_cli(
|
389
|
-
fn,
|
390
|
-
task_id,
|
391
|
-
conversation_id,
|
392
|
-
state_key,
|
393
|
-
ret_key,
|
394
|
-
additional,
|
395
|
-
metadata,
|
396
|
-
provider_id,
|
397
|
-
timeout,
|
398
|
-
callback,
|
399
|
-
)
|
400
|
-
|
401
|
-
return HumanLoopWrapper(decorator)
|
402
|
-
|
403
|
-
def _conversation_cli(
|
404
|
-
self,
|
405
|
-
fn: Callable[[T], R],
|
406
|
-
task_id: str,
|
407
|
-
conversation_id: str,
|
408
|
-
state_key: str = "conv_info",
|
409
|
-
ret_key: str = "conv_result",
|
410
|
-
additional: Optional[str] = "",
|
411
|
-
metadata: Optional[Dict[str, Any]] = None,
|
412
|
-
provider_id: Optional[str] = None,
|
413
|
-
timeout: Optional[int] = None,
|
414
|
-
callback: Optional[
|
415
|
-
Union[HumanLoopCallback, Callable[[Any], HumanLoopCallback]]
|
416
|
-
] = None,
|
417
|
-
) -> Union[
|
418
|
-
Callable[[T], Coroutine[Any, Any, R]], # For async functions
|
419
|
-
Callable[[T], R], # For sync functions
|
420
|
-
]:
|
421
|
-
"""Internal decorator implementation for multi-turn conversation scenario
|
422
|
-
|
423
|
-
Converts function type from Callable[[T], R] to Callable[[T], R]
|
424
|
-
|
425
|
-
Main features:
|
426
|
-
1. Conduct multi-turn conversations through human-machine interaction
|
427
|
-
2. Inject conversation results into function parameters via ret_key
|
428
|
-
3. Support both synchronous and asynchronous function calls
|
429
|
-
|
430
|
-
Parameters:
|
431
|
-
- fn: Target function to be decorated
|
432
|
-
- task_id: Unique task identifier for tracking human interaction requests
|
433
|
-
- conversation_id: Unique conversation identifier for tracking interaction sessions
|
434
|
-
- state_key: Key name used to get conversation input info from state
|
435
|
-
- ret_key: Parameter name used to inject human interaction results into function kwargs
|
436
|
-
- additional: Additional context information to show to users
|
437
|
-
- metadata: Optional metadata dictionary passed along with request
|
438
|
-
- provider_id: Optional provider identifier to route requests to specific provider
|
439
|
-
- timeout: Timeout in seconds for human response, defaults to adapter's default_timeout
|
440
|
-
- callback: Optional callback object or factory function for handling human interaction events
|
441
|
-
|
442
|
-
Returns:
|
443
|
-
- Decorated function maintaining original signature
|
444
|
-
- Raises ValueError if human interaction fails
|
445
|
-
|
446
|
-
Notes:
|
447
|
-
- Decorated function must accept ret_key parameter to receive interaction results
|
448
|
-
- Interaction results contain complete context information including:
|
449
|
-
- conversation_id: Unique conversation identifier
|
450
|
-
- request_id: Unique request identifier
|
451
|
-
- loop_type: Human interaction type (CONVERSATION)
|
452
|
-
- status: Current request status
|
453
|
-
- response: Human provided response
|
454
|
-
- feedback: Optional human feedback
|
455
|
-
- responded_by: Responder identity
|
456
|
-
- responded_at: Response timestamp
|
457
|
-
- error: Error information if any
|
458
|
-
- Automatically adapts to async and sync functions
|
459
|
-
"""
|
460
|
-
|
461
|
-
@wraps(fn)
|
462
|
-
async def async_wrapper(*args: Any, **kwargs: Any) -> R:
|
463
|
-
# Determine if callback is instance or factory function
|
464
|
-
cb = None
|
465
|
-
state = args[0] if args else None
|
466
|
-
if callable(callback) and not isinstance(callback, HumanLoopCallback):
|
467
|
-
cb = callback(state)
|
468
|
-
else:
|
469
|
-
cb = callback
|
470
|
-
|
471
|
-
node_input = None
|
472
|
-
if state:
|
473
|
-
# Get input information from key fields in State
|
474
|
-
node_input = state.get(state_key, {})
|
475
|
-
|
476
|
-
# Compose question content
|
477
|
-
question_content = (
|
478
|
-
f"Please respond to the following information:\n{node_input}"
|
479
|
-
)
|
480
|
-
|
481
|
-
# Check if conversation exists to determine whether to use request_humanloop or continue_humanloop
|
482
|
-
conversation_requests = await self.manager.async_check_conversation_exist(
|
483
|
-
task_id, conversation_id
|
484
|
-
)
|
485
|
-
|
486
|
-
result = None
|
487
|
-
if conversation_requests:
|
488
|
-
# Existing conversation, use continue_humanloop
|
489
|
-
result = await self.manager.async_continue_humanloop(
|
490
|
-
conversation_id=conversation_id,
|
491
|
-
context={
|
492
|
-
"message": {
|
493
|
-
"function_name": fn.__name__,
|
494
|
-
"function_signature": str(fn.__code__.co_varnames),
|
495
|
-
"arguments": str(args),
|
496
|
-
"keyword_arguments": str(kwargs),
|
497
|
-
"documentation": fn.__doc__ or "No documentation available",
|
498
|
-
},
|
499
|
-
"question": question_content,
|
500
|
-
"additional": additional,
|
501
|
-
},
|
502
|
-
timeout=timeout or self.default_timeout,
|
503
|
-
callback=cb,
|
504
|
-
metadata=metadata,
|
505
|
-
provider_id=provider_id,
|
506
|
-
blocking=True,
|
507
|
-
)
|
508
|
-
else:
|
509
|
-
# New conversation, use request_humanloop
|
510
|
-
result = await self.manager.async_request_humanloop(
|
511
|
-
task_id=task_id,
|
512
|
-
conversation_id=conversation_id,
|
513
|
-
loop_type=HumanLoopType.CONVERSATION,
|
514
|
-
context={
|
515
|
-
"message": {
|
516
|
-
"function_name": fn.__name__,
|
517
|
-
"function_signature": str(fn.__code__.co_varnames),
|
518
|
-
"arguments": str(args),
|
519
|
-
"keyword_arguments": str(kwargs),
|
520
|
-
"documentation": fn.__doc__ or "No documentation available",
|
521
|
-
},
|
522
|
-
"question": question_content,
|
523
|
-
"additional": additional,
|
524
|
-
},
|
525
|
-
timeout=timeout or self.default_timeout,
|
526
|
-
callback=cb,
|
527
|
-
metadata=metadata,
|
528
|
-
provider_id=provider_id,
|
529
|
-
blocking=True,
|
530
|
-
)
|
531
|
-
|
532
|
-
# Initialize conversation result object as None
|
533
|
-
conversation_info = None
|
534
|
-
|
535
|
-
if isinstance(result, HumanLoopResult):
|
536
|
-
conversation_info = {
|
537
|
-
"conversation_id": result.conversation_id,
|
538
|
-
"request_id": result.request_id,
|
539
|
-
"loop_type": result.loop_type,
|
540
|
-
"status": result.status,
|
541
|
-
"response": result.response,
|
542
|
-
"feedback": result.feedback,
|
543
|
-
"responded_by": result.responded_by,
|
544
|
-
"responded_at": result.responded_at,
|
545
|
-
"error": result.error,
|
546
|
-
}
|
547
|
-
|
548
|
-
kwargs[ret_key] = conversation_info
|
549
|
-
|
550
|
-
if isinstance(result, HumanLoopResult):
|
551
|
-
if iscoroutinefunction(fn):
|
552
|
-
ret = await fn(*args, **kwargs)
|
553
|
-
else:
|
554
|
-
ret = fn(*args, **kwargs)
|
555
|
-
return cast(R, ret)
|
556
|
-
else:
|
557
|
-
raise ValueError(
|
558
|
-
f"Conversation request timeout or error for {fn.__name__}"
|
559
|
-
)
|
560
|
-
|
561
|
-
@wraps(fn)
|
562
|
-
def sync_wrapper(*args: Any, **kwargs: Any) -> R:
|
563
|
-
ret = run_async_safely(async_wrapper(*args, **kwargs))
|
564
|
-
return cast(R, ret)
|
565
|
-
|
566
|
-
if iscoroutinefunction(fn):
|
567
|
-
return async_wrapper
|
568
|
-
return sync_wrapper
|
569
|
-
|
570
|
-
def require_info(
|
571
|
-
self,
|
572
|
-
task_id: Optional[str] = None,
|
573
|
-
conversation_id: Optional[str] = None,
|
574
|
-
ret_key: str = "info_result",
|
575
|
-
additional: Optional[str] = "",
|
576
|
-
metadata: Optional[Dict[str, Any]] = None,
|
577
|
-
provider_id: Optional[str] = None,
|
578
|
-
timeout: Optional[int] = None,
|
579
|
-
callback: Optional[
|
580
|
-
Union[HumanLoopCallback, Callable[[Any], HumanLoopCallback]]
|
581
|
-
] = None,
|
582
|
-
) -> HumanLoopWrapper:
|
583
|
-
"""Decorator for information gathering scenario"""
|
584
|
-
|
585
|
-
if task_id is None:
|
586
|
-
task_id = str(uuid.uuid4())
|
587
|
-
if conversation_id is None:
|
588
|
-
conversation_id = str(uuid.uuid4())
|
589
|
-
|
590
|
-
def decorator(fn: Callable) -> Callable:
|
591
|
-
return self._get_info_cli(
|
592
|
-
fn,
|
593
|
-
task_id,
|
594
|
-
conversation_id,
|
595
|
-
ret_key,
|
596
|
-
additional,
|
597
|
-
metadata,
|
598
|
-
provider_id,
|
599
|
-
timeout,
|
600
|
-
callback,
|
601
|
-
)
|
602
|
-
|
603
|
-
return HumanLoopWrapper(decorator)
|
604
|
-
|
605
|
-
def _get_info_cli(
|
606
|
-
self,
|
607
|
-
fn: Callable[[T], R],
|
608
|
-
task_id: str,
|
609
|
-
conversation_id: str,
|
610
|
-
ret_key: str = "info_result",
|
611
|
-
additional: Optional[str] = "",
|
612
|
-
metadata: Optional[Dict[str, Any]] = None,
|
613
|
-
provider_id: Optional[str] = None,
|
614
|
-
timeout: Optional[int] = None,
|
615
|
-
callback: Optional[
|
616
|
-
Union[HumanLoopCallback, Callable[[Any], HumanLoopCallback]]
|
617
|
-
] = None,
|
618
|
-
) -> Union[
|
619
|
-
Callable[[T], Coroutine[Any, Any, R]], # For async functions
|
620
|
-
Callable[[T], R], # For sync functions
|
621
|
-
]:
|
622
|
-
"""Internal decorator implementation for information gathering scenario
|
623
|
-
Converts function type from Callable[[T], R] to Callable[[T], R]
|
624
|
-
|
625
|
-
Main features:
|
626
|
-
1. Get required information through human-machine interaction
|
627
|
-
2. Inject obtained information into function parameters via ret_key
|
628
|
-
3. Support both synchronous and asynchronous function calls
|
629
|
-
|
630
|
-
Parameters:
|
631
|
-
- fn: Target function to be decorated
|
632
|
-
- task_id: Unique task identifier for tracking the human loop request
|
633
|
-
- conversation_id: Unique conversation identifier for tracking the interaction session
|
634
|
-
- ret_key: Parameter name used to inject the human loop result into function kwargs
|
635
|
-
- additional: Additional context information to be shown to human user
|
636
|
-
- metadata: Optional metadata dictionary to be passed with the request
|
637
|
-
- provider_id: Optional provider identifier to route request to specific provider
|
638
|
-
- timeout: Timeout in seconds for human response, defaults to adapter's default_timeout
|
639
|
-
- callback: Optional callback object or factory function for handling human loop events
|
640
|
-
|
641
|
-
Returns:
|
642
|
-
- Decorated function maintaining original signature
|
643
|
-
- Raises ValueError if human interaction fails
|
644
|
-
|
645
|
-
Notes:
|
646
|
-
- Decorated function must accept ret_key parameter to receive interaction results
|
647
|
-
- Interaction results contain complete context information including:
|
648
|
-
- conversation_id: Unique conversation identifier
|
649
|
-
- request_id: Unique request identifier
|
650
|
-
- loop_type: Type of human loop (INFORMATION)
|
651
|
-
- status: Current status of the request
|
652
|
-
- response: Human provided response
|
653
|
-
- feedback: Optional feedback from human
|
654
|
-
- responded_by: Identity of responder
|
655
|
-
- responded_at: Response timestamp
|
656
|
-
- error: Error information if any
|
657
|
-
- Automatically adapts to async and sync functions
|
658
|
-
"""
|
659
|
-
|
660
|
-
@wraps(fn)
|
661
|
-
async def async_wrapper(*args: Any, **kwargs: Any) -> R:
|
662
|
-
# Determine if callback is an instance or factory function
|
663
|
-
# callback: can be HumanLoopCallback instance or factory function
|
664
|
-
# - If factory function: accepts state parameter and returns HumanLoopCallback instance
|
665
|
-
# - If HumanLoopCallback instance: use directly
|
666
|
-
cb = None
|
667
|
-
if callable(callback) and not isinstance(callback, HumanLoopCallback):
|
668
|
-
# Factory function mode: get state from args and create callback instance
|
669
|
-
# state is typically the first argument, None if args is empty
|
670
|
-
state = args[0] if args else None
|
671
|
-
cb = callback(state)
|
672
|
-
else:
|
673
|
-
cb = callback
|
674
|
-
|
675
|
-
result = await self.manager.async_request_humanloop(
|
676
|
-
task_id=task_id,
|
677
|
-
conversation_id=conversation_id,
|
678
|
-
loop_type=HumanLoopType.INFORMATION,
|
679
|
-
context={
|
680
|
-
"message": {
|
681
|
-
"function_name": fn.__name__,
|
682
|
-
"function_signature": str(fn.__code__.co_varnames),
|
683
|
-
"arguments": str(args),
|
684
|
-
"keyword_arguments": str(kwargs),
|
685
|
-
"documentation": fn.__doc__ or "No documentation available",
|
686
|
-
},
|
687
|
-
"question": "Please provide the required information for the human loop",
|
688
|
-
"additional": additional,
|
689
|
-
},
|
690
|
-
timeout=timeout or self.default_timeout,
|
691
|
-
callback=cb,
|
692
|
-
metadata=metadata,
|
693
|
-
provider_id=provider_id,
|
694
|
-
blocking=True,
|
695
|
-
)
|
696
|
-
|
697
|
-
# 初始化审批结果对象为None
|
698
|
-
resp_info = None
|
699
|
-
|
700
|
-
if isinstance(result, HumanLoopResult):
|
701
|
-
# 如果结果是HumanLoopResult类型,则构建完整的审批信息
|
702
|
-
resp_info = {
|
703
|
-
"conversation_id": result.conversation_id,
|
704
|
-
"request_id": result.request_id,
|
705
|
-
"loop_type": result.loop_type,
|
706
|
-
"status": result.status,
|
707
|
-
"response": result.response,
|
708
|
-
"feedback": result.feedback,
|
709
|
-
"responded_by": result.responded_by,
|
710
|
-
"responded_at": result.responded_at,
|
711
|
-
"error": result.error,
|
712
|
-
}
|
713
|
-
|
714
|
-
kwargs[ret_key] = resp_info
|
715
|
-
|
716
|
-
# 检查结果是否有效
|
717
|
-
if isinstance(result, HumanLoopResult):
|
718
|
-
# 返回获取信息结果,由用户去判断是否使用
|
719
|
-
if iscoroutinefunction(fn):
|
720
|
-
ret = await fn(*args, **kwargs)
|
721
|
-
else:
|
722
|
-
ret = fn(*args, **kwargs)
|
723
|
-
return cast(R, ret)
|
724
|
-
else:
|
725
|
-
raise ValueError(f"Info request timeout or error for {fn.__name__}")
|
726
|
-
|
727
|
-
@wraps(fn)
|
728
|
-
def sync_wrapper(*args: Any, **kwargs: Any) -> R:
|
729
|
-
ret = run_async_safely(async_wrapper(*args, **kwargs))
|
730
|
-
return cast(R, ret)
|
731
|
-
|
732
|
-
# 根据被装饰函数类型返回对应的wrapper
|
733
|
-
if iscoroutinefunction(fn):
|
734
|
-
return async_wrapper
|
735
|
-
return sync_wrapper
|
736
|
-
|
737
|
-
|
738
65
|
class LangGraphHumanLoopCallback(HumanLoopCallback):
|
739
66
|
"""LangGraph-specific human loop callback, compatible with TypedDict or Pydantic BaseModel State"""
|
740
67
|
|
741
68
|
def __init__(
|
742
69
|
self,
|
743
70
|
state: Any,
|
71
|
+
async_on_request: Optional[
|
72
|
+
Callable[[Any, HumanLoopProvider, HumanLoopRequest], Awaitable[Any]]
|
73
|
+
] = None,
|
744
74
|
async_on_update: Optional[
|
745
|
-
Callable[[Any, HumanLoopProvider, HumanLoopResult], Awaitable[
|
75
|
+
Callable[[Any, HumanLoopProvider, HumanLoopResult], Awaitable[Any]]
|
746
76
|
] = None,
|
747
77
|
async_on_timeout: Optional[
|
748
|
-
Callable[[Any, HumanLoopProvider], Awaitable[
|
78
|
+
Callable[[Any, HumanLoopProvider, HumanLoopResult], Awaitable[Any]]
|
749
79
|
] = None,
|
750
80
|
async_on_error: Optional[
|
751
|
-
Callable[[Any, HumanLoopProvider, Exception], Awaitable[
|
81
|
+
Callable[[Any, HumanLoopProvider, Exception], Awaitable[Any]]
|
752
82
|
] = None,
|
753
83
|
) -> None:
|
754
84
|
self.state = state
|
85
|
+
self.async_on_request = async_on_request
|
755
86
|
self.async_on_update = async_on_update
|
756
87
|
self.async_on_timeout = async_on_timeout
|
757
88
|
self.async_on_error = async_on_error
|
758
89
|
|
90
|
+
async def async_on_humanloop_request(
|
91
|
+
self, provider: HumanLoopProvider, request: HumanLoopRequest
|
92
|
+
) -> Any:
|
93
|
+
if self.async_on_request:
|
94
|
+
await self.async_on_request(self.state, provider, request)
|
95
|
+
|
759
96
|
async def async_on_humanloop_update(
|
760
97
|
self, provider: HumanLoopProvider, result: HumanLoopResult
|
761
|
-
) ->
|
98
|
+
) -> Any:
|
762
99
|
if self.async_on_update:
|
763
100
|
await self.async_on_update(self.state, provider, result)
|
764
101
|
|
765
102
|
async def async_on_humanloop_timeout(
|
766
|
-
self,
|
767
|
-
|
768
|
-
) -> None:
|
103
|
+
self, provider: HumanLoopProvider, result: HumanLoopResult
|
104
|
+
) -> Any:
|
769
105
|
if self.async_on_timeout:
|
770
|
-
await self.async_on_timeout(self.state, provider)
|
106
|
+
await self.async_on_timeout(self.state, provider, result)
|
771
107
|
|
772
108
|
async def async_on_humanloop_error(
|
773
109
|
self, provider: HumanLoopProvider, error: Exception
|
774
|
-
) ->
|
110
|
+
) -> Any:
|
775
111
|
if self.async_on_error:
|
776
112
|
await self.async_on_error(self.state, provider, error)
|
777
113
|
|
@@ -793,13 +129,31 @@ def default_langgraph_callback_factory(state: Any) -> LangGraphHumanLoopCallback
|
|
793
129
|
Configured LangGraphHumanLoopCallback instance
|
794
130
|
"""
|
795
131
|
|
132
|
+
async def async_on_request(
|
133
|
+
state: Any, provider: HumanLoopProvider, request: HumanLoopRequest
|
134
|
+
) -> Any:
|
135
|
+
"""Log human interaction request events"""
|
136
|
+
logger.info(f"Provider ID: {provider.name}")
|
137
|
+
logger.info(
|
138
|
+
f"Human interaction request "
|
139
|
+
f"task_id={request.task_id}, "
|
140
|
+
f"conversation_id={request.conversation_id}, "
|
141
|
+
f"loop_type={request.loop_type}, "
|
142
|
+
f"context={request.context}, "
|
143
|
+
f"metadata={request.metadata}, "
|
144
|
+
f"timeout={request.timeout}, "
|
145
|
+
f"created_at={request.created_at}"
|
146
|
+
)
|
147
|
+
|
796
148
|
async def async_on_update(
|
797
149
|
state: Any, provider: HumanLoopProvider, result: HumanLoopResult
|
798
|
-
) ->
|
150
|
+
) -> Any:
|
799
151
|
"""Log human interaction update events"""
|
800
152
|
logger.info(f"Provider ID: {provider.name}")
|
801
153
|
logger.info(
|
802
154
|
f"Human interaction update "
|
155
|
+
f"conversation_id={result.conversation_id}, "
|
156
|
+
f"request_id={result.request_id},"
|
803
157
|
f"status={result.status}, "
|
804
158
|
f"response={result.response}, "
|
805
159
|
f"responded_by={result.responded_by}, "
|
@@ -807,10 +161,22 @@ def default_langgraph_callback_factory(state: Any) -> LangGraphHumanLoopCallback
|
|
807
161
|
f"feedback={result.feedback}"
|
808
162
|
)
|
809
163
|
|
810
|
-
async def async_on_timeout(
|
164
|
+
async def async_on_timeout(
|
165
|
+
state: Any, provider: HumanLoopProvider, result: HumanLoopResult
|
166
|
+
) -> Any:
|
811
167
|
"""Log human interaction timeout events"""
|
812
168
|
|
813
169
|
logger.info(f"Provider ID: {provider.name}")
|
170
|
+
logger.info(
|
171
|
+
f"Human interaction timeout "
|
172
|
+
f"conversation_id={result.conversation_id}, "
|
173
|
+
f"request_id={result.request_id},"
|
174
|
+
f"status={result.status}, "
|
175
|
+
f"response={result.response}, "
|
176
|
+
f"responded_by={result.responded_by}, "
|
177
|
+
f"responded_at={result.responded_at}, "
|
178
|
+
f"feedback={result.feedback}"
|
179
|
+
)
|
814
180
|
from datetime import datetime
|
815
181
|
|
816
182
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
@@ -820,7 +186,7 @@ def default_langgraph_callback_factory(state: Any) -> LangGraphHumanLoopCallback
|
|
820
186
|
|
821
187
|
async def async_on_error(
|
822
188
|
state: Any, provider: HumanLoopProvider, error: Exception
|
823
|
-
) ->
|
189
|
+
) -> Any:
|
824
190
|
"""Log human interaction error events"""
|
825
191
|
|
826
192
|
logger.info(f"Provider ID: {provider.name}")
|
@@ -831,6 +197,7 @@ def default_langgraph_callback_factory(state: Any) -> LangGraphHumanLoopCallback
|
|
831
197
|
|
832
198
|
return LangGraphHumanLoopCallback(
|
833
199
|
state=state,
|
200
|
+
async_on_request=async_on_request,
|
834
201
|
async_on_update=async_on_update,
|
835
202
|
async_on_timeout=async_on_timeout,
|
836
203
|
async_on_error=async_on_error,
|
@@ -843,14 +210,14 @@ manager = DefaultHumanLoopManager(
|
|
843
210
|
)
|
844
211
|
|
845
212
|
# Create LangGraphAdapter instance
|
846
|
-
default_adapter =
|
213
|
+
default_adapter = HumanloopAdapter(manager, default_timeout=60)
|
847
214
|
|
848
215
|
default_conversation_id = str(uuid.uuid4())
|
849
216
|
|
850
217
|
_SKIP_NEXT_HUMANLOOP = False
|
851
218
|
|
852
219
|
|
853
|
-
def interrupt(value: Any, lg_humanloop:
|
220
|
+
def interrupt(value: Any, lg_humanloop: HumanloopAdapter = default_adapter) -> Any:
|
854
221
|
"""
|
855
222
|
Wraps LangGraph's interrupt functionality to pause graph execution and wait for human input
|
856
223
|
|
@@ -895,7 +262,7 @@ def interrupt(value: Any, lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
895
262
|
return _lg_interrupt(value)
|
896
263
|
|
897
264
|
|
898
|
-
def create_resume_command(lg_humanloop:
|
265
|
+
def create_resume_command(lg_humanloop: HumanloopAdapter = default_adapter) -> Any:
|
899
266
|
"""
|
900
267
|
Create a Command object to resume interrupted graph execution
|
901
268
|
|
@@ -936,7 +303,7 @@ def create_resume_command(lg_humanloop: LangGraphAdapter = default_adapter) -> A
|
|
936
303
|
|
937
304
|
|
938
305
|
async def acreate_resume_command(
|
939
|
-
lg_humanloop:
|
306
|
+
lg_humanloop: HumanloopAdapter = default_adapter
|
940
307
|
) -> Any:
|
941
308
|
"""
|
942
309
|
Create an async version of Command object to resume interrupted graph execution
|