hyperpocket 0.0.2__py3-none-any.whl → 0.1.8__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. hyperpocket/auth/README.md +3 -3
  2. hyperpocket/auth/__init__.py +0 -8
  3. hyperpocket/auth/gumloop/context.py +13 -0
  4. hyperpocket/auth/gumloop/token_context.py +15 -0
  5. hyperpocket/auth/gumloop/token_handler.py +66 -0
  6. hyperpocket/auth/gumloop/token_schema.py +8 -0
  7. hyperpocket/auth/linear/token_context.py +1 -1
  8. hyperpocket/auth/notion/README.md +28 -0
  9. hyperpocket/auth/notion/context.py +15 -0
  10. hyperpocket/auth/notion/token_context.py +14 -0
  11. hyperpocket/auth/notion/token_handler.py +65 -0
  12. hyperpocket/auth/notion/token_schema.py +10 -0
  13. hyperpocket/auth/provider.py +8 -5
  14. hyperpocket/auth/reddit/context.py +15 -0
  15. hyperpocket/auth/reddit/oauth2_context.py +32 -0
  16. hyperpocket/auth/reddit/oauth2_handler.py +151 -0
  17. hyperpocket/auth/reddit/oauth2_schema.py +18 -0
  18. hyperpocket/auth/slack/token_context.py +1 -1
  19. hyperpocket/builtin.py +63 -0
  20. hyperpocket/cli/__main__.py +12 -0
  21. hyperpocket/cli/auth.py +83 -0
  22. hyperpocket/cli/codegen/auth/__init__.py +13 -0
  23. hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
  24. hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
  25. hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
  26. hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
  27. hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
  28. hyperpocket/cli/eject.py +19 -0
  29. hyperpocket/cli/sync.py +5 -5
  30. hyperpocket/config/settings.py +2 -4
  31. hyperpocket/futures/futurestore.py +0 -1
  32. hyperpocket/pocket_auth.py +25 -5
  33. hyperpocket/pocket_core.py +262 -0
  34. hyperpocket/pocket_main.py +125 -171
  35. hyperpocket/prompts.py +6 -8
  36. hyperpocket/repository/__init__.py +2 -2
  37. hyperpocket/repository/lock.py +19 -0
  38. hyperpocket/repository/lockfile.py +19 -13
  39. hyperpocket/repository/repository.py +26 -1
  40. hyperpocket/server/auth/__init__.py +0 -6
  41. hyperpocket/server/auth/gumloop.py +16 -0
  42. hyperpocket/server/auth/notion.py +19 -0
  43. hyperpocket/server/auth/reddit.py +16 -0
  44. hyperpocket/server/server.py +52 -16
  45. hyperpocket/server/tool/dto/script.py +15 -2
  46. hyperpocket/server/tool/wasm.py +20 -8
  47. hyperpocket/session/README.md +2 -2
  48. hyperpocket/session/in_memory.py +18 -5
  49. hyperpocket/session/interface.py +14 -0
  50. hyperpocket/session/redis.py +29 -5
  51. hyperpocket/tool/README.md +16 -12
  52. hyperpocket/tool/__init__.py +4 -3
  53. hyperpocket/tool/function/README.md +39 -10
  54. hyperpocket/tool/function/__init__.py +2 -0
  55. hyperpocket/tool/function/annotation.py +2 -1
  56. hyperpocket/tool/function/tool.py +98 -13
  57. hyperpocket/tool/tests/test_function_tool.py +55 -0
  58. hyperpocket/tool/tests/test_wasm_tool.py +73 -0
  59. hyperpocket/tool/tool.py +65 -2
  60. hyperpocket/tool/wasm/README.md +27 -5
  61. hyperpocket/tool/wasm/script.py +40 -1
  62. hyperpocket/tool/wasm/templates/python.py +32 -14
  63. hyperpocket/tool/wasm/tool.py +21 -18
  64. hyperpocket/tool_like.py +5 -0
  65. hyperpocket/util/__init__.py +1 -1
  66. hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
  67. hyperpocket/util/function_to_model.py +5 -2
  68. hyperpocket/util/json_schema_to_model.py +45 -26
  69. {hyperpocket-0.0.2.dist-info → hyperpocket-0.1.8.dist-info}/METADATA +101 -72
  70. hyperpocket-0.1.8.dist-info/RECORD +139 -0
  71. {hyperpocket-0.0.2.dist-info → hyperpocket-0.1.8.dist-info}/WHEEL +1 -1
  72. hyperpocket-0.1.8.dist-info/entry_points.txt +2 -0
  73. hyperpocket/auth/README.KR.md +0 -309
  74. hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
  75. hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
  76. hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
  77. hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
  78. hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
  79. hyperpocket/external/__init__.py +0 -7
  80. hyperpocket/external/github_client.py +0 -19
  81. hyperpocket/session/README.KR.md +0 -62
  82. hyperpocket/session/tests/test_in_memory.py +0 -145
  83. hyperpocket/session/tests/test_redis.py +0 -151
  84. hyperpocket/tests/test_pocket.py +0 -116
  85. hyperpocket/tests/test_pocket_auth.py +0 -982
  86. hyperpocket/tool/README.KR.md +0 -68
  87. hyperpocket/tool/builtins/__init__.py +0 -0
  88. hyperpocket/tool/builtins/example/__init__.py +0 -0
  89. hyperpocket/tool/builtins/example/add_tool.py +0 -18
  90. hyperpocket/tool/function/README.KR.md +0 -159
  91. hyperpocket/tool/wasm/README.KR.md +0 -144
  92. hyperpocket-0.0.2.dist-info/RECORD +0 -130
  93. hyperpocket-0.0.2.dist-info/entry_points.txt +0 -3
  94. /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
  95. /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
  96. /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
  97. /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
