scout-ai 1.0.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/.vimproject +80 -15
- data/README.md +296 -0
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/doc/Agent.md +279 -0
- data/doc/Chat.md +258 -0
- data/doc/LLM.md +446 -0
- data/doc/Model.md +513 -0
- data/doc/RAG.md +129 -0
- data/lib/scout/llm/agent/chat.rb +51 -1
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +42 -21
- data/lib/scout/llm/ask.rb +38 -6
- data/lib/scout/llm/backends/anthropic.rb +147 -0
- data/lib/scout/llm/backends/bedrock.rb +1 -1
- data/lib/scout/llm/backends/ollama.rb +23 -29
- data/lib/scout/llm/backends/openai.rb +34 -40
- data/lib/scout/llm/backends/responses.rb +158 -110
- data/lib/scout/llm/chat.rb +250 -94
- data/lib/scout/llm/embed.rb +4 -4
- data/lib/scout/llm/mcp.rb +28 -0
- data/lib/scout/llm/parse.rb +1 -0
- data/lib/scout/llm/rag.rb +9 -0
- data/lib/scout/llm/tools/call.rb +66 -0
- data/lib/scout/llm/tools/knowledge_base.rb +158 -0
- data/lib/scout/llm/tools/mcp.rb +59 -0
- data/lib/scout/llm/tools/workflow.rb +69 -0
- data/lib/scout/llm/tools.rb +58 -143
- data/lib/scout-ai.rb +1 -0
- data/scout-ai.gemspec +31 -18
- data/scout_commands/agent/ask +28 -71
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +2 -2
- data/scout_commands/llm/server +319 -0
- data/share/server/chat.html +138 -0
- data/share/server/chat.js +468 -0
- data/test/scout/llm/backends/test_anthropic.rb +134 -0
- data/test/scout/llm/backends/test_openai.rb +45 -6
- data/test/scout/llm/backends/test_responses.rb +124 -0
- data/test/scout/llm/test_agent.rb +0 -70
- data/test/scout/llm/test_ask.rb +3 -1
- data/test/scout/llm/test_chat.rb +43 -1
- data/test/scout/llm/test_mcp.rb +29 -0
- data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
- data/test/scout/llm/tools/test_mcp.rb +11 -0
- data/test/scout/llm/tools/test_workflow.rb +39 -0
- metadata +56 -17
- data/README.rdoc +0 -18
- data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
- data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
- data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
- data/python/scout_ai/atcold/plot_lib.py +0 -141
- data/python/scout_ai/atcold/spiral.py +0 -27
- data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
- data/python/scout_ai/language_model.py +0 -70
- /data/{python/scout_ai/atcold/__init__.py → test/scout/llm/tools/test_call.rb} +0 -0
@@ -0,0 +1,468 @@
|
|
1
|
+
(function(){
|
2
|
+
// Compute API base path so this script works behind a path-prefix proxy (e.g. /sandbox2)
|
3
|
+
function computeApiBase(){
|
4
|
+
// Use location.pathname to derive the directory this UI is served from.
|
5
|
+
// Examples:
|
6
|
+
// /sandbox2/ -> API base: /sandbox2
|
7
|
+
// /sandbox2/chat.html -> API base: /sandbox2
|
8
|
+
// / -> API base: "" (root)
|
9
|
+
const locPath = (window.location && window.location.pathname) ? window.location.pathname : '';
|
10
|
+
let base = locPath || '';
|
11
|
+
// remove trailing slash
|
12
|
+
if(base.length > 1 && base.endsWith('/')) base = base.slice(0, -1);
|
13
|
+
// if the last segment looks like a file (contains a dot), remove it
|
14
|
+
const segs = base.split('/');
|
15
|
+
const last = segs[segs.length - 1] || '';
|
16
|
+
if(last.includes('.')){
|
17
|
+
segs.pop();
|
18
|
+
base = segs.join('/') || '';
|
19
|
+
}
|
20
|
+
// ensure base is empty string for root so concatenation yields '/list' etc.
|
21
|
+
if(base === '/') base = '';
|
22
|
+
return base;
|
23
|
+
}
|
24
|
+
|
25
|
+
const API_BASE = computeApiBase(); // e.g. '/sandbox2' or ''
|
26
|
+
|
27
|
+
const PREDEFINED_KEYS = ['user','system','assistant','import','file','directory', 'continue', 'option','endpoint','model','backend','previous_response_id', 'format','websearch','tool','task','job','inline_job'];
|
28
|
+
const ROLE_ONLY = ['user','system','assistant'];
|
29
|
+
const FILE_KEYS = ['import','file','directory', 'continue'];
|
30
|
+
const MARKED_CDN = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
31
|
+
|
32
|
+
// State
|
33
|
+
let cells = [];
|
34
|
+
let filesListCache = [];
|
35
|
+
|
36
|
+
// DOM
|
37
|
+
const pathEl = document.getElementById('path');
|
38
|
+
const cellsDiv = document.getElementById('cells');
|
39
|
+
const filesDiv = document.getElementById('files');
|
40
|
+
const logDiv = document.getElementById('log');
|
41
|
+
const cellCountEl = document.getElementById('cellCount');
|
42
|
+
|
43
|
+
const newFileBtn = document.getElementById('newFileBtn');
|
44
|
+
const loadBtn = document.getElementById('loadBtn');
|
45
|
+
const saveBtn = document.getElementById('saveBtn');
|
46
|
+
const runBtn = document.getElementById('runBtn');
|
47
|
+
const runBtnBottom = document.getElementById('runBtnBottom');
|
48
|
+
const exportBtn = document.getElementById('exportBtn');
|
49
|
+
const listBtn = document.getElementById('listBtn');
|
50
|
+
const deleteBtn = document.getElementById('deleteBtn');
|
51
|
+
const addUser = document.getElementById('addUser');
|
52
|
+
const addSystem = document.getElementById('addSystem');
|
53
|
+
const addAssistant = document.getElementById('addAssistant');
|
54
|
+
const addOption = document.getElementById('addOption');
|
55
|
+
const clearBtn = document.getElementById('clearBtn');
|
56
|
+
const loadingIndicator = document.getElementById('loadingIndicator');
|
57
|
+
|
58
|
+
// Modal elements
|
59
|
+
const mdModal = document.getElementById('mdModal');
|
60
|
+
const mdBody = document.getElementById('mdBody');
|
61
|
+
const mdTitle = document.getElementById('mdTitle');
|
62
|
+
const mdClose = document.getElementById('mdClose');
|
63
|
+
const mdCopy = document.getElementById('mdCopy');
|
64
|
+
|
65
|
+
function log(...args){ const line = document.createElement('div'); line.textContent = '['+new Date().toLocaleTimeString()+'] ' + args.join(' '); logDiv.appendChild(line); logDiv.scrollTop = logDiv.scrollHeight; console.log(...args); }
|
66
|
+
|
67
|
+
// HTTP helpers
|
68
|
+
async function fetchJSON(url, opts){
|
69
|
+
// If the url is relative and doesn't start with '/', respect it; otherwise ensure we don't accidentally
|
70
|
+
// strip our computed API_BASE if callers pass absolute URLs. Expect callers to use API_BASE + '/path'.
|
71
|
+
const res = await fetch(url, opts);
|
72
|
+
const text = await res.text();
|
73
|
+
let json = null;
|
74
|
+
try{ json = text ? JSON.parse(text) : {}; }catch(e){ throw new Error('Invalid JSON from ' + url + ': ' + text); }
|
75
|
+
if(!res.ok) throw Object.assign(new Error('HTTP ' + res.status), {status: res.status, body: json});
|
76
|
+
return json;
|
77
|
+
}
|
78
|
+
|
79
|
+
// Load marked library dynamically
|
80
|
+
function loadMarked(){
|
81
|
+
return new Promise((resolve, reject)=>{
|
82
|
+
if(window.marked) return resolve(window.marked);
|
83
|
+
const s = document.createElement('script');
|
84
|
+
s.src = MARKED_CDN;
|
85
|
+
s.async = true;
|
86
|
+
s.onload = ()=>{ try{ return resolve(window.marked); }catch(e){ return reject(e); } };
|
87
|
+
s.onerror = (e)=> reject(new Error('Failed to load marked.js'));
|
88
|
+
document.head.appendChild(s);
|
89
|
+
});
|
90
|
+
}
|
91
|
+
|
92
|
+
// Parse/serialize
|
93
|
+
function parseTextToCells(text){
|
94
|
+
const lines = (text||'').split(/\r?\n/);
|
95
|
+
const out = [];
|
96
|
+
const headerRe = /^([A-Za-z][\w-]*):\s*(.*)$/;
|
97
|
+
let i = 0;
|
98
|
+
while(i < lines.length){
|
99
|
+
const ln = lines[i];
|
100
|
+
const m = ln.match(headerRe);
|
101
|
+
if(m){
|
102
|
+
const key = m[1].toLowerCase();
|
103
|
+
const rest = m[2] || '';
|
104
|
+
// collect all lines until the next header (exclusive)
|
105
|
+
const bodyLines = [];
|
106
|
+
// include the same-line rest (may be empty string)
|
107
|
+
bodyLines.push(rest);
|
108
|
+
let j = i + 1;
|
109
|
+
for(; j < lines.length; j++){
|
110
|
+
if(headerRe.test(lines[j])) break;
|
111
|
+
bodyLines.push(lines[j]);
|
112
|
+
}
|
113
|
+
// count non-empty lines in the block
|
114
|
+
const nonEmptyCount = bodyLines.reduce((acc, l) => acc + (l && l.trim().length>0 ? 1 : 0), 0);
|
115
|
+
const isInline = (nonEmptyCount <= 1);
|
116
|
+
let content;
|
117
|
+
if(isInline){
|
118
|
+
// inline blocks: remove line jumps and join non-empty pieces with a single space
|
119
|
+
const parts = bodyLines.map(l => l ? l.trim() : '').filter(s => s.length>0);
|
120
|
+
content = parts.join(' ');
|
121
|
+
} else {
|
122
|
+
// block: preserve lines (including empty lines)
|
123
|
+
content = bodyLines.join('\n');
|
124
|
+
// trim trailing newlines that come from join
|
125
|
+
content = content.replace(/\n+$/,'');
|
126
|
+
}
|
127
|
+
|
128
|
+
if(ROLE_ONLY.includes(key)){
|
129
|
+
out.push({type:'role', role:key, inline:isInline, content: content});
|
130
|
+
} else if(PREDEFINED_KEYS.includes(key)){
|
131
|
+
out.push({type:'option', key:key, inline:isInline, content: content});
|
132
|
+
} else {
|
133
|
+
// unknown headers treated as option-like blocks
|
134
|
+
out.push({type:'option', key:key, inline:isInline, content: content});
|
135
|
+
}
|
136
|
+
i = j;
|
137
|
+
continue;
|
138
|
+
}
|
139
|
+
|
140
|
+
// Lines not starting with a header: collect until next header and treat as a note block
|
141
|
+
let j = i;
|
142
|
+
const body = [];
|
143
|
+
while(j < lines.length && !headerRe.test(lines[j])){ body.push(lines[j]); j++; }
|
144
|
+
const nonEmptyCount = body.reduce((acc, l) => acc + (l && l.trim().length>0 ? 1 : 0), 0);
|
145
|
+
if(nonEmptyCount <= 1){
|
146
|
+
const parts = body.map(l => l ? l.trim() : '').filter(s => s.length>0);
|
147
|
+
const content = parts.join(' ');
|
148
|
+
if(content.length>0) out.push({type:'role', role:'note', inline:true, content: content});
|
149
|
+
} else {
|
150
|
+
out.push({type:'role', role:'note', inline:false, content: body.join('\n')});
|
151
|
+
}
|
152
|
+
i = j;
|
153
|
+
}
|
154
|
+
return out;
|
155
|
+
}
|
156
|
+
|
157
|
+
function cellsToText(cells){
|
158
|
+
let lines = [];
|
159
|
+
cells.forEach(c=>{
|
160
|
+
if(c.type==='role'){
|
161
|
+
const role = c.role || 'user';
|
162
|
+
if(c.inline) lines.push(role + ': ' + (c.content || ''));
|
163
|
+
else lines.push(role + ':', (c.content || ''), '');
|
164
|
+
} else if(c.type==='option'){
|
165
|
+
const k = c.key || 'import';
|
166
|
+
if(c.inline) lines.push(k + ': ' + (c.content || ''));
|
167
|
+
else lines.push(k + ':', (c.content || ''), '');
|
168
|
+
}
|
169
|
+
});
|
170
|
+
return lines.join('\n').replace(/\n{3,}/g,'\n\n');
|
171
|
+
}
|
172
|
+
|
173
|
+
// Safe markdown render (strip scripts)
|
174
|
+
function sanitizeHtml(html){
|
175
|
+
return html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
|
176
|
+
}
|
177
|
+
|
178
|
+
function renderMarkdownSafe(md){
|
179
|
+
let html = '';
|
180
|
+
if(window.marked){
|
181
|
+
try{ html = marked.parse(md || ''); }catch(e){ html = escapeHtml(md || ''); }
|
182
|
+
} else {
|
183
|
+
html = '<pre style="white-space:pre-wrap">' + escapeHtml(md || '') + '</pre>';
|
184
|
+
}
|
185
|
+
html = sanitizeHtml(html);
|
186
|
+
return html;
|
187
|
+
}
|
188
|
+
|
189
|
+
// Render
|
190
|
+
function render(){
|
191
|
+
// Ensure there's always an empty user cell at the end (but don't force it into editing/focus)
|
192
|
+
ensureTrailingEmptyCell();
|
193
|
+
|
194
|
+
cellsDiv.innerHTML = '';
|
195
|
+
cells.forEach((c, idx)=>{
|
196
|
+
const el = document.createElement('div'); el.className='cell panel';
|
197
|
+
|
198
|
+
const header = document.createElement('div'); header.className='cell-header';
|
199
|
+
|
200
|
+
const roleBadge = document.createElement('div'); roleBadge.className = 'role-badge ' + (c.role||c.key||'');
|
201
|
+
roleBadge.textContent = (c.type==='role' ? (c.role||'user') : (c.key||'option'));
|
202
|
+
roleBadge.onclick = (e)=>{ e.stopPropagation(); changeRolePrompt(c); };
|
203
|
+
header.appendChild(roleBadge);
|
204
|
+
|
205
|
+
const controls = document.createElement('div'); controls.className = 'cell-controls';
|
206
|
+
controls.innerHTML = '<div class="cell-actions"></div>';
|
207
|
+
const actionsContainer = controls.querySelector('.cell-actions');
|
208
|
+
|
209
|
+
const up = document.createElement('button'); up.textContent='↑'; up.title='Move up'; up.onclick = (e)=>{ e.stopPropagation(); if(idx>0){ const t = cells[idx-1]; cells[idx-1]=cells[idx]; cells[idx]=t; updateAndRender(); } };
|
210
|
+
const down = document.createElement('button'); down.textContent='↓'; down.title='Move down'; down.onclick = (e)=>{ e.stopPropagation(); if(idx<cells.length-1){ const t = cells[idx+1]; cells[idx+1]=cells[idx]; cells[idx]=t; updateAndRender(); } };
|
211
|
+
const del = document.createElement('button'); del.textContent='✕'; del.title='Delete'; del.onclick = (e)=>{ e.stopPropagation(); if(confirm('Delete this cell?')){ cells.splice(idx,1); updateAndRender(); } };
|
212
|
+
const convert = document.createElement('button'); convert.className='small'; convert.textContent = (c.type==='option' ? 'opt' : (c.inline ? '→block' : '→inline'));
|
213
|
+
convert.onclick = (e)=>{ e.stopPropagation(); if(c.type==='option') return; c.inline = !c.inline; updateAndRender(); };
|
214
|
+
const editBtn = document.createElement('button'); editBtn.className='small'; editBtn.textContent='edit'; editBtn.title='Edit'; editBtn.onclick = (e)=>{ e.stopPropagation(); startEditing(c, idx); };
|
215
|
+
|
216
|
+
actionsContainer.appendChild(up); actionsContainer.appendChild(down); actionsContainer.appendChild(convert); actionsContainer.appendChild(editBtn); actionsContainer.appendChild(del);
|
217
|
+
|
218
|
+
header.appendChild(controls);
|
219
|
+
el.appendChild(header);
|
220
|
+
|
221
|
+
// content area
|
222
|
+
const contentWrap = document.createElement('div'); contentWrap.style.width='100%';
|
223
|
+
|
224
|
+
// If not editing and this is an assistant role, render markdown as HTML; other roles show plain text preview
|
225
|
+
if(!c.editing){
|
226
|
+
if(c.type==='role' && (c.role||'') === 'assistant'){
|
227
|
+
const preview = document.createElement('div'); preview.className = 'assistant-bubble message-preview';
|
228
|
+
preview.innerHTML = renderMarkdownSafe(c.content || '');
|
229
|
+
// clicking preview starts editing that cell
|
230
|
+
preview.onclick = (e)=>{ e.stopPropagation(); startEditing(c, idx); };
|
231
|
+
contentWrap.appendChild(preview);
|
232
|
+
} else {
|
233
|
+
// render plain preview text (for user/system/note and options)
|
234
|
+
const preview = document.createElement('div'); preview.className = ((c.type==='role' && (c.role||'')==='user')? 'user-bubble message-preview' : 'message-preview');
|
235
|
+
preview.textContent = c.content || '';
|
236
|
+
preview.onclick = (e)=>{ e.stopPropagation(); startEditing(c, idx); };
|
237
|
+
contentWrap.appendChild(preview);
|
238
|
+
}
|
239
|
+
} else {
|
240
|
+
// editing mode: show input or textarea depending on inline
|
241
|
+
if(c.inline){
|
242
|
+
const input = document.createElement('input'); input.className='cell-input'; input.value = c.content || '';
|
243
|
+
if(c.type==='option' && FILE_KEYS.includes((c.key||'').toLowerCase())){ input.setAttribute('list','files_datalist'); }
|
244
|
+
input.oninput = (e)=>{ c.content = input.value; // when typing in an editing cell, maintain trailing empty cell
|
245
|
+
if(idx === cells.length - 1) ensureTrailingEmptyCell(); };
|
246
|
+
input.onblur = ()=>{ c.editing = false; updateAndRender(); };
|
247
|
+
input.onkeydown = (e)=>{ if(e.key === 'Enter' && (e.metaKey || e.ctrlKey)){ input.blur(); } };
|
248
|
+
contentWrap.appendChild(input);
|
249
|
+
// autofocus only if this cell was explicitly put into editing mode
|
250
|
+
if(c._shouldFocus){ setTimeout(()=>{ input.focus(); input.selectionStart = input.value.length; c._shouldFocus = false; }, 0); }
|
251
|
+
} else {
|
252
|
+
const ta = document.createElement('textarea'); ta.className='cell-text'; ta.value = c.content || '';
|
253
|
+
ta.oninput = (e)=>{ c.content = ta.value; if(idx === cells.length - 1) ensureTrailingEmptyCell(); };
|
254
|
+
ta.onblur = ()=>{ c.editing = false; updateAndRender(); };
|
255
|
+
ta.onkeydown = (e)=>{ if(e.key === 'Enter' && (e.metaKey || e.ctrlKey)){ ta.blur(); } };
|
256
|
+
contentWrap.appendChild(ta);
|
257
|
+
if(c._shouldFocus){ setTimeout(()=>{ ta.focus(); ta.selectionStart = ta.value.length; c._shouldFocus = false; }, 0); }
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
el.appendChild(contentWrap);
|
262
|
+
cellsDiv.appendChild(el);
|
263
|
+
});
|
264
|
+
cellCountEl.textContent = cells.length;
|
265
|
+
renderDatalists();
|
266
|
+
}
|
267
|
+
|
268
|
+
function startEditing(c, idx){ c.editing = true; c._shouldFocus = true; updateAndRender(); }
|
269
|
+
|
270
|
+
// Always keep a trailing empty user cell, but do not force it into editing/focus when unrelated actions occur
|
271
|
+
function ensureTrailingEmptyCell(){
|
272
|
+
if(cells.length === 0){
|
273
|
+
cells.push({type:'role', role:'user', inline:false, content:'', editing:false});
|
274
|
+
return;
|
275
|
+
}
|
276
|
+
const last = cells[cells.length - 1];
|
277
|
+
if(!(last.type === 'role' && (last.role || '') === 'user' && (!last.content || last.content.trim() === ''))){
|
278
|
+
// append a non-editing empty user cell
|
279
|
+
cells.push({type:'role', role:'user', inline:false, content:'', editing:false});
|
280
|
+
}
|
281
|
+
// do not change editing state of existing cells
|
282
|
+
}
|
283
|
+
|
284
|
+
function updateAndRender(){ render(); }
|
285
|
+
|
286
|
+
// Actions
|
287
|
+
function addCell(role){ if(ROLE_ONLY.includes(role)) cells.push({type:'role', role:role, inline:false, content:'', editing:false}); else cells.push({type:'option', key:role, inline:true, content:'', editing:false}); updateAndRender(); }
|
288
|
+
function addOptionCell(){ cells.push({type:'option', key:'import', inline:true, content:'', editing:false}); updateAndRender(); }
|
289
|
+
function clearCells(){ if(confirm('Clear all cells?')){ cells=[]; ensureTrailingEmptyCell(); updateAndRender(); } }
|
290
|
+
|
291
|
+
// role change prompt (simple)
|
292
|
+
function changeRolePrompt(c){ const ans = prompt('Set role (user, system, assistant, or option key):', (c.type==='role'? c.role : c.key)); if(ans===null) return; const v = ans.trim().toLowerCase(); if(ROLE_ONLY.includes(v)){ c.type='role'; c.role=v; c.key=undefined; } else if(v.length===0){ return; } else { c.type='option'; c.key=v; c.role=undefined; } updateAndRender(); }
|
293
|
+
|
294
|
+
// Server interactions
|
295
|
+
async function renderFileList(){
|
296
|
+
try{
|
297
|
+
const res = await fetchJSON((API_BASE || '') + '/list');
|
298
|
+
const wsFiles = res.files || [];
|
299
|
+
filesListCache = wsFiles.slice();
|
300
|
+
const prevScroll = filesDiv.scrollTop;
|
301
|
+
filesDiv.innerHTML = '';
|
302
|
+
if(wsFiles.length===0) filesDiv.innerHTML = '<div class="small muted">(no files)</div>';
|
303
|
+
wsFiles.forEach(k=>{
|
304
|
+
const div = document.createElement('div'); div.className='file-item';
|
305
|
+
const a = document.createElement('a'); a.href='#'; a.textContent = k; a.onclick = (e)=>{ e.preventDefault(); pathEl.value=k; loadFile(); };
|
306
|
+
const meta = document.createElement('div'); meta.className='small muted'; meta.textContent = '';
|
307
|
+
div.appendChild(a); div.appendChild(meta); filesDiv.appendChild(div);
|
308
|
+
});
|
309
|
+
|
310
|
+
// update datalist for files
|
311
|
+
renderDatalists(wsFiles);
|
312
|
+
filesDiv.scrollTop = prevScroll;
|
313
|
+
log('Listed', wsFiles.length, 'files');
|
314
|
+
}catch(e){ log('Error listing files:', e.message || e); filesDiv.innerHTML = '<div class="small muted">(error)</div>'; renderDatalists([]); }
|
315
|
+
}
|
316
|
+
|
317
|
+
function renderDatalists(filesList){
|
318
|
+
if(typeof filesList === 'undefined') filesList = filesListCache || [];
|
319
|
+
|
320
|
+
const oldKeys = document.getElementById('keys_datalist'); if(oldKeys) oldKeys.remove();
|
321
|
+
const oldFiles = document.getElementById('files_datalist'); if(oldFiles) oldFiles.remove();
|
322
|
+
|
323
|
+
const keys = document.createElement('datalist'); keys.id='keys_datalist';
|
324
|
+
PREDEFINED_KEYS.forEach(k=>{ const o=document.createElement('option'); o.value=k; keys.appendChild(o); });
|
325
|
+
document.body.appendChild(keys);
|
326
|
+
|
327
|
+
const files = document.createElement('datalist'); files.id='files_datalist';
|
328
|
+
(filesList || []).slice().sort().forEach(k=>{ const o=document.createElement('option'); o.value=k; files.appendChild(o); });
|
329
|
+
document.body.appendChild(files);
|
330
|
+
|
331
|
+
if(pathEl){ pathEl.setAttribute('list','files_datalist'); }
|
332
|
+
}
|
333
|
+
|
334
|
+
async function saveFile(){ const p = pathEl.value.trim(); if(!p){ alert('Enter path'); return; } const text = cellsToText(cells);
|
335
|
+
try{
|
336
|
+
const res = await fetchJSON((API_BASE || '') + '/save', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({path: p, content: text})});
|
337
|
+
await renderFileList(); log('Saved ' + p);
|
338
|
+
}catch(e){ log('Save failed:', e.message || e); alert('Save failed: ' + (e.message || e)); }
|
339
|
+
}
|
340
|
+
|
341
|
+
async function loadFile(){ const p = pathEl.value.trim(); if(!p){ alert('Enter path'); return; } try{
|
342
|
+
const res = await fetchJSON((API_BASE || '') + '/load?path=' + encodeURIComponent(p));
|
343
|
+
const txt = res.content || '';
|
344
|
+
cells = parseTextToCells(txt);
|
345
|
+
// after loading, ensure trailing empty cell
|
346
|
+
ensureTrailingEmptyCell();
|
347
|
+
updateAndRender(); log('Loaded ' + p);
|
348
|
+
}catch(e){ log('Load failed:', e.message || e); alert('Load failed: ' + (e.body && e.body.error) ? e.body.error : (e.message || 'error')); }
|
349
|
+
}
|
350
|
+
|
351
|
+
async function newFile(){ const p = pathEl.value.trim(); if(!p){ alert('Enter path'); return; } if(!confirm('Create new file at: ' + p + ' ?')) return; cells = []; try{ await saveFile(); }catch(e){} }
|
352
|
+
|
353
|
+
// Truncate file content (server has no delete endpoint in this simple server)
|
354
|
+
async function deletePath(){ const p = pathEl.value.trim(); if(!p){ alert('Enter path'); return; } if(!confirm('Truncate ' + p + ' ? This will clear the file content.')) return; try{
|
355
|
+
const res = await fetchJSON((API_BASE || '') + '/save', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({path: p, content: ''})});
|
356
|
+
await renderFileList(); log('Truncated ' + p);
|
357
|
+
}catch(e){ log('Truncate failed:', e.message || e); alert('Truncate failed: ' + (e.message || e)); }
|
358
|
+
}
|
359
|
+
|
360
|
+
async function runFile(){ const p = pathEl.value.trim(); if(!p){ alert('Enter path'); return; } const text = cellsToText(cells);
|
361
|
+
// show loading state
|
362
|
+
setLoading(true);
|
363
|
+
try{
|
364
|
+
const res = await fetchJSON((API_BASE || '') + '/run', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({path: p, content: text})});
|
365
|
+
const newText = res.content || '';
|
366
|
+
cells = parseTextToCells(newText);
|
367
|
+
// After running, ensure trailing empty cell
|
368
|
+
ensureTrailingEmptyCell();
|
369
|
+
updateAndRender(); await renderFileList(); log('Ran and appended assistant reply to ' + p);
|
370
|
+
}catch(e){ log('Run failed:', e.message || e); alert('Run failed: ' + (e.message || e)); }
|
371
|
+
finally{ setLoading(false); }
|
372
|
+
}
|
373
|
+
|
374
|
+
function exportText(){ const t = cellsToText(cells); const w = window.open(); w.document.body.style.background='#fff'; w.document.title='Exported Chat'; const pre = w.document.createElement('pre'); pre.textContent = t; pre.style.whiteSpace='pre-wrap'; pre.style.fontFamily='monospace'; pre.style.padding='16px'; w.document.body.appendChild(pre); }
|
375
|
+
|
376
|
+
// Use marked for markdown rendering. If not available, fallback to a safe escaped render.
|
377
|
+
function escapeHtml(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
378
|
+
|
379
|
+
// Updated: showMarkdownModal now ensures the generated HTML is inserted as real DOM using marked if available
|
380
|
+
function showMarkdownModal(md, title){
|
381
|
+
mdTitle.textContent = title || 'Preview';
|
382
|
+
let html = '';
|
383
|
+
if(window.marked){
|
384
|
+
try{ html = marked.parse(md || ''); }catch(e){ html = escapeHtml(md || ''); }
|
385
|
+
} else {
|
386
|
+
html = '<pre style="white-space:pre-wrap">' + escapeHtml(md || '') + '</pre>';
|
387
|
+
loadMarked().then(m=>{ /* noop - next previews will use marked */ }).catch(e=>{ console.warn('Could not load marked:', e); });
|
388
|
+
}
|
389
|
+
|
390
|
+
html = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
|
391
|
+
|
392
|
+
mdBody.innerHTML = html;
|
393
|
+
mdBody.style.whiteSpace = 'normal';
|
394
|
+
mdModal.style.display = 'flex';
|
395
|
+
mdModal.setAttribute('aria-hidden','false');
|
396
|
+
}
|
397
|
+
function hideMarkdownModal(){ mdModal.style.display = 'none'; mdModal.setAttribute('aria-hidden','true'); }
|
398
|
+
mdClose.onclick = hideMarkdownModal;
|
399
|
+
mdModal.onclick = function(e){ if(e.target === mdModal) hideMarkdownModal(); };
|
400
|
+
|
401
|
+
// Copy modal content to clipboard (HTML + text) so it can be pasted into Word with formatting
|
402
|
+
async function copyMarkdownModal(){
|
403
|
+
const html = mdBody.innerHTML || '';
|
404
|
+
const text = mdBody.innerText || mdBody.textContent || '';
|
405
|
+
if(!navigator.clipboard){
|
406
|
+
try{
|
407
|
+
const temp = document.createElement('div'); temp.style.position='fixed'; temp.style.left='-10000px'; temp.innerText = text; document.body.appendChild(temp);
|
408
|
+
const range = document.createRange(); range.selectNodeContents(temp);
|
409
|
+
const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
410
|
+
document.execCommand('copy');
|
411
|
+
sel.removeAllRanges(); document.body.removeChild(temp);
|
412
|
+
log('Copied modal content as plain text (execCommand fallback)');
|
413
|
+
flashCopyBtn();
|
414
|
+
return;
|
415
|
+
}catch(e){ alert('Copy not supported: ' + e.message); return; }
|
416
|
+
}
|
417
|
+
|
418
|
+
if(window.ClipboardItem){
|
419
|
+
try{
|
420
|
+
const blobHtml = new Blob([html], {type: 'text/html'});
|
421
|
+
const blobText = new Blob([text], {type: 'text/plain'});
|
422
|
+
const item = new ClipboardItem({'text/html': blobHtml, 'text/plain': blobText});
|
423
|
+
await navigator.clipboard.write([item]);
|
424
|
+
log('Copied modal content (HTML + text)');
|
425
|
+
flashCopyBtn();
|
426
|
+
return;
|
427
|
+
}catch(e){ console.warn('ClipboardItem write failed, falling back to plain text:', e); }
|
428
|
+
}
|
429
|
+
|
430
|
+
try{
|
431
|
+
await navigator.clipboard.writeText(text);
|
432
|
+
log('Copied modal content as plain text (fallback)');
|
433
|
+
flashCopyBtn();
|
434
|
+
}catch(e){ alert('Copy failed: ' + e.message); }
|
435
|
+
}
|
436
|
+
|
437
|
+
function flashCopyBtn(){ if(!mdCopy) return; const old = mdCopy.textContent; mdCopy.textContent = 'Copied!'; mdCopy.disabled = true; setTimeout(()=>{ mdCopy.textContent = old; mdCopy.disabled = false; }, 1500); }
|
438
|
+
|
439
|
+
if(mdCopy) mdCopy.onclick = copyMarkdownModal;
|
440
|
+
|
441
|
+
function setLoading(on){ if(on){ loadingIndicator.style.display='inline-flex'; runBtn.disabled = true; runBtnBottom.disabled = true; saveBtn.disabled = true; } else { loadingIndicator.style.display='none'; runBtn.disabled = false; runBtnBottom.disabled = false; saveBtn.disabled = false; } }
|
442
|
+
|
443
|
+
// Wire events
|
444
|
+
newFileBtn.onclick = newFile;
|
445
|
+
loadBtn.onclick = loadFile;
|
446
|
+
saveBtn.onclick = saveFile;
|
447
|
+
runBtn.onclick = runFile;
|
448
|
+
runBtnBottom.onclick = runFile;
|
449
|
+
exportBtn.onclick = exportText;
|
450
|
+
listBtn && (listBtn.onclick = renderFileList);
|
451
|
+
deleteBtn && (deleteBtn.onclick = deletePath);
|
452
|
+
addUser.onclick = ()=>addCell('user');
|
453
|
+
addSystem.onclick = ()=>addCell('system');
|
454
|
+
addAssistant.onclick = ()=>addCell('assistant');
|
455
|
+
addOption.onclick = ()=>addOptionCell();
|
456
|
+
clearBtn.onclick = clearCells;
|
457
|
+
|
458
|
+
// Init
|
459
|
+
(async function init(){
|
460
|
+
await renderFileList();
|
461
|
+
try{ await loadMarked(); log('marked.js loaded'); }catch(e){ log('marked.js not available, falling back to plain preview'); }
|
462
|
+
cells = [{type:'role', role:'system', inline:false, content:'You are a helpful assistant.'}, {type:'role', role:'user', inline:false, content:'Tell me about genome editing.'}];
|
463
|
+
ensureTrailingEmptyCell();
|
464
|
+
updateAndRender();
|
465
|
+
log('Notebook editor ready (server-backed).');
|
466
|
+
})();
|
467
|
+
|
468
|
+
})();
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
2
|
+
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
3
|
+
|
4
|
+
class TestLLMAnthropic < Test::Unit::TestCase
|
5
|
+
def _test_say_hi
|
6
|
+
prompt =<<-EOF
|
7
|
+
user: say hi
|
8
|
+
EOF
|
9
|
+
sss 0
|
10
|
+
ppp LLM::Anthropic.ask prompt
|
11
|
+
end
|
12
|
+
|
13
|
+
def _test_ask
|
14
|
+
prompt =<<-EOF
|
15
|
+
user: write a script that sorts files in a directory
|
16
|
+
EOF
|
17
|
+
sss 0
|
18
|
+
ppp LLM::Anthropic.ask prompt
|
19
|
+
end
|
20
|
+
|
21
|
+
def __test_embeddings
|
22
|
+
Log.severity = 0
|
23
|
+
text =<<-EOF
|
24
|
+
Some text
|
25
|
+
EOF
|
26
|
+
emb = LLM::Anthropic.embed text, log_errors: true, model: 'embedding-model'
|
27
|
+
|
28
|
+
assert(Float === emb.first)
|
29
|
+
end
|
30
|
+
|
31
|
+
def _test_tool_call_output_2
|
32
|
+
Log.severity = 0
|
33
|
+
prompt =<<-EOF
|
34
|
+
function_call:
|
35
|
+
|
36
|
+
{"name":"get_current_temperature", "arguments":{"location":"London","unit":"Celsius"},"id":"tNTnsQq2s6jGh0npOh43AwDD"}
|
37
|
+
|
38
|
+
function_call_output:
|
39
|
+
|
40
|
+
{"id":"tNTnsQq2s6jGh0npOh43AwDD", "content":"It's 15 degrees and raining."}
|
41
|
+
|
42
|
+
user:
|
43
|
+
|
44
|
+
should i take an umbrella?
|
45
|
+
EOF
|
46
|
+
ppp LLM::Anthropic.ask prompt
|
47
|
+
end
|
48
|
+
|
49
|
+
def _test_tool_call_output_features
|
50
|
+
Log.severity = 0
|
51
|
+
prompt =<<-EOF
|
52
|
+
function_call:
|
53
|
+
|
54
|
+
{"name":"Baking-bake_muffin_tray","arguments":{},"id":"Baking_bake_muffin_tray_Default"}
|
55
|
+
|
56
|
+
function_call_output:
|
57
|
+
|
58
|
+
{"id":"Baking_bake_muffin_tray_Default","content":"Baking batter (Mixing base (Whisking eggs from share/pantry/eggs) with mixer (share/pantry/flour))"}
|
59
|
+
|
60
|
+
user:
|
61
|
+
|
62
|
+
How do you bake muffins, according to the tool I provided you. Don't
|
63
|
+
tell me the recipe you already know, use the tool call output. Let me
|
64
|
+
know if you didn't get it.
|
65
|
+
EOF
|
66
|
+
ppp LLM::Anthropic.ask prompt
|
67
|
+
end
|
68
|
+
|
69
|
+
def _test_tool_call_output_weather
|
70
|
+
Log.severity = 0
|
71
|
+
prompt =<<-EOF
|
72
|
+
function_call:
|
73
|
+
|
74
|
+
{"name":"get_current_temperature", "arguments":{"location":"London","unit":"Celsius"},"id":"tNTnsQq2s6jGh0npOh43AwDD"}
|
75
|
+
|
76
|
+
function_call_output:
|
77
|
+
|
78
|
+
{"id":"tNTnsQq2s6jGh0npOh43AwDD", "content":"It's 15 degrees and raining."}
|
79
|
+
|
80
|
+
user:
|
81
|
+
|
82
|
+
should i take an umbrella?
|
83
|
+
EOF
|
84
|
+
ppp LLM::Anthropic.ask prompt
|
85
|
+
end
|
86
|
+
|
87
|
+
def _test_tool
|
88
|
+
prompt =<<-EOF
|
89
|
+
user:
|
90
|
+
What is the weather in London. Should I take my umbrella?
|
91
|
+
EOF
|
92
|
+
|
93
|
+
tools = [
|
94
|
+
{
|
95
|
+
"type": "custom",
|
96
|
+
"name": "get_current_temperature",
|
97
|
+
"description": "Get the current temperature and raining conditions for a specific location",
|
98
|
+
"parameters": {
|
99
|
+
"type": "object",
|
100
|
+
"properties": {
|
101
|
+
"location": {
|
102
|
+
"type": "string",
|
103
|
+
"description": "The city and state, e.g., San Francisco, CA"
|
104
|
+
},
|
105
|
+
"unit": {
|
106
|
+
"type": "string",
|
107
|
+
"enum": ["Celsius", "Fahrenheit"],
|
108
|
+
"description": "The temperature unit to use. Infer this from the user's location."
|
109
|
+
}
|
110
|
+
},
|
111
|
+
"required": ["location", "unit"]
|
112
|
+
}
|
113
|
+
},
|
114
|
+
]
|
115
|
+
|
116
|
+
sss 0
|
117
|
+
respose = LLM::Anthropic.ask prompt, tools: tools, log_errors: true do |name,arguments|
|
118
|
+
"It's 15 degrees and raining."
|
119
|
+
end
|
120
|
+
|
121
|
+
ppp respose
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_json_output
|
125
|
+
prompt =<<-EOF
|
126
|
+
user:
|
127
|
+
|
128
|
+
What other movies have the protagonists of the original gost busters played on, just the top.
|
129
|
+
EOF
|
130
|
+
sss 0
|
131
|
+
ppp LLM::Anthropic.ask prompt, format: :json
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|