zerochat 7.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. zerochat-7.3.0.dist-info/METADATA +70 -0
  2. zerochat-7.3.0.dist-info/RECORD +130 -0
  3. zerochat-7.3.0.dist-info/WHEEL +5 -0
  4. zerochat-7.3.0.dist-info/entry_points.txt +2 -0
  5. zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
  6. zerochat-7.3.0.dist-info/top_level.txt +2 -0
  7. zerochat.py +2004 -0
  8. zerochat_runtime/__init__.py +1 -0
  9. zerochat_runtime/assets/css/base.css +199 -0
  10. zerochat_runtime/assets/css/components/composer.css +1258 -0
  11. zerochat_runtime/assets/css/components/debug.css +777 -0
  12. zerochat_runtime/assets/css/components/header.css +97 -0
  13. zerochat_runtime/assets/css/components/icons.css +39 -0
  14. zerochat_runtime/assets/css/components/markdown.css +685 -0
  15. zerochat_runtime/assets/css/components/messages.css +473 -0
  16. zerochat_runtime/assets/css/components/modals.css +1319 -0
  17. zerochat_runtime/assets/css/components/sidebar.css +622 -0
  18. zerochat_runtime/assets/css/components/tools.css +1223 -0
  19. zerochat_runtime/assets/css/layout.css +171 -0
  20. zerochat_runtime/assets/css/print.css +120 -0
  21. zerochat_runtime/assets/css/styles.css +18 -0
  22. zerochat_runtime/assets/css/theme-overrides.css +555 -0
  23. zerochat_runtime/assets/css/tokens.css +165 -0
  24. zerochat_runtime/assets/help/architecture.html +247 -0
  25. zerochat_runtime/assets/help/composer.html +134 -0
  26. zerochat_runtime/assets/help/conversations.html +105 -0
  27. zerochat_runtime/assets/help/debug.html +108 -0
  28. zerochat_runtime/assets/help/en/architecture.html +247 -0
  29. zerochat_runtime/assets/help/en/composer.html +134 -0
  30. zerochat_runtime/assets/help/en/conversations.html +104 -0
  31. zerochat_runtime/assets/help/en/debug.html +105 -0
  32. zerochat_runtime/assets/help/en/gemini-free.html +39 -0
  33. zerochat_runtime/assets/help/en/index.html +191 -0
  34. zerochat_runtime/assets/help/en/learning.html +71 -0
  35. zerochat_runtime/assets/help/en/mcp.html +143 -0
  36. zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
  37. zerochat_runtime/assets/help/en/profiles.html +173 -0
  38. zerochat_runtime/assets/help/en/rag.html +130 -0
  39. zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
  40. zerochat_runtime/assets/help/en/tools-agent.html +133 -0
  41. zerochat_runtime/assets/help/en/webllm.html +355 -0
  42. zerochat_runtime/assets/help/gemini-free.html +39 -0
  43. zerochat_runtime/assets/help/help.css +604 -0
  44. zerochat_runtime/assets/help/help.js +51 -0
  45. zerochat_runtime/assets/help/index.html +194 -0
  46. zerochat_runtime/assets/help/learning.html +71 -0
  47. zerochat_runtime/assets/help/mcp.html +143 -0
  48. zerochat_runtime/assets/help/openrouter-free.html +39 -0
  49. zerochat_runtime/assets/help/profiles.html +174 -0
  50. zerochat_runtime/assets/help/rag.html +130 -0
  51. zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
  52. zerochat_runtime/assets/help/tools-agent.html +133 -0
  53. zerochat_runtime/assets/help/webllm.html +359 -0
  54. zerochat_runtime/assets/js/agent-core.js +1553 -0
  55. zerochat_runtime/assets/js/api.js +844 -0
  56. zerochat_runtime/assets/js/app.js +2558 -0
  57. zerochat_runtime/assets/js/attachments.js +220 -0
  58. zerochat_runtime/assets/js/charts.js +338 -0
  59. zerochat_runtime/assets/js/chat-engine.js +566 -0
  60. zerochat_runtime/assets/js/config-store.js +207 -0
  61. zerochat_runtime/assets/js/context-manager.js +584 -0
  62. zerochat_runtime/assets/js/conversation-service.js +484 -0
  63. zerochat_runtime/assets/js/cookies.js +688 -0
  64. zerochat_runtime/assets/js/data-reset-service.js +136 -0
  65. zerochat_runtime/assets/js/debug.js +508 -0
  66. zerochat_runtime/assets/js/defaults.js +16 -0
  67. zerochat_runtime/assets/js/export.js +179 -0
  68. zerochat_runtime/assets/js/file-parser.js +2088 -0
  69. zerochat_runtime/assets/js/generation-controller.js +417 -0
  70. zerochat_runtime/assets/js/i18n.js +1483 -0
  71. zerochat_runtime/assets/js/icons.js +156 -0
  72. zerochat_runtime/assets/js/ingestionEngine.js +436 -0
  73. zerochat_runtime/assets/js/markdown.js +588 -0
  74. zerochat_runtime/assets/js/mcp.js +1271 -0
  75. zerochat_runtime/assets/js/message-turns.js +62 -0
  76. zerochat_runtime/assets/js/profile-backup.js +118 -0
  77. zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
  78. zerochat_runtime/assets/js/profile-repository.js +213 -0
  79. zerochat_runtime/assets/js/providers-webllm.js +486 -0
  80. zerochat_runtime/assets/js/providers.js +1560 -0
  81. zerochat_runtime/assets/js/rag-index.js +295 -0
  82. zerochat_runtime/assets/js/rag-service.js +530 -0
  83. zerochat_runtime/assets/js/rag-ui.js +997 -0
  84. zerochat_runtime/assets/js/ragStorage.js +676 -0
  85. zerochat_runtime/assets/js/sandbox.js +449 -0
  86. zerochat_runtime/assets/js/state.js +704 -0
  87. zerochat_runtime/assets/js/storage-db.js +144 -0
  88. zerochat_runtime/assets/js/tool-cards.js +330 -0
  89. zerochat_runtime/assets/js/tool-security.js +653 -0
  90. zerochat_runtime/assets/js/tools/README.md +50 -0
  91. zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
  92. zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
  93. zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
  94. zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
  95. zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
  96. zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
  97. zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
  98. zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
  99. zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
  100. zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
  101. zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
  102. zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
  103. zerochat_runtime/assets/js/ui-composer.js +319 -0
  104. zerochat_runtime/assets/js/ui-conversation.js +655 -0
  105. zerochat_runtime/assets/js/ui-dialogs.js +132 -0
  106. zerochat_runtime/assets/js/ui-generation-status.js +119 -0
  107. zerochat_runtime/assets/js/ui-inspector.js +804 -0
  108. zerochat_runtime/assets/js/ui-mcp.js +604 -0
  109. zerochat_runtime/assets/js/ui-profiles.js +824 -0
  110. zerochat_runtime/assets/js/ui-reasoning.js +146 -0
  111. zerochat_runtime/assets/js/ui-settings.js +825 -0
  112. zerochat_runtime/assets/js/ui-shell.js +215 -0
  113. zerochat_runtime/assets/js/ui-sidebar.js +429 -0
  114. zerochat_runtime/assets/js/ui-telemetry.js +404 -0
  115. zerochat_runtime/assets/js/ui-transfer.js +262 -0
  116. zerochat_runtime/assets/js/utils.js +216 -0
  117. zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
  118. zerochat_runtime/assets/js/web-browser.js +569 -0
  119. zerochat_runtime/assets/js/web-search.js +519 -0
  120. zerochat_runtime/assets/manifest.webmanifest +20 -0
  121. zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
  122. zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
  123. zerochat_runtime/assets/services/lsp/installer.json +9 -0
  124. zerochat_runtime/assets/services/lsp/service.json +22 -0
  125. zerochat_runtime/assets/services/memory/installer.json +9 -0
  126. zerochat_runtime/assets/services/memory/service.json +24 -0
  127. zerochat_runtime/assets/services/playwright/installer.json +10 -0
  128. zerochat_runtime/assets/services/playwright/service.json +39 -0
  129. zerochat_runtime/assets/sw.js +164 -0
  130. zerochat_runtime/assets/zerochat.html +671 -0
