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.
Files changed (97) hide show
  1. hyperpocket/auth/README.md +3 -3
  2. hyperpocket/auth/__init__.py +0 -8
  3. hyperpocket/auth/gumloop/context.py +13 -0
  4. hyperpocket/auth/gumloop/token_context.py +15 -0
  5. hyperpocket/auth/gumloop/token_handler.py +66 -0
  6. hyperpocket/auth/gumloop/token_schema.py +8 -0
  7. hyperpocket/auth/linear/token_context.py +1 -1
  8. hyperpocket/auth/notion/README.md +28 -0
  9. hyperpocket/auth/notion/context.py +15 -0
  10. hyperpocket/auth/notion/token_context.py +14 -0
  11. hyperpocket/auth/notion/token_handler.py +65 -0
  12. hyperpocket/auth/notion/token_schema.py +10 -0
  13. hyperpocket/auth/provider.py +8 -5
  14. hyperpocket/auth/reddit/context.py +15 -0
  15. hyperpocket/auth/reddit/oauth2_context.py +32 -0
  16. hyperpocket/auth/reddit/oauth2_handler.py +151 -0
  17. hyperpocket/auth/reddit/oauth2_schema.py +18 -0
  18. hyperpocket/auth/slack/token_context.py +1 -1
  19. hyperpocket/builtin.py +63 -0
  20. hyperpocket/cli/__main__.py +12 -0
  21. hyperpocket/cli/auth.py +83 -0
  22. hyperpocket/cli/codegen/auth/__init__.py +13 -0
  23. hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
  24. hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
  25. hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
  26. hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
  27. hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
  28. hyperpocket/cli/eject.py +19 -0
  29. hyperpocket/cli/sync.py +5 -5
  30. hyperpocket/config/settings.py +14 -12
  31. hyperpocket/futures/futurestore.py +0 -1
  32. hyperpocket/pocket_auth.py +25 -5
  33. hyperpocket/pocket_core.py +264 -0
  34. hyperpocket/pocket_main.py +127 -174
  35. hyperpocket/prompts.py +6 -8
  36. hyperpocket/repository/__init__.py +2 -2
  37. hyperpocket/repository/lock.py +71 -1
  38. hyperpocket/repository/lockfile.py +19 -13
  39. hyperpocket/repository/repository.py +26 -1
  40. hyperpocket/server/auth/__init__.py +0 -6
  41. hyperpocket/server/auth/gumloop.py +16 -0
  42. hyperpocket/server/auth/notion.py +19 -0
  43. hyperpocket/server/auth/reddit.py +16 -0
  44. hyperpocket/server/server.py +56 -20
  45. hyperpocket/server/tool/dto/script.py +15 -2
  46. hyperpocket/server/tool/wasm.py +20 -8
  47. hyperpocket/session/README.md +2 -2
  48. hyperpocket/session/in_memory.py +18 -5
  49. hyperpocket/session/interface.py +14 -0
  50. hyperpocket/session/redis.py +29 -5
  51. hyperpocket/tool/README.md +16 -12
  52. hyperpocket/tool/__init__.py +4 -3
  53. hyperpocket/tool/function/README.md +39 -10
  54. hyperpocket/tool/function/__init__.py +2 -0
  55. hyperpocket/tool/function/annotation.py +2 -1
  56. hyperpocket/tool/function/tool.py +108 -29
  57. hyperpocket/tool/tool.py +100 -28
  58. hyperpocket/tool/wasm/README.md +27 -5
  59. hyperpocket/tool/wasm/browser.py +2 -7
  60. hyperpocket/tool/wasm/script.py +40 -1
  61. hyperpocket/tool/wasm/templates/python.py +32 -14
  62. hyperpocket/tool/wasm/tool.py +21 -18
  63. hyperpocket/tool_like.py +5 -0
  64. hyperpocket/util/__init__.py +1 -1
  65. hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
  66. hyperpocket/util/function_to_model.py +5 -2
  67. hyperpocket/util/json_schema_to_model.py +47 -26
  68. {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/METADATA +107 -88
  69. hyperpocket-0.1.9.dist-info/RECORD +137 -0
  70. {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/WHEEL +1 -1
  71. hyperpocket-0.1.9.dist-info/entry_points.txt +2 -0
  72. hyperpocket/auth/README.KR.md +0 -309
  73. hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
  74. hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
  75. hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
  76. hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
  77. hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
  78. hyperpocket/external/__init__.py +0 -7
  79. hyperpocket/external/github_client.py +0 -19
  80. hyperpocket/session/README.KR.md +0 -62
  81. hyperpocket/session/tests/test_in_memory.py +0 -145
  82. hyperpocket/session/tests/test_redis.py +0 -151
  83. hyperpocket/tests/test_pocket.py +0 -116
  84. hyperpocket/tests/test_pocket_auth.py +0 -982
  85. hyperpocket/tool/README.KR.md +0 -68
  86. hyperpocket/tool/builtins/__init__.py +0 -0
  87. hyperpocket/tool/builtins/example/__init__.py +0 -0
  88. hyperpocket/tool/builtins/example/add_tool.py +0 -18
  89. hyperpocket/tool/function/README.KR.md +0 -159
  90. hyperpocket/tool/tests/test_function_tool.py +0 -266
  91. hyperpocket/tool/wasm/README.KR.md +0 -144
  92. hyperpocket-0.0.3.dist-info/RECORD +0 -130
  93. hyperpocket-0.0.3.dist-info/entry_points.txt +0 -3
  94. /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
  95. /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
  96. /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
  97. /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
@@ -1,6 +1,8 @@
1
+ import asyncio
1
2
  import copy
2
3
  import inspect
3
- from typing import Callable, Awaitable, Optional
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[..., Awaitable[str]]]
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
- return str(self.func(**binding_args))
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.func(**binding_args))
27
- return str(await self.afunc(**binding_args))
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(body=_kwargs["body"])
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.body)
53
+ args = self.model_to_kwargs(model)
39
54
 
