File size: 9,001 Bytes
3e49267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3025f2
 
3e49267
 
 
 
 
 
 
e3025f2
3e49267
e3025f2
 
3e49267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c44b46d
d09ce38
 
e3025f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e49267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3025f2
3e49267
e3025f2
3e49267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3025f2
3e49267
 
 
 
 
e3025f2
3e49267
 
6f33114
 
3e49267
 
 
e3025f2
3e49267
 
 
e3025f2
3e49267
 
 
 
 
 
e3025f2
3e49267
 
e3025f2
6f33114
3e49267
e3025f2
3e49267
e3025f2
3e49267
 
e3025f2
3e49267
 
 
e3025f2
3e49267
 
e3025f2
3e49267
e3025f2
 
6f33114
 
 
b6b668e
c44b46d
6f33114
e3025f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e49267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3025f2
3e49267
 
 
 
e3025f2
3e49267
 
 
 
e3025f2
 
3e49267
e3025f2
3e49267
 
6f33114
 
 
 
 
 
 
 
e3025f2
 
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import threading
import re
from concurrent.futures import ThreadPoolExecutor, TimeoutError

from modules.talker.talk import speak

from modules.langchain_assistant.langchain_productivy_assistant import (
    LangChainProductivityAssistant
)
from modules.langchain_assistant.langchain_browser_assistant import (
    LangChainBrowserAssistant
)
from modules.langchain_assistant.langchain_scientist_assistant import (
    LangChainScientisAssistant
)
from modules.langchain_assistant.langchain_chat_assistant import (
    LangChainChatAssistant
)
from modules.langchain_assistant.langchain_document_assistant import (
    LangChainDocumentAssistant
)

from modules.menus.role import create_notion_page


# ----------------------------------------
# ESTADO GLOBAL DE API KEYS
# ----------------------------------------
api_keys = {
    "gemini": None,
    "nvidia": None,
    "notion": None
}

# ----------------------------------------
# INSTANCIAS GLOBALES
# ----------------------------------------
productivity_role = None
browser_role      = None
scientist_role    = None
regular_chat_role = None


# ----------------------------------------
# RECONSTRUIR ROLES
# ----------------------------------------
def initialize_roles():
    global productivity_role, browser_role, scientist_role, regular_chat_role
    productivity_role = LangChainProductivityAssistant()
    browser_role      = LangChainBrowserAssistant()
    scientist_role    = LangChainScientisAssistant()
    regular_chat_role = LangChainChatAssistant()

initialize_roles()


# ----------------------------------------
# ACTUALIZAR API KEYS DESDE GRADIO
# ----------------------------------------
def update_api_keys(gemini_key, nvidia_key, notion_key):
    global api_keys
    api_keys = {
        "gemini": gemini_key.strip() if gemini_key else None,
        "nvidia": nvidia_key.strip() if nvidia_key else None,
        "notion": notion_key.strip() if notion_key else None
    }
    initialize_roles()
    return "✅ API Keys actualizadas correctamente."


# ----------------------------------------
# CONVERTIR HTML IMG → MARKDOWN
# ----------------------------------------
def convert_images_to_markdown(text):
    if not text:
        return ""
    img_pattern = r'<img[^>]*src="([^"]+)"[^>]*>'
    def replace(match):
        url = match.group(1)
        return f"\n\n![Imagen científica]({url})\n\n"
    text = re.sub(img_pattern, replace, text)
    text = re.sub(r"</?div[^>]*>", "", text)
    return text


# ----------------------------------------
# BUSCAR IMAGEN RELACIONADA (chat web)
# ----------------------------------------
def fetch_related_image(query):
    try:
        from ddgs import DDGS
        with DDGS() as ddgs:
            results = list(ddgs.images(query, max_results=5, safesearch="moderate"))
        for r in results:
            url = r.get("image") or r.get("url") or r.get("thumbnail")
            if url and url.startswith("http"):
                return url
    except Exception as e:
        print(f"⚠️ Error buscando imagen: {e}")
    return None


# ----------------------------------------
# GUARDAR EN NOTION ASÍNCRONO
# ----------------------------------------
def save_to_notion_async(content):
    try:
        threading.Thread(
            target=create_notion_page,
            args=(content,),
            daemon=True
        ).start()
    except Exception as e:
        print("⚠️ Error iniciando guardado en Notion:", e)


# ----------------------------------------
# TIMEOUT CONTROLADO
# Solo para roles que NO son BrowserAssistant
# ----------------------------------------
def run_with_timeout(role, user_input, timeout=120):
    def task():
        return role.chat(input=user_input)

    with ThreadPoolExecutor(max_workers=1) as executor:
        future = executor.submit(task)
        try:
            return future.result(timeout=timeout)
        except TimeoutError:
            print("⚠️ El modelo tardó demasiado.")
            raise Exception("Model timeout")


