Skip to content

Commit 7ba616f

Browse files
add llm_stream_print + voting_ensemble demo
- New: llm_stream_print(prompt, system?, model?) — SSE streaming to stdout token-by-token; returns full accumulated text; works for both Anthropic and OpenAI providers (handles different delta shapes per provider) - New: examples/demos/voting_ensemble.omc — N agents vote on a question via batch_llm_call, majority wins; tests tech choice, startup strategy, ML algo - Docs: llm_stream_print entry in llm_workflow category - llm_call now accepts optional 3rd system arg to match examples/lib/llm.omc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 92ab5a0 commit 7ba616f

4 files changed

Lines changed: 272 additions & 0 deletions

File tree

examples/demos/voting_ensemble.omc

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# voting_ensemble.omc — multiple agents vote on a decision, majority wins
2+
#
3+
# Pattern:
4+
# 1. Ask N agents the same question (batch_llm_call)
5+
# 2. Each agent returns one of the allowed choices
6+
# 3. Tally votes, find majority winner
7+
# 4. Optional: ask dissenting agents to reconsider (soft consensus)
8+
9+
fn extract_choice(text, choices) {
10+
h i = 0
11+
while i < arr_len(choices) {
12+
if str_contains(str_upper(text), str_upper(choices[i])) {
13+
return choices[i]
14+
}
15+
i = i + 1
16+
}
17+
return choices[0]
18+
}
19+
20+
fn tally_votes(votes) {
21+
h counts = {}
22+
h i = 0
23+
while i < arr_len(votes) {
24+
h v = votes[i]
25+
if dict_has(counts, v) {
26+
counts[v] = counts[v] + 1
27+
} else {
28+
counts[v] = 1
29+
}
30+
i = i + 1
31+
}
32+
return counts
33+
}
34+
35+
fn find_winner(counts) {
36+
h best = ""
37+
h best_count = 0
38+
h keys = dict_keys(counts)
39+
h i = 0
40+
while i < arr_len(keys) {
41+
h k = keys[i]
42+
if counts[k] > best_count {
43+
best_count = counts[k]
44+
best = k
45+
}
46+
i = i + 1
47+
}
48+
return best
49+
}
50+
51+
fn vote_ensemble(question, choices, n_agents, system_prompt) {
52+
print(str_concat("Question: ", question))
53+
print(str_concat("Choices: ", arr_join(choices, " | ")))
54+
print(str_concat("Agents: ", to_str(n_agents)))
55+
print("")
56+
57+
h choice_str = arr_join(choices, ", ")
58+
h prompt = str_concat(
59+
question, "\n\n",
60+
"Reply with ONLY one of: ", choice_str,
61+
". No explanation. Just the choice word."
62+
)
63+
h sys = str_concat(
64+
system_prompt, " ",
65+
"You must respond with exactly one word from: ", choice_str
66+
)
67+
68+
h prompts = arr_fill({prompt: prompt, system: sys}, n_agents)
69+
h responses = batch_llm_call(prompts)
70+
71+
h votes = par_map(responses, fn(r) {
72+
return extract_choice(r, choices)
73+
})
74+
75+
h counts = tally_votes(votes)
76+
h winner = find_winner(counts)
77+
78+
print("Vote results:")
79+
h i = 0
80+
while i < arr_len(choices) {
81+
h c = choices[i]
82+
h cnt = 0
83+
if dict_has(counts, c) { cnt = counts[c] }
84+
print(str_concat(" ", c, ": ", to_str(cnt), "/", to_str(n_agents)))
85+
i = i + 1
86+
}
87+
print(str_concat("Winner: ", winner))
88+
return {winner: winner, counts: counts, votes: votes}
89+
}
90+
91+
# ── Decision 1: tech choice ──────────────────────────────────────────────────
92+
h r1 = vote_ensemble(
93+
"Which programming language is best for building production AI systems in 2025?",
94+
["Python", "Rust", "Go"],
95+
5,
96+
"You are a senior software architect with 10 years of production AI experience."
97+
)
98+
print("")
99+
100+
# ── Decision 2: strategy ─────────────────────────────────────────────────────
101+
h r2 = vote_ensemble(
102+
"Should a startup with $500k runway prioritize hiring or marketing?",
103+
["Hiring", "Marketing"],
104+
7,
105+
"You are a startup advisor who has helped 50+ early-stage companies."
106+
)
107+
print("")
108+
109+
# ── Decision 3: algorithm ────────────────────────────────────────────────────
110+
h r3 = vote_ensemble(
111+
"For a real-time recommendation system with 10M users, which approach is better?",
112+
["Collaborative", "Content-based", "Hybrid"],
113+
6,
114+
"You are a machine learning engineer specializing in recommender systems."
115+
)
116+
print("")
117+
118+
print("=== Ensemble voting complete ===")

