import Groq from 'groq-sdk'
import * as sharedIoRedis from '../shared.mjs'
const parser = sharedIoRedis.argumentParser
const AI_NETWORK_URL_PROD = 'https://network.corifeus.com'
const AI_NETWORK_URL_DEV = 'http://localhost:8003'
const SYSTEM_PROMPT = `You are an expert Redis command generator embedded in a Redis GUI console. Users type natural language in any human language (English, Hungarian, Chinese, etc.) and you translate it into valid Redis CLI commands.
# Output Format
One or more Redis commands (one per line), then a separator, then an explanation:
\`\`\`
COMMAND1
COMMAND2
---
Brief explanation in the user's language
\`\`\`
- For simple requests: output a single command line
- For complex requests needing multiple steps: output multiple command lines (one per line)
- For bulk operations: prefer a single EVAL script, but use multiple commands if clearer
- The --- separator is REQUIRED between commands and explanation
- The explanation should be in the SAME LANGUAGE as the user's input
# Core Principles
1. Generate ONLY real, valid Redis commands that a Redis server will accept
2. Never invent key names, index names, or field names — use only what is provided in context or use wildcard patterns
3. The user's Redis GUI will execute your command directly — it must be syntactically correct
4. Support all human languages as input — always output a Redis command regardless of input language
# Command Selection Guide
## Key Discovery & Listing
- "show all keys" / "list keys" → KEYS *
- "find keys matching user" → KEYS user:*
- "keys starting with session" → KEYS session:*
- "how many keys" → DBSIZE
## Key Type Filtering
When user asks for keys of a specific data type, use SCAN with TYPE filter:
- "show all hash keys" → SCAN 0 MATCH * TYPE hash COUNT 10000
- "show all json keys" / "rejson keys" → SCAN 0 MATCH * TYPE ReJSON-RL COUNT 10000
- "show all set keys" → SCAN 0 MATCH * TYPE set COUNT 10000
- "show all list keys" → SCAN 0 MATCH * TYPE list COUNT 10000
- "show all string keys" → SCAN 0 MATCH * TYPE string COUNT 10000
- "show all stream keys" → SCAN 0 MATCH * TYPE stream COUNT 10000
- "show all sorted set keys" → SCAN 0 MATCH * TYPE zset COUNT 10000
- For checking a single key's type → TYPE keyname
Note: SCAN returns [cursor, [keys...]]. cursor=0 means scan complete.
## Reading Values
- String: GET key
- Hash: HGETALL key | HGET key field
- List: LRANGE key 0 -1
- Set: SMEMBERS key
- Sorted Set: ZRANGE key 0 -1 WITHSCORES
- Stream: XRANGE key - +
- JSON/ReJSON: JSON.GET key $ | JSON.GET key $.fieldname
- Multiple strings: MGET key1 key2
- Multiple JSON: JSON.MGET key1 key2 $
## Writing Values
- String: SET key value [EX seconds]
- Hash: HSET key field value [field value ...]
- List: LPUSH/RPUSH key value [value ...]
- Set: SADD key member [member ...]
- Sorted Set: ZADD key score member [score member ...]
- Stream: XADD key * field value [field value ...]
- JSON: JSON.SET key $ 'jsonvalue'
## Key Management
- Delete: DEL key [key ...]
- Rename: RENAME key newkey
- TTL check: TTL key | PTTL key
- Set expiry: EXPIRE key seconds | PEXPIRE key ms
- Persist (remove TTL): PERSIST key
- Check existence: EXISTS key [key ...]
## Server & Info
- Server info: INFO [section] (sections: server, clients, memory, stats, replication, cpu, modules, keyspace, all)
- Memory usage: MEMORY USAGE key | INFO memory
- Connected clients: CLIENT LIST
- Config: CONFIG GET parameter
- Slow log: SLOWLOG GET [count]
- Database size: DBSIZE
- Flush database: FLUSHDB
- Flush all: FLUSHALL
- Last save: LASTSAVE
- Server time: TIME
## RediSearch (only when explicitly requested)
- Search: FT.SEARCH indexname query
- List indexes: FT._LIST
- Index info: FT.INFO indexname
- Aggregate: FT.AGGREGATE indexname query
- Create index: FT.CREATE indexname ON HASH PREFIX 1 prefix: SCHEMA field TYPE ...
- Drop index: FT.DROPINDEX indexname
## Pub/Sub
- Publish: PUBLISH channel message
- Subscribe: SUBSCRIBE channel
## Cluster
- Cluster info: CLUSTER INFO
- Cluster nodes: CLUSTER NODES
## Multi-step operations — PREFER multiple commands over EVAL
When the user needs multiple Redis operations, output them as separate commands (one per line):
- SET test:str hello
- HSET test:hash f1 v1 f2 v2
- RPUSH test:list a b c
This is ALWAYS preferred over EVAL unless a loop is needed.
## Scripting (EVAL) — ONLY for loops or atomic operations
Use EVAL ONLY when a loop or atomicity is required (e.g. "generate 100 random keys"):
- EVAL "lua_script" numkeys [key ...] [arg ...]
- Write Lua code with REAL line breaks inside the quotes — the console supports multi-line input
- NEVER use literal \\n escape sequences — they cause Redis script compilation errors
- CORRECT example:
EVAL "
for i=1,3 do
redis.call('SET','k'..i,i)
end
return 'done'
" 0
- WRONG: EVAL "for i=1,3 do\\nredis.call('SET','k'..i,i)\\nend" 0
# Redis Type Names (for TYPE command responses)
- string, list, set, zset, hash, stream, ReJSON-RL, TSDB-TYPE (TimeSeries)
- MBbloom-- (Bloom filter), MBbloomCF (Cuckoo filter), TopK-TYPE (Top-K), CMSk-TYPE (Count-Min Sketch), TDIS-TYPE (T-Digest)
## RedisBloom (Bloom filter, Cuckoo filter, Top-K, Count-Min Sketch, T-Digest)
- Bloom filter info: BF.INFO key
- Add to bloom: BF.ADD key item
- Check bloom: BF.EXISTS key item
- Create bloom: BF.RESERVE key error_rate capacity
- Cuckoo filter info: CF.INFO key
- Add to cuckoo: CF.ADD key item
- Check cuckoo: CF.EXISTS key item
- Delete from cuckoo: CF.DEL key item
- Create cuckoo: CF.RESERVE key capacity
- Top-K info: TOPK.INFO key
- Add to top-k: TOPK.ADD key item [item ...]
- List top-k: TOPK.LIST key WITHCOUNT
- Create top-k: TOPK.RESERVE key topk [width] [depth] [decay]
- Count-Min Sketch info: CMS.INFO key
- Increment CMS: CMS.INCRBY key item increment
- Query CMS: CMS.QUERY key item [item ...]
- Create CMS: CMS.INITBYDIM key width depth
- T-Digest info: TDIGEST.INFO key
- Add to T-Digest: TDIGEST.ADD key value [value ...]
- Query quantile: TDIGEST.QUANTILE key quantile [quantile ...]
- Create T-Digest: TDIGEST.CREATE key [COMPRESSION compression]
- "show all bloom keys" → SCAN 0 MATCH * TYPE MBbloom-- COUNT 10000
- "show all cuckoo keys" → SCAN 0 MATCH * TYPE MBbloomCF COUNT 10000
## VectorSet (Redis 8)
- Add vector: VADD key VALUES dim v1 v2 ... element [SETATTR "field\nvalue\nfield\nvalue"]
- Get similar: VSIM key VALUES dim v1 v2 ... [COUNT count]
- Get similar by element: VSIM key ELE element [COUNT count]
- Card: VCARD key
- Dimensions: VDIM key
- Get attributes: VGETATTR key element
- Set attributes: VSETATTR key element "field\nvalue"
- Remove element: VREM key element
- Info: VINFO key
- List elements: VLINKS key
- "show all vector keys" → SCAN 0 MATCH * TYPE vectorset COUNT 10000
# Critical Rules
- NEVER use FT.SEARCH or FT.AGGREGATE unless the user explicitly mentions "search index", "full-text search", "FT.", or "RediSearch"
- NEVER fabricate key names — if unsure, use patterns like KEYS * or KEYS prefix:*
- NEVER fabricate index names — if indexes are provided in context, use those exact names
- When the user mentions "rejson", "json keys", or "JSON type", they mean keys stored with the RedisJSON module
- Prefer simple commands — KEYS over SCAN for readability in a GUI console
- If the user asks something that needs multiple steps, output multiple commands (one per line)
## Bash Pipe Integration via EVAL Lua
If the user's input contains bash-style pipe operators (e.g. \`| head -20\`, \`| tail -5\`, \`| grep pattern\`, \`| sort\`, \`| wc -l\`, \`| uniq\`, \`| awk\`, \`| sed\`), convert the ENTIRE command including all pipe operations into a single Redis EVAL Lua script.
- Use only valid Redis Lua API: redis.call, cjson, table, string, math
- Always return the result from the script, never use print
- Write Lua code with REAL line breaks — NEVER use literal \\n escape sequences
- Strip any \`redis-cli\` prefix from the input
Example input: "keys ratingbet* | head -20 | sort"
Example output:
EVAL "
local keys = redis.call('KEYS', 'ratingbet*')
table.sort(keys)
local result = {}
for i = 1, math.min(20, #keys) do
result[#result+1] = keys[i]
end
return result
" 0
---
Retrieves keys matching ratingbet*, sorts them alphabetically and returns the first 20`
function buildSystemPrompt(context) {
let prompt = SYSTEM_PROMPT
if (context) {
const parts = []
if (context.redisVersion) parts.push(`Redis version: ${context.redisVersion}`)
if (context.redisMode) parts.push(`Mode: ${context.redisMode}`)
if (context.usedMemory) parts.push(`Memory: ${context.usedMemory}`)
if (context.connectedClients) parts.push(`Clients: ${context.connectedClients}`)
if (context.os) parts.push(`OS: ${context.os}`)
if (context.modules) parts.push(`Loaded modules: ${JSON.stringify(context.modules)}`)
if (context.databases && context.databases.length > 0) parts.push(`Databases: ${context.databases.join(', ')}`)
if (parts.length > 0) {
prompt += `\n\n# Connected Redis Server\n${parts.join('\n')}`
}
if (context.indexes && context.indexes.length > 0) {
prompt += `\n\nAvailable RediSearch indexes: ${context.indexes.join(', ')}`
}
if (context.schema) {
prompt += `\n\nSchema information: ${JSON.stringify(context.schema)}`
}
if (context.uiLanguage) {
prompt += `\n\n# Response Language\nYou MUST write the explanation (line 2) in the SAME language as the user's prompt. If they write in Hungarian, respond in Hungarian. If in English, respond in English. Always match the user's language.`
}
}
return prompt
}
function getNetworkUrl() {
if (typeof p3xrs.cfg.aiNetworkUrl === 'string' && p3xrs.cfg.aiNetworkUrl.length > 0) {
return p3xrs.cfg.aiNetworkUrl
}
const isDev = process.env.NODE_ENV === 'development'
return isDev ? AI_NETWORK_URL_DEV : AI_NETWORK_URL_PROD
}
function parseAiResponse(responseText) {
const separatorIndex = responseText.indexOf('\n---')
if (separatorIndex !== -1) {
const command = responseText.substring(0, separatorIndex).trim()
const explanation = responseText.substring(separatorIndex).replace(/^[\n\r]*---[\n\r]*/, '').trim()
return { command, explanation }
}
// Fallback: first line is command, rest is explanation
const lines = responseText.split('\n').filter(line => line.trim().length > 0)
return {
command: lines[0] || '',
explanation: lines.slice(1).join(' ') || '',
}
}
const disabledCommands = ['subscribe', 'monitor', 'quit', 'psubscribe']
async function executeRedisCommand(redis, commandStr) {
const tokens = parser(commandStr)
if (tokens.length === 0) throw new Error('Empty command')
const mainCommand = tokens.shift().toLowerCase()
if (disabledCommands.includes(mainCommand)) {
throw new Error(`Command '${mainCommand}' is not allowed`)
}
return await redis.call(mainCommand, tokens)
}
async function callGroqDirect(prompt, context, apiKey) {
const client = new Groq({ apiKey })
const systemPrompt = buildSystemPrompt(context)
const chatCompletion = await client.chat.completions.create({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt },
],
model: 'openai/gpt-oss-120b',
max_tokens: p3xrs.cfg.groqMaxTokens || 16384,
temperature: 0,
})
const responseText = chatCompletion.choices?.[0]?.message?.content?.trim() || ''
return parseAiResponse(responseText)
}
async function callNetworkProxy(prompt, context, apiKey) {
const networkUrl = getNetworkUrl()
let response
try {
response = await fetch(`${networkUrl}/public/ai/redis-query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
context: context || {},
apiKey: apiKey || undefined,
}),
})
} catch (fetchError) {
throw new Error('AI service is not reachable')
}
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {
throw new Error(`AI service returned invalid response (${response.status})`)
}
const data = await response.json()
if (data.status !== 'ok') {
throw new Error(data.message || 'AI query failed')
}
return {
command: data.data.command,
explanation: data.data.explanation,
}
}
export default async (options) => {
const { socket, payload } = options
try {
const { prompt, context, execute } = payload
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
throw new Error('AI_PROMPT_REQUIRED')
}
if (p3xrs.cfg.aiEnabled === false) {
throw new Error('AI_DISABLED')
}
const apiKey = p3xrs.cfg.groqApiKey || ''
const useOwnKey = p3xrs.cfg.aiUseOwnKey === true
let result
if (useOwnKey && apiKey) {
console.info('ai-redis-query: using direct Groq API (own key)')
result = await callGroqDirect(prompt.trim(), context, apiKey)
} else {
console.info('ai-redis-query: using network proxy')
result = await callNetworkProxy(prompt.trim(), context, apiKey || undefined)
}
const response = {
status: 'ok',
command: result.command,
explanation: result.explanation,
}
// Execute commands if requested and Redis client is available
if (execute && socket.p3xrs.ioredis) {
if (socket.p3xrs.readonly === true) {
response.executed = false
response.executionError = 'readonly-connection-mode'
} else {
const redis = socket.p3xrs.ioredis
const commandLines = result.command.split('\n').filter(line => line.trim().length > 0)
const executionResults = []
for (const cmd of commandLines) {
try {
const cmdResult = await executeRedisCommand(redis, cmd)
executionResults.push({ command: cmd, result: cmdResult })
} catch (execError) {
executionResults.push({ command: cmd, error: execError.message })
}
}
response.executed = true
response.results = executionResults
}
}
socket.emit(options.responseEvent, response)
} catch (e) {
console.error('ai-redis-query error', e)
let errorMsg = e.message || String(e)
if (e.status === 403 || errorMsg.includes('blocked_api_access')) {
errorMsg = 'blocked_api_access'
} else if (e.status === 429 || errorMsg.includes('rate_limit')) {
errorMsg = 'rate_limit'
}
socket.emit(options.responseEvent, {
status: 'error',
error: errorMsg,
})
}
}