import gradio as gr from sentence_transformers import SentenceTransformer import faiss import numpy as np import pandas as pd import anthropic import os print("Loading search engine...") model = SentenceTransformer("all-MiniLM-L6-v2") import subprocess subprocess.run(["python", "build_index.py"]) index = faiss.read_index("library.index") df = pd.read_csv("catalog_processed.csv") print("Ready!") def get_claude_explanation(query, books, api_key): try: if not api_key or not api_key.strip().startswith("sk-"): return "**Debug — key check failed:** key was empty or wrong format" client = anthropic.Anthropic(api_key=api_key.strip()) book_list = "\n".join([ f"- {b['title']} by {b['author']} ({b['subject']}): {b['description']}" for b in books ]) message = client.messages.create( model="claude-haiku-4-5-20251001", max_tokens=400, messages=[ { "role": "user", "content": f"""A library patron searched for: "{query}" These books were returned as semantic matches: {book_list} In 2-3 sentences, explain why these books are relevant to the patron's search. Focus on the themes and ideas that connect the search to these results. Write in a warm, helpful librarian voice. Do not use bullet points.""" } ] ) return message.content[0].text except Exception as e: return None def search(query, num_results, api_key): if not query: return "Please enter a search query." if api_key and api_key.strip().startswith("sk-"): os.environ["ANTHROPIC_API_KEY"] = api_key.strip() query_embedding = model.encode([query]) distances, indices = index.search(np.array(query_embedding), int(num_results)) books = [] for idx in indices[0]: book = df.iloc[idx] books.append({ "title": book["title"], "author": book["author"], "subject": book["subject"], "description": book["description"] }) explanation = get_claude_explanation(query, books, api_key) results = "" if explanation: results += f"#### Librarian's Note\n\n{explanation}\n\n---\n\n" for i, book in enumerate(books): results += f"**{i+1}. {book['title']}**\n\n" results += f"*{book['author']}*  ·  {book['subject']}\n\n" results += f"{book['description']}\n\n---\n\n" return results css = """ :root { color-scheme: light; } html { color-scheme: light; } @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&family=Source+Sans+3:ital,wght@0,300;0,400;0,600;1,400&display=swap'); :root { --maroon: #5C1A1A; --maroon-mid: #7A2424; --maroon-rule: #8B3A3A; --maroon-tint: #F9F2F2; --ink: #1A1A1A; --ink-2: #3D3D3D; --ink-3: #767676; --surface: #FFFFFF; --bg: #F5F3EF; --rule: #DDD9D0; --rule-light: #EAE7E0; } html, body, .gradio-container { background: var(--bg) !important; font-family: 'Source Sans 3', Georgia, sans-serif !important; color: var(--ink) !important; } footer, .built-with, .svelte-1rjryqp { display: none !important; } .gradio-container { max-width: 900px !important; margin: 0 auto !important; padding: 0 !important; } .inst-bar { background: var(--maroon) !important; padding: 0.55rem 2.5rem !important; display: flex !important; align-items: center !important; justify-content: space-between !important; border-bottom: 3px solid var(--maroon-rule) !important; } .inst-bar-left { display: flex !important; align-items: center !important; gap: 0.75rem !important; } .inst-bar-icon { width: 26px !important; height: 26px !important; opacity: 0.9 !important; } .inst-bar-name { font-family: 'Source Sans 3', sans-serif !important; font-size: 0.78rem !important; font-weight: 600 !important; letter-spacing: 0.12em !important; text-transform: uppercase !important; color: #F0E8E8 !important; } .inst-bar-badge { font-size: 0.65rem !important; font-weight: 400 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: #C89A9A !important; border: 1px solid #8B5555 !important; padding: 0.2rem 0.55rem !important; border-radius: 2px !important; } .content-wrap { padding: 2.5rem 2.5rem 4rem !important; } .page-title-block { margin-bottom: 2rem !important; padding-bottom: 1.5rem !important; border-bottom: 1px solid var(--rule) !important; } .page-title { font-family: 'Playfair Display', Georgia, serif !important; font-size: 2.1rem !important; font-weight: 400 !important; color: var(--maroon) !important; letter-spacing: -0.01em !important; line-height: 1.15 !important; margin-bottom: 0.4rem !important; } .page-desc { font-size: 0.9rem !important; color: var(--ink-3) !important; line-height: 1.6 !important; max-width: 560px !important; font-weight: 300 !important; } label span, .label-wrap span { font-size: 0.72rem !important; font-weight: 600 !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; color: var(--ink-3) !important; } textarea, input[type="text"], input[type="password"] { background: var(--surface) !important; border: 1px solid var(--rule) !important; border-radius: 3px !important; color: var(--ink) !important; font-family: 'Source Sans 3', sans-serif !important; font-size: 0.95rem !important; font-weight: 300 !important; padding: 0.7rem 0.9rem !important; box-shadow: none !important; transition: border-color 0.15s !important; } textarea:focus, input:focus { border-color: var(--maroon-mid) !important; outline: none !important; box-shadow: 0 0 0 3px rgba(92,26,26,0.07) !important; } input[type="range"] { accent-color: var(--maroon) !important; } button.primary, button[variant="primary"] { background: var(--maroon) !important; color: #FFFFFF !important; border: none !important; border-radius: 3px !important; font-family: 'Source Sans 3', sans-serif !important; font-size: 0.78rem !important; font-weight: 600 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; padding: 0.72rem 1.6rem !important; transition: background 0.15s, transform 0.1s !important; box-shadow: none !important; } button.primary:hover, button[variant="primary"]:hover { background: var(--maroon-mid) !important; transform: translateY(-1px) !important; } button.primary:active { transform: translateY(0) !important; } .examples button, button.secondary { background: var(--surface) !important; color: var(--ink-2) !important; border: 1px solid var(--rule) !important; border-radius: 3px !important; font-family: 'Source Sans 3', sans-serif !important; font-size: 0.82rem !important; font-weight: 300 !important; padding: 0.35rem 0.8rem !important; transition: border-color 0.15s, color 0.15s !important; } .examples button:hover { border-color: var(--maroon) !important; color: var(--maroon) !important; } .prose, .md, .markdown-body, [data-testid="markdown"] { background: transparent !important; font-family: 'Source Sans 3', sans-serif !important; font-size: 0.95rem !important; line-height: 1.8 !important; font-weight: 300 !important; } .prose p, .md p, [data-testid="markdown"] p, .prose li, .md li, [data-testid="markdown"] li { color: #1A1A1A !important; } .inst-bar-name { color: #F0E8E8 !important; } .inst-bar-badge { color: #C89A9A !important; } [data-testid="markdown"] strong, .prose strong, .md strong { font-family: 'Playfair Display', serif !important; font-weight: 400 !important; font-size: 1.05rem !important; color: #1A1A1A !important; } [data-testid="markdown"] em, .prose em, .md em { font-style: normal !important; font-size: 0.83rem !important; letter-spacing: 0.03em !important; color: #767676 !important; font-weight: 400 !important; } [data-testid="markdown"] h4, .prose h4, .md h4 { font-size: 0.72rem !important; font-weight: 600 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: #5C1A1A !important; margin-bottom: 0.5rem !important; font-family: 'Source Sans 3', sans-serif !important; } [data-testid="markdown"] hr, .prose hr, .md hr { border: none !important; border-top: 1px solid #EAE7E0 !important; margin: 1.2rem 0 !important; } .poc-banner { background: var(--maroon-tint) !important; border: 1px solid #D4AAAA !important; border-left: 3px solid var(--maroon) !important; border-radius: 0 3px 3px 0 !important; padding: 0.6rem 1rem !important; margin-bottom: 2rem !important; font-size: 0.8rem !important; color: var(--maroon) !important; font-weight: 400 !important; line-height: 1.5 !important; } """ with gr.Blocks( css=css, theme=gr.themes.Base( font=gr.themes.GoogleFont("Source Sans 3"), ), title="Research Collections Search" ) as demo: gr.HTML("""
Research Collections & Services
Proof of Concept
""") with gr.Column(elem_classes=["content-wrap"]): gr.HTML("""
Semantic Search
Discover collections through meaning rather than exact keywords. Describe what you're looking for in plain language — subjects, themes, questions, or ideas.
""") gr.HTML("""
This tool is an early-stage prototype demonstrating AI-powered semantic search across a 200-title catalog. Results are ranked by conceptual relevance using sentence-transformer embeddings.
""") with gr.Row(equal_height=False): with gr.Column(scale=5): query_input = gr.Textbox( label="Search query", placeholder="e.g. the social consequences of artificial intelligence...", lines=2, ) with gr.Column(scale=1, min_width=110): num_results = gr.Slider( minimum=1, maximum=5, value=3, step=1, label="Results", ) with gr.Row(equal_height=True): api_key_input = gr.Textbox( label="Anthropic API Key — optional, enables AI-generated annotations", placeholder="sk-ant-...", type="password", scale=5, ) with gr.Column(scale=1, min_width=130): gr.HTML("
") search_btn = gr.Button("Search →", variant="primary") results_output = gr.Markdown() gr.Examples( examples=[ ["books about artificial intelligence and society", 3, ""], ["stories about race and justice in America", 3, ""], ["women who made a difference in science", 3, ""], ["how governments control people", 3, ""], ["climate change and the future of the planet", 3, ""], ["libraries and the organization of knowledge", 3, ""], ["survival against the odds", 3, ""], ], inputs=[query_input, num_results, api_key_input], label="Example queries", ) search_btn.click( fn=search, inputs=[query_input, num_results, api_key_input], outputs=results_output, ) query_input.submit( fn=search, inputs=[query_input, num_results, api_key_input], outputs=results_output, ) demo.launch()