GraziePrego commited on
Commit
2ea6210
·
verified ·
1 Parent(s): 4936760

Upload helpers/api.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. helpers/api.py +255 -3
helpers/api.py CHANGED
@@ -1,3 +1,255 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:77eca0607a35b5d1676f4846ab2722113de96d4eb748d5d43d08abf17a3202c2
3
- size 8244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import abstractmethod
2
+ import json
3
+ import threading
4
+ from functools import wraps
5
+ from pathlib import Path
6
+ from typing import Union, Dict, Any
7
+ from flask import (
8
+ Request,
9
+ Response,
10
+ jsonify,
11
+ Flask,
12
+ session,
13
+ request,
14
+ send_file,
15
+ redirect,
16
+ url_for,
17
+ )
18
+ from werkzeug.wrappers.response import Response as BaseResponse
19
+ from agent import AgentContext
20
+ from helpers.print_style import PrintStyle
21
+ from helpers.errors import format_error
22
+ from helpers import files, cache
23
+
24
+ ThreadLockType = Union[threading.Lock, threading.RLock]
25
+
26
+ CACHE_AREA = "api_handlers(api)"
27
+ # cache.toggle_area(CACHE_AREA, False) # cache off for now
28
+
29
+ Input = dict
30
+ Output = Union[Dict[str, Any], Response]
31
+
32
+
33
+ class ApiHandler:
34
+ def __init__(self, app: Flask, thread_lock: ThreadLockType):
35
+ self.app = app
36
+ self.thread_lock = thread_lock
37
+
38
+ @classmethod
39
+ def requires_loopback(cls) -> bool:
40
+ return False
41
+
42
+ @classmethod
43
+ def requires_api_key(cls) -> bool:
44
+ return False
45
+
46
+ @classmethod
47
+ def requires_auth(cls) -> bool:
48
+ return True
49
+
50
+ @classmethod
51
+ def get_methods(cls) -> list[str]:
52
+ return ["POST"]
53
+
54
+ @classmethod
55
+ def requires_csrf(cls) -> bool:
56
+ return cls.requires_auth()
57
+
58
+ @abstractmethod
59
+ async def process(self, input: Input, request: Request) -> Output:
60
+ pass
61
+
62
+ async def handle_request(self, request: Request) -> Response:
63
+ try:
64
+ # input data from request based on type
65
+ input_data: Input = {}
66
+ if request.is_json:
67
+ try:
68
+ if request.data: # Check if there's any data
69
+ input_data = request.get_json()
70
+ # If empty or not valid JSON, use empty dict
71
+ except Exception as e:
72
+ # Just log the error and continue with empty input
73
+ PrintStyle().print(f"Error parsing JSON: {str(e)}")
74
+ input_data = {}
75
+ else:
76
+ # input_data = {"data": request.get_data(as_text=True)}
77
+ input_data = {}
78
+
79
+ # process via handler
80
+ output = await self.process(input_data, request)
81
+
82
+ # return output based on type
83
+ if isinstance(output, Response):
84
+ return output
85
+ else:
86
+ response_json = json.dumps(output)
87
+ return Response(
88
+ response=response_json, status=200, mimetype="application/json"
89
+ )
90
+
91
+ # return exceptions with 500
92
+ except Exception as e:
93
+ error = format_error(e)
94
+ PrintStyle.error(f"API error: {error}")
95
+ return Response(response=error, status=500, mimetype="text/plain")
96
+
97
+ # get context to run agent zero in
98
+ def use_context(self, ctxid: str, create_if_not_exists: bool = True):
99
+ from helpers.context_utils import use_context as _use_context
100
+ return _use_context(self.thread_lock, ctxid, create_if_not_exists)
101
+
102
+
103
+ from helpers.network import is_loopback_address
104
+
105
+
106
+ def requires_api_key(f):
107
+ @wraps(f)
108
+ async def decorated(*args, **kwargs):
109
+ from helpers.settings import get_settings
110
+
111
+ valid_api_key = get_settings()["mcp_server_token"]
112
+
113
+ if api_key := request.headers.get("X-API-KEY"):
114
+ if api_key != valid_api_key:
115
+ return Response("Invalid API key", 401)
116
+ elif request.json and request.json.get("api_key"):
117
+ api_key = request.json.get("api_key")
118
+ if api_key != valid_api_key:
119
+ return Response("Invalid API key", 401)
120
+ else:
121
+ return Response("API key required", 401)
122
+ return await f(*args, **kwargs)
123
+
124
+ return decorated
125
+
126
+
127
+ def requires_loopback(f):
128
+ @wraps(f)
129
+ async def decorated(*args, **kwargs):
130
+ if not is_loopback_address(str(request.remote_addr)):
131
+ return Response("Access denied.", 403, {})
132
+ return await f(*args, **kwargs)
133
+
134
+ return decorated
135
+
136
+
137
+ def requires_auth(f):
138
+ @wraps(f)
139
+ async def decorated(*args, **kwargs):
140
+ from helpers import login
141
+
142
+ user_pass_hash = login.get_credentials_hash()
143
+ if not user_pass_hash:
144
+ return await f(*args, **kwargs)
145
+ if session.get("authentication") != user_pass_hash:
146
+ return redirect(url_for("login_handler"))
147
+ return await f(*args, **kwargs)
148
+
149
+ return decorated
150
+
151
+
152
+ def csrf_protect(f):
153
+ @wraps(f)
154
+ async def decorated(*args, **kwargs):
155
+ from helpers import runtime
156
+
157
+ token = session.get("csrf_token")
158
+ header = request.headers.get("X-CSRF-Token")
159
+ cookie = request.cookies.get("csrf_token_" + runtime.get_runtime_id())
160
+ sent = header or cookie
161
+ if not token or not sent or token != sent:
162
+ return Response("CSRF token missing or invalid", 403)
163
+ return await f(*args, **kwargs)
164
+
165
+ return decorated
166
+
167
+
168
+ def register_api_route(app: Flask, lock: ThreadLockType) -> None:
169
+ from helpers.modules import load_classes_from_file
170
+ from helpers import plugins
171
+
172
+ async def _dispatch(path: str) -> BaseResponse:
173
+ # Return cached wrapped handler if available
174
+ cached = cache.get(CACHE_AREA, path)
175
+ if cached is not None:
176
+ return await cached()
177
+
178
+ # Resolve file path for the handler
179
+ # Try built-in api folder first, then plugin api folders
180
+ handler_cls: type[ApiHandler] | None = None
181
+
182
+ # Check built-in python/api/<path>.py
183
+ builtin_file = files.get_abs_path(f"api/{path}.py")
184
+ if files.is_in_dir(builtin_file, files.get_abs_path("api")) and files.exists(
185
+ builtin_file
186
+ ):
187
+ classes = load_classes_from_file(builtin_file, ApiHandler)
188
+ if classes:
189
+ handler_cls = classes[0]
190
+
191
+ # Check plugin api folders: path format plugins/<plugin_name>/<handler>
192
+ if handler_cls is None and path.startswith("plugins/"):
193
+ parts = path.split("/", 2)
194
+ if len(parts) == 3:
195
+ _, plugin_name, handler_name = parts
196
+ plugin_dir = plugins.find_plugin_dir(plugin_name)
197
+ if plugin_dir:
198
+ plugin_file = Path(plugin_dir) / "api" / f"{handler_name}.py"
199
+ if plugin_file.is_file():
200
+ classes = load_classes_from_file(str(plugin_file), ApiHandler)
201
+ if classes:
202
+ handler_cls = classes[0]
203
+
204
+ if handler_cls is None:
205
+ return Response(f"API endpoint not found: {path}", 404)
206
+
207
+ # Check method is allowed
208
+ if request.method not in handler_cls.get_methods():
209
+ return Response(f"Method {request.method} not allowed for: {path}", 405)
210
+
211
+ # Build handler call, wrapping with security decorators as required
212
+ async def call_handler() -> BaseResponse:
213
+ instance = handler_cls(app, lock)
214
+ return await instance.handle_request(request=request)
215
+
216
+ handler_fn = call_handler
217
+ if handler_cls.requires_csrf():
218
+ handler_fn = csrf_protect(handler_fn)
219
+ if handler_cls.requires_api_key():
220
+ handler_fn = requires_api_key(handler_fn)
221
+ if handler_cls.requires_auth():
222
+ handler_fn = requires_auth(handler_fn)
223
+ if handler_cls.requires_loopback():
224
+ handler_fn = requires_loopback(handler_fn)
225
+
226
+ cache.add(CACHE_AREA, path, handler_fn)
227
+ return await handler_fn()
228
+
229
+ app.add_url_rule(
230
+ "/api/<path:path>",
231
+ "api_dispatch",
232
+ _dispatch,
233
+ methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
234
+ )
235
+
236
+
237
+ def register_watchdogs():
238
+ from helpers import watchdog
239
+ from helpers.ws import CACHE_AREA as WS_CACHE_AREA
240
+
241
+
242
+ def on_api_change(items: list[watchdog.WatchItem]):
243
+ PrintStyle.debug("API endpoint watchdog triggered:", items)
244
+ cache.clear(CACHE_AREA)
245
+ cache.clear(WS_CACHE_AREA)
246
+
247
+ watchdog.add_watchdog(
248
+ "api_handlers",
249
+ roots=[
250
+ files.get_abs_path(files.API_DIR),
251
+ files.get_abs_path(files.USER_DIR, files.API_DIR),
252
+ ],
253
+ patterns=["*.py"],
254
+ handler=on_api_change,
255
+ )