hyperpocket 0.4.5__py3-none-any.whl → 0.5.1__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/builtin.py CHANGED
@@ -12,7 +12,7 @@ def get_builtin_tools(pocket_auth: PocketAuth) -> List[Tool]:
12
12
  Builtin Tool can access to Pocket Core.
13
13
  """
14
14
 
15
- def __get_current_thread_session_state(thread_id: str = "default") -> str:
15
+ async def __get_current_thread_session_state(thread_id: str = "default") -> str:
16
16
  """
17
17
  This tool retrieves the current session state list for the specified thread.
18
18
 
@@ -31,7 +31,7 @@ def get_builtin_tools(pocket_auth: PocketAuth) -> List[Tool]:
31
31
 
32
32
  This tool ensures transparency about the current session but must respect user-driven intent and should never be called automatically or without a specific user request.
33
33
  """
34
- session_list = pocket_auth.list_session_state(thread_id)
34
+ session_list = await pocket_auth.list_session_state(thread_id)
35
35
  return str(session_list)
36
36
 
37
37
  def __delete_session(
@@ -58,7 +58,7 @@ def get_builtin_tools(pocket_auth: PocketAuth) -> List[Tool]:
58
58
  return str(is_deleted)
59
59
 
60
60
  builtin_tools = [
61
- from_func(func=__get_current_thread_session_state),
61
+ from_func(func=__get_current_thread_session_state, afunc=__get_current_thread_session_state),
62
62
  from_func(func=__delete_session),
63
63
  ]
64
64
 
@@ -2,7 +2,6 @@ import click
2
2
 
3
3
  from hyperpocket.cli.eject import eject
4
4
  from hyperpocket.cli.pull import pull
5
- from hyperpocket.cli.sync import sync
6
5
  from hyperpocket.cli.eject import eject
7
6
  from hyperpocket.cli.auth_token import create_token_auth_template
8
7
  from hyperpocket.cli.auth_oauth2 import create_oauth2_auth_template
@@ -28,7 +27,6 @@ devtool.add_command(build_tool)
28
27
  devtool.add_command(export_tool)
29
28
 
30
29
  cli.add_command(pull)
31
- cli.add_command(sync)
32
30
  cli.add_command(eject)
33
31
 
34
32
  cli()
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import pathlib
3
4
  from logging.handlers import RotatingFileHandler
4
5
  from pathlib import Path
5
6
 
@@ -27,9 +28,7 @@ class ColorFormatter(logging.Formatter):
27
28
 
28
29
 
29
30
  def get_logger():
30
- # init log file
31
- package_path = Path(os.path.dirname(hyperpocket.__file__))
32
- log_dir = package_path / ".log"
31
+ log_dir = pathlib.Path(os.getcwd()) / ".log"
33
32
  os.makedirs(log_dir, exist_ok=True)
34
33
  log_file = log_dir / "pocket.log"
35
34
  if not log_file.exists():
@@ -25,7 +25,6 @@ class FutureStore(object):
25
25
  f"the future already exists. the existing future is returned. uid: {uid}"
26
26
  )
27
27
  return future
28
-
29
28
  loop = asyncio.get_running_loop()
30
29
  future = loop.create_future()
31
30
  future_data = FutureData(future=future, data=data)
@@ -42,7 +41,13 @@ class FutureStore(object):
42
41
  if not future_data:
43
42
  raise ValueError(f"Future not found for uid={uid}")
44
43
  if not future_data.future.done():
45
- future_data.future.set_result(value)
44
+ # if the future loop is running, it should be executed in same event loop
45
+ loop = future_data.future.get_loop()
46
+ if loop.is_running():
47
+ loop.call_soon_threadsafe(future_data.future.set_result, value)
48
+ # if the future loop is not running, it can be executed from anywhere.
49
+ else:
50
+ future_data.future.set_result(value)
46
51
 
47
52
  def delete_future(self, uid: str):
48
53
  self.futures.pop(uid, None)
@@ -78,7 +78,7 @@ class PocketAuth(object):
78
78
  handler = self.find_handler_instance(auth_handler_name, auth_provider)
79
79
  return handler.make_request(auth_scopes, **kwargs)
80
80
 
81
- def check(
81
+ async def check(
82
82
  self,
83
83
  auth_req: AuthenticateRequest,
84
84
  auth_handler_name: Optional[str] = None,
@@ -112,12 +112,12 @@ class PocketAuth(object):
112
112
  """
