gohumanloop 0.0.5__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gohumanloop/__init__.py +6 -8
- gohumanloop/adapters/__init__.py +4 -4
- gohumanloop/adapters/langgraph_adapter.py +348 -207
- gohumanloop/cli/main.py +4 -1
- gohumanloop/core/interface.py +181 -215
- gohumanloop/core/manager.py +332 -265
- gohumanloop/manager/ghl_manager.py +223 -185
- gohumanloop/models/api_model.py +32 -7
- gohumanloop/models/glh_model.py +15 -11
- gohumanloop/providers/api_provider.py +233 -189
- gohumanloop/providers/base.py +179 -172
- gohumanloop/providers/email_provider.py +386 -325
- gohumanloop/providers/ghl_provider.py +19 -17
- gohumanloop/providers/terminal_provider.py +111 -92
- gohumanloop/utils/__init__.py +7 -1
- gohumanloop/utils/context_formatter.py +20 -15
- gohumanloop/utils/threadsafedict.py +64 -56
- gohumanloop/utils/utils.py +28 -28
- gohumanloop-0.0.6.dist-info/METADATA +259 -0
- gohumanloop-0.0.6.dist-info/RECORD +30 -0
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/WHEEL +1 -1
- gohumanloop-0.0.5.dist-info/METADATA +0 -35
- gohumanloop-0.0.5.dist-info/RECORD +0 -30
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/entry_points.txt +0 -0
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {gohumanloop-0.0.5.dist-info → gohumanloop-0.0.6.dist-info}/top_level.txt +0 -0
@@ -1,28 +1,32 @@
|
|
1
1
|
import asyncio
|
2
2
|
import logging
|
3
|
-
from typing import Dict, Any, Optional
|
3
|
+
from typing import Dict, Any, Optional, Tuple
|
4
4
|
|
5
5
|
import aiohttp
|
6
6
|
from pydantic import SecretStr
|
7
|
-
from concurrent.futures import ThreadPoolExecutor
|
8
|
-
from gohumanloop.core.interface import
|
9
|
-
HumanLoopResult, HumanLoopStatus, HumanLoopType
|
10
|
-
)
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
8
|
+
from gohumanloop.core.interface import HumanLoopResult, HumanLoopStatus, HumanLoopType
|
11
9
|
from gohumanloop.providers.base import BaseProvider
|
12
10
|
from gohumanloop.models.api_model import (
|
13
|
-
APIResponse,
|
14
|
-
|
11
|
+
APIResponse,
|
12
|
+
HumanLoopRequestData,
|
13
|
+
HumanLoopStatusParams,
|
14
|
+
HumanLoopStatusResponse,
|
15
|
+
HumanLoopCancelData,
|
16
|
+
HumanLoopCancelConversationData,
|
17
|
+
HumanLoopContinueData,
|
15
18
|
)
|
16
19
|
|
17
20
|
logger = logging.getLogger(__name__)
|
18
21
|
|
22
|
+
|
19
23
|
class APIProvider(BaseProvider):
|
20
24
|
"""API-based human-in-the-loop provider that supports integration with third-party service platforms
|
21
|
-
|
25
|
+
|
22
26
|
This provider communicates with a central service platform via HTTP requests, where the service platform
|
23
27
|
handles specific third-party service integrations (such as WeChat, Feishu, DingTalk, etc.).
|
24
28
|
"""
|
25
|
-
|
29
|
+
|
26
30
|
def __init__(
|
27
31
|
self,
|
28
32
|
name: str,
|
@@ -32,10 +36,10 @@ class APIProvider(BaseProvider):
|
|
32
36
|
request_timeout: int = 30,
|
33
37
|
poll_interval: int = 5,
|
34
38
|
max_retries: int = 3,
|
35
|
-
config: Optional[Dict[str, Any]] = None
|
39
|
+
config: Optional[Dict[str, Any]] = None,
|
36
40
|
):
|
37
41
|
"""Initialize API provider
|
38
|
-
|
42
|
+
|
39
43
|
Args:
|
40
44
|
name: Provider name
|
41
45
|
api_base_url: Base URL for API service
|
@@ -47,20 +51,19 @@ class APIProvider(BaseProvider):
|
|
47
51
|
config: Additional configuration parameters
|
48
52
|
"""
|
49
53
|
super().__init__(name, config)
|
50
|
-
self.api_base_url = api_base_url.rstrip(
|
54
|
+
self.api_base_url = api_base_url.rstrip("/")
|
51
55
|
self.api_key = api_key
|
52
56
|
self.default_platform = default_platform
|
53
57
|
self.request_timeout = request_timeout
|
54
58
|
self.poll_interval = poll_interval
|
55
59
|
self.max_retries = max_retries
|
56
|
-
|
60
|
+
|
57
61
|
# Store the currently running polling tasks.
|
58
|
-
self._poll_tasks = {}
|
59
|
-
|
62
|
+
self._poll_tasks: Dict[Tuple[str, str], Future] = {}
|
63
|
+
# Create thread pool for background service execution
|
60
64
|
self._executor = ThreadPoolExecutor(max_workers=10)
|
61
65
|
|
62
|
-
|
63
|
-
def __del__(self):
|
66
|
+
def __del__(self) -> None:
|
64
67
|
"""析构函数,确保线程池被正确关闭"""
|
65
68
|
self._executor.shutdown(wait=False)
|
66
69
|
|
@@ -68,7 +71,7 @@ class APIProvider(BaseProvider):
|
|
68
71
|
for task_key, future in list(self._poll_tasks.items()):
|
69
72
|
future.cancel()
|
70
73
|
self._poll_tasks.clear()
|
71
|
-
|
74
|
+
|
72
75
|
def __str__(self) -> str:
|
73
76
|
"""Returns a string description of this instance"""
|
74
77
|
base_str = super().__str__()
|
@@ -76,49 +79,51 @@ class APIProvider(BaseProvider):
|
|
76
79
|
if self.default_platform:
|
77
80
|
api_info += f" Default Platform: {self.default_platform}\n"
|
78
81
|
return f"{api_info}{base_str}"
|
79
|
-
|
82
|
+
|
80
83
|
async def _async_make_api_request(
|
81
|
-
self,
|
82
|
-
endpoint: str,
|
83
|
-
method: str = "POST",
|
84
|
+
self,
|
85
|
+
endpoint: str,
|
86
|
+
method: str = "POST",
|
84
87
|
data: Optional[Dict[str, Any]] = None,
|
85
88
|
params: Optional[Dict[str, Any]] = None,
|
86
|
-
headers: Optional[Dict[str, Any]] = None
|
89
|
+
headers: Optional[Dict[str, Any]] = None,
|
87
90
|
) -> Optional[Dict[str, Any]]:
|
88
91
|
"""Make API request
|
89
|
-
|
92
|
+
|
90
93
|
Args:
|
91
94
|
endpoint: API endpoint path
|
92
95
|
method: HTTP method (GET, POST, etc.)
|
93
96
|
data: Request body data
|
94
|
-
params: URL query parameters
|
97
|
+
params: URL query parameters
|
95
98
|
headers: Request headers
|
96
|
-
|
99
|
+
|
97
100
|
Returns:
|
98
101
|
Dict[str, Any]: API response data
|
99
|
-
|
102
|
+
|
100
103
|
Raises:
|
101
104
|
Exception: If API request fails
|
102
105
|
"""
|
103
106
|
url = f"{self.api_base_url}/{endpoint.lstrip('/')}"
|
104
|
-
|
107
|
+
|
105
108
|
# Prepare request headers
|
106
109
|
request_headers = {
|
107
110
|
"Content-Type": "application/json",
|
108
111
|
}
|
109
112
|
# Add authentication information
|
110
113
|
if self.api_key:
|
111
|
-
request_headers[
|
112
|
-
|
114
|
+
request_headers[
|
115
|
+
"Authorization"
|
116
|
+
] = f"Bearer {self.api_key.get_secret_value()}"
|
117
|
+
|
113
118
|
# Merge custom headers
|
114
119
|
if headers:
|
115
120
|
request_headers.update(headers)
|
116
|
-
|
121
|
+
|
117
122
|
# Prepare request data
|
118
123
|
json_data = None
|
119
124
|
if data:
|
120
125
|
json_data = data
|
121
|
-
|
126
|
+
|
122
127
|
# Send request
|
123
128
|
for attempt in range(self.max_retries):
|
124
129
|
try:
|
@@ -129,24 +134,30 @@ class APIProvider(BaseProvider):
|
|
129
134
|
json=json_data,
|
130
135
|
params=params,
|
131
136
|
headers=request_headers,
|
132
|
-
timeout=self.request_timeout
|
137
|
+
timeout=self.request_timeout,
|
133
138
|
) as response:
|
134
|
-
response_data = await response.json()
|
139
|
+
response_data: Dict[str, Any] = await response.json()
|
135
140
|
# Check response status
|
136
141
|
if response.status >= 400:
|
137
|
-
error_msg = response_data.get(
|
142
|
+
error_msg = response_data.get(
|
143
|
+
"error", f"API request failed: {response.status}"
|
144
|
+
)
|
138
145
|
logger.error(f"API request failed: {error_msg}")
|
139
|
-
|
146
|
+
|
140
147
|
# Retry if not the last attempt
|
141
148
|
if attempt < self.max_retries - 1:
|
142
|
-
await asyncio.sleep(
|
149
|
+
await asyncio.sleep(
|
150
|
+
1 * (attempt + 1)
|
151
|
+
) # Exponential backoff
|
143
152
|
continue
|
144
|
-
|
153
|
+
|
145
154
|
raise Exception(error_msg)
|
146
|
-
|
155
|
+
|
147
156
|
return response_data
|
148
157
|
except asyncio.TimeoutError:
|
149
|
-
logger.warning(
|
158
|
+
logger.warning(
|
159
|
+
f"API request timeout (attempt {attempt+1}/{self.max_retries})"
|
160
|
+
)
|
150
161
|
if attempt < self.max_retries - 1:
|
151
162
|
await asyncio.sleep(1 * (attempt + 1))
|
152
163
|
continue
|
@@ -157,7 +168,9 @@ class APIProvider(BaseProvider):
|
|
157
168
|
await asyncio.sleep(1 * (attempt + 1))
|
158
169
|
continue
|
159
170
|
raise
|
160
|
-
|
171
|
+
|
172
|
+
return None
|
173
|
+
|
161
174
|
async def async_request_humanloop(
|
162
175
|
self,
|
163
176
|
task_id: str,
|
@@ -165,10 +178,10 @@ class APIProvider(BaseProvider):
|
|
165
178
|
loop_type: HumanLoopType,
|
166
179
|
context: Dict[str, Any],
|
167
180
|
metadata: Optional[Dict[str, Any]] = None,
|
168
|
-
timeout: Optional[int] = None
|
181
|
+
timeout: Optional[int] = None,
|
169
182
|
) -> HumanLoopResult:
|
170
183
|
"""Request human-in-the-loop interaction
|
171
|
-
|
184
|
+
|
172
185
|
Args:
|
173
186
|
task_id: Task identifier
|
174
187
|
conversation_id: Conversation ID for multi-turn dialogue
|
@@ -176,12 +189,12 @@ class APIProvider(BaseProvider):
|
|
176
189
|
context: Context information provided to humans
|
177
190
|
metadata: Additional metadata
|
178
191
|
timeout: Request timeout in seconds
|
179
|
-
|
192
|
+
|
180
193
|
Returns:
|
181
194
|
HumanLoopResult: Result object containing request ID and initial status
|
182
195
|
"""
|
183
196
|
metadata = metadata or {}
|
184
|
-
|
197
|
+
|
185
198
|
# Generate request ID
|
186
199
|
request_id = self._generate_request_id()
|
187
200
|
platform = metadata.get("platform", self.default_platform)
|
@@ -193,20 +206,24 @@ class APIProvider(BaseProvider):
|
|
193
206
|
loop_type=loop_type,
|
194
207
|
context=context,
|
195
208
|
metadata={**metadata, "platform": platform},
|
196
|
-
timeout=timeout
|
209
|
+
timeout=timeout,
|
197
210
|
)
|
198
|
-
|
211
|
+
|
199
212
|
# Determine which platform to use
|
200
213
|
if not platform:
|
201
|
-
self._update_request_status_error(
|
214
|
+
self._update_request_status_error(
|
215
|
+
conversation_id,
|
216
|
+
request_id,
|
217
|
+
"Platform not specified. Please set 'platform' in metadata or set default_platform during initialization",
|
218
|
+
)
|
202
219
|
return HumanLoopResult(
|
203
220
|
conversation_id=conversation_id,
|
204
221
|
request_id=request_id,
|
205
222
|
loop_type=loop_type,
|
206
223
|
status=HumanLoopStatus.ERROR,
|
207
|
-
error="Platform not specified. Please set 'platform' in metadata or set default_platform during initialization"
|
224
|
+
error="Platform not specified. Please set 'platform' in metadata or set default_platform during initialization",
|
208
225
|
)
|
209
|
-
|
226
|
+
|
210
227
|
# Prepare API request data
|
211
228
|
request_data = HumanLoopRequestData(
|
212
229
|
task_id=task_id,
|
@@ -215,62 +232,72 @@ class APIProvider(BaseProvider):
|
|
215
232
|
loop_type=loop_type.value,
|
216
233
|
context=context,
|
217
234
|
platform=platform,
|
218
|
-
metadata=metadata
|
235
|
+
metadata=metadata,
|
219
236
|
).model_dump()
|
220
|
-
|
237
|
+
|
221
238
|
try:
|
222
239
|
# Send API request
|
223
240
|
response = await self._async_make_api_request(
|
224
|
-
endpoint="v1/humanloop/request",
|
225
|
-
method="POST",
|
226
|
-
data=request_data
|
241
|
+
endpoint="v1/humanloop/request", method="POST", data=request_data
|
227
242
|
)
|
228
|
-
|
243
|
+
|
229
244
|
# Check API response
|
230
|
-
|
245
|
+
response_data = response or {}
|
246
|
+
api_response = APIResponse(**response_data)
|
231
247
|
if not api_response.success:
|
232
|
-
error_msg =
|
248
|
+
error_msg = (
|
249
|
+
api_response.error or "API request failed without error message"
|
250
|
+
)
|
233
251
|
# Update request status to error
|
234
|
-
self._update_request_status_error(
|
235
|
-
|
252
|
+
self._update_request_status_error(
|
253
|
+
conversation_id, request_id, error_msg
|
254
|
+
)
|
255
|
+
|
236
256
|
return HumanLoopResult(
|
237
257
|
conversation_id=conversation_id,
|
238
258
|
request_id=request_id,
|
239
259
|
loop_type=loop_type,
|
240
260
|
status=HumanLoopStatus.ERROR,
|
241
|
-
error=error_msg
|
261
|
+
error=error_msg,
|
242
262
|
)
|
243
|
-
|
263
|
+
|
244
264
|
# Create polling task
|
245
265
|
self._poll_tasks[(conversation_id, request_id)] = self._executor.submit(
|
246
|
-
|
266
|
+
self._run_async_poll_request_status,
|
267
|
+
conversation_id,
|
268
|
+
request_id,
|
269
|
+
platform,
|
247
270
|
)
|
248
|
-
|
271
|
+
|
249
272
|
# Create timeout task if timeout is set
|
250
273
|
if timeout:
|
251
|
-
|
252
|
-
|
274
|
+
await self._async_create_timeout_task(
|
275
|
+
conversation_id, request_id, timeout
|
276
|
+
)
|
277
|
+
|
253
278
|
return HumanLoopResult(
|
254
279
|
conversation_id=conversation_id,
|
255
280
|
request_id=request_id,
|
256
281
|
loop_type=loop_type,
|
257
|
-
status=HumanLoopStatus.PENDING
|
282
|
+
status=HumanLoopStatus.PENDING,
|
258
283
|
)
|
259
|
-
|
284
|
+
|
260
285
|
except Exception as e:
|
261
286
|
logger.error(f"Failed to request human-in-the-loop: {str(e)}")
|
262
287
|
# Update request status to error
|
263
|
-
self._update_request_status_error(conversation_id, request_id, str(e))
|
264
|
-
|
288
|
+
self._update_request_status_error(conversation_id, request_id, str(e))
|
289
|
+
|
265
290
|
return HumanLoopResult(
|
266
291
|
conversation_id=conversation_id,
|
267
292
|
request_id=request_id,
|
268
293
|
loop_type=loop_type,
|
269
294
|
status=HumanLoopStatus.ERROR,
|
270
|
-
error=str(e)
|
295
|
+
error=str(e),
|
271
296
|
)
|
272
297
|
|
273
|
-
def _run_async_poll_request_status(
|
298
|
+
def _run_async_poll_request_status(
|
299
|
+
self, conversation_id: str, request_id: str, platform: str
|
300
|
+
) -> None:
|
274
301
|
"""Run asynchronous API interaction in a separate thread"""
|
275
302
|
# Create new event loop
|
276
303
|
loop = asyncio.new_event_loop()
|
@@ -278,7 +305,9 @@ class APIProvider(BaseProvider):
|
|
278
305
|
|
279
306
|
try:
|
280
307
|
# Run interaction processing in the new event loop
|
281
|
-
loop.run_until_complete(
|
308
|
+
loop.run_until_complete(
|
309
|
+
self._async_poll_request_status(conversation_id, request_id, platform)
|
310
|
+
)
|
282
311
|
finally:
|
283
312
|
loop.close()
|
284
313
|
# Remove from task dictionary
|
@@ -286,16 +315,14 @@ class APIProvider(BaseProvider):
|
|
286
315
|
del self._poll_tasks[(conversation_id, request_id)]
|
287
316
|
|
288
317
|
async def async_check_request_status(
|
289
|
-
self,
|
290
|
-
conversation_id: str,
|
291
|
-
request_id: str
|
318
|
+
self, conversation_id: str, request_id: str
|
292
319
|
) -> HumanLoopResult:
|
293
320
|
"""Check request status
|
294
|
-
|
321
|
+
|
295
322
|
Args:
|
296
323
|
conversation_id: Conversation identifier
|
297
324
|
request_id: Request identifier
|
298
|
-
|
325
|
+
|
299
326
|
Returns:
|
300
327
|
HumanLoopResult: Result object containing current status
|
301
328
|
"""
|
@@ -306,9 +333,9 @@ class APIProvider(BaseProvider):
|
|
306
333
|
request_id=request_id,
|
307
334
|
loop_type=HumanLoopType.CONVERSATION,
|
308
335
|
status=HumanLoopStatus.ERROR,
|
309
|
-
error=f"Request '{request_id}' not found in conversation '{conversation_id}'"
|
336
|
+
error=f"Request '{request_id}' not found in conversation '{conversation_id}'",
|
310
337
|
)
|
311
|
-
|
338
|
+
|
312
339
|
result = HumanLoopResult(
|
313
340
|
conversation_id=conversation_id,
|
314
341
|
request_id=request_id,
|
@@ -318,23 +345,18 @@ class APIProvider(BaseProvider):
|
|
318
345
|
feedback=request_info.get("feedback", {}),
|
319
346
|
responded_by=request_info.get("responded_by", None),
|
320
347
|
responded_at=request_info.get("responded_at", None),
|
321
|
-
error=request_info.get("error", None)
|
348
|
+
error=request_info.get("error", None),
|
322
349
|
)
|
323
|
-
|
350
|
+
|
324
351
|
return result
|
325
352
|
|
326
|
-
|
327
|
-
async def async_cancel_request(
|
328
|
-
self,
|
329
|
-
conversation_id: str,
|
330
|
-
request_id: str
|
331
|
-
) -> bool:
|
353
|
+
async def async_cancel_request(self, conversation_id: str, request_id: str) -> bool:
|
332
354
|
"""Cancel human-in-the-loop request
|
333
|
-
|
355
|
+
|
334
356
|
Args:
|
335
357
|
conversation_id: Conversation identifier for multi-turn dialogue
|
336
358
|
request_id: Request identifier for specific interaction request
|
337
|
-
|
359
|
+
|
338
360
|
Returns:
|
339
361
|
bool: Whether cancellation was successful, True for success, False for failure
|
340
362
|
"""
|
@@ -342,59 +364,57 @@ class APIProvider(BaseProvider):
|
|
342
364
|
result = await super().async_cancel_request(conversation_id, request_id)
|
343
365
|
if not result:
|
344
366
|
return False
|
345
|
-
|
367
|
+
|
346
368
|
# Get request information
|
347
369
|
request_info = self._get_request(conversation_id, request_id)
|
348
370
|
if not request_info:
|
349
371
|
return False
|
350
|
-
|
372
|
+
|
351
373
|
# Get platform information
|
352
374
|
platform = request_info.get("metadata", {}).get("platform")
|
353
375
|
if not platform:
|
354
|
-
logger.error(
|
376
|
+
logger.error("Cancel request failed: Platform information not found")
|
355
377
|
return False
|
356
|
-
|
378
|
+
|
357
379
|
try:
|
358
380
|
# Send API request to cancel request
|
359
381
|
cancel_data = HumanLoopCancelData(
|
360
382
|
conversation_id=conversation_id,
|
361
383
|
request_id=request_id,
|
362
|
-
platform=platform
|
384
|
+
platform=platform,
|
363
385
|
).model_dump()
|
364
|
-
|
386
|
+
|
365
387
|
response = await self._async_make_api_request(
|
366
|
-
endpoint="v1/humanloop/cancel",
|
367
|
-
method="POST",
|
368
|
-
data=cancel_data
|
388
|
+
endpoint="v1/humanloop/cancel", method="POST", data=cancel_data
|
369
389
|
)
|
370
|
-
|
390
|
+
|
371
391
|
# Check API response
|
372
|
-
|
392
|
+
response_data = response or {}
|
393
|
+
api_response = APIResponse(**response_data)
|
373
394
|
if not api_response.success:
|
374
|
-
error_msg =
|
395
|
+
error_msg = (
|
396
|
+
api_response.error or "Cancel request failed without error message"
|
397
|
+
)
|
375
398
|
logger.error(f"Cancel request failed: {error_msg}")
|
376
399
|
return False
|
377
|
-
|
400
|
+
|
378
401
|
# Cancel polling task
|
379
402
|
if (conversation_id, request_id) in self._poll_tasks:
|
380
403
|
self._poll_tasks[(conversation_id, request_id)].cancel()
|
381
404
|
del self._poll_tasks[(conversation_id, request_id)]
|
382
|
-
|
405
|
+
|
383
406
|
return True
|
384
|
-
|
407
|
+
|
385
408
|
except Exception as e:
|
386
409
|
logger.error(f"Cancel request failed: {str(e)}")
|
387
410
|
return False
|
388
|
-
|
389
|
-
async def async_cancel_conversation(
|
390
|
-
self,
|
391
|
-
conversation_id: str
|
392
|
-
) -> bool:
|
411
|
+
|
412
|
+
async def async_cancel_conversation(self, conversation_id: str) -> bool:
|
393
413
|
"""Cancel entire conversation
|
394
|
-
|
414
|
+
|
395
415
|
Args:
|
396
416
|
conversation_id: Conversation identifier
|
397
|
-
|
417
|
+
|
398
418
|
Returns:
|
399
419
|
bool: Whether cancellation was successful
|
400
420
|
"""
|
@@ -402,55 +422,57 @@ class APIProvider(BaseProvider):
|
|
402
422
|
result = await super().async_cancel_conversation(conversation_id)
|
403
423
|
if not result:
|
404
424
|
return False
|
405
|
-
|
425
|
+
|
406
426
|
# Get all requests in the conversation
|
407
427
|
request_ids = self._get_conversation_requests(conversation_id)
|
408
428
|
if not request_ids:
|
409
429
|
return True # No requests to cancel
|
410
|
-
|
430
|
+
|
411
431
|
# Get platform info from first request (assuming all requests use same platform)
|
412
432
|
first_request = self._get_request(conversation_id, request_ids[0])
|
413
433
|
if not first_request:
|
414
434
|
return False
|
415
|
-
|
435
|
+
|
416
436
|
platform = first_request.get("metadata", {}).get("platform")
|
417
437
|
if not platform:
|
418
|
-
logger.error(
|
438
|
+
logger.error("Cancel conversation failed: Platform information not found")
|
419
439
|
return False
|
420
|
-
|
440
|
+
|
421
441
|
try:
|
422
442
|
# Send API request to cancel conversation
|
423
443
|
cancel_data = HumanLoopCancelConversationData(
|
424
|
-
conversation_id=conversation_id,
|
425
|
-
platform=platform
|
444
|
+
conversation_id=conversation_id, platform=platform
|
426
445
|
).model_dump()
|
427
|
-
|
446
|
+
|
428
447
|
response = await self._async_make_api_request(
|
429
448
|
endpoint="v1/humanloop/cancel_conversation",
|
430
449
|
method="POST",
|
431
|
-
data=cancel_data
|
450
|
+
data=cancel_data,
|
432
451
|
)
|
433
|
-
|
452
|
+
|
434
453
|
# Check API response
|
435
|
-
|
454
|
+
response_data = response or {}
|
455
|
+
api_response = APIResponse(**response_data)
|
436
456
|
if not api_response.success:
|
437
|
-
error_msg =
|
457
|
+
error_msg = (
|
458
|
+
api_response.error
|
459
|
+
or "Cancel conversation failed without error message"
|
460
|
+
)
|
438
461
|
logger.error(f"Cancel conversation failed: {error_msg}")
|
439
462
|
return False
|
440
|
-
|
463
|
+
|
441
464
|
# Cancel all polling tasks
|
442
465
|
for request_id in request_ids:
|
443
466
|
if (conversation_id, request_id) in self._poll_tasks:
|
444
467
|
self._poll_tasks[(conversation_id, request_id)].cancel()
|
445
468
|
del self._poll_tasks[(conversation_id, request_id)]
|
446
|
-
|
469
|
+
|
447
470
|
return True
|
448
|
-
|
471
|
+
|
449
472
|
except Exception as e:
|
450
473
|
logger.error(f"Cancel conversation failed: {str(e)}")
|
451
474
|
return False
|
452
475
|
|
453
|
-
|
454
476
|
async def async_continue_humanloop(
|
455
477
|
self,
|
456
478
|
conversation_id: str,
|
@@ -459,13 +481,13 @@ class APIProvider(BaseProvider):
|
|
459
481
|
timeout: Optional[int] = None,
|
460
482
|
) -> HumanLoopResult:
|
461
483
|
"""Continue human-in-the-loop interaction
|
462
|
-
|
484
|
+
|
463
485
|
Args:
|
464
486
|
conversation_id: Conversation ID for multi-turn dialogue
|
465
487
|
context: Context information provided to humans
|
466
488
|
metadata: Additional metadata
|
467
489
|
timeout: Request timeout in seconds
|
468
|
-
|
490
|
+
|
469
491
|
Returns:
|
470
492
|
HumanLoopResult: Result object containing request ID and status
|
471
493
|
"""
|
@@ -477,14 +499,14 @@ class APIProvider(BaseProvider):
|
|
477
499
|
request_id="",
|
478
500
|
loop_type=HumanLoopType.CONVERSATION,
|
479
501
|
status=HumanLoopStatus.ERROR,
|
480
|
-
error=f"Conversation '{conversation_id}' not found"
|
502
|
+
error=f"Conversation '{conversation_id}' not found",
|
481
503
|
)
|
482
|
-
|
504
|
+
|
483
505
|
metadata = metadata or {}
|
484
|
-
|
506
|
+
|
485
507
|
# Generate request ID
|
486
508
|
request_id = self._generate_request_id()
|
487
|
-
|
509
|
+
|
488
510
|
# Get task ID
|
489
511
|
task_id = conversation_info.get("task_id", "unknown_task")
|
490
512
|
# Determine which platform to use
|
@@ -498,19 +520,23 @@ class APIProvider(BaseProvider):
|
|
498
520
|
loop_type=HumanLoopType.CONVERSATION,
|
499
521
|
context=context,
|
500
522
|
metadata={**metadata, "platform": platform},
|
501
|
-
timeout=timeout
|
523
|
+
timeout=timeout,
|
502
524
|
)
|
503
525
|
|
504
526
|
if not platform:
|
505
|
-
self._update_request_status_error(
|
527
|
+
self._update_request_status_error(
|
528
|
+
conversation_id,
|
529
|
+
request_id,
|
530
|
+
"Platform not specified. Please set 'platform' in metadata or set default_platform during initialization",
|
531
|
+
)
|
506
532
|
return HumanLoopResult(
|
507
533
|
conversation_id=conversation_id,
|
508
534
|
request_id=request_id,
|
509
535
|
loop_type=HumanLoopType.CONVERSATION,
|
510
536
|
status=HumanLoopStatus.ERROR,
|
511
|
-
error="Platform not specified. Please set 'platform' in metadata or set default_platform during initialization"
|
537
|
+
error="Platform not specified. Please set 'platform' in metadata or set default_platform during initialization",
|
512
538
|
)
|
513
|
-
|
539
|
+
|
514
540
|
# Prepare API request data
|
515
541
|
continue_data = HumanLoopContinueData(
|
516
542
|
conversation_id=conversation_id,
|
@@ -518,68 +544,73 @@ class APIProvider(BaseProvider):
|
|
518
544
|
task_id=task_id,
|
519
545
|
context=context,
|
520
546
|
platform=platform,
|
521
|
-
metadata=metadata
|
547
|
+
metadata=metadata,
|
522
548
|
).model_dump()
|
523
|
-
|
549
|
+
|
524
550
|
try:
|
525
551
|
# Send API request
|
526
552
|
response = await self._async_make_api_request(
|
527
|
-
endpoint="v1/humanloop/continue",
|
528
|
-
method="POST",
|
529
|
-
data=continue_data
|
553
|
+
endpoint="v1/humanloop/continue", method="POST", data=continue_data
|
530
554
|
)
|
531
|
-
|
555
|
+
|
532
556
|
# Check API response
|
533
|
-
|
557
|
+
response_data = response or {}
|
558
|
+
api_response = APIResponse(**response_data)
|
534
559
|
if not api_response.success:
|
535
|
-
error_msg =
|
536
|
-
|
537
|
-
|
560
|
+
error_msg = (
|
561
|
+
api_response.error
|
562
|
+
or "Continue conversation failed without error message"
|
563
|
+
)
|
564
|
+
|
565
|
+
self._update_request_status_error(
|
566
|
+
conversation_id, request_id, error_msg
|
567
|
+
)
|
538
568
|
return HumanLoopResult(
|
539
569
|
conversation_id=conversation_id,
|
540
570
|
request_id=request_id,
|
541
571
|
loop_type=HumanLoopType.CONVERSATION,
|
542
572
|
status=HumanLoopStatus.ERROR,
|
543
|
-
error=error_msg
|
573
|
+
error=error_msg,
|
544
574
|
)
|
545
|
-
|
546
|
-
|
575
|
+
|
576
|
+
# Create polling task
|
547
577
|
self._poll_tasks[(conversation_id, request_id)] = self._executor.submit(
|
548
|
-
|
578
|
+
self._run_async_poll_request_status,
|
579
|
+
conversation_id,
|
580
|
+
request_id,
|
581
|
+
platform,
|
549
582
|
)
|
550
|
-
|
583
|
+
|
551
584
|
# Create timeout task if timeout is set
|
552
585
|
if timeout:
|
553
|
-
await self._async_create_timeout_task(
|
554
|
-
|
586
|
+
await self._async_create_timeout_task(
|
587
|
+
conversation_id, request_id, timeout
|
588
|
+
)
|
589
|
+
|
555
590
|
return HumanLoopResult(
|
556
591
|
conversation_id=conversation_id,
|
557
592
|
request_id=request_id,
|
558
593
|
loop_type=HumanLoopType.CONVERSATION,
|
559
|
-
status=HumanLoopStatus.PENDING
|
594
|
+
status=HumanLoopStatus.PENDING,
|
560
595
|
)
|
561
|
-
|
596
|
+
|
562
597
|
except Exception as e:
|
563
598
|
logger.error(f"Failed to continue human-in-the-loop: {str(e)}")
|
564
599
|
self._update_request_status_error(conversation_id, request_id, str(e))
|
565
600
|
|
566
|
-
|
567
601
|
return HumanLoopResult(
|
568
602
|
conversation_id=conversation_id,
|
569
603
|
request_id=request_id,
|
570
604
|
loop_type=HumanLoopType.CONVERSATION,
|
571
605
|
status=HumanLoopStatus.ERROR,
|
572
|
-
error=str(e)
|
606
|
+
error=str(e),
|
573
607
|
)
|
574
|
-
|
608
|
+
|
575
609
|
async def _async_poll_request_status(
|
576
|
-
self,
|
577
|
-
conversation_id: str,
|
578
|
-
request_id: str,
|
579
|
-
platform: str
|
610
|
+
self, conversation_id: str, request_id: str, platform: str
|
580
611
|
) -> None:
|
581
612
|
"""Poll request status
|
582
|
-
|
613
|
+
|
583
614
|
Args:
|
584
615
|
conversation_id: Conversation identifier
|
585
616
|
request_id: Request identifier
|
@@ -590,65 +621,78 @@ class APIProvider(BaseProvider):
|
|
590
621
|
# Get request information
|
591
622
|
request_info = self._get_request(conversation_id, request_id)
|
592
623
|
if not request_info:
|
593
|
-
logger.warning(
|
624
|
+
logger.warning(
|
625
|
+
f"Polling stopped: Request '{request_id}' not found in conversation '{conversation_id}'"
|
626
|
+
)
|
594
627
|
return
|
595
|
-
|
628
|
+
|
596
629
|
# Stop polling if request is in final status
|
597
630
|
status = request_info.get("status")
|
598
|
-
if status not in
|
631
|
+
if status not in [HumanLoopStatus.PENDING, HumanLoopStatus.INPROGRESS]:
|
599
632
|
return
|
600
|
-
|
633
|
+
|
601
634
|
# Send API request to get status
|
602
635
|
params = HumanLoopStatusParams(
|
603
636
|
conversation_id=conversation_id,
|
604
637
|
request_id=request_id,
|
605
|
-
platform=platform
|
638
|
+
platform=platform,
|
606
639
|
).model_dump()
|
607
|
-
|
640
|
+
|
608
641
|
response = await self._async_make_api_request(
|
609
|
-
endpoint="v1/humanloop/status",
|
610
|
-
method="GET",
|
611
|
-
params=params
|
642
|
+
endpoint="v1/humanloop/status", method="GET", params=params
|
612
643
|
)
|
613
|
-
|
644
|
+
|
614
645
|
# Parse response
|
615
|
-
|
616
|
-
|
646
|
+
response_data = response or {}
|
647
|
+
status_response = HumanLoopStatusResponse(**response_data)
|
648
|
+
|
617
649
|
# Log error but continue polling if request fails
|
618
650
|
if not status_response.success:
|
619
651
|
logger.warning(f"Failed to get status: {status_response.error}")
|
620
652
|
await asyncio.sleep(self.poll_interval)
|
621
653
|
continue
|
622
|
-
|
654
|
+
|
623
655
|
# Parse status
|
624
656
|
try:
|
625
657
|
new_status = HumanLoopStatus(status_response.status)
|
626
658
|
except ValueError:
|
627
|
-
logger.warning(
|
659
|
+
logger.warning(
|
660
|
+
f"Unknown status value: {status_response.status}, using PENDING"
|
661
|
+
)
|
628
662
|
new_status = HumanLoopStatus.PENDING
|
629
|
-
|
663
|
+
|
630
664
|
# Update request information
|
631
665
|
request_key = (conversation_id, request_id)
|
632
666
|
if request_key in self._requests:
|
633
667
|
self._requests[request_key]["status"] = new_status
|
634
|
-
|
668
|
+
|
635
669
|
# Update response data
|
636
|
-
for field in [
|
670
|
+
for field in [
|
671
|
+
"response",
|
672
|
+
"feedback",
|
673
|
+
"responded_by",
|
674
|
+
"responded_at",
|
675
|
+
"error",
|
676
|
+
]:
|
637
677
|
value = getattr(status_response, field, None)
|
638
678
|
if value is not None:
|
639
679
|
self._requests[request_key][field] = value
|
640
|
-
|
641
|
-
|
680
|
+
|
642
681
|
# Stop polling if request is in final status
|
643
|
-
if new_status not in
|
682
|
+
if new_status not in [
|
683
|
+
HumanLoopStatus.PENDING,
|
684
|
+
HumanLoopStatus.INPROGRESS,
|
685
|
+
]:
|
644
686
|
return
|
645
|
-
|
687
|
+
|
646
688
|
# Wait for next polling interval
|
647
689
|
await asyncio.sleep(self.poll_interval)
|
648
|
-
|
690
|
+
|
649
691
|
except asyncio.CancelledError:
|
650
|
-
logger.info(
|
692
|
+
logger.info(
|
693
|
+
f"Polling task cancelled: conversation '{conversation_id}', request '{request_id}'"
|
694
|
+
)
|
651
695
|
return
|
652
696
|
except Exception as e:
|
653
697
|
logger.error(f"Polling task error: {str(e)}")
|
654
|
-
return
|
698
|
+
return
|