hyperpocket 0.0.3__py3-none-any.whl → 0.1.9__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hyperpocket/auth/README.md +3 -3
- hyperpocket/auth/__init__.py +0 -8
- hyperpocket/auth/gumloop/context.py +13 -0
- hyperpocket/auth/gumloop/token_context.py +15 -0
- hyperpocket/auth/gumloop/token_handler.py +66 -0
- hyperpocket/auth/gumloop/token_schema.py +8 -0
- hyperpocket/auth/linear/token_context.py +1 -1
- hyperpocket/auth/notion/README.md +28 -0
- hyperpocket/auth/notion/context.py +15 -0
- hyperpocket/auth/notion/token_context.py +14 -0
- hyperpocket/auth/notion/token_handler.py +65 -0
- hyperpocket/auth/notion/token_schema.py +10 -0
- hyperpocket/auth/provider.py +8 -5
- hyperpocket/auth/reddit/context.py +15 -0
- hyperpocket/auth/reddit/oauth2_context.py +32 -0
- hyperpocket/auth/reddit/oauth2_handler.py +151 -0
- hyperpocket/auth/reddit/oauth2_schema.py +18 -0
- hyperpocket/auth/slack/token_context.py +1 -1
- hyperpocket/builtin.py +63 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/auth.py +83 -0
- hyperpocket/cli/codegen/auth/__init__.py +13 -0
- hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
- hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
- hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
- hyperpocket/cli/eject.py +19 -0
- hyperpocket/cli/sync.py +5 -5
- hyperpocket/config/settings.py +14 -12
- hyperpocket/futures/futurestore.py +0 -1
- hyperpocket/pocket_auth.py +25 -5
- hyperpocket/pocket_core.py +264 -0
- hyperpocket/pocket_main.py +127 -174
- hyperpocket/prompts.py +6 -8
- hyperpocket/repository/__init__.py +2 -2
- hyperpocket/repository/lock.py +71 -1
- hyperpocket/repository/lockfile.py +19 -13
- hyperpocket/repository/repository.py +26 -1
- hyperpocket/server/auth/__init__.py +0 -6
- hyperpocket/server/auth/gumloop.py +16 -0
- hyperpocket/server/auth/notion.py +19 -0
- hyperpocket/server/auth/reddit.py +16 -0
- hyperpocket/server/server.py +56 -20
- hyperpocket/server/tool/dto/script.py +15 -2
- hyperpocket/server/tool/wasm.py +20 -8
- hyperpocket/session/README.md +2 -2
- hyperpocket/session/in_memory.py +18 -5
- hyperpocket/session/interface.py +14 -0
- hyperpocket/session/redis.py +29 -5
- hyperpocket/tool/README.md +16 -12
- hyperpocket/tool/__init__.py +4 -3
- hyperpocket/tool/function/README.md +39 -10
- hyperpocket/tool/function/__init__.py +2 -0
- hyperpocket/tool/function/annotation.py +2 -1
- hyperpocket/tool/function/tool.py +108 -29
- hyperpocket/tool/tool.py +100 -28
- hyperpocket/tool/wasm/README.md +27 -5
- hyperpocket/tool/wasm/browser.py +2 -7
- hyperpocket/tool/wasm/script.py +40 -1
- hyperpocket/tool/wasm/templates/python.py +32 -14
- hyperpocket/tool/wasm/tool.py +21 -18
- hyperpocket/tool_like.py +5 -0
- hyperpocket/util/__init__.py +1 -1
- hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
- hyperpocket/util/function_to_model.py +5 -2
- hyperpocket/util/json_schema_to_model.py +47 -26
- {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/METADATA +107 -88
- hyperpocket-0.1.9.dist-info/RECORD +137 -0
- {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/WHEEL +1 -1
- hyperpocket-0.1.9.dist-info/entry_points.txt +2 -0
- hyperpocket/auth/README.KR.md +0 -309
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
- hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
- hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
- hyperpocket/external/__init__.py +0 -7
- hyperpocket/external/github_client.py +0 -19
- hyperpocket/session/README.KR.md +0 -62
- hyperpocket/session/tests/test_in_memory.py +0 -145
- hyperpocket/session/tests/test_redis.py +0 -151
- hyperpocket/tests/test_pocket.py +0 -116
- hyperpocket/tests/test_pocket_auth.py +0 -982
- hyperpocket/tool/README.KR.md +0 -68
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +0 -18
- hyperpocket/tool/function/README.KR.md +0 -159
- hyperpocket/tool/tests/test_function_tool.py +0 -266
- hyperpocket/tool/wasm/README.KR.md +0 -144
- hyperpocket-0.0.3.dist-info/RECORD +0 -130
- hyperpocket-0.0.3.dist-info/entry_points.txt +0 -3
- /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
- /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
- /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
- /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
@@ -1,6 +1,8 @@
|
|
1
|
+
import asyncio
|
1
2
|
import copy
|
2
3
|
import inspect
|
3
|
-
from typing import
|
4
|
+
from typing import Any, Coroutine
|
5
|
+
from typing import Callable, Optional
|
4
6
|
|
5
7
|
from pydantic import BaseModel
|
6
8
|
|
@@ -13,49 +15,67 @@ class FunctionTool(Tool):
|
|
13
15
|
"""
|
14
16
|
FunctionTool is Tool executing local python method.
|
15
17
|
"""
|
16
|
-
func: Callable
|
17
|
-
afunc: Optional[Callable[...,
|
18
|
+
func: Optional[Callable[..., str]]
|
19
|
+
afunc: Optional[Callable[..., Coroutine[Any, Any, str]]]
|
18
20
|
|
19
21
|
def invoke(self, **kwargs) -> str:
|
20
22
|
binding_args = self._get_binding_args(kwargs)
|
21
|
-
|
23
|
+
if self.func is None:
|
24
|
+
if self.afunc is None:
|
25
|
+
raise ValueError("Both func and afunc are None")
|
26
|
+
try:
|
27
|
+
return str(asyncio.run(self.afunc(**binding_args)))
|
28
|
+
except Exception as e:
|
29
|
+
return "There was an error while executing the tool: " + str(e)
|
30
|
+
try:
|
31
|
+
return str(self.func(**binding_args))
|
32
|
+
except Exception as e:
|
33
|
+
return "There was an error while executing the tool: " + str(e)
|
22
34
|
|
23
35
|
async def ainvoke(self, **kwargs) -> str:
|
24
|
-
binding_args = self._get_binding_args(kwargs)
|
25
36
|
if self.afunc is None:
|
26
|
-
return str(self.
|
27
|
-
|
37
|
+
return str(self.invoke(**kwargs))
|
38
|
+
try:
|
39
|
+
binding_args = self._get_binding_args(kwargs)
|
40
|
+
return str(await self.afunc(**binding_args))
|
41
|
+
except Exception as e:
|
42
|
+
return "There was an error while executing the tool: " + str(e)
|
28
43
|
|
29
44
|
def _get_binding_args(self, kwargs):
|
30
45
|
_kwargs = copy.deepcopy(kwargs)
|
31
46
|
|
32
47
|
# make body args to model
|
33
|
-
schema_model = self.schema_model()
|
34
|
-
model = schema_model(
|
48
|
+
schema_model = self.schema_model(use_profile=False)
|
49
|
+
model = schema_model(**_kwargs["body"])
|
35
50
|
_kwargs.pop("body")
|
36
51
|
|
37
52
|
# body model to dict
|
38
|
-
args = self.model_to_kwargs(model
|
53
|
+
args = self.model_to_kwargs(model)
|
39
54
|
|
40
55
|
# binding args
|
41
56
|
binding_args = {}
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
57
|
+
if self.func.__dict__.get("__model__") is not None:
|
58
|
+
# when a function signature is not inferrable from the function itself
|
59
|
+
binding_args = args.copy()
|
60
|
+
binding_args |= _kwargs.get("envs", {}) | self.tool_vars
|
61
|
+
else:
|
62
|
+
sig = inspect.signature(self.func)
|
63
|
+
for param_name, param in sig.parameters.items():
|
64
|
+
if param_name not in args:
|
65
|
+
continue
|
46
66
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
67
|
+
if param.kind == param.VAR_KEYWORD:
|
68
|
+
# var keyword args should be passed by plain dict
|
69
|
+
binding_args |= args[param_name]
|
70
|
+
binding_args |= _kwargs.get("envs", {}) | self.tool_vars
|
51
71
|
|
52
|
-
|
53
|
-
|
72
|
+
if "envs" in _kwargs:
|
73
|
+
_kwargs.pop("envs")
|
54
74
|
|
55
|
-
|
56
|
-
|
75
|
+
binding_args |= _kwargs # add other kwargs
|
76
|
+
continue
|
57
77
|
|
58
|
-
|
78
|
+
binding_args[param_name] = args[param_name]
|
59
79
|
|
60
80
|
return binding_args
|
61
81
|
|
@@ -69,11 +89,26 @@ class FunctionTool(Tool):
|
|
69
89
|
|
70
90
|
@classmethod
|
71
91
|
def from_func(
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
92
|
+
cls,
|
93
|
+
func: Callable | 'FunctionTool',
|
94
|
+
afunc: Callable[..., Coroutine[Any, Any, str]] | 'FunctionTool' = None,
|
95
|
+
auth: Optional[ToolAuth] = None,
|
96
|
+
tool_vars: dict[str, str] = None,
|
76
97
|
) -> "FunctionTool":
|
98
|
+
if tool_vars is None:
|
99
|
+
tool_vars = dict()
|
100
|
+
|
101
|
+
if isinstance(func, FunctionTool):
|
102
|
+
if tool_vars is not None:
|
103
|
+
func.override_tool_variables(tool_vars)
|
104
|
+
return func
|
105
|
+
elif isinstance(afunc, FunctionTool):
|
106
|
+
if tool_vars is not None:
|
107
|
+
afunc.override_tool_variables(tool_vars)
|
108
|
+
return afunc
|
109
|
+
elif not callable(func) and not callable(afunc):
|
110
|
+
raise ValueError("FunctionTool can only be created from a callable")
|
111
|
+
|
77
112
|
model = function_to_model(func)
|
78
113
|
argument_json_schema = flatten_json_schema(model.model_json_schema())
|
79
114
|
|
@@ -81,7 +116,51 @@ class FunctionTool(Tool):
|
|
81
116
|
func=func,
|
82
117
|
afunc=afunc,
|
83
118
|
name=func.__name__,
|
84
|
-
description=
|
119
|
+
description=func.__doc__ if func.__doc__ is not None else "",
|
85
120
|
argument_json_schema=argument_json_schema,
|
86
|
-
auth=auth
|
121
|
+
auth=auth,
|
122
|
+
default_tool_vars=tool_vars
|
87
123
|
)
|
124
|
+
|
125
|
+
@classmethod
|
126
|
+
def from_dock(
|
127
|
+
cls,
|
128
|
+
dock: list[Callable[..., str]],
|
129
|
+
tool_vars: Optional[dict[str, str]] = None,
|
130
|
+
) -> list["FunctionTool"]:
|
131
|
+
if tool_vars is None:
|
132
|
+
tool_vars = dict()
|
133
|
+
tools = []
|
134
|
+
for func in dock:
|
135
|
+
if (_model := func.__dict__.get("__model__")) is not None:
|
136
|
+
model = _model
|
137
|
+
else:
|
138
|
+
model = function_to_model(func)
|
139
|
+
argument_json_schema = flatten_json_schema(model.model_json_schema())
|
140
|
+
if not callable(func):
|
141
|
+
raise ValueError(f"Dock element should be a list of functions, but found {func}")
|
142
|
+
is_coroutine = inspect.iscoroutinefunction(func)
|
143
|
+
auth = None
|
144
|
+
if func.__dict__.get("__auth__") is not None:
|
145
|
+
auth = ToolAuth(**func.__dict__["__auth__"])
|
146
|
+
if is_coroutine:
|
147
|
+
tools.append(cls(
|
148
|
+
func=None,
|
149
|
+
afunc=func,
|
150
|
+
name=func.__name__,
|
151
|
+
description=func.__doc__,
|
152
|
+
argument_json_schema=argument_json_schema,
|
153
|
+
auth=auth,
|
154
|
+
default_tool_vars=(tool_vars | func.__dict__.get("__vars__", {})),
|
155
|
+
))
|
156
|
+
else:
|
157
|
+
tools.append(cls(
|
158
|
+
func=func,
|
159
|
+
afunc=None,
|
160
|
+
name=func.__name__,
|
161
|
+
description=func.__doc__,
|
162
|
+
argument_json_schema=argument_json_schema,
|
163
|
+
auth=auth,
|
164
|
+
default_tool_vars=(tool_vars | func.__dict__.get("__vars__", {})),
|
165
|
+
))
|
166
|
+
return tools
|
hyperpocket/tool/tool.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
import abc
|
2
|
-
from typing import
|
2
|
+
from typing import Optional, Type, Callable
|
3
3
|
|
4
4
|
from pydantic import BaseModel, Field
|
5
5
|
|
6
6
|
from hyperpocket.auth.provider import AuthProvider
|
7
7
|
from hyperpocket.config.logger import pocket_logger
|
8
|
+
from hyperpocket.prompts import pocket_extended_tool_description
|
8
9
|
from hyperpocket.util.json_schema_to_model import json_schema_to_model
|
9
10
|
|
10
11
|
|
@@ -27,10 +28,34 @@ class ToolAuth(BaseModel):
|
|
27
28
|
|
28
29
|
|
29
30
|
class ToolRequest(abc.ABC):
|
31
|
+
postprocessings: Optional[list[Callable]] = None
|
32
|
+
overridden_tool_vars: dict[str, str] = Field(default_factory=dict, description="overridden tool variables")
|
33
|
+
|
30
34
|
@abc.abstractmethod
|
31
35
|
def __str__(self):
|
32
36
|
raise NotImplementedError
|
33
37
|
|
38
|
+
def add_postprocessing(self, postprocessing: Callable):
|
39
|
+
if self.postprocessings is None:
|
40
|
+
self.postprocessings = [postprocessing]
|
41
|
+
else:
|
42
|
+
self.postprocessings.append(postprocessing)
|
43
|
+
|
44
|
+
def __or__(self, other: Callable):
|
45
|
+
self.add_postprocessing(other)
|
46
|
+
return self
|
47
|
+
|
48
|
+
def with_postprocessings(self, postprocessings: list[Callable]):
|
49
|
+
if self.postprocessings is None:
|
50
|
+
self.postprocessings = postprocessings
|
51
|
+
else:
|
52
|
+
self.postprocessings.extend(postprocessings)
|
53
|
+
return self
|
54
|
+
|
55
|
+
def override_tool_variables(self, override_vars: dict[str, str]) -> 'ToolRequest':
|
56
|
+
self.overridden_tool_vars = override_vars
|
57
|
+
return self
|
58
|
+
|
34
59
|
|
35
60
|
class Tool(BaseModel, abc.ABC):
|
36
61
|
"""
|
@@ -40,6 +65,11 @@ class Tool(BaseModel, abc.ABC):
|
|
40
65
|
description: str = Field(description="tool description")
|
41
66
|
argument_json_schema: Optional[dict] = Field(default=None, description="tool argument json schema")
|
42
67
|
auth: Optional[ToolAuth] = Field(default=None, description="authentication information to invoke tool")
|
68
|
+
postprocessings: Optional[list[Callable]] = Field(default=None,
|
69
|
+
description="postprocessing functions after tool is invoked")
|
70
|
+
default_tool_vars: dict[str, str] = Field(default_factory=dict, description="default tool variables")
|
71
|
+
overridden_tool_vars: dict[str, str] = Field(default_factory=dict, description="overridden tool variables")
|
72
|
+
use_profile: bool = False
|
43
73
|
|
44
74
|
@abc.abstractmethod
|
45
75
|
def invoke(self, **kwargs) -> str:
|
@@ -54,12 +84,26 @@ class Tool(BaseModel, abc.ABC):
|
|
54
84
|
"""
|
55
85
|
raise NotImplementedError()
|
56
86
|
|
57
|
-
def schema_model(self) -> Optional[Type[BaseModel]]:
|
87
|
+
def schema_model(self, use_profile: bool = False) -> Optional[Type[BaseModel]]:
|
58
88
|
"""
|
59
89
|
Returns a schema_model that wraps the existing argument_json_schema
|
60
90
|
to include profile and thread_id as arguments when the tool is invoked
|
61
91
|
"""
|
62
|
-
return self._get_schema_model(self.name, self.argument_json_schema)
|
92
|
+
return self._get_schema_model(self.name, self.argument_json_schema, use_profile=use_profile)
|
93
|
+
|
94
|
+
def get_description(self, use_profile: bool = False) -> str:
|
95
|
+
if use_profile:
|
96
|
+
return pocket_extended_tool_description(self.description)
|
97
|
+
else:
|
98
|
+
return self.description
|
99
|
+
|
100
|
+
def override_tool_variables(self, override_vars: dict[str, str]) -> 'Tool':
|
101
|
+
self.overridden_tool_vars = override_vars
|
102
|
+
return self
|
103
|
+
|
104
|
+
@property
|
105
|
+
def tool_vars(self) -> dict[str, str]:
|
106
|
+
return self.default_tool_vars | self.overridden_tool_vars
|
63
107
|
|
64
108
|
@classmethod
|
65
109
|
def from_tool_request(cls, tool_req: ToolRequest, **kwargs) -> 'Tool':
|
@@ -69,38 +113,66 @@ class Tool(BaseModel, abc.ABC):
|
|
69
113
|
raise ValueError('Unknown tool request type')
|
70
114
|
|
71
115
|
@classmethod
|
72
|
-
def _get_schema_model(cls, name: str, json_schema: Optional[dict]) -> Optional[Type[BaseModel]]:
|
116
|
+
def _get_schema_model(cls, name: str, json_schema: Optional[dict], use_profile: bool) -> Optional[Type[BaseModel]]:
|
73
117
|
try:
|
74
118
|
if not json_schema:
|
75
119
|
pocket_logger.info(f"{name} tool's json_schema is none.")
|
76
120
|
return None
|
77
121
|
if 'description' not in json_schema:
|
78
|
-
json_schema['description'] =
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
'
|
84
|
-
|
85
|
-
'
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
'
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
122
|
+
json_schema['description'] = 'The argument of the tool.'
|
123
|
+
|
124
|
+
if use_profile:
|
125
|
+
json_schema = {
|
126
|
+
'title': name,
|
127
|
+
'type': 'object',
|
128
|
+
'properties': {
|
129
|
+
'thread_id': {
|
130
|
+
'type': 'string',
|
131
|
+
'default': 'default',
|
132
|
+
'description': 'The ID of the chat thread where the tool is invoked. Omitted when unknown.',
|
133
|
+
},
|
134
|
+
'profile': {
|
135
|
+
'type': 'string',
|
136
|
+
'default': 'default',
|
137
|
+
'description': '''The profile of the user invoking the tool. Inferred from user's messages.
|
138
|
+
Users can request tools to be invoked in specific personas, which is called a profile.
|
139
|
+
If the user's profile name can be inferred from the query, pass it as a string in the 'profile'
|
140
|
+
JSON property. Omitted when unknown.''',
|
141
|
+
},
|
142
|
+
'body': json_schema
|
95
143
|
},
|
96
|
-
'
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
model = json_schema_to_model(extended_schema, name)
|
144
|
+
'required': [
|
145
|
+
'body',
|
146
|
+
]
|
147
|
+
}
|
148
|
+
|
149
|
+
model = json_schema_to_model(json_schema, name)
|
103
150
|
return model
|
104
151
|
except Exception as e:
|
105
152
|
pocket_logger.warning(f"failed to get tool({name}) schema model. error : {e}")
|
106
153
|
pass
|
154
|
+
|
155
|
+
def with_postprocessing(self, postprocessing: Callable):
|
156
|
+
"""
|
157
|
+
Add a postprocessing function to the tool
|
158
|
+
"""
|
159
|
+
if self.postprocessings is None:
|
160
|
+
self.postprocessings = [postprocessing]
|
161
|
+
else:
|
162
|
+
self.postprocessings.append(postprocessing)
|
163
|
+
return self
|
164
|
+
|
165
|
+
def __or__(self, other: Callable):
|
166
|
+
self.with_postprocessing(other)
|
167
|
+
return self
|
168
|
+
|
169
|
+
def with_postprocessings(self, postprocessings: list[Callable]):
|
170
|
+
"""
|
171
|
+
Add a list of postprocessing functions to the tool
|
172
|
+
Returns the tool itself
|
173
|
+
"""
|
174
|
+
if self.postprocessings is None:
|
175
|
+
self.postprocessings = postprocessings
|
176
|
+
else:
|
177
|
+
self.postprocessings.extend(postprocessings)
|
178
|
+
return self
|
hyperpocket/tool/wasm/README.md
CHANGED
@@ -52,7 +52,6 @@ WasmToolRequest offer those build method:
|
|
52
52
|
|
53
53
|
- `from_local`
|
54
54
|
- `from_git`
|
55
|
-
- `from_github`
|
56
55
|
|
57
56
|
Each `WasmToolRequest` has it's own `Lock` object
|
58
57
|
|
@@ -98,6 +97,29 @@ classDiagram
|
|
98
97
|
Lockfile o-- Pocket : 1..1
|
99
98
|
```
|
100
99
|
|
100
|
+
### Inject tool variables
|
101
|
+
|
102
|
+
If the user specifies [tool_vars] in the `config.toml` of the tool's repository, which is allowed to be injected dynamically when the user develops an agent, it can be injected through the following steps.
|
103
|
+
|
104
|
+
```toml
|
105
|
+
# config.toml of a tool
|
106
|
+
|
107
|
+
[tool_vars]
|
108
|
+
config1 = "config1"
|
109
|
+
config2 = "config2"
|
110
|
+
```
|
111
|
+
|
112
|
+
1. Injecting tool_vars when importing tool in code.
|
113
|
+
|
114
|
+
```python
|
115
|
+
from_git('https://github.com/your-organization/your-repository/tree/main',tool_vars = {
|
116
|
+
"config1": "modified_config1"
|
117
|
+
})
|
118
|
+
```
|
119
|
+
|
120
|
+
2. Injecting envvars by settings.toml
|
121
|
+
If there are remaining envvars, Hyperpocket checks the `settings.toml` from the agent code directory.
|
122
|
+
|
101
123
|
## WasmTool
|
102
124
|
|
103
125
|
```python
|
@@ -112,11 +134,11 @@ class WasmTool(Tool):
|
|
112
134
|
|
113
135
|
- `_invoker`: A class for executing WASM.
|
114
136
|
- `pkg_lock`: The lock class for the tool.
|
115
|
-
|
137
|
+
- Used for determining the package path where the current WASM code is stored.
|
116
138
|
- `rel_path`: The relative path to the location of the WASM code within the package.
|
117
139
|
- `runtime`: The runtime language of the WASM code.
|
118
140
|
- `json_schema`: The JSON schema for the WASM tool.
|
119
|
-
|
141
|
+
- Information read from schema.json.
|
120
142
|
- `readme`: The README information.
|
121
143
|
|
122
144
|
## WasmInvoker
|
@@ -134,11 +156,11 @@ sequenceDiagram
|
|
134
156
|
participant WasmTool as WasmTool (Includes Server)
|
135
157
|
participant Browser as Browser (Executes WASM Runtime)
|
136
158
|
|
137
|
-
|
159
|
+
|
138
160
|
WasmTool->>WasmTool: Render HTML and Store in Memory
|
139
161
|
WasmTool->>Browser: Launch Browser
|
140
162
|
Browser->>WasmTool: Request Rendered HTML
|
141
163
|
WasmTool->>Browser: Provide Rendered HTML
|
142
164
|
Browser->>Browser: Execute Injected WASM Runtime Script
|
143
165
|
Browser->>WasmTool: Return Execution Result
|
144
|
-
```
|
166
|
+
```
|
hyperpocket/tool/wasm/browser.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import asyncio
|
2
|
+
import os
|
2
3
|
|
3
4
|
from playwright.async_api import async_playwright, Page, Playwright, BrowserContext, Route
|
4
5
|
|
@@ -13,15 +14,9 @@ class InvokerBrowser(object):
|
|
13
14
|
raise RuntimeError("Use InvokerBrowser.get_instance() instead")
|
14
15
|
|
15
16
|
async def _async_init(self):
|
16
|
-
# false only in dev
|
17
|
-
# TODO(moon.dev) : load from config by environment
|
18
|
-
import os
|
19
|
-
pocket_env = os.getenv("POCKET_ENV", "DEVELOPMENT")
|
20
|
-
is_headless = False if pocket_env == "DEVELOPMENT" else True
|
21
|
-
|
22
17
|
self.playwright = await async_playwright().start()
|
23
18
|
self.browser_context = await self.playwright.chromium.launch_persistent_context(
|
24
|
-
headless=
|
19
|
+
headless=True,
|
25
20
|
args=[
|
26
21
|
'--disable-web-security=True',
|
27
22
|
],
|
hyperpocket/tool/wasm/script.py
CHANGED
@@ -3,8 +3,11 @@ import enum
|
|
3
3
|
import pathlib
|
4
4
|
from typing import Optional
|
5
5
|
|
6
|
+
import toml
|
6
7
|
from pydantic import BaseModel
|
7
8
|
|
9
|
+
from hyperpocket.config import pocket_logger
|
10
|
+
|
8
11
|
|
9
12
|
class ScriptRuntime(enum.Enum):
|
10
13
|
Node = "node"
|
@@ -62,7 +65,43 @@ class Script(BaseModel):
|
|
62
65
|
encoded_str = encoded_bytes.decode()
|
63
66
|
file_tree = ScriptFileNode.merge(file_tree, ScriptFileNode.create_file_tree(p, encoded_str))
|
64
67
|
return file_tree
|
65
|
-
|
68
|
+
|
69
|
+
@property
|
70
|
+
def package_name(self) -> Optional[str]:
|
71
|
+
if self.runtime != ScriptRuntime.Python:
|
72
|
+
return
|
73
|
+
pyproject = toml.load(pathlib.Path(self.tool_path) / "pyproject.toml")
|
74
|
+
if "project" in pyproject:
|
75
|
+
name = pyproject["project"]["name"]
|
76
|
+
if "tool" in pyproject and "poetry" in pyproject["tool"]:
|
77
|
+
name = pyproject["tool"]["poetry"]["name"]
|
78
|
+
if not name:
|
79
|
+
raise ValueError("Could not find package name")
|
80
|
+
return name.replace("-", "_")
|
81
|
+
|
82
|
+
@property
|
83
|
+
def entrypoint(self) -> str:
|
84
|
+
pocket_logger.info(self.tool_path)
|
85
|
+
if self.runtime == ScriptRuntime.Node:
|
86
|
+
return "dist/index.js"
|
87
|
+
elif self.runtime == ScriptRuntime.Wasm:
|
88
|
+
return "dist/main.wasm"
|
89
|
+
pyproject = toml.load(pathlib.Path(self.tool_path) / "pyproject.toml")
|
90
|
+
version = None
|
91
|
+
if "project" in pyproject:
|
92
|
+
version = pyproject["project"]["version"]
|
93
|
+
elif "tool" in pyproject and "poetry" in pyproject["tool"]:
|
94
|
+
version = pyproject["tool"]["poetry"]["version"]
|
95
|
+
else:
|
96
|
+
raise ValueError("Could not find package version")
|
97
|
+
wheel_name = f"{self.package_name}-{version}-py3-none-any.whl"
|
98
|
+
wheel_path = pathlib.Path(self.tool_path) / "dist" / wheel_name
|
99
|
+
if not wheel_path.exists():
|
100
|
+
raise ValueError(f"Wheel file {wheel_path} does not exist")
|
101
|
+
return wheel_name
|
102
|
+
|
103
|
+
def dist_file_path(self, file_name: str) -> str:
|
104
|
+
return str(pathlib.Path(self.tool_path) / "dist" / file_name)
|
66
105
|
|
67
106
|
class _ScriptStore(object):
|
68
107
|
scripts: dict[str, Script] = {}
|
@@ -16,27 +16,24 @@ python_template = '''
|
|
16
16
|
scriptID: `{{ SCRIPT_ID }}`
|
17
17
|
}
|
18
18
|
}
|
19
|
-
async function
|
19
|
+
async function _main() {
|
20
|
+
// load the script configs
|
20
21
|
loadConfig();
|
21
|
-
const b64FilesResp = await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/file_tree`);
|
22
|
-
const b64Files = await b64FilesResp.json();
|
23
|
-
const code = atob(b64Files.tree["main.py"].file.contents);
|
24
|
-
const requirements = atob(b64Files.tree["requirements.txt"].file.contents);
|
25
22
|
|
23
|
+
// get entrypoint wheel
|
24
|
+
const entrypointResp = await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/entrypoint`);
|
25
|
+
const { package_name: packageName, entrypoint } = await entrypointResp.json();
|
26
|
+
|
27
|
+
// initialize pyodide
|
26
28
|
const pyodide = await loadPyodide({
|
27
29
|
env: JSON.parse(globalThis.toolConfigs.envs),
|
28
30
|
});
|
29
31
|
await pyodide.loadPackage("micropip");
|
30
32
|
await pyodide.loadPackage("ssl");
|
31
33
|
const micropip = pyodide.pyimport("micropip");
|
34
|
+
await micropip.install(entrypoint);
|
32
35
|
await micropip.install("pyodide-http")
|
33
|
-
|
34
|
-
if (req) {
|
35
|
-
const pkg = req.split("==")[0];
|
36
|
-
await micropip.install(pkg);
|
37
|
-
}
|
38
|
-
});
|
39
|
-
await Promise.all(installation);
|
36
|
+
|
40
37
|
let emitted = false;
|
41
38
|
const decodedBytes = atob(globalThis.toolConfigs.body);
|
42
39
|
pyodide.setStdin({
|
@@ -50,14 +47,19 @@ python_template = '''
|
|
50
47
|
autoEOF: true,
|
51
48
|
})
|
52
49
|
let stdout = "";
|
50
|
+
let stderr = "";
|
53
51
|
pyodide.setStdout({
|
54
52
|
batched: (x) => { stdout += x; },
|
55
53
|
})
|
54
|
+
pyodide.setStderr({
|
55
|
+
batched: (x) => { stderr += x; },
|
56
|
+
})
|
56
57
|
await pyodide.runPythonAsync(`
|
57
58
|
import pyodide_http
|
58
59
|
pyodide_http.patch_all()
|
59
60
|
|
60
|
-
${
|
61
|
+
import ${packageName}
|
62
|
+
${packageName}.main()
|
61
63
|
`);
|
62
64
|
console.log(stdout)
|
63
65
|
await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/done`, {
|
@@ -65,9 +67,25 @@ ${code}
|
|
65
67
|
headers: {
|
66
68
|
'Content-Type': 'application/json'
|
67
69
|
},
|
68
|
-
body: JSON.stringify({ stdout })
|
70
|
+
body: JSON.stringify({ stdout, stderr })
|
69
71
|
});
|
70
72
|
}
|
73
|
+
|
74
|
+
async function main() {
|
75
|
+
try {
|
76
|
+
await _main();
|
77
|
+
} catch (e) {
|
78
|
+
console.error(e);
|
79
|
+
await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/done`, {
|
80
|
+
method: 'POST',
|
81
|
+
headers: {
|
82
|
+
'Content-Type': 'application/json'
|
83
|
+
},
|
84
|
+
body: JSON.stringify({ error: e.message })
|
85
|
+
});
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
71
89
|
main();
|
72
90
|
</script>
|
73
91
|
</body>
|