use std::sync::Arc;
use algocline_core::{BudgetHandle, ProgressHandle, QueryId};
use mlua::prelude::*;
use crate::card::FileCardStore;
use crate::card_context::{self, CardContextSpec};
use crate::llm_bridge::{LlmRequest, QueryRequest};
pub(super) fn register_llm(
lua: &Lua,
alc_table: &LuaTable,
llm_tx: tokio::sync::mpsc::Sender<LlmRequest>,
budget: BudgetHandle,
card_store: Arc<FileCardStore>,
) -> LuaResult<()> {
let llm =
lua.create_async_function(move |_lua, (prompt, opts): (String, Option<LuaTable>)| {
let tx = llm_tx.clone();
let bh = budget.clone();
let cs = card_store.clone();
async move {
bh.check().map_err(LuaError::external)?;
let system = opts.as_ref().and_then(|o| o.get::<String>("system").ok());
let max_tokens = opts
.as_ref()
.and_then(|o| o.get::<u32>("max_tokens").ok())
.unwrap_or(1024);
let grounded = opts
.as_ref()
.and_then(|o| o.get::<bool>("grounded").ok())
.unwrap_or(false);
let underspecified = opts
.as_ref()
.and_then(|o| o.get::<bool>("underspecified").ok())
.unwrap_or(false);
let cache_breakpoint = opts
.as_ref()
.and_then(|o| o.get::<String>("cache_breakpoint").ok());
let role = opts.as_ref().and_then(|o| o.get::<String>("role").ok());
#[cfg(feature = "nn")]
if role.as_deref() == Some("nn") {
let model_name = opts
.as_ref()
.and_then(|o| o.get::<String>("model").ok())
.ok_or_else(|| {
LuaError::external("alc.llm role=\"nn\": missing model = <name>")
})?;
let registry = _lua
.app_data_ref::<algocline_nn::NnModelRegistry>()
.ok_or_else(|| {
LuaError::external(
"alc.llm role=\"nn\": alc.nn registry missing — has alc.nn been \
initialised on this VM?",
)
})?;
let forward = registry.get(&model_name).ok_or_else(|| {
LuaError::external(format!(
"alc.llm role=\"nn\": no model registered as {model_name:?}"
))
})?;
drop(registry);
return forward.call::<String>(prompt);
}
let card_context_spec = opts.as_ref().and_then(|o| {
let raw = o.get::<LuaValue>("card_context").ok()?;
match raw {
LuaValue::String(s) => {
Some(CardContextSpec::CardId(s.to_str().ok()?.to_string()))
}
LuaValue::Table(t) => {
let pkg = t.get::<String>("pkg").ok()?;
let limit = t.get::<usize>("limit").unwrap_or(5).min(100);
Some(CardContextSpec::Query { pkg, limit })
}
_ => None,
}
});
let effective_system = if let Some(spec) = card_context_spec {
match card_context::resolve(cs.as_ref(), spec) {
Ok(cards) if !cards.is_empty() => {
let prefix = card_context::format_past_cards(&cards);
Some(match system {
Some(existing) => format!("{prefix}\n\n{existing}"),
None => prefix,
})
}
Ok(_) => system,
Err(e) => {
tracing::warn!(
target: "algocline::card_context",
error = %e,
"resolve failed, skipping card_context inject"
);
system
}
}
} else {
system
};
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
tx.send(LlmRequest {
queries: vec![QueryRequest {
id: QueryId::single(),
prompt,
system: effective_system,
max_tokens,
grounded,
underspecified,
cache_breakpoint,
role,
resp_tx,
}],
})
.await
.map_err(|e| LuaError::external(format!("LLM bridge send failed: {e}")))?;
resp_rx
.await
.map_err(|e| LuaError::external(format!("LLM bridge recv failed: {e}")))?
.map_err(LuaError::external)
}
})?;
alc_table.set("llm", llm)?;
Ok(())
}
pub(super) fn register_llm_batch(
lua: &Lua,
alc_table: &LuaTable,
llm_tx: tokio::sync::mpsc::Sender<LlmRequest>,
budget: BudgetHandle,
) -> LuaResult<()> {
let llm_batch = lua.create_async_function(move |_, items: LuaTable| {
let tx = llm_tx.clone();
let bh = budget.clone();
async move {
bh.check().map_err(LuaError::external)?;
let len = items.len()? as usize;
if len == 0 {
return Err(LuaError::external("alc.llm_batch: empty items array"));
}
let mut query_requests = Vec::with_capacity(len);
let mut resp_rxs = Vec::with_capacity(len);
for i in 1..=len {
let item: LuaTable = items.get(i)?;
let prompt: String = item.get("prompt")?;
let system: Option<String> = item.get::<LuaValue>("system").ok().and_then(|v| {
if let LuaValue::String(s) = v {
Some(s.to_str().ok()?.to_string())
} else {
None
}
});
let max_tokens: u32 = item.get::<u32>("max_tokens").unwrap_or(1024);
let grounded: bool = item.get::<bool>("grounded").unwrap_or(false);
let underspecified: bool = item.get::<bool>("underspecified").unwrap_or(false);
let cache_breakpoint: Option<String> =
item.get::<LuaValue>("cache_breakpoint").ok().and_then(|v| {
if let LuaValue::String(s) = v {
Some(s.to_str().ok()?.to_string())
} else {
None
}
});
let role: Option<String> = item.get::<LuaValue>("role").ok().and_then(|v| {
if let LuaValue::String(s) = v {
Some(s.to_str().ok()?.to_string())
} else {
None
}
});
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
resp_rxs.push(resp_rx);
query_requests.push(QueryRequest {
id: QueryId::batch(i - 1), prompt,
system,
max_tokens,
grounded,
underspecified,
cache_breakpoint,
role,
resp_tx,
});
}
tx.send(LlmRequest {
queries: query_requests,
})
.await
.map_err(|e| LuaError::external(format!("LLM bridge send failed: {e}")))?;
let mut responses = Vec::with_capacity(len);
for (i, rx) in resp_rxs.into_iter().enumerate() {
let resp = rx
.await
.map_err(|e| {
LuaError::external(format!("LLM bridge recv failed for q-{i}: {e}"))
})?
.map_err(LuaError::external)?;
responses.push(resp);
}
Ok(responses)
}
})?;
alc_table.set("llm_batch", llm_batch)?;
Ok(())
}
pub(super) fn register_budget_remaining(
lua: &Lua,
alc_table: &LuaTable,
budget: BudgetHandle,
) -> LuaResult<()> {
let budget_fn = lua.create_function(move |lua, ()| {
let remaining = budget.remaining();
lua.to_value(&remaining)
})?;
alc_table.set("budget_remaining", budget_fn)?;
Ok(())
}
pub(super) fn register_progress(
lua: &Lua,
alc_table: &LuaTable,
progress: ProgressHandle,
) -> LuaResult<()> {
let progress_fn =
lua.create_function(move |_, (step, total, msg): (u64, u64, Option<String>)| {
progress.set(step, total, msg);
Ok(())
})?;
alc_table.set("progress", progress_fn)?;
Ok(())
}