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.
- hyperpocket/__init__.py +7 -0
- hyperpocket/auth/README.KR.md +309 -0
- hyperpocket/auth/README.md +323 -0
- hyperpocket/auth/__init__.py +24 -0
- hyperpocket/auth/calendly/__init__.py +0 -0
- hyperpocket/auth/calendly/context.py +13 -0
- hyperpocket/auth/calendly/oauth2_context.py +25 -0
- hyperpocket/auth/calendly/oauth2_handler.py +146 -0
- hyperpocket/auth/calendly/oauth2_schema.py +16 -0
- hyperpocket/auth/context.py +38 -0
- hyperpocket/auth/github/__init__.py +0 -0
- hyperpocket/auth/github/context.py +13 -0
- hyperpocket/auth/github/oauth2_context.py +25 -0
- hyperpocket/auth/github/oauth2_handler.py +143 -0
- hyperpocket/auth/github/oauth2_schema.py +16 -0
- hyperpocket/auth/github/token_context.py +12 -0
- hyperpocket/auth/github/token_handler.py +79 -0
- hyperpocket/auth/github/token_schema.py +9 -0
- hyperpocket/auth/google/__init__.py +0 -0
- hyperpocket/auth/google/context.py +15 -0
- hyperpocket/auth/google/oauth2_context.py +31 -0
- hyperpocket/auth/google/oauth2_handler.py +137 -0
- hyperpocket/auth/google/oauth2_schema.py +18 -0
- hyperpocket/auth/handler.py +171 -0
- hyperpocket/auth/linear/__init__.py +0 -0
- hyperpocket/auth/linear/context.py +15 -0
- hyperpocket/auth/linear/token_context.py +15 -0
- hyperpocket/auth/linear/token_handler.py +68 -0
- hyperpocket/auth/linear/token_schema.py +9 -0
- hyperpocket/auth/provider.py +16 -0
- hyperpocket/auth/schema.py +19 -0
- hyperpocket/auth/slack/__init__.py +0 -0
- hyperpocket/auth/slack/context.py +15 -0
- hyperpocket/auth/slack/oauth2_context.py +40 -0
- hyperpocket/auth/slack/oauth2_handler.py +151 -0
- hyperpocket/auth/slack/oauth2_schema.py +40 -0
- hyperpocket/auth/slack/tests/__init__.py +0 -0
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
- hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
- hyperpocket/auth/slack/token_context.py +14 -0
- hyperpocket/auth/slack/token_handler.py +64 -0
- hyperpocket/auth/slack/token_schema.py +9 -0
- hyperpocket/auth/tests/__init__.py +0 -0
- hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
- hyperpocket/cli/__init__.py +0 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/pull.py +18 -0
- hyperpocket/cli/sync.py +17 -0
- hyperpocket/config/__init__.py +9 -0
- hyperpocket/config/auth.py +36 -0
- hyperpocket/config/git.py +17 -0
- hyperpocket/config/logger.py +81 -0
- hyperpocket/config/session.py +35 -0
- hyperpocket/config/settings.py +62 -0
- hyperpocket/constants.py +0 -0
- hyperpocket/curated_tools.py +10 -0
- hyperpocket/external/__init__.py +7 -0
- hyperpocket/external/github_client.py +19 -0
- hyperpocket/futures/__init__.py +7 -0
- hyperpocket/futures/futurestore.py +48 -0
- hyperpocket/pocket_auth.py +344 -0
- hyperpocket/pocket_main.py +351 -0
- hyperpocket/prompts.py +15 -0
- hyperpocket/repository/__init__.py +5 -0
- hyperpocket/repository/lock.py +156 -0
- hyperpocket/repository/lockfile.py +56 -0
- hyperpocket/repository/repository.py +18 -0
- hyperpocket/server/__init__.py +3 -0
- hyperpocket/server/auth/__init__.py +15 -0
- hyperpocket/server/auth/calendly.py +16 -0
- hyperpocket/server/auth/github.py +25 -0
- hyperpocket/server/auth/google.py +16 -0
- hyperpocket/server/auth/linear.py +18 -0
- hyperpocket/server/auth/slack.py +28 -0
- hyperpocket/server/auth/token.py +51 -0
- hyperpocket/server/proxy.py +63 -0
- hyperpocket/server/server.py +178 -0
- hyperpocket/server/tool/__init__.py +10 -0
- hyperpocket/server/tool/dto/__init__.py +0 -0
- hyperpocket/server/tool/dto/script.py +15 -0
- hyperpocket/server/tool/wasm.py +31 -0
- hyperpocket/session/README.KR.md +62 -0
- hyperpocket/session/README.md +61 -0
- hyperpocket/session/__init__.py +4 -0
- hyperpocket/session/in_memory.py +76 -0
- hyperpocket/session/interface.py +118 -0
- hyperpocket/session/redis.py +126 -0
- hyperpocket/session/tests/__init__.py +0 -0
- hyperpocket/session/tests/test_in_memory.py +145 -0
- hyperpocket/session/tests/test_redis.py +151 -0
- hyperpocket/tests/__init__.py +0 -0
- hyperpocket/tests/test_pocket.py +118 -0
- hyperpocket/tests/test_pocket_auth.py +982 -0
- hyperpocket/tool/README.KR.md +68 -0
- hyperpocket/tool/README.md +75 -0
- hyperpocket/tool/__init__.py +13 -0
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +18 -0
- hyperpocket/tool/function/README.KR.md +159 -0
- hyperpocket/tool/function/README.md +169 -0
- hyperpocket/tool/function/__init__.py +9 -0
- hyperpocket/tool/function/annotation.py +30 -0
- hyperpocket/tool/function/tool.py +87 -0
- hyperpocket/tool/tests/__init__.py +0 -0
- hyperpocket/tool/tests/test_function_tool.py +266 -0
- hyperpocket/tool/tool.py +106 -0
- hyperpocket/tool/wasm/README.KR.md +144 -0
- hyperpocket/tool/wasm/README.md +144 -0
- hyperpocket/tool/wasm/__init__.py +3 -0
- hyperpocket/tool/wasm/browser.py +63 -0
- hyperpocket/tool/wasm/invoker.py +41 -0
- hyperpocket/tool/wasm/script.py +82 -0
- hyperpocket/tool/wasm/templates/__init__.py +28 -0
- hyperpocket/tool/wasm/templates/node.py +87 -0
- hyperpocket/tool/wasm/templates/python.py +75 -0
- hyperpocket/tool/wasm/tool.py +147 -0
- hyperpocket/util/__init__.py +1 -0
- hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
- hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
- hyperpocket/util/find_all_subclass_in_package.py +29 -0
- hyperpocket/util/flatten_json_schema.py +45 -0
- hyperpocket/util/function_to_model.py +46 -0
- hyperpocket/util/get_objects_from_subpackage.py +28 -0
- hyperpocket/util/json_schema_to_model.py +69 -0
- hyperpocket-0.0.1.dist-info/METADATA +304 -0
- hyperpocket-0.0.1.dist-info/RECORD +131 -0
- hyperpocket-0.0.1.dist-info/WHEEL +4 -0
- 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)
|