Skip to content

Commit 9146850

Browse files
authored
feat: improve ask/summarize coherence via query-aware scoring, morphological verb detection, corpus sanitization. (#8)
Signed-off-by: wiseaidev <oss@wiseai.dev>
1 parent 6d71ee7 commit 9146850

4 files changed

Lines changed: 326 additions & 115 deletions

File tree

src/bin/lmm.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,12 @@ async fn main() -> anyhow::Result<()> {
518518

519519
let final_corpus = if corpus.trim().is_empty() {
520520
let lite_results = aggregator.fetch(&prompt, limit).await.unwrap_or_default();
521-
lmm::net::corpus_from_results(&lite_results)
521+
let quality = lmm::net::corpus_from_results(&lite_results);
522+
if quality.trim().is_empty() {
523+
lmm::net::corpus_from_results_raw(&lite_results)
524+
} else {
525+
quality
526+
}
522527
} else {
523528
corpus
524529
};
@@ -530,7 +535,7 @@ async fn main() -> anyhow::Result<()> {
530535
error!(" ❌ No extractable content from search results.");
531536
} else {
532537
let summarizer = TextSummarizer::new(sentences, iterations, depth);
533-
match summarizer.summarize(&final_corpus) {
538+
match summarizer.summarize_with_query(&final_corpus, &prompt) {
534539
Ok(summary) => {
535540
for sentence in &summary {
536541
info!(" {}", sentence);

src/net.rs

Lines changed: 131 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -47,90 +47,169 @@ impl Default for SearchAggregator {
4747
}
4848
}
4949

50+
fn ensure_terminal_punct(text: &str) -> String {
51+
let t = text.trim();
52+
if t.ends_with('.') || t.ends_with('!') || t.ends_with('?') {
53+
t.to_string()
54+
} else {
55+
format!("{}.", t)
56+
}
57+
}
58+
59+
fn sanitize(text: &str) -> String {
60+
text.replace("__###newline###__", " ")
61+
.split_whitespace()
62+
.collect::<Vec<_>>()
63+
.join(" ")
64+
}
65+
66+
fn is_category_label(text: &str) -> bool {
67+
let lower = text.to_lowercase();
68+
let word_count = text.split_whitespace().count();
69+
if word_count < 5 {
70+
return true;
71+
}
72+
let verb_indicators = [
73+
" is ",
74+
" are ",
75+
" was ",
76+
" were ",
77+
" has ",
78+
" have ",
79+
" can ",
80+
" will ",
81+
" does ",
82+
" do ",
83+
" provides ",
84+
" supports ",
85+
" describes ",
86+
" represents ",
87+
" enables ",
88+
" includes ",
89+
" spans ",
90+
" emphasizing ",
91+
" provided ",
92+
];
93+
let has_verb = verb_indicators.iter().any(|&v| lower.contains(v));
94+
if !has_verb {
95+
return true;
96+
}
97+
let category_patterns = [
98+
"programming languages",
99+
"software using",
100+
"free software",
101+
"license",
102+
"category",
103+
];
104+
category_patterns.iter().any(|&p| lower.contains(p))
105+
}
106+
107+
fn strip_topic_prefix(text: &str) -> String {
108+
if let Some(dash_pos) = text.find(" - ") {
109+
let after = text[dash_pos + 3..].trim();
110+
if after.split_whitespace().count() >= 5 {
111+
return after.to_string();
112+
}
113+
}
114+
text.to_string()
115+
}
116+
50117
pub fn corpus_from_results(results: &[LiteSearchResult]) -> String {
51118
results
52119
.iter()
53-
.map(|r| {
120+
.filter_map(|r| {
54121
let mut parts: Vec<String> = Vec::new();
55-
if !r.title.is_empty() {
56-
let mut title = r.title.trim().to_string();
57-
if !title.ends_with('.') && !title.ends_with('!') && !title.ends_with('?') {
58-
title.push('.');
59-
}
60-
parts.push(title);
122+
let title = r.title.trim();
123+
if !title.is_empty() && !title.contains('|') && title.split_whitespace().count() >= 3 {
124+
parts.push(ensure_terminal_punct(title));
125+
}
126+
let snippet = r.snippet.trim();
127+
if !snippet.is_empty()
128+
&& !snippet.contains('|')
129+
&& snippet.split_whitespace().count() >= 7
130+
{
131+
parts.push(ensure_terminal_punct(snippet));
61132
}
62-
if !r.snippet.is_empty() {
63-
let mut snippet = r.snippet.trim().to_string();
64-
if !snippet.ends_with('.') && !snippet.ends_with('!') && !snippet.ends_with('?') {
65-
snippet.push('.');
66-
}
67-
parts.push(snippet);
133+
if parts.is_empty() {
134+
None
135+
} else {
136+
Some(parts.join(" "))
68137
}
69-
parts.join(" ")
70138
})
71-
.filter(|s| !s.is_empty())
72139
.collect::<Vec<_>>()
73140
.join(" ")
74141
}
75142

76-
pub fn corpus_from_response(resp: &Response) -> String {
77-
let mut parts = Vec::new();
78-
79-
let mut add_part = |text: &str| {
80-
let trimmed = text.trim();
81-
if !trimmed.is_empty() {
82-
let mut content = trimmed.to_string();
83-
if !content.ends_with('.') && !content.ends_with('!') && !content.ends_with('?') {
84-
content.push('.');
143+
pub fn corpus_from_results_raw(results: &[LiteSearchResult]) -> String {
144+
results
145+
.iter()
146+
.filter_map(|r| {
147+
let mut parts: Vec<String> = Vec::new();
148+
let snippet = r.snippet.trim();
149+
if !snippet.is_empty() && !snippet.contains('|') {
150+
parts.push(ensure_terminal_punct(snippet));
85151
}
86-
parts.push(content);
87-
}
88-
};
152+
let title = r.title.trim();
153+
if !title.is_empty() && !title.contains('|') && parts.is_empty() {
154+
parts.push(ensure_terminal_punct(title));
155+
}
156+
if parts.is_empty() {
157+
None
158+
} else {
159+
Some(parts.join(" "))
160+
}
161+
})
162+
.collect::<Vec<_>>()
163+
.join(" ")
164+
}
165+
166+
pub fn corpus_from_response(resp: &Response) -> String {
167+
let mut parts: Vec<String> = Vec::new();
89168

90169
if let Some(abstract_text) = &resp.abstract_text {
91-
add_part(abstract_text);
170+
let t = sanitize(abstract_text);
171+
if !t.is_empty() {
172+
parts.push(ensure_terminal_punct(&t));
173+
}
92174
}
175+
93176
if let Some(answer) = &resp.answer {
94-
add_part(answer);
177+
let t = sanitize(answer);
178+
if !t.is_empty() {
179+
parts.push(ensure_terminal_punct(&t));
180+
}
95181
}
182+
96183
if let Some(definition) = &resp.definition {
97-
add_part(definition);
184+
let t = sanitize(definition);
185+
if !t.is_empty() {
186+
parts.push(ensure_terminal_punct(&t));
187+
}
98188
}
99189

100-
for topic in resp.related_topics.iter().take(10) {
101-
if let Some(text) = &topic.text
102-
&& text.split_whitespace().count() >= 5
103-
{
104-
add_part(text);
190+
for topic in resp.related_topics.iter().take(15) {
191+
if let Some(raw_text) = &topic.text {
192+
let cleaned = strip_topic_prefix(&sanitize(raw_text));
193+
if !is_category_label(&cleaned) {
194+
parts.push(ensure_terminal_punct(&cleaned));
195+
}
105196
}
106197
}
107198

108199
parts.join(" ")
109200
}
110201

111202
pub fn seed_from_results(query: &str, results: &[LiteSearchResult]) -> String {
203+
let stopwords = [
204+
"the", "and", "for", "with", "that", "this", "from", "what", "how", "are", "was", "were",
205+
"will", "have", "been", "they",
206+
];
112207
let topic_words: Vec<String> = results
113208
.iter()
114209
.flat_map(|r| r.title.split_whitespace().map(str::to_string))
115210
.filter(|w| {
116211
let low = w.to_lowercase();
117-
w.len() > 3
118-
&& !matches!(
119-
low.as_str(),
120-
"the"
121-
| "and"
122-
| "for"
123-
| "with"
124-
| "that"
125-
| "this"
126-
| "from"
127-
| "what"
128-
| "how"
129-
| "are"
130-
| "was"
131-
| "were"
132-
| "will"
133-
)
212+
w.len() > 3 && !stopwords.contains(&low.as_str())
134213
})
135214
.take(6)
136215
.collect();

0 commit comments

Comments
 (0)