fix: handle PG INTERVAL type, robust compact LLM output + feedback

INTERVAL handling
- pg_value_to_json now decodes PG INTERVAL via PgInterval and renders
  it psql-style: `1 year 2 mons 3 days 04:05:06`. Previously
  AVG(timestamp - timestamp) and similar interval-returning queries
  showed `<unsupported type: INTERVAL>` in chat results.
- 7 unit tests covering zero, days-only, mixed, negative, microsecond
  fraction, and the singular/plural unit rules.

Compact reliability
- Sharper system prompt: explicitly instructs plain text starting with
  `-`, no JSON, no fences, no field names. qwen3-coder is heavily
  trained on the agent JSON protocol and was sometimes returning
  `{"action":"final","text":"..."}` even for the compact prompt.
- New clean_summary helper strips ``` fences (with or without lang
  identifier) and extracts the underlying string from a JSON envelope
  if the model still wraps the answer (looks for text/summary/content/
  answer/output keys). 6 unit tests.
- Frontend useChat.compact: success/no-op/error toasts via sonner so
  the user sees what happened. "Nothing to compact" appears when there
  is no older history beyond the last user turn (previously silent).

Verification: cargo test --lib 66 pass (+13), tsc clean, vitest 20
pass.
This commit is contained in:
2026-05-06 20:01:50 +03:00
parent 27fed0dbf8
commit 83f204816a
3 changed files with 196 additions and 6 deletions

View File

@@ -44,6 +44,11 @@ pub fn pg_value_to_json(row: &PgRow, index: usize) -> Value {
}
"DATE" => try_get!(chrono::NaiveDate),
"TIME" => try_get!(chrono::NaiveTime),
"INTERVAL" => match row.try_get::<Option<sqlx::postgres::types::PgInterval>, _>(index) {
Ok(Some(v)) => return Value::String(format_pg_interval(&v)),
Ok(None) => return Value::Null,
Err(_) => {}
},
"BYTEA" => match row.try_get::<Option<Vec<u8>>, _>(index) {
Ok(Some(v)) => return Value::String(format!("\\x{}", hex::encode(&v))),
Ok(None) => return Value::Null,
@@ -76,6 +81,44 @@ pub fn pg_value_to_json(row: &PgRow, index: usize) -> Value {
}
}
/// Render a PostgreSQL INTERVAL the same way `psql` does:
/// "1 year 2 mons 3 days 04:05:06.789012"
/// Components are emitted only when non-zero; the time component fires
/// whenever microseconds != 0 OR everything else is zero.
fn format_pg_interval(iv: &sqlx::postgres::types::PgInterval) -> String {
let years = iv.months / 12;
let months = iv.months % 12;
let mut parts: Vec<String> = Vec::new();
if years != 0 {
parts.push(format!("{} year{}", years, if years.abs() == 1 { "" } else { "s" }));
}
if months != 0 {
parts.push(format!("{} mon{}", months, if months.abs() == 1 { "" } else { "s" }));
}
if iv.days != 0 {
parts.push(format!("{} day{}", iv.days, if iv.days.abs() == 1 { "" } else { "s" }));
}
if iv.microseconds != 0 || parts.is_empty() {
let total_us = iv.microseconds.unsigned_abs();
let total_seconds = total_us / 1_000_000;
let micros = (total_us % 1_000_000) as u32;
let h = total_seconds / 3600;
let m = (total_seconds / 60) % 60;
let s = total_seconds % 60;
let sign = if iv.microseconds < 0 { "-" } else { "" };
let time_part = if micros == 0 {
format!("{}{:02}:{:02}:{:02}", sign, h, m, s)
} else {
format!(
"{}{:02}:{:02}:{:02}.{:06}",
sign, h, m, s, micros
)
};
parts.push(time_part);
}
parts.join(" ")
}
pub async fn execute_query_core(
state: &AppState,
connection_id: &str,
@@ -157,3 +200,57 @@ pub async fn execute_query(
) -> TuskResult<QueryResult> {
execute_query_core(&state, &connection_id, &sql).await
}
#[cfg(test)]
mod tests {
use super::format_pg_interval;
use sqlx::postgres::types::PgInterval;
#[test]
fn interval_zero_renders_as_zero_time() {
let iv = PgInterval { months: 0, days: 0, microseconds: 0 };
assert_eq!(format_pg_interval(&iv), "00:00:00");
}
#[test]
fn interval_pure_time_micros() {
// 1h 30m
let iv = PgInterval { months: 0, days: 0, microseconds: 90 * 60 * 1_000_000 };
assert_eq!(format_pg_interval(&iv), "01:30:00");
}
#[test]
fn interval_days_only() {
let iv = PgInterval { months: 0, days: 3, microseconds: 0 };
assert_eq!(format_pg_interval(&iv), "3 days");
}
#[test]
fn interval_one_day() {
let iv = PgInterval { months: 0, days: 1, microseconds: 0 };
assert_eq!(format_pg_interval(&iv), "1 day");
}
#[test]
fn interval_mixed_components() {
// 1 year 2 mons 3 days 04:05:06
let iv = PgInterval {
months: 14,
days: 3,
microseconds: ((4 * 3600) + (5 * 60) + 6) * 1_000_000,
};
assert_eq!(format_pg_interval(&iv), "1 year 2 mons 3 days 04:05:06");
}
#[test]
fn interval_negative_time() {
let iv = PgInterval { months: 0, days: 0, microseconds: -3_600_000_000 };
assert_eq!(format_pg_interval(&iv), "-01:00:00");
}
#[test]
fn interval_with_microseconds_fraction() {
let iv = PgInterval { months: 0, days: 0, microseconds: 1_500_000 };
assert_eq!(format_pg_interval(&iv), "00:00:01.500000");
}
}