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.
@@ -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
- HumanLoopManager,
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[None]]
75
+ Callable[[Any, HumanLoopProvider, HumanLoopResult], Awaitable[Any]]
746
76
  ] = None,
747
77
  async_on_timeout: Optional[
748
- Callable[[Any, HumanLoopProvider], Awaitable[None]]
78
+ Callable[[Any, HumanLoopProvider, HumanLoopResult], Awaitable[Any]]
749
79
  ] = None,
750
80
  async_on_error: Optional[
751
- Callable[[Any, HumanLoopProvider, Exception], Awaitable[None]]
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
- ) -> None:
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
- provider: HumanLoopProvider,
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
- ) -> None:
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
- ) -> None:
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(state: Any, provider: HumanLoopProvider) -> None:
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
- ) -> None:
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 = LangGraphAdapter(manager, default_timeout=60)
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: LangGraphAdapter = default_adapter) -> Any:
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: LangGraphAdapter = default_adapter) -> Any:
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: LangGraphAdapter = default_adapter
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