-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathapp.py
More file actions
1389 lines (1132 loc) · 56 KB
/
Copy pathapp.py
File metadata and controls
1389 lines (1132 loc) · 56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from fastapi import FastAPI, Request, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates
import pandas as pd
import logging
import asyncio
import uvicorn
from dotenv import load_dotenv
import os
import re
import io
import uuid
from typing import Optional, Any, List
from pydantic import BaseModel
from my_prompts import system_required_columns, system_dynamic_question, system_rephrase_query, system_rephrase_query_forecast, system_classify_query, system_classify_predictive_query
import mlflow
# PDF extraction
from pypdf import PdfReader
# Load the .env file
load_dotenv()
##cfg = Config()
# Import the updated helper functions
from helper import fetch_answer
from table_extraction import get_tables, get_table_columns
#from tracking import create_user_interaction_table, log_user_interaction
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Create FastAPI app
app = FastAPI()
templates = Jinja2Templates(directory="templates")
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.serving import ChatMessage, ChatMessageRole
# Initialize Databricks client
client = WorkspaceClient()
# # Initialize Databricks client (MLFLOW)
w = WorkspaceClient()
client = w.serving_endpoints.get_open_ai_client()
# Load GENIE_ROOM_ID from environment variable early
GENIE_ROOM_ID_FROM_ENV = os.getenv("SPACE_ID")
# Load SERVING_ENDPOINT_NAME from environment variable
SERVING_ENDPOINT_NAME = os.getenv("SERVING_ENDPOINT_NAME")
# Validate SERVING_ENDPOINT_NAME is set
if not SERVING_ENDPOINT_NAME:
raise ValueError("SERVING_ENDPOINT_NAME environment variable is not set. Please configure it in your environment.")
# Global variables
CURRENT_CONVERSATION_ID = None
DYNAMIC_GENIE_ROOM_ID = GENIE_ROOM_ID_FROM_ENV # Initialize from environment variable
# --- Simple in-memory session store (swap for Redis in prod) ---
SESSION_STORE: dict[str, str] = {} # session_id -> pdf_content
SCHEMA_INFO = "" # Global variable to store schema information
from manual_ai_content import MANUAL_AI_CONTENT
# Get configuration from Databricks SDK
from databricks.sdk.core import Config
cfg = Config()
mlflow.set_tracking_uri('databricks') # MLOPS
mlflow.set_experiment("/Workspace/Shared/agent-genie-mlflow") # MLOPS
mlflow.openai.autolog() # MLOPS
# Load environment variables
fallback_workspace_url = "https://" + cfg.hostname
fallback_access_token = cfg.oauth_token().access_token
# FIX: proper logging formatting so host actually appears in logs
logger.info("hostname %s", fallback_workspace_url)
# =========================
# Response Parsing Helpers
# =========================
def _content_to_text(message_content: Any) -> str:
"""
Normalize Databricks/LLM message.content which may be:
- str
- list of parts (each a str or dict with keys like {'type': 'text'|'output_text', 'text': '...'})
- object with a .text property
"""
if message_content is None:
return ""
if isinstance(message_content, str):
return message_content
if isinstance(message_content, list):
parts: List[str] = []
for p in message_content:
if isinstance(p, str):
parts.append(p)
elif isinstance(p, dict):
if "text" in p and isinstance(p.get("text"), str):
parts.append(p["text"])
elif "value" in p and p.get("value") is not None:
parts.append(str(p["value"]))
else:
txt = getattr(p, "text", None)
if isinstance(txt, str):
parts.append(txt)
return "\n".join(t for t in parts if t)
return str(message_content)
def _extract_message_content(resp: Any) -> str:
"""
Pull content out of various response shapes from Databricks Serving:
- Common: resp.choices[0].message.content
- Future/alt: resp.output_text, or resp.output[0].content
"""
raw = None
try:
raw = resp.choices[0].message.content
except Exception:
pass
if raw is None:
raw = getattr(resp, "output_text", None)
if raw is None:
out = getattr(resp, "output", None)
if isinstance(out, list) and out:
raw = getattr(out[0], "content", None)
return _content_to_text(raw).strip()
def determine_required_columns(query, table_schema):
"""
Analyze the user query and determine which columns from the schema are required
Args:
query (str): The user's query
table_schema (dict): The schema of available tables with their columns
Returns:
list: A list of required column names
"""
global SCHEMA_INFO
# Build schema_info from the passed table_schema parameter
schema_info = ""
if table_schema and isinstance(table_schema, dict) and len(table_schema) > 0:
# Use the actual table schema passed to the function
logger.info(f"Using passed table_schema with {len(table_schema)} tables")
for table_name, columns in table_schema.items():
schema_info += f"Table: {table_name}\n"
if columns and len(columns) > 0:
schema_info += f"Columns: {', '.join(columns)}\n"
else:
schema_info += "Columns: [No column information available]\n"
schema_info += "\n"
# Also extract all unique column names for easier processing
all_columns = set()
for columns in table_schema.values():
if columns:
all_columns.update(columns)
if all_columns:
schema_info += f"\nAll Available Columns: {', '.join(sorted(all_columns))}\n"
elif SCHEMA_INFO:
# Use the global SCHEMA_INFO if table_schema is empty but SCHEMA_INFO is available
logger.info("Using global SCHEMA_INFO as fallback")
schema_info = f"Available Columns: {SCHEMA_INFO}"
else:
#pass
# Use hardcoded fallback as last resort
logger.warning("Using hardcoded fallback schema")
schema_info = """Available Columns: order_id, order_datetime, abnormal_flag, first_name, diagnosis_type, message_type, value, address, guarantor_phone, diagnosis_code, discharge_datetime, reference_range, ordering_provider, test_name, recorded_datetime, diagnosis_description, source_file, guarantor_address, sending_fac, phone, guarantor_name, admit_datetime, unit, receiving_app, attending_doctor, observation_id, assigned_location, last_name, patient_class, message_datetime, hl7_version, sending_app, dob, gender, patient_id, event_type"""
#MLOPS 1
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":system_required_columns.format(schema_info=schema_info)
},
{"role":"user", "content":query},
],
)
response = _extract_message_content(resp)
logger.info(f"Required columns for query '{query}': {response}")
# Try to parse the response as JSON
try:
import json
json_str = response.strip()
start = json_str.find('[')
end = json_str.rfind(']') + 1
if start >= 0 and end > start:
json_str = json_str[start:end]
required_columns = json.loads(json_str)
return required_columns
except json.JSONDecodeError as e:
logger.error(f"Failed to parse required columns response as JSON: {response}")
logger.error(f"JSON error: {str(e)}")
return []
def generate_dynamic_questions(schema_info=None):
"""
Generate dynamic questions based on the provided schema information
Args:
schema_info (str): Schema information containing column names
Returns:
list: A list of generated questions based on the schema
"""
global SCHEMA_INFO
# Use provided schema_info or fall back to global SCHEMA_INFO
if not schema_info and SCHEMA_INFO:
schema_info = SCHEMA_INFO
elif not schema_info:
schema_info = "No schema information available"
print("Schema information passed to generate_dynamic_questions:")
print(schema_info)
#MLOPS 2
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":system_dynamic_question.format(schema_info=schema_info)
},
{ "role":"user", "content":"Generate 5 simple questions based on the provided schema information."},
],
)
response = _extract_message_content(resp)
print(f"Generated questions response: {response}")
# Try to parse the response as a Python list
try:
import ast
if response.strip().startswith('[') and response.strip().endswith(']'):
questions = ast.literal_eval(response.strip())
if isinstance(questions, list):
return questions
# If not a proper list format, try to extract questions manually
lines = response.strip().split('\n')
questions = []
for line in lines:
line = line.strip()
if line and not line.startswith('#') and not line.startswith('//'):
import re
clean_line = re.sub(r'^\d+\.?\s*', '', line)
clean_line = re.sub(r'^[-*]\s*', '', clean_line)
clean_line = clean_line.strip('"\'')
if clean_line:
questions.append(clean_line)
if questions:
return questions[:5]
except Exception as e:
logger.error(f"Failed to parse generated questions: {str(e)}")
logger.info("Using predefined sample questions as fallback")
return [
"Show me the first 10 rows of the dataset",
"How many total records are in the dataset?",
"What are the unique values in the main categories?",
"Show me summary statistics for the numerical columns",
"What is the data distribution by key fields?"
]
def rephrase_query(query):
"""
Rephrase the user's query to better understand the intent
"""
#MLOPS 3
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":system_rephrase_query
},
{"role":"user", "content":query},
],
)
rephrased = _extract_message_content(resp)
logger.info(f"Original query: '{query}' → Rephrased: '{rephrased}'")
print(f"Rephrased query: {rephrased}")
return rephrased
def rephrase_query_forecast(query):
"""
Rephrase the user's query to better understand the intent
"""
#MLOPS 4
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":system_rephrase_query_forecast
},
{"role":"user", "content":query},
],
)
rephrased = _extract_message_content(resp)
logger.info(f"Original query: '{query}' → Rephrased: '{rephrased}'")
print(f"Rephrased query: {rephrased}")
return rephrased
def classify_query(query):
#MLOPS 5
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":system_classify_query
},
{"role":"user", "content":query},
],
)
return _extract_message_content(resp)
def classify_predictive_query(query):
"""
Classify predictive queries into specific AI functions
Returns a list of AI function names when multiple functions are detected
"""
#MLOPS 6
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":system_classify_predictive_query
},
{"role":"user", "content":query},
],
)
response = _extract_message_content(resp)
print("advanced classification", response)
# Parse the response to extract AI function names
try:
import json
if response.startswith('[') and response.endswith(']'):
ai_functions = json.loads(response)
if isinstance(ai_functions, list):
return ai_functions
if response in ['ai_analyze_sentiment', 'ai_classify', 'ai_extract', 'ai_fix_grammar',
'ai_gen', 'ai_mask', 'ai_similarity', 'ai_summarize', 'ai_translate', 'ai_forecast']:
return [response]
import re
function_names = re.findall(r'ai_\w+', response)
if function_names:
return function_names
except Exception as e:
logger.error(f"Error parsing AI function classification: {str(e)}")
return [response]
async def explain_dataset_directly(table_schema=None, question=None):
"""
Directly explain the dataset using schema information without classification.
This function is specifically called when user asks "explain the dataset".
Args:
table_schema (dict, optional): Table schema information with table names and columns
question (str, optional): The original question asked by the user
Returns:
str: Comprehensive explanation of the dataset
"""
global SCHEMA_INFO, CURRENT_CONVERSATION_ID
try:
# Prepare schema information
schema_info = ""
if table_schema and isinstance(table_schema, dict) and len(table_schema) > 0:
logger.info(f"Using passed table_schema with {len(table_schema)} tables for dataset explanation")
for table_name, columns in table_schema.items():
schema_info += f"Table: {table_name}\n"
if columns and len(columns) > 0:
schema_info += f"Columns ({len(columns)}): {', '.join(columns)}\n"
else:
schema_info += "Columns: [No column information available]\n"
schema_info += "\n"
all_columns = set()
for columns in table_schema.values():
if columns:
all_columns.update(columns)
if all_columns:
schema_info += f"Total Unique Columns Across All Tables: {len(all_columns)}\n"
schema_info += f"Column Names: {', '.join(sorted(all_columns))}\n"
elif SCHEMA_INFO:
logger.info("Using global SCHEMA_INFO for dataset explanation")
columns_list = SCHEMA_INFO.split('\t') if SCHEMA_INFO else []
schema_info = f"Dataset Contains {len(columns_list)} Columns:\n"
schema_info += f"Column Names: {', '.join(columns_list)}\n"
else:
schema_info = "No schema information is currently available for this dataset."
#MLOPS 7
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{"role": "developer", "content": "You can follow the user instruction"},
{"role": "user", "content": f"""
# === DATASET SCHEMA INFORMATION ===
# {schema_info}
# === USER'S ORIGINAL QUESTION ===
# {question}
# """}
]
)
response = _extract_message_content(resp)
logger.info(f"✅ Generated dataset explanation successfully")
return response
except Exception as e:
logger.exception("❌ Error in explain_dataset_directly")
raise e
from helper import tavily_topk_contents
async def general_information(question, required_columns=None):
"""
Handle general info queries. Uses Tavily if available; otherwise queries the model directly.
"""
global CURRENT_CONVERSATION_ID
try:
hits, internet_contents = tavily_topk_contents(question, k=3)
using_internet = bool(internet_contents)
if using_internet:
system_msg = {
"role": "system",
"content": (
"You are an expert assistant. Use ONLY the Internet Context provided by the user. "
"Do not rely on prior knowledge. If the context does not contain the answer, say: "
"I could not find the answer in the provided internet context. "
"Write in clean plain text. No markdown, no asterisks, no pipes, no special characters."
),
}
user_msg = {
"role": "user",
"content": (
f"Question: {question}\n\n"
f"Internet Context:\n{internet_contents}\n\n"
"Instructions: Answer strictly from the Internet Context above. "
"If any required detail is missing, say you could not find it in the provided internet context.\n\n"
"Format:\n"
"Answer:\n"
"Details:\n\n"
),
}
else:
# Fallback: no internet content available; allow model to use general knowledge
system_msg = {
"role": "system",
"content": (
"You are an expert assistant. Answer using your general knowledge. "
"If you are uncertain, say you do not know. "
"Write in clean plain text. No markdown, no asterisks, no pipes, no special characters."
),
}
user_msg = {
"role": "user",
"content": f"Question: {question}",
}
# Call OpenAI ChatCompletion. -- MLOPS 8
resp = client.chat.completions.create(
model=SERVING_ENDPOINT_NAME,
messages=[system_msg, user_msg],
)
response = _extract_message_content(resp)
logger.info(f"✅ Generated general information response for: {question}")
return response
except Exception as e:
logger.exception("❌ Error in general_information")
raise e
async def final_answer_combine(question, required_columns=None, ai_function_type=None, ai_function_types=None):
"""
Process user question through Tavily search and Databricks Genie.
Adjusts filtering and prompt based on keywords in the question.
Args:
question (str): The user's question
required_columns (list, optional): List of required column names
ai_function_type (str, optional): Primary AI function type from classification (for backward compatibility)
ai_function_types (list, optional): List of all AI function types from classification
"""
global CURRENT_CONVERSATION_ID, DYNAMIC_GENIE_ROOM_ID
try:
if not DYNAMIC_GENIE_ROOM_ID:
raise Exception("Genie Room ID not set. Please configure it first.")
lower_q = question.lower()
contains_forecast = bool(re.search(r'\b(forecast|forecasted|forecasting)\b', lower_q))
contains_classify = bool(re.search(r'\b(classify|classified|classification)\b', lower_q))
if ai_function_types and len(ai_function_types) > 0:
primary_function = ai_function_types[0]
if primary_function == "ai_forecast":
question = rephrase_query_forecast(question)
logger.info(f"🔮 Rephrased forecast query: {question}")
print(f"🔮 Rephrased forecast query: {question}")
filter_keyword = primary_function
logger.info(f"🎯 Using primary AI function type: {primary_function}")
else:
filter_keyword = primary_function
logger.info(f"🎯 Using primary AI function type: {primary_function}")
elif ai_function_type:
if ai_function_type == "ai_forecast":
question = rephrase_query_forecast(question)
logger.info(f"🔮 Rephrased forecast query: {question}")
print(f"🔮 Rephrased forecast query: {question}")
filter_keyword = ai_function_type
logger.info(f"🎯 Using AI function type: {ai_function_type}")
else:
filter_keyword = ai_function_type
logger.info(f"🎯 Using AI function type: {ai_function_type}")
elif contains_forecast:
question = rephrase_query_forecast(question)
logger.info(f"🔮 Rephrased forecast query: {question}")
print(f"🔮 Rephrased forecast query: {question}")
filter_keyword = "ai_forecast"
logger.info("🔮 Detected forecast query, filtering for ai_forecast")
elif contains_classify:
filter_keyword = "ai_classify"
logger.info("🏷️ Detected classify query, filtering for ai_classify")
else:
filter_keyword = "ai_query"
logger.info("🧠 Processing standard predictive query, filtering for ai_query")
logger.info(f"🔍 Looking for manual content for: {filter_keyword}")
global MANUAL_AI_CONTENT
combined_text = ""
if ai_function_types and len(ai_function_types) > 1:
logger.info(f"🔄 Processing multiple AI functions: {ai_function_types}")
combined_contents = []
for func_type in ai_function_types:
content = MANUAL_AI_CONTENT.get(func_type, "")
if content:
combined_contents.append(f"========{func_type.upper()}======\n{content}")
else:
logger.warning(f"No manual content found for '{func_type}'")
if combined_contents:
combined_text = "\n\n".join(combined_contents)
logger.info(f"✅ Combined manual content for {len(ai_function_types)} AI functions (total length: {len(combined_text)} characters)")
print(f"✅ Combined manual content for {len(ai_function_types)} AI functions (total length: {len(combined_text)} characters)")
else:
raise Exception(f"No manual content configured for any of the AI function types: {ai_function_types}. Please set manual content first using /set-ai-content endpoint.")
else:
combined_text = MANUAL_AI_CONTENT.get(filter_keyword, "")
if not combined_text:
logger.warning(f"No manual content found for '{filter_keyword}'. Please set manual content using /set-ai-content endpoint.")
raise Exception(f"No manual content configured for AI function type: {filter_keyword}. Please set manual content first using /set-ai-content endpoint.")
combined_text = f"========{filter_keyword.upper()}======\n{combined_text}"
logger.info(f"✅ Using manual content for '{filter_keyword}' (length: {len(combined_text)} characters)")
print(f"✅ Using manual content for '{filter_keyword}' (length: {len(combined_text)} characters)")
column_context = ""
if required_columns and isinstance(required_columns, list) and len(required_columns) > 0:
column_context = f"Focus on these columns and show these columns in the output by creating new columns for result: {', '.join(required_columns)}\n\n"
task_description = {
"ai_forecast": "forecast",
"ai_classify": "classification logic",
"ai_query": "SQL"
}.get(filter_keyword, "SQL")
if contains_forecast:
forecast_prompt = (
f"""
You are an expert data engineer who writes precise ANSI SQL for Databricks AI functions sql queries {task_description}.
You must return and execute AI_Forecast() SQL query and nothing else. The question is applicable to underlying dataset
----------------------------------------
Here is the information about the ai_forecast sql functions{combined_text}
-----------------------------------------------------------
Here is the schema information of the data {column_context}
-----------------------------------------
here is the question {question}
"""
)
logger.info("🚀 Sending forecast request to Genie...")
logger.info(f"Using existing conversation ID: {CURRENT_CONVERSATION_ID}")
response = await fetch_answer(fallback_workspace_url, DYNAMIC_GENIE_ROOM_ID, None,
forecast_prompt, CURRENT_CONVERSATION_ID)
else:
prompt = (
"You are an expert SQL reasoning assistant. Your task is to read and understand the information below and generate an SQL function query strictly based on the logic and functions mentioned.\n\n"
"=== INPUT DATA AND LOGIC ===\n"
f"{combined_text}\n\n"
"=== COLUMN CONTEXT ===\n"
f"{column_context}\n\n"
"=== TASK ===\n"
f"{task_description}\n\n"
"=== QUESTION ===\n"
f"{question}\n\n"
"=== INSTRUCTIONS ===\n"
"- Use ONLY the logic and functions mentioned in the provided information.\n"
"**- When multiple AI functions are used, place the primary function calls in a subquery and expose their outputs as columns to be consumed by the parent ai_function.**\n"
"- Do NOT make assumptions or introduce new logic.\n"
"- Your output must be a single AI-generated SQL function query.\n"
"- Unless explicitly referenced, apply the logic to only the first 10 rows of data\n"
"- Output ONLY the SQL function query—no explanation, no markdown.\n"
)
#"- If two or more functions are referenced, try to apply them all if relevant.\n"
logger.info("🚀 Sending request to Genie...")
logger.info(f"Using existing conversation ID: {CURRENT_CONVERSATION_ID}")
response = await fetch_answer(fallback_workspace_url, DYNAMIC_GENIE_ROOM_ID, None,
prompt, CURRENT_CONVERSATION_ID)
if isinstance(response, dict) and "conversation_id" in response:
CURRENT_CONVERSATION_ID = response["conversation_id"]
logger.info(f"✅ Updated conversation ID: {CURRENT_CONVERSATION_ID}")
return response
except Exception as e:
logger.exception("❌ Error in final_answer_combine")
raise e
async def direct_genie_answer(question, required_columns=None):
"""
Process user question directly with Databricks Genie without external search
Args:
question (str): The user's question
required_columns (list, optional): List of required column names
"""
global CURRENT_CONVERSATION_ID, DYNAMIC_GENIE_ROOM_ID
try:
if not DYNAMIC_GENIE_ROOM_ID:
raise Exception("Genie Room ID not set. Please configure it first.")
logger.info("🚀 Sending direct request to Genie...")
logger.info(f"Using existing conversation ID: {CURRENT_CONVERSATION_ID}")
question = question + " using the data."
if required_columns and isinstance(required_columns, list) and len(required_columns) > 0:
columns_str = ", ".join(required_columns)
question = f"{question} Please focus on these columns : {columns_str}."
logger.info(f"Enhanced question with column info: {question}")
response = await fetch_answer(fallback_workspace_url, DYNAMIC_GENIE_ROOM_ID, None,
question, CURRENT_CONVERSATION_ID) # need to log question, response in mlflow
if isinstance(response, dict) and "conversation_id" in response:
CURRENT_CONVERSATION_ID = response["conversation_id"]
logger.info(f"✅ Updated conversation ID: {CURRENT_CONVERSATION_ID}")
return response
except Exception as e:
logger.exception("Error in direct_genie_answer")
raise e
def extract_pdf_text(file_bytes: bytes, max_chars: int = 40000, password: str | None = None) -> str:
"""
Extract text from a PDF. Handles encrypted PDFs and degrades gracefully.
- If the PDF is encrypted and no/invalid password is provided, raises ValueError.
- If `cryptography` is missing for AES-encrypted PDFs, raises RuntimeError with a clear message.
"""
from io import BytesIO
import re
try:
from pypdf import PdfReader
from pypdf.errors import DependencyError as PdfDependencyError, PdfReadError
except Exception:
# If pypdf import itself fails, raise a clear error
raise RuntimeError("pypdf is required to process PDFs. Please add 'pypdf' to requirements.txt.")
try:
bio = BytesIO(file_bytes)
reader = PdfReader(bio)
# Encrypted?
if getattr(reader, "is_encrypted", False):
try:
# Try blank password first, or use provided
result = reader.decrypt(password or "")
except PdfDependencyError as e:
# cryptography missing
raise RuntimeError("Encrypted PDF requires 'cryptography>=3.1'. Please add it to requirements and rebuild.") from e
# pypdf returns 0/False when wrong
if not result:
raise ValueError("PDF is encrypted. A valid password was not provided.")
# Extract text page by page
texts = []
for page in reader.pages:
try:
t = page.extract_text() or ""
except Exception:
t = ""
texts.append(t)
text = "\n".join(texts)
text = re.sub(r"\n{3,}", "\n\n", text)
return text[:max_chars]
except PdfDependencyError as e:
raise RuntimeError("Encrypted PDF requires 'cryptography>=3.1'. Please add it to requirements and rebuild.") from e
except PdfReadError as e:
raise ValueError(f"Invalid or corrupted PDF: {e}")
def build_prompt(pdf_content: str, question: str) -> str:
"""Build prompt for PDF-based questions"""
return f"""
=== PDF CONTENT ===
{pdf_content}
=== USER'S ORIGINAL QUESTION ===
{question}
""".strip()
def ask_databricks(pdf_content: str, question: str) -> str:
"""Ask Databricks serving endpoint with PDF context"""
prompt = build_prompt(pdf_content, question)
#MLOPS 9
resp = client.chat.completions.create(
model = SERVING_ENDPOINT_NAME,
messages=[
{
"role":"system",
"content":("You are an expert in answering questions based on PDF content. "
"Give the answer in less than 100 words. Give me a beautiful summary.")
},
{"role":"user", "content":prompt},
],
)
try:
return _extract_message_content(resp)
except Exception:
return "I couldn't produce an answer from the endpoint."
class ChatIn(BaseModel):
"""Model for chat input with optional session ID for PDF mode"""
question: str
session_id: Optional[str] = None # when provided, force QA over stored PDF
@app.post("/upload_pdf")
async def upload_pdf(file: UploadFile = File(...)):
from fastapi.responses import JSONResponse
from fastapi import UploadFile, File, HTTPException
import uuid
"""Upload and process PDF file (returns a session_id). Always responds with JSON."""
if file.content_type not in ("application/pdf",):
raise HTTPException(status_code=400, detail="Please upload a PDF.")
data = await file.read()
if not data:
raise HTTPException(status_code=400, detail="Empty file uploaded.")
try:
pdf_content = extract_pdf_text(data)
except ValueError as e:
# e.g., encrypted without password, invalid PDF, etc.
return JSONResponse(status_code=400, content={"error": str(e)})
except RuntimeError as e:
# e.g., missing cryptography for AES
return JSONResponse(status_code=500, content={"error": str(e)})
except Exception as e:
# Catch-all to ensure frontend always gets JSON
return JSONResponse(status_code=500, content={"error": f"Failed to process PDF: {str(e)}"})
session_id = str(uuid.uuid4())
# Assumes SESSION_STORE exists; keep behavior unchanged
try:
SESSION_STORE[session_id] = pdf_content
except Exception:
# Fallback if SESSION_STORE isn't defined for some reason
pass
return JSONResponse({"session_id": session_id})
@app.post("/chat")
def chat(body: ChatIn):
"""Handle chat with optional PDF mode"""
question = body.question.strip()
if not question:
raise HTTPException(status_code=400, detail="Question is required.")
# If session_id present & valid → ALWAYS answer using PDF (QA with PDF)
if body.session_id:
pdf_content = SESSION_STORE.get(body.session_id)
if not pdf_content:
raise HTTPException(status_code=400, detail="Invalid or expired session_id. Upload the PDF again.")
answer = ask_databricks(pdf_content=pdf_content, question=question)
return JSONResponse({"answer": answer})
return JSONResponse({"answer": "PDF mode is off. Turn on the checkbox (and upload a PDF) to answer from the document."})
# --- New: simple health/probe endpoint to avoid 404 spam ---
@app.get("/stats")
async def stats():
return {"ok": True}
@app.get("/")
async def home(request: Request):
"""Render the home page with chat interface"""
global CURRENT_CONVERSATION_ID, DYNAMIC_GENIE_ROOM_ID
CURRENT_CONVERSATION_ID = None # Reset the conversation when the home page is loaded
logger.info("🔄 Conversation ID reset on page load")
if DYNAMIC_GENIE_ROOM_ID:
logger.info(f"✅ Genie Room ID loaded from environment: {DYNAMIC_GENIE_ROOM_ID}")
else:
logger.warning("⚠️ No Genie Room ID found in environment variable GENIE_ROOM_ID")
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/query")
async def query(request: Request):
"""Process user query and return response"""
try:
if not fallback_workspace_url:
return JSONResponse({
"error": "Workspace URL not configured in environment. Please check .env file."
}, status_code=400)
if not fallback_access_token:
return JSONResponse({
"error": "Access Token not configured in environment. Please check .env file."
}, status_code=400)
if not DYNAMIC_GENIE_ROOM_ID:
return JSONResponse({
"error": "Genie Room ID not configured. Please set it first."
}, status_code=400)
body = await request.json()
user_query = body.get("query", "").strip()
# Check for PDF mode
session_id = body.get("session_id")
if session_id:
pdf_content = SESSION_STORE.get(session_id)
if not pdf_content:
return JSONResponse({
"error": "Invalid or expired session_id. Upload the PDF again."
}, status_code=400)
try:
answer = ask_databricks(pdf_content=pdf_content, question=user_query)
return JSONResponse({
"response_type": "text",
"message": answer,
"original_query": user_query,
"query_classification": "pdf_qa",
"ai_function_type": "pdf_chat"
})
except Exception as e:
logger.exception("Error in PDF Q&A")
return JSONResponse({
"error": f"Error processing PDF question: {str(e)}"
}, status_code=500)
catalog_name = body.get("catalog_name")
schema_name = body.get("schema_name")
reset_conversation = body.get("reset_conversation", False)
if reset_conversation:
global CURRENT_CONVERSATION_ID
CURRENT_CONVERSATION_ID = None
logger.info("🔄 Conversation reset requested - starting new conversation")
return JSONResponse({
"response_type": "text",
"message": "Conversation has been reset. Starting a new conversation."
})
if not user_query:
return JSONResponse({"error": "Query parameter is required"}, status_code=400)
if catalog_name and schema_name:
context_message = f"Using catalog '{catalog_name}' and schema '{schema_name}': "
user_query_with_context = f"{context_message}{user_query}"
logger.info(f"Query with catalog/schema context: {user_query_with_context}")
else:
user_query_with_context = user_query
query_for_classification = user_query_with_context
query_for_processing = user_query_with_context
table_schema = {}
if catalog_name and schema_name:
try:
tables_result = get_tables(catalog_name, schema_name, fallback_workspace_url, None)
if tables_result["success"]:
table_schema = {}
for table in tables_result["tables"]:
table_name = table["name"]
try:
columns_result = get_table_columns(catalog_name, schema_name, table_name, fallback_workspace_url, None)
if columns_result["success"]:
columns = columns_result["columns"]
table_schema[table_name] = columns
logger.info(f"Retrieved columns for {table_name}: {columns}")
else:
logger.warning(f"API call failed for {table_name}: {columns_result.get('error')}")
describe_query = f"DESCRIBE TABLE {catalog_name}.{schema_name}.{table_name}"
describe_response = await fetch_answer(fallback_workspace_url, DYNAMIC_GENIE_ROOM_ID, None,
describe_query, None)
columns = []
if isinstance(describe_response, dict) and "statement_response" in describe_response:
stmt = describe_response["statement_response"]
if stmt and "result" in stmt and "data_array" in stmt["result"]: