feat: re-process re-classifies AI metadata via review screen
Old behavior: clicking "עיבוד מחדש" on a source quietly re-chunked + re-embedded the file but kept the existing metadata (title, identifier, summary, labels). Sources whose original AI classification produced weak metadata had no path to recover short of deleting and re-uploading. New behavior: re-process now opens the same batch review screen as a fresh upload — AI suggests fresh metadata (kind, title, identifier, summary, labels, dates), the user reviews/edits, and committing overwrites kb_source + replaces chunks. The card is tagged "עיבוד מחדש" with a 🔄 icon so the user knows what they're committing. Form precedence on the review card: user edits > AI suggestion > existing source metadata. Existing labels are pre-filled in metadata_json and surface as fallback if the AI classifier times out — so the form is never blank, and existing labels are never silently dropped just because the AI didn't re-suggest them. Backend: shira-hermes 4ad569d. Refs Task Master #22 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -833,18 +833,15 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
const src = this._findSourceById(id);
|
||||
if (!src) return;
|
||||
const titleShort = (src.title || '').slice(0, 60);
|
||||
if (!confirm(`לעבד מחדש את "${titleShort}"?\n\nהקטעים הקיימים יימחקו והקובץ יעובר parse + chunk + embed מחדש.\nהמטא-דאטה (כותרת, מזהה, תאריכים) נשמרים.`)) return;
|
||||
if (!confirm(`לעבד מחדש את "${titleShort}"?\n\nהקובץ ינותח מחדש על ידי AI; תקבל הצעות מטא-דאטה (כותרת, סיכום, תוויות) לעריכה במסך הסקירה.\nהקטעים יוחלפו רק לאחר שתאשר את ההצעות.`)) return;
|
||||
const self = this;
|
||||
Espo.Ajax.postRequest('KnowledgeBase/action/reingestSource', {id})
|
||||
.then(res => {
|
||||
if (res && res.job_id) {
|
||||
// Surface in the upload-status banner + jobs list, like an upload would.
|
||||
self._renderJobStatus({
|
||||
original_filename: src.title || ('source #' + id),
|
||||
status: res.status || 'queued',
|
||||
});
|
||||
self._startJobPolling(res.job_id);
|
||||
// Refresh sources after the polling reports done.
|
||||
if (res && res.batch_id) {
|
||||
// Open the same review UI as a fresh upload — the
|
||||
// backend created a single-file batch wrapping
|
||||
// this re-ingest job.
|
||||
self._openBatch(res.batch_id, src.topic_id || self.topicId);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -1003,6 +1000,18 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
if (typeof ai === 'string') {
|
||||
try { ai = JSON.parse(ai); } catch (e) { ai = {}; }
|
||||
}
|
||||
// For re-ingest jobs, metadata_json carries the
|
||||
// existing source metadata so the form can pre-fill
|
||||
// values (title/labels/etc.) before the AI classifier
|
||||
// returns. AI suggestions take precedence once they
|
||||
// arrive, but the fallback prevents an empty form
|
||||
// during the classify wait — and keeps existing
|
||||
// labels visible if the user wants to retain them.
|
||||
let md = it.metadata_json;
|
||||
if (typeof md === 'string') {
|
||||
try { md = JSON.parse(md); } catch (e) { md = {}; }
|
||||
}
|
||||
const isReingest = !!(md && md.reingest);
|
||||
let s = it.status;
|
||||
if (s === 'processing' && it.processing_stage === 'classifying') s = 'classifying';
|
||||
else if (s === 'processing' && it.processing_stage === 'embedding') s = 'committing';
|
||||
@@ -1011,6 +1020,9 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
filename: it.original_filename,
|
||||
status: s,
|
||||
ai: (ai && Object.keys(ai).length) ? ai : null,
|
||||
defaults: isReingest ? md : null,
|
||||
isReingest: isReingest,
|
||||
sourceId: it.source_id || null,
|
||||
edits: null,
|
||||
error: it.error_message || null,
|
||||
};
|
||||
@@ -1158,9 +1170,19 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
const html = _activeBatch.cards.map((c, idx) => {
|
||||
if (c.status === 'awaiting_review') readyCount++;
|
||||
const ai = c.ai || {};
|
||||
const defaults = c.defaults || {};
|
||||
const e = c.edits || {};
|
||||
const val = (k) => this.escape(e[k] != null ? e[k] : (ai[k] || ''));
|
||||
const labelsStr = e.labelsStr != null ? e.labelsStr : ((ai.labels || []).join(', '));
|
||||
// Precedence: user edits > AI suggestion > existing source
|
||||
// (only set for re-ingest cards). For fresh uploads `defaults`
|
||||
// is empty so this collapses to the previous behavior.
|
||||
const val = (k) => this.escape(
|
||||
e[k] != null ? e[k] :
|
||||
(ai[k] != null && ai[k] !== '' ? ai[k] :
|
||||
(defaults[k] || ''))
|
||||
);
|
||||
const aiLabels = (ai.labels || []);
|
||||
const fallbackLabels = aiLabels.length ? aiLabels : (defaults.labels || []);
|
||||
const labelsStr = e.labelsStr != null ? e.labelsStr : fallbackLabels.join(', ');
|
||||
let body = '';
|
||||
if (c.status === 'failed') {
|
||||
body = `<div class="text-danger small">${this.escape(c.error || 'שגיאה')}</div>`;
|
||||
@@ -1181,9 +1203,13 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
} else {
|
||||
body = '<div class="text-muted small">ממתין לניתוח…</div>';
|
||||
}
|
||||
const cardIcon = c.isReingest ? '🔄' : '📄';
|
||||
const reingestTag = c.isReingest
|
||||
? `<span class="label label-warning" style="font-size:10px;">עיבוד מחדש</span>`
|
||||
: '';
|
||||
return `<div class="kb-batch-card panel" data-card="${idx}" style="padding:10px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px;">
|
||||
<div style="font-weight:500;">📄 ${this.escape(c.filename)}</div>
|
||||
<div style="font-weight:500;">${cardIcon} ${this.escape(c.filename)} ${reingestTag}</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
${statusLabel(c.status)}
|
||||
${c.status !== 'done' && c.status !== 'committing' ? `<button class="btn btn-link btn-sm" data-action="discardCard" data-jobid="${c.jobId || 0}" title="הסר" style="color:#d9534f;"><span class="glyphicon glyphicon-remove"></span></button>` : ''}
|
||||
@@ -1294,8 +1320,16 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
if (!ready.length) return;
|
||||
for (const card of ready) {
|
||||
const ai = card.ai || {};
|
||||
const defaults = card.defaults || {};
|
||||
const e = card.edits || {};
|
||||
const labelsStr = e.labelsStr != null ? e.labelsStr : ((ai.labels || []).join(', '));
|
||||
// Same precedence as _renderBatchCards: edits > AI > defaults.
|
||||
const pick = (k) =>
|
||||
(e[k] != null ? e[k] :
|
||||
(ai[k] != null && ai[k] !== '' ? ai[k] :
|
||||
(defaults[k] || '')));
|
||||
const aiLabels = (ai.labels || []);
|
||||
const fallbackLabels = aiLabels.length ? aiLabels : (defaults.labels || []);
|
||||
const labelsStr = e.labelsStr != null ? e.labelsStr : fallbackLabels.join(', ');
|
||||
const newLabels = labelsStr
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
@@ -1303,12 +1337,12 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
const payload = {
|
||||
id: card.jobId,
|
||||
kind: e.kind || ai.kind || _activeBatch.kind,
|
||||
title: (e.title != null ? e.title : (ai.title || card.filename)).trim() || card.filename,
|
||||
identifier: (e.identifier != null ? e.identifier : (ai.identifier || '')).trim() || null,
|
||||
published_at: (e.published_at != null ? e.published_at : (ai.published_at || '')) || null,
|
||||
effective_at: (e.effective_at != null ? e.effective_at : (ai.effective_at || '')) || null,
|
||||
source_url: (e.source_url != null ? e.source_url : (ai.source_url || '')) || null,
|
||||
summary: (e.summary != null ? e.summary : (ai.summary || '')) || null,
|
||||
title: (pick('title') || card.filename).trim() || card.filename,
|
||||
identifier: (pick('identifier') || '').trim() || null,
|
||||
published_at: pick('published_at') || null,
|
||||
effective_at: pick('effective_at') || null,
|
||||
source_url: pick('source_url') || null,
|
||||
summary: pick('summary') || null,
|
||||
label_slugs: [],
|
||||
new_labels: newLabels,
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "KnowledgeBase",
|
||||
"module": "KnowledgeBase",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
"php": [
|
||||
">=8.1"
|
||||
],
|
||||
"releaseDate": "2026-04-26",
|
||||
"releaseDate": "2026-04-28",
|
||||
"author": "klear",
|
||||
"description": "Knowledge Base — Israeli National Insurance law, regulations, and circulars. Hybrid search + ask-shira, powered by shira-hermes KB.",
|
||||
"displayLabel": "מאגר ידע"
|
||||
|
||||
Reference in New Issue
Block a user