openaxies 1.0.3 → 1.1.0
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.
- package/package.json +1 -1
- package/src/App.js +238 -236
- package/src/agent/prompt.js +46 -0
- package/src/agent/tools.js +129 -0
- package/src/providers/index.js +100 -62
- package/src/providers/streaming.js +2 -2
- package/src/providers/websearch.js +1 -1
package/package.json
CHANGED
package/src/App.js
CHANGED
|
@@ -7,7 +7,7 @@ import { callModel } from './providers/index.js';
|
|
|
7
7
|
|
|
8
8
|
const h = React.createElement;
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const STARTUP = [
|
|
11
11
|
' \u2588\u2588\u2588\u2588\u2588\u2588\u2557',
|
|
12
12
|
' \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557',
|
|
13
13
|
' \u2588\u2588\u2551 \u2588\u2588\u2551',
|
|
@@ -15,13 +15,13 @@ const LOGO = [
|
|
|
15
15
|
' \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d',
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
{ id: 'model',
|
|
20
|
-
{ id: 'thoughts',
|
|
21
|
-
{ id: 'resume',
|
|
22
|
-
{ id: 'clear',
|
|
23
|
-
{ id: 'help',
|
|
24
|
-
{ id: 'exit',
|
|
18
|
+
const CMDS = [
|
|
19
|
+
{ id: 'model', t: '/model', d: 'Switch model' },
|
|
20
|
+
{ id: 'thoughts', t: '/thoughts', d: 'Toggle thinking' },
|
|
21
|
+
{ id: 'resume', t: '/resume', d: 'Re-run last query' },
|
|
22
|
+
{ id: 'clear', t: '/clear', d: 'Clear conversation' },
|
|
23
|
+
{ id: 'help', t: '/help', d: 'Show commands' },
|
|
24
|
+
{ id: 'exit', t: '/exit', d: 'Quit' },
|
|
25
25
|
];
|
|
26
26
|
|
|
27
27
|
export default function AppRoot() {
|
|
@@ -29,304 +29,306 @@ export default function AppRoot() {
|
|
|
29
29
|
const { exit } = useApp();
|
|
30
30
|
const cols = stdout.columns || 80;
|
|
31
31
|
const rows = stdout.rows || 24;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const [
|
|
35
|
-
const [
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
38
|
-
const [
|
|
39
|
-
const [
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
const [
|
|
43
|
-
const [
|
|
44
|
-
const [
|
|
45
|
-
const [
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
32
|
+
const WID = Math.min(cols - 4, 56);
|
|
33
|
+
|
|
34
|
+
const [msgs, setMsgs] = React.useState([]);
|
|
35
|
+
const [model, setModel] = React.useState(DEFAULT_MODEL_ID);
|
|
36
|
+
const [inp, setInp] = React.useState('');
|
|
37
|
+
const [busy, setBusy] = React.useState(false);
|
|
38
|
+
const [stream, setStream] = React.useState('');
|
|
39
|
+
const [think, setThink] = React.useState(false);
|
|
40
|
+
const [thinkTxt, setThinkTxt] = React.useState('');
|
|
41
|
+
const [thinkSec, setThinkSec] = React.useState(0);
|
|
42
|
+
const [ready, setReady] = React.useState(false);
|
|
43
|
+
const [overlay, setOverlay] = React.useState(false);
|
|
44
|
+
const [ovIdx, setOvIdx] = React.useState(0);
|
|
45
|
+
const [showT, setShowT] = React.useState(true);
|
|
46
|
+
const [tool, setTool] = React.useState(null);
|
|
47
|
+
const [pendingQ, setPendingQ] = React.useState(false);
|
|
48
|
+
const [qText, setQText] = React.useState('');
|
|
49
|
+
const [todos, setTodos] = React.useState([]);
|
|
50
|
+
const [toolCalls, setToolCalls] = React.useState([]);
|
|
51
|
+
|
|
52
|
+
const acRef = React.useRef(null);
|
|
53
|
+
const accRef = React.useRef('');
|
|
54
|
+
const lastRef = React.useRef('');
|
|
55
|
+
const qResolveRef = React.useRef(null);
|
|
56
|
+
const genRef = React.useRef(null);
|
|
50
57
|
|
|
51
58
|
React.useEffect(() => {
|
|
52
|
-
if (!
|
|
53
|
-
const t = setInterval(() =>
|
|
59
|
+
if (!think) return;
|
|
60
|
+
const t = setInterval(() => setThinkSec(p => p + 0.1), 100);
|
|
54
61
|
return () => clearInterval(t);
|
|
55
|
-
}, [
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const label =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
}, [think]);
|
|
63
|
+
|
|
64
|
+
const mi = getModelById(model);
|
|
65
|
+
const label = mi ? mi.label.replace('OpenAxies ', '') : 'llama';
|
|
66
|
+
|
|
67
|
+
function strip(s) { return showT ? s : s.split('<think>').join('').split('</think>').join(''); }
|
|
68
|
+
|
|
69
|
+
function headBox() {
|
|
70
|
+
const R = '\u2502';
|
|
71
|
+
const D = '\u2500';
|
|
72
|
+
const W = WID;
|
|
73
|
+
const hd = function (s) { return R + ' ' + s + ' '.repeat(Math.max(0, W - s.length)) + ' ' + R; };
|
|
74
|
+
return h(Box, { flexShrink: 0, flexDirection: 'column', paddingLeft: 1, paddingRight: 1 },
|
|
75
|
+
h(Text, { color: '#555577' }, '\u250C' + D.repeat(W + 2) + '\u2510'),
|
|
76
|
+
h(Text, { color: '#555577' }, hd('\u203A_ OpenAxies (v1.1.0)')),
|
|
77
|
+
h(Text, { color: '#555577' }, hd('')),
|
|
78
|
+
h(Text, { color: '#555577' }, hd('model: ' + label + ' /model to change')),
|
|
79
|
+
h(Text, { color: '#555577' }, hd('directory: ~')),
|
|
80
|
+
h(Text, { color: '#555577' }, '\u2514' + D.repeat(W + 2) + '\u2518'),
|
|
81
|
+
);
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
async function
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setMessages(p => [...p, { role: 'user', content: s }]);
|
|
77
|
-
setInput('');
|
|
78
|
-
|
|
79
|
-
const m = getModelById(activeModel);
|
|
80
|
-
if (!m) { setMessages(p => [...p, { role: 'assistant', content: 'No model loaded.' }]); setStreaming(false); return; }
|
|
81
|
-
|
|
84
|
+
async function submit(s) {
|
|
85
|
+
const t = typeof s === 'string' ? s.trim() : '';
|
|
86
|
+
if (!t || busy) return;
|
|
87
|
+
setReady(true); lastRef.current = t; accRef.current = '';
|
|
88
|
+
setBusy(true); setStream(''); setThinkTxt(''); setThinkSec(0); setTool(null);
|
|
89
|
+
setToolCalls([]);
|
|
90
|
+
setMsgs(p => [...p, { role: 'user', c: t }]);
|
|
91
|
+
setInp('');
|
|
92
|
+
|
|
93
|
+
const m = getModelById(model);
|
|
94
|
+
if (!m) { setMsgs(p => [...p, { role: 'asst', c: 'No model loaded.' }]); setBusy(false); return; }
|
|
82
95
|
const ac = new AbortController();
|
|
83
|
-
|
|
84
|
-
let
|
|
96
|
+
acRef.current = ac;
|
|
97
|
+
let op = false;
|
|
85
98
|
|
|
86
99
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (ev.type === '
|
|
97
|
-
|
|
100
|
+
const hist = msgs.map(x => ({ role: x.role === 'user' ? 'user' : 'assistant', content: x.c || '' }));
|
|
101
|
+
hist.push({ role: 'user', content: t });
|
|
102
|
+
|
|
103
|
+
const gen = callModel({ id: model }, hist, ac.signal);
|
|
104
|
+
genRef.current = gen;
|
|
105
|
+
let result = await gen.next();
|
|
106
|
+
|
|
107
|
+
while (!result.done && !ac.signal.aborted) {
|
|
108
|
+
const ev = result.value;
|
|
109
|
+
if (ev.type === 'question') {
|
|
110
|
+
setPendingQ(true); setQText(ev.text);
|
|
111
|
+
const answer = await new Promise(resolve => { qResolveRef.current = resolve; });
|
|
112
|
+
setPendingQ(false); setQText('');
|
|
113
|
+
result = await gen.next(answer);
|
|
98
114
|
continue;
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
if (ev.type === 'tool_call') {
|
|
117
|
+
setToolCalls(p => [...p, { tool: ev.tool, args: ev.args }]);
|
|
118
|
+
setTool({ tool: ev.tool, query: ev.args?.query || '', sites: 0 });
|
|
119
|
+
result = await gen.next();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ev.type === 'tool_result') {
|
|
123
|
+
setToolCalls(p => [...p, { tool: ev.tool, result: ev.result, done: true }]);
|
|
124
|
+
result = await gen.next();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ev.type === 'todo_add') {
|
|
128
|
+
setTodos(p => [...p, { text: ev.text, status: 'pending' }]);
|
|
129
|
+
result = await gen.next();
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ev.type === 'todo_done') {
|
|
133
|
+
setTodos(p => p.map((td, i) => i === ev.id ? { ...td, status: 'done' } : td));
|
|
134
|
+
result = await gen.next();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ev.type === 'token') {
|
|
102
138
|
const c = typeof ev.content === 'string' ? ev.content : '';
|
|
103
|
-
if (!c) continue;
|
|
104
|
-
if (c.includes('<think>'))
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
thinkOpen = false;
|
|
115
|
-
setThinkingActive(false);
|
|
116
|
-
setStreamText(clean);
|
|
117
|
-
setThinkingText('');
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
setStreamText(clean);
|
|
121
|
-
}
|
|
139
|
+
if (!c) { result = await gen.next(); continue; }
|
|
140
|
+
if (c.includes('<think>')) op = true;
|
|
141
|
+
accRef.current += c;
|
|
142
|
+
const cl = strip(accRef.current);
|
|
143
|
+
if (op || accRef.current.includes('<think>')) {
|
|
144
|
+
const st = op || !accRef.current.includes('</think>');
|
|
145
|
+
if (st) { setThink(true); setThinkTxt(cl); setStream(''); }
|
|
146
|
+
else { op = false; setThink(false); setStream(cl); setThinkTxt(''); }
|
|
147
|
+
} else setStream(cl);
|
|
148
|
+
result = await gen.next();
|
|
149
|
+
continue;
|
|
122
150
|
}
|
|
123
151
|
if (ev.type === 'done' || ev.type === 'timeout') {
|
|
124
|
-
if (ev.type === 'timeout' &&
|
|
152
|
+
if (ev.type === 'timeout' && accRef.current) accRef.current += '\n\u2014timed out\u2014';
|
|
125
153
|
break;
|
|
126
154
|
}
|
|
155
|
+
result = await gen.next();
|
|
127
156
|
}
|
|
128
|
-
} catch (
|
|
129
|
-
if (!ac.signal.aborted)
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (!ac.signal.aborted) setMsgs(p => [...p, { role: 'asst', c: 'Error: ' + (e.message || 'connection') }]);
|
|
130
159
|
} finally {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
setStreamText('');
|
|
137
|
-
setThinkingText('');
|
|
160
|
+
setBusy(false); setThink(false); setPendingQ(false);
|
|
161
|
+
acRef.current = null; genRef.current = null;
|
|
162
|
+
const f = accRef.current;
|
|
163
|
+
if (f) setMsgs(p => [...p, { role: 'asst', c: strip(f) }]);
|
|
164
|
+
setStream(''); setThinkTxt('');
|
|
138
165
|
}
|
|
139
166
|
}
|
|
140
167
|
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
let f = false;
|
|
145
|
-
for (const m of models) { if (f) return m.id; if (m.id === p) f = true; }
|
|
146
|
-
return models[0].id;
|
|
147
|
-
});
|
|
168
|
+
function cycle() {
|
|
169
|
+
const ms = getModels();
|
|
170
|
+
setModel(p => { let f = false; for (const m of ms) { if (f) return m.id; if (m.id === p) f = true; } return ms[0].id; });
|
|
148
171
|
}
|
|
149
172
|
|
|
150
|
-
function
|
|
173
|
+
function cmd(item) {
|
|
151
174
|
if (!item) return;
|
|
152
|
-
|
|
153
|
-
if (t === '/
|
|
154
|
-
if (t === '/
|
|
155
|
-
if (t === '/
|
|
156
|
-
if (t === '/
|
|
157
|
-
if (t === '/
|
|
158
|
-
|
|
159
|
-
setInput(''); setShowOverlay(false); setOverlayIdx(0);
|
|
175
|
+
if (item.t === '/exit') { exit(); return; }
|
|
176
|
+
if (item.t === '/clear') { setMsgs([]); setTodos([]); }
|
|
177
|
+
if (item.t === '/model') cycle();
|
|
178
|
+
if (item.t === '/thoughts') setShowT(p => !p);
|
|
179
|
+
if (item.t === '/resume') { const q = lastRef.current; if (q && !busy) submit(q); return; }
|
|
180
|
+
if (item.t === '/help') setMsgs(p => [...p, { role: 'asst', c: 'Commands: /help /clear /model /thoughts /resume /exit' }]);
|
|
181
|
+
setInp(''); setOverlay(false); setOvIdx(0);
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
useInput((
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
if (key.return) { execCmd(COMMANDS[overlayIdx]); return; }
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (!startupDone && !streaming) {
|
|
171
|
-
if (key.return) { setStartupDone(true); setInput(''); return; }
|
|
184
|
+
useInput((i, k) => {
|
|
185
|
+
if (k.ctrl && (i === 'c' || i === 'C')) { exit(); return; }
|
|
186
|
+
if (k.escape && pendingQ) {
|
|
187
|
+
if (acRef.current) acRef.current.abort();
|
|
188
|
+
setPendingQ(false); setQText(''); setBusy(false); setStream(''); setThink(false);
|
|
172
189
|
return;
|
|
173
190
|
}
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
if (pendingQ) return;
|
|
192
|
+
if (overlay) {
|
|
193
|
+
if (k.escape) { setOverlay(false); setInp(''); setOvIdx(0); return; }
|
|
194
|
+
if (k.upArrow) setOvIdx(p => p > 0 ? p - 1 : CMDS.length - 1);
|
|
195
|
+
if (k.downArrow) setOvIdx(p => p < CMDS.length - 1 ? p + 1 : 0);
|
|
196
|
+
if (k.return) cmd(CMDS[ovIdx]);
|
|
177
197
|
return;
|
|
178
198
|
}
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
185
|
-
if (
|
|
199
|
+
if (!ready && !busy) { if (k.return) { setReady(true); setInp(''); } return; }
|
|
200
|
+
if (k.escape && busy) { if (acRef.current) acRef.current.abort(); setBusy(false); setStream(''); setThink(false); setPendingQ(false); return; }
|
|
201
|
+
if (k.ctrl) {
|
|
202
|
+
if (i === 'c' || i === 'C') { exit(); return; }
|
|
203
|
+
if (i === 't' || i === 'T') { setShowT(p => !p); return; }
|
|
204
|
+
if (i === 'p' || i === 'P') { cycle(); return; }
|
|
205
|
+
if (i === 'r' || i === 'R') { const q = lastRef.current; if (q && !busy) submit(q); return; }
|
|
206
|
+
if (i === 'l' || i === 'L') { setMsgs([]); setTodos([]); return; }
|
|
207
|
+
if (i === 'k' || i === 'K') { setInp('/'); setOverlay(true); setOvIdx(0); return; }
|
|
186
208
|
}
|
|
187
209
|
});
|
|
188
210
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
for (const msg of messages) {
|
|
192
|
-
if (msg.role === 'user' || msg.role === 'assistant') entries.push(msg);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (toolEv && streaming) entries.push({ role: 'tool', tool: toolEv.tool, query: toolEv.query, sites: toolEv.sites });
|
|
196
|
-
if (thinkingActive && thinkingText) entries.push({ role: 'thought', content: thinkingText, time: thinkTime.toFixed(1) + 's' });
|
|
197
|
-
if (streaming && streamText && !thinkingActive) entries.push({ role: 'ongoing', content: streamText });
|
|
198
|
-
|
|
199
|
-
const MAX_CHAT = Math.max(3, rows - 10);
|
|
200
|
-
const vis = entries.slice(-MAX_CHAT);
|
|
201
|
-
|
|
202
|
-
// Startup screen
|
|
203
|
-
if (!startupDone && messages.length === 0) {
|
|
211
|
+
// Startup
|
|
212
|
+
if (!ready && msgs.length === 0) {
|
|
204
213
|
return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
|
|
205
|
-
h(Box, { height:
|
|
206
|
-
...
|
|
207
|
-
h(Text, { color: '#00BBFF', bold: true }, l)
|
|
208
|
-
)),
|
|
214
|
+
h(Box, { height: 3 }),
|
|
215
|
+
...STARTUP.map((l, i) => h(Box, { key: 'l' + i, height: 1, paddingLeft: 2 }, h(Text, { color: '#00BBFF', bold: true }, l))),
|
|
209
216
|
h(Box, { height: 1 }),
|
|
210
217
|
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#FFFFFF', bold: true }, 'OpenAxies')),
|
|
211
|
-
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#777788' }, '
|
|
218
|
+
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#777788' }, 'autonomous agent')),
|
|
212
219
|
h(Box, { height: 2 }),
|
|
213
220
|
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#555577' }, 'Press Enter to begin')),
|
|
214
221
|
h(Box, { flexGrow: 1 }),
|
|
215
|
-
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#333344' }, 'esc
|
|
222
|
+
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#333344' }, 'esc quit ctrl+p models /help')),
|
|
216
223
|
);
|
|
217
224
|
}
|
|
218
225
|
|
|
219
|
-
//
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
function brdr(line) {
|
|
225
|
-
return '\u2502 ' + line + ' '.repeat(Math.max(0, W + 1 - line.length)) + '\u2502';
|
|
226
|
+
// Build entries
|
|
227
|
+
const entries = [];
|
|
228
|
+
for (const m of msgs) {
|
|
229
|
+
if (m.role === 'user') entries.push({ t: 'user', c: m.c });
|
|
230
|
+
else if (m.role === 'asst') entries.push({ t: 'asst', c: m.c });
|
|
226
231
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const line1 = brdr(' ' + LOGO[0] + ' OpenAxies');
|
|
230
|
-
const line2 = brdr(' ' + LOGO[1] + ' model: ' + label);
|
|
231
|
-
const line3 = brdr(' ' + LOGO[2] + ' dir: ~');
|
|
232
|
-
const line4 = brdr(' ' + LOGO[3] + ' cmds: /model /help /exit');
|
|
233
|
-
const line5 = brdr(' ' + LOGO[4]);
|
|
234
|
-
|
|
235
|
-
const header = h(Box, { flexShrink: 0, flexDirection: 'column', paddingLeft: 1, paddingRight: 1 },
|
|
236
|
-
h(Text, { color: '#555577' }, borderT),
|
|
237
|
-
h(Text, { color: '#555577' }, pad),
|
|
238
|
-
h(Text, { color: '#555577' }, line1),
|
|
239
|
-
h(Text, { color: '#555577' }, line2),
|
|
240
|
-
h(Text, { color: '#555577' }, line3),
|
|
241
|
-
h(Text, { color: '#555577' }, line4),
|
|
242
|
-
h(Text, { color: '#555577' }, line5),
|
|
243
|
-
h(Text, { color: '#555577' }, pad),
|
|
244
|
-
h(Text, { color: '#555577' }, borderB),
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
// Overlay
|
|
248
|
-
if (showOverlay) {
|
|
249
|
-
vis.unshift({ role: '_ocmd', idx: overlayIdx });
|
|
232
|
+
for (const tc of toolCalls) {
|
|
233
|
+
if (tc.done) entries.push({ t: 'tool', tool: tc.tool, result: tc.result });
|
|
250
234
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
vis.push({ role: '_sep' });
|
|
255
|
-
vis.push({ role: '_idle' });
|
|
235
|
+
if (tool && busy) entries.push({ t: 'tool', tool: tool.tool, q: tool.query, sites: tool.sites });
|
|
236
|
+
for (const td of todos) {
|
|
237
|
+
entries.push({ t: 'todo', text: td.text, status: td.status });
|
|
256
238
|
}
|
|
239
|
+
if (think && thinkTxt) entries.push({ t: 'thought', c: thinkTxt, sec: thinkSec.toFixed(1) + 's' });
|
|
240
|
+
if (busy && stream && !think) entries.push({ t: 'asst', c: stream });
|
|
241
|
+
if (pendingQ) entries.push({ t: 'question', c: qText });
|
|
242
|
+
if (overlay) entries.push({ t: '_ov', idx: ovIdx });
|
|
243
|
+
|
|
244
|
+
const maxChat = Math.max(3, rows - 7);
|
|
245
|
+
const vis = entries.slice(-maxChat);
|
|
257
246
|
|
|
258
|
-
// Render
|
|
259
|
-
const
|
|
247
|
+
// Render chat
|
|
248
|
+
const chEls = [];
|
|
260
249
|
for (let i = 0; i < vis.length; i++) {
|
|
261
250
|
const e = vis[i];
|
|
262
251
|
const k = 'e' + i;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const sel = ci === e.idx;
|
|
271
|
-
els.push(h(Box, { key: k + 'c' + ci, height: 1, paddingLeft: 3 },
|
|
272
|
-
h(Text, { color: sel ? hex.neonBlue : '#666688' }, (sel ? '\u276F ' : ' ') + c.trigger),
|
|
273
|
-
h(Text, { color: '#444466' }, ' ' + (c.desc || ''))
|
|
252
|
+
if (e.t === '_ov') {
|
|
253
|
+
chEls.push(h(Box, { key: k + 'h', height: 1, paddingLeft: 2 }, h(Text, { color: '#888899', bold: true }, 'Commands:')));
|
|
254
|
+
for (let ci = 0; ci < CMDS.length; ci++) {
|
|
255
|
+
const c = CMDS[ci]; const sel = ci === e.idx;
|
|
256
|
+
chEls.push(h(Box, { key: k + 'c' + ci, height: 1, paddingLeft: 3 },
|
|
257
|
+
h(Text, { color: sel ? hex.neonBlue : '#666688' }, (sel ? '\u276F ' : ' ') + c.t),
|
|
258
|
+
h(Text, { color: '#444466' }, ' ' + (c.d || ''))
|
|
274
259
|
));
|
|
275
260
|
}
|
|
276
|
-
} else if (e.
|
|
277
|
-
|
|
261
|
+
} else if (e.t === 'user') {
|
|
262
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
278
263
|
h(Text, { color: '#888899' }, '\u203A '),
|
|
279
|
-
h(Text, { color: hex.neonBlue, wrap: 'wrap' }, e.
|
|
264
|
+
h(Text, { color: hex.neonBlue, wrap: 'wrap' }, e.c),
|
|
280
265
|
));
|
|
281
|
-
} else if (e.
|
|
282
|
-
|
|
266
|
+
} else if (e.t === 'asst') {
|
|
267
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
283
268
|
h(Text, { color: '#88FF88' }, '\u2022 '),
|
|
284
|
-
h(Text, { color: '#FFFFFF', wrap: 'wrap' }, e.
|
|
269
|
+
h(Text, { color: '#FFFFFF', wrap: 'wrap' }, e.c),
|
|
285
270
|
));
|
|
286
|
-
} else if (e.
|
|
287
|
-
|
|
288
|
-
|
|
271
|
+
} else if (e.t === 'todo') {
|
|
272
|
+
const mark = e.status === 'done' ? '\u2713' : '\u25A1';
|
|
273
|
+
const color = e.status === 'done' ? '#44AA44' : '#888899';
|
|
274
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
275
|
+
h(Text, { color: color }, '\u2022 ' + mark + ' ' + e.text),
|
|
289
276
|
));
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
} else if (e.
|
|
296
|
-
|
|
297
|
-
h(Text, { color: '#5B5B8A' }, '\u2022 ' + (e.tool || 'tool') + (e.
|
|
277
|
+
} else if (e.t === 'thought') {
|
|
278
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
279
|
+
h(Text, { color: '#FF9F43', bold: true }, '\u2022 Thinking \u2022 ' + e.sec),
|
|
280
|
+
));
|
|
281
|
+
if (e.c) chEls.push(h(Box, { key: k + 'c', height: 1, paddingLeft: 4 }, h(Text, { color: '#FF9F43', wrap: 'wrap' }, e.c)));
|
|
282
|
+
} else if (e.t === 'tool') {
|
|
283
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
284
|
+
h(Text, { color: '#5B5B8A' }, '\u2022 ' + (e.tool || 'tool') + (e.q ? ' ' + e.q : '') + (e.sites ? ' (' + e.sites + ' sites)' : '') + (e.result ? ' \u2713' : ' \u2026')),
|
|
285
|
+
));
|
|
286
|
+
} else if (e.t === 'question') {
|
|
287
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
288
|
+
h(Text, { color: '#FFD700' }, '\u2753 '),
|
|
289
|
+
h(Text, { color: '#FFD700', wrap: 'wrap' }, e.c),
|
|
298
290
|
));
|
|
299
291
|
}
|
|
300
292
|
}
|
|
301
293
|
|
|
302
|
-
|
|
294
|
+
if (chEls.length === 0) {
|
|
295
|
+
chEls.push(h(Box, { key: 'idle', height: 1, paddingLeft: 2 }, h(Text, { color: '#666688' }, '\u203A Ask me anything')));
|
|
296
|
+
}
|
|
303
297
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
298
|
+
const chatArea = h(Box, { flexGrow: 1, flexDirection: 'column', overflow: 'hidden' }, ...chEls);
|
|
299
|
+
|
|
300
|
+
// Input line
|
|
301
|
+
const inputBar = h(Box, { height: 1, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
|
|
302
|
+
h(Text, { color: pendingQ ? '#FFD700' : '#888899' }, pendingQ ? '\u2753 ' : '\u203A '),
|
|
303
|
+
pendingQ ? h(Text, { color: '#FFFFFF' }, '\u258C') : h(Text, { color: hex.greenOnline }, '\u258C'),
|
|
308
304
|
h(Box, { width: 1 }),
|
|
309
305
|
h(TextInput, {
|
|
310
|
-
value:
|
|
311
|
-
onChange:
|
|
312
|
-
onSubmit: v => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
306
|
+
value: inp,
|
|
307
|
+
onChange: setInp,
|
|
308
|
+
onSubmit: v => {
|
|
309
|
+
if (pendingQ && qResolveRef.current) {
|
|
310
|
+
qResolveRef.current(v);
|
|
311
|
+
setInp('');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (typeof v === 'string' && v.trim() && !busy) submit(v);
|
|
315
|
+
},
|
|
316
|
+
placeholder: pendingQ ? '' : '',
|
|
317
|
+
focus: !overlay,
|
|
318
|
+
}),
|
|
317
319
|
);
|
|
318
320
|
|
|
319
|
-
// Footer
|
|
321
|
+
// Footer
|
|
320
322
|
const footer = h(Box, { height: 1, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
|
|
321
323
|
h(Text, { color: '#444466' }, 'esc \u2248 ctrl+t ctrl+p ctrl+k'),
|
|
322
324
|
h(Box, { flexGrow: 1 }),
|
|
323
|
-
h(Text, { color: '#555577' }, label.toLowerCase() + ' \u2022
|
|
325
|
+
h(Text, { color: '#555577' }, label.toLowerCase() + ' \u2022 ~'),
|
|
324
326
|
);
|
|
325
327
|
|
|
326
328
|
return h(Box, { flexDirection: 'column', width: '100%', height: rows, overflow: 'hidden' },
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
headBox(),
|
|
330
|
+
inputBar,
|
|
331
|
+
chatArea,
|
|
330
332
|
footer,
|
|
331
333
|
);
|
|
332
334
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TOOL_DEFS } from './tools.js';
|
|
2
|
+
|
|
3
|
+
export function buildSystemPrompt(cwd) {
|
|
4
|
+
const toolLines = TOOL_DEFS.map(t => {
|
|
5
|
+
const args = Object.entries(t.args).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
6
|
+
return `- ${t.name}(${args}): ${t.desc}`;
|
|
7
|
+
}).join('\n');
|
|
8
|
+
|
|
9
|
+
return `You are OpenAxies, an autonomous AI coding agent running in the terminal.
|
|
10
|
+
|
|
11
|
+
You have access to tools. When you need to gather information, modify files, or interact with the system, call a tool.
|
|
12
|
+
|
|
13
|
+
TOOLS:
|
|
14
|
+
${toolLines}
|
|
15
|
+
|
|
16
|
+
IMPORTANT RULES:
|
|
17
|
+
1. When you need to use a tool, respond with EXACTLY this format (no extra text before/after):
|
|
18
|
+
Action: tool_name
|
|
19
|
+
Action Input: {"arg1": "value1", "arg2": "value2"}
|
|
20
|
+
|
|
21
|
+
2. After each tool result (Observation), decide what to do next.
|
|
22
|
+
|
|
23
|
+
3. When you have enough information to respond to the user, use:
|
|
24
|
+
Final Answer: your complete response here
|
|
25
|
+
|
|
26
|
+
4. You can call multiple tools sequentially. Each tool call is followed by an Observation.
|
|
27
|
+
|
|
28
|
+
5. Current working directory: ${cwd}
|
|
29
|
+
|
|
30
|
+
6. For coding tasks:
|
|
31
|
+
- Read files before editing them
|
|
32
|
+
- Use glob/grep to find relevant files
|
|
33
|
+
- Write clear, correct code
|
|
34
|
+
- Run bash commands to verify your work (tests, lint, etc.)
|
|
35
|
+
- Edit files with edit_file (search/replace) when possible
|
|
36
|
+
|
|
37
|
+
7. For web searches, use web_search to get current information.
|
|
38
|
+
|
|
39
|
+
8. If you need user input, use ask_question.
|
|
40
|
+
|
|
41
|
+
9. Track your progress with todo_add and todo_done.
|
|
42
|
+
|
|
43
|
+
10. Keep your Final Answer concise and helpful. Show the user what you did.
|
|
44
|
+
|
|
45
|
+
Remember: only call ONE tool at a time. Wait for the Observation before deciding next step.`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { searchWeb } from '../providers/websearch.js';
|
|
5
|
+
|
|
6
|
+
function r(s) { return typeof s === 'string' ? s : ''; }
|
|
7
|
+
|
|
8
|
+
function simpleGlob(pattern) {
|
|
9
|
+
const parts = pattern.split('/');
|
|
10
|
+
const hasStar = parts.some(p => p.includes('*'));
|
|
11
|
+
if (!hasStar) return fs.existsSync(pattern) ? [pattern] : [];
|
|
12
|
+
const root = pattern.startsWith('/') ? '/' : '.';
|
|
13
|
+
const results = [];
|
|
14
|
+
function walk(dir, idx) {
|
|
15
|
+
if (idx >= parts.length) { results.push(dir); return; }
|
|
16
|
+
const part = parts[idx];
|
|
17
|
+
if (part.includes('**')) {
|
|
18
|
+
walk(dir, idx + 1);
|
|
19
|
+
try { for (const e of fs.readdirSync(dir)) { const p = dir + '/' + e; if (fs.statSync(p).isDirectory()) walk(p, idx); } } catch {}
|
|
20
|
+
} else if (part.includes('*')) {
|
|
21
|
+
const re = new RegExp('^' + part.replace(/\*/g, '.*') + '$');
|
|
22
|
+
try { for (const e of fs.readdirSync(dir)) { if (re.test(e)) walk(dir + '/' + e, idx + 1); } } catch {}
|
|
23
|
+
} else {
|
|
24
|
+
const p = dir + '/' + part;
|
|
25
|
+
if (fs.existsSync(p)) walk(p, idx + 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
walk(root, 0);
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const TOOL_DEFS = [
|
|
33
|
+
{ name: 'read_file', args: { path: 'string' }, desc: 'Read a file from the filesystem' },
|
|
34
|
+
{ name: 'write_file', args: { path: 'string', content: 'string' }, desc: 'Write content to a file (overwrites)' },
|
|
35
|
+
{ name: 'edit_file', args: { path: 'string', old_string: 'string', new_string: 'string' }, desc: 'Edit a file by replacing old_string with new_string' },
|
|
36
|
+
{ name: 'glob', args: { pattern: 'string' }, desc: 'Find files matching a glob pattern (e.g. "src/**/*.js")' },
|
|
37
|
+
{ name: 'grep', args: { pattern: 'string', path: 'string' }, desc: 'Search file contents with a regex pattern' },
|
|
38
|
+
{ name: 'bash', args: { command: 'string' }, desc: 'Execute a shell command and get output' },
|
|
39
|
+
{ name: 'web_search', args: { query: 'string' }, desc: 'Search the web for current information' },
|
|
40
|
+
{ name: 'ask_question', args: { question: 'string' }, desc: 'Ask the user a question when you need clarification' },
|
|
41
|
+
{ name: 'todo_add', args: { text: 'string' }, desc: 'Add a task to the todo list' },
|
|
42
|
+
{ name: 'todo_done', args: { id: 'number' }, desc: 'Mark a todo item as complete by its index (0-based)' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function parseToolCall(text) {
|
|
46
|
+
const s = r(text);
|
|
47
|
+
const m = s.match(/Action:\s*(\w+)\s*\nAction Input:\s*(\{[\s\S]*?\}|"[^"]*"|`[^`]*`|\S+)/);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
const name = m[1];
|
|
50
|
+
let raw = m[2].trim();
|
|
51
|
+
let args = {};
|
|
52
|
+
try {
|
|
53
|
+
if (raw.startsWith('{')) args = JSON.parse(raw);
|
|
54
|
+
else if (raw.startsWith('"')) args = { query: JSON.parse(raw) };
|
|
55
|
+
else if (raw.startsWith('`')) args = { command: raw.slice(1, -1) };
|
|
56
|
+
else args = { path: raw };
|
|
57
|
+
} catch { return null; }
|
|
58
|
+
return { name, args };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function execTool(toolCall, signal, questionCb) {
|
|
62
|
+
const { name, args } = toolCall;
|
|
63
|
+
try {
|
|
64
|
+
switch (name) {
|
|
65
|
+
case 'read_file': {
|
|
66
|
+
const p = r(args.path);
|
|
67
|
+
if (!fs.existsSync(p)) return 'Error: file not found: ' + p;
|
|
68
|
+
const c = fs.readFileSync(p, 'utf8');
|
|
69
|
+
return 'File ' + p + ':\n' + c.slice(0, 8000) + (c.length > 8000 ? '\n... [truncated]' : '');
|
|
70
|
+
}
|
|
71
|
+
case 'write_file': {
|
|
72
|
+
const p = r(args.path);
|
|
73
|
+
const d = path.dirname(p);
|
|
74
|
+
if (d && !fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
75
|
+
fs.writeFileSync(p, r(args.content));
|
|
76
|
+
const lines = r(args.content).split('\n').length;
|
|
77
|
+
return 'Wrote ' + p + ' (' + lines + ' lines)';
|
|
78
|
+
}
|
|
79
|
+
case 'edit_file': {
|
|
80
|
+
const p = r(args.path);
|
|
81
|
+
if (!fs.existsSync(p)) return 'Error: file not found: ' + p;
|
|
82
|
+
let c = fs.readFileSync(p, 'utf8');
|
|
83
|
+
const old = r(args.old_string);
|
|
84
|
+
const nw = r(args.new_string);
|
|
85
|
+
if (!c.includes(old)) return 'Error: old_string not found in ' + p;
|
|
86
|
+
c = c.replace(old, nw);
|
|
87
|
+
fs.writeFileSync(p, c);
|
|
88
|
+
return 'Edited ' + p;
|
|
89
|
+
}
|
|
90
|
+
case 'glob': {
|
|
91
|
+
const results = simpleGlob(r(args.pattern));
|
|
92
|
+
return results.length > 0 ? results.join('\n') : 'No files matched: ' + r(args.pattern);
|
|
93
|
+
}
|
|
94
|
+
case 'grep': {
|
|
95
|
+
const pat = r(args.pattern).replace(/"/g, '\\"');
|
|
96
|
+
const pth = r(args.path) || '.';
|
|
97
|
+
try {
|
|
98
|
+
const out = execSync('rg -l "' + pat + '" ' + pth + ' 2>/dev/null || true', { encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
99
|
+
return out.trim() || 'No matches for: ' + r(args.pattern);
|
|
100
|
+
} catch { return 'No matches for: ' + r(args.pattern); }
|
|
101
|
+
}
|
|
102
|
+
case 'bash': {
|
|
103
|
+
const out = execSync(r(args.command), { encoding: 'utf8', maxBuffer: 1024 * 1024, timeout: 30000 });
|
|
104
|
+
return out.slice(0, 4000) || '(no output)';
|
|
105
|
+
}
|
|
106
|
+
case 'web_search': {
|
|
107
|
+
const res = await searchWeb(r(args.query), signal);
|
|
108
|
+
return res.sites > 0 ? 'Search results for "' + r(args.query) + '":\n' + res.summary : 'No search results';
|
|
109
|
+
}
|
|
110
|
+
case 'ask_question': {
|
|
111
|
+
if (typeof questionCb === 'function') {
|
|
112
|
+
const answer = await questionCb(r(args.question));
|
|
113
|
+
return 'User answered: ' + answer;
|
|
114
|
+
}
|
|
115
|
+
return 'Error: no question handler';
|
|
116
|
+
}
|
|
117
|
+
case 'todo_add': {
|
|
118
|
+
return 'Todo added: ' + r(args.text);
|
|
119
|
+
}
|
|
120
|
+
case 'todo_done': {
|
|
121
|
+
return 'Todo ' + args.id + ' marked done';
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return 'Error: unknown tool "' + name + '"';
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return 'Error: ' + (e.message || String(e));
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -1,104 +1,142 @@
|
|
|
1
|
+
import { StateGraph, START, END } from '@langchain/langgraph';
|
|
1
2
|
import { streamResponse } from './streaming.js';
|
|
2
|
-
import {
|
|
3
|
+
import { buildSystemPrompt } from '../agent/prompt.js';
|
|
4
|
+
import { parseToolCall, execTool } from '../agent/tools.js';
|
|
3
5
|
|
|
4
6
|
const MODEL_ROUTES = Object.freeze({
|
|
5
7
|
'openaxis/openaxis-llama': Object.freeze([
|
|
6
8
|
'https://universal-618-clarity-main.hf.space/v1/chat/completions',
|
|
9
|
+
'https://universal-618-clarity-4.hf.space/v1/chat/completions',
|
|
7
10
|
]),
|
|
8
11
|
'openaxis/openaxis-gpt': Object.freeze([
|
|
9
12
|
'https://universal-618-clarity-2.hf.space/v1/chat/completions',
|
|
13
|
+
'https://universal-618-clarity-5.hf.space/v1/chat/completions',
|
|
10
14
|
]),
|
|
11
15
|
'openaxis/openaxis-deepseek': Object.freeze([
|
|
12
16
|
'https://universal-618-clarity-3.hf.space/v1/chat/completions',
|
|
17
|
+
'https://universal-618-clarity-6.hf.space/v1/chat/completions',
|
|
13
18
|
]),
|
|
14
19
|
});
|
|
15
20
|
|
|
16
21
|
function checkMessages(messages) {
|
|
17
|
-
if (Array.isArray(messages)
|
|
18
|
-
throw new Error('messages must be an array');
|
|
19
|
-
}
|
|
22
|
+
if (!Array.isArray(messages)) throw new Error('messages must be an array');
|
|
20
23
|
for (let i = 0; i < messages.length; i++) {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (typeof msg.role !== 'string' || msg.role.length === 0) {
|
|
26
|
-
throw new Error('message at index ' + i + ' must have a non-empty role');
|
|
27
|
-
}
|
|
28
|
-
if (typeof msg.content !== 'string') {
|
|
29
|
-
throw new Error('message at index ' + i + ' content must be a string');
|
|
30
|
-
}
|
|
24
|
+
const m = messages[i];
|
|
25
|
+
if (!m || typeof m !== 'object') throw new Error('message at ' + i + ' must be an object');
|
|
26
|
+
if (typeof m.role !== 'string' || !m.role) throw new Error('message at ' + i + ' must have non-empty role');
|
|
27
|
+
if (typeof m.content !== 'string') throw new Error('message at ' + i + ' content must be string');
|
|
31
28
|
}
|
|
32
29
|
return messages;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const endpoints = MODEL_ROUTES[modelConfig.id];
|
|
46
|
-
if (endpoints === null || endpoints === undefined) {
|
|
47
|
-
throw new Error('No endpoints configured for model: ' + modelConfig.id);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
|
|
51
|
-
let modelMessages = messages;
|
|
52
|
-
let webUsed = false;
|
|
53
|
-
let webSites = 0;
|
|
54
|
-
let webQuery = '';
|
|
32
|
+
function buildRouterGraph() {
|
|
33
|
+
const graph = new StateGraph({
|
|
34
|
+
channels: {
|
|
35
|
+
lastResponse: { reducer: (a, b) => b ?? a, default: () => '' },
|
|
36
|
+
iteration: { reducer: (a, b) => b ?? a, default: () => 0 },
|
|
37
|
+
decision: { reducer: (a, b) => b ?? a, default: () => 'final' },
|
|
38
|
+
toolName: { reducer: (a, b) => b ?? a, default: () => null },
|
|
39
|
+
toolArgs: { reducer: (a, b) => b ?? a, default: () => null },
|
|
40
|
+
}
|
|
41
|
+
});
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
webUsed = true;
|
|
61
|
-
webSites = webResult.sites;
|
|
62
|
-
webQuery = webResult.query || '';
|
|
63
|
-
modelMessages = [{
|
|
64
|
-
role: 'system',
|
|
65
|
-
content:
|
|
66
|
-
'Current web search results (use when relevant, cite URLs inline):\n\n' +
|
|
67
|
-
webResult.summary +
|
|
68
|
-
'\n\nNow respond to the user using these results as needed.',
|
|
69
|
-
}].concat(messages);
|
|
70
|
-
}
|
|
71
|
-
} catch (_) {
|
|
43
|
+
graph.addNode('router', async (state) => {
|
|
44
|
+
const tc = parseToolCall(state.lastResponse);
|
|
45
|
+
if (tc) {
|
|
46
|
+
return { decision: 'tool', toolName: tc.name, toolArgs: tc.args };
|
|
72
47
|
}
|
|
73
|
-
|
|
48
|
+
return { decision: 'final' };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
graph.addEdge(START, 'router');
|
|
52
|
+
graph.addEdge('router', END);
|
|
74
53
|
|
|
75
|
-
|
|
54
|
+
return graph.compile();
|
|
55
|
+
}
|
|
76
56
|
|
|
57
|
+
async function* callLLMStream(endpoints, modelShort, messages, signal) {
|
|
77
58
|
const body = {
|
|
78
59
|
model: modelShort,
|
|
79
|
-
messages
|
|
60
|
+
messages,
|
|
80
61
|
max_tokens: 4096,
|
|
81
62
|
temperature: 0.7,
|
|
82
63
|
stream: true,
|
|
83
64
|
};
|
|
84
65
|
|
|
85
66
|
let lastError = null;
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < endpoints.length; i++) {
|
|
88
|
-
const endpoint = endpoints[i];
|
|
67
|
+
for (const ep of endpoints) {
|
|
89
68
|
try {
|
|
90
|
-
const stream = streamResponse(
|
|
91
|
-
for await (const
|
|
92
|
-
yield
|
|
69
|
+
const stream = streamResponse(ep, body, signal);
|
|
70
|
+
for await (const ev of stream) {
|
|
71
|
+
yield ev;
|
|
93
72
|
}
|
|
94
73
|
return;
|
|
95
74
|
} catch (err) {
|
|
96
75
|
lastError = err;
|
|
97
|
-
if (signal
|
|
76
|
+
if (signal?.aborted) return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError || new Error('All endpoints failed');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const routerGraph = buildRouterGraph();
|
|
83
|
+
|
|
84
|
+
export async function* callModel(modelConfig, messages, signal) {
|
|
85
|
+
if (!modelConfig || typeof modelConfig !== 'object') throw new Error('modelConfig must be object');
|
|
86
|
+
if (typeof modelConfig.id !== 'string') throw new Error('modelConfig.id must be string');
|
|
87
|
+
checkMessages(messages);
|
|
88
|
+
|
|
89
|
+
const endpoints = MODEL_ROUTES[modelConfig.id];
|
|
90
|
+
if (!endpoints) throw new Error('No endpoints for ' + modelConfig.id);
|
|
91
|
+
const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
|
|
92
|
+
const systemPrompt = buildSystemPrompt(process.cwd());
|
|
93
|
+
|
|
94
|
+
let msgs = [{ role: 'system', content: systemPrompt }].concat(messages);
|
|
95
|
+
let fullResponse = '';
|
|
96
|
+
|
|
97
|
+
for (let iter = 0; iter < 15 && !signal?.aborted; iter++) {
|
|
98
|
+
fullResponse = '';
|
|
99
|
+
|
|
100
|
+
const llmStream = callLLMStream(endpoints, modelShort, msgs, signal);
|
|
101
|
+
for await (const ev of llmStream) {
|
|
102
|
+
if (ev.type === 'token') {
|
|
103
|
+
fullResponse += ev.content || '';
|
|
104
|
+
yield ev;
|
|
105
|
+
} else if (ev.type === 'done') {
|
|
106
|
+
break;
|
|
107
|
+
} else if (ev.type === 'timeout') {
|
|
108
|
+
yield ev;
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
|
-
}
|
|
102
112
|
|
|
103
|
-
|
|
113
|
+
if (signal?.aborted) return;
|
|
114
|
+
|
|
115
|
+
const state = await routerGraph.invoke({ lastResponse: fullResponse, iteration: iter });
|
|
116
|
+
|
|
117
|
+
if (state.decision === 'final') {
|
|
118
|
+
yield { type: 'done' };
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
yield { type: 'tool_call', tool: state.toolName, args: state.toolArgs };
|
|
123
|
+
|
|
124
|
+
let result;
|
|
125
|
+
if (state.toolName === 'ask_question') {
|
|
126
|
+
const answer = yield { type: 'question', text: state.toolArgs.question };
|
|
127
|
+
result = 'User answered: ' + (answer || '');
|
|
128
|
+
} else if (state.toolName === 'todo_add') {
|
|
129
|
+
result = await execTool({ name: state.toolName, args: state.toolArgs }, signal);
|
|
130
|
+
yield { type: 'todo_add', text: state.toolArgs.text, result };
|
|
131
|
+
} else if (state.toolName === 'todo_done') {
|
|
132
|
+
result = await execTool({ name: state.toolName, args: state.toolArgs }, signal);
|
|
133
|
+
yield { type: 'todo_done', id: state.toolArgs.id, result };
|
|
134
|
+
} else {
|
|
135
|
+
result = await execTool({ name: state.toolName, args: state.toolArgs }, signal);
|
|
136
|
+
yield { type: 'tool_result', tool: state.toolName, result: result.substring(0, 300) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
msgs.push({ role: 'assistant', content: fullResponse });
|
|
140
|
+
msgs.push({ role: 'user', content: 'Observation: ' + result });
|
|
141
|
+
}
|
|
104
142
|
}
|
|
@@ -46,7 +46,7 @@ export function shouldUseWebSearch(messages) {
|
|
|
46
46
|
return asksExternalFact === true && hasNamedThing === true;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function searchWeb(query, signal) {
|
|
49
|
+
export async function searchWeb(query, signal) {
|
|
50
50
|
const key = getWebSearchKey();
|
|
51
51
|
if (key.length === 0) {
|
|
52
52
|
return {
|