felo-ai 0.2.17 → 0.2.19
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/CHANGELOG.md +13 -0
- package/felo-superAgent/README.md +354 -80
- package/felo-superAgent/SKILL.md +459 -63
- package/felo-superAgent/scripts/run_superagent.mjs +568 -0
- package/package.json +2 -3
- package/src/cli.js +21 -0
- package/src/livedoc.js +34 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
|
+
const DEFAULT_TIMEOUT_SEC = 60;
|
|
5
|
+
const STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
6
|
+
const RECONNECT_DELAY_MS = 2000;
|
|
7
|
+
const HIDDEN_TOOLS = new Set(['manage_outline']);
|
|
8
|
+
|
|
9
|
+
function usage() {
|
|
10
|
+
console.error(
|
|
11
|
+
[
|
|
12
|
+
'Usage:',
|
|
13
|
+
' node felo-superAgent/scripts/run_superagent.mjs --query "your question" [options]',
|
|
14
|
+
'',
|
|
15
|
+
'Options:',
|
|
16
|
+
' --query <text> User question (required, 1-2000 chars)',
|
|
17
|
+
' --thread-id <id> Existing thread ID for follow-up',
|
|
18
|
+
' --live-doc-id <id> Reuse existing LiveDoc short_id',
|
|
19
|
+
' --skill-id <id> Skill ID (new conversations only)',
|
|
20
|
+
' --selected-resource-ids <ids> Comma-separated resource IDs (new conversations only)',
|
|
21
|
+
' --ext <json> Extra params JSON (new conversations only)',
|
|
22
|
+
' --accept-language <lang> Language preference (e.g. zh, en)',
|
|
23
|
+
' --timeout <seconds> Request/stream timeout, default 60',
|
|
24
|
+
' --json Output JSON with answer, thread_short_id, live_doc_short_id',
|
|
25
|
+
' --verbose Log stream details to stderr',
|
|
26
|
+
' --help Show this help',
|
|
27
|
+
].join('\n')
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const out = {
|
|
33
|
+
query: '',
|
|
34
|
+
threadId: '',
|
|
35
|
+
liveDocId: '',
|
|
36
|
+
skillId: '',
|
|
37
|
+
selectedResourceIds: [],
|
|
38
|
+
ext: null,
|
|
39
|
+
acceptLanguage: '',
|
|
40
|
+
timeoutSec: DEFAULT_TIMEOUT_SEC,
|
|
41
|
+
json: false,
|
|
42
|
+
verbose: false,
|
|
43
|
+
help: false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
if (a === '--help' || a === '-h') {
|
|
49
|
+
out.help = true;
|
|
50
|
+
} else if (a === '--json' || a === '-j') {
|
|
51
|
+
out.json = true;
|
|
52
|
+
} else if (a === '--verbose' || a === '-v') {
|
|
53
|
+
out.verbose = true;
|
|
54
|
+
} else if (a === '--query' || a === '-q') {
|
|
55
|
+
out.query = (argv[++i] || '').trim();
|
|
56
|
+
} else if (a === '--thread-id') {
|
|
57
|
+
out.threadId = (argv[++i] || '').trim();
|
|
58
|
+
} else if (a === '--live-doc-id') {
|
|
59
|
+
out.liveDocId = (argv[++i] || '').trim();
|
|
60
|
+
} else if (a === '--skill-id') {
|
|
61
|
+
out.skillId = (argv[++i] || '').trim();
|
|
62
|
+
} else if (a === '--selected-resource-ids') {
|
|
63
|
+
out.selectedResourceIds = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
64
|
+
} else if (a === '--ext') {
|
|
65
|
+
const raw = (argv[++i] || '').trim();
|
|
66
|
+
try {
|
|
67
|
+
out.ext = JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
console.error('Error: --ext must be valid JSON');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
} else if (a === '--accept-language') {
|
|
73
|
+
out.acceptLanguage = (argv[++i] || '').trim();
|
|
74
|
+
} else if (a === '--timeout' || a === '-t') {
|
|
75
|
+
const n = parseInt(argv[++i] || '', 10);
|
|
76
|
+
if (Number.isFinite(n) && n > 0) out.timeoutSec = n;
|
|
77
|
+
} else if (!a.startsWith('-') && !out.query) {
|
|
78
|
+
out.query = a.trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sleep(ms) {
|
|
86
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getMessage(payload) {
|
|
90
|
+
return payload?.message || payload?.error || payload?.msg || payload?.code || 'Unknown error';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isApiError(payload) {
|
|
94
|
+
const status = payload?.status;
|
|
95
|
+
const code = payload?.code;
|
|
96
|
+
if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
|
|
97
|
+
if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
106
|
+
let body = {};
|
|
107
|
+
try {
|
|
108
|
+
body = await res.json();
|
|
109
|
+
} catch {
|
|
110
|
+
body = {};
|
|
111
|
+
}
|
|
112
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
|
|
113
|
+
if (isApiError(body)) throw new Error(getMessage(body));
|
|
114
|
+
return body;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err?.name === 'AbortError') throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
|
|
117
|
+
throw err;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── API ──
|
|
124
|
+
|
|
125
|
+
async function createConversation(apiKey, apiBase, body, timeoutMs, threadId) {
|
|
126
|
+
const url = threadId
|
|
127
|
+
? `${apiBase}/v2/conversations/${encodeURIComponent(threadId)}/follow_up`
|
|
128
|
+
: `${apiBase}/v2/conversations`;
|
|
129
|
+
const payload = await fetchJson(
|
|
130
|
+
url,
|
|
131
|
+
{
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
Accept: 'application/json',
|
|
135
|
+
Authorization: `Bearer ${apiKey}`,
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
},
|
|
140
|
+
timeoutMs
|
|
141
|
+
);
|
|
142
|
+
const data = payload?.data ?? {};
|
|
143
|
+
if (!data.stream_key) throw new Error('Unexpected response: missing stream_key');
|
|
144
|
+
return data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── SSE ──
|
|
148
|
+
|
|
149
|
+
function extractToolParams(data) {
|
|
150
|
+
const out = [];
|
|
151
|
+
const tools = data?.tools;
|
|
152
|
+
if (!Array.isArray(tools)) return out;
|
|
153
|
+
for (const t of tools) {
|
|
154
|
+
if (HIDDEN_TOOLS.has(t?.name) || HIDDEN_TOOLS.has(t?.tool_name)) continue;
|
|
155
|
+
if (t?.name && t?.params) out.push({ name: t.name, params: t.params });
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractToolResults(data) {
|
|
161
|
+
const out = [];
|
|
162
|
+
const tools = data?.tools;
|
|
163
|
+
if (!Array.isArray(tools)) return out;
|
|
164
|
+
for (const t of tools) {
|
|
165
|
+
if (HIDDEN_TOOLS.has(t?.name) || HIDDEN_TOOLS.has(t?.tool_name)) continue;
|
|
166
|
+
const callResult = t?.call_result;
|
|
167
|
+
if (t?.tool_name === 'generate_images' || t?.name === 'generate_images') {
|
|
168
|
+
if (!callResult) continue;
|
|
169
|
+
if (Array.isArray(callResult)) {
|
|
170
|
+
for (const item of callResult) {
|
|
171
|
+
if (item?.image_url) out.push({ type: 'image', title: item?.title || '', image_url: item.image_url });
|
|
172
|
+
}
|
|
173
|
+
} else if (callResult?.images && Array.isArray(callResult.images)) {
|
|
174
|
+
for (const img of callResult.images) {
|
|
175
|
+
if (img?.image_url) out.push({ type: 'image', title: img?.title || '', image_url: img.image_url });
|
|
176
|
+
}
|
|
177
|
+
} else if (callResult?.image_url) {
|
|
178
|
+
out.push({ type: 'image', title: callResult?.title || '', image_url: callResult.image_url });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (t?.name === 'generate_discovery' && callResult?.status === 'success') {
|
|
182
|
+
out.push({ type: 'discovery', title: callResult?.title || t?.params?.title || 'Discovery' });
|
|
183
|
+
}
|
|
184
|
+
if (t?.name === 'generate_document' && callResult?.status === 'success') {
|
|
185
|
+
out.push({ type: 'document', title: callResult?.title || t?.params?.title || 'Document' });
|
|
186
|
+
}
|
|
187
|
+
if (t?.name === 'generate_ppt' && callResult?.status === 'success') {
|
|
188
|
+
out.push({ type: 'ppt', title: callResult?.title || t?.params?.title || 'PPT' });
|
|
189
|
+
}
|
|
190
|
+
if (t?.name === 'generate_html' && callResult?.status === 'success') {
|
|
191
|
+
out.push({ type: 'html', title: callResult?.title || t?.params?.title || 'HTML' });
|
|
192
|
+
}
|
|
193
|
+
if (t?.name === 'search_x' && callResult?.tweets && Array.isArray(callResult.tweets)) {
|
|
194
|
+
out.push({ type: 'search_x', status: callResult.status, tweets: callResult.tweets });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dispatch(eventType, dataStr, callbacks) {
|
|
201
|
+
const { onMessage, onToolCall, onToolResult } = callbacks;
|
|
202
|
+
let payload = {};
|
|
203
|
+
if (dataStr) {
|
|
204
|
+
try {
|
|
205
|
+
payload = JSON.parse(dataStr);
|
|
206
|
+
} catch {
|
|
207
|
+
payload = { content: dataStr };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
switch (eventType) {
|
|
212
|
+
case 'message':
|
|
213
|
+
if (typeof payload.content === 'string') onMessage(payload.content);
|
|
214
|
+
break;
|
|
215
|
+
case 'stream': {
|
|
216
|
+
const content = payload?.content;
|
|
217
|
+
if (typeof content === 'string') {
|
|
218
|
+
try {
|
|
219
|
+
const inner = JSON.parse(content);
|
|
220
|
+
const type = inner?.type;
|
|
221
|
+
const data = inner?.data;
|
|
222
|
+
if (type === 'content' || type === 'text' || type === 'delta' || type === 'answer') {
|
|
223
|
+
const text = data?.content ?? data?.text ?? data?.delta;
|
|
224
|
+
if (typeof text === 'string') onMessage(text);
|
|
225
|
+
} else if (type === 'tools' && onToolCall) {
|
|
226
|
+
const params = extractToolParams(data);
|
|
227
|
+
for (const item of params) onToolCall(item);
|
|
228
|
+
} else if ((type === 'tools_result_stream' || type === 'tools_result') && onToolResult) {
|
|
229
|
+
const results = extractToolResults(data);
|
|
230
|
+
for (const item of results) onToolResult(item);
|
|
231
|
+
} else if (type !== 'processing' && type !== 'tools' && type !== 'message' && data?.message && typeof data.message === 'string') {
|
|
232
|
+
onMessage(data.message);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
onMessage(content);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'heartbeat':
|
|
241
|
+
case 'connected':
|
|
242
|
+
break;
|
|
243
|
+
default:
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function readSSE(url, apiKey, startOffset, callbacks) {
|
|
249
|
+
const { onMessage, onDone, onEvent, onToolCall, onToolResult } = callbacks;
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
let idleTimer = null;
|
|
252
|
+
const resetIdleTimer = () => {
|
|
253
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
254
|
+
idleTimer = setTimeout(() => controller.abort(), STREAM_IDLE_TIMEOUT_MS);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const connectUrl = startOffset >= 0 ? `${url}?offset=${startOffset}` : url;
|
|
258
|
+
let maxOffset = startOffset;
|
|
259
|
+
let streamDone = false;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch(connectUrl, {
|
|
263
|
+
method: 'GET',
|
|
264
|
+
headers: {
|
|
265
|
+
Accept: 'text/event-stream',
|
|
266
|
+
Authorization: `Bearer ${apiKey}`,
|
|
267
|
+
},
|
|
268
|
+
signal: controller.signal,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
const text = await res.text();
|
|
273
|
+
let msg = text;
|
|
274
|
+
try {
|
|
275
|
+
const j = JSON.parse(text);
|
|
276
|
+
msg = getMessage(j) || text;
|
|
277
|
+
} catch {}
|
|
278
|
+
throw new Error(`HTTP ${res.status}: ${msg}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const reader = res.body?.getReader();
|
|
282
|
+
if (!reader) throw new Error('No response body');
|
|
283
|
+
|
|
284
|
+
const decoder = new TextDecoder();
|
|
285
|
+
let buffer = '';
|
|
286
|
+
let currentEvent = '';
|
|
287
|
+
let currentData = undefined;
|
|
288
|
+
|
|
289
|
+
const processEvent = (evt, data) => {
|
|
290
|
+
if (!evt || data === undefined) return;
|
|
291
|
+
let eventOffset = -1;
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(data);
|
|
294
|
+
if (typeof parsed?.offset === 'number') {
|
|
295
|
+
eventOffset = parsed.offset;
|
|
296
|
+
if (parsed.offset > maxOffset) maxOffset = parsed.offset;
|
|
297
|
+
}
|
|
298
|
+
} catch {}
|
|
299
|
+
|
|
300
|
+
if (eventOffset >= 0 && eventOffset <= startOffset) return;
|
|
301
|
+
|
|
302
|
+
if (onEvent) onEvent(evt, data);
|
|
303
|
+
|
|
304
|
+
if (evt === 'error') return; // server "not ready yet" signal, keep reading
|
|
305
|
+
if (evt === 'done' || evt === 'completed' || evt === 'complete') {
|
|
306
|
+
streamDone = true;
|
|
307
|
+
onDone();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
dispatch(evt, data, { onMessage, onToolCall, onToolResult });
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
resetIdleTimer();
|
|
314
|
+
while (true) {
|
|
315
|
+
const { done, value } = await reader.read();
|
|
316
|
+
resetIdleTimer();
|
|
317
|
+
if (done) break;
|
|
318
|
+
|
|
319
|
+
buffer += decoder.decode(value, { stream: true });
|
|
320
|
+
const lines = buffer.split(/\r?\n/);
|
|
321
|
+
buffer = lines.pop() ?? '';
|
|
322
|
+
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
if (line.startsWith('event:')) {
|
|
325
|
+
processEvent(currentEvent, currentData);
|
|
326
|
+
currentEvent = line.slice(6).trim();
|
|
327
|
+
currentData = undefined;
|
|
328
|
+
} else if (line.startsWith('data:')) {
|
|
329
|
+
currentData = line.slice(5).trim();
|
|
330
|
+
} else if (line === '') {
|
|
331
|
+
processEvent(currentEvent, currentData);
|
|
332
|
+
currentEvent = '';
|
|
333
|
+
currentData = undefined;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
339
|
+
processEvent(currentEvent, currentData);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
342
|
+
if (err?.name === 'AbortError') {
|
|
343
|
+
return { maxOffset, streamDone, streamError: `Stream idle timeout (no data for ${STREAM_IDLE_TIMEOUT_MS / 1000}s)` };
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { maxOffset, streamDone, streamError: null };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function consumeStream(apiKey, apiBase, streamKey, callbacks) {
|
|
352
|
+
const url = `${apiBase}/v2/conversations/stream/${encodeURIComponent(streamKey)}`;
|
|
353
|
+
let lastOffset = -1;
|
|
354
|
+
const startTime = Date.now();
|
|
355
|
+
|
|
356
|
+
while (true) {
|
|
357
|
+
if (Date.now() - startTime > STREAM_IDLE_TIMEOUT_MS) {
|
|
358
|
+
callbacks.onError('Stream timeout: no completion after ' + (STREAM_IDLE_TIMEOUT_MS / 1000) + 's');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const result = await readSSE(url, apiKey, lastOffset, callbacks);
|
|
363
|
+
|
|
364
|
+
if (result.streamDone) return;
|
|
365
|
+
if (result.maxOffset > lastOffset) lastOffset = result.maxOffset;
|
|
366
|
+
|
|
367
|
+
await sleep(RECONNECT_DELAY_MS);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Format helpers ──
|
|
372
|
+
|
|
373
|
+
function formatTweet(tweet) {
|
|
374
|
+
const info = tweet?.other_info || {};
|
|
375
|
+
const author = info.author || {};
|
|
376
|
+
const metrics = info.metrics || {};
|
|
377
|
+
const name = author.display_name || author.username || 'Unknown';
|
|
378
|
+
const handle = author.username ? `@${author.username}` : '';
|
|
379
|
+
const text = tweet?.snippet || tweet?.title || '';
|
|
380
|
+
const link = tweet?.link || info.url || '';
|
|
381
|
+
const stats = [];
|
|
382
|
+
if (metrics.favorite_count) stats.push(`${metrics.favorite_count} likes`);
|
|
383
|
+
if (metrics.retweet_count) stats.push(`${metrics.retweet_count} retweets`);
|
|
384
|
+
if (metrics.view_count) stats.push(`${metrics.view_count} views`);
|
|
385
|
+
const statsStr = stats.length > 0 ? ` [${stats.join(' | ')}]` : '';
|
|
386
|
+
return ` ${name} (${handle})${statsStr}\n ${text}\n ${link}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Main ──
|
|
390
|
+
|
|
391
|
+
async function main() {
|
|
392
|
+
const args = parseArgs(process.argv.slice(2));
|
|
393
|
+
if (args.help) {
|
|
394
|
+
usage();
|
|
395
|
+
process.exit(0);
|
|
396
|
+
}
|
|
397
|
+
if (!args.query) {
|
|
398
|
+
usage();
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const apiKey = process.env.FELO_API_KEY?.trim();
|
|
403
|
+
if (!apiKey) {
|
|
404
|
+
console.error(
|
|
405
|
+
'ERROR: FELO_API_KEY not set\n\n' +
|
|
406
|
+
'To use SuperAgent, set FELO_API_KEY:\n' +
|
|
407
|
+
' export FELO_API_KEY="your-api-key-here"\n' +
|
|
408
|
+
'Get your API key from https://felo.ai (Settings -> API Keys).'
|
|
409
|
+
);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
|
|
414
|
+
const timeoutMs = args.timeoutSec * 1000;
|
|
415
|
+
|
|
416
|
+
// Build request body
|
|
417
|
+
const body = { query: args.query.slice(0, 2000) };
|
|
418
|
+
if (args.liveDocId) body.live_doc_short_id = args.liveDocId;
|
|
419
|
+
if (args.acceptLanguage) body.accept_language = args.acceptLanguage;
|
|
420
|
+
|
|
421
|
+
const threadId = args.threadId || undefined;
|
|
422
|
+
|
|
423
|
+
// skill_id, selected_resource_ids, ext only for new conversations
|
|
424
|
+
if (threadId && (args.skillId || args.selectedResourceIds.length || args.ext)) {
|
|
425
|
+
process.stderr.write('Warning: --skill-id, --selected-resource-ids, --ext are ignored in follow-up mode.\n');
|
|
426
|
+
}
|
|
427
|
+
if (!threadId) {
|
|
428
|
+
if (args.skillId) body.skill_id = args.skillId;
|
|
429
|
+
if (args.selectedResourceIds.length) body.selected_resource_ids = args.selectedResourceIds;
|
|
430
|
+
if (args.ext) body.ext = args.ext;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
process.stderr.write(threadId ? 'SuperAgent: following up...\n' : 'SuperAgent: creating conversation...\n');
|
|
434
|
+
|
|
435
|
+
const createData = await createConversation(apiKey, apiBase, body, timeoutMs, threadId);
|
|
436
|
+
const { stream_key, thread_short_id, live_doc_short_id } = createData;
|
|
437
|
+
|
|
438
|
+
if (args.verbose) {
|
|
439
|
+
process.stderr.write(`Stream key: ${stream_key}\n`);
|
|
440
|
+
process.stderr.write(`Thread ID: ${thread_short_id}\n`);
|
|
441
|
+
process.stderr.write(`LiveDoc ID: ${live_doc_short_id}\n`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const feloBase = (process.env.FELO_WEB_BASE?.trim() || apiBase.replace(/\/\/openapi-/, '//').replace(/\/\/openapi\./, '//')).replace(/\/$/, '');
|
|
445
|
+
const liveDocUrl = live_doc_short_id ? `${feloBase}/zh-Hans/livedoc/${live_doc_short_id}` : '';
|
|
446
|
+
|
|
447
|
+
const chunks = [];
|
|
448
|
+
const toolResults = [];
|
|
449
|
+
const seenKeys = new Set();
|
|
450
|
+
const isJson = args.json;
|
|
451
|
+
|
|
452
|
+
const onToolCall = (item) => {
|
|
453
|
+
if (isJson) return;
|
|
454
|
+
const { name, params } = item;
|
|
455
|
+
console.log(`\n[Tool: ${name}]`);
|
|
456
|
+
if (name === 'search_x') {
|
|
457
|
+
console.log(` Query: ${params.query || ''}`);
|
|
458
|
+
if (params.query_type) console.log(` Type: ${params.query_type}`);
|
|
459
|
+
if (params.limit) console.log(` Limit: ${params.limit}`);
|
|
460
|
+
} else if (name === 'generate_images') {
|
|
461
|
+
const images = params?.images;
|
|
462
|
+
if (Array.isArray(images)) {
|
|
463
|
+
for (const img of images) console.log(` Image: ${img.title || '(untitled)'}`);
|
|
464
|
+
}
|
|
465
|
+
} else if (name === 'generate_discovery') {
|
|
466
|
+
console.log(` Title: ${params.title || params.query || ''}`);
|
|
467
|
+
} else if (name === 'generate_document') {
|
|
468
|
+
console.log(` Title: ${params.title || ''}`);
|
|
469
|
+
} else if (name === 'generate_ppt') {
|
|
470
|
+
console.log(` Title: ${params.title || ''}`);
|
|
471
|
+
} else if (name === 'generate_html') {
|
|
472
|
+
console.log(` Title: ${params.title || ''}`);
|
|
473
|
+
} else {
|
|
474
|
+
console.log(` Params: ${JSON.stringify(params)}`);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const onToolResult = (item) => {
|
|
479
|
+
const LIVEDOC_TYPES = new Set(['document', 'ppt', 'html', 'discovery']);
|
|
480
|
+
const key = item?.image_url || (LIVEDOC_TYPES.has(item?.type) ? item.type : `${item?.type}:${item?.title}`);
|
|
481
|
+
if (seenKeys.has(key)) return;
|
|
482
|
+
seenKeys.add(key);
|
|
483
|
+
toolResults.push(item);
|
|
484
|
+
|
|
485
|
+
if (isJson) return;
|
|
486
|
+
if (item.type === 'image') {
|
|
487
|
+
console.log(liveDocUrl ? `[${item.title || 'Image'}](${liveDocUrl})` : item.image_url);
|
|
488
|
+
} else if (item.type === 'discovery') {
|
|
489
|
+
console.log(liveDocUrl ? `[${item.title}](${liveDocUrl})` : item.title);
|
|
490
|
+
} else if (item.type === 'document') {
|
|
491
|
+
console.log(liveDocUrl ? `[${item.title || 'Document'}](${liveDocUrl})` : (item.title || 'Document'));
|
|
492
|
+
} else if (item.type === 'ppt') {
|
|
493
|
+
console.log(liveDocUrl ? `[${item.title || 'PPT'}](${liveDocUrl})` : (item.title || 'PPT'));
|
|
494
|
+
} else if (item.type === 'html') {
|
|
495
|
+
console.log(liveDocUrl ? `[${item.title || 'HTML'}](${liveDocUrl})` : (item.title || 'HTML'));
|
|
496
|
+
} else if (item.type === 'search_x') {
|
|
497
|
+
console.log(`\n[Twitter Search Results] (${item.tweets.length} tweets)`);
|
|
498
|
+
for (const tweet of item.tweets) {
|
|
499
|
+
console.log(formatTweet(tweet));
|
|
500
|
+
console.log('');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
let streamError = null;
|
|
506
|
+
const onEvent = args.verbose
|
|
507
|
+
? (eventType, dataStr) => {
|
|
508
|
+
process.stderr.write(`[stream] event=${eventType}\n`);
|
|
509
|
+
process.stderr.write(`[stream] data=${dataStr || ''}\n`);
|
|
510
|
+
}
|
|
511
|
+
: undefined;
|
|
512
|
+
|
|
513
|
+
await consumeStream(apiKey, apiBase, stream_key, {
|
|
514
|
+
onMessage: (content) => {
|
|
515
|
+
chunks.push(content);
|
|
516
|
+
if (!isJson) process.stdout.write(content);
|
|
517
|
+
},
|
|
518
|
+
onError: (err) => {
|
|
519
|
+
streamError = err;
|
|
520
|
+
},
|
|
521
|
+
onDone: () => {},
|
|
522
|
+
onEvent,
|
|
523
|
+
onToolCall,
|
|
524
|
+
onToolResult,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
if (streamError) throw new Error(streamError);
|
|
528
|
+
|
|
529
|
+
const answer = chunks.join('').trim();
|
|
530
|
+
|
|
531
|
+
if (isJson) {
|
|
532
|
+
const images = toolResults.filter((r) => r.type === 'image');
|
|
533
|
+
const discoveries = toolResults.filter((r) => r.type === 'discovery');
|
|
534
|
+
const documents = toolResults.filter((r) => r.type === 'document');
|
|
535
|
+
const ppts = toolResults.filter((r) => r.type === 'ppt');
|
|
536
|
+
const htmls = toolResults.filter((r) => r.type === 'html');
|
|
537
|
+
const searches = toolResults.filter((r) => r.type === 'search_x');
|
|
538
|
+
console.log(
|
|
539
|
+
JSON.stringify(
|
|
540
|
+
{
|
|
541
|
+
status: 'ok',
|
|
542
|
+
data: {
|
|
543
|
+
answer: answer || null,
|
|
544
|
+
thread_short_id: thread_short_id ?? null,
|
|
545
|
+
live_doc_short_id: live_doc_short_id ?? null,
|
|
546
|
+
image_urls: images.length > 0 ? images.map((r) => ({ url: r.image_url, title: r.title })) : undefined,
|
|
547
|
+
discoveries: discoveries.length > 0 ? discoveries.map((r) => ({ title: r.title })) : undefined,
|
|
548
|
+
documents: documents.length > 0 ? documents.map((r) => ({ title: r.title })) : undefined,
|
|
549
|
+
ppts: ppts.length > 0 ? ppts.map((r) => ({ title: r.title })) : undefined,
|
|
550
|
+
htmls: htmls.length > 0 ? htmls.map((r) => ({ title: r.title })) : undefined,
|
|
551
|
+
search_x: searches.length > 0 ? searches.map((r) => ({ tweets: r.tweets })) : undefined,
|
|
552
|
+
live_doc_url: liveDocUrl || undefined,
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
null,
|
|
556
|
+
2
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
} else {
|
|
560
|
+
if (answer) console.log('');
|
|
561
|
+
if (!answer && toolResults.length === 0) console.log('(No content in stream)');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
main().catch((err) => {
|
|
566
|
+
console.error(`ERROR: ${err?.message || err}`);
|
|
567
|
+
process.exit(1);
|
|
568
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"description": "Felo AI CLI - real-time search, PPT generation, SuperAgent conversation, LiveDoc management, web fetch, YouTube subtitles, LiveDoc knowledge base, and X (Twitter) search from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
"url": "git+https://github.com/Felo-Inc/felo-skills.git"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"commander": "^12.0.0"
|
|
36
|
-
"felo-search": "^0.1.1"
|
|
35
|
+
"commander": "^12.0.0"
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
39
38
|
"test": "node --test tests/",
|
package/src/cli.js
CHANGED
|
@@ -640,12 +640,33 @@ livedocCmd
|
|
|
640
640
|
.command("retrieve <short_id>")
|
|
641
641
|
.description("Semantic search across resources")
|
|
642
642
|
.requiredOption("--query <query>", "search query")
|
|
643
|
+
.option("--resource-ids <ids>", "comma-separated resource IDs to search within")
|
|
643
644
|
.option("-j, --json", "output raw JSON")
|
|
644
645
|
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
645
646
|
.action(async (shortId, opts) => {
|
|
646
647
|
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
647
648
|
const code = await livedoc.retrieve(shortId, {
|
|
648
649
|
query: opts.query,
|
|
650
|
+
resourceIds: opts.resourceIds,
|
|
651
|
+
json: opts.json,
|
|
652
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
653
|
+
});
|
|
654
|
+
process.exitCode = code;
|
|
655
|
+
flushStdioThenExit(code);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
livedocCmd
|
|
659
|
+
.command("route <short_id>")
|
|
660
|
+
.description("Route relevant resource IDs by query")
|
|
661
|
+
.requiredOption("--query <query>", "routing query")
|
|
662
|
+
.option("--max-resources <n>", "max resources to return")
|
|
663
|
+
.option("-j, --json", "output raw JSON")
|
|
664
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
665
|
+
.action(async (shortId, opts) => {
|
|
666
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
667
|
+
const code = await livedoc.route(shortId, {
|
|
668
|
+
query: opts.query,
|
|
669
|
+
maxResources: opts.maxResources,
|
|
649
670
|
json: opts.json,
|
|
650
671
|
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
651
672
|
});
|
package/src/livedoc.js
CHANGED
|
@@ -374,6 +374,36 @@ export async function removeResource(shortId, resourceId, opts = {}) {
|
|
|
374
374
|
} finally { stopSpinner(spinnerId); }
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
export async function route(shortId, opts = {}) {
|
|
378
|
+
const apiKey = await getApiKey();
|
|
379
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
380
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
381
|
+
if (!opts.query) { process.stderr.write('ERROR: --query is required.\n'); return 1; }
|
|
382
|
+
|
|
383
|
+
const apiBase = await getApiBase();
|
|
384
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
385
|
+
const spinnerId = startSpinner('Routing relevant resources');
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const body = { query: opts.query };
|
|
389
|
+
if (opts.maxResources) {
|
|
390
|
+
const n = parseInt(opts.maxResources, 10);
|
|
391
|
+
if (Number.isFinite(n) && n > 0) body.max_resources = n;
|
|
392
|
+
}
|
|
393
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/route`, body, apiKey, apiBase, timeoutMs);
|
|
394
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
395
|
+
|
|
396
|
+
const resourceIds = payload?.data || [];
|
|
397
|
+
if (!resourceIds.length) { process.stderr.write('No relevant resources found.\n'); return 0; }
|
|
398
|
+
process.stdout.write(`Found ${resourceIds.length} relevant resource(s):\n\n`);
|
|
399
|
+
for (const id of resourceIds) process.stdout.write(`- ${id}\n`);
|
|
400
|
+
return 0;
|
|
401
|
+
} catch (err) {
|
|
402
|
+
process.stderr.write(`Failed to route resources: ${err?.message || err}\n`);
|
|
403
|
+
return 1;
|
|
404
|
+
} finally { stopSpinner(spinnerId); }
|
|
405
|
+
}
|
|
406
|
+
|
|
377
407
|
export async function retrieve(shortId, opts = {}) {
|
|
378
408
|
const apiKey = await getApiKey();
|
|
379
409
|
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
@@ -385,7 +415,10 @@ export async function retrieve(shortId, opts = {}) {
|
|
|
385
415
|
const spinnerId = startSpinner('Retrieving from knowledge base');
|
|
386
416
|
|
|
387
417
|
try {
|
|
388
|
-
const body = {
|
|
418
|
+
const body = { query: opts.query };
|
|
419
|
+
if (opts.resourceIds) {
|
|
420
|
+
body.resource_ids = opts.resourceIds.split(',').map(id => id.trim()).filter(Boolean);
|
|
421
|
+
}
|
|
389
422
|
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/retrieve`, body, apiKey, apiBase, timeoutMs);
|
|
390
423
|
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
391
424
|
|