#![allow(clippy::missing_inline_in_public_items)]
mod inc;
use api_huggingface::
{
Client,
environment::HuggingFaceEnvironmentImpl,
components::
{
input::InferenceParameters,
models::Models,
},
};
#[ cfg( feature = "integration" ) ]
use api_huggingface::secret::Secret;
use std::collections::HashMap;
use core::fmt::Write;
#[ allow( missing_docs ) ]
#[ derive( Debug, Clone ) ]
pub struct ConversationContext
{
pub session_id : String,
pub history : Vec< ( String, String ) >, pub style : ConversationStyle,
pub model : String,
pub parameters : InferenceParameters,
}
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum ConversationStyle
{
Casual,
Formal,
Creative,
Technical,
Supportive,
}
#[ derive( Debug ) ]
pub struct ChatbotSystem
{
client : Client< HuggingFaceEnvironmentImpl >,
active_sessions : HashMap< String, ConversationContext >,
}
impl ChatbotSystem
{
#[ must_use ]
pub fn new( client : Client< HuggingFaceEnvironmentImpl > ) -> Self
{
Self
{
client,
active_sessions : HashMap::new(),
}
}
pub fn start_conversation( &mut self, session_id : &str, style : ConversationStyle ) -> &ConversationContext
{
let context = ConversationContext
{
session_id : session_id.to_string(),
history : Vec::new(),
style,
model : match style
{
ConversationStyle::Technical => Models::mistral_7b_instruct().to_string(),
_ => Models::llama_3_3_70b_instruct().to_string(),
},
parameters : match style
{
ConversationStyle::Creative => InferenceParameters::new()
.with_temperature( 0.9 )
.with_max_new_tokens( 150 ),
ConversationStyle::Formal => InferenceParameters::new()
.with_temperature( 0.3 )
.with_max_new_tokens( 200 ),
ConversationStyle::Technical => InferenceParameters::new()
.with_temperature( 0.5 )
.with_max_new_tokens( 300 ),
_ => InferenceParameters::new()
.with_temperature( 0.7 )
.with_max_new_tokens( 150 ),
},
};
self.active_sessions.insert( session_id.to_string(), context );
self.active_sessions.get( session_id ).expect( "[start_conversation] Session should exist in active_sessions immediately after insert - check HashMap::insert() and HashMap::get() implementation" )
}
pub async fn process_input(
&mut self,
session_id : &str,
user_input : &str
) -> Result< String, Box< dyn std::error::Error > >
{
let prompt = {
let context = self.active_sessions.get( session_id )
.ok_or( "Session not found" )?;
Self::build_contextual_prompt( context, user_input )
};
let ( model, parameters ) = {
let context = self.active_sessions.get( session_id )
.ok_or( "Session not found" )?;
( context.model.clone(), context.parameters.clone() )
};
let response = self.client
.inference()
.create_with_parameters( &prompt, &model, parameters )
.await?;
let bot_response = response.extract_text_or_default( "Sorry, I couldn't generate a response." );
let context = self.active_sessions.get_mut( session_id )
.ok_or( "Session not found" )?;
context.history.push( ( user_input.to_string(), bot_response.clone() ) );
if context.history.len() > 5
{
context.history.remove( 0 );
}
Ok( bot_response )
}
fn build_contextual_prompt( context : &ConversationContext, user_input : &str ) -> String
{
let style_prefix = match context.style
{
ConversationStyle::Casual => "You are a friendly, casual AI assistant. Respond in a relaxed, conversational way.",
ConversationStyle::Formal => "You are a professional AI assistant. Respond formally and precisely.",
ConversationStyle::Creative => "You are a creative AI assistant. Be imaginative and expressive in your responses.",
ConversationStyle::Technical => "You are a technical AI assistant. Provide detailed, accurate technical information.",
ConversationStyle::Supportive => "You are a supportive AI assistant. Be empathetic and encouraging.",
};
let mut prompt = format!( "{style_prefix}\n\n" );
for ( user_msg, bot_msg ) in &context.history
{
write!( &mut prompt, "User : {user_msg}\nAssistant : {bot_msg}\n\n" ).expect( "[build_contextual_prompt] Failed to write conversation history to String - String write! should never fail" );
}
write!( &mut prompt, "User : {user_input}\nAssistant : " ).expect( "[build_contextual_prompt] Failed to write user input to String - String write! should never fail" );
prompt
}
#[ must_use ]
pub fn get_context( &self, session_id : &str ) -> Option< &ConversationContext >
{
self.active_sessions.get( session_id )
}
pub fn end_conversation( &mut self, session_id : &str ) -> Option< ConversationContext >
{
self.active_sessions.remove( session_id )
}
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ cfg( feature = "integration" ) ]
fn create_integration_client() -> Client< HuggingFaceEnvironmentImpl >
{
let api_key = crate::inc::get_api_key_for_integration();
let secret = Secret::new( api_key );
let env = HuggingFaceEnvironmentImpl::build( secret, None )
.expect( "Failed to build environment" );
Client::build( env ).expect( "Failed to create client" )
}
#[ test ]
fn test_conversation_context_creation()
{
let context = ConversationContext
{
session_id : "test-session".to_string(),
history : Vec::new(),
style : ConversationStyle::Casual,
model : Models::llama_3_3_70b_instruct().to_string(),
parameters : InferenceParameters::default(),
};
assert_eq!( context.session_id, "test-session" );
assert_eq!( context.style, ConversationStyle::Casual );
assert!( context.history.is_empty() );
}
#[ test ]
fn test_conversation_style_parameters()
{
let creative_params = match ConversationStyle::Creative
{
ConversationStyle::Creative => InferenceParameters::new()
.with_temperature( 0.9 )
.with_max_new_tokens( 150 ),
_ => panic!( "Unexpected style" ),
};
assert_eq!( creative_params.temperature, Some( 0.9 ) );
assert_eq!( creative_params.max_new_tokens, Some( 150 ) );
let formal_params = match ConversationStyle::Formal
{
ConversationStyle::Formal => InferenceParameters::new()
.with_temperature( 0.3 )
.with_max_new_tokens( 200 ),
_ => panic!( "Unexpected style" ),
};
assert_eq!( formal_params.temperature, Some( 0.3 ) );
assert_eq!( formal_params.max_new_tokens, Some( 200 ) );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_chatbot_system_creation()
{
let client = create_integration_client();
let chatbot = ChatbotSystem::new( client );
assert!( chatbot.active_sessions.is_empty() );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_conversation_session_management()
{
let client = create_integration_client();
let mut chatbot = ChatbotSystem::new( client );
let session_id = "test-session-123";
let context = chatbot.start_conversation( session_id, ConversationStyle::Casual );
assert_eq!( context.session_id, session_id );
assert_eq!( context.style, ConversationStyle::Casual );
assert!( context.history.is_empty() );
assert!( chatbot.get_context( session_id ).is_some() );
let ended_context = chatbot.end_conversation( session_id );
assert!( ended_context.is_some() );
assert!( chatbot.get_context( session_id ).is_none() );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_contextual_prompt_building()
{
let client = create_integration_client();
let _chatbot = ChatbotSystem::new( client );
let context = ConversationContext
{
session_id : "test".to_string(),
history : vec!
[
( "Hello".to_string(), "Hi there!".to_string() ),
( "How are you?".to_string(), "I'm doing well, thank you!".to_string() ),
],
style : ConversationStyle::Casual,
model : Models::llama_3_3_70b_instruct().to_string(),
parameters : InferenceParameters::default(),
};
let prompt = ChatbotSystem::build_contextual_prompt( &context, "What's the weather like?" );
assert!( prompt.contains( "friendly, casual AI assistant" ) );
assert!( prompt.contains( "User : Hello" ) );
assert!( prompt.contains( "Assistant : Hi there!" ) );
assert!( prompt.contains( "User : How are you?" ) );
assert!( prompt.contains( "User : What's the weather like?" ) );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_different_conversation_styles()
{
let client = create_integration_client();
let _chatbot = ChatbotSystem::new( client );
let casual_context = ConversationContext
{
session_id : "casual".to_string(),
history : Vec::new(),
style : ConversationStyle::Casual,
model : Models::llama_3_3_70b_instruct().to_string(),
parameters : InferenceParameters::default(),
};
let formal_context = ConversationContext
{
session_id : "formal".to_string(),
history : Vec::new(),
style : ConversationStyle::Formal,
model : Models::llama_3_3_70b_instruct().to_string(),
parameters : InferenceParameters::default(),
};
let casual_prompt = ChatbotSystem::build_contextual_prompt( &casual_context, "Hello" );
let formal_prompt = ChatbotSystem::build_contextual_prompt( &formal_context, "Hello" );
assert!( casual_prompt.contains( "friendly, casual" ) );
assert!( formal_prompt.contains( "professional" ) );
assert_ne!( casual_prompt, formal_prompt );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_model_selection_by_style()
{
let client = create_integration_client();
let mut chatbot = ChatbotSystem::new( client );
let creative_context = chatbot.start_conversation(
"creative-test",
ConversationStyle::Creative
);
assert_eq!( creative_context.model, Models::llama_3_3_70b_instruct() );
let technical_context = chatbot.start_conversation(
"technical-test",
ConversationStyle::Technical
);
assert_eq!( technical_context.model, Models::mistral_7b_instruct() );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_error_handling_invalid_session()
{
let client = create_integration_client();
let mut chatbot = ChatbotSystem::new( client );
let result = chatbot.process_input( "non-existent", "Hello" ).await;
assert!( result.is_err() );
assert!( result.unwrap_err().to_string().contains( "Session not found" ) );
}
#[ tokio::test ]
async fn test_conversation_style_enum_completeness()
{
let styles = vec!
[
ConversationStyle::Casual,
ConversationStyle::Formal,
ConversationStyle::Creative,
ConversationStyle::Technical,
ConversationStyle::Supportive,
];
for style in styles
{
let cloned = style;
assert_eq!( format!( "{style:?}" ), format!( "{cloned:?}" ) );
}
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_parameter_optimization_per_style()
{
let client = create_integration_client();
let mut chatbot = ChatbotSystem::new( client );
let creative_temp;
let creative_tokens;
{
let creative = chatbot.start_conversation( "c1", ConversationStyle::Creative );
creative_temp = creative.parameters.temperature.expect( "[test_conversation_personality_styles] Creative InferenceParameters.temperature should be Some - check start_conversation() Creative style parameters" );
creative_tokens = creative.parameters.max_new_tokens.expect( "[test_conversation_personality_styles] Creative InferenceParameters.max_new_tokens should be Some - check start_conversation() Creative style parameters" );
}
let formal_temp;
{
let formal = chatbot.start_conversation( "f1", ConversationStyle::Formal );
formal_temp = formal.parameters.temperature.expect( "[test_conversation_personality_styles] Formal InferenceParameters.temperature should be Some - check start_conversation() Formal style parameters" );
}
let technical_tokens;
{
let technical = chatbot.start_conversation( "t1", ConversationStyle::Technical );
technical_tokens = technical.parameters.max_new_tokens.expect( "[test_conversation_personality_styles] Technical InferenceParameters.max_new_tokens should be Some - check start_conversation() Technical style parameters" );
}
assert!( creative_temp > formal_temp );
assert!( technical_tokens > creative_tokens );
assert!( ( formal_temp - 0.3 ).abs() < f32::EPSILON );
}
}