#![ allow( clippy::all, clippy::pedantic ) ]
#![ allow( missing_docs ) ]
#![ allow( clippy::missing_errors_doc ) ]
#![ allow( clippy::must_use_candidate ) ]
#![ allow( clippy::unwrap_or_default ) ]
#![ allow( clippy::redundant_closure_for_method_calls ) ]
#![ allow( clippy::uninlined_format_args ) ]
#![ allow( clippy::unnecessary_wraps ) ]
#![ allow( clippy::manual_string_new ) ]
#![ allow( clippy::useless_vec ) ]
#![ allow( clippy::get_first ) ]
#![ allow( clippy::trivially_copy_pass_by_ref ) ]
#![ allow( clippy::manual_clamp ) ]
#![ allow( clippy::single_char_pattern ) ]
#![ allow( clippy::manual_range_contains ) ]
#![ allow( clippy::len_zero ) ]
use std::collections::HashMap;
use serde::{ Deserialize, Serialize };
use api_huggingface::*;
use api_huggingface::components::input::InferenceParameters;
use api_huggingface::environment::HuggingFaceEnvironmentImpl;
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize ) ]
pub enum QuestionType
{
ContextBased,
GeneralKnowledge,
Factual,
Opinion,
YesNo,
MultipleChoice,
}
impl QuestionType
{
pub fn preferred_model( &self ) -> &'static str
{
"meta-llama/Llama-3.3-70B-Instruct"
}
pub fn confidence_threshold( &self ) -> f32
{
match self
{
Self::ContextBased => 0.8, Self::GeneralKnowledge => 0.7, Self::Factual => 0.9, Self::Opinion => 0.5, Self::YesNo => 0.8, Self::MultipleChoice => 0.8, }
}
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize ) ]
pub enum DifficultyLevel
{
Easy,
Medium,
Hard,
Expert,
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct Question
{
pub id : String,
pub text : String,
pub question_type : QuestionType,
pub difficulty : DifficultyLevel,
pub context : Option< String >,
pub expected_answer : Option< String >,
pub keywords : Vec< String >,
pub metadata : HashMap< String, String >,
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct Answer
{
pub text : String,
pub confidence : f32,
pub sources : Vec< String >,
pub reasoning : Option< String >,
pub question_id : String,
pub response_time_ms : u64,
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct KnowledgeSource
{
pub id : String,
pub title : String,
pub content : String,
pub source_type : SourceType,
pub reliability_score : f32,
pub last_updated : String,
pub metadata : HashMap< String, String >,
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize ) ]
pub enum SourceType
{
Document,
WebPage,
Database,
Manual,
Encyclopedia,
FAQ,
}
impl SourceType
{
pub fn reliability_multiplier( &self ) -> f32
{
match self
{
Self::Encyclopedia => 0.9,
Self::Manual => 0.85,
Self::Database => 0.8,
Self::Document => 0.75,
Self::WebPage => 0.6,
Self::FAQ => 0.7,
}
}
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct ConversationTurn
{
pub turn_id : usize,
pub question : Question,
pub answer : Option< Answer >,
pub context_from_previous : Vec< String >,
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct QASession
{
pub session_id : String,
pub turns : Vec< ConversationTurn >,
pub active_context : HashMap< String, String >,
pub user_preferences : UserPreferences,
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct UserPreferences
{
pub preferred_length : AnswerLength,
pub include_sources : bool,
pub include_reasoning : bool,
pub confidence_threshold : f32,
pub preferred_topics : Vec< String >,
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize ) ]
pub enum AnswerLength
{
Brief, Standard, Detailed, Comprehensive, }
impl AnswerLength
{
pub fn word_count_range( &self ) -> ( usize, usize )
{
match self
{
Self::Brief => ( 10, 30 ),
Self::Standard => ( 30, 80 ),
Self::Detailed => ( 80, 150 ),
Self::Comprehensive => ( 150, 300 ),
}
}
pub fn max_tokens( &self ) -> u32
{
match self
{
Self::Brief => 50u32,
Self::Standard => 120u32,
Self::Detailed => 200u32,
Self::Comprehensive => 400u32,
}
}
}
#[ derive( Debug ) ]
pub struct QASystem
{
pub client : Client< HuggingFaceEnvironmentImpl >,
pub knowledge_sources : HashMap< String, KnowledgeSource >,
pub active_sessions : HashMap< String, QASession >,
pub default_model : String,
pub accuracy_cache : HashMap< String, f32 >,
}
impl QASystem
{
pub fn new( client : Client< HuggingFaceEnvironmentImpl > ) -> Self
{
Self
{
client,
knowledge_sources : HashMap::new(),
active_sessions : HashMap::new(),
default_model : "meta-llama/Llama-3.3-70B-Instruct".to_string(),
accuracy_cache : HashMap::new(),
}
}
pub async fn answer_question( &self, question : &Question ) -> Result< Answer, Box< dyn std::error::Error > >
{
let model = question.question_type.preferred_model();
let prompt = Self::build_qa_prompt( question );
let params = InferenceParameters::new()
.with_temperature( 0.3 ) .with_max_new_tokens( Self::get_max_tokens_for_question( question ) )
.with_top_p( 0.8 );
let start_time = std::time::Instant::now();
let result = self.client.inference().create_with_parameters( &prompt, model, params ).await?;
let response_time = start_time.elapsed().as_millis() as u64;
let generated_text = result.extract_text_or_default( "" );
let answer = Self::parse_answer( &generated_text, question, response_time )?;
Ok( answer )
}
pub fn start_session( &mut self, session_id : String, preferences : UserPreferences ) -> &QASession
{
let session = QASession
{
session_id : session_id.clone(),
turns : Vec::new(),
active_context : HashMap::new(),
user_preferences : preferences,
};
self.active_sessions.insert( session_id.clone(), session );
self.active_sessions.get( &session_id ).expect( "[start_session] Session should exist in active_sessions immediately after insert - check HashMap::insert() and HashMap::get() implementation" )
}
pub async fn answer_in_session( &mut self, session_id : &str, question : Question ) -> Result< Answer, Box< dyn std::error::Error > >
{
let session_context = if let Some( session ) = self.active_sessions.get( session_id )
{
self.build_session_context( session )
} else {
String::new()
};
let mut context_question = question.clone();
if !session_context.is_empty()
{
context_question.context = Some( format!( "{}\n\nPrevious context : {}",
context_question.context.unwrap_or_default(), session_context ) );
}
let answer = self.answer_question( &context_question ).await?;
if let Some( session ) = self.active_sessions.get_mut( session_id )
{
let turn = ConversationTurn
{
turn_id : session.turns.len(),
question : context_question,
answer : Some( answer.clone() ),
context_from_previous : Vec::new(), };
session.turns.push( turn );
session.active_context.insert( format!( "turn_{}", session.turns.len() - 1 ), answer.text.clone() );
}
Ok( answer )
}
pub fn add_knowledge_source( &mut self, source : KnowledgeSource )
{
self.knowledge_sources.insert( source.id.clone(), source );
}
pub fn search_knowledge_sources( &self, query : &str, max_results : usize ) -> Vec< &KnowledgeSource >
{
let mut scored_sources : Vec< ( &KnowledgeSource, f32 ) > = self.knowledge_sources
.values()
.map( | source | {
let relevance = Self::calculate_relevance_score( query, source );
( source, relevance )
} )
.collect();
scored_sources.sort_by( | a, b | b.1.partial_cmp( &a.1 ).unwrap_or( core::cmp::Ordering::Equal ) );
scored_sources.into_iter().take( max_results ).map( | ( source, _ ) | source ).collect()
}
pub fn evaluate_accuracy( &self, generated_answer : &str, expected_answer : &str ) -> f32
{
let generated_lower = generated_answer.to_lowercase();
let expected_lower = expected_answer.to_lowercase();
let generated_words = generated_lower.split_whitespace().collect::< Vec< _ > >();
let expected_words = expected_lower.split_whitespace().collect::< Vec< _ > >();
let common_words = generated_words.iter()
.filter( | word | expected_words.contains( word ) )
.count();
if expected_words.is_empty()
{
return 0.0;
}
common_words as f32 / expected_words.len() as f32
}
pub fn get_system_stats( &self ) -> QASystemStats
{
let total_questions_answered = self.active_sessions.values()
.map( | session | session.turns.len() )
.sum();
let avg_confidence = self.active_sessions.values()
.flat_map( | session | session.turns.iter() )
.filter_map( | turn | turn.answer.as_ref().map( | a | a.confidence ) )
.fold( ( 0.0, 0 ), | ( sum, count ), conf | ( sum + conf, count + 1 ) );
let average_confidence = if avg_confidence.1 > 0
{
avg_confidence.0 / avg_confidence.1 as f32
} else {
0.0
};
QASystemStats
{
total_knowledge_sources : self.knowledge_sources.len(),
active_sessions : self.active_sessions.len(),
total_questions_answered,
average_confidence,
cached_accuracies : self.accuracy_cache.len(),
}
}
fn build_qa_prompt( question : &Question ) -> String
{
let mut prompt = String::new();
if let Some( context ) = &question.context
{
prompt.push_str( &format!( "Context : {}\n\n", context ) );
}
prompt.push_str( &format!( "Question : {}\n", question.text ) );
match question.question_type
{
QuestionType::ContextBased => prompt.push_str( "Answer based on the provided context:" ),
QuestionType::GeneralKnowledge => prompt.push_str( "Answer based on general knowledge:" ),
QuestionType::Factual => prompt.push_str( "Provide a factual answer:" ),
QuestionType::Opinion => prompt.push_str( "Provide a balanced perspective:" ),
QuestionType::YesNo => prompt.push_str( "Answer with Yes or No and explain briefly:" ),
QuestionType::MultipleChoice => prompt.push_str( "Choose the best answer and explain:" ),
}
prompt
}
fn get_max_tokens_for_question( question : &Question ) -> u32
{
match question.difficulty
{
DifficultyLevel::Easy => 100u32,
DifficultyLevel::Medium => 150u32,
DifficultyLevel::Hard => 200u32,
DifficultyLevel::Expert => 300u32,
}
}
fn parse_answer( text : &str, question : &Question, response_time : u64 ) -> Result< Answer, Box< dyn std::error::Error > >
{
let cleaned_text = text.trim().to_string();
let confidence = Self::calculate_confidence_score( &cleaned_text, question );
Ok( Answer
{
text : cleaned_text,
confidence,
sources : Vec::new(), reasoning : None, question_id : question.id.clone(),
response_time_ms : response_time,
} )
}
fn calculate_confidence_score( answer : &str, question : &Question ) -> f32
{
let mut confidence : f32 = 0.5;
if !answer.is_empty()
{
confidence += 0.2;
}
let word_count = answer.split_whitespace().count();
if word_count >= 5 && word_count <= 100
{
confidence += 0.2;
}
match question.question_type
{
QuestionType::YesNo => {
if answer.to_lowercase().contains( "yes" ) || answer.to_lowercase().contains( "no" )
{
confidence += 0.1;
}
},
QuestionType::Factual => {
if answer.contains( "." ) { confidence += 0.1;
}
},
_ => {}
}
confidence.min( 1.0 )
}
fn build_session_context( &self, session : &QASession ) -> String
{
let recent_turns = session.turns.iter().rev().take( 3 ).rev();
let context_parts : Vec< String > = recent_turns
.filter_map( | turn | turn.answer.as_ref().map( | a | format!( "Q: {} A: {}", turn.question.text, a.text ) ) )
.collect();
context_parts.join( "\n" )
}
#[ allow( dead_code ) ]
fn extract_context_from_previous_turns( &self, turns : &[ ConversationTurn ] ) -> Vec< String >
{
turns.iter().rev().take( 2 )
.filter_map( | turn | turn.answer.as_ref().map( | a | a.text.clone() ) )
.collect()
}
fn calculate_relevance_score( query : &str, source : &KnowledgeSource ) -> f32
{
let query_lower = query.to_lowercase();
let query_words = query_lower.split_whitespace().collect::< Vec< _ > >();
let source_text = format!( "{} {}", source.title.to_lowercase(), source.content.to_lowercase() );
let matches = query_words.iter()
.filter( | word | source_text.contains( *word ) )
.count();
let base_score = if query_words.is_empty() { 0.0 } else { matches as f32 / query_words.len() as f32 };
base_score * source.source_type.reliability_multiplier() * source.reliability_score
}
}
#[ derive( Debug, Clone, Serialize, Deserialize ) ]
pub struct QASystemStats
{
pub total_knowledge_sources : usize,
pub active_sessions : usize,
pub total_questions_answered : usize,
pub average_confidence : f32,
pub cached_accuracies : usize,
}