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
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,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,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.")
|