@@ -0,0 +1,83 @@
1
+ import os
2
+ import click
3
+ from pathlib import Path
4
+ from hyperpocket.cli.codegen.auth import get_server_auth_token_template, get_auth_context_template, get_auth_token_context_template, get_auth_token_handler_template, get_auth_token_schema_template
5
+
6
+ @click.command()
7
+ @click.argument('service_name', type=str)
8
+ def create_token_auth_template(service_name):
9
+ ## Validate service_name
10
+ if not service_name.islower() or not service_name.replace('_', '').isalpha():
11
+ raise ValueError("service_name must be lowercase and contain only letters and underscores")
12
+
13
+ capitliazed_service_name = service_name.capitalize()
14
+ if '_' in service_name:
15
+ capitliazed_service_name = ''.join([word.capitalize() for word in service_name.split('_')])
16
+
17
+ ## Get the current working directory
18
+ cwd = Path.cwd()
19
+ parent_path = cwd.parent
20
+
21
+ generate_server_auth(service_name, parent_path)
22
+ generate_hyperpocket_auth_dir(service_name, parent_path)
23
+ generate_auth_context(service_name, capitliazed_service_name, parent_path)
24
+ generate_auth_token_context(service_name, capitliazed_service_name, parent_path)
25
+ generate_auth_token_handler(service_name, capitliazed_service_name, parent_path)
26
+ generate_auth_token_schema(service_name, capitliazed_service_name, parent_path)
27
+ ##TODO: Add service to hyperpocket/auth/provider
28
+
29
+ def generate_server_auth(service_name, parent_path):
30
+ print(f"Generating server/auth for '{service_name}'.")
31
+ output_from_parsed_template = get_server_auth_token_template().render(service_name=service_name)
32
+ output_path = parent_path / f'hyperpocket/hyperpocket/server/auth/{service_name}.py'
33
+ with open(output_path, "w") as f:
34
+ f.write(output_from_parsed_template)
35
+
36
+ def generate_hyperpocket_auth_dir(service_name, parent_path):
37
+ if not os.path.exists(parent_path / f'hyperpocket/hyperpocket/auth/{service_name}'):
38
+ os.makedirs(parent_path / f'hyperpocket/hyperpocket/auth/{service_name}')
39
+
40
+ output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/__init__.py'
41
+ with open(output_path, "w") as f:
42
+ pass
43
+
44
+ def generate_auth_context(service_name, capitliazed_service_name, parent_path):
45
+ print(f"Generating auth/context for '{service_name}'.")
46
+ output_from_parsed_template = get_auth_context_template().render(
47
+ caplitalized_service_name=capitliazed_service_name,
48
+ upper_service_name=service_name.upper()
49
+ )
50
+ output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/context.py'
51
+ with open(output_path, "w") as f:
52
+ f.write(output_from_parsed_template)
53
+
54
+ def generate_auth_token_context(service_name, capitliazed_service_name, parent_path):
55
+ print(f"Generating auth/token context for '{service_name}'.")
56
+ output_from_parsed_template = get_auth_token_context_template().render(
57
+ service_name = service_name,
58
+ caplitalized_service_name=capitliazed_service_name,
59
+ )
60
+ output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/token_context.py'
61
+ with open(output_path, "w") as f:
62
+ f.write(output_from_parsed_template)
63
+
64
+ def generate_auth_token_handler(service_name, capitliazed_service_name, parent_path):
65
+ print(f"Generating auth/token handler for '{service_name}'.")
66
+ output_from_parsed_template = get_auth_token_handler_template().render(
67
+ service_name = service_name,
68
+ auth_handler_name = service_name.replace('_','-'),
69
+ caplitalized_service_name=capitliazed_service_name,
70
+ upper_service_name=service_name.upper()
71
+ )
72
+ output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/token_handler.py'
73
+ with open(output_path, "w") as f:
74
+ f.write(output_from_parsed_template)
75
+
76
+ def generate_auth_token_schema(service_name, capitliazed_service_name, parent_path):
77
+ print(f"Generating auth/token schema for '{service_name}'.")
78
+ output_from_parsed_template = get_auth_token_schema_template().render(
79
+ caplitalized_service_name=capitliazed_service_name,
80
+ )
81
+ output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/token_schema.py'
82
+ with open(output_path, "w") as f:
83
+ f.write(output_from_parsed_template)
@@ -0,0 +1,13 @@
1
+ from .auth_context_template import get_auth_context_template
2
+ from .auth_token_context_template import get_auth_token_context_template
3
+ from .auth_token_handler_template import get_auth_token_handler_template
4
+ from .auth_token_schema_template import get_auth_token_schema_template
5
+ from .server_auth_template import get_server_auth_token_template
6
+
7
+ __all__ = [
8
+ "get_auth_context_template",
9
+ "get_auth_token_context_template",
10
+ "get_auth_token_handler_template",
11
+ "get_auth_token_schema_template",
12
+ "get_server_auth_token_template",
13
+ ]
@@ -0,0 +1,16 @@
1
+ from jinja2 import Template
2
+
3
+ def get_auth_context_template() -> Template:
4
+ return Template('''
5
+ from hyperpocket.auth.context import AuthContext
6
+ class {{ caplitalized_service_name }}AuthContext(AuthContext):
7
+ _ACCESS_TOKEN_KEY: str = "{{ upper_service_name }}_TOKEN"
8
+ def to_dict(self) -> dict[str, str]:
9
+ return {
10
+ self._ACCESS_TOKEN_KEY: self.access_token,
11
+ }
12
+ def to_profiled_dict(self, profile: str) -> dict[str, str]:
13
+ return {
14
+ f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
15
+ }
16
+ ''')
@@ -0,0 +1,16 @@
1
+ from jinja2 import Template
2
+
3
+ def get_auth_token_context_template() -> Template:
4
+ return Template('''
5
+ from hyperpocket.auth.{{ service_name }}.context import {{ caplitalized_service_name }}AuthContext
6
+ from hyperpocket.auth.{{ service_name }}.token_schema import {{ caplitalized_service_name }}TokenResponse
7
+ class {{ caplitalized_service_name }}TokenAuthContext({{ caplitalized_service_name }}AuthContext):
8
+ @classmethod
9
+ def from_{{ service_name }}_token_response(cls, response: {{ caplitalized_service_name }}TokenResponse):
10
+ description = f'{{ caplitalized_service_name }} Token Context logged in'
11
+ return cls(
12
+ access_token=response.access_token,
13
+ description=description,
14
+ expires_at=None
15
+ )
16
+ ''')
@@ -0,0 +1,69 @@
1
+ from jinja2 import Template
2
+
3
+ def get_auth_token_handler_template() -> Template:
4
+ return Template('''
5
+ from typing import Optional
6
+ from urllib.parse import urljoin, urlencode
7
+
8
+ from hyperpocket.auth import AuthProvider
9
+ from hyperpocket.auth.context import AuthContext
10
+ from hyperpocket.auth.handler import AuthHandlerInterface, AuthenticateRequest
11
+ from hyperpocket.auth.{{ service_name }}.token_context import {{ caplitalized_service_name }}TokenAuthContext
12
+ from hyperpocket.auth.{{ service_name }}.token_schema import {{ caplitalized_service_name }}TokenResponse, {{ caplitalized_service_name }}TokenRequest
13
+ from hyperpocket.config import config
14
+ from hyperpocket.futures import FutureStore
15
+
16
+
17
+ class {{ caplitalized_service_name }}TokenAuthHandler(AuthHandlerInterface):
18
+ name: str = "{{ auth_handler_name }}-token"
19
+ description: str = "This handler is used to authenticate users using the {{ caplitalized_service_name }} token."
20
+ scoped: bool = False
21
+
22
+ _TOKEN_URL: str = urljoin(config.public_base_url + "/", f"{config.callback_url_rewrite_prefix}/auth/token")
23
+
24
+ @staticmethod
25
+ def provider() -> AuthProvider:
26
+ return AuthProvider.{{ upper_service_name }}
27
+
28
+ @staticmethod
29
+ def recommended_scopes() -> set[str]:
30
+ return set()
31
+
32
+ def prepare(self, auth_req: {{ caplitalized_service_name }}TokenRequest, thread_id: str, profile: str,
33
+ future_uid: str, *args, **kwargs) -> str:
34
+ redirect_uri = urljoin(
35
+ config.public_base_url + "/",
36
+ f"{config.callback_url_rewrite_prefix}/auth/{{ service_name }}/token/callback",
37
+ )
38
+ url = self._make_auth_url(auth_req=auth_req, redirect_uri=redirect_uri, state=future_uid)
39
+ FutureStore.create_future(future_uid, data={
40
+ "redirect_uri": redirect_uri,
41
+ "thread_id": thread_id,
42
+ "profile": profile,
43
+ })
44
+
45
+ return f'User needs to authenticate using the following URL: {url}'
46
+
47
+ async def authenticate(self, auth_req: {{ caplitalized_service_name }}TokenRequest, future_uid: str, *args, **kwargs) -> AuthContext:
48
+ future_data = FutureStore.get_future(future_uid)
49
+ access_token = await future_data.future
50
+
51
+ response = {{ caplitalized_service_name }}TokenResponse(access_token=access_token)
52
+ context = {{ caplitalized_service_name }}TokenAuthContext.from_{{ service_name }}_token_response(response)
53
+
54
+ return context
55
+
56
+ async def refresh(self, auth_req: {{ caplitalized_service_name }}TokenRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
57
+ raise Exception("{{ caplitalized_service_name }} token doesn't support refresh")
58
+
59
+ def _make_auth_url(self, auth_req: {{ caplitalized_service_name }}TokenRequest, redirect_uri: str, state: str):
60
+ params = {
61
+ "redirect_uri": redirect_uri,
62
+ "state": state,
63
+ }
64
+ auth_url = f"{self._TOKEN_URL}?{urlencode(params)}"
65
+ return auth_url
66
+
67
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> {{ caplitalized_service_name }}TokenRequest:
68
+ return {{ caplitalized_service_name }}TokenRequest()
69
+ ''')
@@ -0,0 +1,12 @@
1
+ from jinja2 import Template
2
+
3
+ def get_auth_token_schema_template() -> Template:
4
+ return Template('''
5
+ from typing import List, Optional
6
+ from pydantic import BaseModel
7
+ from hyperpocket.auth.schema import AuthenticateRequest, AuthenticateResponse
8
+ class {{ caplitalized_service_name }}TokenRequest(AuthenticateRequest):
9
+ pass
10
+ class {{ caplitalized_service_name }}TokenResponse(AuthenticateResponse):
11
+ access_token: str
12
+ ''')
@@ -0,0 +1,18 @@
1
+ from jinja2 import Template
2
+
3
+ def get_server_auth_token_template() -> Template:
4
+ return Template('''
5
+ from fastapi import APIRouter
6
+ from starlette.responses import HTMLResponse
7
+ from hyperpocket.futures import FutureStore
8
+ {{ service_name }}_auth_router = APIRouter(
9
+ prefix="/{{ service_name }}"
10
+ )
11
+ @{{ service_name }}_auth_router.get("/token/callback")
12
+ async def {{ service_name }}_token_callback(state: str, token: str):
13
+ try:
14
+ FutureStore.resolve_future(state, token)
15
+ except ValueError:
16
+ return HTMLResponse(content="failed")
17
+ return HTMLResponse(content="success")
18
+ ''')
@@ -0,0 +1,19 @@
1
+ import pathlib
2
+ from typing import Optional
3
+
4
+ import click
5
+
6
+ import hyperpocket.repository as repository
7
+
8
+
9
+ @click.command()
10
+ @click.argument("url", type=str)
11
+ @click.argument("ref", type=str)
12
+ @click.argument("remote_path", type=str)
13
+ @click.option("--lockfile", envvar="PATHS", type=click.Path(exists=True))
14
+ def eject(url: str, ref: str, remote_path: str, lockfile: Optional[pathlib.Path]):
15
+ if not lockfile:
16
+ lockfile = pathlib.Path.cwd() / "pocket.lock"
17
+ if not lockfile.exists():
18
+ raise ValueError("To eject a tool, you first need to pull it")
19
+ repository.eject(url, ref, remote_path, repository.Lockfile(lockfile))
hyperpocket/cli/sync.py CHANGED
@@ -7,11 +7,11 @@ import hyperpocket.repository as repository
7
7
 
