gohumanloop 0.0.4__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.
@@ -1,17 +1,18 @@
1
1
  import os
2
2
  from typing import Dict, Any, Optional
3
- from pydantic import BaseModel, Field, field_validator, SecretStr
4
3
 
5
4
  from gohumanloop.models.glh_model import GoHumanLoopConfig
6
5
  from gohumanloop.providers.api_provider import APIProvider
7
6
  from gohumanloop.utils import get_secret_from_env
8
7
 
8
+
9
9
  class GoHumanLoopProvider(APIProvider):
10
10
  """
11
11
  GoHumanLoop platform provider class.
12
12
  This class is a concrete implementation of the `APIProvider` class.
13
13
  The `GoHumanLoopProvider` class is responsible for interacting with the GoHumanLoop platform.
14
14
  """
15
+
15
16
  def __init__(
16
17
  self,
17
18
  name: str,
@@ -19,14 +20,12 @@ class GoHumanLoopProvider(APIProvider):
19
20
  poll_interval: int = 5,
20
21
  max_retries: int = 3,
21
22
  default_platform: Optional[str] = "GoHumanLoop",
22
- config: Optional[Dict[str, Any]] = None
23
+ config: Optional[Dict[str, Any]] = None,
23
24
  ):
24
25
  """Initialize GoHumanLoop provider
25
-
26
+
26
27
  Args:
27
28
  name: Provider name
28
- api_key: GoHumanLoop API key, if not provided will be fetched from environment variables
29
- api_base_url: GoHumanLoop API base URL, if not provided will use default value
30
29
  default_platform: Default platform, e.g. "wechat", "feishu" etc.
31
30
  request_timeout: API request timeout in seconds
32
31
  poll_interval: Polling interval in seconds
@@ -35,16 +34,18 @@ class GoHumanLoopProvider(APIProvider):
35
34
  """
36
35
  # Get API key from environment variables (if not provided)
37
36
  api_key = get_secret_from_env("GOHUMANLOOP_API_KEY")
38
-
37
+
38
+ if api_key is None:
39
+ raise ValueError("GOHUMANLOOP_API_KEY environment variable is not set!")
40
+
39
41
  # Get API base URL from environment variables (if not provided)
