No results.
"
cards = []
for r in results:
score_pct = int(r["score"] * 100)
score_bar = "█" * (score_pct // 5) + "░" * (20 - score_pct // 5)
text_preview = r["text"][:400] + ("…" if len(r["text"]) > 400 else "")
date_note = f" · {r['date']}" if r.get("date") else ""
cards.append(f"""
{score_bar} {r['score']:.3f}
{r['author']}
{text_preview}
""")
return "\n".join(cards)
def run_search(query, top_k, author_filter, century_filter, genre_filter,
llm_model, do_synthesize):
if not query.strip():
yield "Enter a search query.
", "", "", [], "", []
return
results = semantic_search(
query,
top_k=int(top_k),
author_filter=author_filter or None,
century_filter=century_filter or None,
genre_filter=genre_filter or None,
)
html = format_results_html(results)
top_authors = author_summary(results)
weak_match = bool(results) and results[0]["score"] < WEAK_MATCH_THRESHOLD
if weak_match:
html = (
''
f'⚠️ No strong matches — the best passage scored only '
f'{results[0]["score"]:.2f}. The corpus may not contain this '
'concept; the results below are the nearest retrieved text, not '
'confirmed matches.
'
) + html
# New search grounds a fresh chat: store results/query, clear history.
# Results render immediately; synthesis (if requested) streams in after.
yield html, top_authors, "", results, query, []
if do_synthesize and results:
if weak_match:
note = (
f"*Synthesis skipped: no retrieved passage scored above "
f"{WEAK_MATCH_THRESHOLD:.2f}, so the corpus likely does not "
f"address this concept directly.*"
)
yield html, top_authors, note, results, query, []
else:
for acc in synthesize_stream(query, results, llm_model):
yield html, top_authors, acc, results, query, []
def chat_respond(message, history, llm_model, query, results, synthesis):
"""Chat about the last search's passages; streams the assistant reply."""
message = (message or "").strip()
if not message:
yield history, ""
return
history = history + [{"role": "user", "content": message}]
if not results:
history.append({
"role": "assistant",
"content": "Run a search first — then I can discuss the retrieved passages with you.",
})
yield history, ""
return
messages = [
{"role": "system",
"content": chat_system_prompt(query, results, synthesis or "")}
] + history
history.append({"role": "assistant", "content": ""})
for acc in llm_chat_stream(messages, llm_model):
history[-1]["content"] = acc
yield history, ""
def build_ui() -> gr.Blocks:
with gr.Blocks(title="ζήτημα") as demo:
gr.Markdown(
"# Zetema\n"
"Query the ancient Greek corpus using natural language. "
"Passages are retrieved by embedding similarity, then reranked "
"with a cross-encoder for final ordering.",
elem_classes="sem-title",
)
with gr.Row():
with gr.Column(scale=3):
query_box = gr.Textbox(
label="Search query (in English or Greek)",
placeholder="e.g. 'the immortality of the soul', 'ψυχή', 'rhetoric and democracy'…",
lines=2,
)
with gr.Column(scale=1):
search_btn = gr.Button("Search", variant="primary", size="md")
with gr.Row():
top_k = gr.Slider(5, 50, value=15, step=5, label="Number of results")
author_filter = gr.Dropdown(
choices=_authors,
multiselect=True,
label="Filter by author",
value=None,
)
century_filter = gr.Dropdown(
choices=_century_labels,
multiselect=True,
label="Filter by century",
value=None,
)
genre_filter = gr.Dropdown(
choices=_genre_labels,
multiselect=True,
label="Filter by genre",
value=None,
)
with gr.Row():
llm_model = gr.Dropdown(
choices=LLM_MODELS,
value=DEFAULT_LLM,
label="LLM for synthesis / chat (loaded on first use)",
)
do_synthesize = gr.Checkbox(
label="Synthesize results",
value=True,
)
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("### Retrieved passages")
results_html = gr.HTML()
with gr.Column(scale=1):
gr.Markdown("### Top authors on this topic")
top_authors_md = gr.Markdown()
gr.Markdown("### LLM synthesis")
synthesis_md = gr.Markdown()
gr.Markdown("### Chat about these results")
chatbot = gr.Chatbot(
height=400,
label="Grounded in the retrieved passages — cleared on each new search",
)
with gr.Row():
chat_input = gr.Textbox(
placeholder="Ask about the retrieved passages — interpretation, translation, comparisons…",
show_label=False,
scale=5,
)
chat_send = gr.Button("Send", scale=1)
results_state = gr.State([])
query_state = gr.State("")
search_outputs = [
results_html, top_authors_md, synthesis_md,
results_state, query_state, chatbot,
]
search_inputs = [
query_box, top_k, author_filter, century_filter, genre_filter,
llm_model, do_synthesize,
]
search_btn.click(fn=run_search, inputs=search_inputs, outputs=search_outputs)
query_box.submit(fn=run_search, inputs=search_inputs, outputs=search_outputs)
# synthesis_md is passed by value: the chat needs to see the summary
# the user is currently reading.
chat_inputs = [chat_input, chatbot, llm_model, query_state,
results_state, synthesis_md]
chat_send.click(fn=chat_respond, inputs=chat_inputs, outputs=[chatbot, chat_input])
chat_input.submit(fn=chat_respond, inputs=chat_inputs, outputs=[chatbot, chat_input])
# Live scheme switcher: restyles cards AND app chrome instantly
# (pure client-side; per-session, not persisted).
scheme_picker = gr.Radio(
choices=list(SCHEME_CLASSES),
value="Parchment",
label="Color scheme",
)
scheme_picker.change(fn=None, inputs=scheme_picker, outputs=None,
js=SCHEME_SWITCH_JS)
return demo
def main() -> None:
print("Loading index...")
load_index()
load_reranker()
if IS_SPACE:
# ZeroGPU functions have a time budget: pay the LLM download/load at
# startup (CPU side) instead of inside the first @gpu call.
get_llm(DEFAULT_LLM)
demo = build_ui()
# Spaces health-checks port 7860 and doesn't always set GRADIO_SERVER_PORT.
default_port = "7860" if IS_SPACE else "7861"
demo.launch(server_name="0.0.0.0",
server_port=int(os.environ.get("GRADIO_SERVER_PORT", default_port)),
inbrowser=not IS_SPACE,
theme=gr.themes.Soft(), css=APP_CSS)
if __name__ == "__main__":
main()