langchainrust 0.7.0

A LangChain-inspired framework for building LLM applications in Rust. Supports OpenAI, Agents, Tools, Memory, Chains, RAG, BM25, Hybrid Retrieval, LangGraph, HyDE, Reranking, MultiQuery, and native Function Calling.
//! Computer Use tool for screen interaction via Anthropic API or native mode.

pub mod actions;
pub mod screen;

pub use actions::{ComputerMode, ComputerUseInput, ComputerUseOutput};
pub use screen::ComputerUseTool;

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::tools::ToolError;
    use crate::BaseTool;

    fn tool() -> ComputerUseTool {
        ComputerUseTool::new_anthropic("test-key", 1024, 768)
    }

    // -- name / description / schema --

    #[test]
    fn test_name() {
        let t = tool();
        assert_eq!(t.name(), "computer_use");
    }

    #[test]
    fn test_description_contains_actions() {
        let t = tool();
        let desc = t.description();
        for action in &["screenshot", "click", "type", "scroll", "key_press", "wait"] {
            assert!(
                desc.contains(action),
                "description should mention '{}'",
                action
            );
        }
    }

    #[test]
    fn test_args_schema_has_action_field() {
        let t = tool();
        let schema = t.args_schema().expect("schema should be present");
        let props = schema
            .get("properties")
            .expect("schema should have properties");
        assert!(
            props.get("action").is_some(),
            "schema should define 'action' property"
        );
    }

    // -- input validation --

    #[tokio::test]
    async fn test_run_invalid_json() {
        let t = tool();
        let result = t.run("not json".to_string()).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
    }

    #[tokio::test]
    async fn test_run_unknown_action() {
        let t = tool();
        let input = r#"{"action":"fly"}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("Unknown action"),
            "error should mention unknown action, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_click_missing_coordinate() {
        let t = tool();
        let input = r#"{"action":"click"}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("coordinate"),
            "error should mention missing coordinate, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_type_missing_text() {
        let t = tool();
        let input = r#"{"action":"type"}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("text"),
            "error should mention missing text, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_key_press_missing_keys() {
        let t = tool();
        let input = r#"{"action":"key_press"}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("keys"),
            "error should mention missing keys, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_scroll_missing_direction() {
        let t = tool();
        let input = r#"{"action":"scroll","coordinate":[100,200]}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("direction"),
            "error should mention missing direction, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_coordinate_out_of_bounds() {
        let t = tool();
        let input = r#"{"action":"click","coordinate":[2000,500]}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("out of bounds"),
            "error should mention out of bounds, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_coordinate_wrong_length() {
        let t = tool();
        let input = r#"{"action":"click","coordinate":[100]}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("exactly [x, y]"),
            "error should mention coordinate format, got: {}",
            err
        );
    }

    #[tokio::test]
    async fn test_run_empty_api_key() {
        let t = ComputerUseTool::new_anthropic("", 1024, 768);
        let input = r#"{"action":"screenshot"}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ToolError::InvalidInput(_)));
        assert!(
            err.to_string().contains("API key"),
            "error should mention API key, got: {}",
            err
        );
    }

    // -- build_tool_input (unit tests) --

    #[test]
    fn test_build_tool_input_screenshot() {
        let t = tool();
        let input = ComputerUseInput {
            action: "screenshot".to_string(),
            coordinate: None,
            text: None,
            keys: None,
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["action"], "screenshot");
    }

    #[test]
    fn test_build_tool_input_click() {
        let t = tool();
        let input = ComputerUseInput {
            action: "click".to_string(),
            coordinate: Some(vec![100, 200]),
            text: None,
            keys: None,
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["action"], "mouse_click");
        assert_eq!(result["coordinate"], serde_json::json!([100, 200]));
        assert_eq!(result["text"], "left");
    }

    #[test]
    fn test_build_tool_input_click_right_button() {
        let t = tool();
        let input = ComputerUseInput {
            action: "click".to_string(),
            coordinate: Some(vec![50, 75]),
            text: Some("right".to_string()),
            keys: None,
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["text"], "right");
    }

    #[test]
    fn test_build_tool_input_type() {
        let t = tool();
        let input = ComputerUseInput {
            action: "type".to_string(),
            coordinate: None,
            text: Some("hello world".to_string()),
            keys: None,
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["action"], "type");
        assert_eq!(result["text"], "hello world");
    }

    #[test]
    fn test_build_tool_input_scroll() {
        let t = tool();
        let input = ComputerUseInput {
            action: "scroll".to_string(),
            coordinate: Some(vec![500, 300]),
            text: None,
            keys: None,
            direction: Some("up".to_string()),
            amount: Some(5),
            duration_ms: None,
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["action"], "scroll");
        assert_eq!(result["coordinate"], serde_json::json!([500, 300]));
        assert_eq!(result["direction"], "up");
        assert_eq!(result["amount"], 5);
    }

    #[test]
    fn test_build_tool_input_key_press() {
        let t = tool();
        let input = ComputerUseInput {
            action: "key_press".to_string(),
            coordinate: None,
            text: None,
            keys: Some(vec!["ctrl".to_string(), "c".to_string()]),
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["action"], "key");
        assert_eq!(result["text"], "ctrl+c");
    }

    #[test]
    fn test_build_tool_input_wait() {
        let t = tool();
        let input = ComputerUseInput {
            action: "wait".to_string(),
            coordinate: None,
            text: None,
            keys: None,
            direction: None,
            amount: None,
            duration_ms: Some(2000),
        };
        let result = t.build_tool_input(&input).unwrap();
        assert_eq!(result["action"], "wait");
        assert_eq!(result["duration_ms"], 2000);
    }

    #[test]
    fn test_build_tool_input_unknown_action() {
        let t = tool();
        let input = ComputerUseInput {
            action: "fly".to_string(),
            coordinate: None,
            text: None,
            keys: None,
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let result = t.build_tool_input(&input);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Unknown action"));
    }

    // -- constructor tests --

    #[test]
    fn test_default() {
        let t = ComputerUseTool::default();
        assert_eq!(t.name(), "computer_use");
        assert_eq!(t.display_width, 1024);
        assert_eq!(t.display_height, 768);
    }

    #[test]
    fn test_with_base_url() {
        let t = ComputerUseTool::new_anthropic("key", 1920, 1080)
            .with_base_url("https://custom.api.com");
        assert_eq!(t.base_url, "https://custom.api.com");
    }

    #[test]
    fn test_mode_returns_anthropic() {
        let t = tool();
        assert!(matches!(t.mode(), ComputerMode::AnthropicApi));
    }

    // -- build_anthropic_request --

    #[test]
    fn test_build_anthropic_request_contains_model() {
        let t = tool();
        let input = ComputerUseInput {
            action: "screenshot".to_string(),
            coordinate: None,
            text: None,
            keys: None,
            direction: None,
            amount: None,
            duration_ms: None,
        };
        let body = t.build_anthropic_request(&input).unwrap();
        assert_eq!(body["model"], "claude-sonnet-4-20250514");
        assert!(body["tools"].is_array());
        assert_eq!(body["tools"][0]["type"], "computer_20250124");
    }

    // -- integration test (requires network, ignored by default) --

    #[tokio::test]
    #[ignore = "requires ANTHROPIC_API_KEY environment variable"]
    async fn test_screenshot_with_real_api() {
        let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
        let t = ComputerUseTool::new_anthropic(api_key, 1024, 768);
        let input = r#"{"action":"screenshot"}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_ok(), "screenshot should succeed: {:?}", result);
    }

    #[tokio::test]
    #[ignore = "requires ANTHROPIC_API_KEY environment variable"]
    async fn test_click_with_real_api() {
        let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
        let t = ComputerUseTool::new_anthropic(api_key, 1024, 768);
        let input = r#"{"action":"click","coordinate":[500,400]}"#.to_string();
        let result = t.run(input).await;
        assert!(result.is_ok(), "click should succeed: {:?}", result);
    }
}