ff1-cli 1.0.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/LICENSE +21 -0
- package/README.md +65 -0
- package/config.json.example +78 -0
- package/dist/index.js +627 -0
- package/dist/src/ai-orchestrator/index.js +870 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/config.js +352 -0
- package/dist/src/intent-parser/index.js +1342 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +72 -0
- package/dist/src/main.js +393 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/feed-fetcher.js +387 -0
- package/dist/src/utilities/ff1-device.js +176 -0
- package/dist/src/utilities/functions.js +325 -0
- package/dist/src/utilities/index.js +372 -0
- package/dist/src/utilities/nft-indexer.js +1013 -0
- package/dist/src/utilities/playlist-builder.js +522 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +241 -0
- package/dist/src/utilities/playlist-signer.js +171 -0
- package/dist/src/utilities/playlist-verifier.js +156 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +178 -0
- package/docs/EXAMPLES.md +331 -0
- package/docs/FUNCTION_CALLING.md +92 -0
- package/docs/README.md +267 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Intent Parser
|
|
4
|
+
* Parses user intent and breaks down into structured requirements.
|
|
5
|
+
* Each requirement specifies: blockchain, contract address, token ID, source (media URL).
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.intentParserFunctionSchemas = void 0;
|
|
45
|
+
exports.processIntentParserRequest = processIntentParserRequest;
|
|
46
|
+
exports.buildIntentParserSystemPrompt = buildIntentParserSystemPrompt;
|
|
47
|
+
const openai_1 = __importDefault(require("openai"));
|
|
48
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
49
|
+
const config_1 = require("../config");
|
|
50
|
+
const utils_1 = require("./utils");
|
|
51
|
+
// Cache for AI clients
|
|
52
|
+
const clientCache = new Map();
|
|
53
|
+
/**
|
|
54
|
+
* Create AI client for intent parser
|
|
55
|
+
*
|
|
56
|
+
* @param {string} [modelName] - Model name
|
|
57
|
+
* @returns {OpenAI} OpenAI client
|
|
58
|
+
*/
|
|
59
|
+
function createIntentParserClient(modelName) {
|
|
60
|
+
const config = (0, config_1.getConfig)();
|
|
61
|
+
const selectedModel = modelName || config.defaultModel;
|
|
62
|
+
if (clientCache.has(selectedModel)) {
|
|
63
|
+
return clientCache.get(selectedModel);
|
|
64
|
+
}
|
|
65
|
+
const modelConfig = (0, config_1.getModelConfig)(selectedModel);
|
|
66
|
+
const client = new openai_1.default({
|
|
67
|
+
apiKey: modelConfig.apiKey,
|
|
68
|
+
baseURL: modelConfig.baseURL,
|
|
69
|
+
timeout: modelConfig.timeout,
|
|
70
|
+
maxRetries: modelConfig.maxRetries,
|
|
71
|
+
});
|
|
72
|
+
clientCache.set(selectedModel, client);
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build intent parser system prompt
|
|
77
|
+
*
|
|
78
|
+
* @returns {string} System prompt for intent parser
|
|
79
|
+
*/
|
|
80
|
+
function buildIntentParserSystemPrompt() {
|
|
81
|
+
const deviceConfig = (0, config_1.getFF1DeviceConfig)();
|
|
82
|
+
const hasDevices = deviceConfig.devices && deviceConfig.devices.length > 0;
|
|
83
|
+
let deviceInfo = '';
|
|
84
|
+
if (hasDevices) {
|
|
85
|
+
const deviceList = deviceConfig.devices
|
|
86
|
+
.map((d, i) => ` ${i + 1}. ${d.name || d.host}`)
|
|
87
|
+
.filter((line) => !line.includes('undefined'))
|
|
88
|
+
.join('\n');
|
|
89
|
+
if (deviceList) {
|
|
90
|
+
deviceInfo = `\n\nAVAILABLE FF1 DEVICES:\n${deviceList}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return `SYSTEM: FF1 Intent Parser
|
|
94
|
+
|
|
95
|
+
ROLE
|
|
96
|
+
- Turn user text into deterministic parameters for non‑AI execution. Keep public output minimal and structured.
|
|
97
|
+
|
|
98
|
+
REASONING (private scratchpad)
|
|
99
|
+
- Use Plan→Check→Act→Reflect for each step.
|
|
100
|
+
- Default to a single deterministic path.
|
|
101
|
+
- Only branch in two cases:
|
|
102
|
+
1) Multiple plausible feed candidates after search.
|
|
103
|
+
2) Verification failure requiring targeted repair.
|
|
104
|
+
- When branching, keep BEAM_WIDTH=2, DEPTH_LIMIT=2.
|
|
105
|
+
- Score candidates by: correctness, coverage, determinism, freshness, cost.
|
|
106
|
+
- Keep reasoning hidden; publicly print one status sentence before each tool call.
|
|
107
|
+
|
|
108
|
+
OUTPUT CONTRACT
|
|
109
|
+
- BUILD → call parse_requirements with { requirements: Requirement[], playlistSettings?: { title?: string | null, slug?: string | null, durationPerItem?: number, preserveOrder?: boolean, deviceName?: string, feedServer?: { baseUrl: string, apiKey?: string } } }
|
|
110
|
+
- SEND → call confirm_send_playlist with { filePath: string, deviceName?: string }
|
|
111
|
+
- PUBLISH (existing file) → call confirm_publish_playlist with { filePath: string, feedServer: { baseUrl: string, apiKey?: string } }
|
|
112
|
+
- QUESTION → answer briefly (no tool call)
|
|
113
|
+
- Use correct types; never truncate addresses/tokenIds; tokenIds are strings; quantity is a number.
|
|
114
|
+
|
|
115
|
+
REQUIREMENT TYPES (BUILD)
|
|
116
|
+
- build_playlist: { type, blockchain: "ethereum"|"tezos", contractAddress, tokenIds: string[], quantity?: number, source?: string }
|
|
117
|
+
• ONLY use when user explicitly provides BOTH contract address AND specific token IDs
|
|
118
|
+
• Example: "tokens 1, 2, 3 from contract 0x123"
|
|
119
|
+
- query_address: { type, ownerAddress: 0x…|tz…|domain.eth|domain.tez, quantity?: number | "all" }
|
|
120
|
+
• Domains (.eth/.tez) are OWNER DOMAINS. Do not ask for tokenIds. Do not treat as contracts.
|
|
121
|
+
• A raw 0x…/tz… without tokenIds is an OWNER ADDRESS (query_address), not a contract.
|
|
122
|
+
• CRITICAL: Phrases like "N items from [address]", "NFTs from [address]", "tokens from [address]" → query_address
|
|
123
|
+
• Example: "30 items from 0xABC" → query_address with quantity=30
|
|
124
|
+
• When user says "all", "all tokens", "all NFTs" → use quantity="all" (string, not number)
|
|
125
|
+
• quantity="all" will fetch ALL tokens using pagination, can handle thousands of tokens
|
|
126
|
+
- fetch_feed: { type, playlistName: string, quantity?: number (default 5) }
|
|
127
|
+
|
|
128
|
+
DOMAIN OWNER RULES (CRITICAL)
|
|
129
|
+
- Interpret \`*.eth\` as an Ethereum OWNER DOMAIN → produce \`query_address\` with \`ownerAddress\` set to the domain string (e.g., \`reas.eth\`).
|
|
130
|
+
- Interpret \`*.tez\` as a Tezos OWNER DOMAIN → produce \`query_address\` with \`ownerAddress\` set to the domain string (e.g., \`einstein-rosen.tez\`).
|
|
131
|
+
- Never treat \.eth or \.tez as a contract or collection identifier.
|
|
132
|
+
- Never invent or request \`tokenIds\` for \.eth/\.tez domains. Use \`quantity\` only.
|
|
133
|
+
|
|
134
|
+
EXAMPLES (query_address - NO tokenIds needed)
|
|
135
|
+
- "Pick 3 artworks from reas.eth" → \`query_address\` { ownerAddress: "reas.eth", quantity: 3 }
|
|
136
|
+
- "3 from einstein-rosen.tez and play on my FF1" → \`query_address\` { ownerAddress: "einstein-rosen.tez", quantity: 3 } and set \`playlistSettings.deviceName\` accordingly
|
|
137
|
+
- "create a playlist of 30 items from 0xABC" → \`query_address\` { ownerAddress: "0xABC", quantity: 30 }
|
|
138
|
+
- "get 20 NFTs from 0x123" → \`query_address\` { ownerAddress: "0x123", quantity: 20 }
|
|
139
|
+
- "create a playlist from all tokens in reas.eth" → \`query_address\` { ownerAddress: "reas.eth", quantity: "all" }
|
|
140
|
+
- "get all NFTs from 0xABC" → \`query_address\` { ownerAddress: "0xABC", quantity: "all" }
|
|
141
|
+
|
|
142
|
+
EXAMPLES (fetch_feed)
|
|
143
|
+
- "Pick 3 artworks from Social Codes and 2 from a2p. Mix them up." → \`fetch_feed\` { playlistName: "Social Codes", quantity: 3 } + \`fetch_feed\` { playlistName: "a2p", quantity: 2 }, and set \`playlistSettings.preserveOrder\` = false
|
|
144
|
+
|
|
145
|
+
EXAMPLES (build_playlist - requires BOTH contract AND tokenIds)
|
|
146
|
+
- "tokens 5, 10, 15 from contract 0xABC on ethereum" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xABC", tokenIds: ["5", "10", "15"] }
|
|
147
|
+
|
|
148
|
+
PLAYLIST SETTINGS EXTRACTION
|
|
149
|
+
- durationPerItem: parse phrases (e.g., "6 seconds each" → 6)
|
|
150
|
+
- preserveOrder: default true; synonyms ("shuffle", "randomize", "mix", "mix them up", "scramble") → false
|
|
151
|
+
- title/slug: optional; include only if provided by the user
|
|
152
|
+
- deviceName: from phrases like "send to", "display on", "play on", "push to"${hasDevices ? '\n- available devices:\n' + deviceInfo.replace('\n\nAVAILABLE FF1 DEVICES:\n', '') : ''}
|
|
153
|
+
|
|
154
|
+
GENERIC DEVICE RESOLUTION (CRITICAL)
|
|
155
|
+
- When the user references a generic device like "FF1", "my FF1", "my device", "my display", or similar (without a specific name), you MUST:
|
|
156
|
+
1. Immediately call get_configured_devices() to retrieve the list of devices
|
|
157
|
+
2. Extract the first device's name from the returned list
|
|
158
|
+
3. Use that exact device name in playlistSettings.deviceName
|
|
159
|
+
4. After resolving, acknowledge the resolved device name in your bullet summary (e.g., "send to device: Living Room")
|
|
160
|
+
- Example: "push to my FF1" → call get_configured_devices() → use devices[0].name as deviceName → show "device: Living Room" in bullets
|
|
161
|
+
- Do NOT ask the user which device to use when they say generic names like "FF1" or "my device"
|
|
162
|
+
|
|
163
|
+
MISSING INFO POLICY (ASK AT MOST ONE QUESTION)
|
|
164
|
+
- build_playlist: ask for blockchain/contract/tokenIds if unclear
|
|
165
|
+
- fetch_feed: ask for playlistName if unclear
|
|
166
|
+
- query_address: ask for owner/domain if unclear
|
|
167
|
+
- send: ask for device name only if user specifies a device by name and it's ambiguous; for generic references, always use get_configured_devices
|
|
168
|
+
|
|
169
|
+
ADDRESS VALIDATION (CRITICAL)
|
|
170
|
+
- When user enters any Ethereum (0x...) or Tezos (tz.../KT1) addresses, IMMEDIATELY call verify_addresses() BEFORE parsing requirements
|
|
171
|
+
- This includes: contract addresses in build_playlist, owner addresses in query_address, or any wallet/contract address mentioned
|
|
172
|
+
- Example: user says "get tokens from 0xABC" → first call verify_addresses(['0xABC']) → get validation result → then parse_requirements
|
|
173
|
+
- If verify_addresses returns valid=false, show user the error and ask them to provide the correct address
|
|
174
|
+
- If valid=true, proceed to parse_requirements with the verified addresses
|
|
175
|
+
|
|
176
|
+
FREE‑FORM COLLECTION NAMES
|
|
177
|
+
- Treat as fetch_feed; do not guess contracts. If user says "some", default quantity = 5.
|
|
178
|
+
|
|
179
|
+
FEED NAME HEURISTICS (CRITICAL)
|
|
180
|
+
- If a source is named without an address or domain (no 0x… / tz… / *.eth / *.tez), interpret it as a feed playlist name and produce \`fetch_feed\` immediately.
|
|
181
|
+
- Prefer acting over asking: only ask when there are zero matches or multiple plausible feed candidates after search.
|
|
182
|
+
- Multi‑source phrasing like "X and Y" should yield multiple \`fetch_feed\` requirements, each with its own \`quantity\` when specified.
|
|
183
|
+
- Never convert a plain name into a contract query; keep it as \`fetch_feed\`.
|
|
184
|
+
|
|
185
|
+
SEND INTENT
|
|
186
|
+
- Triggers: display/push/send/cast/send to device/play on FF1
|
|
187
|
+
- Always call confirm_send_playlist with filePath (default "./playlist.json") and optional deviceName
|
|
188
|
+
- Device selection: exact match → case‑insensitive → if multiple/none → ask user to choose
|
|
189
|
+
|
|
190
|
+
PUBLISH INTENT (CRITICAL)
|
|
191
|
+
- Triggers: "publish", "publish to my feed", "push to feed", "send to feed", "publish to feed"
|
|
192
|
+
- Distinguish from FF1 device commands: "publish" = feed server, "display/send to device" = FF1 device
|
|
193
|
+
- TWO MODES:
|
|
194
|
+
|
|
195
|
+
MODE 1: BUILD AND PUBLISH (user includes sources/requirements)
|
|
196
|
+
- Example: "Get tokens from 0xabc and publish to feed"
|
|
197
|
+
- When user mentions publishing WITH sources/requirements:
|
|
198
|
+
1. Immediately call get_feed_servers() to retrieve available feed servers
|
|
199
|
+
2. If only 1 server → use it directly in playlistSettings.feedServer
|
|
200
|
+
3. If 2+ servers → ask user "Which feed server?" with numbered list (e.g., "1) https://feed.feralfile.com 2) http://localhost:8787")
|
|
201
|
+
4. After selection, set playlistSettings.feedServer = { baseUrl, apiKey } from selected server
|
|
202
|
+
5. Acknowledge in Settings bullets (e.g., "publish to: https://feed.feralfile.com/api/v1")
|
|
203
|
+
- User can request both device display AND publishing (e.g., "send to FF1 and publish to feed") → set both deviceName and feedServer
|
|
204
|
+
- Publishing happens automatically after playlist verification passes
|
|
205
|
+
|
|
206
|
+
MODE 2: PUBLISH EXISTING FILE (user mentions "publish playlist" or "publish the playlist")
|
|
207
|
+
- Triggers: "publish playlist", "publish the playlist", "publish this playlist", "publish last playlist"
|
|
208
|
+
- Default file path: "./playlist.json" (unless user specifies a different path like "publish ./playlist-temp.json")
|
|
209
|
+
- When user wants to publish an existing file WITHOUT specifying sources:
|
|
210
|
+
1. Immediately call get_feed_servers() to retrieve available feed servers
|
|
211
|
+
2. If only 1 server → use it directly
|
|
212
|
+
3. If 2+ servers → ask user "Which feed server?" with numbered list
|
|
213
|
+
4. After selection, call confirm_publish_playlist with { filePath: "./playlist.json" (or user-specified path), feedServer: { baseUrl, apiKey } }
|
|
214
|
+
- DO NOT ask for sources/requirements in this mode—user wants to publish an already-created playlist file
|
|
215
|
+
|
|
216
|
+
COMMUNICATION STYLE
|
|
217
|
+
- Acknowledge briefly: "Got it." or "Understood." (one line).
|
|
218
|
+
- Do not repeat the user's request; do not paraphrase it.
|
|
219
|
+
- Bullet the extracted facts using friendly labels (no camelCase):
|
|
220
|
+
• What we're building (sources/collections/addresses)
|
|
221
|
+
• Settings (duration per item, keep order or shuffle, device, title/slug if provided)
|
|
222
|
+
- Prefer human units and plain words (e.g., "2 minutes per item", "send to device: Living Room").
|
|
223
|
+
- When device is resolved via get_configured_devices, ALWAYS show the resolved device name in Settings bullets (e.g., "send to device: Living Room").
|
|
224
|
+
- Use clear, direct language; no filler or corporate jargon; neutral, warm tone.
|
|
225
|
+
- Immediately call the function when ready. No extra narration.`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Intent parser function schemas
|
|
229
|
+
*/
|
|
230
|
+
const intentParserFunctionSchemas = [
|
|
231
|
+
{
|
|
232
|
+
type: 'function',
|
|
233
|
+
function: {
|
|
234
|
+
name: 'get_configured_devices',
|
|
235
|
+
description: 'Get the list of configured FF1 devices. Call this IMMEDIATELY when the user references a generic device name like "FF1", "my FF1", "my device", "my display", or similar. Use the first device from the returned list.',
|
|
236
|
+
parameters: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
type: 'function',
|
|
244
|
+
function: {
|
|
245
|
+
name: 'get_feed_servers',
|
|
246
|
+
description: 'Get the list of configured feed servers for publishing playlists. Call this IMMEDIATELY when the user mentions "publish", "push to feed", "send to feed", or "publish to my feed". Return list of available feed servers.',
|
|
247
|
+
parameters: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: {},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: 'function',
|
|
255
|
+
function: {
|
|
256
|
+
name: 'parse_requirements',
|
|
257
|
+
description: 'Parse validated and complete requirements. Only call this when you have all required information for each requirement: blockchain, contract address, token IDs, and source.',
|
|
258
|
+
parameters: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: {
|
|
261
|
+
requirements: {
|
|
262
|
+
type: 'array',
|
|
263
|
+
description: 'Array of parsed requirements. Each can be either build_playlist (specific NFTs), fetch_feed (feed playlist), or query_address (all NFTs from address)',
|
|
264
|
+
items: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
type: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
enum: ['build_playlist', 'fetch_feed', 'query_address'],
|
|
270
|
+
description: 'Type of requirement: build_playlist (specific NFTs), fetch_feed (feed playlist), or query_address (all NFTs from address)',
|
|
271
|
+
},
|
|
272
|
+
blockchain: {
|
|
273
|
+
type: 'string',
|
|
274
|
+
description: 'Blockchain network (ethereum, tezos) - only for build_playlist type',
|
|
275
|
+
},
|
|
276
|
+
contractAddress: {
|
|
277
|
+
type: 'string',
|
|
278
|
+
description: 'NFT contract address - only for build_playlist type',
|
|
279
|
+
},
|
|
280
|
+
tokenIds: {
|
|
281
|
+
type: 'array',
|
|
282
|
+
description: 'Array of token IDs to fetch - only for build_playlist type',
|
|
283
|
+
items: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
ownerAddress: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
description: 'Owner wallet address (0x... for Ethereum, tz... for Tezos) - only for query_address type',
|
|
290
|
+
},
|
|
291
|
+
source: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'Media URL or source identifier - optional for build_playlist type',
|
|
294
|
+
},
|
|
295
|
+
playlistName: {
|
|
296
|
+
type: 'string',
|
|
297
|
+
description: 'Playlist name in the feed (can be any playlist name) - only for fetch_feed type',
|
|
298
|
+
},
|
|
299
|
+
quantity: {
|
|
300
|
+
type: ['number', 'string'],
|
|
301
|
+
description: 'Number of items to fetch. Can be a number for specific count, or "all" to fetch all available tokens (default: 5 for fetch_feed, all for query_address unless specified)',
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
required: ['type'],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
playlistSettings: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
description: 'Playlist configuration settings',
|
|
310
|
+
properties: {
|
|
311
|
+
title: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'Playlist title (null for auto-generation)',
|
|
314
|
+
},
|
|
315
|
+
slug: {
|
|
316
|
+
type: 'string',
|
|
317
|
+
description: 'Playlist slug (null for auto-generation)',
|
|
318
|
+
},
|
|
319
|
+
durationPerItem: {
|
|
320
|
+
type: 'number',
|
|
321
|
+
description: 'Duration per item in seconds (e.g., 5 for "5 seconds each")',
|
|
322
|
+
},
|
|
323
|
+
totalDuration: {
|
|
324
|
+
type: 'number',
|
|
325
|
+
description: 'Total playlist duration in seconds (optional)',
|
|
326
|
+
},
|
|
327
|
+
preserveOrder: {
|
|
328
|
+
type: 'boolean',
|
|
329
|
+
description: 'Whether to preserve source order (true) or randomize (false)',
|
|
330
|
+
},
|
|
331
|
+
deviceName: {
|
|
332
|
+
type: 'string',
|
|
333
|
+
description: 'Device name to display on (null for first device, omit if no display requested)',
|
|
334
|
+
},
|
|
335
|
+
feedServer: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
description: 'Feed server for publishing playlist (omit if no publishing requested)',
|
|
338
|
+
properties: {
|
|
339
|
+
baseUrl: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
description: 'Feed server base URL',
|
|
342
|
+
},
|
|
343
|
+
apiKey: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
description: 'Optional API key for authentication',
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
required: ['baseUrl'],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
required: ['requirements'],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
type: 'function',
|
|
359
|
+
function: {
|
|
360
|
+
name: 'confirm_send_playlist',
|
|
361
|
+
description: 'Confirm the playlist file path and device name for sending. This function is called after the user mentions "send" or similar phrases.',
|
|
362
|
+
parameters: {
|
|
363
|
+
type: 'object',
|
|
364
|
+
properties: {
|
|
365
|
+
filePath: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
description: 'Path to the playlist file (default: "./playlist.json")',
|
|
368
|
+
},
|
|
369
|
+
deviceName: {
|
|
370
|
+
type: 'string',
|
|
371
|
+
description: 'Name of the device to send to (omit or leave empty if no specific device)',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
required: ['filePath'],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
type: 'function',
|
|
380
|
+
function: {
|
|
381
|
+
name: 'confirm_publish_playlist',
|
|
382
|
+
description: 'Confirm the playlist file path and feed server for publishing. This function is called when the user wants to publish an existing playlist file (e.g., "publish playlist", "publish the playlist").',
|
|
383
|
+
parameters: {
|
|
384
|
+
type: 'object',
|
|
385
|
+
properties: {
|
|
386
|
+
filePath: {
|
|
387
|
+
type: 'string',
|
|
388
|
+
description: 'Path to the playlist file (default: "./playlist.json")',
|
|
389
|
+
},
|
|
390
|
+
feedServer: {
|
|
391
|
+
type: 'object',
|
|
392
|
+
description: 'Feed server configuration for publishing',
|
|
393
|
+
properties: {
|
|
394
|
+
baseUrl: {
|
|
395
|
+
type: 'string',
|
|
396
|
+
description: 'Feed server base URL',
|
|
397
|
+
},
|
|
398
|
+
apiKey: {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description: 'Optional API key for authentication',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
required: ['baseUrl'],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
required: ['filePath', 'feedServer'],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: 'function',
|
|
412
|
+
function: {
|
|
413
|
+
name: 'verify_addresses',
|
|
414
|
+
description: 'Verify and validate Ethereum (0x...) and Tezos (tz1/tz2/tz3/KT1) wallet addresses. Call this when you detect the user has entered addresses and need to validate them before proceeding to parse requirements. This helps catch format errors early.',
|
|
415
|
+
parameters: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
addresses: {
|
|
419
|
+
type: 'array',
|
|
420
|
+
description: 'Array of Ethereum or Tezos addresses to verify',
|
|
421
|
+
items: {
|
|
422
|
+
type: 'string',
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
required: ['addresses'],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
];
|
|
431
|
+
exports.intentParserFunctionSchemas = intentParserFunctionSchemas;
|
|
432
|
+
/**
|
|
433
|
+
* Format markdown text for terminal display
|
|
434
|
+
*
|
|
435
|
+
* @param {string} text - Markdown text
|
|
436
|
+
* @returns {string} Formatted text with styling
|
|
437
|
+
*/
|
|
438
|
+
function formatMarkdown(text) {
|
|
439
|
+
if (!text) {
|
|
440
|
+
return '';
|
|
441
|
+
}
|
|
442
|
+
let formatted = text;
|
|
443
|
+
// Headings: # text, ## text, ### text, etc.
|
|
444
|
+
formatted = formatted.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, content) => {
|
|
445
|
+
const level = hashes.length;
|
|
446
|
+
if (level === 1) {
|
|
447
|
+
return chalk_1.default.bold.underline(content);
|
|
448
|
+
}
|
|
449
|
+
else if (level === 2) {
|
|
450
|
+
return chalk_1.default.bold(content);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
return chalk_1.default.bold(content);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
// Bold: **text** or __text__
|
|
457
|
+
formatted = formatted.replace(/\*\*(.+?)\*\*/g, (_, p1) => chalk_1.default.bold(p1));
|
|
458
|
+
formatted = formatted.replace(/__(.+?)__/g, (_, p1) => chalk_1.default.bold(p1));
|
|
459
|
+
// Italic: *text* or _text_
|
|
460
|
+
formatted = formatted.replace(/\*([^*]+)\*/g, (_, p1) => chalk_1.default.italic(p1));
|
|
461
|
+
formatted = formatted.replace(/(?<!\w)_([^_]+)_(?!\w)/g, (_, p1) => chalk_1.default.italic(p1));
|
|
462
|
+
// Inline code: `code` - light grey color
|
|
463
|
+
formatted = formatted.replace(/`([^`]+)`/g, (_, p1) => chalk_1.default.grey(p1));
|
|
464
|
+
// Links: [text](url) - show text in blue
|
|
465
|
+
formatted = formatted.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, p1) => chalk_1.default.blue(p1));
|
|
466
|
+
return formatted;
|
|
467
|
+
}
|
|
468
|
+
function printMarkdownContent(content) {
|
|
469
|
+
if (!content) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const lines = content.split('\n');
|
|
473
|
+
for (const line of lines) {
|
|
474
|
+
if (line.trim()) {
|
|
475
|
+
console.log(formatMarkdown(line));
|
|
476
|
+
}
|
|
477
|
+
else if (line === '') {
|
|
478
|
+
console.log();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function sleep(ms) {
|
|
483
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
484
|
+
}
|
|
485
|
+
async function processNonStreamingResponse(response) {
|
|
486
|
+
const message = response.choices[0]?.message;
|
|
487
|
+
if (!message) {
|
|
488
|
+
return { message: { role: 'assistant', content: null, refusal: null } };
|
|
489
|
+
}
|
|
490
|
+
if (message.content) {
|
|
491
|
+
printMarkdownContent(message.content);
|
|
492
|
+
console.log();
|
|
493
|
+
}
|
|
494
|
+
return { message };
|
|
495
|
+
}
|
|
496
|
+
async function createChatCompletion(client, requestParams, baseURL, maxRetries = 0) {
|
|
497
|
+
const isGoogle = Boolean(baseURL && baseURL.includes('generativelanguage.googleapis.com'));
|
|
498
|
+
const shouldStream = !isGoogle;
|
|
499
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
500
|
+
try {
|
|
501
|
+
if (shouldStream) {
|
|
502
|
+
const stream = await client.chat.completions.create({
|
|
503
|
+
...requestParams,
|
|
504
|
+
stream: true,
|
|
505
|
+
});
|
|
506
|
+
return await processStreamingResponse(stream);
|
|
507
|
+
}
|
|
508
|
+
const response = (await client.chat.completions.create({
|
|
509
|
+
...requestParams,
|
|
510
|
+
stream: false,
|
|
511
|
+
}));
|
|
512
|
+
return await processNonStreamingResponse(response);
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
const err = error;
|
|
516
|
+
const status = err.response?.status ?? err.status;
|
|
517
|
+
if (status === 429 && attempt < maxRetries) {
|
|
518
|
+
const retryAfterHeader = err.response?.headers?.['retry-after'] || err.response?.headers?.['Retry-After'];
|
|
519
|
+
const retryAfterMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : null;
|
|
520
|
+
const backoffMs = Math.min(10000, 2000 * Math.pow(2, attempt));
|
|
521
|
+
const delayMs = retryAfterMs && !Number.isNaN(retryAfterMs) ? retryAfterMs : backoffMs;
|
|
522
|
+
await sleep(delayMs);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
throw new Error('Failed to create chat completion');
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Process streaming response from AI
|
|
532
|
+
*
|
|
533
|
+
* @param {AsyncIterator} stream - OpenAI streaming response
|
|
534
|
+
* @returns {Promise<Object>} Collected message with content and tool calls
|
|
535
|
+
*/
|
|
536
|
+
async function processStreamingResponse(stream) {
|
|
537
|
+
let contentBuffer = '';
|
|
538
|
+
let toolCalls = [];
|
|
539
|
+
const toolCallsMap = {};
|
|
540
|
+
let role = 'assistant';
|
|
541
|
+
let printedUpTo = 0;
|
|
542
|
+
try {
|
|
543
|
+
for await (const chunk of stream) {
|
|
544
|
+
if (process.env.DEBUG_STREAMING) {
|
|
545
|
+
console.log('\n[DEBUG] Chunk:', JSON.stringify(chunk, null, 2));
|
|
546
|
+
}
|
|
547
|
+
const delta = chunk.choices[0]?.delta;
|
|
548
|
+
if (!delta) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (delta.role) {
|
|
552
|
+
role = delta.role;
|
|
553
|
+
}
|
|
554
|
+
// Collect content and print line by line
|
|
555
|
+
if (delta.content) {
|
|
556
|
+
contentBuffer += delta.content;
|
|
557
|
+
const lastNewlineIndex = contentBuffer.lastIndexOf('\n', contentBuffer.length - 1);
|
|
558
|
+
if (lastNewlineIndex >= printedUpTo) {
|
|
559
|
+
const textToPrint = contentBuffer.substring(printedUpTo, lastNewlineIndex + 1);
|
|
560
|
+
const lines = textToPrint.split('\n');
|
|
561
|
+
for (const line of lines) {
|
|
562
|
+
if (line.trim()) {
|
|
563
|
+
const formatted = formatMarkdown(line);
|
|
564
|
+
console.log(formatted);
|
|
565
|
+
}
|
|
566
|
+
else if (line === '') {
|
|
567
|
+
console.log();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
printedUpTo = lastNewlineIndex + 1;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Collect tool calls
|
|
574
|
+
if (delta.tool_calls) {
|
|
575
|
+
for (const toolCallDelta of delta.tool_calls) {
|
|
576
|
+
const index = toolCallDelta.index;
|
|
577
|
+
if (!toolCallsMap[index]) {
|
|
578
|
+
toolCallsMap[index] = {
|
|
579
|
+
id: '',
|
|
580
|
+
type: 'function',
|
|
581
|
+
function: {
|
|
582
|
+
name: '',
|
|
583
|
+
arguments: '',
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (toolCallDelta.id) {
|
|
588
|
+
toolCallsMap[index].id = toolCallDelta.id;
|
|
589
|
+
}
|
|
590
|
+
if (toolCallDelta.function) {
|
|
591
|
+
if (toolCallDelta.function.name) {
|
|
592
|
+
toolCallsMap[index].function.name += toolCallDelta.function.name;
|
|
593
|
+
}
|
|
594
|
+
if (toolCallDelta.function.arguments) {
|
|
595
|
+
toolCallsMap[index].function.arguments += toolCallDelta.function.arguments;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
// Log streaming error but continue with what we have
|
|
604
|
+
if (process.env.DEBUG) {
|
|
605
|
+
console.error(chalk_1.default.red('\n[Streaming Error]'), error.message);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Print remaining content
|
|
609
|
+
if (printedUpTo < contentBuffer.length) {
|
|
610
|
+
const remainingText = contentBuffer.substring(printedUpTo);
|
|
611
|
+
if (remainingText.trim()) {
|
|
612
|
+
const formatted = formatMarkdown(remainingText);
|
|
613
|
+
console.log(formatted);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (contentBuffer.length > 0) {
|
|
617
|
+
console.log(); // Extra newline after AI response
|
|
618
|
+
}
|
|
619
|
+
// Convert toolCallsMap to array
|
|
620
|
+
toolCalls = Object.values(toolCallsMap).filter((tc) => tc.id);
|
|
621
|
+
const message = {
|
|
622
|
+
role: role,
|
|
623
|
+
content: contentBuffer.trim() || null,
|
|
624
|
+
refusal: null,
|
|
625
|
+
};
|
|
626
|
+
if (toolCalls.length > 0) {
|
|
627
|
+
message.tool_calls = toolCalls;
|
|
628
|
+
}
|
|
629
|
+
return { message };
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Process intent parser conversation
|
|
633
|
+
*
|
|
634
|
+
* @param {string} userRequest - User's natural language request
|
|
635
|
+
* @param {Object} options - Options
|
|
636
|
+
* @param {string} [options.modelName] - Model to use
|
|
637
|
+
* @param {Object} [options.conversationContext] - Previous conversation context
|
|
638
|
+
* @returns {Promise<Object>} Intent parser result
|
|
639
|
+
*/
|
|
640
|
+
async function processIntentParserRequest(userRequest, options = {}) {
|
|
641
|
+
const { modelName, conversationContext } = options;
|
|
642
|
+
const client = createIntentParserClient(modelName);
|
|
643
|
+
const modelConfig = (0, config_1.getModelConfig)(modelName);
|
|
644
|
+
const config = (0, config_1.getConfig)();
|
|
645
|
+
const systemMessage = buildIntentParserSystemPrompt();
|
|
646
|
+
const messages = [
|
|
647
|
+
{ role: 'system', content: systemMessage },
|
|
648
|
+
];
|
|
649
|
+
// Add conversation context if continuing
|
|
650
|
+
if (conversationContext && conversationContext.messages) {
|
|
651
|
+
messages.push(...conversationContext.messages);
|
|
652
|
+
}
|
|
653
|
+
messages.push({ role: 'user', content: userRequest });
|
|
654
|
+
try {
|
|
655
|
+
const requestParams = {
|
|
656
|
+
model: modelConfig.model,
|
|
657
|
+
messages,
|
|
658
|
+
tools: intentParserFunctionSchemas,
|
|
659
|
+
tool_choice: 'auto',
|
|
660
|
+
stream: true,
|
|
661
|
+
};
|
|
662
|
+
// Set temperature based on model
|
|
663
|
+
if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
|
|
664
|
+
requestParams.temperature = modelConfig.temperature;
|
|
665
|
+
}
|
|
666
|
+
else if (modelConfig.temperature === 1) {
|
|
667
|
+
requestParams.temperature = 1;
|
|
668
|
+
}
|
|
669
|
+
if (modelConfig.model.startsWith('gpt-')) {
|
|
670
|
+
requestParams.max_completion_tokens = 2000;
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
requestParams.max_tokens = 2000;
|
|
674
|
+
}
|
|
675
|
+
const { message } = await createChatCompletion(client, requestParams, modelConfig.baseURL, modelConfig.maxRetries);
|
|
676
|
+
// Check if AI wants to pass parsed requirements
|
|
677
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
678
|
+
const toolCall = message.tool_calls[0];
|
|
679
|
+
if (toolCall.function.name === 'get_configured_devices') {
|
|
680
|
+
// Get the list of configured devices
|
|
681
|
+
const { getConfiguredDevices } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
|
|
682
|
+
const result = await getConfiguredDevices();
|
|
683
|
+
// Add tool result to messages and continue conversation
|
|
684
|
+
const toolResultMessage = {
|
|
685
|
+
role: 'tool',
|
|
686
|
+
tool_call_id: toolCall.id,
|
|
687
|
+
content: JSON.stringify(result),
|
|
688
|
+
};
|
|
689
|
+
const updatedMessages = [...messages, message, toolResultMessage];
|
|
690
|
+
// Continue the conversation with the device list
|
|
691
|
+
const followUpRequest = {
|
|
692
|
+
model: modelConfig.model,
|
|
693
|
+
messages: updatedMessages,
|
|
694
|
+
tools: intentParserFunctionSchemas,
|
|
695
|
+
tool_choice: 'auto',
|
|
696
|
+
stream: true,
|
|
697
|
+
};
|
|
698
|
+
if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
|
|
699
|
+
followUpRequest.temperature = modelConfig.temperature;
|
|
700
|
+
}
|
|
701
|
+
else if (modelConfig.temperature === 1) {
|
|
702
|
+
followUpRequest.temperature = 1;
|
|
703
|
+
}
|
|
704
|
+
if (modelConfig.model.startsWith('gpt-')) {
|
|
705
|
+
followUpRequest.max_completion_tokens = 2000;
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
followUpRequest.max_tokens = 2000;
|
|
709
|
+
}
|
|
710
|
+
const { message: followUpMessage } = await createChatCompletion(client, followUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
|
|
711
|
+
// Check if AI now wants to parse requirements
|
|
712
|
+
if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
|
|
713
|
+
const followUpToolCall = followUpMessage.tool_calls[0];
|
|
714
|
+
if (followUpToolCall.function.name === 'parse_requirements') {
|
|
715
|
+
const params = JSON.parse(followUpToolCall.function.arguments);
|
|
716
|
+
// Apply constraints and defaults
|
|
717
|
+
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
718
|
+
return {
|
|
719
|
+
approved: true,
|
|
720
|
+
params: validatedParams,
|
|
721
|
+
needsMoreInfo: false,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
else if (followUpToolCall.function.name === 'get_feed_servers') {
|
|
725
|
+
// Get the list of configured feed servers
|
|
726
|
+
const feedConfig = (0, config_1.getFeedConfig)();
|
|
727
|
+
// Build the server list for the AI
|
|
728
|
+
const serverList = feedConfig.servers?.map((server) => ({
|
|
729
|
+
baseUrl: server.baseUrl,
|
|
730
|
+
apiKey: server.apiKey,
|
|
731
|
+
})) ||
|
|
732
|
+
feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
|
|
733
|
+
// Add tool result to messages and continue conversation
|
|
734
|
+
const feedToolResultMessage = {
|
|
735
|
+
role: 'tool',
|
|
736
|
+
tool_call_id: followUpToolCall.id,
|
|
737
|
+
content: JSON.stringify({ servers: serverList }),
|
|
738
|
+
};
|
|
739
|
+
const feedUpdatedMessages = [
|
|
740
|
+
...updatedMessages,
|
|
741
|
+
followUpMessage,
|
|
742
|
+
feedToolResultMessage,
|
|
743
|
+
];
|
|
744
|
+
// Continue the conversation with the feed server list
|
|
745
|
+
const feedFollowUpRequest = {
|
|
746
|
+
model: modelConfig.model,
|
|
747
|
+
messages: feedUpdatedMessages,
|
|
748
|
+
tools: intentParserFunctionSchemas,
|
|
749
|
+
tool_choice: 'auto',
|
|
750
|
+
stream: true,
|
|
751
|
+
};
|
|
752
|
+
if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
|
|
753
|
+
feedFollowUpRequest.temperature = modelConfig.temperature;
|
|
754
|
+
}
|
|
755
|
+
else if (modelConfig.temperature === 1) {
|
|
756
|
+
feedFollowUpRequest.temperature = 1;
|
|
757
|
+
}
|
|
758
|
+
if (modelConfig.model.startsWith('gpt-')) {
|
|
759
|
+
feedFollowUpRequest.max_completion_tokens =
|
|
760
|
+
2000;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
feedFollowUpRequest.max_tokens = 2000;
|
|
764
|
+
}
|
|
765
|
+
const { message: feedFollowUpMessage } = await createChatCompletion(client, feedFollowUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
|
|
766
|
+
// Check if AI now wants to parse requirements or publish
|
|
767
|
+
if (feedFollowUpMessage.tool_calls && feedFollowUpMessage.tool_calls.length > 0) {
|
|
768
|
+
const feedToolCall = feedFollowUpMessage.tool_calls[0];
|
|
769
|
+
if (feedToolCall.function.name === 'parse_requirements') {
|
|
770
|
+
const params = JSON.parse(feedToolCall.function.arguments);
|
|
771
|
+
// Apply constraints and defaults
|
|
772
|
+
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
773
|
+
return {
|
|
774
|
+
approved: true,
|
|
775
|
+
params: validatedParams,
|
|
776
|
+
needsMoreInfo: false,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
else if (feedToolCall.function.name === 'confirm_publish_playlist') {
|
|
780
|
+
const args = JSON.parse(feedToolCall.function.arguments);
|
|
781
|
+
const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
|
|
782
|
+
console.log();
|
|
783
|
+
console.log(chalk_1.default.cyan('Publishing to feed server...'));
|
|
784
|
+
const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
|
|
785
|
+
if (publishResult.success) {
|
|
786
|
+
console.log(chalk_1.default.green('✓ Published to feed server'));
|
|
787
|
+
if (publishResult.playlistId) {
|
|
788
|
+
console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
|
|
789
|
+
}
|
|
790
|
+
if (publishResult.feedServer) {
|
|
791
|
+
console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
|
|
792
|
+
}
|
|
793
|
+
console.log();
|
|
794
|
+
return {
|
|
795
|
+
approved: true,
|
|
796
|
+
params: {
|
|
797
|
+
action: 'publish_playlist',
|
|
798
|
+
filePath: args.filePath,
|
|
799
|
+
feedServer: args.feedServer,
|
|
800
|
+
playlistId: publishResult.playlistId,
|
|
801
|
+
success: true,
|
|
802
|
+
},
|
|
803
|
+
needsMoreInfo: false,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
console.error(chalk_1.default.red('✗ Failed to publish: ' + publishResult.error));
|
|
808
|
+
if (publishResult.message) {
|
|
809
|
+
console.error(chalk_1.default.gray(` ${publishResult.message}`));
|
|
810
|
+
}
|
|
811
|
+
console.log();
|
|
812
|
+
return {
|
|
813
|
+
approved: false,
|
|
814
|
+
needsMoreInfo: false,
|
|
815
|
+
params: {
|
|
816
|
+
action: 'publish_playlist',
|
|
817
|
+
success: false,
|
|
818
|
+
error: publishResult.error,
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// AI might be asking a question or needs more info
|
|
825
|
+
return {
|
|
826
|
+
approved: false,
|
|
827
|
+
needsMoreInfo: true,
|
|
828
|
+
question: feedFollowUpMessage.content || undefined,
|
|
829
|
+
messages: [...feedUpdatedMessages, feedFollowUpMessage],
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
else if (followUpToolCall.function.name === 'confirm_publish_playlist') {
|
|
833
|
+
// Handle publish after feed server selection
|
|
834
|
+
const args = JSON.parse(followUpToolCall.function.arguments);
|
|
835
|
+
const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
|
|
836
|
+
console.log();
|
|
837
|
+
console.log(chalk_1.default.cyan('Publishing to feed server...'));
|
|
838
|
+
const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
|
|
839
|
+
if (publishResult.success) {
|
|
840
|
+
console.log(chalk_1.default.green('✓ Published to feed server'));
|
|
841
|
+
if (publishResult.playlistId) {
|
|
842
|
+
console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
|
|
843
|
+
}
|
|
844
|
+
if (publishResult.feedServer) {
|
|
845
|
+
console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
|
|
846
|
+
}
|
|
847
|
+
console.log();
|
|
848
|
+
return {
|
|
849
|
+
approved: true,
|
|
850
|
+
params: {
|
|
851
|
+
action: 'publish_playlist',
|
|
852
|
+
filePath: args.filePath,
|
|
853
|
+
feedServer: args.feedServer,
|
|
854
|
+
playlistId: publishResult.playlistId,
|
|
855
|
+
success: true,
|
|
856
|
+
},
|
|
857
|
+
needsMoreInfo: false,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
console.error(chalk_1.default.red('✗ Failed to publish: ' + publishResult.error));
|
|
862
|
+
if (publishResult.message) {
|
|
863
|
+
console.error(chalk_1.default.gray(` ${publishResult.message}`));
|
|
864
|
+
}
|
|
865
|
+
console.log();
|
|
866
|
+
return {
|
|
867
|
+
approved: false,
|
|
868
|
+
needsMoreInfo: false,
|
|
869
|
+
params: {
|
|
870
|
+
action: 'publish_playlist',
|
|
871
|
+
success: false,
|
|
872
|
+
error: publishResult.error,
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
// Unhandled tool call - add assistant message and tool response to messages
|
|
879
|
+
const toolResultMessage = {
|
|
880
|
+
role: 'tool',
|
|
881
|
+
tool_call_id: followUpToolCall.id,
|
|
882
|
+
content: JSON.stringify({
|
|
883
|
+
error: `Unknown function: ${followUpToolCall.function.name}`,
|
|
884
|
+
}),
|
|
885
|
+
};
|
|
886
|
+
const validMessages = [...updatedMessages, followUpMessage, toolResultMessage];
|
|
887
|
+
// AI is still asking for more information after the error
|
|
888
|
+
return {
|
|
889
|
+
approved: false,
|
|
890
|
+
needsMoreInfo: true,
|
|
891
|
+
question: followUpMessage.content ||
|
|
892
|
+
`Encountered unknown function: ${followUpToolCall.function.name}`,
|
|
893
|
+
messages: validMessages,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// AI is still asking for more information
|
|
898
|
+
return {
|
|
899
|
+
approved: false,
|
|
900
|
+
needsMoreInfo: true,
|
|
901
|
+
question: followUpMessage.content || undefined,
|
|
902
|
+
messages: [...updatedMessages, followUpMessage],
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
else if (toolCall.function.name === 'get_feed_servers') {
|
|
906
|
+
// Get the list of configured feed servers
|
|
907
|
+
const feedConfig = (0, config_1.getFeedConfig)();
|
|
908
|
+
// Build the server list for the AI
|
|
909
|
+
const serverList = feedConfig.servers?.map((server) => ({
|
|
910
|
+
baseUrl: server.baseUrl,
|
|
911
|
+
apiKey: server.apiKey,
|
|
912
|
+
})) || feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
|
|
913
|
+
// Add tool result to messages and continue conversation
|
|
914
|
+
const toolResultMessage = {
|
|
915
|
+
role: 'tool',
|
|
916
|
+
tool_call_id: toolCall.id,
|
|
917
|
+
content: JSON.stringify({ servers: serverList }),
|
|
918
|
+
};
|
|
919
|
+
const updatedMessages = [...messages, message, toolResultMessage];
|
|
920
|
+
// Continue the conversation with the feed server list
|
|
921
|
+
const followUpRequest = {
|
|
922
|
+
model: modelConfig.model,
|
|
923
|
+
messages: updatedMessages,
|
|
924
|
+
tools: intentParserFunctionSchemas,
|
|
925
|
+
tool_choice: 'auto',
|
|
926
|
+
stream: true,
|
|
927
|
+
};
|
|
928
|
+
if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
|
|
929
|
+
followUpRequest.temperature = modelConfig.temperature;
|
|
930
|
+
}
|
|
931
|
+
else if (modelConfig.temperature === 1) {
|
|
932
|
+
followUpRequest.temperature = 1;
|
|
933
|
+
}
|
|
934
|
+
if (modelConfig.model.startsWith('gpt-')) {
|
|
935
|
+
followUpRequest.max_completion_tokens = 2000;
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
followUpRequest.max_tokens = 2000;
|
|
939
|
+
}
|
|
940
|
+
const { message: followUpMessage } = await createChatCompletion(client, followUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
|
|
941
|
+
// Check if AI now wants to parse requirements or publish
|
|
942
|
+
if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
|
|
943
|
+
const followUpToolCall = followUpMessage.tool_calls[0];
|
|
944
|
+
if (followUpToolCall.function.name === 'parse_requirements') {
|
|
945
|
+
const params = JSON.parse(followUpToolCall.function.arguments);
|
|
946
|
+
// Apply constraints and defaults
|
|
947
|
+
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
948
|
+
return {
|
|
949
|
+
approved: true,
|
|
950
|
+
params: validatedParams,
|
|
951
|
+
needsMoreInfo: false,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
else if (followUpToolCall.function.name === 'confirm_publish_playlist') {
|
|
955
|
+
const args = JSON.parse(followUpToolCall.function.arguments);
|
|
956
|
+
const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
|
|
957
|
+
console.log();
|
|
958
|
+
console.log(chalk_1.default.cyan('Publishing to feed server...'));
|
|
959
|
+
const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
|
|
960
|
+
if (publishResult.success) {
|
|
961
|
+
console.log(chalk_1.default.green('✓ Published to feed server'));
|
|
962
|
+
if (publishResult.playlistId) {
|
|
963
|
+
console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
|
|
964
|
+
}
|
|
965
|
+
if (publishResult.feedServer) {
|
|
966
|
+
console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
|
|
967
|
+
}
|
|
968
|
+
console.log();
|
|
969
|
+
return {
|
|
970
|
+
approved: true,
|
|
971
|
+
params: {
|
|
972
|
+
action: 'publish_playlist',
|
|
973
|
+
filePath: args.filePath,
|
|
974
|
+
feedServer: args.feedServer,
|
|
975
|
+
playlistId: publishResult.playlistId,
|
|
976
|
+
success: true,
|
|
977
|
+
},
|
|
978
|
+
needsMoreInfo: false,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
console.error(chalk_1.default.red('✗ Failed to publish: ' + publishResult.error));
|
|
983
|
+
if (publishResult.message) {
|
|
984
|
+
console.error(chalk_1.default.gray(` ${publishResult.message}`));
|
|
985
|
+
}
|
|
986
|
+
console.log();
|
|
987
|
+
return {
|
|
988
|
+
approved: false,
|
|
989
|
+
needsMoreInfo: false,
|
|
990
|
+
params: {
|
|
991
|
+
action: 'publish_playlist',
|
|
992
|
+
success: false,
|
|
993
|
+
error: publishResult.error,
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// AI might be asking a question or needs more info
|
|
1000
|
+
return {
|
|
1001
|
+
approved: false,
|
|
1002
|
+
needsMoreInfo: true,
|
|
1003
|
+
question: followUpMessage.content || undefined,
|
|
1004
|
+
messages: [...updatedMessages, followUpMessage],
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
else if (toolCall.function.name === 'parse_requirements') {
|
|
1008
|
+
const params = JSON.parse(toolCall.function.arguments);
|
|
1009
|
+
// Apply constraints and defaults
|
|
1010
|
+
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
1011
|
+
return {
|
|
1012
|
+
approved: true,
|
|
1013
|
+
params: validatedParams,
|
|
1014
|
+
needsMoreInfo: false,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
else if (toolCall.function.name === 'confirm_send_playlist') {
|
|
1018
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
1019
|
+
const { confirmPlaylistForSending } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-send')));
|
|
1020
|
+
// Validate and confirm the playlist
|
|
1021
|
+
const confirmation = await confirmPlaylistForSending(args.filePath, args.deviceName);
|
|
1022
|
+
if (!confirmation.success) {
|
|
1023
|
+
// Add tool response message to make conversation valid
|
|
1024
|
+
const toolResultMessage = {
|
|
1025
|
+
role: 'tool',
|
|
1026
|
+
tool_call_id: toolCall.id,
|
|
1027
|
+
content: JSON.stringify({
|
|
1028
|
+
success: false,
|
|
1029
|
+
error: confirmation.error,
|
|
1030
|
+
message: confirmation.message,
|
|
1031
|
+
}),
|
|
1032
|
+
};
|
|
1033
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1034
|
+
// Check if this is a device selection needed case
|
|
1035
|
+
if (confirmation.needsDeviceSelection) {
|
|
1036
|
+
// Multiple devices available - ask user to choose
|
|
1037
|
+
console.log();
|
|
1038
|
+
return {
|
|
1039
|
+
approved: false,
|
|
1040
|
+
needsMoreInfo: true,
|
|
1041
|
+
question: confirmation.message || 'Please choose a device',
|
|
1042
|
+
messages: validMessages,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// File not found or playlist invalid - ask user for more info
|
|
1046
|
+
console.log();
|
|
1047
|
+
return {
|
|
1048
|
+
approved: false,
|
|
1049
|
+
needsMoreInfo: true,
|
|
1050
|
+
question: confirmation.message || `Failed to send playlist: ${confirmation.error}`,
|
|
1051
|
+
messages: validMessages,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
// Playlist is valid - return as approved with send_playlist action
|
|
1055
|
+
return {
|
|
1056
|
+
approved: true,
|
|
1057
|
+
params: {
|
|
1058
|
+
action: 'send_playlist',
|
|
1059
|
+
filePath: confirmation.filePath,
|
|
1060
|
+
deviceName: confirmation.deviceName,
|
|
1061
|
+
playlist: confirmation.playlist,
|
|
1062
|
+
message: confirmation.message,
|
|
1063
|
+
},
|
|
1064
|
+
needsMoreInfo: false,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
else if (toolCall.function.name === 'confirm_publish_playlist') {
|
|
1068
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
1069
|
+
const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
|
|
1070
|
+
// Publish the playlist
|
|
1071
|
+
console.log();
|
|
1072
|
+
console.log(chalk_1.default.cyan('Publishing to feed server...'));
|
|
1073
|
+
const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
|
|
1074
|
+
if (publishResult.success) {
|
|
1075
|
+
console.log(chalk_1.default.green('✓ Published to feed server'));
|
|
1076
|
+
if (publishResult.playlistId) {
|
|
1077
|
+
console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
|
|
1078
|
+
}
|
|
1079
|
+
if (publishResult.feedServer) {
|
|
1080
|
+
console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
|
|
1081
|
+
}
|
|
1082
|
+
console.log();
|
|
1083
|
+
return {
|
|
1084
|
+
approved: true,
|
|
1085
|
+
params: {
|
|
1086
|
+
action: 'publish_playlist',
|
|
1087
|
+
filePath: args.filePath,
|
|
1088
|
+
feedServer: args.feedServer,
|
|
1089
|
+
playlistId: publishResult.playlistId,
|
|
1090
|
+
success: true,
|
|
1091
|
+
},
|
|
1092
|
+
needsMoreInfo: false,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
console.error(chalk_1.default.red('✗ Failed to publish: ' + publishResult.error));
|
|
1097
|
+
if (publishResult.message) {
|
|
1098
|
+
console.error(chalk_1.default.gray(` ${publishResult.message}`));
|
|
1099
|
+
}
|
|
1100
|
+
console.log();
|
|
1101
|
+
return {
|
|
1102
|
+
approved: false,
|
|
1103
|
+
needsMoreInfo: false,
|
|
1104
|
+
params: {
|
|
1105
|
+
action: 'publish_playlist',
|
|
1106
|
+
success: false,
|
|
1107
|
+
error: publishResult.error,
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
else if (toolCall.function.name === 'verify_addresses') {
|
|
1113
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
1114
|
+
const { verifyAddresses } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
|
|
1115
|
+
const verificationResult = await verifyAddresses({ addresses: args.addresses });
|
|
1116
|
+
if (!verificationResult.valid) {
|
|
1117
|
+
// Add tool response message for invalid addresses
|
|
1118
|
+
const toolResultMessage = {
|
|
1119
|
+
role: 'tool',
|
|
1120
|
+
tool_call_id: toolCall.id,
|
|
1121
|
+
content: JSON.stringify({
|
|
1122
|
+
valid: false,
|
|
1123
|
+
errors: verificationResult.errors,
|
|
1124
|
+
results: verificationResult.results,
|
|
1125
|
+
}),
|
|
1126
|
+
};
|
|
1127
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1128
|
+
// Ask user to correct the addresses
|
|
1129
|
+
return {
|
|
1130
|
+
approved: false,
|
|
1131
|
+
needsMoreInfo: true,
|
|
1132
|
+
question: `Some addresses are invalid. ${verificationResult.errors.join(' ')} Please provide correct addresses.`,
|
|
1133
|
+
messages: validMessages,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
// All addresses are valid - continue to next step
|
|
1137
|
+
const toolResultMessage = {
|
|
1138
|
+
role: 'tool',
|
|
1139
|
+
tool_call_id: toolCall.id,
|
|
1140
|
+
content: JSON.stringify({
|
|
1141
|
+
valid: true,
|
|
1142
|
+
results: verificationResult.results,
|
|
1143
|
+
}),
|
|
1144
|
+
};
|
|
1145
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1146
|
+
// Continue conversation after validation
|
|
1147
|
+
const followUpRequest = {
|
|
1148
|
+
model: modelConfig.model,
|
|
1149
|
+
messages: validMessages,
|
|
1150
|
+
tools: intentParserFunctionSchemas,
|
|
1151
|
+
tool_choice: 'auto',
|
|
1152
|
+
stream: true,
|
|
1153
|
+
};
|
|
1154
|
+
if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
|
|
1155
|
+
followUpRequest.temperature = modelConfig.temperature;
|
|
1156
|
+
}
|
|
1157
|
+
else if (modelConfig.temperature === 1) {
|
|
1158
|
+
followUpRequest.temperature = 1;
|
|
1159
|
+
}
|
|
1160
|
+
if (modelConfig.model.startsWith('gpt-')) {
|
|
1161
|
+
followUpRequest.max_completion_tokens = 2000;
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
followUpRequest.max_tokens = 2000;
|
|
1165
|
+
}
|
|
1166
|
+
const { message: followUpMessage } = await createChatCompletion(client, followUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
|
|
1167
|
+
// Check if AI now wants to parse requirements or get feed servers
|
|
1168
|
+
if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
|
|
1169
|
+
const followUpToolCall = followUpMessage.tool_calls[0];
|
|
1170
|
+
if (followUpToolCall.function.name === 'parse_requirements') {
|
|
1171
|
+
const params = JSON.parse(followUpToolCall.function.arguments);
|
|
1172
|
+
// Apply constraints and defaults
|
|
1173
|
+
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
1174
|
+
return {
|
|
1175
|
+
approved: true,
|
|
1176
|
+
params: validatedParams,
|
|
1177
|
+
needsMoreInfo: false,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
else if (followUpToolCall.function.name === 'get_feed_servers') {
|
|
1181
|
+
// Get the list of configured feed servers
|
|
1182
|
+
const feedConfig = (0, config_1.getFeedConfig)();
|
|
1183
|
+
// Build the server list for the AI
|
|
1184
|
+
const serverList = feedConfig.servers?.map((server) => ({
|
|
1185
|
+
baseUrl: server.baseUrl,
|
|
1186
|
+
apiKey: server.apiKey,
|
|
1187
|
+
})) ||
|
|
1188
|
+
feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
|
|
1189
|
+
// Add tool result to messages and continue conversation
|
|
1190
|
+
const feedToolResultMessage = {
|
|
1191
|
+
role: 'tool',
|
|
1192
|
+
tool_call_id: followUpToolCall.id,
|
|
1193
|
+
content: JSON.stringify({ servers: serverList }),
|
|
1194
|
+
};
|
|
1195
|
+
const feedUpdatedMessages = [...validMessages, followUpMessage, feedToolResultMessage];
|
|
1196
|
+
// Continue the conversation with the feed server list
|
|
1197
|
+
const feedFollowUpRequest = {
|
|
1198
|
+
model: modelConfig.model,
|
|
1199
|
+
messages: feedUpdatedMessages,
|
|
1200
|
+
tools: intentParserFunctionSchemas,
|
|
1201
|
+
tool_choice: 'auto',
|
|
1202
|
+
stream: true,
|
|
1203
|
+
};
|
|
1204
|
+
if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
|
|
1205
|
+
feedFollowUpRequest.temperature = modelConfig.temperature;
|
|
1206
|
+
}
|
|
1207
|
+
else if (modelConfig.temperature === 1) {
|
|
1208
|
+
feedFollowUpRequest.temperature = 1;
|
|
1209
|
+
}
|
|
1210
|
+
if (modelConfig.model.startsWith('gpt-')) {
|
|
1211
|
+
feedFollowUpRequest.max_completion_tokens =
|
|
1212
|
+
2000;
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
feedFollowUpRequest.max_tokens = 2000;
|
|
1216
|
+
}
|
|
1217
|
+
const { message: feedFollowUpMessage } = await createChatCompletion(client, feedFollowUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
|
|
1218
|
+
// Check if AI now wants to parse requirements or publish
|
|
1219
|
+
if (feedFollowUpMessage.tool_calls && feedFollowUpMessage.tool_calls.length > 0) {
|
|
1220
|
+
const feedToolCall = feedFollowUpMessage.tool_calls[0];
|
|
1221
|
+
if (feedToolCall.function.name === 'parse_requirements') {
|
|
1222
|
+
const params = JSON.parse(feedToolCall.function.arguments);
|
|
1223
|
+
// Apply constraints and defaults
|
|
1224
|
+
const validatedParams = (0, utils_1.applyConstraints)(params, config);
|
|
1225
|
+
return {
|
|
1226
|
+
approved: true,
|
|
1227
|
+
params: validatedParams,
|
|
1228
|
+
needsMoreInfo: false,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
else if (feedToolCall.function.name === 'confirm_publish_playlist') {
|
|
1232
|
+
const args = JSON.parse(feedToolCall.function.arguments);
|
|
1233
|
+
const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
|
|
1234
|
+
console.log();
|
|
1235
|
+
console.log(chalk_1.default.cyan('Publishing to feed server...'));
|
|
1236
|
+
const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
|
|
1237
|
+
if (publishResult.success) {
|
|
1238
|
+
console.log(chalk_1.default.green('✓ Published to feed server'));
|
|
1239
|
+
if (publishResult.playlistId) {
|
|
1240
|
+
console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
|
|
1241
|
+
}
|
|
1242
|
+
if (publishResult.feedServer) {
|
|
1243
|
+
console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
|
|
1244
|
+
}
|
|
1245
|
+
console.log();
|
|
1246
|
+
return {
|
|
1247
|
+
approved: true,
|
|
1248
|
+
params: {
|
|
1249
|
+
action: 'publish_playlist',
|
|
1250
|
+
filePath: args.filePath,
|
|
1251
|
+
feedServer: args.feedServer,
|
|
1252
|
+
playlistId: publishResult.playlistId,
|
|
1253
|
+
success: true,
|
|
1254
|
+
},
|
|
1255
|
+
needsMoreInfo: false,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
console.error(chalk_1.default.red('✗ Failed to publish: ' + publishResult.error));
|
|
1260
|
+
if (publishResult.message) {
|
|
1261
|
+
console.error(chalk_1.default.gray(` ${publishResult.message}`));
|
|
1262
|
+
}
|
|
1263
|
+
console.log();
|
|
1264
|
+
return {
|
|
1265
|
+
approved: false,
|
|
1266
|
+
needsMoreInfo: false,
|
|
1267
|
+
params: {
|
|
1268
|
+
action: 'publish_playlist',
|
|
1269
|
+
success: false,
|
|
1270
|
+
error: publishResult.error,
|
|
1271
|
+
},
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
// AI might be asking a question or needs more info
|
|
1277
|
+
return {
|
|
1278
|
+
approved: false,
|
|
1279
|
+
needsMoreInfo: true,
|
|
1280
|
+
question: feedFollowUpMessage.content || undefined,
|
|
1281
|
+
messages: [...feedUpdatedMessages, feedFollowUpMessage],
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
// AI might be asking a question or needs more info
|
|
1286
|
+
if (followUpMessage.content) {
|
|
1287
|
+
return {
|
|
1288
|
+
approved: false,
|
|
1289
|
+
needsMoreInfo: true,
|
|
1290
|
+
question: followUpMessage.content,
|
|
1291
|
+
messages: [...validMessages, followUpMessage],
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
approved: false,
|
|
1296
|
+
needsMoreInfo: false,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
// Unhandled tool call at top level
|
|
1301
|
+
const toolResultMessage = {
|
|
1302
|
+
role: 'tool',
|
|
1303
|
+
tool_call_id: toolCall.id,
|
|
1304
|
+
content: JSON.stringify({
|
|
1305
|
+
error: `Unknown function: ${toolCall.function.name}`,
|
|
1306
|
+
}),
|
|
1307
|
+
};
|
|
1308
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1309
|
+
return {
|
|
1310
|
+
approved: false,
|
|
1311
|
+
needsMoreInfo: true,
|
|
1312
|
+
question: message.content || `Encountered unknown function: ${toolCall.function.name}`,
|
|
1313
|
+
messages: validMessages,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
// AI is asking for more information
|
|
1318
|
+
return {
|
|
1319
|
+
approved: false,
|
|
1320
|
+
needsMoreInfo: true,
|
|
1321
|
+
question: message.content || undefined,
|
|
1322
|
+
messages: [...messages, message],
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
catch (error) {
|
|
1326
|
+
const err = error;
|
|
1327
|
+
const status = err.response?.status ?? err.status;
|
|
1328
|
+
const statusText = err.response?.statusText;
|
|
1329
|
+
const responseDetails = err.response?.data && typeof err.response.data === 'string'
|
|
1330
|
+
? err.response.data
|
|
1331
|
+
: err.response?.data
|
|
1332
|
+
? JSON.stringify(err.response.data)
|
|
1333
|
+
: null;
|
|
1334
|
+
const context = `model=${modelConfig.model}, baseURL=${modelConfig.baseURL}`;
|
|
1335
|
+
const detailParts = [
|
|
1336
|
+
err.message,
|
|
1337
|
+
status ? `status ${status}${statusText ? ` ${statusText}` : ''}` : null,
|
|
1338
|
+
responseDetails ? `response ${responseDetails}` : null,
|
|
1339
|
+
].filter(Boolean);
|
|
1340
|
+
throw new Error(`Intent parser failed (${context}): ${detailParts.join(' | ')}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|