The orchestrator now calls a configured Ollama-compatible LLM endpoint over wasi-http and emits the LLM-generated plan back to the hub. When llm_endpoint is empty, the v0.1.0 deterministic stub is preserved as fallback (useful for tests and air-gapped environments). - module.yaml: bump to 0.2.0; new inputs llm_endpoint, llm_model, llm_api_key; net: permissions for OpenAI, Anthropic, localhost, 127.0.0.1 (Ollama defaults) - src/llm.rs: LlmClient trait, OllamaParams, build_user_prompt, build_ollama_body, extract_ollama_content, generate_plan; 9 unit tests for prompt-building and response-parsing - src/plan.rs: tagged enum with Explain/SaveFlow/RunFlow/ InstallModule variants; parse_and_validate; build_stub_plan (renamed from build_plan); validate(); 6 unit tests - src/lib.rs: WakiClient implementing LlmClient over the waki WASI-0.2 HTTP crate (wasm32-only); empty endpoint → stub - Cargo.toml: 0.2.0; thiserror dep; waki dep gated on wasm32 17 host-side tests pass. WASM bundle is 386KB (was 74KB; the delta is wasi-http via waki, expected). The deny path is exercised by the platform-side integration test in fai_platform/crates/fai_hub/tests/orchestrator_llm_path.rs. Signed-off-by: flemming-it <sf@flemming.it>
47 lines
1.7 KiB
Rust
47 lines
1.7 KiB
Rust
//! Verify that plan JSON emitted by orchestrator-llm parses
|
|
//! cleanly as a `fai_hub::plan::Plan`. This protects against
|
|
//! schema drift between this module and the hub.
|
|
//!
|
|
//! Note: this test runs only on the host target. The WASM build
|
|
//! does not include test harnesses.
|
|
#![cfg(not(target_arch = "wasm32"))]
|
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
|
|
use orchestrator_llm::build_stub_plan;
|
|
use serde_json::Value;
|
|
|
|
#[test]
|
|
fn emitted_plan_has_required_top_level_fields() {
|
|
let json = build_stub_plan("test goal");
|
|
let parsed: Value = serde_json::from_str(&json).expect("plan must be valid json");
|
|
assert!(parsed["schema_version"].is_u64());
|
|
assert!(parsed["goal"].is_string());
|
|
assert!(parsed["steps"].is_array());
|
|
}
|
|
|
|
#[test]
|
|
fn each_step_has_kind_tag() {
|
|
let json = build_stub_plan("test goal");
|
|
let parsed: Value = serde_json::from_str(&json).unwrap();
|
|
for step in parsed["steps"].as_array().unwrap() {
|
|
assert!(step["kind"].is_string(), "step missing kind tag: {step}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn save_flow_step_yaml_parses_as_flow_definition() {
|
|
// The save_flow step embeds a YAML flow body. Verify it
|
|
// round-trips through serde_yaml.
|
|
let json = build_stub_plan("anything");
|
|
let parsed: Value = serde_json::from_str(&json).unwrap();
|
|
let save_flow_step = parsed["steps"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|s| s["kind"] == "save_flow")
|
|
.expect("expected a save_flow step");
|
|
let yaml_text = save_flow_step["yaml"].as_str().unwrap();
|
|
let flow: Value = serde_yaml::from_str(yaml_text).expect("embedded yaml must parse");
|
|
assert_eq!(flow["name"], "orchestrator-stub-greeting");
|
|
assert!(flow["steps"].is_array());
|
|
}
|