hadadrjt commited on
Commit
6b509f7
·
1 Parent(s): ce9d223

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 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.0
9
  app_file: app.py
10
  pinned: true
11
  short_description: Just a Rather Very Intelligent System
12
  models:
13
- - hadadrjt/JARVIS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
- # SPDX-License-Identifier: Apache-2.0
4
- #
5
 
6
- from src.main.gradio import launch_ui # Import the function responsible for starting the graphical user interface
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
- launch_ui() # Start the graphical user interface for the application
 
 
 
 
 
 
 
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 access environment variables and interact with the operating system
7
- import json # Import json module to parse JSON strings into Python objects
8
 
9
- # Load initial welcome messages for the system from the environment variable "HELLO"
10
- # If "HELLO" is not set, default to an empty JSON array represented as "[]"
11
- # This variable typically contains a list of greeting messages or initialization instructions for the AI
12
- JARVIS_INIT = json.loads(os.getenv("HELLO", "[]"))
13
 
14
- # Deep Search service configuration variables loaded from environment variables
15
- # DEEP_SEARCH_PROVIDER_HOST holds the URL or IP address of the deep search service provider
16
- DEEP_SEARCH_PROVIDER_HOST = os.getenv("DEEP_SEARCH_PROVIDER_HOST")
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
- # Internal AI server configuration and system instructions
23
- # INTERNAL_AI_GET_SERVER stores the endpoint URL or IP address for internal AI GET requests
24
- INTERNAL_AI_GET_SERVER = os.getenv("INTERNAL_AI_GET_SERVER")
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
- META_TAGS = os.getenv("META_TAGS")
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
+ "![Generated Image]({image_link})\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()