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.
File without changes
@@ -0,0 +1,532 @@
1
+ from typing import Dict, Any, Optional, List, Union
2
+ import os
3
+ import asyncio
4
+ import aiohttp
5
+ import time
6
+ import threading
7
+ from datetime import datetime
8
+
9
+ from gohumanloop.core.manager import DefaultHumanLoopManager
10
+ from gohumanloop.providers.ghl_provider import GoHumanLoopProvider
11
+ from gohumanloop.core.interface import HumanLoopProvider, HumanLoopStatus, HumanLoopType, HumanLoopResult
12
+ from gohumanloop.utils import get_secret_from_env
13
+ from gohumanloop.models.glh_model import GoHumanLoopConfig
14
+
15
+ class GoHumanLoopManager(DefaultHumanLoopManager):
16
+ """
17
+ GoHumanLoop 官方平台的人机交互管理器
18
+
19
+ 这个管理器专门使用 GoHumanLoopProvider 作为提供者,用于将交互过程数据传输到 GoHumanLoop 平台。
20
+ 它是 DefaultHumanLoopManager 的一个特殊实现,简化了与 GoHumanLoop 平台的集成。
21
+
22
+ 主要职责:
23
+ 1. 管理整个人机交互任务
24
+ 2. 将交互任务数据统一传输到 GoHumanLoop 平台
25
+ """
26
+ def __init__(
27
+ self,
28
+ request_timeout: int = 60,
29
+ poll_interval: int = 5,
30
+ max_retries: int = 3,
31
+ sync_interval: int = 60, # 数据同步间隔(秒)
32
+ additional_providers: Optional[List[HumanLoopProvider]] = None,
33
+ auto_start_sync: bool = True, # 是否自动启动数据同步任务
34
+ config: Optional[Dict[str, Any]] = None,
35
+ ):
36
+ """
37
+ 初始化 GoHumanLoop 管理器
38
+
39
+ Args:
40
+ request_timeout: API 请求超时时间(秒),默认60秒
41
+ poll_interval: 轮询检查请求状态的时间间隔(秒),默认5秒
42
+ max_retries: API 请求失败时的最大重试次数,默认3次
43
+ sync_interval: 数据同步到平台的时间间隔(秒),默认60秒
44
+ additional_providers: 额外的人机交互提供者列表,可选
45
+ auto_start_sync: 是否在初始化时自动启动数据同步任务,默认为True
46
+ config: 附加配置参数字典,可选
47
+ """
48
+ # Get API key from environment variables (if not provided)
49
+ api_key = get_secret_from_env("GOHUMANLOOP_API_KEY")
50
+
51
+ # Get API base URL from environment variables (if not provided)
52
+ api_base_url = os.environ.get("GOHUMANLOOP_API_BASE_URL", "https://www.gohumanloop.com")
53
+
54
+ # Validate configuration using pydantic model
55
+ self.ghl_config = GoHumanLoopConfig(
56
+ api_key=api_key,
57
+ api_base_url=api_base_url
58
+ )
59
+
60
+ self.name = "GoHumanLoop"
61
+ # 创建 GoHumanLoop 提供者
62
+ ghl_provider = GoHumanLoopProvider(
63
+ name=self.name,
64
+ request_timeout=request_timeout,
65
+ poll_interval=poll_interval,
66
+ max_retries=max_retries,
67
+ config=config
68
+ )
69
+
70
+ # 初始化提供者列表
71
+ providers = [ghl_provider]
72
+ if additional_providers:
73
+ providers.extend(additional_providers)
74
+
75
+ # 调用父类初始化方法
76
+ super().__init__(initial_providers=providers)
77
+
78
+ # 设置 GoHumanLoop 提供者为默认提供者
79
+ self.default_provider_id = self.name
80
+
81
+ # 存储最近同步时间
82
+ self._last_sync_time = time.time()
83
+
84
+ # 同步间隔
85
+ self.sync_interval = sync_interval
86
+
87
+ # 存储已取消的请求和对话信息
88
+ self._cancelled_conversations = {} # conversation_id -> 取消信息
89
+
90
+ # 同步任务引用
91
+ self._sync_task = None
92
+
93
+ # 同步模式相关属性
94
+ self._sync_thread = None
95
+ self._sync_thread_stop_event = threading.Event()
96
+
97
+ # 启动数据同步任务
98
+ if auto_start_sync:
99
+ # 判断是否处在异步环境
100
+ if asyncio.get_event_loop().is_running():
101
+ asyncio.create_task(self.async_start_sync_task())
102
+ else:
103
+ self.start_sync_task()
104
+
105
+
106
+ async def async_get_ghl_provider(self) -> GoHumanLoopProvider:
107
+ """
108
+ 获取 GoHumanLoop 提供者实例
109
+
110
+ Returns:
111
+ GoHumanLoopProvider: GoHumanLoop 提供者实例
112
+ """
113
+ provider = await self.async_get_provider(self.default_provider_id)
114
+ return provider
115
+
116
+ async def async_start_sync_task(self):
117
+ """启动数据同步任务"""
118
+ if self._sync_task is None or self._sync_task.done():
119
+ self._sync_task = asyncio.create_task(self._async_data_periodically())
120
+
121
+ async def _async_data_periodically(self):
122
+ """定期同步数据到 GoHumanLoop 平台"""
123
+ while True:
124
+ try:
125
+ await asyncio.sleep(self.sync_interval)
126
+ await self.async_data_to_platform()
127
+ except Exception as e:
128
+ # 记录错误但不中断同步循环
129
+ print(f"数据同步错误: {str(e)}")
130
+ except asyncio.CancelledError:
131
+ # 任务被取消时,确保最后同步一次数据
132
+ try:
133
+ await self.async_data_to_platform()
134
+ except Exception as e:
135
+ print(f"最终数据同步错误: {str(e)}")
136
+ raise # 重新抛出取消异常
137
+
138
+ def start_sync_task(self):
139
+ """启动同步版本的数据同步任务"""
140
+ if self._sync_thread is None or not self._sync_thread.is_alive():
141
+ self._sync_thread_stop_event.clear()
142
+ self._sync_thread = threading.Thread(
143
+ target=self._sync_data_periodically,
144
+ daemon=True
145
+ )
146
+ self._sync_thread.start()
147
+
148
+ def _sync_data_periodically(self):
149
+ """同步版本:定期同步数据到 GoHumanLoop 平台"""
150
+ while not self._sync_thread_stop_event.is_set():
151
+ try:
152
+ # 同步版本使用 time.sleep 而不是 asyncio.sleep
153
+ time.sleep(self.sync_interval)
154
+ self.sync_data_to_platform()
155
+ except Exception as e:
156
+ # 记录错误但不中断同步循环
157
+ print(f"同步数据同步错误: {str(e)}")
158
+
159
+ # 线程结束前执行最后一次同步
160
+ try:
161
+ self.sync_data_to_platform()
162
+ except Exception as e:
163
+ print(f"最终同步数据同步错误: {str(e)}")
164
+
165
+ async def async_data_to_platform(self):
166
+ """
167
+ 同步数据到 GoHumanLoop 平台
168
+
169
+ 此方法收集所有任务的数据,并通过 API 发送到 GoHumanLoop 平台
170
+ """
171
+ current_time = time.time()
172
+
173
+ # 获取所有任务ID
174
+ task_ids = list(self._task_conversations.keys())
175
+
176
+ # 对每个任务进行数据同步
177
+ for task_id in task_ids:
178
+ # 获取任务相关的所有对话
179
+ conversations = await self.async_get_task_conversations(task_id)
180
+
181
+ # 收集任务数据
182
+ task_data = {
183
+ "task_id": task_id,
184
+ "conversations": [],
185
+ "timestamp": datetime.now().isoformat()
186
+ }
187
+
188
+ # 收集每个对话的数据
189
+ for conversation_id in conversations:
190
+ # 获取对话中的所有请求
191
+ request_ids = await self.async_get_conversation_requests(conversation_id)
192
+
193
+ conversation_data = {
194
+ "conversation_id": conversation_id,
195
+ "provider_id": self._conversation_provider.get(conversation_id),
196
+ "requests": []
197
+ }
198
+
199
+ # 收集每个请求的数据
200
+ for request_id in request_ids:
201
+ result = await self._async_get_request_status(conversation_id, request_id)
202
+
203
+ # 添加请求数据
204
+ conversation_data["requests"].append({
205
+ "request_id": request_id,
206
+ "status": result.status.value,
207
+ "loop_type": result.loop_type.value,
208
+ "response": result.response,
209
+ "feedback": result.feedback,
210
+ "responded_by": result.responded_by,
211
+ "responded_at": result.responded_at,
212
+ "error": result.error
213
+ })
214
+
215
+ # 添加对话数据
216
+ task_data["conversations"].append(conversation_data)
217
+
218
+ # 检查是否有已取消的对话需要添加
219
+ for conv_id, cancel_info in self._cancelled_conversations.items():
220
+ if conv_id not in conversations:
221
+ # 创建已取消对话的数据
222
+ cancelled_conv_data = {
223
+ "conversation_id": conv_id,
224
+ "provider_id": cancel_info.get("provider_id"),
225
+ "requests": []
226
+ }
227
+
228
+ # 添加此对话中的已取消请求
229
+ for request_id in cancel_info.get("request_ids", []):
230
+ result = await self._async_get_request_status(conv_id, request_id, cancel_info.get("provider_id"))
231
+
232
+ # 添加请求数据
233
+ cancelled_conv_data["requests"].append({
234
+ "request_id": request_id,
235
+ "status": result.status.value,
236
+ "loop_type": result.loop_type.value,
237
+ "response": result.response,
238
+ "feedback": result.feedback,
239
+ "responded_by": result.responded_by,
240
+ "responded_at": result.responded_at,
241
+ "error": result.error
242
+ })
243
+
244
+ # 添加已取消的对话
245
+ task_data["conversations"].append(cancelled_conv_data)
246
+
247
+ # 发送数据到平台
248
+ await self._async_send_task_data_to_platform(task_data)
249
+
250
+ # 更新最后同步时间
251
+ self._last_sync_time = current_time
252
+
253
+ def sync_data_to_platform(self):
254
+ """
255
+ 同步版本:同步数据到 GoHumanLoop 平台
256
+
257
+ 此方法收集所有任务的数据,并通过 API 发送到 GoHumanLoop 平台
258
+ """
259
+ current_time = time.time()
260
+
261
+ # 获取所有任务ID
262
+ task_ids = list(self._task_conversations.keys())
263
+
264
+ # 对每个任务进行数据同步
265
+ for task_id in task_ids:
266
+ # 获取任务相关的所有对话
267
+ # 使用同步方式运行异步方法
268
+ loop = asyncio.new_event_loop()
269
+ conversations = self.get_task_conversations(task_id)
270
+
271
+ # 收集任务数据
272
+ task_data = {
273
+ "task_id": task_id,
274
+ "conversations": [],
275
+ "timestamp": datetime.now().isoformat()
276
+ }
277
+
278
+ # 收集每个对话的数据
279
+ for conversation_id in conversations:
280
+ # 获取对话中的所有请求
281
+ request_ids = self.get_conversation_requests(conversation_id)
282
+
283
+ conversation_data = {
284
+ "conversation_id": conversation_id,
285
+ "provider_id": self._conversation_provider.get(conversation_id),
286
+ "requests": []
287
+ }
288
+
289
+ # 收集每个请求的数据
290
+ for request_id in request_ids:
291
+ result = loop.run_until_complete(self._async_get_request_status(conversation_id, request_id))
292
+
293
+ # 添加请求数据
294
+ conversation_data["requests"].append({
295
+ "request_id": request_id,
296
+ "status": result.status.value,
297
+ "loop_type": result.loop_type.value,
298
+ "response": result.response,
299
+ "feedback": result.feedback,
300
+ "responded_by": result.responded_by,
301
+ "responded_at": result.responded_at,
302
+ "error": result.error
303
+ })
304
+
305
+ # 添加对话数据
306
+ task_data["conversations"].append(conversation_data)
307
+
308
+ # 检查是否有已取消的对话需要添加
309
+ for conv_id, cancel_info in self._cancelled_conversations.items():
310
+ if conv_id not in conversations:
311
+ # 创建已取消对话的数据
312
+ cancelled_conv_data = {
313
+ "conversation_id": conv_id,
314
+ "provider_id": cancel_info.get("provider_id"),
315
+ "requests": []
316
+ }
317
+
318
+ # 添加此对话中的已取消请求
319
+ for request_id in cancel_info.get("request_ids", []):
320
+ result = loop.run_until_complete(
321
+ self._async_get_request_status(conv_id, request_id, cancel_info.get("provider_id"))
322
+ )
323
+
324
+ # 添加请求数据
325
+ cancelled_conv_data["requests"].append({
326
+ "request_id": request_id,
327
+ "status": result.status.value,
328
+ "loop_type": result.loop_type.value,
329
+ "response": result.response,
330
+ "feedback": result.feedback,
331
+ "responded_by": result.responded_by,
332
+ "responded_at": result.responded_at,
333
+ "error": result.error
334
+ })
335
+
336
+ # 添加已取消的对话
337
+ task_data["conversations"].append(cancelled_conv_data)
338
+
339
+ # 发送数据到平台
340
+ loop.run_until_complete(self._async_send_task_data_to_platform(task_data))
341
+ loop.close()
342
+
343
+ # 更新最后同步时间
344
+ self._last_sync_time = current_time
345
+
346
+ async def _async_send_task_data_to_platform(self, task_data: Dict[str, Any]):
347
+ """发送任务数据到 GoHumanLoop 平台"""
348
+ try:
349
+ # 构建 API 请求 URL
350
+ api_base_url = self.ghl_config.api_base_url
351
+ url = f"{api_base_url}/v1/humanloop/tasks/sync"
352
+
353
+ # 构建请求头
354
+ headers = {
355
+ "Content-Type": "application/json",
356
+ "Authorization": f"Bearer {self.ghl_config.api_key.get_secret_value()}"
357
+ }
358
+
359
+ # 使用 aiohttp 直接发送请求,而不依赖于 provider
360
+ async with aiohttp.ClientSession() as session:
361
+ try:
362
+ async with session.post(
363
+ url=url,
364
+ headers=headers,
365
+ json=task_data,
366
+ timeout=30 # 设置合理的超时时间
367
+ ) as response:
368
+ # 处理 404 错误
369
+ if response.status == 404:
370
+ print(f"同步任务数据失败: API 端点不存在 - {url}")
371
+ return
372
+ # 处理 401 错误
373
+ elif response.status == 401:
374
+ print(f"同步任务数据失败: 认证失败 - 请检查 API 密钥")
375
+ return
376
+
377
+ # 处理其他错误状态码
378
+ if not response.ok:
379
+ print(f"同步任务数据失败: HTTP {response.status} - {response.reason}")
380
+ return
381
+
382
+ response_data = await response.json()
383
+
384
+ if not response_data.get("success", False):
385
+ error_msg = response_data.get("error", "未知错误")
386
+ print(f"同步任务数据失败: {error_msg}")
387
+ except aiohttp.ClientError as e:
388
+ print(f"HTTP 请求异常: {str(e)}")
389
+ except Exception as e:
390
+ print(f"发送任务数据到平台异常: {str(e)}")
391
+
392
+
393
+ async def async_cancel_conversation(
394
+ self,
395
+ conversation_id: str,
396
+ provider_id: Optional[str] = None
397
+ ) -> bool:
398
+ """
399
+ 取消整个对话,并保存取消信息用于后续同步
400
+
401
+ 重写父类方法,在调用父类方法前保存对话信息
402
+ """
403
+ # 获取对话关联的provider_id
404
+ if provider_id is None:
405
+ provider_id = self._conversation_provider.get(conversation_id)
406
+
407
+ # 保存对话取消信息,包括provider_id
408
+ self._cancelled_conversations[conversation_id] = {
409
+ "request_ids": list(self._conversation_requests.get(conversation_id, [])),
410
+ "provider_id": provider_id
411
+ }
412
+
413
+ # 调用父类方法执行实际取消操作
414
+ return await super().async_cancel_conversation(conversation_id, provider_id)
415
+
416
+ def __str__(self) -> str:
417
+ """返回此实例的字符串描述"""
418
+ return f"GoHumanLoop(default_provider={self.default_provider_id}, providers={len(self.providers)})"
419
+
420
+ async def _async_get_request_status(
421
+ self,
422
+ conversation_id: str,
423
+ request_id: str,
424
+ provider_id: Optional[str] = None
425
+ ) -> HumanLoopResult:
426
+ """
427
+ 获取请求状态的辅助方法
428
+
429
+ Args:
430
+ conversation_id: 对话ID
431
+ request_id: 请求ID
432
+ provider_id: 提供者ID(可选)
433
+
434
+ Returns:
435
+ HumanLoopResult: 请求状态结果
436
+ """
437
+ # 如果没有指定provider_id,从存储的映射中获取
438
+ if provider_id is None:
439
+ provider_id = self._conversation_provider.get(conversation_id)
440
+
441
+ if not provider_id or provider_id not in self.providers:
442
+ raise ValueError(f"Provider '{provider_id}' not found")
443
+
444
+ provider = self.providers[provider_id]
445
+ return await provider.check_request_status(conversation_id, request_id)
446
+
447
+ async def async_check_request_status(
448
+ self,
449
+ conversation_id: str,
450
+ request_id: str,
451
+ provider_id: Optional[str] = None
452
+ ) -> HumanLoopResult:
453
+ """
454
+ 重写父类方法,增加数据同步操作
455
+ """
456
+ # 如果没有指定provider_id,尝试从存储的映射中获取
457
+ if provider_id is None:
458
+ stored_provider_id = self._conversation_provider.get(conversation_id)
459
+ provider_id = stored_provider_id or self.default_provider_id
460
+
461
+ if not provider_id or provider_id not in self.providers:
462
+ raise ValueError(f"Provider '{provider_id}' not found")
463
+
464
+ provider = self.providers[provider_id]
465
+ result = await provider.async_check_request_status(conversation_id, request_id)
466
+
467
+ # 如果有回调且状态不是等待或进行中
468
+ if result.status not in [HumanLoopStatus.PENDING]:
469
+ # 同步数据到平台
470
+ await self.async_data_to_platform()
471
+ # 触发状态更新回调
472
+ if (conversation_id, request_id) in self._callbacks:
473
+ await self._async_trigger_update_callback(conversation_id, request_id, provider, result)
474
+
475
+ return result
476
+
477
+ def shutdown(self):
478
+ """
479
+ 关闭管理器并确保数据同步(同步版本)
480
+
481
+ 在程序退出前调用此方法,确保所有数据都被同步到平台
482
+ """
483
+ # 停止同步线程
484
+ if self._sync_thread and self._sync_thread.is_alive():
485
+ self._sync_thread_stop_event.set()
486
+ self._sync_thread.join(timeout=5) # 等待线程结束,最多5秒
487
+
488
+ # 执行最后一次同步数据同步
489
+ try:
490
+ self.sync_data_to_platform()
491
+ print("最终同步数据同步完成")
492
+ except Exception as e:
493
+ print(f"最终同步数据同步失败: {str(e)}")
494
+
495
+ async def async_shutdown(self):
496
+ """
497
+ 关闭管理器并确保数据同步(异步版本)
498
+
499
+ 在程序退出前调用此方法,确保所有数据都被同步到平台
500
+ """
501
+ # 取消周期性同步任务
502
+ if self._sync_task and not self._sync_task.done():
503
+ self._sync_task.cancel()
504
+ try:
505
+ await self._sync_task
506
+ except asyncio.CancelledError:
507
+ pass # 预期的异常,忽略
508
+ # 执行最后一次数据同步
509
+ try:
510
+ await self.async_data_to_platform()
511
+ print("最终异步数据同步完成")
512
+ except Exception as e:
513
+ print(f"最终异步数据同步失败: {str(e)}")
514
+
515
+ async def __aenter__(self):
516
+ """实现异步上下文管理器协议的进入方法"""
517
+ await self.async_start_sync_task()
518
+ return self
519
+
520
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
521
+ """实现异步上下文管理器协议的退出方法"""
522
+ await self.async_shutdown()
523
+
524
+ def __enter__(self):
525
+ """实现同步上下文管理器协议的进入方法"""
526
+ # 使用同步模式
527
+ self.start_sync_task()
528
+ return self
529
+
530
+ def __exit__(self, exc_type, exc_val, exc_tb):
531
+ """实现同步上下文管理器协议的退出方法"""
532
+ self.shutdown()
File without changes
@@ -0,0 +1,54 @@
1
+ from typing import Dict, Any, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ # Define the data models for requests and responses
6
+ class APIResponse(BaseModel):
7
+ """Base model for API responses"""
8
+ success: bool = Field(default=False, description="Whether the request was successful")
9
+ error: Optional[str] = Field(default=None, description="Error message if any")
10
+
11
+ class HumanLoopRequestData(BaseModel):
12
+ """Model for human-in-the-loop request data"""
13
+ task_id: str = Field(description="Task identifier")
14
+ conversation_id: str = Field(description="Conversation identifier")
15
+ request_id: str = Field(description="Request identifier")
16
+ loop_type: str = Field(description="Type of loop")
17
+ context: Dict[str, Any] = Field(description="Context information provided to humans")
18
+ platform: str = Field(description="Platform being used, e.g. wechat, feishu")
19
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
20
+
21
+ class HumanLoopStatusParams(BaseModel):
22
+ """Model for getting human-in-the-loop status parameters"""
23
+ conversation_id: str = Field(description="Conversation identifier")
24
+ request_id: str = Field(description="Request identifier")
25
+ platform: str = Field(description="Platform being used")
26
+
27
+ class HumanLoopStatusResponse(APIResponse):
28
+ """Model for human-in-the-loop status response"""
29
+ status: str = Field(default="pending", description="Request status")
30
+ response: Optional[Any] = Field(default=None, description="Human response data")
31
+ feedback: Optional[Any] = Field(default=None, description="Feedback data")
32
+ responded_by: Optional[str] = Field(default=None, description="Responder information")
33
+ responded_at: Optional[str] = Field(default=None, description="Response timestamp")
34
+
35
+ class HumanLoopCancelData(BaseModel):
36
+ """Model for canceling human-in-the-loop request"""
37
+ conversation_id: str = Field(description="Conversation identifier")
38
+ request_id: str = Field(description="Request identifier")
39
+ platform: str = Field(description="Platform being used")
40
+
41
+ class HumanLoopCancelConversationData(BaseModel):
42
+ """Model for canceling entire conversation"""
43
+ conversation_id: str = Field(description="Conversation identifier")
44
+ platform: str = Field(description="Platform being used")
45
+
46
+ class HumanLoopContinueData(BaseModel):
47
+ """Model for continuing human-in-the-loop interaction"""
48
+ conversation_id: str = Field(description="Conversation identifier")
49
+ request_id: str = Field(description="Request identifier")
50
+ task_id: str = Field(description="Task identifier")
51
+ context: Dict[str, Any] = Field(description="Context information provided to humans")
52
+ platform: str = Field(description="Platform being used")
53
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
54
+
@@ -0,0 +1,23 @@
1
+ from pydantic import BaseModel, Field, field_validator, SecretStr
2
+
3
+ class GoHumanLoopConfig(BaseModel):
4
+ """GoHumanLoop Configuration Model"""
5
+ api_key: SecretStr = Field(..., description="GoHumanLoop API Key")
6
+ api_base_url: str = Field(
7
+ default="https://www.gohumanloop.com",
8
+ description="GoHumanLoop API Base URL"
9
+ )
10
+
11
+ @field_validator('api_key')
12
+ def validate_api_key(cls, v):
13
+ """Validate that API Key is not empty"""
14
+ if not v:
15
+ raise ValueError("GoHumanLoop API Key cannot be None or empty")
16
+ return v
17
+
18
+ @field_validator('api_base_url')
19
+ def validate_api_base_url(cls, v):
20
+ """Validate API Base URL"""
21
+ if not v.startswith(('http://', 'https://')):
22
+ raise ValueError("GoHumanLoop API Base URL must start with http:// or https://")
23
+ return v.rstrip('/')
File without changes