#![allow(clippy::missing_inline_in_public_items)]
use api_huggingface::
{
Client,
environment::HuggingFaceEnvironmentImpl,
components::
{
input::InferenceParameters,
models::Models,
},
providers::ChatMessage,
secret::Secret,
};
use std::
{
collections::HashMap,
io::{ self, Write as IoWrite },
fs,
time::{ SystemTime, UNIX_EPOCH },
};
use core::fmt::Write;
#[ derive( Debug, Clone ) ]
pub struct ConversationContext
{
pub session_id : String,
pub history : Vec< ( String, String ) >, pub style : ConversationStyle,
pub model : String,
pub parameters : InferenceParameters,
pub started_at : SystemTime,
}
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum ConversationStyle
{
Casual,
Formal,
Creative,
Technical,
Supportive,
}
impl ConversationStyle
{
fn from_str( s : &str ) -> Option< Self >
{
match s.to_lowercase().as_str()
{
"casual" => Some( Self::Casual ),
"formal" => Some( Self::Formal ),
"creative" => Some( Self::Creative ),
"technical" => Some( Self::Technical ),
"supportive" => Some( Self::Supportive ),
_ => None,
}
}
fn description( self ) -> &'static str
{
match self
{
Self::Casual => "Friendly, relaxed conversation style",
Self::Formal => "Professional, precise conversation style",
Self::Creative => "Imaginative, expressive conversation style",
Self::Technical => "Detailed, technical conversation style",
Self::Supportive => "Empathetic, encouraging conversation style",
}
}
}
#[ derive( Debug ) ]
pub struct ChatbotSystem
{
client : Client< HuggingFaceEnvironmentImpl >,
sessions : HashMap< String, ConversationContext >,
current_session : String,
}
impl ChatbotSystem
{
#[ must_use ]
pub fn new( client : Client< HuggingFaceEnvironmentImpl > ) -> Self
{
let current_session = format!( "session_{}", SystemTime::now().duration_since( UNIX_EPOCH ).unwrap().as_secs() );
Self
{
client,
sessions : HashMap::new(),
current_session,
}
}
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 => "codellama/CodeLlama-7b-Instruct-hf".to_string(),
_ => "meta-llama/Meta-Llama-3-8B-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 ),
},
started_at : SystemTime::now(),
};
self.sessions.insert( session_id.to_string(), context );
self.current_session = session_id.to_string();
self.sessions.get( session_id ).unwrap()
}
pub async fn process_input(
&mut self,
session_id : &str,
user_input : &str
) -> Result< String, Box< dyn std::error::Error > >
{
let ( model, style ) = {
let context = self.sessions.get( session_id )
.ok_or( "Session not found" )?;
( context.model.clone(), context.style )
};
let bot_response = if Self::is_pro_model( &model )
{
match self.try_pro_model_response( session_id, user_input, &model, style ).await
{
Ok( response ) => response,
Err( e ) =>
{
println!( "⚠️ Pro model {model} failed : {e}" );
println!( "🔄 Falling back to BART model..." );
if let Some( context ) = self.sessions.get_mut( session_id )
{
context.model = "facebook/bart-large-cnn".to_string();
}
self.try_fallback_model_response( session_id, user_input ).await?
}
}
}
else
{
self.try_fallback_model_response( session_id, user_input ).await?
};
let context = self.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 )
}
async fn try_pro_model_response(
&self,
session_id : &str,
user_input : &str,
model : &str,
style : ConversationStyle,
) -> Result< String, Box< dyn std::error::Error > >
{
let providers = self.client.providers();
let messages = self.build_chat_messages( session_id, user_input, style )?;
let ( max_tokens, temperature, top_p ) = match style
{
ConversationStyle::Creative => ( Some( 200 ), Some( 0.9 ), Some( 0.95 ) ),
ConversationStyle::Formal => ( Some( 150 ), Some( 0.3 ), Some( 0.85 ) ),
ConversationStyle::Technical => ( Some( 250 ), Some( 0.2 ), Some( 0.9 ) ),
ConversationStyle::Supportive => ( Some( 180 ), Some( 0.7 ), Some( 0.9 ) ),
ConversationStyle::Casual => ( Some( 150 ), Some( 0.7 ), Some( 0.9 ) ),
};
let response = if user_input.contains( '=' ) || user_input.contains( '*' ) || user_input.contains( '/' ) || user_input.contains( '+' ) || user_input.contains( '-' )
{
providers.math_completion( model, user_input ).await?
}
else
{
providers.chat_completion( model, messages, max_tokens, temperature, top_p ).await?
};
if let Some( choice ) = response.choices.first()
{
let content = choice.message.content.trim().to_string();
if content.is_empty()
{
Err( "Empty response from Pro model".into() )
}
else
{
Ok( Self::clean_response( &content, user_input ) )
}
}
else
{
Err( "No response choices from Pro model".into() )
}
}
async fn try_fallback_model_response(
&self,
session_id : &str,
user_input : &str,
) -> Result< String, Box< dyn std::error::Error > >
{
let context = self.sessions.get( session_id )
.ok_or( "Session not found" )?;
let prompt = Self::build_contextual_prompt( context, user_input );
let response = self.client
.inference()
.create( &prompt, "facebook/bart-large-cnn" )
.await?;
let bot_response = response.extract_text_or_default( "Sorry, I couldn't generate a response." );
Ok( Self::clean_response( &bot_response, user_input ) )
}
fn build_chat_messages(
&self,
session_id : &str,
user_input : &str,
style : ConversationStyle
) -> Result< Vec< ChatMessage >, Box< dyn std::error::Error > >
{
let context = self.sessions.get( session_id )
.ok_or( "Session not found" )?;
let mut messages = Vec::new();
let system_content = match style
{
ConversationStyle::Casual => "You are a helpful, friendly AI assistant. Be conversational and casual in your responses.",
ConversationStyle::Formal => "You are a professional AI assistant. Respond formally and precisely with accurate information.",
ConversationStyle::Creative => "You are a creative AI assistant. Be imaginative, expressive, and think outside the box.",
ConversationStyle::Technical => "You are a technical AI assistant. Provide detailed, accurate, and technically precise information.",
ConversationStyle::Supportive => "You are a supportive AI assistant. Be empathetic, encouraging, and helpful.",
};
messages.push( ChatMessage
{
role : "system".to_string(),
content : system_content.to_string(),
tool_calls : None,
tool_call_id : None,
} );
for ( user_msg, bot_msg ) in &context.history
{
messages.push( ChatMessage
{
role : "user".to_string(),
content : user_msg.clone(),
tool_calls : None,
tool_call_id : None,
} );
messages.push( ChatMessage
{
role : "assistant".to_string(),
content : bot_msg.clone(),
tool_calls : None,
tool_call_id : None,
} );
}
messages.push( ChatMessage
{
role : "user".to_string(),
content : user_input.to_string(),
tool_calls : None,
tool_call_id : None,
} );
Ok( messages )
}
fn is_pro_model( model : &str ) -> bool
{
model.contains( "meta-llama" ) ||
model.contains( "Llama" ) ||
model.contains( "mistralai" ) ||
model.contains( "Mistral" ) ||
model.contains( "codellama" ) ||
model.contains( "CodeLlama" ) ||
( !model.contains( "bart" ) && !model.contains( "flan" ) && !model.contains( "t5" ) )
}
fn clean_response( response : &str, user_input : &str ) -> String
{
let response = response.trim();
let prefixes_to_remove = [
&format!( "User : {user_input}\nAssistant:" ),
&format!( "User : {user_input}" ),
"Assistant:",
"AI:",
];
let mut cleaned = response.to_string();
for prefix in &prefixes_to_remove
{
if cleaned.starts_with( prefix )
{
cleaned = cleaned[ prefix.len().. ].trim().to_string();
}
}
while cleaned.contains( "\n\n\n" )
{
cleaned = cleaned.replace( "\n\n\n", "\n\n" );
}
cleaned.trim().to_string()
}
fn build_contextual_prompt( context : &ConversationContext, user_input : &str ) -> String
{
if context.model.contains( "bart" )
{
let style_desc = match context.style
{
ConversationStyle::Casual => "friendly and casual manner",
ConversationStyle::Formal => "professional and formal manner",
ConversationStyle::Creative => "creative and expressive manner",
ConversationStyle::Technical => "detailed technical manner",
ConversationStyle::Supportive => "supportive and encouraging manner",
};
format!(
"Question : {user_input}\n\nAnswer : A {style_desc} AI assistant responds with helpful information. \
Summary of the response:"
)
}
else if context.model.contains( "llama" ) || context.model.contains( "Llama" )
{
let system_msg = match context.style
{
ConversationStyle::Casual => "You are a helpful, friendly AI assistant. Be conversational and casual.",
ConversationStyle::Formal => "You are a professional AI assistant. Respond formally and precisely.",
ConversationStyle::Creative => "You are a creative AI assistant. Be imaginative and expressive.",
ConversationStyle::Technical => "You are a technical AI assistant. Provide detailed, accurate information.",
ConversationStyle::Supportive => "You are a supportive AI assistant. Be empathetic and encouraging.",
};
let mut prompt = format!( "< s >[INST] << SYS > >\n{system_msg}\n<</SYS > >\n\n" );
for ( user_msg, bot_msg ) in &context.history
{
write!( &mut prompt, "User : {user_msg} [/INST] Assistant : {bot_msg} </s >< s >[INST] " ).unwrap();
}
write!( &mut prompt, "User : {user_input} [/INST] Assistant : " ).unwrap();
prompt
}
else
{
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" ).unwrap();
}
write!( &mut prompt, "User : {user_input}\nAssistant : " ).unwrap();
prompt
}
}
#[ must_use ]
pub fn get_current_context( &self ) -> Option< &ConversationContext >
{
self.sessions.get( &self.current_session )
}
pub fn change_style( &mut self, style : ConversationStyle ) -> Result< (), &'static str >
{
let session_id = self.current_session.clone();
let context = self.sessions.get_mut( &session_id )
.ok_or( "No active session" )?;
context.style = style;
context.model = match style
{
ConversationStyle::Technical => Models::mistral_7b_instruct().to_string(),
_ => Models::llama_3_3_70b_instruct().to_string(),
};
context.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 ),
};
Ok( () )
}
pub fn change_model( &mut self, model : &str ) -> Result< (), &'static str >
{
let session_id = self.current_session.clone();
let context = self.sessions.get_mut( &session_id )
.ok_or( "No active session" )?;
context.model = model.to_string();
Ok( () )
}
pub fn clear_history( &mut self ) -> Result< (), &'static str >
{
let session_id = self.current_session.clone();
let context = self.sessions.get_mut( &session_id )
.ok_or( "No active session" )?;
context.history.clear();
Ok( () )
}
pub fn export_conversation( &self ) -> Result< String, Box< dyn std::error::Error > >
{
let context = self.get_current_context()
.ok_or( "No active session" )?;
let timestamp = SystemTime::now()
.duration_since( UNIX_EPOCH )?
.as_secs();
let filename = format!( "conversation_{}_{}.txt", context.session_id, timestamp );
let mut export_content = format!(
"Chatbot Conversation Export\n\
Session ID: {}\n\
Style : {:?}\n\
Model : {}\n\
Started : {:?}\n\
Exported : {:?}\n\
\n\
=== Conversation History ===\n\n",
context.session_id,
context.style,
context.model,
context.started_at,
SystemTime::now()
);
for ( i, ( user_msg, bot_msg ) ) in context.history.iter().enumerate()
{
write!( &mut export_content, "Turn {}:\nUser : {}\nAssistant : {}\n\n", i + 1, user_msg, bot_msg )?;
}
fs::write( &filename, export_content )?;
Ok( filename )
}
}
#[ derive( Debug ) ]
pub struct InteractiveChatbot
{
system : ChatbotSystem,
}
impl InteractiveChatbot
{
#[ must_use ]
pub fn new( client : Client< HuggingFaceEnvironmentImpl > ) -> Self
{
Self
{
system : ChatbotSystem::new( client ),
}
}
pub async fn start( &mut self ) -> Result< (), Box< dyn std::error::Error > >
{
println!( "🤖 AI Chatbot - HuggingFace Edition (Pro Models Support)" );
println!( "========================================================" );
println!( "🎯 Automatically uses Pro plan models (Llama-3, Mistral, CodeLlama) when available" );
println!( "🔄 Falls back to BART model for free tier users" );
println!( "📚 Type '/help' for commands or start chatting!" );
println!();
self.system.start_conversation( &self.system.current_session.clone(), ConversationStyle::Casual );
if let Some( context ) = self.system.get_current_context()
{
println!( "Session : {} | Style : {:?} | Model : {}",
context.session_id, context.style, context.model );
}
println!();
let stdin = io::stdin();
let mut stdout = io::stdout();
loop
{
print!( "You : " );
stdout.flush()?;
let mut input = String::new();
let bytes_read = stdin.read_line( &mut input )?;
if bytes_read == 0
{
println!("Goodbye!");
break Ok(());
}
let input = input.trim();
if input.is_empty()
{
continue;
}
if input.starts_with( '/' )
{
match self.handle_command( input )
{
Ok( Some( response ) ) => println!( "🤖 {response}" ),
Ok( None ) => {}, Err( e ) => println!( "❌ Error : {e}" ),
}
continue;
}
print!( "🤖 " );
stdout.flush()?;
match self.system.process_input( &self.system.current_session.clone(), input ).await
{
Ok( response ) => println!( "{response}" ),
Err( e ) => println!( "❌ Sorry, I encountered an error : {e}" ),
}
println!();
}
}
fn handle_command( &mut self, command : &str ) -> Result< Option< String >, Box< dyn std::error::Error > >
{
let parts : Vec< &str > = command[ 1.. ].split_whitespace().collect();
if parts.is_empty()
{
return Ok( Some( "Invalid command. Type '/help' for available commands.".to_string() ) );
}
match parts[ 0 ].to_lowercase().as_str()
{
"help" => Ok( Some( Self::show_help() ) ),
"quit" | "exit" =>
{
println!( "👋 Goodbye!" );
std::process::exit( 0 );
},
"style" =>
{
if parts.len() < 2
{
return Ok( Some( "Usage : /style < casual|formal|creative|technical|supportive >".to_string() ) );
}
match ConversationStyle::from_str( parts[ 1 ] )
{
Some( style ) =>
{
self.system.change_style( style )?;
Ok( Some( format!( "Changed to {} style : {}", parts[ 1 ], style.description() ) ) )
},
None => Ok( Some( "Invalid style. Use : casual, formal, creative, technical, or supportive".to_string() ) ),
}
},
"model" =>
{
if parts.len() < 2
{
return Ok( Some( "Usage : /model < model-name >".to_string() ) );
}
let model = parts[ 1.. ].join( " " );
self.system.change_model( &model )?;
Ok( Some( format!( "Changed to model : {model}" ) ) )
},
"history" =>
{
match self.system.get_current_context()
{
Some( context ) if !context.history.is_empty() =>
{
let mut history = String::from( "Conversation History:\n" );
for ( i, ( user_msg, bot_msg ) ) in context.history.iter().enumerate()
{
write!( &mut history, "\n{}. You : {}\n Bot : {}\n", i + 1, user_msg, bot_msg )?;
}
Ok( Some( history ) )
},
Some( _ ) => Ok( Some( "No conversation history yet.".to_string() ) ),
None => Ok( Some( "No active session.".to_string() ) ),
}
},
"clear" =>
{
self.system.clear_history()?;
Ok( Some( "Conversation history cleared.".to_string() ) )
},
"export" =>
{
match self.system.export_conversation()
{
Ok( filename ) => Ok( Some( format!( "Conversation exported to : {filename}" ) ) ),
Err( e ) => Ok( Some( format!( "Export failed : {e}" ) ) ),
}
},
"status" =>
{
match self.system.get_current_context()
{
Some( context ) =>
{
let duration = SystemTime::now().duration_since( context.started_at )?;
Ok( Some( format!(
"Session : {}\nStyle : {:?} ({})\nModel : {}\nHistory : {} turns\nDuration : {}m {}s",
context.session_id,
context.style,
context.style.description(),
context.model,
context.history.len(),
duration.as_secs() / 60,
duration.as_secs() % 60
) ) )
},
None => Ok( Some( "No active session.".to_string() ) ),
}
},
_ => Ok( Some( format!( "Unknown command : /{}\nType '/help' for available commands.", parts[ 0 ] ) ) ),
}
}
fn show_help() -> String
{
r"Available Commands:
===================
/style < type > - Change conversation style
Options : casual, formal, creative, technical, supportive
/model < name > - Change AI model (Pro : Llama-3, Mistral, CodeLlama)
/history - Show conversation history
/clear - Clear conversation history
/export - Export conversation to file
/status - Show session information
/help - Show this help message
/quit or /exit - Exit the chatbot
Pro Model Features:
==================
🎯 Intelligent Model Selection:
• Pro plan users get access to advanced models (Llama-3, Mistral, CodeLlama)
• Automatic fallback to BART for free tier users
• Math questions use specialized math completion
🤖 Advanced Capabilities:
• Proper conversational AI (not summarization)
• Mathematical reasoning and calculations
• Context-aware responses
• Multi-turn conversation memory
Conversation Styles:
===================
• casual - Friendly, relaxed conversation (Llama-3-8B)
• formal - Professional, precise responses (Llama-3-8B)
• creative - Imaginative, expressive replies (Llama-3-8B)
• technical - Detailed, technical information (CodeLlama-7b)
• supportive - Empathetic, encouraging responses (Llama-3-8B)
Simply type your message to start chatting!
Try math questions like : 'x=13, what is x*3?' for better results with Pro models!".to_string()
}
}
#[ tokio::main ]
async fn main() -> Result< (), Box< dyn std::error::Error > >
{
let api_key = std::env::var("HUGGINGFACE_API_KEY")
.or_else(|_| {
use workspace_tools as workspace;
let workspace = workspace::workspace()
.map_err(|_| std::env::VarError::NotPresent)?;
let secrets = workspace.load_secrets_from_file("-secrets.sh")
.map_err(|_| std::env::VarError::NotPresent)?;
secrets.get("HUGGINGFACE_API_KEY")
.cloned()
.ok_or(std::env::VarError::NotPresent)
})
.map_err(|_| "HUGGINGFACE_API_KEY not found in environment or workspace secrets (./secret/-secrets.sh or -secrets.sh)")?;
let secret_key = Secret::new( api_key );
let environment = HuggingFaceEnvironmentImpl::build( secret_key, None )?;
let client = Client::build( environment )?;
let mut chatbot = InteractiveChatbot::new( client );
chatbot.start().await?;
Ok( () )
}