Skip to content

Commit 822aa68

Browse files
AlexMikhalevclaude
andcommitted
feat(types): add layered search output (--layer 1/2/3) for token efficiency
Add Layer enum (One/Two/Three) to SearchQuery controlling result detail: - Layer 1: title + tags only (~50 tokens/result) - Layer 2: + first paragraph summary (~150 tokens/result) - Layer 3: full content (current default, backwards compatible) CLI flag --layer, HTTP query parameter, and result filtering all wired. Refs #88 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 501bf43 commit 822aa68

25 files changed

Lines changed: 327 additions & 32 deletions

crates/terraphim_agent/src/main.rs

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ mod repl;
3838

3939
use client::{ApiClient, SearchResponse};
4040
use service::TuiService;
41-
use terraphim_types::{Document, LogicalOperator, NormalizedTermValue, RoleName, SearchQuery};
41+
use terraphim_types::{
42+
Document, Layer, LogicalOperator, NormalizedTermValue, RoleName, SearchQuery,
43+
extract_first_paragraph,
44+
};
4245
use terraphim_update::{check_for_updates, check_for_updates_startup, update_binary};
4346

4447
#[derive(clap::ValueEnum, Debug, Clone)]
@@ -475,6 +478,10 @@ struct SearchDocumentOutput {
475478
title: String,
476479
url: String,
477480
rank: Option<u64>,
481+
#[serde(skip_serializing_if = "Option::is_none")]
482+
tags: Option<Vec<String>>,
483+
#[serde(skip_serializing_if = "Option::is_none")]
484+
summary: Option<String>,
478485
}
479486

480487
#[derive(Debug, Serialize)]
@@ -485,6 +492,38 @@ struct SearchOutput {
485492
results: Vec<SearchDocumentOutput>,
486493
}
487494

