fix: make schema types root-level object
This commit is contained in:
@@ -1,16 +1,17 @@
|
|||||||
import { llm } from "@/openrouter";
|
import { llm } from "@/openrouter";
|
||||||
import { extractedFactSchema } from "@/openrouter/schema";
|
import { extractedFactSchema, type ExtractedFactResult } from "@/openrouter/schema";
|
||||||
import { type ExtractedFact, LlmFactExtractor } from "identitydb";
|
import { type ExtractedFact, LlmFactExtractor } from "identitydb";
|
||||||
|
|
||||||
export const factExtractor = new LlmFactExtractor({
|
export const factExtractor = new LlmFactExtractor({
|
||||||
model: {
|
model: {
|
||||||
async generateText({ instruction, input }) {
|
async generateText({ instruction, input }) {
|
||||||
return await llm.call<ExtractedFact[]>(llm.models.identity, {
|
const result = await llm.call<ExtractedFactResult>(llm.models.identity, {
|
||||||
instruction,
|
instruction,
|
||||||
message: input,
|
message: input,
|
||||||
jsonSchemaName: "fact-extractor",
|
jsonSchemaName: "fact-extractor",
|
||||||
jsonSchema: extractedFactSchema,
|
jsonSchema: extractedFactSchema,
|
||||||
});
|
});
|
||||||
|
return result.items;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,16 +80,20 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
|
|||||||
return { items: customDailySlots ?? build48Slots() } as unknown as T;
|
return { items: customDailySlots ?? build48Slots() } as unknown as T;
|
||||||
}
|
}
|
||||||
if (options.jsonSchemaName === "monthly-schedule") {
|
if (options.jsonSchemaName === "monthly-schedule") {
|
||||||
if (customMonthlyDays) return customMonthlyDays as unknown as T;
|
if (customMonthlyDays) {
|
||||||
|
return { items: customMonthlyDays } as unknown as T;
|
||||||
|
}
|
||||||
const match = options.message.match(/\((\d+) days\)/);
|
const match = options.message.match(/\((\d+) days\)/);
|
||||||
const days = match ? parseInt(match[1]!, 10) : 30;
|
const days = match ? parseInt(match[1]!, 10) : 30;
|
||||||
return Array.from({ length: days }, (_, i) => ({
|
return {
|
||||||
day: i + 1,
|
items: Array.from({ length: days }, (_, i) => ({
|
||||||
summary: `Day ${i + 1} summary`,
|
day: i + 1,
|
||||||
})) as unknown as T;
|
summary: `Day ${i + 1} summary`,
|
||||||
|
})),
|
||||||
|
} as unknown as T;
|
||||||
}
|
}
|
||||||
if (options.jsonSchemaName === "availability") {
|
if (options.jsonSchemaName === "availability") {
|
||||||
return (customAvailability ?? buildAvailability()) as unknown as T;
|
return { items: customAvailability ?? buildAvailability() } as unknown as T;
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
|
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
|
||||||
});
|
});
|
||||||
@@ -267,9 +271,11 @@ describe("Brain.createMonthlySchedule", () => {
|
|||||||
const result = await brain.createMonthlySchedule(today, "study for GRE");
|
const result = await brain.createMonthlySchedule(today, "study for GRE");
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result).toHaveLength(expected.daysInMonth);
|
expect(result!.items).toHaveLength(expected.daysInMonth);
|
||||||
expect(result![0]!.day).toBe(1);
|
expect(result!.items[0]!.day).toBe(1);
|
||||||
expect(result![result!.length - 1]!.day).toBe(expected.daysInMonth);
|
expect(result!.items[result!.items.length - 1]!.day).toBe(
|
||||||
|
expected.daysInMonth,
|
||||||
|
);
|
||||||
|
|
||||||
const llmCall = llmCalls.find(
|
const llmCall = llmCalls.find(
|
||||||
(c) => c.options.jsonSchemaName === "monthly-schedule",
|
(c) => c.options.jsonSchemaName === "monthly-schedule",
|
||||||
@@ -285,7 +291,9 @@ describe("Brain.createMonthlySchedule", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(facts).toHaveLength(1);
|
expect(facts).toHaveLength(1);
|
||||||
expect(JSON.parse(facts[0]!.statement)).toHaveLength(expected.daysInMonth);
|
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(
|
||||||
|
expected.daysInMonth,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("S5: year wrap (December 15 -> January next year)", async () => {
|
test("S5: year wrap (December 15 -> January next year)", async () => {
|
||||||
@@ -296,7 +304,7 @@ describe("Brain.createMonthlySchedule", () => {
|
|||||||
const result = await brain.createMonthlySchedule(today, "");
|
const result = await brain.createMonthlySchedule(today, "");
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result).toHaveLength(31);
|
expect(result!.items).toHaveLength(31);
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const facts = await brain.db.getTopicFacts(
|
||||||
`monthly-schedule:${expectedKey}`,
|
`monthly-schedule:${expectedKey}`,
|
||||||
@@ -337,8 +345,8 @@ describe("Brain.getTodayScheduledAvailability", () => {
|
|||||||
const result = await brain.getTodayScheduledAvailability(today);
|
const result = await brain.getTodayScheduledAvailability(today);
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result!.length).toBeGreaterThan(0);
|
expect(result!.items.length).toBeGreaterThan(0);
|
||||||
for (const w of result!) {
|
for (const w of result!.items) {
|
||||||
expect(["online", "do-not-disturb", "offline"]).toContain(w.status);
|
expect(["online", "do-not-disturb", "offline"]).toContain(w.status);
|
||||||
expect(w.start).toMatch(/^([01][0-9]|2[0-3]):[0-5][0-9]$/);
|
expect(w.start).toMatch(/^([01][0-9]|2[0-3]):[0-5][0-9]$/);
|
||||||
expect(w.end).toMatch(/^([01][0-9]|2[0-3]):[0-5][0-9]$|^24:00$/);
|
expect(w.end).toMatch(/^([01][0-9]|2[0-3]):[0-5][0-9]$|^24:00$/);
|
||||||
@@ -451,7 +459,7 @@ describe("Brain.createDebug", () => {
|
|||||||
|
|
||||||
const schedule = await brain.createMonthlySchedule(today, "msg");
|
const schedule = await brain.createMonthlySchedule(today, "msg");
|
||||||
expect(schedule).not.toBeNull();
|
expect(schedule).not.toBeNull();
|
||||||
expect(schedule).toHaveLength(expected.daysInMonth);
|
expect(schedule!.items).toHaveLength(expected.daysInMonth);
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const facts = await brain.db.getTopicFacts(
|
||||||
`monthly-schedule:${monthKey}`,
|
`monthly-schedule:${monthKey}`,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
dailyScheduleSchema,
|
dailyScheduleSchema,
|
||||||
monthlyScheduleSchema,
|
monthlyScheduleSchema,
|
||||||
type Availability,
|
type Availability,
|
||||||
|
type AvailabilityWindows,
|
||||||
type DailySchedule,
|
type DailySchedule,
|
||||||
type MonthlySchedule,
|
type MonthlySchedule,
|
||||||
} from "@/openrouter/schema";
|
} from "@/openrouter/schema";
|
||||||
@@ -28,7 +29,7 @@ export interface DebugOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Brain {
|
export class Brain {
|
||||||
private availabilityCache: Map<string, Availability[]> = new Map();
|
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public db: IdentityDB,
|
public db: IdentityDB,
|
||||||
@@ -142,7 +143,7 @@ export class Brain {
|
|||||||
await this.db.addFact({
|
await this.db.addFact({
|
||||||
spaceName: this.space.name,
|
spaceName: this.space.name,
|
||||||
statement: JSON.stringify(schedule),
|
statement: JSON.stringify(schedule),
|
||||||
summary: `Monthly schedule for ${monthKey} (${schedule.length} days)`,
|
summary: `Monthly schedule for ${monthKey} (${schedule.items.length} days)`,
|
||||||
source: "createMonthlySchedule",
|
source: "createMonthlySchedule",
|
||||||
confidence: 1.0,
|
confidence: 1.0,
|
||||||
topics: [
|
topics: [
|
||||||
@@ -178,7 +179,7 @@ export class Brain {
|
|||||||
|
|
||||||
async getTodayScheduledAvailability(
|
async getTodayScheduledAvailability(
|
||||||
datetime: Date,
|
datetime: Date,
|
||||||
): Promise<Availability[] | null> {
|
): Promise<AvailabilityWindows | null> {
|
||||||
try {
|
try {
|
||||||
const dateKey = formatDateKey(datetime);
|
const dateKey = formatDateKey(datetime);
|
||||||
const cached = this.availabilityCache.get(dateKey);
|
const cached = this.availabilityCache.get(dateKey);
|
||||||
@@ -212,7 +213,7 @@ export class Brain {
|
|||||||
|
|
||||||
async deriveAvailabilityFromSchedule(
|
async deriveAvailabilityFromSchedule(
|
||||||
schedule: DailySchedule,
|
schedule: DailySchedule,
|
||||||
): Promise<Availability[]> {
|
): Promise<AvailabilityWindows> {
|
||||||
try {
|
try {
|
||||||
const instruction = await loadPrompt("SCHEDULE_AVAILABILITY");
|
const instruction = await loadPrompt("SCHEDULE_AVAILABILITY");
|
||||||
const promptMessage = JSON.stringify({
|
const promptMessage = JSON.stringify({
|
||||||
@@ -220,7 +221,7 @@ export class Brain {
|
|||||||
personality: this.brainbase.baseSystemPrompt,
|
personality: this.brainbase.baseSystemPrompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await llm.call<Availability[]>(llm.models.identity, {
|
return await llm.call<AvailabilityWindows>(llm.models.identity, {
|
||||||
instruction,
|
instruction,
|
||||||
message: promptMessage,
|
message: promptMessage,
|
||||||
jsonSchemaName: "availability",
|
jsonSchemaName: "availability",
|
||||||
@@ -249,7 +250,7 @@ export class Brain {
|
|||||||
|
|
||||||
const monthly = JSON.parse(facts[0]!.statement) as MonthlySchedule;
|
const monthly = JSON.parse(facts[0]!.statement) as MonthlySchedule;
|
||||||
const day = target.getDate();
|
const day = target.getDate();
|
||||||
const entry = monthly.find((d) => d.day === day);
|
const entry = monthly.items.find((d) => d.day === day);
|
||||||
return entry?.summary ?? null;
|
return entry?.summary ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
|
|||||||
return GENERATED_BASE_SYSTEM_PROMPT as unknown as T;
|
return GENERATED_BASE_SYSTEM_PROMPT as unknown as T;
|
||||||
}
|
}
|
||||||
if (options.jsonSchemaName === "fact-extractor") {
|
if (options.jsonSchemaName === "fact-extractor") {
|
||||||
return EXTRACTED_FACTS as unknown as T;
|
return { items: EXTRACTED_FACTS } as unknown as T;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`,
|
`unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|||||||
import type {
|
import type {
|
||||||
Availability,
|
Availability,
|
||||||
DailySlot,
|
DailySlot,
|
||||||
MonthlySchedule,
|
MonthlyDay,
|
||||||
} from "@/openrouter/schema";
|
} from "@/openrouter/schema";
|
||||||
|
|
||||||
interface RecordedCall {
|
interface RecordedCall {
|
||||||
@@ -39,7 +39,7 @@ function buildAvailability(): Availability[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMonthly(): MonthlySchedule {
|
function buildMonthlyDays(): MonthlyDay[] {
|
||||||
return Array.from({ length: 30 }, (_, i) => ({
|
return Array.from({ length: 30 }, (_, i) => ({
|
||||||
day: i + 1,
|
day: i + 1,
|
||||||
summary: `Day ${i + 1} summary`,
|
summary: `Day ${i + 1} summary`,
|
||||||
@@ -53,12 +53,15 @@ const mockCall = mock(async (_model: unknown, options: any) => {
|
|||||||
if (options.jsonSchemaName === "monthly-schedule") {
|
if (options.jsonSchemaName === "monthly-schedule") {
|
||||||
const match = (options.message as string).match(/\((\d+) days\)/);
|
const match = (options.message as string).match(/\((\d+) days\)/);
|
||||||
const days = match ? parseInt(match[1]!, 10) : 30;
|
const days = match ? parseInt(match[1]!, 10) : 30;
|
||||||
return Array.from({ length: days }, (_, i) => ({
|
return {
|
||||||
day: i + 1,
|
items: Array.from({ length: days }, (_, i) => ({
|
||||||
summary: `Day ${i + 1} summary`,
|
day: i + 1,
|
||||||
}));
|
summary: `Day ${i + 1} summary`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (options.jsonSchemaName === "availability") return buildAvailability();
|
if (options.jsonSchemaName === "availability")
|
||||||
|
return { items: buildAvailability() };
|
||||||
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
|
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,7 +90,6 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// ensure no on-disk braindb was created
|
|
||||||
const { unlink } = await import("fs/promises");
|
const { unlink } = await import("fs/promises");
|
||||||
try {
|
try {
|
||||||
await unlink("/tmp/brainbox-test-braindb-debug-schedule.json");
|
await unlink("/tmp/brainbox-test-braindb-debug-schedule.json");
|
||||||
@@ -106,7 +108,7 @@ describe("runDebugScheduleDaily", () => {
|
|||||||
|
|
||||||
expect(result.kind).toBe("daily");
|
expect(result.kind).toBe("daily");
|
||||||
expect(result.schedule.items).toHaveLength(48);
|
expect(result.schedule.items).toHaveLength(48);
|
||||||
expect(result.availability.length).toBeGreaterThan(0);
|
expect(result.availability.items.length).toBeGreaterThan(0);
|
||||||
expect(result.dateKey).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
expect(result.dateKey).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
|
||||||
const dailyCall = llmCalls.find(
|
const dailyCall = llmCalls.find(
|
||||||
@@ -145,7 +147,7 @@ describe("runDebugScheduleMonthly", () => {
|
|||||||
if (!result.ok) throw new Error("expected ok");
|
if (!result.ok) throw new Error("expected ok");
|
||||||
|
|
||||||
expect(result.kind).toBe("monthly");
|
expect(result.kind).toBe("monthly");
|
||||||
expect(result.schedule).toHaveLength(result.daysInMonth);
|
expect(result.schedule.items).toHaveLength(result.daysInMonth);
|
||||||
expect(result.monthKey).toMatch(/^\d{4}-\d{2}$/);
|
expect(result.monthKey).toMatch(/^\d{4}-\d{2}$/);
|
||||||
|
|
||||||
const call = llmCalls.find(
|
const call = llmCalls.find(
|
||||||
@@ -187,3 +189,4 @@ describe("debug schedule no-disk invariant", () => {
|
|||||||
expect(afterJson).toBe(false);
|
expect(afterJson).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Command } from "commander";
|
|||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import { Brain } from "@/brain";
|
import { Brain } from "@/brain";
|
||||||
import {
|
import {
|
||||||
type Availability,
|
type AvailabilityWindows,
|
||||||
type DailySchedule,
|
type DailySchedule,
|
||||||
type MonthlySchedule,
|
type MonthlySchedule,
|
||||||
} from "@/openrouter/schema";
|
} from "@/openrouter/schema";
|
||||||
@@ -21,7 +21,7 @@ export type DailyRunResult =
|
|||||||
dateKey: string;
|
dateKey: string;
|
||||||
tomorrow: Date;
|
tomorrow: Date;
|
||||||
schedule: DailySchedule;
|
schedule: DailySchedule;
|
||||||
availability: Availability[];
|
availability: AvailabilityWindows;
|
||||||
}
|
}
|
||||||
| { ok: false; error: string };
|
| { ok: false; error: string };
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ export async function runDebugScheduleDaily(
|
|||||||
return { ok: false, error: "Availability derivation failed" };
|
return { ok: false, error: "Availability derivation failed" };
|
||||||
}
|
}
|
||||||
availSpinner.succeed(
|
availSpinner.succeed(
|
||||||
`Availability derived (${availability.length} windows)`,
|
`Availability derived (${availability.items.length} windows)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
printSection(`Availability — ${dateKey}`);
|
printSection(`Availability — ${dateKey}`);
|
||||||
@@ -108,7 +108,7 @@ export async function runDebugScheduleMonthly(
|
|||||||
return { ok: false, error: "Monthly schedule generation failed" };
|
return { ok: false, error: "Monthly schedule generation failed" };
|
||||||
}
|
}
|
||||||
scheduleSpinner.succeed(
|
scheduleSpinner.succeed(
|
||||||
`Monthly schedule generated (${schedule.length} day summaries)`,
|
`Monthly schedule generated (${schedule.items.length} day summaries)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
printSection(`Monthly Schedule — ${monthKey} (${next.daysInMonth} days)`);
|
printSection(`Monthly Schedule — ${monthKey} (${next.daysInMonth} days)`);
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ export const availabilitySchema = {
|
|||||||
// Types — co-located with their schemas.
|
// Types — co-located with their schemas.
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import type { ExtractedFact } from "identitydb";
|
||||||
|
|
||||||
/** A single 30-minute slot in a daily schedule. Matches `dailyScheduleSchema.items.items`. */
|
/** A single 30-minute slot in a daily schedule. Matches `dailyScheduleSchema.items.items`. */
|
||||||
export type DailySlot = {
|
export type DailySlot = {
|
||||||
start: string;
|
start: string;
|
||||||
@@ -150,24 +152,43 @@ export type DailySchedule = {
|
|||||||
items: DailySlot[];
|
items: DailySlot[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A single day's summary inside a monthly schedule. Matches `monthlyScheduleSchema.items`. */
|
/** A single day's summary inside a monthly schedule. Matches `monthlyScheduleSchema.items.items`. */
|
||||||
export type MonthlyDay = {
|
export type MonthlyDay = {
|
||||||
day: number;
|
day: number;
|
||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A complete monthly schedule. Matches `monthlyScheduleSchema`. */
|
/**
|
||||||
export type MonthlySchedule = MonthlyDay[];
|
* A complete monthly schedule: a wrapped object containing one entry per day
|
||||||
|
* of the month. Matches `monthlyScheduleSchema`.
|
||||||
|
*/
|
||||||
|
export type MonthlySchedule = {
|
||||||
|
items: MonthlyDay[];
|
||||||
|
};
|
||||||
|
|
||||||
/** Reachability status for a single availability window. */
|
/** Reachability status for a single availability window. */
|
||||||
export type AvailabilityStatus = "online" | "do-not-disturb" | "offline";
|
export type AvailabilityStatus = "online" | "do-not-disturb" | "offline";
|
||||||
|
|
||||||
/** A single availability window. Matches `availabilitySchema.items`. */
|
/** A single availability window. Matches `availabilitySchema.items.items`. */
|
||||||
export type Availability = {
|
export type Availability = {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
status: AvailabilityStatus;
|
status: AvailabilityStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The full set of availability windows for a day. Matches `availabilitySchema`. */
|
/**
|
||||||
export type AvailabilityWindows = Availability[];
|
* The full set of availability windows for a day: a wrapped object containing
|
||||||
|
* one or more windows. Matches `availabilitySchema`.
|
||||||
|
*/
|
||||||
|
export type AvailabilityWindows = {
|
||||||
|
items: Availability[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wrapped envelope the LLM returns for `extractedFactSchema`. The inner
|
||||||
|
* `items` array is the list of `ExtractedFact` (which is defined in the
|
||||||
|
* external `identitydb` package).
|
||||||
|
*/
|
||||||
|
export type ExtractedFactResult = {
|
||||||
|
items: ExtractedFact[];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user