40
55
  # binding args
41
56
  binding_args = {}
42
- sig = inspect.signature(self.func)
43
- for param_name, param in sig.parameters.items():
44
- if param_name not in args:
45
- continue
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
- if param.kind == param.VAR_KEYWORD:
48
- # var keyword args should be passed by plain dict
49
- binding_args |= args[param_name]
50
- binding_args |= _kwargs.get("envs", {})
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
- if "envs" in _kwargs:
53
- _kwargs.pop("envs")
72
+ if "envs" in _kwargs:
73
+ _kwargs.pop("envs")
54
74
 
55
- binding_args |= _kwargs # add other kwargs
56
- continue
75
+ binding_args |= _kwargs # add other kwargs
76
+ continue
57
77
 
58
- binding_args[param_name] = args[param_name]
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
- cls,
73
- func: Callable,
74
- afunc: Callable[..., Awaitable[str]] = None,
75
- auth: Optional[ToolAuth] = None
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=model.__doc__,
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 TypeVar, Optional, Type
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'] = f'The argument of the tool.'
79
- extended_schema = {
80
- 'title': name,
81
- 'type': 'object',
82
- 'properties': {
83
- 'thread_id': {
84
- 'type': 'string',
85
- 'default': 'default',
86
- 'description': 'The ID of the chat thread where the tool is invoked. Omitted when unknown.',
87
- },
88
- 'profile': {
89
- 'type': 'string',
90
- 'default': 'default',
91
- 'description': '''The profile of the user invoking the tool. Inferred from user's messages.
92
- Users can request tools to be invoked in specific personas, which is called a profile.
93
- If the user's profile name can be inferred from the query, pass it as a string in the 'profile'
94
- JSON property. Omitted when unknown.''',
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
- 'body': json_schema
97
- },
98
- 'required': [
99
- 'body',
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
@@ -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
- - Used to determine the package path where the current WASM code is stored.
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
- - Information read from schema.json.
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
+ ```
@@ -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=is_headless,
19
+ headless=True,
25
20
  args=[
26
21
  '--disable-web-security=True',
27
22
  ],
@@ -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 main() {
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
- const installation = requirements.split("\\n").map(async (req) => {
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
- ${code}
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>