slugless/shouty-clean
0
1import json2import re3import torch4import openai5from fastapi import FastAPI, Request6from fastapi.responses import JSONResponse7from fastapi.middleware.cors import CORSMiddleware8from sentence_transformers import SentenceTransformer, util9import os10import time11from supabase import create_client12 13SUPABASE_URL = os.environ.get("SUPABASE_URL")14SUPABASE_KEY = os.environ.get("SUPABASE_KEY")15supabase = create_client(SUPABASE_URL, SUPABASE_KEY)16SUPABASE_TABLE = "Questions"17 18app = FastAPI()19 20app.add_middleware(21 CORSMiddleware,22 allow_origins=["*"],23 allow_methods=["*"],24 allow_headers=["*"]25)26 27openai.api_key = os.environ.get("openaikey")28 29data = torch.load("precomputed_embeddings.pt", map_location=torch.device("cpu"))30all_entries = data["entries"]31all_embeddings = data["embeddings"]32embedding_model = SentenceTransformer(33 "sentence-transformers/all-MiniLM-L6-v2"34)35 36DEFAULT_FIRST_PROMPT = (37 "You are a multi-perspective responder.\n\n"38 "For any user question, return exactly 5 short, one-sentence answers. Each answer must:\n"39 "- Be exactly one short sentence, no more.\n"40 "- Repeat the key phrase or subject of the original question verbatim.\n"41 "- Express a distinct point of view that could come from a different type of person, mind, or world.\n"42 "- Vary widely in tone: include at least one response that is emotional, one that is absurd or surreal, and one that is confrontational or intense.\n"43 "- At least one response must express strong certainty, and at least one must express strong uncertainty or doubt.\n"44 "- Responses may be angry, spiritual, naive, bizarre, sarcastic, sincere, alien, conspiratorial, or poetic—just not repetitive or safe.\n\n"45 "Format rules:\n"46 "- Output only the 5 sentences, each on its own line.\n"47 "- Do not include the question, headers, numbers, bullets, or commentary.\n"48 "- Do not explain anything before or after the 5 lines.\n\n"49 "Your goal is to simulate the raw inner monologues of 5 radically different beings reacting to the same question."50)51 52SELECTION_PROMPT = (53 "You will be given a question and a list of possible responses, each numbered from 0 to N-1.\n"54 "Your job is to choose the single best response based on two criteria:\n\n"55 "1. Conversational Reasonability — how naturally it could be said in response to the question. "56 "It does not need to be helpful or polite, just something that would make sense in any kind of conversation (even sarcastic or confrontational ones).\n"57 "2. Humor — how funny it is, whether dry, ironic, or absurd.\n\n"58 "Prefer witty or fitting deflections over wild absurdity or randomness.\n"59 "Return ONLY the index of the best response. Do not explain your reasoning.\n\n"60)61 62def get_multiperspective_responses(question, system_prompt):63 messages = [64 {"role": "system", "content": system_prompt},65 {"role": "user", "content": f"Question: {question}"}66 ]67 response = openai.ChatCompletion.create(68 model="gpt-3.5-turbo", messages=messages, temperature=069 )70 output = response['choices'][0]['message']['content']71 sentences = re.split(r'[.!?]', output)72 return [s.strip() for s in sentences if s.strip()]73 74def get_top_semantic_matches(text, entries, embeddings, model, top_k=10):75 query_embedding = model.encode(text, convert_to_tensor=True)76 cosine_scores = util.cos_sim(query_embedding, embeddings)[0]77 top_results = torch.topk(cosine_scores, k=top_k)78 return [entries[i]["text"] for i in top_results.indices.tolist()]79 80def remove_duplicate_lines(candidates):81 seen = set()82 unique = []83 for line in candidates:84 if line not in seen:85 unique.append(line)86 seen.add(line)87 return unique88 89def select_best_candidate(question, candidates):90 prompt = SELECTION_PROMPT + f"Question: {question}\n\nCandidates:\n"91 for i, candidate in enumerate(candidates):92 prompt += f"{i}: {candidate}\n"93 messages = [{"role": "system", "content": prompt}]94 response = openai.ChatCompletion.create(95 model="gpt-4.1", messages=messages, temperature=096 )97 output = response['choices'][0]['message']['content'].strip()98 try:99 return int(output)100 except:101 return None102 103def process_question(question):104 base_responses = get_multiperspective_responses(question, DEFAULT_FIRST_PROMPT)105 all_candidates = []106 for resp in base_responses:107 similar = get_top_semantic_matches(resp, all_entries, all_embeddings, embedding_model)108 all_candidates.extend(similar)109 unique_candidates = remove_duplicate_lines(all_candidates)110 unique_candidates.sort(key=len, reverse=True)111 best_index = select_best_candidate(question, unique_candidates)112 if best_index is None or best_index < 0 or best_index >= len(unique_candidates):113 return None, None, "No valid candidate was selected."114 best_candidate = unique_candidates[best_index]115 with open("matches_output_with_hash.json", "r") as f:116 hash_matches = json.load(f)117 for entry in hash_matches:118 if "text" in entry and entry["text"] == best_candidate:119 hash_val = entry.get("hash")120 if hash_val:121 return hash_val, best_candidate, None122 return None, best_candidate, "No matching hash found for the selected candidate."123 124@app.post("/lookup")125async def lookup(request: Request):126 try:127 data = await request.json()128 question = data.get("question", "")129 if not question:130 raise ValueError("No question provided")131 132 # Get real client IP if behind proxy133 client_ip = request.headers.get("x-forwarded-for", request.client.host)134 client_ip = client_ip.split(",")[0].strip()135 136 # Generate 5 base responses from multiperspective prompt137 base_responses = get_multiperspective_responses(question, DEFAULT_FIRST_PROMPT)138 139 # Collect all semantically similar lines140 all_candidates = []141 for resp in base_responses:142 similar = get_top_semantic_matches(resp, all_entries, all_embeddings, embedding_model)143 all_candidates.extend(similar)144 145 # Deduplicate + sort146 unique_candidates = remove_duplicate_lines(all_candidates)147 unique_candidates.sort(key=len, reverse=True)148 149 # Select best final line150 best_index = select_best_candidate(question, unique_candidates)151 if best_index is None or best_index < 0 or best_index >= len(unique_candidates):152 raise ValueError("No valid candidate was selected.")153 final_choice = unique_candidates[best_index]154 155 # Match to hash156 with open("matches_output_with_hash.json", "r") as f:157 hash_matches = json.load(f)158 159 matched_hash = None160 for entry in hash_matches:161 if "text" in entry and entry["text"] == final_choice:162 matched_hash = entry.get("hash")163 break164 165 if not matched_hash:166 raise ValueError("No matching hash found for the selected candidate.")167 168 # Prepare log payload169 log_payload = {170 "client_ip": client_ip,171 "question": question,172 "generated_answers": base_responses,173 "candidate_lines": unique_candidates,174 "final_line_choice": final_choice,175 "hash": matched_hash176 }177 178 # Insert and time it179 import time180 start = time.time()181 supabase.table(SUPABASE_TABLE).insert(log_payload, returning="minimal").execute()182 elapsed = time.time() - start183 print(f"✅ Supabase insert took {elapsed:.4f} seconds")184 185 return {"video": f"{matched_hash}.mp4"}186 187 except Exception as e:188 return JSONResponse(status_code=500, content={"error": str(e)})189 