use std::sync::OnceLock;
use crate::seed;
pub use crate::seed::{ConceptRecord, ContextRecord};
fn concepts() -> &'static [ConceptRecord] {
static CELL: OnceLock<Vec<ConceptRecord>> = OnceLock::new();
CELL.get_or_init(seed::concepts).as_slice()
}
fn concept_contexts() -> &'static [ContextRecord] {
static CELL: OnceLock<Vec<ContextRecord>> = OnceLock::new();
CELL.get_or_init(seed::concept_contexts).as_slice()
}
#[must_use]
pub fn resolve_context_label(raw_context: &str) -> Option<&'static ContextRecord> {
let normalized = normalize_concept_term(raw_context);
if normalized.is_empty() {
return None;
}
concept_contexts()
.iter()
.find(|record| record.matches(&normalized))
}
fn concept_prefixes() -> &'static [(String, String)] {
static CELL: OnceLock<Vec<(String, String)>> = OnceLock::new();
CELL.get_or_init(|| {
let mut prefixes = seed::prompt_patterns()
.into_iter()
.filter(|p| p.intent == "concept_lookup" && p.kind == "prefix")
.map(|p| (p.text.to_lowercase(), p.language))
.collect::<Vec<_>>();
prefixes.sort_by_key(|p| std::cmp::Reverse(p.0.len()));
prefixes
})
.as_slice()
}
fn concept_suffixes() -> &'static [String] {
static CELL: OnceLock<Vec<String>> = OnceLock::new();
CELL.get_or_init(|| {
let mut suffixes = seed::prompt_patterns()
.into_iter()
.filter(|p| p.intent == "concept_lookup" && p.kind == "suffix")
.map(|p| p.text)
.collect::<Vec<_>>();
suffixes.sort_by_key(|s| std::cmp::Reverse(s.len()));
suffixes
})
.as_slice()
}
fn concept_context_delimiters() -> &'static [String] {
static CELL: OnceLock<Vec<String>> = OnceLock::new();
CELL.get_or_init(|| {
let mut delimiters = seed::prompt_patterns()
.into_iter()
.filter(|p| p.intent == "concept_lookup" && p.kind == "context_delimiter")
.map(|p| p.text)
.collect::<Vec<_>>();
delimiters.sort_by_key(|d| std::cmp::Reverse(d.len()));
delimiters
})
.as_slice()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConceptQuery {
pub term: String,
pub context: Option<String>,
}
pub fn extract_concept_query(prompt: &str) -> Option<ConceptQuery> {
let trimmed = prompt.trim();
let trimmed = trimmed
.trim_end_matches(['?', '。', '.', '!', '!', ',', ',', ';', ':'])
.trim();
if trimmed.is_empty() {
return None;
}
if let Some(body) = strip_suffix_pattern(trimmed) {
return finalize_concept_query(&body);
}
let lower = trimmed.to_lowercase();
let mut body: Option<&str> = None;
for (prefix, _language) in concept_prefixes() {
if let Some(rest) = lower.strip_prefix(prefix.as_str()) {
let start = trimmed.len() - rest.len();
body = Some(trimmed[start..].trim());
break;
}
}
let body = body?;
finalize_concept_query(body)
}
fn finalize_concept_query(body: &str) -> Option<ConceptQuery> {
let body = body
.trim()
.trim_end_matches(['?', '。', '.', '!', '!', ',', ',', ';', ':'])
.trim()
.to_lowercase();
if body.is_empty() {
return None;
}
let trimmed_body = body
.strip_suffix(" mean")
.or_else(|| body.strip_suffix(" stand for"))
.unwrap_or(&body)
.trim();
if trimmed_body.is_empty() {
return None;
}
let (term, context) = split_term_and_context(trimmed_body);
if term.is_empty() {
return None;
}
Some(ConceptQuery {
term,
context: context.filter(|c| !c.is_empty()),
})
}
fn split_term_and_context(body: &str) -> (String, Option<String>) {
for delimiter in concept_context_delimiters() {
if let Some(idx) = body.find(delimiter.as_str()) {
let term = body[..idx].trim().to_owned();
let context = body[idx + delimiter.len()..].trim().to_owned();
if !term.is_empty() && !context.is_empty() {
return (term, Some(context));
}
}
}
(body.to_owned(), None)
}
fn strip_suffix_pattern(input: &str) -> Option<String> {
for suffix in concept_suffixes() {
if let Some(rest) = input.strip_suffix(suffix.as_str()) {
return Some(rest.trim().to_owned());
}
}
None
}
#[derive(Debug, Clone)]
pub struct ConceptLookup {
pub record: &'static ConceptRecord,
pub context_match: bool,
pub context: Option<String>,
}
#[must_use]
pub fn lookup_concept_query(query: &ConceptQuery) -> Option<ConceptLookup> {
if let Some(lookup) = rank_for_pair(&query.term, query.context.as_deref()) {
return Some(lookup);
}
if let Some(context) = query.context.as_deref() {
if let Some(lookup) = rank_for_pair(context, Some(&query.term)) {
return Some(lookup);
}
}
None
}
fn rank_for_pair(term: &str, context: Option<&str>) -> Option<ConceptLookup> {
let normalized = normalize_concept_term(term);
if normalized.is_empty() {
return None;
}
let context_normalized = context
.map(normalize_concept_term)
.filter(|c| !c.is_empty());
let mut term_matches: Vec<&'static ConceptRecord> = concepts()
.iter()
.filter(|record| record_matches_term(record, &normalized))
.collect();
if term_matches.is_empty() {
return None;
}
if let Some(ctx) = context_normalized.as_deref() {
if let Some(record) = term_matches
.iter()
.copied()
.find(|record| record_has_context(record, ctx))
{
return Some(ConceptLookup {
record,
context_match: true,
context: Some(ctx.to_owned()),
});
}
}
term_matches.sort_by_key(|record| u8::from(!record.contexts.is_empty()));
let record = term_matches.into_iter().next()?;
Some(ConceptLookup {
record,
context_match: false,
context: context_normalized,
})
}
fn record_matches_term(record: &ConceptRecord, normalized: &str) -> bool {
if normalize_concept_term(&record.term) == normalized
|| normalize_concept_term(&record.slug) == normalized
{
return true;
}
record
.aliases
.iter()
.any(|alias| normalize_concept_term(alias) == normalized)
}
fn record_has_context(record: &ConceptRecord, context_normalized: &str) -> bool {
if record
.contexts
.iter()
.any(|candidate| normalize_concept_term(candidate) == context_normalized)
{
return true;
}
if let Some(context_record) = concept_contexts()
.iter()
.find(|c| c.matches(context_normalized))
{
return record
.context_links
.iter()
.any(|slug| slug.trim() == context_record.slug);
}
false
}
fn normalize_concept_term(value: &str) -> String {
let lower = value.to_lowercase();
let mut stripped = lower.as_str();
for prefix in ["the ", "a ", "an "] {
if let Some(rest) = stripped.strip_prefix(prefix) {
stripped = rest;
break;
}
}
stripped
.trim()
.trim_end_matches(['?', '.', '!', ',', ';', ':'])
.trim()
.to_owned()
}