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
@@ -0,0 +1,838 @@
|
|
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
|
+
import importlib.util
|
712
|
+
|
713
|
+
if importlib.util.find_spec("agentops") is not None:
|
714
|
+
from agentops.sdk import session, agent, operation, task # type: ignore
|
715
|
+
else:
|
716
|
+
logger.debug(
|
717
|
+
"AgentOps package not installed. AgentOps features disabled. Please pip install agentops"
|
718
|
+
)
|
719
|
+
|
720
|
+
def __init__(self, session_tags: Optional[List[str]] = None) -> None:
|
721
|
+
"""Initialize the AgentOps human loop callback.
|
722
|
+
|
723
|
+
Args:
|
724
|
+
session_tags: Optional tags for AgentOps session tracking
|
725
|
+
"""
|
726
|
+
self.session_tags = session_tags or ["gohumanloop"]
|
727
|
+
|
728
|
+
try:
|
729
|
+
import agentops # type: ignore
|
730
|
+
|
731
|
+
agentops.init(tags=self.session_tags)
|
732
|
+
except Exception as e:
|
733
|
+
logger.warning(f"Failed to initialize AgentOps: {str(e)}")
|
734
|
+
|
735
|
+
@operation
|
736
|
+
async def async_on_humanloop_request(
|
737
|
+
self, provider: HumanLoopProvider, request: HumanLoopRequest
|
738
|
+
) -> Any:
|
739
|
+
"""Handle human loop start events."""
|
740
|
+
try:
|
741
|
+
# Create event data
|
742
|
+
event_data = {
|
743
|
+
"event_type": "gohumanloop_request",
|
744
|
+
"provider": provider.name,
|
745
|
+
"task_id": request.task_id,
|
746
|
+
"conversation_id": request.conversation_id,
|
747
|
+
"request_id": request.request_id,
|
748
|
+
"loop_type": request.loop_type.value,
|
749
|
+
"context": request.context,
|
750
|
+
"metadata": request.metadata,
|
751
|
+
"timeout": request.timeout,
|
752
|
+
"created_at": request.created_at,
|
753
|
+
}
|
754
|
+
return event_data
|
755
|
+
except (ImportError, Exception) as e:
|
756
|
+
logger.warning(f"Failed to record AgentOps event: {str(e)}")
|
757
|
+
|
758
|
+
@operation
|
759
|
+
async def async_on_humanloop_update(
|
760
|
+
self, provider: HumanLoopProvider, result: HumanLoopResult
|
761
|
+
) -> Any:
|
762
|
+
"""Handle human loop update events.
|
763
|
+
|
764
|
+
Args:
|
765
|
+
provider: The human loop provider instance
|
766
|
+
result: The human loop result containing status and response
|
767
|
+
"""
|
768
|
+
try:
|
769
|
+
# Create event data
|
770
|
+
event_data = {
|
771
|
+
"event_type": "gohumanloop_update",
|
772
|
+
"provider": provider.name,
|
773
|
+
"conversation_id": result.conversation_id,
|
774
|
+
"request_id": result.request_id,
|
775
|
+
"loop_type": result.loop_type.value,
|
776
|
+
"status": result.status.value,
|
777
|
+
"response": result.response,
|
778
|
+
"feedback": result.feedback,
|
779
|
+
"responded_by": result.responded_by,
|
780
|
+
"responded_at": result.responded_at,
|
781
|
+
"error": result.error,
|
782
|
+
}
|
783
|
+
|
784
|
+
return event_data
|
785
|
+
except Exception as e:
|
786
|
+
logger.warning(f"Failed to record AgentOps event: {str(e)}")
|
787
|
+
|
788
|
+
@operation
|
789
|
+
async def async_on_humanloop_timeout(
|
790
|
+
self, provider: HumanLoopProvider, result: HumanLoopResult
|
791
|
+
) -> Any:
|
792
|
+
"""Handle human loop timeout events.
|
793
|
+
|
794
|
+
Args:
|
795
|
+
provider: The human loop provider instance
|
796
|
+
"""
|
797
|
+
try:
|
798
|
+
# Create error event
|
799
|
+
error_data = {
|
800
|
+
"event_type": "gohumanloop_timeout",
|
801
|
+
"provider": provider.name,
|
802
|
+
"conversation_id": result.conversation_id,
|
803
|
+
"request_id": result.request_id,
|
804
|
+
"loop_type": result.loop_type.value,
|
805
|
+
"status": result.status.value,
|
806
|
+
"response": result.response,
|
807
|
+
"feedback": result.feedback,
|
808
|
+
"responded_by": result.responded_by,
|
809
|
+
"responded_at": result.responded_at,
|
810
|
+
"error": result.error,
|
811
|
+
}
|
812
|
+
|
813
|
+
return error_data
|
814
|
+
except Exception as e:
|
815
|
+
logger.warning(f"Failed to record AgentOps timeout event: {str(e)}")
|
816
|
+
|
817
|
+
@operation
|
818
|
+
async def async_on_humanloop_error(
|
819
|
+
self, provider: HumanLoopProvider, error: Exception
|
820
|
+
) -> Any:
|
821
|
+
"""Handle human loop error events.
|
822
|
+
|
823
|
+
Args:
|
824
|
+
provider: The human loop provider instance
|
825
|
+
error: The exception that occurred
|
826
|
+
"""
|
827
|
+
try:
|
828
|
+
# Create error event
|
829
|
+
error_data = {
|
830
|
+
"event_type": "gohumanloop_error",
|
831
|
+
"provider": provider.name,
|
832
|
+
"error": str(error),
|
833
|
+
}
|
834
|
+
|
835
|
+
# Record the error event
|
836
|
+
return error_data
|
837
|
+
except Exception as e:
|
838
|
+
logger.warning(f"Failed to record AgentOps error event: {str(e)}")
|