hyperpocket 0.4.4__py3-none-any.whl → 0.5.0__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.
Files changed (34) hide show
  1. hyperpocket/auth/kraken/README.md +8 -0
  2. hyperpocket/auth/kraken/context.py +14 -0
  3. hyperpocket/auth/kraken/keypair_context.py +17 -0
  4. hyperpocket/auth/kraken/keypair_handler.py +97 -0
  5. hyperpocket/auth/kraken/keypair_schema.py +10 -0
  6. hyperpocket/auth/provider.py +1 -0
  7. hyperpocket/auth/valyu/token_handler.py +1 -2
  8. hyperpocket/builtin.py +3 -3
  9. hyperpocket/cli/__main__.py +0 -2
  10. hyperpocket/config/logger.py +2 -3
  11. hyperpocket/futures/futurestore.py +7 -2
  12. hyperpocket/pocket_auth.py +10 -8
  13. hyperpocket/pocket_main.py +251 -100
  14. hyperpocket/server/auth/kraken.py +58 -0
  15. hyperpocket/server/server.py +70 -239
  16. hyperpocket/session/in_memory.py +20 -26
  17. hyperpocket/tool/__init__.py +1 -2
  18. hyperpocket/tool/dock/dock.py +6 -25
  19. hyperpocket/tool/function/tool.py +1 -1
  20. hyperpocket/tool/tool.py +6 -35
  21. hyperpocket/tool_like.py +2 -3
  22. hyperpocket/util/git_parser.py +63 -0
  23. hyperpocket/util/json_schema_to_model.py +2 -2
  24. hyperpocket/util/short_hashing_str.py +5 -0
  25. {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/METADATA +5 -5
  26. {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/RECORD +29 -26
  27. hyperpocket/pocket_core.py +0 -283
  28. hyperpocket/repository/__init__.py +0 -4
  29. hyperpocket/repository/repository.py +0 -8
  30. hyperpocket/repository/tool_reference.py +0 -28
  31. hyperpocket/util/generate_slug.py +0 -4
  32. /hyperpocket/{tool/tests → auth/kraken}/__init__.py +0 -0
  33. {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/WHEEL +0 -0
  34. {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,18 +1,34 @@
1
1
  import asyncio
2
- import uuid
3
- from typing import Any, List, Union
2
+ import concurrent.futures
3
+ from threading import Lock
4
+ from typing import Any, List, Union, Callable, Optional
4
5
 
6
+ from hyperpocket.builtin import get_builtin_tools
5
7
  from hyperpocket.config import pocket_logger
6
8
  from hyperpocket.pocket_auth import PocketAuth
7
- from hyperpocket.pocket_core import PocketCore
8
- from hyperpocket.server.server import PocketServer, PocketServerOperations
9
+ from hyperpocket.server.server import PocketServer
10
+ from hyperpocket.tool import Tool, from_func
11
+ from hyperpocket.tool.dock import Dock
9
12
  from hyperpocket.tool_like import ToolLike
10
13
 
11
14
 
12
15
  class Pocket(object):
13
16
  server: PocketServer
14
- core: PocketCore
15
- _uid: str
17
+ auth: PocketAuth
18
+ tools: dict[str, Tool]
19
+
20
+ _cnt_pocket_count: int = 0
21
+ _pocket_count_lock = Lock()
22
+
23
+ @staticmethod
24
+ def _default_dock() -> Dock:
25
+ try:
26
+ from hyperdock_container.dock import ContainerDock
27
+ pocket_logger.info("hyperdock-container is loaded.")
28
+ return ContainerDock()
29
+ except ImportError as e:
30
+ pocket_logger.warning("Failed to import hyperdock_container.")
31
+ raise e
16
32
 
17
33
  def __init__(
18
34
  self,
@@ -21,30 +37,31 @@ class Pocket(object):
21
37
  use_profile: bool = False,
22
38
  ):
23
39
  try:
24
- self._uid = str(uuid.uuid4())
40
+ if auth is None:
41
+ auth = PocketAuth()
42
+ self.auth = auth
25
43
  self.use_profile = use_profile
26
- self.server = PocketServer.get_instance_and_refcnt_up(self._uid)
27
- self.server.wait_initialized()
28
- self.core = PocketCore(
29
- tools=tools,
30
- auth=auth,
44
+ self.server = PocketServer.get_instance()
45
+
46
+ self._load_tools(tools)
47
+ pocket_logger.info(
48
+ f"All Registered Tools Loaded successfully. total registered tools : {len(self.tools)}"
49
+ )
50
+
51
+ # load builtin tool
52
+ builtin_tools = get_builtin_tools(self.auth)
53
+ for tool in builtin_tools:
54
+ self.tools[tool.name] = tool
55
+ pocket_logger.info(
56
+ f"All BuiltIn Tools Loaded successfully. total tools : {len(self.tools)}"
31
57
  )
58
+
59
+ with Pocket._pocket_count_lock:
60
+ Pocket._cnt_pocket_count += 1
32
61
  except Exception as e:
33
- if hasattr(self, "server"):
34
- self.server.refcnt_down(self._uid)
62
+ self.teardown()
35
63
  pocket_logger.error(f"Failed to initialize pocket server. error : {e}")
36
- self._teardown_server()
37
64
  raise e
38
-
39
- try:
40
- asyncio.get_running_loop()
41
- except RuntimeError:
42
- loop = asyncio.new_event_loop()
43
- else:
44
- import nest_asyncio
45
- loop = asyncio.new_event_loop()
46
- nest_asyncio.apply(loop=loop)
47
- loop.run_until_complete(self.server.plug_core(self._uid, self.core))
48
65
 
49
66
  def invoke(
50
67
  self,
@@ -177,17 +194,12 @@ class Pocket(object):
177
194
  **kwargs,
178
195
  }
179
196
 
180
- result, paused = await self.server.call_in_subprocess(
181
- PocketServerOperations.CALL,
182
- self._uid,
183
- args,
184
- {
185
- "tool_name": tool_name,
186
- "body": body,
187
- "thread_id": thread_id,
188
- "profile": profile,
189
- **kwargs,
190
- },
197
+ result, paused = await self.acall(
198
+ tool_name=tool_name,
199
+ body=body,
200
+ thread_id=thread_id,
201
+ profile=profile,
202
+ **kwargs
191
203
  )
192
204
  if not isinstance(result, str):
193
205
  result = str(result)
@@ -214,15 +226,16 @@ class Pocket(object):
214
226
  Returns:
215
227
  List[str]: A list of authentication URIs for the tools that require authentication.
216
228
  """
217
- tool_by_provider = self.core.grouping_tool_by_auth_provider()
229
+ tool_by_provider = self.grouping_tool_by_auth_provider()
218
230
 
219
231
  prepare_list = {}
220
232
  for provider, tools in tool_by_provider.items():
221
233
  tool_name_list = [tool.name for tool in tools]
222
- prepare = await self.prepare_in_subprocess(
223
- tool_name=tool_name_list, thread_id=thread_id, profile=profile
234
+ prepare = await self.prepare_auth(
235
+ tool_name=tool_name_list,
236
+ thread_id=thread_id,
237
+ profile=profile,
224
238
  )
225
-
226
239
  if prepare is not None:
227
240
  prepare_list[provider] = prepare
228
241
 
@@ -246,16 +259,17 @@ class Pocket(object):
246
259
  or `False` if the process was interrupted or failed.
247
260
  """
248
261
  try:
249
- tool_by_provider = self.core.grouping_tool_by_auth_provider()
262
+ tool_by_provider = self.grouping_tool_by_auth_provider()
250
263
 
251
264
  waiting_futures = []
252
265
  for provider, tools in tool_by_provider.items():
253
266
  if len(tools) == 0:
254
267
  continue
255
-
256
268
  waiting_futures.append(
257
- self.authenticate_in_subprocess(
258
- tool_name=tools[0].name, thread_id=thread_id, profile=profile
269
+ self.authenticate(
270
+ tool_name=tools[0].name,
271
+ thread_id=thread_id,
272
+ profile=profile,
259
273
  )
260
274
  )
261
275
 
@@ -267,51 +281,150 @@ class Pocket(object):
267
281
  pocket_logger.error("authentication time out.")
268
282
  raise e
269
283
 
270
- async def prepare_in_subprocess(
284
+ async def acall(
271
285
  self,
272
- tool_name: Union[str, List[str]],
286
+ tool_name: str,
287
+ body: Any,
273
288
  thread_id: str = "default",
274
289
  profile: str = "default",
275
290
  *args,
276
291
  **kwargs,
277
- ):
278
- prepare = await self.server.call_in_subprocess(
279
- PocketServerOperations.PREPARE_AUTH,
280
- self._uid,
281
- args,
282
- {
283
- "tool_name": tool_name,
284
- "thread_id": thread_id,
285
- "profile": profile,
286
- **kwargs,
287
- },
292
+ ) -> tuple[str, bool]:
293
+ """
294
+ Invoke tool asynchronously, not that different from `Pocket.invoke`
295
+ But this method is called only in subprocess.
296
+
297
+ This function performs the following steps:
298
+ 1. `prepare_auth` : preparing the authentication process for the tool if necessary.
299
+ 2. `authenticate` : performing authentication that needs to invoke tool.
300
+ 3. `tool_call` : Executing tool actually with authentication information.
301
+
302
+ Args:
303
+ tool_name(str): tool name to invoke
304
+ body(Any): tool arguments. should be json format
305
+ thread_id(str): thread id
306
+ profile(str): profile name
307
+
308
+ Returns:
309
+ tuple[str, bool]: tool result and state.
310
+ """
311
+ pocket_logger.debug(f"{tool_name} tool call. body: {body}")
312
+ tool = self._tool_instance(tool_name)
313
+ if tool.auth is not None:
314
+ callback_info = await self.prepare_auth(tool_name, thread_id, profile, **kwargs)
315
+ if callback_info:
316
+ return callback_info, True
317
+ # 02. authenticate
318
+ credentials = await self.authenticate(tool_name, thread_id, profile, **kwargs)
319
+ # 03. call tool
320
+ result = await self.tool_call(tool_name, body=body, envs=credentials, **kwargs)
321
+ pocket_logger.debug(f"{tool_name} tool call result: {result}")
322
+ return result, False
323
+
324
+ async def prepare_auth(
325
+ self,
326
+ tool_name: Union[str, List[str]],
327
+ thread_id: str = "default",
328
+ profile: str = "default",
329
+ **kwargs,
330
+ ) -> Optional[str]:
331
+ """
332
+ Prepares the authentication process for the tool if necessary.
333
+ Returns callback URL and whether the tool requires authentication.
334
+
335
+ Args:
336
+ tool_name(Union[str,List[str]]): tool name to invoke
337
+ thread_id(str): thread id
338
+ profile(str): profile name
339
+
340
+ Returns:
341
+ Optional[str]: callback URI if necessary
342
+ """
343
+
344
+ if isinstance(tool_name, str):
345
+ tool_name = [tool_name]
346
+
347
+ tools: List[Tool] = []
348
+ for name in tool_name:
349
+ tool = self._tool_instance(name)
350
+ if tool.auth is not None:
351
+ tools.append(tool)
352
+
353
+ if len(tools) == 0:
354
+ return None
355
+
356
+ auth_handler_name = tools[0].auth.auth_handler
357
+ auth_provider = tools[0].auth.auth_provider
358
+ auth_scopes = set()
359
+
360
+ for tool in tools:
361
+ if tool.auth.auth_handler != auth_handler_name:
362
+ pocket_logger.error(
363
+ f"All Tools should have same auth handler. but it's different {tool.auth.auth_handler}, {auth_handler_name}"
364
+ )
365
+
366
+ return f"All Tools should have same auth handler. but it's different {tool.auth.auth_handler}, {auth_handler_name}"
367
+ if tool.auth.auth_provider != auth_provider:
368
+ pocket_logger.error(
369
+ f"All Tools should have same auth provider. but it's different {tool.auth.auth_provider}, {auth_provider}"
370
+ )
371
+ return f"All Tools should have same auth provider. but it's different {tool.auth.auth_provider}, {auth_provider}"
372
+
373
+ if tool.auth.scopes is not None:
374
+ auth_scopes |= set(tool.auth.scopes)
375
+
376
+ auth_req = self.auth.make_request(
377
+ auth_handler_name=auth_handler_name,
378
+ auth_provider=auth_provider,
379
+ auth_scopes=list(auth_scopes),
288
380
  )
289
381
 
290
- return prepare
382
+ return await self.auth.prepare(
383
+ auth_req=auth_req,
384
+ auth_handler_name=auth_handler_name,
385
+ auth_provider=auth_provider,
386
+ thread_id=thread_id,
387
+ profile=profile,
388
+ **kwargs,
389
+ )
291
390
 
292
- async def authenticate_in_subprocess(
391
+ async def authenticate(
293
392
  self,
294
393
  tool_name: str,
295
394
  thread_id: str = "default",
296
395
  profile: str = "default",
297
- *args,
298
396
  **kwargs,
299
- ):
300
- credentials = await self.server.call_in_subprocess(
301
- PocketServerOperations.AUTHENTICATE,
302
- self._uid,
303
- args,
304
- {
305
- "tool_name": tool_name,
306
- "thread_id": thread_id,
307
- "profile": profile,
308
- **kwargs,
309
- },
310
- )
397
+ ) -> dict[str, str]:
398
+ """
399
+ Authenticates the handler included in the tool and returns credentials.
311
400
 
312
- return credentials
401
+ Args:
402
+ tool_name(str): tool name to invoke
403
+ thread_id(str): thread id
404
+ profile(str): profile name
405
+
406
+ Returns:
407
+ dict[str, str]: credentials
408
+ """
409
+ tool = self._tool_instance(tool_name)
410
+ if tool.auth is None:
411
+ return {}
412
+ auth_req = self.auth.make_request(
413
+ auth_handler_name=tool.auth.auth_handler,
414
+ auth_provider=tool.auth.auth_provider,
415
+ auth_scopes=tool.auth.scopes,
416
+ )
417
+ auth_ctx = await self.auth.authenticate_async(
418
+ auth_req=auth_req,
419
+ auth_handler_name=tool.auth.auth_handler,
420
+ auth_provider=tool.auth.auth_provider,
421
+ thread_id=thread_id,
422
+ profile=profile,
423
+ **kwargs,
424
+ )
425
+ return auth_ctx.to_dict()
313
426
 
314
- async def tool_call_in_subprocess(
427
+ async def tool_call(
315
428
  self,
316
429
  tool_name: str,
317
430
  body: Any,
@@ -320,38 +433,76 @@ class Pocket(object):
320
433
  *args,
321
434
  **kwargs,
322
435
  ):
323
- result = await self.server.call_in_subprocess(
324
- PocketServerOperations.TOOL_CALL,
325
- self._uid,
326
- args,
327
- {
328
- "tool_name": tool_name,
329
- "body": body,
330
- "thread_id": thread_id,
331
- "profile": profile,
332
- **kwargs,
333
- },
334
- )
436
+ try:
437
+ tool = self._tool_instance(tool_name)
438
+ result = await asyncio.wait_for(
439
+ tool.ainvoke(body=body, thread_id=thread_id, profile=profile, **kwargs), timeout=180)
440
+ except asyncio.TimeoutError:
441
+ pocket_logger.warning("Timeout tool call.")
442
+ return "timeout tool call"
443
+
444
+ # TODO(moon): extract
445
+ if tool.postprocessings is not None:
446
+ for postprocessing in tool.postprocessings:
447
+ try:
448
+ result = postprocessing(result)
449
+ except Exception as e:
450
+ exception_str = (
451
+ f"Error in postprocessing `{postprocessing.__name__}`: {e}"
452
+ )
453
+ pocket_logger.error(exception_str)
454
+ return exception_str
335
455
 
336
456
  return result
337
457
 
458
+ def grouping_tool_by_auth_provider(self) -> dict[str, List[Tool]]:
459
+ tool_by_provider = {}
460
+ for tool_name, tool in self.tools.items():
461
+ if tool.auth is None:
462
+ continue
463
+
464
+ auth_provider_name = tool.auth.auth_provider.name
465
+ if tool_by_provider.get(auth_provider_name):
466
+ tool_by_provider[auth_provider_name].append(tool)
467
+ else:
468
+ tool_by_provider[auth_provider_name] = [tool]
469
+ return tool_by_provider
470
+
471
+ def _load_tools(self, tools):
472
+ self.tools = dict()
473
+ dock = self._default_dock()
474
+
475
+ def _load(tool_like):
476
+ if isinstance(tool_like, str) or isinstance(tool_like, tuple):
477
+ return dock(tool_like)
478
+ elif isinstance(tool_like, Tool):
479
+ return tool_like
480
+ elif isinstance(tool_like, Callable):
481
+ return from_func(tool_like)
482
+ else:
483
+ raise ValueError(f"Invalid tool type: {type(tool_like)}")
484
+
485
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10, thread_name_prefix="tool-loader") as executor:
486
+ futures = [executor.submit(_load, tool_like) for tool_like in tools]
487
+ for future in concurrent.futures.as_completed(futures):
488
+ tool = future.result()
489
+ self.tools[tool.name] = tool
490
+
491
+ def _tool_instance(self, tool_name: str) -> Tool:
492
+ return self.tools[tool_name]
493
+
338
494
  def _teardown_server(self):
339
- self.server.teardown()
495
+ self.teardown()
496
+
497
+ def teardown(self):
498
+ if hasattr(self, 'server'):
499
+ with Pocket._pocket_count_lock:
500
+ Pocket._cnt_pocket_count -= 1
501
+ if Pocket._cnt_pocket_count <= 0:
502
+ self.server.teardown()
340
503
 
341
504
  def __enter__(self):
342
505
  return self
343
506
 
344
507
  def __exit__(self, exc_type, exc_val, exc_tb):
345
- self.server.refcnt_down(self._uid)
346
-
347
- def __del__(self):
348
- self.server.refcnt_down(self._uid)
349
-
350
- def __getstate__(self):
351
- state = self.__dict__.copy()
352
- if "server" in state:
353
- del state["server"]
354
- return state
355
-
356
- def __setstate__(self, state):
357
- self.__dict__.update(state)
508
+ self.teardown()
@@ -0,0 +1,58 @@
1
+ from http import HTTPStatus
2
+
3
+ from fastapi import APIRouter, Form
4
+ from starlette.responses import HTMLResponse, RedirectResponse
5
+ from hyperpocket.futures import FutureStore
6
+ from hyperpocket.server.auth.token import add_query_params
7
+
8
+ kraken_auth_router = APIRouter(prefix="/kraken")
9
+
10
+ @kraken_auth_router.get("/keypair", response_class=HTMLResponse)
11
+ async def keypair_form(redirect_uri: str, state: str = ""):
12
+ html = f"""
13
+ <html>
14
+ <body>
15
+ <h2>Enter Token</h2>
16
+ <form action="submit" method="post">
17
+ <input type="hidden" name="redirect_uri" value="{redirect_uri}">
18
+ <input type="hidden" name="state" value="{state}">
19
+
20
+ <label for="kraken_api_key">Kraken API Key:</label>
21
+ <input type="text" id="kraken_api_key" name="kraken_api_key" required>
22
+
23
+ <label for="kraken_api_secret">Kraken API Secret:</label>
24
+ <input type="text" id="kraken_api_secret" name="kraken_api_secret" required>
25
+
26
+ <button type="submit">submit</button>
27
+ </form>
28
+ </body>
29
+ </html>
30
+ """
31
+ return HTMLResponse(content=html)
32
+
33
+ @kraken_auth_router.post("/submit", response_class=RedirectResponse)
34
+ async def submit_keypair(
35
+ kraken_api_key: str = Form(...),
36
+ kraken_api_secret: str = Form(...),
37
+ redirect_uri: str = Form(...),
38
+ state: str = Form(...),
39
+ ):
40
+ new_callback_url = add_query_params(
41
+ redirect_uri, {
42
+ "kraken_api_key": kraken_api_key,
43
+ "kraken_api_secret": kraken_api_secret,
44
+ "state": state,
45
+ }
46
+ )
47
+ return RedirectResponse(url=new_callback_url, status_code=HTTPStatus.SEE_OTHER)
48
+
49
+ @kraken_auth_router.get("/keypair/callback")
50
+ async def kraken_keypair_callback(state: str, kraken_api_key: str, kraken_api_secret: str):
51
+ try:
52
+ FutureStore.resolve_future(state, {
53
+ "kraken_api_key": kraken_api_key,
54
+ "kraken_api_secret": kraken_api_secret,
55
+ })
56
+ except ValueError:
57
+ return HTMLResponse(content="failed")
58
+ return HTMLResponse(content="success")