Skip to content

Commit b4543ba

Browse files
salmanapSalman ParachaMeiyuZhongnehcgs
authored
Introduce signals change (#655)
* adding support for signals * reducing false positives for signals like positive interaction * adding docs. Still need to fix the messages list, but waiting on PR #621 * Improve frustration detection: normalize contractions and refine punctuation * Further refine test cases with longer messages * minor doc changes * fixing echo statement for build * fixing the messages construction and using the trait for signals * update signals docs * fixed some minor doc changes * added more tests and fixed docuemtnation. PR 100% ready * made fixes based on PR comments * Optimize latency 1. replace sliding window approach with trigram containment check 2. add code to pre-compute ngrams for patterns * removed some debug statements to make tests easier to read * PR comments to make ObservableStreamProcessor accept optonal Vec<Messagges> * fixed PR comments --------- Co-authored-by: Salman Paracha <salmanparacha@MacBook-Pro-342.local> Co-authored-by: MeiyuZhong <mariazhong9612@gmail.com> Co-authored-by: nehcgs <54548843+nehcgs@users.noreply.github.com>
1 parent 57327ba commit b4543ba

17 files changed

Lines changed: 3972 additions & 191 deletions

File tree

cli/planoai/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def main(ctx, version):
8181

8282
@click.command()
8383
def build():
84-
"""Build Arch from source. Works from any directory within the repo."""
84+
"""Build Plano from source. Works from any directory within the repo."""
8585

8686
# Find the repo root
8787
repo_root = find_repo_root()
@@ -112,7 +112,7 @@ def build():
112112
],
113113
check=True,
114114
)
115-
click.echo("archgw image built successfully.")
115+
click.echo("plano image built successfully.")
116116
except subprocess.CalledProcessError as e:
117117
click.echo(f"Error building plano image: {e}")
118118
sys.exit(1)

crates/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/brightstaff/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ reqwest = { version = "0.12.15", features = ["stream"] }
3030
serde = { version = "1.0.219", features = ["derive"] }
3131
serde_json = "1.0.140"
3232
serde_with = "3.13.0"
33+
strsim = "0.11"
3334
serde_yaml = "0.9.34"
3435
thiserror = "2.0.12"
3536
tokio = { version = "1.44.2", features = ["full"] }

