galbendavids commited on
Commit
85b82f6
·
1 Parent(s): a52ea90

יצירת גישה חדשה SQL-based: SQL query generator, executor, response synthesizer, API endpoint, ו-frontend support

Browse files
Files changed (5) hide show
  1. .query_history.json +24 -0
  2. app/api.py +53 -0
  3. app/sql_service.py +375 -0
  4. app/static/app.js +112 -6
  5. app/static/index.html +7 -0
.query_history.json CHANGED
@@ -10,5 +10,29 @@
10
  "response": {
11
  "summary": "את הנוחות של המשתמש נוחות השירות והאינטואיטיביות של הממשק שירות לקוחות על הפנים מענה זמין יותר בשירות לקוחות שירות קל וידידותי למשתמש "
12
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
  ]
 
10
  "response": {
11
  "summary": "את הנוחות של המשתמש נוחות השירות והאינטואיטיביות של הממשק שירות לקוחות על הפנים מענה זמין יותר בשירות לקוחות שירות קל וידידותי למשתמש "
12
  }
13
+ },
14
+ {
15
+ "query": "איך המשתמשים מרגישים לגבי השירות?",
16
+ "response": {
17
+ "summary": "את הנוחות של המשתמש נוחות השירות והאינטואיטיביות של הממשק שירות לקוחות על הפנים מענה זמין יותר בשירות לקוחות שירות קל וידידותי למשתמש "
18
+ }
19
+ },
20
+ {
21
+ "query": "בלה",
22
+ "response": {
23
+ "summary": "תוגה בלה בלה בלה זריז סבבה סבבה"
24
+ }
25
+ },
26
+ {
27
+ "query": "מה שלומך אחי?",
28
+ "response": {
29
+ "summary": "אלופים אתם סיבכתם עם הורה 1 הורה 2 אבתי מאוד\nקליל, פשוט וזריז אתם אלופים אתם אלופים"
30
+ }
31
+ },
32
+ {
33
+ "query": "כמה אחוזים של משתמשים אוהב לאכול שוקולד?",
34
+ "response": {
35
+ "summary": "0 משובים מכילים את הביטוי 'של משתמשים אוהב לאכול שוקולד'."
36
+ }
37
  }
38
  ]
app/api.py CHANGED
@@ -15,6 +15,7 @@ from .config import settings
15
  from .data_loader import load_feedback
16
  from .embedding import EmbeddingModel
17
  from .rag_service import RAGService
 
18
  from .sentiment import analyze_sentiments
19
  from .topics import kmeans_topics
20
  from .vector_store import FaissVectorStore
@@ -22,6 +23,7 @@ from .vector_store import FaissVectorStore
22
 
23
  app = FastAPI(title="Feedback Analysis RAG Agent", version="1.0.0", default_response_class=ORJSONResponse)
24
  svc = RAGService()
 
25
  embedder = svc.embedder
26
 
27
  # Simple in-memory history persisted best-effort to `.query_history.json`
@@ -55,6 +57,14 @@ class QueryResponse(BaseModel):
55
  results: Optional[List[Dict[str, Any]]] = None # Optional results for frontend
56
 
57
 
 
 
 
 
 
 
 
 
58
  @app.post("/health")
59
  def health() -> Dict[str, str]:
60
  """Healthcheck endpoint.
@@ -64,6 +74,49 @@ def health() -> Dict[str, str]:
64
  return {"status": "ok"}
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  @app.post("/ingest")
68
  def ingest() -> Dict[str, Any]:
69
  """Build the vector index from Feedback.csv"""
 
15
  from .data_loader import load_feedback
16
  from .embedding import EmbeddingModel
17
  from .rag_service import RAGService
18
+ from .sql_service import SQLFeedbackService
19
  from .sentiment import analyze_sentiments
20
  from .topics import kmeans_topics
21
  from .vector_store import FaissVectorStore
 
23
 
24
  app = FastAPI(title="Feedback Analysis RAG Agent", version="1.0.0", default_response_class=ORJSONResponse)
25
  svc = RAGService()
26
+ sql_svc = SQLFeedbackService() # SQL-based service
27
  embedder = svc.embedder
28
 
29
  # Simple in-memory history persisted best-effort to `.query_history.json`
 
57
  results: Optional[List[Dict[str, Any]]] = None # Optional results for frontend
58
 
59
 
60
+ class SQLQueryResponse(BaseModel):
61
+ query: str
62
+ summary: str
63
+ sql_queries: List[str]
64
+ query_results: List[Dict[str, Any]] # Results of SQL queries
65
+ visualizations: Optional[List[Dict[str, Any]]] = None
66
+
67
+
68
  @app.post("/health")
69
  def health() -> Dict[str, str]:
70
  """Healthcheck endpoint.
 
74
  return {"status": "ok"}
75
 
76
 
77
+ @app.post("/query-sql", response_model=SQLQueryResponse)
78
+ def query_sql(req: QueryRequest) -> SQLQueryResponse:
79
+ """SQL-based question answering over feedback data.
80
+
81
+ This endpoint uses a SQL-based approach:
82
+ 1. LLM generates 1-5 SQL queries
83
+ 2. Executes queries on feedback data
84
+ 3. LLM synthesizes comprehensive answer
85
+ 4. Returns answer with query results and visualizations
86
+ """
87
+ try:
88
+ result = sql_svc.analyze_query(req.query)
89
+
90
+ # Convert query results to JSON-serializable format
91
+ query_results = []
92
+ for qr in result.query_results:
93
+ query_results.append({
94
+ "query": qr.query,
95
+ "result": qr.result.to_dict('records') if not qr.error else [],
96
+ "error": qr.error,
97
+ "row_count": len(qr.result) if not qr.error else 0
98
+ })
99
+
100
+ return SQLQueryResponse(
101
+ query=result.user_query,
102
+ summary=result.summary,
103
+ sql_queries=result.sql_queries,
104
+ query_results=query_results,
105
+ visualizations=result.visualizations
106
+ )
107
+ except Exception as e:
108
+ import traceback
109
+ error_details = traceback.format_exc()
110
+ print(f"Error in /query-sql endpoint: {error_details}", flush=True)
111
+ return SQLQueryResponse(
112
+ query=req.query,
113
+ summary=f"שגיאה: {str(e)}. אנא בדוק את הלוגים לפרטים נוספים.",
114
+ sql_queries=[],
115
+ query_results=[],
116
+ visualizations=None
117
+ )
118
+
119
+
120
  @app.post("/ingest")
121
  def ingest() -> Dict[str, Any]:
122
  """Build the vector index from Feedback.csv"""
