promptstat / ui /parsing /paste.py
xxixx1028's picture
Deploy PromptStat โ€” UI shell + MiniCPM4.1-8B + 4-LoRA hybrid (Modal)
dc9f530 verified
Raw
History Blame Contribute Delete
3.38 kB
"""Paste input โ†’ a single-conversation ParsedExport (source="paste").
Mirrors the backend `prompt_card.adapters.paste` (the reuse target) and adds markdown variants. Accepts:
- role markers at line start: User:/You:/Human:/Me:/Prompt:/Q: and Assistant:/AI:/ChatGPT:/Claude:/GPT:/Bot:/Gemini:/Answer:/Response:/A:
- separators `:` `>` `-` (en-dash too), e.g. "User -" / "AI >"
- markdown: **User**:, __Assistant__:, "## Assistant", "> User:", "- User:"
- plain alternating turns (blank-line-separated blocks) โ†’ user/assistant starting with user
Requires >=2 user turns (same substance bar as the export path) so all five axes can show signal.
Single conversation only; we never fetch share URLs (paste text only โ€” legal safety).
"""
from __future__ import annotations
import re
from .parser import Conversation, Turn, ParsedExport, ParseError
_USER_WORDS = {"user", "you", "human", "me", "prompt", "q"}
_ASST_WORDS = {"assistant", "ai", "chatgpt", "chat gpt", "gpt", "claude", "bot", "gemini",
"model", "answer", "response", "a"}
# line -> (role_word, separator?, rest). Strips leading markdown (#, >, -, *, spaces) and bold/italic.
_LINE = re.compile(r"^[\s>#\-]*[*_]{0,3}\s*([A-Za-z][A-Za-z ]{0,11})\s*[*_]{0,3}\s*([:>โ€“-])?\s*(.*)$")
def _marker(line: str):
"""Return (role, rest_text) if the line opens a turn, else None."""
stripped_lead = re.sub(r"^[\s>#\-]+", "", line)
had_md = (stripped_lead != line.lstrip()) or "*" in line[:3] or "_" in line[:3]
m = _LINE.match(line)
if not m:
return None
word, sep, rest = m.group(1).strip().lower(), m.group(2), m.group(3)
if not (sep or had_md): # a bare word with no separator/markdown is prose, not a marker
return None
if word in _USER_WORDS:
return "user", rest
if word in _ASST_WORDS:
return "assistant", rest
return None
def _validate(turns: list[Turn]) -> list[Conversation]:
user_n = sum(1 for t in turns if t.role == "user")
if user_n < 2:
raise ParseError("Paste a conversation with at least 2 of YOUR messages "
"(use 'User:' / 'Assistant:' markers, or blank lines between turns).")
return [Conversation(turns=turns)]
def parse_paste(text: str) -> ParsedExport:
if not isinstance(text, str) or not text.strip():
raise ParseError("Nothing to analyze โ€” paste a conversation first.")
marked, role, buf = [], None, []
def flush():
if role and buf:
t = "\n".join(buf).strip()
if t:
marked.append(Turn(role=role, text=t))
for ln in text.splitlines():
hit = _marker(ln)
if hit:
flush()
role, buf = hit[0], ([hit[1]] if hit[1] else [])
elif role is not None:
buf.append(ln)
flush()
if marked:
return ParsedExport(source="paste", conversations=_validate(marked))
# no markers: blank-line blocks, alternating user/assistant starting with the user
blocks = [b.strip() for b in re.split(r"\n\s*\n", text) if b.strip()]
if len(blocks) < 2:
blocks = [ln.strip() for ln in text.splitlines() if ln.strip()]
roles = ("user", "assistant")
turns = [Turn(role=roles[i % 2], text=b) for i, b in enumerate(blocks)]
return ParsedExport(source="paste", conversations=_validate(turns))