113
113
  handler = self.find_handler_instance(auth_handler_name, auth_provider)
114
114
  session = self.session_storage.get(handler.provider(), thread_id, profile)
115
- auth_state = self.get_session_state(session=session, auth_req=auth_req)
115
+ auth_state = await self.get_session_state(session=session, auth_req=auth_req)
116
116
 
117
117
  return auth_state
118
118
 
119
119
  @staticmethod
120
- def get_session_state(
120
+ async def get_session_state(
121
121
  session: Optional[BaseSessionValue], auth_req: Optional[AuthenticateRequest]
122
122
  ) -> AuthState:
123
123
  if not session:
@@ -125,6 +125,8 @@ class PocketAuth(object):
125
125
 
126
126
  if session.auth_resolve_uid:
127
127
  future_data = FutureStore.get_future(session.auth_resolve_uid)
128
+ # it yields before checking future's state, because the future is being resolved on another thread's event loop.
129
+ await asyncio.sleep(0)
128
130
  if future_data is not None and future_data.future.done():
129
131
  return AuthState.RESOLVED
130
132
 
@@ -140,7 +142,7 @@ class PocketAuth(object):
140
142
 
141
143
  return AuthState.SKIP_AUTH
142
144
 
143
- def prepare(
145
+ async def prepare(
144
146
  self,
145
147
  auth_req: AuthenticateRequest,
146
148
  auth_handler_name: Optional[str] = None,
@@ -166,7 +168,7 @@ class PocketAuth(object):
166
168
  Returns:
167
169
  Optional[str]: authentication URL
168
170
  """
169
- auth_state = self.check(
171
+ auth_state = await self.check(
170
172
  auth_req=auth_req,
171
173
  auth_handler_name=auth_handler_name,
172
174
  auth_provider=auth_provider,
@@ -263,7 +265,7 @@ class PocketAuth(object):
263
265
  Returns:
264
266
  AuthContext: authentication context
265
267
  """
266
- auth_state = self.check(
268
+ auth_state = await self.check(
267
269
  auth_req=auth_req,
268
270
  auth_handler_name=auth_handler_name,
269
271
  auth_provider=auth_provider,
@@ -353,7 +355,7 @@ class PocketAuth(object):
353
355
 
354
356
  return session.auth_context
355
357
 
356
- def list_session_state(
358
+ async def list_session_state(
357
359
  self, thread_id: str, auth_provider: Optional[AuthProvider] = None
358
360
  ):
359
361
  session_list = self.session_storage.get_by_thread_id(
@@ -361,7 +363,7 @@ class PocketAuth(object):
361
363
  )
362
364
  session_state_list = []
363
365
  for session in session_list:
364
- state = self.get_session_state(session=session, auth_req=None)
366
+ state = await self.get_session_state(session=session, auth_req=None)
365
367
 
366
368
  session_state_list.append(
367
369
  {
@@ -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,29 +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
62
  self.teardown()
34
63
  pocket_logger.error(f"Failed to initialize pocket server. error : {e}")
35
- # self._teardown_server()
36
64
  raise e
37
-
38
- try:
39
- asyncio.get_running_loop()
40
- except RuntimeError:
41
- loop = asyncio.new_event_loop()
42
- else:
43
- import nest_asyncio
44
- loop = asyncio.new_event_loop()
45
- nest_asyncio.apply(loop=loop)
46
- loop.run_until_complete(self.server.plug_core(self._uid, self.core))
47
65
 
48
66
  def invoke(
49
67
  self,
@@ -176,17 +194,12 @@ class Pocket(object):
176
194
  **kwargs,
177
195
  }
178
196
 
179
- result, paused = await self.server.call_in_subprocess(
180
- PocketServerOperations.CALL,
181
- self._uid,
182
- args,
183
- {
184
- "tool_name": tool_name,
185
- "body": body,
186
- "thread_id": thread_id,
187
- "profile": profile,
188
- **kwargs,
189
- },
197
+ result, paused = await self.acall(
198
+ tool_name=tool_name,
199
+ body=body,
200
+ thread_id=thread_id,
201
+ profile=profile,
202
+ **kwargs
190
203
  )
191
204
  if not isinstance(result, str):
192
205
  result = str(result)
@@ -213,15 +226,16 @@ class Pocket(object):
213
226
  Returns:
214
227
  List[str]: A list of authentication URIs for the tools that require authentication.
215
228
  """
216
- tool_by_provider = self.core.grouping_tool_by_auth_provider()
229
+ tool_by_provider = self.grouping_tool_by_auth_provider()
217
230
 
218
231
  prepare_list = {}
219
232
  for provider, tools in tool_by_provider.items():
220
233
  tool_name_list = [tool.name for tool in tools]
221
- prepare = await self.prepare_in_subprocess(
222
- 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,
223
238
  )
224
-
225
239
  if prepare is not None:
226
240
  prepare_list[provider] = prepare
227
241
 
@@ -245,16 +259,17 @@ class Pocket(object):
245
259
  or `False` if the process was interrupted or failed.
246
260
  """
247
261
  try:
248
- tool_by_provider = self.core.grouping_tool_by_auth_provider()
262
+ tool_by_provider = self.grouping_tool_by_auth_provider()
249
263
 
250
264
  waiting_futures = []
251
265
  for provider, tools in tool_by_provider.items():
252
266
  if len(tools) == 0:
253
267
  continue
254
-
255
268
  waiting_futures.append(
256
- self.authenticate_in_subprocess(
257
- 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,
258
273
  )
259
274
  )
260
275
 
@@ -266,51 +281,150 @@ class Pocket(object):
266
281
  pocket_logger.error("authentication time out.")
267
282
  raise e
268
283
 
269
- async def prepare_in_subprocess(
284
+ async def acall(
270
285
  self,
271
- tool_name: Union[str, List[str]],
286
+ tool_name: str,
287
+ body: Any,
272
288
  thread_id: str = "default",
273
289
  profile: str = "default",
274
290
  *args,
275
291
  **kwargs,
276
- ):
277
- prepare = await self.server.call_in_subprocess(
278
- PocketServerOperations.PREPARE_AUTH,
279
- self._uid,
280
- args,
281
- {
282
- "tool_name": tool_name,
283
- "thread_id": thread_id,
284
- "profile": profile,
285
- **kwargs,
286
- },
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),
287
380
  )
288
381
 
289
- 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
+ )
290
390
 
291
- async def authenticate_in_subprocess(
391
+ async def authenticate(
292
392
  self,
293
393
  tool_name: str,
294
394
  thread_id: str = "default",
295
395
  profile: str = "default",
296
- *args,
297
396
  **kwargs,
298
- ):
299
- credentials = await self.server.call_in_subprocess(
300
- PocketServerOperations.AUTHENTICATE,
301
- self._uid,
302
- args,
303
- {
304
- "tool_name": tool_name,
305
- "thread_id": thread_id,
306
- "profile": profile,
307
- **kwargs,
308
- },
309
- )
397
+ ) -> dict[str, str]:
398
+ """
399
+ Authenticates the handler included in the tool and returns credentials.
310
400
 
311
- 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()
312
426
 
313
- async def tool_call_in_subprocess(
427
+ async def tool_call(
314
428
  self,
315
429
  tool_name: str,
316
430
  body: Any,
@@ -319,42 +433,76 @@ class Pocket(object):
319
433
  *args,
320
434
  **kwargs,
321
435
  ):
322
- result = await self.server.call_in_subprocess(
323
- PocketServerOperations.TOOL_CALL,
324
- self._uid,
325
- args,
326
- {
327
- "tool_name": tool_name,
328
- "body": body,
329
- "thread_id": thread_id,
330
- "profile": profile,
331
- **kwargs,
332
- },
333
- )
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
334
455
 
335
456
  return result
336
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
+
337
494
  def _teardown_server(self):
338
- self.server.teardown()
339
-
495
+ self.teardown()
496
+
340
497
  def teardown(self):
341
498
  if hasattr(self, 'server'):
342
- self.server.refcnt_down(self._uid)
499
+ with Pocket._pocket_count_lock:
500
+ Pocket._cnt_pocket_count -= 1
501
+ if Pocket._cnt_pocket_count <= 0:
502
+ self.server.teardown()
343
503
 
344
504
  def __enter__(self):
345
505
  return self
346
506
 
347
507
  def __exit__(self, exc_type, exc_val, exc_tb):
348
508
  self.teardown()
349
-
350
- def __del__(self):
351
- self.teardown()
352
-
353
- def __getstate__(self):
354
- state = self.__dict__.copy()
355
- if "server" in state:
356
- del state["server"]
357
- return state
358
-
359
- def __setstate__(self, state):
360
- self.__dict__.update(state)