Improve writer prompt testing
This commit is contained in:
@@ -916,10 +916,39 @@ async def category_rows() -> list[dict[str, Any]]:
|
||||
return result
|
||||
|
||||
|
||||
async def prompt_test_posts(selected_post_id: int | None = None) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
||||
PROMPT_TEST_STATUS_OPTIONS = [
|
||||
{"value": "all", "label": "Все"},
|
||||
{"value": "qualified", "label": "Оцененные"},
|
||||
{"value": "accepted", "label": "Принятые"},
|
||||
{"value": "maybe", "label": "Спорные"},
|
||||
{"value": "rejected", "label": "Отклоненные"},
|
||||
{"value": "pending", "label": "Ждут AI"},
|
||||
]
|
||||
|
||||
|
||||
def normalize_prompt_test_status(value: str | None) -> str:
|
||||
value = str(value or "all").strip().lower()
|
||||
allowed = {item["value"] for item in PROMPT_TEST_STATUS_OPTIONS}
|
||||
return value if value in allowed else "all"
|
||||
|
||||
|
||||
def prompt_test_status_where(status_filter: str) -> str:
|
||||
if status_filter == "qualified":
|
||||
return "AND rp.qualification_status IN ('accepted', 'maybe', 'rejected')"
|
||||
if status_filter in {"accepted", "maybe", "rejected", "pending"}:
|
||||
return f"AND rp.qualification_status = '{status_filter}'"
|
||||
return ""
|
||||
|
||||
|
||||
async def prompt_test_posts(
|
||||
selected_post_id: int | None = None,
|
||||
status_filter: str = "all",
|
||||
) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
||||
pool = await get_pool()
|
||||
status_filter = normalize_prompt_test_status(status_filter)
|
||||
where_sql = prompt_test_status_where(status_filter)
|
||||
rows = await pool.fetch(
|
||||
"""
|
||||
f"""
|
||||
SELECT rp.id, rp.raw_text, rp.original_url, rp.posted_at, rp.created_at, rp.qualification_status,
|
||||
rp.qualification_score, s.name AS source_name, s.tag AS source_tag,
|
||||
rp.qualification_reason,
|
||||
@@ -928,6 +957,8 @@ async def prompt_test_posts(selected_post_id: int | None = None) -> tuple[list[d
|
||||
FROM raw_post_media m WHERE m.raw_post_id=rp.id) AS media_types
|
||||
FROM raw_posts rp
|
||||
JOIN sources s ON s.id=rp.source_id
|
||||
WHERE TRUE
|
||||
{where_sql}
|
||||
ORDER BY rp.created_at DESC, rp.id DESC
|
||||
LIMIT 150
|
||||
"""
|
||||
@@ -990,6 +1021,34 @@ def prompt_test_payload(post: dict[str, Any], max_text_chars: int) -> dict[str,
|
||||
}
|
||||
|
||||
|
||||
def prompt_test_first_rewrite(parsed: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(parsed, dict):
|
||||
return None
|
||||
rewrites = parsed.get("rewrites")
|
||||
if not isinstance(rewrites, list) and isinstance(parsed.get("data"), dict):
|
||||
rewrites = parsed["data"].get("rewrites")
|
||||
if not isinstance(rewrites, list) or not rewrites:
|
||||
return None
|
||||
first = rewrites[0]
|
||||
return first if isinstance(first, dict) else None
|
||||
|
||||
|
||||
def prompt_test_preview_text(parsed: Any) -> str:
|
||||
rewrite = prompt_test_first_rewrite(parsed)
|
||||
return str((rewrite or {}).get("text") or "").strip()
|
||||
|
||||
|
||||
def prompt_test_preview_category(parsed: Any) -> str:
|
||||
rewrite = prompt_test_first_rewrite(parsed)
|
||||
return str((rewrite or {}).get("category") or "").strip()
|
||||
|
||||
|
||||
def prompt_test_preview_notes(parsed: Any) -> str:
|
||||
rewrite = prompt_test_first_rewrite(parsed)
|
||||
notes = (rewrite or {}).get("notes")
|
||||
return "" if notes in {None, ""} else str(notes).strip()
|
||||
|
||||
|
||||
def setting_value(settings_rows: list[dict], key: str, default: Any = None) -> Any:
|
||||
for row in settings_rows:
|
||||
if row.get("key") == key:
|
||||
@@ -2272,11 +2331,12 @@ async def raw_post_detail(request: Request, post_id: int):
|
||||
|
||||
|
||||
@app.get("/prompt-test", response_class=HTMLResponse)
|
||||
async def prompt_test(request: Request, post_id: int | None = None):
|
||||
async def prompt_test(request: Request, post_id: int | None = None, q_status: str = "all"):
|
||||
user = await get_current_user(request)
|
||||
if not user:
|
||||
return redirect("/login")
|
||||
posts, selected = await prompt_test_posts(post_id)
|
||||
q_status = normalize_prompt_test_status(q_status)
|
||||
posts, selected = await prompt_test_posts(post_id, q_status)
|
||||
categories = await writer_categories()
|
||||
max_text_chars = max(200, int(await fetch_setting("ai_writer_max_text_chars", 3500) or 3500))
|
||||
prompt = str(await fetch_setting("ai_writer_prompt", "") or "")
|
||||
@@ -2295,6 +2355,8 @@ async def prompt_test(request: Request, post_id: int | None = None):
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
model=model,
|
||||
q_status=q_status,
|
||||
status_options=PROMPT_TEST_STATUS_OPTIONS,
|
||||
max_text_chars=max_text_chars,
|
||||
payload_json=json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
result=None,
|
||||
@@ -2309,12 +2371,14 @@ async def prompt_test_run(
|
||||
csrf_token: str = Form(...),
|
||||
post_id: int = Form(...),
|
||||
prompt: str = Form(...),
|
||||
q_status: str = Form("all"),
|
||||
):
|
||||
user = await get_current_user(request)
|
||||
if not user:
|
||||
return redirect("/login")
|
||||
require_csrf(user, csrf_token)
|
||||
posts, selected = await prompt_test_posts(post_id)
|
||||
q_status = normalize_prompt_test_status(q_status)
|
||||
posts, selected = await prompt_test_posts(post_id, q_status)
|
||||
categories = await writer_categories()
|
||||
max_text_chars = max(200, int(await fetch_setting("ai_writer_max_text_chars", 3500) or 3500))
|
||||
provider = str(await fetch_setting("ai_writer_provider", "anthropic") or "anthropic")
|
||||
@@ -2367,6 +2431,9 @@ async def prompt_test_run(
|
||||
result = {
|
||||
"content": content,
|
||||
"parsed_json": json.dumps(parsed, ensure_ascii=False, indent=2) if parsed is not None else "",
|
||||
"preview_text": prompt_test_preview_text(parsed),
|
||||
"preview_category": prompt_test_preview_category(parsed),
|
||||
"preview_notes": prompt_test_preview_notes(parsed),
|
||||
"usage": response_usage(response),
|
||||
"model": kwargs["model"],
|
||||
}
|
||||
@@ -2384,6 +2451,8 @@ async def prompt_test_run(
|
||||
prompt=normalized_prompt,
|
||||
provider=provider,
|
||||
model=model,
|
||||
q_status=q_status,
|
||||
status_options=PROMPT_TEST_STATUS_OPTIONS,
|
||||
max_text_chars=max_text_chars,
|
||||
payload_json=json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
result=result,
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
.prompt-hint summary { cursor:pointer; font-weight:700; }
|
||||
.prompt-hint pre, .prompt-box { white-space:pre-wrap; overflow:auto; padding:10px; border:1px solid var(--line); border-radius:8px; background:#f8fafc; color:var(--text); }
|
||||
.prompt-test-layout { display:grid; grid-template-columns:minmax(0, 1fr) 420px; gap:16px; align-items:start; }
|
||||
.prompt-test-controls { display:grid; grid-template-columns:150px minmax(260px, 1fr) minmax(220px, .7fr); gap:12px; align-items:end; }
|
||||
.prompt-test-main { margin-bottom:0; }
|
||||
.prompt-test-side { display:grid; gap:16px; position:sticky; top:74px; max-height:calc(100vh - 92px); overflow:auto; }
|
||||
.prompt-test-model { min-height:38px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
@@ -125,6 +126,7 @@
|
||||
.prompt-test-post-head { display:flex; justify-content:space-between; gap:12px; align-items:center; margin-bottom:4px; }
|
||||
.prompt-test-post pre { max-height:260px; overflow:auto; margin:10px 0 0; white-space:pre-wrap; font:inherit; color:var(--text); }
|
||||
.prompt-test-help p { margin:0; }
|
||||
.human-answer { white-space:pre-wrap; overflow:auto; max-height:360px; padding:12px; border:1px solid #b7caf8; border-radius:8px; background:#f8fbff; color:var(--text); font:16px/1.55 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; }
|
||||
.hint-grid { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
|
||||
.hint-label { color:var(--muted); font-size:12px; font-weight:700; text-transform:uppercase; margin-bottom:3px; }
|
||||
.small-help { font-size:12px; margin-top:4px; }
|
||||
@@ -216,7 +218,7 @@
|
||||
.regen-form { margin-top:16px; border-top:1px solid var(--line); padding-top:14px; }
|
||||
.compact-kv { margin-top:12px; grid-template-columns:90px minmax(0,1fr); }
|
||||
.empty-state { text-align:center; color:var(--muted); padding:34px; }
|
||||
@media (max-width: 900px) { header { padding:8px 12px; align-items:flex-start; } .filter-grid, .sources-filter, .source-add-grid, .hint-grid, .advanced-grid, .tool-row, .tool-row.sort-row, .editor-filter, .page-head, .compare-grid, .dialog-grid, .catalog-layout, .media-upload-form, .category-create, .prompt-test-layout { grid-template-columns:1fr; } .page-head { display:grid; } .form-grid { grid-template-columns:1fr; } .detail-grid { grid-template-columns:1fr; } .editor-card { grid-template-columns:1fr; } .editor-actions { grid-template-columns:repeat(3, minmax(0,1fr)); } .context-pane { border-right:0; border-bottom:1px solid var(--line); max-height:none; } .edit-pane { max-height:none; } .side-panel, .filter-sidebar, .prompt-test-side { position:static; max-height:none; } .list-toolbar { display:grid; align-items:start; } .compact-controls { justify-content:start; } main { padding:14px; } }
|
||||
@media (max-width: 900px) { header { padding:8px 12px; align-items:flex-start; } .filter-grid, .sources-filter, .source-add-grid, .hint-grid, .advanced-grid, .tool-row, .tool-row.sort-row, .editor-filter, .page-head, .compare-grid, .dialog-grid, .catalog-layout, .media-upload-form, .category-create, .prompt-test-layout, .prompt-test-controls { grid-template-columns:1fr; } .page-head { display:grid; } .form-grid { grid-template-columns:1fr; } .detail-grid { grid-template-columns:1fr; } .editor-card { grid-template-columns:1fr; } .editor-actions { grid-template-columns:repeat(3, minmax(0,1fr)); } .context-pane { border-right:0; border-bottom:1px solid var(--line); max-height:none; } .edit-pane { max-height:none; } .side-panel, .filter-sidebar, .prompt-test-side { position:static; max-height:none; } .list-toolbar { display:grid; align-items:start; } .compact-controls { justify-content:start; } main { padding:14px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -33,7 +33,15 @@
|
||||
<form class="prompt-test-layout" method="post" action="/prompt-test">
|
||||
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
|
||||
<section class="panel prompt-test-main">
|
||||
<div class="form-grid">
|
||||
<div class="prompt-test-controls">
|
||||
<label>
|
||||
<span>Фильтр</span>
|
||||
<select name="q_status" data-status-select>
|
||||
{% for item in status_options %}
|
||||
<option value="{{ item.value }}" {% if q_status == item.value %}selected{% endif %}>{{ item.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Пост для теста</span>
|
||||
<select name="post_id" data-post-select>
|
||||
@@ -85,6 +93,14 @@
|
||||
<dt>Токены</dt><dd>{{ result.usage.total_tokens or "—" }} total, {{ result.usage.prompt_tokens or "—" }} prompt, {{ result.usage.completion_tokens or "—" }} completion</dd>
|
||||
<dt>Стоимость</dt><dd>{{ result.usage.estimated_cost_usd or "—" }}</dd>
|
||||
</dl>
|
||||
{% if result.preview_text %}
|
||||
<h3>Как будет выглядеть</h3>
|
||||
<pre class="human-answer">{{ result.preview_text }}</pre>
|
||||
<dl class="kv compact-kv">
|
||||
<dt>Категория</dt><dd>{{ result.preview_category or "—" }}</dd>
|
||||
<dt>Заметка</dt><dd>{{ result.preview_notes or "—" }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if result.parsed_json %}
|
||||
<h3>JSON разобран</h3>
|
||||
<pre class="json-box">{{ result.parsed_json }}</pre>
|
||||
@@ -98,9 +114,14 @@
|
||||
</aside>
|
||||
</form>
|
||||
<script>
|
||||
const statusSelect = document.querySelector("[data-status-select]");
|
||||
document.querySelector("[data-post-select]")?.addEventListener("change", (event) => {
|
||||
const id = event.target.value;
|
||||
if (id) window.location.href = `/prompt-test?post_id=${encodeURIComponent(id)}`;
|
||||
const status = statusSelect?.value || "all";
|
||||
if (id) window.location.href = `/prompt-test?post_id=${encodeURIComponent(id)}&q_status=${encodeURIComponent(status)}`;
|
||||
});
|
||||
statusSelect?.addEventListener("change", (event) => {
|
||||
window.location.href = `/prompt-test?q_status=${encodeURIComponent(event.target.value)}`;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user