feat(feedback): Phase 2 — HITL thumbs + correction capture with prompt-conditioning loop
Closes the human-in-the-loop gap from docs/brief/feature-set.md and Phase 2 of the 2026-04-23 feature-complete roadmap. Storage (kon-storage): - Migration v10 adds the `feedback` table: (target_type, target_id, rating, original_text, corrected_text, context_json, profile_id, created_at) with CHECK constraints on target_type and rating, plus indexes on (target_type, rating, created_at DESC) for prompt-time retrieval and (profile_id, target_type, created_at DESC) for per-profile scoping. - New public API: `FeedbackTargetType`, `RecordFeedbackParams`, `FeedbackRow`, `record_feedback`, `list_feedback_examples`. - Tests updated — the RB-02 rollback regression now discovers the real max version at runtime instead of hard-coding v10 for its poison migration. LLM (kon-llm): - `prompts::FeedbackExample` — local shape for few-shot exemplars so kon-llm stays independent of kon-storage. - `prompts::build_conditioned_system_prompt` — appends a "here is the style this user prefers" block to the base system prompt when examples are available; returns the base prompt unchanged when empty, so new users and early sessions see generic output. - `LlmEngine::decompose_task_with_feedback` and `LlmEngine::extract_tasks_with_feedback` thread examples through to the builder. The old one-arg variants are preserved and now call through with an empty slice. - 4 unit tests covering empty, empty-input-skip, correction-wins, and thumbs-up-only fallback. Tauri (src-tauri): - New commands::feedback module: `record_feedback`, `list_feedback_examples_cmd`. - `decompose_and_store` and `extract_tasks_from_transcript_cmd` now fetch the last 5 positive/neutral feedback rows for their target type and pass them through to the LLM, wiring the learning loop end-to-end. - Shared `to_llm_examples` helper parses the `context_json.input` field (where the recorder stashes the parent task text / transcript chunk) back into the exemplar shape. Frontend (MicroSteps.svelte): - Thumbs-up and thumbs-down buttons on every micro-step row. Hover-revealed; the vote recolours the icon; clicking again clears the local highlight (the row itself stays in the audit trail). - Pencil icon + double-click to edit step text. Save flows through update_task_cmd for persistence and records a correction feedback row with (original_text, corrected_text) — the highest-value training signal. - Parent task text is captured in context_json.input at record time so the prompt builder can reconstruct the (input, preferred-output) pair on subsequent decompositions. - Feedback capture is best-effort — a record_feedback failure never interrupts the primary action. What's deferred to a later phase: - Thumbs + corrections on extracted tasks (same pipeline, different surface — probably TasksPage after the AI-extraction path) - Thumbs on transcript cleanup output - Semantic retrieval over the feedback corpus (once there is enough data to justify embedding infrastructure; the storage shape is already ready for it)
This commit is contained in:
@@ -240,11 +240,30 @@ impl LlmEngine {
|
||||
}
|
||||
|
||||
pub fn decompose_task(&self, task_text: &str) -> Result<Vec<String>, EngineError> {
|
||||
self.decompose_task_with_feedback(task_text, &[])
|
||||
}
|
||||
|
||||
/// Same as `decompose_task` but allows callers to pass recent HITL
|
||||
/// feedback rows so the system prompt gets conditioned on the
|
||||
/// user's preferred decomposition style. The `examples` vec is
|
||||
/// rendered into a few-shot block appended to the base system
|
||||
/// prompt by `prompts::build_conditioned_system_prompt`.
|
||||
///
|
||||
/// Callers should pass most-recent-first; older examples still
|
||||
/// participate but weigh less because of their position in the
|
||||
/// prompt. Empty slice keeps behaviour identical to `decompose_task`.
|
||||
pub fn decompose_task_with_feedback(
|
||||
&self,
|
||||
task_text: &str,
|
||||
examples: &[prompts::FeedbackExample],
|
||||
) -> Result<Vec<String>, EngineError> {
|
||||
let model = self.loaded_model_arc()?;
|
||||
let system =
|
||||
prompts::build_conditioned_system_prompt(prompts::DECOMPOSE_TASK_SYSTEM, examples);
|
||||
let prompt = render_chat_prompt(
|
||||
&model,
|
||||
&[
|
||||
("system", prompts::DECOMPOSE_TASK_SYSTEM),
|
||||
("system", system.as_str()),
|
||||
("user", &format!("Task: {task_text}")),
|
||||
],
|
||||
)?;
|
||||
@@ -261,15 +280,27 @@ impl LlmEngine {
|
||||
}
|
||||
|
||||
pub fn extract_tasks(&self, transcript: &str) -> Result<Vec<String>, EngineError> {
|
||||
self.extract_tasks_with_feedback(transcript, &[])
|
||||
}
|
||||
|
||||
/// Feedback-conditioned variant of `extract_tasks`. See
|
||||
/// `decompose_task_with_feedback` for the `examples` semantics.
|
||||
pub fn extract_tasks_with_feedback(
|
||||
&self,
|
||||
transcript: &str,
|
||||
examples: &[prompts::FeedbackExample],
|
||||
) -> Result<Vec<String>, EngineError> {
|
||||
if transcript.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let model = self.loaded_model_arc()?;
|
||||
let system =
|
||||
prompts::build_conditioned_system_prompt(prompts::EXTRACT_TASKS_SYSTEM, examples);
|
||||
let prompt = render_chat_prompt(
|
||||
&model,
|
||||
&[
|
||||
("system", prompts::EXTRACT_TASKS_SYSTEM),
|
||||
("system", system.as_str()),
|
||||
("user", &format!("Transcript:\n{transcript}")),
|
||||
],
|
||||
)?;
|
||||
|
||||
Reference in New Issue
Block a user