Skip to content

Commit fdd0043

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 f2c80bc commit fdd0043

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 {
@@ -1130,13 +1172,18 @@ async fn run_offline_command(
11301172
operator,
11311173
role,
11321174
limit,
1175+
layer,
11331176
} => {
11341177
let role_name = if let Some(role) = role {
11351178
RoleName::new(&role)
11361179
} else {
11371180
service.get_selected_role().await
11381181
};
11391182

1183+
// Parse and validate layer
1184+
let layer =
1185+
terraphim_types::Layer::from_u8(layer).unwrap_or(terraphim_types::Layer::Three);
1186+
11401187
let results = if let Some(additional_terms) = terms {
11411188
// Multi-term query with logical operators
11421189
let mut all_terms = vec![query.clone()];
@@ -1170,6 +1217,7 @@ async fn run_offline_command(
11701217
skip: Some(0),
11711218
limit: Some(limit),
11721219
role: Some(role_name.clone()),
1220+
layer,
11731221
};
11741222

11751223
service.search_with_query(&search_query).await?
@@ -1182,23 +1230,33 @@ async fn run_offline_command(
11821230

11831231
if output.is_machine_readable() {
11841232
let payload = SearchOutput {
1185-
query,
1233+
query: query.clone(),
11861234
role: role_name.to_string(),
11871235
count: results.len(),
11881236
results: results
11891237
.iter()
1190-
.map(|doc| SearchDocumentOutput {
1191-
id: doc.id.clone(),
1192-
title: doc.title.clone(),
1193-
url: doc.url.clone(),
1194-
rank: doc.rank,
1195-
})
1238+
.map(|doc| SearchDocumentOutput::from_document(doc, &layer))
11961239
.collect(),
11971240
};
11981241
print_json_output(&payload, output.mode)?;
11991242
} else {
12001243
for doc in results.iter() {
1201-
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1244+
match layer {
1245+
Layer::One => {
1246+
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1247+
}
1248+
Layer::Two => {
1249+
let summary = extract_first_paragraph(&doc.body);
1250+
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1251+
println!(" {}", summary);
1252+
}
1253+
Layer::Three => {
1254+
println!("- {}\t{}", doc.rank.unwrap_or_default(), doc.title);
1255+
if let Some(ref tags) = doc.tags {
1256+
println!(" Tags: {}", tags.join(", "));
1257+
}
1258+
}
1259+
}
12021260
}
12031261
}
12041262
Ok(())
@@ -2172,6 +2230,7 @@ async fn run_server_command(
21722230
operator,
21732231
role,
21742232
limit,
2233+
layer,
21752234
} => {
21762235
// Get selected role from server if not specified
21772236
let role_name = if let Some(role) = role {
@@ -2181,6 +2240,10 @@ async fn run_server_command(
21812240
config_res.config.selected_role
21822241
};
21832242

2243+
// Parse and validate layer
2244+
let layer =
2245+
terraphim_types::Layer::from_u8(layer).unwrap_or(terraphim_types::Layer::Three);
2246+
21842247
let q = if let Some(additional_terms) = terms {
21852248
// Multi-term query with logical operators
21862249
let search_terms: Vec<NormalizedTermValue> = additional_terms
@@ -2195,6 +2258,7 @@ async fn run_server_command(
21952258
skip: Some(0),
21962259
limit: Some(limit),
21972260
role: Some(role_name),
2261+
layer,
21982262
}
21992263
} else {
22002264
// Single term query (backward compatibility)
@@ -2205,6 +2269,7 @@ async fn run_server_command(
22052269
skip: Some(0),
22062270
limit: Some(limit),
22072271
role: Some(role_name),
2272+
layer,
22082273
}
22092274
};
22102275

@@ -2239,12 +2304,7 @@ async fn run_server_command(
22392304
results: res
22402305
.results
22412306
.iter()
2242-
.map(|doc| SearchDocumentOutput {
2243-
id: doc.id.clone(),
2244-
title: doc.title.clone(),
2245-
url: doc.url.clone(),
2246-
rank: doc.rank,
2247-
})
2307+
.map(|doc| SearchDocumentOutput::from_document(doc, &layer))
22482308
.collect(),
22492309
};
22502310
print_json_output(&payload, output.mode)?;
@@ -2955,6 +3015,7 @@ fn ui_loop(
29553015
skip: Some(0),
29563016
limit: Some(10),
29573017
role: Some(RoleName::new(&role)),
3018+
layer: Layer::default(),
29583019
};
29593020
let resp = api.search(&q).await?;
29603021
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)