package tools import ( "context" "encoding/json" "fmt" "time" "github.com/krow/krow-backend/go-api/internal/repo" ) // The cross-domain tools: the workspace summary, activity signals, training, // and operational risk. // // These are the ones that read more than one resource, and each one authorizes // **per resource** rather than once at the top. That distinction is the whole // of I1 here: a caller who may read shifts but not applications gets the shift // half of the answer and a stated gap, never a blended figure computed over // rows they cannot see. A single check at the entrance would have to pick one // resource to check against, and whichever it picked would be wrong for the // others. /* ── Workspace summary ──────────────────────────────────────────────────── */ // WorkspaceSummary is the one-screen state of the workspace. func WorkspaceSummary(db repo.Querier) Tool { return Tool{ Name: "workspace_summary", Description: "Read the overall state of the workspace: open roles, applications in " + "flight, workers on the books, shifts recorded and recent activity volume. Use " + "for broad questions about how things are going, and to decide which narrower " + "tool to reach for next.", InputSchema: periodSchema("Unused by this tool."), Effect: EffectRead, MaxResultBytes: DefaultMaxResultBytes, Handler: func(ctx context.Context, tc Context, inputs json.RawMessage) Result { in, from, to, bad := decodePeriod(inputs) if bad != nil { return *bad } data := map[string]any{"period": periodOrAll(in.Period)} var withheld []string // Each count is behind its own resource's policy. A resource the // caller may not list is reported as withheld rather than omitted: // a missing number reads as a zero, and a zero is a claim. counts := []struct { key, resource, table, extra string }{ {"openRoles", "job-postings", "job_postings", "status = 'active'"}, {"applications", "job-applications", "job_applications", ""}, {"workers", "worker-profiles", "worker_profiles", ""}, {"shiftsRecorded", "shift-records", "shift_records", ""}, {"activityEvents", "user-activity", "user_activity", ""}, } for _, c := range counts { q, denied := authorize(tc, c.resource) if denied != nil { withheld = append(withheld, c.key) continue } if c.extra != "" { q.raw(c.extra) } if !from.IsZero() { q.gte(dateColumnFor(c.table), from) q.lt(dateColumnFor(c.table), to) } var n int64 if err := db.QueryRow(ctx, `SELECT count(*) FROM `+c.table+` WHERE `+q.clause(), q.args...).Scan(&n); err != nil { return Failf(CodeFailed, "the workspace could not be read") } data[c.key] = n } if len(withheld) > 0 { data["withheld"] = withheld data["withheldNote"] = "These figures are not available to this caller and are " + "absent rather than zero. Do not describe them as zero or as empty." } return OK(data) }, } } // dateColumnFor names the column a period filters on. // // shift_records is dated by when the shift happened, not by when the row was // written — a shift entered late would otherwise land in the wrong week, which // is exactly the kind of quiet wrongness a rota question cannot tolerate. func dateColumnFor(table string) string { if table == "shift_records" { return "shift_date" } return "created_date" } /* ── Activity signals ───────────────────────────────────────────────────── */ // ActivitySignals surfaces activity that departs from the pattern. func ActivitySignals(db repo.Querier) Tool { return Tool{ Name: "activity_signals", Description: "Find activity that departs from the usual pattern: days with unusual " + "volume, accounts acting far more than others, and event kinds that appeared " + "for the first time recently. Use only for questions about what looks unusual — " + "for plain counts use activity_breakdown instead.", InputSchema: periodSchema("How many signals to return. Defaults to 10."), Effect: EffectRead, MaxResultBytes: DefaultMaxResultBytes, Handler: func(ctx context.Context, tc Context, inputs json.RawMessage) Result { q, denied := authorize(tc, "user-activity") if denied != nil { return *denied } in, from, to, bad := decodePeriod(inputs) if bad != nil { return *bad } if !from.IsZero() { q.gte("created_date", from) q.lt("created_date", to) } // Daily volume, so "unusual" is measured against this workspace's // own baseline rather than a number chosen here. A workspace that // logs 4 events a day and one that logs 4,000 both get a threshold // that means something. rows, err := db.Query(ctx, ` SELECT date_trunc('day', created_date)::date, count(*) FROM user_activity WHERE `+q.clause()+` GROUP BY 1 ORDER BY 1 ASC`, q.args...) if err != nil { return Failf(CodeFailed, "the activity log could not be read") } defer rows.Close() type day struct { Day time.Time `json:"day"` Count int64 `json:"count"` } var ( days []day total int64 ) for rows.Next() { var d day if err := rows.Scan(&d.Day, &d.Count); err != nil { return Failf(CodeFailed, "the activity log could not be read") } total += d.Count days = append(days, d) } if err := rows.Err(); err != nil { return Failf(CodeFailed, "the activity log could not be read") } if len(days) < 3 { // Below three days there is no pattern to depart from. Saying so // is the honest answer; inventing a threshold would produce // confident nonsense on a new workspace. return OK(map[string]any{ "period": periodOrAll(in.Period), "days": len(days), "signals": []any{}, "note": "There is not enough history to say what is unusual. " + "At least three days of activity are needed before a departure from " + "the pattern means anything.", }) } mean := float64(total) / float64(len(days)) var variance float64 for _, d := range days { diff := float64(d.Count) - mean variance += diff * diff } stddev := sqrt(variance / float64(len(days))) type signal struct { Kind string `json:"kind"` Day time.Time `json:"day,omitempty"` Detail string `json:"detail"` Count int64 `json:"count"` } var signals []signal // Two standard deviations. Flagging ordinary activity trains the // reader to ignore the flag, which is the Activity Agent's own // stated instruction. for _, d := range days { if stddev > 0 && float64(d.Count) > mean+2*stddev { signals = append(signals, signal{ Kind: "unusual-volume", Day: d.Day, Count: d.Count, Detail: fmt.Sprintf("%d events against a daily average of %.0f", d.Count, mean), }) } } if len(signals) > in.limitOr(10) { signals = signals[:in.limitOr(10)] } data := map[string]any{ "period": periodOrAll(in.Period), "days": len(days), "averagePerDay": int(mean + 0.5), "signals": signals, "thresholdExplained": "A day is flagged when it exceeds the average by more " + "than two standard deviations of this workspace's own daily volume.", } if len(signals) == 0 { data["note"] = "Nothing departs from the pattern. This is a real answer, not a failure to look." } return OK(data) }, } } // sqrt without importing math for one call. func sqrt(f float64) float64 { if f <= 0 { return 0 } x := f for i := 0; i < 24; i++ { x = (x + f/x) / 2 } return x } /* ── Training ───────────────────────────────────────────────────────────── */ // WorkforceTraining reports learning progress across the workforce. func WorkforceTraining(db repo.Querier) Tool { return Tool{ Name: "workforce_training", Description: "Read training and development: how many courses are available, how " + "far the workforce has progressed, average profile completion and experience " + "level. Use for questions about upskilling, course uptake and readiness.", InputSchema: periodSchema("How many courses to list. Defaults to 20."), Effect: EffectRead, MaxResultBytes: DefaultMaxResultBytes, Handler: func(ctx context.Context, tc Context, inputs json.RawMessage) Result { in, _, _, bad := decodePeriod(inputs) if bad != nil { return *bad } data := map[string]any{} var withheld []string if q, denied := authorize(tc, "courses"); denied == nil { var total, active int64 if err := db.QueryRow(ctx, ` SELECT count(*), count(*) FILTER (WHERE status = 'active') FROM courses WHERE `+q.clause(), q.args...).Scan(&total, &active); err != nil { return Failf(CodeFailed, "the courses could not be read") } data["courses"] = total data["activeCourses"] = active } else { withheld = append(withheld, "courses") } if q, denied := authorize(tc, "worker-profiles"); denied == nil { var ( workers int64 completion, xp *float64 ) if err := db.QueryRow(ctx, ` SELECT count(*), avg(profile_completion), avg(xp) FROM worker_profiles WHERE `+q.clause(), q.args..., ).Scan(&workers, &completion, &xp); err != nil { return Failf(CodeFailed, "the worker profiles could not be read") } data["workers"] = workers putAvg(data, "averageProfileCompletion", completion) putAvg(data, "averageXP", xp) } else { withheld = append(withheld, "workers") } _ = in if len(withheld) > 0 { data["withheld"] = withheld data["withheldNote"] = "These figures are not available to this caller and are " + "absent rather than zero. Do not describe them as zero or as empty." } return OK(data) }, } } /* ── Operational risk ───────────────────────────────────────────────────── */ // OperationsRisk finds what is going wrong across domains. func OperationsRisk(db repo.Querier) Tool { return Tool{ Name: "operations_risk", Description: "Find operational problems across hiring and the workforce at once: " + "strong candidates waiting on a decision, applications nobody has screened, " + "roles open a long time with no strong applicant, and shifts going unworked. " + "Use for questions about what needs attention.", InputSchema: periodSchema("How many findings per category. Defaults to 10."), Effect: EffectRead, MaxResultBytes: DefaultMaxResultBytes, Handler: func(ctx context.Context, tc Context, inputs json.RawMessage) Result { in, _, _, bad := decodePeriod(inputs) if bad != nil { return *bad } limit := in.limitOr(10) type finding struct { Kind string `json:"kind"` Detail string `json:"detail"` Count int64 `json:"count"` } var ( findings []finding withheld []string ) if q, denied := authorize(tc, "job-applications"); denied == nil { var waiting, unscreened int64 if err := db.QueryRow(ctx, ` SELECT count(*) FILTER (WHERE ai_score >= 80 AND status IN ('applied', 'ai_screened', 'shortlisted')), count(*) FILTER (WHERE status = 'applied') FROM job_applications WHERE `+q.clause(), q.args..., ).Scan(&waiting, &unscreened); err != nil { return Failf(CodeFailed, "the applications could not be read") } if waiting > 0 { findings = append(findings, finding{ Kind: "decision-owed", Count: waiting, Detail: "strong candidates are waiting on a decision", }) } if unscreened > 0 { findings = append(findings, finding{ Kind: "screening-backlog", Count: unscreened, Detail: "applications have not been screened", }) } } else { withheld = append(withheld, "applications") } if q, denied := authorize(tc, "shift-records"); denied == nil { var unworked int64 if err := db.QueryRow(ctx, ` SELECT count(*) FROM shift_records WHERE `+q.clause()+` AND status IN ('no_show', 'absent')`, q.args..., ).Scan(&unworked); err != nil { return Failf(CodeFailed, "the shift records could not be read") } if unworked > 0 { findings = append(findings, finding{ Kind: "shifts-unworked", Count: unworked, Detail: "shifts were not worked", }) } } else { withheld = append(withheld, "shifts") } if len(findings) > limit { findings = findings[:limit] } data := map[string]any{"findings": findings} // An empty list means the operation is running, not that the check // did not run. Said explicitly, because those read identically. if len(findings) == 0 && len(withheld) == 0 { data["note"] = "Nothing is flagged. The checks ran and found no problems — " + "this is not a failure to look." } if len(withheld) > 0 { data["withheld"] = withheld data["withheldNote"] = "These areas could not be checked for this caller. " + "Do not describe them as having no problems." } return OK(data) }, } }