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.
- hyperpocket/auth/kraken/README.md +8 -0
- hyperpocket/auth/kraken/context.py +14 -0
- hyperpocket/auth/kraken/keypair_context.py +17 -0
- hyperpocket/auth/kraken/keypair_handler.py +97 -0
- hyperpocket/auth/kraken/keypair_schema.py +10 -0
- hyperpocket/auth/provider.py +1 -0
- hyperpocket/auth/valyu/token_handler.py +1 -2
- hyperpocket/builtin.py +3 -3
- hyperpocket/cli/__main__.py +0 -2
- hyperpocket/config/logger.py +2 -3
- hyperpocket/futures/futurestore.py +7 -2
- hyperpocket/pocket_auth.py +10 -8
- hyperpocket/pocket_main.py +251 -100
- hyperpocket/server/auth/kraken.py +58 -0
- hyperpocket/server/server.py +70 -239
- hyperpocket/session/in_memory.py +20 -26
- hyperpocket/tool/__init__.py +1 -2
- hyperpocket/tool/dock/dock.py +6 -25
- hyperpocket/tool/function/tool.py +1 -1
- hyperpocket/tool/tool.py +6 -35
- hyperpocket/tool_like.py +2 -3
- hyperpocket/util/git_parser.py +63 -0
- hyperpocket/util/json_schema_to_model.py +2 -2
- hyperpocket/util/short_hashing_str.py +5 -0
- {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/METADATA +5 -5
- {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/RECORD +29 -26
- hyperpocket/pocket_core.py +0 -283
- hyperpocket/repository/__init__.py +0 -4
- hyperpocket/repository/repository.py +0 -8
- hyperpocket/repository/tool_reference.py +0 -28
- hyperpocket/util/generate_slug.py +0 -4
- /hyperpocket/{tool/tests → auth/kraken}/__init__.py +0 -0
- {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/WHEEL +0 -0
- {hyperpocket-0.4.4.dist-info → hyperpocket-0.5.0.dist-info}/entry_points.txt +0 -0
hyperpocket/pocket_main.py
CHANGED
@@ -1,18 +1,34 @@
|
|
1
1
|
import asyncio
|
2
|
-
import
|
3
|
-
from
|
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.
|
8
|
-
from hyperpocket.
|
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
|
-
|
15
|
-
|
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
|
-
|
40
|
+
if auth is None:
|
41
|
+
auth = PocketAuth()
|
42
|
+
self.auth = auth
|
25
43
|
self.use_profile = use_profile
|
26
|
-
self.server = PocketServer.
|
27
|
-
|
28
|
-
self.
|
29
|
-
|
30
|
-
|
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
|
-
|
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.
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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.
|
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.
|
223
|
-
tool_name=tool_name_list,
|
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.
|
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.
|
258
|
-
tool_name=tools[0].name,
|
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
|
284
|
+
async def acall(
|
271
285
|
self,
|
272
|
-
tool_name:
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
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
|
-
|
301
|
-
|
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
|
-
|
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
|
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
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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.
|
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.
|
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")
|