#!/usr/bin/env python3
"""Streamlit app to reconstruct prompts from AI assistant responses."""
import html as html_lib
import os
import re
import requests
import torch
import pandas as pd
import streamlit as st
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = "dejanseo/reverse-prompter"
SEPARATOR = "\n###\n"
CONTRASTIVE_CONFIGS = [
{"penalty_alpha": a, "top_k": k}
for k in [2, 4, 6, 15]
for a in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
]
@st.cache_resource
def load_model():
# bfloat16 on CUDA; float32 on CPU to prevent CPU kernel incompatibilities
if torch.cuda.is_available():
device = "cuda"
dtype = torch.bfloat16
else:
device = "cpu"
dtype = torch.float32
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
torch_dtype=dtype,
low_cpu_mem_usage=True,
).to(device).eval()
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
return model, tokenizer
def analyze_output(model, tokenizer, input_ids, prefix_len):
with torch.no_grad():
outputs = model(input_ids.unsqueeze(0))
logits = outputs.logits[0, prefix_len - 1:-1]
targets = input_ids[prefix_len:]
log_probs = torch.log_softmax(logits, dim=-1)
token_log_probs = log_probs[torch.arange(len(targets)), targets]
perplexity = torch.exp(-token_log_probs.mean()).item()
probs = torch.exp(token_log_probs).tolist()
tokens = [tokenizer.decode([t]) for t in targets.tolist()]
return perplexity, tokens, probs
def render_colored(tokens, probs):
parts = []
for token, prob in zip(tokens, probs):
opacity = 0.15 + 0.85 * prob
clean = html_lib.escape(token).replace("\n", " ").replace("\r", "")
clean = re.sub(r"[^\x20-\x7E\u00A0-\uFFFF]", "", clean)
parts.append(f'{clean}')
return "".join(parts)
def render_result(tokens, probs):
colored = render_colored(tokens, probs)
return f'
{colored}
'
def fetch_url_markdown(url):
login = os.getenv("DATAFORSEO_LOGIN", "")
password = os.getenv("DATAFORSEO_PASSWORD", "")
if not login or not password:
raise ValueError("DATAFORSEO_LOGIN / DATAFORSEO_PASSWORD not set in Spaces secrets")
resp = requests.post(
"https://api.dataforseo.com/v3/on_page/content_parsing/live",
json=[{"url": url, "enable_javascript": True, "markdown_view": True}],
auth=(login, password),
timeout=30,
)
if resp.status_code != 200:
raise ValueError(f"API returned status {resp.status_code}")
data = resp.json()
tasks = data.get("tasks", [])
if not tasks or tasks[0].get("status_code") != 20000:
raise ValueError(f"API task error: {tasks[0].get('status_message') if tasks else 'no tasks'}")
items = (tasks[0].get("result") or [{}])[0].get("items")
if not items:
raise ValueError("No items returned from API")
text = items[0].get("page_as_markdown") or ""
if len(text) < 100:
raise ValueError("Page content too short or empty")
return text
st.set_page_config(layout="wide")
st.logo("https://dejan.ai/media/dejan-logo.png", size="LARGE", link="https://dejan.ai/")
st.subheader("Reverse Prompting")
st.caption(
"Paste any AI-generated text and this tool will reverse-engineer the most likely prompts that produced it. "
"It runs the text through a fine-tuned language model across multiple decoding configurations and ranks "
"the reconstructed prompts by model confidence."
)
model, tokenizer = load_model()
TEST_FILE = os.path.join(os.path.dirname(__file__), "test.md")
FALLBACK_TEST_TEXT = (
"The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. "
"It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889."
)
if "quick_test" not in st.session_state:
st.session_state.quick_test = False
if "fetched_text" not in st.session_state:
st.session_state.fetched_text = ""
if "run_after_fetch" not in st.session_state:
st.session_state.run_after_fetch = False
if st.session_state.fetched_text:
st.session_state.text_input = st.session_state.fetched_text
st.session_state.fetched_text = ""
st.session_state.run_after_fetch = True
if st.session_state.quick_test:
if os.path.exists(TEST_FILE):
with open(TEST_FILE, encoding="utf-8") as f:
st.session_state.text_input = f.read()
else:
st.session_state.text_input = FALLBACK_TEST_TEXT
tab_paste, tab_url = st.tabs(["Paste", "URL Mode \u1D49\u02E3\u1D56\u1D49\u02B3\u2071\u1D50\u1D49\u207F\u1D57\u1D43\u02E1"])
with tab_paste:
text_input = st.text_area("Paste an AI assistant response", height=200, key="text_input")
col_btn1, col_btn2, col_btn3, _ = st.columns([1, 1, 1, 3])
run = col_btn1.button("Reconstruct Prompts", type="primary", width="stretch")
quick = col_btn2.button("Quick Test", type="secondary", width="stretch")
clear = col_btn3.button("Clear", type="secondary", width="stretch")
if quick and not st.session_state.quick_test:
st.session_state.quick_test = True
st.rerun()
if clear:
st.session_state.text_input = ""
st.rerun()
run = run or st.session_state.quick_test or st.session_state.run_after_fetch
if st.session_state.quick_test:
st.session_state.quick_test = False
if st.session_state.run_after_fetch:
st.session_state.run_after_fetch = False
with tab_url:
st.caption(
"This experimental feature reveals what prompts would lead to generating a page such as the one you entered. "
"While *not the intended use of the model*, it's certainly an interesting feature for exploring semantic make-up of the page."
)
url_col, url_btn_col = st.columns([5, 1])
url_input = url_col.text_input("URL to scrape", label_visibility="collapsed", placeholder="Enter a URL to scrape")
fetch = url_btn_col.button("Fetch", type="primary", width="stretch")
if fetch and url_input.strip():
url = url_input.strip()
if not url.startswith(("http://", "https://")):
url = "https://" + url
if "/" not in url.split("//", 1)[-1]:
url += "/"
with st.spinner("Fetching page..."):
try:
st.session_state.fetched_text = fetch_url_markdown(url)
st.rerun()
except Exception:
st.warning(
"This page couldn't be scraped. The site may be blocking automated access or "
"the page has insufficient content. Try pasting the content manually in the text area."
)
if run and text_input.strip():
prompt = text_input.strip() + SEPARATOR
inputs = tokenizer(prompt, return_tensors="pt", add_special_tokens=False).to(model.device)
prefix_len = inputs["input_ids"].shape[-1]
tables_container = st.container()
st.subheader("Word Frequency Heatmap")
results = []
seen = set()
progress = st.progress(0)
output_container = st.empty()
for i, config in enumerate(CONTRASTIVE_CONFIGS):
progress.progress((i + 1) / len(CONTRASTIVE_CONFIGS))
try:
outputs = model.generate(
**inputs,
max_new_tokens=256,
trust_remote_code=True,
num_return_sequences=1,
**config,
)
gen_ids = outputs[0][prefix_len:]
eos_id = tokenizer.eos_token_id
if eos_id is not None and (gen_ids == eos_id).any():
gen_ids = gen_ids[:(gen_ids == eos_id).nonzero(as_tuple=True)[0][0]]
text = tokenizer.decode(gen_ids, skip_special_tokens=True).strip()
if text and text not in seen:
seen.add(text)
trimmed = torch.cat([outputs[0][:prefix_len], gen_ids])
ppl, tokens, probs = analyze_output(model, tokenizer, trimmed, prefix_len)
results.append((text, ppl, tokens, probs, config))
top = sorted(results, key=lambda x: x[1])[:10]
html = "".join(render_result(t, p) for _, _, t, p, c in top)
output_container.markdown(html, unsafe_allow_html=True)
except Exception:
pass
progress.empty()
results.sort(key=lambda x: x[1])
if results:
top10 = results[:10]
col_prompts, col_phrases = tables_container.columns(2)
with col_prompts:
st.subheader("Reconstructed Prompts")
st.caption(
"Top 10 most likely prompts ranked by lowest perplexity. "
"Token opacity reflects the model's confidence in each word."
)
df_top = pd.DataFrame(
[(text, round(ppl, 2), [round(p * 100) for p in probs]) for text, ppl, _, probs, _ in top10],
columns=["Prompt", "Perplexity", "Confidence"],
)
st.dataframe(
df_top,
width="content",
hide_index=True,
column_config={
"Prompt": st.column_config.TextColumn(width=None),
"Perplexity": st.column_config.NumberColumn(width=None),
"Confidence": st.column_config.BarChartColumn(y_min=0, y_max=100),
},
)
with col_phrases:
st.subheader("Key Phrases")
st.caption("The most important phrases scored by balance between length and frequency.")
texts = [re.sub(r"[^\w\s]", "", text.lower()).split() for text, _, _, _, _ in top10]
phrase_hits = {}
for idx, words in enumerate(texts):
for length in range(2, len(words) + 1):
for start in range(len(words) - length + 1):
phrase = " ".join(words[start:start + length])
if phrase not in phrase_hits:
phrase_hits[phrase] = set()
phrase_hits[phrase].add(idx)
shared = [
(p, len(ids), len(p.split()), len(ids) * len(p.split()))
for p, ids in phrase_hits.items()
if len(ids) >= 2
]
shared.sort(key=lambda x: x[3], reverse=True)
filtered = []
for phrase, count, length, score in shared:
if not any(phrase in longer and score <= lscore for longer, _, _, lscore in filtered):
filtered.append((phrase, count, length, score))
if filtered:
max_score = filtered[0][3]
rows = [(p, round(s / max_score * 100)) for p, _, _, s in filtered[:20]]
df = pd.DataFrame(rows, columns=["Phrase", "Score"])
st.dataframe(
df,
width="content",
hide_index=True,
column_config={
"Score": st.column_config.ProgressColumn(format="%d%%", min_value=0, max_value=100),
},
)