Skip to content

Commit 92ab5a0

Browse files
fix llm dispatch + add llm_system builtin + multi_agent_debate demo
- Fix: legacy llm_call/llm_chat/llm_embed match arms now cfg-gated with #[cfg(feature = "llm-builtins")] so the modern native-llm arms at the bottom of the match actually fire in default builds (they were blocked) - Fix: deduplicate is_known_builtin() LLM pattern list — duplicate arms caused "unreachable pattern" warnings in 14 spots; now only listed once - New: llm_call(prompt, model?, system?) supports optional 3rd system-prompt argument (matches usage in examples/lib/llm.omc) - New: llm_system(prompt, system, model?) convenience builtin for one-shot system-prompt calls; llm_call_sys() internal helper shared with llm_system - New: multi_agent_debate.omc — agents argue FOR/AGAINST via batch_llm_call, judge evaluates all rounds and returns winner + confidence via json_extract - Docs: llm_system entry added to llm_workflow category in docs.rs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 79d4915 commit 92ab5a0

4 files changed

Lines changed: 176 additions & 12 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# multi_agent_debate.omc — agents argue a topic, judge selects the winner
2+
#
3+
# Pattern:
4+
# 1. Decompose topic into FOR/AGAINST positions
5+
# 2. Run N debate rounds via batch_llm_call
6+
# 3. Judge evaluates all arguments and picks winner
7+
# 4. Report verdict + confidence
8+
9+
fn build_debater_prompt(position, topic, round, prior_args) {
10+
h sys = str_concat("You are a rigorous debater arguing the ", position, " side. Be concise, logical, and make exactly 2 strong points.")
11+
h prompt = str_concat(
12+
"Topic: ", topic,
13+
"\nYour position: ", position,
14+
"\nRound: ", to_str(round)
15+
)
16+
if arr_len(prior_args) > 0 {
17+
prompt = str_concat(prompt, "\nPrior arguments:\n", arr_join(prior_args, "\n"))
18+
}
19+
prompt = str_concat(prompt, "\n\nMake your 2 best arguments for the ", position, " side.")
20+
return {prompt: prompt, system: sys}
21+
}
22+
23+
fn judge_debate(topic, for_args, against_args) {
24+
h sys = "You are an impartial judge. Evaluate the quality of arguments on both sides. Respond in JSON: {winner: 'FOR'|'AGAINST', confidence: 1-10, reasoning: '...'}"
25+
h combined_for = arr_join(for_args, "\n---\n")
26+
h combined_against = arr_join(against_args, "\n---\n")
27+
h prompt = str_concat(
28+
"Topic: ", topic,
29+
"\n\nFOR arguments:\n", combined_for,
30+
"\n\nAGAINST arguments:\n", combined_against,
31+
"\n\nWho made the stronger case? Reply with JSON only."
32+
)
33+
h raw = llm_call(prompt, null, sys)
34+
return json_extract(raw)
35+
}
36+
37+
fn run_debate(topic, rounds) {
38+
print(str_concat("=== Debate: ", topic, " ==="))
39+
print(str_concat("Rounds: ", to_str(rounds)))
40+
print("")
41+
42+
h for_args = []
43+
h against_args = []
44+
45+
h round = 0
46+
while round < rounds {
47+
print(str_concat("--- Round ", to_str(round + 1), " ---"))
48+
49+
h for_prompt = build_debater_prompt("FOR", topic, round + 1, against_args)
50+
h against_prompt = build_debater_prompt("AGAINST", topic, round + 1, for_args)
51+
52+
h responses = batch_llm_call([for_prompt, against_prompt])
53+
54+
h for_arg = responses[0]
55+
h against_arg = responses[1]
56+
57+
arr_push(for_args, str_concat("[FOR R", to_str(round + 1), "] ", for_arg))
58+
arr_push(against_args, str_concat("[AGAINST R", to_str(round + 1), "] ", against_arg))
59+
60+
print(str_concat("FOR: ", str_slice(for_arg, 0, 100), "..."))
61+
print(str_concat("AGAINST: ", str_slice(against_arg, 0, 100), "..."))
62+
print("")
63+
64+
round = round + 1
65+
}
66+
67+
print("=== JUDGING ===")
68+
h verdict = judge_debate(topic, for_args, against_args)
69+
if verdict == null {
70+
print("Judge could not parse verdict")
71+
return null
72+
}
73+
h winner = verdict["winner"]
74+
h confidence = verdict["confidence"]
75+
h reasoning = verdict["reasoning"]
76+
print(str_concat("Winner: ", to_str(winner)))
77+
print(str_concat("Confidence: ", to_str(confidence), "/10"))
78+
print(str_concat("Reasoning: ", to_str(reasoning)))
79+
return verdict
80+
}
81+
82+
# ── Debate 1: AI regulation ──────────────────────────────────────────────────
83+
h result1 = run_debate("AI systems should be regulated like pharmaceutical drugs", 2)
84+
print("")
85+
86+
# ── Debate 2: Remote work ────────────────────────────────────────────────────
87+
h result2 = run_debate("Remote work is better than office work for productivity", 2)
88+
print("")
89+
90+
print("=== Debate series complete ===")

