-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprompt.service.js
More file actions
329 lines (281 loc) · 10.4 KB
/
prompt.service.js
File metadata and controls
329 lines (281 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/**
* Prompt service for centralized prompt management
*/
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
import { aiConfig } from '../../configs/ai.config.js'
import { log } from '../../functions/logger.js'
import { CacheManager } from '../../utils/cacheManager.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Cache for prompt templates
const promptCache = new CacheManager({
maxSize: 100,
ttl: 86400000 // 24 hours
})
// Define supported prompt file extensions
const PROMPT_EXTENSIONS = ['.echo', '.txt', '.md']
/**
* Try to fetch a file via HTTP(S) or file URL, fallback to fs if not available.
* @param {string} filePath - The path or URL to the file
* @returns {Promise<string|null>} The file contents or null if not found
*/
async function fetchPromptFile(filePath) {
try {
// Try fetch if filePath is a URL
if (filePath.startsWith('http://') || filePath.startsWith('https://') || filePath.startsWith('file://')) {
const res = await fetch(filePath)
if (res.ok) {
return await res.text()
}
}
} catch (err) {
// Ignore and fallback
}
try {
// Fallback to fs for local files
return await fs.readFile(filePath, 'utf8')
} catch (err) {
log(`Prompt file not found: ${filePath}`, 'warn')
return null
}
}
class PromptService {
constructor() {
this.prompts = new Map()
this.initialized = false
this.basePath = process.env.PROMPT_PATH || path.join(__dirname, '../prompts')
}
/**
* Initialize the prompt service and load all prompts
* @param {Object} options - Initialization options
* @param {boolean} [options.force=false] - Force reload all prompts
* @returns {Promise<void>}
*/
async initialize(options = {}) {
if (this.initialized && !options.force) {
return
}
try {
// Get all prompt files from the directory
const files = await fs.readdir(this.basePath)
// Load each prompt file
for (const file of files) {
if (PROMPT_EXTENSIONS.some(ext => file.endsWith(ext))) {
const promptName = path.basename(file, path.extname(file))
await this.loadPrompt(promptName)
}
}
this.initialized = true
log(`Prompt service initialized with ${this.prompts.size} prompts`, 'info')
} catch (error) {
log(`Error initializing prompt service: ${error.message}`, 'error')
throw error
}
}
/**
* Load a specific prompt by name
* @param {string} promptName - The name of the prompt to load
* @returns {Promise<string|null>} The prompt template or null if not found
*/
async loadPrompt(promptName) {
for (const extension of PROMPT_EXTENSIONS) {
const filePath = path.join(this.basePath, `${promptName}${extension}`)
const template = await fetchPromptFile(filePath)
if (template) {
this.prompts.set(promptName, template)
return template
}
}
log(`Prompt ${promptName} not found with any supported extension`, 'warn')
return null
}
/**
* Get a prompt template by name
* @param {string} promptName - The name of the prompt
* @returns {Promise<string|null>} The prompt template or null if not found
*/
async getPrompt(promptName) {
// Check if already loaded
if (this.prompts.has(promptName)) {
return this.prompts.get(promptName)
}
// Try to load it
return await this.loadPrompt(promptName)
}
/**
* Get an agent-specific prompt
* @param {string} agentName - The agent class name
* @param {Object} context - The context data
* @returns {Promise<string|null>} The agent prompt or null if not found
*/
async getAgentPrompt(agentName, context) {
// Convert agent class name to prompt name (e.g., ConversationAgent -> conversation)
const promptName = agentName.replace('Agent', '').toLowerCase()
// Try to get the agent-specific prompt
let prompt = await this.getPrompt(promptName)
// Fall back to default if not found
if (!prompt) {
prompt = await this.getPrompt('default')
}
// Process the prompt with the context
if (prompt) {
return this.processTemplate(prompt, context)
}
return null
}
/**
* Get a prompt based on context
* @param {Object} context - The context data
* @returns {Promise<string>} The processed prompt
*/
async getPromptForContext(context) {
// Only allow new prompt names
const allowedPrompts = ['core', 'conversation', 'technical', 'synthesis']
const promptName = context.messageType
if (!allowedPrompts.includes(promptName)) {
throw new Error('Invalid or deprecated prompt requested: ' + promptName)
}
// Generate a cache key based on relevant context data
const cacheKey = this._generatePromptCacheKey(context)
// Check cache first
const cachedPrompt = promptCache.get(cacheKey)
if (cachedPrompt) {
return cachedPrompt
}
// Get the prompt template
let template = await this.getPrompt(promptName)
// Fall back to default if not found
if (!template) {
template = await this.getPrompt('default')
// If still not found, use hardcoded default
if (!template) {
template = aiConfig.systemPrompt
}
}
// Process the template with context variables
const processedPrompt = this.processTemplate(template, context)
// Cache the processed prompt
promptCache.set(cacheKey, processedPrompt)
return processedPrompt
}
/**
* Process a template string with variables
* @param {string} template - Template string with {{variable}} placeholders
* @param {Object} context - Context object with variables
* @returns {string} Processed template
*/
processTemplate(template, context = {}) {
if (!template) {
return ''
}
let processed = template
// Simple variable replacement
processed = processed.replace(/{{(\w+)}}/g, (match, variable) => {
return context[variable] !== undefined ? context[variable] : match
})
// Handle conditional blocks - basic implementation
// {{#if variable}}content{{/if}}
processed = processed.replace(/{{#if (\w+)}}([\s\S]*?){{\/if}}/g, (match, variable, content) => {
return context[variable] ? content : ''
})
// Handle conditional blocks with else - basic implementation
// {{#if variable}}content{{else}}alternative{{/if}}
processed = processed.replace(
/{{#if (\w+)}}([\s\S]*?){{else}}([\s\S]*?){{\/if}}/g,
(match, variable, content, alternative) => {
return context[variable] ? content : alternative
}
)
// Handle each loops - basic implementation
// {{#each variable}}content with {{name}} etc{{/each}}
processed = processed.replace(/{{#each (\w+)}}([\s\S]*?){{\/each}}/g, (match, variable, template) => {
if (!context[variable] || !Array.isArray(context[variable])) {
return ''
}
return context[variable]
.map(item => {
let itemContent = template
// Replace item properties
Object.keys(item).forEach(key => {
itemContent = itemContent.replace(
new RegExp(`{{${key}}}`, 'g'),
item[key] !== undefined ? item[key] : ''
)
})
return itemContent
})
.join('\n')
})
return processed
}
/**
* Create a context object for prompt processing
* @param {string} message - User message
* @param {Object} additionalContext - Additional context data
* @returns {Promise<Object>} Context object
*/
async createContext(message, additionalContext = {}) {
// Base context with message
const context = {
message,
timestamp: new Date().toISOString(),
...additionalContext
}
// Add default values for commonly used context variables
if (!context.guildName && additionalContext.guild) {
context.guildName = additionalContext.guild.name
}
if (!context.channelName && additionalContext.channel) {
context.channelName = additionalContext.channel.name
}
if (!context.userName && additionalContext.user) {
context.userName = additionalContext.user.username || 'User'
}
return context
}
/**
* Generate a cache key for prompt context
* @param {Object} context - Context data
* @returns {string} Cache key
* @private
*/
_generatePromptCacheKey(context) {
// Only include relevant fields for cache key
const keyParts = [
context.messageType || 'default',
context.agentType || '',
context.isDM ? 'dm' : '',
context.detectedEntities?.length ? 'entities' : ''
].filter(Boolean)
return `prompt:${keyParts.join(':')}`
}
/**
* Determine which prompt to use based on context
* @param {Object} context - Context data
* @returns {string} Prompt name
* @private
*/
_determinePromptName(context) {
// First check if a specific message type is requested
if (context.messageType) {
return context.messageType
}
// Check for specialized contexts
if (context.isDM) {
return 'dm'
}
if (context.isPersonaQuery) {
return 'persona'
}
if (context.detectedEntities?.length > 0) {
return 'entity_mentions'
}
// Fall back to default
return 'default'
}
}
// Create singleton instance
const promptService = new PromptService()
export { promptService }