Add native LLM calling functions (llm_call, llm_parallel, llm_configure) to El VM
This commit is contained in:
@@ -5996,6 +5996,110 @@ fn dispatch_builtin(
|
||||
stack.push(Value::Nil); BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// ── LLM native functions ──────────────────────────────────────────────
|
||||
|
||||
// llm_call(model, prompt) -> String
|
||||
"llm_call" => {
|
||||
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) {
|
||||
Value::Str(s) => s,
|
||||
_ => "claude".to_string(),
|
||||
};
|
||||
let result = call_llm_blocking(&model, None, &prompt);
|
||||
stack.push(Value::Str(result));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// llm_call_system(model, system, prompt) -> String
|
||||
"llm_call_system" => {
|
||||
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let system = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) {
|
||||
Value::Str(s) => s,
|
||||
_ => "claude".to_string(),
|
||||
};
|
||||
let result = call_llm_blocking(&model, Some(&system), &prompt);
|
||||
stack.push(Value::Str(result));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// llm_stream(model, prompt) -> String (blocking for now; streaming is future work)
|
||||
"llm_stream" => {
|
||||
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let model = match stack.pop().unwrap_or(Value::Str("claude".to_string())) {
|
||||
Value::Str(s) => s,
|
||||
_ => "claude".to_string(),
|
||||
};
|
||||
let result = call_llm_blocking(&model, None, &prompt);
|
||||
stack.push(Value::Str(result));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// llm_parallel(models: List, prompt: String) -> Map
|
||||
"llm_parallel" => {
|
||||
let prompt = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let models: Vec<String> = match stack.pop().unwrap_or(Value::List(vec![])) {
|
||||
Value::List(l) => l.into_iter().filter_map(|v| {
|
||||
if let Value::Str(s) = v { Some(s) } else { None }
|
||||
}).collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let mut pairs: Vec<(String, Value)> = Vec::new();
|
||||
for model in models {
|
||||
let response = call_llm_blocking(&model, None, &prompt);
|
||||
pairs.push((model, Value::Str(response)));
|
||||
}
|
||||
stack.push(Value::Map(pairs));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// llm_models() -> List
|
||||
"llm_models" => {
|
||||
let mut model_names: Vec<String> = LLM_CONFIG.with(|c| {
|
||||
c.borrow().keys().cloned().collect()
|
||||
});
|
||||
model_names.sort();
|
||||
let list = model_names.into_iter().map(Value::Str).collect();
|
||||
stack.push(Value::List(list));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// llm_configure(name, endpoint, api_key) -> Bool
|
||||
"llm_configure" => {
|
||||
let api_key = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let endpoint = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
let name = match stack.pop().unwrap_or(Value::Str(String::new())) {
|
||||
Value::Str(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
LLM_CONFIG.with(|c| {
|
||||
c.borrow_mut().insert(name, (endpoint, api_key));
|
||||
});
|
||||
stack.push(Value::Bool(true));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
_ => BuiltinResult::NotBuiltin,
|
||||
}
|
||||
}
|
||||
@@ -6132,6 +6236,167 @@ thread_local! {
|
||||
static OBSERVER_ID_COUNTER: std::cell::Cell<i64> = std::cell::Cell::new(1);
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// LLM model name → (endpoint_url, api_key)
|
||||
static LLM_CONFIG: std::cell::RefCell<std::collections::HashMap<String, (String, String)>> =
|
||||
std::cell::RefCell::new({
|
||||
let mut m = std::collections::HashMap::new();
|
||||
// Claude via Anthropic API
|
||||
m.insert("claude".to_string(), (
|
||||
"https://api.anthropic.com/v1/messages".to_string(),
|
||||
std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
|
||||
));
|
||||
m.insert("claude-3-5-sonnet".to_string(), (
|
||||
"https://api.anthropic.com/v1/messages".to_string(),
|
||||
std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
|
||||
));
|
||||
m.insert("claude-opus".to_string(), (
|
||||
"https://api.anthropic.com/v1/messages".to_string(),
|
||||
std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
|
||||
));
|
||||
// OpenAI
|
||||
m.insert("gpt4o".to_string(), (
|
||||
"https://api.openai.com/v1/chat/completions".to_string(),
|
||||
std::env::var("OPENAI_API_KEY").unwrap_or_default(),
|
||||
));
|
||||
m.insert("gpt4".to_string(), (
|
||||
"https://api.openai.com/v1/chat/completions".to_string(),
|
||||
std::env::var("OPENAI_API_KEY").unwrap_or_default(),
|
||||
));
|
||||
// Local Ollama
|
||||
m.insert("ollama".to_string(), (
|
||||
"http://localhost:11434/api/chat".to_string(),
|
||||
String::new(),
|
||||
));
|
||||
// Neuron (the platform itself)
|
||||
m.insert("neuron".to_string(), (
|
||||
std::env::var("NEURON_ENDPOINT")
|
||||
.unwrap_or_else(|_| "https://api.neurontechnologies.ai/v1/chat/completions".to_string()),
|
||||
std::env::var("NEURON_API_KEY").unwrap_or_default(),
|
||||
));
|
||||
m
|
||||
});
|
||||
}
|
||||
|
||||
// ── LLM helper functions ──────────────────────────────────────────────────────
|
||||
|
||||
/// Map a short model name to the actual Anthropic model id string.
|
||||
fn get_anthropic_model(name: &str) -> &'static str {
|
||||
match name {
|
||||
"claude-opus" | "claude-3-opus" => "claude-opus-4-5",
|
||||
"claude-haiku" => "claude-haiku-4-5",
|
||||
_ => "claude-sonnet-4-5",
|
||||
}
|
||||
}
|
||||
|
||||
/// Call an LLM synchronously. Returns the response text (or an error string).
|
||||
/// Supports Anthropic, Ollama, and OpenAI-compatible endpoints based on URL.
|
||||
fn call_llm_blocking(model: &str, system: Option<&str>, prompt: &str) -> String {
|
||||
let (endpoint, api_key) = LLM_CONFIG.with(|c| {
|
||||
c.borrow()
|
||||
.get(model)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ("http://localhost:11434/api/chat".to_string(), String::new()))
|
||||
});
|
||||
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => return format!("llm_call error: {}", e),
|
||||
};
|
||||
|
||||
if endpoint.contains("anthropic.com") {
|
||||
// Anthropic Messages API
|
||||
let body = if let Some(sys) = system {
|
||||
serde_json::json!({
|
||||
"model": get_anthropic_model(model),
|
||||
"max_tokens": 4096,
|
||||
"system": sys,
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"model": get_anthropic_model(model),
|
||||
"max_tokens": 4096,
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
})
|
||||
};
|
||||
match client
|
||||
.post(&endpoint)
|
||||
.header("x-api-key", &api_key)
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("content-type", "application/json")
|
||||
.json(&body)
|
||||
.send()
|
||||
{
|
||||
Ok(r) => match r.json::<serde_json::Value>() {
|
||||
Ok(v) => v["content"][0]["text"].as_str().unwrap_or("").to_string(),
|
||||
Err(e) => format!("llm_call error: {}", e),
|
||||
},
|
||||
Err(e) => format!("llm_call error: {}", e),
|
||||
}
|
||||
} else if endpoint.contains("ollama") || endpoint.contains("11434") {
|
||||
// Ollama chat API
|
||||
let model_name = model.trim_start_matches("ollama:").to_string();
|
||||
let messages_arr = if let Some(sys) = system {
|
||||
serde_json::json!([
|
||||
{"role": "system", "content": sys},
|
||||
{"role": "user", "content": prompt}
|
||||
])
|
||||
} else {
|
||||
serde_json::json!([{"role": "user", "content": prompt}])
|
||||
};
|
||||
let body = serde_json::json!({
|
||||
"model": if model_name == "ollama" { "llama3.2".to_string() } else { model_name },
|
||||
"messages": messages_arr,
|
||||
"stream": false
|
||||
});
|
||||
match client.post(&endpoint).json(&body).send() {
|
||||
Ok(r) => match r.json::<serde_json::Value>() {
|
||||
Ok(v) => v["message"]["content"].as_str().unwrap_or("").to_string(),
|
||||
Err(e) => format!("llm_call error: {}", e),
|
||||
},
|
||||
Err(e) => format!("llm_call error: {}", e),
|
||||
}
|
||||
} else {
|
||||
// OpenAI-compatible format (OpenAI, Neuron, and most others)
|
||||
let messages_arr = if let Some(sys) = system {
|
||||
serde_json::json!([
|
||||
{"role": "system", "content": sys},
|
||||
{"role": "user", "content": prompt}
|
||||
])
|
||||
} else {
|
||||
serde_json::json!([{"role": "user", "content": prompt}])
|
||||
};
|
||||
let model_id = model
|
||||
.replace("gpt4o", "gpt-4o")
|
||||
.replace("gpt4", "gpt-4-turbo");
|
||||
let body = serde_json::json!({
|
||||
"model": model_id,
|
||||
"messages": messages_arr,
|
||||
"max_tokens": 4096
|
||||
});
|
||||
match client
|
||||
.post(&endpoint)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("content-type", "application/json")
|
||||
.json(&body)
|
||||
.send()
|
||||
{
|
||||
Ok(r) => match r.json::<serde_json::Value>() {
|
||||
Ok(v) => v["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
Err(e) => format!("llm_call error: {}", e),
|
||||
},
|
||||
Err(e) => format!("llm_call error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two runtime values for ordering (used by Lt/Gt/LtEq/GtEq).
|
||||
fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Ordering {
|
||||
use el_compiler::Value;
|
||||
@@ -6192,6 +6457,8 @@ fn is_builtin(name: &str) -> bool {
|
||||
| "time_add" | "time_diff" | "time_start_of" | "time_tz_offset" | "time_to_tz"
|
||||
// Observer
|
||||
| "observe" | "unobserve"
|
||||
// LLM native functions
|
||||
| "llm_call" | "llm_call_system" | "llm_stream" | "llm_parallel" | "llm_models" | "llm_configure"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user