updates on the ai agents and time series prediction and updates on the api to

This commit is contained in:
2026-10-09 13:50:30 +05:30
parent 29690c56f2
commit 153be40e5c
42 changed files with 4889 additions and 186 deletions

92
models/ai_findings.go Normal file
View File

@@ -0,0 +1,92 @@
package models
import "time"
// AISkillFinding is one thing a console ops skill noticed.
//
// ─── Why this table exists ─────────────────────────────────────────────────
//
// The eight rule skills (SlaGuardian, DoorstepStall, FleetBalancer,
// HighValueCod, RiderBatterySafety, HubCongestion, LateDispatch, CashExposure)
// run in the operator's browser, on a 60-second React Query interval, and their
// findings were thrown away. Nothing persisted them, so three questions had no
// answer at all:
//
// Has this booking been flagged before, and how many times?
// Which skills fire most, and which are ignored every time they do?
// Did the proposal an operator carried out actually clear the finding?
//
// The third is the one that matters. The console already re-runs its scan after
// executing a proposal to see whether the finding disappears — that evidence
// existed for one render and then vanished. Persisting it turns "the skills seem
// useful" into something measurable, and it is the raw material for any later
// per-rider or per-zone memory.
//
// ─── What this is NOT ──────────────────────────────────────────────────────
//
// Not an exception. `consignmentexceptions` records a real operational problem
// with a parcel (Lost, Damaged, Misrouted) raised by a person. A finding is a
// rule noticing a pattern, most of which resolve themselves without anyone
// doing anything. Writing findings into that table would flood a queue people
// are supposed to work.
//
// Not a decision either: `agent_decisions` is the engine's model-made choices,
// with embeddings for retrieval. A finding is deterministic and carries no
// vector.
type AISkillFinding struct {
Findingid int `json:"findingid" gorm:"primaryKey;column:findingid;autoIncrement"`
// Skillid matches aiskills.skillid, so a finding can be grouped by the rule
// that raised it and read alongside the thresholds in force at the time.
Skillid string `json:"skillid" gorm:"column:skillid;size:64;not null;index:idx_aiskillfindings_skill_time,priority:1"`
// Fingerprint is what makes a finding the SAME finding across polls. The
// skills re-evaluate every 60 seconds and will re-raise an unchanged
// problem every time; without this the table would grow by the number of
// open findings per minute per operator with the page open.
//
// Computed by the console from the skill id and the scope's booking ids —
// deliberately not including severity or counts, which drift while the
// underlying problem stays the same.
Fingerprint string `json:"fingerprint" gorm:"column:fingerprint;size:120;not null;uniqueIndex:uq_aiskillfindings_fingerprint"`
Severity string `json:"severity" gorm:"column:severity;size:20"` // critical, warning, info
Title string `json:"title" gorm:"column:title"`
// Proposaltool is the verb the finding proposes, e.g. notifyRider,
// assignMiler, enforce_cash_handoff. Matches aitools.toolname where one
// exists; review-only verbs are recorded too, which is how "this skill
// keeps proposing something nobody can act on" becomes visible.
Proposaltool string `json:"proposaltool" gorm:"column:proposaltool;size:64;index"`
// Bookingcount and Scope: the count is for grouping, the scope is the
// booking ids as a JSON array so a specific parcel's history can be found.
// Not a join table — a finding's scope is read whole or not at all, and a
// row per booking per finding would multiply this table by average scope
// size for no query anyone needs.
Bookingcount int `json:"bookingcount" gorm:"column:bookingcount;default:0"`
Scope string `json:"scope" gorm:"column:scope;type:jsonb"`
Tenantid *int `json:"tenantid" gorm:"column:tenantid;index"`
// Firstseenat vs Lastseenat: an upsert on fingerprint bumps the last-seen
// and the count, so how LONG a finding has been open is answerable. That is
// the actual signal of an ignored finding — not that it exists, but that it
// has existed for six hours.
Firstseenat time.Time `json:"firstseenat" gorm:"column:firstseenat;not null;index:idx_aiskillfindings_skill_time,priority:2"`
Lastseenat time.Time `json:"lastseenat" gorm:"column:lastseenat;not null"`
Seencount int `json:"seencount" gorm:"column:seencount;default:1"`
// Actedat / Actedby / Actionresult record an operator carrying out the
// proposal. Null means nobody did — which is data, not a gap.
Actedat *time.Time `json:"actedat" gorm:"column:actedat"`
Actedby *int `json:"actedby" gorm:"column:actedby"`
Actionresult string `json:"actionresult" gorm:"column:actionresult;size:20"` // ok, partial, failed
// Clearedat is set when a later scan no longer raises this fingerprint.
// Paired with Actedat it answers the question the whole table is for: did
// acting clear it, or did it clear on its own?
Clearedat *time.Time `json:"clearedat" gorm:"column:clearedat"`
}
func (AISkillFinding) TableName() string { return "aiskillfindings" }