#[ cfg( all( test, feature = "integration" ) ) ]
mod tests
{
use api_huggingface::
{
Client,
environment::HuggingFaceEnvironmentImpl,
providers::ChatCompletionOptions,
components::
{
tools::{ Tool, ToolParameters, ParameterProperty },
models::Models,
},
secret::Secret,
};
fn setup_client() -> Client< HuggingFaceEnvironmentImpl >
{
use workspace_tools as workspace;
let workspace = workspace::workspace()
.expect( "[setup_client] Failed to access workspace - required for integration tests" );
let secrets = workspace.load_secrets_from_file( "-secrets.sh" )
.expect( "[setup_client] Failed to load secret/-secrets.sh - required for integration tests" );
let api_key = secrets.get( "HUGGINGFACE_API_KEY" )
.expect( "[setup_client] HUGGINGFACE_API_KEY not found in secret/-secrets.sh - required for integration tests" )
.clone();
let env = HuggingFaceEnvironmentImpl::build( Secret::new( api_key ), None )
.expect( "Failed to build environment" );
Client::build( env ).expect( "Failed to build client" )
}
fn create_weather_tool() -> Tool
{
let parameters = ToolParameters::new()
.with_property
(
"location",
ParameterProperty::string( "The city and state, e.g. San Francisco, CA" )
)
.with_property
(
"unit",
ParameterProperty::string( "Temperature unit (celsius or fahrenheit)" )
)
.with_required( vec![ "location".to_string() ] );
Tool::new
(
"get_current_weather",
"Get the current weather in a given location",
parameters
)
}
fn create_calculator_tool() -> Tool
{
let parameters = ToolParameters::new()
.with_property
(
"operation",
ParameterProperty::string( "The mathematical operation (add, subtract, multiply, divide)" )
)
.with_property
(
"a",
ParameterProperty::number( "First operand" )
)
.with_property
(
"b",
ParameterProperty::number( "Second operand" )
)
.with_required( vec![ "operation".to_string(), "a".to_string(), "b".to_string() ] );
Tool::new
(
"calculate",
"Perform basic mathematical operations",
parameters
)
}
#[ tokio::test ]
async fn test_basic_function_calling()
{
let client = setup_client();
let tool = create_weather_tool();
let response = client.providers()
.chat_completion_with_tools
(
Models::kimi_k2_instruct(),
vec!
[
api_huggingface::components::inference_shared::ChatMessage
{
role : "user".to_string(),
content : "What's the weather like in San Francisco?".to_string(),
tool_calls : None,
tool_call_id : None,
}
],
vec![ tool ],
ChatCompletionOptions::default(), )
.await;
assert!( response.is_ok(), "Function calling request failed : {:?}", response.err() );
let response = response.expect( "[test_basic_function_calling] Response should be Ok after is_ok() check - check chat_completion_with_tools() implementation" );
assert!( !response.choices.is_empty(), "Response has no choices" );
if let Some( ref tool_calls ) = response.choices[ 0 ].message.tool_calls
{
assert!( !tool_calls.is_empty(), "Expected tool calls in response" );
let tool_call = &tool_calls[ 0 ];
assert_eq!( tool_call.function.name, "get_current_weather", "Wrong tool called" );
let args : serde_json::Value = serde_json::from_str( &tool_call.function.arguments )
.unwrap_or_else( |e|
{
panic!
(
"Failed to parse function arguments from HuggingFace API.\n\
This indicates the API returned malformed JSON (likely rate limit/service degradation).\n\
\nArguments string received: {:?}\n\
Parse error: {}\n\
\nRetry the test. If failures persist, check HuggingFace API status.",
tool_call.function.arguments,
e
);
} );
assert!( args.get( "location" ).is_some(), "Missing location argument" );
}
}
#[ tokio::test ]
async fn test_tool_choice_none()
{
let client = setup_client();
let tool = create_weather_tool();
let response = client.providers()
.chat_completion_with_tools
(
Models::kimi_k2_instruct(),
vec!
[
api_huggingface::components::inference_shared::ChatMessage
{
role : "user".to_string(),
content : "What's the weather like in San Francisco?".to_string(),
tool_calls : None,
tool_call_id : None,
}
],
vec![ tool ],
ChatCompletionOptions { tool_choice : Some( "none".to_string() ), ..Default::default() },
)
.await;
assert!( response.is_ok(), "Request failed : {:?}", response.err() );
let response = response.expect( "[test_tool_choice_none] Response should be Ok after is_ok() check - check chat_completion_with_tools() with tool_choice='none' implementation" );
let has_tool_calls = response.choices[ 0 ].message.tool_calls.as_ref()
.is_some_and( | tc | !tc.is_empty() );
let has_content = !response.choices[ 0 ].message.content.is_empty();
assert!( has_content || has_tool_calls, "Model should provide some response" );
}
#[ tokio::test ]
async fn test_tool_choice_required()
{
let client = setup_client();
let tool = create_weather_tool();
let response = client.providers()
.chat_completion_with_tools
(
Models::kimi_k2_instruct(),
vec!
[
api_huggingface::components::inference_shared::ChatMessage
{
role : "user".to_string(),
content : "What is the current weather in San Francisco?".to_string(),
tool_calls : None,
tool_call_id : None,
}
],
vec![ tool ],
ChatCompletionOptions { tool_choice : Some( "required".to_string() ), ..Default::default() },
)
.await;
match response
{
Ok( ref resp ) =>
{
assert!( resp.choices[ 0 ].message.tool_calls.is_some(), "Model must call tool when tool_choice is 'required'" );
}
Err( ref e ) =>
{
let err_str = format!( "{e:?}" );
assert!( err_str.contains( "tool_use_failed" ) || err_str.contains( "tool" ),
"Expected tool-related error when model fails to call tool, got : {err_str}" );
}
}
}
#[ tokio::test ]
async fn test_multiple_tools()
{
let client = setup_client();
let weather_tool = create_weather_tool();
let calc_tool = create_calculator_tool();
let response = client.providers()
.chat_completion_with_tools
(
Models::kimi_k2_instruct(),
vec!
[
api_huggingface::components::inference_shared::ChatMessage
{
role : "user".to_string(),
content : "What is 15 multiplied by 3?".to_string(),
tool_calls : None,
tool_call_id : None,
}
],
vec![ weather_tool, calc_tool ],
ChatCompletionOptions::default(), )
.await;
assert!( response.is_ok(), "Request failed : {:?}", response.err() );
let response = response.expect( "[test_multiple_tools] Response should be Ok after is_ok() check - check chat_completion_with_tools() with multiple tools implementation" );
if let Some( ref tool_calls ) = response.choices[ 0 ].message.tool_calls
{
assert!( !tool_calls.is_empty() );
assert_eq!( tool_calls[ 0 ].function.name, "calculate", "Model should choose calculator for math" );
}
}
#[ tokio::test ]
async fn test_function_calling_conversation_flow()
{
let client = setup_client();
let tool = create_weather_tool();
let response1 = client.providers()
.chat_completion_with_tools
(
Models::kimi_k2_instruct(),
vec!
[
api_huggingface::components::inference_shared::ChatMessage
{
role : "user".to_string(),
content : "What's the weather in Tokyo?".to_string(),
tool_calls : None,
tool_call_id : None,
}
],
vec![ tool.clone() ],
ChatCompletionOptions::default(),
)
.await
.expect( "First request failed" );
let tool_calls = response1.choices[ 0 ].message.tool_calls.as_ref()
.expect( "Model should request tool call" );
let tool_call_id = &tool_calls[ 0 ].id;
let response2 = client.providers()
.chat_completion_with_tools
(
Models::kimi_k2_instruct(),
vec!
[
api_huggingface::components::inference_shared::ChatMessage
{
role : "user".to_string(),
content : "What's the weather in Tokyo?".to_string(),
tool_calls : None,
tool_call_id : None,
},
response1.choices[ 0 ].message.clone(),
api_huggingface::components::inference_shared::ChatMessage
{
role : "tool".to_string(),
content : r#"{"temperature": 22, "condition": "sunny", "unit": "celsius"}"#.to_string(),
tool_calls : None,
tool_call_id : Some( tool_call_id.clone() ),
}
],
vec![ tool ],
ChatCompletionOptions::default(),
)
.await
.expect( "Second request failed" );
let has_tool_calls = response2.choices[ 0 ].message.tool_calls.as_ref()
.is_some_and( | tc | !tc.is_empty() );
let has_content = !response2.choices[ 0 ].message.content.is_empty();
assert!( has_content || has_tool_calls, "Model should provide response after tool result" );
}
#[ test ]
fn test_tool_serialization()
{
let tool = create_weather_tool();
let json = serde_json::to_value( &tool ).expect( "Failed to serialize tool" );
assert_eq!( json[ "name" ], "get_current_weather" );
assert_eq!( json[ "description" ], "Get the current weather in a given location" );
assert_eq!( json[ "parameters" ][ "type" ], "object" );
assert!( json[ "parameters" ][ "properties" ].is_object() );
assert!( json[ "parameters" ][ "required" ].is_array() );
}
#[ test ]
fn test_parameter_property_types()
{
let string_prop = ParameterProperty::string( "A string parameter" );
let number_prop = ParameterProperty::number( "A number parameter" );
let boolean_prop = ParameterProperty::boolean( "A boolean parameter" );
let json_string = serde_json::to_value( &string_prop ).expect( "[test_parameter_property_types] Failed to serialize string ParameterProperty to JSON - check serde_json::to_value() and ParameterProperty serialization implementation" );
let json_number = serde_json::to_value( &number_prop ).expect( "[test_parameter_property_types] Failed to serialize number ParameterProperty to JSON - check serde_json::to_value() and ParameterProperty serialization implementation" );
let json_boolean = serde_json::to_value( &boolean_prop ).expect( "[test_parameter_property_types] Failed to serialize boolean ParameterProperty to JSON - check serde_json::to_value() and ParameterProperty serialization implementation" );
assert_eq!( json_string[ "type" ], "string" );
assert_eq!( json_number[ "type" ], "number" );
assert_eq!( json_boolean[ "type" ], "boolean" );
}
}