agent build
This commit is contained in:
388
go-api/internal/tools/workspace.go
Normal file
388
go-api/internal/tools/workspace.go
Normal file
@@ -0,0 +1,388 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user