| """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 = 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): |
| 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)) |
|
|
| |
| 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)) |
|
|