8829a2e93a
KnowledgeBase extension for EspoCRM: UI on top of the shira-hermes KB of
Israeli National Insurance law, regulations, and circulars.
Three modes:
- חיפוש — Hybrid (pgvector + tsvector) + Voyage rerank-2.5, returns ranked
chunks with heading_path and citations.
- שאל את שירה — Full agent loop; Shira picks up search_insurance_kb as
needed and returns a summary with citations.
- עיון — Browse all active sources. Click a source:
- PDF source (ספר הליקויים, ספר המבחנים, circulars): renders the
original PDF inline via an iframe proxied through the
KnowledgeBasePdf EntryPoint, so the layout/columns/tables are
preserved and Ctrl+F works natively.
- TXT source (חוק הביטוח הלאומי scraped from Wikisource): falls back
to the hierarchical chunk list with RTL styling.
Architecture:
- Controller: KnowledgeBase.php — thin proxy to shira-hermes /kb/*.
- Service: KnowledgeBaseService.php — shared curl plumbing; derives the
shira-hermes base URL from the SmartAssistant integration record so
there is no second admin config.
- EntryPoint: KnowledgeBasePdf.php — streams the PDF inline, wraps the
body in a php://temp stream for setBody, applies a locked-down CSP.
- JS: views/kb/index.js branches on source.original_path; modes wired
through the SmartAssistant fa_IR i18n convention.
Auth model:
- Browser → EspoCRM: session cookie / X-Api-Key (EspoCRM's existing auth).
- EspoCRM → shira-hermes: X-Api-Key from the SmartAssistant integration
(never exposed to the browser).
- Portal users are blocked at both the Controller and the EntryPoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
3.1 KiB
JavaScript
70 lines
3.1 KiB
JavaScript
define('modules/knowledge-base/views/dashlets/kb-search', ['views/dashlets/abstract/base'], function (Dep) {
|
|
|
|
return Dep.extend({
|
|
|
|
name: 'KbSearch',
|
|
|
|
templateContent: `
|
|
<div dir="rtl" style="padding:8px;">
|
|
<div style="display:flex;gap:6px;">
|
|
<input type="text" class="form-control input-sm" data-name="query"
|
|
placeholder="חפש בבסיס הידע…" style="flex:1;" />
|
|
<button type="button" class="btn btn-primary btn-sm" data-action="submit">חפש</button>
|
|
</div>
|
|
<div class="kb-dashlet-results" style="margin-top:8px;max-height:300px;overflow-y:auto;"></div>
|
|
<div style="margin-top:6px;">
|
|
<a href="#KnowledgeBase" class="small">פתח בבסיס הידע המלא →</a>
|
|
</div>
|
|
</div>
|
|
`,
|
|
|
|
events: {
|
|
'click [data-action="submit"]': function () {
|
|
this.runSearch();
|
|
},
|
|
'keypress input[data-name="query"]': function (e) {
|
|
if (e.which === 13) this.runSearch();
|
|
},
|
|
},
|
|
|
|
runSearch: function () {
|
|
const query = (this.$el.find('input[data-name="query"]').val() || '').trim();
|
|
if (!query) return;
|
|
const $r = this.$el.find('.kb-dashlet-results');
|
|
$r.html('<div class="text-muted small">מחפש…</div>');
|
|
Espo.Ajax.postRequest('KnowledgeBase/action/search', {
|
|
query: query,
|
|
kind: 'any',
|
|
topK: 5,
|
|
}).then(res => this.renderHits(res.hits || []))
|
|
.catch(err => {
|
|
$r.html('<div class="alert alert-danger small">' + ((err && err.statusText) || 'Error') + '</div>');
|
|
});
|
|
},
|
|
|
|
renderHits: function (hits) {
|
|
const $r = this.$el.find('.kb-dashlet-results');
|
|
if (!hits.length) {
|
|
$r.html('<div class="text-muted small">לא נמצאו תוצאות.</div>');
|
|
return;
|
|
}
|
|
const esc = s => String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
const kindHe = {law: 'חוק', regulation: 'תקנה', circular: 'חוזר'};
|
|
const rows = hits.map(h => {
|
|
const kind = kindHe[h.kind] || h.kind;
|
|
const ident = h.identifier ? ' ' + esc(h.identifier) : '';
|
|
const ref = h.section_ref || h.heading_path || '';
|
|
const preview = esc((h.content || '').slice(0, 200)) + ((h.content || '').length > 200 ? '…' : '');
|
|
return `<div style="margin-bottom:8px;padding:6px;border-bottom:1px solid #eee;">
|
|
<div class="small"><strong>[${kind}${ident}]</strong> ${esc(h.title || '')}
|
|
${ref ? '<span class="text-muted"> — ' + esc(ref) + '</span>' : ''}</div>
|
|
<div class="small text-muted" style="white-space:pre-wrap;margin-top:3px;">${preview}</div>
|
|
</div>`;
|
|
}).join('');
|
|
$r.html(rows);
|
|
},
|
|
|
|
});
|
|
|
|
});
|