# ----------------------------------------
# EXTRAER CONTENIDO
# ----------------------------------------
def extract_content(role, assistant_response):
    if assistant_response is None:
        return "No se recibió respuesta."
    if isinstance(role, LangChainChatAssistant):
        return str(assistant_response)
    if isinstance(assistant_response, dict):
        return assistant_response.get("content", "Sin contenido.")
    return str(assistant_response)


# ----------------------------------------
# STREAMING RESPUESTA
# HF usa tuplas [user, bot] — NO dicts
# ----------------------------------------
def stream_response(chat_history, user_input, content):
    if chat_history is None:
        chat_history = []
    response = ""
    for letter in content:
        response += letter
        yield chat_history + [[user_input, response]], None


# ----------------------------------------
# FUNCIÓN BASE (todos los roles excepto browser)
# ----------------------------------------
def chat(chat_history, user_input, role):
    if chat_history is None:
        chat_history = []

    try:
        assistant_response = run_with_timeout(role, user_input)

    except Exception:
        yield chat_history + [[user_input, "PENSANDO 🧠🎯🖥️..."]], None

        try:
            assistant_response = role.chat(input=user_input)
        except Exception as e:
            yield chat_history + [[user_input, f"❌ Error del asistente: {str(e)}"]], None
            return

    content = extract_content(role, assistant_response)

    if isinstance(role, LangChainProductivityAssistant):
        if isinstance(assistant_response, dict):
            save_to_notion_async(assistant_response)

    if isinstance(role, LangChainScientisAssistant):
        content = convert_images_to_markdown(content)

    # Generar audio
    audio_path = None
    try:
        audio_path = speak(content)
        print("🔊 Audio generado:", audio_path)
    except Exception as e:
        print("⚠️ Error en voz:", e)

    for history in stream_response(chat_history, user_input, content):
        yield history[0], audio_path


# ----------------------------------------
# BROWSER CHAT — flujo especial para HF
# No usa run_with_timeout (NVIDIA tarda ~90s)
# Agrega imagen relacionada al final
# ----------------------------------------
def browser_chat(chat_history, user_input):
    if chat_history is None:
        chat_history = []

    # Mensaje de espera mientras busca
    yield chat_history + [[user_input, "🔍 Buscando en la web en tiempo real..."]], None

    try:
        # Llamada directa sin timeout externo
        assistant_response = browser_role.chat(input=user_input)
        content = extract_content(browser_role, assistant_response)
    except Exception as e:
        yield chat_history + [[user_input, f"❌ Error en búsqueda web: {str(e)}"]], None
        return

    # Agregar imagen relacionada
    try:
        image_url = fetch_related_image(user_input)
        if image_url:
            content += f"\n\n![Imagen relacionada]({image_url})"
    except Exception:
        pass

    # Audio
    audio_path = None
    try:
        audio_path = speak(content)
    except Exception as e:
        print("⚠️ Error en voz browser:", e)

    # Stream letra a letra
    for history in stream_response(chat_history, user_input, content):
        yield history[0], audio_path


# ----------------------------------------
# CHATS POR MÓDULO
# ----------------------------------------
def productivity_chat(chat_history, user_input):
    yield from chat(
        chat_history=chat_history,
        user_input=user_input,
        role=productivity_role
    )


def scientist_chat(chat_history, user_input):
    yield from chat(
        chat_history=chat_history,
        user_input=user_input,
        role=scientist_role
    )


def regular_chat(chat_history, user_input):
    yield from chat(
        chat_history=chat_history,
        user_input=user_input,
        role=regular_chat_role
    )


# ----------------------------------------
# CHAT PDF
# ----------------------------------------
def document_chat(chat_history, user_input, document_file):
    if chat_history is None:
        chat_history = []

    if document_file is None:
        yield chat_history + [[user_input, "⚠️ Debes subir un archivo PDF primero."]], None
        return

    try:
        file_path = document_file.name
        document_role = LangChainDocumentAssistant(document_name=file_path)
        assistant_response = document_role.chat(query=user_input)
    except Exception as e:
        yield chat_history + [[user_input, f"❌ Error procesando el PDF: {str(e)}"]], None
        return

    content = str(assistant_response)

    audio_path = None
    try:
        audio_path = speak(content)
    except Exception as e:
        print("⚠️ Error en voz PDF:", e)

    for history in stream_response(chat_history, user_input, content):
        yield history[0], audio_path