first commit
This commit is contained in:
125
base44/functions/fillEscalationMonitor/entry.ts
Normal file
125
base44/functions/fillEscalationMonitor/entry.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.23';
|
||||
|
||||
/**
|
||||
* Fill Escalation Monitor
|
||||
* Runs every 15 minutes. Detects events at risk of not being filled
|
||||
* and creates escalation alerts in ActivityLog.
|
||||
*
|
||||
* Fill probability formula:
|
||||
* score = (assigned / requested) * 60
|
||||
* + timeBuffer * 20 (more time = more chance to fill)
|
||||
* + dayOfWeek * 20 (based on historical fill rate for that day)
|
||||
*
|
||||
* If fillProbability < 60% → escalation alert
|
||||
*/
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const base44 = createClientFromRequest(req);
|
||||
|
||||
// Allow scheduled/service calls without user auth
|
||||
const now = new Date();
|
||||
const in24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
const in4h = new Date(now.getTime() + 4 * 60 * 60 * 1000);
|
||||
|
||||
// Fetch all upcoming active events
|
||||
const allEvents = await base44.asServiceRole.entities.Event.list();
|
||||
const upcoming = allEvents.filter(e => {
|
||||
if (!e.date) return false;
|
||||
const eventDate = new Date(e.date);
|
||||
return eventDate > now && eventDate <= in24h &&
|
||||
["Active", "Pending", "Confirmed", "Draft"].includes(e.status);
|
||||
});
|
||||
|
||||
// Historical fill rate by day of week
|
||||
const completed = allEvents.filter(e => e.status === "Completed" && e.date);
|
||||
const dayFillRate = Array(7).fill(null).map((_, dow) => {
|
||||
const dayEvents = completed.filter(e => new Date(e.date).getDay() === dow);
|
||||
if (dayEvents.length === 0) return 0.75; // default 75%
|
||||
const filled = dayEvents.filter(e => (e.assigned_staff?.length || 0) >= (e.requested || 1));
|
||||
return filled.length / dayEvents.length;
|
||||
});
|
||||
|
||||
const escalations = [];
|
||||
const alerts = [];
|
||||
|
||||
for (const event of upcoming) {
|
||||
const assigned = event.assigned_staff?.length || 0;
|
||||
const requested = event.requested || 0;
|
||||
if (requested === 0) continue;
|
||||
|
||||
const fillRatio = assigned / requested;
|
||||
const eventDate = new Date(event.date);
|
||||
const hoursUntil = (eventDate - now) / (1000 * 60 * 60);
|
||||
const dow = eventDate.getDay();
|
||||
|
||||
// Time buffer score: 0 = within 2h (critical), 1 = 24h out (comfortable)
|
||||
const timeBuffer = Math.min(hoursUntil / 24, 1);
|
||||
|
||||
// Fill probability 0-100
|
||||
const fillProbability = Math.round(
|
||||
fillRatio * 60 +
|
||||
timeBuffer * 20 +
|
||||
dayFillRate[dow] * 20
|
||||
);
|
||||
|
||||
const isCritical = hoursUntil <= 4 && fillRatio < 1;
|
||||
const isAtRisk = fillProbability < 60;
|
||||
|
||||
if (isCritical || isAtRisk) {
|
||||
escalations.push({
|
||||
event_id: event.id,
|
||||
event_name: event.event_name,
|
||||
hours_until: Math.round(hoursUntil),
|
||||
fill_ratio: fillRatio,
|
||||
fill_probability: fillProbability,
|
||||
assigned,
|
||||
requested,
|
||||
is_critical: isCritical,
|
||||
});
|
||||
|
||||
// Create ActivityLog alert (avoid duplicates — check if one exists in last 2h)
|
||||
const existingAlerts = await base44.asServiceRole.entities.ActivityLog.filter({
|
||||
related_entity_id: event.id,
|
||||
activity_type: "event_updated",
|
||||
});
|
||||
|
||||
const recentAlert = existingAlerts.find(a => {
|
||||
const created = new Date(a.created_date);
|
||||
return (now - created) < 2 * 60 * 60 * 1000; // within 2h
|
||||
});
|
||||
|
||||
if (!recentAlert) {
|
||||
const severity = isCritical ? "🚨 CRITICAL" : "⚠️ AT RISK";
|
||||
const gapCount = requested - assigned;
|
||||
const description = isCritical
|
||||
? `${event.event_name} starts in ${Math.round(hoursUntil)}h — ${gapCount} position${gapCount > 1 ? "s" : ""} still open. Fill immediately.`
|
||||
: `${event.event_name} has a ${fillProbability}% fill probability. ${gapCount} position${gapCount > 1 ? "s" : ""} unfilled with ${Math.round(hoursUntil)}h remaining.`;
|
||||
|
||||
alerts.push(
|
||||
base44.asServiceRole.entities.ActivityLog.create({
|
||||
title: `${severity}: ${event.event_name} needs staffing`,
|
||||
description,
|
||||
activity_type: "event_updated",
|
||||
related_entity_type: "event",
|
||||
related_entity_id: event.id,
|
||||
action_link: `EventDetail?id=${event.id}`,
|
||||
action_label: "Assign Staff Now",
|
||||
user_id: event.created_by || "system",
|
||||
is_read: false,
|
||||
icon_type: "alert",
|
||||
icon_color: isCritical ? "red" : "yellow",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(alerts);
|
||||
|
||||
return Response.json({
|
||||
checked: upcoming.length,
|
||||
escalations: escalations.length,
|
||||
details: escalations,
|
||||
timestamp: now.toISOString(),
|
||||
});
|
||||
});
|
||||
504
base44/functions/queryWorkforceGraph/entry.ts
Normal file
504
base44/functions/queryWorkforceGraph/entry.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.31';
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const base44 = createClientFromRequest(req);
|
||||
const user = await base44.auth.me();
|
||||
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { query_type, entity_id, entity_type, depth = 2 } = body;
|
||||
|
||||
const entities = base44.asServiceRole.entities;
|
||||
const graph = { nodes: [], edges: [] };
|
||||
const visited = new Set();
|
||||
|
||||
async function addNode(type, id, label, data = {}) {
|
||||
const key = `${type}:${id}`;
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
graph.nodes.push({ type, id, label, data });
|
||||
}
|
||||
|
||||
function edge(from, to, label) {
|
||||
graph.edges.push({ from, to, label });
|
||||
}
|
||||
|
||||
// ─── CONTRACT ───────────────────────────────────────────────
|
||||
// Contract → Vendor, Client, Sector, RateCard
|
||||
async function traverseContract(contractId) {
|
||||
const contract = await entities.Contract.get(contractId);
|
||||
if (!contract) return;
|
||||
await addNode('Contract', contract.id, `Contract: ${contract.contract_name || contract.contract_number || ''}`, contract);
|
||||
|
||||
if (contract.vendor_id) {
|
||||
await addNode('Vendor', contract.vendor_id, `Vendor: ${contract.vendor_name || ''}`);
|
||||
edge(`Contract:${contract.id}`, `Vendor:${contract.vendor_id}`, 'vendor_party');
|
||||
}
|
||||
if (contract.client_id) {
|
||||
await addNode('Client', contract.client_id, `Client: ${contract.client_name || ''}`);
|
||||
edge(`Contract:${contract.id}`, `Client:${contract.client_id}`, 'client_party');
|
||||
}
|
||||
if (contract.sector_id) {
|
||||
await addNode('Sector', contract.sector_id, `Sector: ${contract.sector_name || ''}`);
|
||||
edge(`Contract:${contract.id}`, `Sector:${contract.sector_id}`, 'governs_sector');
|
||||
}
|
||||
if (contract.rate_card_id) {
|
||||
await addNode('RateCard', contract.rate_card_id, `RateCard`);
|
||||
edge(`Contract:${contract.id}`, `RateCard:${contract.rate_card_id}`, 'defines_rates');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── TRANSPORTATION ────────────────────────────────────────
|
||||
// Transportation → Shift, Worker, Vendor, Site, Client
|
||||
async function traverseTransportation(transportId) {
|
||||
const transport = await entities.Transportation.get(transportId);
|
||||
if (!transport) return;
|
||||
await addNode('Transportation', transport.id, `Transport: ${transport.transport_type} → ${transport.dropoff_location || ''}`, transport);
|
||||
|
||||
if (transport.shift_id) {
|
||||
await addNode('Shift', transport.shift_id, `Shift`);
|
||||
edge(`Transportation:${transport.id}`, `Shift:${transport.shift_id}`, 'serves_shift');
|
||||
}
|
||||
if (transport.workforce_id) {
|
||||
await addNode('Worker', transport.workforce_id, `Worker: ${transport.workforce_name || ''}`);
|
||||
edge(`Transportation:${transport.id}`, `Worker:${transport.workforce_id}`, 'transports_worker');
|
||||
}
|
||||
if (transport.vendor_id) {
|
||||
await addNode('Vendor', transport.vendor_id, `Vendor: ${transport.vendor_name || ''}`);
|
||||
edge(`Transportation:${transport.id}`, `Vendor:${transport.vendor_id}`, 'arranged_by');
|
||||
}
|
||||
if (transport.client_id) {
|
||||
await addNode('Client', transport.client_id, `Client: ${transport.client_name || ''}`);
|
||||
edge(`Transportation:${transport.id}`, `Client:${transport.client_id}`, 'destination_client');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── EQUIPMENT ─────────────────────────────────────────────
|
||||
// Equipment → Vendor, Client, Site, Shift, Worker
|
||||
async function traverseEquipment(equipmentId) {
|
||||
const equipment = await entities.Equipment.get(equipmentId);
|
||||
if (!equipment) return;
|
||||
await addNode('Equipment', equipment.id, `Equipment: ${equipment.equipment_name || ''} (${equipment.category})`, equipment);
|
||||
|
||||
if (equipment.vendor_id) {
|
||||
await addNode('Vendor', equipment.vendor_id, `Vendor: ${equipment.vendor_name || ''}`);
|
||||
edge(`Equipment:${equipment.id}`, `Vendor:${equipment.vendor_id}`, 'owned_by');
|
||||
}
|
||||
if (equipment.client_id) {
|
||||
await addNode('Client', equipment.client_id, `Client: ${equipment.client_name || ''}`);
|
||||
edge(`Equipment:${equipment.id}`, `Client:${equipment.client_id}`, 'assigned_to_client');
|
||||
}
|
||||
if (equipment.shift_id) {
|
||||
await addNode('Shift', equipment.shift_id, `Shift`);
|
||||
edge(`Equipment:${equipment.id}`, `Shift:${equipment.shift_id}`, 'used_in_shift');
|
||||
}
|
||||
if (equipment.assigned_to_worker_id) {
|
||||
await addNode('Worker', equipment.assigned_to_worker_id, `Worker: ${equipment.assigned_to_worker_name || ''}`);
|
||||
edge(`Equipment:${equipment.id}`, `Worker:${equipment.assigned_to_worker_id}`, 'assigned_to_worker');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── TRAINING ──────────────────────────────────────────────
|
||||
// Training → Worker, Vendor, Client, Certification
|
||||
async function traverseTraining(trainingId) {
|
||||
const training = await entities.Training.get(trainingId);
|
||||
if (!training) return;
|
||||
await addNode('Training', training.id, `Training: ${training.training_name || ''}`, training);
|
||||
|
||||
if (training.workforce_id) {
|
||||
await addNode('Worker', training.workforce_id, `Worker: ${training.workforce_name || ''}`);
|
||||
edge(`Training:${training.id}`, `Worker:${training.workforce_id}`, 'enrolled_worker');
|
||||
}
|
||||
if (training.vendor_id) {
|
||||
await addNode('Vendor', training.vendor_id, `Vendor: ${training.vendor_name || ''}`);
|
||||
edge(`Training:${training.id}`, `Vendor:${training.vendor_id}`, 'sponsored_by');
|
||||
}
|
||||
if (training.client_id) {
|
||||
await addNode('Client', training.client_id, `Client: ${training.client_name || ''}`);
|
||||
edge(`Training:${training.id}`, `Client:${training.client_id}`, 'client_specific');
|
||||
}
|
||||
if (training.certification_id) {
|
||||
await addNode('Certification', training.certification_id, `Cert: ${training.certification_earned || ''}`);
|
||||
edge(`Training:${training.id}`, `Certification:${training.certification_id}`, 'issues_certification');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── FOOD SERVICE OPERATION ────────────────────────────────
|
||||
// FoodServiceOperation → Client, Site, Sector, Vendor, Manager, Shift, Event, Contract, Invoice, Equipment
|
||||
async function traverseFoodServiceOperation(opId) {
|
||||
const op = await entities.FoodServiceOperation.get(opId);
|
||||
if (!op) return;
|
||||
await addNode('FoodServiceOperation', op.id, `FSO: ${op.operation_name || ''}`, op);
|
||||
|
||||
if (op.client_id) {
|
||||
await addNode('Client', op.client_id, `Client: ${op.client_name || ''}`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Client:${op.client_id}`, 'serves_client');
|
||||
}
|
||||
if (op.vendor_id) {
|
||||
await addNode('Vendor', op.vendor_id, `Vendor: ${op.vendor_name || ''}`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Vendor:${op.vendor_id}`, 'operated_by');
|
||||
}
|
||||
if (op.manager_id) {
|
||||
await addNode('Manager', op.manager_id, `Manager: ${op.manager_name || ''}`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Manager:${op.manager_id}`, 'managed_by');
|
||||
}
|
||||
if (op.shift_id) {
|
||||
await addNode('Shift', op.shift_id, `Shift`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Shift:${op.shift_id}`, 'runs_during_shift');
|
||||
}
|
||||
if (op.contract_id) {
|
||||
await addNode('Contract', op.contract_id, `Contract`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Contract:${op.contract_id}`, 'governed_by_contract');
|
||||
}
|
||||
if (op.invoice_id) {
|
||||
await addNode('Invoice', op.invoice_id, `Invoice`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Invoice:${op.invoice_id}`, 'billed_via');
|
||||
}
|
||||
if (op.equipment_assigned) {
|
||||
for (const eq of op.equipment_assigned) {
|
||||
if (eq.equipment_id) {
|
||||
await addNode('Equipment', eq.equipment_id, `Equipment: ${eq.equipment_name || ''}`);
|
||||
edge(`FoodServiceOperation:${op.id}`, `Equipment:${eq.equipment_id}`, 'uses_equipment');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── SHIFT (extended) ──────────────────────────────────────
|
||||
// Shift → Manager, Location, Event, Transportation, Equipment, FoodServiceOperation
|
||||
async function traverseShift(shiftId) {
|
||||
const shift = await entities.Shift.get(shiftId);
|
||||
if (!shift) return;
|
||||
await addNode('Shift', shift.id, `Shift: ${shift.shift_name || ''}`, shift);
|
||||
|
||||
if (shift.manager_id) {
|
||||
await addNode('Manager', shift.manager_id, `Manager: ${shift.manager_name || ''}`);
|
||||
edge(`Shift:${shift.id}`, `Manager:${shift.manager_id}`, 'managed_by');
|
||||
}
|
||||
if (shift.event_id) {
|
||||
await addNode('Event', shift.event_id, `Event`);
|
||||
edge(`Shift:${shift.id}`, `Event:${shift.event_id}`, 'part_of_event');
|
||||
}
|
||||
// Transportation serving this shift
|
||||
const transports = await entities.Transportation.filter({ shift_id: shiftId }, '-created_date', 10);
|
||||
for (const t of transports) {
|
||||
await addNode('Transportation', t.id, `Transport: ${t.transport_type}`, t);
|
||||
edge(`Shift:${shiftId}`, `Transportation:${t.id}`, 'worker_transport');
|
||||
}
|
||||
// Equipment assigned to this shift
|
||||
const equipment = await entities.Equipment.filter({ shift_id: shiftId }, '-created_date', 10);
|
||||
for (const e of equipment) {
|
||||
await addNode('Equipment', e.id, `Equipment: ${e.equipment_name || ''}`, e);
|
||||
edge(`Shift:${shiftId}`, `Equipment:${e.id}`, 'requires_equipment');
|
||||
}
|
||||
// Food service operations running during this shift
|
||||
const fsos = await entities.FoodServiceOperation.filter({ shift_id: shiftId }, '-created_date', 10);
|
||||
for (const fso of fsos) {
|
||||
await addNode('FoodServiceOperation', fso.id, `FSO: ${fso.operation_name || ''}`, fso);
|
||||
edge(`Shift:${shiftId}`, `FoodServiceOperation:${fso.id}`, 'runs_operation');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ASSIGNMENT (existing, extended) ───────────────────────
|
||||
async function traverseAssignment(assignmentId) {
|
||||
const assignment = await entities.Assignment.get(assignmentId);
|
||||
if (!assignment) return;
|
||||
await addNode('Assignment', assignment.id, `Assignment: ${assignment.workforce_name || 'Unknown'}`, assignment);
|
||||
|
||||
if (assignment.order_id) {
|
||||
await addNode('Order', assignment.order_id, `Order: ${assignment.order_number || ''}`);
|
||||
edge(`Assignment:${assignment.id}`, `Order:${assignment.order_id}`, 'belongs_to');
|
||||
await traverseOrder(assignment.order_id, 1);
|
||||
}
|
||||
if (assignment.workforce_id) {
|
||||
await addNode('Worker', assignment.workforce_id, `Worker: ${assignment.workforce_name || ''}`);
|
||||
edge(`Assignment:${assignment.id}`, `Worker:${assignment.workforce_id}`, 'assigned_to');
|
||||
}
|
||||
if (assignment.vendor_id) {
|
||||
await addNode('Vendor', assignment.vendor_id, `Vendor: ${assignment.vendor_name || ''}`);
|
||||
edge(`Assignment:${assignment.id}`, `Vendor:${assignment.vendor_id}`, 'provided_by');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ORDER (existing, extended) ────────────────────────────
|
||||
async function traverseOrder(orderId, currentDepth = 0) {
|
||||
if (currentDepth >= depth) return;
|
||||
try {
|
||||
const order = await entities.Order.get(orderId);
|
||||
if (!order) return;
|
||||
await addNode('Order', order.id, `Order: ${order.order_number || order.event_name || ''}`, order);
|
||||
|
||||
if (order.partner_id) {
|
||||
await addNode('Partner', order.partner_id, `Partner: ${order.partner_name || ''}`);
|
||||
edge(`Order:${order.id}`, `Partner:${order.partner_id}`, 'for_client');
|
||||
}
|
||||
if (order.vendor_id) {
|
||||
await addNode('Vendor', order.vendor_id, `Vendor: ${order.vendor_name || ''}`);
|
||||
edge(`Order:${order.id}`, `Vendor:${order.vendor_id}`, 'assigned_to');
|
||||
}
|
||||
// Contracts governing this client-vendor pair
|
||||
if (order.vendor_id && order.business_id) {
|
||||
const contracts = await entities.Contract.filter({ vendor_id: order.vendor_id, client_id: order.business_id, status: 'active' }, '-created_date', 5);
|
||||
for (const c of contracts) {
|
||||
await addNode('Contract', c.id, `Contract: ${c.contract_name || ''}`, c);
|
||||
edge(`Order:${order.id}`, `Contract:${c.id}`, 'governed_by');
|
||||
}
|
||||
}
|
||||
// Food service operations for this event
|
||||
if (order.event_id) {
|
||||
const fsos = await entities.FoodServiceOperation.filter({ event_id: order.event_id }, '-created_date', 10);
|
||||
for (const fso of fsos) {
|
||||
await addNode('FoodServiceOperation', fso.id, `FSO: ${fso.operation_name || ''}`, fso);
|
||||
edge(`Order:${order.id}`, `FoodServiceOperation:${fso.id}`, 'fulfills_operation');
|
||||
}
|
||||
}
|
||||
} catch (e) { /* order might not exist */ }
|
||||
}
|
||||
|
||||
// ─── VENDOR (existing, extended) ───────────────────────────
|
||||
async function traverseVendor(vendorId) {
|
||||
const vendor = await entities.Vendor.get(vendorId);
|
||||
if (!vendor) return;
|
||||
await addNode('Vendor', vendor.id, `Vendor: ${vendor.legal_name || vendor.doing_business_as || ''}`, vendor);
|
||||
|
||||
// Rates
|
||||
const rates = await entities.VendorRate.filter({ vendor_id: vendorId }, '-created_date', 10);
|
||||
for (const rate of rates) {
|
||||
await addNode('VendorRate', rate.id, `Rate: ${rate.role_name || 'Unknown'} - $${rate.client_rate || 0}/hr`, rate);
|
||||
edge(`Vendor:${vendorId}`, `VendorRate:${rate.id}`, 'has_rate');
|
||||
}
|
||||
|
||||
// Workforce
|
||||
const workforce = await entities.Workforce.filter({ vendor_id: vendorId }, '-created_date', 10);
|
||||
for (const w of workforce) {
|
||||
await addNode('Worker', w.id, `Worker: ${w.full_name || w.name || ''}`, w);
|
||||
edge(`Vendor:${vendorId}`, `Worker:${w.id}`, 'employs');
|
||||
}
|
||||
|
||||
// Contracts
|
||||
const contracts = await entities.Contract.filter({ vendor_id: vendorId }, '-created_date', 10);
|
||||
for (const c of contracts) {
|
||||
await addNode('Contract', c.id, `Contract: ${c.contract_name || ''}`, c);
|
||||
edge(`Vendor:${vendorId}`, `Contract:${c.id}`, 'party_to');
|
||||
if (c.client_id) {
|
||||
await addNode('Client', c.client_id, `Client: ${c.client_name || ''}`);
|
||||
edge(`Contract:${c.id}`, `Client:${c.client_id}`, 'client_party');
|
||||
}
|
||||
}
|
||||
|
||||
// Equipment owned
|
||||
const equipment = await entities.Equipment.filter({ vendor_id: vendorId }, '-created_date', 10);
|
||||
for (const e of equipment) {
|
||||
await addNode('Equipment', e.id, `Equipment: ${e.equipment_name || ''}`, e);
|
||||
edge(`Vendor:${vendorId}`, `Equipment:${e.id}`, 'owns');
|
||||
}
|
||||
|
||||
// Transportation arranged
|
||||
const transports = await entities.Transportation.filter({ vendor_id: vendorId }, '-created_date', 10);
|
||||
for (const t of transports) {
|
||||
await addNode('Transportation', t.id, `Transport: ${t.transport_type}`, t);
|
||||
edge(`Vendor:${vendorId}`, `Transportation:${t.id}`, 'arranges');
|
||||
}
|
||||
|
||||
// Training sponsored
|
||||
const trainings = await entities.Training.filter({ vendor_id: vendorId }, '-created_date', 10);
|
||||
for (const t of trainings) {
|
||||
await addNode('Training', t.id, `Training: ${t.training_name || ''}`, t);
|
||||
edge(`Vendor:${vendorId}`, `Training:${t.id}`, 'sponsors_training');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── WORKER (existing, extended) ───────────────────────────
|
||||
async function traverseWorker(workerId) {
|
||||
const worker = await entities.Workforce.get(workerId);
|
||||
if (!worker) return;
|
||||
await addNode('Worker', worker.id, `Worker: ${worker.full_name || worker.name || ''}`, worker);
|
||||
|
||||
if (worker.vendor_id) {
|
||||
await addNode('Vendor', worker.vendor_id, `Vendor: ${worker.vendor_name || ''}`);
|
||||
edge(`Worker:${workerId}`, `Vendor:${worker.vendor_id}`, 'employed_by');
|
||||
}
|
||||
|
||||
// Assignments
|
||||
const assignments = await entities.Assignment.filter({ workforce_id: workerId }, '-created_date', 10);
|
||||
for (const a of assignments) {
|
||||
await addNode('Assignment', a.id, `Assignment: ${a.role || ''}`, a);
|
||||
edge(`Worker:${workerId}`, `Assignment:${a.id}`, 'assigned');
|
||||
}
|
||||
|
||||
// Availability
|
||||
const availability = await entities.WorkerAvailability.filter({ staff_id: workerId }, '-created_date', 1);
|
||||
if (availability.length > 0) {
|
||||
await addNode('Availability', availability[0].id, `Availability: ${availability[0].availability_status}`, availability[0]);
|
||||
edge(`Worker:${workerId}`, `Availability:${availability[0].id}`, 'availability');
|
||||
}
|
||||
|
||||
// Reputation
|
||||
const reputation = await entities.WorkerReputationProfile.filter({ worker_id: workerId }, '-created_date', 1);
|
||||
if (reputation.length > 0) {
|
||||
await addNode('Reputation', reputation[0].id, `Reputation: ${reputation[0].trust_tier} (${reputation[0].trust_score || 0})`, reputation[0]);
|
||||
edge(`Worker:${workerId}`, `Reputation:${reputation[0].id}`, 'reputation');
|
||||
}
|
||||
|
||||
// Training records
|
||||
const trainings = await entities.Training.filter({ workforce_id: workerId }, '-created_date', 10);
|
||||
for (const t of trainings) {
|
||||
await addNode('Training', t.id, `Training: ${t.training_name || ''}`, t);
|
||||
edge(`Worker:${workerId}`, `Training:${t.id}`, 'enrolled_in');
|
||||
}
|
||||
|
||||
// Equipment assigned
|
||||
const equipment = await entities.Equipment.filter({ assigned_to_worker_id: workerId }, '-created_date', 10);
|
||||
for (const e of equipment) {
|
||||
await addNode('Equipment', e.id, `Equipment: ${e.equipment_name || ''}`, e);
|
||||
edge(`Worker:${workerId}`, `Equipment:${e.id}`, 'issued_equipment');
|
||||
}
|
||||
|
||||
// Transportation
|
||||
const transports = await entities.Transportation.filter({ workforce_id: workerId }, '-created_date', 10);
|
||||
for (const t of transports) {
|
||||
await addNode('Transportation', t.id, `Transport: ${t.transport_type}`, t);
|
||||
edge(`Worker:${workerId}`, `Transportation:${t.id}`, 'transported_by');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── FULL GRAPH (new) ──────────────────────────────────────
|
||||
// Walks the entire connected chain from any starting node, up to depth hops
|
||||
async function traverseFull(startType, startId, maxDepth = 3) {
|
||||
const queue = [{ type: startType, id: startId, depth: 0 }];
|
||||
const seen = new Set();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { type, id, depth: curDepth } = queue.shift();
|
||||
const key = `${type}:${id}`;
|
||||
if (seen.has(key) || curDepth > maxDepth) continue;
|
||||
seen.add(key);
|
||||
|
||||
switch (type) {
|
||||
case 'Worker': {
|
||||
const w = await entities.Workforce.get(id).catch(() => null);
|
||||
if (!w) break;
|
||||
await addNode('Worker', w.id, `Worker: ${w.full_name || w.name || ''}`, w);
|
||||
if (w.vendor_id) queue.push({ type: 'Vendor', id: w.vendor_id, depth: curDepth + 1 });
|
||||
const as = await entities.Assignment.filter({ workforce_id: id }, '-created_date', 5);
|
||||
for (const a of as) queue.push({ type: 'Assignment', id: a.id, depth: curDepth + 1 });
|
||||
const ts = await entities.Training.filter({ workforce_id: id }, '-created_date', 5);
|
||||
for (const t of ts) queue.push({ type: 'Training', id: t.id, depth: curDepth + 1 });
|
||||
const eq = await entities.Equipment.filter({ assigned_to_worker_id: id }, '-created_date', 5);
|
||||
for (const e of eq) queue.push({ type: 'Equipment', id: e.id, depth: curDepth + 1 });
|
||||
const tr = await entities.Transportation.filter({ workforce_id: id }, '-created_date', 5);
|
||||
for (const t of tr) queue.push({ type: 'Transportation', id: t.id, depth: curDepth + 1 });
|
||||
break;
|
||||
}
|
||||
case 'Vendor': {
|
||||
const v = await entities.Vendor.get(id).catch(() => null);
|
||||
if (!v) break;
|
||||
await addNode('Vendor', v.id, `Vendor: ${v.legal_name || v.doing_business_as || ''}`, v);
|
||||
const ws = await entities.Workforce.filter({ vendor_id: id }, '-created_date', 10);
|
||||
for (const w of ws) { await addNode('Worker', w.id, `Worker: ${w.full_name || ''}`, w); edge(`Vendor:${id}`, `Worker:${w.id}`, 'employs'); queue.push({ type: 'Worker', id: w.id, depth: curDepth + 1 }); }
|
||||
const cs = await entities.Contract.filter({ vendor_id: id }, '-created_date', 10);
|
||||
for (const c of cs) { await addNode('Contract', c.id, `Contract: ${c.contract_name || ''}`, c); edge(`Vendor:${id}`, `Contract:${c.id}`, 'party_to'); queue.push({ type: 'Contract', id: c.id, depth: curDepth + 1 }); }
|
||||
const eqs = await entities.Equipment.filter({ vendor_id: id }, '-created_date', 10);
|
||||
for (const e of eqs) { await addNode('Equipment', e.id, `Equipment: ${e.equipment_name || ''}`, e); edge(`Vendor:${id}`, `Equipment:${e.id}`, 'owns'); }
|
||||
const fsos = await entities.FoodServiceOperation.filter({ vendor_id: id }, '-created_date', 10);
|
||||
for (const f of fsos) { await addNode('FoodServiceOperation', f.id, `FSO: ${f.operation_name || ''}`, f); edge(`Vendor:${id}`, `FoodServiceOperation:${f.id}`, 'operates'); queue.push({ type: 'FoodServiceOperation', id: f.id, depth: curDepth + 1 }); }
|
||||
break;
|
||||
}
|
||||
case 'Contract': {
|
||||
const c = await entities.Contract.get(id).catch(() => null);
|
||||
if (!c) break;
|
||||
await addNode('Contract', c.id, `Contract: ${c.contract_name || ''}`, c);
|
||||
if (c.vendor_id) { await addNode('Vendor', c.vendor_id, `Vendor: ${c.vendor_name || ''}`); edge(`Contract:${id}`, `Vendor:${c.vendor_id}`, 'vendor_party'); }
|
||||
if (c.client_id) { await addNode('Client', c.client_id, `Client: ${c.client_name || ''}`); edge(`Contract:${id}`, `Client:${c.client_id}`, 'client_party'); }
|
||||
break;
|
||||
}
|
||||
case 'Shift': {
|
||||
await traverseShift(id);
|
||||
break;
|
||||
}
|
||||
case 'FoodServiceOperation': {
|
||||
await traverseFoodServiceOperation(id);
|
||||
break;
|
||||
}
|
||||
case 'Transportation': {
|
||||
await traverseTransportation(id);
|
||||
break;
|
||||
}
|
||||
case 'Equipment': {
|
||||
await traverseEquipment(id);
|
||||
break;
|
||||
}
|
||||
case 'Training': {
|
||||
await traverseTraining(id);
|
||||
break;
|
||||
}
|
||||
case 'Assignment': {
|
||||
await traverseAssignment(id);
|
||||
break;
|
||||
}
|
||||
case 'Order': {
|
||||
await traverseOrder(id, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── EXECUTE ───────────────────────────────────────────────
|
||||
if (query_type === 'subgraph' && entity_id) {
|
||||
switch (entity_type) {
|
||||
case 'Assignment': await traverseAssignment(entity_id); break;
|
||||
case 'Order': await traverseOrder(entity_id, 0); break;
|
||||
case 'Vendor': await traverseVendor(entity_id); break;
|
||||
case 'Worker': await traverseWorker(entity_id); break;
|
||||
case 'Shift': await traverseShift(entity_id); break;
|
||||
case 'Contract': await traverseContract(entity_id); break;
|
||||
case 'Transportation': await traverseTransportation(entity_id); break;
|
||||
case 'Equipment': await traverseEquipment(entity_id); break;
|
||||
case 'Training': await traverseTraining(entity_id); break;
|
||||
case 'FoodServiceOperation': await traverseFoodServiceOperation(entity_id); break;
|
||||
default: break;
|
||||
}
|
||||
} else if (query_type === 'full_graph' && entity_id && entity_type) {
|
||||
await traverseFull(entity_type, entity_id, depth);
|
||||
} else if (query_type === 'site_labor') {
|
||||
const assignments = await entities.Assignment.filter({}, '-created_date', 100);
|
||||
graph.nodes = assignments.map(a => ({
|
||||
type: 'Assignment',
|
||||
id: a.id,
|
||||
label: `${a.workforce_name} - ${a.role}`,
|
||||
data: a
|
||||
}));
|
||||
} else if (query_type === 'shortage_prediction') {
|
||||
const orders = await entities.Order.filter({ status: 'Open' }, '-created_date', 50);
|
||||
for (const order of orders) {
|
||||
await addNode('Order', order.id, `Order: ${order.order_number || ''}`, order);
|
||||
const assignments = await entities.Assignment.filter({ order_id: order.id }, '-created_date', 50);
|
||||
const filled = assignments.filter(a => a.assignment_status === 'Confirmed' || a.assignment_status === 'Checked In').length;
|
||||
const needed = order.total_staff || 0;
|
||||
if (filled < needed) {
|
||||
edge(`Order:${order.id}`, `SHORTAGE:${order.id}`, `short_by_${needed - filled}`);
|
||||
graph.nodes.push({ type: 'Alert', id: `SHORTAGE:${order.id}`, label: `Shortage: ${needed - filled} unfilled positions`, data: { order_id: order.id, needed, filled, gap: needed - filled } });
|
||||
}
|
||||
}
|
||||
} else if (query_type === 'entity_summary') {
|
||||
// Returns counts of every entity type in the graph — the "graph census"
|
||||
const counts = {};
|
||||
const entityTypes = ['Workforce', 'Vendor', 'Business', 'Site', 'Shift', 'Event', 'Order', 'Assignment', 'Invoice', 'CompliancePackage', 'Scorecard', 'Contract', 'Transportation', 'Equipment', 'Training', 'FoodServiceOperation'];
|
||||
for (const et of entityTypes) {
|
||||
try {
|
||||
const list = await entities[et].list('-created_date', 1);
|
||||
counts[et] = list?.length || 0;
|
||||
} catch (e) {
|
||||
counts[et] = 0;
|
||||
}
|
||||
}
|
||||
return Response.json({ success: true, counts });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, graph, node_count: graph.nodes.length, edge_count: graph.edges.length });
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
});
|
||||
235
base44/functions/reasonAboutEvent/entry.ts
Normal file
235
base44/functions/reasonAboutEvent/entry.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.31';
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const base44 = createClientFromRequest(req);
|
||||
const user = await base44.auth.me();
|
||||
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const {
|
||||
event_id,
|
||||
event_type,
|
||||
subject_entity_type,
|
||||
subject_entity_id,
|
||||
context_summary,
|
||||
metadata = {},
|
||||
related_entities = []
|
||||
} = body;
|
||||
|
||||
const entities = base44.asServiceRole.entities;
|
||||
|
||||
// 1. Fetch context from the Workforce Graph
|
||||
let graphContext = {};
|
||||
try {
|
||||
const graphRes = await base44.asServiceRole.functions.invoke('queryWorkforceGraph', {
|
||||
query_type: 'subgraph',
|
||||
entity_type: subject_entity_type,
|
||||
entity_id: subject_entity_id
|
||||
});
|
||||
graphContext = graphRes.data?.graph || {};
|
||||
} catch (e) {
|
||||
console.log('Graph query failed (non-blocking):', e.message);
|
||||
}
|
||||
|
||||
// 2. Fetch similar past memories (Operational Memory)
|
||||
const similarMemories = await entities.OperationalMemory.filter(
|
||||
{ trigger_event_type: event_type },
|
||||
'-created_date',
|
||||
5
|
||||
);
|
||||
|
||||
const memoryContext = similarMemories.map(m => ({
|
||||
decision: m.decision?.recommendation || 'N/A',
|
||||
outcome: m.outcome || 'pending',
|
||||
outcome_score: m.outcome_score,
|
||||
reasoning: m.reasoning?.substring(0, 200) || ''
|
||||
}));
|
||||
|
||||
// 3. Determine department based on event type
|
||||
const departmentMap = {
|
||||
'shift.completed': 'operations',
|
||||
'shift.cancelled': 'operations',
|
||||
'shift.no_show': 'operations',
|
||||
'order.created': 'operations',
|
||||
'order.cancelled': 'operations',
|
||||
'order.filled': 'operations',
|
||||
'assignment.cancelled': 'operations',
|
||||
'invoice.overdue': 'finance',
|
||||
'vendor.rate_changed': 'procurement',
|
||||
'worker.availability_changed': 'operations',
|
||||
'worker.certification_expired': 'compliance',
|
||||
'budget.threshold_exceeded': 'finance',
|
||||
'compliance.flagged': 'compliance',
|
||||
'system.anomaly_detected': 'executive'
|
||||
};
|
||||
const department = departmentMap[event_type] || 'operations';
|
||||
|
||||
// 4. Build the reasoning prompt with full context — CAUSAL CHAIN reasoning
|
||||
const prompt = `You are the KROW Intelligence Engine — an AI workforce reasoning system for contingent labor management.
|
||||
|
||||
You do not answer. You REASON.
|
||||
|
||||
When an event occurs, you trace the CAUSAL CHAIN backwards — from the observed event through each contributing factor (connected by ↓) to the root cause(s). You do not give generic analysis. You follow the chain.
|
||||
|
||||
AN EVENT HAS OCCURRED:
|
||||
Event Type: ${event_type}
|
||||
Summary: ${context_summary || 'No summary provided'}
|
||||
Subject: ${subject_entity_type} (${subject_entity_id})
|
||||
|
||||
CURRENT GRAPH STATE (connected entities, relationships, metrics):
|
||||
${JSON.stringify(graphContext, null, 2).substring(0, 3000)}
|
||||
|
||||
SIMILAR PAST DECISIONS (Operational Memory — what happened last time):
|
||||
${memoryContext.length > 0 ? JSON.stringify(memoryContext, null, 2) : 'No similar past decisions found.'}
|
||||
|
||||
EVENT METADATA:
|
||||
${JSON.stringify(metadata, null, 2).substring(0, 1000)}
|
||||
|
||||
REASONING INSTRUCTIONS:
|
||||
|
||||
1. OBSERVATION: State what happened as a fact.
|
||||
|
||||
2. CAUSAL CHAIN: Trace WHY this event occurred. Walk backwards from the event through contributing factors. Each link in the chain is a factor that CAUSED the next. The chain flows:
|
||||
|
||||
Factor A → caused → Factor B → caused → Factor C → caused → [Root Cause]
|
||||
|
||||
Use the graph state, metadata, and past memories as evidence. Reference specific entities, workers, vendors, shifts, or metrics. Do not be generic.
|
||||
|
||||
Categories: vendor, operations, workforce, training, demand, external, compliance, finance, equipment, transportation, system.
|
||||
|
||||
3. ROOT CAUSE(S): The deepest factor(s) — the thing that, if fixed, prevents recurrence.
|
||||
|
||||
4. PREDICTION: What will happen next if no action is taken.
|
||||
|
||||
5. RECOMMENDATION: Target the root cause, not just the symptom.
|
||||
|
||||
Respond as JSON.`;
|
||||
|
||||
// 5. Call the LLM for reasoning
|
||||
let aiDecision = {};
|
||||
try {
|
||||
const llmResponse = await base44.asServiceRole.integrations.Core.InvokeLLM({
|
||||
prompt,
|
||||
response_json_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
observation: { type: 'string', description: 'What happened, stated as a fact' },
|
||||
causal_chain: {
|
||||
type: 'array',
|
||||
description: 'Causal chain traced backwards from the event to root cause(s)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
step: { type: 'number' },
|
||||
factor: { type: 'string' },
|
||||
evidence: { type: 'string' },
|
||||
category: { type: 'string', enum: ['vendor', 'operations', 'workforce', 'training', 'demand', 'external', 'compliance', 'finance', 'equipment', 'transportation', 'system'] },
|
||||
leads_to: { type: 'string' }
|
||||
}
|
||||
}
|
||||
},
|
||||
root_causes: {
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
},
|
||||
prediction: { type: 'string' },
|
||||
recommendation: { type: 'string' },
|
||||
confidence_score: { type: 'number' },
|
||||
risk_level: { type: 'string' },
|
||||
proposed_actions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action_type: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
auto_execute: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
},
|
||||
reasoning: { type: 'string', description: 'Full reasoning chain in natural language' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
aiDecision = typeof llmResponse === 'string' ? JSON.parse(llmResponse) : llmResponse;
|
||||
} catch (llmErr) {
|
||||
console.log('LLM reasoning failed:', llmErr.message);
|
||||
aiDecision = {
|
||||
prediction: 'Unable to generate prediction',
|
||||
recommendation: 'Manual review recommended',
|
||||
confidence_score: 0,
|
||||
risk_level: 'medium',
|
||||
proposed_actions: [],
|
||||
reasoning: 'LLM reasoning failed: ' + llmErr.message
|
||||
};
|
||||
}
|
||||
|
||||
// 6. Create Operational Memory — store the causal chain
|
||||
const memory_id = `MEM-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
const memory = await entities.OperationalMemory.create({
|
||||
memory_id,
|
||||
trigger_event_id: event_id,
|
||||
trigger_event_type: event_type,
|
||||
context_snapshot: { graph: graphContext, metadata },
|
||||
decision: aiDecision,
|
||||
reasoning: aiDecision.reasoning || '',
|
||||
causal_chain: aiDecision.causal_chain || [],
|
||||
root_causes: aiDecision.root_causes || [],
|
||||
similar_memories_consulted: similarMemories.map(m => m.memory_id),
|
||||
status: 'pending',
|
||||
agent_name: `${department}_agent`,
|
||||
department,
|
||||
tags: [event_type, department, aiDecision.risk_level || 'medium']
|
||||
});
|
||||
|
||||
// 7. Mark event as reasoning-triggered
|
||||
await entities.WorkforceEvent.update(event_id, {
|
||||
reasoning_triggered: true,
|
||||
memory_id: memory.id
|
||||
});
|
||||
|
||||
// 8. Log agent activity
|
||||
await entities.AgentActivity.create({
|
||||
agent_name: `${department}_agent`,
|
||||
department,
|
||||
activity_type: 'event_processed',
|
||||
trigger_event_id: event_id,
|
||||
memory_id: memory.id,
|
||||
summary: `Processed ${event_type}: ${aiDecision.recommendation || 'No recommendation'}`,
|
||||
details: { confidence: aiDecision.confidence_score, risk: aiDecision.risk_level },
|
||||
impact_level: aiDecision.risk_level === 'high' ? 'high' : (aiDecision.risk_level === 'medium' ? 'medium' : 'low'),
|
||||
entities_affected: [{ entity_type: subject_entity_type, entity_id: subject_entity_id }]
|
||||
});
|
||||
|
||||
// 9. If high risk, queue for human approval; if low risk + auto_execute, execute
|
||||
const hasAutoActions = (aiDecision.proposed_actions || []).some(a => a.auto_execute);
|
||||
if (aiDecision.risk_level === 'high' || (aiDecision.confidence_score || 0) < 60) {
|
||||
await entities.DecisionQueue.create({
|
||||
memory_id: memory.id,
|
||||
trigger_event_id: event_id,
|
||||
title: `${department} decision: ${event_type}`,
|
||||
summary: aiDecision.recommendation || 'AI recommends manual review',
|
||||
department,
|
||||
agent_name: `${department}_agent`,
|
||||
proposed_actions: aiDecision.proposed_actions || [],
|
||||
confidence_score: aiDecision.confidence_score || 0,
|
||||
risk_level: aiDecision.risk_level || 'medium',
|
||||
status: 'pending_approval',
|
||||
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
context_snapshot: { graph: graphContext, event_summary: context_summary }
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
memory_id: memory.id,
|
||||
decision: aiDecision,
|
||||
graph_nodes: graphContext.nodes?.length || 0,
|
||||
similar_memories: similarMemories.length
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
});
|
||||
79
base44/functions/recordEvent/entry.ts
Normal file
79
base44/functions/recordEvent/entry.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.31';
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const base44 = createClientFromRequest(req);
|
||||
const user = await base44.auth.me();
|
||||
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const {
|
||||
event_type,
|
||||
severity = 'info',
|
||||
actor_type = 'system',
|
||||
actor_id,
|
||||
actor_name,
|
||||
subject_entity_type,
|
||||
subject_entity_id,
|
||||
subject_krow_id,
|
||||
context_summary,
|
||||
related_entities = [],
|
||||
metadata = {},
|
||||
tags = []
|
||||
} = body;
|
||||
|
||||
if (!event_type || !subject_entity_type || !subject_entity_id) {
|
||||
return Response.json({ error: 'event_type, subject_entity_type, and subject_entity_id are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const event_id = `EVT-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
|
||||
const event = await base44.asServiceRole.entities.WorkforceEvent.create({
|
||||
event_id,
|
||||
event_type,
|
||||
severity,
|
||||
actor_type,
|
||||
actor_id: actor_id || user.id,
|
||||
actor_name: actor_name || user.full_name || 'System',
|
||||
subject_entity_type,
|
||||
subject_entity_id,
|
||||
subject_krow_id,
|
||||
context_summary: context_summary || `${event_type} on ${subject_entity_type}`,
|
||||
related_entities,
|
||||
metadata,
|
||||
reasoning_triggered: false,
|
||||
tags
|
||||
});
|
||||
|
||||
// Determine if this event should trigger reasoning
|
||||
const reasoningTriggers = [
|
||||
'shift.completed', 'shift.cancelled', 'shift.no_show',
|
||||
'order.created', 'order.cancelled', 'order.filled',
|
||||
'assignment.cancelled', 'invoice.overdue',
|
||||
'vendor.rate_changed', 'worker.availability_changed',
|
||||
'worker.certification_expired', 'budget.threshold_exceeded',
|
||||
'compliance.flagged', 'system.anomaly_detected'
|
||||
];
|
||||
|
||||
if (reasoningTriggers.includes(event_type)) {
|
||||
try {
|
||||
await base44.asServiceRole.functions.invoke('reasonAboutEvent', {
|
||||
event_id: event.id,
|
||||
event_type,
|
||||
subject_entity_type,
|
||||
subject_entity_id,
|
||||
context_summary,
|
||||
metadata,
|
||||
related_entities
|
||||
});
|
||||
} catch (reasonErr) {
|
||||
// Reasoning failure shouldn't fail the event recording
|
||||
console.log('Reasoning engine error (non-blocking):', reasonErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ success: true, event_id: event.id, event });
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
});
|
||||
104
base44/functions/reviewOutcomes/entry.ts
Normal file
104
base44/functions/reviewOutcomes/entry.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.31';
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const base44 = createClientFromRequest(req);
|
||||
const user = await base44.auth.me();
|
||||
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
if (user.role !== 'admin') return Response.json({ error: 'Forbidden — admin only' }, { status: 403 });
|
||||
|
||||
const entities = base44.asServiceRole.entities;
|
||||
|
||||
// Fetch memories with pending outcomes (executed but not yet learned)
|
||||
const pendingMemories = await entities.OperationalMemory.filter(
|
||||
{ status: 'outcome_pending' },
|
||||
'-created_date',
|
||||
50
|
||||
);
|
||||
|
||||
let learned = 0;
|
||||
let stillPending = 0;
|
||||
|
||||
for (const memory of pendingMemories) {
|
||||
try {
|
||||
// Re-query the graph to see what happened since the decision was made
|
||||
const graphRes = await base44.asServiceRole.functions.invoke('queryWorkforceGraph', {
|
||||
query_type: 'subgraph',
|
||||
entity_type: memory.context_snapshot?.metadata?.subject_entity_type || 'Order',
|
||||
entity_id: memory.context_snapshot?.metadata?.subject_entity_id || memory.trigger_event_id
|
||||
});
|
||||
|
||||
const currentGraph = graphRes.data?.graph || {};
|
||||
|
||||
// Ask the LLM to evaluate the outcome
|
||||
const prompt = `You are evaluating the outcome of a past KROW AI decision.
|
||||
|
||||
ORIGINAL EVENT: ${memory.trigger_event_type}
|
||||
ORIGINAL DECISION: ${memory.decision?.recommendation || 'N/A'}
|
||||
ORIGINAL PREDICTION: ${memory.decision?.prediction || 'N/A'}
|
||||
|
||||
CURRENT STATE (after decision):
|
||||
${JSON.stringify(currentGraph, null, 2).substring(0, 2000)}
|
||||
|
||||
ORIGINAL CONTEXT (at decision time):
|
||||
${JSON.stringify(memory.context_snapshot?.graph || {}, null, 2).substring(0, 2000)}
|
||||
|
||||
Evaluate: Did the decision help? Did the prediction come true? What was the outcome?
|
||||
Respond as JSON:
|
||||
{
|
||||
"outcome": "Description of what actually happened",
|
||||
"outcome_score": -100 to 100 (negative = made things worse, 0 = no effect, positive = helped),
|
||||
"outcome_metrics": {"key_metric": value}
|
||||
}`;
|
||||
|
||||
const evalResponse = await base44.asServiceRole.integrations.Core.InvokeLLM({
|
||||
prompt,
|
||||
response_json_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
outcome: { type: 'string' },
|
||||
outcome_score: { type: 'number' },
|
||||
outcome_metrics: { type: 'object', additionalProperties: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const evaluation = typeof evalResponse === 'string' ? JSON.parse(evalResponse) : evalResponse;
|
||||
|
||||
await entities.OperationalMemory.update(memory.id, {
|
||||
outcome: evaluation.outcome,
|
||||
outcome_score: evaluation.outcome_score,
|
||||
outcome_metrics: evaluation.outcome_metrics,
|
||||
status: 'learned',
|
||||
learned_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Log learning activity
|
||||
await entities.AgentActivity.create({
|
||||
agent_name: memory.agent_name || 'system',
|
||||
department: memory.department || 'operations',
|
||||
activity_type: 'learning_completed',
|
||||
memory_id: memory.id,
|
||||
summary: `Learned from ${memory.trigger_event_type}: outcome score ${evaluation.outcome_score}`,
|
||||
details: evaluation,
|
||||
impact_level: evaluation.outcome_score > 50 ? 'high' : (evaluation.outcome_score > 0 ? 'medium' : 'low'),
|
||||
entities_affected: []
|
||||
});
|
||||
|
||||
learned++;
|
||||
} catch (e) {
|
||||
console.log(`Failed to evaluate memory ${memory.id}:`, e.message);
|
||||
stillPending++;
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
reviewed: pendingMemories.length,
|
||||
learned,
|
||||
still_pending: stillPending
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user