Spaces:
Runtime error
Runtime error
Deploy Gradio app with multiple files
Browse files- app.py +56 -0
- config.py +85 -0
- requirements.txt +4 -0
- src/api/client.py +71 -0
- src/chat/handler.py +117 -0
- src/media/bytes_loader.py +8 -0
- src/media/content_assembler.py +23 -0
- src/media/encoding_converter.py +10 -0
- src/media/filetype_resolver.py +10 -0
- src/media/message_adapter.py +45 -0
- src/media/url_composer.py +7 -0
- src/tools/executor.py +17 -0
- src/tools/mapping.py +41 -0
- src/tools/workflows/open_link.py +27 -0
- src/tools/workflows/web_search.py +28 -0
- src/utils/time.py +11 -0
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 |
+
)
|