do
local _cache = {} local _order = {} local _hits = 0
local _misses = 0
local _max_entries = 256
function alc.cache(prompt, opts)
opts = opts or {}
if opts.cache_skip then
return alc.llm(prompt, opts)
end
local key = opts.cache_key
if not key then
local sig = prompt
if opts.system then sig = sig .. "\0" .. opts.system end
if opts.max_tokens then sig = sig .. "\0" .. tostring(opts.max_tokens) end
key = alc.fingerprint(sig)
end
if _cache[key] ~= nil then
_hits = _hits + 1
alc.log("debug", "alc.cache: hit " .. key)
return _cache[key]
end
local resp = alc.llm(prompt, opts)
_cache[key] = resp
_order[#_order + 1] = key
_misses = _misses + 1
alc.log("debug", "alc.cache: miss " .. key)
while #_order > _max_entries do
local evict_key = table.remove(_order, 1)
_cache[evict_key] = nil
end
return resp
end
function alc.cache_info()
return { entries = #_order, hits = _hits, misses = _misses, max_entries = _max_entries }
end
function alc.cache_clear()
_cache = {}
_hits = 0
_misses = 0
end
end
function alc.map(items, fn)
local results = {}
for i, item in ipairs(items) do
results[i] = fn(item, i)
end
return results
end
function alc.reduce(items, fn, init)
local acc = init
local start = 1
if acc == nil then
acc = items[1]
start = 2
end
for i = start, #items do
acc = fn(acc, items[i], i)
end
return acc
end
function alc.vote(answers)
local counts = {}
local order = {}
for _, a in ipairs(answers) do
local key = tostring(a):gsub("^%s+", ""):gsub("%s+$", "")
if counts[key] == nil then
counts[key] = 0
order[#order + 1] = key
end
counts[key] = counts[key] + 1
end
local winner, max_count = nil, 0
for _, key in ipairs(order) do
if counts[key] > max_count then
winner = key
max_count = counts[key]
end
end
return { winner = winner, count = max_count, total = #answers }
end
function alc.filter(items, fn)
local result = {}
for i, item in ipairs(items) do
if fn(item, i) then
result[#result + 1] = item
end
end
return result
end
function alc.ground(claim, opts)
local merged = {}
for k, v in pairs(opts or {}) do merged[k] = v end
merged.grounded = true
return alc.llm(claim, merged)
end
function alc.specify(prompt, opts)
local merged = {}
for k, v in pairs(opts or {}) do merged[k] = v end
merged.underspecified = true
return alc.llm(prompt, merged)
end
function alc.parse_score(str, default)
default = default or 5
local n = tonumber(tostring(str):match("%d+"))
if n == nil then return default end
if n < 1 then return 1 end
if n > 10 then return 10 end
return n
end
function alc.parse_number(text, pattern)
if type(text) ~= "string" then return nil end
if pattern then
local m = text:match(pattern)
return tonumber(m)
end
return tonumber(text:match("%-?%d+%.?%d*"))
end
function alc.json_extract(raw)
if type(raw) ~= "string" then return nil end
local ok, result = pcall(alc.json_decode, raw)
if ok and type(result) == "table" then return result end
local stripped = raw:match("```json%s*(.-)%s*```")
or raw:match("```%s*(.-)%s*```")
if stripped then
ok, result = pcall(alc.json_decode, stripped)
if ok and type(result) == "table" then return result end
end
for json_str in raw:gmatch("%b{}") do
ok, result = pcall(alc.json_decode, json_str)
if ok and type(result) == "table" then return result end
end
for json_str in raw:gmatch("%b[]") do
ok, result = pcall(alc.json_decode, json_str)
if ok and type(result) == "table" then return result end
end
return nil
end
function alc.state.update(key, fn, default)
local current = alc.state.get(key, default)
local updated = fn(current)
alc.state.set(key, updated)
return updated
end
function alc.llm_safe(prompt, opts, default)
local ok, result = pcall(alc.llm, prompt, opts)
if ok then return result end
alc.log("warn", "alc.llm_safe: " .. tostring(result))
return default
end
function alc.llm_json(prompt, opts)
opts = opts or {}
local raw = alc.llm(prompt, opts)
local parsed = alc.json_extract(raw)
if parsed then return parsed, raw end
alc.log("warn", "alc.llm_json: JSON parse failed, retrying")
local retry_opts = {}
for k, v in pairs(opts) do retry_opts[k] = v end
retry_opts.system = "Output ONLY valid JSON. No markdown fences, no explanation."
raw = alc.llm(
"The previous response was not valid JSON.\n\n"
.. "Previous output:\n" .. raw .. "\n\n"
.. "Fix the JSON and return ONLY valid JSON.\n\n"
.. "Original request:\n" .. prompt,
retry_opts
)
parsed = alc.json_extract(raw)
if not parsed then
alc.log("warn", "alc.llm_json: JSON parse failed after retry")
end
return parsed, raw
end
function alc.fingerprint(str)
local s = tostring(str):lower():gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
local hash = 5381
for i = 1, #s do
hash = ((hash * 33) + s:byte(i)) % 0x100000000
end
return string.format("%08x", hash)
end
function alc.budget_check()
local r = alc.budget_remaining()
if r == nil then return true end
if type(r.llm_calls) == "number" and r.llm_calls <= 0 then return false end
if type(r.elapsed_ms) == "number" and r.elapsed_ms <= 0 then return false end
return true
end
function alc.tuning(defaults, ctx, opts)
if type(defaults) ~= "table" then return defaults end
opts = opts or {}
local source = ctx or {}
if opts.prefix then
local ns = source[opts.prefix]
if type(ns) == "table" then
source = ns
elseif ns ~= nil then
alc.log("warn", "alc.tuning: prefix '" .. opts.prefix
.. "' exists but is not a table, ignoring")
source = {}
end
end
local result = {}
for k, v in pairs(defaults) do
if k == "_schema" then
elseif source[k] ~= nil then
if type(v) == "table" and type(source[k]) == "table" and v[1] == nil then
result[k] = alc.tuning(v, source[k])
else
result[k] = source[k]
end
else
result[k] = v
end
end
return result
end
function alc.parallel(items, prompt_fn, opts)
if type(items) ~= "table" or #items == 0 then
error("alc.parallel: items must be a non-empty array", 2)
end
if type(prompt_fn) ~= "function" then
error("alc.parallel: prompt_fn must be a function", 2)
end
opts = opts or {}
local batch = {}
for i, item in ipairs(items) do
local p = prompt_fn(item, i)
if type(p) == "string" then
local entry = { prompt = p }
if opts.system then entry.system = opts.system end
if opts.max_tokens then entry.max_tokens = opts.max_tokens end
batch[i] = entry
elseif type(p) == "table" then
if type(p.prompt) ~= "string" then
error("alc.parallel: prompt_fn returned table without .prompt at index " .. i, 2)
end
batch[i] = p
else
error("alc.parallel: prompt_fn must return string or table, got "
.. type(p) .. " at index " .. i, 2)
end
end
local responses = alc.llm_batch(batch)
if opts.post_fn then
local results = {}
for i, resp in ipairs(responses) do
results[i] = opts.post_fn(resp, items[i], i)
end
return results
end
return responses
end
function alc.pipe(strategies, ctx, opts)
if type(strategies) ~= "table" or #strategies == 0 then
error("alc.pipe: strategies must be a non-empty array", 2)
end
if type(ctx) ~= "table" then
error("alc.pipe: ctx must be a table", 2)
end
opts = opts or {}
local on_error = opts.on_error or "abort"
local pipe_ctx = {}
for k, v in pairs(ctx) do pipe_ctx[k] = v end
pipe_ctx.pipe_history = {}
for i, entry in ipairs(strategies) do
local name, run_fn
if type(entry) == "string" then
name = entry
local ok, pkg = pcall(require, entry)
if not ok then
error("alc.pipe: failed to load strategy '" .. entry .. "': " .. tostring(pkg), 2)
end
if type(pkg) ~= "table" or type(pkg.run) ~= "function" then
error("alc.pipe: strategy '" .. entry .. "' must export run(ctx)", 2)
end
run_fn = pkg.run
elseif type(entry) == "function" then
name = "(inline-" .. i .. ")"
run_fn = entry
else
error("alc.pipe: strategy[" .. i .. "] must be a string or function", 2)
end
if on_error == "abort" then
pipe_ctx = run_fn(pipe_ctx)
if type(pipe_ctx) ~= "table" then
error("alc.pipe: strategy '" .. name .. "' must return a table (ctx)", 2)
end
else
local ok, result = pcall(run_fn, pipe_ctx)
if not ok then
alc.log("warn", "alc.pipe: stage '" .. name .. "' failed: " .. tostring(result))
pipe_ctx.pipe_history[#pipe_ctx.pipe_history + 1] = {
strategy = name,
error = tostring(result),
}
goto next_stage
end
pipe_ctx = result
if type(pipe_ctx) ~= "table" then
error("alc.pipe: strategy '" .. name .. "' must return a table (ctx)", 2)
end
end
local result_snapshot = pipe_ctx.result
pipe_ctx.pipe_history[#pipe_ctx.pipe_history + 1] = {
strategy = name,
result = result_snapshot,
}
if pipe_ctx.result ~= nil and i < #strategies then
if type(pipe_ctx.result) == "table" then
pipe_ctx.task = alc.json_encode(pipe_ctx.result)
else
pipe_ctx.task = tostring(pipe_ctx.result)
end
end
if opts.on_stage then
opts.on_stage(i, name, pipe_ctx)
end
::next_stage::
end
return pipe_ctx
end