Compare commits
4 commits
d5aad5c1a4
...
0612e1bc41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0612e1bc41 | ||
|
|
c8922c9408 | ||
|
|
c8280ae871 | ||
|
|
c79b415ada |
3 changed files with 174 additions and 62 deletions
|
|
@ -276,70 +276,20 @@ async fn query(args: &serde_json::Value) -> Result<String> {
|
|||
let store = arc.lock().await;
|
||||
let graph = store.build_graph();
|
||||
|
||||
let stages = crate::query_parser::parse_stages(query_str)
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
let results = crate::search::run_query(&stages, vec![], &graph, &store, false, 100);
|
||||
let keys: Vec<String> = results.into_iter().map(|(k, _)| k).collect();
|
||||
|
||||
match format {
|
||||
"full" => {
|
||||
// Rich output with full content, graph metrics, hub analysis
|
||||
let results = crate::query_parser::execute_query(&store, &graph, query_str)
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
let keys: Vec<String> = results.into_iter().map(|r| r.key).collect();
|
||||
let items = crate::subconscious::defs::keys_to_replay_items(&store, &keys, &graph);
|
||||
Ok(crate::subconscious::prompts::format_nodes_section(&store, &items, &graph))
|
||||
}
|
||||
_ => {
|
||||
// Compact output: check for count/select stages, else just list keys
|
||||
use crate::search::{Stage, Transform};
|
||||
let has_count = stages.iter().any(|s| matches!(s, Stage::Transform(Transform::Count)));
|
||||
if has_count {
|
||||
return Ok(keys.len().to_string());
|
||||
// Compact output: handles count, select, and all expression types
|
||||
crate::query_parser::query_to_string(&store, &graph, query_str)
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))
|
||||
}
|
||||
if keys.is_empty() {
|
||||
return Ok("no results".to_string());
|
||||
}
|
||||
let select_fields: Option<&Vec<String>> = stages.iter().find_map(|s| match s {
|
||||
Stage::Transform(Transform::Select(f)) => Some(f),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(fields) = select_fields {
|
||||
let mut out = String::from("key\t");
|
||||
out.push_str(&fields.join("\t"));
|
||||
out.push('\n');
|
||||
for key in &keys {
|
||||
out.push_str(key);
|
||||
for f in fields {
|
||||
out.push('\t');
|
||||
out.push_str(&resolve_field_str(&store, &graph, key, f));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
Ok(out)
|
||||
} else {
|
||||
Ok(keys.join("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_field_str(store: &crate::store::Store, graph: &crate::graph::Graph, key: &str, field: &str) -> String {
|
||||
let node = match store.nodes.get(key) {
|
||||
Some(n) => n,
|
||||
None => return "-".to_string(),
|
||||
};
|
||||
match field {
|
||||
"key" => key.to_string(),
|
||||
"weight" => format!("{:.3}", node.weight),
|
||||
"node_type" => format!("{:?}", node.node_type),
|
||||
"provenance" => node.provenance.clone(),
|
||||
"emotion" => format!("{}", node.emotion),
|
||||
"retrievals" => format!("{}", node.retrievals),
|
||||
"uses" => format!("{}", node.uses),
|
||||
"wrongs" => format!("{}", node.wrongs),
|
||||
"created" => format!("{}", node.created_at),
|
||||
"timestamp" => format!("{}", node.timestamp),
|
||||
"degree" => format!("{}", graph.degree(key)),
|
||||
"content_len" => format!("{}", node.content.len()),
|
||||
_ => "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use std::collections::BTreeMap;
|
|||
|
||||
// Re-export engine types used by Query
|
||||
pub use super::engine::{
|
||||
Stage, Filter, Transform, Generator, SortField,
|
||||
Stage, Filter, Transform, Generator, SortField, ScoreField,
|
||||
Algorithm, AlgoStage, Cmp,
|
||||
};
|
||||
|
||||
|
|
@ -92,13 +92,16 @@ peg::parser! {
|
|||
/ "connectivity" { Stage::Transform(Transform::Connectivity) }
|
||||
/ "dominating-set" { Stage::Transform(Transform::DominatingSet) }
|
||||
// Pipeline syntax (colon-separated)
|
||||
/ "sort:" f:field() { Stage::Transform(Transform::Sort(make_sort_field(&f, false))) }
|
||||
/ "sort:" c:composite_sort() { Stage::Transform(Transform::Sort(c)) }
|
||||
/ "limit:" n:integer() { Stage::Transform(Transform::Limit(n)) }
|
||||
/ "select:" f:field_list_colon() { Stage::Transform(Transform::Select(f)) }
|
||||
/ "type:" t:ident() { make_type_filter(&t) }
|
||||
/ "age:" c:cmp_duration() { Stage::Filter(Filter::Age(c)) }
|
||||
/ "key:" g:ident() { Stage::Filter(Filter::KeyGlob(g)) }
|
||||
/ "key:" g:glob_pattern() { Stage::Filter(Filter::KeyGlob(g)) }
|
||||
/ "!key:" g:glob_pattern() { Stage::Filter(Filter::Negated(Box::new(Filter::KeyGlob(g)))) }
|
||||
/ "provenance:" p:ident() { Stage::Filter(Filter::Provenance(p)) }
|
||||
/ "not-visited:" a:ident() "," d:integer() { Stage::Filter(Filter::NotVisited { agent: a, duration: d as i64 }) }
|
||||
/ "visited:" a:ident() { Stage::Filter(Filter::Visited { agent: a }) }
|
||||
/ "all" { Stage::Generator(Generator::All) }
|
||||
// Graph algorithms
|
||||
/ "spread" { Stage::Algorithm(AlgoStage { algo: Algorithm::Spread, params: std::collections::HashMap::new() }) }
|
||||
|
|
@ -109,6 +112,27 @@ peg::parser! {
|
|||
/ "desc" { false }
|
||||
/ { false } // default: descending
|
||||
|
||||
// Composite sort: degree*0.5+isolation*0.3+recency(organize)*0.2
|
||||
// Falls back to simple field if no weighted terms found.
|
||||
rule composite_sort() -> SortField
|
||||
= t:score_term() ts:("+" t:score_term() { t })+ {
|
||||
let mut terms = vec![t];
|
||||
terms.extend(ts);
|
||||
SortField::Composite(terms)
|
||||
}
|
||||
/ f:field() { make_sort_field(&f, false) }
|
||||
|
||||
rule score_term() -> (ScoreField, f64)
|
||||
= "recency(" a:ident() ")" "*" w:number() { (ScoreField::Recency(a), w) }
|
||||
/ f:score_field_name() "*" w:number() { (f, w) }
|
||||
|
||||
rule score_field_name() -> ScoreField
|
||||
= "isolation" { ScoreField::Isolation }
|
||||
/ "degree" { ScoreField::Degree }
|
||||
/ "weight" { ScoreField::Weight }
|
||||
/ "content-len" { ScoreField::ContentLen }
|
||||
/ "priority" { ScoreField::Priority }
|
||||
|
||||
rule field_list_colon() -> Vec<String>
|
||||
= f:field() fs:("," f:field() { f })* {
|
||||
let mut v = vec![f];
|
||||
|
|
@ -177,9 +201,22 @@ peg::parser! {
|
|||
|
||||
rule value() -> Value
|
||||
= f:fn_call() { Value::FnCall(f) }
|
||||
/ n:number() { Value::Num(n) }
|
||||
/ s:string() { Value::Str(s) }
|
||||
/ i:ident() { Value::Ident(i) }
|
||||
/ t:token() { t }
|
||||
|
||||
// Token: number or identifier, with alphanumeric fallback (e.g., "27b")
|
||||
rule token() -> Value
|
||||
= n:$(['0'..='9']+ ("." ['0'..='9']+)?) !['a'..='z' | 'A'..='Z'] {
|
||||
Value::Num(n.parse().unwrap())
|
||||
}
|
||||
/ s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) {
|
||||
// Try as number first, fall back to string
|
||||
if let Ok(n) = s.parse::<f64>() {
|
||||
Value::Num(n)
|
||||
} else {
|
||||
Value::Str(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
rule fn_call() -> FnCall
|
||||
= "community" _ "(" _ k:string() _ ")" { FnCall::Community(k) }
|
||||
|
|
@ -192,11 +229,24 @@ peg::parser! {
|
|||
|
||||
rule string() -> String
|
||||
= "'" s:$([^ '\'']*) "'" { s.to_string() }
|
||||
/ "\"" s:$([^ '"']*) "\"" { s.to_string() }
|
||||
|
||||
rule ident() -> String
|
||||
= s:$(['a'..='z' | 'A'..='Z' | '_']['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']*) {
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
// Bare word for matching (allows digits at start, e.g. "27b")
|
||||
rule word() -> String
|
||||
= s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) {
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
// Glob pattern for key matching (allows * and ?)
|
||||
rule glob_pattern() -> String
|
||||
= s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.' | '*' | '?']+) {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -800,3 +850,113 @@ fn print_connectivity(results: &[QueryResult], graph: &Graph) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Tests --
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Helper to check if a query parses successfully
|
||||
fn parses(s: &str) -> bool {
|
||||
query_parser::query(s).is_ok()
|
||||
}
|
||||
|
||||
// Helper to get parse error for debugging
|
||||
fn parse_err(s: &str) -> String {
|
||||
query_parser::query(s).err().map(|e| format!("{}", e)).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generators() {
|
||||
assert!(parses("all"));
|
||||
assert!(parses("*"));
|
||||
assert!(parses("all | limit:10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_filters() {
|
||||
assert!(parses("all | type:semantic"));
|
||||
assert!(parses("all | type:episodic"));
|
||||
assert!(parses("all | provenance:observe"));
|
||||
assert!(parses("all | key:journal-*"));
|
||||
assert!(parses("all | !key:_*")); // negated key glob
|
||||
assert!(parses("all | age:>7d"));
|
||||
assert!(parses("all | not-visited:organize,86400"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_transforms() {
|
||||
assert!(parses("all | sort:weight"));
|
||||
assert!(parses("all | sort:timestamp"));
|
||||
assert!(parses("all | sort:degree"));
|
||||
assert!(parses("all | limit:20"));
|
||||
assert!(parses("all | sort:weight | limit:10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composite_sort() {
|
||||
// Weighted composite sort expressions (require 2+ terms with +)
|
||||
assert!(parses("all | sort:degree*0.5+isolation*0.3"));
|
||||
assert!(parses("all | sort:degree*0.5+isolation*0.3+recency(organize)*0.2"));
|
||||
assert!(parses("all | sort:weight*0.5+degree*0.5"));
|
||||
// Single field (no weight) falls back to simple sort
|
||||
assert!(parses("all | sort:weight"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expression_syntax() {
|
||||
// Expression comparisons (legacy syntax)
|
||||
assert!(parses("weight > 0.5"));
|
||||
assert!(parses("degree >= 10"));
|
||||
assert!(parses("key ~ 'journal.*'"));
|
||||
assert!(parses("content ~ 27b"), "alphanumeric pattern: {}", parse_err("content ~ 27b"));
|
||||
assert!(parses("content ~ qwen35"));
|
||||
// Both single and double quotes work for strings
|
||||
assert!(parses("content ~ '27b'"));
|
||||
assert!(parses("content ~ \"27b\""), "double quotes: {}", parse_err("content ~ \"27b\""));
|
||||
assert!(parses("neighbors(\"my-key\")"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_expressions() {
|
||||
assert!(parses("weight > 0.5 AND degree > 10"));
|
||||
assert!(parses("key ~ 'a' OR key ~ 'b'"));
|
||||
assert!(parses("NOT weight < 0.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_parsing() {
|
||||
assert!(parses("all | age:>1d"));
|
||||
assert!(parses("all | age:>=24h"));
|
||||
assert!(parses("all | age:<30m"));
|
||||
assert!(parses("all | age:=3600s"));
|
||||
assert!(parses("all | age:>86400")); // raw seconds
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glob_patterns() {
|
||||
assert!(parses("all | key:*"));
|
||||
assert!(parses("all | key:journal-*"));
|
||||
assert!(parses("all | key:*-2026-*"));
|
||||
assert!(parses("all | key:dream-cycle-?"));
|
||||
assert!(parses("all | !key:subconscious-*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_pipelines() {
|
||||
assert!(parses("all | type:semantic | sort:weight | limit:50"));
|
||||
assert!(parses("all | !key:_* | sort:degree*0.5+isolation*0.5 | limit:10"));
|
||||
assert!(parses("all | provenance:observe | age:>1d | sort:timestamp | limit:20"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_stages_output() {
|
||||
// Ensure parse_stages produces expected Stage types
|
||||
let stages = parse_stages("all | type:semantic | limit:10").unwrap();
|
||||
assert_eq!(stages.len(), 3);
|
||||
assert!(matches!(stages[0], Stage::Generator(Generator::All)));
|
||||
assert!(matches!(stages[1], Stage::Filter(Filter::Type(_))));
|
||||
assert!(matches!(stages[2], Stage::Transform(Transform::Limit(10))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,8 +111,10 @@ impl Unconscious {
|
|||
}
|
||||
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
let max_concurrent = crate::config::get().llm_concurrency;
|
||||
|
||||
Self {
|
||||
agents, max_concurrent: 2,
|
||||
agents, max_concurrent,
|
||||
graph_health: None,
|
||||
last_health_check: None,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue