gohumanloop 0.0.1__py3-none-any.whl → 0.0.3__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 +17 -0
- gohumanloop/adapters/langgraph_adapter.py +817 -0
- gohumanloop/cli/__init__.py +0 -0
- gohumanloop/cli/main.py +29 -0
- gohumanloop/core/__init__.py +0 -0
- gohumanloop/core/interface.py +758 -0
- gohumanloop/core/manager.py +839 -0
- gohumanloop/manager/__init__.py +0 -0
- gohumanloop/manager/ghl_manager.py +532 -0
- gohumanloop/models/__init__.py +0 -0
- gohumanloop/models/api_model.py +54 -0
- gohumanloop/models/glh_model.py +23 -0
- gohumanloop/providers/__init__.py +0 -0
- gohumanloop/providers/api_provider.py +628 -0
- gohumanloop/providers/base.py +658 -0
- gohumanloop/providers/email_provider.py +1019 -0
- gohumanloop/providers/ghl_provider.py +64 -0
- gohumanloop/providers/terminal_provider.py +301 -0
- gohumanloop/utils/__init__.py +1 -0
- gohumanloop/utils/context_formatter.py +59 -0
- gohumanloop/utils/threadsafedict.py +243 -0
- gohumanloop/utils/utils.py +67 -0
- {gohumanloop-0.0.1.dist-info → gohumanloop-0.0.3.dist-info}/METADATA +2 -1
- gohumanloop-0.0.3.dist-info/RECORD +30 -0
- {gohumanloop-0.0.1.dist-info → gohumanloop-0.0.3.dist-info}/WHEEL +1 -1
- gohumanloop-0.0.1.dist-info/RECORD +0 -8
- {gohumanloop-0.0.1.dist-info → gohumanloop-0.0.3.dist-info}/entry_points.txt +0 -0
- {gohumanloop-0.0.1.dist-info → gohumanloop-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {gohumanloop-0.0.1.dist-info → gohumanloop-0.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,658 @@
|
|
1
|
+
from abc import ABC
|
2
|
+
import re
|
3
|
+
from typing import Dict, Any, Optional, List
|
4
|
+
import asyncio
|
5
|
+
import json
|
6
|
+
import uuid
|
7
|
+
from datetime import datetime
|
8
|
+
from collections import defaultdict
|
9
|
+
from gohumanloop.utils.threadsafedict import ThreadSafeDict
|
10
|
+
|
11
|
+
from gohumanloop.core.interface import (
|
12
|
+
HumanLoopProvider, HumanLoopResult, HumanLoopStatus, HumanLoopType
|
13
|
+
)
|
14
|
+
|
15
|
+
class BaseProvider(HumanLoopProvider, ABC):
|
16
|
+
"""Base implementation of human-in-the-loop provider"""
|
17
|
+
|
18
|
+
|
19
|
+
def __init__(self, name: str, config: Optional[Dict[str, Any]] = None):
|
20
|
+
self.config = config or {}
|
21
|
+
# Custom name, will use UUID if not provided
|
22
|
+
self.name = name
|
23
|
+
# Store request information using (conversation_id, request_id) as key
|
24
|
+
self._requests = ThreadSafeDict() # Using thread-safe dictionary to store request information
|
25
|
+
# Store conversation information, including request list and latest request ID
|
26
|
+
self._conversations = {}
|
27
|
+
# For quick lookup of requests in conversations
|
28
|
+
self._conversation_requests = defaultdict(list)
|
29
|
+
# Store timeout tasks
|
30
|
+
self._timeout_tasks = {}
|
31
|
+
|
32
|
+
self.prompt_template = self.config.get("prompt_template", "{context}")
|
33
|
+
|
34
|
+
def __str__(self) -> str:
|
35
|
+
"""Returns a string description of this instance"""
|
36
|
+
total_conversations = len(self._conversations)
|
37
|
+
total_requests = len(self._requests)
|
38
|
+
active_requests = sum(1 for req in self._requests.values()
|
39
|
+
if req["status"] in [HumanLoopStatus.PENDING, HumanLoopStatus.INPROGRESS])
|
40
|
+
|
41
|
+
return (f"conversations={total_conversations}, "
|
42
|
+
f"total_requests={total_requests}, "
|
43
|
+
f"active_requests={active_requests})")
|
44
|
+
|
45
|
+
def __repr__(self) -> str:
|
46
|
+
"""Returns a detailed string representation of this instance"""
|
47
|
+
return self.__str__()
|
48
|
+
|
49
|
+
def _generate_request_id(self) -> str:
|
50
|
+
"""Generates a unique request ID"""
|
51
|
+
return str(uuid.uuid4())
|
52
|
+
|
53
|
+
def _update_request_status_error(
|
54
|
+
self,
|
55
|
+
conversation_id: str,
|
56
|
+
request_id: str,
|
57
|
+
error: Optional[str] = None,
|
58
|
+
):
|
59
|
+
"""Update request status"""
|
60
|
+
request_key = (conversation_id, request_id)
|
61
|
+
if request_key in self._requests:
|
62
|
+
self._requests[request_key]["status"] = HumanLoopStatus.ERROR
|
63
|
+
self._requests[request_key]["error"] = error
|
64
|
+
|
65
|
+
def _store_request(
|
66
|
+
self,
|
67
|
+
conversation_id: str,
|
68
|
+
request_id: str,
|
69
|
+
task_id: str,
|
70
|
+
loop_type: HumanLoopType,
|
71
|
+
context: Dict[str, Any],
|
72
|
+
metadata: Dict[str, Any],
|
73
|
+
timeout: Optional[int],
|
74
|
+
) -> None:
|
75
|
+
"""Store request information"""
|
76
|
+
# Store request information using tuple (conversation_id, request_id) as key
|
77
|
+
self._requests[(conversation_id, request_id)] = {
|
78
|
+
"task_id": task_id,
|
79
|
+
"loop_type": loop_type,
|
80
|
+
"context": context,
|
81
|
+
"metadata": metadata,
|
82
|
+
"created_at": datetime.now().isoformat(),
|
83
|
+
"status": HumanLoopStatus.PENDING,
|
84
|
+
"timeout": timeout,
|
85
|
+
}
|
86
|
+
|
87
|
+
# Update conversation information
|
88
|
+
if conversation_id not in self._conversations:
|
89
|
+
self._conversations[conversation_id] = {
|
90
|
+
"task_id": task_id,
|
91
|
+
"latest_request_id": None,
|
92
|
+
"created_at": datetime.now().isoformat(),
|
93
|
+
}
|
94
|
+
|
95
|
+
# Add request to conversation request list
|
96
|
+
self._conversation_requests[conversation_id].append(request_id)
|
97
|
+
# Update latest request ID
|
98
|
+
self._conversations[conversation_id]["latest_request_id"] = request_id
|
99
|
+
|
100
|
+
def _get_request(self, conversation_id: str, request_id: str) -> Optional[Dict[str, Any]]:
|
101
|
+
"""Get request information"""
|
102
|
+
return self._requests.get((conversation_id, request_id))
|
103
|
+
|
104
|
+
def _get_conversation(self, conversation_id: str) -> Optional[Dict[str, Any]]:
|
105
|
+
"""Get conversation information"""
|
106
|
+
return self._conversations.get(conversation_id)
|
107
|
+
|
108
|
+
def _get_conversation_requests(self, conversation_id: str) -> List[str]:
|
109
|
+
"""Get all request IDs in the conversation"""
|
110
|
+
return self._conversation_requests.get(conversation_id, [])
|
111
|
+
|
112
|
+
async def async_request_humanloop(
|
113
|
+
self,
|
114
|
+
task_id: str,
|
115
|
+
conversation_id: str,
|
116
|
+
loop_type: HumanLoopType,
|
117
|
+
context: Dict[str, Any],
|
118
|
+
metadata: Optional[Dict[str, Any]] = None,
|
119
|
+
timeout: Optional[int] = None
|
120
|
+
) -> HumanLoopResult:
|
121
|
+
"""Request human-in-the-loop interaction
|
122
|
+
|
123
|
+
Args:
|
124
|
+
task_id: Task identifier
|
125
|
+
conversation_id: Conversation ID for multi-turn dialogues
|
126
|
+
loop_type: Type of human loop interaction
|
127
|
+
context: Context information provided to human
|
128
|
+
metadata: Additional metadata
|
129
|
+
timeout: Request timeout in seconds
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
HumanLoopResult: Result object containing request ID and initial status
|
133
|
+
"""
|
134
|
+
# Subclasses must implement this method
|
135
|
+
raise NotImplementedError("Subclasses must implement request_humanloop")
|
136
|
+
|
137
|
+
def request_humanloop(
|
138
|
+
self,
|
139
|
+
task_id: str,
|
140
|
+
conversation_id: str,
|
141
|
+
loop_type: HumanLoopType,
|
142
|
+
context: Dict[str, Any],
|
143
|
+
metadata: Optional[Dict[str, Any]] = None,
|
144
|
+
timeout: Optional[int] = None
|
145
|
+
) -> HumanLoopResult:
|
146
|
+
"""Request human-in-the-loop interaction (synchronous version)
|
147
|
+
|
148
|
+
Args:
|
149
|
+
task_id: Task identifier
|
150
|
+
conversation_id: Conversation ID for multi-turn dialogues
|
151
|
+
loop_type: Type of human loop interaction
|
152
|
+
context: Context information provided to human
|
153
|
+
metadata: Additional metadata
|
154
|
+
timeout: Request timeout in seconds
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
HumanLoopResult: Result object containing request ID and initial status
|
158
|
+
"""
|
159
|
+
|
160
|
+
loop = asyncio.get_event_loop()
|
161
|
+
if loop.is_running():
|
162
|
+
# 如果事件循环已经在运行,创建一个新的事件循环
|
163
|
+
new_loop = asyncio.new_event_loop()
|
164
|
+
asyncio.set_event_loop(new_loop)
|
165
|
+
loop = new_loop
|
166
|
+
|
167
|
+
try:
|
168
|
+
return loop.run_until_complete(
|
169
|
+
self.async_request_humanloop(
|
170
|
+
task_id=task_id,
|
171
|
+
conversation_id=conversation_id,
|
172
|
+
loop_type=loop_type,
|
173
|
+
context=context,
|
174
|
+
metadata=metadata,
|
175
|
+
timeout=timeout
|
176
|
+
))
|
177
|
+
finally:
|
178
|
+
if loop != asyncio.get_event_loop():
|
179
|
+
loop.close()
|
180
|
+
|
181
|
+
|
182
|
+
async def async_check_request_status(
|
183
|
+
self,
|
184
|
+
conversation_id: str,
|
185
|
+
request_id: str
|
186
|
+
) -> HumanLoopResult:
|
187
|
+
"""Check request status
|
188
|
+
|
189
|
+
Args:
|
190
|
+
conversation_id: Conversation identifier for multi-turn dialogues
|
191
|
+
request_id: Request identifier for specific interaction request
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
HumanLoopResult: Result object containing current request status, including status, response data, etc.
|
195
|
+
"""
|
196
|
+
request_info = self._get_request(conversation_id, request_id)
|
197
|
+
if not request_info:
|
198
|
+
return HumanLoopResult(
|
199
|
+
conversation_id=conversation_id,
|
200
|
+
request_id=request_id,
|
201
|
+
loop_type=HumanLoopType.CONVERSATION,
|
202
|
+
status=HumanLoopStatus.ERROR,
|
203
|
+
error=f"Request '{request_id}' not found in conversation '{conversation_id}'"
|
204
|
+
)
|
205
|
+
|
206
|
+
# Subclasses need to implement specific status check logic
|
207
|
+
raise NotImplementedError("Subclasses must implement check_request_status")
|
208
|
+
|
209
|
+
|
210
|
+
def check_request_status(
|
211
|
+
self,
|
212
|
+
conversation_id: str,
|
213
|
+
request_id: str
|
214
|
+
) -> HumanLoopResult:
|
215
|
+
"""Check conversation status (synchronous version)
|
216
|
+
|
217
|
+
Args:
|
218
|
+
conversation_id: Conversation identifier
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
HumanLoopResult: Result containing the status of the latest request in the conversation
|
222
|
+
"""
|
223
|
+
|
224
|
+
loop = asyncio.get_event_loop()
|
225
|
+
if loop.is_running():
|
226
|
+
# 如果事件循环已经在运行,创建一个新的事件循环
|
227
|
+
new_loop = asyncio.new_event_loop()
|
228
|
+
asyncio.set_event_loop(new_loop)
|
229
|
+
loop = new_loop
|
230
|
+
|
231
|
+
try:
|
232
|
+
return loop.run_until_complete(
|
233
|
+
self.async_check_request_status(
|
234
|
+
conversation_id=conversation_id,
|
235
|
+
request_id=request_id
|
236
|
+
))
|
237
|
+
finally:
|
238
|
+
if loop != asyncio.get_event_loop():
|
239
|
+
loop.close()
|
240
|
+
|
241
|
+
|
242
|
+
async def async_check_conversation_status(
|
243
|
+
self,
|
244
|
+
conversation_id: str
|
245
|
+
) -> HumanLoopResult:
|
246
|
+
"""Check conversation status
|
247
|
+
|
248
|
+
Args:
|
249
|
+
conversation_id: Conversation identifier
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
HumanLoopResult: Result containing the status of the latest request in the conversation
|
253
|
+
"""
|
254
|
+
conversation_info = self._get_conversation(conversation_id)
|
255
|
+
if not conversation_info:
|
256
|
+
return HumanLoopResult(
|
257
|
+
conversation_id=conversation_id,
|
258
|
+
request_id="",
|
259
|
+
loop_type=HumanLoopType.CONVERSATION,
|
260
|
+
status=HumanLoopStatus.ERROR,
|
261
|
+
error=f"Conversation '{conversation_id}' not found"
|
262
|
+
)
|
263
|
+
|
264
|
+
latest_request_id = conversation_info.get("latest_request_id")
|
265
|
+
if not latest_request_id:
|
266
|
+
return HumanLoopResult(
|
267
|
+
conversation_id=conversation_id,
|
268
|
+
request_id="",
|
269
|
+
loop_type=HumanLoopType.CONVERSATION,
|
270
|
+
status=HumanLoopStatus.ERROR,
|
271
|
+
error=f"No requests found in conversation '{conversation_id}'"
|
272
|
+
)
|
273
|
+
|
274
|
+
return await self.async_check_request_status(conversation_id, latest_request_id)
|
275
|
+
|
276
|
+
|
277
|
+
def check_conversation_status(
|
278
|
+
self,
|
279
|
+
conversation_id: str
|
280
|
+
) -> HumanLoopResult:
|
281
|
+
"""Check conversation status (synchronous version)
|
282
|
+
|
283
|
+
Args:
|
284
|
+
conversation_id: Conversation identifier
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
HumanLoopResult: Result containing the status of the latest request in the conversation
|
288
|
+
"""
|
289
|
+
|
290
|
+
loop = asyncio.get_event_loop()
|
291
|
+
if loop.is_running():
|
292
|
+
# 如果事件循环已经在运行,创建一个新的事件循环
|
293
|
+
new_loop = asyncio.new_event_loop()
|
294
|
+
asyncio.set_event_loop(new_loop)
|
295
|
+
loop = new_loop
|
296
|
+
|
297
|
+
try:
|
298
|
+
return loop.run_until_complete(
|
299
|
+
self.async_check_conversation_status(
|
300
|
+
conversation_id=conversation_id
|
301
|
+
))
|
302
|
+
finally:
|
303
|
+
if loop != asyncio.get_event_loop():
|
304
|
+
loop.close()
|
305
|
+
|
306
|
+
|
307
|
+
async def async_cancel_request(
|
308
|
+
self,
|
309
|
+
conversation_id: str,
|
310
|
+
request_id: str
|
311
|
+
) -> bool:
|
312
|
+
"""Cancel human-in-the-loop request
|
313
|
+
|
314
|
+
Args:
|
315
|
+
conversation_id: Conversation identifier for multi-turn dialogues
|
316
|
+
request_id: Request identifier for specific interaction request
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
bool: Whether cancellation was successful, True indicates success, False indicates failure
|
320
|
+
"""
|
321
|
+
|
322
|
+
# Cancel timeout task
|
323
|
+
if (conversation_id, request_id) in self._timeout_tasks:
|
324
|
+
self._timeout_tasks[(conversation_id, request_id)].cancel()
|
325
|
+
del self._timeout_tasks[(conversation_id, request_id)]
|
326
|
+
|
327
|
+
request_key = (conversation_id, request_id)
|
328
|
+
if request_key in self._requests:
|
329
|
+
# Update request status to cancelled
|
330
|
+
self._requests[request_key]["status"] = HumanLoopStatus.CANCELLED
|
331
|
+
return True
|
332
|
+
return False
|
333
|
+
|
334
|
+
def cancel_request(
|
335
|
+
self,
|
336
|
+
conversation_id: str,
|
337
|
+
request_id: str
|
338
|
+
) -> bool:
|
339
|
+
"""Cancel human-in-the-loop request (synchronous version)
|
340
|
+
|
341
|
+
Args:
|
342
|
+
conversation_id: Conversation identifier for multi-turn dialogues
|
343
|
+
request_id: Request identifier for specific interaction request
|
344
|
+
|
345
|
+
Returns:
|
346
|
+
bool: Whether cancellation was successful, True indicates success, False indicates failure
|
347
|
+
"""
|
348
|
+
loop = asyncio.get_event_loop()
|
349
|
+
if loop.is_running():
|
350
|
+
# 如果事件循环已经在运行,创建一个新的事件循环
|
351
|
+
new_loop = asyncio.new_event_loop()
|
352
|
+
asyncio.set_event_loop(new_loop)
|
353
|
+
loop = new_loop
|
354
|
+
|
355
|
+
try:
|
356
|
+
return loop.run_until_complete(
|
357
|
+
self.async_cancel_request(
|
358
|
+
conversation_id=conversation_id,
|
359
|
+
request_id=request_id
|
360
|
+
))
|
361
|
+
finally:
|
362
|
+
if loop != asyncio.get_event_loop():
|
363
|
+
loop.close()
|
364
|
+
|
365
|
+
async def async_cancel_conversation(
|
366
|
+
self,
|
367
|
+
conversation_id: str
|
368
|
+
) -> bool:
|
369
|
+
"""Cancel the entire conversation
|
370
|
+
|
371
|
+
Args:
|
372
|
+
conversation_id: Conversation identifier
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
bool: Whether the cancellation was successful
|
376
|
+
"""
|
377
|
+
if conversation_id not in self._conversations:
|
378
|
+
return False
|
379
|
+
|
380
|
+
# Cancel all requests in the conversation
|
381
|
+
success = True
|
382
|
+
for request_id in self._get_conversation_requests(conversation_id):
|
383
|
+
request_key = (conversation_id, request_id)
|
384
|
+
if request_key in self._requests:
|
385
|
+
# Update request status to cancelled
|
386
|
+
# Only requests in intermediate states (PENDING/IN_PROGRESS) can be cancelled
|
387
|
+
if self._requests[request_key]["status"] in [HumanLoopStatus.PENDING, HumanLoopStatus.INPROGRESS]:
|
388
|
+
self._requests[request_key]["status"] = HumanLoopStatus.CANCELLED
|
389
|
+
|
390
|
+
# Cancel the timeout task for this request
|
391
|
+
if request_key in self._timeout_tasks:
|
392
|
+
self._timeout_tasks[request_key].cancel()
|
393
|
+
del self._timeout_tasks[request_key]
|
394
|
+
else:
|
395
|
+
success = False
|
396
|
+
|
397
|
+
return success
|
398
|
+
|
399
|
+
|
400
|
+
def cancel_conversation(
|
401
|
+
self,
|
402
|
+
conversation_id: str
|
403
|
+
) -> bool:
|
404
|
+
"""Cancel the entire conversation (synchronous version)
|
405
|
+
|
406
|
+
Args:
|
407
|
+
conversation_id: Conversation identifier
|
408
|
+
|
409
|
+
Returns:
|
410
|
+
bool: Whether the cancellation was successful
|
411
|
+
"""
|
412
|
+
|
413
|
+
loop = asyncio.get_event_loop()
|
414
|
+
if loop.is_running():
|
415
|
+
# 如果事件循环已经在运行,创建一个新的事件循环
|
416
|
+
new_loop = asyncio.new_event_loop()
|
417
|
+
asyncio.set_event_loop(new_loop)
|
418
|
+
loop = new_loop
|
419
|
+
|
420
|
+
try:
|
421
|
+
return loop.run_until_complete(
|
422
|
+
self.async_cancel_conversation(
|
423
|
+
conversation_id=conversation_id
|
424
|
+
))
|
425
|
+
finally:
|
426
|
+
if loop != asyncio.get_event_loop():
|
427
|
+
loop.close()
|
428
|
+
|
429
|
+
|
430
|
+
async def async_continue_humanloop(
|
431
|
+
self,
|
432
|
+
conversation_id: str,
|
433
|
+
context: Dict[str, Any],
|
434
|
+
metadata: Optional[Dict[str, Any]] = None,
|
435
|
+
timeout: Optional[int] = None,
|
436
|
+
) -> HumanLoopResult:
|
437
|
+
"""Continue human-in-the-loop interaction
|
438
|
+
|
439
|
+
Args:
|
440
|
+
conversation_id: Conversation ID for multi-turn dialogues
|
441
|
+
context: Context information provided to human
|
442
|
+
metadata: Additional metadata
|
443
|
+
timeout: Request timeout in seconds
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
HumanLoopResult: Result object containing request ID and status
|
447
|
+
"""
|
448
|
+
# Check if conversation exists
|
449
|
+
conversation_info = self._get_conversation(conversation_id)
|
450
|
+
if not conversation_info:
|
451
|
+
return HumanLoopResult(
|
452
|
+
conversation_id=conversation_id,
|
453
|
+
request_id="",
|
454
|
+
loop_type=HumanLoopType.CONVERSATION,
|
455
|
+
status=HumanLoopStatus.ERROR,
|
456
|
+
error=f"Conversation '{conversation_id}' not found"
|
457
|
+
)
|
458
|
+
|
459
|
+
# Subclasses need to implement specific continuation logic
|
460
|
+
raise NotImplementedError("Subclasses must implement continue_humanloop")
|
461
|
+
|
462
|
+
|
463
|
+
def continue_humanloop(
|
464
|
+
self,
|
465
|
+
conversation_id: str,
|
466
|
+
context: Dict[str, Any],
|
467
|
+
metadata: Optional[Dict[str, Any]] = None,
|
468
|
+
timeout: Optional[int] = None,
|
469
|
+
) -> HumanLoopResult:
|
470
|
+
"""Continue human-in-the-loop interaction (synchronous version)
|
471
|
+
|
472
|
+
Args:
|
473
|
+
conversation_id: Conversation ID for multi-turn dialogues
|
474
|
+
context: Context information provided to human
|
475
|
+
metadata: Additional metadata
|
476
|
+
timeout: Request timeout in seconds
|
477
|
+
|
478
|
+
Returns:
|
479
|
+
HumanLoopResult: Result object containing request ID and status
|
480
|
+
"""
|
481
|
+
|
482
|
+
loop = asyncio.get_event_loop()
|
483
|
+
if loop.is_running():
|
484
|
+
# 如果事件循环已经在运行,创建一个新的事件循环
|
485
|
+
new_loop = asyncio.new_event_loop()
|
486
|
+
asyncio.set_event_loop(new_loop)
|
487
|
+
loop = new_loop
|
488
|
+
|
489
|
+
try:
|
490
|
+
return loop.run_until_complete(
|
491
|
+
self.async_continue_humanloop(
|
492
|
+
conversation_id=conversation_id,
|
493
|
+
context=context,
|
494
|
+
metadata=metadata,
|
495
|
+
timeout=timeout
|
496
|
+
))
|
497
|
+
finally:
|
498
|
+
if loop != asyncio.get_event_loop():
|
499
|
+
loop.close()
|
500
|
+
|
501
|
+
def async_get_conversation_history(self, conversation_id: str) -> List[Dict[str, Any]]:
|
502
|
+
"""Get complete history for the specified conversation
|
503
|
+
|
504
|
+
Args:
|
505
|
+
conversation_id: Conversation identifier
|
506
|
+
|
507
|
+
Returns:
|
508
|
+
List[Dict[str, Any]]: List of conversation history records, each containing request ID,
|
509
|
+
status, context, response and other information
|
510
|
+
"""
|
511
|
+
conversation_history = []
|
512
|
+
for request_id in self._get_conversation_requests(conversation_id):
|
513
|
+
request_key = (conversation_id, request_id)
|
514
|
+
if request_key in self._requests:
|
515
|
+
request_info = self._requests[request_key]
|
516
|
+
conversation_history.append({
|
517
|
+
"request_id": request_id,
|
518
|
+
"status": request_info.get("status").value if request_info.get("status") else None,
|
519
|
+
"context": request_info.get("context"),
|
520
|
+
"response": request_info.get("response"),
|
521
|
+
"responded_by": request_info.get("responded_by"),
|
522
|
+
"responded_at": request_info.get("responded_at")
|
523
|
+
})
|
524
|
+
return conversation_history
|
525
|
+
|
526
|
+
def get_conversation_history(self, conversation_id: str) -> List[Dict[str, Any]]:
|
527
|
+
"""Get complete history for the specified conversation (synchronous version)
|
528
|
+
|
529
|
+
Args:
|
530
|
+
conversation_id: Conversation identifier
|
531
|
+
|
532
|
+
Returns:
|
533
|
+
List[Dict[str, Any]]: List of conversation history records, each containing request ID,
|
534
|
+
status, context, response and other information
|
535
|
+
"""
|
536
|
+
loop = asyncio.get_event_loop()
|
537
|
+
if loop.is_running():
|
538
|
+
new_loop = asyncio.new_event_loop()
|
539
|
+
asyncio.set_event_loop(new_loop)
|
540
|
+
loop = new_loop
|
541
|
+
|
542
|
+
try:
|
543
|
+
return loop.run_until_complete(self.async_get_conversation_history(conversation_id))
|
544
|
+
finally:
|
545
|
+
if loop != asyncio.get_event_loop():
|
546
|
+
loop.close()
|
547
|
+
|
548
|
+
|
549
|
+
async def _async_create_timeout_task(
|
550
|
+
self,
|
551
|
+
conversation_id: str,
|
552
|
+
request_id: str,
|
553
|
+
timeout: int
|
554
|
+
):
|
555
|
+
"""Create timeout task
|
556
|
+
|
557
|
+
Args:
|
558
|
+
conversation_id: Conversation ID
|
559
|
+
request_id: Request ID
|
560
|
+
timeout: Timeout duration in seconds
|
561
|
+
"""
|
562
|
+
async def timeout_task():
|
563
|
+
await asyncio.sleep(timeout)
|
564
|
+
|
565
|
+
# Check current status
|
566
|
+
request_info = self._get_request(conversation_id, request_id)
|
567
|
+
if not request_info:
|
568
|
+
return
|
569
|
+
|
570
|
+
current_status = request_info.get("status", HumanLoopStatus.PENDING)
|
571
|
+
|
572
|
+
# Only trigger timeout when status is PENDING
|
573
|
+
# INPROGRESS status means conversation is ongoing, should not be considered as timeout
|
574
|
+
if current_status == HumanLoopStatus.PENDING:
|
575
|
+
# Update request status to expired
|
576
|
+
request_info["status"] = HumanLoopStatus.EXPIRED
|
577
|
+
request_info["error"] = "Request timed out"
|
578
|
+
# If status is INPROGRESS, reset timeout task
|
579
|
+
elif current_status == HumanLoopStatus.INPROGRESS:
|
580
|
+
# For ongoing conversations, we can choose to extend the timeout
|
581
|
+
# Here we simply create a new timeout task with the same timeout duration
|
582
|
+
if (conversation_id, request_id) in self._timeout_tasks:
|
583
|
+
self._timeout_tasks[(conversation_id, request_id)].cancel()
|
584
|
+
new_task = asyncio.create_task(timeout_task())
|
585
|
+
self._timeout_tasks[(conversation_id, request_id)] = new_task
|
586
|
+
|
587
|
+
task = asyncio.create_task(timeout_task())
|
588
|
+
self._timeout_tasks[(conversation_id, request_id)] = task
|
589
|
+
|
590
|
+
|
591
|
+
def build_prompt(
|
592
|
+
self,
|
593
|
+
task_id: str,
|
594
|
+
conversation_id: str,
|
595
|
+
request_id: str,
|
596
|
+
loop_type: Any,
|
597
|
+
created_at: str,
|
598
|
+
context: Dict[str, Any],
|
599
|
+
metadata: Optional[Dict[str, Any]] = None,
|
600
|
+
color: Optional[bool] = None
|
601
|
+
) -> str:
|
602
|
+
"""
|
603
|
+
Dynamically generate prompt based on content, only showing sections with content,
|
604
|
+
and adapt to different terminal color display.
|
605
|
+
color: None=auto detect, True=force color, False=no color
|
606
|
+
"""
|
607
|
+
# Auto detect if terminal supports ANSI colors
|
608
|
+
def _supports_color():
|
609
|
+
try:
|
610
|
+
import sys
|
611
|
+
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
612
|
+
return False
|
613
|
+
import os
|
614
|
+
if os.name == "nt":
|
615
|
+
# Windows 10+ supports ANSI, older versions don't
|
616
|
+
return "ANSICON" in os.environ or "WT_SESSION" in os.environ
|
617
|
+
return True
|
618
|
+
except Exception:
|
619
|
+
return False
|
620
|
+
|
621
|
+
if color is None:
|
622
|
+
color = _supports_color()
|
623
|
+
|
624
|
+
# Define colors
|
625
|
+
if color:
|
626
|
+
COLOR_TITLE = "\033[94m" # bright blue
|
627
|
+
COLOR_RESET = "\033[0m"
|
628
|
+
else:
|
629
|
+
COLOR_TITLE = ""
|
630
|
+
COLOR_RESET = ""
|
631
|
+
|
632
|
+
lines = []
|
633
|
+
lines.append(f"{COLOR_TITLE}=== Task Information ==={COLOR_RESET}")
|
634
|
+
lines.append(f"Task ID: {task_id}")
|
635
|
+
lines.append(f"Conversation ID: {conversation_id}")
|
636
|
+
lines.append(f"Request ID: {request_id}")
|
637
|
+
lines.append(f"HumanLoop Type: {getattr(loop_type, 'value', loop_type)}")
|
638
|
+
lines.append(f"Created At: {created_at}")
|
639
|
+
|
640
|
+
if context.get("message"):
|
641
|
+
lines.append(f"\n{COLOR_TITLE}=== Main Context ==={COLOR_RESET}")
|
642
|
+
lines.append(json.dumps(context["message"], indent=2, ensure_ascii=False))
|
643
|
+
|
644
|
+
if context.get("additional"):
|
645
|
+
lines.append(f"\n{COLOR_TITLE}=== Additional Context ==={COLOR_RESET}")
|
646
|
+
lines.append(json.dumps(context["additional"], indent=2, ensure_ascii=False))
|
647
|
+
|
648
|
+
if metadata:
|
649
|
+
lines.append(f"\n{COLOR_TITLE}=== Metadata ==={COLOR_RESET}")
|
650
|
+
lines.append(json.dumps(metadata, indent=2, ensure_ascii=False))
|
651
|
+
|
652
|
+
if context.get("question"):
|
653
|
+
lines.append(f"\n{COLOR_TITLE}=== Question ==={COLOR_RESET}")
|
654
|
+
lines.append(str(context["question"]))
|
655
|
+
|
656
|
+
lines.append(f"\n{COLOR_TITLE}=== END ==={COLOR_RESET}")
|
657
|
+
|
658
|
+
return "\n".join(lines)
|