AiCoderv2 commited on
Commit
9f9394b
·
verified ·
1 Parent(s): 3741606

Deploy Gradio app with multiple files

Browse files
app.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ from src.chat.handler import chat
7
+ from config import DESCRIPTION
8
+ import gradio as gr
9
+
10
+ with gr.Blocks(fill_height=True, fill_width=True) as app:
11
+ with gr.Sidebar():
12
+ gr.HTML(DESCRIPTION)
13
+
14
+ gr.ChatInterface(
15
+ fn=chat,
16
+ chatbot=gr.Chatbot(
17
+ label="SearchGPT | V3",
18
+ type="messages",
19
+ show_copy_button=True,
20
+ scale=1
21
+ ),
22
+ type="messages",
23
+ multimodal=True,
24
+ flagging_mode="manual",
25
+ flagging_dir="/app",
26
+ examples=[
27
+ ["Introduce yourself fully without withholding anything"],
28
+ ["Give me a short introduction to large language model"],
29
+ ["Open this link https://huggingface.co/spaces?sort=trending and check what is currently trending?"],
30
+ ["Find information about UltimaX Intelligence"],
31
+ ["DeepSeek has just released DeepSeek V3.2, can you find out more?"],
32
+ ["Find information for me about SearchGPT by umint and directly compare it with ChatGPT Search and Perplexity"],
33
+ ["Please find information online regarding the current trends for this month"],
34
+ ["Find information related to the dangers of AI addiction, including real-life examples"],
35
+ ["Search for images related to artificial intelligence"],
36
+ [{"text": "Find similar themes online (using web search) as shown in this image",
37
+ "files": ["assets/images/ai-generated.png"]}]
38
+ ],
39
+ cache_examples=False,
40
+ textbox=gr.MultimodalTextbox(
41
+ file_types=["image"],
42
+ placeholder="Ask SearchGPT anything…",
43
+ stop_btn=True
44
+ ),
45
+ show_api=False
46
+ )
47
+
48
+ app.queue(
49
+ max_size=1,
50
+ default_concurrency_limit=1
51
+ ).launch(
52
+ server_name="0.0.0.0",
53
+ pwa=True,
54
+ max_file_size="1mb",
55
+ mcp_server=True
56
+ )
config.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import os
7
+
8
+ ENDPOINT = os.getenv("OPENAI_API_BASE_URL") # /v1/chat/completions
9
+ API_KEY = os.getenv("OPENAI_API_KEY")
10
+ MODEL = "openai/gpt-4o-mini"
11
+ STREAM = True
12
+
13
+ RETRY = 10 # max retries for api request
14
+
15
+ # See the endpoint list at https://searx.space
16
+ # Public instances do not support JSON.
17
+ # You will need to modify the main logic to use HTML instead.
18
+ # Please refer to the SearchGPT 1.0 version for guidance.
19
+ # https://huggingface.co/spaces/umint/searchgpt/blob/0ceb431c97449f214fe952ca356d6f79f0d10983/src/engine/browser_engine.py#L34
20
+ SEARXNG = "https://umint-searxng.hf.space/search"
21
+ FORMAT = "json" # Do not use this when using public instances (doesn't support). See src/tools/workflows/web_search.py#21
22
+
23
+ READER = "https://r.jina.ai/"
24
+
25
+ TIMEOUT = 60 # 1 minute | for tools
26
+
27
+ AIOHTTP = {
28
+ "use_dns_cache": True,
29
+ "ttl_dns_cache": 300,
30
+ "limit": 100,
31
+ "limit_per_host": 30,
32
+ "enable_cleanup_closed": True
33
+ }
34
+
35
+ HEADERS = {
36
+ "User-Agent": (
37
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
38
+ "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
39
+ ),
40
+ "Accept": (
41
+ "text/html,application/xhtml+xml,application/xml;q=0.9,"
42
+ "application/json,image/*,*/*;q=0.8"
43
+ ),
44
+ "Accept-Encoding": "gzip, deflate, br",
45
+ "DNT": "1",
46
+ "Upgrade-Insecure-Requests": "1",
47
+ "Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private",
48
+ "Pragma": "no-cache",
49
+ "Sec-Fetch-Dest": "document",
50
+ "Sec-Fetch-Mode": "navigate",
51
+ "Sec-Fetch-Site": "cross-site",
52
+ "Sec-Fetch-User": "?1"
53
+ }
54
+
55
+ REMINDERS = """
56
+ <system>
57
+
58
+ 1. Collect all URLs, hyperlinks, references, and citations mentioned in the content.
59
+
60
+ 2. Include all the source references or source links or source URLs using HTML format:
61
+ `<a href='source_link' target='_blank'>source_name_title_or_article</a>`.
62
+
63
+ </system>
64
+ """
65
+
66
+ DESCRIPTION = """
67
+ <h2>Hi there,</h2>
68
+ <p>Welcome to <b>SearchGPT</b> V3!</p><br>
69
+ <p>Faster, smarter, and built for a seamless search experience.</p><br>
70
+ <p>Enjoy private and tracker free searching powered by
71
+ <a href="https://umint-searxng.hf.space" target="_blank">SearXNG</a> and GPT-4o Mini.
72
+ </p><br>
73
+ <p>This is a dedicated version separate from the
74
+ <a href="https://umint-openwebui.hf.space" target="_blank">main spaces</a> and designed specifically for public use.
75
+ </p><br>
76
+ <p>Interested in exploring the <b>limited models</b> available in the
77
+ <a href="https://umint-openwebui.hf.space" target="_blank">main spaces</a>?
78
+ </p>
79
+ <p><br>
80
+ <a href="https://huggingface.co/spaces/umint/ai/discussions/55" target="_blank">Click here</a> to discover them now!
81
+ </p><br>
82
+ <p><b>Like this project?</b> Feel free to buy me a
83
+ <a href="https://ko-fi.com/hadad" target="_blank">coffee</a>.
84
+ </p>
85
+ """ # Gradio
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ Pillow
4
+ numpy
src/api/client.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import aiohttp
7
+ import json
8
+ from config import (
9
+ ENDPOINT,
10
+ API_KEY,
11
+ MODEL,
12
+ STREAM,
13
+ AIOHTTP,
14
+ RETRY
15
+ )
16
+ from ..tools.mapping import TOOLS
17
+
18
+ async def client(messages):
19
+ async with aiohttp.ClientSession(
20
+ connector=aiohttp.TCPConnector(**AIOHTTP),
21
+ headers={
22
+ "Content-Type": "application/json",
23
+ "Authorization": f"Bearer {API_KEY}"
24
+ }
25
+ ) as session:
26
+ for attempt in range(RETRY):
27
+ async with session.post(
28
+ ENDPOINT,
29
+ json={
30
+ "model": MODEL,
31
+ "messages": messages,
32
+ "tools": TOOLS,
33
+ "tool_choice": "auto",
34
+ "stream": STREAM
35
+ }
36
+ ) as response:
37
+ if response.status != 200:
38
+ if attempt == RETRY - 1:
39
+ error_message = await response.text()
40
+ raise Exception(f"Error ({response.status}): {error_message}")
41
+ continue
42
+
43
+ buffer = ""
44
+
45
+ async for parts in response.content.iter_any():
46
+ if not parts:
47
+ continue
48
+
49
+ buffer += parts.decode('utf-8')
50
+
51
+ while '\n' in buffer:
52
+ line, buffer = buffer.split('\n', 1)
53
+ data = line.strip()
54
+
55
+ if not data:
56
+ continue
57
+
58
+ if data.startswith("data: "):
59
+ data = data[6:]
60
+
61
+ if data == "[DONE]":
62
+ return
63
+
64
+ if data:
65
+ try:
66
+ chunk = json.loads(data)
67
+ yield chunk
68
+ except json.JSONDecodeError:
69
+ continue
70
+
71
+ return
src/chat/handler.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import json
7
+ from ...config import REMINDERS
8
+ from ..api.client import client
9
+ from ..tools.executor import tool_execution
10
+ from ..utils.time import get_current_time
11
+ from ..media.message_adapter import adapt_message_format
12
+
13
+ async def chat(user_message, history):
14
+ if not user_message or (
15
+ isinstance(user_message, dict) and not (user_message.get("text") or user_message.get("files"))
16
+ ) or (isinstance(user_message, str) and not user_message.strip()):
17
+ yield []
18
+ return
19
+
20
+ messages = []
21
+
22
+ messages.append({"role": "system", "content": f"Today is: {get_current_time()}\n\n{REMINDERS}"})
23
+
24
+ for history_entry in history:
25
+ entry_role = history_entry.get("role")
26
+ entry_content = history_entry.get("content")
27
+
28
+ if entry_role == "user":
29
+ adapted_content = await adapt_message_format(entry_content)
30
+ messages.append({"role": "user", "content": adapted_content})
31
+ elif entry_role == "assistant":
32
+ messages.append({"role": "assistant", "content": entry_content})
33
+
34
+ adapted_user_message = await adapt_message_format(user_message)
35
+
36
+ messages.append({"role": "user", "content": adapted_user_message})
37
+
38
+ normal_response = ""
39
+
40
+ while True:
41
+ tools_mapping = []
42
+ final_response = ""
43
+ finish_reason = None
44
+
45
+ async for chunk in client(messages):
46
+ if chunk.get("choices") and len(chunk["choices"]) > 0:
47
+ choice = chunk["choices"][0]
48
+ delta = choice.get("delta", {})
49
+
50
+ if choice.get("finish_reason"):
51
+ finish_reason = choice["finish_reason"]
52
+
53
+ if delta.get("content") is not None:
54
+ final_response += delta["content"]
55
+ normal_response += delta["content"]
56
+ yield normal_response
57
+
58
+ if delta.get("tool_calls"):
59
+ for tool_delta in delta["tool_calls"]:
60
+ tool_index = tool_delta.get("index", 0)
61
+
62
+ while len(tools_mapping) <= tool_index:
63
+ tools_mapping.append({
64
+ "id": "",
65
+ "type": "function",
66
+ "function": {
67
+ "name": "",
68
+ "arguments": ""
69
+ }
70
+ })
71
+
72
+ if tool_delta.get("id"):
73
+ tools_mapping[tool_index]["id"] = tool_delta["id"]
74
+
75
+ if tool_delta.get("function"):
76
+ if tool_delta["function"].get("name"):
77
+ tools_mapping[tool_index]["function"]["name"] = tool_delta["function"]["name"]
78
+
79
+ if tool_delta["function"].get("arguments"):
80
+ tools_mapping[tool_index]["function"]["arguments"] += tool_delta["function"]["arguments"]
81
+
82
+ if tools_mapping:
83
+ messages.append({
84
+ "role": "assistant",
85
+ "content": final_response if final_response else None,
86
+ "tool_calls": tools_mapping
87
+ })
88
+
89
+ for tool_call in tools_mapping:
90
+ try:
91
+ tool_name = tool_call["function"]["name"]
92
+ tool_args = json.loads(tool_call["function"]["arguments"])
93
+
94
+ tool_result = await tool_execution(tool_name, tool_args)
95
+
96
+ messages.append({
97
+ "role": "tool",
98
+ "tool_call_id": tool_call["id"],
99
+ "content": tool_result
100
+ })
101
+ except Exception as error:
102
+ messages.append({
103
+ "role": "tool",
104
+ "tool_call_id": tool_call["id"],
105
+ "content": f"Error: {str(error)}"
106
+ })
107
+
108
+ continue
109
+
110
+ if final_response:
111
+ messages.append({"role": "assistant", "content": final_response})
112
+ break
113
+
114
+ if finish_reason:
115
+ break
116
+
117
+ yield normal_response
src/media/bytes_loader.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ async def load_file_bytes(file_path):
7
+ with open(file_path, 'rb') as stream:
8
+ return stream.read()
src/media/content_assembler.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ async def assemble_content_parts(text_value, url_collection):
7
+ parts = []
8
+
9
+ if text_value:
10
+ parts.append({
11
+ "type": "text",
12
+ "text": text_value
13
+ })
14
+
15
+ for url_item in url_collection:
16
+ parts.append({
17
+ "type": "image_url",
18
+ "image_url": {
19
+ "url": url_item
20
+ }
21
+ })
22
+
23
+ return parts
src/media/encoding_converter.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import base64
7
+
8
+ async def convert_to_base64(binary_data):
9
+ encoded = base64.b64encode(binary_data)
10
+ return encoded.decode('utf-8')
src/media/filetype_resolver.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import mimetypes
7
+
8
+ async def resolve_filetype(file_path):
9
+ detected_type, _ = mimetypes.guess_type(file_path)
10
+ return detected_type if detected_type else "image/jpeg"
src/media/message_adapter.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ from .bytes_loader import load_file_bytes
7
+ from .encoding_converter import convert_to_base64
8
+ from .filetype_resolver import resolve_filetype
9
+ from .url_composer import compose_data_url
10
+ from .content_assembler import assemble_content_parts
11
+
12
+ async def adapt_message_format(incoming_message):
13
+ if isinstance(incoming_message, str):
14
+ return incoming_message
15
+
16
+ if not isinstance(incoming_message, dict):
17
+ return str(incoming_message)
18
+
19
+ text_value = incoming_message.get("text", "")
20
+ attached_files = incoming_message.get("files", [])
21
+
22
+ if not attached_files:
23
+ return text_value if text_value else ""
24
+
25
+ url_collection = []
26
+
27
+ for file_entry in attached_files:
28
+ file_location = file_entry if isinstance(file_entry, str) else file_entry.get("path")
29
+
30
+ if not file_location:
31
+ continue
32
+
33
+ binary_data = await load_file_bytes(file_location)
34
+ encoded_string = await convert_to_base64(binary_data)
35
+ file_type = await resolve_filetype(file_location)
36
+ url_item = await compose_data_url(encoded_string, file_type)
37
+
38
+ url_collection.append(url_item)
39
+
40
+ if not url_collection:
41
+ return text_value if text_value else ""
42
+
43
+ content_parts = await assemble_content_parts(text_value, url_collection)
44
+
45
+ return content_parts
src/media/url_composer.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ async def compose_data_url(encoded_string, file_type):
7
+ return f"data:{file_type};base64,{encoded_string}"
src/tools/executor.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ from .workflows.open_link import open_link
7
+ from .workflows.web_search import web_search
8
+
9
+ async def tool_execution(tool_name, tool_arguments):
10
+ if tool_name == "open_link":
11
+ return await open_link(tool_arguments["url"])
12
+
13
+ elif tool_name == "web_search":
14
+ return await web_search(tool_arguments["query"])
15
+
16
+ else:
17
+ return f"Unknown tool: {tool_name}"
src/tools/mapping.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ TOOLS = [
7
+ {
8
+ "type": "function",
9
+ "function": {
10
+ "name": "web_search",
11
+ "description": "Search the web using SearXNG and return results",
12
+ "parameters": {
13
+ "type": "object",
14
+ "properties": {
15
+ "query": {
16
+ "type": "string",
17
+ "description": "The search query"
18
+ }
19
+ },
20
+ "required": ["query"]
21
+ }
22
+ }
23
+ },
24
+ {
25
+ "type": "function",
26
+ "function": {
27
+ "name": "open_link",
28
+ "description": "Open a web page using a URL, link, or hyperlink and extract its main content",
29
+ "parameters": {
30
+ "type": "object",
31
+ "properties": {
32
+ "url": {
33
+ "type": "string",
34
+ "description": "The URL, link, or hyperlink of the web page to open and read"
35
+ }
36
+ },
37
+ "required": ["url"]
38
+ }
39
+ }
40
+ }
41
+ ]
src/tools/workflows/open_link.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import aiohttp
7
+ from config import (
8
+ READER,
9
+ TIMEOUT,
10
+ AIOHTTP,
11
+ HEADERS,
12
+ REMINDERS
13
+ )
14
+
15
+ async def open_link(url):
16
+ try:
17
+ async with aiohttp.ClientSession(
18
+ connector=aiohttp.TCPConnector(**AIOHTTP),
19
+ timeout=aiohttp.ClientTimeout(total=TIMEOUT),
20
+ headers=HEADERS
21
+ ) as session:
22
+ async with session.post(READER, data={"url": url}) as response:
23
+ response.raise_for_status()
24
+ content = await response.text()
25
+ return content + "\n\n\n" + REMINDERS
26
+ except Exception as error:
27
+ return f"Error reading URL: {str(error)}"
src/tools/workflows/web_search.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ import aiohttp
7
+ from config import (
8
+ SEARXNG,
9
+ FORMAT,
10
+ TIMEOUT,
11
+ AIOHTTP,
12
+ HEADERS,
13
+ REMINDERS
14
+ )
15
+
16
+ async def web_search(query):
17
+ try:
18
+ async with aiohttp.ClientSession(
19
+ connector=aiohttp.TCPConnector(**AIOHTTP),
20
+ timeout=aiohttp.ClientTimeout(total=TIMEOUT),
21
+ headers=HEADERS
22
+ ) as session:
23
+ async with session.get(f"{SEARXNG}?q={query}&format={FORMAT}") as response:
24
+ response.raise_for_status()
25
+ content = await response.text()
26
+ return content + "\n\n\n" + REMINDERS
27
+ except Exception as error:
28
+ return f"Error during web search: {str(error)}"
src/utils/time.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+
6
+ from datetime import datetime, timezone
7
+
8
+ def get_current_time() -> str:
9
+ return datetime.now(timezone.utc).strftime(
10
+ "%H:%M %Z. %A, %d %B %Y."
11
+ )