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

View File

@@ -42,6 +42,8 @@ func Migrate(db *gorm.DB) error {
&models.CarrierPricing{},
&models.DoormilePricing{},
&models.AgentDecision{},
&models.AISkillFinding{},
&models.DemandForecast{},
&models.HubStaffAccount{},
&models.HubConversation{},
&models.HubMessage{},
@@ -89,6 +91,23 @@ func Migrate(db *gorm.DB) error {
utils.Error("⚠️ AI registry seed failed", "error", err.Error())
}
// pgvector must exist before the vector column can be declared. This was
// missing: the ALTER TABLE below has always run against a database where
// the extension was installed by hand, and its failure is logged
// non-fatally — so on any database where it was not, the column and its
// index silently never existed and every similarity query 500s.
//
// Creating an extension needs rights a plain application role may not
// have. A failure here is logged and not fatal for the same reason the
// column add is not: the registry and decision log are not on a booking's
// path, and the API must keep serving orders.
if res := db.Exec(`CREATE EXTENSION IF NOT EXISTS vector`); res.Error != nil {
utils.Error("⚠️ pgvector extension unavailable — decision memory and similarity search are disabled",
"error", res.Error)
} else {
utils.Info("✅ pgvector extension ready")
}
if res := db.Exec(`ALTER TABLE agent_decisions ADD COLUMN IF NOT EXISTS context_embedding vector(1536)`); res.Error != nil {
utils.Error("❌ Failed to add context_embedding column", "error", res.Error)
} else {
@@ -169,5 +188,105 @@ func Migrate(db *gorm.DB) error {
utils.Info("✅ bookingstageevents (bookingid, occurredat) index ready")
}
// Reverse logistics for clients onboarded before the field existed.
//
// They have no delivery category and were all using returns, so they must
// keep them — new information must not withdraw a working capability. Done
// as a one-off backfill rather than a column default: a default makes GORM
// omit an explicit `false` on every future INSERT, which is how Food
// clients were silently created with returns enabled.
//
// Guarded on deliverycategory being empty, so it only ever touches rows
// that predate the field and can never re-enable returns for a client an
// operator has deliberately switched off.
if res := db.Exec(`UPDATE tenants SET reverselogisticsenabled = true
WHERE COALESCE(deliverycategory, '') = ''`); res.Error != nil {
utils.Error("❌ Failed to backfill tenants.reverselogisticsenabled", "error", res.Error)
} else if res.RowsAffected > 0 {
utils.Info("✅ reverse logistics kept on for pre-existing clients", "rows", res.RowsAffected)
}
// ── ETA calibration (docs/prediction-plan.md rung 1.1) ──────────────────
//
// consignment_booking resolves a consignment to its booking. There is no
// consignments.bookingid column, and the link runs two ways: through
// bookingdestinations for multi-destination customer pickups, and through
// the legacy pickupbookings.consignmentid for console/express bookings and
// anything written before the fan-out existed. That legacy column names
// only the FIRST order of a multi-destination pickup, which is why the
// bookingdestinations path is tried first and the fallback is guarded.
//
// Every query that needs a consignment's booking goes through this view.
// Joining consignments to bookings by hand attaches features to the wrong
// parcel for multi-destination bookings, silently — the defect CLAUDE.md
// §8.5 describes and docs/prediction-plan.md records as hazard H4.
if res := db.Exec(`CREATE OR REPLACE VIEW consignment_booking AS
SELECT c.consignmentid,
COALESCE(bd.bookingid, pb.bookingid) AS bookingid,
bd.bookingdestinationid
FROM consignments c
LEFT JOIN bookingdestinations bd ON bd.consignmentid = c.consignmentid
LEFT JOIN pickupbookings pb ON pb.consignmentid = c.consignmentid
AND bd.bookingid IS NULL`); res.Error != nil {
utils.Error("❌ Failed to create consignment_booking view", "error", res.Error)
} else {
utils.Info("✅ consignment_booking view ready")
}
// etacalibration holds the grouped p80 of actual-over-routed duration that
// internal/prediction multiplies a routed ETA by. Written only by the
// calibration sweeper, read at boot. Empty is the normal state until
// ROUTE_OPTIMIZER_URL is configured and deliveries accumulate — the
// estimator falls back to the existing promise tables while it is.
if res := db.Exec(`CREATE TABLE IF NOT EXISTS etacalibration (
calibrationid SERIAL PRIMARY KEY,
scope VARCHAR(24) NOT NULL,
zone VARCHAR(8),
hourbucket INTEGER,
weekday INTEGER,
factor DOUBLE PRECISION NOT NULL,
handling DOUBLE PRECISION NOT NULL DEFAULT 0,
samples INTEGER NOT NULL,
refreshedat TIMESTAMPTZ NOT NULL
)`); res.Error != nil {
utils.Error("❌ Failed to create etacalibration table", "error", res.Error)
} else {
utils.Info("✅ etacalibration table ready")
}
if res := db.Exec(`CREATE INDEX IF NOT EXISTS idx_etacalibration_lookup
ON etacalibration (scope, zone, hourbucket, weekday)`); res.Error != nil {
utils.Error("❌ Failed to create etacalibration lookup index", "error", res.Error)
} else {
utils.Info("✅ etacalibration lookup index ready")
}
// The calibration scan joins deliveries to their assignment. Without this
// the refresh sequential-scans consignmenthistory on every run.
//
// CONCURRENTLY is not optional here. A plain CREATE INDEX takes a SHARE
// lock, which blocks INSERTs for as long as the build takes — and
// consignmenthistory gets a row on EVERY parcel status change. On a table
// of any size that means riders cannot complete deliveries while the
// migration runs: a revenue-path outage caused by an index for a
// background job that is switched off by default.
//
// The tradeoffs CONCURRENTLY brings, and why they are acceptable:
// - It cannot run inside a transaction. db.Exec is autocommit, so this
// is fine, but it must never be moved inside a tx block.
// - It can fail and leave an INVALID index behind, which is then not
// used by the planner and must be dropped by hand. That degrades the
// calibration scan to a sequential one; it does not affect any
// booking. The log line below names the recovery.
if res := db.Exec(`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_consignmenthistory_status_consignment
ON consignmenthistory (eventstatus, consignmentid)`); res.Error != nil {
utils.Error("⚠️ consignmenthistory index not created — the ETA calibration scan will be slower. "+
"If this left an INVALID index, drop it before retrying: "+
"DROP INDEX IF EXISTS idx_consignmenthistory_status_consignment",
"error", res.Error)
} else {
utils.Info("✅ consignmenthistory (eventstatus, consignmentid) index ready")
}
return nil
}