hyperpocket 0.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. hyperpocket/__init__.py +7 -0
  2. hyperpocket/auth/README.KR.md +309 -0
  3. hyperpocket/auth/README.md +323 -0
  4. hyperpocket/auth/__init__.py +24 -0
  5. hyperpocket/auth/calendly/__init__.py +0 -0
  6. hyperpocket/auth/calendly/context.py +13 -0
  7. hyperpocket/auth/calendly/oauth2_context.py +25 -0
  8. hyperpocket/auth/calendly/oauth2_handler.py +146 -0
  9. hyperpocket/auth/calendly/oauth2_schema.py +16 -0
  10. hyperpocket/auth/context.py +38 -0
  11. hyperpocket/auth/github/__init__.py +0 -0
  12. hyperpocket/auth/github/context.py +13 -0
  13. hyperpocket/auth/github/oauth2_context.py +25 -0
  14. hyperpocket/auth/github/oauth2_handler.py +143 -0
  15. hyperpocket/auth/github/oauth2_schema.py +16 -0
  16. hyperpocket/auth/github/token_context.py +12 -0
  17. hyperpocket/auth/github/token_handler.py +79 -0
  18. hyperpocket/auth/github/token_schema.py +9 -0
  19. hyperpocket/auth/google/__init__.py +0 -0
  20. hyperpocket/auth/google/context.py +15 -0
  21. hyperpocket/auth/google/oauth2_context.py +31 -0
  22. hyperpocket/auth/google/oauth2_handler.py +137 -0
  23. hyperpocket/auth/google/oauth2_schema.py +18 -0
  24. hyperpocket/auth/handler.py +171 -0
  25. hyperpocket/auth/linear/__init__.py +0 -0
  26. hyperpocket/auth/linear/context.py +15 -0
  27. hyperpocket/auth/linear/token_context.py +15 -0
  28. hyperpocket/auth/linear/token_handler.py +68 -0
  29. hyperpocket/auth/linear/token_schema.py +9 -0
  30. hyperpocket/auth/provider.py +16 -0
  31. hyperpocket/auth/schema.py +19 -0
  32. hyperpocket/auth/slack/__init__.py +0 -0
  33. hyperpocket/auth/slack/context.py +15 -0
  34. hyperpocket/auth/slack/oauth2_context.py +40 -0
  35. hyperpocket/auth/slack/oauth2_handler.py +151 -0
  36. hyperpocket/auth/slack/oauth2_schema.py +40 -0
  37. hyperpocket/auth/slack/tests/__init__.py +0 -0
  38. hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
  39. hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
  40. hyperpocket/auth/slack/token_context.py +14 -0
  41. hyperpocket/auth/slack/token_handler.py +64 -0
  42. hyperpocket/auth/slack/token_schema.py +9 -0
  43. hyperpocket/auth/tests/__init__.py +0 -0
  44. hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
  45. hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
  46. hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
  47. hyperpocket/cli/__init__.py +0 -0
  48. hyperpocket/cli/__main__.py +12 -0
  49. hyperpocket/cli/pull.py +18 -0
  50. hyperpocket/cli/sync.py +17 -0
  51. hyperpocket/config/__init__.py +9 -0
  52. hyperpocket/config/auth.py +36 -0
  53. hyperpocket/config/git.py +17 -0
  54. hyperpocket/config/logger.py +81 -0
  55. hyperpocket/config/session.py +35 -0
  56. hyperpocket/config/settings.py +62 -0
  57. hyperpocket/constants.py +0 -0
  58. hyperpocket/curated_tools.py +10 -0
  59. hyperpocket/external/__init__.py +7 -0
  60. hyperpocket/external/github_client.py +19 -0
  61. hyperpocket/futures/__init__.py +7 -0
  62. hyperpocket/futures/futurestore.py +48 -0
  63. hyperpocket/pocket_auth.py +344 -0
  64. hyperpocket/pocket_main.py +351 -0
  65. hyperpocket/prompts.py +15 -0
  66. hyperpocket/repository/__init__.py +5 -0
  67. hyperpocket/repository/lock.py +156 -0
  68. hyperpocket/repository/lockfile.py +56 -0
  69. hyperpocket/repository/repository.py +18 -0
  70. hyperpocket/server/__init__.py +3 -0
  71. hyperpocket/server/auth/__init__.py +15 -0
  72. hyperpocket/server/auth/calendly.py +16 -0
  73. hyperpocket/server/auth/github.py +25 -0
  74. hyperpocket/server/auth/google.py +16 -0
  75. hyperpocket/server/auth/linear.py +18 -0
  76. hyperpocket/server/auth/slack.py +28 -0
  77. hyperpocket/server/auth/token.py +51 -0
  78. hyperpocket/server/proxy.py +63 -0
  79. hyperpocket/server/server.py +178 -0
  80. hyperpocket/server/tool/__init__.py +10 -0
  81. hyperpocket/server/tool/dto/__init__.py +0 -0
  82. hyperpocket/server/tool/dto/script.py +15 -0
  83. hyperpocket/server/tool/wasm.py +31 -0
  84. hyperpocket/session/README.KR.md +62 -0
  85. hyperpocket/session/README.md +61 -0
  86. hyperpocket/session/__init__.py +4 -0
  87. hyperpocket/session/in_memory.py +76 -0
  88. hyperpocket/session/interface.py +118 -0
  89. hyperpocket/session/redis.py +126 -0
  90. hyperpocket/session/tests/__init__.py +0 -0
  91. hyperpocket/session/tests/test_in_memory.py +145 -0
  92. hyperpocket/session/tests/test_redis.py +151 -0
  93. hyperpocket/tests/__init__.py +0 -0
  94. hyperpocket/tests/test_pocket.py +118 -0
  95. hyperpocket/tests/test_pocket_auth.py +982 -0
  96. hyperpocket/tool/README.KR.md +68 -0
  97. hyperpocket/tool/README.md +75 -0
  98. hyperpocket/tool/__init__.py +13 -0
  99. hyperpocket/tool/builtins/__init__.py +0 -0
  100. hyperpocket/tool/builtins/example/__init__.py +0 -0
  101. hyperpocket/tool/builtins/example/add_tool.py +18 -0
  102. hyperpocket/tool/function/README.KR.md +159 -0
  103. hyperpocket/tool/function/README.md +169 -0
  104. hyperpocket/tool/function/__init__.py +9 -0
  105. hyperpocket/tool/function/annotation.py +30 -0
  106. hyperpocket/tool/function/tool.py +87 -0
  107. hyperpocket/tool/tests/__init__.py +0 -0
  108. hyperpocket/tool/tests/test_function_tool.py +266 -0
  109. hyperpocket/tool/tool.py +106 -0
  110. hyperpocket/tool/wasm/README.KR.md +144 -0
  111. hyperpocket/tool/wasm/README.md +144 -0
  112. hyperpocket/tool/wasm/__init__.py +3 -0
  113. hyperpocket/tool/wasm/browser.py +63 -0
  114. hyperpocket/tool/wasm/invoker.py +41 -0
  115. hyperpocket/tool/wasm/script.py +82 -0
  116. hyperpocket/tool/wasm/templates/__init__.py +28 -0
  117. hyperpocket/tool/wasm/templates/node.py +87 -0
  118. hyperpocket/tool/wasm/templates/python.py +75 -0
  119. hyperpocket/tool/wasm/tool.py +147 -0
  120. hyperpocket/util/__init__.py +1 -0
  121. hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
  122. hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
  123. hyperpocket/util/find_all_subclass_in_package.py +29 -0
  124. hyperpocket/util/flatten_json_schema.py +45 -0
  125. hyperpocket/util/function_to_model.py +46 -0
  126. hyperpocket/util/get_objects_from_subpackage.py +28 -0
  127. hyperpocket/util/json_schema_to_model.py +69 -0
  128. hyperpocket-0.0.1.dist-info/METADATA +304 -0
  129. hyperpocket-0.0.1.dist-info/RECORD +131 -0
  130. hyperpocket-0.0.1.dist-info/WHEEL +4 -0
  131. hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,344 @@