omnimcode-core/src/docs.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,19 @@ pub const BUILTINS: &[BuiltinDoc] = &[
13391339
example: r#"batch_llm_chat([[{"role":"user","content":"hi"}], [{"role":"user","content":"bye"}]])"#,
13401340
unique_to_omc: true,
13411341
},
1342+
BuiltinDoc {
1343+
name: "llm_system", category: "llm_workflow",
1344+
signature: "(prompt: string, system: string, model?: string) -> string",
1345+
description: concat!(
1346+
"Convenience wrapper for a single user prompt with a system instruction. ",
1347+
"Equivalent to `llm_call(prompt, model, system)` or calling `llm_chat` with a system message prepended. ",
1348+
"Model defaults to the active provider default (claude-3-5-haiku-latest for Anthropic). ",
1349+
"Reads ANTHROPIC_API_KEY or OPENAI_API_KEY from environment."
1350+
),
1351+
example: r#"h answer = llm_system("List 3 Fibonacci numbers", "You are a concise math tutor. Reply in one sentence.")
1352+
print(answer)"#,
1353+
unique_to_omc: true,
1354+
},
13421355
BuiltinDoc {
13431356
name: "llm_tools", category: "llm_workflow",
13441357
signature: "(messages: dict[], tools: dict[], model?: string) -> dict",

omnimcode-core/src/interpreter.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,10 +2227,10 @@ impl Interpreter {
22272227
| "re_match" | "re_find" | "re_find_all" | "re_replace" | "re_split"
22282228
| "json_parse" | "json_stringify" | "json_extract" | "str_format"
22292229
| "sha256" | "sha512" | "base64_encode" | "base64_decode"
2230-
// LLM builtins (Anthropic API)
2231-
| "llm_call" | "llm_chat" | "llm_embed"
2232-
| "batch_llm_call" | "batch_llm_chat"
2230+
// LLM builtins
2231+
| "llm_call" | "llm_chat" | "llm_embed" | "llm_models" | "llm_system"
22332232
| "llm_tools" | "substrate_embed"
2233+
| "batch_llm_call" | "batch_llm_chat"
22342234
// HTTP builtins
22352235
| "http_get" | "http_post" | "http_post_json" | "http_put" | "http_delete"
22362236
| "now_iso" | "now_unix" | "format_time" | "parse_time"
@@ -2342,14 +2342,8 @@ impl Interpreter {
23422342
| "arr_enumerate" | "arr_window"
23432343
// Meta-evaluation
23442344
| "eval_omc" | "eval_omc_fresh" | "eval_omc_ctx" | "omc_source"
2345-
// Native LLM builtins
2346-
| "llm_call" | "llm_chat" | "llm_embed" | "llm_models"
2347-
| "batch_llm_call" | "batch_llm_chat"
2348-
| "llm_tools" | "substrate_embed"
23492345
// Process execution builtins
23502346
| "omc_spawn" | "omc_pipe"
2351-
// Native HTTP builtins
2352-
| "http_get" | "http_post" | "http_post_json" | "http_put" | "http_delete"
23532347
)
23542348
}
23552349

@@ -3632,6 +3626,9 @@ impl Interpreter {
36323626
// returns the assistant's text reply as a String.
36333627
// Model defaults to "claude-3-5-haiku-latest".
36343628
// Reads API key from env var ANTHROPIC_API_KEY.
3629+
// NOTE: cfg-gated so modern native-llm arms at the bottom fire when
3630+
// llm-builtins is not in the feature set (default build).
3631+
#[cfg(feature = "llm-builtins")]
36353632
"llm_call" => {
36363633
#[cfg(feature = "llm-builtins")]
36373634
{
@@ -3687,6 +3684,7 @@ impl Interpreter {
36873684
// Multi-turn chat. `messages` is an OMC array of dicts, each with
36883685
// keys "role" ("user" | "assistant") and "content" (String).
36893686
// Returns the assistant's reply String.
3687+
#[cfg(feature = "llm-builtins")]
36903688
"llm_chat" => {
36913689
#[cfg(feature = "llm-builtins")]
36923690
{
@@ -3767,6 +3765,7 @@ impl Interpreter {
37673765
// the voyage-3-lite model. If VOYAGE_API_KEY is not set and
37683766
// ANTHROPIC_API_KEY is not set, returns an error.
37693767
// Model defaults to "voyage-3-lite".
3768+
#[cfg(feature = "llm-builtins")]
37703769
"llm_embed" => {
37713770
#[cfg(feature = "llm-builtins")]
37723771
{
@@ -9515,11 +9514,22 @@ impl Interpreter {
95159514
}
95169515
let prompt = self.eval_expr(&args[0])?.to_display_string();
95179516
let model = if args.len() > 1 {
9518-
Some(self.eval_expr(&args[1])?.to_display_string())
9517+
match self.eval_expr(&args[1])? {
9518+
Value::Null => None,
9519+
v => Some(v.to_display_string()),
9520+
}
9521+
} else {
9522+
None
9523+
};
9524+
let system = if args.len() > 2 {
9525+
match self.eval_expr(&args[2])? {
9526+
Value::Null => None,
9527+
v => Some(v.to_display_string()),
9528+
}
95199529
} else {
95209530
None
95219531
};
9522-
crate::llm_builtins::llm_call(&prompt, model.as_deref())
9532+
crate::llm_builtins::llm_call_sys(&prompt, model.as_deref(), system.as_deref())
95239533
}
95249534
#[cfg(feature = "native-llm")]
95259535
"llm_chat" => {
@@ -9549,6 +9559,21 @@ impl Interpreter {
95499559
let floats = crate::llm_builtins::llm_embed(&text, model.as_deref())?;
95509560
Ok(floats)
95519561
}
9562+
// llm_system(prompt, system, model?) -> string
9563+
// Convenience: send a user prompt with a system instruction in one call.
9564+
"llm_system" => {
9565+
if args.len() < 2 {
9566+
return Err("llm_system requires (prompt, system, model?)".to_string());
9567+
}
9568+
let prompt = self.eval_expr(&args[0])?.to_display_string();
9569+
let system = self.eval_expr(&args[1])?.to_display_string();
9570+
let model = if args.len() > 2 {
9571+
Some(self.eval_expr(&args[2])?.to_display_string())
9572+
} else {
9573+
None
9574+
};
9575+
crate::llm_builtins::llm_system(&prompt, &system, model.as_deref())
9576+
}
95529577
// llm_models() -> dict[]
95539578
// Returns the list of models available from the active provider.
95549579
// Each element is a dict with at least {"id": string, "provider": string}.

omnimcode-core/src/llm_builtins.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,25 @@ use std::collections::BTreeMap;
4646
/// `model` overrides `LLM_MODEL` for this call only.
4747
#[cfg(feature = "native-llm")]
4848
pub fn llm_call(prompt: &str, model_override: Option<&str>) -> Result<Value, String> {
49+
llm_call_sys(prompt, model_override, None)
50+
}
51+
52+
pub fn llm_call_sys(
53+
prompt: &str,
54+
model_override: Option<&str>,
55+
system: Option<&str>,
56+
) -> Result<Value, String> {
4957
let cfg = Config::from_env()?;
5058
let model = model_override.unwrap_or(&cfg.model).to_string();
51-
let reply = cfg.provider.complete_single(&cfg, &model, prompt)?;
59+
let reply = if let Some(sys) = system {
60+
let msgs = vec![
61+
ChatMessage { role: "system".to_string(), content: sys.to_string() },
62+
ChatMessage { role: "user".to_string(), content: prompt.to_string() },
63+
];
64+
cfg.provider.complete_chat(&cfg, &model, &msgs)?
65+
} else {
66+
cfg.provider.complete_single(&cfg, &model, prompt)?
67+
};
5268
Ok(Value::String(reply))
5369
}
5470

@@ -65,6 +81,17 @@ pub fn llm_chat(messages: &[ChatMessage], model_override: Option<&str>) -> Resul
6581
Ok(Value::String(reply))
6682
}
6783

84+
/// `llm_system(prompt, system, model?) -> string`
85+
///
86+
/// Convenience wrapper for llm_call with a system prompt.
87+
pub fn llm_system(
88+
prompt: &str,
89+
system: &str,
90+
model_override: Option<&str>,
91+
) -> Result<Value, String> {
92+
llm_call_sys(prompt, model_override, Some(system))
93+
}
94+
6895
/// `batch_llm_call(prompts, model?, concurrency?) -> string[]`
6996
///
7097
/// Send multiple prompts to the LLM sequentially and return all responses in
@@ -805,6 +832,15 @@ pub fn llm_call(_prompt: &str, _model_override: Option<&str>) -> Result<Value, S
805832
Err("llm_call: recompile with --features native-llm".to_string())
806833
}
807834

835+
#[cfg(not(feature = "native-llm"))]
836+
pub fn llm_call_sys(
837+
_prompt: &str,
838+
_model_override: Option<&str>,
839+
_system: Option<&str>,
840+
) -> Result<Value, String> {
841+
Err("llm_call: recompile with --features native-llm".to_string())
842+
}
843+
808844
/// Stub: `llm_chat` requires the `native-llm` Cargo feature.
809845
#[cfg(not(feature = "native-llm"))]
810846
pub fn llm_chat(_messages: &[ChatMessage], _model_override: Option<&str>) -> Result<Value, String> {

0 commit comments

Comments
 (0)