ask_operator: multi-select + free-text fallback
ask_operator now accepts a multi: bool. when true and options is
non-empty, the dashboard renders the choices as checkboxes — operator
picks any subset, answer comes back as a ', '-joined string. when
false (default), options are radio buttons.
independent of multi, a free-text input ('or type your own…') is
always rendered alongside options so the operator is never trapped
by an incomplete list. submit merges checked options + free text into
the single 'answer' field.
schema migration: operator_questions grows a multi INTEGER column
with a one-shot ALTER TABLE on open. backward compatible — old rows
default to 0 (not multi).
prompt + mcp tool description updated; existing dashboard css for
.qform was rewritten around the new vertical layout.
This commit is contained in:
parent
c337cc06f8
commit
8344dd9ab7
7 changed files with 130 additions and 35 deletions
|
|
@ -182,9 +182,16 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse
|
|||
},
|
||||
}
|
||||
}
|
||||
ManagerRequest::AskOperator { question, options } => {
|
||||
tracing::info!(%question, ?options, "manager: ask_operator");
|
||||
match coord.questions.submit(MANAGER_AGENT, question, options) {
|
||||
ManagerRequest::AskOperator {
|
||||
question,
|
||||
options,
|
||||
multi,
|
||||
} => {
|
||||
tracing::info!(%question, ?options, multi, "manager: ask_operator");
|
||||
match coord
|
||||
.questions
|
||||
.submit(MANAGER_AGENT, question, options, *multi)
|
||||
{
|
||||
Ok(id) => {
|
||||
tracing::info!(%id, "operator question queued");
|
||||
ManagerResponse::QuestionQueued { id }
|
||||
|
|
|
|||
|
|
@ -25,12 +25,29 @@ CREATE INDEX IF NOT EXISTS idx_operator_questions_pending
|
|||
ON operator_questions (id) WHERE answered_at IS NULL;
|
||||
";
|
||||
|
||||
/// Add the `multi` column to pre-existing databases. `ALTER TABLE ADD COLUMN`
|
||||
/// has no `IF NOT EXISTS` form in sqlite, so we check pragma_table_info first.
|
||||
fn ensure_multi_column(conn: &Connection) -> Result<()> {
|
||||
let has: bool = conn
|
||||
.prepare("SELECT 1 FROM pragma_table_info('operator_questions') WHERE name = 'multi'")?
|
||||
.exists([])?;
|
||||
if !has {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE operator_questions ADD COLUMN multi INTEGER NOT NULL DEFAULT 0;",
|
||||
)
|
||||
.context("add operator_questions.multi column")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub struct OpQuestion {
|
||||
pub id: i64,
|
||||
pub asker: String,
|
||||
pub question: String,
|
||||
pub options: Vec<String>,
|
||||
pub multi: bool,
|
||||
pub asked_at: i64,
|
||||
pub answered_at: Option<i64>,
|
||||
pub answer: Option<String>,
|
||||
|
|
@ -51,18 +68,25 @@ impl OperatorQuestions {
|
|||
.with_context(|| format!("open operator_questions db {}", path.display()))?;
|
||||
conn.execute_batch(SCHEMA)
|
||||
.context("apply operator_questions schema")?;
|
||||
ensure_multi_column(&conn).context("migrate operator_questions.multi")?;
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn submit(&self, asker: &str, question: &str, options: &[String]) -> Result<i64> {
|
||||
pub fn submit(
|
||||
&self,
|
||||
asker: &str,
|
||||
question: &str,
|
||||
options: &[String],
|
||||
multi: bool,
|
||||
) -> Result<i64> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let options_json = serde_json::to_string(options).unwrap_or_else(|_| "[]".into());
|
||||
conn.execute(
|
||||
"INSERT INTO operator_questions (asker, question, options_json, asked_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![asker, question, options_json, now_unix()],
|
||||
"INSERT INTO operator_questions (asker, question, options_json, multi, asked_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![asker, question, options_json, i64::from(multi), now_unix()],
|
||||
)?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
|
@ -95,7 +119,7 @@ impl OperatorQuestions {
|
|||
pub fn get(&self, id: i64) -> Result<Option<OpQuestion>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.query_row(
|
||||
"SELECT id, asker, question, options_json, asked_at, answered_at, answer
|
||||
"SELECT id, asker, question, options_json, multi, asked_at, answered_at, answer
|
||||
FROM operator_questions WHERE id = ?1",
|
||||
params![id],
|
||||
row_to_question,
|
||||
|
|
@ -107,7 +131,7 @@ impl OperatorQuestions {
|
|||
pub fn pending(&self) -> Result<Vec<OpQuestion>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, asker, question, options_json, asked_at, answered_at, answer
|
||||
"SELECT id, asker, question, options_json, multi, asked_at, answered_at, answer
|
||||
FROM operator_questions
|
||||
WHERE answered_at IS NULL
|
||||
ORDER BY id ASC",
|
||||
|
|
@ -121,14 +145,16 @@ impl OperatorQuestions {
|
|||
fn row_to_question(row: &rusqlite::Row<'_>) -> rusqlite::Result<OpQuestion> {
|
||||
let options_json: String = row.get(3)?;
|
||||
let options: Vec<String> = serde_json::from_str(&options_json).unwrap_or_default();
|
||||
let multi: i64 = row.get(4)?;
|
||||
Ok(OpQuestion {
|
||||
id: row.get(0)?,
|
||||
asker: row.get(1)?,
|
||||
question: row.get(2)?,
|
||||
options,
|
||||
asked_at: row.get(4)?,
|
||||
answered_at: row.get(5)?,
|
||||
answer: row.get(6)?,
|
||||
multi: multi != 0,
|
||||
asked_at: row.get(5)?,
|
||||
answered_at: row.get(6)?,
|
||||
answer: row.get(7)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue