Commit ·
e405ff6
1
Parent(s): 395afb7
תיקונים: היסטוריה, תשובות מפורטות, ניגודיות כפתורים, תיקון CreationDate
Browse files- .gitignore +1 -0
- README.md +20 -1
- app/api.py +20 -9
- app/config.py +2 -1
- app/sql_service.py +74 -31
- app/static/index.html +24 -6
- scripts/fix_creation_date.py +155 -0
.gitignore
CHANGED
|
@@ -16,3 +16,4 @@ uvicorn.log
|
|
| 16 |
.uvicorn.log
|
| 17 |
.query_history.json
|
| 18 |
req
|
|
|
|
|
|
| 16 |
.uvicorn.log
|
| 17 |
.query_history.json
|
| 18 |
req
|
| 19 |
+
feedback_transformed.csv
|
README.md
CHANGED
|
@@ -31,6 +31,24 @@ license: mit
|
|
| 31 |
- Python 3.10+
|
| 32 |
- קובץ `Feedback.csv` עם העמודות: ID, ServiceName, Level, Text, CreationDate (אופציונלי)
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
### התקנה
|
| 35 |
|
| 36 |
```bash
|
|
@@ -87,7 +105,8 @@ python run.py
|
|
| 87 |
│ └── static/
|
| 88 |
│ ├── index.html # ממשק משתמש
|
| 89 |
│ └── app.js # לוגיקת frontend
|
| 90 |
-
├── Feedback.csv # נתוני המשובים (לא ב-git)
|
|
|
|
| 91 |
├── .env # API keys (לא ב-git)
|
| 92 |
├── requirements.txt # תלויות Python
|
| 93 |
├── run.py # נקודת כניסה
|
|
|
|
| 31 |
- Python 3.10+
|
| 32 |
- קובץ `Feedback.csv` עם העמודות: ID, ServiceName, Level, Text, CreationDate (אופציונלי)
|
| 33 |
|
| 34 |
+
### תיקון עמודת CreationDate
|
| 35 |
+
|
| 36 |
+
אם קובץ `Feedback.csv` מכיל עמודת `CreationDate` בפורמט לא תקין (למשל `MM:SS.s` - דקות:שניות.חלקי שנייה), יש להריץ את הסקריפט לתיקון:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
python scripts/fix_creation_date.py
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
הסקריפט:
|
| 43 |
+
1. מנתח את פורמט `MM:SS.s` וממיר אותו לשניות
|
| 44 |
+
2. מפיץ את הרשומות על פני תקופה של שנה (מ-2020-01-01)
|
| 45 |
+
3. משתמש בערך `MM:SS.s` כזמן ביום (שעות:דקות:שניות)
|
| 46 |
+
4. שומר את הקובץ המתוקן כ-`feedback_transformed.csv`
|
| 47 |
+
|
| 48 |
+
המערכת משתמשת ב-`feedback_transformed.csv` כברירת מחדל (אם קיים), אחרת ב-`Feedback.csv`.
|
| 49 |
+
|
| 50 |
+
**הערה**: אם יש לך תאריכים אמיתיים, עדכן את `CSV_PATH` ב-`.env` או העלה את הקובץ עם התאריכים הנכונים.
|
| 51 |
+
|
| 52 |
### התקנה
|
| 53 |
|
| 54 |
```bash
|
|
|
|
| 105 |
│ └── static/
|
| 106 |
│ ├── index.html # ממשק משתמש
|
| 107 |
│ └── app.js # לוגיקת frontend
|
| 108 |
+
├── Feedback.csv # נתוני המשובים המקוריים (לא ב-git)
|
| 109 |
+
├── feedback_transformed.csv # נתוני המשובים עם CreationDate מתוקן (לא ב-git)
|
| 110 |
├── .env # API keys (לא ב-git)
|
| 111 |
├── requirements.txt # תלויות Python
|
| 112 |
├── run.py # נקודת כניסה
|
app/api.py
CHANGED
|
@@ -46,15 +46,16 @@ def save_history() -> None:
|
|
| 46 |
Save query history to disk.
|
| 47 |
|
| 48 |
This is a best-effort operation - if saving fails (e.g., disk full,
|
| 49 |
-
permissions issue), the error is
|
| 50 |
-
|
| 51 |
"""
|
| 52 |
try:
|
| 53 |
with history_file.open("w", encoding="utf-8") as f:
|
| 54 |
json.dump(history, f, ensure_ascii=False, indent=2)
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
class QueryRequest(BaseModel):
|
|
@@ -168,12 +169,22 @@ def query_sql(req: QueryRequest) -> SQLQueryResponse:
|
|
| 168 |
"row_count": len(qr.result) if not qr.error else 0
|
| 169 |
})
|
| 170 |
|
| 171 |
-
# Save to history
|
| 172 |
try:
|
| 173 |
-
history.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
save_history()
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
return SQLQueryResponse(
|
| 179 |
query=result.user_query,
|
|
|
|
| 46 |
Save query history to disk.
|
| 47 |
|
| 48 |
This is a best-effort operation - if saving fails (e.g., disk full,
|
| 49 |
+
permissions issue), the error is logged but doesn't break the main flow.
|
| 50 |
+
History is stored in `.query_history.json`.
|
| 51 |
"""
|
| 52 |
try:
|
| 53 |
with history_file.open("w", encoding="utf-8") as f:
|
| 54 |
json.dump(history, f, ensure_ascii=False, indent=2)
|
| 55 |
+
print(f"History saved successfully to {history_file}", flush=True)
|
| 56 |
+
except Exception as e:
|
| 57 |
+
# Log error but don't break main flow
|
| 58 |
+
print(f"Warning: Could not save history to {history_file}: {e}", flush=True)
|
| 59 |
|
| 60 |
|
| 61 |
class QueryRequest(BaseModel):
|
|
|
|
| 169 |
"row_count": len(qr.result) if not qr.error else 0
|
| 170 |
})
|
| 171 |
|
| 172 |
+
# Save to history - ensure it's always saved
|
| 173 |
try:
|
| 174 |
+
history.append({
|
| 175 |
+
"query": result.user_query,
|
| 176 |
+
"response": {"summary": result.summary},
|
| 177 |
+
"timestamp": __import__("datetime").datetime.now().isoformat()
|
| 178 |
+
})
|
| 179 |
save_history()
|
| 180 |
+
print(f"History saved: {len(history)} entries", flush=True)
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f"Error saving history: {e}", flush=True)
|
| 183 |
+
# Try to save anyway, even if there's an error
|
| 184 |
+
try:
|
| 185 |
+
save_history()
|
| 186 |
+
except:
|
| 187 |
+
pass
|
| 188 |
|
| 189 |
return SQLQueryResponse(
|
| 190 |
query=result.user_query,
|
app/config.py
CHANGED
|
@@ -29,7 +29,8 @@ class Settings:
|
|
| 29 |
gemini_api_key: str | None = os.getenv("GEMINI_API_KEY")
|
| 30 |
|
| 31 |
# CSV data file path - relative to project root
|
| 32 |
-
|
|
|
|
| 33 |
|
| 34 |
# Column names in the CSV file - adjust if your CSV uses different column names
|
| 35 |
text_column: str = os.getenv("TEXT_COLUMN", "Text")
|
|
|
|
| 29 |
gemini_api_key: str | None = os.getenv("GEMINI_API_KEY")
|
| 30 |
|
| 31 |
# CSV data file path - relative to project root
|
| 32 |
+
# Default to feedback_transformed.csv which has properly formatted CreationDate
|
| 33 |
+
csv_path: str = os.getenv("CSV_PATH", "feedback_transformed.csv")
|
| 34 |
|
| 35 |
# Column names in the CSV file - adjust if your CSV uses different column names
|
| 36 |
text_column: str = os.getenv("TEXT_COLUMN", "Text")
|
app/sql_service.py
CHANGED
|
@@ -483,7 +483,7 @@ class SQLFeedbackService:
|
|
| 483 |
results_text += qr.result.to_string(index=False)
|
| 484 |
results_text += "\n\n"
|
| 485 |
|
| 486 |
-
prompt = f"""אתה אנליסט עסקי בכיר במשרד הפנים, מומחה בייעול תהליכים דיגיטליים ושיפור חוויות
|
| 487 |
|
| 488 |
המשתמש שאל שאלה על משובי משתמשים על שירותים דיגיטליים.
|
| 489 |
|
|
@@ -495,29 +495,38 @@ class SQLFeedbackService:
|
|
| 495 |
|
| 496 |
המשימה שלך: כתוב תשובה מסכמת, ברורה ובשפה חופשית שמבוססת על התוצאות.
|
| 497 |
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
| 499 |
- אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
|
| 500 |
- אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
|
| 501 |
- אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
|
| 502 |
- תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
|
| 503 |
|
| 504 |
-
|
| 505 |
-
1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים)
|
| 506 |
2. תשובה ברורה ומסודרת - לא גיבוב של מילים
|
| 507 |
-
3. כלול מספרים מדויקים מהתוצאות
|
| 508 |
-
4. הסבר את המשמעות העסקית של הממצאים
|
| 509 |
-
5. כלול המלצות מעשיות לשיפור
|
| 510 |
-
6. כתוב בעברית מקצועית
|
| 511 |
-
7. תן תשובה שמראה הבנה עמוקה של הנתונים
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
| 515 |
2. ניתוח מפורט של הממצאים (3-4 פסקאות)
|
| 516 |
-
- אם נדרש סיווג לפי שירותים/דירוגים/תאריכים - כלול כאן ניתוח נפרד לכל קטגוריה
|
|
|
|
| 517 |
3. תובנות עסקיות והמלצות (2-3 פסקאות)
|
| 518 |
-
|
|
|
|
|
|
|
| 519 |
|
| 520 |
-
אם יש שגיאות בשאילתות, ציין זאת בתשובה, אבל עדיין תן תשובה מפורטת על בסיס התוצאות שהתקבלו.
|
|
|
|
|
|
|
| 521 |
|
| 522 |
# Try Gemini first
|
| 523 |
if settings.gemini_api_key and genai is not None:
|
|
@@ -555,19 +564,24 @@ class SQLFeedbackService:
|
|
| 555 |
כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
|
| 556 |
התשובה חייבת לענות ישירות על השא��ה: {query}
|
| 557 |
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
| 559 |
- אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
|
| 560 |
- אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
|
| 561 |
- אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
|
| 562 |
- תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
|
| 563 |
|
| 564 |
-
|
| 565 |
-
1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים)
|
| 566 |
2. תשובה שמתמקדת ישירות בשאלה שנשאלה
|
| 567 |
-
3. כלול מספרים מדויקים מהתוצאות
|
| 568 |
-
4. הסבר את המשמעות העסקית של הממצאים
|
| 569 |
-
5. כלול המלצות מעשיות לשיפור
|
| 570 |
-
6. כתוב בעברית מקצועית
|
|
|
|
|
|
|
| 571 |
|
| 572 |
try:
|
| 573 |
response = model.generate_content(improvement_prompt, generation_config=generation_config)
|
|
@@ -619,11 +633,24 @@ class SQLFeedbackService:
|
|
| 619 |
כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
|
| 620 |
התשובה חייבת לענות ישירות על השאלה: {query}
|
| 621 |
|
| 622 |
-
|
|
|
|
|
|
|
|
|
|
| 623 |
- אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
|
| 624 |
- אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
|
| 625 |
- אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
|
| 626 |
-
- תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
|
| 628 |
try:
|
| 629 |
response = client.chat.completions.create(
|
|
@@ -650,20 +677,36 @@ class SQLFeedbackService:
|
|
| 650 |
# This ensures we always return a meaningful answer, not just a status message
|
| 651 |
successful_results = [r for r in query_results if not r.error and len(r.result) > 0]
|
| 652 |
if successful_results:
|
| 653 |
-
fallback_text = f"
|
| 654 |
-
fallback_text += "
|
|
|
|
|
|
|
| 655 |
for i, qr in enumerate(successful_results, 1):
|
| 656 |
-
fallback_text += f"
|
| 657 |
-
fallback_text += f"
|
| 658 |
-
|
|
|
|
|
|
|
| 659 |
if len(qr.result) > 0:
|
| 660 |
-
fallback_text += "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
fallback_text += qr.result.head(5).to_string(index=False)
|
| 662 |
fallback_text += "\n\n"
|
|
|
|
|
|
|
| 663 |
return fallback_text
|
| 664 |
else:
|
| 665 |
# If no successful results, still provide a helpful message
|
| 666 |
-
return f"בוצעו {len(sql_queries)} שאילתות, אך לא התקבלו
|
| 667 |
|
| 668 |
def _generate_visualizations(self, query_results: List[SQLQueryResult]) -> Optional[List[Dict[str, Any]]]:
|
| 669 |
"""
|
|
|
|
| 483 |
results_text += qr.result.to_string(index=False)
|
| 484 |
results_text += "\n\n"
|
| 485 |
|
| 486 |
+
prompt = f"""אתה אנליסט עסקי בכיר במשרד הפנים, מומחה בייעול תהליכים דיגיטליים ושיפור חוויות המשתמשים בעולם התוכן הממשלתי.
|
| 487 |
|
| 488 |
המשתמש שאל שאלה על משובי משתמשים על שירותים דיגיטליים.
|
| 489 |
|
|
|
|
| 495 |
|
| 496 |
המשימה שלך: כתוב תשובה מסכמת, ברורה ובשפה חופשית שמבוססת על התוצאות.
|
| 497 |
|
| 498 |
+
⚠️ חובה קריטית - תשובה מילולית מפורטת:
|
| 499 |
+
- אתה חייב לכתוב תשובה מילולית מפורטת ומשמעותית - לא רק לרשום את השאילתות!
|
| 500 |
+
- התשובה חייבת להיות כתובה בשפה טבעית, מקצועית וקולחת
|
| 501 |
+
- התשובה חייבת להסביר את הממצאים, לא רק להציג אותם
|
| 502 |
- אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
|
| 503 |
- אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
|
| 504 |
- אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
|
| 505 |
- תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
|
| 506 |
|
| 507 |
+
דרישות מקצועיות:
|
| 508 |
+
1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים) - חובה!
|
| 509 |
2. תשובה ברורה ומסודרת - לא גיבוב של מילים
|
| 510 |
+
3. כלול מספרים מדויקים מהתוצאות עם הסבר על המשמעות שלהם
|
| 511 |
+
4. הסבר את המשמעות העסקית של הממצאים בהקשר של תהליכים דיגיטליים ממשלתיים
|
| 512 |
+
5. כלול המלצות מעשיות לשיפור תהליכים ושירותים דיגיטליים
|
| 513 |
+
6. כתוב בעברית מקצועית וקולחת, תוך שימוש במונחים מקצועיים מתחום הממשל הדיגיטלי
|
| 514 |
+
7. תן תשובה שמראה הבנה עמוקה של הנתונים והקשר שלהם לשיפור חוויות המשתמשים
|
| 515 |
+
8. התשובה חייבת להיות רלוונטית לשאלה שנשאלה - לא תשובה כללית
|
| 516 |
+
|
| 517 |
+
מבנה התשובה (חובה):
|
| 518 |
+
1. פתיחה - סיכום מנהלים קצר (2-3 משפטים) שמסכם את הממצאים העיקריים
|
| 519 |
2. ניתוח מפורט של הממצאים (3-4 פסקאות)
|
| 520 |
+
- אם נדרש סיווג לפי שירותים/דירוגים/תאריכים - כלול כאן ניתוח נפרד ומפורט לכל קטגוריה
|
| 521 |
+
- הסבר את המספרים והנתונים בהקשר של שירותים דיגיטליים ממשלתיים
|
| 522 |
3. תובנות עסקיות והמלצות (2-3 פסקאות)
|
| 523 |
+
- תובנות על תהליכים דיגיטליים שניתן לשפר
|
| 524 |
+
- המלצות מעשיות לשיפור חוויות המשתמשים
|
| 525 |
+
4. סיכום (1-2 משפטים) - מסקנות עיקריות
|
| 526 |
|
| 527 |
+
אם יש שגיאות בשאילתות, ציין זאת בתשובה, אבל עדיין תן תשובה מפורטת על בסיס התוצאות שהתקבלו.
|
| 528 |
+
|
| 529 |
+
זכור: אתה מומחה תהליכי עבודה עסקיים דיגיטליים בעולם התוכן הממשלתי. התשובה שלך חייבת להיות מקצועית, רלוונטית, וברורה."""
|
| 530 |
|
| 531 |
# Try Gemini first
|
| 532 |
if settings.gemini_api_key and genai is not None:
|
|
|
|
| 564 |
כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
|
| 565 |
התשובה חייבת לענות ישירות על השא��ה: {query}
|
| 566 |
|
| 567 |
+
⚠️ חובה קריטית - תשובה מילולית מפורטת:
|
| 568 |
+
- אתה חייב לכתוב תשובה מילולית מפורטת ומשמעותית - לא רק לרשום את השאילתות!
|
| 569 |
+
- התשובה חייבת להיות כתובה בשפה טבעית, מקצועית וקולחת
|
| 570 |
+
- התשובה חייבת להסביר את הממצאים, לא רק להציג אותם
|
| 571 |
- אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
|
| 572 |
- אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
|
| 573 |
- אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
|
| 574 |
- תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
|
| 575 |
|
| 576 |
+
דרישות מקצועיות:
|
| 577 |
+
1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים) - חובה!
|
| 578 |
2. תשובה שמתמקדת ישירות בשאלה שנשאלה
|
| 579 |
+
3. כלול מספרים מדויקים מהתוצאות עם הסבר על המשמעות שלהם
|
| 580 |
+
4. הסבר את המשמעות העסקית של הממצאים בהקשר של תהליכים דיגיטליים ממשלתיים
|
| 581 |
+
5. כלול המלצות מעשיות לשיפור תהליכים ושירותים דיגיטליים
|
| 582 |
+
6. כתוב בעברית מקצועית וקולחת, תוך שימוש במונחים מקצועיים מתחום הממשל הדיגיטלי
|
| 583 |
+
|
| 584 |
+
זכור: אתה מומחה תהליכי עבודה עסקיים דיגיטליים בעולם התוכן הממשלתי. התשובה שלך חייבת להיות מקצועית, רלוונטית, וברורה."""
|
| 585 |
|
| 586 |
try:
|
| 587 |
response = model.generate_content(improvement_prompt, generation_config=generation_config)
|
|
|
|
| 633 |
כתוב תשובה משופרת שמתמקדת יותר בשאלה המקורית, מבוססת יותר על הנתונים, ומפורטת יותר.
|
| 634 |
התשובה חייבת לענות ישירות על השאלה: {query}
|
| 635 |
|
| 636 |
+
⚠️ חובה קריטית - תשובה מילולית מפורטת:
|
| 637 |
+
- אתה חייב לכתוב תשובה מילולית מפורטת ומשמעותית - לא רק לרשום את השאילתות!
|
| 638 |
+
- התשובה חייבת להיות כתובה בשפה טבעית, מקצועית וקולחת
|
| 639 |
+
- התשובה חייבת להסביר את הממצאים, לא רק להציג אותם
|
| 640 |
- אם השאלה מבקשת סיווג/חלוקה לפי שירותים (ServiceName) - חובה לכלול ניתוח נפרד ומפורט לכל שירות!
|
| 641 |
- אם השאלה מבקשת סיווג/חלוקה לפי דירוגים (Level) - חובה לכלול ניתוח נפרד ומפורט לכל דירוג!
|
| 642 |
- אם השאלה מבקשת סיווג/חלוקה לפי תאריכים - חובה לכלול ניתוח נפרד לפי תקופות!
|
| 643 |
+
- תמיד תן תשובה מפורטת ומשמעותית - לא רק הודעה ששאילתות בוצעו!
|
| 644 |
+
|
| 645 |
+
דרישות מקצועיות:
|
| 646 |
+
1. תשובה מפורטת ומקיפה (5-7 פסקאות, 400-600 מילים) - חובה!
|
| 647 |
+
2. תשובה שמתמקדת ישירות בשאלה שנשאלה
|
| 648 |
+
3. כלול מספרים מדויקים מהתוצאות עם הסבר על המשמעות שלהם
|
| 649 |
+
4. הסבר את המשמעות העסקית של הממצאים בהקשר של תהליכים דיגיטליים ממשלתיים
|
| 650 |
+
5. כלול המלצות מעשיות לשיפור תהליכים ושירותים דיגיטליים
|
| 651 |
+
6. כתוב בעברית מקצועית וקולחת, תוך שימוש במונחים מקצועיים מתחום הממשל הדיגיטלי
|
| 652 |
+
|
| 653 |
+
זכור: אתה מומחה תהליכי עבודה עסקיים דיגיטליים בעולם התוכן הממשלתי. התשובה שלך חייבת להיות מקצועית, רלוונטית, וברורה."""
|
| 654 |
|
| 655 |
try:
|
| 656 |
response = client.chat.completions.create(
|
|
|
|
| 677 |
# This ensures we always return a meaningful answer, not just a status message
|
| 678 |
successful_results = [r for r in query_results if not r.error and len(r.result) > 0]
|
| 679 |
if successful_results:
|
| 680 |
+
fallback_text = f"סיכום מפורט של הממצאים:\n\n"
|
| 681 |
+
fallback_text += f"בוצעו {len(sql_queries)} שאילתות, מתוכן {len(successful_results)} הצליחו והחזירו תוצאות.\n\n"
|
| 682 |
+
|
| 683 |
+
# Analyze and summarize each result
|
| 684 |
for i, qr in enumerate(successful_results, 1):
|
| 685 |
+
fallback_text += f"ממצאים משאילתה {i}:\n"
|
| 686 |
+
fallback_text += f"שאילתה: {qr.query}\n"
|
| 687 |
+
fallback_text += f"מספר רשומות: {len(qr.result)}\n\n"
|
| 688 |
+
|
| 689 |
+
# Try to provide meaningful analysis
|
| 690 |
if len(qr.result) > 0:
|
| 691 |
+
fallback_text += "תוצאות:\n"
|
| 692 |
+
# Show summary statistics if possible
|
| 693 |
+
numeric_cols = qr.result.select_dtypes(include=['number']).columns
|
| 694 |
+
if len(numeric_cols) > 0:
|
| 695 |
+
fallback_text += "סטטיסטיקות:\n"
|
| 696 |
+
for col in numeric_cols[:3]: # Limit to first 3 numeric columns
|
| 697 |
+
fallback_text += f"- {col}: ממוצע {qr.result[col].mean():.2f}, סכום {qr.result[col].sum():.0f}\n"
|
| 698 |
+
fallback_text += "\n"
|
| 699 |
+
|
| 700 |
+
# Show sample data
|
| 701 |
+
fallback_text += "דוגמאות מהנתונים:\n"
|
| 702 |
fallback_text += qr.result.head(5).to_string(index=False)
|
| 703 |
fallback_text += "\n\n"
|
| 704 |
+
|
| 705 |
+
fallback_text += "הערה: תשובה זו נוצרה אוטומטית מהתוצאות. לניתוח מפורט יותר, נסה לשאול שאלה ספציפית יותר."
|
| 706 |
return fallback_text
|
| 707 |
else:
|
| 708 |
# If no successful results, still provide a helpful message
|
| 709 |
+
return f"בוצעו {len(sql_queries)} שאילתות, אך לא התקבלו תוצאות מהנתונים.\n\nייתכן שהנתונים לא מכילים מידע התואם לשאלה שנשאלה. נסה לשאול שאלה אחרת או לבדוק את הנתונים הזמינים."
|
| 710 |
|
| 711 |
def _generate_visualizations(self, query_results: List[SQLQueryResult]) -> Optional[List[Dict[str, Any]]]:
|
| 712 |
"""
|
app/static/index.html
CHANGED
|
@@ -88,21 +88,39 @@
|
|
| 88 |
cursor: not-allowed;
|
| 89 |
}
|
| 90 |
.primary {
|
| 91 |
-
background: linear-gradient(135deg, #
|
| 92 |
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
.primary:hover:not(:disabled) {
|
| 95 |
-
background: linear-gradient(135deg, #
|
| 96 |
transform: translateY(-3px);
|
| 97 |
-
box-shadow: 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
.muted {
|
| 100 |
-
background: #
|
| 101 |
-
color: #
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
.muted:hover {
|
| 104 |
-
background: #
|
| 105 |
transform: translateY(-2px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
.card {
|
| 108 |
border-radius: 20px;
|
|
|
|
| 88 |
cursor: not-allowed;
|
| 89 |
}
|
| 90 |
.primary {
|
| 91 |
+
background: linear-gradient(135deg, #1565c0 0%, #0d47a1 100%);
|
| 92 |
color: white;
|
| 93 |
+
font-weight: 700;
|
| 94 |
+
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
| 95 |
+
border: 2px solid #0d47a1;
|
| 96 |
+
box-shadow: 0 4px 12px rgba(13, 71, 161, 0.4), inset 0 1px 0 rgba(255,255,255,0.2);
|
| 97 |
}
|
| 98 |
.primary:hover:not(:disabled) {
|
| 99 |
+
background: linear-gradient(135deg, #0d47a1 0%, #01579b 100%);
|
| 100 |
transform: translateY(-3px);
|
| 101 |
+
box-shadow: 0 8px 24px rgba(13, 71, 161, 0.5), inset 0 1px 0 rgba(255,255,255,0.2);
|
| 102 |
+
border-color: #01579b;
|
| 103 |
+
}
|
| 104 |
+
.primary:active:not(:disabled) {
|
| 105 |
+
transform: translateY(-1px);
|
| 106 |
+
box-shadow: 0 4px 12px rgba(13, 71, 161, 0.4);
|
| 107 |
}
|
| 108 |
.muted {
|
| 109 |
+
background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%);
|
| 110 |
+
color: #0d47a1;
|
| 111 |
+
font-weight: 600;
|
| 112 |
+
border: 2px solid #1976d2;
|
| 113 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.8);
|
| 114 |
}
|
| 115 |
.muted:hover {
|
| 116 |
+
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
| 117 |
transform: translateY(-2px);
|
| 118 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.8);
|
| 119 |
+
border-color: #1565c0;
|
| 120 |
+
}
|
| 121 |
+
.muted:active {
|
| 122 |
+
transform: translateY(0);
|
| 123 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
| 124 |
}
|
| 125 |
.card {
|
| 126 |
border-radius: 20px;
|
scripts/fix_creation_date.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Script to fix CreationDate column in Feedback.csv.
|
| 3 |
+
|
| 4 |
+
The CreationDate column contains values in MM:SS.s format (minutes:seconds.fraction),
|
| 5 |
+
which is not a valid date format. This script:
|
| 6 |
+
1. Parses the MM:SS.s format
|
| 7 |
+
2. Converts it to a proper datetime (assuming these are timestamps from a specific epoch)
|
| 8 |
+
3. Saves the transformed data to feedback_transformed.csv
|
| 9 |
+
|
| 10 |
+
Since we don't have the actual date, we'll use a logical approach:
|
| 11 |
+
- Treat MM:SS.s as minutes:seconds since some reference point
|
| 12 |
+
- Convert to datetime by adding to a base date (e.g., 2020-01-01)
|
| 13 |
+
- This allows temporal queries to work correctly
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import pandas as pd
|
| 19 |
+
from datetime import datetime, timedelta
|
| 20 |
+
import re
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def parse_mmss_format(time_str: str) -> float | None:
|
| 25 |
+
"""
|
| 26 |
+
Parse MM:SS.s format to total seconds.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
time_str: String in format "MM:SS.s" (e.g., "21:56.3")
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Total seconds as float, or None if parsing fails
|
| 33 |
+
"""
|
| 34 |
+
if pd.isna(time_str) or not isinstance(time_str, str):
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
# Match pattern MM:SS.s or MM:SS
|
| 38 |
+
match = re.match(r'^(\d+):(\d+)\.?(\d*)$', time_str.strip())
|
| 39 |
+
if not match:
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
minutes = int(match.group(1))
|
| 43 |
+
seconds = int(match.group(2))
|
| 44 |
+
fraction = float(f"0.{match.group(3)}") if match.group(3) else 0.0
|
| 45 |
+
|
| 46 |
+
total_seconds = minutes * 60 + seconds + fraction
|
| 47 |
+
return total_seconds
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def convert_to_datetime(seconds: float | None, index: int, total_rows: int,
|
| 51 |
+
base_date: datetime = datetime(2020, 1, 1),
|
| 52 |
+
period_days: int = 365) -> datetime | None:
|
| 53 |
+
"""
|
| 54 |
+
Convert seconds to datetime by distributing records over a time period.
|
| 55 |
+
|
| 56 |
+
Since the original MM:SS.s values don't contain actual date information,
|
| 57 |
+
we distribute the records evenly over a period (default: 1 year) and use
|
| 58 |
+
the MM:SS.s value as a time-of-day component.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
seconds: Total seconds from MM:SS.s format (used as time-of-day)
|
| 62 |
+
index: Row index (0-based)
|
| 63 |
+
total_rows: Total number of rows
|
| 64 |
+
base_date: Base date to start from
|
| 65 |
+
period_days: Number of days to distribute records over
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Datetime object or None
|
| 69 |
+
"""
|
| 70 |
+
if seconds is None:
|
| 71 |
+
return None
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
# Distribute records evenly over the period
|
| 75 |
+
days_offset = (index / total_rows) * period_days
|
| 76 |
+
|
| 77 |
+
# Use the seconds as time-of-day (hours, minutes, seconds)
|
| 78 |
+
hours = int(seconds // 3600)
|
| 79 |
+
minutes = int((seconds % 3600) // 60)
|
| 80 |
+
secs = int(seconds % 60)
|
| 81 |
+
microseconds = int((seconds % 1) * 1000000)
|
| 82 |
+
|
| 83 |
+
# Calculate the date
|
| 84 |
+
target_date = base_date + timedelta(days=days_offset)
|
| 85 |
+
|
| 86 |
+
# Set the time component
|
| 87 |
+
return target_date.replace(hour=hours % 24, minute=minutes % 60,
|
| 88 |
+
second=secs % 60, microsecond=microseconds)
|
| 89 |
+
except Exception:
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def fix_creation_date(input_csv: str = "Feedback.csv", output_csv: str = "feedback_transformed.csv") -> None:
|
| 94 |
+
"""
|
| 95 |
+
Fix CreationDate column and save transformed CSV.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
input_csv: Path to input CSV file
|
| 99 |
+
output_csv: Path to output CSV file
|
| 100 |
+
"""
|
| 101 |
+
print(f"Loading {input_csv}...")
|
| 102 |
+
df = pd.read_csv(input_csv)
|
| 103 |
+
|
| 104 |
+
print(f"Original shape: {df.shape}")
|
| 105 |
+
print(f"Original CreationDate sample: {df['CreationDate'].head(5).tolist()}")
|
| 106 |
+
|
| 107 |
+
if 'CreationDate' not in df.columns:
|
| 108 |
+
print("Warning: CreationDate column not found!")
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
# Parse MM:SS.s format to seconds
|
| 112 |
+
print("Parsing CreationDate values...")
|
| 113 |
+
df['_seconds'] = df['CreationDate'].apply(parse_mmss_format)
|
| 114 |
+
|
| 115 |
+
# Check if we have valid values
|
| 116 |
+
valid_count = df['_seconds'].notna().sum()
|
| 117 |
+
print(f"Valid parsed values: {valid_count} / {len(df)}")
|
| 118 |
+
|
| 119 |
+
if valid_count == 0:
|
| 120 |
+
print("Error: No valid CreationDate values found!")
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
# Convert to datetime
|
| 124 |
+
# Distribute records over a 1-year period starting from 2020-01-01
|
| 125 |
+
# Use the MM:SS.s value as time-of-day component
|
| 126 |
+
base_date = datetime(2020, 1, 1, 0, 0, 0)
|
| 127 |
+
total_rows = len(df)
|
| 128 |
+
|
| 129 |
+
print(f"Converting to datetime (distributing over 1 year from {base_date.date()})...")
|
| 130 |
+
df['CreationDate'] = [
|
| 131 |
+
convert_to_datetime(seconds, idx, total_rows, base_date, period_days=365)
|
| 132 |
+
for idx, seconds in enumerate(df['_seconds'])
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
# Convert to string format for CSV
|
| 136 |
+
df['CreationDate'] = df['CreationDate'].apply(
|
| 137 |
+
lambda x: x.strftime('%Y-%m-%d %H:%M:%S') if pd.notna(x) else None
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# Drop temporary column
|
| 141 |
+
df = df.drop(columns=['_seconds'])
|
| 142 |
+
|
| 143 |
+
# Save transformed CSV
|
| 144 |
+
print(f"Saving transformed data to {output_csv}...")
|
| 145 |
+
df.to_csv(output_csv, index=False)
|
| 146 |
+
|
| 147 |
+
print(f"Transformed shape: {df.shape}")
|
| 148 |
+
print(f"New CreationDate sample: {df['CreationDate'].head(5).tolist()}")
|
| 149 |
+
print(f"CreationDate range: {df['CreationDate'].min()} to {df['CreationDate'].max()}")
|
| 150 |
+
print(f"✅ Successfully created {output_csv}")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
fix_creation_date()
|
| 155 |
+
|