feat: surface retrieved evidence when agent abstains
This commit is contained in:
@@ -167,6 +167,7 @@ python scripts/eval_knowledge_retrieval.py --dataset evals/knowledge_retrieval_p
|
||||
- الوكيل يطابق السؤال مع ملفات النص/الكود المدعومة، ويمكنه قراءة PDF رقمي يحدده المستخدم؛ يمرر مقتطفات محدودة للنموذج، وعند ذكر مسار نسبي مثل `app/main.py` يزيد المقتطف إلى 12,000 حرف كحد أقصى. يعيد أسماء الملفات التي قرأها. الملفات المخفية، مجلدات `.git` وبيئات البناء، والملفات الكبيرة أو خارج جذر مساحة العمل مستثناة.
|
||||
- في وضع الوكيل اختر المهارة من أيقونة الدماغ بجانب زر مساحة العمل: **شرح الكود** أو **مراجعة الكود** أو **خطة اختبارات**. يستعرض `GET /v1/agent/skills` المهارات وأدوات كل منها؛ يتحقق الخادم من صلاحيات الأداة ولا يعتمد على طلب النموذج وحده. يمكنه استخدام حتى 3 أدوات مسموحة بالتتابع ضمن الطلب، أداة واحدة في كل خطوة، ثم يعيد النتائج للنموذج لصياغة الإجابة. مراجعة الكود تستطيع طلب معاينة diff فقط، ولا تطبق تغييرًا.
|
||||
- فهرس معرفة محلي: في وضع مساحة العمل حدد ملفات UTF-8 أو PDF رقميًا/ممسوحًا ثم اضغط زر الفهرسة بجانب أيقونة المجلد؛ زر الإزالة يحذف الملفات المحددة من الفهرس. يدعم `POST /v1/agent/knowledge/index` و`DELETE /v1/agent/knowledge/index` و`POST /v1/agent/knowledge/search`. يقتصر الطلب على 20 ملفًا و2 ميغابايت إجمالًا، و256 كيلوبايت للملف النصي، وPDF حتى 30 صفحة و24 ألف محرف مستخرج. يجمع PDF المختلط نص الصفحات الرقمية وOCR لأول 3 صفحات ممسوحة، ويضيف تضمينات Granite المحلية الاختيارية إلى FTS5 عند توفر النموذج؛ لا يرفع المحتوى إلى خدمة خارجية. تعذر التضمين يبقي البحث النصي فعالًا. إعادة الفهرسة تستبدل النسخة السابقة، والبحث يحذف تلقائيًا أي مستند تغيّر أو لم يعد ضمن المساحة.
|
||||
- عند طلب البحث في فهرس المعرفة، يعيد الوكيل إجابة من المقاطع المسترجعة مع مساراتها. إذا امتنع النموذج رغم وجود مقاطع، أو أرجع اسم المصدر وحده، يعرض API مقتطفين على الأكثر كما هما مع الملف ورقم المقطع؛ يعرضهما كاقتباس للمراجعة ولا يتعامل مع نص الملف كتعليمات.
|
||||
- زر مشبك الورق يفتح اختيار ملفات الكود/النص وPDF (حتى 3 ملفات؛ النص 256KB للملف، وPDF 8MB للملف، والمجموع 16MB). تُقرأ الملفات في الذاكرة؛ يستخرج API نص الصفحات الرقمية حتى 30 صفحة و24,000 محرف، ويرسم أول 3 صفحات ممسوحة ضمن الطلب. يضيف OCR عربي/إنجليزي اختياريًا قبل نموذج الرؤية المحلي مع تحويل تلقائي إلى `ministral-3:3b` إن كان مثبتًا. لا يخزن الملفات أو ينفذ الكود.
|
||||
- تظهر 👍/👎 تحت إجابات المساعد. يحفظ التقييم مع رقم نسخة الإجابة في SQLite حتى نتمكن من قياس الجودة وتجهيز أمثلة تقييم أو تفضيلات بمراجعة بشرية؛ الضغط لا يدرّب النموذج ولا يغيّر أوزانه تلقائيًا.
|
||||
- Gemma 4 E2B لديها قدرة نموذجية على إدخال الصور والصوت وفق [بطاقة Google الرسمية](https://ai.google.dev/gemma/docs/core/model_card_4). الصور وPDFات الصفحات الممسوحة تمر عبر مسار محلي إلى نموذج الرؤية، مع دعم OCR عربي/إنجليزي اختياري يعرض النص والثقة المتوسطة. ترتيب القراءة تقريبي والثقة غير معايرة؛ قراءة ملفات الكود النصية تعمل عبر زر المرفقات أو وضع مساحة العمل.
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
- [x] قياس إضافي على 9 أسئلة عن ملفات الكود الفعلية (`workspace.py`, `knowledge.py`, `pdf_documents.py`, `main.py`). كشف التقييم أن تكرار مقاطع الملف الواحد يزاحم مصادر أخرى؛ حدّثنا البحث لجلب مجموعة أوسع ثم توزيع حتى مقطعين لكل ملف. بعد التعديل: Hit@1=0.667، Hit@3=1.0، MRR=0.833، وظهور الدليل=1.0. النتائج في `retrieval_2026-10-02_141132.json` (قبل التعديل) و`retrieval_2026-10-02_141337.json` (بعده). العينة صغيرة واختبارها معجمي؛ الدقة الدلالية وجودة الإجابة لم تُقاسا.
|
||||
- [x] جولة تحسين استرجاع المقاطع العميقة على 11 سؤالًا من المجموعة المحلية (2026-10-03): توسيع محدود لمرادفات صلاحيات المهارات بالعربية والإنجليزية، ورفع سقف النتائج من 5 إلى 8 مع إبقاء حد 4 مقاطع لكل ملف لحماية تنوع المصادر. على API محلي تجريبي بقاعدة معزولة: `Hit@1=1.0`, `Hit@3=1.0`, `MRR=1.0`, وظهور الدليل `11/11`؛ تقرير `evals/results/retrieval_2026-10-03_160000.json`. اجتازت اختبارات قاعدة المعرفة `12/12`.
|
||||
- [x] توسيع الحزمة وإضافة جواب الوكيل من الأدلة المفهرسة (2026-10-03): أضيف دليل تقني بطول 15.7 كيلوبايت فيه المعلومة المستهدفة في المقطع 17 وPDF نصي؛ حفظ المشغّل اختياريًا إجابات الوكيل ومصادرها للمراجعة دون منح درجة آلية. الجولة الأولى على 13 حالة أعادت `Hit@1=1.0`, `Hit@3=1.0`, `MRR=1.0`, وظهور الدليل `13/13` (`evals/results/retrieval_extended_2026-10-03.json`). بعد إصلاح وزن OCR المحلي المعطوب وإعادة تشغيله دون تنزيل، أضيف PDF ممسوح فعلي إلى المجموعة؛ على API التطبيق الحالي بقاعدة SQLite الحية المؤقتة ثم حذف عناصرها بعد القياس: `Hit@1=1.0`, `Hit@3=1.0`, `MRR=1.0`, وظهور الدليل `14/14` (`evals/results/retrieval_extended_14_2026-10-03.json`). جواب الوكيل على PDF الممسوح استخرج الموعد `17 October 2026` وذكر مصدره (`evals/results/retrieval_scanned_pdf_agent_2026-10-03.json`). تجربة EasyOCR المباشرة والـAPI استخدمتا أوزانًا محلية فقط؛ النسخة التالفة محفوظة احتياطيًا بجانب الوزن المُصلح.
|
||||
- [ ] متابعة التقييم على ملفات طويلة وPDFات متنوعة وأسئلة جديدة، وجمع مراجعة مستقلة لجودة إجابات النموذج فوق السياق المسترجع. النتائج الحالية fixtures صغيرة ومصطنعة؛ الدرجات تصف هذه الحالات ولا تثبت جودة عامة. في 2026-10-04 جُمعت 14 إجابة محفوظة من المجموعة ذات 11 حالة ومجموعتي PDF الطويل/الممسوح في ورقة مراجعة `evals/HUMAN_REVIEW_2026-10.md` مع سلم موحد وخانات فارغة. أضيفت مقتطفات المرجع بجانب كل إجابة؛ نص PDF الممسوح استُخرج محليًا بـEasyOCR (ثقة 0.591) ووسم للمراجعة مقابل الصورة الأصلية. خانات درجات البشر الأربع عشرة ما زالت فارغة؛ المراجعة لا تكتمل حتى يراجعها شخص فعليًا.
|
||||
- [ ] متابعة التقييم على ملفات طويلة وPDFات متنوعة وأسئلة جديدة، وجمع مراجعة مستقلة لجودة إجابات النموذج فوق السياق المسترجع. النتائج الحالية fixtures صغيرة ومصطنعة؛ الدرجات تصف هذه الحالات ولا تثبت جودة عامة. في 2026-10-04 جُمعت 14 إجابة محفوظة من المجموعة ذات 11 حالة ومجموعتي PDF الطويل/الممسوح في ورقة مراجعة `evals/HUMAN_REVIEW_2026-10.md` مع سلم موحد وخانات فارغة. أضيفت مقتطفات المرجع بجانب كل إجابة؛ نص PDF الممسوح استُخرج محليًا بـEasyOCR (ثقة 0.591). مراجعة Codex الآلية في `evals/MODEL_REVIEW_2026-10.md` أعطت خط أساس 48/56 وكشفت امتناعين رغم وجود الدليل. أضيف مسار احتياطي يعرض مقتطفات موثقة حين يرفض النموذج أو يكتفي باسم المصدر؛ اختبارات مهارات الوكيل 21/21، وإعادة API المؤقتة على الحالتين أظهرت الإجابة المطلوبة 2/2 مع تنظيف مؤكد. لا يُعد هذا إعادة تقييم للمجموعة كاملة؛ مراجعة بشرية مستقلة ودرجات البشر الأربع عشرة ما زالت مطلوبة.
|
||||
- [x] مهارات محلية أولية قابلة للاختيار من واجهة وضع الوكيل: شرح الكود، مراجعة الكود، وخطة اختبارات. تعرض `/v1/agent/skills` وصف كل مهارة وأدواتها؛ تُحقن التعليمات الموثوقة في سياق الوكيل وتُفلتر قائمة الأدوات، ويرفض الخادم استدعاء أداة خارج صلاحيات المهارة. لمهارة مراجعة الكود معاينة فقط ولا تطبيق مباشر. اجتازت اختبارات الصلاحيات والواجهة؛ يبقى تقييم دقة كل مهارة على أمثلة أكثر قبل اعتمادها افتراضيًا.
|
||||
- [x] قراءة رابط عام محدد عبر `POST /v1/web/read`: استخراج نص HTML الثابت وتمريره إلى Gemma المحلية للإجابة مع إرجاع الرابط والعنوان.
|
||||
- [x] بحث ويب متعدد المصادر تجريبي عبر DuckDuckGo بلا مفتاح API: اختيار نطاقات مختلفة، محاولة جلب الصفحات العامة، تلخيص بالنموذج المحلي مع روابط المصادر، ووسم المقتطفات عند تعذر فتح الصفحة. (2026-10-01: استجابة حية أعادت ملخصًا ومصدرين مختلفين عبر FastAPI/Gemma.) أضيف زمن جلب إجمالي وزمن لكل مصدر إلى الاستجابة لمساعدة تشخيص المصادر البطيئة؛ اختبارات الحجب ومزوّد رسمي اختياري ما زالت لاحقًا.
|
||||
|
||||
@@ -721,6 +721,57 @@ def _knowledge_context_for_model(
|
||||
return context
|
||||
|
||||
|
||||
def _is_knowledge_answer_insufficient(answer: str) -> bool:
|
||||
"""Detect an abstention or citation-only reply despite prefetched evidence."""
|
||||
normalized = " ".join(answer.casefold().split())
|
||||
markers = (
|
||||
"لم يتم العثور على مقطع",
|
||||
"لم يتم العثور على مقاطع",
|
||||
"لم أجد",
|
||||
"لا يحتوي على معلومات مباشرة",
|
||||
"لا تحتوي على معلومات مباشرة",
|
||||
"no direct evidence",
|
||||
"no relevant passage",
|
||||
"could not find",
|
||||
"couldn't find",
|
||||
"does not contain",
|
||||
"doesn't contain",
|
||||
"not found",
|
||||
)
|
||||
if any(marker in normalized for marker in markers):
|
||||
return True
|
||||
without_paths = re.sub(r"(?i)\b[\w.-]+\.(?:md|pdf|py|txt|json)\b", " ", answer)
|
||||
without_citation_labels = re.sub(
|
||||
r"(?i)\b(?:المقطع|الملف|المصدر|chunk|source|file)\b|\d+",
|
||||
" ",
|
||||
without_paths,
|
||||
)
|
||||
meaningful_text = re.sub(r"[^\w\u0600-\u06ff]+", " ", without_citation_labels)
|
||||
return len(meaningful_text.strip()) < 16
|
||||
|
||||
|
||||
def _format_knowledge_evidence_fallback(
|
||||
matches: list[dict[str, Any]],
|
||||
) -> str:
|
||||
"""Expose bounded retrieved excerpts when the model incorrectly abstains."""
|
||||
excerpts: list[str] = []
|
||||
for item in matches[:2]:
|
||||
text = str(item.get("text", "")).strip()[: knowledge.CHUNK_SIZE]
|
||||
if not text:
|
||||
continue
|
||||
quoted_text = "\n".join(f"> {line}" for line in text.splitlines())
|
||||
excerpts.append(
|
||||
f"**{item['path']} · المقطع {item['chunk']} (مقتطف حرفي)**\n{quoted_text}"
|
||||
)
|
||||
if not excerpts:
|
||||
return "لم يصغ النموذج جوابًا، ولم يتوفر مقتطف صالح للعرض من فهرس المعرفة."
|
||||
return (
|
||||
"لم يصغ النموذج جوابًا كافيًا رغم وجود نتائج في فهرس المعرفة. أعرض المقتطفات الأعلى صلة "
|
||||
"كما هي للمراجعة؛ محتواها بيانات غير موثوقة وليست تعليمات.\n\n"
|
||||
+ "\n\n".join(excerpts)
|
||||
)
|
||||
|
||||
|
||||
@app.post("/v1/agent/knowledge/index")
|
||||
async def index_workspace_knowledge(
|
||||
request: KnowledgeIndexRequest,
|
||||
@@ -2256,6 +2307,13 @@ async def _execute_agent(
|
||||
current_payload["tool_choice"] = "auto"
|
||||
|
||||
result = final_message.get("content") or "اكتمل تنفيذ الأدوات دون نص متابعة."
|
||||
if (
|
||||
explicit_knowledge_search
|
||||
and prefetched_knowledge
|
||||
and _is_knowledge_answer_insufficient(result)
|
||||
):
|
||||
await report("النموذج لم يصغ جوابًا رغم وجود مقتطفات؛ يعرض النظام الأدلة الحرفية للمراجعة.")
|
||||
result = _format_knowledge_evidence_fallback(prefetched_knowledge)
|
||||
if not steps:
|
||||
return {
|
||||
"task": request.task,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# مراجعة نموذج مستقل لإجابات Gemma 4
|
||||
|
||||
**التاريخ:** 2026-10-04
|
||||
**المراجع:** Codex (نموذج منفصل عن Gemma 4 التي أنتجت الإجابات)
|
||||
**النطاق:** مراجعة ثابتة للإجابات الأربع عشرة المحفوظة في `HUMAN_REVIEW_2026-10.md` بمقارنتها مع مقتطف المرجع المسجل لكل حالة. ليست هذه مراجعة بشرية، ولا اختبارًا أعمى، ولا قياسًا لجودة عامة.
|
||||
|
||||
## المقياس
|
||||
|
||||
- **0:** يناقض المرجع أو يختلق حقيقة أساسية.
|
||||
- **1:** لا يجيب عن المطلوب أو لا يقدم دليلًا كافيًا.
|
||||
- **2:** إجابة جزئية مع نقص مهم.
|
||||
- **3:** صحيحة إجمالًا مع نقص أو التباس صغير.
|
||||
- **4:** إجابة مباشرة وصحيحة ومسنودة.
|
||||
|
||||
تعطي الدرجة وزنًا لصحة المعلومة واكتمال الإجابة. الامتناع الآمن أفضل من اختلاق معلومة، لكنه لا ينجح في سؤال الإجابة عندما يكون الدليل المطلوب متاحًا.
|
||||
|
||||
## النتائج
|
||||
|
||||
| الحالة | الدرجة | الملاحظة |
|
||||
|---|---:|---|
|
||||
| `conversation-storage-ar` | 4 | يطابق المرجع مباشرة ويذكر SQLite واستمرار المحادثات. |
|
||||
| `image-model-ar` | 4 | يحدد Ministral 3:3b المحلي ودوره كما في المصدر. |
|
||||
| `preview-safety-ar` | 4 | يذكر بصمة SHA256 وموافقة المستخدم قبل التطبيق. |
|
||||
| `pdf-extraction-ar` | 3 | المعلومة صحيحة ومسنودة؛ أضيف تنبيه عام عن حدود المقاطع لا يغير الإجابة لكنه زائد. |
|
||||
| `voice-transcription-ar` | 4 | يطابق مسار Groq وWhisper وعودة النص للواجهة. |
|
||||
| `workspace-boundary-ar` | 3 | يذكر حدود الجذر والاستثناءات؛ صيغة «يستطيع» أضعف قليلًا من تقرير سلوك المنع الفعلي. |
|
||||
| `web-search-en` | 4 | ينقل وصف DuckDuckGo والمصادر المتعددة بدقة. |
|
||||
| `database-version-en` | 1 | لم يجب عن الترحيل رغم أن `migration.md` هو الملف المتوقع ونتيجة البحث تضمنت أدلة. الامتناع تجنب الاختلاق لكنه أخفق في الإجابة. |
|
||||
| `model-routing-ar` | 1 | لم يذكر `requested_model` أو `model` أو `auto_routed` رغم توفر `routing.md` ضمن النتائج المسجلة. |
|
||||
| `api-timeout-ar` | 4 | يذكر HTTP 504 والرسالة العربية ومعرّف الطلب. |
|
||||
| `skill-permissions-en` | 4 | يصف `allowed_tools` وتصفية الأدوات ورفض الاستدعاءات غير المسموحة. |
|
||||
| `long-guide-deep-approval-en` | 4 | يغطي مراجعة diff، تحقق بصمة الملف، والتأكيد الصريح قبل الكتابة. |
|
||||
| `pdf-archive-page-limit-en` | 4 | يجيب عن حد 30 صفحة مع ذكر المصدر. |
|
||||
| `scanned-pdf-ocr-deadline-en` | 4 | يطابق التاريخ المقروء بصريًا من الصفحة الأصلية: 17 October 2026. |
|
||||
|
||||
**المجموع:** 48/56 (**85.7%**) على هذه الحالات فقط.
|
||||
**الإجابة غير المكتملة رغم توفر دليل مسجل:** حالتان من 14 (`database-version-en`, `model-routing-ar`).
|
||||
**التفسير:** النتيجة تشير إلى نقطتي فشل في توليد الإجابة فوق السياق المسترجع؛ لا تساوي دقة عامة أو معدل نجاح إنتاجي، والعينة صغيرة ومصطنعة. يلزم إصلاح الحالتين وإعادة تشغيل التقييم، ثم مراجعة بشرية مستقلة قبل اعتماد أي نتيجة.
|
||||
|
||||
## إعادة اختبار موجهة عبر API الحي
|
||||
|
||||
في 2026-10-04 أُعيد تشغيل الحالتين السابقتين بعد إصلاح مشغّل التقييم كي يسجل مجلد fixtures المؤقت في API قبل الفهرسة ويلغي التسجيل بعد حذف عناصره. التقرير `evals/results/retrieval_targeted_agent_2026-10-04.json` يثبت أن التسجيل والفهرسة والتنظيف نجحت، وأن ترتيب المرجع والدليل بقي 1/1 في الحالتين.
|
||||
|
||||
- `model-routing-ar`: أجاب Gemma هذه المرة بمقتطف `requested_model` و`model` و`auto_routed` من `routing.md`. نتيجة الإعادة 4/4 نوعيًا، لكن الحالة تبقى متغيرة بين التشغيلات لأن الإجابة الأساسية في التقرير الأقدم كانت امتناعًا.
|
||||
- `database-version-en`: كررت Gemma الامتناع عن الإجابة رغم أن البحث أعاد `migration.md` أولًا وسجل وجود `selected_version`. بقيت هذه حالة فشل مؤكدة تحتاج تحسين توجيه الإجابة فوق السياق.
|
||||
|
||||
لا ندمج درجتي الإعادة مع مجموع 48/56 كي لا نخفي اختلاف التشغيل. يلزم اختبار تكراري مستقل ثم مراجعة بشرية للحسم.
|
||||
|
||||
## إعادة بعد إصلاح الوكيل
|
||||
|
||||
أضيف في 2026-10-04 مسار احتياطي يعرض مقتطفات المعرفة عندما تكون إجابة النموذج امتناعًا أو إحالة إلى المصدر بلا محتوى. اجتاز اختبار الوكيل الكامل 21/21، ثم أعاد API مؤقت مع Gemma 4 تشغيل الحالتين: `database-version-en` عرض نص `migration.md` الحرفي مع المصدر، و`model-routing-ar` أجاب من `routing.md`. بقي ترتيب المرجع والدليل 1/1 في الحالتين. شُغّل API على منفذ وقاعدة مؤقتين، وأكد المشغّل حذف مستندات الفهرس وإلغاء تسجيل مساحة العمل قبل إيقاف الخدمة. النتيجة تثبت أن المحتوى المطلوب أصبح ظاهرًا في الحالتين، لكنها لا تحل محل إعادة تقييم المجموعة كاملة أو مراجعة بشرية.
|
||||
|
||||
## المصادر
|
||||
|
||||
- مجموعة النتائج المحفوظة: `evals/results/retrieval_project_agent_fresh_api_2026-10-03.json`، `evals/results/retrieval_long_pdf_agent_2026-10-03.json`، و`evals/results/retrieval_scanned_pdf_agent_2026-10-03.json`.
|
||||
- ورقة الحالات والأدلة والإجابات: `evals/HUMAN_REVIEW_2026-10.md`.
|
||||
- إعادة الاختبار الموجهة: `evals/results/retrieval_targeted_agent_2026-10-04.json`.
|
||||
- إعادة الاختبار بعد إصلاح الوكيل: `evals/results/retrieval_targeted_agent_fixed_2026-10-04.json`.
|
||||
- راجعت بصريًا الصفحة الوحيدة من `evals/fixtures/scanned_approval_notice.pdf`؛ الموعد ظاهر بوضوح كما ورد في OCR.
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"created_at_utc": "2026-10-04T07:32:06.053863+00:00",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"dataset": "evals\\knowledge_retrieval.jsonl",
|
||||
"indexing": {
|
||||
"semantic_indexed_documents": 2,
|
||||
"documents": 2,
|
||||
"embedding_models": [
|
||||
"granite-embedding:278m"
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"cases": 2,
|
||||
"hit_at_1": 1.0,
|
||||
"hit_at_3": 1.0,
|
||||
"mean_reciprocal_rank": 1.0,
|
||||
"evidence_rate": 1.0
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "database-version-en",
|
||||
"query": "How are old conversation answers migrated?",
|
||||
"expected_document": "migration.md",
|
||||
"expected_evidence": "selected_version",
|
||||
"rank": 1,
|
||||
"top_paths": [
|
||||
"migration.md",
|
||||
"routing.md"
|
||||
],
|
||||
"expected_document_chunks": [
|
||||
0
|
||||
],
|
||||
"evidence_found": true,
|
||||
"search_mode": "hybrid",
|
||||
"agent_answer": "لم يتم العثور على مقاطع تجيب مباشرة على سؤال \"How are old conversation answers migrated؟\" في فهرس المعرفة المحلي.",
|
||||
"agent_tool": "search_knowledge",
|
||||
"agent_files": [
|
||||
"migration.md",
|
||||
"routing.md"
|
||||
],
|
||||
"agent_steps": [
|
||||
{
|
||||
"tool": "search_knowledge",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"human_rating": null
|
||||
},
|
||||
{
|
||||
"id": "model-routing-ar",
|
||||
"query": "ما الحقول التي يعيدها API بعد تحويل الصور: requested_model وauto_routed؟",
|
||||
"expected_document": "routing.md",
|
||||
"expected_evidence": "auto_routed",
|
||||
"rank": 1,
|
||||
"top_paths": [
|
||||
"routing.md",
|
||||
"migration.md"
|
||||
],
|
||||
"expected_document_chunks": [
|
||||
0
|
||||
],
|
||||
"evidence_found": true,
|
||||
"search_mode": "hybrid",
|
||||
"agent_answer": "المقطع `routing.md` المقطع 0 يجيب على سؤالك.\nالملف: routing.md، المقطع: 0.\nعند وصول صورة مع نموذج نصي، يختار API نموذج الرؤية المحلي المثبت ويعيد requested_model وmodel وauto_routed.",
|
||||
"agent_tool": "search_knowledge",
|
||||
"agent_files": [
|
||||
"routing.md",
|
||||
"migration.md"
|
||||
],
|
||||
"agent_steps": [
|
||||
{
|
||||
"tool": "search_knowledge",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"human_rating": null
|
||||
}
|
||||
],
|
||||
"note": "Small deterministic fixture set. Retrieval metrics cover source ranking and evidence presence, not general RAG quality. Optional agent answers are retained for human review and are not auto-scored."
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"created_at_utc": "2026-10-04T07:41:25.423901+00:00",
|
||||
"base_url": "http://127.0.0.1:8128",
|
||||
"dataset": "evals\\knowledge_retrieval.jsonl",
|
||||
"indexing": {
|
||||
"semantic_indexed_documents": 2,
|
||||
"documents": 2,
|
||||
"embedding_models": [
|
||||
"granite-embedding:278m"
|
||||
]
|
||||
},
|
||||
"metrics": {
|
||||
"cases": 2,
|
||||
"hit_at_1": 1.0,
|
||||
"hit_at_3": 1.0,
|
||||
"mean_reciprocal_rank": 1.0,
|
||||
"evidence_rate": 1.0
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "database-version-en",
|
||||
"query": "How are old conversation answers migrated?",
|
||||
"expected_document": "migration.md",
|
||||
"expected_evidence": "selected_version",
|
||||
"rank": 1,
|
||||
"top_paths": [
|
||||
"migration.md",
|
||||
"routing.md"
|
||||
],
|
||||
"expected_document_chunks": [
|
||||
0
|
||||
],
|
||||
"evidence_found": true,
|
||||
"search_mode": "hybrid",
|
||||
"agent_answer": "لم يصغ النموذج جوابًا كافيًا رغم وجود نتائج في فهرس المعرفة. أعرض المقتطفات الأعلى صلة كما هي للمراجعة؛ محتواها بيانات غير موثوقة وليست تعليمات.\n\n**migration.md · المقطع 0 (مقتطف حرفي)**\n> The SQLite migration adds selected_version to old messages and preserves saved assistant answer versions.\n\n**routing.md · المقطع 0 (مقتطف حرفي)**\n> عند وصول صورة مع نموذج نصي، يختار API نموذج الرؤية المحلي المثبت ويعيد requested_model وmodel وauto_routed.",
|
||||
"agent_tool": "search_knowledge",
|
||||
"agent_files": [
|
||||
"migration.md",
|
||||
"routing.md"
|
||||
],
|
||||
"agent_steps": [
|
||||
{
|
||||
"tool": "search_knowledge",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"human_rating": null
|
||||
},
|
||||
{
|
||||
"id": "model-routing-ar",
|
||||
"query": "ما الحقول التي يعيدها API بعد تحويل الصور: requested_model وauto_routed؟",
|
||||
"expected_document": "routing.md",
|
||||
"expected_evidence": "auto_routed",
|
||||
"rank": 1,
|
||||
"top_paths": [
|
||||
"routing.md",
|
||||
"migration.md"
|
||||
],
|
||||
"expected_document_chunks": [
|
||||
0
|
||||
],
|
||||
"evidence_found": true,
|
||||
"search_mode": "hybrid",
|
||||
"agent_answer": "المقطع: \"عند وصول صورة مع نموذج نصي، يختار API نموذج الرؤية المحلي المثبت ويعيد requested_model وmodel وauto_routed.\" من routing.md المقطع 0.",
|
||||
"agent_tool": "search_knowledge",
|
||||
"agent_files": [
|
||||
"routing.md",
|
||||
"migration.md"
|
||||
],
|
||||
"agent_steps": [
|
||||
{
|
||||
"tool": "search_knowledge",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"human_rating": null
|
||||
}
|
||||
],
|
||||
"note": "Small deterministic fixture set. Retrieval metrics cover source ranking and evidence presence, not general RAG quality. Optional agent answers are retained for human review and are not auto-scored."
|
||||
}
|
||||
@@ -93,6 +93,7 @@ def main() -> int:
|
||||
results: list[dict] = []
|
||||
index_summary: dict[str, object] = {}
|
||||
files = sorted({case["document"] for case in cases})
|
||||
projects_url = args.base_url.rstrip("/") + "/v1/agent/projects"
|
||||
index_url = args.base_url.rstrip("/") + "/v1/agent/knowledge/index"
|
||||
search_url = args.base_url.rstrip("/") + "/v1/agent/knowledge/search"
|
||||
base_url = args.base_url.rstrip("/")
|
||||
@@ -121,7 +122,21 @@ def main() -> int:
|
||||
destination.write_text(case["text"], encoding="utf-8")
|
||||
index_payload = {"workspace_path": str(workspace_path), "files": files}
|
||||
index_attempted = False
|
||||
project_registration_attempted = False
|
||||
project_registered = False
|
||||
try:
|
||||
# A temporary directory is outside the default workspace grant. Register
|
||||
# it explicitly through the same local-user API used by the desktop app.
|
||||
project_registration_attempted = True
|
||||
registration = post_json(
|
||||
projects_url,
|
||||
{"workspace_path": str(workspace_path)},
|
||||
timeout=args.request_timeout,
|
||||
token=token,
|
||||
)
|
||||
project_registered = True
|
||||
if Path(registration.get("path", "")).resolve() != workspace_path.resolve():
|
||||
raise RuntimeError("The API registered a different fixture workspace.")
|
||||
index_attempted = True
|
||||
index_result = post_json(
|
||||
index_url, index_payload, timeout=args.request_timeout, token=token
|
||||
@@ -195,8 +210,9 @@ def main() -> int:
|
||||
result["human_rating"] = None
|
||||
results.append(result)
|
||||
finally:
|
||||
original_error = sys.exc_info()[0] is not None
|
||||
cleanup_errors: list[Exception] = []
|
||||
if index_attempted:
|
||||
original_error = sys.exc_info()[0] is not None
|
||||
try:
|
||||
deletion = delete_json(
|
||||
index_url,
|
||||
@@ -209,10 +225,29 @@ def main() -> int:
|
||||
"The API did not confirm cleanup for every fixture document."
|
||||
)
|
||||
except Exception as cleanup_error:
|
||||
if original_error:
|
||||
cleanup_errors.append(cleanup_error)
|
||||
if project_registration_attempted:
|
||||
try:
|
||||
revocation = delete_json(
|
||||
projects_url,
|
||||
{"workspace_path": str(workspace_path)},
|
||||
timeout=args.request_timeout,
|
||||
token=token,
|
||||
)
|
||||
if project_registered and not revocation.get("removed"):
|
||||
raise RuntimeError(
|
||||
"The API did not confirm removal of the temporary project registration."
|
||||
)
|
||||
except Exception as cleanup_error:
|
||||
cleanup_errors.append(cleanup_error)
|
||||
if cleanup_errors:
|
||||
if original_error:
|
||||
for cleanup_error in cleanup_errors:
|
||||
print(f"Cleanup also failed: {cleanup_error}", file=sys.stderr)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"; ".join(str(error) for error in cleanup_errors)
|
||||
) from cleanup_errors[0]
|
||||
|
||||
logout()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.main import (
|
||||
AgentRequest,
|
||||
_execute_agent,
|
||||
_knowledge_context_for_model,
|
||||
_is_knowledge_answer_insufficient,
|
||||
app,
|
||||
safe_arithmetic,
|
||||
select_workspace_file_excerpt,
|
||||
@@ -247,6 +248,44 @@ class AgentSkillTests(unittest.TestCase):
|
||||
self.assertEqual(result["tool"], "search_knowledge")
|
||||
self.assertEqual(result["files"], ["ROADMAP.md"])
|
||||
|
||||
def test_knowledge_refusal_falls_back_to_bounded_cited_evidence(self) -> None:
|
||||
match = {
|
||||
"path": "migration.md",
|
||||
"chunk": 0,
|
||||
"text": "The SQLite migration adds selected_version to old messages and preserves saved assistant answer versions.",
|
||||
}
|
||||
refusal = "لم يتم العثور على مقاطع تجيب مباشرة على السؤال في فهرس المعرفة المحلي."
|
||||
with (
|
||||
patch(
|
||||
"app.main._search_local_knowledge",
|
||||
new=AsyncMock(return_value=([match], "keyword")),
|
||||
),
|
||||
patch(
|
||||
"app.main.get_completion",
|
||||
new=AsyncMock(return_value={"choices": [{"message": {"content": refusal}}]}),
|
||||
),
|
||||
):
|
||||
result = asyncio.run(
|
||||
_execute_agent(
|
||||
AgentRequest(
|
||||
task=(
|
||||
"ابحث في فهرس المعرفة المحلي عن المقاطع التي تجيب عن السؤال، "
|
||||
"ثم أجب استنادًا إلى المقاطع فقط. السؤال: "
|
||||
"How are old conversation answers migrated?"
|
||||
),
|
||||
workspace_path=self.workspace,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
citation_only = "المقطع `routing.md`, المقطع 0."
|
||||
self.assertTrue(_is_knowledge_answer_insufficient(refusal))
|
||||
self.assertTrue(_is_knowledge_answer_insufficient(citation_only))
|
||||
self.assertFalse(_is_knowledge_answer_insufficient("تضيف الهجرة selected_version."))
|
||||
self.assertIn("لم يصغ النموذج جوابًا كافيًا رغم وجود نتائج", result["result"])
|
||||
self.assertIn("migration.md · المقطع 0", result["result"])
|
||||
self.assertIn("> The SQLite migration adds selected_version", result["result"])
|
||||
|
||||
def test_test_plan_skill_only_advertises_workspace_search(self) -> None:
|
||||
completion = {"choices": [{"message": {"content": "ثلاث حالات اختبار مقترحة."}}]}
|
||||
with patch("app.main.get_completion", new=AsyncMock(return_value=completion)) as model:
|
||||
|
||||
Reference in New Issue
Block a user