1
+ import asyncio
2
+ import enum
3
+ import uuid
4
+ from typing import Optional, Type
5
+
6
+ from hyperpocket.auth import AuthProvider, PREBUILT_AUTH_HANDLERS
7
+ from hyperpocket.auth.context import AuthContext
8
+ from hyperpocket.auth.handler import AuthHandlerInterface, AuthenticateRequest
9
+ from hyperpocket.config import config, pocket_logger
10
+ from hyperpocket.futures import FutureStore
11
+ from hyperpocket.session import SESSION_STORAGE_LIST
12
+ from hyperpocket.session.interface import SessionStorageInterface
13
+
14
+
15
+ class AuthState(enum.Enum):
16
+ SKIP_AUTH = "skip_auth"
17
+ DO_AUTH = "do_auth"
18
+ DO_REFRESH = "do_refresh"
19
+ NO_SESSION = "no_session"
20
+ PENDING_RESOLVE = "pending_resolve"
21
+ RESOLVED = "resolved"
22
+
23
+
24
+ class PocketAuth(object):
25
+ handlers: dict[str, AuthHandlerInterface]
26
+ session_storage: SessionStorageInterface
27
+
28
+ def __init__(
29
+ self,
30
+ handlers: Optional[list[Type[AuthHandlerInterface]]] = None,
31
+ session_storage: Optional[SessionStorageInterface] = None,
32
+ use_prebuilt_handlers: bool = None):
33
+ if config.auth.use_prebuilt_auth or use_prebuilt_handlers:
34
+ handlers = PREBUILT_AUTH_HANDLERS + (handlers or [])
35
+ handler_impls = [C() for C in handlers] if handlers else []
36
+ self.handlers = {handler.name: handler for handler in handler_impls}
37
+ if session_storage:
38
+ self.session_storage = session_storage
39
+ else:
40
+ for session_type in SESSION_STORAGE_LIST:
41
+ if session_type.session_storage_type() == config.session.session_type:
42
+ session_config = getattr(config.session, config.session.session_type.value)
43
+
44
+ pocket_logger.info(f"init {session_type.session_storage_type()} session storage..")
45
+ self.session_storage = session_type(session_config)
46
+
47
+ if self.session_storage is None:
48
+ pocket_logger.error(f"not supported session type({config.session.session_type})")
49
+ raise RuntimeError(f"Not Supported Session Type({config.session.session_type})")
50
+
51
+ def make_request(self,
52
+ auth_scopes: list[str] = None,
53
+ auth_handler_name: Optional[str] = None,
54
+ auth_provider: Optional[AuthProvider] = None,
55
+ **kwargs) -> AuthenticateRequest:
56
+ """
57
+ Make AuthenticationRequest based on authentication handler.
58
+
59
+ Args:
60
+ auth_scopes (list[str]): list of auth scopes
61
+ auth_handler_name (Optional[str]): auth handler name
62
+ auth_provider (Optional[AuthProvider]): auth provider
63
+
64
+ Returns:
65
+ AuthenticateRequest: authenticate request of the handler
66
+ """
67
+ handler = self.find_handler_instance(auth_handler_name, auth_provider)
68
+ return handler.make_request(auth_scopes, **kwargs)
69
+
70
+ def check(self,
71
+ auth_req: AuthenticateRequest,
72
+ auth_handler_name: Optional[str] = None,
73
+ auth_provider: Optional[AuthProvider] = None,
74
+ thread_id: str = "default",
75
+ profile: str = 'default',
76
+ *args, **kwargs
77
+ ) -> AuthState:
78
+ """
79
+ Check current authentication state.
80
+
81
+ The `AuthState` includes 5 states:
82
+
83
+ - `NO_SESSION` : the session does not exist. it needs to create new session.
84
+ - `PENDING_RESOLVE` : the session already exists. but it needs user interaction.
85
+ - `RESOLVED` : the session already exists, and user interaction has already been completed. waiting for authentication to be continue.
86
+ - `SKIP_AUTH` : the session already exists, and the request can be processed using this session. so authentication is not required.
87
+ - `DO_AUTH` : the session already exists, but the request can't be processed using this session. so authentication is required.
88
+ - `DO_REFRESH` : the session already exists, but the session is expired. so refreshing session is required.
89
+
90
+ Args:
91
+ auth_req (AuthenticateRequest): authenticate request
92
+ auth_handler_name (Optional[str]): auth handler name
93
+ auth_provider (Optional[AuthProvider]): auth provider
94
+ thread_id (Optional[str]): thread id
95
+ profile (Optional[str]): profile name
96
+
97
+ Returns:
98
+ AuthState: current authentication state
99
+ """
100
+ handler = self.find_handler_instance(auth_handler_name, auth_provider)
101
+ session = self.session_storage.get(handler.provider(), thread_id, profile)
102
+
103
+ if not session:
104
+ return AuthState.NO_SESSION
105
+
106
+ if session.auth_resolve_uid:
107
+ future_data = FutureStore.get_future(session.auth_resolve_uid)
108
+ if future_data is not None and future_data.future.done():
109
+ return AuthState.RESOLVED
110
+
111
+ return AuthState.PENDING_RESOLVE
112
+
113
+ if not session.is_auth_applicable(auth_provider_name=handler.provider().name, auth_req=auth_req):
114
+ return AuthState.DO_AUTH
115
+
116
+ if session.is_near_expires():
117
+ return AuthState.DO_REFRESH
118
+
119
+ return AuthState.SKIP_AUTH
120
+
121
+ def prepare(self,
122
+ auth_req: AuthenticateRequest,
123
+ auth_handler_name: Optional[str] = None,
124
+ auth_provider: Optional[AuthProvider] = None,
125
+ thread_id: str = "default",
126
+ profile: str = 'default',
127
+ **kwargs) -> Optional[str]:
128
+ """
129
+ Prepare authentication.
130
+
131
+ - If the session is pending(e.g., PENDING_RESOLVE), return the existing URL.
132
+ - If the session is not created or not applicable(e.g., NO_SESSION, DO_AUTH), create a new session and future, then return the authentication URL.
133
+ - Other cases (e.g., DO_REFRESH, SKIP_AUTH, RESOLVED) is not handled in this method , it just returns None.
134
+
135
+ Args:
136
+ auth_req (AuthenticateRequest): authenticate request
137
+ auth_handler_name (Optional[str]): auth handler name
138
+ auth_provider (Optional[AuthProvider]): auth provider
139
+ thread_id (Optional[str]): thread id
140
+ profile (Optional[str]): profile name
141
+
142
+ Returns:
143
+ Optional[str]: authentication URL
144
+ """
145
+ auth_state = self.check(
146
+ auth_req=auth_req,
147
+ auth_handler_name=auth_handler_name,
148
+ auth_provider=auth_provider,
149
+ thread_id=thread_id,
150
+ profile=profile,
151
+ **kwargs,
152
+ )
153
+
154
+ pocket_logger.debug(
155
+ f"[thread_id({thread_id}):profile({profile})] {auth_provider.name} provider current auth state in prepare : {auth_state}")
156
+ if auth_state in [AuthState.SKIP_AUTH, AuthState.DO_REFRESH, AuthState.RESOLVED]:
157
+ return None
158
+
159
+ handler = self.find_handler_instance(auth_handler_name, auth_provider)
160
+ scope = handler.recommended_scopes().union(auth_req.auth_scopes)
161
+ session = self.session_storage.get(handler.provider(), thread_id, profile)
162
+ if session:
163
+ scope = scope.union(session.auth_scopes)
164
+
165
+ modified_req = auth_req.model_copy(update={'auth_scopes': scope})
166
+
167
+ # session in pending
168
+ if session and session.auth_resolve_uid:
169
+ pocket_logger.debug(
170
+ f"[thread_id({thread_id}):profile({profile})] already exists pending session(auth_resolve_uid:{session.auth_resolve_uid}).")
171
+ future_uid = session.auth_resolve_uid
172
+
173
+ # update session, in case of requesting new scopes before session pending resolved.
174
+ self._upsert_pending_session(
175
+ auth_handler=handler,
176
+ future_uid=session.auth_resolve_uid,
177
+ profile=profile,
178
+ thread_id=thread_id,
179
+ scope=scope
180
+ )
181
+ else: # create new pending session
182
+ future_uid = str(uuid.uuid4())
183
+ self._upsert_pending_session(
184
+ auth_handler=handler,
185
+ future_uid=future_uid,
186
+ profile=profile,
187
+ thread_id=thread_id,
188
+ scope=scope)
189
+
190
+ pocket_logger.debug(
191
+ f"[thread_id({thread_id}):profile({profile})] create new pending session(auth_resolve_uid:{future_uid}).")
192
+ asyncio.create_task(self._check_session_pending_resolved(handler, thread_id, profile))
193
+
194
+ prepare_url = handler.prepare(modified_req, thread_id, profile, future_uid, **kwargs)
195
+ pocket_logger.debug(
196
+ f"[thread_id({thread_id}):profile({profile})] auth_handler({auth_handler_name})'s prepare_url : {prepare_url}.")
197
+
198
+ return prepare_url
199
+
200
+ async def authenticate_async(self,
201
+ auth_req: AuthenticateRequest,
202
+ auth_handler_name: Optional[str] = None,
203
+ auth_provider: Optional[AuthProvider] = None,
204
+ thread_id: str = "default",
205
+ profile: str = 'default',
206
+ **kwargs) -> AuthContext:
207
+ """
208
+ Performing authentication.
209
+ It is performing authentication. and save the session in session storage.
210
+ And return `AuthContext`. `AuthContext` has only necessary fields of session to invoke tool.
211
+
212
+ - If auth state is SKIP_AUTH, return the existing AuthContext.
213
+ - If auth state is DO_REFRESH, it refreshes session, and update session.
214
+ - If auth state is PENDING_RESOLVE or RESOLVED, it performs authentication. in pending_resolve state, it waits for user interaction.
215
+ - Other cases(NO_SESSION or DO_AUTH) is not handled in this method , raise RuntimeError
216
+
217
+ Args:
218
+ auth_req (AuthenticateRequest): authenticate request
219
+ auth_handler_name (Optional[str]): auth handler name
220
+ auth_provider (Optional[AuthProvider]): auth provider
221
+ thread_id (Optional[str]): thread id
222
+ profile (Optional[str]): profile name
223
+
224
+ Returns:
225
+ AuthContext: authentication context
226
+ """
227
+ auth_state = self.check(
228
+ auth_req=auth_req,
229
+ auth_handler_name=auth_handler_name,
230
+ auth_provider=auth_provider,
231
+ thread_id=thread_id,
232
+ profile=profile,
233
+ **kwargs,
234
+ )
235
+ handler = self.find_handler_instance(auth_handler_name, auth_provider)
236
+
237
+ session = self.session_storage.get(handler.provider(), thread_id, profile)
238
+ if session is None:
239
+ pocket_logger.warning(
240
+ f"[thread_id({thread_id}):profile({profile})] Session can't find. session should exist in 'authenticate'.")
241
+ raise RuntimeError(
242
+ f"[thread_id({thread_id}):profile({profile})] Session can't find. session should exist in 'authenticate'.")
243
+
244
+ pocket_logger.debug(
245
+ f"[thread_id({thread_id}):profile({profile})] auth_handler({auth_handler_name})'s auth state : {auth_state}")
246
+ try:
247
+ if auth_state == AuthState.SKIP_AUTH:
248
+ context = session.auth_context
249
+ elif auth_state == AuthState.DO_REFRESH:
250
+ try:
251
+ context = await asyncio.wait_for(
252
+ handler.refresh(auth_req=auth_req, context=session.auth_context, **kwargs), timeout=300)
253
+ except Exception as e:
254
+ self.session_storage.delete(handler.provider(), thread_id, profile)
255
+ FutureStore.delete_future(session.auth_resolve_uid)
256
+
257
+ pocket_logger.warning(
258
+ f"[thread_id({thread_id}):profile({profile})] auth_handler({auth_handler_name}) failed to refresh the token.")
259
+ raise RuntimeError("Failed to refresh the token. Please re-authenticate.") from e
260
+ elif auth_state == AuthState.PENDING_RESOLVE or auth_state == AuthState.RESOLVED:
261
+ future_uid = session.auth_resolve_uid
262
+ context = await asyncio.wait_for(
263
+ handler.authenticate(auth_req=auth_req, future_uid=future_uid, **kwargs), timeout=300)
264
+ else:
265
+ # maybe auth_state is either AuthState.NO_SESSION or AuthState.DO_AUTH
266
+ pocket_logger.warning(
267
+ f"[thread_id({thread_id}):profile({profile})] Invalid State. 'authenticate' cannot be reached while in state {auth_state}")
268
+ raise RuntimeError(
269
+ f"[thread_id({thread_id}):profile({profile})] Invalid State. 'authenticate' cannot be reached while in state {auth_state}")
270
+
271
+ session = await self._set_session_active(
272
+ context=context,
273
+ provider=handler.provider(),
274
+ profile=profile,
275
+ thread_id=thread_id
276
+ )
277
+
278
+ return session.auth_context
279
+ except asyncio.TimeoutError as e:
280
+ pocket_logger.warning(f"Authentication Timeout. {session.auth_resolve_uid}")
281
+ self.delete_session(handler.provider(), thread_id, profile)
282
+ FutureStore.delete_future(session.auth_resolve_uid)
283
+ raise e
284
+
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
+ def get_auth_context(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default",
289
+ **kwargs) -> Optional[AuthContext]:
290
+ session = self.session_storage.get(auth_provider, thread_id, profile, **kwargs)
291
+ if session is None:
292
+ return None
293
+
294
+ return session.auth_context
295
+
296
+ def find_handler_instance(self, name: Optional[str] = None,
297
+ auth_provider: Optional[AuthProvider] = None) -> AuthHandlerInterface:
298
+ if name:
299
+ return self.handlers[name]
300
+ if auth_provider:
301
+ for handler in self.handlers.values():
302
+ if handler.provider() == auth_provider and handler.provider_default():
303
+ return handler
304
+ raise ValueError("No handler found")
305
+
306
+ async def _check_session_pending_resolved(self, auth_handler: AuthHandlerInterface, thread_id: str = "default",
307
+ profile: str = "default", timeout_seconds=300, **kwargs):
308
+ await asyncio.sleep(timeout_seconds)
309
+ session = self.session_storage.get(auth_handler.provider(), thread_id, profile, **kwargs)
310
+ if session.auth_resolve_uid is not None:
311
+ pocket_logger.info(f"session({session.auth_resolve_uid}) is not resolved yet and timeout. remove session")
312
+ self.delete_session(auth_handler.provider(), thread_id, profile)
313
+ FutureStore.delete_future(session.auth_resolve_uid)
314
+
315
+ return
316
+
317
+ def _upsert_pending_session(
318
+ self, auth_handler: AuthHandlerInterface, future_uid: str, profile: str, thread_id: str, scope: set[str]):
319
+ return self.session_storage.set(
320
+ auth_provider=auth_handler.provider(),
321
+ thread_id=thread_id,
322
+ profile=profile,
323
+ auth_scopes=list(scope),
324
+ auth_context=None,
325
+ is_auth_scope_universal=auth_handler.scoped,
326
+ auth_resolve_uid=future_uid,
327
+ )
328
+
329
+ async def _set_session_active(self, context: AuthContext, provider: AuthProvider, profile: str, thread_id: str):
330
+ session = self.session_storage.get(provider, thread_id, profile)
331
+ if session is None:
332
+ pocket_logger.error("the session to be active doesn't exist.")
333
+ return None
334
+
335
+ active_session = self.session_storage.set(
336
+ auth_provider=provider,
337
+ thread_id=thread_id,
338
+ profile=profile,
339
+ auth_scopes=list(session.auth_scopes),
340
+ is_auth_scope_universal=session.scoped,
341
+ auth_resolve_uid=None,
342
+ auth_context=context,
343
+ )
344
+ return active_session
@@ -0,0 +1,351 @@
1
+ import asyncio
2
+ import pathlib
3
+ from typing import Union, Any, Optional, Callable
4
+
5
+ from hyperpocket.config import pocket_logger
6
+ from hyperpocket.pocket_auth import PocketAuth
7
+ from hyperpocket.repository import Lockfile
8
+ from hyperpocket.repository.lock import LocalLock, GitLock
9
+ from hyperpocket.server.server import PocketServer, PocketServerOperations
10
+ from hyperpocket.tool import Tool, ToolRequest
11
+ from hyperpocket.tool.function.tool import FunctionTool
12
+ from hyperpocket.tool.wasm import WasmTool
13
+ from hyperpocket.tool.wasm.tool import WasmToolRequest
14
+
15
+ ToolLike = Union[Tool, str, Callable, ToolRequest]
16
+
17
+
18
+ class Pocket(object):
19
+ auth: PocketAuth
20
+ server: PocketServer
21
+ tools: dict[str, Tool]
22
+
23
+ def __init__(self,
24
+ tools: list[ToolLike],
25
+ auth: PocketAuth = None,
26
+ lockfile_path: Optional[str] = None,
27
+ force_update: bool = False):
28
+ if auth is None:
29
+ auth = PocketAuth()
30
+
31
+ if lockfile_path is None:
32
+ lockfile_path = "./pocket.lock"
33
+ lockfile_pathlib_path = pathlib.Path(lockfile_path)
34
+ lockfile = Lockfile(lockfile_pathlib_path)
35
+ tool_likes = []
36
+ for tool_like in tools:
37
+ if isinstance(tool_like, str):
38
+ if pathlib.Path(tool_like).exists():
39
+ lock = LocalLock(tool_like)
40
+ req = WasmToolRequest(lock, "")
41
+ else:
42
+ lock = GitLock(repository_url=tool_like, git_ref='HEAD')
43
+ req = WasmToolRequest(lock, "")
44
+ lockfile.add_lock(lock)
45
+ tool_likes.append(req)
46
+ elif isinstance(tool_like, WasmToolRequest):
47
+ lockfile.add_lock(tool_like.lock)
48
+ tool_likes.append(tool_like)
49
+ elif isinstance(tool_like, ToolRequest):
50
+ raise ValueError(f"unreachable. tool_like:{tool_like}")
51
+ elif isinstance(tool_like, WasmTool):
52
+ raise ValueError("WasmTool should pass ToolRequest instance instead.")
53
+ else:
54
+ tool_likes.append(tool_like)
55
+ lockfile.sync(force_update=force_update, referenced_only=True)
56
+
57
+ self.tools = dict()
58
+ for tool_like in tool_likes:
59
+ tool = self._load_tool(tool_like, lockfile)
60
+ if tool.name in self.tools:
61
+ pocket_logger.error(f"Duplicate tool name: {tool.name}.")
62
+ raise ValueError(f"Duplicate tool name: {tool.name}")
63
+ self.tools[tool.name] = tool
64
+
65
+ pocket_logger.info(f"All Tools Loaded successfully. total registered tools : {len(self.tools)}")
66
+
67
+ self.auth = auth
68
+ self.server = PocketServer()
69
+ self.server.run(self)
70
+
71
+ def _load_tool(self, tool_like: ToolLike, lockfile: Lockfile) -> Tool:
72
+ pocket_logger.info(f"Loading Tool {tool_like}")
73
+ if isinstance(tool_like, Tool):
74
+ tool = tool_like
75
+ elif isinstance(tool_like, ToolRequest):
76
+ tool = Tool.from_tool_request(tool_like, lockfile=lockfile)
77
+ elif isinstance(tool_like, Callable):
78
+ tool = FunctionTool.from_func(tool_like)
79
+ else:
80
+ raise ValueError(f"Invalid tool type: {type(tool_like)}")
81
+
82
+ pocket_logger.info(f"Complete Loading Tool {tool.name}")
83
+ return tool
84
+
85
+ def invoke(self,
86
+ tool_name: str,
87
+ body: Any,
88
+ thread_id: str = 'default',
89
+ profile: str = 'default',
90
+ *args, **kwargs) -> str:
91
+ """
92
+ Invoke Tool synchronously
93
+
94
+ Args:
95
+ tool_name(str): tool name to invoke
96
+ body(Any): tool arguments. should be json format
97
+ thread_id(str): thread id
98
+ profile(str): profile name
99
+
100
+ Returns:
101
+ str: tool result
102
+ """
103
+ result = asyncio.run(self.ainvoke(tool_name, body, thread_id, profile, *args, **kwargs))
104
+ return result
105
+
106
+ async def ainvoke(self,
107
+ tool_name: str,
108
+ body: Any,
109
+ thread_id: str = 'default',
110
+ profile: str = 'default',
111
+ *args, **kwargs) -> str:
112
+ """
113
+ Invoke Tool asynchronously
114
+
115
+ Args:
116
+ tool_name(str): tool name to invoke
117
+ body(Any): tool arguments. should be json format
118
+ thread_id(str): thread id
119
+ profile(str): profile name
120
+
121
+ Returns:
122
+ str: tool result
123
+ """
124
+ result, _ = await self.ainvoke_with_state(
125
+ tool_name=tool_name,
126
+ body=body,
127
+ thread_id=thread_id,
128
+ profile=profile,
129
+ *args,
130
+ **kwargs,
131
+ )
132
+ return result
133
+
134
+ def invoke_with_state(self,
135
+ tool_name: str,
136
+ body: Any,
137
+ thread_id: str = 'default',
138
+ profile: str = 'default',
139
+ *args, **kwargs) -> tuple[str, bool]:
140
+ """
141
+ Invoke Tool with state synchronously
142
+ State indicates whether this tool is paused or not.
143
+ If the tool needs user's interaction or waiting for some process, this tool is paused.
144
+
145
+ Args:
146
+ tool_name(str): tool name to invoke
147
+ body(Any): tool arguments. should be json format
148
+ thread_id(str): thread id
149
+ profile(str): profile name
150
+
151
+ Returns:
152
+ tuple[str, bool]: tool result and state.
153
+ """
154
+ try:
155
+ loop = asyncio.new_event_loop()
156
+ result = loop.run_until_complete(
157
+ self.ainvoke_with_state(tool_name, body, thread_id, profile, *args, **kwargs))
158
+
159
+ except RuntimeError as e:
160
+ pocket_logger.warning("Can't execute sync def in event loop. use nest-asyncio")
161
+
162
+ import nest_asyncio
163
+ loop = asyncio.new_event_loop()
164
+ nest_asyncio.apply(loop=loop)
165
+
166
+ result = loop.run_until_complete(
167
+ self.ainvoke_with_state(tool_name, body, thread_id, profile, *args, **kwargs))
168
+
169
+ return result
170
+
171
+ async def ainvoke_with_state(self,
172
+ tool_name: str,
173
+ body: Any,
174
+ thread_id: str = 'default',
175
+ profile: str = 'default',
176
+ *args, **kwargs) -> tuple[str, bool]:
177
+ """
178
+ Invoke Tool with state synchronously
179
+ State indicates whether this tool is paused or not.
180
+ If the tool needs user's interaction or waiting for some process, this tool is paused.
181
+
182
+ Args:
183
+ tool_name(str): tool name to invoke
184
+ body(Any): tool arguments. should be json format
185
+ thread_id(str): thread id
186
+ profile(str): profile name
187
+
188
+ Returns:
189
+ tuple[str, bool]: tool result and state.
190
+ """
191
+ result, paused = await self.server.call_in_subprocess(
192
+ PocketServerOperations.CALL,
193
+ args,
194
+ {
195
+ 'tool_name': tool_name,
196
+ 'body': body,
197
+ 'thread_id': thread_id,
198
+ 'profile': profile,
199
+ **kwargs,
200
+ },
201
+ )
202
+ if not isinstance(result, str):
203
+ result = str(result)
204
+
205
+ return result, paused
206
+
207
+ # DO NOT EVER THINK ABOUT USING SERVER PROPERTY BELOW HERE
208
+ async def acall(self,
209
+ tool_name: str,
210
+ body: Any,
211
+ thread_id: str = 'default',
212
+ profile: str = 'default',
213
+ *args, **kwargs) -> tuple[str, bool]:
214
+ """
215
+ Invoke tool asynchronously, not that different from `Pocket.invoke`
216
+ But this method is called only in subprocess.
217
+
218
+ This function performs the following steps:
219
+ 1. `prepare_auth` : preparing the authentication process for the tool if necessary.
220
+ 2. `authenticate` : performing authentication that needs to invoke tool.
221
+ 3. `tool_call` : Executing tool actually with authentication information.
222
+
223
+ Args:
224
+ tool_name(str): tool name to invoke
225
+ body(Any): tool arguments. should be json format
226
+ thread_id(str): thread id
227
+ profile(str): profile name
228
+
229
+ Returns:
230
+ tuple[str, bool]: tool result and state.
231
+ """
232
+ tool = self._tool_instance(tool_name)
233
+ if tool.auth is not None:
234
+ callback_info = self.prepare_auth(tool_name, thread_id, profile, **kwargs)
235
+ if callback_info:
236
+ return callback_info, True
237
+ # 02. authenticate
238
+ credentials = await self.authenticate(tool_name, thread_id, profile, **kwargs)
239
+ # 03. call tool
240
+ result = await self.tool_call(tool_name, body=body, envs=credentials, **kwargs)
241
+ return result, False
242
+
243
+ def prepare_auth(self,
244
+ tool_name: str,
245
+ thread_id: str = 'default',
246
+ profile: str = 'default',
247
+ **kwargs) -> Optional[str]:
248
+ """
249
+ Prepares the authentication process for the tool if necessary.
250
+ Returns callback URL and whether the tool requires authentication.
251
+
252
+ Args:
253
+ tool_name(str): tool name to invoke
254
+ thread_id(str): thread id
255
+ profile(str): profile name
256
+
257
+ Returns:
258
+ Optional[str]: callback URI if necessary
259
+ """
260
+ tool = self._tool_instance(tool_name)
261
+ if tool.auth is None:
262
+ return None
263
+
264
+ auth_req = self.auth.make_request(
265
+ auth_handler_name=tool.auth.auth_handler,
266
+ auth_provider=tool.auth.auth_provider,
267
+ auth_scopes=tool.auth.scopes)
268
+
269
+ return self.auth.prepare(
270
+ auth_req=auth_req,
271
+ auth_handler_name=tool.auth.auth_handler,
272
+ auth_provider=tool.auth.auth_provider,
273
+ thread_id=thread_id,
274
+ profile=profile,
275
+ **kwargs
276
+ )
277
+
278
+ async def authenticate(
279
+ self,
280
+ tool_name: str,
281
+ thread_id: str = 'default',
282
+ profile: str = 'default',
283
+ **kwargs) -> dict[str, str]:
284
+ """
285
+ Authenticates the handler included in the tool and returns credentials.
286
+
287
+ Args:
288
+ tool_name(str): tool name to invoke
289
+ thread_id(str): thread id
290
+ profile(str): profile name
291
+
292
+ Returns:
293
+ dict[str, str]: credentials
294
+ """
295
+ tool = self._tool_instance(tool_name)
296
+ if tool.auth is None:
297
+ return {}
298
+ auth_req = self.auth.make_request(
299
+ auth_handler_name=tool.auth.auth_handler,
300
+ auth_provider=tool.auth.auth_provider,
301
+ auth_scopes=tool.auth.scopes)
302
+ auth_ctx = await self.auth.authenticate_async(
303
+ auth_req=auth_req,
304
+ auth_handler_name=tool.auth.auth_handler,
305
+ auth_provider=tool.auth.auth_provider,
306
+ thread_id=thread_id,
307
+ profile=profile,
308
+ **kwargs,
309
+ )
310
+ return auth_ctx.to_dict()
311
+
312
+ async def tool_call(self, tool_name: str, **kwargs) -> str:
313
+ """
314
+ Executing tool actually
315
+
316
+ Args:
317
+ tool_name(str): tool name to invoke
318
+ kwargs(dict): keyword arguments. authentication information is passed through this.
319
+
320
+ Returns:
321
+ str: tool result
322
+ """
323
+ tool = self._tool_instance(tool_name)
324
+ try:
325
+ return await asyncio.wait_for(tool.ainvoke(**kwargs), timeout=180)
326
+ except asyncio.TimeoutError:
327
+ pocket_logger.warning("Timeout tool call.")
328
+ return "timeout tool call"
329
+
330
+ def _tool_instance(self, tool_name: str) -> Tool:
331
+ return self.tools[tool_name]
332
+
333
+ def __enter__(self):
334
+ return self
335
+
336
+ def __exit__(self, exc_type, exc_val, exc_tb):
337
+ if self.__dict__.get('server'):
338
+ self.server.teardown()
339
+
340
+ def __del__(self):
341
+ if self.__dict__.get('server'):
342
+ self.server.teardown()
343
+
344
+ def __getstate__(self):
345
+ state = self.__dict__.copy()
346
+ if 'server' in state:
347
+ del state['server']
348
+ return state
349
+
350
+ def __setstate__(self, state):
351
+ self.__dict__.update(state)