backend_api.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. from __future__ import annotations
  2. import json
  3. import flask
  4. import os
  5. import logging
  6. import asyncio
  7. import shutil
  8. from flask import Flask, Response, request, jsonify
  9. from typing import Generator
  10. from pathlib import Path
  11. from urllib.parse import quote_plus
  12. from hashlib import sha256
  13. from werkzeug.utils import secure_filename
  14. from ...image import is_allowed_extension, to_image
  15. from ...client.service import convert_to_provider
  16. from ...providers.asyncio import to_sync_generator
  17. from ...client.helper import filter_markdown
  18. from ...tools.files import supports_filename, get_streaming, get_bucket_dir, get_buckets
  19. from ...tools.run_tools import iter_run_tools
  20. from ...errors import ProviderNotFoundError
  21. from ...cookies import get_cookies_dir
  22. from ... import ChatCompletion
  23. from .api import Api
  24. logger = logging.getLogger(__name__)
  25. def safe_iter_generator(generator: Generator) -> Generator:
  26. start = next(generator)
  27. def iter_generator():
  28. yield start
  29. yield from generator
  30. return iter_generator()
  31. class Backend_Api(Api):
  32. """
  33. Handles various endpoints in a Flask application for backend operations.
  34. This class provides methods to interact with models, providers, and to handle
  35. various functionalities like conversations, error handling, and version management.
  36. Attributes:
  37. app (Flask): A Flask application instance.
  38. routes (dict): A dictionary mapping API endpoints to their respective handlers.
  39. """
  40. def __init__(self, app: Flask) -> None:
  41. """
  42. Initialize the backend API with the given Flask application.
  43. Args:
  44. app (Flask): Flask application instance to attach routes to.
  45. """
  46. self.app: Flask = app
  47. def jsonify_models(**kwargs):
  48. response = self.get_models(**kwargs)
  49. if isinstance(response, list):
  50. return jsonify(response)
  51. return response
  52. def jsonify_provider_models(**kwargs):
  53. response = self.get_provider_models(**kwargs)
  54. if isinstance(response, list):
  55. return jsonify(response)
  56. return response
  57. def jsonify_providers(**kwargs):
  58. response = self.get_providers(**kwargs)
  59. if isinstance(response, list):
  60. return jsonify(response)
  61. return response
  62. self.routes = {
  63. '/backend-api/v2/models': {
  64. 'function': jsonify_models,
  65. 'methods': ['GET']
  66. },
  67. '/backend-api/v2/models/<provider>': {
  68. 'function': jsonify_provider_models,
  69. 'methods': ['GET']
  70. },
  71. '/backend-api/v2/providers': {
  72. 'function': jsonify_providers,
  73. 'methods': ['GET']
  74. },
  75. '/backend-api/v2/version': {
  76. 'function': self.get_version,
  77. 'methods': ['GET']
  78. },
  79. '/backend-api/v2/conversation': {
  80. 'function': self.handle_conversation,
  81. 'methods': ['POST']
  82. },
  83. '/backend-api/v2/synthesize/<provider>': {
  84. 'function': self.handle_synthesize,
  85. 'methods': ['GET']
  86. },
  87. '/backend-api/v2/upload_cookies': {
  88. 'function': self.upload_cookies,
  89. 'methods': ['POST']
  90. },
  91. '/images/<path:name>': {
  92. 'function': self.serve_images,
  93. 'methods': ['GET']
  94. }
  95. }
  96. @app.route('/backend-api/v2/create', methods=['GET', 'POST'])
  97. def create():
  98. try:
  99. tool_calls = [{
  100. "function": {
  101. "name": "bucket_tool"
  102. },
  103. "type": "function"
  104. }]
  105. web_search = request.args.get("web_search")
  106. if web_search:
  107. tool_calls.append({
  108. "function": {
  109. "name": "search_tool",
  110. "arguments": {"query": web_search, "instructions": "", "max_words": 1000} if web_search != "true" else {}
  111. },
  112. "type": "function"
  113. })
  114. do_filter_markdown = request.args.get("filter_markdown")
  115. cache_id = request.args.get('cache')
  116. parameters = {
  117. "model": request.args.get("model"),
  118. "messages": [{"role": "user", "content": request.args.get("prompt")}],
  119. "provider": request.args.get("provider", None),
  120. "stream": not do_filter_markdown and not cache_id,
  121. "ignore_stream": not request.args.get("stream"),
  122. "tool_calls": tool_calls,
  123. }
  124. if cache_id:
  125. cache_id = sha256(cache_id.encode() + json.dumps(parameters, sort_keys=True).encode()).hexdigest()
  126. cache_dir = Path(get_cookies_dir()) / ".scrape_cache" / "create"
  127. cache_file = cache_dir / f"{quote_plus(request.args.get('prompt').strip()[:20])}.{cache_id}.txt"
  128. if cache_file.exists():
  129. with cache_file.open("r") as f:
  130. response = f.read()
  131. else:
  132. response = iter_run_tools(ChatCompletion.create, **parameters)
  133. cache_dir.mkdir(parents=True, exist_ok=True)
  134. with cache_file.open("w") as f:
  135. f.write(response)
  136. else:
  137. response = iter_run_tools(ChatCompletion.create, **parameters)
  138. if do_filter_markdown:
  139. return Response(filter_markdown(response, do_filter_markdown), mimetype='text/plain')
  140. def cast_str():
  141. for chunk in response:
  142. yield str(chunk)
  143. return Response(cast_str(), mimetype='text/plain')
  144. except Exception as e:
  145. logger.exception(e)
  146. return jsonify({"error": {"message": f"{type(e).__name__}: {e}"}}), 500
  147. @app.route('/backend-api/v2/buckets', methods=['GET'])
  148. def list_buckets():
  149. try:
  150. buckets = get_buckets()
  151. if buckets is None:
  152. return jsonify({"error": {"message": "Error accessing bucket directory"}}), 500
  153. sanitized_buckets = [secure_filename(b) for b in buckets]
  154. return jsonify(sanitized_buckets), 200
  155. except Exception as e:
  156. return jsonify({"error": {"message": str(e)}}), 500
  157. @app.route('/backend-api/v2/files/<bucket_id>', methods=['GET', 'DELETE'])
  158. def manage_files(bucket_id: str):
  159. bucket_id = secure_filename(bucket_id)
  160. bucket_dir = get_bucket_dir(bucket_id)
  161. if not os.path.isdir(bucket_dir):
  162. return jsonify({"error": {"message": "Bucket directory not found"}}), 404
  163. if request.method == 'DELETE':
  164. try:
  165. shutil.rmtree(bucket_dir)
  166. return jsonify({"message": "Bucket deleted successfully"}), 200
  167. except OSError as e:
  168. return jsonify({"error": {"message": f"Error deleting bucket: {str(e)}"}}), 500
  169. except Exception as e:
  170. return jsonify({"error": {"message": str(e)}}), 500
  171. delete_files = request.args.get('delete_files', True)
  172. refine_chunks_with_spacy = request.args.get('refine_chunks_with_spacy', False)
  173. event_stream = 'text/event-stream' in request.headers.get('Accept', '')
  174. mimetype = "text/event-stream" if event_stream else "text/plain";
  175. return Response(get_streaming(bucket_dir, delete_files, refine_chunks_with_spacy, event_stream), mimetype=mimetype)
  176. @self.app.route('/backend-api/v2/files/<bucket_id>', methods=['POST'])
  177. def upload_files(bucket_id: str):
  178. bucket_id = secure_filename(bucket_id)
  179. bucket_dir = get_bucket_dir(bucket_id)
  180. os.makedirs(bucket_dir, exist_ok=True)
  181. filenames = []
  182. for file in request.files.getlist('files[]'):
  183. try:
  184. filename = secure_filename(file.filename)
  185. if supports_filename(filename):
  186. with open(os.path.join(bucket_dir, filename), 'wb') as f:
  187. shutil.copyfileobj(file.stream, f)
  188. filenames.append(filename)
  189. finally:
  190. file.stream.close()
  191. with open(os.path.join(bucket_dir, "files.txt"), 'w') as f:
  192. [f.write(f"{filename}\n") for filename in filenames]
  193. return {"bucket_id": bucket_id, "files": filenames}
  194. @app.route('/backend-api/v2/files/<bucket_id>/<filename>', methods=['PUT'])
  195. def upload_file(bucket_id, filename):
  196. bucket_id = secure_filename(bucket_id)
  197. bucket_dir = get_bucket_dir(bucket_id)
  198. filename = secure_filename(filename)
  199. bucket_path = Path(bucket_dir)
  200. if not supports_filename(filename):
  201. return jsonify({"error": {"message": f"File type not allowed"}}), 400
  202. if not bucket_path.exists():
  203. bucket_path.mkdir(parents=True, exist_ok=True)
  204. try:
  205. file_path = bucket_path / filename
  206. file_data = request.get_data()
  207. if not file_data:
  208. return jsonify({"error": {"message": "No file data received"}}), 400
  209. with file_path.open('wb') as f:
  210. f.write(file_data)
  211. return jsonify({"message": f"File '{filename}' uploaded successfully to bucket '{bucket_id}'"}), 201
  212. except Exception as e:
  213. return jsonify({"error": {"message": f"Error uploading file: {str(e)}"}}), 500
  214. def upload_cookies(self):
  215. file = None
  216. if "file" in request.files:
  217. file = request.files['file']
  218. if file.filename == '':
  219. return 'No selected file', 400
  220. if file and file.filename.endswith(".json") or file.filename.endswith(".har"):
  221. filename = secure_filename(file.filename)
  222. file.save(os.path.join(get_cookies_dir(), filename))
  223. return "File saved", 200
  224. return 'Not supported file', 400
  225. def handle_conversation(self):
  226. """
  227. Handles conversation requests and streams responses back.
  228. Returns:
  229. Response: A Flask response object for streaming.
  230. """
  231. kwargs = {}
  232. if "files[]" in request.files:
  233. images = []
  234. for file in request.files.getlist('files[]'):
  235. if file.filename != '' and is_allowed_extension(file.filename):
  236. images.append((to_image(file.stream, file.filename.endswith('.svg')), file.filename))
  237. kwargs['images'] = images
  238. if "json" in request.form:
  239. json_data = json.loads(request.form['json'])
  240. else:
  241. json_data = request.json
  242. kwargs = self._prepare_conversation_kwargs(json_data, kwargs)
  243. return self.app.response_class(
  244. self._create_response_stream(
  245. kwargs,
  246. json_data.get("conversation_id"),
  247. json_data.get("provider"),
  248. json_data.get("download_images", True),
  249. ),
  250. mimetype='text/event-stream'
  251. )
  252. def handle_synthesize(self, provider: str):
  253. try:
  254. provider_handler = convert_to_provider(provider)
  255. except ProviderNotFoundError:
  256. return "Provider not found", 404
  257. if not hasattr(provider_handler, "synthesize"):
  258. return "Provider doesn't support synthesize", 500
  259. response_data = provider_handler.synthesize({**request.args})
  260. if asyncio.iscoroutinefunction(provider_handler.synthesize):
  261. response_data = asyncio.run(response_data)
  262. else:
  263. if hasattr(response_data, "__aiter__"):
  264. response_data = to_sync_generator(response_data)
  265. response_data = safe_iter_generator(response_data)
  266. content_type = getattr(provider_handler, "synthesize_content_type", "application/octet-stream")
  267. response = flask.Response(response_data, content_type=content_type)
  268. response.headers['Cache-Control'] = "max-age=604800"
  269. return response
  270. def get_provider_models(self, provider: str):
  271. api_key = request.headers.get("x_api_key")
  272. api_base = request.headers.get("x_api_base")
  273. models = super().get_provider_models(provider, api_key, api_base)
  274. if models is None:
  275. return "Provider not found", 404
  276. return models
  277. def _format_json(self, response_type: str, content) -> str:
  278. """
  279. Formats and returns a JSON response.
  280. Args:
  281. response_type (str): The type of the response.
  282. content: The content to be included in the response.
  283. Returns:
  284. str: A JSON formatted string.
  285. """
  286. return json.dumps(super()._format_json(response_type, content)) + "\n"