feat(blog): جدول — ستون‌ها قابل تغییر اندازه/افزودن و حذف؛ لینک کپشن تصویر؛ هدینگ‌های نسخه انگلیسی بزرگ‌تر

This commit is contained in:
aramesh-site deploy
2026-10-06 10:57:01 +00:00
parent 61a2e95278
commit 1b491357ab
45 changed files with 2341 additions and 81 deletions
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""اجرای مجموعه طلایی روی سرویس چت‌بات و ساخت گزارش مارک‌داون.
استفاده (داخل کانتینر یا هر جا که به سرویس دسترسی هست):
python eval_run.py --base http://localhost:8000
python eval_run.py --out eval-report.md
هر پرسش از /chat/eval سرویس می‌شود؛ پاسخ با زیررشته‌های انتظار
(پس از نرمال‌سازی فارسی) مقایسه و رتبه‌بندی می‌شود:
pass — همه/یکی از انتظارها برآورده شد
miss — انتظار برآورده نشد (پاسخ احتمالاً نادرست یا بی‌ربط)
manual — انتظار عددی تعریف نشده؛ بازبینی دستی
fallback-expected / unexpected-fallback — پیام جایگزین
"""
import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
FALLBACK_MESSAGE = (
"متأسفانه اطلاعات دقیقی در این باره در سایت ثبت نشده است. "
"برای راهنمایی بیشتر می‌توانید از صفحه «تماس با ما» با ما در ارتباط باشید."
)
def ask(base: str, question: str, timeout: int = 180) -> dict:
url = f"{base}/chat/eval?{urllib.parse.urlencode({'message': question})}"
with urllib.request.urlopen(url, timeout=timeout) as r:
return json.load(r)
def main() -> int:
ap = argparse.ArgumentParser(description="اجرای مجموعه طلایی ارزیابی چت‌بات")
ap.add_argument("--base", default="http://localhost:8000", help="نشانی سرویس چت‌بات")
ap.add_argument("--set", dest="eval_set",
default=str(Path(__file__).with_name("eval_set.json")))
ap.add_argument("--out", default="-", help="فایل خروجی گزارش (- = stdout)")
ap.add_argument("--sleep", type=float, default=0.5, help="مکث بین پرسش‌ها (ثانیه)")
args = ap.parse_args()
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from chatbot.persian import normalize
data = json.loads(Path(args.eval_set).read_text(encoding="utf-8"))
rows: list[dict] = []
for item in data["questions"]:
q = item["q"]
t0 = time.time()
try:
res = ask(args.base, q)
except Exception as e:
rows.append({**item, "answer": f"ERROR: {e}", "sources": [], "seconds": 0.0,
"status": "error", "fallback": False})
print(f" error: {q[:50]}", file=sys.stderr)
continue
seconds = time.time() - t0
answer = str(res.get("answer", ""))
contexts = res.get("contexts") or []
is_fallback = answer.strip() == FALLBACK_MESSAGE or not contexts
if item.get("expect_fallback"):
status = "fallback-expected" if is_fallback else "unexpected-answer"
elif is_fallback:
status = "unexpected-fallback"
else:
needles = [normalize(s) for s in item.get("expect_any", [])]
if not needles:
status = "manual"
elif any(n in normalize(answer) for n in needles):
status = "pass"
else:
status = "miss"
rows.append({**item, "answer": answer, "sources": contexts, "seconds": seconds,
"status": status, "fallback": is_fallback})
print(f" [{status}] {q[:50]}", file=sys.stderr)
time.sleep(args.sleep)
# --- گزارش ---
lines = [f"# گزارش ارزیابی چت‌بات — {time.strftime('%Y-%m-%d %H:%M')}", ""]
counts: dict[str, int] = {}
for r in rows:
counts[r["status"]] = counts.get(r["status"], 0) + 1
graded = [r for r in rows if r["status"] in ("pass", "miss", "unexpected-fallback", "unexpected-answer")]
ok = sum(1 for r in rows if r["status"] in ("pass", "fallback-expected"))
lines += [
f"- مجموع پرسش‌ها: {len(rows)} — درست: {ok} — خطا: {counts.get('miss', 0) + counts.get('unexpected-fallback', 0) + counts.get('unexpected-answer', 0)}",
f"- بازبینی دستی: {counts.get('manual', 0)} — خطای فنی: {counts.get('error', 0)}",
f"- میانگین زمان پاسخ: {sum(r['seconds'] for r in rows) / max(len(rows), 1):.1f}s",
"",
]
for r in rows:
srcs = [c.rsplit("(منبع:", 1)[-1].strip(" ()\n") for c in r["sources"]] if r["sources"] else []
lines += [
f"## [{r['status']}] {r['q']}",
f"*موضوع: {r.get('topic', '?')} — {r['seconds']:.1f}s — منابع: {'، '.join(srcs) or '—'}*",
"",
r["answer"],
"",
]
report = "\n".join(lines)
if args.out == "-":
print(report)
else:
Path(args.out).write_text(report, encoding="utf-8")
print(f"Report written to {args.out}", file=sys.stderr)
return 1 if any(r["status"] in ("miss", "unexpected-answer") for r in rows) else 0
if __name__ == "__main__":
sys.exit(main())