2323from langchain_openai import ChatOpenAI
2424from langchain_google_genai import ChatGoogleGenerativeAI
2525from typing import Optional
26+ import pandas as pd
27+ import numpy as np
28+ from sklearn .metrics .pairwise import cosine_similarity
2629
2730
2831# -------------------- CONFIG --------------------
@@ -471,6 +474,7 @@ async def _background_update_and_save(user_id: str, user_message: str, bot_reply
471474 - Always answer ONLY from <context> provided below which caters to the query.
472475 - NEVER use the web, external sources, or prior knowledge outside the given context.
473476 - Always consider query and answer in relevance to it.
477+ - Be Very PRECISE AND TO THE POINT ABOUT WHAT IS ASKED IN THE QUERY AND ANSWER SHOULD BE STRICTLY IN THE CONTEXT PROVIDED AND NOT ANYTHING ELSE.
474478 - ANSWER STRICTLY IN 150-200 WORDS (USING BULLET AND SUB-BULLET POINTS [MAX 5-6 POINTS]) which encapusaltes the key points and information from the context to answer the query in an APT FLOW
475479 - Always follow below mentioned rules at all times :
476480 • Begin each answer with: **"According to <source>"** (extract filename from metadata if available **DONT MENTION EXTENSIONS** eg, According to abc✅(correct), According to xyz.pdf❌(incorrect)).
@@ -985,6 +989,166 @@ def handle_chitchat(user_message: str, chat_history: str) -> str:
985989 print (f"[chitchat] Gemini LLM failed: { e } " )
986990 return "Whoa, let's keep it polite, please! 😊"
987991
992+ # -------------------- VIDEO MATCHING SYSTEM --------------------
993+ class VideoMatchingSystem :
994+ def __init__ (self , video_file_path : str = "D:\\ RHL-WH\\ RHL-FASTAPI\\ FILES\\ video_link_topic.xlsx" ):
995+ """Initialize the video matching system"""
996+ self .video_file_path = video_file_path
997+ self .topic_dict = {} # topic -> index
998+ self .url_dict = {} # index -> URL
999+
1000+ # Load video data
1001+ self ._load_video_data ()
1002+
1003+ def _load_video_data (self ):
1004+ """Load and preprocess video data"""
1005+ try :
1006+ df = pd .read_excel (self .video_file_path )
1007+ print (f"[VIDEO_SYSTEM] Loaded { len (df )} videos from { self .video_file_path } " )
1008+
1009+ # Create dictionaries
1010+ for idx , row in df .iterrows ():
1011+ topic = row ['video_topic' ].strip ()
1012+ url = row ['URL' ].strip ()
1013+
1014+ if topic and url :
1015+ self .topic_dict [topic ] = idx
1016+ self .url_dict [idx ] = url
1017+
1018+ print (f"[VIDEO_SYSTEM] Created topic_dict with { len (self .topic_dict )} topics" )
1019+
1020+ except Exception as e :
1021+ print (f"[VIDEO_SYSTEM] Error loading video data: { e } " )
1022+ self .topic_dict = {}
1023+ self .url_dict = {}
1024+
1025+ def pre_filter_topics (self , answer : str , min_matches : int = 4 ) -> List [Tuple [int , int ]]:
1026+ """Strict word matching to reduce candidates - only highly relevant topics"""
1027+ candidates = []
1028+ answer_words = set (answer .lower ().split ())
1029+
1030+ # Remove common words that don't add meaning
1031+ stop_words = {'the' , 'a' , 'an' , 'and' , 'or' , 'but' , 'in' , 'on' , 'at' , 'to' , 'for' , 'of' , 'with' , 'by' , 'is' , 'are' , 'was' , 'were' , 'be' , 'been' , 'have' , 'has' , 'had' , 'do' , 'does' , 'did' , 'will' , 'would' , 'could' , 'should' , 'may' , 'might' , 'can' , 'must' , 'this' , 'that' , 'these' , 'those' , 'i' , 'you' , 'he' , 'she' , 'it' , 'we' , 'they' , 'me' , 'him' , 'her' , 'us' , 'them' }
1032+ answer_words = answer_words - stop_words
1033+
1034+ for topic , idx in self .topic_dict .items ():
1035+ # Split topic by comma and get individual words
1036+ topic_words = set ()
1037+ for term in topic .split (',' ):
1038+ topic_words .update (term .strip ().lower ().split ())
1039+
1040+ # Remove stop words from topic words too
1041+ topic_words = topic_words - stop_words
1042+
1043+ # Count matches
1044+ matches = len (answer_words .intersection (topic_words ))
1045+
1046+ # Much stricter criteria: need at least 4 meaningful word matches
1047+ if matches >= min_matches :
1048+ candidates .append ((idx , matches ))
1049+
1050+ # Sort by matches (descending)
1051+ candidates .sort (key = lambda x : x [1 ], reverse = True )
1052+ return candidates
1053+
1054+ def llm_score_candidates (self , answer : str , candidates : List [Tuple [int , int ]]) -> Optional [int ]:
1055+ """Use Gemini to score top candidates"""
1056+ if len (candidates ) <= 1 :
1057+ return candidates [0 ][0 ] if candidates else None
1058+
1059+ # Create prompt with top candidates
1060+ topic_list = []
1061+ for idx , matches in candidates [:10 ]: # Limit to top 10 for efficiency
1062+ topic = list (self .topic_dict .keys ())[list (self .topic_dict .values ()).index (idx )]
1063+ topic_list .append (f"{ idx } : { topic } " )
1064+
1065+ prompt = f"""Score these video topics against the medical answer (0-100 each):
1066+
1067+ Answer: { answer }
1068+
1069+ Topics:
1070+ { chr (10 ).join (topic_list )}
1071+
1072+ IMPORTANT: Only give high scores (80+) if the video topic is DIRECTLY and STRONGLY related to the medical answer.
1073+ - 90-100: Perfect match, video directly addresses the answer topic
1074+ - 80-89: Strong match, video covers the same medical condition/treatment
1075+ - 70-79: Moderate match, video is somewhat related
1076+ - 60-69: Weak match, video has some connection
1077+ - 0-59: No meaningful connection
1078+
1079+ Return JSON: {{"scores": [85, 92, 45, ...]}}"""
1080+
1081+ try :
1082+ response = gemini_llm .invoke ([HumanMessage (content = prompt )]).content
1083+
1084+ # Parse JSON response
1085+ try :
1086+ # Extract JSON from response
1087+ json_start = response .find ('{' )
1088+ json_end = response .rfind ('}' ) + 1
1089+ if json_start != - 1 and json_end > json_start :
1090+ json_str = response [json_start :json_end ]
1091+ scores_data = json .loads (json_str )
1092+ scores = scores_data .get ('scores' , [])
1093+
1094+ if scores and len (scores ) == len (candidates [:10 ]):
1095+ # Find best score
1096+ best_score = max (scores )
1097+ best_score_idx = scores .index (best_score )
1098+ best_candidate_idx = candidates [best_score_idx ][0 ]
1099+
1100+ # Only return if score is high enough (80+ for strong relevance)
1101+ if best_score >= 80 :
1102+ print (f"[VIDEO_SYSTEM] Best score: { best_score } (meets threshold)" )
1103+ return best_candidate_idx
1104+ else :
1105+ print (f"[VIDEO_SYSTEM] Best score: { best_score } (below 80 threshold, no video)" )
1106+ return None
1107+
1108+ except Exception as e :
1109+ print (f"[VIDEO_SYSTEM] Error parsing LLM response: { e } " )
1110+
1111+ except Exception as e :
1112+ print (f"[VIDEO_SYSTEM] LLM call failed: { e } " )
1113+
1114+ # Fallback to first candidate
1115+ return candidates [0 ][0 ] if candidates else None
1116+
1117+ def find_relevant_video (self , answer : str ) -> Optional [str ]:
1118+ """Find relevant video URL for the answer - STRICT MATCHING ONLY"""
1119+ if not self .topic_dict :
1120+ return None
1121+
1122+ print (f"[VIDEO_SYSTEM] Searching for video for answer: { answer [:100 ]} ..." )
1123+
1124+ # Step 1: Pre-filtering (strict - need 4+ meaningful word matches)
1125+ candidates = self .pre_filter_topics (answer , min_matches = 4 )
1126+
1127+ if not candidates :
1128+ print ("[VIDEO_SYSTEM] No candidates found with 4+ meaningful word matches" )
1129+ return None
1130+
1131+ print (f"[VIDEO_SYSTEM] Found { len (candidates )} candidates after pre-filtering" )
1132+
1133+ # Step 2: LLM scoring (if multiple candidates)
1134+ if len (candidates ) == 1 :
1135+ # For single candidate, still use LLM to verify relevance
1136+ best_idx = self .llm_score_candidates (answer , candidates )
1137+ else :
1138+ best_idx = self .llm_score_candidates (answer , candidates )
1139+
1140+ # Step 3: Get URL only if we have a valid, high-scoring match
1141+ if best_idx is not None and best_idx in self .url_dict :
1142+ video_url = self .url_dict [best_idx ]
1143+ print (f"[VIDEO_SYSTEM] Found relevant video: { video_url } " )
1144+ return video_url
1145+ else :
1146+ print ("[VIDEO_SYSTEM] No video found - no high-relevance matches" )
1147+ return None
1148+
1149+ # Global video matching system
1150+ video_system : VideoMatchingSystem = None
1151+
9881152# -------------------- MAIN PIPELINE (called by API) --------------------
9891153async def medical_pipeline_api (user_id : str , user_message : str , background_tasks : BackgroundTasks ) -> Dict [str , Any ]:
9901154 print (f"[pipeline] Start user_id={ user_id } , message={ user_message [:50 ]} " )
@@ -1073,14 +1237,36 @@ async def medical_pipeline_api(user_id: str, user_message: str, background_tasks
10731237 if label != "FOLLOW_UP" and correction :
10741238 correction_msg = "I guess you meant " + " and " .join (correction .values ())
10751239 answer = correction_msg + "\n " + answer
1240+
1241+ # Find relevant video URL
1242+ print ("[pipeline] Step 6: Finding relevant video..." )
1243+ video_url = None
1244+ if video_system :
1245+ video_start = time .perf_counter ()
1246+ video_url = video_system .find_relevant_video (answer )
1247+ video_end = time .perf_counter ()
1248+ print (f"[pipeline] Video matching took { video_end - video_start :.3f} secs" )
1249+ if video_url :
1250+ print (f"[pipeline] Found relevant video: { video_url } " )
1251+ else :
1252+ print ("[pipeline] No relevant video found" )
1253+
10761254 # Schedule full update+save in background (do not run update_chat_history in request path)
10771255 print ("[pipeline] schedule background save: answer" )
10781256 background_tasks .add_task (_background_update_and_save , user_id , user_message , answer , "answer" , history_pairs , current_summary )
10791257 print ("[pipeline] done with answer" )
10801258 t_end = time .perf_counter ()
10811259 timer .total ("request" )
10821260 print (f"total took { t_end - start_time :.2f} secs" )
1083- return {"answer" : answer , "intent" : "answer" , "follow_up" : followup_q if followup_q else None }
1261+
1262+ # Return response with video URL
1263+ response = {"answer" : answer , "intent" : "answer" , "follow_up" : followup_q if followup_q else None }
1264+ if video_url :
1265+ response ["video_url" ] = video_url
1266+ else :
1267+ response ["video_url" ] = None
1268+
1269+ return response
10841270
10851271 else :
10861272 msg = (
@@ -1094,13 +1280,15 @@ async def medical_pipeline_api(user_id: str, user_message: str, background_tasks
10941280 t_end = time .perf_counter ()
10951281 timer .total ("request" )
10961282 print (f"total took { t_end - start_time :.2f} secs" )
1097- return {"answer" : msg , "intent" : "no_context" , "follow_up" : None }
1283+
1284+ # Return response with video URL (None for no_context)
1285+ return {"answer" : msg , "intent" : "no_context" , "follow_up" : None , "video_url" : None }
10981286
10991287
11001288# -------------------- API ENDPOINTS --------------------
11011289@app .on_event ("startup" )
11021290async def startup_event ():
1103- global embedding_model , reranker , pinecone_index , llm , summarizer_llm , reformulate_llm , classifier_llm , gemini_llm , EMBED_DIM
1291+ global embedding_model , reranker , pinecone_index , llm , summarizer_llm , reformulate_llm , classifier_llm , gemini_llm , EMBED_DIM , video_system
11041292 print ("[startup] Initializing models and Pinecone client..." )
11051293 t = CheckpointTimer ("startup" )
11061294 embedding_model = SentenceTransformer ("sentence-transformers/all-mpnet-base-v2" )
@@ -1136,6 +1324,11 @@ async def startup_event():
11361324 gemini_llm = ChatGoogleGenerativeAI (model = "gemini-2.5-flash-lite" , api_key = GOOGLE_API_KEY )
11371325 t .mark ("init_llms" )
11381326
1327+ # Initialize video matching system
1328+ print ("[startup] Initializing video matching system..." )
1329+ video_system = VideoMatchingSystem ()
1330+ t .mark ("init_video_system" )
1331+
11391332 await init_db ()
11401333 t .mark ("init_db" )
11411334 print ("[startup] FastAPI application startup complete." )
@@ -1163,4 +1356,4 @@ async def chat_endpoint(
11631356
11641357if __name__ == "__main__" :
11651358 import uvicorn
1166- uvicorn .run (app , host = "0.0.0.0" , port = 8000 )
1359+ uvicorn .run (app , host = "0.0.0.0" , port = 8000 )
0 commit comments