495+
/// Extension trait to convert Document to layered output
496+
impl SearchDocumentOutput {
497+
fn from_document(doc: &Document, layer: &Layer) -> Self {
498+
match layer {
499+
Layer::One => Self {
500+
id: doc.id.clone(),
501+
title: doc.title.clone(),
502+
url: doc.url.clone(),
503+
rank: doc.rank,
504+
tags: doc.tags.clone(),
505+
summary: None,
506+
},
507+
Layer::Two => Self {
508+
id: doc.id.clone(),
509+
title: doc.title.clone(),
510+
url: doc.url.clone(),
511+
rank: doc.rank,
512+
tags: doc.tags.clone(),
513+
summary: Some(extract_first_paragraph(&doc.body)),
514+
},
515+
Layer::Three => Self {
516+
id: doc.id.clone(),
517+
title: doc.title.clone(),
518+
url: doc.url.clone(),
519+
rank: doc.rank,
520+
tags: doc.tags.clone(),
521+
summary: None, // For full content, summary not needed
522+
},
523+
}
524+
}
525+
}
526+
488527
fn print_json_output<T: Serialize>(value: &T, mode: CommandOutputMode) -> Result<()> {
489528
let out = match mode {
490529
CommandOutputMode::Human => serde_json::to_string_pretty(value)?,
@@ -540,6 +579,9 @@ enum Command {
540579
role: Option<String>,
541580
#[arg(long, default_value_t = 10)]
542581
limit: usize,
582+
/// Output layer: 1=minimal (title+tags), 2=summary, 3=full (default)
583+
#[arg(long, default_value_t = 3, value_name = "1|2|3")]
584+
layer: u8,
543585
},
544586
/// Manage roles (list, select)
545587
Roles {
@@ -1119,13 +1161,18 @@ async fn run_offline_command(
11191161
operator,
11201162
role,
11211163
limit,
1164+
layer,
11221165
} => {
11231166
let role_name = if let Some(role) = role {
11241167
RoleName::new(&role)
11251168
} else {
11261169
service.get_selected_role().await
11271170
};
11281171

1172+
// Parse and validate layer
1173+
let layer =
1174+
terraphim_types::Layer::from_u8(layer).unwrap_or(terraphim_types::Layer::Three);
1175+
11291176
let results = if let Some(additional_terms) = terms {
11301177
// Multi-term query with logical operators
11311178
let mut all_terms = vec![query.clone()];
@@ -1159,6 +1206,7 @@ async fn run_offline_command(
11591206
skip: Some(0),
11601207
limit: Some(limit),
11611208
role: Some(role_name.clone()),
1209+
layer,
11621210
};
11631211

11641212
service.search_with_query(&search_query).await?
@@ -1171,23 +1219,33 @@ async fn run_offline_command(
11711219

11721220
if output.is_machine_readable() {
11731221
let payload = SearchOutput {
1174-
query,
1222+
query: query.clone(),
11751223
role: role_name.to_string(),
11761224
count: results.len(),
11771225
results: results
11781226
.iter()
1179-
.map(|doc| SearchDocumentOutput {
1180-
id: doc.id.clone(),
1181-
title: doc.title.clone(),
1182-
url: doc.url.clone(),
1183-
rank: doc.rank,
1184-
})
1227+
.map(|doc| SearchDocumentOutput::from_document(doc, &layer))
11851228
.collect(),
11861229
};
11871230
print_json_output(&payload, output.mode)?;
11881231
} else {
11891232
for doc in results.iter() {
1190-
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1233+
match layer {
1234+
Layer::One => {
1235+
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1236+
}
1237+
Layer::Two => {
1238+
let summary = extract_first_paragraph(&doc.body);
1239+
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1240+
println!(" {}", summary);
1241+
}
1242+
Layer::Three => {
1243+
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1244+
if let Some(ref tags) = doc.tags {
1245+
println!(" Tags: {}", tags.join(", "));
1246+
}
1247+
}
1248+
}
11911249
}
11921250
}
11931251
Ok(())
@@ -2110,6 +2168,7 @@ async fn run_server_command(
21102168
operator,
21112169
role,
21122170
limit,
2171+
layer,
21132172
} => {
21142173
// Get selected role from server if not specified
21152174
let role_name = if let Some(role) = role {
@@ -2119,6 +2178,10 @@ async fn run_server_command(
21192178
config_res.config.selected_role
21202179
};
21212180

2181+
// Parse and validate layer
2182+
let layer =
2183+
terraphim_types::Layer::from_u8(layer).unwrap_or(terraphim_types::Layer::Three);
2184+
21222185
let q = if let Some(additional_terms) = terms {
21232186
// Multi-term query with logical operators
21242187
let search_terms: Vec<NormalizedTermValue> = additional_terms
@@ -2133,6 +2196,7 @@ async fn run_server_command(
21332196
skip: Some(0),
21342197
limit: Some(limit),
21352198
role: Some(role_name),
2199+
layer,
21362200
}
21372201
} else {
21382202
// Single term query (backward compatibility)
@@ -2143,6 +2207,7 @@ async fn run_server_command(
21432207
skip: Some(0),
21442208
limit: Some(limit),
21452209
role: Some(role_name),
2210+
layer,
21462211
}
21472212
};
21482213

@@ -2177,12 +2242,7 @@ async fn run_server_command(
21772242
results: res
21782243
.results
21792244
.iter()
2180-
.map(|doc| SearchDocumentOutput {
2181-
id: doc.id.clone(),
2182-
title: doc.title.clone(),
2183-
url: doc.url.clone(),
2184-
rank: doc.rank,
2185-
})
2245+
.map(|doc| SearchDocumentOutput::from_document(doc, &layer))
21862246
.collect(),
21872247
};
21882248
print_json_output(&payload, output.mode)?;
@@ -2893,6 +2953,7 @@ fn ui_loop(
28932953
skip: Some(0),
28942954
limit: Some(10),
28952955
role: Some(RoleName::new(&role)),
2956+
layer: Layer::default(),
28962957
};
28972958
let resp = api.search(&q).await?;
28982959
let lines: Vec<String> = resp

crates/terraphim_agent/src/repl/handler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ impl ReplHandler {
409409
}
410410
} else if let Some(api_client) = &self.api_client {
411411
// Server mode - use current role if no role specified
412-
use terraphim_types::{NormalizedTermValue, RoleName, SearchQuery};
412+
use terraphim_types::{Layer, NormalizedTermValue, RoleName, SearchQuery};
413413

414414
let effective_role = role.unwrap_or_else(|| self.current_role.clone());
415415
let role_name = Some(RoleName::new(&effective_role));
@@ -420,6 +420,7 @@ impl ReplHandler {
420420
skip: Some(0),
421421
limit,
422422
role: role_name,
423+
layer: Layer::default(),
423424
};
424425

425426
match api_client.search(&search_query).await {

crates/terraphim_agent/src/service.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use terraphim_persistence::Persistable;
55
use terraphim_service::TerraphimService;
66
use terraphim_service::llm::{ChatOptions, build_llm_from_role};
77
use terraphim_settings::{DeviceSettings, Error as DeviceSettingsError};
8-
use terraphim_types::{Document, NormalizedTermValue, RoleName, SearchQuery, Thesaurus};
8+
use terraphim_types::{Document, Layer, NormalizedTermValue, RoleName, SearchQuery, Thesaurus};
99
use tokio::sync::Mutex;
1010

1111
#[derive(Clone)]
@@ -254,6 +254,7 @@ impl TuiService {
254254
skip: Some(0),
255255
limit,
256256
role: Some(role.clone()),
257+
layer: Layer::default(),
257258
};
258259

259260
let mut service = self.service.lock().await;

crates/terraphim_agent/tests/cross_mode_consistency_test.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::time::Duration;
1717
use anyhow::Result;
1818
use serial_test::serial;
1919
use terraphim_agent::client::ApiClient;
20-
use terraphim_types::{NormalizedTermValue, RoleName, SearchQuery};
20+
use terraphim_types::{Layer, NormalizedTermValue, RoleName, SearchQuery};
2121

2222
/// Result structure normalized across all modes
2323
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
@@ -244,6 +244,7 @@ async fn search_via_server(
244244
skip: Some(0),
245245
limit: Some(10),
246246
role: Some(RoleName::new(role)),
247+
layer: Layer::default(),
247248
};
248249

249250
let response = client.search(&search_query).await?;

crates/terraphim_agent/tests/error_handling_test.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::time::Duration;
22

33
use serial_test::serial;
44
use terraphim_agent::client::ApiClient;
5-
use terraphim_types::{Document, DocumentType, NormalizedTermValue, RoleName, SearchQuery};
5+
use terraphim_types::{Document, DocumentType, Layer, NormalizedTermValue, RoleName, SearchQuery};
66
use tokio::time::timeout;
77

88
const TEST_SERVER_URL: &str = "http://localhost:8000";
@@ -92,6 +92,7 @@ async fn test_malformed_server_response() {
9292
skip: Some(0),
9393
limit: Some(100000), // Extremely large limit
9494
role: Some(RoleName::new("Default")),
95+
layer: Layer::default(),
9596
};
9697

9798
let result = client.search(&extreme_query).await;
@@ -137,6 +138,7 @@ async fn test_invalid_role_handling() {
137138
skip: Some(0),
138139
limit: Some(5),
139140
role: Some(RoleName::new("CompleteLyInvalidRoleName12345")),
141+
layer: Layer::default(),
140142
};
141143

142144
let result = client.search(&invalid_query).await;
@@ -200,6 +202,7 @@ async fn test_empty_and_special_character_queries() {
200202
skip: Some(0),
201203
limit: Some(5),
202204
role: Some(RoleName::new("Default")),
205+
layer: Layer::default(),
203206
};
204207

205208
let result = client.search(&search_query).await;
@@ -253,6 +256,7 @@ async fn test_concurrent_request_handling() {
253256
skip: Some(0),
254257
limit: Some(3),
255258
role: Some(RoleName::new("Default")),
259+
layer: Layer::default(),
256260
};
257261
client_clone.search(&query).await
258262
});
@@ -517,6 +521,7 @@ async fn test_graceful_degradation() {
517521
skip: Some(0),
518522
limit: Some(1),
519523
role: Some(RoleName::new("Default")),
524+
layer: Layer::default(),
520525
};
521526
client
522527
.search(&query)

crates/terraphim_agent/tests/integration_test.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::time::Duration;
44
use anyhow::Result;
55
use serial_test::serial;
66
use terraphim_agent::client::{ApiClient, ChatResponse, ConfigResponse, SearchResponse};
7-
use terraphim_types::{NormalizedTermValue, RoleName, SearchQuery};
7+
use terraphim_types::{Layer, NormalizedTermValue, RoleName, SearchQuery};
88

99
const TEST_SERVER_URL: &str = "http://localhost:8000";
1010
#[allow(dead_code)]
@@ -58,6 +58,7 @@ async fn test_api_client_search() {
5858
skip: Some(0),
5959
limit: Some(5),
6060
role: Some(RoleName::new("Terraphim Engineer")),
61+
layer: Layer::default(),
6162
};
6263

6364
let result = client.search(&query).await;
@@ -203,6 +204,7 @@ async fn test_search_with_different_roles() {
203204
skip: Some(0),
204205
limit: Some(3),
205206
role: Some(RoleName::new(role_name)),
207+
layer: Layer::default(),
206208
};
207209

208210
let result = client.search(&query).await;
@@ -244,6 +246,7 @@ async fn test_search_pagination() {
244246
skip: Some(0),
245247
limit: Some(2),
246248
role: Some(RoleName::new("Default")),
249+
layer: Layer::default(),
247250
};
248251

249252
let result1 = client.search(&query1).await;
@@ -257,6 +260,7 @@ async fn test_search_pagination() {
257260
skip: Some(2),
258261
limit: Some(2),
259262
role: Some(RoleName::new("Default")),
263+
layer: Layer::default(),
260264
};
261265

262266
let result2 = client.search(&query2).await;
@@ -449,6 +453,7 @@ async fn test_api_error_handling() {
449453
skip: Some(0),
450454
limit: Some(0), // Invalid limit
451455
role: Some(RoleName::new("NonExistentRole")),
456+
layer: Layer::default(),
452457
};
453458

454459
let result = client.search(&query).await;

crates/terraphim_agent/tests/kg_ranking_integration_test.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use anyhow::Result;
2222

2323
use serial_test::serial;
2424
use terraphim_agent::client::ApiClient;
25-
use terraphim_types::{Document, NormalizedTermValue, RoleName, SearchQuery};
25+
use terraphim_types::{Document, Layer, NormalizedTermValue, RoleName, SearchQuery};
2626

2727
/// Get workspace root directory
2828
fn get_workspace_root() -> Result<PathBuf> {
@@ -276,6 +276,7 @@ async fn search_via_server(
276276
skip: Some(0),
277277
limit: Some(20),
278278
role: Some(RoleName::new(role)),
279+
layer: Layer::default(),
279280
};
280281

281282
let response = client.search(&search_query).await?;

crates/terraphim_cli/src/service.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use terraphim_persistence::Persistable;
88
use terraphim_service::TerraphimService;
99
use terraphim_settings::{DeviceSettings, Error as DeviceSettingsError};
1010
use terraphim_types::{
11-
CoverageSignal, Document, ExtractedEntity, GroundingMetadata, NormalizationMethod,
11+
CoverageSignal, Document, ExtractedEntity, GroundingMetadata, Layer, NormalizationMethod,
1212
NormalizedTerm, NormalizedTermValue, OntologySchema, RoleName, SchemaSignal, SearchQuery,
1313
Thesaurus,
1414
};
@@ -265,6 +265,7 @@ impl CliService {
265265
skip: Some(0),
266266
limit,
267267
role: Some(role.clone()),
268+
layer: Layer::default(),
268269
};
269270

270271
let mut service = self.service.lock().await;

crates/terraphim_cli/tests/service_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ mod search_query_tests {
205205
skip: Some(0),
206206
limit: Some(10),
207207
role: Some(RoleName::new("Default")),
208+
layer: Default::default(),
208209
};
209210

210211
assert_eq!(query.search_term.to_string(), "rust async");
@@ -221,6 +222,7 @@ mod search_query_tests {
221222
skip: None,
222223
limit: None,
223224
role: None,
225+
layer: Default::default(),
224226
};
225227

226228
assert!(query.role.is_none());

0 commit comments

Comments
 (0)