harmony-protocol 0.1.0

Reverse-engineered OpenAI Harmony response format library for structured conversation handling
Documentation
use harmony_response::{
    chat::{Conversation, DeveloperContent, Message, Role, SystemContent, ToolDescription, ToolNamespaceConfig},
    load_harmony_encoding, HarmonyEncodingName, StreamableParser,
};

#[test]
fn test_full_conversation_roundtrip() {
    // Create a complete conversation with all features
    let system_content = SystemContent::new()
        .with_model_identity("Test Assistant")
        .with_required_channels(["analysis", "commentary", "final"])
        .with_browser_tool();

    let tools = vec![
        ToolDescription::new(
            "calculate",
            "Performs calculations",
            Some(serde_json::json!({
                "type": "object",
                "properties": {
                    "expression": {"type": "string"}
                },
                "required": ["expression"]
            }))
        )
    ];

    let dev_content = DeveloperContent::new()
        .with_instructions("You are a helpful assistant.")
        .with_function_tools(tools);

    let conversation = Conversation::from_messages([
        Message::from_role_and_content(Role::System, system_content),
        Message::from_role_and_content(Role::Developer, dev_content),
        Message::from_role_and_content(Role::User, "What is 2 + 2?"),
        Message::from_role_and_content(Role::Assistant, "I need to calculate this.")
            .with_channel("analysis"),
        Message::from_role_and_content(Role::Assistant, "The answer is 4.")
            .with_channel("final"),
    ]);

    // Test encoding loading (may fail without network, but that's expected)
    match load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss) {
        Ok(encoding) => {
            // Test conversation rendering
            let tokens = encoding
                .render_conversation(&conversation, None)
                .expect("Should render conversation");

            assert!(!tokens.is_empty(), "Should generate tokens");

            // Test completion rendering
            let completion_tokens = encoding
                .render_conversation_for_completion(&conversation, Role::Assistant, None)
                .expect("Should render for completion");

            assert!(completion_tokens.len() > tokens.len(), "Completion should have more tokens");

            // Test training rendering
            let training_tokens = encoding
                .render_conversation_for_training(&conversation, None)
                .expect("Should render for training");

            assert!(!training_tokens.is_empty(), "Should generate training tokens");

            println!("✅ Full conversation roundtrip test passed with {} tokens", tokens.len());
        }
        Err(e) => {
            println!("⚠️  Encoding load failed (expected without network): {}", e);
            // This is expected in CI environments without network access
        }
    }
}

#[test]
fn test_streaming_parser_comprehensive() {
    match load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss) {
        Ok(encoding) => {
            // Create a simple conversation to get realistic tokens
            let conversation = Conversation::from_messages([
                Message::from_role_and_content(Role::User, "Hello"),
            ]);

            let tokens = encoding
                .render_conversation_for_completion(&conversation, Role::Assistant, None)
                .expect("Should render");

            // Test streaming parser
            let mut parser = StreamableParser::new(encoding.clone(), Some(Role::Assistant))
                .expect("Should create parser");

            // Process tokens one by one (but be careful with incomplete sequences)
            for (i, &token) in tokens.iter().enumerate().take(10) { // Limit to avoid test issues
                if let Err(e) = parser.process(token) {
                    println!("Warning: Token processing failed at position {}: {}", i, e);
                    break; // Stop processing if we hit an error
                }

                // Test parser state queries
                let _current_role = parser.current_role();
                let _current_content = parser.current_content().unwrap_or_default();
                let _current_channel = parser.current_channel();
                let _current_recipient = parser.current_recipient();

                println!("Processed token {} at position {}", token, i);
            }

            // Try to finalize parsing (may fail with incomplete token sequence)
            match parser.process_eos() {
                Ok(_) => println!("✅ Streaming parser completed successfully"),
                Err(e) => println!("⚠️ Parser EOS warning (expected with partial tokens): {}", e),
            }

            let messages = parser.into_messages();
            println!("✅ Streaming parser processed {} messages", messages.len());
        }
        Err(e) => {
            println!("⚠️  Encoding load failed (expected without network): {}", e);
        }
    }
}

#[test]
fn test_message_validation() {
    // Test various message configurations
    let valid_system = Message::from_role_and_content(Role::System, SystemContent::new());
    assert_eq!(valid_system.author.role, Role::System);

    let valid_dev = Message::from_role_and_content(Role::Developer, DeveloperContent::new());
    assert_eq!(valid_dev.author.role, Role::Developer);

    let user_with_channel = Message::from_role_and_content(Role::User, "Hello")
        .with_channel("test")
        .with_recipient("assistant");

    assert_eq!(user_with_channel.channel, Some("test".to_string()));
    assert_eq!(user_with_channel.recipient, Some("assistant".to_string()));

    // Test tool message
    let tool_msg = Message::from_role_and_content(Role::Tool, "Tool response")
        .with_recipient("user");

    assert_eq!(tool_msg.author.role, Role::Tool);
}

#[test]
fn test_complex_tool_definitions() {
    let browser_ns = ToolNamespaceConfig::browser();
    assert_eq!(browser_ns.name, "browser");
    assert!(!browser_ns.tools.is_empty());

    let python_ns = ToolNamespaceConfig::python();
    assert_eq!(python_ns.name, "python");

    // Test custom namespace
    let custom_tools = vec![
        ToolDescription::new(
            "complex_tool",
            "A complex tool with nested schema",
            Some(serde_json::json!({
                "type": "object",
                "properties": {
                    "config": {
                        "type": "object",
                        "properties": {
                            "mode": {"type": "string", "enum": ["fast", "accurate"]},
                            "threshold": {"type": "number", "minimum": 0, "maximum": 1}
                        },
                        "required": ["mode"]
                    },
                    "data": {
                        "type": "array",
                        "items": {"type": "string"}
                    }
                },
                "required": ["config"]
            }))
        )
    ];

    let custom_ns = ToolNamespaceConfig::new(
        "custom",
        Some("Custom tool namespace".to_string()),
        custom_tools
    );

    assert_eq!(custom_ns.name, "custom");
    assert_eq!(custom_ns.tools.len(), 1);
    assert_eq!(custom_ns.tools[0].name, "complex_tool");
}

#[test]
fn test_channel_configuration() {
    let channel_config = SystemContent::new()
        .with_required_channels(["analysis", "commentary", "final"])
        .with_conversation_start_date("2024-01-01")
        .with_knowledge_cutoff("2024-06");

    if let Some(config) = &channel_config.channel_config {
        assert_eq!(config.valid_channels.len(), 3);
        assert!(config.channel_required);
        assert!(config.valid_channels.contains(&"analysis".to_string()));
    }

    assert_eq!(channel_config.conversation_start_date, Some("2024-01-01".to_string()));
    assert_eq!(channel_config.knowledge_cutoff, Some("2024-06".to_string()));
}

#[test]
fn test_error_handling() {
    // Test invalid role conversion
    let invalid_role = Role::try_from("invalid_role");
    assert!(invalid_role.is_err());

    // Test valid role conversions
    assert_eq!(Role::try_from("user").unwrap(), Role::User);
    assert_eq!(Role::try_from("assistant").unwrap(), Role::Assistant);
    assert_eq!(Role::try_from("system").unwrap(), Role::System);
    assert_eq!(Role::try_from("developer").unwrap(), Role::Developer);
    assert_eq!(Role::try_from("tool").unwrap(), Role::Tool);

    // Test role string conversion
    assert_eq!(Role::User.as_str(), "user");
    assert_eq!(Role::Assistant.as_str(), "assistant");
}