From 7893a0835e2c3f4e06c1ccfaec9baef1bdacea7d Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Wed, 1 Jan 2025 04:20:02 +0100 Subject: Add filessupport, scrape and refine your data Remove Webdriver usages Add continue messages for other providers --- g4f/gui/server/backend_api.py | 264 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 g4f/gui/server/backend_api.py (limited to 'g4f/gui/server/backend_api.py') diff --git a/g4f/gui/server/backend_api.py b/g4f/gui/server/backend_api.py new file mode 100644 index 00000000..07505424 --- /dev/null +++ b/g4f/gui/server/backend_api.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +import json +import flask +import os +import logging +import asyncio +import shutil +from flask import Flask, Response, request, jsonify +from typing import Generator +from pathlib import Path +from werkzeug.utils import secure_filename + +from ...image import is_allowed_extension, to_image +from ...client.service import convert_to_provider +from ...providers.asyncio import to_sync_generator +from ...tools.files import supports_filename, get_streaming, get_bucket_dir, get_buckets +from ...errors import ProviderNotFoundError +from ...cookies import get_cookies_dir +from .api import Api + +logger = logging.getLogger(__name__) + +def safe_iter_generator(generator: Generator) -> Generator: + start = next(generator) + def iter_generator(): + yield start + yield from generator + return iter_generator() + +class Backend_Api(Api): + """ + Handles various endpoints in a Flask application for backend operations. + + This class provides methods to interact with models, providers, and to handle + various functionalities like conversations, error handling, and version management. + + Attributes: + app (Flask): A Flask application instance. + routes (dict): A dictionary mapping API endpoints to their respective handlers. + """ + def __init__(self, app: Flask) -> None: + """ + Initialize the backend API with the given Flask application. + + Args: + app (Flask): Flask application instance to attach routes to. + """ + self.app: Flask = app + + def jsonify_models(**kwargs): + response = self.get_models(**kwargs) + if isinstance(response, list): + return jsonify(response) + return response + + def jsonify_provider_models(**kwargs): + response = self.get_provider_models(**kwargs) + if isinstance(response, list): + return jsonify(response) + return response + + def jsonify_providers(**kwargs): + response = self.get_providers(**kwargs) + if isinstance(response, list): + return jsonify(response) + return response + + self.routes = { + '/backend-api/v2/models': { + 'function': jsonify_models, + 'methods': ['GET'] + }, + '/backend-api/v2/models/': { + 'function': jsonify_provider_models, + 'methods': ['GET'] + }, + '/backend-api/v2/providers': { + 'function': jsonify_providers, + 'methods': ['GET'] + }, + '/backend-api/v2/version': { + 'function': self.get_version, + 'methods': ['GET'] + }, + '/backend-api/v2/conversation': { + 'function': self.handle_conversation, + 'methods': ['POST'] + }, + '/backend-api/v2/synthesize/': { + 'function': self.handle_synthesize, + 'methods': ['GET'] + }, + '/backend-api/v2/upload_cookies': { + 'function': self.upload_cookies, + 'methods': ['POST'] + }, + '/images/': { + 'function': self.serve_images, + 'methods': ['GET'] + } + } + + @app.route('/backend-api/v2/buckets', methods=['GET']) + def list_buckets(): + try: + buckets = get_buckets() + if buckets is None: + return jsonify({"error": {"message": "Error accessing bucket directory"}}), 500 + sanitized_buckets = [secure_filename(b) for b in buckets] + return jsonify(sanitized_buckets), 200 + except Exception as e: + return jsonify({"error": {"message": str(e)}}), 500 + + @app.route('/backend-api/v2/files/', methods=['GET', 'DELETE']) + def manage_files(bucket_id: str): + bucket_id = secure_filename(bucket_id) + bucket_dir = get_bucket_dir(secure_filename(bucket_id)) + + if not os.path.isdir(bucket_dir): + return jsonify({"error": {"message": "Bucket directory not found"}}), 404 + + if request.method == 'DELETE': + try: + shutil.rmtree(bucket_dir) + return jsonify({"message": "Bucket deleted successfully"}), 200 + except OSError as e: + return jsonify({"error": {"message": f"Error deleting bucket: {str(e)}"}}), 500 + except Exception as e: + return jsonify({"error": {"message": str(e)}}), 500 + + delete_files = request.args.get('delete_files', True) + refine_chunks_with_spacy = request.args.get('refine_chunks_with_spacy', False) + event_stream = 'text/event-stream' in request.headers.get('Accept', '') + mimetype = "text/event-stream" if event_stream else "text/plain"; + return Response(get_streaming(bucket_dir, delete_files, refine_chunks_with_spacy, event_stream), mimetype=mimetype) + + @self.app.route('/backend-api/v2/files/', methods=['POST']) + def upload_files(bucket_id: str): + bucket_id = secure_filename(bucket_id) + bucket_dir = get_bucket_dir(bucket_id) + os.makedirs(bucket_dir, exist_ok=True) + filenames = [] + for file in request.files.getlist('files[]'): + try: + filename = secure_filename(file.filename) + if supports_filename(filename): + with open(os.path.join(bucket_dir, filename), 'wb') as f: + shutil.copyfileobj(file.stream, f) + filenames.append(filename) + finally: + file.stream.close() + with open(os.path.join(bucket_dir, "files.txt"), 'w') as f: + [f.write(f"{filename}\n") for filename in filenames] + return {"bucket_id": bucket_id, "files": filenames} + + @app.route('/backend-api/v2/files//', methods=['PUT']) + def upload_file(bucket_id, filename): + bucket_id = secure_filename(bucket_id) + bucket_dir = get_bucket_dir(bucket_id) + filename = secure_filename(filename) + bucket_path = Path(bucket_dir) + + if not supports_filename(filename): + return jsonify({"error": {"message": f"File type not allowed"}}), 400 + + if not bucket_path.exists(): + bucket_path.mkdir(parents=True, exist_ok=True) + + try: + file_path = bucket_path / filename + file_data = request.get_data() + if not file_data: + return jsonify({"error": {"message": "No file data received"}}), 400 + + with open(str(file_path), 'wb') as f: + f.write(file_data) + + return jsonify({"message": f"File '{filename}' uploaded successfully to bucket '{bucket_id}'"}), 201 + except Exception as e: + return jsonify({"error": {"message": f"Error uploading file: {str(e)}"}}), 500 + + def upload_cookies(self): + file = None + if "file" in request.files: + file = request.files['file'] + if file.filename == '': + return 'No selected file', 400 + if file and file.filename.endswith(".json") or file.filename.endswith(".har"): + filename = secure_filename(file.filename) + file.save(os.path.join(get_cookies_dir(), filename)) + return "File saved", 200 + return 'Not supported file', 400 + + def handle_conversation(self): + """ + Handles conversation requests and streams responses back. + + Returns: + Response: A Flask response object for streaming. + """ + + kwargs = {} + if "files[]" in request.files: + images = [] + for file in request.files.getlist('files[]'): + if file.filename != '' and is_allowed_extension(file.filename): + images.append((to_image(file.stream, file.filename.endswith('.svg')), file.filename)) + kwargs['images'] = images + if "json" in request.form: + json_data = json.loads(request.form['json']) + else: + json_data = request.json + + kwargs = self._prepare_conversation_kwargs(json_data, kwargs) + + return self.app.response_class( + self._create_response_stream( + kwargs, + json_data.get("conversation_id"), + json_data.get("provider"), + json_data.get("download_images", True), + ), + mimetype='text/event-stream' + ) + + def handle_synthesize(self, provider: str): + try: + provider_handler = convert_to_provider(provider) + except ProviderNotFoundError: + return "Provider not found", 404 + if not hasattr(provider_handler, "synthesize"): + return "Provider doesn't support synthesize", 500 + response_data = provider_handler.synthesize({**request.args}) + if asyncio.iscoroutinefunction(provider_handler.synthesize): + response_data = asyncio.run(response_data) + else: + if hasattr(response_data, "__aiter__"): + response_data = to_sync_generator(response_data) + response_data = safe_iter_generator(response_data) + content_type = getattr(provider_handler, "synthesize_content_type", "application/octet-stream") + response = flask.Response(response_data, content_type=content_type) + response.headers['Cache-Control'] = "max-age=604800" + return response + + def get_provider_models(self, provider: str): + api_key = request.headers.get("x_api_key") + models = super().get_provider_models(provider, api_key) + if models is None: + return "Provider not found", 404 + return models + + def _format_json(self, response_type: str, content) -> str: + """ + Formats and returns a JSON response. + + Args: + response_type (str): The type of the response. + content: The content to be included in the response. + + Returns: + str: A JSON formatted string. + """ + return json.dumps(super()._format_json(response_type, content)) + "\n" \ No newline at end of file -- cgit v1.2.3