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
hyperpocket/prompts.py ADDED
@@ -0,0 +1,15 @@
1
+ def pocket_extended_tool_description(description: str):
2
+ return f'''
3
+ This tool performs as stated in the <tool-description></tool-description> XML tag.
4
+ <tool-description>
5
+ {description}
6
+ </tool-description>
7
+
8
+ This tool requires arguments as follows:
9
+ - 'thread_id': The ID of the chat thread where the tool is invoked. Omitted when unknown.
10
+ - 'profile': The profile of the user invoking the tool. Inferred from user's messages.
11
+ Users can request tools to be invoked in specific personas, which is called a profile.
12
+ If the user's profile name can be inferred from the query, pass it as a string in the 'profile' JSON property.
13
+ Omitted when unknown.
14
+ - 'body': The argument of the tool. The argument is passed as JSON property 'body' in the JSON schema.
15
+ '''
@@ -0,0 +1,5 @@
1
+ from hyperpocket.repository.lock import Lock
2
+ from hyperpocket.repository.lockfile import Lockfile
3
+ from hyperpocket.repository.repository import pull, sync
4
+
5
+ __all__ = ["Lock", "Lockfile", "pull", "sync"]
@@ -0,0 +1,156 @@
1
+ import abc
2
+ import pathlib
3
+ import shutil
4
+ from typing import Optional
5
+
6
+ import git
7
+ from pydantic import BaseModel, Field
8
+
9
+ from hyperpocket.config import settings, pocket_logger
10
+
11
+
12
+ class Lock(BaseModel, abc.ABC):
13
+ tool_source: str = None
14
+
15
+ @abc.abstractmethod
16
+ def __str__(self):
17
+ raise NotImplementedError
18
+
19
+ @abc.abstractmethod
20
+ def key(self) -> tuple[str, ...]:
21
+ raise NotImplementedError
22
+
23
+ @abc.abstractmethod
24
+ def sync(self, **kwargs):
25
+ raise NotImplementedError
26
+
27
+ @abc.abstractmethod
28
+ def toolpkg_path(self) -> pathlib.Path:
29
+ raise NotImplementedError
30
+
31
+
32
+ class LocalLock(Lock):
33
+ tool_source: str = Field(default='local')
34
+ tool_path: str
35
+
36
+ def __init__(self, tool_path: str):
37
+ super().__init__(tool_source="local", tool_path=str(pathlib.Path(tool_path).resolve()))
38
+
39
+ def __str__(self):
40
+ return f"local\t{self.tool_path}"
41
+
42
+ def key(self):
43
+ return self.tool_source, self.tool_path.rstrip('/')
44
+
45
+ def sync(self, **kwargs):
46
+ pocket_logger.info(f"Syncing path: {self.tool_path} ...")
47
+ pkg_path = self.toolpkg_path()
48
+ if pkg_path.exists():
49
+ shutil.rmtree(pkg_path)
50
+ shutil.copytree(self.tool_path, pkg_path)
51
+
52
+ def toolpkg_path(self) -> pathlib.Path:
53
+ pocket_pkgs = settings.toolpkg_path
54
+ return pocket_pkgs / 'local' / self.tool_path[1:]
55
+
56
+
57
+ class GitLock(Lock):
58
+ tool_source: str = 'git'
59
+ repository_url: str
60
+ git_ref: str
61
+ ref_sha: Optional[str] = None
62
+
63
+ def __str__(self):
64
+ return f"git\t{self.repository_url}\t{self.git_ref}\t{self.ref_sha}"
65
+
66
+ def key(self):
67
+ return self.tool_source, self.repository_url.rstrip('/'), self.git_ref
68
+
69
+ def toolpkg_path(self) -> pathlib.Path:
70
+ if not self.ref_sha:
71
+ raise ValueError("ref_sha is not set")
72
+ cleansed_url = self.repository_url
73
+ if self.repository_url.startswith('http://'):
74
+ cleansed_url = self.repository_url[7:]
75
+ elif self.repository_url.startswith('https://'):
76
+ cleansed_url = self.repository_url[8:]
77
+ elif self.repository_url.startswith('git@'):
78
+ cleansed_url = self.repository_url[4:]
79
+ return settings.toolpkg_path / cleansed_url / self.ref_sha
80
+
81
+ def sync(self, force_update: bool = False, **kwargs):
82
+ """
83
+ Synchronize the local git repository with the target remote branch.
84
+
85
+ 1. Check if the SHA of the target ref in the remote repository matches the current local SHA.
86
+ 2. If they do not match, fetch the target ref from the remote repository and do a hard reset
87
+ to align the local repository with the remote version.
88
+ """
89
+ try:
90
+ pocket_logger.info(f"Syncing git: {self.repository_url} @ ref: {self.git_ref} ...")
91
+
92
+ # get new sha from refs
93
+ new_sha = self._get_new_sha_if_exists_in_remote()
94
+ if new_sha is None:
95
+ raise ValueError(f"Could not find ref {self.git_ref} in {self.repository_url}")
96
+
97
+ # check self.ref_sha should be updated
98
+ if self.ref_sha != new_sha:
99
+ if force_update or self.ref_sha is None:
100
+ self.ref_sha = new_sha
101
+
102
+ # make pkg_version_path dir if not exists
103
+ pkg_version_path = self.toolpkg_path()
104
+ if not pkg_version_path.exists():
105
+ pkg_version_path.mkdir(parents=True)
106
+
107
+ # init git repo in local and set origin url
108
+ repo = git.Repo.init(pkg_version_path)
109
+ try:
110
+ remote = repo.remote('origin')
111
+ remote.set_url(self.repository_url)
112
+ except ValueError:
113
+ remote = repo.create_remote('origin', self.repository_url)
114
+
115
+ # check current local commit include new_sha
116
+ # if not included, fetch and do hard reset
117
+ exist_sha = None
118
+ try:
119
+ exist_sha = repo.head.commit.hexsha
120
+ except ValueError:
121
+ pass
122
+ if exist_sha is None or exist_sha != self.ref_sha:
123
+ remote.fetch(depth=1, refspec=self.ref_sha)
124
+ repo.git.checkout(new_sha)
125
+ repo.git.reset('--hard', new_sha)
126
+ repo.git.clean('-fd')
127
+ except Exception as e:
128
+ pocket_logger.error(f"failed to sync git: {self.repository_url} @ ref: {self.git_ref}. reason : {e}")
129
+ raise e
130
+
131
+ def _get_new_sha_if_exists_in_remote(self):
132
+ """
133
+ get new sha in refs
134
+ First, check remote sha is matched to saved ref_sha
135
+ Second, check remote ref name is matched to saved ref name
136
+ Third, check local ref name is matched to saved ref name
137
+ And last, check tag ref name is matched to saved ref name
138
+ """
139
+ refs = git.cmd.Git().ls_remote(self.repository_url)
140
+
141
+ new_sha = None
142
+ for r in refs.split('\n'):
143
+ sha, ref = r.split('\t')
144
+ if sha == self.ref_sha:
145
+ new_sha = sha
146
+ break
147
+ elif ref == self.git_ref:
148
+ new_sha = sha
149
+ break
150
+ elif ref == f"refs/heads/{self.git_ref}":
151
+ new_sha = sha
152
+ break
153
+ elif ref == f"refs/tags/{self.git_ref}":
154
+ new_sha = sha
155
+ break
156
+ return new_sha
@@ -0,0 +1,56 @@
1
+ import pathlib
2
+ from concurrent.futures.thread import ThreadPoolExecutor
3
+
4
+ from hyperpocket.repository.lock import Lock, LocalLock, GitLock
5
+
6
+
7
+ class Lockfile:
8
+ path: pathlib.Path = None
9
+ locks: dict[tuple, Lock] = None
10
+ referenced_locks: set[tuple] = None
11
+
12
+ def __init__(self, path: pathlib.Path):
13
+ self.path = path
14
+ self.locks = {}
15
+ self.referenced_locks = set()
16
+ if self.path.exists():
17
+ with open(self.path, 'r') as f:
18
+ for line in f:
19
+ split = line.strip().split('\t')
20
+ source = split[0]
21
+ if source == 'local':
22
+ lock = LocalLock(tool_path=split[1])
23
+ elif source == 'git':
24
+ lock = GitLock(
25
+ repository_url=split[1],
26
+ git_ref=split[2],
27
+ ref_sha=split[3],
28
+ )
29
+ else:
30
+ raise ValueError(f"Unknown tool source: {source}")
31
+ self.locks[lock.key()] = lock
32
+ else:
33
+ self.path.touch()
34
+
35
+ def add_lock(self, lock: Lock):
36
+ if lock.key() not in self.locks:
37
+ self.locks[lock.key()] = lock
38
+ self.referenced_locks.add(lock.key())
39
+
40
+ def get_lock(self, key: tuple[str, ...]):
41
+ return self.locks[key]
42
+
43
+ def sync(self, force_update: bool, referenced_only: bool = False):
44
+ if referenced_only:
45
+ locks = [self.get_lock(key) for key in self.referenced_locks]
46
+ else:
47
+ locks = list(self.locks.values())
48
+ with ThreadPoolExecutor(max_workers=min(len(locks), 100), thread_name_prefix="repository_loader") as executor:
49
+ executor.map(lambda l: l.sync(force_update=force_update), locks)
50
+ self.write()
51
+
52
+ def write(self):
53
+ with open(self.path, 'w') as f:
54
+ for lock in self.locks.values():
55
+ f.write(str(lock) + '\n')
56
+
@@ -0,0 +1,18 @@
1
+ import pathlib
2
+
3
+ from hyperpocket.repository.lock import LocalLock, GitLock
4
+ from hyperpocket.repository.lockfile import Lockfile
5
+
6
+
7
+ def pull(lockfile: Lockfile, urllike: str, git_ref: str):
8
+ path = pathlib.Path(urllike)
9
+ if path.exists():
10
+ lockfile.add_lock(LocalLock(tool_path=str(path)))
11
+ else:
12
+ lockfile.add_lock(GitLock(repository_url=urllike, git_ref=git_ref))
13
+ lockfile.sync(force_update=False)
14
+ lockfile.write()
15
+
16
+ def sync(lockfile: Lockfile, force_update: bool):
17
+ lockfile.sync(force_update=force_update)
18
+ lockfile.write()
@@ -0,0 +1,3 @@
1
+ from hyperpocket.server.proxy import add_callback_proxy
2
+
3
+ __all__ = ["add_callback_proxy"]
@@ -0,0 +1,15 @@
1
+ from fastapi import APIRouter
2
+
3
+ from hyperpocket.server.auth.github import github_auth_router
4
+ from hyperpocket.server.auth.google import google_auth_router
5
+ from hyperpocket.server.auth.linear import linear_auth_router
6
+ from hyperpocket.server.auth.slack import slack_auth_router
7
+ from hyperpocket.server.auth.token import token_router
8
+ from hyperpocket.server.auth.calendly import calendly_auth_router
9
+ from hyperpocket.util.get_objects_from_subpackage import get_objects_from_subpackage
10
+
11
+ auth_router = APIRouter(prefix="/auth")
12
+
13
+ routers = get_objects_from_subpackage("hyperpocket.server.auth", APIRouter)
14
+ for r in routers:
15
+ auth_router.include_router(r)
@@ -0,0 +1,16 @@
1
+ from fastapi import APIRouter, Request
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ calendly_auth_router = APIRouter(prefix="/calendly")
7
+
8
+
9
+ @calendly_auth_router.get("/oauth2/callback")
10
+ async def calendly_oauth2_callback(request: Request, state: str, code: str):
11
+ try:
12
+ FutureStore.resolve_future(state, code)
13
+ except ValueError:
14
+ return HTMLResponse(content="failed")
15
+
16
+ return HTMLResponse(content="success")
@@ -0,0 +1,25 @@
1
+ from fastapi import APIRouter, Request
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ github_auth_router = APIRouter(prefix="/github")
7
+
8
+
9
+ @github_auth_router.get("/oauth2/callback")
10
+ async def github_oauth2_callback(request: Request, state: str, code: str):
11
+ try:
12
+ FutureStore.resolve_future(state, code)
13
+ except ValueError:
14
+ return HTMLResponse(content="failed")
15
+
16
+ return HTMLResponse(content="success")
17
+
18
+ @github_auth_router.get("/token/callback")
19
+ async def github_token_callback(request: Request, state: str, token: str):
20
+ try:
21
+ FutureStore.resolve_future(state, token)
22
+ except ValueError:
23
+ return HTMLResponse(content="failed")
24
+
25
+ return HTMLResponse(content="success")
@@ -0,0 +1,16 @@
1
+ from fastapi import APIRouter, Request
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ google_auth_router = APIRouter(prefix="/google")
7
+
8
+
9
+ @google_auth_router.get("/oauth2/callback")
10
+ async def google_oauth2_callback(request: Request, state: str, code: str):
11
+ try:
12
+ FutureStore.resolve_future(state, code)
13
+ except ValueError:
14
+ return HTMLResponse(content="failed")
15
+
16
+ return HTMLResponse(content="success")
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ linear_auth_router = APIRouter(
7
+ prefix="/linear"
8
+ )
9
+
10
+
11
+ @linear_auth_router.get("/token/callback")
12
+ async def slack_token_callback(state: str, token: str):
13
+ try:
14
+ FutureStore.resolve_future(state, token)
15
+ except ValueError:
16
+ return HTMLResponse(content="failed")
17
+
18
+ return HTMLResponse(content="success")
@@ -0,0 +1,28 @@
1
+ from fastapi import APIRouter
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ slack_auth_router = APIRouter(
7
+ prefix="/slack"
8
+ )
9
+
10
+
11
+ @slack_auth_router.get("/oauth2/callback")
12
+ async def slack_oauth2_callback(state: str, code: str):
13
+ try:
14
+ FutureStore.resolve_future(state, code)
15
+ except ValueError:
16
+ return HTMLResponse(content="failed")
17
+
18
+ return HTMLResponse(content="success")
19
+
20
+
21
+ @slack_auth_router.get("/token/callback")
22
+ async def slack_token_callback(state: str, token: str):
23
+ try:
24
+ FutureStore.resolve_future(state, token)
25
+ except ValueError:
26
+ return HTMLResponse(content="failed")
27
+
28
+ return HTMLResponse(content="success")
@@ -0,0 +1,51 @@
1
+ from http import HTTPStatus
2
+ from urllib.parse import urlencode, urlunparse, urlparse, parse_qs
3
+
4
+ from fastapi import APIRouter, Form
5
+ from starlette.responses import HTMLResponse, RedirectResponse
6
+
7
+ token_router = APIRouter()
8
+
9
+
10
+ @token_router.get("/token", response_class=HTMLResponse)
11
+ async def token_form(redirect_uri: str, state: str = ""):
12
+ html = f"""
13
+ <html>
14
+ <body>
15
+ <h2>Enter Token</h2>
16
+ <form action="submit" method="post">
17
+ <input type="hidden" name="redirect_uri" value="{redirect_uri}">
18
+ <input type="hidden" name="state" value="{state}">
19
+
20
+ <label for="user_token">Token:</label>
21
+ <input type="text" id="user_token" name="user_token" required>
22
+
23
+ <button type="submit">submit</button>
24
+ </form>
25
+ </body>
26
+ </html>
27
+ """
28
+ return HTMLResponse(content=html)
29
+
30
+
31
+ @token_router.post("/submit", response_class=RedirectResponse)
32
+ async def submit_token(user_token: str = Form(...), redirect_uri: str = Form(...), state: str = Form(...)):
33
+ new_callback_url = add_query_params(redirect_uri, {"token": user_token, "state": state})
34
+ return RedirectResponse(url=new_callback_url, status_code=HTTPStatus.SEE_OTHER)
35
+
36
+
37
+ def add_query_params(url: str, params: dict):
38
+ url_parts = urlparse(url)
39
+ query_params = parse_qs(url_parts.query)
40
+ query_params.update(params)
41
+ new_query = urlencode(query_params, doseq=True)
42
+
43
+ new_url = urlunparse((
44
+ url_parts.scheme,
45
+ url_parts.netloc,
46
+ url_parts.path,
47
+ url_parts.params,
48
+ new_query,
49
+ url_parts.fragment
50
+ ))
51
+ return new_url
@@ -0,0 +1,63 @@
1
+ import httpx
2
+ from fastapi import FastAPI, Request
3
+ from starlette.responses import HTMLResponse
4
+
5
+ from hyperpocket.config import config, pocket_logger
6
+
7
+
8
+ async def proxy(request: Request, path: str):
9
+ async with httpx.AsyncClient() as client:
10
+ resp = await client.request(
11
+ method=request.method,
12
+ url=f"{config.internal_base_url}/{path}",
13
+ headers=request.headers,
14
+ content=await request.body(),
15
+ params=request.query_params,
16
+ timeout=300,
17
+ )
18
+ return HTMLResponse(content=resp.text, headers=resp.headers, status_code=resp.status_code)
19
+
20
+
21
+ def add_callback_proxy(app: FastAPI):
22
+ app.add_api_route(f"/{config.callback_url_rewrite_prefix}/{{path:path}}", proxy,
23
+ methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
24
+
25
+
26
+ https_proxy_app = None
27
+ if config.enable_local_callback_proxy:
28
+ https_proxy_app = FastAPI()
29
+ add_callback_proxy(https_proxy_app)
30
+
31
+
32
+ def _generate_ssl_certificates(ssl_keypath, ssl_certpath):
33
+ import subprocess
34
+
35
+ pocket_logger.info("generate default ssl file")
36
+
37
+ subj = (
38
+ "/C=US"
39
+ "/ST=California"
40
+ "/L=San Jose"
41
+ "/O=local"
42
+ "/OU=local"
43
+ "/CN=localhost"
44
+ "/emailAddress=local@example.com"
45
+ )
46
+ command = [
47
+ "openssl", "req", "-x509",
48
+ "-newkey", "rsa:4096",
49
+ "-keyout", ssl_keypath,
50
+ "-out", ssl_certpath,
51
+ "-days", "1",
52
+ "-nodes",
53
+ '-subj', subj,
54
+ "-sha256",
55
+ ]
56
+
57
+ try:
58
+ # 명령 실행
59
+ subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
60
+ pocket_logger.info("SSL Certificates generated: callback_server.key, callback_server.crt")
61
+ except subprocess.CalledProcessError as e:
62
+ pocket_logger.warning(f"An error occurred while generating certificates: {e}")
63
+ raise e
@@ -0,0 +1,178 @@
1
+ import asyncio
2
+ import enum
3
+ import uuid
4
+ from typing import Optional
5
+
6
+ import multiprocess as mp
7
+ from fastapi import FastAPI
8
+ from uvicorn import Config, Server
9
+
10
+ from hyperpocket.config import config, pocket_logger
11
+ from hyperpocket.server.auth import auth_router
12
+ from hyperpocket.server.tool import tool_router
13
+
14
+
15
+ class PocketServerOperations(enum.Enum):
16
+ CALL = "call"
17
+ PREPARE_AUTH = "prepare_auth"
18
+ AUTHENTICATE = "authenticate"
19
+ TOOL_CALL = "tool_call"
20
+
21
+
22
+ class PocketServer(object):
23
+ main_server: Server
24
+ internal_server_port: int
25
+ proxy_server: Optional[Server]
26
+ proxy_port: int
27
+ pipe: mp.Pipe
28
+ process: mp.Process
29
+ future_store: dict[str, asyncio.Future]
30
+
31
+ def __init__(self,
32
+ internal_server_port: int = config.internal_server_port,
33
+ proxy_port: int = config.public_server_port):
34
+ self.internal_server_port = internal_server_port
35
+ self.proxy_port = proxy_port
36
+ self.future_store = dict()
37
+
38
+ def teardown(self):
39
+ if self.process and self.process.is_alive():
40
+ self.process.terminate()
41
+ self.process.join()
42
+
43
+ async def _run_async(self):
44
+ try:
45
+ await asyncio.gather(
46
+ self.main_server.serve(),
47
+ self.proxy_server.serve() if self.proxy_server is not None else asyncio.sleep(0),
48
+ self.poll_in_child(),
49
+ )
50
+ except Exception as e:
51
+ pocket_logger.warning(f"failed to start pocket server. error : {e}")
52
+
53
+ async def poll_in_child(self):
54
+ loop = asyncio.get_running_loop()
55
+ _, conn = self.pipe
56
+
57
+ async def _acall(_conn, _op, _uid, a, kw):
58
+ result = await self.child_pocket.acall(*a, **kw)
59
+ _conn.send((_op, _uid, result))
60
+
61
+ async def _prepare(_conn, _op, _uid, a, kw):
62
+ result = self.child_pocket.prepare_auth(*a, **kw)
63
+ _conn.send((_op, _uid, result))
64
+
65
+ async def _authenticate(_conn, _op, _uid, a, kw):
66
+ result = await self.child_pocket.authenticate(*a, **kw)
67
+ _conn.send((_op, _uid, result))
68
+
69
+ async def _tool_call(_conn, _op, _uid, a, kw):
70
+ result = await self.child_pocket.tool_call(*a, **kw)
71
+ _conn.send((_op, _uid, result))
72
+
73
+ while True:
74
+ if conn.poll():
75
+ op, uid, args, kwargs = conn.recv()
76
+ if op == PocketServerOperations.CALL.value:
77
+ loop.create_task(_acall(conn, op, uid, args, kwargs))
78
+ elif op == PocketServerOperations.PREPARE_AUTH.value:
79
+ loop.create_task(_prepare(conn, op, uid, args, kwargs))
80
+ elif op == PocketServerOperations.AUTHENTICATE.value:
81
+ loop.create_task(_authenticate(conn, op, uid, args, kwargs))
82
+ elif op == PocketServerOperations.TOOL_CALL.value:
83
+ loop.create_task(_tool_call(conn, op, uid, args, kwargs))
84
+ else:
85
+ raise AttributeError(f"Can't find operations. op:{op}")
86
+ else:
87
+ await asyncio.sleep(0)
88
+
89
+ def send_in_parent(self,
90
+ op: PocketServerOperations,
91
+ args: tuple,
92
+ kwargs: dict):
93
+ conn, _ = self.pipe
94
+ uid = str(uuid.uuid4())
95
+ message = (op.value, uid, args, kwargs)
96
+ future = asyncio.Future()
97
+ self.future_store[uid] = future
98
+ conn.send(message)
99
+ return uid
100
+
101
+ async def poll_in_parent(self):
102
+ conn, _ = self.pipe
103
+ while True:
104
+ if conn.poll():
105
+ op, uid, result = conn.recv()
106
+ future = self.future_store[uid]
107
+ future.set_result(result)
108
+ break
109
+ else:
110
+ await asyncio.sleep(0)
111
+
112
+ async def call_in_subprocess(self,
113
+ op: PocketServerOperations,
114
+ args: tuple,
115
+ kwargs: dict):
116
+ uid = self.send_in_parent(op, args, kwargs)
117
+ loop = asyncio.get_running_loop()
118
+ loop.create_task(self.poll_in_parent())
119
+ return await self.future_store[uid]
120
+
121
+ def run(self, child_pocket):
122
+ self._set_mp_start_method()
123
+
124
+ self.pipe = mp.Pipe()
125
+ self.process = mp.Process(target=self._run, args=(child_pocket,), daemon=True)
126
+ self.process.start()
127
+
128
+ def _run(self, child_pocket):
129
+ self.child_pocket = child_pocket
130
+ self.main_server = self._create_main_server()
131
+ self.proxy_server = self._create_https_proxy_server()
132
+ loop = asyncio.new_event_loop()
133
+ loop.run_until_complete(self._run_async())
134
+ loop.close()
135
+
136
+ def _create_main_server(self) -> Server:
137
+ app = FastAPI()
138
+ _config = Config(app, host="0.0.0.0", port=self.internal_server_port)
139
+ app.include_router(tool_router)
140
+ app.include_router(auth_router)
141
+ app.add_api_route("/health", lambda: {"status": "ok"}, methods=["GET"])
142
+
143
+ app = Server(_config)
144
+ return app
145
+
146
+ def _create_https_proxy_server(self) -> Optional[Server]:
147
+ if not config.enable_local_callback_proxy:
148
+ return None
149
+ from hyperpocket.server.proxy import _generate_ssl_certificates
150
+ from hyperpocket.server.proxy import https_proxy_app
151
+
152
+ from hyperpocket.config.settings import pocket_root
153
+ ssl_keypath = pocket_root / "callback_server.key"
154
+ ssl_certpath = pocket_root / "callback_server.crt"
155
+
156
+ if not ssl_keypath.exists() or not ssl_certpath.exists():
157
+ _generate_ssl_certificates(ssl_keypath, ssl_certpath)
158
+
159
+ _config = Config(https_proxy_app, host="0.0.0.0", port=self.proxy_port, ssl_keyfile=ssl_keypath,
160
+ ssl_certfile=ssl_certpath)
161
+ proxy_server = Server(_config)
162
+ return proxy_server
163
+
164
+ def _set_mp_start_method(self):
165
+ import platform
166
+
167
+ os_name = platform.system()
168
+ if os_name == "Windows":
169
+ mp.set_start_method("spawn", force=True)
170
+ pocket_logger.debug("Process start method set to 'spawn' for Windows.")
171
+ elif os_name == "Darwin": # macOS
172
+ mp.set_start_method("spawn", force=True)
173
+ pocket_logger.debug("Process start method set to 'spawn' for macOS.")
174
+ elif os_name == "Linux":
175
+ mp.set_start_method("fork", force=True)
176
+ pocket_logger.debug("Process start method set to 'fork' for Linux.")
177
+ else:
178
+ pocket_logger.debug(f"Unrecognized OS: {os_name}. Default start method will be used.")