crates/brightstaff/src/handlers/llm.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ pub async fn llm_chat(
111111
.get_recent_user_message()
112112
.map(|msg| truncate_message(&msg, 50));
113113

114+
// Extract messages for signal analysis (clone before moving client_request)
115+
let messages_for_signals = client_request.get_messages();
116+
114117
client_request.set_model(resolved_model.clone());
115118
if client_request.remove_metadata_key("archgw_preference_config") {
116119
debug!(
@@ -292,6 +295,7 @@ pub async fn llm_chat(
292295
operation_component::LLM,
293296
llm_span,
294297
request_start_time,
298+
Some(messages_for_signals),
295299
);
296300

297301
// === v1/responses state management: Wrap with ResponsesStateProcessor ===

crates/brightstaff/src/handlers/utils.rs

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ use tokio_stream::wrappers::ReceiverStream;
1010
use tokio_stream::StreamExt;
1111
use tracing::warn;
1212

13-
// Import tracing constants
14-
use crate::tracing::{error, llm};
13+
// Import tracing constants and signals
14+
use crate::signals::{InteractionQuality, SignalAnalyzer, TextBasedSignalAnalyzer, FLAG_MARKER};
15+
use crate::tracing::{error, llm, signals as signal_constants};
16+
use hermesllm::apis::openai::Message;
1517

1618
/// Trait for processing streaming chunks
1719
/// Implementors can inject custom logic during streaming (e.g., hallucination detection, logging)
@@ -38,6 +40,7 @@ pub struct ObservableStreamProcessor {
3840
chunk_count: usize,
3941
start_time: Instant,
4042
time_to_first_token: Option<u128>,
43+
messages: Option<Vec<Message>>,
4144
}
4245

4346
impl ObservableStreamProcessor {
@@ -48,11 +51,13 @@ impl ObservableStreamProcessor {
4851
/// * `service_name` - The service name for this span (e.g., "archgw(llm)")
4952
/// * `span` - The span to finalize after streaming completes
5053
/// * `start_time` - When the request started (for duration calculation)
54+
/// * `messages` - Optional conversation messages for signal analysis
5155
pub fn new(
5256
collector: Arc<TraceCollector>,
5357
service_name: impl Into<String>,
5458
span: Span,
5559
start_time: Instant,
60+
messages: Option<Vec<Message>>,
5661
) -> Self {
5762
Self {
5863
collector,
@@ -62,6 +67,7 @@ impl ObservableStreamProcessor {
6267
chunk_count: 0,
6368
start_time,
6469
time_to_first_token: None,
70+
messages,
6571
}
6672
}
6773
}
@@ -133,6 +139,94 @@ impl StreamProcessor for ObservableStreamProcessor {
133139
}
134140
}
135141

142+
// Analyze signals if messages are available and add to span attributes
143+
if let Some(ref messages) = self.messages {
144+
let analyzer: Box<dyn SignalAnalyzer> = Box::new(TextBasedSignalAnalyzer::new());
145+
let report = analyzer.analyze(messages);
146+
147+
// Add overall quality
148+
self.span.attributes.push(Attribute {
149+
key: signal_constants::QUALITY.to_string(),
150+
value: AttributeValue {
151+
string_value: Some(format!("{:?}", report.overall_quality)),
152+
},
153+
});
154+
155+
// Add repair/follow-up metrics if concerning
156+
if report.follow_up.is_concerning || report.follow_up.repair_count > 0 {
157+
self.span.attributes.push(Attribute {
158+
key: signal_constants::REPAIR_COUNT.to_string(),
159+
value: AttributeValue {
160+
string_value: Some(report.follow_up.repair_count.to_string()),
161+
},
162+
});
163+
164+
self.span.attributes.push(Attribute {
165+
key: signal_constants::REPAIR_RATIO.to_string(),
166+
value: AttributeValue {
167+
string_value: Some(format!("{:.3}", report.follow_up.repair_ratio)),
168+
},
169+
});
170+
}
171+
172+
// Add flag marker to operation name if any concerning signal is detected
173+
let should_flag = report.frustration.has_frustration
174+
|| report.repetition.has_looping
175+
|| report.escalation.escalation_requested
176+
|| matches!(
177+
report.overall_quality,
178+
InteractionQuality::Poor | InteractionQuality::Severe
179+
);
180+
181+
if should_flag {
182+
// Prepend flag marker to the operation name
183+
self.span.name = format!("{} {}", self.span.name, FLAG_MARKER);
184+
}
185+
186+
// Add key signal metrics
187+
if report.frustration.has_frustration {
188+
self.span.attributes.push(Attribute {
189+
key: signal_constants::FRUSTRATION_COUNT.to_string(),
190+
value: AttributeValue {
191+
string_value: Some(report.frustration.frustration_count.to_string()),
192+
},
193+
});
194+
self.span.attributes.push(Attribute {
195+
key: signal_constants::FRUSTRATION_SEVERITY.to_string(),
196+
value: AttributeValue {
197+
string_value: Some(report.frustration.severity.to_string()),
198+
},
199+
});
200+
}
201+
202+
if report.repetition.has_looping {
203+
self.span.attributes.push(Attribute {
204+
key: signal_constants::REPETITION_COUNT.to_string(),
205+
value: AttributeValue {
206+
string_value: Some(report.repetition.repetition_count.to_string()),
207+
},
208+
});
209+
}
210+
211+
if report.escalation.escalation_requested {
212+
self.span.attributes.push(Attribute {
213+
key: signal_constants::ESCALATION_REQUESTED.to_string(),
214+
value: AttributeValue {
215+
string_value: Some("true".to_string()),
216+
},
217+
});
218+
}
219+
220+
if report.positive_feedback.has_positive_feedback {
221+
self.span.attributes.push(Attribute {
222+
key: signal_constants::POSITIVE_FEEDBACK_COUNT.to_string(),
223+
value: AttributeValue {
224+
string_value: Some(report.positive_feedback.positive_count.to_string()),
225+
},
226+
});
227+
}
228+
}
229+
136230
// Record the finalized span
137231
self.collector
138232
.record_span(&self.service_name, self.span.clone());

crates/brightstaff/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod handlers;
22
pub mod router;
3+
pub mod signals;
34
pub mod state;
45
pub mod tracing;
56
pub mod utils;

0 commit comments

Comments
 (0)