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