Jeremy Live commited on
Commit
241f37e
·
1 Parent(s): c22eca1

Revert "API solved"

Browse files

This reverts commit 0f16c64a587e372a3a073c11e3f7e374cae9dbfd.

Files changed (4) hide show
  1. README.md +3 -96
  2. api.py +0 -103
  3. app.py +994 -10
  4. requirements.txt +0 -1
README.md CHANGED
@@ -19,104 +19,11 @@ 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
- - 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.
 
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.
api.py DELETED
@@ -1,103 +0,0 @@
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,23 +12,976 @@ 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
- 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,6 +1034,37 @@ def create_application():
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
 
 
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
  # 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
 
requirements.txt CHANGED
@@ -15,4 +15,3 @@ 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
18
- flask>=2.0.0 # Required for API endpoints
 
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