omnimcode-core/src/docs.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,20 @@ pub const BUILTINS: &[BuiltinDoc] = &[
13521352
print(answer)"#,
13531353
unique_to_omc: true,
13541354
},
1355+
BuiltinDoc {
1356+
name: "llm_stream_print", category: "llm_workflow",
1357+
signature: "(prompt: string, system?: string, model?: string) -> string",
1358+
description: concat!(
1359+
"Stream the LLM response token-by-token to stdout as it arrives, then return the full accumulated text. ",
1360+
"Uses SSE streaming (stream:true in the API body). ",
1361+
"Works with both Anthropic and OpenAI providers (auto-detected via LLM_PROVIDER). ",
1362+
"Ideal for interactive CLI tools and demos where you want visible token-by-token output. ",
1363+
"Returns the complete response string when finished."
1364+
),
1365+
example: r#"h full = llm_stream_print("Write a haiku about recursion", "You are a poet.")
1366+
print(str_concat("Total chars: ", to_str(str_len(full))))"#,
1367+
unique_to_omc: true,
1368+
},
13551369
BuiltinDoc {
13561370
name: "llm_tools", category: "llm_workflow",
13571371
signature: "(messages: dict[], tools: dict[], model?: string) -> dict",

omnimcode-core/src/interpreter.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,6 +2229,7 @@ impl Interpreter {
22292229
| "sha256" | "sha512" | "base64_encode" | "base64_decode"
22302230
// LLM builtins
22312231
| "llm_call" | "llm_chat" | "llm_embed" | "llm_models" | "llm_system"
2232+
| "llm_stream_print"
22322233
| "llm_tools" | "substrate_embed"
22332234
| "batch_llm_call" | "batch_llm_chat"
22342235
// HTTP builtins
@@ -9574,6 +9575,32 @@ impl Interpreter {
95749575
};
95759576
crate::llm_builtins::llm_system(&prompt, &system, model.as_deref())
95769577
}
9578+
// llm_stream_print(prompt, system?, model?) -> string
9579+
// Streams LLM response to stdout token-by-token, returns full text.
9580+
// Uses SSE streaming API. system defaults to null (no system prompt).
9581+
"llm_stream_print" => {
9582+
if args.is_empty() {
9583+
return Err("llm_stream_print requires (prompt, system?, model?)".to_string());
9584+
}
9585+
let prompt = self.eval_expr(&args[0])?.to_display_string();
9586+
let system = if args.len() > 1 {
9587+
match self.eval_expr(&args[1])? {
9588+
Value::Null => None,
9589+
v => Some(v.to_display_string()),
9590+
}
9591+
} else {
9592+
None
9593+
};
9594+
let model = if args.len() > 2 {
9595+
match self.eval_expr(&args[2])? {
9596+
Value::Null => None,
9597+
v => Some(v.to_display_string()),
9598+
}
9599+
} else {
9600+
None
9601+
};
9602+
crate::llm_builtins::llm_stream_print(&prompt, system.as_deref(), model.as_deref())
9603+
}
95779604
// llm_models() -> dict[]
95789605
// Returns the list of models available from the active provider.
95799606
// Each element is a dict with at least {"id": string, "provider": string}.

omnimcode-core/src/llm_builtins.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,119 @@ pub fn llm_system(
9292
llm_call_sys(prompt, model_override, Some(system))
9393
}
9494

95+
/// `llm_stream_print(prompt, system?, model?) -> string`
96+
///
97+
/// Streams the LLM response token-by-token to stdout, then returns the full
98+
/// accumulated text. Uses SSE streaming (stream:true). Supports both Anthropic
99+
/// and OpenAI providers (auto-detected via LLM_PROVIDER env var).
100+
#[cfg(feature = "native-llm")]
101+
pub fn llm_stream_print(
102+
prompt: &str,
103+
system: Option<&str>,
104+
model_override: Option<&str>,
105+
) -> Result<Value, String> {
106+
use std::io::{BufRead, BufReader, Write};
107+
108+
let cfg = Config::from_env()?;
109+
let model = model_override.unwrap_or(&cfg.model).to_string();
110+
111+
// Build messages list
112+
let mut messages: Vec<ChatMessage> = Vec::new();
113+
if let Some(sys) = system {
114+
if !sys.is_empty() {
115+
messages.push(ChatMessage { role: "system".to_string(), content: sys.to_string() });
116+
}
117+
}
118+
messages.push(ChatMessage { role: "user".to_string(), content: prompt.to_string() });
119+
120+
match cfg.provider {
121+
Provider::Anthropic => {
122+
let mut system_parts: Vec<String> = Vec::new();
123+
let mut msgs_json: Vec<serde_json::Value> = Vec::new();
124+
for m in &messages {
125+
if m.role == "system" {
126+
system_parts.push(m.content.clone());
127+
} else {
128+
msgs_json.push(serde_json::json!({ "role": m.role, "content": m.content }));
129+
}
130+
}
131+
let mut body = serde_json::json!({
132+
"model": model, "max_tokens": 4096,
133+
"messages": msgs_json, "stream": true
134+
});
135+
if !system_parts.is_empty() {
136+
body["system"] = serde_json::Value::String(system_parts.join("\n\n"));
137+
}
138+
let resp = ureq::post(&cfg.base_url)
139+
.set("Content-Type", "application/json")
140+
.set("Authorization", &format!("Bearer {}", cfg.api_key))
141+
.set("anthropic-version", "2023-06-01")
142+
.set("x-api-key", &cfg.api_key)
143+
.send_json(body)
144+
.map_err(|e| format!("llm_stream HTTP error: {}", e))?;
145+
let reader = BufReader::new(resp.into_reader());
146+
let mut full_text = String::new();
147+
for line in reader.lines() {
148+
let line = line.map_err(|e| format!("llm_stream read error: {}", e))?;
149+
if let Some(data) = line.strip_prefix("data: ") {
150+
if data == "[DONE]" { break; }
151+
if let Ok(event) = serde_json::from_str::<serde_json::Value>(data) {
152+
if event["type"] == "content_block_delta" {
153+
if let Some(text) = event["delta"]["text"].as_str() {
154+
print!("{}", text);
155+
let _ = std::io::stdout().flush();
156+
full_text.push_str(text);
157+
}
158+
}
159+
}
160+
}
161+
}
162+
println!();
163+
Ok(Value::String(full_text))
164+
}
165+
Provider::OpenAI => {
166+
let msgs_json: Vec<serde_json::Value> = messages
167+
.iter()
168+
.map(|m| serde_json::json!({ "role": m.role, "content": m.content }))
169+
.collect();
170+
let body = serde_json::json!({
171+
"model": model, "messages": msgs_json, "stream": true
172+
});
173+
let resp = ureq::post(&cfg.base_url)
174+
.set("Content-Type", "application/json")
175+
.set("Authorization", &format!("Bearer {}", cfg.api_key))
176+
.send_json(body)
177+
.map_err(|e| format!("llm_stream HTTP error: {}", e))?;
178+
let reader = BufReader::new(resp.into_reader());
179+
let mut full_text = String::new();
180+
for line in reader.lines() {
181+
let line = line.map_err(|e| format!("llm_stream read error: {}", e))?;
182+
if let Some(data) = line.strip_prefix("data: ") {
183+
if data == "[DONE]" { break; }
184+
if let Ok(event) = serde_json::from_str::<serde_json::Value>(data) {
185+
if let Some(text) = event["choices"][0]["delta"]["content"].as_str() {
186+
print!("{}", text);
187+
let _ = std::io::stdout().flush();
188+
full_text.push_str(text);
189+
}
190+
}
191+
}
192+
}
193+
println!();
194+
Ok(Value::String(full_text))
195+
}
196+
}
197+
}
198+
199+
#[cfg(not(feature = "native-llm"))]
200+
pub fn llm_stream_print(
201+
_prompt: &str,
202+
_system: Option<&str>,
203+
_model_override: Option<&str>,
204+
) -> Result<Value, String> {
205+
Err("llm_stream_print: recompile with --features native-llm".to_string())
206+
}
207+
95208
/// `batch_llm_call(prompts, model?, concurrency?) -> string[]`
96209
///
97210
/// Send multiple prompts to the LLM sequentially and return all responses in

0 commit comments

Comments
 (0)