use std::sync::Arc;
use async_trait::async_trait;
use noema_core::{NoemaError, Result, Route, RoutedAction, Router};
use noema_needle::{DylibEngine, EngineSettings, NeedleEngine};
use tokio_util::sync::CancellationToken;
use crate::action::ActionRegistry;
pub const DEFAULT_MIN_CONFIDENCE: f32 = 0.6;
#[derive(Debug)]
pub struct NeedleRouter<E: NeedleEngine> {
engine: Arc<E>,
registry: ActionRegistry,
min_confidence: f32,
}
impl<E: NeedleEngine> NeedleRouter<E> {
pub fn new(engine: Arc<E>, registry: ActionRegistry) -> Self {
Self {
engine,
registry,
min_confidence: DEFAULT_MIN_CONFIDENCE,
}
}
pub fn with_min_confidence(mut self, min: f32) -> Self {
self.min_confidence = min;
self
}
}
impl NeedleRouter<DylibEngine> {
pub fn from_default() -> Result<Self> {
let registry = ActionRegistry::builtin();
let settings = EngineSettings::new(registry.tools_json()).with_system(format!(
"You are the Noema application router.\n\
date: {}\n\
locale: en",
today()
));
let engine = DylibEngine::from_default(settings).map_err(|error| {
NoemaError::Router(format!("failed to load the Needle engine: {error}"))
})?;
Ok(Self::new(Arc::new(engine), registry))
}
}
fn today() -> String {
let now = std::time::SystemTime::now();
let seconds = now
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = seconds / 86_400;
let z = days as i64 + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{d:02}")
}
#[async_trait]
impl<E: NeedleEngine + 'static> Router for NeedleRouter<E> {
fn id(&self) -> &str {
"needle-router"
}
async fn route(&self, text: &str, _cancel: CancellationToken) -> Result<Route> {
let text = text.to_string();
let engine = Arc::clone(&self.engine);
let response = tokio::task::spawn_blocking(move || -> noema_needle::Result<noema_needle::NeedleResponse> {
engine.reset()?;
engine.complete(&text, 256)
})
.await
.map_err(|join| NoemaError::Router(format!("router task failed: {join}")))?
.map_err(|error| NoemaError::Router(error.to_string()))?;
match response.calls().first() {
Some(call)
if self.registry.get(&call.name).is_some()
&& response.confidence.unwrap_or(1.0) >= self.min_confidence =>
{
tracing::debug!(
action = %call.name,
confidence = ?response.confidence,
"routed to action"
);
Ok(Route::Action(RoutedAction {
id: call.name.clone(),
arguments: call.arguments.clone(),
confidence: response.confidence,
}))
}
Some(call) if self.registry.get(&call.name).is_some() => Ok(Route::Escalate {
reason: format!(
"low confidence ({:?} < {}): uncertain about {}",
response.confidence, self.min_confidence, call.name
),
}),
_ => Ok(Route::Escalate {
reason: "no registered action matches the request".into(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use noema_core::Route;
use noema_needle::{FunctionCall, NeedleResponse};
use serde_json::json;
#[derive(Debug)]
struct FakeEngine {
response: std::sync::Mutex<NeedleResponse>,
prompts: std::sync::Mutex<Vec<String>>,
}
impl FakeEngine {
fn new(response: NeedleResponse) -> Self {
Self {
response: std::sync::Mutex::new(response),
prompts: std::sync::Mutex::new(Vec::new()),
}
}
}
impl NeedleEngine for FakeEngine {
fn id(&self) -> &str {
"fake"
}
fn complete(&self, input: &str, _max_new_tokens: u32) -> noema_needle::Result<NeedleResponse> {
self.prompts.lock().unwrap().push(input.to_string());
Ok(self.response.lock().unwrap().clone())
}
fn reset(&self) -> noema_needle::Result<()> {
Ok(())
}
}
fn call_response(name: &str, args: serde_json::Value, confidence: Option<f32>) -> NeedleResponse {
NeedleResponse {
response_type: "call".into(),
function_calls: vec![FunctionCall {
name: name.into(),
arguments: args,
}],
confidence,
..Default::default()
}
}
fn refusal() -> NeedleResponse {
NeedleResponse {
response_type: "call".into(),
function_calls: vec![],
..Default::default()
}
}
#[tokio::test]
async fn registered_action_becomes_routed_action() {
let engine = Arc::new(FakeEngine::new(call_response(
"open_flashcards",
json!({}),
Some(0.9),
)));
let router = NeedleRouter::new(engine, ActionRegistry::default());
let route = router
.route("open my flashcards", CancellationToken::new())
.await
.expect("route");
match route {
Route::Action(action) => {
assert_eq!(action.id, "open_flashcards");
assert_eq!(action.confidence, Some(0.9));
}
other => panic!("expected action, got {other:?}"),
}
}
#[tokio::test]
async fn low_confidence_call_escalates() {
let engine = Arc::new(FakeEngine::new(call_response(
"go_to_settings",
json!({}),
Some(0.53),
)));
let router = NeedleRouter::new(engine, ActionRegistry::default());
let route = router
.route("go to settings", CancellationToken::new())
.await
.expect("route");
assert!(
matches!(route, Route::Escalate { .. }),
"sub-threshold calls must escalate, got {route:?}"
);
let engine = Arc::new(FakeEngine::new(call_response(
"go_to_settings",
json!({}),
Some(0.53),
)));
let router = NeedleRouter::new(engine, ActionRegistry::default())
.with_min_confidence(0.4);
let route = router
.route("go to settings", CancellationToken::new())
.await
.expect("route");
assert!(matches!(route, Route::Action(_)));
}
#[tokio::test]
async fn refusal_escalates() {
let engine = Arc::new(FakeEngine::new(refusal()));
let router = NeedleRouter::new(engine, ActionRegistry::default());
let route = router
.route("what is the capital of france", CancellationToken::new())
.await
.expect("route");
assert!(matches!(route, Route::Escalate { .. }));
}
#[tokio::test]
async fn unregistered_tool_name_escalates() {
let engine = Arc::new(FakeEngine::new(call_response(
"delete_all_files",
json!({}),
None,
)));
let router = NeedleRouter::new(engine, ActionRegistry::default());
let route = router
.route("delete everything", CancellationToken::new())
.await
.expect("route");
assert!(matches!(route, Route::Escalate { .. }));
}
#[tokio::test]
async fn engine_failure_is_a_router_error() {
let engine = Arc::new(FakeEngine::new(NeedleResponse {
response_type: "call".into(),
function_calls: vec![],
error: Some("boom".into()),
..Default::default()
}));
let router = NeedleRouter::new(engine, ActionRegistry::default());
let route = router
.route("open my flashcards", CancellationToken::new())
.await
.expect("route");
assert!(matches!(route, Route::Escalate { .. }));
}
}