40
- api_base_url = os.environ.get("GOHUMANLOOP_API_BASE_URL", "https://www.gohumanloop.com")
41
-
42
- # Validate configuration using pydantic model
43
- ghl_config = GoHumanLoopConfig(
44
- api_key=api_key,
45
- api_base_url=api_base_url
42
+ api_base_url = os.environ.get(
43
+ "GOHUMANLOOP_API_BASE_URL", "https://www.gohumanloop.com"
46
44
  )
47
-
45
+
46
+ # Validate configuration using pydantic model
47
+ ghl_config = GoHumanLoopConfig(api_key=api_key, api_base_url=api_base_url)
48
+
48
49
  super().__init__(
49
50
  name=name,
50
51
  api_base_url=ghl_config.api_base_url,
@@ -53,12 +54,13 @@ class GoHumanLoopProvider(APIProvider):
53
54
  request_timeout=request_timeout,
54
55
  poll_interval=poll_interval,
55
56
  max_retries=max_retries,
56
- config=config
57
+ config=config,
57
58
  )
58
-
59
+
59
60
  def __str__(self) -> str:
60
61
  """Returns a string description of this instance"""
61
62
  base_str = super().__str__()
62
- ghl_info = f"- GoHumanLoop Provider: Connected to GoHumanLoop Official Platform\n"
63
+ ghl_info = (
64
+ "- GoHumanLoop Provider: Connected to GoHumanLoop Official Platform\n"
65
+ )
63
66
  return f"{ghl_info}{base_str}"
64
-
@@ -1,21 +1,22 @@
1
1
  import asyncio
2
- from concurrent.futures import ThreadPoolExecutor
3
- from typing import Dict, Any, Optional
2
+ from concurrent.futures import ThreadPoolExecutor, Future
3
+ from typing import Dict, Any, Optional, Tuple
4
4
  from datetime import datetime
5
5
 
6
- from gohumanloop.core.interface import (HumanLoopResult, HumanLoopStatus, HumanLoopType)
6
+ from gohumanloop.core.interface import HumanLoopResult, HumanLoopStatus, HumanLoopType
7
7
  from gohumanloop.providers.base import BaseProvider
8
8
 
9
+
9
10
  class TerminalProvider(BaseProvider):
10
11
  """Terminal-based human-in-the-loop provider implementation
11
-
12
+
12
13
  This provider interacts with users through command line interface, suitable for testing and simple scenarios.
13
14
  Users can respond to requests via terminal input, supporting approval, information collection and conversation type interactions.
14
15
  """
15
-
16
+
16
17
  def __init__(self, name: str, config: Optional[Dict[str, Any]] = None):
17
18
  """Initialize terminal provider
18
-
19
+
19
20
  Args:
20
21
  name: Provider name
21
22
  config: Configuration options, may include:
@@ -23,11 +24,11 @@ class TerminalProvider(BaseProvider):
23
24
  super().__init__(name, config)
24
25
 
25
26
  # Store running terminal input tasks
26
- self._terminal_input_tasks = {}
27
+ self._terminal_input_tasks: Dict[Tuple[str, str], Future] = {}
27
28
  # Create thread pool for background service execution
28
29
  self._executor = ThreadPoolExecutor(max_workers=10)
29
30
 
30
- def __del__(self):
31
+ def __del__(self) -> None:
31
32
  """Destructor to ensure thread pool is properly closed"""
32
33
  self._executor.shutdown(wait=False)
33
34
 
@@ -37,9 +38,11 @@ class TerminalProvider(BaseProvider):
37
38
 
38
39
  def __str__(self) -> str:
39
40
  base_str = super().__str__()
40
- terminal_info = f"- Terminal Provider: Terminal-based human-in-the-loop implementation\n"
41
+ terminal_info = (
42
+ "- Terminal Provider: Terminal-based human-in-the-loop implementation\n"
43
+ )
41
44
  return f"{terminal_info}{base_str}"
42
-
45
+
43
46
  async def async_request_humanloop(
44
47
  self,
45
48
  task_id: str,
@@ -47,10 +50,10 @@ class TerminalProvider(BaseProvider):
47
50
  loop_type: HumanLoopType,
48
51
  context: Dict[str, Any],
49
52
  metadata: Optional[Dict[str, Any]] = None,
50
- timeout: Optional[int] = None
53
+ timeout: Optional[int] = None,
51
54
  ) -> HumanLoopResult:
52
55
  """Request human-in-the-loop interaction through terminal
53
-
56
+
54
57
  Args:
55
58
  task_id: Task identifier
56
59
  conversation_id: Conversation ID for multi-turn dialogs
@@ -58,13 +61,13 @@ class TerminalProvider(BaseProvider):
58
61
  context: Context information provided to human
59
62
  metadata: Additional metadata
60
63
  timeout: Request timeout in seconds
61
-
64
+
62
65
  Returns:
63
66
  HumanLoopResult: Result object containing request ID and initial status
64
67
  """
65
68
  # Generate request ID
66
69
  request_id = self._generate_request_id()
67
-
70
+
68
71
  # Store request information
69
72
  self._store_request(
70
73
  conversation_id=conversation_id,
@@ -73,36 +76,42 @@ class TerminalProvider(BaseProvider):
73
76
  loop_type=loop_type,
74
77
  context=context,
75
78
  metadata=metadata or {},
76
- timeout=timeout
79
+ timeout=timeout,
77
80
  )
78
-
81
+
79
82
  # Create initial result object
80
83
  result = HumanLoopResult(
81
84
  conversation_id=conversation_id,
82
85
  request_id=request_id,
83
86
  loop_type=loop_type,
84
- status=HumanLoopStatus.PENDING
87
+ status=HumanLoopStatus.PENDING,
85
88
  )
86
-
87
89
 
88
- self._terminal_input_tasks[(conversation_id, request_id)] = self._executor.submit(self._run_async_terminal_interaction, conversation_id, request_id)
90
+ self._terminal_input_tasks[
91
+ (conversation_id, request_id)
92
+ ] = self._executor.submit(
93
+ self._run_async_terminal_interaction, conversation_id, request_id
94
+ )
89
95
 
90
96
  # Create timeout task if timeout is specified
91
97
  if timeout:
92
98
  await self._async_create_timeout_task(conversation_id, request_id, timeout)
93
-
94
- return result
95
99
 
100
+ return result
96
101
 
97
- def _run_async_terminal_interaction(self, conversation_id: str, request_id: str):
102
+ def _run_async_terminal_interaction(
103
+ self, conversation_id: str, request_id: str
104
+ ) -> None:
98
105
  """Run asynchronous terminal interaction in a separate thread"""
99
106
  # Create new event loop
100
107
  loop = asyncio.new_event_loop()
101
108
  asyncio.set_event_loop(loop)
102
-
109
+
103
110
  try:
104
111
  # Run interaction processing in the new event loop
105
- loop.run_until_complete(self._process_terminal_interaction(conversation_id, request_id))
112
+ loop.run_until_complete(
113
+ self._process_terminal_interaction(conversation_id, request_id)
114
+ )
106
115
  finally:
107
116
  loop.close()
108
117
  # Remove from task dictionary
@@ -110,16 +119,14 @@ class TerminalProvider(BaseProvider):
110
119
  del self._terminal_input_tasks[(conversation_id, request_id)]
111
120
 
112
121
  async def async_check_request_status(
113
- self,
114
- conversation_id: str,
115
- request_id: str
122
+ self, conversation_id: str, request_id: str
116
123
  ) -> HumanLoopResult:
117
124
  """Check request status
118
-
125
+
119
126
  Args:
120
127
  conversation_id: Conversation identifier
121
128
  request_id: Request identifier
122
-
129
+
123
130
  Returns:
124
131
  HumanLoopResult: Result object containing current status
125
132
  """
@@ -130,9 +137,9 @@ class TerminalProvider(BaseProvider):
130
137
  request_id=request_id,
131
138
  loop_type=HumanLoopType.CONVERSATION,
132
139
  status=HumanLoopStatus.ERROR,
133
- error=f"Request '{request_id}' not found in conversation '{conversation_id}'"
140
+ error=f"Request '{request_id}' not found in conversation '{conversation_id}'",
134
141
  )
135
-
142
+
136
143
  # Build result object
137
144
  result = HumanLoopResult(
138
145
  conversation_id=conversation_id,
@@ -143,11 +150,11 @@ class TerminalProvider(BaseProvider):
143
150
  feedback=request_info.get("feedback", {}),
144
151
  responded_by=request_info.get("responded_by", None),
145
152
  responded_at=request_info.get("responded_at", None),
146
- error=request_info.get("error", None)
153
+ error=request_info.get("error", None),
147
154
  )
148
-
155
+
149
156
  return result
150
-
157
+
151
158
  async def async_continue_humanloop(
152
159
  self,
153
160
  conversation_id: str,
@@ -156,13 +163,13 @@ class TerminalProvider(BaseProvider):
156
163
  timeout: Optional[int] = None,
157
164
  ) -> HumanLoopResult:
158
165
  """Continue human-in-the-loop interaction for multi-turn conversations
159
-
166
+
160
167
  Args:
161
168
  conversation_id: Conversation identifier
162
169
  context: Context information provided to human
163
170
  metadata: Additional metadata
164
171
  timeout: Request timeout in seconds
165
-
172
+
166
173
  Returns:
167
174
  HumanLoopResult: Result object containing request ID and status
168
175
  """
@@ -174,15 +181,15 @@ class TerminalProvider(BaseProvider):
174
181
  request_id="",
175
182
  loop_type=HumanLoopType.CONVERSATION,
176
183
  status=HumanLoopStatus.ERROR,
177
- error=f"Conversation '{conversation_id}' not found"
184
+ error=f"Conversation '{conversation_id}' not found",
178
185
  )
179
-
186
+
180
187
  # Generate new request ID
181
188
  request_id = self._generate_request_id()
182
-
189
+
183
190
  # Get task ID
184
191
  task_id = conversation_info.get("task_id", "unknown_task")
185
-
192
+
186
193
  # Store request information
187
194
  self._store_request(
188
195
  conversation_id=conversation_id,
@@ -191,30 +198,36 @@ class TerminalProvider(BaseProvider):
191
198
  loop_type=HumanLoopType.CONVERSATION, # Default to conversation type for continued dialog
192
199
  context=context,
193
200
  metadata=metadata or {},
194
- timeout=timeout
201
+ timeout=timeout,
195
202
  )
196
-
203
+
197
204
  # Create initial result object
198
205
  result = HumanLoopResult(
199
206
  conversation_id=conversation_id,
200
207
  request_id=request_id,
201
208
  loop_type=HumanLoopType.CONVERSATION,
202
- status=HumanLoopStatus.PENDING
209
+ status=HumanLoopStatus.PENDING,
203
210
  )
204
-
211
+
205
212
  # Start async task to process user input
206
- self._terminal_input_tasks[(conversation_id, request_id)] = self._executor.submit(self._run_async_terminal_interaction, conversation_id, request_id)
207
-
213
+ self._terminal_input_tasks[
214
+ (conversation_id, request_id)
215
+ ] = self._executor.submit(
216
+ self._run_async_terminal_interaction, conversation_id, request_id
217
+ )
218
+
208
219
  # Create timeout task if timeout is specified
209
220
  if timeout:
210
221
  await self._async_create_timeout_task(conversation_id, request_id, timeout)
211
-
222
+
212
223
  return result
213
-
214
- async def _process_terminal_interaction(self, conversation_id: str, request_id: str):
224
+
225
+ async def _process_terminal_interaction(
226
+ self, conversation_id: str, request_id: str
227
+ ) -> None:
215
228
  request_info = self._get_request(conversation_id, request_id)
216
229
  if not request_info:
217
- return
230
+ return
218
231
 
219
232
  prompt = self.build_prompt(
220
233
  task_id=request_info["task_id"],
@@ -223,33 +236,40 @@ class TerminalProvider(BaseProvider):
223
236
  loop_type=request_info["loop_type"],
224
237
  created_at=request_info.get("created_at", ""),
225
238
  context=request_info["context"],
226
- metadata=request_info.get("metadata")
239
+ metadata=request_info.get("metadata"),
227
240
  )
228
241
 
229
242
  loop_type = request_info["loop_type"]
230
-
243
+
231
244
  # Display prompt message
232
245
  print(prompt)
233
-
246
+
234
247
  # Handle different interaction types based on loop type
235
248
  if loop_type == HumanLoopType.APPROVAL:
236
- await self._async_handle_approval_interaction(conversation_id, request_id, request_info)
249
+ await self._async_handle_approval_interaction(
250
+ conversation_id, request_id, request_info
251
+ )
237
252
  elif loop_type == HumanLoopType.INFORMATION:
238
- await self._async_handle_information_interaction(conversation_id, request_id, request_info)
253
+ await self._async_handle_information_interaction(
254
+ conversation_id, request_id, request_info
255
+ )
239
256
  else: # HumanLoopType.CONVERSATION
240
- await self._async_handle_conversation_interaction(conversation_id, request_id, request_info)
257
+ await self._async_handle_conversation_interaction(
258
+ conversation_id, request_id, request_info
259
+ )
241
260
 
242
-
243
- async def _async_handle_approval_interaction(self, conversation_id: str, request_id: str, request_info: Dict[str, Any]):
261
+ async def _async_handle_approval_interaction(
262
+ self, conversation_id: str, request_id: str, request_info: Dict[str, Any]
263
+ ) -> None:
244
264
  """Handle approval type interaction
245
-
265
+
246
266
  Args:
247
267
  conversation_id: Conversation ID
248
268
  request_id: Request ID
249
269
  request_info: Request information
250
270
  """
251
271
  print("\nPlease enter your decision (approve/reject):")
252
-
272
+
253
273
  # Execute blocking input() call in thread pool using run_in_executor
254
274
  loop = asyncio.get_event_loop()
255
275
  response = await loop.run_in_executor(None, input)
@@ -266,53 +286,59 @@ class TerminalProvider(BaseProvider):
266
286
  else:
267
287
  print("\nInvalid input, please enter 'approve' or 'reject'")
268
288
  # Recursively handle approval interaction
269
- await self._async_handle_approval_interaction(conversation_id, request_id, request_info)
289
+ await self._async_handle_approval_interaction(
290
+ conversation_id, request_id, request_info
291
+ )
270
292
  return
271
-
293
+
272
294
  # Update request information
273
295
  request_info["status"] = status
274
296
  request_info["response"] = response_data
275
297
  request_info["responded_by"] = "terminal_user"
276
298
  request_info["responded_at"] = datetime.now().isoformat()
277
-
299
+
278
300
  print(f"\nYour decision has been recorded: {status.value}")
279
-
280
- async def _async_handle_information_interaction(self, conversation_id: str, request_id: str, request_info: Dict[str, Any]):
301
+
302
+ async def _async_handle_information_interaction(
303
+ self, conversation_id: str, request_id: str, request_info: Dict[str, Any]
304
+ ) -> None:
281
305
  """Handle information collection type interaction
282
-
306
+
283
307
  Args:
284
308
  conversation_id: Conversation ID
285
309
  request_id: Request ID
286
310
  request_info: Request information
287
311
  """
288
312
  print("\nPlease provide the required information:")
289
-
313
+
290
314
  # Execute blocking input() call in thread pool using run_in_executor
291
315
  loop = asyncio.get_event_loop()
292
316
  response = await loop.run_in_executor(None, input)
293
-
317
+
294
318
  # Update request information
295
319
  request_info["status"] = HumanLoopStatus.COMPLETED
296
320
  request_info["response"] = response
297
321
  request_info["responded_by"] = "terminal_user"
298
322
  request_info["responded_at"] = datetime.now().isoformat()
299
-
323
+
300
324
  print("\nYour information has been recorded")
301
-
302
- async def _async_handle_conversation_interaction(self, conversation_id: str, request_id: str, request_info: Dict[str, Any]):
325
+
326
+ async def _async_handle_conversation_interaction(
327
+ self, conversation_id: str, request_id: str, request_info: Dict[str, Any]
328
+ ) -> None:
303
329
  """Handle conversation type interaction
304
-
330
+
305
331
  Args:
306
332
  conversation_id: Conversation ID
307
333
  request_id: Request ID
308
334
  request_info: Request information
309
335
  """
310
336
  print("\nPlease enter your response (type 'exit' to end conversation):")
311
-
337
+
312
338
  # Execute blocking input() call in thread pool using run_in_executor
313
339
  loop = asyncio.get_event_loop()
314
340
  response = await loop.run_in_executor(None, input)
315
-
341
+
316
342
  # Process response
317
343
  if response.strip().lower() in ["exit", "quit", "结束", "退出"]:
318
344
  status = HumanLoopStatus.COMPLETED
@@ -325,20 +351,16 @@ class TerminalProvider(BaseProvider):
325
351
  request_info["response"] = response
326
352
  request_info["responded_by"] = "terminal_user"
327
353
  request_info["responded_at"] = datetime.now().isoformat()
328
-
354
+
329
355
  print("\nYour response has been recorded")
330
356
 
331
- async def async_cancel_request(
332
- self,
333
- conversation_id: str,
334
- request_id: str
335
- ) -> bool:
357
+ async def async_cancel_request(self, conversation_id: str, request_id: str) -> bool:
336
358
  """Cancel human-in-the-loop request
337
-
359
+
338
360
  Args:
339
361
  conversation_id: Conversation identifier for multi-turn dialogues
340
362
  request_id: Request identifier for specific interaction request
341
-
363
+
342
364
  Return:
343
365
  bool: Whether cancellation was successful, True indicates successful cancellation,
344
366
  False indicates cancellation failed
@@ -347,19 +369,16 @@ class TerminalProvider(BaseProvider):
347
369
  if request_key in self._terminal_input_tasks:
348
370
  self._terminal_input_tasks[request_key].cancel()
349
371
  del self._terminal_input_tasks[request_key]
350
-
372
+
351
373
  # 调用父类方法取消请求
352
374
  return await super().async_cancel_request(conversation_id, request_id)
353
-
354
- async def async_cancel_conversation(
355
- self,
356
- conversation_id: str
357
- ) -> bool:
375
+
376
+ async def async_cancel_conversation(self, conversation_id: str) -> bool:
358
377
  """Cancel the entire conversation
359
-
378
+
360
379
  Args:
361
380
  conversation_id: Conversation identifier
362
-
381
+
363
382
  Returns:
364
383
  bool: Whether cancellation was successful
365
384
  """
@@ -369,6 +388,6 @@ class TerminalProvider(BaseProvider):
369
388
  if request_key in self._terminal_input_tasks:
370
389
  self._terminal_input_tasks[request_key].cancel()
371
390
  del self._terminal_input_tasks[request_key]
372
-
391
+
373
392
  # 调用父类方法取消对话
374
- return await super().async_cancel_conversation(conversation_id)
393
+ return await super().async_cancel_conversation(conversation_id)
@@ -1 +1,7 @@
1
- from .utils import run_async_safely, get_secret_from_env
1
+ from .utils import run_async_safely, get_secret_from_env
2
+
3
+
4
+ __all__ = [
5
+ "run_async_safely",
6
+ "get_secret_from_env",
7
+ ]
@@ -1,40 +1,45 @@
1
- from typing import Dict, Any, List, Optional, Union
1
+ from typing import Dict, Any
2
2
  import json
3
3
 
4
+
4
5
  class ContextFormatter:
5
6
  """上下文格式化工具"""
6
-
7
+
7
8
  @staticmethod
8
9
  def format_for_human(context: Dict[str, Any]) -> str:
9
10
  """将上下文格式化为人类可读的文本"""
10
11
  result = []
11
-
12
+
12
13
  # 添加标题(如果有)
13
14
  if "title" in context:
14
15
  result.append(f"# {context['title']}\n")
15
-
16
+
16
17
  # 添加描述(如果有)
17
18
  if "description" in context:
18
19
  result.append(f"{context['description']}\n")
19
-
20
+
20
21
  # 添加任务信息
21
22
  if "task" in context:
22
23
  result.append(f"## 任务\n{context['task']}\n")
23
-
24
+
24
25
  # 添加代理信息
25
26
  if "agent" in context:
26
27
  result.append(f"## 代理\n{context['agent']}\n")
27
-
28
+
28
29
  # 添加操作信息
29
30
  if "action" in context:
30
31
  result.append(f"## 请求的操作\n{context['action']}\n")
31
-
32
+
32
33
  # 添加原因
33
34
  if "reason" in context:
34
35
  result.append(f"## 原因\n{context['reason']}\n")
35
-
36
+
36
37
  # 添加其他键值对
37
- other_keys = [k for k in context.keys() if k not in ["title", "description", "task", "agent", "action", "reason"]]
38
+ other_keys = [
39
+ k
40
+ for k in context.keys()
41
+ if k not in ["title", "description", "task", "agent", "action", "reason"]
42
+ ]
38
43
  if other_keys:
39
44
  result.append("## 附加信息\n")
40
45
  for key in other_keys:
@@ -42,18 +47,18 @@ class ContextFormatter:
42
47
  if isinstance(value, (dict, list)):
43
48
  value = json.dumps(value, ensure_ascii=False, indent=2)
44
49
  result.append(f"### {key}\n```\n{value}\n```\n")
45
-
50
+
46
51
  return "\n".join(result)
47
-
52
+
48
53
  @staticmethod
49
54
  def format_for_api(context: Dict[str, Any]) -> Dict[str, Any]:
50
55
  """将上下文格式化为API友好的格式"""
51
56
  # 复制上下文以避免修改原始数据
52
57
  formatted = context.copy()
53
-
58
+
54
59
  # 确保所有值都是可序列化的
55
60
  for key, value in formatted.items():
56
61
  if not isinstance(value, (str, int, float, bool, list, dict, type(None))):
57
62
  formatted[key] = str(value)
58
-
59
- return formatted
63
+
64
+ return formatted