app/sql_service.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SQL-based Feedback Analysis Service
3
+
4
+ This module implements a SQL-based approach to analyzing feedback:
5
+ 1. LLM analyzes user query
6
+ 2. LLM generates 1-5 SQL queries to answer the question
7
+ 3. Execute SQL queries on the feedback DataFrame
8
+ 4. LLM synthesizes a comprehensive answer from query + SQL queries + results
9
+ 5. (Optional) Generate visualizations of the results
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ from dataclasses import dataclass
17
+ from typing import List, Dict, Any, Optional
18
+ import pandas as pd
19
+ import sqlite3
20
+ from io import StringIO
21
+
22
+ from .config import settings
23
+ from .data_loader import load_feedback
24
+
25
+ try:
26
+ from openai import OpenAI # type: ignore
27
+ except Exception:
28
+ OpenAI = None
29
+
30
+ try:
31
+ import google.generativeai as genai # type: ignore
32
+ except Exception:
33
+ genai = None
34
+
35
+
36
+ @dataclass
37
+ class SQLQueryResult:
38
+ """Result of a single SQL query execution."""
39
+ query: str
40
+ result: pd.DataFrame
41
+ error: Optional[str] = None
42
+
43
+
44
+ @dataclass
45
+ class AnalysisResult:
46
+ """Complete analysis result."""
47
+ user_query: str
48
+ sql_queries: List[str]
49
+ query_results: List[SQLQueryResult]
50
+ summary: str
51
+ visualizations: Optional[List[Dict[str, Any]]] = None
52
+
53
+
54
+ class SQLFeedbackService:
55
+ """Service for SQL-based feedback analysis."""
56
+
57
+ def __init__(self):
58
+ self.df: Optional[pd.DataFrame] = None
59
+ self._load_data()
60
+
61
+ def _load_data(self):
62
+ """Load feedback data into memory."""
63
+ try:
64
+ self.df = load_feedback()
65
+ print(f"Loaded {len(self.df)} feedback records", flush=True)
66
+ except Exception as e:
67
+ print(f"Error loading feedback data: {e}", flush=True)
68
+ self.df = None
69
+
70
+ def _get_schema_info(self) -> str:
71
+ """Get schema information for the feedback table."""
72
+ if self.df is None:
73
+ return "No data available"
74
+
75
+ schema_info = f"""
76
+ טבלת Feedback מכילה את השדות הבאים:
77
+ - ID: מזהה ייחודי של כל משוב (מספר שלם)
78
+ - ServiceName: שם השירות הדיגיטלי (טקסט)
79
+ - Level: הציון שהמשתמש נתן לשירות (מספר שלם מ-1 עד 5, כאשר 1=גרוע, 5=מעולה)
80
+ - Text: הטקסט החופשי שהמשתמש הזין כחלק מהפידבק (טקסט)
81
+
82
+ סטטיסטיקות כלליות:
83
+ - סך הכל משובים: {len(self.df)}
84
+ - מספר שירותים ייחודיים: {self.df['ServiceName'].nunique()}
85
+ - חלוקת דירוגים: {dict(self.df['Level'].value_counts().sort_index())}
86
+ - דירוג ממוצע: {self.df['Level'].mean():.2f}
87
+ """
88
+ return schema_info
89
+
90
+ def analyze_query(self, query: str) -> AnalysisResult:
91
+ """
92
+ Main analysis pipeline:
93
+ 1. Analyze user query
94
+ 2. Generate SQL queries
95
+ 3. Execute SQL queries
96
+ 4. Synthesize answer
97
+ """
98
+ if self.df is None:
99
+ raise ValueError("No feedback data available. Please ensure Feedback.csv exists.")
100
+
101
+ # Step 1: Generate SQL queries
102
+ sql_queries = self._generate_sql_queries(query)
103
+
104
+ # Step 2: Execute SQL queries
105
+ query_results = self._execute_sql_queries(sql_queries)
106
+
107
+ # Step 3: Synthesize answer
108
+ summary = self._synthesize_answer(query, sql_queries, query_results)
109
+
110
+ # Step 4: (Optional) Generate visualizations
111
+ visualizations = self._generate_visualizations(query_results)
112
+
113
+ return AnalysisResult(
114
+ user_query=query,
115
+ sql_queries=sql_queries,
116
+ query_results=query_results,
117
+ summary=summary,
118
+ visualizations=visualizations
119
+ )
120
+
121
+ def _generate_sql_queries(self, query: str) -> List[str]:
122
+ """
123
+ Use LLM to generate 1-5 SQL queries that will help answer the user's question.
124
+ """
125
+ schema_info = self._get_schema_info()
126
+
127
+ prompt = f"""אתה אנליסט נתונים מומחה. המשתמש שאל שאלה על משובי משתמשים.
128
+
129
+ שאלת המשתמש: {query}
130
+
131
+ מידע על הטבלה:
132
+ {schema_info}
133
+
134
+ המשימה שלך: צור 1 עד 5 שאילתות SQL שיעזרו לענות על השאלה. כל שאילתה צריכה להיות שימושית וממוקדת.
135
+
136
+ כללים חשובים:
137
+ 1. השתמש בשמות השדות המדויקים: ID, ServiceName, Level, Text
138
+ 2. Level הוא מספר שלם מ-1 עד 5 (1=גרוע, 5=מעולה)
139
+ 3. ServiceName הוא טקסט
140
+ 4. Text הוא הטקסט החופשי של המשוב
141
+ 5. כל שאילתה צריכה להיות תקפה SQLite
142
+ 6. השתמש בפונקציות SQL סטנדרטיות: COUNT, AVG, GROUP BY, WHERE, LIKE, etc.
143
+ 7. אם השאלה מתייחסת לטקסט, השתמש ב-LIKE או INSTR לחיפוש
144
+ 8. אם השאלה מתייחסת לדירוגים, השתמש ב-Level עם תנאים מתאימים
145
+ 9. אם השאלה מתייחסת לשירותים, השתמש ב-ServiceName
146
+
147
+ פורמט התשובה - JSON בלבד:
148
+ {{
149
+ "queries": [
150
+ "SELECT ...",
151
+ "SELECT ...",
152
+ ...
153
+ ]
154
+ }}
155
+
156
+ תן רק את ה-JSON, ללא טקסט נוסף."""
157
+
158
+ # Try Gemini first
159
+ if settings.gemini_api_key and genai is not None:
160
+ try:
161
+ genai.configure(api_key=settings.gemini_api_key)
162
+ model = genai.GenerativeModel("gemini-1.5-flash")
163
+ response = model.generate_content(prompt)
164
+ text = getattr(response, "text", None)
165
+ if text:
166
+ return self._parse_sql_queries(text)
167
+ except Exception as e:
168
+ print(f"Gemini error in SQL generation: {e}", flush=True)
169
+
170
+ # Fallback to OpenAI
171
+ if settings.openai_api_key and OpenAI is not None:
172
+ try:
173
+ client = OpenAI(api_key=settings.openai_api_key)
174
+ response = client.chat.completions.create(
175
+ model="gpt-4o-mini",
176
+ messages=[{"role": "user", "content": prompt}],
177
+ temperature=0.3,
178
+ )
179
+ text = response.choices[0].message.content
180
+ if text:
181
+ return self._parse_sql_queries(text)
182
+ except Exception as e:
183
+ print(f"OpenAI error in SQL generation: {e}", flush=True)
184
+
185
+ # Fallback: return empty list
186
+ return []
187
+
188
+ def _parse_sql_queries(self, text: str) -> List[str]:
189
+ """Parse SQL queries from LLM response."""
190
+ # Try to extract JSON
191
+ try:
192
+ # Remove markdown code blocks if present
193
+ text = re.sub(r'```json\s*', '', text)
194
+ text = re.sub(r'```\s*', '', text)
195
+ text = text.strip()
196
+
197
+ # Try to parse as JSON
198
+ data = json.loads(text)
199
+ if isinstance(data, dict) and "queries" in data:
200
+ queries = data["queries"]
201
+ if isinstance(queries, list):
202
+ return [q for q in queries if isinstance(q, str) and q.strip()]
203
+ except Exception:
204
+ pass
205
+
206
+ # Fallback: try to extract SQL queries directly
207
+ sql_pattern = r'SELECT\s+.*?(?=\n\n|\nSELECT|$)'
208
+ matches = re.findall(sql_pattern, text, re.IGNORECASE | re.DOTALL)
209
+ if matches:
210
+ return [m.strip() for m in matches]
211
+
212
+ return []
213
+
214
+ def _execute_sql_queries(self, sql_queries: List[str]) -> List[SQLQueryResult]:
215
+ """Execute SQL queries on the feedback DataFrame."""
216
+ if self.df is None:
217
+ return []
218
+
219
+ results = []
220
+
221
+ # Create in-memory SQLite database
222
+ conn = sqlite3.connect(':memory:')
223
+ try:
224
+ # Write DataFrame to SQLite
225
+ self.df.to_sql('feedback', conn, index=False, if_exists='replace')
226
+
227
+ for query in sql_queries:
228
+ try:
229
+ # Execute query
230
+ result_df = pd.read_sql_query(query, conn)
231
+ results.append(SQLQueryResult(
232
+ query=query,
233
+ result=result_df,
234
+ error=None
235
+ ))
236
+ except Exception as e:
237
+ results.append(SQLQueryResult(
238
+ query=query,
239
+ result=pd.DataFrame(),
240
+ error=str(e)
241
+ ))
242
+ finally:
243
+ conn.close()
244
+
245
+ return results
246
+
247
+ def _synthesize_answer(self, query: str, sql_queries: List[str],
248
+ query_results: List[SQLQueryResult]) -> str:
249
+ """
250
+ Use LLM to synthesize a comprehensive answer from:
251
+ - User query
252
+ - SQL queries that were executed
253
+ - Results of those queries
254
+ """
255
+ # Format query results for the prompt
256
+ results_text = ""
257
+ for i, qr in enumerate(query_results, 1):
258
+ results_text += f"\nשאילתה {i}:\n{qr.query}\n\n"
259
+ if qr.error:
260
+ results_text += f"שגיאה: {qr.error}\n\n"
261
+ else:
262
+ # Format result as table
263
+ if len(qr.result) == 0:
264
+ results_text += "תוצאה: אין תוצאות\n\n"
265
+ else:
266
+ results_text += f"תוצאה ({len(qr.result)} שורות):\n"
267
+ results_text += qr.result.to_string(index=False)
268
+ results_text += "\n\n"
269
+
270
+ prompt = f"""אתה אנליסט עסקי בכיר במשרד הפנים, מומחה בייעול תהליכים דיגיטליים ושיפור חוויות המשתמשים.
271
+
272
+ המשתמש שאל שאלה על משובי משתמשים על שירותים דיגיטליים.
273
+
274
+ שאלת המשתמש: {query}
275
+
276
+ כדי לענות על השאלה, בוצעו השאילתות הבאות והתקב��ו התוצאות הבאות:
277
+
278
+ {results_text}
279
+
280
+ המשימה שלך: כתוב תשובה מסכמת, ברורה ובשפה חופשית שמבוססת על התוצאות.
281
+
282
+ דרישות:
283
+ 1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים)
284
+ 2. תשובה ברורה ומסודרת - לא גיבוב של מילים
285
+ 3. כלול מספרים מדויקים מהתוצאות
286
+ 4. הסבר את המשמעות העסקית של הממצאים
287
+ 5. כלול המלצות מעשיות לשיפור
288
+ 6. כתוב בעברית מקצועית וקולחת
289
+ 7. תן תשובה שמראה הבנה עמוקה של הנתונים
290
+
291
+ מבנה התשובה:
292
+ 1. פתיחה - סיכום מנהלים קצר (2-3 משפטים)
293
+ 2. ניתוח מפורט של הממצאים (3-4 פסקאות)
294
+ 3. תובנות עסקיות והמלצות (2-3 פסקאות)
295
+ 4. סיכום (1-2 משפטים)
296
+
297
+ אם יש שגיאות בשאילתות, ציין זאת בתשובה."""
298
+
299
+ # Try Gemini first
300
+ if settings.gemini_api_key and genai is not None:
301
+ try:
302
+ genai.configure(api_key=settings.gemini_api_key)
303
+ model = genai.GenerativeModel("gemini-1.5-flash")
304
+ generation_config = {
305
+ "temperature": 0.8,
306
+ "top_p": 0.95,
307
+ "top_k": 40,
308
+ "max_output_tokens": 4000,
309
+ }
310
+ response = model.generate_content(prompt, generation_config=generation_config)
311
+ text = getattr(response, "text", None)
312
+ if text and text.strip():
313
+ return text.strip()
314
+ except Exception as e:
315
+ print(f"Gemini error in synthesis: {e}", flush=True)
316
+
317
+ # Fallback to OpenAI
318
+ if settings.openai_api_key and OpenAI is not None:
319
+ try:
320
+ client = OpenAI(api_key=settings.openai_api_key)
321
+ response = client.chat.completions.create(
322
+ model="gpt-4o-mini",
323
+ messages=[{"role": "user", "content": prompt}],
324
+ temperature=0.8,
325
+ max_tokens=3000,
326
+ )
327
+ text = response.choices[0].message.content
328
+ if text:
329
+ return text.strip()
330
+ except Exception as e:
331
+ print(f"OpenAI error in synthesis: {e}", flush=True)
332
+
333
+ # Fallback: return basic summary
334
+ return f"בוצעו {len(sql_queries)} שאילתות. {len([r for r in query_results if not r.error])} הצליחו."
335
+
336
+ def _generate_visualizations(self, query_results: List[SQLQueryResult]) -> Optional[List[Dict[str, Any]]]:
337
+ """
338
+ Generate visualization specifications for query results.
339
+ Returns a list of visualization configs (for frontend to render).
340
+ """
341
+ visualizations = []
342
+
343
+ for i, qr in enumerate(query_results, 1):
344
+ if qr.error or len(qr.result) == 0:
345
+ continue
346
+
347
+ # Determine visualization type based on result structure
348
+ result = qr.result
349
+
350
+ # If result has 2 columns, might be a bar chart
351
+ if len(result.columns) == 2:
352
+ col1, col2 = result.columns
353
+ # If first column is categorical and second is numeric
354
+ if result[col2].dtype in ['int64', 'float64']:
355
+ visualizations.append({
356
+ "type": "bar",
357
+ "title": f"תוצאה של שאילתה {i}",
358
+ "x": col1,
359
+ "y": col2,
360
+ "data": result.to_dict('records')
361
+ })
362
+
363
+ # If result has 1 column with numeric values, might be a distribution
364
+ elif len(result.columns) == 1:
365
+ col = result.columns[0]
366
+ if result[col].dtype in ['int64', 'float64']:
367
+ visualizations.append({
368
+ "type": "histogram",
369
+ "title": f"תוצאה של שאילתה {i}",
370
+ "x": col,
371
+ "data": result[col].tolist()
372
+ })
373
+
374
+ return visualizations if visualizations else None
375
+
app/static/app.js CHANGED
@@ -60,7 +60,8 @@ async function sendQuery() {
60
  return;
61
  }
