Path 1 negative signal + Path 2 pattern discovery + name validation
New: - /vectors/playbook_memory/patterns: meta-index pattern discovery. Given a query, finds top-K similar playbooks, pulls each endorsed worker's full workers_500k profile, aggregates shared traits (cert frequencies, skill frequencies, modal archetype, reliability distribution), returns a human-readable discovered_pattern. Surfaces signals operators didn't explicitly query — the original PRD's "identify things we didn't know" dimension. - /vectors/playbook_memory/mark_failed: records worker failures per (city, state, name). compute_boost_for applies 0.5^n penalty per recorded failure, so 3 failures quarter a worker's positive boost and 5 effectively zero it. Path 1 negative signal — recruiter trust depends on the system NOT recommending people who no-showed. - Bun /log_failure: validates failed_names against workers_500k (same ghost-guard as /log), forwards to /mark_failed. Improved: - /log now validates endorsed_names against workers_500k for the contract's city+state before seeding. Ghost names (names that don't correspond to real workers) are rejected in the response and excluded from the seed, preventing silent boost failures. - Bun /search auto-appends `CAST(availability AS DOUBLE) > 0.5` to sql_filter when the caller didn't constrain availability. Opt out with `include_unavailable: true`. Recruiter trust bug: surfacing already-placed workers breaks the first call. - DEFAULT_TOP_K_PLAYBOOKS 25 → 100. Direct cosine measurement showed similarities cluster 0.55-0.67 across all playbooks regardless of geo, so k=25 missed relevant geo-matched playbooks. Brute-force is still sub-ms at this size. Verified end-to-end on live data: - Ghost names rejected on /log + /log_failure - Availability filter drops unavailable workers from candidate pool - Pattern discovery on unseen Cleveland OH Welder query returned recurring skills (first aid 43%, grinder 43%, blueprint 43%) and modal archetype (specialist) across 20 semantically similar past playbooks in 0.24s - Negative signal: Helen Sanchez boost dropped +0.250 → +0.163 after 3 failures recorded via /log_failure (34% reduction)
This commit is contained in:
@@ -124,6 +124,7 @@ pub fn router(state: VectorState) -> Router {
|
||||
.route("/playbook_memory/seed", post(seed_playbook_memory))
|
||||
.route("/playbook_memory/persist_sql", post(persist_playbook_memory_sql))
|
||||
.route("/playbook_memory/patterns", post(discover_playbook_patterns))
|
||||
.route("/playbook_memory/mark_failed", post(mark_playbook_failed))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -2242,6 +2243,47 @@ async fn discover_playbook_patterns(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MarkFailedRequest {
|
||||
/// Operation text, same shape as seed: "fill: Role xN in City, ST"
|
||||
operation: String,
|
||||
/// Names of workers who didn't deliver on the fill.
|
||||
failed_names: Vec<String>,
|
||||
/// Short reason (no-show, fired, unreliable). Stored verbatim.
|
||||
#[serde(default)]
|
||||
reason: String,
|
||||
}
|
||||
|
||||
async fn mark_playbook_failed(
|
||||
State(state): State<VectorState>,
|
||||
Json(req): Json<MarkFailedRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// Parse city + state from the operation — mirrors seed's parser.
|
||||
let after_in = req.operation.split(" in ").nth(1).unwrap_or("");
|
||||
let mut parts = after_in.splitn(2, ',');
|
||||
let city = parts.next().map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
||||
let state_ = parts.next().map(|s|
|
||||
s.trim().chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>()
|
||||
).filter(|s| !s.is_empty());
|
||||
let (Some(city), Some(state_code)) = (city, state_) else {
|
||||
return Err((StatusCode::BAD_REQUEST,
|
||||
"operation must match 'fill: Role xN in City, ST' shape".into()));
|
||||
};
|
||||
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
let records: Vec<playbook_memory::FailureRecord> = req.failed_names.iter()
|
||||
.map(|n| playbook_memory::FailureRecord {
|
||||
city: city.clone(), state: state_code.clone(), name: n.clone(),
|
||||
reason: req.reason.clone(), timestamp: ts.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
match state.playbook_memory.mark_failures(records).await {
|
||||
Ok(added) => Ok(Json(serde_json::json!({ "added": added, "city": city, "state": state_code }))),
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn playbook_memory_stats(
|
||||
State(state): State<VectorState>,
|
||||
) -> impl IntoResponse {
|
||||
|
||||
Reference in New Issue
Block a user