Rebuild Piggy's interface, and give the demo book a business to describe
Piggy answered in raw markdown, threw away every tool result it streamed, and fought the reader's scroll on every token. The three surfaces that made it worth having — what it read, how it reasoned, what it cost — were all on the wire and none of them reached the screen. The transcript is now composed of five parts under components/piggy: answers render through streamdown, the container sticks to the bottom without pinning the reader there, tool steps say what they read and link to the record, and each turn carries its model and token count. Three lifecycle bugs went with them: Stop left a permanent spinner, a truncated stream was indistinguishable from thinking, and a failed send destroyed the message it failed to send. Underneath, the inference path grew timeouts, jittered retries on 429 and 5xx, tolerance of the malformed frames a 30B model emits, and an agent_runs row per turn so chat spend is observable. The system prompt now states that a field ending in Cents is cents — without it nemotron renders costPerGpuHourCents: 189 as "$189 per GPU-hour", which is a 100x error on the most scrutinised number in the room. The demo book was arithmetically incoherent: every deal's value contradicted its own allocation revenue by up to 3.6x, nothing had ever closed, no customer had any paper, and the marketplace was empty. Deal value is now derived from the allocation, the book clears 5.3% across five blocks with one deliberately underwater, and the renewal, compliance and agent-provenance machinery finally has rows to act on. A --clear that deleted every obligation, SLA term and capacity request in the database regardless of origin is scoped to the demo's own ids. Around that: accounts have a detail page, ⌘K searches the book, Settings can mint the API keys it always claimed to, and deploy.sh actually ships the agent instead of silently skipping its compose profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+838
-37
@@ -1,3 +1,29 @@
|
||||
/**
|
||||
* The tools interactive chat gets.
|
||||
*
|
||||
* Two layers, and the distinction between them is the whole design.
|
||||
*
|
||||
* FOCUSED tools answer where the user already is: the record the panel was
|
||||
* opened from, or the page it is docked on. They take no id, because the id is
|
||||
* the context, and a tool that could pivot would let the model wander off the
|
||||
* thing the user is looking at.
|
||||
*
|
||||
* LOOKUP tools are the opposite, and exist because the focused layer capped
|
||||
* every conversation at one question. "Compare Halcyon and Northwind", "which
|
||||
* customer has the nearest renewal", "what can we buy H200 for" are all
|
||||
* questions about rows nobody handed Piggy, and until it could find one by name
|
||||
* the only honest answer was that it could not look.
|
||||
*
|
||||
* Lookup tools are offered on every message, so each is a permanent tax on the
|
||||
* prompt and one more thing a 30B model can choose wrongly. Four earned that —
|
||||
* see the note above `createLookupPigTools` for what was declined and why.
|
||||
*/
|
||||
import {
|
||||
PIGGY_RECORD_TYPES,
|
||||
formatCents,
|
||||
isPageContext,
|
||||
type PiggyRecordType,
|
||||
} from '@pig/core';
|
||||
import {
|
||||
accounts,
|
||||
allocations,
|
||||
@@ -9,65 +35,72 @@ import {
|
||||
slaMetricTargets,
|
||||
slaTerms,
|
||||
supplyDeals,
|
||||
type Contract,
|
||||
type Database,
|
||||
type InventoryListing,
|
||||
} from '@pig/db';
|
||||
import { isPageContext } from '@pig/core';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { CapacityService } from '@pig/api/src/services/capacity';
|
||||
import { renewalAlarm } from '@pig/api/src/services/contracts';
|
||||
import { and, asc, eq, gt, ilike, inArray, isNotNull, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import type { PiggyChatContext } from './chat';
|
||||
import { createAccountLifecycleTool } from './lifecycle-tools';
|
||||
import { createPagePigTools } from './page-tools';
|
||||
import { defineTool, type AgentTool } from './provider';
|
||||
import { createAccountLifecycleTool } from './lifecycle-tools';
|
||||
|
||||
const noInput = z.object({}).strict();
|
||||
|
||||
/**
|
||||
* Interactive chat gets one scoped read tool and no ambient access.
|
||||
* Interactive chat gets one scoped read tool for where it is, plus the lookup
|
||||
* layer, and no ambient access.
|
||||
*
|
||||
* A record context gets `pig_get_record`, which takes no id and so cannot
|
||||
* pivot to another row. A page context gets the single tool that answers that
|
||||
* page — and never `pig_get_record`, because there is no record to read and a
|
||||
* tool that would throw is a wasted turn out of four.
|
||||
* A record context gets `pig_get_record`, which takes no id and so always reads
|
||||
* the row the user opened. A page context gets the single tool that answers
|
||||
* that page — and never `pig_get_record`, because there is no record to read
|
||||
* and a tool that would throw is a wasted turn out of four.
|
||||
*/
|
||||
export function createInteractivePigTools(
|
||||
db: Database,
|
||||
context: PiggyChatContext | undefined,
|
||||
): AgentTool[] {
|
||||
return [...focusedPigTools(db, context), ...createLookupPigTools(db)];
|
||||
}
|
||||
|
||||
function focusedPigTools(db: Database, context: PiggyChatContext | undefined): AgentTool[] {
|
||||
// No context is the dashboard case by another name: the same bounded
|
||||
// workspace overview, rather than a second definition that could drift.
|
||||
if (!context) return createPagePigTools(db, '/');
|
||||
if (isPageContext(context)) return createPagePigTools(db, context.route);
|
||||
if (context.type === 'account') {
|
||||
return [
|
||||
defineTool({
|
||||
name: 'pig_get_record',
|
||||
description:
|
||||
'Read the PIG record currently in focus and its directly related commercial data. ' +
|
||||
'This tool accepts no id and cannot inspect a different record.',
|
||||
inputSchema: noInput,
|
||||
execute: async () => readFocusedRecord(db, context),
|
||||
}),
|
||||
createAccountLifecycleTool(db, context.id),
|
||||
];
|
||||
}
|
||||
return [
|
||||
defineTool({
|
||||
name: 'pig_get_record',
|
||||
description:
|
||||
'Read the PIG record currently in focus and its directly related commercial data. ' +
|
||||
'This tool accepts no id and cannot inspect a different record.',
|
||||
inputSchema: noInput,
|
||||
execute: async () => readFocusedRecord(db, context),
|
||||
}),
|
||||
];
|
||||
const focused = defineTool({
|
||||
name: 'pig_get_record',
|
||||
description:
|
||||
'Read the PIG record currently in focus and its directly related commercial data. ' +
|
||||
'This tool accepts no id; use pig_get_record_by_id to read a different record.',
|
||||
inputSchema: noInput,
|
||||
execute: async () => readFocusedRecord(db, context),
|
||||
});
|
||||
if (context.type === 'account') return [focused, createAccountLifecycleTool(db, context.id)];
|
||||
return [focused];
|
||||
}
|
||||
|
||||
type PiggyRecordContext = Exclude<PiggyChatContext, { type: 'page' }>;
|
||||
|
||||
/**
|
||||
* One not-found message for both entry points.
|
||||
*
|
||||
* `readFocusedRecord` is now reached from a context the panel supplied AND from
|
||||
* an id the model chose, so "the account in focus no longer exists" was wrong
|
||||
* half the time — and wrong in the direction that makes a model retry rather
|
||||
* than correct the id it invented.
|
||||
*/
|
||||
function missingRecord(type: PiggyRecordType, id: string): Error {
|
||||
return new Error(`No ${type} record exists with id ${id}.`);
|
||||
}
|
||||
|
||||
async function readFocusedRecord(db: Database, context: PiggyRecordContext): Promise<unknown> {
|
||||
if (context.type === 'account') {
|
||||
const [account] = await db.select().from(accounts).where(eq(accounts.id, context.id)).limit(1);
|
||||
if (!account) throw new Error('The account in focus no longer exists.');
|
||||
if (!account) throw missingRecord(context.type, context.id);
|
||||
const [people, demand, supply, paperwork] = await Promise.all([
|
||||
db.select().from(contacts).where(eq(contacts.accountId, context.id)).limit(100),
|
||||
db.select().from(demandDeals).where(eq(demandDeals.accountId, context.id)).limit(100),
|
||||
@@ -79,7 +112,7 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
|
||||
|
||||
if (context.type === 'contact') {
|
||||
const [contact] = await db.select().from(contacts).where(eq(contacts.id, context.id)).limit(1);
|
||||
if (!contact) throw new Error('The contact in focus no longer exists.');
|
||||
if (!contact) throw missingRecord(context.type, context.id);
|
||||
const [account] = contact.accountId
|
||||
? await db.select().from(accounts).where(eq(accounts.id, contact.accountId)).limit(1)
|
||||
: [];
|
||||
@@ -88,7 +121,7 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
|
||||
|
||||
if (context.type === 'demand_deal') {
|
||||
const [deal] = await db.select().from(demandDeals).where(eq(demandDeals.id, context.id)).limit(1);
|
||||
if (!deal) throw new Error('The demand deal in focus no longer exists.');
|
||||
if (!deal) throw missingRecord(context.type, context.id);
|
||||
const [account] = await db.select().from(accounts).where(eq(accounts.id, deal.accountId)).limit(1);
|
||||
const reservations = await db
|
||||
.select()
|
||||
@@ -100,7 +133,7 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
|
||||
|
||||
if (context.type === 'supply_deal') {
|
||||
const [deal] = await db.select().from(supplyDeals).where(eq(supplyDeals.id, context.id)).limit(1);
|
||||
if (!deal) throw new Error('The supply deal in focus no longer exists.');
|
||||
if (!deal) throw missingRecord(context.type, context.id);
|
||||
const [account] = await db.select().from(accounts).where(eq(accounts.id, deal.accountId)).limit(1);
|
||||
const commitments = await db
|
||||
.select()
|
||||
@@ -116,7 +149,7 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
|
||||
.from(capacityCommitments)
|
||||
.where(eq(capacityCommitments.id, context.id))
|
||||
.limit(1);
|
||||
if (!commitment) throw new Error('The capacity commitment in focus no longer exists.');
|
||||
if (!commitment) throw missingRecord(context.type, context.id);
|
||||
const reservations = await db
|
||||
.select()
|
||||
.from(allocations)
|
||||
@@ -126,7 +159,7 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
|
||||
}
|
||||
|
||||
const [contract] = await db.select().from(contracts).where(eq(contracts.id, context.id)).limit(1);
|
||||
if (!contract) throw new Error('The contract in focus no longer exists.');
|
||||
if (!contract) throw missingRecord(context.type, context.id);
|
||||
const [serviceLevels, obligations] = await Promise.all([
|
||||
db.select().from(slaTerms).where(eq(slaTerms.contractId, contract.id)).limit(10),
|
||||
db
|
||||
@@ -144,3 +177,771 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
|
||||
: [];
|
||||
return { contract, slaTerms: serviceLevels, slaMetricTargets: metrics, obligations };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The lookup layer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Rows any one search may carry back per table, before ranking. */
|
||||
const SEARCH_PER_TYPE = 5;
|
||||
|
||||
/** Rows a search result may carry in total, after ranking. */
|
||||
const SEARCH_RESULTS = 12;
|
||||
|
||||
/** The longest name fragment a model may send. Long enough for any real name. */
|
||||
const SEARCH_QUERY_MAX = 64;
|
||||
|
||||
/** Rows a ranked list may carry. Everything above it is reported as a count. */
|
||||
const EXEMPLARS = 8;
|
||||
|
||||
/** Bound on an internal scan. Wide enough for a real book, still finite. */
|
||||
const SCAN_LIMIT = 200;
|
||||
|
||||
/**
|
||||
* The tools that are not about where the user is standing.
|
||||
*
|
||||
* Four, chosen against a fixed budget: every entry here is in the prompt of
|
||||
* every message and is another candidate for a small model to pick wrongly.
|
||||
*
|
||||
* `pig_search_records` and `pig_get_record_by_id` are the pair that lifts the
|
||||
* one-question ceiling — find a row by name, then open it. `readFocusedRecord`
|
||||
* does the opening, so a record fetched by id is shaped exactly like the record
|
||||
* the panel was opened from and the model has one shape to learn, not two.
|
||||
*
|
||||
* `pig_list_renewals` exists because the renewal deadline is expiry minus
|
||||
* notice days, which nothing in a record payload states outright: given the raw
|
||||
* contract a model has to do date arithmetic it is bad at, and a notice window
|
||||
* that quietly opened last week is the most expensive thing in this book to
|
||||
* miss. The calendar tool does surface renewal notices, but only on /calendar,
|
||||
* mixed into thirteen other kinds, and without the contract ids.
|
||||
*
|
||||
* `pig_list_inventory` reads what providers are currently offering. Nothing
|
||||
* else can see that table at all, and "what would this cost us to buy today"
|
||||
* is the supply half of every pricing conversation.
|
||||
*
|
||||
* Declined, deliberately:
|
||||
*
|
||||
* - A commitment-comparison tool. `pig_search_records` plus two
|
||||
* `pig_get_record_by_id` calls already answer it inside the four-turn budget,
|
||||
* and `pig_get_margin_summary` already ranks live blocks by margin. A fifth
|
||||
* tool would be a fifth wrong choice for a question two calls cover.
|
||||
* - Anything over `activities`. There are 185 of them, they are prose, and a
|
||||
* bounded slice of somebody's notes is the fastest way to spend a 1024-token
|
||||
* answer on transcription. The lifecycle tool already carries the one
|
||||
* activity fact that changes a decision — when the account last moved.
|
||||
* - Contacts in the search index. A person is not a commercial record, the
|
||||
* account read already returns its contacts, and every extra searched table
|
||||
* dilutes the twelve result slots the model actually reads.
|
||||
*/
|
||||
export function createLookupPigTools(db: Database): AgentTool[] {
|
||||
// Two things about the optional parameters below are load-bearing and
|
||||
// invisible in TypeScript, both found by printing what the model is actually
|
||||
// sent (`zodToJsonSchema(..., { target: 'openAi' })`).
|
||||
//
|
||||
// They are `.nullish()`, not `.optional()`. The OpenAI target emits an
|
||||
// optional field as required-and-nullable, so a model that follows the schema
|
||||
// it was given sends `{"side": null}` — which `.optional()` rejects, turning a
|
||||
// correct call into a failed tool result.
|
||||
//
|
||||
// And `.describe()` comes BEFORE `.nullish()`. Applied after, the description
|
||||
// is attached to the wrapper and dropped from the emitted schema, so the
|
||||
// sentence explaining the parameter never reaches the model at all.
|
||||
return [
|
||||
defineTool({
|
||||
name: 'pig_search_records',
|
||||
description:
|
||||
'Find PIG records by name when they are not already in focus. Matches the name or title ' +
|
||||
'of accounts, demand deals, supply deals, contracts and capacity commitments, ' +
|
||||
'case-insensitively, on a fragment. Returns each match with its type and id, for ' +
|
||||
'pig_get_record_by_id. Names only: this does not search notes, activities or people.',
|
||||
inputSchema: z
|
||||
.object({
|
||||
query: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2)
|
||||
.max(SEARCH_QUERY_MAX)
|
||||
.describe(
|
||||
`Name fragment, 2 to ${SEARCH_QUERY_MAX} characters. Use the distinctive word, not a whole sentence: "Halcyon", not "the Halcyon Research account".`,
|
||||
),
|
||||
})
|
||||
.strict(),
|
||||
execute: async ({ query }) => searchRecords(db, query),
|
||||
}),
|
||||
defineTool({
|
||||
name: 'pig_get_record_by_id',
|
||||
description:
|
||||
'Read one PIG record by type and id, with its directly related commercial data. Use it ' +
|
||||
'to open a result from pig_search_records. Ids must come from a tool result; never ' +
|
||||
'invent one.',
|
||||
inputSchema: z
|
||||
.object({
|
||||
type: z
|
||||
.enum(PIGGY_RECORD_TYPES)
|
||||
.describe('Record type, exactly as pig_search_records reported it.'),
|
||||
id: z.string().uuid().describe('Record id from a previous tool result.'),
|
||||
})
|
||||
.strict(),
|
||||
execute: async ({ type, id }) => readFocusedRecord(db, { type, id }),
|
||||
}),
|
||||
defineTool({
|
||||
name: 'pig_list_renewals',
|
||||
description:
|
||||
'List executed contracts that have not yet expired, ordered by the nearest deadline: ' +
|
||||
'the renewal-notice date where the contract auto-renews, otherwise the expiry date. ' +
|
||||
'renewalState is "due" when the notice window is already open, "scheduled" when it is ' +
|
||||
'still ahead, "not_applicable" when the contract does not auto-renew.',
|
||||
inputSchema: z
|
||||
.object({
|
||||
side: z
|
||||
.enum(['demand', 'supply'])
|
||||
.describe('demand for customer paper, supply for provider paper. null for both.')
|
||||
.nullish(),
|
||||
})
|
||||
.strict(),
|
||||
execute: async ({ side }) => listRenewals(db, side ?? undefined),
|
||||
}),
|
||||
defineTool({
|
||||
name: 'pig_list_inventory',
|
||||
description:
|
||||
'List the GPU capacity third-party providers currently offer for purchase, cheapest ' +
|
||||
'first: provider, region, interconnect, stock level and on-demand price per GPU-hour. ' +
|
||||
'This is capacity on offer, not capacity PIG already owns — what PIG owns is a capacity ' +
|
||||
'commitment.',
|
||||
inputSchema: z
|
||||
.object({
|
||||
gpuType: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(24)
|
||||
.describe('GPU model fragment, matched loosely: "H100" finds H100_80GB. null for any.')
|
||||
.nullish(),
|
||||
minGpuCount: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100_000)
|
||||
.describe('Smallest acceptable GPU count per listing. null for any.')
|
||||
.nullish(),
|
||||
requiresFastInterconnect: z
|
||||
.boolean()
|
||||
.describe('True to keep only Infiniband, RoCE or NVLink — the training-grade fabrics.')
|
||||
.nullish(),
|
||||
})
|
||||
.strict(),
|
||||
execute: async (input) =>
|
||||
listInventory(db, {
|
||||
gpuType: input.gpuType ?? undefined,
|
||||
minGpuCount: input.minGpuCount ?? undefined,
|
||||
requiresFastInterconnect: input.requiresFastInterconnect ?? undefined,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** A hit plus the fields ranking needs and the payload does not. */
|
||||
interface RankedHit {
|
||||
rank: number;
|
||||
name: string;
|
||||
hit: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* `%` and `_` are LIKE wildcards and this string arrives from a model. Left
|
||||
* unescaped, a query of `%` matches every row in every table and the model is
|
||||
* handed the first five rows of each as though they were answers.
|
||||
*/
|
||||
export function likeFragment(query: string): string {
|
||||
return `%${query.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact name first, then prefix, then anything containing the fragment.
|
||||
*
|
||||
* "Meridian" matches two accounts in the demo book — a sovereign customer and a
|
||||
* supply partner — so the tie-break is not academic: an unranked list buries the
|
||||
* exact match the user named behind whichever row the planner returned first.
|
||||
*/
|
||||
function matchRank(name: string, query: string): number {
|
||||
const lowered = name.toLowerCase();
|
||||
const needle = query.toLowerCase();
|
||||
if (lowered === needle) return 0;
|
||||
if (lowered.startsWith(needle)) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tie-break when the match quality is identical.
|
||||
*
|
||||
* Searching "Halcyon" in the demo book matches one account and three contracts,
|
||||
* none of them a prefix match, so without this the list opens with a data
|
||||
* processing addendum and the account — the record that reaches the other three
|
||||
* through `pig_get_record_by_id` — is third. The hub record goes first.
|
||||
*/
|
||||
const TYPE_PRIORITY: Record<string, number> = {
|
||||
account: 0,
|
||||
demand_deal: 1,
|
||||
supply_deal: 2,
|
||||
commitment: 3,
|
||||
contract: 4,
|
||||
};
|
||||
|
||||
async function accountNames(
|
||||
db: Database,
|
||||
ids: readonly string[],
|
||||
): Promise<Map<string, string>> {
|
||||
const unique = [...new Set(ids)];
|
||||
if (unique.length === 0) return new Map();
|
||||
const rows = await db
|
||||
.select({ id: accounts.id, name: accounts.name })
|
||||
.from(accounts)
|
||||
.where(inArray(accounts.id, unique));
|
||||
return new Map(rows.map((row) => [row.id, row.name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Five bounded name searches, ranked into one list.
|
||||
*
|
||||
* Each table is read one row past its budget so the result can say it was cut
|
||||
* rather than let the model report "two contracts match" over a capped five.
|
||||
* The counts are per type because "which Meridian?" is answered by the shape of
|
||||
* the result set, not by the first row of it.
|
||||
*/
|
||||
async function searchRecords(db: Database, query: string): Promise<unknown> {
|
||||
const fragment = likeFragment(query);
|
||||
const take = SEARCH_PER_TYPE + 1;
|
||||
|
||||
const [accountRows, demandRows, supplyRows, contractRows, commitmentRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: accounts.id,
|
||||
name: accounts.name,
|
||||
side: accounts.side,
|
||||
customerSegment: accounts.customerSegment,
|
||||
country: accounts.country,
|
||||
})
|
||||
.from(accounts)
|
||||
.where(and(isNull(accounts.archivedAt), ilike(accounts.name, fragment)))
|
||||
.limit(take),
|
||||
db
|
||||
.select({
|
||||
id: demandDeals.id,
|
||||
name: demandDeals.name,
|
||||
accountId: demandDeals.accountId,
|
||||
stage: demandDeals.stage,
|
||||
acvCents: demandDeals.acvCents,
|
||||
tcvCents: demandDeals.tcvCents,
|
||||
expectedCloseDate: demandDeals.expectedCloseDate,
|
||||
})
|
||||
.from(demandDeals)
|
||||
.where(ilike(demandDeals.name, fragment))
|
||||
.limit(take),
|
||||
db
|
||||
.select({
|
||||
id: supplyDeals.id,
|
||||
name: supplyDeals.name,
|
||||
accountId: supplyDeals.accountId,
|
||||
stage: supplyDeals.stage,
|
||||
gpuType: supplyDeals.gpuType,
|
||||
gpuCount: supplyDeals.gpuCount,
|
||||
targetCostPerGpuHourCents: supplyDeals.targetCostPerGpuHourCents,
|
||||
})
|
||||
.from(supplyDeals)
|
||||
.where(ilike(supplyDeals.name, fragment))
|
||||
.limit(take),
|
||||
db
|
||||
.select({
|
||||
id: contracts.id,
|
||||
title: contracts.title,
|
||||
accountId: contracts.accountId,
|
||||
contractType: contracts.type,
|
||||
status: contracts.status,
|
||||
side: contracts.side,
|
||||
expiresAt: contracts.expiresAt,
|
||||
valueCents: contracts.valueCents,
|
||||
})
|
||||
.from(contracts)
|
||||
.where(ilike(contracts.title, fragment))
|
||||
.limit(take),
|
||||
db
|
||||
.select({
|
||||
id: capacityCommitments.id,
|
||||
name: capacityCommitments.name,
|
||||
accountId: capacityCommitments.accountId,
|
||||
gpuType: capacityCommitments.gpuType,
|
||||
gpuCount: capacityCommitments.gpuCount,
|
||||
startsAt: capacityCommitments.startsAt,
|
||||
endsAt: capacityCommitments.endsAt,
|
||||
costPerGpuHourCents: capacityCommitments.costPerGpuHourCents,
|
||||
})
|
||||
.from(capacityCommitments)
|
||||
.where(ilike(capacityCommitments.name, fragment))
|
||||
.limit(take),
|
||||
]);
|
||||
|
||||
const names = await accountNames(db, [
|
||||
...demandRows.map((row) => row.accountId),
|
||||
...supplyRows.map((row) => row.accountId),
|
||||
...contractRows.map((row) => row.accountId),
|
||||
...commitmentRows.map((row) => row.accountId),
|
||||
]);
|
||||
|
||||
return assembleSearchResult(query, {
|
||||
accounts: accountRows,
|
||||
demandDeals: demandRows,
|
||||
supplyDeals: supplyRows,
|
||||
contracts: contractRows,
|
||||
commitments: commitmentRows,
|
||||
accountNames: names,
|
||||
});
|
||||
}
|
||||
|
||||
/** The five row sets a search reads, one row past each budget. */
|
||||
export interface SearchRowSets {
|
||||
accounts: readonly {
|
||||
id: string;
|
||||
name: string;
|
||||
side: string;
|
||||
customerSegment: string | null;
|
||||
country: string | null;
|
||||
}[];
|
||||
demandDeals: readonly {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
stage: string;
|
||||
acvCents: number | null;
|
||||
tcvCents: number | null;
|
||||
expectedCloseDate: Date | null;
|
||||
}[];
|
||||
supplyDeals: readonly {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
stage: string;
|
||||
gpuType: string | null;
|
||||
gpuCount: number | null;
|
||||
targetCostPerGpuHourCents: number | null;
|
||||
}[];
|
||||
contracts: readonly {
|
||||
id: string;
|
||||
title: string;
|
||||
accountId: string;
|
||||
contractType: string;
|
||||
status: string;
|
||||
side: string;
|
||||
expiresAt: Date | null;
|
||||
valueCents: number | null;
|
||||
}[];
|
||||
commitments: readonly {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
gpuType: string;
|
||||
gpuCount: number;
|
||||
startsAt: Date;
|
||||
endsAt: Date;
|
||||
costPerGpuHourCents: number;
|
||||
}[];
|
||||
accountNames: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranking, capping and counting, with no database in sight.
|
||||
*
|
||||
* Split out from the reads so the two things that can silently go wrong here —
|
||||
* a count taken off a capped list, and an exact match sorted below a
|
||||
* coincidental substring — are pinned by the unit suite. That suite runs in CI
|
||||
* before the migration step, so anything it can reach must not need tables.
|
||||
*/
|
||||
export function assembleSearchResult(query: string, sets: SearchRowSets): unknown {
|
||||
const names = sets.accountNames;
|
||||
const cut = <Row>(rows: readonly Row[]): { rows: readonly Row[]; truncated: boolean } => ({
|
||||
rows: rows.slice(0, SEARCH_PER_TYPE),
|
||||
truncated: rows.length > SEARCH_PER_TYPE,
|
||||
});
|
||||
const accountsCut = cut(sets.accounts);
|
||||
const demandCut = cut(sets.demandDeals);
|
||||
const supplyCut = cut(sets.supplyDeals);
|
||||
const contractsCut = cut(sets.contracts);
|
||||
const commitmentsCut = cut(sets.commitments);
|
||||
|
||||
const ranked: RankedHit[] = [
|
||||
...accountsCut.rows.map((row) => ({
|
||||
rank: matchRank(row.name, query),
|
||||
name: row.name,
|
||||
hit: {
|
||||
type: 'account',
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
side: row.side,
|
||||
customerSegment: row.customerSegment,
|
||||
country: row.country,
|
||||
},
|
||||
})),
|
||||
...demandCut.rows.map((row) => ({
|
||||
rank: matchRank(row.name, query),
|
||||
name: row.name,
|
||||
hit: {
|
||||
type: 'demand_deal',
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
accountName: names.get(row.accountId) ?? null,
|
||||
stage: row.stage,
|
||||
acvCents: row.acvCents,
|
||||
tcvCents: row.tcvCents,
|
||||
expectedCloseDate: row.expectedCloseDate?.toISOString() ?? null,
|
||||
},
|
||||
})),
|
||||
...supplyCut.rows.map((row) => ({
|
||||
rank: matchRank(row.name, query),
|
||||
name: row.name,
|
||||
hit: {
|
||||
type: 'supply_deal',
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
accountName: names.get(row.accountId) ?? null,
|
||||
stage: row.stage,
|
||||
gpuType: row.gpuType,
|
||||
gpuCount: row.gpuCount,
|
||||
targetCostPerGpuHourCents: row.targetCostPerGpuHourCents,
|
||||
},
|
||||
})),
|
||||
...contractsCut.rows.map((row) => ({
|
||||
rank: matchRank(row.title, query),
|
||||
name: row.title,
|
||||
hit: {
|
||||
type: 'contract',
|
||||
id: row.id,
|
||||
name: row.title,
|
||||
accountName: names.get(row.accountId) ?? null,
|
||||
contractType: row.contractType,
|
||||
status: row.status,
|
||||
side: row.side,
|
||||
expiresAt: row.expiresAt?.toISOString() ?? null,
|
||||
valueCents: row.valueCents,
|
||||
},
|
||||
})),
|
||||
...commitmentsCut.rows.map((row) => ({
|
||||
rank: matchRank(row.name, query),
|
||||
name: row.name,
|
||||
hit: {
|
||||
type: 'commitment',
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
accountName: names.get(row.accountId) ?? null,
|
||||
gpuType: row.gpuType,
|
||||
gpuCount: row.gpuCount,
|
||||
startsAt: row.startsAt.toISOString(),
|
||||
endsAt: row.endsAt.toISOString(),
|
||||
costPerGpuHourCents: row.costPerGpuHourCents,
|
||||
},
|
||||
})),
|
||||
].sort(
|
||||
(a, b) =>
|
||||
a.rank - b.rank ||
|
||||
(TYPE_PRIORITY[String(a.hit.type)] ?? 9) - (TYPE_PRIORITY[String(b.hit.type)] ?? 9) ||
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
|
||||
const counts = {
|
||||
account: accountsCut.rows.length,
|
||||
demand_deal: demandCut.rows.length,
|
||||
supply_deal: supplyCut.rows.length,
|
||||
contract: contractsCut.rows.length,
|
||||
commitment: commitmentsCut.rows.length,
|
||||
};
|
||||
const perTypeTruncated =
|
||||
accountsCut.truncated ||
|
||||
demandCut.truncated ||
|
||||
supplyCut.truncated ||
|
||||
contractsCut.truncated ||
|
||||
commitmentsCut.truncated;
|
||||
const results = ranked.slice(0, SEARCH_RESULTS);
|
||||
const truncated = perTypeTruncated || ranked.length > results.length;
|
||||
|
||||
return {
|
||||
headline:
|
||||
results.length === 0
|
||||
? `No account, deal, contract or capacity commitment has a name containing "${query}".`
|
||||
: `${truncated ? 'at least ' : ''}${ranked.length} record(s) match "${query}": ` +
|
||||
Object.entries(counts)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([type, count]) => `${count} ${type}(s)`)
|
||||
.join(', ') +
|
||||
'.',
|
||||
query,
|
||||
truncated,
|
||||
counts,
|
||||
results: results.map((entry) => entry.hit),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Renewals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Executed paper that has not yet expired, nearest deadline first.
|
||||
*
|
||||
* The deadline is the notice date where one exists, because that — not the
|
||||
* expiry — is the date after which the decision is no longer available. A
|
||||
* contract whose notice window opened last week therefore sorts to the top with
|
||||
* a negative `daysUntilDeadline` and `renewalState: "due"`, which is exactly the
|
||||
* row somebody is looking for when they ask what they have missed.
|
||||
*
|
||||
* `renewalAlarm` is the API's own definition of that arithmetic and is called
|
||||
* rather than repeated: two implementations of expiry-minus-notice would
|
||||
* eventually disagree, and Piggy contradicting the contracts page is worse than
|
||||
* Piggy having no renewals tool.
|
||||
*/
|
||||
async function listRenewals(db: Database, side: 'demand' | 'supply' | undefined): Promise<unknown> {
|
||||
const now = new Date();
|
||||
const rows = await db
|
||||
.select({ contract: contracts, accountName: accounts.name })
|
||||
.from(contracts)
|
||||
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
|
||||
.where(
|
||||
and(
|
||||
eq(contracts.status, 'executed'),
|
||||
isNull(contracts.terminatedAt),
|
||||
isNotNull(contracts.expiresAt),
|
||||
gt(contracts.expiresAt, now),
|
||||
side ? eq(contracts.side, side) : undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(asc(contracts.expiresAt))
|
||||
.limit(SCAN_LIMIT + 1);
|
||||
|
||||
return assembleRenewals(rows.slice(0, SCAN_LIMIT), {
|
||||
now,
|
||||
side,
|
||||
truncated: rows.length > SCAN_LIMIT,
|
||||
});
|
||||
}
|
||||
|
||||
/** Exactly the contract columns the renewal projection reads. */
|
||||
export type RenewalContract = Pick<
|
||||
Contract,
|
||||
'id' | 'title' | 'side' | 'type' | 'isAutoRenew' | 'noticeDays' | 'expiresAt' | 'valueCents'
|
||||
>;
|
||||
|
||||
export interface RenewalRow {
|
||||
contract: RenewalContract;
|
||||
accountName: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The deadline projection, with no database in sight.
|
||||
*
|
||||
* Separated from the read because the two things worth pinning here are the
|
||||
* ordering — a lapsed notice must outrank a distant expiry — and the fact that
|
||||
* `count` is the whole set while `renewals` is a capped slice of it.
|
||||
*/
|
||||
export function assembleRenewals(
|
||||
rows: readonly RenewalRow[],
|
||||
options: { now: Date; side?: 'demand' | 'supply'; truncated: boolean },
|
||||
): unknown {
|
||||
const { now, side, truncated } = options;
|
||||
const renewals = rows
|
||||
.flatMap(({ contract, accountName }) => {
|
||||
// The query already requires an expiry; narrowing here rather than
|
||||
// asserting keeps the sort key a date the compiler agrees exists.
|
||||
const expiresAt = contract.expiresAt;
|
||||
if (!expiresAt) return [];
|
||||
const alarm = renewalAlarm(contract, now);
|
||||
const deadlineAt = alarm.renewalNoticeAt ?? expiresAt;
|
||||
return [{
|
||||
id: contract.id,
|
||||
title: contract.title,
|
||||
accountName,
|
||||
side: contract.side,
|
||||
contractType: contract.type,
|
||||
isAutoRenew: contract.isAutoRenew,
|
||||
noticeDays: contract.noticeDays,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
renewalNoticeAt: alarm.renewalNoticeAt?.toISOString() ?? null,
|
||||
renewalState: alarm.renewalState,
|
||||
deadlineAt: deadlineAt.toISOString(),
|
||||
deadlineKind: alarm.renewalNoticeAt ? ('renewal_notice' as const) : ('expiry' as const),
|
||||
// Negative once the notice window has opened. Read alongside
|
||||
// renewalState rather than on its own.
|
||||
daysUntilDeadline: Math.ceil((deadlineAt.getTime() - now.getTime()) / 86_400_000),
|
||||
valueCents: contract.valueCents,
|
||||
}];
|
||||
})
|
||||
.sort((a, b) => a.deadlineAt.localeCompare(b.deadlineAt));
|
||||
|
||||
const noticeOpen = renewals.filter((row) => row.renewalState === 'due');
|
||||
const nearest = renewals[0];
|
||||
/**
|
||||
* Master agreements routinely carry no `valueCents` — the money sits on the
|
||||
* order forms beneath them. Summing nulls to zero and printing "$0.00 of
|
||||
* stated contract value" reads as a worthless renewal rather than an
|
||||
* unpriced one, so the clause is only stated where a figure exists.
|
||||
*/
|
||||
const statedValueCents = noticeOpen.reduce((sum, row) => sum + (row.valueCents ?? 0), 0);
|
||||
const anyStatedValue = noticeOpen.some((row) => row.valueCents != null);
|
||||
|
||||
return {
|
||||
headline:
|
||||
(nearest
|
||||
? `${truncated ? 'At least ' : ''}${renewals.length} executed contract(s) still live` +
|
||||
`${side ? ` on the ${side} side` : ''}. Nearest deadline: the ` +
|
||||
`${nearest.deadlineKind === 'renewal_notice' ? 'renewal notice' : 'expiry'} for ` +
|
||||
`${nearest.title}${nearest.accountName ? ` (${nearest.accountName})` : ''} on ` +
|
||||
`${nearest.deadlineAt.slice(0, 10)}` +
|
||||
`${nearest.daysUntilDeadline < 0 ? ', which has already passed' : ''}.`
|
||||
: `No executed contract${side ? ` on the ${side} side` : ''} has an expiry date ahead of it.`) +
|
||||
(noticeOpen.length > 0
|
||||
? ` ${noticeOpen.length} notice window(s) already open` +
|
||||
(anyStatedValue
|
||||
? `, covering ${formatCents(statedValueCents)} of stated contract value.`
|
||||
: '; none of those contracts states a value of its own.')
|
||||
: ''),
|
||||
side: side ?? 'both',
|
||||
truncated,
|
||||
count: renewals.length,
|
||||
noticeWindowOpenCount: noticeOpen.length,
|
||||
renewals: renewals.slice(0, EXEMPLARS),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider inventory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface InventoryQuery {
|
||||
gpuType?: string;
|
||||
minGpuCount?: number;
|
||||
requiresFastInterconnect?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* What providers are offering right now, cheapest first.
|
||||
*
|
||||
* `CapacityService.searchInventory` decides what counts as purchasable — it
|
||||
* drops `Unavailable` stock — and is called rather than re-queried so that
|
||||
* Piggy and the capacity page never disagree about what is on the market.
|
||||
*
|
||||
* Its `gpuType` filter is an exact match, which is wrong for this caller: a
|
||||
* model asked about "H100" sends "H100", and the seeded SKU is `H100_80GB`, so
|
||||
* an exact filter answers "nothing" to a question with eleven answers. The
|
||||
* filter is therefore applied here as a case-insensitive fragment over a wide
|
||||
* bounded read. The width never leaves this process; only EXEMPLARS rows do.
|
||||
*/
|
||||
async function listInventory(db: Database, query: InventoryQuery): Promise<unknown> {
|
||||
const listings = await new CapacityService(db).searchInventory({
|
||||
minGpuCount: query.minGpuCount,
|
||||
requiresHighSpeedInterconnect: query.requiresFastInterconnect,
|
||||
limit: SCAN_LIMIT,
|
||||
});
|
||||
const providerNames = await accountNames(
|
||||
db,
|
||||
listings.flatMap((listing) => (listing.accountId ? [listing.accountId] : [])),
|
||||
);
|
||||
return assembleInventoryResult(query, listings, {
|
||||
// The service caps at its own ceiling, so a full page is the only signal
|
||||
// available that there was more behind it.
|
||||
truncated: listings.length >= SCAN_LIMIT,
|
||||
providerNames,
|
||||
});
|
||||
}
|
||||
|
||||
/** Exactly the listing columns the offer projection reads. */
|
||||
export type InventoryOffer = Pick<
|
||||
InventoryListing,
|
||||
| 'accountId'
|
||||
| 'providerSlug'
|
||||
| 'gpuType'
|
||||
| 'gpuCount'
|
||||
| 'interconnectType'
|
||||
| 'region'
|
||||
| 'country'
|
||||
| 'securityTier'
|
||||
| 'stockStatus'
|
||||
| 'isSpot'
|
||||
| 'onDemandPriceCents'
|
||||
| 'priceIsVariable'
|
||||
| 'observedAt'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Fragment matching, price ranking and the exemplar cap, with no database in
|
||||
* sight — so the unit suite can pin the ordering that decides which offer a
|
||||
* seller is shown first.
|
||||
*/
|
||||
export function assembleInventoryResult(
|
||||
query: InventoryQuery,
|
||||
listings: readonly InventoryOffer[],
|
||||
options: { truncated: boolean; providerNames: ReadonlyMap<string, string> },
|
||||
): unknown {
|
||||
const { truncated, providerNames: providers } = options;
|
||||
const needle = query.gpuType?.toLowerCase();
|
||||
const matched = needle
|
||||
? listings.filter((listing) => listing.gpuType.toLowerCase().includes(needle))
|
||||
: listings;
|
||||
|
||||
// Unpriced listings are real — some providers quote on request — but they
|
||||
// cannot be ranked on price, so they sort last rather than as free capacity.
|
||||
const ranked = [...matched].sort(
|
||||
(a, b) => (a.onDemandPriceCents ?? Infinity) - (b.onDemandPriceCents ?? Infinity),
|
||||
);
|
||||
const cheapest = ranked.find((listing) => listing.onDemandPriceCents != null);
|
||||
|
||||
return {
|
||||
headline:
|
||||
ranked.length === 0
|
||||
? `No provider is currently listing capacity matching that request${query.gpuType ? ` for ${query.gpuType}` : ''}.`
|
||||
: `${truncated ? 'At least ' : ''}${ranked.length} purchasable listing(s)` +
|
||||
`${query.gpuType ? ` matching ${query.gpuType}` : ''}` +
|
||||
(cheapest
|
||||
? `; cheapest on-demand is ${formatCents(cheapest.onDemandPriceCents ?? 0)} per ` +
|
||||
`GPU-hour for ${cheapest.gpuType}.`
|
||||
: '; none of them carry a published on-demand price.'),
|
||||
truncated,
|
||||
count: ranked.length,
|
||||
filters: {
|
||||
gpuType: query.gpuType ?? null,
|
||||
minGpuCount: query.minGpuCount ?? null,
|
||||
requiresFastInterconnect: query.requiresFastInterconnect ?? false,
|
||||
},
|
||||
listings: ranked.slice(0, EXEMPLARS).map((listing) => shapeListing(listing, providers)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One listing, small enough to quote.
|
||||
*
|
||||
* The price column is stored as `onDemandPriceCents` but is normalised per GPU
|
||||
* on the way in (`packages/prime/src/map.ts`), so it is renamed on the way out.
|
||||
* A model that reads a bare "price" for an eight-GPU node as the node price
|
||||
* quotes a rate eight times too low, and the suffix is what the units rule in
|
||||
* the system prompt keys on.
|
||||
*/
|
||||
function shapeListing(
|
||||
listing: InventoryOffer,
|
||||
providers: ReadonlyMap<string, string>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
providerName: listing.accountId ? providers.get(listing.accountId) ?? null : null,
|
||||
providerSlug: listing.providerSlug,
|
||||
gpuType: listing.gpuType,
|
||||
gpuCount: listing.gpuCount,
|
||||
interconnectType: listing.interconnectType,
|
||||
region: listing.region,
|
||||
country: listing.country,
|
||||
securityTier: listing.securityTier,
|
||||
stockStatus: listing.stockStatus,
|
||||
isSpot: listing.isSpot,
|
||||
onDemandPricePerGpuHourCents: listing.onDemandPriceCents,
|
||||
priceIsVariable: listing.priceIsVariable,
|
||||
// A listing nobody has confirmed for a week is a quote, not a price.
|
||||
observedAt: listing.observedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user