62
 
63
- const body = { query: q, top_k: 100 }; // Use 100 records for comprehensive RAG-based analysis
 
64
 
65
  // Show loading state
66
  const sendBtn = document.getElementById('send');
@@ -69,7 +70,12 @@ async function sendQuery() {
69
  sendBtn.textContent = '⏳ שולח...';
70
 
71
  try {
72
- const r = await fetch('/query', {
 
 
 
 
 
73
  method: 'POST',
74
  headers: {'Content-Type':'application/json'},
75
  body: JSON.stringify(body)
@@ -93,16 +99,29 @@ async function sendQuery() {
93
  summaryDiv.innerHTML = '<span style="color: #d32f2f;">לא התקבלה תשובה מהשרת</span>';
94
  }
95
 
96
- // Show sources if checkbox is checked
97
  const showSources = document.getElementById('show-sources')?.checked;
98
  const sourcesDiv = document.getElementById('resp-sources');
99
- if (showSources && j.results && j.results.length > 0) {
100
- sourcesDiv.style.display = 'block';
101
- sourcesDiv.innerHTML = formatSources(j.results);
 
 
 
 
 
 
 
 
102
  } else {
103
  if (sourcesDiv) sourcesDiv.style.display = 'none';
104
  }
105
 
 
 
 
 
 
106
  // Refresh history
107
  await refreshHistory();
108
 
@@ -121,6 +140,93 @@ async function sendQuery() {
121
  }
122
  }
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  function formatResponse(text) {
125
  // Format markdown-like text (bold, lists, etc.)
126
  let formatted = escapeHtml(text);
 
60
  return;
61
  }
62
 
63
+ // Check which approach to use
64
+ const approach = document.querySelector('input[name="approach"]:checked')?.value || 'sql';
65
 
66
  // Show loading state
67
  const sendBtn = document.getElementById('send');
 
70
  sendBtn.textContent = '⏳ שולח...';
71
 
72
  try {
73
+ let endpoint = approach === 'sql' ? '/query-sql' : '/query';
74
+ const body = approach === 'sql'
75
+ ? { query: q, top_k: 5 } // top_k not used in SQL approach but kept for compatibility
76
+ : { query: q, top_k: 100 };
77
+
78
+ const r = await fetch(endpoint, {
79
  method: 'POST',
80
  headers: {'Content-Type':'application/json'},
81
  body: JSON.stringify(body)
 
99
  summaryDiv.innerHTML = '<span style="color: #d32f2f;">לא התקבלה תשובה מהשרת</span>';
100
  }
101
 
102
+ // Show sources/query results if checkbox is checked
103
  const showSources = document.getElementById('show-sources')?.checked;
104
  const sourcesDiv = document.getElementById('resp-sources');
105
+
106
+ if (showSources) {
107
+ if (approach === 'sql' && j.query_results && j.query_results.length > 0) {
108
+ sourcesDiv.style.display = 'block';
109
+ sourcesDiv.innerHTML = formatSQLResults(j);
110
+ } else if (approach === 'rag' && j.results && j.results.length > 0) {
111
+ sourcesDiv.style.display = 'block';
112
+ sourcesDiv.innerHTML = formatSources(j.results);
113
+ } else {
114
+ if (sourcesDiv) sourcesDiv.style.display = 'none';
115
+ }
116
  } else {
117
  if (sourcesDiv) sourcesDiv.style.display = 'none';
118
  }
119
 
120
+ // Show visualizations if SQL approach and visualizations available
121
+ if (approach === 'sql' && j.visualizations && j.visualizations.length > 0) {
122
+ showVisualizations(j.visualizations);
123
+ }
124
+
125
  // Refresh history
126
  await refreshHistory();
127
 
 
140
  }
141
  }
142
 
143
+ function formatSQLResults(data) {
144
+ let html = '<div style="margin-top: 20px;"><h4 style="color: #1976d2; margin-bottom: 12px;">שאילתות SQL שבוצעו:</h4>';
145
+
146
+ if (data.sql_queries && data.sql_queries.length > 0) {
147
+ html += '<div style="margin-bottom: 20px;">';
148
+ data.sql_queries.forEach((query, idx) => {
149
+ html += `<div style="background: #f5f5f5; padding: 12px; border-radius: 8px; margin-bottom: 12px; font-family: monospace; font-size: 13px; direction: ltr; text-align: left;">`;
150
+ html += `<strong>שאילתה ${idx + 1}:</strong><br>${escapeHtml(query)}</div>`;
151
+ });
152
+ html += '</div>';
153
+ }
154
+
155
+ if (data.query_results && data.query_results.length > 0) {
156
+ html += '<h4 style="color: #1976d2; margin-bottom: 12px;">תוצאות:</h4>';
157
+ data.query_results.forEach((qr, idx) => {
158
+ html += `<div style="margin-bottom: 20px; padding: 16px; background: #f8f9fa; border-radius: 8px;">`;
159
+ html += `<strong>תוצאה ${idx + 1}:</strong> `;
160
+ if (qr.error) {
161
+ html += `<span style="color: #d32f2f;">שגיאה: ${escapeHtml(qr.error)}</span>`;
162
+ } else {
163
+ html += `<span style="color: #4caf50;">${qr.row_count} שורות</span>`;
164
+ if (qr.result && qr.result.length > 0) {
165
+ html += '<table style="width: 100%; margin-top: 12px; border-collapse: collapse;">';
166
+ // Header
167
+ html += '<thead><tr style="background: #e3f2fd;">';
168
+ Object.keys(qr.result[0]).forEach(col => {
169
+ html += `<th style="padding: 8px; text-align: right; border: 1px solid #ddd;">${escapeHtml(col)}</th>`;
170
+ });
171
+ html += '</tr></thead><tbody>';
172
+ // Rows (limit to 10 for display)
173
+ qr.result.slice(0, 10).forEach(row => {
174
+ html += '<tr>';
175
+ Object.values(row).forEach(val => {
176
+ html += `<td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(String(val))}</td>`;
177
+ });
178
+ html += '</tr>';
179
+ });
180
+ html += '</tbody></table>';
181
+ if (qr.result.length > 10) {
182
+ html += `<div style="margin-top: 8px; color: #666; font-size: 14px;">...ועוד ${qr.result.length - 10} שורות</div>`;
183
+ }
184
+ }
185
+ }
186
+ html += '</div>';
187
+ });
188
+ }
189
+
190
+ html += '</div>';
191
+ return html;
192
+ }
193
+
194
+ function showVisualizations(visualizations) {
195
+ // Create or get visualizations container
196
+ let vizContainer = document.getElementById('resp-visualizations');
197
+ if (!vizContainer) {
198
+ vizContainer = document.createElement('div');
199
+ vizContainer.id = 'resp-visualizations';
200
+ vizContainer.style.marginTop = '20px';
201
+ document.getElementById('last-response').appendChild(vizContainer);
202
+ }
203
+
204
+ vizContainer.style.display = 'block';
205
+ vizContainer.innerHTML = '<h4 style="color: #1976d2; margin-bottom: 12px;">📊 גרפיקות:</h4>';
206
+
207
+ visualizations.forEach((viz, idx) => {
208
+ const vizDiv = document.createElement('div');
209
+ vizDiv.style.marginBottom = '24px';
210
+ vizDiv.style.padding = '16px';
211
+ vizDiv.style.background = '#f8f9fa';
212
+ vizDiv.style.borderRadius = '8px';
213
+
214
+ vizDiv.innerHTML = `<h5 style="margin-top: 0; color: #1976d2;">${viz.title}</h5>`;
215
+ vizDiv.innerHTML += `<div id="viz-${idx}" style="height: 300px;"></div>`;
216
+
217
+ vizContainer.appendChild(vizDiv);
218
+
219
+ // Use Chart.js or similar for visualization
220
+ // For now, just show a placeholder
221
+ document.getElementById(`viz-${idx}`).innerHTML = `
222
+ <div style="text-align: center; padding: 40px; color: #666;">
223
+ 📊 גרפיקה מסוג ${viz.type}<br>
224
+ <small>תמיכה בגרפיקות תתווסף בקרוב</small>
225
+ </div>
226
+ `;
227
+ });
228
+ }
229
+
230
  function formatResponse(text) {
231
  // Format markdown-like text (bold, lists, etc.)
232
  let formatted = escapeHtml(text);
app/static/index.html CHANGED
@@ -203,6 +203,13 @@
203
  <label><input type="checkbox" id="show-sources" /> הצג דוגמאות מהנתונים</label>
204
  <span class="small" style="margin-left:12px;">ברירת מחדל: מוסתר — יוצג רק הסיכום האנליטי</span>
205
  </div>
 
 
 
 
 
 
 
206
  <div style="display:flex;gap:8px;margin-top:12px;">
207
  <button id="send" class="primary">🔍 שאל</button>
208
  <button id="clear-history" class="muted">🗑️ נקה היסטוריה</button>
 
203
  <label><input type="checkbox" id="show-sources" /> הצג דוגמאות מהנתונים</label>
204
  <span class="small" style="margin-left:12px;">ברירת מחדל: מוסתר — יוצג רק הסיכום האנליטי</span>
205
  </div>
206
+ <div style="margin-top:12px;">
207
+ <label style="font-weight: 600;">גישת ניתוח:</label>
208
+ <div style="margin-top:8px;">
209
+ <label><input type="radio" name="approach" value="sql" checked /> SQL-based (מומלץ - חדש)</label>
210
+ <label style="margin-right:20px;"><input type="radio" name="approach" value="rag" /> RAG-based (ישן)</label>
211
+ </div>
212
+ </div>
213
  <div style="display:flex;gap:8px;margin-top:12px;">
214
  <button id="send" class="primary">🔍 שאל</button>
215
  <button id="clear-history" class="muted">🗑️ נקה היסטוריה</button>