active-call 0.3.83

A SIP/WebRTC voice agent
Documentation
use active_call::{
    playbook::{LlmConfig, Playbook, PlaybookConfig},
    synthesis::SynthesisType,
    transcription::TranscriptionType,
};
use dotenvy::dotenv;
use reqwest::Client;
use serde_json::json;
use std::fs;

#[tokio::test]
async fn test_parse_playbook() {
    let content = r#"---
asr:
  provider: "aliyun"
llm:
  provider: "aliyun"
  model: "qwen-plus"
  apiKey: "test-key"
tts:
  provider: "aliyun"
vad:
  provider: "silero"
denoise: true
extra:
  key1: value1
---
Hello, I am an AI assistant.
"#;
    let path = "test_playbook.md";
    fs::write(path, content).unwrap();

    let playbook = Playbook::load(path).await.unwrap();
    assert_eq!(
        playbook
            .config
            .llm
            .as_ref()
            .unwrap()
            .model
            .as_ref()
            .unwrap(),
        "qwen-plus"
    );
    assert_eq!(
        playbook
            .config
            .llm
            .as_ref()
            .unwrap()
            .api_key
            .as_ref()
            .unwrap(),
        "test-key"
    );
    assert_eq!(playbook.config.denoise, Some(true));
    assert_eq!(
        playbook.config.extra.as_ref().unwrap().get("key1").unwrap(),
        "value1"
    );
    assert_eq!(
        playbook
            .config
            .llm
            .as_ref()
            .unwrap()
            .prompt
            .as_ref()
            .unwrap(),
        "Hello, I am an AI assistant."
    );

    fs::remove_file(path).unwrap();
}

#[tokio::test]
async fn test_original_style_deepgram_asr_tts_config_loads() {
    let content = r#"---
asr:
  provider: "deepgram"
  language: "en"
  model: "nova-3"
  apiKey: "dg-test-key"
tts:
  provider: "deepgram"
  voice: "aura-2-thalia-en"
  apiKey: "dg-test-key"
llm:
  provider: "openai"
  model: "qwen-plus"
  apiKey: "dashscope-test-key"
  baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1"
---
Test prompt
"#;

    let playbook = Playbook::parse(content).unwrap();

    let asr = playbook.config.asr.as_ref().unwrap();
    assert_eq!(asr.provider, Some(TranscriptionType::Deepgram));
    assert_eq!(asr.model_type.as_deref(), Some("nova-3"));
    assert_eq!(asr.secret_key.as_deref(), Some("dg-test-key"));

    let tts = playbook.config.tts.as_ref().unwrap();
    assert_eq!(tts.provider, Some(SynthesisType::Deepgram));
    assert_eq!(tts.model.as_deref(), Some("aura-2-thalia-en"));
    assert_eq!(tts.secret_key.as_deref(), Some("dg-test-key"));

    let llm = playbook.config.llm.as_ref().unwrap();
    assert_eq!(llm.model.as_deref(), Some("qwen-plus"));
    assert_eq!(llm.api_key.as_deref(), Some("dashscope-test-key"));
}

#[tokio::test]
async fn test_aliyun_llm_integration() {
    dotenv().ok();
    let api_key = std::env::var("ALIYUN_API_KEY").unwrap_or_default();
    if api_key.is_empty() || api_key == "your_aliyun_api_key_here" {
        println!("Skipping Aliyun LLM integration test: ALIYUN_API_KEY not set");
        return;
    }

    let config = PlaybookConfig {
        llm: Some(LlmConfig {
            provider: "aliyun".to_string(),
            model: Some("qwen-plus".to_string()),
            base_url: Some("https://dashscope.aliyuncs.com/compatible-mode/v1".to_string()),
            api_key: Some(api_key),
            prompt: Some("You are a helpful assistant. Reply with 'PONG'.".to_string()),
            ..Default::default()
        }),
        ..Default::default()
    };

    let playbook = Playbook {
        config,
        scenes: Default::default(),
        initial_scene_id: None,
        raw_content: String::new(),
    };

    let mut history = Vec::new();
    history.push(json!({
        "role": "system",
        "content": playbook
            .config
            .llm
            .as_ref()
            .unwrap()
            .prompt
            .as_ref()
            .unwrap()
    }));
    history.push(json!({
        "role": "user",
        "content": "PING",
    }));

    let client = Client::new();
    let llm_config = playbook.config.llm.as_ref().unwrap();
    let url = format!("{}/chat/completions", llm_config.base_url.as_ref().unwrap());

    let body = json!({
        "model": llm_config.model.as_ref().unwrap(),
        "messages": history,
    });

    let res = client
        .post(&url)
        .header(
            "Authorization",
            format!("Bearer {}", llm_config.api_key.as_ref().unwrap()),
        )
        .json(&body)
        .send()
        .await;

    match res {
        Ok(response) => {
            assert!(
                response.status().is_success(),
                "Aliyun LLM request failed with status: {}",
                response.status()
            );
            let json: serde_json::Value = response.json().await.unwrap();
            let content = json["choices"][0]["message"]["content"].as_str().unwrap();
            println!("Aliyun LLM response: {}", content);
            assert!(content.contains("PONG"));
        }
        Err(e) => {
            panic!("Failed to connect to Aliyun LLM: {}", e);
        }
    }
}

#[tokio::test]
async fn test_parse_playbook_with_recorder() {
    let content = r#"---
recorder:
  recorderFile: "records/{id}.wav"
  samplerate: 16000
---
Hello
"#;
    let path = "test_playbook_recorder.md";
    fs::write(path, content).unwrap();

    let playbook = Playbook::load(path).await.unwrap();
    assert_eq!(
        playbook.config.recorder.as_ref().unwrap().recorder_file,
        "records/{id}.wav"
    );
    assert_eq!(playbook.config.recorder.as_ref().unwrap().samplerate, 16000);

    fs::remove_file(path).unwrap();
}