File size: 8,656 Bytes
f99ad65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#
# SPDX-FileCopyrightText: Hadad <[email protected]>
# SPDX-License-Identifier: Apache-2.0
#

import gradio as gr
import asyncio

from pathlib import Path
from src.config import *
from src.cores.session import create_session, ensure_stop_event, get_model_key
from src.main.file_extractors import extract_file_content
from src.cores.client import chat_with_model_async

async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
    """
    Main async handler for user input submission.
    Supports text + file uploads (multi-modal input).
    Extracts file content and appends to user input.
    Streams AI responses back to UI, updating chat history live.
    Allows stopping response generation gracefully.
    """
    ensure_stop_event(sess)
    sess.stop_event.clear()
    sess.cancel_token["cancelled"] = False
    # Extract text and files from multimodal input
    msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
    # If no input, reset UI state and return
    if not msg_input["text"] and not msg_input["files"]:
        yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
        return
    # Initialize input with extracted file contents
    inp = ""
    for f in msg_input["files"]:
        # Support dict or direct file path
        fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
        inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
    # Append user text input if any
    if msg_input["text"]:
        inp += msg_input["text"]
    # Append user input to chat history with placeholder response
    history.append([inp, RESPONSES["RESPONSE_8"]])
    yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
    queue = asyncio.Queue()
    # Background async task to fetch streamed AI responses
    async def background():
        reasoning = ""
        responses = ""
        content_started = False
        ignore_reasoning = False
        async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
            if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
                break
            if typ == "reasoning":
                if ignore_reasoning:
                    continue
                reasoning += chunk
                await queue.put(("reasoning", reasoning))
            elif typ == "content":
                if not content_started:
                    content_started = True
                    ignore_reasoning = True
                    responses = chunk
                    await queue.put(("reasoning", ""))  # Clear reasoning on content start
                    await queue.put(("replace", responses))
                else:
                    responses += chunk
                    await queue.put(("append", responses))
        await queue.put(None)
        return responses
    bg_task = asyncio.create_task(background())
    stop_task = asyncio.create_task(sess.stop_event.wait())
    pending_tasks = {bg_task, stop_task}
    try:
        while True:
            queue_task = asyncio.create_task(queue.get())
            pending_tasks.add(queue_task)
            done, _ = await asyncio.wait({stop_task, queue_task}, return_when=asyncio.FIRST_COMPLETED)
            for task in done:
                pending_tasks.discard(task)
                if task is stop_task:
                    # User requested stop, cancel background task and update UI
                    sess.cancel_token["cancelled"] = True
                    bg_task.cancel()
                    try:
                        await bg_task
                    except asyncio.CancelledError:
                        pass
                    history[-1][1] = RESPONSES["RESPONSE_1"]
                    yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
                    return
                result = task.result()
                if result is None:
                    raise StopAsyncIteration
                action, text = result
                # Update last message content in history with streamed text
                history[-1][1] = text
                yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
    except StopAsyncIteration:
        pass
    finally:
        for task in pending_tasks:
            task.cancel()
        await asyncio.gather(*pending_tasks, return_exceptions=True)
    yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess

def toggle_deep_search(deep_search_value, history, sess, prompt, model):
    """
    Toggle deep search checkbox. Keeps chat intact for production compatibility.
    """
    return history, sess, prompt, model, gr.update(value=deep_search_value)

def change_model(new):
    """
    Handler to change selected AI model.
    Resets chat history and session.
    Updates system instructions and deep search checkbox visibility accordingly.
    Deep search is only available for default model.
    """
    visible = new == MODEL_CHOICES[0]
    default_prompt = SYSTEM_PROMPT_MAPPING.get(get_model_key(new, MODEL_MAPPING, DEFAULT_MODEL_KEY), SYSTEM_PROMPT_DEFAULT)
    # On model change, clear chat, create new session, reset deep search, update visibility
    return [], create_session(), new, default_prompt, False, gr.update(visible=visible)

def stop_response(history, sess):
    """
    Handler to stop ongoing AI response generation.
    Sets cancellation flags and updates last message to cancellation notice.
    """
    ensure_stop_event(sess)
    sess.stop_event.set()
    sess.cancel_token["cancelled"] = True
    if history:
        history[-1][1] = RESPONSES["RESPONSE_1"]
    return history, None, create_session()

def launch_ui():
    # ============================
    # System Setup
    # ============================

    # Install Tesseract OCR and dependencies for text extraction from images.
    import os
    os.system("apt-get update -q -y && \
               apt-get install -q -y tesseract-ocr \
               tesseract-ocr-eng tesseract-ocr-ind \
               libleptonica-dev libtesseract-dev"
    )

    with gr.Blocks(fill_height=True, fill_width=True, title=AI_TYPES["AI_TYPE_4"], head=META_TAGS) as jarvis:
        user_history = gr.State([])
        user_session = gr.State(create_session())
        selected_model = gr.State(MODEL_CHOICES[0] if MODEL_CHOICES else "")
        J_A_R_V_I_S = gr.State("")
        # Chatbot UI
        with gr.Column(): 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)
        # Deep search
        deep_search = gr.Checkbox(label=AI_TYPES["AI_TYPE_8"], value=False, info=AI_TYPES["AI_TYPE_9"], visible=True)
        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])
        # User's input
        msg = gr.MultimodalTextbox(show_label=False, placeholder=RESPONSES["RESPONSE_5"], interactive=True, file_count="single", file_types=ALLOWED_EXTENSIONS)
        # Sidebar to select AI models
        with gr.Sidebar(open=False): model_radio = gr.Radio(show_label=False, choices=MODEL_CHOICES, value=MODEL_CHOICES[0])
        # Models change
        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])
        # Initial welcome messages
        def on_example_select(evt: gr.SelectData): return evt.value
        chatbot.example_select(fn=on_example_select, inputs=[], outputs=[msg]).then(fn=respond_async, inputs=[msg, user_history, selected_model, user_session, J_A_R_V_I_S, deep_search], outputs=[chatbot, msg, user_session])
        # Clear chat
        def clear_chat(history, sess, prompt, model): return [], create_session(), prompt, model
        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])
        # Submit message
        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)
        # Stop message
        msg.stop(fn=stop_response, inputs=[user_history, user_session], outputs=[chatbot, msg, user_session])
    # Launch
    jarvis.queue(default_concurrency_limit=2).launch(max_file_size="1mb")