copilot_work_3 / src /lib /tools.ts
Nafris's picture
Fix demo rating average calculation and add tests
a0a0a1b
Raw History Blame
10.8 kB
import { randomUUID } from "node:crypto";
import { BigQuery } from "@google-cloud/bigquery";
import type { SupplyChainTool, ToolCategory } from "@/lib/tool-types";
import { categories } from "@/lib/tool-types";
type NewTool = Omit<
SupplyChainTool,
"id" | "slug" | "createdAt" | "averageRating" | "ratingCount"
>;
type DemoState = {
tools: SupplyChainTool[];
ratings: Map<string, number>;
ratingSums: Map<string, number>;
};
declare global {
var __chainkitDemoState: DemoState | undefined;
}
const demoTools: SupplyChainTool[] = [
{
id: "intelligent-supply-chain",
slug: "intelligent-supply-chain-management",
name: "Intelligent Supply Chain Management",
summary:
"Azure ML accelerator using deep-learning forecasts and distributed computing to simulate inventory optimization.",
category: "Planning",
repoUrl: "https://github.com/MSUSAzureAccelerators/Intelligent-Supply-Chain-Management",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
{
id: "scimai-gym",
slug: "scimai-gym",
name: "SCIMAI Gym",
summary:
"Python environment for supply-chain inventory management experiments with deep reinforcement learning.",
category: "Planning",
repoUrl: "https://github.com/frenkowski/SCIMAI-Gym",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
{
id: "product-demand-forecasting",
slug: "product-demand-forecasting-ml",
name: "Product Demand Forecasting with ML",
summary:
"Machine-learning project exploring product demand forecasting for supply-chain planning.",
category: "Planning",
repoUrl: "https://github.com/trydoff/Product-Demand-Forecasting-Using-ML",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
{
id: "timefold-solver",
slug: "timefold-solver",
name: "Timefold Solver",
summary:
"Open-source AI solver for vehicle routing, task assignment, and other operational planning problems.",
category: "Logistics",
repoUrl: "https://github.com/TimefoldAI/timefold-solver",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
{
id: "late-delivery-prediction",
slug: "supply-chain-late-delivery-prediction",
name: "Late Delivery Prediction",
summary:
"Industrial ML pipeline for shipment-delay prediction with model comparisons and a Streamlit batch app.",
category: "Visibility",
repoUrl: "https://github.com/workkrishnpatel/supply-chain-late-delivery-prediction",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
{
id: "ai-warehouse-slotting",
slug: "ai-warehouse-slotting",
name: "AI Warehouse Slotting",
summary:
"Warehouse-management prototype integrating ML demand forecasting and dynamic slotting optimization.",
category: "Warehousing",
repoUrl: "https://github.com/ArwaElsayed5/Graduation-Project",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
{
id: "fraudlens-ai",
slug: "fraudlens-ai",
name: "FraudLens AI",
summary:
"Procurement audit prototype that applies anomaly detection and NLP to flag suspicious bid and vendor patterns.",
category: "Procurement",
repoUrl: "https://github.com/thenishantraj/fraudlens-ai",
websiteUrl: "",
submittedBy: "Chainkit demo",
createdAt: "2026-11-06T10:00:00.000Z",
averageRating: 0,
ratingCount: 0,
},
];
function getDemoState(): DemoState {
globalThis.__chainkitDemoState ??= {
tools: demoTools.map((tool) => ({ ...tool })),
ratings: new Map(),
ratingSums: new Map(),
};
return globalThis.__chainkitDemoState;
}
function getBigQueryConfig() {
const projectId = process.env.BIGQUERY_PROJECT_ID;
const dataset = process.env.BIGQUERY_DATASET;
if (!projectId && !dataset) return undefined;
if (!projectId || !dataset) {
throw new Error(
"Set both BIGQUERY_PROJECT_ID and BIGQUERY_DATASET to enable BigQuery.",
);
}
if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(projectId)) {
throw new Error("BIGQUERY_PROJECT_ID is not a valid Google Cloud project ID.");
}
if (!/^[A-Za-z0-9_]{1,1024}$/.test(dataset)) {
throw new Error("BIGQUERY_DATASET contains unsupported characters.");
}
return { projectId, dataset };
}
let bigQueryClient: BigQuery | undefined;
function getBigQuery() {
const config = getBigQueryConfig();
if (!config) return undefined;
bigQueryClient ??= new BigQuery({ projectId: config.projectId });
return { client: bigQueryClient, ...config };
}
function toolTable(projectId: string, dataset: string) {
return `\`${projectId}.${dataset}.tools\``;
}
function ratingTable(projectId: string, dataset: string) {
return `\`${projectId}.${dataset}.tool_ratings\``;
}
function isToolCategory(value: string): value is ToolCategory {
return categories.some((item) => item === value);
}
function mapBigQueryRow(row: Record<string, unknown>): SupplyChainTool {
const category = String(row.category);
if (!isToolCategory(category)) {
throw new Error(`BigQuery returned an unsupported category: ${category}`);
}
return {
id: String(row.id),
slug: String(row.slug),
name: String(row.name),
summary: String(row.summary),
category,
repoUrl: String(row.repoUrl),
websiteUrl: String(row.websiteUrl ?? ""),
submittedBy: String(row.submittedBy),
createdAt: new Date(String(row.createdAt)).toISOString(),
averageRating: Number(row.averageRating ?? 0),
ratingCount: Number(row.ratingCount ?? 0),
};
}
function buildToolQuery(
projectId: string,
dataset: string,
where = "",
order = "",
) {
return `
SELECT
t.tool_id AS id,
t.slug,
t.name,
t.summary,
t.category,
t.repo_url AS repoUrl,
t.website_url AS websiteUrl,
t.submitted_by AS submittedBy,
t.created_at AS createdAt,
COALESCE(AVG(r.rating), 0) AS averageRating,
COUNT(r.rating) AS ratingCount
FROM ${toolTable(projectId, dataset)} AS t
LEFT JOIN ${ratingTable(projectId, dataset)} AS r
ON t.tool_id = r.tool_id
${where}
GROUP BY
t.tool_id, t.slug, t.name, t.summary, t.category, t.repo_url,
t.website_url, t.submitted_by, t.created_at
${order}
`;
}
export async function getTools(): Promise<SupplyChainTool[]> {
const bigQuery = getBigQuery();
if (!bigQuery) return getDemoState().tools;
const [rows] = await bigQuery.client.query({
query: buildToolQuery(
bigQuery.projectId,
bigQuery.dataset,
"",
"ORDER BY t.created_at DESC",
),
});
return rows.map(mapBigQueryRow);
}
export async function getToolBySlug(slug: string) {
const bigQuery = getBigQuery();
if (!bigQuery) {
return getDemoState().tools.find((tool) => tool.slug === slug);
}
const [rows] = await bigQuery.client.query({
query: `${buildToolQuery(
bigQuery.projectId,
bigQuery.dataset,
"WHERE t.slug = @slug",
)} LIMIT 1`,
params: { slug },
});
return rows[0] ? mapBigQueryRow(rows[0]) : undefined;
}
async function getToolById(toolId: string) {
const bigQuery = getBigQuery();
if (!bigQuery) {
return getDemoState().tools.find((tool) => tool.id === toolId);
}
const [rows] = await bigQuery.client.query({
query: `${buildToolQuery(
bigQuery.projectId,
bigQuery.dataset,
"WHERE t.tool_id = @toolId",
)} LIMIT 1`,
params: { toolId },
});
return rows[0] ? mapBigQueryRow(rows[0]) : undefined;
}
export async function submitTool(input: NewTool) {
const createdAt = new Date().toISOString();
const id = randomUUID();
const slugBase =
input.name
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 48) || "supply-chain-tool";
const tool: SupplyChainTool = {
...input,
id,
slug: `${slugBase}-${id.slice(0, 8)}`,
createdAt,
averageRating: 0,
ratingCount: 0,
};
const bigQuery = getBigQuery();
if (!bigQuery) {
getDemoState().tools.unshift(tool);
return tool;
}
await bigQuery.client.query({
query: `
INSERT INTO ${toolTable(bigQuery.projectId, bigQuery.dataset)}
(tool_id, slug, name, summary, category, repo_url, website_url, submitted_by, created_at)
VALUES
(@id, @slug, @name, @summary, @category, @repoUrl, @websiteUrl, @submittedBy, TIMESTAMP(@createdAt))
`,
params: {
id: tool.id,
slug: tool.slug,
name: tool.name,
summary: tool.summary,
category: tool.category,
repoUrl: tool.repoUrl,
websiteUrl: tool.websiteUrl,
submittedBy: tool.submittedBy,
createdAt: tool.createdAt,
},
});
return tool;
}
export async function rateTool(toolId: string, voterId: string, rating: number) {
const bigQuery = getBigQuery();
if (!bigQuery) {
const state = getDemoState();
const tool = state.tools.find((candidate) => candidate.id === toolId);
if (!tool) return undefined;
const key = `${toolId}:${voterId}`;
const previousRating = state.ratings.get(key);
const total = state.ratingSums.get(toolId) ?? tool.averageRating * tool.ratingCount;
const nextCount = tool.ratingCount + (previousRating === undefined ? 1 : 0);
const nextTotal = total - (previousRating ?? 0) + rating;
state.ratings.set(key, rating);
state.ratingSums.set(toolId, nextTotal);
tool.averageRating = nextTotal / nextCount;
tool.ratingCount = nextCount;
return tool;
}
const tool = await getToolById(toolId);
if (!tool) return undefined;
await bigQuery.client.query({
query: `
MERGE ${ratingTable(bigQuery.projectId, bigQuery.dataset)} AS target
USING (SELECT @toolId AS tool_id, @voterId AS voter_id, @rating AS rating) AS source
ON target.tool_id = source.tool_id AND target.voter_id = source.voter_id
WHEN MATCHED THEN
UPDATE SET rating = source.rating, updated_at = CURRENT_TIMESTAMP()
WHEN NOT MATCHED THEN
INSERT (tool_id, voter_id, rating, created_at, updated_at)
VALUES (source.tool_id, source.voter_id, source.rating, CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP())
`,
params: { toolId, voterId, rating },
});
return getToolById(toolId);
}