8
8
 
9
9
  @click.command()
10
- @click.option("--lockfile", envvar='PATHS', type=click.Path(exists=True))
11
- @click.option("--force-update", type=str, default='HEAD')
12
- def sync(url: str, lockfile: Optional[pathlib.Path], force_update: bool):
10
+ @click.option("--lockfile", envvar="PATHS", type=click.Path(exists=True))
11
+ @click.option("--force-update", type=str, default="HEAD")
12
+ def sync(lockfile: Optional[pathlib.Path], force_update: bool):
13
13
  if not lockfile:
14
- lockfile = pathlib.Path.cwd() / 'pocket.lock'
14
+ lockfile = pathlib.Path.cwd() / "pocket.lock"
15
15
  if not lockfile.exists():
16
16
  lockfile.touch()
17
- repository.sync(repository.Lockfile(path=lockfile), force_update)
17
+ repository.sync(repository.Lockfile(path=lockfile), force_update)
@@ -1,12 +1,10 @@
1
1
  import os
2
2
  from pathlib import Path
3
- from typing import Literal
4
3
 
5
4
  from dynaconf import Dynaconf
6
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
7
6
 
8
7
  from hyperpocket.config.auth import AuthConfig, DefaultAuthConfig
9
- from hyperpocket.config.git import DefaultGitConfig, GitConfig
10
8
  from hyperpocket.config.session import DefaultSessionConfig, SessionConfig
