use super::parser_trait::*;
use crate::memory::query_parser as legacy;
use chrono::{DateTime, Datelike, NaiveDate, Utc};
pub struct RuleBasedParser {
_private: (),
}
impl RuleBasedParser {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for RuleBasedParser {
fn default() -> Self {
Self::new()
}
}
impl QueryParser for RuleBasedParser {
fn parse(&self, query: &str, context_date: Option<DateTime<Utc>>) -> ParsedQuery {
let analysis = legacy::analyze_query(query);
let temporal_intent = legacy::detect_temporal_intent(query);
let has_temporal = !matches!(temporal_intent, legacy::TemporalIntent::None);
let temporal_refs = legacy::extract_temporal_refs(query);
let relative_refs = extract_relative_refs(query, context_date);
let resolved_dates = if context_date.is_some() {
relative_refs.iter().filter_map(|r| r.resolved).collect()
} else {
Vec::new()
};
let absolute_dates: Vec<NaiveDate> = temporal_refs.refs.iter().map(|r| r.date).collect();
let entities: Vec<Entity> = analysis
.focal_entities
.iter()
.map(|e| Entity {
text: e.text.clone(),
stem: e.stem.clone(),
entity_type: detect_entity_type(&e.text),
ic_weight: e.ic_weight,
negated: e.negated,
})
.collect();
let events: Vec<Event> = analysis
.relational_context
.iter()
.map(|r| Event {
text: r.text.clone(),
stem: r.stem.clone(),
ic_weight: r.ic_weight,
})
.collect();
let modifiers: Vec<String> = analysis
.discriminative_modifiers
.iter()
.map(|m| m.text.clone())
.collect();
let (is_attribute_query, attribute) = match legacy::detect_attribute_query(query) {
Some(aq) => (
true,
Some(AttributeQuery {
entity: aq.entity.clone(),
attribute: aq.attribute.clone(),
synonyms: aq.attribute_synonyms.clone(),
}),
),
None => (false, None),
};
let ic_weights = analysis.to_ic_weights();
ParsedQuery {
original: query.to_string(),
entities,
events,
modifiers,
temporal: TemporalInfo {
has_temporal_intent: has_temporal,
intent: convert_temporal_intent(temporal_intent),
relative_refs,
resolved_dates,
absolute_dates,
},
is_attribute_query,
attribute,
compounds: analysis.compound_nouns.clone(),
ic_weights,
confidence: 0.85, }
}
fn name(&self) -> &'static str {
"RuleBasedParser"
}
}
fn convert_temporal_intent(intent: legacy::TemporalIntent) -> TemporalIntent {
match intent {
legacy::TemporalIntent::WhenQuestion => TemporalIntent::WhenQuestion,
legacy::TemporalIntent::SpecificTime => TemporalIntent::SpecificTime,
legacy::TemporalIntent::Ordering => TemporalIntent::Ordering,
legacy::TemporalIntent::Duration => TemporalIntent::Duration,
legacy::TemporalIntent::None => TemporalIntent::None,
}
}
fn detect_entity_type(text: &str) -> EntityType {
let text_lower = text.to_lowercase();
if text
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
&& !text.chars().all(|c| c.is_uppercase())
{
let first_word = text.split_whitespace().next().unwrap_or("");
if is_likely_person_name(first_word) {
return EntityType::Person;
}
}
if [
"morning",
"evening",
"afternoon",
"night",
"day",
"week",
"month",
"year",
]
.iter()
.any(|t| text_lower.contains(t))
{
return EntityType::Time;
}
if [
"meeting", "party", "wedding", "concert", "race", "trip", "vacation",
]
.iter()
.any(|e| text_lower.contains(e))
{
return EntityType::Event;
}
EntityType::Unknown
}
fn is_likely_person_name(word: &str) -> bool {
if word.len() < 2 || word.len() > 20 {
return false;
}
let first_char = word.chars().next().unwrap_or(' ');
if !first_char.is_uppercase() {
return false;
}
let non_names = [
"The",
"This",
"That",
"What",
"When",
"Where",
"Who",
"How",
"Why",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
!non_names.iter().any(|n| n.eq_ignore_ascii_case(word))
}
fn extract_relative_refs(query: &str, context_date: Option<DateTime<Utc>>) -> Vec<RelativeTimeRef> {
let query_lower = query.to_lowercase();
let mut refs = Vec::new();
let patterns = [
("last year", TimeDirection::Past, TimeUnit::Year, 1),
("last month", TimeDirection::Past, TimeUnit::Month, 1),
("last week", TimeDirection::Past, TimeUnit::Week, 1),
("last saturday", TimeDirection::Past, TimeUnit::Day, -1), ("last sunday", TimeDirection::Past, TimeUnit::Day, -1),
("last friday", TimeDirection::Past, TimeUnit::Day, -1),
("yesterday", TimeDirection::Past, TimeUnit::Day, 1),
("next year", TimeDirection::Future, TimeUnit::Year, 1),
("next month", TimeDirection::Future, TimeUnit::Month, 1),
("next week", TimeDirection::Future, TimeUnit::Week, 1),
("tomorrow", TimeDirection::Future, TimeUnit::Day, 1),
("two weeks ago", TimeDirection::Past, TimeUnit::Week, 2),
("three weeks ago", TimeDirection::Past, TimeUnit::Week, 3),
("two months ago", TimeDirection::Past, TimeUnit::Month, 2),
("a week ago", TimeDirection::Past, TimeUnit::Week, 1),
("a month ago", TimeDirection::Past, TimeUnit::Month, 1),
("a year ago", TimeDirection::Past, TimeUnit::Year, 1),
];
for (pattern, direction, unit, offset) in patterns {
if query_lower.contains(pattern) {
let resolved = context_date
.and_then(|ctx| resolve_relative_date(ctx, direction, unit, offset, pattern));
refs.push(RelativeTimeRef {
text: pattern.to_string(),
resolved,
direction,
unit,
offset,
});
}
}
refs
}
fn resolve_relative_date(
context: DateTime<Utc>,
direction: TimeDirection,
unit: TimeUnit,
offset: i32,
pattern: &str,
) -> Option<NaiveDate> {
use chrono::Duration;
let base_date = context.date_naive();
if pattern.starts_with("last ") && pattern.len() > 5 {
let weekday_str = &pattern[5..];
if let Some(target_weekday) = parse_weekday(weekday_str) {
let current_weekday = base_date.weekday();
let days_back = (current_weekday.num_days_from_monday() as i32
- target_weekday.num_days_from_monday() as i32
+ 7)
% 7;
let days_back = if days_back == 0 { 7 } else { days_back };
return Some(base_date - Duration::days(days_back as i64));
}
}
let result = match (direction, unit) {
(TimeDirection::Past, TimeUnit::Day) => base_date - Duration::days(offset as i64),
(TimeDirection::Past, TimeUnit::Week) => base_date - Duration::weeks(offset as i64),
(TimeDirection::Past, TimeUnit::Month) => {
let months_back = offset as i64;
let new_month = (base_date.month() as i64 - months_back - 1).rem_euclid(12) + 1;
let year_offset = (base_date.month() as i64 - months_back - 1).div_euclid(12);
NaiveDate::from_ymd_opt(
base_date.year() + year_offset as i32,
new_month as u32,
base_date.day().min(28),
)?
}
(TimeDirection::Past, TimeUnit::Year) => NaiveDate::from_ymd_opt(
base_date.year() - offset,
base_date.month(),
base_date.day(),
)?,
(TimeDirection::Future, TimeUnit::Day) => base_date + Duration::days(offset as i64),
(TimeDirection::Future, TimeUnit::Week) => base_date + Duration::weeks(offset as i64),
(TimeDirection::Future, TimeUnit::Month) => {
let months_forward = offset as i64;
let new_month = (base_date.month() as i64 + months_forward - 1).rem_euclid(12) + 1;
let year_offset = (base_date.month() as i64 + months_forward - 1).div_euclid(12);
NaiveDate::from_ymd_opt(
base_date.year() + year_offset as i32,
new_month as u32,
base_date.day().min(28),
)?
}
(TimeDirection::Future, TimeUnit::Year) => NaiveDate::from_ymd_opt(
base_date.year() + offset,
base_date.month(),
base_date.day(),
)?,
_ => return None,
};
Some(result)
}
fn parse_weekday(s: &str) -> Option<chrono::Weekday> {
use chrono::Weekday;
match s.to_lowercase().as_str() {
"monday" | "mon" => Some(Weekday::Mon),
"tuesday" | "tue" | "tues" => Some(Weekday::Tue),
"wednesday" | "wed" => Some(Weekday::Wed),
"thursday" | "thu" | "thur" | "thurs" => Some(Weekday::Thu),
"friday" | "fri" => Some(Weekday::Fri),
"saturday" | "sat" => Some(Weekday::Sat),
"sunday" | "sun" => Some(Weekday::Sun),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_parse_basic_query() {
let parser = RuleBasedParser::new();
let parsed = parser.parse("What is Caroline's relationship status?", None);
assert!(parsed.is_attribute_query);
assert!(!parsed.entities.is_empty());
}
#[test]
fn test_parse_temporal_query() {
let parser = RuleBasedParser::new();
let parsed = parser.parse("When did Melanie paint a sunrise?", None);
assert!(parsed.temporal.has_temporal_intent);
assert_eq!(parsed.temporal.intent, TemporalIntent::WhenQuestion);
}
#[test]
fn test_resolve_last_year() {
let parser = RuleBasedParser::new();
let context = chrono::Utc.with_ymd_and_hms(2023, 5, 8, 12, 0, 0).unwrap();
let parsed = parser.parse("Melanie painted it last year", Some(context));
assert!(!parsed.temporal.relative_refs.is_empty());
let ref_ = &parsed.temporal.relative_refs[0];
assert_eq!(ref_.text, "last year");
assert_eq!(
ref_.resolved,
Some(NaiveDate::from_ymd_opt(2022, 5, 8).unwrap())
);
}
}