Jeremy Live commited on
Commit
0f16c64
·
1 Parent(s): 683b6ad

API solved

Browse files
Files changed (4) hide show
  1. README.md +96 -3
  2. api.py +103 -0
  3. app.py +10 -994
  4. requirements.txt +1 -0
README.md CHANGED
@@ -19,11 +19,104 @@ A powerful chatbot that can answer questions by querying your SQL database using
19
  - Interactive chat interface
20
  - Direct database connectivity
21
  - Powered by Google's Gemini AI
 
22
 
23
  ## Setup
24
 
25
- 1. Set up your environment variables in `.env` file
26
- 2. Install dependencies: `pip install -r requirements.txt`
27
- 3. Run the app: `python app.py`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  Check out the [configuration reference](https://huggingface.co/docs/hub/spaces-config-reference) for more options.
 
19
  - Interactive chat interface
20
  - Direct database connectivity
21
  - Powered by Google's Gemini AI
22
+ - RESTful API endpoints for integration
23
 
24
  ## Setup
25
 
26
+ 1. Set up your environment variables in `.env` file:
27
+ ```env
28
+ DB_USER=tu_usuario
29
+ DB_PASSWORD=tu_contraseña
30
+ DB_HOST=tu_servidor
31
+ DB_NAME=tu_base_de_datos
32
+ GOOGLE_API_KEY=tu_api_key_de_google
33
+ ```
34
+
35
+ 2. Install dependencies:
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ 3. Run the web interface:
41
+ ```bash
42
+ python app.py
43
+ ```
44
+
45
+ 4. Run the API server:
46
+ ```bash
47
+ python api.py
48
+ ```
49
+
50
+ ## API Usage
51
+
52
+ La API proporciona dos endpoints principales:
53
+
54
+ ### 1. Enviar Mensaje de Usuario
55
+
56
+ **Endpoint:** `/user_message`
57
+
58
+ **Método:** POST
59
+
60
+ **Headers:**
61
+ ```
62
+ Content-Type: application/json
63
+ ```
64
+
65
+ **Body:**
66
+ ```json
67
+ {
68
+ "message": "tu pregunta aquí"
69
+ }
70
+ ```
71
+
72
+ **Respuesta exitosa:**
73
+ ```json
74
+ {
75
+ "message_id": "uuid-generado",
76
+ "status": "success"
77
+ }
78
+ ```
79
+
80
+ ### 2. Obtener Respuesta
81
+
82
+ **Endpoint:** `/ask`
83
+
84
+ **Método:** POST
85
+
86
+ **Headers:**
87
+ ```
88
+ Content-Type: application/json
89
+ ```
90
+
91
+ **Body:**
92
+ ```json
93
+ {
94
+ "message_id": "uuid-del-mensaje"
95
+ }
96
+ ```
97
+
98
+ **Respuesta exitosa:**
99
+ ```json
100
+ {
101
+ "response": "respuesta del chatbot",
102
+ "status": "success"
103
+ }
104
+ ```
105
+
106
+ ## Ejemplo de uso de la API
107
+
108
+ 1. Primero, envía tu pregunta:
109
+ ```bash
110
+ curl -X POST http://localhost:5000/user_message \
111
+ -H "Content-Type: application/json" \
112
+ -d '{"message": "¿Cuántos usuarios hay en la base de datos?"}'
113
+ ```
114
+
115
+ 2. Luego, usa el message_id recibido para obtener la respuesta:
116
+ ```bash
117
+ curl -X POST http://localhost:5000/ask \
118
+ -H "Content-Type: application/json" \
119
+ -d '{"message_id": "uuid-recibido"}'
120
+ ```
121
 
122
  Check out the [configuration reference](https://huggingface.co/docs/hub/spaces-config-reference) for more options.
api.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from typing import Dict, Optional
3
+ import uuid
4
+ import os
5
+ from app import initialize_llm, setup_database_connection, create_agent, gr
6
+
7
+ app = Flask(__name__)
8
+
9
+ # Almacenamiento en memoria de los mensajes
10
+ message_store: Dict[str, str] = {}
11
+
12
+ @app.route('/user_message', methods=['POST'])
13
+ def handle_user_message():
14
+ try:
15
+ data = request.get_json()
16
+ if not data or 'message' not in data:
17
+ return jsonify({'error': 'Se requiere el campo message'}), 400
18
+
19
+ user_message = data['message']
20
+
21
+ # Generar un ID único para este mensaje
22
+ message_id = str(uuid.uuid4())
23
+
24
+ # Almacenar el mensaje
25
+ message_store[message_id] = user_message
26
+
27
+ return jsonify({
28
+ 'message_id': message_id,
29
+ 'status': 'success'
30
+ })
31
+
32
+ except Exception as e:
33
+ return jsonify({'error': str(e)}), 500
34
+
35
+ @app.route('/ask', methods=['POST'])
36
+ def handle_ask():
37
+ try:
38
+ data = request.get_json()
39
+ if not data or 'message_id' not in data:
40
+ return jsonify({'error': 'Se requiere el campo message_id'}), 400
41
+
42
+ message_id = data['message_id']
43
+
44
+ # Recuperar el mensaje almacenado
45
+ if message_id not in message_store:
46
+ return jsonify({'error': 'ID de mensaje no encontrado'}), 404
47
+
48
+ user_message = message_store[message_id]
49
+
50
+ # Inicializar componentes necesarios
51
+ llm, llm_error = initialize_llm()
52
+ if llm_error:
53
+ return jsonify({'error': f'Error al inicializar LLM: {llm_error}'}), 500
54
+
55
+ db_connection, db_error = setup_database_connection()
56
+ if db_error:
57
+ return jsonify({'error': f'Error de conexión a la base de datos: {db_error}'}), 500
58
+
59
+ agent, agent_error = create_agent(llm, db_connection)
60
+ if agent_error:
61
+ return jsonify({'error': f'Error al crear el agente: {agent_error}'}), 500
62
+
63
+ # Obtener respuesta del agente
64
+ response = agent.invoke({"input": user_message})
65
+
66
+ # Procesar la respuesta
67
+ if hasattr(response, 'output') and response.output:
68
+ response_text = response.output
69
+ elif isinstance(response, str):
70
+ response_text = response
71
+ elif hasattr(response, 'get') and callable(response.get) and 'output' in response:
72
+ response_text = response['output']
73
+ else:
74
+ response_text = str(response)
75
+
76
+ # Eliminar el mensaje almacenado después de procesarlo
77
+ del message_store[message_id]
78
+
79
+ return jsonify({
80
+ 'response': response_text,
81
+ 'status': 'success'
82
+ })
83
+
84
+ except Exception as e:
85
+ return jsonify({'error': str(e)}), 500
86
+
87
+ # Integración con Gradio para Hugging Face Spaces
88
+ def mount_in_app(gradio_app):
89
+ """Monta la API Flask en la aplicación Gradio."""
90
+ return gradio_app
91
+
92
+ if __name__ == '__main__':
93
+ # Si se ejecuta directamente, inicia el servidor Flask
94
+ port = int(os.environ.get('PORT', 5000))
95
+ app.run(host='0.0.0.0', port=port)
96
+ else:
97
+ # Si se importa como módulo (en Hugging Face Spaces),
98
+ # expone la función para montar en Gradio
99
+ gradio_app = gr.mount_gradio_app(
100
+ app,
101
+ "/api", # Prefijo para los endpoints de la API
102
+ lambda: True # Autenticación deshabilitada
103
+ )
app.py CHANGED
@@ -12,976 +12,23 @@ import pandas as pd
12
  import plotly.express as px
13
  import plotly.graph_objects as go
14
  from plotly.subplots import make_subplots
15
- try:
16
- from sqlalchemy import text as sa_text
17
- except Exception:
18
- sa_text = None
19
 
20
- try:
21
- # Intentar importar dependencias opcionales
22
- from langchain_community.agent_toolkits import create_sql_agent
23
- from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
24
- from langchain_community.utilities import SQLDatabase
25
- from langchain_google_genai import ChatGoogleGenerativeAI
26
- from langchain.agents.agent_types import AgentType
27
- from langchain.memory import ConversationBufferWindowMemory
28
- from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
29
- import pymysql
30
- from dotenv import load_dotenv
31
-
32
- DEPENDENCIES_AVAILABLE = True
33
- except ImportError as e:
34
- logger.warning(f"Some dependencies are not available: {e}")
35
- DEPENDENCIES_AVAILABLE = False
36
-
37
- # Configuración de logging
38
- logging.basicConfig(level=logging.INFO)
39
- logger = logging.getLogger(__name__)
40
-
41
- # Configure logging
42
- logging.basicConfig(level=logging.INFO)
43
-
44
- def generate_chart(data: Union[Dict, List[Dict], pd.DataFrame],
45
- chart_type: str,
46
- x: str,
47
- y: str = None,
48
- title: str = "",
49
- x_label: str = None,
50
- y_label: str = None):
51
- """
52
- Generate an interactive Plotly figure from data.
53
-
54
- Args:
55
- data: The data to plot (can be a list of dicts or a pandas DataFrame)
56
- chart_type: Type of chart to generate (bar, line, pie, scatter, histogram)
57
- x: Column name for x-axis (names for pie)
58
- y: Column name for y-axis (values for pie)
59
- title: Chart title
60
- x_label: Label for x-axis
61
- y_label: Label for y-axis
62
-
63
- Returns:
64
- A Plotly Figure object (interactive) or None on error
65
- """
66
- try:
67
- # Convert data to DataFrame if it's a list of dicts
68
- if isinstance(data, list):
69
- df = pd.DataFrame(data)
70
- elif isinstance(data, dict):
71
- df = pd.DataFrame([data])
72
- else:
73
- df = data
74
-
75
- if not isinstance(df, pd.DataFrame):
76
- return None
77
-
78
- # Generate the appropriate chart type
79
- fig = None
80
- if chart_type == 'bar':
81
- fig = px.bar(df, x=x, y=y, title=title)
82
- elif chart_type == 'line':
83
- fig = px.line(df, x=x, y=y, title=title)
84
- elif chart_type == 'pie':
85
- fig = px.pie(df, names=x, values=y, title=title, hole=0)
86
- elif chart_type == 'scatter':
87
- fig = px.scatter(df, x=x, y=y, title=title)
88
- elif chart_type == 'histogram':
89
- fig = px.histogram(df, x=x, title=title)
90
- else:
91
- return None
92
-
93
- # Update layout
94
- fig.update_layout(
95
- xaxis_title=x_label or x,
96
- yaxis_title=y_label or (y if y != x else ''),
97
- title=title or f"{chart_type.capitalize()} Chart of {x} vs {y}" if y else f"{chart_type.capitalize()} Chart of {x}",
98
- template="plotly_white",
99
- margin=dict(l=20, r=20, t=40, b=20),
100
- height=400
101
- )
102
-
103
- return fig
104
-
105
- except Exception as e:
106
- error_msg = f"Error generating chart: {str(e)}"
107
- logger.error(error_msg, exc_info=True)
108
- return None
109
-
110
- logger = logging.getLogger(__name__)
111
-
112
- def check_environment():
113
- """Verifica si el entorno está configurado correctamente."""
114
- if not DEPENDENCIES_AVAILABLE:
115
- return False, "Missing required Python packages. Please install them with: pip install -r requirements.txt"
116
-
117
- # Verificar si estamos en un entorno con variables de entorno
118
- required_vars = ["DB_USER", "DB_PASSWORD", "DB_HOST", "DB_NAME", "GOOGLE_API_KEY"]
119
- missing_vars = [var for var in required_vars if not os.getenv(var)]
120
-
121
- if missing_vars:
122
- return False, f"Missing required environment variables: {', '.join(missing_vars)}"
123
-
124
- return True, "Environment is properly configured"
125
-
126
- def setup_database_connection():
127
- """Intenta establecer una conexión a la base de datos."""
128
- if not DEPENDENCIES_AVAILABLE:
129
- return None, "Dependencies not available"
130
-
131
- try:
132
- load_dotenv(override=True)
133
-
134
- # Debug: Log all environment variables (without sensitive values)
135
- logger.info("Environment variables:")
136
- for key, value in os.environ.items():
137
- if any(s in key.lower() for s in ['pass', 'key', 'secret']):
138
- logger.info(f" {key}: {'*' * 8} (hidden for security)")
139
- else:
140
- logger.info(f" {key}: {value}")
141
-
142
- db_user = os.getenv("DB_USER")
143
- db_password = os.getenv("DB_PASSWORD")
144
- db_host = os.getenv("DB_HOST")
145
- db_name = os.getenv("DB_NAME")
146
-
147
- # Debug: Log database connection info (without password)
148
- logger.info(f"Database connection attempt - Host: {db_host}, User: {db_user}, DB: {db_name}")
149
- if not all([db_user, db_password, db_host, db_name]):
150
- missing = [var for var, val in [
151
- ("DB_USER", db_user),
152
- ("DB_PASSWORD", "*" if db_password else ""),
153
- ("DB_HOST", db_host),
154
- ("DB_NAME", db_name)
155
- ] if not val]
156
- logger.error(f"Missing required database configuration: {', '.join(missing)}")
157
- return None, f"Missing database configuration: {', '.join(missing)}"
158
-
159
- if not all([db_user, db_password, db_host, db_name]):
160
- return None, "Missing database configuration"
161
-
162
- logger.info(f"Connecting to database: {db_user}@{db_host}/{db_name}")
163
-
164
- # Probar conexión
165
- connection = pymysql.connect(
166
- host=db_host,
167
- user=db_user,
168
- password=db_password,
169
- database=db_name,
170
- connect_timeout=5,
171
- cursorclass=pymysql.cursors.DictCursor
172
- )
173
- connection.close()
174
-
175
- # Si la conexión es exitosa, crear motor SQLAlchemy
176
- db_uri = f"mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}"
177
- logger.info("Database connection successful")
178
- return SQLDatabase.from_uri(db_uri), ""
179
-
180
- except Exception as e:
181
- error_msg = f"Error connecting to database: {str(e)}"
182
- logger.error(error_msg)
183
- return None, error_msg
184
-
185
- def initialize_llm():
186
- """Inicializa el modelo de lenguaje."""
187
- if not DEPENDENCIES_AVAILABLE:
188
- error_msg = "Dependencies not available. Make sure all required packages are installed."
189
- logger.error(error_msg)
190
- return None, error_msg
191
-
192
- google_api_key = os.getenv("GOOGLE_API_KEY")
193
- logger.info(f"GOOGLE_API_KEY found: {'Yes' if google_api_key else 'No'}")
194
-
195
- if not google_api_key:
196
- error_msg = "GOOGLE_API_KEY not found in environment variables. Please check your Hugging Face Space secrets."
197
- logger.error(error_msg)
198
- return None, error_msg
199
-
200
- try:
201
- logger.info("Initializing Google Generative AI...")
202
- llm = ChatGoogleGenerativeAI(
203
- model="gemini-2.0-flash",
204
- temperature=0,
205
- google_api_key=google_api_key,
206
- convert_system_message_to_human=True # Convert system messages to human messages
207
- )
208
-
209
- # Test the model with a simple prompt
210
- test_prompt = "Hello, this is a test."
211
- logger.info(f"Testing model with prompt: {test_prompt}")
212
- test_response = llm.invoke(test_prompt)
213
- logger.info(f"Model test response: {str(test_response)[:100]}...") # Log first 100 chars
214
-
215
- logger.info("Google Generative AI initialized successfully")
216
- return llm, ""
217
-
218
- except Exception as e:
219
- error_msg = f"Error initializing Google Generative AI: {str(e)}"
220
- logger.error(error_msg, exc_info=True) # Include full stack trace
221
- return None, error_msg
222
-
223
- def create_agent():
224
- """Crea el agente SQL si es posible."""
225
- if not DEPENDENCIES_AVAILABLE:
226
- error_msg = "Dependencies not available. Please check if all required packages are installed."
227
- logger.error(error_msg)
228
- return None, error_msg
229
-
230
- logger.info("Starting agent creation process...")
231
-
232
- def create_agent(llm, db_connection):
233
- """Create and return a SQL database agent with conversation memory."""
234
- if not llm:
235
- error_msg = "Cannot create agent: LLM is not available"
236
- logger.error(error_msg)
237
- return None, error_msg
238
-
239
- if not db_connection:
240
- error_msg = "Cannot create agent: Database connection is not available"
241
- logger.error(error_msg)
242
- return None, error_msg
243
-
244
- try:
245
- logger.info("Creating SQL agent with memory...")
246
-
247
- # Create conversation memory
248
- memory = ConversationBufferWindowMemory(
249
- memory_key="chat_history",
250
- k=5, # Keep last 5 message exchanges in memory
251
- return_messages=True,
252
- output_key="output"
253
- )
254
-
255
- # Create the database toolkit with additional configuration
256
- toolkit = SQLDatabaseToolkit(
257
- db=db_connection,
258
- llm=llm
259
- )
260
-
261
- # Create the agent with memory and more detailed configuration
262
- agent = create_sql_agent(
263
- llm=llm,
264
- toolkit=toolkit,
265
- agent_type=AgentType.OPENAI_FUNCTIONS,
266
- verbose=True,
267
- handle_parsing_errors=True, # Better error handling for parsing
268
- max_iterations=10, # Limit the number of iterations
269
- early_stopping_method="generate", # Stop early if the agent is stuck
270
- memory=memory, # Add memory to the agent
271
- return_intermediate_steps=True # Important for memory to work properly
272
- )
273
-
274
- # Test the agent with a simple query
275
- logger.info("Testing agent with a simple query...")
276
- try:
277
- test_query = "SELECT 1"
278
- test_result = agent.run(test_query)
279
- logger.info(f"Agent test query successful: {str(test_result)[:200]}...")
280
- except Exception as e:
281
- logger.warning(f"Agent test query failed (this might be expected): {str(e)}")
282
- # Continue even if test fails, as it might be due to model limitations
283
-
284
- logger.info("SQL agent created successfully")
285
- return agent, ""
286
-
287
- except Exception as e:
288
- error_msg = f"Error creating SQL agent: {str(e)}"
289
- logger.error(error_msg, exc_info=True)
290
- return None, error_msg
291
-
292
- # Inicializar el agente
293
- logger.info("="*50)
294
- logger.info("Starting application initialization...")
295
- logger.info(f"Python version: {sys.version}")
296
- logger.info(f"Current working directory: {os.getcwd()}")
297
- logger.info(f"Files in working directory: {os.listdir()}")
298
-
299
- # Verificar las variables de entorno
300
- logger.info("Checking environment variables...")
301
- for var in ["DB_USER", "DB_PASSWORD", "DB_HOST", "DB_NAME", "GOOGLE_API_KEY"]:
302
- logger.info(f"{var}: {'*' * 8 if os.getenv(var) else 'NOT SET'}")
303
-
304
- # Initialize components
305
- logger.info("Initializing database connection...")
306
- db_connection, db_error = setup_database_connection()
307
- if db_error:
308
- logger.error(f"Failed to initialize database: {db_error}")
309
-
310
- logger.info("Initializing language model...")
311
- llm, llm_error = initialize_llm()
312
- if llm_error:
313
- logger.error(f"Failed to initialize language model: {llm_error}")
314
-
315
- logger.info("Initializing agent...")
316
- agent, agent_error = create_agent(llm, db_connection)
317
- db_connected = agent is not None
318
-
319
- if agent:
320
- logger.info("Agent initialized successfully")
321
- else:
322
- logger.error(f"Failed to initialize agent: {agent_error}")
323
-
324
- logger.info("="*50)
325
-
326
- def looks_like_sql(s: str) -> bool:
327
- """Heuristic to check if a string looks like an executable SQL statement."""
328
- if not s:
329
- return False
330
- s_strip = s.strip().lstrip("-- ")
331
- # common starters
332
- return bool(re.match(r"^(WITH|SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE)\b", s_strip, re.IGNORECASE))
333
-
334
-
335
- def extract_sql_query(text):
336
- """Extrae consultas SQL del texto. Acepta solo bloques etiquetados como ```sql
337
- o cadenas que claramente parezcan SQL. Evita ejecutar texto genérico.
338
- """
339
- if not text:
340
- return None
341
-
342
- # Buscar TODOS los bloques en backticks y elegir los que sean 'sql'
343
- for m in re.finditer(r"```(\w+)?\s*(.*?)```", text, re.DOTALL | re.IGNORECASE):
344
- lang = (m.group(1) or '').lower()
345
- body = (m.group(2) or '').strip()
346
- if lang in {"sql", "postgresql", "mysql"} and looks_like_sql(body):
347
- return body
348
-
349
- # Si no hay bloques etiquetados, buscar una consulta SQL simple con palabras clave
350
- simple = re.search(r"(WITH|SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE)[\s\S]*?;", text, re.IGNORECASE)
351
- if simple:
352
- candidate = simple.group(0).strip()
353
- if looks_like_sql(candidate):
354
- return candidate
355
-
356
- return None
357
-
358
- def execute_sql_query(query, db_connection):
359
- """Ejecuta una consulta SQL y devuelve los resultados como una cadena."""
360
- if not db_connection:
361
- return "Error: No hay conexión a la base de datos"
362
-
363
- try:
364
- with db_connection._engine.connect() as connection:
365
- # Ensure SQLAlchemy receives a SQL expression
366
- if sa_text is not None and isinstance(query, str):
367
- result = connection.execute(sa_text(query))
368
- else:
369
- result = connection.execute(query)
370
-
371
- # Fetch data and column names
372
- columns = list(result.keys()) if hasattr(result, "keys") else []
373
- rows = result.fetchall()
374
-
375
- # Convertir los resultados a un formato legible
376
- if not rows:
377
- return "La consulta no devolvió resultados"
378
-
379
- # Si es un solo resultado, devolverlo directamente
380
- try:
381
- if len(rows) == 1 and len(rows[0]) == 1:
382
- return str(rows[0][0])
383
- except Exception:
384
- pass
385
-
386
- # Si hay múltiples filas, formatear como tabla Markdown
387
- try:
388
- import pandas as pd
389
-
390
- # Convert SQLAlchemy Row objects to list of dicts using column names
391
- if columns:
392
- data = [
393
- {col: val for col, val in zip(columns, tuple(row))}
394
- for row in rows
395
- ]
396
- df = pd.DataFrame(data)
397
- else:
398
- # Fallback: let pandas infer columns
399
- df = pd.DataFrame(rows)
400
-
401
- # Prefer Markdown output for downstream chart parsing
402
- try:
403
- return df.to_markdown(index=False)
404
- except Exception:
405
- # If optional dependency 'tabulate' is missing, build a simple Markdown table
406
- headers = list(map(str, df.columns))
407
- header_line = "| " + " | ".join(headers) + " |"
408
- sep_line = "| " + " | ".join(["---"] * len(headers)) + " |"
409
- body_lines = []
410
- for _, r in df.iterrows():
411
- body_lines.append("| " + " | ".join(map(lambda v: str(v), r.values)) + " |")
412
- return "\n".join([header_line, sep_line, *body_lines])
413
- except ImportError:
414
- # Si pandas no está disponible, usar formato simple
415
- return "\n".join([str(row) for row in rows])
416
-
417
- except Exception as e:
418
- return f"Error ejecutando la consulta: {str(e)}"
419
-
420
- def detect_chart_preferences(question: str) -> Tuple[bool, str]:
421
- """Detect whether the user is asking for a chart and infer desired type.
422
-
423
- Returns (wants_chart, chart_type) where chart_type is one of
424
- {'bar', 'pie', 'line', 'scatter', 'histogram'}.
425
- Defaults to 'bar' when ambiguous.
426
- """
427
- try:
428
- q = (question or "").lower()
429
-
430
- # Broad triggers indicating any chart request
431
- chart_triggers = [
432
- "grafico", "gráfico", "grafica", "gráfica", "chart", "graph",
433
- "visualizacion", "visualización", "plot", "plotly", "diagrama"
434
- ]
435
- wants_chart = any(k in q for k in chart_triggers)
436
-
437
- # Specific type hints
438
- if any(k in q for k in ["pastel", "pie", "circular", "donut", "dona", "anillo"]):
439
- return wants_chart or True, "pie"
440
- if any(k in q for k in ["linea", "línea", "line", "tendencia"]):
441
- return wants_chart or True, "line"
442
- if any(k in q for k in ["dispersión", "dispersion", "scatter", "puntos"]):
443
- return wants_chart or True, "scatter"
444
- if any(k in q for k in ["histograma", "histogram"]):
445
- return wants_chart or True, "histogram"
446
- if any(k in q for k in ["barra", "barras", "columnas", "column"]):
447
- return wants_chart or True, "bar"
448
-
449
- # Default
450
- return wants_chart, "bar"
451
- except Exception:
452
- return False, "bar"
453
-
454
- def generate_plot(data, x_col, y_col, title, x_label, y_label):
455
- """Generate a plot from data and return the file path."""
456
- plt.figure(figsize=(10, 6))
457
- plt.bar(data[x_col], data[y_col])
458
- plt.title(title)
459
- plt.xlabel(x_label)
460
- plt.ylabel(y_label)
461
- plt.xticks(rotation=45)
462
- plt.tight_layout()
463
-
464
- # Save to a temporary file
465
- temp_dir = tempfile.mkdtemp()
466
- plot_path = os.path.join(temp_dir, "plot.png")
467
- plt.savefig(plot_path)
468
- plt.close()
469
-
470
- return plot_path
471
-
472
- def convert_to_messages_format(chat_history):
473
- """Convert chat history to the format expected by Gradio 5.x"""
474
- if not chat_history:
475
- return []
476
-
477
- messages = []
478
-
479
- # If the first element is a list, assume it's in the old format
480
- if isinstance(chat_history[0], list):
481
- for msg in chat_history:
482
- if isinstance(msg, list) and len(msg) == 2:
483
- # Format: [user_msg, bot_msg]
484
- user_msg, bot_msg = msg
485
- if user_msg:
486
- messages.append({"role": "user", "content": user_msg})
487
- if bot_msg:
488
- messages.append({"role": "assistant", "content": bot_msg})
489
- else:
490
- # Assume it's already in the correct format or can be used as is
491
- for msg in chat_history:
492
- if isinstance(msg, dict) and "role" in msg and "content" in msg:
493
- messages.append(msg)
494
- elif isinstance(msg, str):
495
- # If it's a string, assume it's a user message
496
- messages.append({"role": "user", "content": msg})
497
-
498
- return messages
499
-
500
- async def stream_agent_response(question: str, chat_history: List[List[str]]) -> Tuple[str, Optional["go.Figure"]]:
501
- """Procesa la pregunta del usuario y devuelve la respuesta del agente con memoria de conversación."""
502
- global agent # Make sure we can modify the agent's memory
503
-
504
- # Initialize response
505
- response_text = ""
506
- chart_fig = None
507
- messages = []
508
-
509
- # Add previous chat history in the correct format for the agent
510
- for msg_pair in chat_history:
511
- if len(msg_pair) >= 1 and msg_pair[0]: # User message
512
- messages.append(HumanMessage(content=msg_pair[0]))
513
- if len(msg_pair) >= 2 and msg_pair[1]: # Assistant message
514
- messages.append(AIMessage(content=msg_pair[1]))
515
-
516
- # Add current user's question
517
- user_message = HumanMessage(content=question)
518
- messages.append(user_message)
519
-
520
- if not agent:
521
- error_msg = (
522
- "## ⚠️ Error: Agente no inicializado\n\n"
523
- "No se pudo inicializar el agente de base de datos. Por favor, verifica que:\n"
524
- "1. Todas las variables de entorno estén configuradas correctamente\n"
525
- "2. La base de datos esté accesible\n"
526
- f"3. El modelo de lenguaje esté disponible\n\n"
527
- f"Error: {agent_error}"
528
- )
529
- return error_msg, None
530
-
531
- # Update the agent's memory with the full conversation history
532
- try:
533
- # Rebuild agent memory from chat history pairs
534
- if hasattr(agent, 'memory') and agent.memory is not None:
535
- agent.memory.clear()
536
- for i in range(0, len(messages)-1, 2): # (user, assistant)
537
- if i+1 < len(messages):
538
- agent.memory.save_context(
539
- {"input": messages[i].content},
540
- {"output": messages[i+1].content}
541
- )
542
- except Exception as e:
543
- logger.error(f"Error updating agent memory: {str(e)}", exc_info=True)
544
-
545
- try:
546
- # Add empty assistant message that will be updated
547
- assistant_message = {"role": "assistant", "content": ""}
548
- messages.append(assistant_message)
549
-
550
- # Execute the agent with proper error handling
551
- try:
552
- # Let the agent use its memory; don't pass raw chat_history
553
- response = await agent.ainvoke({"input": question})
554
- logger.info(f"Agent response type: {type(response)}")
555
- logger.info(f"Agent response content: {str(response)[:500]}...")
556
-
557
- # Handle different response formats
558
- if hasattr(response, 'output') and response.output:
559
- response_text = response.output
560
- elif isinstance(response, str):
561
- response_text = response
562
- elif hasattr(response, 'get') and callable(response.get) and 'output' in response:
563
- response_text = response['output']
564
- else:
565
- response_text = str(response)
566
-
567
- # logger.info(f"Extracted response text: {response_text[:200]}...")
568
-
569
- # # Check if the response contains an SQL query and it truly looks like SQL
570
- # sql_query = extract_sql_query(response_text)
571
- # if sql_query and looks_like_sql(sql_query):
572
- # logger.info(f"Detected SQL query: {sql_query}")
573
- # # Execute the query and update the response
574
- # db_connection, _ = setup_database_connection()
575
- # if db_connection:
576
- # query_result = execute_sql_query(sql_query, db_connection)
577
-
578
- # # Add the query and its result to the response
579
- # response_text += f"\n\n### 🔍 Resultado de la consulta:\n```sql\n{sql_query}\n```\n\n{query_result}"
580
-
581
- # # Try to generate an interactive chart if the result is tabular
582
- # try:
583
- # if isinstance(query_result, str) and '|' in query_result and '---' in query_result:
584
- # # Convert markdown table to DataFrame
585
-
586
- # # Clean up the markdown table
587
- # lines = [line.strip() for line in query_result.split('\n')
588
- # if line.strip() and '---' not in line and '|' in line]
589
- # if len(lines) > 1: # At least header + 1 data row
590
- # # Get column names from the first line
591
- # columns = [col.strip() for col in lines[0].split('|')[1:-1]]
592
- # # Get data rows
593
- # data = []
594
- # for line in lines[1:]:
595
- # values = [val.strip() for val in line.split('|')[1:-1]]
596
- # if len(values) == len(columns):
597
- # data.append(dict(zip(columns, values)))
598
-
599
- # if data and len(columns) >= 2:
600
- # # Determine chart type from user's question
601
- # _, desired_type = detect_chart_preferences(question)
602
-
603
- # # Choose x/y columns (assume first is category, second numeric)
604
- # x_col = columns[0]
605
- # y_col = columns[1]
606
-
607
- # # Coerce numeric values for y
608
- # for row in data:
609
- # try:
610
- # row[y_col] = float(re.sub(r"[^0-9.\-]", "", str(row[y_col])))
611
- # except Exception:
612
- # pass
613
-
614
- # chart_fig = generate_chart(
615
- # data=data,
616
- # chart_type=desired_type,
617
- # x=x_col,
618
- # y=y_col,
619
- # title=f"{y_col} por {x_col}"
620
- # )
621
- # if chart_fig is not None:
622
- # logger.info(f"Chart generated from SQL table: type={desired_type}, x={x_col}, y={y_col}, rows={len(data)}")
623
- # except Exception as e:
624
- # logger.error(f"Error generating chart: {str(e)}", exc_info=True)
625
- # # Don't fail the whole request if chart generation fails
626
- # response_text += "\n\n⚠️ No se pudo generar la visualización de los datos."
627
- # else:
628
- # response_text += "\n\n⚠️ No se pudo conectar a la base de datos para ejecutar la consulta."
629
- # elif sql_query and not looks_like_sql(sql_query):
630
- # logger.info("Detected code block but it does not look like SQL; skipping execution.")
631
-
632
- # If we still have no chart but the user clearly wants one,
633
- # try a second pass to get ONLY a SQL query from the agent and execute it.
634
- if chart_fig is None:
635
- wants_chart, default_type = detect_chart_preferences(question)
636
- if wants_chart:
637
- try:
638
- logger.info("Second pass: asking agent for ONLY SQL query in fenced block.")
639
- sql_only_prompt = (
640
- "Devuelve SOLO la consulta SQL en un bloque ```sql``` para responder a: "
641
- f"{question}. No incluyas explicación ni texto adicional."
642
- )
643
- sql_only_resp = await agent.ainvoke({"input": sql_only_prompt})
644
- sql_only_text = str(sql_only_resp)
645
- sql_query2 = extract_sql_query(sql_only_text)
646
- if sql_query2 and looks_like_sql(sql_query2):
647
- logger.info(f"Second pass SQL detected: {sql_query2}")
648
- db_connection, _ = setup_database_connection()
649
- if db_connection:
650
- query_result = execute_sql_query(sql_query2, db_connection)
651
- # Try to parse table-like text into DataFrame if possible
652
- data = None
653
- if isinstance(query_result, str):
654
- try:
655
- import pandas as pd
656
- df = pd.read_csv(io.StringIO(query_result), sep="|")
657
- data = df
658
- except Exception:
659
- pass
660
- # As a fallback, don't rely on text table; just skip charting here
661
- if data is not None and hasattr(data, "empty") and not data.empty:
662
- # Heuristics: choose first column as x and second as y if numeric
663
- x_col = data.columns[0]
664
- # pick first numeric column different to x
665
- y_col = None
666
- for col in data.columns[1:]:
667
- try:
668
- pd.to_numeric(data[col])
669
- y_col = col
670
- break
671
- except Exception:
672
- continue
673
- if y_col:
674
- desired_type = default_type
675
- chart_fig = generate_chart(
676
- data=data,
677
- chart_type=desired_type,
678
- x=x_col,
679
- y=y_col,
680
- title=f"{y_col} por {x_col}"
681
- )
682
- if chart_fig is not None:
683
- logger.info("Chart generated from second-pass SQL execution.")
684
- else:
685
- logger.info("No DB connection on second pass; skipping.")
686
- except Exception as e:
687
- logger.error(f"Second-pass SQL synthesis failed: {e}")
688
-
689
- # Fallback: if user asked for a chart and we didn't get SQL or chart yet,
690
- # parse the most recent assistant text for lines like "LABEL: NUMBER" (bulleted or plain).
691
- if chart_fig is None:
692
- wants_chart, desired_type = detect_chart_preferences(question)
693
- if wants_chart:
694
- # Find the most recent assistant message with usable numeric pairs
695
- candidate_text = ""
696
- if chat_history:
697
- for pair in reversed(chat_history):
698
- if len(pair) >= 2 and isinstance(pair[1], str) and pair[1].strip():
699
- candidate_text = pair[1]
700
- break
701
- # Also consider current response_text as a data source
702
- if not candidate_text and isinstance(response_text, str) and response_text.strip():
703
- candidate_text = response_text
704
- if candidate_text:
705
- raw_lines = candidate_text.split('\n')
706
- # Normalize lines: strip bullets and markdown symbols
707
- norm_lines = []
708
- for l in raw_lines:
709
- s = l.strip()
710
- if not s:
711
- continue
712
- s = s.lstrip("•*-\t ")
713
- # Remove surrounding markdown emphasis from labels later
714
- norm_lines.append(s)
715
- data = []
716
- for l in norm_lines:
717
- # Accept patterns like "**LABEL**: 123" or "LABEL: 1,234"
718
- m = re.match(r"^(.+?):\s*([0-9][0-9.,]*)$", l)
719
- if m:
720
- label = m.group(1).strip()
721
- # Strip common markdown emphasis
722
- label = re.sub(r"[*_`]+", "", label).strip()
723
- try:
724
- val = float(m.group(2).replace(',', ''))
725
- except Exception:
726
- continue
727
- data.append({"label": label, "value": val})
728
- logger.info(f"Fallback parse from text: extracted {len(data)} items for potential chart")
729
- if len(data) >= 2:
730
- chart_fig = generate_chart(
731
- data=data,
732
- chart_type=desired_type,
733
- x="label",
734
- y="value",
735
- title="Distribución"
736
- )
737
- if chart_fig is not None:
738
- logger.info(f"Chart generated from text fallback: type={desired_type}, items={len(data)}")
739
-
740
- # Update the assistant's message with the response
741
- assistant_message["content"] = response_text
742
-
743
- except Exception as e:
744
- error_msg = f"Error al ejecutar el agente: {str(e)}"
745
- logger.error(error_msg, exc_info=True)
746
- assistant_message["content"] = f"## ❌ Error\n\n{error_msg}"
747
-
748
- # Return the message in the correct format for Gradio Chatbot
749
- # Format: list of tuples where each tuple is (user_msg, bot_msg)
750
- # For a single response, we return [(None, message)]
751
- message_content = ""
752
-
753
- if isinstance(assistant_message, dict) and "content" in assistant_message:
754
- message_content = assistant_message["content"]
755
- elif isinstance(assistant_message, str):
756
- message_content = assistant_message
757
- else:
758
- message_content = str(assistant_message)
759
-
760
- # Return the assistant's response and an optional interactive chart figure
761
- if chart_fig is None:
762
- logger.info("No chart generated for this turn.")
763
- else:
764
- logger.info("Returning a chart figure to UI.")
765
- return message_content, chart_fig
766
-
767
- except Exception as e:
768
- error_msg = f"## ❌ Error\n\nOcurrió un error al procesar tu solicitud:\n\n```\n{str(e)}\n```"
769
- logger.error(f"Error in stream_agent_response: {str(e)}", exc_info=True)
770
- # Return error message and no chart
771
- return error_msg, None
772
-
773
- # Custom CSS for the app
774
- custom_css = """
775
- .gradio-container {
776
- max-width: 1200px !important;
777
- margin: 0 auto !important;
778
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
779
- }
780
-
781
- #chatbot {
782
- min-height: 500px;
783
- border: 1px solid #e0e0e0;
784
- border-radius: 8px;
785
- margin-bottom: 20px;
786
- padding: 20px;
787
- background-color: #f9f9f9;
788
- }
789
-
790
- .user-message, .bot-message {
791
- padding: 12px 16px;
792
- border-radius: 18px;
793
- margin: 8px 0;
794
- max-width: 80%;
795
- line-height: 1.5;
796
- }
797
-
798
- .user-message {
799
- background-color: #007bff;
800
- color: white;
801
- margin-left: auto;
802
- border-bottom-right-radius: 4px;
803
- }
804
-
805
- .bot-message {
806
- background-color: #f1f1f1;
807
- color: #333;
808
- margin-right: auto;
809
- border-bottom-left-radius: 4px;
810
- }
811
-
812
- #question-input textarea {
813
- min-height: 50px !important;
814
- border-radius: 8px !important;
815
- padding: 12px !important;
816
- font-size: 16px !important;
817
- }
818
-
819
- #send-button {
820
- height: 100%;
821
- background-color: #007bff !important;
822
- color: white !important;
823
- border: none !important;
824
- border-radius: 8px !important;
825
- font-weight: 500 !important;
826
- transition: background-color 0.2s !important;
827
- }
828
-
829
- #send-button:hover {
830
- background-color: #0056b3 !important;
831
- }
832
-
833
- .status-message {
834
- text-align: center;
835
- color: #666;
836
- font-style: italic;
837
- margin: 10px 0;
838
- }
839
- """
840
-
841
- def create_ui():
842
- """Crea y devuelve los componentes de la interfaz de usuario de Gradio."""
843
- # Verificar el estado del entorno
844
- env_ok, env_message = check_environment()
845
-
846
- # Crear el tema personalizado
847
- theme = gr.themes.Soft(
848
- primary_hue="blue",
849
- secondary_hue="indigo",
850
- neutral_hue="slate"
851
- )
852
-
853
- with gr.Blocks(
854
- css=custom_css,
855
- title="Asistente de Base de Datos SQL",
856
- theme=theme
857
- ) as demo:
858
- # Encabezado
859
- gr.Markdown("""
860
- # 🤖 Asistente de Base de Datos SQL
861
-
862
- Haz preguntas en lenguaje natural sobre tu base de datos y obtén resultados de consultas SQL.
863
- """)
864
-
865
- # Mensaje de estado
866
- if not env_ok:
867
- gr.Warning("⚠️ " + env_message)
868
-
869
- # Create the chat interface
870
- with gr.Row():
871
- chatbot = gr.Chatbot(
872
- value=[],
873
- elem_id="chatbot",
874
- type="messages", # migrate to messages format to avoid deprecation
875
- avatar_images=(
876
- None,
877
- (os.path.join(os.path.dirname(__file__), "logo.svg")),
878
- ),
879
- height=600,
880
- render_markdown=True, # Enable markdown rendering
881
- show_label=False,
882
- show_share_button=False,
883
- container=True,
884
- layout="panel" # Better layout for messages
885
- )
886
-
887
- # Chart display area (interactive Plotly figure)
888
- # In Gradio 5, gr.Plot accepts a plotly.graph_objects.Figure
889
- chart_display = gr.Plot(
890
- label="📊 Visualización",
891
- )
892
-
893
- # Input area
894
- with gr.Row():
895
- question_input = gr.Textbox(
896
- label="",
897
- placeholder="Escribe tu pregunta aquí...",
898
- container=False,
899
- scale=5,
900
- min_width=300,
901
- max_lines=3,
902
- autofocus=True,
903
- elem_id="question-input"
904
- )
905
- submit_button = gr.Button(
906
- "Enviar",
907
- variant="primary",
908
- min_width=100,
909
- scale=1,
910
- elem_id="send-button"
911
- )
912
-
913
- # System status
914
- with gr.Accordion("ℹ️ Estado del sistema", open=not env_ok):
915
- if not DEPENDENCIES_AVAILABLE:
916
- gr.Markdown("""
917
- ## ❌ Dependencias faltantes
918
-
919
- Para ejecutar esta aplicación localmente, necesitas instalar las dependencias:
920
-
921
- ```bash
922
- pip install -r requirements.txt
923
- ```
924
- """)
925
- else:
926
- if not agent:
927
- gr.Markdown(f"""
928
- ## ⚠️ Configuración incompleta
929
-
930
- No se pudo inicializar el agente de base de datos. Por favor, verifica que:
931
-
932
- 1. Todas las variables de entorno estén configuradas correctamente
933
- 2. La base de datos esté accesible
934
- 3. La API de Google Gemini esté configurada
935
-
936
- **Error:** {agent_error if agent_error else 'No se pudo determinar el error'}
937
-
938
- ### Configuración local
939
-
940
- Crea un archivo `.env` en la raíz del proyecto con las siguientes variables:
941
-
942
- ```
943
- DB_USER=tu_usuario
944
- DB_PASSWORD=tu_contraseña
945
- DB_HOST=tu_servidor
946
- DB_NAME=tu_base_de_datos
947
- GOOGLE_API_KEY=tu_api_key_de_google
948
- ```
949
- """)
950
- else:
951
- if os.getenv('SPACE_ID'):
952
- # Modo demo en Hugging Face Spaces
953
- gr.Markdown("""
954
- ## 🚀 Modo Demo
955
-
956
- Esta es una demostración del asistente de base de datos SQL. Para usar la versión completa con conexión a base de datos:
957
-
958
- 1. Clona este espacio en tu cuenta de Hugging Face
959
- 2. Configura las variables de entorno en la configuración del espacio:
960
- - `DB_USER`: Tu usuario de base de datos
961
- - `DB_PASSWORD`: Tu contraseña de base de datos
962
- - `DB_HOST`: La dirección del servidor de base de datos
963
- - `DB_NAME`: El nombre de la base de datos
964
- - `GOOGLE_API_KEY`: Tu clave de API de Google Gemini
965
-
966
- **Nota:** Actualmente estás en modo de solo demostración.
967
- """)
968
- else:
969
- gr.Markdown("""
970
- ## ✅ Sistema listo
971
-
972
- El asistente está listo para responder tus preguntas sobre la base de datos.
973
- """)
974
-
975
- # Hidden component for streaming output
976
- streaming_output_display = gr.Textbox(visible=False)
977
-
978
- return demo, chatbot, chart_display, question_input, submit_button, streaming_output_display
979
 
980
  def create_application():
981
  """Create and configure the Gradio application."""
982
  # Create the UI components
983
  demo, chatbot, chart_display, question_input, submit_button, streaming_output_display = create_ui()
984
 
 
 
 
 
 
 
 
 
985
  def user_message(user_input: str, chat_history: List[Dict[str, str]]) -> Tuple[str, List[Dict[str, str]]]:
986
  """Add user message to chat history (messages format) and clear input."""
987
  if not user_input.strip():
@@ -1034,37 +81,6 @@ def create_application():
1034
  # Append assistant message back into messages history
1035
  chat_history.append({"role": "assistant", "content": assistant_message})
1036
 
1037
- # If user asked for a chart but none was produced, try to build one
1038
- # from the latest assistant text using the same fallback logic.
1039
- if chart_fig is None:
1040
- wants_chart, desired_type = detect_chart_preferences(question)
1041
- if wants_chart and isinstance(assistant_message, str):
1042
- candidate_text = assistant_message
1043
- raw_lines = candidate_text.split('\n')
1044
- norm_lines = []
1045
- for l in raw_lines:
1046
- s = l.strip().lstrip("•*\t -")
1047
- if s:
1048
- norm_lines.append(s)
1049
- data = []
1050
- for l in norm_lines:
1051
- m = re.match(r"^(.+?):\s*([0-9][0-9.,]*)$", l)
1052
- if m:
1053
- label = re.sub(r"[*_`]+", "", m.group(1)).strip()
1054
- try:
1055
- val = float(m.group(2).replace(',', ''))
1056
- except Exception:
1057
- continue
1058
- data.append({"label": label, "value": val})
1059
- if len(data) >= 2:
1060
- chart_fig = generate_chart(
1061
- data=data,
1062
- chart_type=desired_type,
1063
- x="label",
1064
- y="value",
1065
- title="Distribución"
1066
- )
1067
-
1068
  logger.info("Response generation complete")
1069
  return chat_history, chart_fig
1070
 
 
12
  import plotly.express as px
13
  import plotly.graph_objects as go
14
  from plotly.subplots import make_subplots
15
+ from api import app as flask_app
 
 
 
16
 
17
+ # ... (resto del código existente sin cambios) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  def create_application():
20
  """Create and configure the Gradio application."""
21
  # Create the UI components
22
  demo, chatbot, chart_display, question_input, submit_button, streaming_output_display = create_ui()
23
 
24
+ # Montar la API Flask en la aplicación Gradio
25
+ if os.getenv('SPACE_ID'):
26
+ demo = gr.mount_gradio_app(
27
+ flask_app,
28
+ "/api", # Prefijo para los endpoints de la API
29
+ lambda: True # Autenticación deshabilitada
30
+ )
31
+
32
  def user_message(user_input: str, chat_history: List[Dict[str, str]]) -> Tuple[str, List[Dict[str, str]]]:
33
  """Add user message to chat history (messages format) and clear input."""
34
  if not user_input.strip():
 
81
  # Append assistant message back into messages history
82
  chat_history.append({"role": "assistant", "content": assistant_message})
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  logger.info("Response generation complete")
85
  return chat_history, chart_fig
86
 
requirements.txt CHANGED
@@ -15,3 +15,4 @@ python-multipart>=0.0.18 # Required by gradio
15
  plotly==5.18.0 # For interactive charts
16
  kaleido==0.2.1 # For saving plotly charts as images
17
  tabulate>=0.9.0 # Enables DataFrame.to_markdown used for chart parsing
 
 
15
  plotly==5.18.0 # For interactive charts
16
  kaleido==0.2.1 # For saving plotly charts as images
17
  tabulate>=0.9.0 # Enables DataFrame.to_markdown used for chart parsing
18
+ flask>=2.0.0 # Required for API endpoints