11
9
 
12
10
  pocket_root = Path.home() / ".pocket"
@@ -43,8 +41,8 @@ class Config(BaseModel):
43
41
  callback_url_rewrite_prefix: str = "proxy" # should not start with a slash
44
42
  log_level: str = "INFO"
45
43
  auth: AuthConfig = DefaultAuthConfig
46
- git: GitConfig = DefaultGitConfig
47
44
  session: SessionConfig = DefaultSessionConfig
45
+ tool_vars: dict[str, str] = Field(default_factory=dict)
48
46
 
49
47
  @property
50
48
  def internal_base_url(self):
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import enum
3
2
  from typing import Any
4
3
 
5
4
  from hyperpocket.config import pocket_logger
@@ -9,7 +9,7 @@ from hyperpocket.auth.handler import AuthHandlerInterface, AuthenticateRequest
9
9
  from hyperpocket.config import config, pocket_logger
10
10
  from hyperpocket.futures import FutureStore
11
11
  from hyperpocket.session import SESSION_STORAGE_LIST
12
- from hyperpocket.session.interface import SessionStorageInterface
12
+ from hyperpocket.session.interface import SessionStorageInterface, BaseSessionValue
13
13
 
14
14
 
15
15
  class AuthState(enum.Enum):
@@ -99,7 +99,12 @@ class PocketAuth(object):
99
99
  """
100
100
  handler = self.find_handler_instance(auth_handler_name, auth_provider)
101
101
  session = self.session_storage.get(handler.provider(), thread_id, profile)
102
+ auth_state = self.get_session_state(session=session, auth_req=auth_req)
102
103
 
104
+ return auth_state
105
+
106
+ @staticmethod
107
+ def get_session_state(session: Optional[BaseSessionValue], auth_req: Optional[AuthenticateRequest]) -> AuthState:
103
108
  if not session:
104
109
  return AuthState.NO_SESSION
105
110
 
@@ -110,7 +115,8 @@ class PocketAuth(object):
110
115
 
111
116
  return AuthState.PENDING_RESOLVE
112
117
 
113
- if not session.is_auth_applicable(auth_provider_name=handler.provider().name, auth_req=auth_req):
118
+ if auth_req is not None and not session.is_auth_applicable(auth_provider_name=session.auth_provider_name,
119
+ auth_req=auth_req):
114
120
  return AuthState.DO_AUTH
115
121
 
116
122
  if session.is_near_expires():
@@ -282,9 +288,6 @@ class PocketAuth(object):
282
288
  FutureStore.delete_future(session.auth_resolve_uid)
283
289
  raise e
284
290
 
285
- def delete_session(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default") -> bool:
286
- return self.session_storage.delete(auth_provider, thread_id, profile)
287
-
288
291
  def get_auth_context(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default",
289
292
  **kwargs) -> Optional[AuthContext]:
290
293
  session = self.session_storage.get(auth_provider, thread_id, profile, **kwargs)
@@ -293,6 +296,23 @@ class PocketAuth(object):
293
296
 
294
297
  return session.auth_context
295
298
 
299
+ def list_session_state(self, thread_id: str, auth_provider: Optional[AuthProvider] = None):
300
+ session_list = self.session_storage.get_by_thread_id(thread_id=thread_id, auth_provider=auth_provider)
301
+ session_state_list = []
302
+ for session in session_list:
303
+ state = self.get_session_state(session=session, auth_req=None)
304
+
305
+ session_state_list.append({
306
+ "provider": session.auth_provider_name,
307
+ "scope": session.auth_scopes,
308
+ "state": state,
309
+ })
310
+
311
+ return session_state_list
312
+
313
+ def delete_session(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default") -> bool:
314
+ return self.session_storage.delete(auth_provider, thread_id, profile)
315
+
296
316
  def find_handler_instance(self, name: Optional[str] = None,
297
317
  auth_provider: Optional[AuthProvider] = None) -> AuthHandlerInterface:
298
318
  if name:
@@ -0,0 +1,262 @@
1
+ import asyncio
2
+ import pathlib
3
+ from typing import Any, Optional, Callable, List, Union
4
+
5
+ from hyperpocket.builtin import get_builtin_tools
6
+ from hyperpocket.config import pocket_logger
7
+ from hyperpocket.pocket_auth import PocketAuth
8
+ from hyperpocket.repository import Lockfile
9
+ from hyperpocket.repository.lock import LocalLock, GitLock
10
+ from hyperpocket.tool import Tool, ToolRequest
11
+ from hyperpocket.tool.function import from_func
12
+ from hyperpocket.tool.wasm import WasmTool
13
+ from hyperpocket.tool.wasm.tool import WasmToolRequest
14
+ from hyperpocket.tool_like import ToolLike
15
+
16
+
17
+ class PocketCore:
18
+ auth: PocketAuth
19
+ tools: dict[str, Tool]
20
+
21
+ def __init__(self,
22
+ tools: list[ToolLike],
23
+ auth: PocketAuth = None,
24
+ lockfile_path: Optional[str] = None,
25
+ force_update: bool = False):
26
+ if auth is None:
27
+ auth = PocketAuth()
28
+ self.auth = auth
29
+
30
+ if lockfile_path is None:
31
+ lockfile_path = "./pocket.lock"
32
+ lockfile_pathlib_path = pathlib.Path(lockfile_path)
33
+ lockfile = Lockfile(lockfile_pathlib_path)
34
+ tool_likes = []
35
+ for tool_like in tools:
36
+ if isinstance(tool_like, str):
37
+ if pathlib.Path(tool_like).exists():
38
+ lock = LocalLock(tool_like)
39
+ req = WasmToolRequest(lock, "")
40
+ else:
41
+ lock = GitLock(repository_url=tool_like, git_ref='HEAD')
42
+ req = WasmToolRequest(lock, "")
43
+ lockfile.add_lock(lock)
44
+ tool_likes.append(req)
45
+ elif isinstance(tool_like, WasmToolRequest):
46
+ lockfile.add_lock(tool_like.lock)
47
+ tool_likes.append(tool_like)
48
+ elif isinstance(tool_like, ToolRequest):
49
+ raise ValueError(f"unreachable. tool_like:{tool_like}")
50
+ elif isinstance(tool_like, WasmTool):
51
+ raise ValueError("WasmTool should pass ToolRequest instance instead.")
52
+ else:
53
+ tool_likes.append(tool_like)
54
+ lockfile.sync(force_update=force_update, referenced_only=True)
55
+
56
+ self.tools = dict()
57
+ for tool_like in tool_likes:
58
+ tool = self._load_tool(tool_like, lockfile)
59
+ if tool.name in self.tools:
60
+ pocket_logger.error(f"Duplicate tool name: {tool.name}.")
61
+ raise ValueError(f"Duplicate tool name: {tool.name}")
62
+ self.tools[tool.name] = tool
63
+
64
+ pocket_logger.info(f"All Registered Tools Loaded successfully. total registered tools : {len(self.tools)}")
65
+
66
+ builtin_tools = get_builtin_tools(self.auth)
67
+ for tool in builtin_tools:
68
+ self.tools[tool.name] = tool
69
+ pocket_logger.info(f"All BuiltIn Tools Loaded successfully. total tools : {len(self.tools)}")
70
+
71
+ async def acall(self,
72
+ tool_name: str,
73
+ body: Any,
74
+ thread_id: str = 'default',
75
+ profile: str = 'default',
76
+ *args, **kwargs) -> tuple[str, bool]:
77
+ """
78
+ Invoke tool asynchronously, not that different from `Pocket.invoke`
79
+ But this method is called only in subprocess.
80
+
81
+ This function performs the following steps:
82
+ 1. `prepare_auth` : preparing the authentication process for the tool if necessary.
83
+ 2. `authenticate` : performing authentication that needs to invoke tool.
84
+ 3. `tool_call` : Executing tool actually with authentication information.
85
+
86
+ Args:
87
+ tool_name(str): tool name to invoke
88
+ body(Any): tool arguments. should be json format
89
+ thread_id(str): thread id
90
+ profile(str): profile name
91
+
92
+ Returns:
93
+ tuple[str, bool]: tool result and state.
94
+ """
95
+ tool = self._tool_instance(tool_name)
96
+ if tool.auth is not None:
97
+ callback_info = self.prepare_auth(tool_name, thread_id, profile, **kwargs)
98
+ if callback_info:
99
+ return callback_info, True
100
+ # 02. authenticate
101
+ credentials = await self.authenticate(tool_name, thread_id, profile, **kwargs)
102
+ # 03. call tool
103
+ result = await self.tool_call(tool_name, body=body, envs=credentials, **kwargs)
104
+ return result, False
105
+
106
+ def prepare_auth(self,
107
+ tool_name: Union[str, List[str]],
108
+ thread_id: str = 'default',
109
+ profile: str = 'default',
110
+ **kwargs) -> Optional[str]:
111
+ """
112
+ Prepares the authentication process for the tool if necessary.
113
+ Returns callback URL and whether the tool requires authentication.
114
+
115
+ Args:
116
+ tool_name(Union[str,List[str]]): tool name to invoke
117
+ thread_id(str): thread id
118
+ profile(str): profile name
119
+
120
+ Returns:
121
+ Optional[str]: callback URI if necessary
122
+ """
123
+
124
+ if isinstance(tool_name, str):
125
+ tool_name = [tool_name]
126
+
127
+ tools: List[Tool] = []
128
+ for name in tool_name:
129
+ tool = self._tool_instance(name)
130
+ if tool.auth is not None:
131
+ tools.append(tool)
132
+
133
+ if len(tools) == 0:
134
+ return None
135
+
136
+ auth_handler_name = tools[0].auth.auth_handler
137
+ auth_provider = tools[0].auth.auth_provider
138
+ auth_scopes = set()
139
+
140
+ for tool in tools:
141
+ if tool.auth.auth_handler != auth_handler_name:
142
+ pocket_logger.error(
143
+ f"All Tools should have same auth handler. but it's different {tool.auth.auth_handler}, {auth_handler_name}")
144
+
145
+ return f"All Tools should have same auth handler. but it's different {tool.auth.auth_handler}, {auth_handler_name}"
146
+ if tool.auth.auth_provider != auth_provider:
147
+ pocket_logger.error(
148
+ f"All Tools should have same auth provider. but it's different {tool.auth.auth_provider}, {auth_provider}")
149
+ return f"All Tools should have same auth provider. but it's different {tool.auth.auth_provider}, {auth_provider}"
150
+
151
+ if tool.auth.scopes is not None:
152
+ auth_scopes |= set(tool.auth.scopes)
153
+
154
+ auth_req = self.auth.make_request(
155
+ auth_handler_name=auth_handler_name,
156
+ auth_provider=auth_provider,
157
+ auth_scopes=list(auth_scopes))
158
+
159
+ return self.auth.prepare(
160
+ auth_req=auth_req,
161
+ auth_handler_name=auth_handler_name,
162
+ auth_provider=auth_provider,
163
+ thread_id=thread_id,
164
+ profile=profile,
165
+ **kwargs
166
+ )
167
+
168
+ async def authenticate(
169
+ self,
170
+ tool_name: str,
171
+ thread_id: str = 'default',
172
+ profile: str = 'default',
173
+ **kwargs) -> dict[str, str]:
174
+ """
175
+ Authenticates the handler included in the tool and returns credentials.
176
+
177
+ Args:
178
+ tool_name(str): tool name to invoke
179
+ thread_id(str): thread id
180
+ profile(str): profile name
181
+
182
+ Returns:
183
+ dict[str, str]: credentials
184
+ """
185
+ tool = self._tool_instance(tool_name)
186
+ if tool.auth is None:
187
+ return {}
188
+ auth_req = self.auth.make_request(
189
+ auth_handler_name=tool.auth.auth_handler,
190
+ auth_provider=tool.auth.auth_provider,
191
+ auth_scopes=tool.auth.scopes)
192
+ auth_ctx = await self.auth.authenticate_async(
193
+ auth_req=auth_req,
194
+ auth_handler_name=tool.auth.auth_handler,
195
+ auth_provider=tool.auth.auth_provider,
196
+ thread_id=thread_id,
197
+ profile=profile,
198
+ **kwargs,
199
+ )
200
+ return auth_ctx.to_dict()
201
+
202
+ async def tool_call(self, tool_name: str, **kwargs) -> str:
203
+ """
204
+ Executing tool actually
205
+
206
+ Args:
207
+ tool_name(str): tool name to invoke
208
+ kwargs(dict): keyword arguments. authentication information is passed through this.
209
+
210
+ Returns:
211
+ str: tool result
212
+ """
213
+ tool = self._tool_instance(tool_name)
214
+ try:
215
+ result = await asyncio.wait_for(tool.ainvoke(**kwargs), timeout=180)
216
+ except asyncio.TimeoutError:
217
+ pocket_logger.warning("Timeout tool call.")
218
+ return "timeout tool call"
219
+
220
+ if tool.postprocessings is not None:
221
+ for postprocessing in tool.postprocessings:
222
+ try:
223
+ result = postprocessing(result)
224
+ except Exception as e:
225
+ exception_str = (
226
+ f"Error in postprocessing `{postprocessing.__name__}`: {e}"
227
+ )
228
+ pocket_logger.error(exception_str)
229
+ return exception_str
230
+
231
+ return result
232
+
233
+ def grouping_tool_by_auth_provider(self) -> dict[str, List[Tool]]:
234
+ tool_by_provider = {}
235
+ for tool_name, tool in self.tools.items():
236
+ if tool.auth is None:
237
+ continue
238
+
239
+ auth_provider_name = tool.auth.auth_provider.name
240
+ if tool_by_provider.get(auth_provider_name):
241
+ tool_by_provider[auth_provider_name].append(tool)
242
+ else:
243
+ tool_by_provider[auth_provider_name] = [tool]
244
+ return tool_by_provider
245
+
246
+ def _tool_instance(self, tool_name: str) -> Tool:
247
+ return self.tools[tool_name]
248
+
249
+ @staticmethod
250
+ def _load_tool(tool_like: ToolLike, lockfile: Lockfile) -> Tool:
251
+ pocket_logger.info(f"Loading Tool {tool_like}")
252
+ if isinstance(tool_like, Tool):
253
+ tool = tool_like
254
+ elif isinstance(tool_like, ToolRequest):
255
+ tool = Tool.from_tool_request(tool_like, lockfile=lockfile)
256
+ elif isinstance(tool_like, Callable):
257
+ tool = from_func(tool_like)
258
+ else:
259
+ raise ValueError(f"Invalid tool type: {type(tool_like)}")
260
+
261
+ pocket_logger.info(f"Complete Loading Tool {tool.name}")
262
+ return tool