diff --git a/CodeGen/Dockerfile b/CodeGen/Dockerfile index 5305a9d89f..b2b4155fd7 100644 --- a/CodeGen/Dockerfile +++ b/CodeGen/Dockerfile @@ -1,8 +1,51 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -ARG BASE_TAG=latest -FROM opea/comps-base:$BASE_TAG +# Stage 1: base setup used by other stages +FROM python:3.11-slim AS base + +# get security updates +RUN apt-get update && apt-get upgrade -y && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +ENV HOME=/home/user + +RUN useradd -m -s /bin/bash user && \ + mkdir -p $HOME && \ + chown -R user $HOME + +WORKDIR $HOME + + +# Stage 2: latest GenAIComps sources +FROM base AS git + +RUN apt-get update && apt-get install -y --no-install-recommends git +# RUN git clone --depth 1 https://github.com/opea-project/GenAIComps.git +COPY GenAIComps GenAIComps + + +# Stage 3: common layer shared by services using GenAIComps +FROM base AS comps-base + +# copy just relevant parts +COPY --from=git $HOME/GenAIComps/comps $HOME/GenAIComps/comps +COPY --from=git $HOME/GenAIComps/*.* $HOME/GenAIComps/LICENSE $HOME/GenAIComps/ + +WORKDIR $HOME/GenAIComps +RUN pip install --no-cache-dir --upgrade pip setuptools && \ + pip install --no-cache-dir -r $HOME/GenAIComps/requirements.txt +WORKDIR $HOME + +ENV PYTHONPATH=$PYTHONPATH:$HOME/GenAIComps + +USER user + + +# Stage 4: unique part +FROM comps-base + +ENV LANG=C.UTF-8 COPY ./codegen.py $HOME/codegen.py diff --git a/CodeGen/codegen.py b/CodeGen/codegen.py index 6384efaa47..00521175f0 100644 --- a/CodeGen/codegen.py +++ b/CodeGen/codegen.py @@ -313,4 +313,4 @@ def start(self): if __name__ == "__main__": chatqna = CodeGenService(port=MEGA_SERVICE_PORT) chatqna.add_remote_service() - chatqna.start() + chatqna.start() \ No newline at end of file diff --git a/CodeGen/docker_compose/intel/cpu/xeon/README.md b/CodeGen/docker_compose/intel/cpu/xeon/README.md index 75c7b1851b..fc8b81b45f 100644 --- a/CodeGen/docker_compose/intel/cpu/xeon/README.md +++ b/CodeGen/docker_compose/intel/cpu/xeon/README.md @@ -330,4 +330,4 @@ Then run the command `docker images`, you will have the following Docker Images: - `opea/llm-textgen:latest` - `opea/codegen:latest` - `opea/codegen-ui:latest` -- `opea/codegen-react-ui:latest` (optional) +- `opea/codegen-react-ui:latest` (optional) \ No newline at end of file diff --git a/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml b/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml index 3d132d29f9..c932ece069 100644 --- a/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml +++ b/CodeGen/docker_compose/intel/cpu/xeon/compose.yaml @@ -1,6 +1,3 @@ -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 - services: tgi-service: @@ -100,7 +97,7 @@ services: ipc: host restart: always codegen-xeon-ui-server: - image: ${REGISTRY:-opea}/codegen-ui:${TAG:-latest} + image: ${REGISTRY:-opea}/codegen-gradio-ui:${TAG:-latest} container_name: codegen-xeon-ui-server depends_on: - codegen-xeon-backend-server @@ -111,6 +108,9 @@ services: - https_proxy=${https_proxy} - http_proxy=${http_proxy} - BASIC_URL=${BACKEND_SERVICE_ENDPOINT} + - MEGA_SERVICE_PORT=${MEGA_SERVICE_PORT} + - host_ip=${host_ip} + - DATAPREP_ENDPOINT=${DATAPREP_ENDPOINT} ipc: host restart: always redis-vector-db: diff --git a/CodeGen/docker_compose/set_env.sh b/CodeGen/docker_compose/set_env.sh index dd0b97a551..559f00cf2a 100644 --- a/CodeGen/docker_compose/set_env.sh +++ b/CodeGen/docker_compose/set_env.sh @@ -47,5 +47,6 @@ export TEI_EMBEDDING_HOST_IP=${host_ip} export TEI_EMBEDDING_ENDPOINT="http://${host_ip}:${TEI_EMBEDDER_PORT}" export DATAPREP_REDIS_PORT=6007 +export DATAPREP_ENDPOINT="http://${host_ip}:${DATAPREP_REDIS_PORT}/v1/dataprep" export LOGFLAG=false -export MODEL_CACHE="./data" +export MODEL_CACHE="./data" \ No newline at end of file diff --git a/CodeGen/docker_image_build/build.yaml b/CodeGen/docker_image_build/build.yaml index 3275aa71bf..52ca23b109 100644 --- a/CodeGen/docker_image_build/build.yaml +++ b/CodeGen/docker_image_build/build.yaml @@ -23,6 +23,12 @@ services: dockerfile: ./docker/Dockerfile.react extends: codegen image: ${REGISTRY:-opea}/codegen-react-ui:${TAG:-latest} + codegen-gradio-ui: + build: + context: ../ui + dockerfile: ./docker/Dockerfile.gradio + extends: codegen + image: ${REGISTRY:-opea}/codegen-gradio-ui:${TAG:-latest} llm-textgen: build: context: GenAIComps diff --git a/CodeGen/ui/docker/Dockerfile.gradio b/CodeGen/ui/docker/Dockerfile.gradio new file mode 100644 index 0000000000..11a4f4f581 --- /dev/null +++ b/CodeGen/ui/docker/Dockerfile.gradio @@ -0,0 +1,33 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +FROM python:3.11-slim + +ENV LANG=C.UTF-8 + +ARG ARCH="cpu" + +RUN apt-get update -y && apt-get install -y --no-install-recommends --fix-missing \ + build-essential \ + default-jre \ + libgl1-mesa-glx \ + libjemalloc-dev \ + wget + +# Install ffmpeg static build +WORKDIR /root +RUN wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz && \ + mkdir ffmpeg-git-amd64-static && tar -xvf ffmpeg-git-amd64-static.tar.xz -C ffmpeg-git-amd64-static --strip-components 1 && \ + export PATH=/root/ffmpeg-git-amd64-static:$PATH && \ + cp /root/ffmpeg-git-amd64-static/ffmpeg /usr/local/bin/ && \ + cp /root/ffmpeg-git-amd64-static/ffprobe /usr/local/bin/ + +RUN mkdir -p /home/user + +COPY gradio /home/user/gradio + +RUN pip install --no-cache-dir --upgrade pip setuptools && \ +pip install --no-cache-dir -r /home/user/gradio/requirements.txt + +WORKDIR /home/user/gradio +ENTRYPOINT ["python", "codegen_ui_gradio.py"] diff --git a/CodeGen/ui/gradio/README.md b/CodeGen/ui/gradio/README.md new file mode 100644 index 0000000000..9769efb317 --- /dev/null +++ b/CodeGen/ui/gradio/README.md @@ -0,0 +1,65 @@ +# Document Summary + +This project provides a user interface for summarizing documents and text using a Dockerized frontend application. Users can upload files or paste text to generate summaries. + +## Docker + +### Build UI Docker Image + +To build the frontend Docker image, navigate to the `GenAIExamples/DocSum/ui` directory and run the following command: + +```bash +cd GenAIExamples/CodeGen/ui +docker build -t opea/codegen-gradio-ui:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f docker/Dockerfile.gradio . +``` + +This command builds the Docker image with the tag `opea/codegen-gradio-ui:latest`. It also passes the proxy settings as build arguments to ensure that the build process can access the internet if you are behind a corporate firewall. + +### Run UI Docker Image + +To run the frontend Docker image, navigate to the `GenAIExamples/CodeGen/ui/gradio` directory and execute the following commands: + +```bash +cd GenAIExamples/CodeGen/ui/gradio + +ip_address=$(hostname -I | awk '{print $1}') +docker run -d -p 5173:5173 --ipc=host \ + -e http_proxy=$http_proxy \ + -e https_proxy=$https_proxy \ + -e no_proxy=$no_proxy \ + -e BACKEND_SERVICE_ENDPOINT=http://$ip_address:7778/v1/codegen \ + opea/codegen-gradio-ui:latest +``` + +This command runs the Docker container in interactive mode, mapping port 5173 of the host to port 5173 of the container. It also sets several environment variables, including the backend service endpoint, which is required for the frontend to communicate with the backend service. + +### Python + +To run the frontend application directly using Python, navigate to the `GenAIExamples/CodeGen/ui/gradio` directory and run the following command: + +```bash +cd GenAIExamples/CodeGen/ui/gradio +python codegen_ui_gradio.py +``` + +This command starts the frontend application using Python. + +## Additional Information + +### Prerequisites + +Ensure you have Docker installed and running on your system. Also, make sure you have the necessary proxy settings configured if you are behind a corporate firewall. + +### Environment Variables + +- `http_proxy`: Proxy setting for HTTP connections. +- `https_proxy`: Proxy setting for HTTPS connections. +- `no_proxy`: Comma-separated list of hosts that should be excluded from proxying. +- `BACKEND_SERVICE_ENDPOINT`: The endpoint of the backend service that the frontend will communicate with. + +### Troubleshooting + +- Docker Build Issues: If you encounter issues while building the Docker image, ensure that your proxy settings are correctly configured and that you have internet access. +- Docker Run Issues: If the Docker container fails to start, check the environment variables and ensure that the backend service is running and accessible. + +This README file provides detailed instructions and explanations for building and running the Dockerized frontend application, as well as running it directly using Python. It also highlights the key features of the project and provides additional information for troubleshooting and configuring the environment. diff --git a/CodeGen/ui/gradio/codegen_ui_gradio.py b/CodeGen/ui/gradio/codegen_ui_gradio.py new file mode 100644 index 0000000000..873d0c42b4 --- /dev/null +++ b/CodeGen/ui/gradio/codegen_ui_gradio.py @@ -0,0 +1,402 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# This is a Gradio app that includes two tabs: one for code generation and another for resource management. +# The resource management tab has been updated to allow file uploads, deletion, and a table listing all the files. +# Additionally, three small text boxes have been added for managing file dataframe parameters. + +import argparse +import os +from pathlib import Path +import gradio as gr +from gradio_pdf import PDF +import requests +import pandas as pd +import os +import uvicorn +import json +import argparse +# from utils import build_logger, make_temp_image, server_error_msg, split_video +from urllib.parse import urlparse +from pathlib import Path +from fastapi import FastAPI +# from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles + +# logger = build_logger("gradio_web_server", "gradio_web_server.log") +logflag = os.getenv("LOGFLAG", False) + +# create a FastAPI app +app = FastAPI() +cur_dir = os.getcwd() +static_dir = Path(os.path.join(cur_dir, "static/")) +tmp_dir = Path(os.path.join(cur_dir, "split_tmp_videos/")) + +Path(static_dir).mkdir(parents=True, exist_ok=True) +app.mount("/static", StaticFiles(directory=static_dir), name="static") + +tmp_upload_folder = "/tmp/gradio/" + + + +host_ip = os.getenv("host_ip") +DATAPREP_REDIS_PORT = os.getenv("DATAPREP_REDIS_PORT", 6007) +DATAPREP_ENDPOINT = os.getenv("DATAPREP_ENDPOINT", f"http://{host_ip}:{DATAPREP_REDIS_PORT}/v1/dataprep") +MEGA_SERVICE_PORT = os.getenv("MEGA_SERVICE_PORT", 7778) + +backend_service_endpoint = os.getenv( + "BACKEND_SERVICE_ENDPOINT", f"http://{host_ip}:{MEGA_SERVICE_PORT}/v1/codegen" + ) + +dataprep_ingest_endpoint = f"{DATAPREP_ENDPOINT}/ingest" +dataprep_get_files_endpoint = f"{DATAPREP_ENDPOINT}/get" +dataprep_delete_files_endpoint = f"{DATAPREP_ENDPOINT}/delete" +dataprep_get_indices_endpoint = f"{DATAPREP_ENDPOINT}/indices" + + + +# Define the functions that will be used in the app +def conversation_history(prompt, index, use_agent, history): + # Print the language and prompt, and return a placeholder code + print(f"Generating code for prompt: {prompt} using index: {index} and use_agent is {use_agent}") + history.append([prompt, ""]) + response_generator = generate_code(prompt, index, use_agent) + for token in response_generator: + history[-1][-1] += token + yield history + + +def upload_media(media, index=None, chunk_size=1500, chunk_overlap=100): + media = media.strip().split("\n") + print("Files passed is ", media, flush=True) + if not chunk_size: + chunk_size = 1500 + if not chunk_overlap: + chunk_overlap = 100 + + requests = [] + if type(media) is list: + for file in media: + file_ext = os.path.splitext(file)[-1] + if is_valid_url(file): + print(file, " is valid URL") + print("Ingesting URL...") + value = ingest_url(file, index, chunk_size, chunk_overlap) + requests.append(value) + yield value + elif file_ext in ['.pdf', '.txt']: + print("Ingesting File...") + value = ingest_file(file, index, chunk_size, chunk_overlap) + requests.append(value) + yield value + else: + print(file, "File type not supported") + yield ( + gr.Textbox( + visible=True, + value="Your file extension type is not supported.", + ) + ) + return + yield requests + + else: + file_ext = os.path.splitext(media)[-1] + if is_valid_url(media): + value = ingest_url(media, index, chunk_size, chunk_overlap) + yield value + elif file_ext in ['.pdf', '.txt']: + print("Ingesting File...") + value = ingest_file(media, index, chunk_size, chunk_overlap) + # print("Return value is: ", value, flush=True) + yield value + else: + print(media, "File type not supported") + yield ( + gr.Textbox( + visible=True, + value="Your file extension type is not supported.", + ) + ) + return + +def generate_code(query, index=None, use_agent=False): + if index is None or index == "None": + input_dict = {"messages": query, "agents_flag": use_agent} + else: + input_dict = {"messages": query, "index_name": index, "agents_flag": use_agent} + + print("Query is ", input_dict) + headers = {"Content-Type": "application/json"} + + response = requests.post(url=backend_service_endpoint, headers=headers, data=json.dumps(input_dict), stream=True) + + for line in response.iter_lines(): + if line: + line = line.decode('utf-8') + if line.startswith("data: "): # Only process lines starting with "data: " + json_part = line[len("data: "):] # Remove the "data: " prefix + if json_part.strip() == "[DONE]": # Ignore the DONE marker + continue + try: + json_obj = json.loads(json_part) # Convert to dictionary + if "choices" in json_obj: + for choice in json_obj["choices"]: + if "text" in choice: + # Yield each token individually + yield choice["text"] + except json.JSONDecodeError: + print("Error parsing JSON:", json_part) + + +def ingest_file(file, index=None, chunk_size=100, chunk_overlap=150): + headers = { + # "Content-Type: multipart/form-data" + } + file_input = {"files": open(file, "rb")} + + if index: + print("Index is", index) + data = {"index_name": index, "chunk_size": chunk_size, "chunk_overlap": chunk_overlap} + else: + data = {"chunk_size": chunk_size, "chunk_overlap": chunk_overlap} + + print("Calling Request Now!") + response = requests.post(url=dataprep_ingest_endpoint, headers=headers, files=file_input, data=data) + # print("Ingest Files", response) + print(response.text) + + # table = update_table() + return response.text + +def ingest_url(url, index=None, chunk_size=100, chunk_overlap=150): + print("URL is ", url) + url = str(url) + if not is_valid_url(url): + print("Invalid URL") + # yield ( + # gr.Textbox( + # visible=True, + # value="Invalid URL entered. Please enter a valid URL", + # ) + # ) + return + headers = { + # "Content-Type: multipart/form-data" + } + + if index: + url_input = {"link_list": json.dumps([url]), "index_name": index, "chunk_size": chunk_size, "chunk_overlap": chunk_overlap} + else: + url_input = {"link_list": json.dumps([url]), "chunk_size": chunk_size, "chunk_overlap": chunk_overlap} + response = requests.post(url=dataprep_ingest_endpoint, headers=headers, data=url_input) + # print("Ingest URL", response) + # table = update_table() + return response.text + + +def is_valid_url(url): + url = str(url) + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + + +# Initialize the file list +file_list = [] + +# def update_files(file): +# # Add the uploaded file to the file list +# file_list.append(file.name) +# file_df["Files"] = file_list +# return file_df + + +def get_files(index=None): + headers = { + # "Content-Type: multipart/form-data" + } + if index == "All Files": + index = None + + if index: + index = {"index_name": index} + response = requests.post(url=dataprep_get_files_endpoint, headers=headers, data=index) + print("Get files with ", index, response) + table = response.json() + return table + else: + # print("URL IS ", dataprep_get_files_endpoint) + response = requests.post(url=dataprep_get_files_endpoint, headers=headers) + print("Get files ", response) + table = response.json() + return table + +def update_table(index=None): + if index == "All Files": + index = None + files = get_files(index) + print("Files is ", files) + if len(files) == 0: + df = pd.DataFrame(files, columns=["Files"]) + return df + else: + df = pd.DataFrame(files) + return df + +def update_indices(): + indices = get_indices() + df = pd.DataFrame(indices, columns=["File Databases"]) + return df + +def delete_file(file, index=None): + # Remove the selected file from the file list + headers = { + # "Content-Type: application/json" + } + print("URL IS ", dataprep_delete_files_endpoint) + if index: + file_input = {"files": open(file, "rb"), "index_name": index} + else: + file_input = {"files": open(file, "rb")} + response = requests.post(url=dataprep_delete_files_endpoint, headers=headers, data=file_input) + print("Delete file ", response) + table = update_table() + return response.text + +def delete_all_files(index=None): + # Remove all files from the file list + headers = { + # "Content-Type: application/json" + } + response = requests.post(url=dataprep_delete_files_endpoint, headers=headers, data='{"file_path": "all"}') + print("Delete all files ", response) + table = update_table() + + return response.text + +def get_indices(): + headers = { + # "Content-Type: application/json" + } + response = requests.post(url=dataprep_get_indices_endpoint, headers=headers) + print("Get Indices", response) + indices = response.json() + return indices + +def update_indices_dropdown(): + indices = ["None"] + get_indices() + new_dd = gr.update(choices=indices, value="None") + return new_dd + + +def get_file_names(files): + file_str = "" + if not files: + return file_str + + for file in files: + file_str += file + '\n' + file_str.strip() + return file_str + + +# Define UI components +with gr.Blocks() as ui: + with gr.Tab("Code Generation"): + gr.Markdown("### Generate Code from Natural Language") + chatbot = gr.Chatbot(label="Chat History") + prompt_input = gr.Textbox(label="Enter your query") + with gr.Column(): + with gr.Row(scale=8): + # indices = ["None"] + get_indices() + database_dropdown = gr.Dropdown(choices=get_indices(), label="Select Index", value="None") + with gr.Row(scale=1): + db_refresh_button = gr.Button("Refresh", variant="primary") + db_refresh_button.click(update_indices_dropdown, outputs=database_dropdown) + use_agent = gr.Checkbox(label="Use Agent", container=False) + + generate_button = gr.Button("Generate Code") + + # Connect the generate button to the conversation_history function + generate_button.click(conversation_history, inputs=[prompt_input, database_dropdown, use_agent, chatbot], outputs=chatbot) + + with gr.Tab("Resource Management"): + # File management components + # url_button = gr.Button("Process") + with gr.Row(): + with gr.Column(scale=1): + index_name_input = gr.Textbox(label="Index Name") + chunk_size_input = gr.Textbox(label="Chunk Size", value="1500", placeholder="Enter an integer (default: 1500)") + chunk_overlap_input = gr.Textbox(label="Chunk Overlap", value="100", placeholder="Enter an integer (default: 100)") + with gr.Column(scale=3): + file_upload = gr.File(label="Upload Files", file_count="multiple") + url_input = gr.Textbox(label="Media to be ingested (Append URL's in a new line)") + upload_button = gr.Button("Upload", variant="primary") + upload_status = gr.Textbox(label="Upload Status") + file_upload.change(get_file_names, inputs=file_upload, outputs=url_input) + with gr.Column(scale=1): + # table_dropdown = gr.Dropdown(indices) + # file_table = gr.Dataframe(interactive=False, value=update_table()) + file_table = gr.Dataframe(interactive=False, value=update_indices()) + refresh_button = gr.Button("Refresh", variant="primary", size="sm") + refresh_button.click(update_indices, outputs=file_table) + # refresh_button.click(update_indices, outputs=database_dropdown) + # table_dropdown.change(fn=update_table, inputs=table_dropdown, outputs=file_table) + # upload_button.click(upload_media, inputs=[file_upload, index_name_input, chunk_size_input, chunk_overlap_input], outputs=file_table) + upload_button.click(upload_media, inputs=[url_input, index_name_input, chunk_size_input, chunk_overlap_input], outputs=upload_status) + + delete_all_button = gr.Button("Delete All", variant="primary", size="sm") + delete_all_button.click(delete_all_files, outputs=upload_status) + + + + # delete_button = gr.Button("Delete Index") + + # selected_file_output = gr.Textbox(label="Selected File") + # delete_button.click(delete_file, inputs=indices, outputs=upload_status) + + + +ui.queue() +app = gr.mount_gradio_app(app, ui, path="/") +share = False +enable_queue = True + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--host", type=str, default="0.0.0.0") + parser.add_argument("--port", type=int, default=os.getenv("UI_PORT", 5173)) + parser.add_argument("--concurrency-count", type=int, default=20) + parser.add_argument("--share", action="store_true") + + host_ip = os.getenv("host_ip") + DATAPREP_REDIS_PORT = os.getenv("DATAPREP_REDIS_PORT", 6007) + DATAPREP_ENDPOINT = os.getenv("DATAPREP_ENDPOINT", f"http://{host_ip}:{DATAPREP_REDIS_PORT}/v1/dataprep") + MEGA_SERVICE_PORT = os.getenv("MEGA_SERVICE_PORT", 7778) + + + backend_service_endpoint = os.getenv( + "BACKEND_SERVICE_ENDPOINT", f"http://{host_ip}:{MEGA_SERVICE_PORT}/v1/codegen" + ) + + # dataprep_ingest_endpoint = f"{DATAPREP_ENDPOINT}/ingest" + # dataprep_get_files_endpoint = f"{DATAPREP_ENDPOINT}/get" + # dataprep_delete_files_endpoint = f"{DATAPREP_ENDPOINT}/delete" + # dataprep_get_indices_endpoint = f"{DATAPREP_ENDPOINT}/indices" + + + args = parser.parse_args() + # logger.info(f"args: {args}") + global gateway_addr + gateway_addr = backend_service_endpoint + global dataprep_ingest_addr + dataprep_ingest_addr = dataprep_ingest_endpoint + global dataprep_get_files_addr + dataprep_get_files_addr = dataprep_get_files_endpoint + + + uvicorn.run(app, host=args.host, port=args.port) diff --git a/CodeGen/ui/gradio/requirements.txt b/CodeGen/ui/gradio/requirements.txt new file mode 100644 index 0000000000..2a4c8e1a30 --- /dev/null +++ b/CodeGen/ui/gradio/requirements.txt @@ -0,0 +1,4 @@ +gradio==5.22.0 +numpy==1.26.4 +opencv-python==4.10.0.82 +Pillow==10.3.0