Spaces:
Running
Running
ai: Release J.A.R.V.I.S. Spaces Next-Gen!
Browse files* Implemented Audio Generation capabilities
* Added Image Generation features
* Upgrade Deep Search version 2.0 with no limit access
* Redesigned User Interface for enhanced usability
* Integrated with Gradio Chat Interface for seamless interaction
* Refactored core logic for improved performance and maintainability
- README.md +38 -2
- app.py +12 -10
- assets/bin/ai +0 -125
- assets/bin/install.sh +0 -36
- config.py +13 -69
- docs/API.md +0 -50
- requirements.txt +0 -9
- src/{cores → client}/__init__.py +0 -0
- src/client/chat_handler.py +292 -0
- src/{main → core}/__init__.py +0 -0
- src/core/parameter.py +53 -0
- src/core/server.py +188 -0
- src/core/session.py +12 -0
- src/cores/client.py +0 -161
- src/cores/server.py +0 -101
- src/cores/session.py +0 -93
- src/main/file_extractors.py +0 -393
- src/main/gradio.py +0 -332
- src/tools/__init__.py +0 -0
- src/tools/audio.py +68 -0
- src/tools/deep_search.py +98 -0
- src/tools/image.py +116 -0
- src/ui/__init__.py +0 -0
- src/ui/interface.py +184 -0
- src/ui/reasoning.py +75 -0
- src/utils/__init__.py +0 -0
- src/utils/helper.py +24 -0
- src/utils/ip_generator.py +24 -0
- src/utils/session_mapping.py +63 -0
- src/utils/tools.py +63 -0
README.md
CHANGED
@@ -4,11 +4,47 @@ license: apache-2.0
|
|
4 |
license_link: https://huggingface.co/hadadrjt/JARVIS/blob/main/LICENSE
|
5 |
colorFrom: yellow
|
6 |
colorTo: purple
|
|
|
7 |
sdk: gradio
|
8 |
-
sdk_version: 5.34.
|
9 |
app_file: app.py
|
10 |
pinned: true
|
11 |
short_description: Just a Rather Very Intelligent System
|
12 |
models:
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
license_link: https://huggingface.co/hadadrjt/JARVIS/blob/main/LICENSE
|
5 |
colorFrom: yellow
|
6 |
colorTo: purple
|
7 |
+
emoji: 🌍
|
8 |
sdk: gradio
|
9 |
+
sdk_version: 5.34.2
|
10 |
app_file: app.py
|
11 |
pinned: true
|
12 |
short_description: Just a Rather Very Intelligent System
|
13 |
models:
|
14 |
+
- hadadrjt/JARVIS
|
15 |
+
- agentica-org/DeepCoder-14B-Preview
|
16 |
+
- deepseek-ai/DeepSeek-V3-0324
|
17 |
+
- deepseek-ai/DeepSeek-R1
|
18 |
+
- deepseek-ai/DeepSeek-R1-0528
|
19 |
+
- deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
|
20 |
+
- deepseek-ai/DeepSeek-R1-Distill-Llama-70B
|
21 |
+
- google/gemma-3-1b-it
|
22 |
+
- google/gemma-3-4b-it
|
23 |
+
- google/gemma-3-27b-it
|
24 |
+
- meta-llama/Llama-3.1-8B-Instruct
|
25 |
+
- meta-llama/Llama-3.2-3B-Instruct
|
26 |
+
- meta-llama/Llama-3.3-70B-Instruct
|
27 |
+
- meta-llama/Llama-4-Maverick-17B-128E-Instruct
|
28 |
+
- meta-llama/Llama-4-Scout-17B-16E-Instruct
|
29 |
+
- Qwen/Qwen2.5-VL-3B-Instruct
|
30 |
+
- Qwen/Qwen2.5-VL-32B-Instruct
|
31 |
+
- Qwen/Qwen2.5-VL-72B-Instruct
|
32 |
+
- Qwen/QwQ-32B
|
33 |
+
- Qwen/Qwen3-235B-A22B
|
34 |
+
- mistralai/Devstral-Small-2505
|
35 |
+
- google/gemma-3n-E4B-it-litert-preview
|
36 |
---
|
37 |
+
|
38 |
+
## Credits
|
39 |
+
|
40 |
+
This project expresses sincere gratitude to [Pollinations AI](https://pollinations.ai) for providing audio and image generation services that support the open source community.
|
41 |
+
|
42 |
+
Thanks are extended to [SearXNG](https://paulgo.io), [Baidu](https://www.baidu.com), and [Jina AI](https://r.jina.ai) as valuable sources for data retrieval and processing, which contribute to the deep search functionality developed independently.
|
43 |
+
|
44 |
+
The latest version of Deep Search is entirely inspired by the [OpenWebUI](https://openwebui.com/t/cooksleep/infinite_search) tools script.
|
45 |
+
|
46 |
+
Special appreciation is given to [Hugging Face](https://huggingface.co) for hosting this Space as the primary deployment platform.
|
47 |
+
|
48 |
+
## API
|
49 |
+
|
50 |
+
Efforts are underway to restore API and multi-platform support at the earliest opportunity.
|
app.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1 |
-
#
|
2 |
-
#
|
3 |
-
|
4 |
-
#
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
# The following condition checks if this script is being run as the main program.
|
9 |
-
# If true, it calls the launch_ui function to start the user interface.
|
10 |
-
# This ensures that the UI only launches when this file is executed directly, not when imported as a module.
|
11 |
if __name__ == "__main__":
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Import the 'ui' class or function from the 'interface' module located inside the 'src.ui' package.
|
2 |
+
# This import statement allows us to use the user interface component defined in that module.
|
3 |
+
from src.ui.interface import ui
|
|
|
4 |
|
5 |
+
# This conditional statement checks whether the current script is being run directly (not imported as a module).
|
6 |
+
# If this script is executed as the main program, the code inside this block will run.
|
|
|
|
|
|
|
7 |
if __name__ == "__main__":
|
8 |
+
# Create an instance of the 'ui' class or call the 'ui' function to initialize the user interface application.
|
9 |
+
# This object 'app' will represent the running UI application.
|
10 |
+
app = ui()
|
11 |
+
|
12 |
+
# Call the 'launch' method on the 'app' object to start the user interface.
|
13 |
+
# This typically opens the UI window or begins the event loop, making the application interactive.
|
14 |
+
app.queue(default_concurrency_limit=2).launch(show_api=False)
|
assets/bin/ai
DELETED
@@ -1,125 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
#
|
3 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
4 |
-
# SPDX-License-Identifier: Apache-2.0
|
5 |
-
#
|
6 |
-
|
7 |
-
import sys
|
8 |
-
import re
|
9 |
-
|
10 |
-
from gradio_client import Client
|
11 |
-
from rich.console import Console, Group
|
12 |
-
from rich.markdown import Markdown
|
13 |
-
from rich.syntax import Syntax
|
14 |
-
from rich.live import Live
|
15 |
-
|
16 |
-
# Prepares the display screen to show the final output
|
17 |
-
console = Console()
|
18 |
-
|
19 |
-
# Creates a connection to the server
|
20 |
-
jarvis = Client("hadadrjt/ai")
|
21 |
-
|
22 |
-
# Defines the specific AI model to use for responding
|
23 |
-
# Change to the model you want, see:
|
24 |
-
# https://huggingface.co/spaces/hadadrjt/ai/blob/main/docs/API.md#multi-platform
|
25 |
-
model = "JARVIS: 2.1.3"
|
26 |
-
|
27 |
-
# Reads user-provided input from the command line, if available
|
28 |
-
args = sys.argv[1:]
|
29 |
-
|
30 |
-
# Keeps track of whether deep search mode is activated
|
31 |
-
deep_search = False
|
32 |
-
|
33 |
-
# Checks if the input includes "-d", which turns on deep search
|
34 |
-
if "-d" in args:
|
35 |
-
deep_search = True
|
36 |
-
args.remove("-d")
|
37 |
-
|
38 |
-
# Combines the rest of the input into one complete message
|
39 |
-
# If nothing was typed, it defaults to a basic greeting
|
40 |
-
input = " ".join(args) if args else "Hi!"
|
41 |
-
|
42 |
-
# Sets the AI to the desired version before sending any messages
|
43 |
-
jarvis.predict(new=model, api_name="/change_model")
|
44 |
-
|
45 |
-
# Ensures deep search is only enabled for the correct AI model
|
46 |
-
if deep_search and model != "JARVIS: 2.1.3":
|
47 |
-
deep_search = False
|
48 |
-
|
49 |
-
# Prepares and structures the AI’s response for display
|
50 |
-
def layout(text):
|
51 |
-
# Searches for blocks of code within the full response text
|
52 |
-
code_blocks = list(re.finditer(r"\n\n```(.*?)\n\n(.*?)\n\n```\n\n\n", text, re.DOTALL))
|
53 |
-
segments = [] # Stores parts of the final display, including both text and code
|
54 |
-
last_end = 0 # Tracks where the previous segment ended
|
55 |
-
|
56 |
-
# Loops through each code block found in the AI's response
|
57 |
-
for block in code_blocks:
|
58 |
-
# Collects any normal text before the current code block
|
59 |
-
pre_text = text[last_end:block.start()]
|
60 |
-
if pre_text.strip():
|
61 |
-
# Converts plain text into styled text for easier reading
|
62 |
-
segments.append(Markdown(prepare_markdown(pre_text.strip())))
|
63 |
-
|
64 |
-
# Identifies the programming language used in the code block, if available
|
65 |
-
lang = block.group(1).strip() or "text"
|
66 |
-
|
67 |
-
# Extracts the actual code content
|
68 |
-
code = block.group(2).rstrip()
|
69 |
-
|
70 |
-
# Formats the code with syntax highlighting for clear presentation
|
71 |
-
segments.append(Syntax(code, lang, theme="monokai", line_numbers=False, word_wrap=True))
|
72 |
-
|
73 |
-
# Updates the position tracker to move past the current code block
|
74 |
-
last_end = block.end()
|
75 |
-
|
76 |
-
# Checks for any remaining text after the last code block
|
77 |
-
remaining = text[last_end:]
|
78 |
-
if remaining.strip():
|
79 |
-
# Formats and adds this final portion of text to the display
|
80 |
-
segments.append(Markdown(prepare_markdown(remaining.strip())))
|
81 |
-
|
82 |
-
# Returns a complete set of styled segments ready to be shown
|
83 |
-
return Group(*segments)
|
84 |
-
|
85 |
-
# Adjusts special characters in the text to ensure they display correctly
|
86 |
-
def prepare_markdown(text):
|
87 |
-
return text.replace("•", "*")
|
88 |
-
|
89 |
-
# Displays the AI's response in real time as it’s being received
|
90 |
-
def response(jarvis):
|
91 |
-
buffer = "" # Holds the entire reply as it builds up
|
92 |
-
with Live(console=console, transient=False) as live:
|
93 |
-
# Continuously receives and processes parts of the reply
|
94 |
-
for partial in jarvis:
|
95 |
-
# Extracts the latest version of the message from the AI
|
96 |
-
text = partial[0][0][1]
|
97 |
-
|
98 |
-
# Determines what has changed since the last update
|
99 |
-
if text.startswith(buffer):
|
100 |
-
delta = text[len(buffer):]
|
101 |
-
else:
|
102 |
-
delta = text
|
103 |
-
|
104 |
-
# Updates the full message with any new content
|
105 |
-
buffer = text
|
106 |
-
|
107 |
-
# Refreshes the screen with the most recent version of the reply
|
108 |
-
live.update(layout(buffer))
|
109 |
-
|
110 |
-
# Ensures the final reply is printed once it’s complete
|
111 |
-
console.print()
|
112 |
-
|
113 |
-
# Sends the user's input to the AI and selects the appropriate method
|
114 |
-
if deep_search:
|
115 |
-
# Uses the deeper, slower method for more thoughtful responses
|
116 |
-
jarvis = jarvis.submit(multi={"text": input}, deep_search=True, api_name="/respond_async")
|
117 |
-
else:
|
118 |
-
# Uses the standard method for quicker, more direct replies
|
119 |
-
jarvis = jarvis.submit(multi={"text": input}, api_name="/api")
|
120 |
-
|
121 |
-
# Create a line break before the main AI's response
|
122 |
-
print("")
|
123 |
-
|
124 |
-
# Begins the process of showing the AI's response on the screen
|
125 |
-
response(jarvis)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/bin/install.sh
DELETED
@@ -1,36 +0,0 @@
|
|
1 |
-
#!/bin/sh
|
2 |
-
#
|
3 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
4 |
-
# SPDX-License-Identifier: Apache-2.0
|
5 |
-
#
|
6 |
-
|
7 |
-
echo "Installing required Python packages..."
|
8 |
-
pip install gradio_client rich --upgrade
|
9 |
-
echo "Installation complete."
|
10 |
-
echo ""
|
11 |
-
echo ""
|
12 |
-
echo "Downloading the J.A.R.V.I.S. script..."
|
13 |
-
wget https://huggingface.co/spaces/hadadrjt/ai/raw/main/assets/bin/ai
|
14 |
-
echo "Download complete."
|
15 |
-
echo ""
|
16 |
-
echo ""
|
17 |
-
echo "Setting executable permission..."
|
18 |
-
chmod a+x ai
|
19 |
-
echo "Permission set."
|
20 |
-
echo ""
|
21 |
-
echo ""
|
22 |
-
echo "Removing installer script..."
|
23 |
-
rm install.sh
|
24 |
-
echo "Done."
|
25 |
-
echo ""
|
26 |
-
echo ""
|
27 |
-
echo "To send a regular message:"
|
28 |
-
echo "./ai Your message here"
|
29 |
-
echo ""
|
30 |
-
echo "To use Deep Search mode:"
|
31 |
-
echo "./ai -d Your message here"
|
32 |
-
echo ""
|
33 |
-
echo ""
|
34 |
-
echo "For more details and advanced options, visit:"
|
35 |
-
echo "https://huggingface.co/spaces/hadadrjt/ai/blob/main/docs/API.md#installations"
|
36 |
-
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.py
CHANGED
@@ -3,78 +3,22 @@
|
|
3 |
# SPDX-License-Identifier: Apache-2.0
|
4 |
#
|
5 |
|
6 |
-
import os # Import os module to
|
7 |
-
import json # Import json module to parse JSON strings
|
8 |
|
9 |
-
# Load
|
10 |
-
#
|
11 |
-
|
12 |
-
JARVIS_INIT = json.loads(os.getenv("HELLO", "[]"))
|
13 |
|
14 |
-
#
|
15 |
-
#
|
16 |
-
|
17 |
-
# DEEP_SEARCH_PROVIDER_KEY contains the API key or authentication token required to access the deep search provider
|
18 |
-
DEEP_SEARCH_PROVIDER_KEY = os.getenv('DEEP_SEARCH_PROVIDER_KEY')
|
19 |
-
# DEEP_SEARCH_INSTRUCTIONS may include specific instructions or parameters guiding how deep search queries should be handled
|
20 |
-
DEEP_SEARCH_INSTRUCTIONS = os.getenv("DEEP_SEARCH_INSTRUCTIONS")
|
21 |
|
22 |
-
#
|
23 |
-
#
|
24 |
-
|
25 |
-
# INTERNAL_AI_INSTRUCTIONS contains system instructions used to guide the AI behavior
|
26 |
-
INTERNAL_AI_INSTRUCTIONS = os.getenv("INTERNAL_TRAINING_DATA")
|
27 |
-
|
28 |
-
# System instructions mappings and default instructions loaded from environment variables
|
29 |
-
# SYSTEM_PROMPT_MAPPING is a dictionary mapping instructions keys to their corresponding instructions texts, parsed from JSON
|
30 |
-
SYSTEM_PROMPT_MAPPING = json.loads(os.getenv("SYSTEM_PROMPT_MAPPING", "{}"))
|
31 |
-
# SYSTEM_PROMPT_DEFAULT is the fallback instructions text used when no specific instructions mapping is found
|
32 |
-
SYSTEM_PROMPT_DEFAULT = os.getenv("DEFAULT_SYSTEM")
|
33 |
-
|
34 |
-
# List of available server hosts for connections or operations
|
35 |
-
# This list is parsed from a JSON array string and filtered to exclude any empty or invalid entries
|
36 |
-
LINUX_SERVER_HOSTS = [h for h in json.loads(os.getenv("LINUX_SERVER_HOST", "[]")) if h]
|
37 |
-
|
38 |
-
# List of provider keys associated with servers, used for authentication
|
39 |
-
# The list is parsed from JSON and filtered to remove empty strings
|
40 |
-
LINUX_SERVER_PROVIDER_KEYS = [k for k in json.loads(os.getenv("LINUX_SERVER_PROVIDER_KEY", "[]")) if k]
|
41 |
-
# Set to keep track of provider keys that have been marked or flagged during runtime
|
42 |
-
LINUX_SERVER_PROVIDER_KEYS_MARKED = set()
|
43 |
-
# Dictionary to record the number of attempts made with each provider key
|
44 |
-
LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS = {}
|
45 |
-
|
46 |
-
# Set of server error codes that the system recognizes as critical or requiring special handling
|
47 |
-
# The error codes are read from a comma-separated string, filtered to remove empty entries, converted to integers, and stored in a set
|
48 |
-
LINUX_SERVER_ERRORS = set(map(int, filter(None, os.getenv("LINUX_SERVER_ERROR", "").split(","))))
|
49 |
-
|
50 |
-
# Human-friendly AI types and response messages loaded from environment variables
|
51 |
-
# AI_TYPES maps keys like "AI_TYPE_1" to descriptive names or categories of AI models or behaviors
|
52 |
-
AI_TYPES = {f"AI_TYPE_{i}": os.getenv(f"AI_TYPE_{i}") for i in range(1, 10)}
|
53 |
-
# RESPONSES maps keys like "RESPONSE_1" to predefined response templates or messages used by the AI system
|
54 |
-
RESPONSES = {f"RESPONSE_{i}": os.getenv(f"RESPONSE_{i}") for i in range(1, 11)}
|
55 |
-
|
56 |
-
# Model-related configurations loaded from environment variables
|
57 |
-
# MODEL_MAPPING is a dictionary mapping model keys to their corresponding model names or identifiers, parsed from JSON
|
58 |
-
MODEL_MAPPING = json.loads(os.getenv("MODEL_MAPPING", "{}"))
|
59 |
-
# MODEL_CONFIG contains detailed configuration settings for each model, such as parameters or options, parsed from JSON
|
60 |
-
MODEL_CONFIG = json.loads(os.getenv("MODEL_CONFIG", "{}"))
|
61 |
-
# MODEL_CHOICES is a list of available model names extracted from the values of MODEL_MAPPING, useful for selection menus or validation
|
62 |
-
MODEL_CHOICES = list(MODEL_MAPPING.values())
|
63 |
-
|
64 |
-
# Default model configuration and key used as fallback if no specific model is selected
|
65 |
-
# DEFAULT_CONFIG contains default parameters or settings for the AI model, parsed from JSON
|
66 |
-
DEFAULT_CONFIG = json.loads(os.getenv("DEFAULT_CONFIG", "{}"))
|
67 |
-
# DEFAULT_MODEL_KEY is set to the first key found in MODEL_MAPPING if available, otherwise None
|
68 |
-
DEFAULT_MODEL_KEY = list(MODEL_MAPPING.keys())[0] if MODEL_MAPPING else None
|
69 |
|
70 |
# HTML meta tags for SEO and other purposes, loaded as a raw string from environment variables
|
71 |
# These tags are intended to be inserted into the <head> section of generated HTML pages
|
72 |
-
|
73 |
-
|
74 |
-
# List of allowed file extensions for upload or processing, parsed from a JSON array string
|
75 |
-
# This list helps enforce file type restrictions within the system
|
76 |
-
ALLOWED_EXTENSIONS = json.loads(os.getenv("ALLOWED_EXTENSIONS", "[]"))
|
77 |
-
|
78 |
-
# Notices or announcements that may be displayed to users or logged by the system
|
79 |
-
# The content is loaded as a raw string from the environment variable "NOTICES"
|
80 |
-
NOTICES = os.getenv('NOTICES')
|
|
|
3 |
# SPDX-License-Identifier: Apache-2.0
|
4 |
#
|
5 |
|
6 |
+
import os # Import os module to interact with environment variables
|
7 |
+
import json # Import json module to parse JSON-formatted strings
|
8 |
|
9 |
+
# Load the 'auth' configuration from an environment variable named "auth"
|
10 |
+
# This variable is expected to contain a JSON-formatted string representing authentication details
|
11 |
+
auth = json.loads(os.getenv("auth"))
|
|
|
12 |
|
13 |
+
# Load the 'restrictions' configuration from an environment variable named "restrictions"
|
14 |
+
# This variable is expected to contain a plain string defining usage restrictions or guidelines
|
15 |
+
restrictions = os.getenv("restrictions")
|
|
|
|
|
|
|
|
|
16 |
|
17 |
+
# Load the 'model' configuration from an environment variable named "model"
|
18 |
+
# This variable is expected to contain a JSON-formatted string mapping model labels to model names or configurations
|
19 |
+
model = json.loads(os.getenv("model"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
# HTML meta tags for SEO and other purposes, loaded as a raw string from environment variables
|
22 |
# These tags are intended to be inserted into the <head> section of generated HTML pages
|
23 |
+
# Used in https://hadadrjt-ai.hf.space
|
24 |
+
meta_tags = os.getenv("META_TAGS")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/API.md
DELETED
@@ -1,50 +0,0 @@
|
|
1 |
-
#### Installations
|
2 |
-
|
3 |
-
```bash
|
4 |
-
# Linux/Android (Termux)/MacOS/Windows.
|
5 |
-
# Make sure you have "wget", "python3" and "pip" installed.
|
6 |
-
# This package have very small size.
|
7 |
-
wget https://huggingface.co/spaces/hadadrjt/ai/raw/main/assets/bin/install.sh && chmod a+x install.sh && ./install.sh
|
8 |
-
```
|
9 |
-
|
10 |
-
#### Run J.A.R.V.I.S. in your terminal
|
11 |
-
|
12 |
-
```bash
|
13 |
-
# Example normal usage.
|
14 |
-
./ai Your message here.
|
15 |
-
|
16 |
-
# Example with Deep Search.
|
17 |
-
./ai -d Your message here.
|
18 |
-
```
|
19 |
-
|
20 |
-
#### Linux user's
|
21 |
-
|
22 |
-
```bash
|
23 |
-
# Bonus for more flexible.
|
24 |
-
sudo mv ai /bin/
|
25 |
-
|
26 |
-
# Now you can run with simple command.
|
27 |
-
ai Your message here.
|
28 |
-
```
|
29 |
-
|
30 |
-
### OpenAI Style (developers only)
|
31 |
-
|
32 |
-
If you are using the OpenAI style, there is no need to install all the processes mentioned above.
|
33 |
-
|
34 |
-
```
|
35 |
-
curl https://hadadrjt-api.hf.space/v1/responses \
|
36 |
-
-H "Content-Type: application/json" \
|
37 |
-
-d '{
|
38 |
-
"model": "JARVIS: 2.1.3",
|
39 |
-
"input": "Write a one-sentence bedtime story about a unicorn.",
|
40 |
-
"stream": true
|
41 |
-
}'
|
42 |
-
```
|
43 |
-
|
44 |
-
This is a powerful solution for integration with various systems and software, including building your own chatbot. No API key is required.
|
45 |
-
|
46 |
-
```
|
47 |
-
# Endpoint
|
48 |
-
# See at https://huggingface.co/spaces/hadadrjt/api
|
49 |
-
https://hadadrjt-api.hf.space/v1
|
50 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
httpx
|
2 |
-
openpyxl
|
3 |
-
pandas
|
4 |
-
pdfplumber
|
5 |
-
pillow
|
6 |
-
python-docx
|
7 |
-
python-pptx
|
8 |
-
pytesseract
|
9 |
-
requests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/{cores → client}/__init__.py
RENAMED
File without changes
|
src/client/chat_handler.py
ADDED
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import json # Import JSON module for encoding and decoding JSON data
|
7 |
+
import uuid # Import UUID module to generate unique session identifiers
|
8 |
+
from typing import Any, List # Import typing annotations for type hinting
|
9 |
+
from config import model # Import model configuration dictionary from config module
|
10 |
+
from src.core.server import jarvis # Import the async function to interact with AI backend
|
11 |
+
from src.core.parameter import parameters # Import parameters (not used directly here but imported for completeness)
|
12 |
+
from src.core.session import session # Import session dictionary to store conversation histories
|
13 |
+
from src.tools.audio import AudioGeneration # Import AudioGeneration class to handle audio creation
|
14 |
+
from src.tools.image import ImageGeneration # Import ImageGeneration class to handle image creation
|
15 |
+
from src.tools.deep_search import SearchTools # Import SearchTools class for deep search functionality
|
16 |
+
import gradio as gr # Import Gradio library for UI and request handling
|
17 |
+
|
18 |
+
# Define an asynchronous function 'respond' to process user messages and generate AI responses
|
19 |
+
# This version uses the "messages" style for chat history, where history is a list of dicts with "role" and "content" keys,
|
20 |
+
# supporting content as strings, dicts with "path" keys, or Gradio components.
|
21 |
+
async def respond(
|
22 |
+
message, # Incoming user message, can be a string or a dictionary containing text and files
|
23 |
+
history: List[Any], # List containing conversation history as pairs of user and assistant messages (tuples style)
|
24 |
+
model_label, # Label/key to select the AI model from the available models
|
25 |
+
temperature, # Sampling temperature controlling randomness of AI response generation
|
26 |
+
top_k, # Number of highest probability tokens to keep for sampling
|
27 |
+
min_p, # Minimum probability threshold for token sampling
|
28 |
+
top_p, # Cumulative probability threshold for nucleus sampling
|
29 |
+
repetition_penalty, # Penalty factor to reduce repetitive tokens in generated text
|
30 |
+
thinking, # Boolean flag indicating if AI should operate in "thinking" mode
|
31 |
+
image_gen, # Boolean flag to enable image generation commands
|
32 |
+
audio_gen, # Boolean flag to enable audio generation commands
|
33 |
+
search_gen, # Boolean flag to enable deep search commands
|
34 |
+
request: gr.Request # Gradio request object to access session information such as session hash
|
35 |
+
):
|
36 |
+
# Select the AI model based on the provided label, if label not found, fallback to the first model in the config
|
37 |
+
selected_model = model.get(model_label, list(model.values())[0])
|
38 |
+
|
39 |
+
# Instantiate SearchTools to enable deep search capabilities if requested
|
40 |
+
search_tools = SearchTools()
|
41 |
+
|
42 |
+
# Retrieve session ID from the Gradio request's session hash, generate a new UUID if none exists
|
43 |
+
session_id = request.session_hash or str(uuid.uuid4())
|
44 |
+
|
45 |
+
# Initialize an empty conversation history for this session if it does not already exist
|
46 |
+
if session_id not in session:
|
47 |
+
session[session_id] = []
|
48 |
+
|
49 |
+
# Determine the mode string based on the 'thinking' flag, affects AI response generation behavior
|
50 |
+
mode = "/think" if thinking else "/no_think"
|
51 |
+
|
52 |
+
# Initialize variables for user input text and any attached files
|
53 |
+
input = ""
|
54 |
+
files = None
|
55 |
+
|
56 |
+
# Check if the incoming message is a dictionary (which may contain text and files)
|
57 |
+
if isinstance(message, dict):
|
58 |
+
# Extract the text content from the message dictionary, default to empty string if missing
|
59 |
+
input = message.get("text", "")
|
60 |
+
# Extract the first file from the files list if present, otherwise, set files to None
|
61 |
+
files = message.get("files")[0] if message.get("files") else None
|
62 |
+
else:
|
63 |
+
# If the message is a simple string, assign it directly to input
|
64 |
+
input = message
|
65 |
+
|
66 |
+
# Strip leading and trailing whitespace from the input for clean processing
|
67 |
+
stripped_input = input.strip()
|
68 |
+
# Convert the stripped input to lowercase for case-insensitive command detection
|
69 |
+
lowered_input = stripped_input.lower()
|
70 |
+
|
71 |
+
# If the input is empty after stripping, yield an empty list and exit the function early
|
72 |
+
if not stripped_input:
|
73 |
+
yield []
|
74 |
+
return
|
75 |
+
|
76 |
+
# If the input is exactly one of the command keywords without parameters, yield empty and exit early
|
77 |
+
if lowered_input in ["/audio", "/image", "/dp"]:
|
78 |
+
yield []
|
79 |
+
return
|
80 |
+
|
81 |
+
# Prepare a new conversation history list formatted with roles and content for AI model consumption
|
82 |
+
# Here we convert the old "tuples" style history (list of [user_msg, assistant_msg]) into "messages" style:
|
83 |
+
# a flat list of dicts with "role" and "content" keys.
|
84 |
+
new_history = []
|
85 |
+
for entry in history:
|
86 |
+
# Ensure the entry is a list with exactly two elements: user message and assistant message
|
87 |
+
if isinstance(entry, list) and len(entry) == 2:
|
88 |
+
user_msg, assistant_msg = entry
|
89 |
+
# Append the user message with role 'user' to the new history if not None
|
90 |
+
if user_msg is not None:
|
91 |
+
new_history.append({"role": "user", "content": user_msg})
|
92 |
+
# Append the assistant message with role 'assistant' if it exists and is not None
|
93 |
+
if assistant_msg is not None:
|
94 |
+
new_history.append({"role": "assistant", "content": assistant_msg})
|
95 |
+
|
96 |
+
# Update the global session dictionary with the newly formatted conversation history for this session
|
97 |
+
session[session_id] = new_history
|
98 |
+
|
99 |
+
# Handle audio generation command if enabled and input starts with '/audio'
|
100 |
+
if audio_gen and lowered_input.startswith("/audio"):
|
101 |
+
# Extract the audio instruction text after the '/audio' command prefix and strip whitespace
|
102 |
+
audio_instruction = input[6:].strip()
|
103 |
+
# If no instruction text is provided, yield empty and exit early
|
104 |
+
if not audio_instruction:
|
105 |
+
yield []
|
106 |
+
return
|
107 |
+
try:
|
108 |
+
# Asynchronously create audio content based on the instruction using AudioGeneration class
|
109 |
+
audio = await AudioGeneration.create_audio(audio_instruction)
|
110 |
+
# Serialize the audio data and instruction into a JSON formatted string
|
111 |
+
audio_generation_content = json.dumps({
|
112 |
+
"audio": audio,
|
113 |
+
"audio_instruction": audio_instruction
|
114 |
+
})
|
115 |
+
# Construct the conversation history including the audio generation result and detailed instructions
|
116 |
+
audio_generation_result = (
|
117 |
+
new_history
|
118 |
+
+ [
|
119 |
+
{
|
120 |
+
"role": "system",
|
121 |
+
"content": (
|
122 |
+
f"Audio generation result:\n\n{audio_generation_content}\n\n\n"
|
123 |
+
"Show the audio using the following HTML audio tag format, where '{audio_link}' is the URL of the generated audio:\n\n"
|
124 |
+
"<audio controls src='{audio_link}' style='width:100%; max-width:100%;'></audio>\n\n"
|
125 |
+
"Please replace '{audio_link}' with the actual audio URL provided in the context.\n\n"
|
126 |
+
"Then, describe the generated audio based on the above information.\n\n\n"
|
127 |
+
"Use the same language as the previous user input or user request.\n"
|
128 |
+
"For example, if the previous user input or user request is in Indonesian, explain in Indonesian.\n"
|
129 |
+
"If it is in English, explain in English. This also applies to other languages.\n\n\n"
|
130 |
+
)
|
131 |
+
}
|
132 |
+
]
|
133 |
+
)
|
134 |
+
|
135 |
+
# Use async generator to get descriptive text about the generated audio
|
136 |
+
async for audio_description in jarvis(
|
137 |
+
session_id=session_id,
|
138 |
+
model=selected_model,
|
139 |
+
history=audio_generation_result,
|
140 |
+
user_message=input,
|
141 |
+
mode="/no_think", # Use no_think mode to avoid extra processing
|
142 |
+
temperature=0.7, # Fixed temperature for audio description generation
|
143 |
+
top_k=20, # Limit token sampling to top 20 tokens
|
144 |
+
min_p=0, # Minimum probability threshold
|
145 |
+
top_p=0.8, # Nucleus sampling threshold
|
146 |
+
repetition_penalty=1.0 # No repetition penalty for this step
|
147 |
+
):
|
148 |
+
# Yield the audio description wrapped in a tool role for UI display
|
149 |
+
yield [{"role": "tool", "content": f'{audio_description}'}]
|
150 |
+
return
|
151 |
+
except Exception:
|
152 |
+
# If audio generation fails, yield an error message and exit
|
153 |
+
yield [{"role": "tool", "content": "Audio generation failed. Please wait 15 seconds before trying again."}]
|
154 |
+
return
|
155 |
+
|
156 |
+
# Handle image generation command if enabled and input starts with '/image'
|
157 |
+
if image_gen and lowered_input.startswith("/image"):
|
158 |
+
# Extract the image generation instruction after the '/image' command prefix and strip whitespace
|
159 |
+
generate_image_instruction = input[6:].strip()
|
160 |
+
# If no instruction text is provided, yield empty and exit early
|
161 |
+
if not generate_image_instruction:
|
162 |
+
yield []
|
163 |
+
return
|
164 |
+
try:
|
165 |
+
# Asynchronously create image content based on the instruction using ImageGeneration class
|
166 |
+
image = await ImageGeneration.create_image(generate_image_instruction)
|
167 |
+
|
168 |
+
# Serialize the image data and instruction into a JSON formatted string
|
169 |
+
image_generation_content = json.dumps({
|
170 |
+
"image": image,
|
171 |
+
"generate_image_instruction": generate_image_instruction
|
172 |
+
})
|
173 |
+
|
174 |
+
# Construct the conversation history including the image generation result and detailed instructions
|
175 |
+
image_generation_result = (
|
176 |
+
new_history
|
177 |
+
+ [
|
178 |
+
{
|
179 |
+
"role": "system",
|
180 |
+
"content": (
|
181 |
+
f"Image generation result:\n\n{image_generation_content}\n\n\n"
|
182 |
+
"Show the generated image using the following markdown syntax format, where '{image_link}' is the URL of the image:\n\n"
|
183 |
+
"\n\n"
|
184 |
+
"Please replace '{image_link}' with the actual image URL provided in the context.\n\n"
|
185 |
+
"Then, describe the generated image based on the above information.\n\n\n"
|
186 |
+
"Use the same language as the previous user input or user request.\n"
|
187 |
+
"For example, if the previous user input or user request is in Indonesian, explain in Indonesian.\n"
|
188 |
+
"If it is in English, explain in English. This also applies to other languages.\n\n\n"
|
189 |
+
)
|
190 |
+
}
|
191 |
+
]
|
192 |
+
)
|
193 |
+
|
194 |
+
# Use async generator to get descriptive text about the generated image
|
195 |
+
async for image_description in jarvis(
|
196 |
+
session_id=session_id,
|
197 |
+
model=selected_model,
|
198 |
+
history=image_generation_result,
|
199 |
+
user_message=input,
|
200 |
+
mode="/no_think", # Use no_think mode to avoid extra processing
|
201 |
+
temperature=0.7, # Fixed temperature for image description generation
|
202 |
+
top_k=20, # Limit token sampling to top 20 tokens
|
203 |
+
min_p=0, # Minimum probability threshold
|
204 |
+
top_p=0.8, # Nucleus sampling threshold
|
205 |
+
repetition_penalty=1.0 # No repetition penalty for this step
|
206 |
+
):
|
207 |
+
# Yield the image description wrapped in a tool role for UI display
|
208 |
+
yield [{"role": "tool", "content": f"{image_description}"}]
|
209 |
+
return
|
210 |
+
except Exception:
|
211 |
+
# If image generation fails, yield an error message and exit
|
212 |
+
yield [{"role": "tool", "content": "Image generation failed. Please wait 15 seconds before trying again."}]
|
213 |
+
return
|
214 |
+
|
215 |
+
# Handle deep search command if enabled and input starts with '/dp'
|
216 |
+
if search_gen and lowered_input.startswith("/dp"):
|
217 |
+
# Extract the search query after the '/dp' command prefix and strip whitespace
|
218 |
+
search_query = input[3:].strip()
|
219 |
+
# If no search query is provided, yield empty and exit early
|
220 |
+
if not search_query:
|
221 |
+
yield []
|
222 |
+
return
|
223 |
+
|
224 |
+
try:
|
225 |
+
# Perform an asynchronous deep search using SearchTools with the given query
|
226 |
+
search_results = await search_tools.search(search_query)
|
227 |
+
|
228 |
+
# Serialize the search query and results (limited to first 5000 characters) into JSON string
|
229 |
+
search_content = json.dumps({
|
230 |
+
"query": search_query,
|
231 |
+
"search_results": search_results[:5000]
|
232 |
+
})
|
233 |
+
|
234 |
+
# Construct conversation history including deep search results and detailed instructions for summarization
|
235 |
+
search_instructions = (
|
236 |
+
new_history
|
237 |
+
+ [
|
238 |
+
{
|
239 |
+
"role": "system",
|
240 |
+
"content": (
|
241 |
+
f"Deep search results for query: '{search_query}':\n\n{search_content}\n\n\n"
|
242 |
+
"Please analyze these search results and provide a comprehensive summary of the information.\n"
|
243 |
+
"Identify the most relevant information related to the query.\n"
|
244 |
+
"Format your response in a clear, structured way with appropriate headings and bullet points if needed.\n"
|
245 |
+
"If the search results don't provide sufficient information, acknowledge this limitation.\n"
|
246 |
+
"Please provide links or URLs from each of your search results.\n\n"
|
247 |
+
"Use the same language as the previous user input or user request.\n"
|
248 |
+
"For example, if the previous user input or user request is in Indonesian, explain in Indonesian.\n"
|
249 |
+
"If it is in English, explain in English. This also applies to other languages.\n\n\n"
|
250 |
+
)
|
251 |
+
}
|
252 |
+
]
|
253 |
+
)
|
254 |
+
|
255 |
+
# Use async generator to process the deep search results and generate a summary response
|
256 |
+
async for search_response in jarvis(
|
257 |
+
session_id=session_id,
|
258 |
+
model=selected_model,
|
259 |
+
history=search_instructions,
|
260 |
+
user_message=input,
|
261 |
+
mode=mode, # Use the mode determined by the thinking flag
|
262 |
+
temperature=temperature,
|
263 |
+
top_k=top_k,
|
264 |
+
min_p=min_p,
|
265 |
+
top_p=top_p,
|
266 |
+
repetition_penalty=repetition_penalty
|
267 |
+
):
|
268 |
+
# Yield the search summary wrapped in a tool role for UI display
|
269 |
+
yield [{"role": "tool", "content": f"{search_response}"}]
|
270 |
+
return
|
271 |
+
|
272 |
+
except Exception as e:
|
273 |
+
# If deep search fails, yield an error message and exit
|
274 |
+
yield [{"role": "tool", "content": "Search failed, please try again later."}]
|
275 |
+
return
|
276 |
+
|
277 |
+
# For all other inputs that do not match special commands, use the jarvis function to generate a response
|
278 |
+
async for response in jarvis(
|
279 |
+
session_id=session_id,
|
280 |
+
model=selected_model,
|
281 |
+
history=new_history, # Pass the conversation history in "messages" style format
|
282 |
+
user_message=input,
|
283 |
+
mode=mode, # Use the mode determined by the thinking flag
|
284 |
+
files=files, # Pass any attached files along with the message
|
285 |
+
temperature=temperature,
|
286 |
+
top_k=top_k,
|
287 |
+
min_p=min_p,
|
288 |
+
top_p=top_p,
|
289 |
+
repetition_penalty=repetition_penalty
|
290 |
+
):
|
291 |
+
# Yield each chunk of the response as it is generated
|
292 |
+
yield response
|
src/{main → core}/__init__.py
RENAMED
File without changes
|
src/core/parameter.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
# Model parameters
|
7 |
+
def parameters(reasoning: bool):
|
8 |
+
"""
|
9 |
+
Determine and return a set of model generation parameters based on whether reasoning mode is enabled.
|
10 |
+
|
11 |
+
Args:
|
12 |
+
reasoning (bool): A flag indicating if reasoning mode is active.
|
13 |
+
When True, parameters favor more controlled and focused generation suitable for reasoning tasks.
|
14 |
+
When False, parameters allow for more creative or diverse outputs.
|
15 |
+
|
16 |
+
Returns:
|
17 |
+
tuple: A tuple containing five parameters used for text generation:
|
18 |
+
- temperature (float): Controls randomness in generation. Lower values make output more deterministic.
|
19 |
+
- top_k (int): Limits sampling to the top_k most likely next tokens.
|
20 |
+
- min_p (float): Minimum probability threshold for token inclusion (0 means no minimum).
|
21 |
+
- top_p (float): Nucleus sampling threshold, cumulative probability cutoff for token selection.
|
22 |
+
- repetition_penalty (float): Penalizes repeated tokens to reduce repetition in generated text.
|
23 |
+
"""
|
24 |
+
# Reasoning
|
25 |
+
if reasoning:
|
26 |
+
# Parameters tuned for reasoning tasks:
|
27 |
+
# Lower temperature (0.6) to reduce randomness and improve logical consistency.
|
28 |
+
# top_k set to 20 to limit choices to the 20 most probable tokens, focusing generation.
|
29 |
+
# min_p is 0, meaning no minimum probability cutoff is enforced.
|
30 |
+
# top_p is 0.95, allowing nucleus sampling to consider tokens covering 95% cumulative probability.
|
31 |
+
# repetition_penalty is 1.0, meaning no penalty applied for token repetition.
|
32 |
+
return (
|
33 |
+
0.6, # temperature: less randomness for focused reasoning
|
34 |
+
20, # top_k: restrict to top 20 tokens for more precise output
|
35 |
+
0, # min_p: no minimum probability threshold
|
36 |
+
0.95, # top_p: nucleus sampling cutoff to include tokens up to 95% cumulative probability
|
37 |
+
1.0 # repetition_penalty: no penalty on repeated tokens
|
38 |
+
)
|
39 |
+
# Non-reasoning
|
40 |
+
else:
|
41 |
+
# Parameters tuned for non-reasoning or more creative generation:
|
42 |
+
# Slightly higher temperature (0.7) to allow more diversity and creativity.
|
43 |
+
# top_k remains 20 to keep some restriction on token selection.
|
44 |
+
# min_p is 0.0, no minimum probability cutoff.
|
45 |
+
# top_p is lower at 0.8, narrowing nucleus sampling to more probable tokens for balanced creativity.
|
46 |
+
# repetition_penalty remains 1.0, no penalty on repeated tokens.
|
47 |
+
return (
|
48 |
+
0.7, # temperature: more randomness for creative outputs
|
49 |
+
20, # top_k: restrict to top 20 tokens to maintain some control
|
50 |
+
0.0, # min_p: no minimum probability threshold
|
51 |
+
0.8, # top_p: nucleus sampling cutoff at 80% cumulative probability
|
52 |
+
1.0 # repetition_penalty: no penalty on repeated tokens
|
53 |
+
)
|
src/core/server.py
ADDED
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import json # Import JSON module to parse and handle JSON data
|
7 |
+
import uuid # Import UUID module to generate unique identifiers for sessions
|
8 |
+
from typing import List, Dict, Any # Import type hints for lists, dictionaries, and generic types
|
9 |
+
from datetime import datetime # Import datetime to get and format current date/time
|
10 |
+
from config import * # Import all configuration variables including 'auth' and 'restrictions'
|
11 |
+
from src.utils.session_mapping import get_host # Import function to get server info by session ID
|
12 |
+
from src.utils.ip_generator import generate_ip # Import function to generate random IP for headers
|
13 |
+
from src.utils.helper import mark # Import function to mark a server as busy/unavailable
|
14 |
+
from src.ui.reasoning import styles # Import function to apply CSS styling to reasoning output
|
15 |
+
import httpx # Import httpx for async HTTP requests with streaming support
|
16 |
+
|
17 |
+
async def jarvis(
|
18 |
+
session_id: str, # Unique session identifier to maintain consistent server assignment
|
19 |
+
model: str, # AI model name to specify which model to use
|
20 |
+
history: List[Dict[str, str]], # List of previous conversation messages with roles and content
|
21 |
+
user_message: str, # Latest user input message to send to the AI model
|
22 |
+
mode: str, # Mode string to guide AI behavior, e.g., '/think' or '/no_think'
|
23 |
+
files=None, # Optional files or attachments to include with the user message
|
24 |
+
temperature: float = 0.6, # Sampling temperature controlling randomness in token generation
|
25 |
+
top_k: int = 20, # Limit token selection to top_k probable tokens
|
26 |
+
min_p: float = 0, # Minimum probability threshold for token selection
|
27 |
+
top_p: float = 0.95, # Nucleus sampling cumulative probability threshold
|
28 |
+
repetition_penalty: float = 1.0, # Penalty factor to reduce token repetition
|
29 |
+
):
|
30 |
+
"""
|
31 |
+
Asynchronously send a chat request to a Jarvis AI server and handle streaming response incrementally.
|
32 |
+
|
33 |
+
This function manages server selection based on the session ID, retries requests on specific error codes,
|
34 |
+
and yields incremental parts of the AI-generated response as they arrive. It integrates CSS styling into
|
35 |
+
the reasoning output only if the mode is not '/no_think', preserving the behavior where reasoning is streamed
|
36 |
+
first inside a styled HTML block, followed by the main content streamed normally.
|
37 |
+
|
38 |
+
Args:
|
39 |
+
session_id (str): Identifier for the user session to maintain consistent server assignment.
|
40 |
+
model (str): Name of the AI model to use for generating the response.
|
41 |
+
history (List[Dict[str, str]]): List of previous messages in the conversation.
|
42 |
+
user_message (str): The current message from the user to send to the AI model.
|
43 |
+
mode (str): Contextual instructions to guide the AI model's response style.
|
44 |
+
files (optional): Additional files or attachments to include with the user message.
|
45 |
+
temperature (float): Controls randomness in token generation.
|
46 |
+
top_k (int): Limits token selection to top_k probable tokens.
|
47 |
+
min_p (float): Minimum probability threshold for token selection.
|
48 |
+
top_p (float): Nucleus sampling cumulative probability threshold.
|
49 |
+
repetition_penalty (float): Factor to reduce token repetition.
|
50 |
+
|
51 |
+
Yields:
|
52 |
+
str: Incremental strings of AI-generated response streamed from the server.
|
53 |
+
Reasoning is wrapped in a styled HTML details block and streamed incrementally only if mode is not '/no_think'.
|
54 |
+
After reasoning finishes, the main content is streamed normally.
|
55 |
+
|
56 |
+
Notes:
|
57 |
+
The function attempts to send the request to a server assigned for the session.
|
58 |
+
If the server returns a specific error code indicating it is busy, it retries with another server.
|
59 |
+
If all servers are busy or fail, it yields a message indicating the server is busy.
|
60 |
+
"""
|
61 |
+
tried = set() # Track servers already tried to avoid repeated retries
|
62 |
+
|
63 |
+
# Loop until all available servers have been tried without success
|
64 |
+
while len(tried) < len(auth):
|
65 |
+
# Get server setup info assigned for this session, including endpoint, token, and error code
|
66 |
+
setup = get_host(session_id)
|
67 |
+
server = setup["jarvis"] # Server identifier
|
68 |
+
host = setup["endpoint"] # API endpoint URL
|
69 |
+
token = setup["token"] # Authorization token
|
70 |
+
error = setup["error"] # HTTP error code triggering retry
|
71 |
+
tried.add(server) # Mark this server as tried
|
72 |
+
|
73 |
+
# Format current date/time string for system instructions
|
74 |
+
date = datetime.now().strftime("%A, %B %d, %Y, %I:%M %p %Z")
|
75 |
+
|
76 |
+
# Combine mode instructions, usage restrictions, and date into system instructions string
|
77 |
+
instructions = f"{mode}\n\n\n{restrictions}\n\n\nToday: {date}\n\n\n"
|
78 |
+
|
79 |
+
# Copy conversation history to avoid mutating original
|
80 |
+
messages = history.copy()
|
81 |
+
# Insert system instructions as first message
|
82 |
+
messages.insert(0, {"role": "system", "content": instructions})
|
83 |
+
|
84 |
+
# Prepare user message dict, include files if provided
|
85 |
+
msg = {"role": "user", "content": user_message}
|
86 |
+
if files:
|
87 |
+
msg["files"] = files
|
88 |
+
messages.append(msg) # Append user message to conversation
|
89 |
+
|
90 |
+
# Prepare HTTP headers with authorization and randomized client IP
|
91 |
+
headers = {
|
92 |
+
"Authorization": f"Bearer {token}", # Bearer token for API access
|
93 |
+
"Content-Type": "application/json", # JSON content type
|
94 |
+
"X-Forwarded-For": generate_ip() # Random IP to simulate different client origins
|
95 |
+
}
|
96 |
+
|
97 |
+
# Prepare JSON payload with model parameters and conversation messages
|
98 |
+
payload = {
|
99 |
+
"model": model,
|
100 |
+
"messages": messages,
|
101 |
+
"stream": True,
|
102 |
+
"temperature": temperature,
|
103 |
+
"top_k": top_k,
|
104 |
+
"min_p": min_p,
|
105 |
+
"top_p": top_p,
|
106 |
+
"repetition_penalty": repetition_penalty,
|
107 |
+
}
|
108 |
+
|
109 |
+
# Initialize accumulators and flags for streamed response parts
|
110 |
+
reasoning = "" # Accumulate reasoning text
|
111 |
+
reasoning_check = None # Flag to detect presence of reasoning in response
|
112 |
+
reasoning_done = False # Flag marking reasoning completion
|
113 |
+
content = "" # Accumulate main content text
|
114 |
+
|
115 |
+
try:
|
116 |
+
# Create async HTTP client with no timeout for long streaming
|
117 |
+
async with httpx.AsyncClient(timeout=None) as client:
|
118 |
+
# Open async streaming POST request to Jarvis server
|
119 |
+
async with client.stream("POST", host, headers=headers, json=payload) as response:
|
120 |
+
# Iterate asynchronously over each line of streaming response
|
121 |
+
async for chunk in response.aiter_lines():
|
122 |
+
# Skip lines not starting with "data:"
|
123 |
+
if not chunk.strip().startswith("data:"):
|
124 |
+
continue
|
125 |
+
try:
|
126 |
+
# Parse JSON data after "data:" prefix
|
127 |
+
data = json.loads(chunk[5:])
|
128 |
+
# Extract incremental delta message from first choice
|
129 |
+
choice = data["choices"][0]["delta"]
|
130 |
+
|
131 |
+
# On first delta received, detect if 'reasoning' field is present and non-empty
|
132 |
+
if reasoning_check is None:
|
133 |
+
# Initialize reasoning_check to empty string if reasoning exists and is non-empty, else None
|
134 |
+
reasoning_check = "" if ("reasoning" in choice and choice["reasoning"]) else None
|
135 |
+
|
136 |
+
# If reasoning is present and mode is not '/no_think' and reasoning not done
|
137 |
+
if (
|
138 |
+
reasoning_check == "" # Reasoning detected in response
|
139 |
+
and mode != "/no_think" # Mode allows reasoning output
|
140 |
+
and not reasoning_done # Reasoning phase not finished yet
|
141 |
+
and "reasoning" in choice # Current delta includes reasoning part
|
142 |
+
and choice["reasoning"] # Reasoning content is not empty
|
143 |
+
):
|
144 |
+
reasoning += choice["reasoning"] # Append incremental reasoning text
|
145 |
+
# Yield reasoning wrapped in styled HTML block with details expanded
|
146 |
+
yield styles(reasoning=reasoning, content="", expanded=True)
|
147 |
+
continue # Continue streaming reasoning increments
|
148 |
+
|
149 |
+
# When reasoning ends and content starts, mark reasoning done, yield empty string, then content
|
150 |
+
if (
|
151 |
+
reasoning_check == "" # Reasoning was detected previously
|
152 |
+
and mode != "/no_think" # Mode allows reasoning output
|
153 |
+
and not reasoning_done # Reasoning phase not finished yet
|
154 |
+
and "content" in choice # Current delta includes content part
|
155 |
+
and choice["content"] # Content is not empty
|
156 |
+
):
|
157 |
+
reasoning_done = True # Mark reasoning phase complete
|
158 |
+
yield "" # Yield empty string to signal end of reasoning block
|
159 |
+
content += choice["content"] # Start accumulating content text
|
160 |
+
yield content # Yield first part of content
|
161 |
+
continue # Continue streaming content increments
|
162 |
+
|
163 |
+
# If no reasoning present or reasoning done, accumulate content and yield incrementally
|
164 |
+
if (
|
165 |
+
(reasoning_check is None or reasoning_done or mode == "/no_think") # No reasoning or reasoning finished or mode disables reasoning
|
166 |
+
and "content" in choice # Current delta includes content part
|
167 |
+
and choice["content"] # Content is not empty
|
168 |
+
):
|
169 |
+
content += choice["content"] # Append incremental content text
|
170 |
+
yield content # Yield updated content string
|
171 |
+
except Exception:
|
172 |
+
# Ignore exceptions during JSON parsing or key access and continue streaming
|
173 |
+
continue
|
174 |
+
return # Exit function after successful streaming completion
|
175 |
+
except httpx.HTTPStatusError as e:
|
176 |
+
# If server returns specific error code indicating busy, retry with another server
|
177 |
+
if e.response.status_code == error:
|
178 |
+
continue # Try next available server
|
179 |
+
else:
|
180 |
+
# For other HTTP errors, mark this server as busy
|
181 |
+
mark(server)
|
182 |
+
except Exception:
|
183 |
+
# For other exceptions (network errors, timeouts), mark server as busy
|
184 |
+
mark(server)
|
185 |
+
|
186 |
+
# If all servers tried and none succeeded, yield busy message
|
187 |
+
yield "The server is currently busy. Please wait a moment or try again later."
|
188 |
+
return # End of function
|
src/core/session.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
from typing import Dict, List # Import type hinting tools 'Dict' and 'List' from the typing module to specify complex data structures
|
7 |
+
|
8 |
+
# Initialize an empty dictionary named 'session' to store session-related data.
|
9 |
+
# The dictionary keys are strings, which could represent session IDs or user identifiers.
|
10 |
+
# Each key maps to a list of dictionaries, where each dictionary contains string keys and string values.
|
11 |
+
# This structure allows storing multiple records per session, with each record represented as a dictionary of string-to-string pairs.
|
12 |
+
session: Dict[str, List[Dict[str, str]]] = {} # Empty dictionary ready to hold session data structured as described
|
src/cores/client.py
DELETED
@@ -1,161 +0,0 @@
|
|
1 |
-
#
|
2 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
-
# SPDX-License-Identifier: Apache-2.0
|
4 |
-
#
|
5 |
-
|
6 |
-
import asyncio # Import asyncio for asynchronous programming capabilities
|
7 |
-
import httpx # Import httpx to perform asynchronous HTTP requests
|
8 |
-
import json # Import json to handle JSON encoding and decoding
|
9 |
-
import random # Import random to shuffle lists for load balancing
|
10 |
-
import uuid # Import uuid to generate unique session identifiers
|
11 |
-
|
12 |
-
from config import * # Import all configuration constants and variables from config module
|
13 |
-
from src.cores.server import fetch_response_stream_async # Import async function to fetch streamed AI responses
|
14 |
-
from src.cores.session import ensure_stop_event, get_model_key # Import session helper functions
|
15 |
-
from datetime import datetime # Import datetime to get current date and time information
|
16 |
-
|
17 |
-
async def chat_with_model_async(history, user_input, model_display, sess, custom_prompt, deep_search):
|
18 |
-
"""
|
19 |
-
Asynchronous function to handle interaction with an AI model and stream its responses.
|
20 |
-
|
21 |
-
Parameters:
|
22 |
-
- history: List of tuples containing previous conversation messages (user and assistant)
|
23 |
-
- user_input: The current input string from the user
|
24 |
-
- model_display: The display name of the AI model to use
|
25 |
-
- sess: Session object containing session state, stop event, and cancellation token
|
26 |
-
- custom_prompt: Optional custom system instructions to override default instructions
|
27 |
-
- deep_search: Boolean flag indicating whether to integrate deep search results into the instructions
|
28 |
-
|
29 |
-
This function prepares the message history and system instructions, optionally enriches the instructions
|
30 |
-
with deep search results if enabled, and attempts to fetch streamed responses from multiple backend
|
31 |
-
providers with fallback. It yields chunks of the response asynchronously for real-time UI updates.
|
32 |
-
"""
|
33 |
-
|
34 |
-
# Ensure the session has a stop event initialized to control streaming cancellation
|
35 |
-
ensure_stop_event(sess)
|
36 |
-
|
37 |
-
# Clear any previous stop event state to allow new streaming session
|
38 |
-
sess.stop_event.clear()
|
39 |
-
|
40 |
-
# Reset the cancellation token to indicate the session is active and not cancelled
|
41 |
-
sess.cancel_token["cancelled"] = False
|
42 |
-
|
43 |
-
# Check if provider keys and hosts are configured; if not, yield a predefined error response and exit
|
44 |
-
if not LINUX_SERVER_PROVIDER_KEYS or not LINUX_SERVER_HOSTS:
|
45 |
-
yield ("content", RESPONSES["RESPONSE_3"]) # Inform user no backend providers are available
|
46 |
-
return
|
47 |
-
|
48 |
-
# Assign a unique session ID if not already present to track conversation context
|
49 |
-
if not hasattr(sess, "session_id") or not sess.session_id:
|
50 |
-
sess.session_id = str(uuid.uuid4())
|
51 |
-
|
52 |
-
# Determine the internal model key based on the display name, falling back to default if not found
|
53 |
-
model_key = get_model_key(model_display, MODEL_MAPPING, DEFAULT_MODEL_KEY)
|
54 |
-
|
55 |
-
# Retrieve model-specific configuration parameters or use default configuration
|
56 |
-
cfg = MODEL_CONFIG.get(model_key, DEFAULT_CONFIG)
|
57 |
-
|
58 |
-
# Initialize a list to hold the messages that will be sent to the AI model
|
59 |
-
msgs = []
|
60 |
-
|
61 |
-
# Obtain the current date and time formatted as a readable string for context in instructions
|
62 |
-
current_date = datetime.now().strftime("%A, %B %d, %Y, %I:%M %p %Z")
|
63 |
-
|
64 |
-
# Combine internal AI instructions with the current date to form a comprehensive system instructions
|
65 |
-
COMBINED_AI_INSTRUCTIONS = (
|
66 |
-
INTERNAL_AI_INSTRUCTIONS
|
67 |
-
+ "\n\n\n"
|
68 |
-
+ f"Today is: {current_date}"
|
69 |
-
+ "\n\n\n"
|
70 |
-
)
|
71 |
-
|
72 |
-
# If deep search is enabled and the primary model is selected, prepend deep search instructions and results
|
73 |
-
if deep_search and model_display == MODEL_CHOICES[0]:
|
74 |
-
# Add deep search instructions as a system message to guide the AI
|
75 |
-
msgs.append({"role": "system", "content": DEEP_SEARCH_INSTRUCTIONS})
|
76 |
-
try:
|
77 |
-
# Create an asynchronous HTTP client session for making the deep search request
|
78 |
-
async with httpx.AsyncClient() as client:
|
79 |
-
# Define the payload with parameters for the deep search query
|
80 |
-
payload = {
|
81 |
-
"query": user_input,
|
82 |
-
"topic": "general",
|
83 |
-
"search_depth": "basic",
|
84 |
-
"chunks_per_source": 5,
|
85 |
-
"max_results": 5,
|
86 |
-
"time_range": None,
|
87 |
-
"days": 7,
|
88 |
-
"include_answer": True,
|
89 |
-
"include_raw_content": False,
|
90 |
-
"include_images": False,
|
91 |
-
"include_image_descriptions": False,
|
92 |
-
"include_domains": [],
|
93 |
-
"exclude_domains": []
|
94 |
-
}
|
95 |
-
# Send a POST request to the deep search provider with authorization header and JSON payload
|
96 |
-
r = await client.post(
|
97 |
-
DEEP_SEARCH_PROVIDER_HOST,
|
98 |
-
headers={"Authorization": f"Bearer {DEEP_SEARCH_PROVIDER_KEY}"},
|
99 |
-
json=payload
|
100 |
-
)
|
101 |
-
# Parse the JSON response from the deep search provider
|
102 |
-
sr_json = r.json()
|
103 |
-
# Append the deep search results as a system message in JSON string format
|
104 |
-
msgs.append({"role": "system", "content": json.dumps(sr_json)})
|
105 |
-
except Exception:
|
106 |
-
# If any error occurs during deep search, fail silently without interrupting the chat flow
|
107 |
-
pass
|
108 |
-
# Append the combined AI instructions after the deep search content to maintain context
|
109 |
-
msgs.append({"role": "system", "content": COMBINED_AI_INSTRUCTIONS})
|
110 |
-
|
111 |
-
# If deep search is not enabled but the primary model is selected, use only the combined AI instructions
|
112 |
-
elif model_display == MODEL_CHOICES[0]:
|
113 |
-
msgs.append({"role": "system", "content": COMBINED_AI_INSTRUCTIONS})
|
114 |
-
|
115 |
-
# For other models, use a custom instructions if provided, otherwise default to the system instructions mapping or default instructions
|
116 |
-
else:
|
117 |
-
msgs.append({"role": "system", "content": custom_prompt or SYSTEM_PROMPT_MAPPING.get(model_key, SYSTEM_PROMPT_DEFAULT)})
|
118 |
-
|
119 |
-
# Append the conversation history to the message list, alternating user and assistant messages
|
120 |
-
# First add all user messages from history
|
121 |
-
msgs.extend([{"role": "user", "content": u} for u, _ in history])
|
122 |
-
# Then add all assistant messages from history that are not empty
|
123 |
-
msgs.extend([{"role": "assistant", "content": a} for _, a in history if a])
|
124 |
-
|
125 |
-
# Append the current user input as the latest user message
|
126 |
-
msgs.append({"role": "user", "content": user_input})
|
127 |
-
|
128 |
-
# Create a list of all possible combinations of backend hosts and provider keys for load balancing and fallback
|
129 |
-
candidates = [(h, k) for h in LINUX_SERVER_HOSTS for k in LINUX_SERVER_PROVIDER_KEYS]
|
130 |
-
|
131 |
-
# Randomly shuffle the list of host-key pairs to distribute load evenly and avoid bias
|
132 |
-
random.shuffle(candidates)
|
133 |
-
|
134 |
-
# Iterate over each host and key pair to attempt fetching a streamed response
|
135 |
-
for h, k in candidates:
|
136 |
-
# Call the async generator function to fetch streamed response chunks from the backend
|
137 |
-
stream_gen = fetch_response_stream_async(
|
138 |
-
h, k, model_key, msgs, cfg, sess.session_id, sess.stop_event, sess.cancel_token
|
139 |
-
)
|
140 |
-
|
141 |
-
# Flag to track if any response chunks were received from this provider
|
142 |
-
got_responses = False
|
143 |
-
|
144 |
-
# Asynchronously iterate over each chunk yielded by the streaming generator
|
145 |
-
async for chunk in stream_gen:
|
146 |
-
# If the stop event is set or cancellation requested, terminate streaming immediately
|
147 |
-
if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
|
148 |
-
return
|
149 |
-
|
150 |
-
# Mark that at least one response chunk has been received
|
151 |
-
got_responses = True
|
152 |
-
|
153 |
-
# Yield the current chunk to the caller for incremental UI update or processing
|
154 |
-
yield chunk
|
155 |
-
|
156 |
-
# If any responses were received from this host-key pair, stop trying others and return
|
157 |
-
if got_responses:
|
158 |
-
return
|
159 |
-
|
160 |
-
# If no responses were received from any provider, yield a fallback message indicating failure
|
161 |
-
yield ("content", RESPONSES["RESPONSE_2"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/cores/server.py
DELETED
@@ -1,101 +0,0 @@
|
|
1 |
-
#
|
2 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
-
# SPDX-License-Identifier: Apache-2.0
|
4 |
-
#
|
5 |
-
|
6 |
-
import codecs # Import codecs module for encoding and decoding operations, useful for handling text data
|
7 |
-
import httpx # Import httpx for making asynchronous HTTP requests to external servers or APIs
|
8 |
-
import json # Import json module to parse JSON formatted strings into Python objects and vice versa
|
9 |
-
|
10 |
-
from src.cores.session import marked_item # Import marked_item function to track and mark keys that fail repeatedly, helping to avoid using problematic keys
|
11 |
-
from config import LINUX_SERVER_ERRORS, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS, RESPONSES # Import various constants used for error handling, key marking, retry attempts, and predefined responses
|
12 |
-
|
13 |
-
async def fetch_response_stream_async(host, key, model, msgs, cfg, sid, stop_event, cancel_token):
|
14 |
-
"""
|
15 |
-
Asynchronous generator function that streams AI-generated responses from a backend server endpoint.
|
16 |
-
|
17 |
-
Parameters:
|
18 |
-
- host: The URL of the backend server to send the request to.
|
19 |
-
- key: Authorization token (API key) used in the request header for authentication.
|
20 |
-
- model: The AI model identifier to be used for generating responses.
|
21 |
-
- msgs: The list of messages forming the conversation or prompt to send to the AI.
|
22 |
-
- cfg: Configuration dictionary containing additional parameters for the request.
|
23 |
-
- sid: Session ID string to associate the request with a particular session.
|
24 |
-
- stop_event: An asynchronous event object that signals when to stop streaming responses.
|
25 |
-
- cancel_token: A dictionary containing a 'cancelled' boolean flag to abort the streaming operation.
|
26 |
-
|
27 |
-
This function attempts to connect to the backend server twice with different timeout values (5 and 10 seconds).
|
28 |
-
It sends a POST request with JSON payload that includes model, messages, session ID, stream flag, and configuration.
|
29 |
-
The function streams the response line-by-line, parsing JSON data chunks as they arrive.
|
30 |
-
|
31 |
-
The streamed data contains two types of text parts:
|
32 |
-
- 'reasoning': Additional reasoning text that can be displayed separately in the UI for richer user experience.
|
33 |
-
- 'content': The main content text generated by the AI.
|
34 |
-
|
35 |
-
The function yields tuples of the form ('reasoning', text) or ('content', text) to the caller asynchronously.
|
36 |
-
|
37 |
-
If the server returns an error status code listed in LINUX_SERVER_ERRORS, the key is marked as problematic to avoid future use.
|
38 |
-
The function also respects stop_event and cancel_token to allow graceful cancellation of the streaming process.
|
39 |
-
|
40 |
-
If the response signals completion with a specific message defined in RESPONSES["RESPONSE_10"], the function ends the stream.
|
41 |
-
|
42 |
-
The function handles exceptions gracefully, including network errors and JSON parsing issues, retrying or marking keys as needed.
|
43 |
-
"""
|
44 |
-
# Loop over two timeout values to attempt the request with increasing timeout durations for robustness
|
45 |
-
for timeout in [5, 10]:
|
46 |
-
try:
|
47 |
-
# Create an asynchronous HTTP client with the specified timeout for the request
|
48 |
-
async with httpx.AsyncClient(timeout=timeout) as client:
|
49 |
-
# Open a streaming POST request to the backend server with JSON payload and authorization header
|
50 |
-
async with client.stream(
|
51 |
-
"POST",
|
52 |
-
host,
|
53 |
-
# Combine fixed parameters with additional configuration into the JSON body
|
54 |
-
json={**{"model": model, "messages": msgs, "session_id": sid, "stream": True}, **cfg},
|
55 |
-
headers={"Authorization": f"Bearer {key}"} # Use Bearer token authentication
|
56 |
-
) as response:
|
57 |
-
# Check if the response status code indicates a server error that should mark the key
|
58 |
-
if response.status_code in LINUX_SERVER_ERRORS:
|
59 |
-
# Mark the key as problematic with the provided tracking function and exit the generator
|
60 |
-
marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
|
61 |
-
return
|
62 |
-
|
63 |
-
# Iterate asynchronously over each line of the streamed response content
|
64 |
-
async for line in response.aiter_lines():
|
65 |
-
# If the stop event is set or cancellation is requested, stop streaming and exit
|
66 |
-
if stop_event.is_set() or cancel_token["cancelled"]:
|
67 |
-
return
|
68 |
-
# Skip empty lines to avoid unnecessary processing
|
69 |
-
if not line:
|
70 |
-
continue
|
71 |
-
# Process lines that start with the prefix 'data: ' which contain JSON payloads
|
72 |
-
if line.startswith("data: "):
|
73 |
-
data = line[6:] # Extract the JSON string after 'data: '
|
74 |
-
# If the data matches the predefined end-of-response message, stop streaming
|
75 |
-
if data.strip() == RESPONSES["RESPONSE_10"]:
|
76 |
-
return
|
77 |
-
try:
|
78 |
-
# Attempt to parse the JSON data string into a Python dictionary
|
79 |
-
j = json.loads(data)
|
80 |
-
# Check if the parsed object is a dictionary containing 'choices' key
|
81 |
-
if isinstance(j, dict) and j.get("choices"):
|
82 |
-
# Iterate over each choice in the response to extract text deltas
|
83 |
-
for ch in j["choices"]:
|
84 |
-
delta = ch.get("delta", {}) # Get the incremental update part
|
85 |
-
# If 'reasoning' text is present in the delta, decode unicode escapes and yield it
|
86 |
-
if "reasoning" in delta and delta["reasoning"]:
|
87 |
-
decoded = delta["reasoning"].encode('utf-8').decode('unicode_escape')
|
88 |
-
yield ("reasoning", decoded) # Yield reasoning text for UI display
|
89 |
-
# If main 'content' text is present in the delta, yield it directly
|
90 |
-
if "content" in delta and delta["content"]:
|
91 |
-
yield ("content", delta["content"]) # Yield main content text
|
92 |
-
except Exception:
|
93 |
-
# Ignore exceptions from malformed JSON or unexpected data formats and continue streaming
|
94 |
-
continue
|
95 |
-
except Exception:
|
96 |
-
# Catch network errors, timeouts, or other exceptions and try the next timeout or retry
|
97 |
-
continue
|
98 |
-
# If all attempts fail, mark the key as problematic to avoid future use
|
99 |
-
marked_item(key, LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS)
|
100 |
-
# Return None explicitly when streaming ends or fails after retries
|
101 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/cores/session.py
DELETED
@@ -1,93 +0,0 @@
|
|
1 |
-
#
|
2 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
-
# SPDX-License-Identifier: Apache-2.0
|
4 |
-
#
|
5 |
-
|
6 |
-
import asyncio # Import the asyncio library to handle asynchronous operations and events
|
7 |
-
import requests # Import the requests library for HTTP requests and session management
|
8 |
-
import uuid # Import the uuid library to generate unique identifiers
|
9 |
-
import threading # Import threading to run background timers for delayed operations
|
10 |
-
|
11 |
-
from config import LINUX_SERVER_PROVIDER_KEYS_MARKED, LINUX_SERVER_PROVIDER_KEYS_ATTEMPTS # Import configuration variables that track marked provider keys and their failure attempts
|
12 |
-
|
13 |
-
class SessionWithID(requests.Session):
|
14 |
-
"""
|
15 |
-
Custom session class extending requests.Session to add unique session identification
|
16 |
-
and asynchronous cancellation control. This allows tracking individual user sessions
|
17 |
-
and managing cancellation of ongoing HTTP requests asynchronously.
|
18 |
-
"""
|
19 |
-
def __init__(self):
|
20 |
-
super().__init__() # Initialize the base requests.Session class
|
21 |
-
self.session_id = str(uuid.uuid4())
|
22 |
-
# Generate and assign a unique string ID for this session instance to identify it uniquely
|
23 |
-
self.stop_event = asyncio.Event()
|
24 |
-
# Create an asyncio Event object used to signal when the session should stop or cancel operations
|
25 |
-
self.cancel_token = {"cancelled": False}
|
26 |
-
# Dictionary flag to indicate if the current session's operations have been cancelled
|
27 |
-
|
28 |
-
def create_session():
|
29 |
-
"""
|
30 |
-
Factory function to create and return a new SessionWithID instance.
|
31 |
-
This should be called whenever a new user session starts or a chat session is reset,
|
32 |
-
ensuring each session has its own unique ID and cancellation controls.
|
33 |
-
"""
|
34 |
-
return SessionWithID()
|
35 |
-
|
36 |
-
def ensure_stop_event(sess):
|
37 |
-
"""
|
38 |
-
Utility function to verify that a given session object has the required asynchronous
|
39 |
-
control attributes: stop_event and cancel_token. If they are missing (e.g., when restoring
|
40 |
-
sessions from storage), this function adds them to maintain consistent session behavior.
|
41 |
-
|
42 |
-
Parameters:
|
43 |
-
- sess: The session object to check and update.
|
44 |
-
"""
|
45 |
-
if not hasattr(sess, "stop_event"):
|
46 |
-
sess.stop_event = asyncio.Event()
|
47 |
-
# Add an asyncio Event to signal stop requests if missing
|
48 |
-
if not hasattr(sess, "cancel_token"):
|
49 |
-
sess.cancel_token = {"cancelled": False}
|
50 |
-
# Add a cancellation flag dictionary if missing
|
51 |
-
|
52 |
-
def marked_item(item, marked, attempts):
|
53 |
-
"""
|
54 |
-
Mark a provider key or host as temporarily problematic after repeated failures to prevent
|
55 |
-
using unreliable providers continuously. This function adds the item to a 'marked' set
|
56 |
-
and increments its failure attempt count. If the failure count reaches 3 or more, a timer
|
57 |
-
is started to automatically unmark the item after 5 minutes (300 seconds), allowing retries.
|
58 |
-
|
59 |
-
Parameters:
|
60 |
-
- item: The provider key or host identifier to mark as problematic.
|
61 |
-
- marked: A set containing currently marked items.
|
62 |
-
- attempts: A dictionary tracking the number of failure attempts per item.
|
63 |
-
"""
|
64 |
-
marked.add(item)
|
65 |
-
# Add the item to the set of marked problematic providers
|
66 |
-
attempts[item] = attempts.get(item, 0) + 1
|
67 |
-
# Increment the failure attempt count for this item, initializing if necessary
|
68 |
-
if attempts[item] >= 3:
|
69 |
-
# If the item has failed 3 or more times, schedule removal from marked after 5 minutes
|
70 |
-
def remove():
|
71 |
-
marked.discard(item)
|
72 |
-
# Remove the item from the marked set to allow retrying
|
73 |
-
attempts.pop(item, None)
|
74 |
-
# Remove the attempt count entry for this item to reset its failure state
|
75 |
-
threading.Timer(300, remove).start()
|
76 |
-
# Start a background timer that will call remove() after 300 seconds (5 minutes)
|
77 |
-
|
78 |
-
def get_model_key(display, MODEL_MAPPING, DEFAULT_MODEL_KEY):
|
79 |
-
"""
|
80 |
-
Translate a human-readable model display name into its internal model key identifier.
|
81 |
-
Searches the MODEL_MAPPING dictionary for the key whose value matches the display name.
|
82 |
-
Returns the DEFAULT_MODEL_KEY if no matching display name is found.
|
83 |
-
|
84 |
-
Parameters:
|
85 |
-
- display: The display name of the model as a string.
|
86 |
-
- MODEL_MAPPING: Dictionary mapping internal model keys to display names.
|
87 |
-
- DEFAULT_MODEL_KEY: The fallback model key to return if no match is found.
|
88 |
-
|
89 |
-
Returns:
|
90 |
-
- The internal model key string corresponding to the display name.
|
91 |
-
"""
|
92 |
-
# Iterate through the MODEL_MAPPING dictionary items and return the key where the value matches the display name
|
93 |
-
return next((k for k, v in MODEL_MAPPING.items() if v == display), DEFAULT_MODEL_KEY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/main/file_extractors.py
DELETED
@@ -1,393 +0,0 @@
|
|
1 |
-
#
|
2 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
-
# SPDX-License-Identifier: Apache-2.0
|
4 |
-
#
|
5 |
-
|
6 |
-
import pdfplumber # Library to extract text and tables from PDF files
|
7 |
-
import pytesseract # OCR tool to extract text from images
|
8 |
-
import docx # Library to read Microsoft Word (.docx) files
|
9 |
-
import zipfile # To handle zipped archives, used here to access embedded images in Word files
|
10 |
-
import io # Provides tools for handling byte streams, used to open images from bytes
|
11 |
-
import pandas as pd # Data analysis library, used here to handle tables from Excel and other files
|
12 |
-
import warnings # Used to suppress warnings during Excel file reading
|
13 |
-
import re # Regular expressions for text cleaning
|
14 |
-
|
15 |
-
from openpyxl import load_workbook # Excel file reading library, used for .xlsx files
|
16 |
-
from pptx import Presentation # Library to read Microsoft PowerPoint files
|
17 |
-
from PIL import Image, ImageEnhance, ImageFilter # Image processing libraries for OCR preprocessing
|
18 |
-
from pathlib import Path # Object-oriented filesystem paths
|
19 |
-
|
20 |
-
def clean_text(text):
|
21 |
-
"""
|
22 |
-
Clean and normalize extracted text to improve readability and remove noise.
|
23 |
-
|
24 |
-
This function performs several cleaning steps:
|
25 |
-
- Removes characters that are not letters, digits, spaces, or common punctuation.
|
26 |
-
- Removes isolated single letters which are often OCR errors or noise.
|
27 |
-
- Strips whitespace from each line and removes empty lines.
|
28 |
-
- Joins cleaned lines back into a single string separated by newlines.
|
29 |
-
|
30 |
-
Args:
|
31 |
-
text (str): Raw extracted text from any source.
|
32 |
-
|
33 |
-
Returns:
|
34 |
-
str: Cleaned and normalized text ready for display or further processing.
|
35 |
-
"""
|
36 |
-
# Remove all characters except letters, digits, spaces, and common punctuation marks
|
37 |
-
text = re.sub(r'[^a-zA-Z0-9\s.,?!():;\'"-]', '', text)
|
38 |
-
# Remove single isolated letters which are likely errors or noise from OCR
|
39 |
-
text = re.sub(r'\b[a-zA-Z]\b', '', text)
|
40 |
-
# Split text into lines, strip whitespace, and remove empty lines
|
41 |
-
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
42 |
-
# Join cleaned lines with newline characters
|
43 |
-
return "\n".join(lines)
|
44 |
-
|
45 |
-
def format_table(df, max_rows=10):
|
46 |
-
"""
|
47 |
-
Convert a pandas DataFrame into a clean, readable string representation of a table.
|
48 |
-
|
49 |
-
This function:
|
50 |
-
- Removes rows and columns that are completely empty to reduce clutter.
|
51 |
-
- Replaces any NaN values with empty strings for cleaner output.
|
52 |
-
- Limits the output to a maximum number of rows for brevity.
|
53 |
-
- Adds a note if there are more rows than displayed.
|
54 |
-
|
55 |
-
Args:
|
56 |
-
df (pandas.DataFrame): The table data to format.
|
57 |
-
max_rows (int): Maximum number of rows to display from the table.
|
58 |
-
|
59 |
-
Returns:
|
60 |
-
str: Formatted string representation of the table or empty string if no data.
|
61 |
-
"""
|
62 |
-
if df.empty:
|
63 |
-
return ""
|
64 |
-
# Remove rows and columns where all values are NaN to clean the table
|
65 |
-
df_clean = df.dropna(axis=0, how='all').dropna(axis=1, how='all')
|
66 |
-
# Replace remaining NaN values with empty strings for better readability
|
67 |
-
df_clean = df_clean.fillna('')
|
68 |
-
if df_clean.empty:
|
69 |
-
return ""
|
70 |
-
# Select only the first max_rows rows for display
|
71 |
-
display_df = df_clean.head(max_rows)
|
72 |
-
# Convert DataFrame to string without row indices
|
73 |
-
table_str = display_df.to_string(index=False)
|
74 |
-
# Append a message if there are more rows than displayed
|
75 |
-
if len(df_clean) > max_rows:
|
76 |
-
table_str += f"\n... ({len(df_clean) - max_rows} more rows)"
|
77 |
-
return table_str
|
78 |
-
|
79 |
-
def preprocess_image(img):
|
80 |
-
"""
|
81 |
-
Enhance an image to improve OCR accuracy by applying several preprocessing steps.
|
82 |
-
|
83 |
-
The preprocessing includes:
|
84 |
-
- Converting the image to grayscale to simplify colors.
|
85 |
-
- Increasing contrast to make text stand out more.
|
86 |
-
- Applying a median filter to reduce noise.
|
87 |
-
- Binarizing the image by thresholding to black and white.
|
88 |
-
|
89 |
-
Args:
|
90 |
-
img (PIL.Image.Image): The original image to preprocess.
|
91 |
-
|
92 |
-
Returns:
|
93 |
-
PIL.Image.Image: The processed image ready for OCR.
|
94 |
-
If an error occurs during processing, returns the original image.
|
95 |
-
"""
|
96 |
-
try:
|
97 |
-
# Convert image to grayscale mode
|
98 |
-
img = img.convert("L")
|
99 |
-
# Enhance contrast by a factor of 2 to make text clearer
|
100 |
-
enhancer = ImageEnhance.Contrast(img)
|
101 |
-
img = enhancer.enhance(2)
|
102 |
-
# Apply median filter to reduce noise and smooth the image
|
103 |
-
img = img.filter(ImageFilter.MedianFilter())
|
104 |
-
# Convert image to black and white using a threshold of 140
|
105 |
-
img = img.point(lambda x: 0 if x < 140 else 255, '1')
|
106 |
-
return img
|
107 |
-
except Exception:
|
108 |
-
# In case of any error, return the original image without changes
|
109 |
-
return img
|
110 |
-
|
111 |
-
def ocr_image(img):
|
112 |
-
"""
|
113 |
-
Extract text from an image using OCR after preprocessing to improve results.
|
114 |
-
|
115 |
-
This function:
|
116 |
-
- Preprocesses the image to enhance text visibility.
|
117 |
-
- Uses pytesseract with page segmentation mode 6 (assumes a single uniform block of text).
|
118 |
-
- Cleans the extracted text using the clean_text function.
|
119 |
-
|
120 |
-
Args:
|
121 |
-
img (PIL.Image.Image): The image from which to extract text.
|
122 |
-
|
123 |
-
Returns:
|
124 |
-
str: The cleaned OCR-extracted text. Returns empty string if OCR fails.
|
125 |
-
"""
|
126 |
-
try:
|
127 |
-
# Preprocess image to improve OCR quality
|
128 |
-
img = preprocess_image(img)
|
129 |
-
# Perform OCR using pytesseract with English language and specified config
|
130 |
-
text = pytesseract.image_to_string(img, lang='eng', config='--psm 6')
|
131 |
-
# Clean the OCR output to remove noise and normalize text
|
132 |
-
text = clean_text(text)
|
133 |
-
return text
|
134 |
-
except Exception:
|
135 |
-
# Return empty string if OCR fails for any reason
|
136 |
-
return ""
|
137 |
-
|
138 |
-
def extract_pdf_content(fp):
|
139 |
-
"""
|
140 |
-
Extract text and tables from a PDF file, including OCR on embedded images.
|
141 |
-
|
142 |
-
This function:
|
143 |
-
- Opens the PDF file and iterates through each page.
|
144 |
-
- Extracts and cleans text from each page.
|
145 |
-
- Performs OCR on images embedded in pages to extract any text within images.
|
146 |
-
- Extracts tables from pages and formats them as readable text.
|
147 |
-
- Handles exceptions by appending error messages to the content.
|
148 |
-
|
149 |
-
Args:
|
150 |
-
fp (str or Path): File path to the PDF document.
|
151 |
-
|
152 |
-
Returns:
|
153 |
-
str: Combined extracted text, OCR results, and formatted tables from the PDF.
|
154 |
-
"""
|
155 |
-
content = ""
|
156 |
-
try:
|
157 |
-
with pdfplumber.open(fp) as pdf:
|
158 |
-
for i, page in enumerate(pdf.pages, 1):
|
159 |
-
# Extract text from the current page, defaulting to empty string if None
|
160 |
-
text = page.extract_text() or ""
|
161 |
-
# Clean extracted text and add page header
|
162 |
-
content += f"Page {i} Text:\n{clean_text(text)}\n\n"
|
163 |
-
# If there are images on the page, perform OCR on each
|
164 |
-
if page.images:
|
165 |
-
# Create an image object of the page with 300 dpi resolution for cropping
|
166 |
-
img_obj = page.to_image(resolution=300)
|
167 |
-
for img in page.images:
|
168 |
-
# Define bounding box coordinates for the image on the page
|
169 |
-
bbox = (img["x0"], img["top"], img["x1"], img["bottom"])
|
170 |
-
# Crop the image from the page image
|
171 |
-
cropped = img_obj.original.crop(bbox)
|
172 |
-
# Perform OCR on the cropped image
|
173 |
-
ocr_text = ocr_image(cropped)
|
174 |
-
if ocr_text:
|
175 |
-
# Append OCR text with page and image reference
|
176 |
-
content += f"[OCR Text from image on page {i}]:\n{ocr_text}\n\n"
|
177 |
-
# Extract tables from the page
|
178 |
-
tables = page.extract_tables()
|
179 |
-
for idx, table in enumerate(tables, 1):
|
180 |
-
if table:
|
181 |
-
# Convert table list to DataFrame using first row as header
|
182 |
-
df = pd.DataFrame(table[1:], columns=table[0])
|
183 |
-
# Format and append the table text
|
184 |
-
content += f"Table {idx} on page {i}:\n{format_table(df)}\n\n"
|
185 |
-
except Exception as e:
|
186 |
-
# Append error message if PDF reading fails
|
187 |
-
content += f"\n[Error reading PDF {fp}: {e}]"
|
188 |
-
# Return the combined content with whitespace trimmed
|
189 |
-
return content.strip()
|
190 |
-
|
191 |
-
def extract_docx_content(fp):
|
192 |
-
"""
|
193 |
-
Extract text, tables, and OCR text from images embedded in a Microsoft Word (.docx) file.
|
194 |
-
|
195 |
-
This function:
|
196 |
-
- Reads paragraphs and tables from the document.
|
197 |
-
- Cleans and formats extracted text and tables.
|
198 |
-
- Opens the .docx file as a zip archive to extract embedded images.
|
199 |
-
- Performs OCR on embedded images to extract any text they contain.
|
200 |
-
- Handles exceptions and appends error messages if reading fails.
|
201 |
-
|
202 |
-
Args:
|
203 |
-
fp (str or Path): File path to the Word document.
|
204 |
-
|
205 |
-
Returns:
|
206 |
-
str: Combined extracted paragraphs, tables, and OCR text from embedded images.
|
207 |
-
"""
|
208 |
-
content = ""
|
209 |
-
try:
|
210 |
-
# Load the Word document
|
211 |
-
doc = docx.Document(fp)
|
212 |
-
# Extract and clean all non-empty paragraphs
|
213 |
-
paragraphs = [para.text.strip() for para in doc.paragraphs if para.text.strip()]
|
214 |
-
if paragraphs:
|
215 |
-
content += "Paragraphs:\n" + "\n".join(paragraphs) + "\n\n"
|
216 |
-
# Extract tables from the document
|
217 |
-
tables = []
|
218 |
-
for table in doc.tables:
|
219 |
-
rows = []
|
220 |
-
for row in table.rows:
|
221 |
-
# Extract and clean text from each cell in the row
|
222 |
-
cells = [cell.text.strip() for cell in row.cells]
|
223 |
-
rows.append(cells)
|
224 |
-
if rows:
|
225 |
-
# Convert rows to DataFrame using first row as header
|
226 |
-
df = pd.DataFrame(rows[1:], columns=rows[0])
|
227 |
-
tables.append(df)
|
228 |
-
# Format and append each extracted table
|
229 |
-
for i, df in enumerate(tables, 1):
|
230 |
-
content += f"Table {i}:\n{format_table(df)}\n\n"
|
231 |
-
# Open the .docx file as a zip archive to access embedded media files
|
232 |
-
with zipfile.ZipFile(fp) as z:
|
233 |
-
for file in z.namelist():
|
234 |
-
# Look for images inside the word/media directory
|
235 |
-
if file.startswith("word/media/"):
|
236 |
-
data = z.read(file)
|
237 |
-
try:
|
238 |
-
# Open image from bytes
|
239 |
-
img = Image.open(io.BytesIO(data))
|
240 |
-
# Perform OCR on the image
|
241 |
-
ocr_text = ocr_image(img)
|
242 |
-
if ocr_text:
|
243 |
-
# Append OCR text extracted from embedded image
|
244 |
-
content += f"[OCR Text from embedded image]:\n{ocr_text}\n\n"
|
245 |
-
except Exception:
|
246 |
-
# Ignore errors in image processing to continue extraction
|
247 |
-
pass
|
248 |
-
except Exception as e:
|
249 |
-
# Append error message if Word document reading fails
|
250 |
-
content += f"\n[Error reading Microsoft Word {fp}: {e}]"
|
251 |
-
# Return combined content trimmed of extra whitespace
|
252 |
-
return content.strip()
|
253 |
-
|
254 |
-
def extract_excel_content(fp):
|
255 |
-
"""
|
256 |
-
Extract readable table content from Microsoft Excel files (.xlsx, .xls).
|
257 |
-
|
258 |
-
This function:
|
259 |
-
- Reads all sheets in the Excel file.
|
260 |
-
- Converts each sheet to a formatted table string.
|
261 |
-
- Suppresses warnings during reading to avoid clutter.
|
262 |
-
- Does not attempt to extract images to avoid errors.
|
263 |
-
- Handles exceptions by appending error messages.
|
264 |
-
|
265 |
-
Args:
|
266 |
-
fp (str or Path): File path to the Excel workbook.
|
267 |
-
|
268 |
-
Returns:
|
269 |
-
str: Combined formatted tables from all sheets in the workbook.
|
270 |
-
"""
|
271 |
-
content = ""
|
272 |
-
try:
|
273 |
-
# Suppress warnings such as openpyxl deprecation or data type warnings
|
274 |
-
with warnings.catch_warnings():
|
275 |
-
warnings.simplefilter("ignore")
|
276 |
-
# Read all sheets into a dictionary of DataFrames using openpyxl engine
|
277 |
-
sheets = pd.read_excel(fp, sheet_name=None, engine='openpyxl')
|
278 |
-
# Iterate over each sheet and format its content
|
279 |
-
for sheet_name, df in sheets.items():
|
280 |
-
content += f"Sheet: {sheet_name}\n"
|
281 |
-
content += format_table(df) + "\n\n"
|
282 |
-
except Exception as e:
|
283 |
-
# Append error message if Excel reading fails
|
284 |
-
content += f"\n[Error reading Microsoft Excel {fp}: {e}]"
|
285 |
-
# Return combined sheet contents trimmed of whitespace
|
286 |
-
return content.strip()
|
287 |
-
|
288 |
-
def extract_pptx_content(fp):
|
289 |
-
"""
|
290 |
-
Extract text, tables, and OCR text from images in Microsoft PowerPoint (.pptx) files.
|
291 |
-
|
292 |
-
This function:
|
293 |
-
- Reads each slide in the presentation.
|
294 |
-
- Extracts text from shapes and tables on each slide.
|
295 |
-
- Performs OCR on images embedded in shapes.
|
296 |
-
- Handles exceptions and appends error messages if reading fails.
|
297 |
-
|
298 |
-
Args:
|
299 |
-
fp (str or Path): File path to the PowerPoint presentation.
|
300 |
-
|
301 |
-
Returns:
|
302 |
-
str: Combined extracted text, tables, and OCR results from all slides.
|
303 |
-
"""
|
304 |
-
content = ""
|
305 |
-
try:
|
306 |
-
# Load the PowerPoint presentation
|
307 |
-
prs = Presentation(fp)
|
308 |
-
# Iterate through each slide by index starting at 1
|
309 |
-
for i, slide in enumerate(prs.slides, 1):
|
310 |
-
slide_texts = []
|
311 |
-
# Iterate through all shapes on the slide
|
312 |
-
for shape in slide.shapes:
|
313 |
-
# Extract and clean text from shapes that have text attribute
|
314 |
-
if hasattr(shape, "text") and shape.text.strip():
|
315 |
-
slide_texts.append(shape.text.strip())
|
316 |
-
# Check if the shape is a picture (shape_type 13) with an image
|
317 |
-
if shape.shape_type == 13 and hasattr(shape, "image") and shape.image:
|
318 |
-
try:
|
319 |
-
# Open image from the shape's binary blob data
|
320 |
-
img = Image.open(io.BytesIO(shape.image.blob))
|
321 |
-
# Perform OCR on the image
|
322 |
-
ocr_text = ocr_image(img)
|
323 |
-
if ocr_text:
|
324 |
-
# Append OCR text extracted from the image
|
325 |
-
slide_texts.append(f"[OCR Text from image]:\n{ocr_text}")
|
326 |
-
except Exception:
|
327 |
-
# Ignore errors in image OCR to continue processing
|
328 |
-
pass
|
329 |
-
# Add slide text or note if no text found
|
330 |
-
if slide_texts:
|
331 |
-
content += f"Slide {i} Text:\n" + "\n".join(slide_texts) + "\n\n"
|
332 |
-
else:
|
333 |
-
content += f"Slide {i} Text:\nNo text found on this slide.\n\n"
|
334 |
-
# Extract tables from shapes that have tables
|
335 |
-
for shape in slide.shapes:
|
336 |
-
if shape.has_table:
|
337 |
-
rows = []
|
338 |
-
table = shape.table
|
339 |
-
# Extract text from each cell in the table rows
|
340 |
-
for row in table.rows:
|
341 |
-
cells = [cell.text.strip() for cell in row.cells]
|
342 |
-
rows.append(cells)
|
343 |
-
if rows:
|
344 |
-
# Convert rows to DataFrame using first row as header
|
345 |
-
df = pd.DataFrame(rows[1:], columns=rows[0])
|
346 |
-
# Format and append the table text
|
347 |
-
content += f"Table on slide {i}:\n{format_table(df)}\n\n"
|
348 |
-
except Exception as e:
|
349 |
-
# Append error message if PowerPoint reading fails
|
350 |
-
content += f"\n[Error reading Microsoft PowerPoint {fp}: {e}]"
|
351 |
-
# Return combined slide content trimmed of whitespace
|
352 |
-
return content.strip()
|
353 |
-
|
354 |
-
def extract_file_content(fp):
|
355 |
-
"""
|
356 |
-
Determine the file type based on its extension and extract text content accordingly.
|
357 |
-
|
358 |
-
This function supports:
|
359 |
-
- PDF files with text, tables, and OCR on images.
|
360 |
-
- Microsoft Word documents with paragraphs, tables, and OCR on embedded images.
|
361 |
-
- Microsoft Excel workbooks with formatted sheet tables.
|
362 |
-
- Microsoft PowerPoint presentations with slide text, tables, and OCR on images.
|
363 |
-
- Other file types are attempted to be read as plain UTF-8 text.
|
364 |
-
|
365 |
-
Args:
|
366 |
-
fp (str or Path): File path to the document to extract content from.
|
367 |
-
|
368 |
-
Returns:
|
369 |
-
str: Extracted and cleaned text content from the file, or an error message.
|
370 |
-
"""
|
371 |
-
# Get the file extension in lowercase to identify file type
|
372 |
-
ext = Path(fp).suffix.lower()
|
373 |
-
if ext == ".pdf":
|
374 |
-
# Extract content from PDF files
|
375 |
-
return extract_pdf_content(fp)
|
376 |
-
elif ext in [".doc", ".docx"]:
|
377 |
-
# Extract content from Word documents
|
378 |
-
return extract_docx_content(fp)
|
379 |
-
elif ext in [".xlsx", ".xls"]:
|
380 |
-
# Extract content from Excel workbooks
|
381 |
-
return extract_excel_content(fp)
|
382 |
-
elif ext in [".ppt", ".pptx"]:
|
383 |
-
# Extract content from PowerPoint presentations
|
384 |
-
return extract_pptx_content(fp)
|
385 |
-
else:
|
386 |
-
try:
|
387 |
-
# Attempt to read unknown file types as plain UTF-8 text
|
388 |
-
text = Path(fp).read_text(encoding="utf-8")
|
389 |
-
# Clean the extracted text before returning
|
390 |
-
return clean_text(text)
|
391 |
-
except Exception as e:
|
392 |
-
# Return error message if reading fails
|
393 |
-
return f"\n[Error reading file {fp}: {e}]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/main/gradio.py
DELETED
@@ -1,332 +0,0 @@
|
|
1 |
-
#
|
2 |
-
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
-
# SPDX-License-Identifier: Apache-2.0
|
4 |
-
#
|
5 |
-
|
6 |
-
import gradio as gr # Import Gradio library for building the web UI
|
7 |
-
import asyncio # Import asyncio for asynchronous programming
|
8 |
-
|
9 |
-
from pathlib import Path # Import Path for filesystem path manipulations
|
10 |
-
from config import * # Import all configuration constants and variables
|
11 |
-
from src.cores.session import create_session, ensure_stop_event, get_model_key # Import session management utilities
|
12 |
-
from src.main.file_extractors import extract_file_content # Import function to extract content from uploaded files
|
13 |
-
from src.cores.client import chat_with_model_async # Import async chat function with AI model
|
14 |
-
|
15 |
-
async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
|
16 |
-
"""
|
17 |
-
Asynchronous handler for processing user input submissions.
|
18 |
-
Supports multi-modal input including text and file uploads.
|
19 |
-
Extracts content from uploaded files and appends it to user text input.
|
20 |
-
Streams AI-generated responses back to the UI, updating chat history live.
|
21 |
-
Allows graceful stopping of response generation upon user request.
|
22 |
-
|
23 |
-
Parameters:
|
24 |
-
- multi: dict containing user text input and uploaded files
|
25 |
-
- history: list of previous chat messages (user and AI)
|
26 |
-
- model_display: selected AI model identifier
|
27 |
-
- sess: current session object managing state and cancellation
|
28 |
-
- custom_prompt: user-defined system instructions
|
29 |
-
- deep_search: boolean flag to enable extended search capabilities
|
30 |
-
|
31 |
-
Yields:
|
32 |
-
- Updated chat history and UI state for real-time interaction
|
33 |
-
"""
|
34 |
-
ensure_stop_event(sess) # Ensure the session has a stop event initialized
|
35 |
-
sess.stop_event.clear() # Clear any previous stop signals
|
36 |
-
sess.cancel_token["cancelled"] = False # Reset cancellation flag
|
37 |
-
|
38 |
-
# Extract text and files from multimodal input dictionary
|
39 |
-
msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
|
40 |
-
|
41 |
-
# If no input text or files, reset UI input and return early
|
42 |
-
if not msg_input["text"] and not msg_input["files"]:
|
43 |
-
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
44 |
-
return
|
45 |
-
|
46 |
-
# Initialize combined input string with extracted file contents
|
47 |
-
inp = ""
|
48 |
-
for f in msg_input["files"]:
|
49 |
-
# Support both dict format or direct file path string
|
50 |
-
fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
|
51 |
-
# Append extracted file content with spacing
|
52 |
-
inp += f"```\n{extract_file_content(fp)}\n``` \n\n\n"
|
53 |
-
|
54 |
-
# Append user text input if present
|
55 |
-
if msg_input["text"]:
|
56 |
-
inp += msg_input["text"]
|
57 |
-
|
58 |
-
# Append user input to chat history
|
59 |
-
history.append([inp, ""]) # placeholder
|
60 |
-
|
61 |
-
# Yield updated history and disable input while AI is responding
|
62 |
-
yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
|
63 |
-
|
64 |
-
# Create queue for streaming AI response chunks
|
65 |
-
queue = asyncio.Queue()
|
66 |
-
|
67 |
-
async def background():
|
68 |
-
"""
|
69 |
-
This coroutine handles streaming responses from an AI model asynchronously.
|
70 |
-
It processes two types of streamed data separately: 'reasoning' chunks and 'content' chunks.
|
71 |
-
The function supports graceful cancellation if a stop event or cancel token is triggered in the session.
|
72 |
-
|
73 |
-
Reasoning text is accumulated until content streaming starts, after which reasoning is ignored.
|
74 |
-
Special tags <think> and </think> are managed to mark reasoning sections for UI display.
|
75 |
-
Content chunks are streamed and accumulated separately, with incremental UI updates.
|
76 |
-
|
77 |
-
When streaming ends, any open reasoning tags are closed properly.
|
78 |
-
Finally, the function signals completion by putting None into the queue and returns the full content response.
|
79 |
-
"""
|
80 |
-
reasoning = "" # String to accumulate reasoning text chunks
|
81 |
-
responses = "" # String to accumulate content text chunks
|
82 |
-
content_started = False # Flag to indicate if content streaming has begun
|
83 |
-
ignore_reasoning = False # Flag to ignore reasoning after content starts streaming
|
84 |
-
think_opened = False # Flag to track if reasoning <think> tag has been sent
|
85 |
-
|
86 |
-
# Asynchronously iterate over streamed response chunks from the AI model
|
87 |
-
async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
|
88 |
-
# Break the loop if user requested stop or cancellation is flagged
|
89 |
-
if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
|
90 |
-
break
|
91 |
-
|
92 |
-
if typ == "reasoning":
|
93 |
-
# Append reasoning chunk unless ignoring reasoning after content started
|
94 |
-
if ignore_reasoning:
|
95 |
-
continue
|
96 |
-
# Handle opening <think> tag for reasoning
|
97 |
-
if chunk.strip() == "<think>":
|
98 |
-
if not think_opened:
|
99 |
-
think_opened = True # Mark that reasoning tag has been opened
|
100 |
-
continue # Skip sending the tag itself to UI
|
101 |
-
if not think_opened:
|
102 |
-
# If reasoning tag not yet opened, prepend it and mark as opened
|
103 |
-
reasoning += "<think>\n" + chunk
|
104 |
-
think_opened = True
|
105 |
-
else:
|
106 |
-
# Append reasoning chunk normally
|
107 |
-
reasoning += chunk
|
108 |
-
# Send current reasoning content to queue for UI update (without sending tag again)
|
109 |
-
await queue.put(("reasoning", reasoning))
|
110 |
-
|
111 |
-
elif typ == "content":
|
112 |
-
if not content_started:
|
113 |
-
# On first content chunk, mark content started and ignore further reasoning
|
114 |
-
content_started = True
|
115 |
-
ignore_reasoning = True
|
116 |
-
if think_opened:
|
117 |
-
# Close reasoning tag before sending content
|
118 |
-
reasoning += "\n</think>\n\n"
|
119 |
-
await queue.put(("reasoning", reasoning)) # Update UI with closed reasoning
|
120 |
-
else:
|
121 |
-
# No reasoning was sent, clear reasoning display in UI
|
122 |
-
await queue.put(("reasoning", ""))
|
123 |
-
# Start accumulating content and send initial content to UI replacing placeholder
|
124 |
-
responses = chunk
|
125 |
-
await queue.put(("replace", responses))
|
126 |
-
else:
|
127 |
-
# Append subsequent content chunks and update UI incrementally
|
128 |
-
responses += chunk
|
129 |
-
await queue.put(("append", responses))
|
130 |
-
|
131 |
-
# If stream ends without content, close reasoning tag if it was opened
|
132 |
-
if think_opened and not content_started:
|
133 |
-
reasoning += "\n</think>\n\n"
|
134 |
-
await queue.put(("reasoning", reasoning))
|
135 |
-
|
136 |
-
# Signal completion of streaming by putting None into the queue
|
137 |
-
await queue.put(None)
|
138 |
-
# Return the full accumulated content response
|
139 |
-
return responses
|
140 |
-
|
141 |
-
bg_task = asyncio.create_task(background()) # Start background streaming task
|
142 |
-
stop_task = asyncio.create_task(sess.stop_event.wait()) # Task to wait for stop event
|
143 |
-
pending_tasks = {bg_task, stop_task} # Track pending async tasks
|
144 |
-
|
145 |
-
try:
|
146 |
-
while True:
|
147 |
-
queue_task = asyncio.create_task(queue.get()) # Task to get next queued update
|
148 |
-
pending_tasks.add(queue_task)
|
149 |
-
|
150 |
-
# Wait for either stop event or new queue item
|
151 |
-
done, _ = await asyncio.wait({stop_task, queue_task}, return_when=asyncio.FIRST_COMPLETED)
|
152 |
-
|
153 |
-
for task in done:
|
154 |
-
pending_tasks.discard(task)
|
155 |
-
|
156 |
-
if task is stop_task:
|
157 |
-
# User requested stop, cancel background task and update UI accordingly
|
158 |
-
sess.cancel_token["cancelled"] = True
|
159 |
-
bg_task.cancel()
|
160 |
-
try:
|
161 |
-
await bg_task
|
162 |
-
except asyncio.CancelledError:
|
163 |
-
pass
|
164 |
-
# Update last message with cancellation notice
|
165 |
-
history[-1][1] = RESPONSES["RESPONSE_1"]
|
166 |
-
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
167 |
-
return
|
168 |
-
|
169 |
-
result = task.result()
|
170 |
-
if result is None:
|
171 |
-
# Streaming finished, stop iteration
|
172 |
-
raise StopAsyncIteration
|
173 |
-
|
174 |
-
action, text = result
|
175 |
-
# Update last message content in history with streamed text chunk
|
176 |
-
history[-1][1] = text
|
177 |
-
# Yield updated history and UI state to refresh chat display
|
178 |
-
yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
|
179 |
-
|
180 |
-
except StopAsyncIteration:
|
181 |
-
# Normal completion of streaming
|
182 |
-
pass
|
183 |
-
|
184 |
-
finally:
|
185 |
-
# Cancel any remaining pending tasks to clean up
|
186 |
-
for task in pending_tasks:
|
187 |
-
task.cancel()
|
188 |
-
await asyncio.gather(*pending_tasks, return_exceptions=True)
|
189 |
-
|
190 |
-
# After completion, reset UI input to ready state
|
191 |
-
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
192 |
-
|
193 |
-
def toggle_deep_search(deep_search_value, history, sess, prompt, model):
|
194 |
-
"""
|
195 |
-
Toggle the deep search checkbox state.
|
196 |
-
Maintains current chat history and session for production use.
|
197 |
-
|
198 |
-
Parameters:
|
199 |
-
- deep_search_value: new checkbox boolean value
|
200 |
-
- history: current chat history
|
201 |
-
- sess: current session object
|
202 |
-
- prompt: current system instructions
|
203 |
-
- model: currently selected model
|
204 |
-
|
205 |
-
Returns:
|
206 |
-
- Unchanged history, session, prompt, model
|
207 |
-
- Updated deep search checkbox UI state
|
208 |
-
"""
|
209 |
-
return history, sess, prompt, model, gr.update(value=deep_search_value)
|
210 |
-
|
211 |
-
def change_model(new):
|
212 |
-
"""
|
213 |
-
Handler to change the selected AI model.
|
214 |
-
Resets chat history and creates a new session.
|
215 |
-
Updates system instructions and deep search checkbox visibility.
|
216 |
-
Deep search is only enabled for the default model.
|
217 |
-
|
218 |
-
Parameters:
|
219 |
-
- new: newly selected model identifier
|
220 |
-
|
221 |
-
Returns:
|
222 |
-
- Empty chat history list
|
223 |
-
- New session object
|
224 |
-
- New model identifier
|
225 |
-
- Corresponding system instructions string
|
226 |
-
- Deep search checkbox reset to False
|
227 |
-
- UI update for deep search checkbox visibility
|
228 |
-
"""
|
229 |
-
visible = new == MODEL_CHOICES[0] # Deep search visible only for default model
|
230 |
-
|
231 |
-
# Get system instructions for new model or fallback to default instructions
|
232 |
-
default_prompt = SYSTEM_PROMPT_MAPPING.get(get_model_key(new, MODEL_MAPPING, DEFAULT_MODEL_KEY), SYSTEM_PROMPT_DEFAULT)
|
233 |
-
|
234 |
-
# Clear chat, create new session, reset deep search, update UI visibility
|
235 |
-
return [], create_session(), new, default_prompt, False, gr.update(visible=visible)
|
236 |
-
|
237 |
-
def stop_response(history, sess):
|
238 |
-
"""
|
239 |
-
Handler to stop ongoing AI response generation.
|
240 |
-
Sets cancellation flags and updates the last message to a cancellation notice.
|
241 |
-
|
242 |
-
Parameters:
|
243 |
-
- history: current chat history list
|
244 |
-
- sess: current session object
|
245 |
-
|
246 |
-
Returns:
|
247 |
-
- Updated chat history with cancellation message
|
248 |
-
- None for input box reset
|
249 |
-
- New session object for fresh state
|
250 |
-
"""
|
251 |
-
ensure_stop_event(sess) # Ensure stop event exists in session
|
252 |
-
sess.stop_event.set() # Signal stop event to cancel ongoing tasks
|
253 |
-
sess.cancel_token["cancelled"] = True # Mark cancellation flag
|
254 |
-
|
255 |
-
if history:
|
256 |
-
# Replace last AI response with cancellation message
|
257 |
-
history[-1][1] = RESPONSES["RESPONSE_1"]
|
258 |
-
|
259 |
-
return history, None, create_session()
|
260 |
-
|
261 |
-
def launch_ui():
|
262 |
-
"""
|
263 |
-
Launch the Gradio UI for the chatbot application.
|
264 |
-
Sets up the UI components, event handlers, and starts the server.
|
265 |
-
Installs required OCR dependencies for file content extraction.
|
266 |
-
"""
|
267 |
-
# ============================
|
268 |
-
# System Setup
|
269 |
-
# ============================
|
270 |
-
|
271 |
-
# Install Tesseract OCR and dependencies for extracting text from images
|
272 |
-
import os
|
273 |
-
os.system("apt-get update -q -y && \
|
274 |
-
apt-get install -q -y tesseract-ocr \
|
275 |
-
tesseract-ocr-eng tesseract-ocr-ind \
|
276 |
-
libleptonica-dev libtesseract-dev"
|
277 |
-
)
|
278 |
-
|
279 |
-
# Create Gradio Blocks container for full UI layout
|
280 |
-
with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
|
281 |
-
# State variables to hold chat history, session, selected model, and instructions
|
282 |
-
user_history = gr.State([])
|
283 |
-
user_session = gr.State(create_session())
|
284 |
-
selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
|
285 |
-
J_A_R_V_I_S = gr.State("")
|
286 |
-
|
287 |
-
# Chatbot UI
|
288 |
-
with gr.Column():
|
289 |
-
chatbot = gr.Chatbot(label=AI_TYPES["AI_TYPE_1"], show_copy_button=True, scale=1, elem_id=AI_TYPES["AI_TYPE_2"], examples=JARVIS_INIT, allow_tags=["think", "thinking"])
|
290 |
-
|
291 |
-
# User input
|
292 |
-
msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count=None, file_types=None, sources=[])
|
293 |
-
|
294 |
-
# Sidebar on left for model selection and deep search toggle
|
295 |
-
with gr.Sidebar(open=False):
|
296 |
-
deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
|
297 |
-
# When deep search checkbox changes, call toggle_deep_search handler
|
298 |
-
deep_search.change(fn=toggle_deep_search, inputs=[deep_search, user_history, user_session, J_A_R_V_I_S, selected_model], outputs=[chatbot, user_session, J_A_R_V_I_S, selected_model, deep_search])
|
299 |
-
gr.Markdown() # Add spacing line
|
300 |
-
model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
|
301 |
-
|
302 |
-
# Sidebar on right for notices and additional information
|
303 |
-
with gr.Sidebar(open=False, position="right"):
|
304 |
-
gr.Markdown(NOTICES)
|
305 |
-
|
306 |
-
# When model selection changes, call change_model handler
|
307 |
-
model_radio.change(fn=change_model, inputs=[model_radio], outputs=[user_history, user_session, selected_model, J_A_R_V_I_S, deep_search, deep_search])
|
308 |
-
|
309 |
-
# Event handler for selecting example messages in chatbot UI
|
310 |
-
def on_example_select(evt: gr.SelectData):
|
311 |
-
return evt.value
|
312 |
-
|
313 |
-
chatbot.example_select(fn=on_example_select, inputs=[], outputs=[msg]).then(
|
314 |
-
fn=respond_async,
|
315 |
-
inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search],
|
316 |
-
outputs=[chatbot, msg, user_session]
|
317 |
-
)
|
318 |
-
|
319 |
-
# Clear chat button handler resets chat, session, instructions, model, and history
|
320 |
-
def clear_chat(history, sess, prompt, model):
|
321 |
-
return [], create_session(), prompt, model, []
|
322 |
-
|
323 |
-
chatbot.clear(fn=clear_chat, inputs=[user_history, user_session, J_A_R_V_I_S, selected_model], outputs=[chatbot, user_session, J_A_R_V_I_S, selected_model, user_history])
|
324 |
-
|
325 |
-
# Submit user message triggers respond_async to generate AI response
|
326 |
-
msg.submit(fn=respond_async, inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search], outputs=[chatbot, msg, user_session], api_name=INTERNAL_AI_GET_SERVER)
|
327 |
-
|
328 |
-
# Stop button triggers stop_response handler to cancel ongoing AI generation
|
329 |
-
msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
|
330 |
-
|
331 |
-
# Launch
|
332 |
-
jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb", mcp_server=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/tools/__init__.py
ADDED
File without changes
|
src/tools/audio.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import asyncio # Import asyncio to enable asynchronous waiting and retries
|
7 |
+
import httpx # Import the httpx library to perform asynchronous HTTP requests efficiently
|
8 |
+
from urllib.parse import quote # Import the quote function to safely encode strings for use in URLs
|
9 |
+
from src.utils.ip_generator import generate_ip # Import a custom utility function to generate random IP addresses
|
10 |
+
from config import auth # Import authentication configuration or credentials from the config module
|
11 |
+
from src.utils.tools import initialize_tools # Import a utility function to initialize and retrieve tool endpoints or resources
|
12 |
+
|
13 |
+
# Define a class named AudioGeneration to encapsulate functionalities related to generating audio content
|
14 |
+
class AudioGeneration:
|
15 |
+
# This class provides methods to create audio files based on text instructions and voice parameters
|
16 |
+
|
17 |
+
@staticmethod # Decorator indicating that the following method does not depend on instance state and can be called on the class itself
|
18 |
+
# Define an asynchronous method to create audio from a text instruction, optionally specifying a voice style
|
19 |
+
async def create_audio(generate_audio_instruction: str, voice: str = "echo") -> str:
|
20 |
+
"""
|
21 |
+
Generate an audio file URL by sending a request to an audio generation service.
|
22 |
+
This method will keep retrying until a successful response with status code 200 and audio content is received.
|
23 |
+
|
24 |
+
Args:
|
25 |
+
generate_audio_instruction (str): The textual instruction or content to convert into audio.
|
26 |
+
voice (str, optional): The voice style or effect to apply on the generated audio. Defaults to "echo".
|
27 |
+
|
28 |
+
Returns:
|
29 |
+
str: The URL to the generated audio file if successful.
|
30 |
+
|
31 |
+
Raises:
|
32 |
+
Exception: If the audio generation continuously fails after retries (optional, currently infinite retry).
|
33 |
+
"""
|
34 |
+
# Encode the text instruction to make it safe for inclusion in a URL path segment
|
35 |
+
generate_audio_instruct = quote(generate_audio_instruction)
|
36 |
+
|
37 |
+
# Initialize tools and retrieve the audio generation service endpoint from the returned tuple
|
38 |
+
_, _, audio_tool = initialize_tools()
|
39 |
+
|
40 |
+
# Construct the full URL by appending the encoded instruction to the audio tool's base URL
|
41 |
+
url = f"{audio_tool}/{generate_audio_instruct}"
|
42 |
+
|
43 |
+
# Define query parameters for the HTTP request specifying the model and voice to use for audio generation
|
44 |
+
params = {
|
45 |
+
"model": "openai-audio", # Specify the audio generation model to be used by the service
|
46 |
+
"voice": voice # Specify the desired voice style or effect
|
47 |
+
}
|
48 |
+
|
49 |
+
# Create an asynchronous HTTP client with no timeout limit to perform the request
|
50 |
+
async with httpx.AsyncClient(timeout=None) as client:
|
51 |
+
# Enter an infinite loop to keep retrying the request until success criteria are met
|
52 |
+
while True:
|
53 |
+
# Define HTTP headers for the request, including random IP address to simulate different client origins
|
54 |
+
headers = {
|
55 |
+
"X-Forwarded-For": generate_ip() # Generate and set a random IP address for the request header
|
56 |
+
}
|
57 |
+
|
58 |
+
# Send a GET request to the audio generation service with specified URL, parameters, and headers
|
59 |
+
resp = await client.get(url, params=params, headers=headers)
|
60 |
+
|
61 |
+
# Check if the response status code indicates success and the content type is an audio MPEG stream
|
62 |
+
if resp.status_code == 200 and 'audio/mpeg' in resp.headers.get('Content-Type', ''):
|
63 |
+
# Return the final URL of the generated audio resource as a string
|
64 |
+
return str(resp.url)
|
65 |
+
else:
|
66 |
+
# If the response is not successful, wait for a short delay before retrying to avoid hammering the server
|
67 |
+
await asyncio.sleep(15) # Pause for 15 second before retrying the request
|
68 |
+
# The loop will continue and try again without closing the connection prematurely
|
src/tools/deep_search.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import requests # Import the requests library to perform HTTP requests synchronously
|
7 |
+
from src.utils.ip_generator import generate_ip # Import function to generate random IP addresses for request headers
|
8 |
+
|
9 |
+
# Define a class named SearchTools to encapsulate functionalities related to deep search
|
10 |
+
class SearchTools:
|
11 |
+
# This class provides methods to connect to the web
|
12 |
+
|
13 |
+
"""
|
14 |
+
A class providing tools to perform web searches and read content from URLs using various search engines
|
15 |
+
and a reader API service.
|
16 |
+
|
17 |
+
Attributes:
|
18 |
+
searxng_url (str): Base URL for the SearXNG search proxy service.
|
19 |
+
baidu_url (str): Base URL for Baidu search engine.
|
20 |
+
timeout (int): Timeout duration in seconds for HTTP requests.
|
21 |
+
reader_api (str): Base URL for the reader API service used to extract content from URLs.
|
22 |
+
|
23 |
+
Methods:
|
24 |
+
read_url(url): Asynchronously reads and returns the textual content of the specified URL using the reader API.
|
25 |
+
search(query, engine): Asynchronously performs a web search with the given query on the specified search engine,
|
26 |
+
returning the raw HTML response text.
|
27 |
+
"""
|
28 |
+
|
29 |
+
def __init__(self):
|
30 |
+
"""
|
31 |
+
Initialize the SearchTools instance with predefined URLs and timeout settings.
|
32 |
+
"""
|
33 |
+
self.searxng_url = "https://paulgo.io/search" # URL for the SearXNG search proxy service
|
34 |
+
self.baidu_url = "https://www.baidu.com/s" # URL for Baidu search engine
|
35 |
+
self.timeout = 30 # Timeout in seconds for HTTP requests to avoid long hanging connections
|
36 |
+
self.reader_api = "https://r.jina.ai/" # Reader API endpoint to extract readable content from URLs
|
37 |
+
|
38 |
+
async def read_url(self, url: str) -> str:
|
39 |
+
"""
|
40 |
+
Asynchronously read and retrieve the textual content of a given URL using the reader API.
|
41 |
+
|
42 |
+
Args:
|
43 |
+
url (str): The URL of the webpage to read content from.
|
44 |
+
|
45 |
+
Returns:
|
46 |
+
str: The textual content extracted from the URL if successful.
|
47 |
+
None: If the request fails or an exception occurs.
|
48 |
+
"""
|
49 |
+
try:
|
50 |
+
data = {"url": url} # Prepare POST data with the target URL
|
51 |
+
# Send a synchronous POST request to the reader API with the URL data and timeout
|
52 |
+
response = requests.post(self.reader_api, data=data, timeout=self.timeout)
|
53 |
+
response.raise_for_status() # Raise an exception if the response status is an HTTP error
|
54 |
+
return response.text # Return the textual content of the response
|
55 |
+
except Exception:
|
56 |
+
# Return None if any error occurs during the request or response processing
|
57 |
+
return None
|
58 |
+
|
59 |
+
async def search(self, query: str, engine: str = "google") -> str:
|
60 |
+
"""
|
61 |
+
Asynchronously perform a web search for the given query using the specified search engine.
|
62 |
+
|
63 |
+
Args:
|
64 |
+
query (str): The search query string.
|
65 |
+
engine (str, optional): The search engine to use. Supported values are "google" and "baidu".
|
66 |
+
Defaults to "google".
|
67 |
+
|
68 |
+
Returns:
|
69 |
+
str: The raw HTML content of the search results page if successful.
|
70 |
+
None: If the request fails or an exception occurs.
|
71 |
+
"""
|
72 |
+
try:
|
73 |
+
if engine == "baidu":
|
74 |
+
# Construct the URL for Baidu search by appending the query parameter 'wd' with the search term
|
75 |
+
url = f"{self.reader_api}{self.baidu_url}?wd={query}"
|
76 |
+
# Set the HTTP header to target the main content container of Baidu search results
|
77 |
+
headers = {
|
78 |
+
"X-Target-Selector": "#content_left",
|
79 |
+
"X-Forwarded-For": generate_ip() # Random IP address to simulate different client origins
|
80 |
+
}
|
81 |
+
else:
|
82 |
+
# For Google or other engines, define a prefix for the search command (!go for Google, !bi for Bing)
|
83 |
+
prefix = "!go" if engine == "google" else "!bi"
|
84 |
+
# Construct the URL for SearXNG search proxy with the prefixed query
|
85 |
+
url = f"{self.reader_api}{self.searxng_url}?q={prefix} {query}"
|
86 |
+
# Set the HTTP header to target the URLs container in the search results
|
87 |
+
headers = {
|
88 |
+
"X-Target-Selector": "#urls",
|
89 |
+
"X-Forwarded-For": generate_ip() # Random IP address to simulate different client origins
|
90 |
+
}
|
91 |
+
|
92 |
+
# Send a synchronous GET request to the constructed URL with headers and timeout
|
93 |
+
response = requests.get(url, headers=headers, timeout=self.timeout)
|
94 |
+
response.raise_for_status() # Raise an exception if the response status is an HTTP error
|
95 |
+
return response.text # Return the raw HTML content of the search results
|
96 |
+
except Exception:
|
97 |
+
# Return None if any error occurs during the request or response processing
|
98 |
+
return None
|
src/tools/image.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import httpx # Import httpx library for performing asynchronous HTTP requests efficiently
|
7 |
+
from urllib.parse import quote # Import quote function to safely encode strings for use in URLs
|
8 |
+
from typing import Optional # Import Optional type hint for parameters that can be None
|
9 |
+
from src.utils.ip_generator import generate_ip # Import custom utility to generate random IP addresses for request headers
|
10 |
+
from src.utils.tools import initialize_tools # Import utility function to initialize and retrieve tool endpoints
|
11 |
+
|
12 |
+
# Define a class named ImageGeneration to encapsulate functionalities related to generating image content
|
13 |
+
class ImageGeneration:
|
14 |
+
# This class provides methods to create image files based on text instructions
|
15 |
+
|
16 |
+
"""
|
17 |
+
A class to handle image generation requests to an external image generation service.
|
18 |
+
|
19 |
+
Attributes:
|
20 |
+
FORMATS (dict): A dictionary mapping image format names to their (width, height) dimensions.
|
21 |
+
|
22 |
+
Methods:
|
23 |
+
create_image: Asynchronously generates an image based on a textual instruction and parameters,
|
24 |
+
returning the URL of the generated image.
|
25 |
+
"""
|
26 |
+
|
27 |
+
# Image formats
|
28 |
+
FORMATS = {
|
29 |
+
"default": (1024, 1024), # Default square image size (width x height)
|
30 |
+
"square": (1024, 1024), # Square image format with equal width and height
|
31 |
+
"landscape": (1024, 768), # Landscape format with wider width than height
|
32 |
+
"landscape_large": (1440, 1024), # Larger landscape format with increased resolution
|
33 |
+
"portrait": (768, 1024), # Portrait format with taller height than width
|
34 |
+
"portrait_large": (1024, 1440), # Larger portrait format with increased resolution
|
35 |
+
}
|
36 |
+
|
37 |
+
@staticmethod # Decorator indicating that the following method does not depend on instance state and can be called on the class itself
|
38 |
+
# Define an asynchronous method to create image from a text instruction
|
39 |
+
async def create_image(
|
40 |
+
generate_image_instruction: str, # Text instruction describing the image to generate
|
41 |
+
image_format: str = "default", # Desired image format key from FORMATS dictionary
|
42 |
+
model: Optional[str] = "flux-realism", # Optional model name for image generation; defaults to 'flux-realism'
|
43 |
+
seed: Optional[int] = None, # Optional seed value for randomization control in image generation
|
44 |
+
nologo: bool = True, # Whether to generate image without logo watermark; defaults to True
|
45 |
+
private: bool = True, # Whether the generated image should be private; defaults to True
|
46 |
+
enhance: bool = True, # Whether to apply enhancement filters to the generated image; defaults to True
|
47 |
+
) -> str:
|
48 |
+
"""
|
49 |
+
Asynchronously generate an image URL by sending a request to the image generation service.
|
50 |
+
This method will keep retrying until a successful response with status code 200 is received.
|
51 |
+
|
52 |
+
Args:
|
53 |
+
generate_image_instruction (str): The textual instruction or description for the desired image.
|
54 |
+
image_format (str, optional): The format key specifying image dimensions. Defaults to "default".
|
55 |
+
model (Optional[str], optional): The image generation model to use. Defaults to "flux-realism".
|
56 |
+
seed (Optional[int], optional): Seed for randomization to reproduce images. Defaults to None.
|
57 |
+
nologo (bool, optional): Flag to exclude logo watermark. Defaults to True.
|
58 |
+
private (bool, optional): Flag to mark image as private. Defaults to True.
|
59 |
+
enhance (bool, optional): Flag to apply image enhancement. Defaults to True.
|
60 |
+
|
61 |
+
Returns:
|
62 |
+
str: The URL of the generated image if the request is successful.
|
63 |
+
|
64 |
+
Raises:
|
65 |
+
ValueError: If the specified image_format is not supported.
|
66 |
+
Exception: If the image generation continuously fails (currently infinite retry).
|
67 |
+
"""
|
68 |
+
# Validate that the requested image format exists in the FORMATS dictionary
|
69 |
+
if image_format not in ImageGeneration.FORMATS:
|
70 |
+
raise ValueError("Invalid image format.")
|
71 |
+
|
72 |
+
# Retrieve width and height based on the requested image format
|
73 |
+
width, height = ImageGeneration.FORMATS[image_format]
|
74 |
+
|
75 |
+
# Initialize tools and retrieve the image generation service endpoint
|
76 |
+
_, image_tool, _ = initialize_tools()
|
77 |
+
|
78 |
+
# Encode the image instruction to safely include it in the URL path
|
79 |
+
generate_image_instruct = quote(generate_image_instruction)
|
80 |
+
|
81 |
+
# Construct the full URL for the image generation request by appending the encoded instruction
|
82 |
+
url = f"{image_tool}{generate_image_instruct}" # Full endpoint URL for image generation
|
83 |
+
|
84 |
+
# Prepare query parameters including image dimensions, model, and flags converted to string "true"/"false"
|
85 |
+
params = {
|
86 |
+
"width": width, # Image width parameter
|
87 |
+
"height": height, # Image height parameter
|
88 |
+
"model": model, # Model name for image generation
|
89 |
+
"nologo": "true" if nologo else "false", # Flag to exclude logo watermark as string
|
90 |
+
"private": "true" if private else "false", # Flag to mark image as private as string
|
91 |
+
"enhance": "true" if enhance else "false" # Flag to apply enhancement as string
|
92 |
+
}
|
93 |
+
|
94 |
+
# Include seed parameter if provided to control randomness in image generation
|
95 |
+
if seed is not None:
|
96 |
+
params["seed"] = seed # Add seed to parameters to reproduce images
|
97 |
+
|
98 |
+
# Prepare HTTP headers with a generated random IP to simulate different client origins
|
99 |
+
headers = {
|
100 |
+
"X-Forwarded-For": generate_ip() # Random IP address for request header to simulate client origin
|
101 |
+
}
|
102 |
+
|
103 |
+
# Create an asynchronous HTTP client with no timeout limit to perform the request
|
104 |
+
async with httpx.AsyncClient(timeout=None) as client:
|
105 |
+
# Keep retrying the request until a successful response with status 200 is received
|
106 |
+
while True:
|
107 |
+
# Send a GET request to the image generation service with URL, parameters, and headers
|
108 |
+
resp = await client.get(url, params=params, headers=headers)
|
109 |
+
|
110 |
+
# Check if the response status code indicates success
|
111 |
+
if resp.status_code == 200:
|
112 |
+
# Return the URL of the generated image as a string
|
113 |
+
return str(resp.url)
|
114 |
+
else:
|
115 |
+
# Wait briefly before retrying to avoid overwhelming the server
|
116 |
+
await asyncio.sleep(15) # Pause 15 second before retrying
|
src/ui/__init__.py
ADDED
File without changes
|
src/ui/interface.py
ADDED
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import gradio as gr # Import the Gradio library to build interactive web interfaces for machine learning applications
|
7 |
+
from src.core.parameter import parameters # Import the 'parameters' function from the core parameter module, which returns model parameter settings based on reasoning mode
|
8 |
+
from src.client.chat_handler import respond # Import the 'respond' function from the chat handler module, responsible for generating AI assistant responses
|
9 |
+
from config import model, meta_tags # Import 'model' dictionary containing available model precision options and their details, and 'meta_tags' containing HTML meta tag data
|
10 |
+
|
11 |
+
# Gradio
|
12 |
+
def ui():
|
13 |
+
"""
|
14 |
+
Constructs the Gradio user interface for the J.A.R.V.I.S. AI assistant application.
|
15 |
+
|
16 |
+
This function sets up a web app with a sidebar for configuring model parameters and a main chat interface
|
17 |
+
for user interaction. It returns the Gradio Blocks object representing the entire app.
|
18 |
+
"""
|
19 |
+
# Create a Gradio Blocks container that fills the entire available height and width of the browser window
|
20 |
+
with gr.Blocks(fill_height=True, fill_width=True, head=meta_tags) as app:
|
21 |
+
# Create a sidebar panel on the left side, initially closed, to hold model configuration controls
|
22 |
+
with gr.Sidebar(open=False):
|
23 |
+
# Dropdown menu for selecting the model precision from the keys of the 'model' dictionary
|
24 |
+
model_precision = gr.Dropdown(
|
25 |
+
choices=list(model.keys()), # List of available model precision options, e.g., "F16", "F32"
|
26 |
+
label="Model Precision", # Label displayed above the dropdown menu
|
27 |
+
info=(
|
28 |
+
# Tooltip explaining the tradeoff between speed and accuracy based on precision choice
|
29 |
+
"The smaller the value, the faster the response but less accurate. "
|
30 |
+
"Conversely, the larger the value, the response is slower but more accurate."
|
31 |
+
),
|
32 |
+
value="F16" # Default selected precision value
|
33 |
+
)
|
34 |
+
|
35 |
+
# Checkbox to enable or disable reasoning mode, which toggles the AI's "thinking" capability
|
36 |
+
reasoning = gr.Checkbox(
|
37 |
+
label="Reasoning", # Label shown next to the checkbox
|
38 |
+
info="Switching between thinking and non-thinking mode.", # Tooltip describing the feature
|
39 |
+
value=True # Default state is enabled (checked)
|
40 |
+
)
|
41 |
+
|
42 |
+
# Slider controlling the 'Temperature' parameter, affecting randomness in AI responses, initially non-interactive
|
43 |
+
temperature = gr.Slider(
|
44 |
+
minimum=0.0, # Minimum slider value
|
45 |
+
maximum=2.0, # Maximum slider value
|
46 |
+
step=0.01, # Increment step size
|
47 |
+
label="Temperature", # Label for the slider
|
48 |
+
interactive=False # User cannot directly adjust this slider, updated dynamically
|
49 |
+
)
|
50 |
+
|
51 |
+
# Slider controlling the 'Top K' parameter, which limits the number of highest probability tokens considered, non-interactive initially
|
52 |
+
top_k = gr.Slider(
|
53 |
+
minimum=0,
|
54 |
+
maximum=100,
|
55 |
+
step=1,
|
56 |
+
label="Top K",
|
57 |
+
interactive=False
|
58 |
+
)
|
59 |
+
|
60 |
+
# Slider for 'Min P' parameter, representing minimum cumulative probability threshold, non-interactive initially
|
61 |
+
min_p = gr.Slider(
|
62 |
+
minimum=0.0,
|
63 |
+
maximum=1.0,
|
64 |
+
step=0.01,
|
65 |
+
label="Min P",
|
66 |
+
interactive=False
|
67 |
+
)
|
68 |
+
|
69 |
+
# Slider for 'Top P' parameter, controlling nucleus sampling probability, non-interactive initially
|
70 |
+
top_p = gr.Slider(
|
71 |
+
minimum=0.0,
|
72 |
+
maximum=1.0,
|
73 |
+
step=0.01,
|
74 |
+
label="Top P",
|
75 |
+
interactive=False
|
76 |
+
)
|
77 |
+
|
78 |
+
# Slider for 'Repetition Penalty' parameter to reduce repetitive text generation, non-interactive initially
|
79 |
+
repetition_penalty = gr.Slider(
|
80 |
+
minimum=0.1,
|
81 |
+
maximum=2.0,
|
82 |
+
step=0.01,
|
83 |
+
label="Repetition Penalty",
|
84 |
+
interactive=False
|
85 |
+
)
|
86 |
+
|
87 |
+
# Define a function to update the model parameter sliders based on the reasoning checkbox state
|
88 |
+
def update_parameters(switching):
|
89 |
+
"""
|
90 |
+
Retrieve updated model parameter values based on reasoning mode.
|
91 |
+
|
92 |
+
Args:
|
93 |
+
switching (bool): Current state of the reasoning checkbox.
|
94 |
+
|
95 |
+
Returns:
|
96 |
+
tuple: Updated values for temperature, top_k, min_p, top_p, and repetition_penalty sliders.
|
97 |
+
"""
|
98 |
+
# Call the 'parameters' function passing the reasoning state to get new parameter values
|
99 |
+
return parameters(switching)
|
100 |
+
|
101 |
+
# Set up an event listener to update parameter sliders when the reasoning checkbox state changes
|
102 |
+
reasoning.change(
|
103 |
+
fn=update_parameters, # Function to call on checkbox state change
|
104 |
+
inputs=[reasoning], # Input is the reasoning checkbox's current value
|
105 |
+
outputs=[temperature, top_k, min_p, top_p, repetition_penalty] # Update these sliders with new values
|
106 |
+
)
|
107 |
+
|
108 |
+
# Initialize the parameter sliders with values corresponding to the default reasoning checkbox state
|
109 |
+
values = parameters(reasoning.value)
|
110 |
+
temperature.value, top_k.value, min_p.value, top_p.value, repetition_penalty.value = values
|
111 |
+
|
112 |
+
# Checkbox to enable or disable the image generation feature in the chat interface
|
113 |
+
image_generation = gr.Checkbox(
|
114 |
+
label="Image Generation", # Label displayed next to the checkbox
|
115 |
+
info=(
|
116 |
+
# Tooltip explaining how to trigger image generation via chat commands
|
117 |
+
"Type <i><b>/image</b></i> followed by the instructions to start generating an image."
|
118 |
+
),
|
119 |
+
value=True # Enabled by default
|
120 |
+
)
|
121 |
+
|
122 |
+
# Checkbox to enable or disable the audio generation feature in the chat interface
|
123 |
+
audio_generation = gr.Checkbox(
|
124 |
+
label="Audio Generation",
|
125 |
+
info=(
|
126 |
+
"Type <i><b>/audio</b></i> followed by the instructions to start generating audio."
|
127 |
+
),
|
128 |
+
value=True
|
129 |
+
)
|
130 |
+
|
131 |
+
# Checkbox to enable or disable the deep web search feature in the chat interface
|
132 |
+
search_generation = gr.Checkbox(
|
133 |
+
label="Deep Search",
|
134 |
+
info=(
|
135 |
+
"Type <i><b>/dp</b></i> followed by the instructions to search the web."
|
136 |
+
),
|
137 |
+
value=True
|
138 |
+
)
|
139 |
+
|
140 |
+
# Create the main chat interface where users interact with the AI assistant
|
141 |
+
gr.ChatInterface(
|
142 |
+
fn=respond, # Function called to generate responses to user inputs
|
143 |
+
additional_inputs=[
|
144 |
+
# Pass the current states of all configuration controls as additional inputs to the respond function
|
145 |
+
model_precision,
|
146 |
+
temperature,
|
147 |
+
top_k,
|
148 |
+
min_p,
|
149 |
+
top_p,
|
150 |
+
repetition_penalty,
|
151 |
+
reasoning,
|
152 |
+
image_generation,
|
153 |
+
audio_generation,
|
154 |
+
search_generation
|
155 |
+
],
|
156 |
+
type='tuples', # The format of the messages
|
157 |
+
chatbot=gr.Chatbot(
|
158 |
+
label="J.A.R.V.I.S.", # Title label displayed above the chat window
|
159 |
+
show_copy_button=True, # Show a button allowing users to copy chat messages
|
160 |
+
scale=1, # Scale factor for the chatbot UI size
|
161 |
+
type='tuples' # Duplicate form Chat Interface to Chatbot
|
162 |
+
),
|
163 |
+
examples=[
|
164 |
+
# Predefined example inputs to help users quickly test the assistant's features
|
165 |
+
["Please introduce yourself."],
|
166 |
+
["/audio Could you explain what Artificial Intelligence (AI) is?"],
|
167 |
+
["/audio What is Hugging Face?"],
|
168 |
+
["/dp Please search for the J.A.R.V.I.S. AI model on Hugging Face."],
|
169 |
+
["/dp What is the capital city of Indonesia?"],
|
170 |
+
["/image Create an image of a futuristic city."],
|
171 |
+
["/image Create a cartoon-style image of a man."],
|
172 |
+
["What day is it today, what's the date, and what time is it?"],
|
173 |
+
['/audio Say "I am J.A.R.V.I.S.".'],
|
174 |
+
["Please generate a highly complex code snippet on any topic."],
|
175 |
+
["Explain about quantum computers."]
|
176 |
+
],
|
177 |
+
cache_examples=False, # Disable caching of example outputs to always generate fresh responses
|
178 |
+
multimodal=False, # Disable support for multimodal inputs such as images or audio files
|
179 |
+
fill_height=True, # Duplicate from Blocks to Chat Interface
|
180 |
+
fill_width=True, # Duplicate from Blocks to Chat Interface
|
181 |
+
head=meta_tags # Duplicate from Blocks to Chat Interface
|
182 |
+
)
|
183 |
+
# Return the complete Gradio app object for launching or embedding
|
184 |
+
return app
|
src/ui/reasoning.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
def styles(reasoning: str, content: str, expanded: bool = False) -> str:
|
7 |
+
"""
|
8 |
+
Create a visually captivating and interactive HTML <details> block that elegantly presents reasoning text
|
9 |
+
inside a beautifully styled collapsible container with enhanced user experience features.
|
10 |
+
|
11 |
+
This function generates a sophisticated collapsible section using HTML and inline CSS, designed to grab
|
12 |
+
attention with a modern, polished look. It leverages subtle shadows, smooth transitions, and vibrant styling
|
13 |
+
to make the reasoning content stand out while maintaining excellent readability on dark backgrounds.
|
14 |
+
The container uses the default background color to blend seamlessly with its surroundings, ensuring
|
15 |
+
versatility in different UI contexts. The summary header includes an engaging emoji and changes color on hover
|
16 |
+
to invite user interaction. The reasoning text is carefully spaced and styled with a clean font and
|
17 |
+
crisp white color for maximum clarity. The collapsible block can start expanded or collapsed based on the
|
18 |
+
'expanded' parameter. The 'content' parameter remains unused here to keep the function signature consistent
|
19 |
+
with similar functions.
|
20 |
+
|
21 |
+
Args:
|
22 |
+
reasoning (str): The main explanation or reasoning text to be displayed inside the collapsible block.
|
23 |
+
This content is wrapped in a styled <div> for clear presentation.
|
24 |
+
content (str): An unused parameter retained for compatibility with other functions sharing this signature.
|
25 |
+
expanded (bool): Determines if the collapsible block is initially open (True) or closed (False) when rendered.
|
26 |
+
|
27 |
+
Returns:
|
28 |
+
str: A complete HTML snippet string containing a <details> element with inline CSS that styles it as
|
29 |
+
a sleek, interactive collapsible container. The styling includes padding, rounded corners,
|
30 |
+
a subtle but dynamic shadow, smooth hover effects on the summary header, and carefully sized fonts
|
31 |
+
with white text for optimal contrast and readability.
|
32 |
+
"""
|
33 |
+
# Conditionally add the 'open' attribute to the <details> element if expanded is True,
|
34 |
+
# so the block starts expanded when rendered in the browser.
|
35 |
+
open_attr = "open" if expanded else ""
|
36 |
+
|
37 |
+
# Return the full HTML string with inline CSS styles applied to create an eye-catching, user-friendly collapsible block.
|
38 |
+
# The <details> element acts as a toggleable container with smooth rounded corners and a dynamic shadow that subtly intensifies on hover.
|
39 |
+
# The <summary> element serves as the clickable header with a brain emoji to visually represent reasoning,
|
40 |
+
# featuring a color transition on hover to encourage user interaction.
|
41 |
+
# The reasoning text is enclosed in a <div> with generous spacing, a delicate top border, and crisp white text for excellent readability.
|
42 |
+
# The entire block uses a clean, modern sans-serif font and avoids any background color override to maintain design flexibility.
|
43 |
+
return f"""
|
44 |
+
<details {open_attr} style="
|
45 |
+
padding: 16px; /* Comfortable inner spacing for a spacious feel */
|
46 |
+
border-radius: 12px; /* Smoothly rounded corners for a modern, friendly appearance */
|
47 |
+
margin: 12px 0; /* Vertical margin to separate from other page elements */
|
48 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.35); /* Deeper, softly diffused shadow to create a subtle floating effect */
|
49 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Crisp, modern font stack for excellent readability */
|
50 |
+
color: white; /* Bright white text to stand out clearly on dark or varied backgrounds */
|
51 |
+
transition: box-shadow 0.3s ease-in-out; /* Smooth shadow transition for dynamic visual feedback on hover */
|
52 |
+
">
|
53 |
+
<summary style="
|
54 |
+
font-weight: 700; /* Bold font weight to make the summary header prominent */
|
55 |
+
color: white; /* White text color for consistent contrast */
|
56 |
+
font-size: 14px !important; /* Slightly larger font size for better emphasis */
|
57 |
+
cursor: pointer; /* Pointer cursor to indicate the summary is clickable */
|
58 |
+
user-select: none; /* Prevent text selection on click for cleaner interaction */
|
59 |
+
transition: color 0.25s ease-in-out; /* Smooth color transition when hovering */
|
60 |
+
" onmouseover="this.style.color='#FFD700';" onmouseout="this.style.color='white';">
|
61 |
+
🧠 Reasoning
|
62 |
+
</summary>
|
63 |
+
<div style="
|
64 |
+
margin-top: 12px; /* Clear separation between summary and content */
|
65 |
+
padding-top: 8px; /* Additional padding for comfortable reading space */
|
66 |
+
border-top: 1.5px solid rgba(255, 255, 255, 0.25); /* Elegant translucent top border to visually separate content */
|
67 |
+
font-size: 11px !important; /* Slightly larger font size for improved readability */
|
68 |
+
line-height: 1.7; /* Increased line height for comfortable text flow */
|
69 |
+
color: white; /* Maintain white text color for clarity */
|
70 |
+
letter-spacing: 0.02em; /* Slight letter spacing to enhance text legibility */
|
71 |
+
">
|
72 |
+
{reasoning} <!-- Reasoning -->
|
73 |
+
</div>
|
74 |
+
</details>
|
75 |
+
"""
|
src/utils/__init__.py
ADDED
File without changes
|
src/utils/helper.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
from datetime import datetime, timedelta # Import datetime and timedelta classes to work with dates and time durations
|
7 |
+
|
8 |
+
# Dictionary to track busy status of servers with their busy expiration timestamps
|
9 |
+
busy = {}
|
10 |
+
|
11 |
+
def mark(server: str):
|
12 |
+
"""
|
13 |
+
Mark a server as busy by setting its busy expiration time to one hour from the current UTC time.
|
14 |
+
|
15 |
+
Args:
|
16 |
+
server (str): The identifier or name of the server to mark as busy.
|
17 |
+
|
18 |
+
Explanation:
|
19 |
+
This function updates the 'busy' dictionary by associating the given server
|
20 |
+
with a timestamp representing one hour from the current UTC time.
|
21 |
+
This indicates that the server is considered busy until that future time.
|
22 |
+
"""
|
23 |
+
# Set the busy expiration time for the specified server to current UTC time plus one hour
|
24 |
+
busy[server] = datetime.utcnow() + timedelta(hours=1)
|
src/utils/ip_generator.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import random # Import the random module to generate random numbers
|
7 |
+
|
8 |
+
def generate_ip() -> str:
|
9 |
+
"""
|
10 |
+
Generate a random IPv4 address as a string.
|
11 |
+
|
12 |
+
Returns:
|
13 |
+
str: A randomly generated IPv4 address in dotted decimal notation,
|
14 |
+
where each octet is a number between 1 and 254 inclusive.
|
15 |
+
|
16 |
+
Explanation:
|
17 |
+
This function creates an IP address by generating four random integers,
|
18 |
+
each representing one octet of the IP address.
|
19 |
+
The range 1 to 254 is used to avoid special addresses like 0 (network) and 255 (broadcast).
|
20 |
+
The four octets are then joined together with dots to form a standard IPv4 address string.
|
21 |
+
"""
|
22 |
+
# Generate four random integers between 1 and 254 inclusive, convert each to string,
|
23 |
+
# then join them with '.' to form a valid IPv4 address string
|
24 |
+
return ".".join(str(random.randint(1, 254)) for _ in range(4))
|
src/utils/session_mapping.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import random # Import random module to enable random selection from a list
|
7 |
+
from datetime import datetime # Import datetime class to work with current UTC time
|
8 |
+
from typing import Dict, List # Import type hints for dictionaries and lists (not explicitly used here but imported)
|
9 |
+
from config import auth # Import authentication configuration, likely a list of host dictionaries with credentials
|
10 |
+
from src.utils.helper import busy, mark # Import 'busy' dictionary and 'mark' function to track and update host busy status
|
11 |
+
|
12 |
+
# Dictionary to map session IDs to their assigned host information
|
13 |
+
mapping = {}
|
14 |
+
|
15 |
+
def get_host(session_id: str):
|
16 |
+
"""
|
17 |
+
Retrieve or assign a host for the given session ID.
|
18 |
+
|
19 |
+
Args:
|
20 |
+
session_id (str): A unique identifier for the current session.
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
dict: The selected host dictionary from the auth configuration.
|
24 |
+
|
25 |
+
Raises:
|
26 |
+
Exception: If no available hosts are found to assign.
|
27 |
+
|
28 |
+
Explanation:
|
29 |
+
This function manages host assignment per session. If the session ID already has a host assigned,
|
30 |
+
it returns that host immediately. Otherwise, it filters the list of hosts from 'auth' to find those
|
31 |
+
that are currently not busy or whose busy period has expired (based on the 'busy' dictionary).
|
32 |
+
From the available hosts, it randomly selects one, records the assignment in 'mapping',
|
33 |
+
marks the selected host as busy for one hour, and returns the selected host.
|
34 |
+
"""
|
35 |
+
# Check if the session ID already has an assigned host in the mapping dictionary
|
36 |
+
if session_id in mapping:
|
37 |
+
# Return the previously assigned host for this session
|
38 |
+
return mapping[session_id]
|
39 |
+
|
40 |
+
# Get the current UTC time to compare against busy timestamps
|
41 |
+
now = datetime.utcnow()
|
42 |
+
|
43 |
+
# Filter hosts from auth that are either not marked busy or whose busy period has expired
|
44 |
+
connect = [
|
45 |
+
h for h in auth
|
46 |
+
if h["jarvis"] not in busy or busy[h["jarvis"]] <= now
|
47 |
+
]
|
48 |
+
|
49 |
+
# If no hosts are available after filtering, raise an exception to indicate no hosts can be assigned
|
50 |
+
if not connect:
|
51 |
+
raise Exception("No available hosts to assign.")
|
52 |
+
|
53 |
+
# Randomly select one host from the list of available hosts
|
54 |
+
selected = random.choice(connect)
|
55 |
+
|
56 |
+
# Map the session ID to the selected host for future reference
|
57 |
+
mapping[session_id] = selected
|
58 |
+
|
59 |
+
# Mark the selected host as busy for the next hour to prevent immediate reassignment
|
60 |
+
mark(selected["jarvis"])
|
61 |
+
|
62 |
+
# Return the selected host dictionary
|
63 |
+
return selected
|
src/utils/tools.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#
|
2 |
+
# SPDX-FileCopyrightText: Hadad <[email protected]>
|
3 |
+
# SPDX-License-Identifier: Apache-2.0
|
4 |
+
#
|
5 |
+
|
6 |
+
import random # Import random module to enable random selection from a list
|
7 |
+
from datetime import datetime # Import datetime class to work with current UTC time
|
8 |
+
from config import auth # Import authentication configuration, likely a list of host dictionaries with credentials
|
9 |
+
from src.utils.helper import busy, mark # Import 'busy' dictionary and 'mark' function to track and update host busy status
|
10 |
+
|
11 |
+
def initialize_tools():
|
12 |
+
"""
|
13 |
+
Initialize and select available tools (endpoints) from the configured hosts.
|
14 |
+
|
15 |
+
Returns:
|
16 |
+
tuple: A tuple containing three elements:
|
17 |
+
- tool_setup (str): The endpoint or configuration for the main tool setup.
|
18 |
+
- image_tool (str): The endpoint URL for image generation services.
|
19 |
+
- audio_tool (str): The endpoint URL for audio generation services.
|
20 |
+
|
21 |
+
Raises:
|
22 |
+
Exception: If no available hosts are found or if any required tool endpoint is missing.
|
23 |
+
|
24 |
+
Explanation:
|
25 |
+
This function filters the list of hosts from 'auth' to find those not currently busy or whose busy period has expired.
|
26 |
+
It randomly selects one available host, marks it as busy for one hour,
|
27 |
+
and retrieves the required tool endpoints ('done', 'image', 'audio') from the selected host's configuration.
|
28 |
+
If any of these endpoints are missing, it raises an exception.
|
29 |
+
Finally, it returns the three tool endpoints as a tuple.
|
30 |
+
"""
|
31 |
+
# Get the current UTC time for busy status comparison
|
32 |
+
now = datetime.utcnow()
|
33 |
+
|
34 |
+
# Filter hosts that are either not marked busy or whose busy period has expired
|
35 |
+
available = [
|
36 |
+
item for item in auth
|
37 |
+
if item["jarvis"] not in busy or busy[item["jarvis"]] <= now
|
38 |
+
]
|
39 |
+
|
40 |
+
# Raise an exception if no hosts are currently available
|
41 |
+
if not available:
|
42 |
+
raise Exception("No available hosts to initialize tools.")
|
43 |
+
|
44 |
+
# Randomly select one host from the available list
|
45 |
+
selected = random.choice(available)
|
46 |
+
|
47 |
+
# Mark the selected host as busy for the next hour to prevent immediate reassignment
|
48 |
+
mark(selected["jarvis"])
|
49 |
+
|
50 |
+
# Retrieve the tool endpoints from the selected host's configuration dictionary
|
51 |
+
tool_setup = selected.get("done") # Main tool setup endpoint or configuration
|
52 |
+
image_tool = selected.get("image") # Image generation service endpoint
|
53 |
+
audio_tool = selected.get("audio") # Audio generation service endpoint
|
54 |
+
|
55 |
+
# Verify that all required tool endpoints are present, raise exception if any is missing
|
56 |
+
if not tool_setup or not image_tool or not audio_tool:
|
57 |
+
raise Exception("Selected host is missing required tool endpoints.")
|
58 |
+
|
59 |
+
# Return the three tool endpoints as a tuple
|
60 |
+
return tool_setup, image_tool, audio_tool
|
61 |
+
|
62 |
+
# Initialize the tools by selecting an available host and retrieving its endpoints
|
63 |
+
tool_setup, image_tool, audio_tool = initialize_tools()
|