@@ -0,0 +1,530 @@
1
+ /** Connects the agent tools with IndexedDB storage and the Orama index. */
2
+ (function (root, factory) {
3
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
4
+ module.exports = factory(require('./ragStorage.js'), require('./rag-index.js'));
5
+ } else {
6
+ root.ChatRagService = factory(root.ChatRagStorage, root.ChatRagIndex);
7
+ }
8
+ })(typeof self !== 'undefined' ? self : this, function (RagStorage, RagIndex) {
9
+ 'use strict';
10
+
11
+ function getFileParser() {
12
+ if (typeof window !== 'undefined' && window.ChatFileParser) return window.ChatFileParser;
13
+ if (typeof require !== 'undefined') { try { return require('./file-parser.js'); } catch (_) {} }
14
+ return null;
15
+ }
16
+
17
+ function parseArguments(rawArgs) {
18
+ if (!rawArgs) return {};
19
+ if (typeof rawArgs === 'object') return rawArgs;
20
+ try { return JSON.parse(String(rawArgs)); } catch (_) { return { query: String(rawArgs) }; }
21
+ }
22
+
23
+ function normalizeBranchIds(input) {
24
+ if (!input) return [];
25
+ const list = Array.isArray(input) ? input : String(input).split(',');
26
+ return Array.from(new Set(list.map(id => String(id || '').trim()).filter(Boolean)));
27
+ }
28
+
29
+ const DOCUMENT_REFERENCE_STOPWORDS = new Set([
30
+ 'a', 'al', 'and', 'annual', 'archivo', 'de', 'del', 'document', 'documento',
31
+ 'el', 'en', 'file', 'for', 'form', 'in', 'informe', 'la', 'las', 'los',
32
+ 'of', 'para', 'por', 'report', 'the', 'un', 'una', 'y', '10k', '10q'
33
+ ]);
34
+ function normalizeDocumentReference(value) {
35
+ return String(value || '')
36
+ .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
37
+ .toLowerCase()
38
+ .replace(/\.(?:pdf|txt|md|csv|json)\b/g, ' ')
39
+ .replace(/[_\-]+/g, ' ')
40
+ .replace(/[^a-z0-9]+/g, ' ')
41
+ .replace(/\bfy\s*(\d{4})\b/g, '$1')
42
+ .replace(/\s+/g, ' ')
43
+ .trim();
44
+ }
45
+
46
+ function documentReferenceTokens(value) {
47
+ return Array.from(new Set(normalizeDocumentReference(value).split(' ')
48
+ .map(token => /^fy\d{4}$/.test(token) ? token.slice(2) : token)
49
+ .filter(token => token && !DOCUMENT_REFERENCE_STOPWORDS.has(token))));
50
+ }
51
+
52
+ function selectDocumentCandidate(documents, reference) {
53
+ const queryText = normalizeDocumentReference(reference);
54
+ const queryTokens = documentReferenceTokens(reference);
55
+ if (!queryText || queryTokens.length === 0 || documents.length === 0) {
56
+ return { selected: null, candidates: [], confident: false };
57
+ }
58
+
59
+ const prepared = documents.map(document => {
60
+ const normalizedTitle = normalizeDocumentReference(document.title);
61
+ return { ...document, normalizedTitle, titleTokens: new Set(documentReferenceTokens(document.title)) };
62
+ });
63
+ const frequencies = new Map();
64
+ for (const token of queryTokens) {
65
+ frequencies.set(token, prepared.filter(document => document.titleTokens.has(token)).length);
66
+ }
67
+ const rareThreshold = Math.max(1, Math.floor(prepared.length * 0.1));
68
+ const candidates = prepared.map(document => {
69
+ const matchedTerms = queryTokens.filter(token => document.titleTokens.has(token));
70
+ const distinctiveTerms = matchedTerms.filter(token => (frequencies.get(token) || 0) <= rareThreshold);
71
+ let score = matchedTerms.reduce((total, token) => {
72
+ const frequency = frequencies.get(token) || prepared.length;
73
+ let weight = 1 + Math.log2((prepared.length + 1) / (frequency + 1));
74
+ if (/^\d{4}$/.test(token)) weight *= 0.8;
75
+ if (/^[a-z]{1,2}$/.test(token)) weight *= 0.75;
76
+ return total + weight;
77
+ }, 0);
78
+ const exactTitle = queryText === document.normalizedTitle;
79
+ if (exactTitle) score += 10;
80
+ return {
81
+ branchId: document.branchId,
82
+ documentId: document.id,
83
+ title: document.title,
84
+ score,
85
+ matchedTerms,
86
+ distinctiveTerms,
87
+ exactTitle
88
+ };
89
+ }).filter(candidate => candidate.matchedTerms.length > 0)
90
+ .sort((a, b) => b.score - a.score || b.matchedTerms.length - a.matchedTerms.length);
91
+
92
+ const best = candidates[0] || null;
93
+ const second = candidates[1] || null;
94
+ const exactTitleCount = candidates.filter(candidate => candidate.exactTitle).length;
95
+ const hasDistinctiveMatch = Boolean(best && (best.distinctiveTerms.length > 0 || (best.exactTitle && exactTitleCount === 1)));
96
+ const leadsClearly = Boolean(best && (!second || (best.exactTitle && exactTitleCount === 1) ||
97
+ best.matchedTerms.length > second.matchedTerms.length || best.score >= second.score * 1.25));
98
+ const confident = Boolean(best && hasDistinctiveMatch && leadsClearly);
99
+ return { selected: confident ? best : null, candidates: candidates.slice(0, 5), confident };
100
+ }
101
+
102
+ async function resolveBranches(branchIdsInput) {
103
+ const ids = normalizeBranchIds(branchIdsInput);
104
+ if (ids.length === 0) throw new Error('No hay ninguna rama de conocimiento activa.');
105
+ const branches = (await Promise.all(ids.map(id => RagStorage.getBranchById(id)))).filter(Boolean);
106
+ if (branches.length === 0) throw new Error(`No se encontró ninguna de las ramas especificadas: ${ids.join(', ')}.`);
107
+ return branches;
108
+ }
109
+
110
+ async function buildRagSystemContext(branchIds, options = {}) {
111
+ if (!branchIds) return '';
112
+ try {
113
+ const branches = await resolveBranches(branchIds);
114
+ const names = branches.map(b => b.name).join(', ');
115
+ const isCheckpoint = !!options.isCheckpointEnabled;
116
+
117
+ const label = branches.length === 1
118
+ ? `[ACTIVE KNOWLEDGE BASE: ${names}]`
119
+ : `[ACTIVE KNOWLEDGE BASES: ${names}]`;
120
+
121
+ // Expose the language of the documentary connection so the model knows
122
+ // in what language to formulate queries and what to expect in results.
123
+ const branchLang = branches[0]?.language || options.branchLanguage || null;
124
+ const langNote = branchLang
125
+ ? `\n- Document language: the documents in this knowledge base are written in **${branchLang}**. Formulate search queries in that language for best recall.`
126
+ : '';
127
+
128
+ const checkpointRule = isCheckpoint
129
+ ? '\n- After extracting key data from 1-2 documents or before concluding complex inquiries, invoke "agent_checkpoint" to consolidate findings and record the next step.'
130
+ : '';
131
+
132
+ return `${label}\n\nDocument retrieval protocol:${langNote}\n- Start with search_knowledge_base using short, key terms; do not concatenate long phrases.\n- Choose scope before searching: use scope="document" with documentHint for one exact document; use scope="corpus" for comparisons, multiple documents, companies, or years; use scope="auto" only when neither intent is clear.\n- For multi-document retrieval, make one scope="corpus" search. Do not repeat the same query with different documentHint values.\n- Consult list_documents (optionally specifying filter, e.g. filter="Walmart") only if search yields no results, you do not know the available sources, or the query references a document whose exact name you are unsure of.\n- In scope="corpus" results, at most 2 chunks per document are returned; if you need more depth from a specific document, repeat the search with scope="document" and documentHint.\n- After finding relevant chunks, use read_knowledge_chunk with chunkIds for deeper or adjacent content; do not re-search for it.\n- Treat tool outputs as private internal evidence: synthesize and answer directly without reproducing full fragments or technical identifiers.\n- Document images are identified as ![description](rag-image://docId:imgId). If an image can provide relevant information and you have native vision, use read_knowledge_image with its full reference to inspect it before answering; request only what is necessary.${checkpointRule}\n- If evidence is insufficient or you find no conclusive data, state it accurately and conclude; do not invent or wander.`;
133
+ } catch (err) {
134
+ if (typeof console !== 'undefined' && console.warn) {
135
+ console.warn('[ChatRagService] Error building RAG system context:', err);
136
+ }
137
+ return '';
138
+ }
139
+ }
140
+
141
+ async function injectRagContext(systemPrompt, branchIds, options = {}) {
142
+ const context = await buildRagSystemContext(branchIds, options);
143
+ return [context, String(systemPrompt || '').trim()].filter(Boolean).join('\n\n');
144
+ }
145
+
146
+ async function listDocuments(branchIds, rawArgs) {
147
+ try {
148
+ const branches = await resolveBranches(branchIds);
149
+ const args = (typeof rawArgs === 'string')
150
+ ? { filter: rawArgs }
151
+ : parseArguments(rawArgs);
152
+ const filterText = String(args.filter || args.query || '').trim();
153
+ const filterTokens = filterText ? documentReferenceTokens(filterText) : [];
154
+
155
+ const allDocs = [];
156
+ const sections = [];
157
+ for (const branch of branches) {
158
+ const documents = await RagStorage.getDocumentsByBranch(branch.id);
159
+ const filteredDocs = filterText
160
+ ? documents.filter(doc => {
161
+ const normTitle = normalizeDocumentReference(doc.title);
162
+ const normFilter = normalizeDocumentReference(filterText);
163
+ if (normFilter && normTitle.includes(normFilter)) return true;
164
+ const compactTitle = normTitle.replace(/\s+/g, '');
165
+ const compactFilter = normFilter.replace(/\s+/g, '');
166
+ if (compactFilter && compactTitle.includes(compactFilter)) return true;
167
+ if (filterTokens.length > 0) {
168
+ const titleTokens = new Set(documentReferenceTokens(doc.title));
169
+ return filterTokens.some(tok => titleTokens.has(tok));
170
+ }
171
+ return false;
172
+ })
173
+ : documents;
174
+
175
+ allDocs.push(...filteredDocs);
176
+ const filterBadge = filterText ? ` (filtered by "${filterText}")` : '';
177
+ const lines = [`[DOCUMENTS IN ${branch.name}${filterBadge}]`];
178
+ for (const document of filteredDocs) {
179
+ const imgCount = Number.isInteger(document.imageCount) ? document.imageCount : 0;
180
+ const imgLabel = imgCount === 1 ? '1 image' : `${imgCount} images`;
181
+ lines.push(`- ${document.title} (documentId: ${document.id}, ${document.chunkCount} chunks, ${imgLabel}, ${document.fileType})`);
182
+ }
183
+ if (!filteredDocs.length) {
184
+ lines.push(filterText
185
+ ? `No documents matching "${filterText}" were found in this branch.`
186
+ : 'The branch contains no documents.'
187
+ );
188
+ }
189
+ sections.push(lines.join('\n'));
190
+ }
191
+ return {
192
+ success: true,
193
+ branchId: branches[0]?.id || '',
194
+ branchName: branches.map(b => b.name).join(', '),
195
+ branchIds: branches.map(b => b.id),
196
+ filter: filterText || null,
197
+ count: allDocs.length,
198
+ documents: allDocs,
199
+ text: sections.join('\n\n')
200
+ };
201
+ } catch (error) {
202
+ return { success: false, error: error.message || String(error) };
203
+ }
204
+ }
205
+
206
+ function makeSnippet(content, query, maxLength = 850) {
207
+ if (!content) return '';
208
+ let text = String(content)
209
+ .replace(/[^\S\r\n]+/g, ' ')
210
+ .replace(/\n{3,}/g, '\n\n')
211
+ .trim();
212
+
213
+ if (text.length <= maxLength) return text;
214
+
215
+ const term = String(query || '').split(/\s+/).find(word => word.length > 3) || '';
216
+ const found = term ? text.toLowerCase().indexOf(term.toLowerCase()) : -1;
217
+
218
+ let start = 0;
219
+ if (found > 0) {
220
+ const idealStart = Math.max(0, found - Math.floor(maxLength * 0.35));
221
+ const prevNewline = text.lastIndexOf('\n', idealStart);
222
+ start = (prevNewline >= 0 && idealStart - prevNewline < 80) ? prevNewline + 1 : idealStart;
223
+ }
224
+
225
+ let end = start + maxLength;
226
+ if (end < text.length) {
227
+ const nextNewline = text.indexOf('\n', end);
228
+ if (nextNewline >= 0 && nextNewline - end < 80) {
229
+ end = nextNewline;
230
+ }
231
+ }
232
+
233
+ const slice = text.slice(start, end).trim();
234
+ return `${start > 0 ? '… ' : ''}${slice}${end < text.length ? ' …' : ''}`;
235
+ }
236
+
237
+ async function searchKnowledgeBase(branchIds, rawArgs) {
238
+ const args = parseArguments(rawArgs);
239
+ const query = String(args.query || '').trim();
240
+ if (!query) return { success: false, error: 'La consulta de búsqueda está vacía.' };
241
+ try {
242
+ const branches = await resolveBranches(branchIds);
243
+ const branchNamesById = new Map(branches.map(b => [b.id, b.name]));
244
+ const ids = branches.map(b => b.id);
245
+ const limit = Number(args.limit) > 0 ? Number(args.limit) : 10;
246
+ const requestedScope = ['auto', 'document', 'corpus'].includes(String(args.scope || '').toLowerCase())
247
+ ? String(args.scope).toLowerCase()
248
+ : 'auto';
249
+ const hint = String(args.documentHint || '').trim();
250
+ const documentsByBranch = await Promise.all(branches.map(branch => RagStorage.getDocumentsByBranch(branch.id)));
251
+ const documents = documentsByBranch.flat();
252
+
253
+ let selection = { selected: null, candidates: [], confident: false };
254
+ if (requestedScope !== 'corpus') {
255
+ if (hint) {
256
+ selection = selectDocumentCandidate(documents, hint);
257
+ }
258
+ if (!selection.selected) {
259
+ const combinedReference = [hint, query].map(value => String(value || '').trim()).filter(Boolean).join(' ');
260
+ const combinedSelection = selectDocumentCandidate(documents, combinedReference);
261
+ if (combinedSelection.selected) {
262
+ selection = combinedSelection;
263
+ } else if (!selection.candidates.length) {
264
+ selection = combinedSelection;
265
+ }
266
+ }
267
+ }
268
+
269
+ let appliedScope = 'corpus';
270
+ let scopeReason = requestedScope === 'corpus'
271
+ ? 'The query requested cross-document coverage.'
272
+ : 'No unambiguous document match found; cross-document search applied.';
273
+ let result;
274
+ let appliedMaxPerDoc = null;
275
+ if (selection.selected && typeof RagIndex.searchDocuments === 'function') {
276
+ appliedScope = 'document';
277
+ scopeReason = `Unambiguous match with title "${selection.selected.title}".`;
278
+ result = await RagIndex.searchDocuments(
279
+ selection.selected.branchId,
280
+ [selection.selected.documentId],
281
+ query,
282
+ { limit, tolerance: args.tolerance }
283
+ );
284
+ } else {
285
+ let dynamicMaxPerDoc = 2;
286
+ const explicitMaxPerDoc = Number(args.maxPerDocument);
287
+ if (Number.isInteger(explicitMaxPerDoc) && explicitMaxPerDoc > 0) {
288
+ dynamicMaxPerDoc = explicitMaxPerDoc;
289
+ } else if (requestedScope !== 'corpus') {
290
+ // Búsqueda auto o document con ambigüedad: si hay pocos documentos o pocos candidatos,
291
+ // permitir 3 o 4 chunks por documento para no perder tablas contables complementarias (ej: P&L + Cash Flows)
292
+ if (documents.length <= 2 || (selection.candidates.length > 0 && selection.candidates.length <= 2)) {
293
+ dynamicMaxPerDoc = 4;
294
+ } else if (documents.length <= 4) {
295
+ dynamicMaxPerDoc = 3;
296
+ }
297
+ }
298
+ appliedMaxPerDoc = dynamicMaxPerDoc;
299
+
300
+ const corpusOptions = {
301
+ limit,
302
+ tolerance: args.tolerance,
303
+ groupByDocument: true,
304
+ maxPerDocument: dynamicMaxPerDoc
305
+ };
306
+ result = typeof RagIndex.searchBranches === 'function'
307
+ ? await RagIndex.searchBranches(ids, query, corpusOptions)
308
+ : await RagIndex.searchBranch(ids[0], query, corpusOptions);
309
+ }
310
+
311
+ const matches = result.hits.map(hit => {
312
+ const bName = branchNamesById.get(hit.branchId) || branches[0].name;
313
+ return {
314
+ branchId: hit.branchId || branches[0].id,
315
+ branchName: bName,
316
+ documentId: hit.documentId,
317
+ chunkId: hit.chunkId,
318
+ documentTitle: hit.documentTitle,
319
+ sectionTitle: hit.sectionTitle,
320
+ score: hit.score,
321
+ snippet: makeSnippet(hit.content, query)
322
+ };
323
+ });
324
+
325
+ const branchLabel = branches.map(b => b.name).join(', ');
326
+ const lines = [
327
+ `[RESULTS IN ${branchLabel} FOR: ${query}]`,
328
+ `Requested scope: ${requestedScope}`,
329
+ `Applied scope: ${appliedScope}`,
330
+ `Reason: ${scopeReason}`
331
+ ];
332
+ if (selection.selected) lines.push(`Selected document: ${selection.selected.title} (${selection.selected.documentId})`);
333
+ if (!selection.selected && selection.candidates.length > 0) {
334
+ lines.push(`Document candidates: ${selection.candidates.map(candidate => candidate.title).join(', ')} (tip: specify documentHint with the target document to retrieve more depth).`);
335
+ }
336
+ for (const match of matches) {
337
+ const branchBadge = branches.length > 1 ? ` [Branch: ${match.branchName}]` : '';
338
+ lines.push(`- ${match.documentTitle} · ${match.sectionTitle}${branchBadge} (chunkId: ${match.chunkId}, score: ${match.score.toFixed(3)})`);
339
+ const indentedSnippet = match.snippet.split('\n').map(line => ` ${line}`).join('\n');
340
+ lines.push(indentedSnippet);
341
+ }
342
+ if (!matches.length) lines.push('No relevant chunks found.');
343
+
344
+ return {
345
+ success: true,
346
+ branchId: branches[0]?.id || '',
347
+ branchName: branchLabel,
348
+ branchIds: ids,
349
+ query,
350
+ requestedScope,
351
+ appliedScope,
352
+ scopeReason,
353
+ documentHint: String(args.documentHint || ''),
354
+ selectedDocument: selection.selected,
355
+ documentCandidates: selection.candidates,
356
+ maxChunksPerDocument: appliedScope === 'corpus' ? appliedMaxPerDoc : null,
357
+ matchesCount: matches.length,
358
+ totalMatches: result.count,
359
+ matches,
360
+ text: lines.join('\n')
361
+ };
362
+ } catch (error) {
363
+ return { success: false, error: error.message || String(error) };
364
+ }
365
+ }
366
+
367
+ async function readKnowledgeChunk(branchIds, rawArgs) {
368
+ const args = parseArguments(rawArgs);
369
+ let requestedIds = [];
370
+ if (Array.isArray(args.chunkIds)) {
371
+ requestedIds = args.chunkIds.map(id => String(id || '').trim()).filter(Boolean);
372
+ } else if (typeof args.chunkIds === 'string' && args.chunkIds.trim()) {
373
+ requestedIds = args.chunkIds.split(/[\s,]+/).map(id => id.trim()).filter(Boolean);
374
+ } else if (args.chunkId) {
375
+ if (Array.isArray(args.chunkId)) {
376
+ requestedIds = args.chunkId.map(id => String(id || '').trim()).filter(Boolean);
377
+ } else if (typeof args.chunkId === 'string' && args.chunkId.includes(',')) {
378
+ requestedIds = args.chunkId.split(/[\s,]+/).map(id => id.trim()).filter(Boolean);
379
+ } else {
380
+ const single = String(args.chunkId || '').trim();
381
+ if (single) requestedIds = [single];
382
+ }
383
+ }
384
+
385
+ if (!requestedIds.length) return { success: false, error: 'chunkId o chunkIds es obligatorio.' };
386
+ const MAX_CHUNKS_PER_CALL = 5;
387
+ const uniqueIds = Array.from(new Set(requestedIds)).slice(0, MAX_CHUNKS_PER_CALL);
388
+
389
+ try {
390
+ const branches = await resolveBranches(branchIds);
391
+ const allowedBranchIds = new Set(branches.map(b => b.id));
392
+ const docCache = new Map();
393
+
394
+ const items = [];
395
+ for (const id of uniqueIds) {
396
+ const chunk = await RagStorage.getChunkById(id);
397
+ if (!chunk || !allowedBranchIds.has(chunk.branchId)) continue;
398
+ let document = docCache.get(chunk.documentId);
399
+ if (!document) {
400
+ document = await RagStorage.getDocumentById(chunk.documentId);
401
+ if (document) docCache.set(chunk.documentId, document);
402
+ }
403
+ const totalChunks = Number.isInteger(document?.chunkCount) ? document.chunkCount : 0;
404
+ const order = Number.isInteger(chunk.order) ? chunk.order : null;
405
+ const prevChunkId = (order !== null && order > 0) ? `${chunk.documentId}:chunk:${order - 1}` : null;
406
+ const nextChunkId = (order !== null && totalChunks > 0 && order + 1 < totalChunks) ? `${chunk.documentId}:chunk:${order + 1}` : null;
407
+
408
+ items.push({
409
+ chunkId: chunk.id,
410
+ documentId: chunk.documentId,
411
+ branchId: chunk.branchId,
412
+ documentTitle: document?.title || '',
413
+ sectionTitle: chunk.title,
414
+ order,
415
+ totalChunks,
416
+ prevChunkId,
417
+ nextChunkId,
418
+ documentImageCount: Number.isInteger(document?.imageCount) ? document.imageCount : 0,
419
+ charCount: chunk.content.length,
420
+ content: chunk.content,
421
+ pageStart: chunk.pageStart,
422
+ pageEnd: chunk.pageEnd
423
+ });
424
+ }
425
+
426
+ if (!items.length) {
427
+ return {
428
+ success: false,
429
+ error: uniqueIds.length === 1
430
+ ? `No existe el fragmento ${uniqueIds[0]} en las ramas activas.`
431
+ : `Ninguno de los fragmentos solicitados (${uniqueIds.join(', ')}) existe en las ramas activas.`
432
+ };
433
+ }
434
+
435
+ if (items.length === 1 && uniqueIds.length === 1) {
436
+ const single = items[0];
437
+ return {
438
+ success: true,
439
+ chunkId: single.chunkId,
440
+ chunkIds: [single.chunkId],
441
+ documentId: single.documentId,
442
+ branchId: single.branchId,
443
+ documentTitle: single.documentTitle,
444
+ sectionTitle: single.sectionTitle,
445
+ order: single.order,
446
+ totalChunks: single.totalChunks,
447
+ prevChunkId: single.prevChunkId,
448
+ nextChunkId: single.nextChunkId,
449
+ documentImageCount: single.documentImageCount,
450
+ charCount: single.charCount,
451
+ content: single.content,
452
+ pageStart: single.pageStart,
453
+ pageEnd: single.pageEnd
454
+ };
455
+ }
456
+
457
+ const formattedSections = items.map((item, idx) => {
458
+ const meta = [
459
+ `chunkId: ${item.chunkId}`,
460
+ `Chunk ${Number.isInteger(item.order) ? item.order + 1 : idx + 1} of ${item.totalChunks || '?'}`,
461
+ item.pageStart ? `Page: ${item.pageStart}${item.pageEnd && item.pageEnd !== item.pageStart ? `-${item.pageEnd}` : ''}` : null,
462
+ item.prevChunkId ? `Prev: ${item.prevChunkId}` : null,
463
+ item.nextChunkId ? `Next: ${item.nextChunkId}` : null
464
+ ].filter(Boolean).join(' | ');
465
+ return `### ${item.documentTitle} · ${item.sectionTitle} (${meta})\n\n${item.content}`;
466
+ });
467
+
468
+ return {
469
+ success: true,
470
+ chunkId: items[0].chunkId,
471
+ chunkIds: items.map(it => it.chunkId),
472
+ count: items.length,
473
+ items,
474
+ documentTitle: items[0].documentTitle,
475
+ sectionTitle: items.map(it => it.sectionTitle).join(', '),
476
+ charCount: items.reduce((sum, it) => sum + it.charCount, 0),
477
+ content: formattedSections.join('\n\n---\n\n')
478
+ };
479
+ } catch (error) {
480
+ return { success: false, error: error.message || String(error) };
481
+ }
482
+ }
483
+
484
+ function parseImageReference(value) {
485
+ const match = String(value || '').trim().match(/^rag-image:\/\/([^:\s]+):([^\s:]+)$/i);
486
+ return match ? { documentId: match[1], imageId: match[2] } : null;
487
+ }
488
+
489
+ async function readKnowledgeImage(branchIds, rawArgs) {
490
+ const args = parseArguments(rawArgs);
491
+ const imageRef = String(args.imageRef || '').trim();
492
+ const reference = parseImageReference(imageRef);
493
+ if (!reference) return { success: false, error: 'imageRef debe ser una referencia rag-image://docId:imgId válida.' };
494
+
495
+ try {
496
+ const branches = await resolveBranches(branchIds);
497
+ const document = await RagStorage.getDocumentById(reference.documentId);
498
+ if (!document || !branches.some(branch => branch.id === document.branchId)) {
499
+ return { success: false, error: `La imagen solicitada no pertenece a las ramas activas.` };
500
+ }
501
+ const image = await RagStorage.getDocumentImage(reference.documentId, reference.imageId);
502
+ if (!image?.dataUrl) return { success: false, error: `No existe una imagen utilizable para ${imageRef}.` };
503
+
504
+ const fileParser = getFileParser();
505
+ const dataUrl = image.isCmyk && fileParser?.convertCmykDataUrlToRgb
506
+ ? fileParser.convertCmykDataUrlToRgb(image.dataUrl)
507
+ : image.dataUrl;
508
+ return {
509
+ success: true,
510
+ imageRef,
511
+ documentId: document.id,
512
+ documentTitle: document.title,
513
+ page: Number.isFinite(image.page) ? image.page : null,
514
+ label: String(image.label || ''),
515
+ mimeType: String(image.mimeType || ''),
516
+ dataUrl
517
+ };
518
+ } catch (error) {
519
+ return { success: false, error: error.message || String(error) };
520
+ }
521
+ }
522
+
523
+ return {
524
+ parseArguments, normalizeBranchIds, resolveBranches,
525
+ normalizeDocumentReference, documentReferenceTokens, selectDocumentCandidate,
526
+ buildRagSystemContext, injectRagContext,
527
+ listDocuments, searchKnowledgeBase, readKnowledgeChunk, readKnowledgeImage,
528
+ parseImageReference
529
+ };
530
+ });