From 6c8ea50f31a491783287c5adfbf8d62255fb1782 Mon Sep 17 00:00:00 2001 From: p-sw Date: Tue, 2 Jun 2026 23:42:38 +0900 Subject: [PATCH] feat: implemented supporters --- .env.example | 3 +- src/brain/factExtractor.ts | 16 ++++++++++ src/brain/index.ts | 32 ++++++++++++++++++++ src/brain/manager.ts | 55 ++++++++++++++++++++++++++++++++++ src/config.ts | 9 +++++- src/openrouter/promptLoader.ts | 16 ++++++++++ src/openrouter/schema.ts | 37 +++++++++++++++++++++++ 7 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 src/brain/factExtractor.ts create mode 100644 src/brain/index.ts create mode 100644 src/brain/manager.ts create mode 100644 src/openrouter/promptLoader.ts create mode 100644 src/openrouter/schema.ts diff --git a/.env.example b/.env.example index 36d8ee6..4ffc765 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ -DB_PATH= +DB_PATH=./brainbox.db +BRAINDB_PATH=./braindb.json OPENROUTER_API_KEY= diff --git a/src/brain/factExtractor.ts b/src/brain/factExtractor.ts new file mode 100644 index 0000000..2de8436 --- /dev/null +++ b/src/brain/factExtractor.ts @@ -0,0 +1,16 @@ +import { llm } from "@/openrouter"; +import { extractedFactSchema } from "@/openrouter/schema"; +import { type ExtractedFact, LlmFactExtractor } from "identitydb"; + +export const factExtractor = new LlmFactExtractor({ + model: { + async generateText({ instruction, input }) { + return await llm.call(llm.models.identity, { + instruction, + message: input, + jsonSchemaName: "fact-extractor", + jsonSchema: extractedFactSchema, + }); + }, + }, +}); diff --git a/src/brain/index.ts b/src/brain/index.ts new file mode 100644 index 0000000..46fcd4a --- /dev/null +++ b/src/brain/index.ts @@ -0,0 +1,32 @@ +import { config } from "@/config"; +import { IdentityDB, type Space } from "identitydb"; +import { llm } from "@/openrouter"; +import { brainManager, type BrainItem } from "./manager"; + +export class Brain { + constructor( + public db: IdentityDB, + public space: Space, + public brainbase: BrainItem, + ) {} + + static async create( + displayName: string, + seed: string, + ): Promise {} + + static async load(brainId: string): Promise { + const brain = await brainManager.loadBrain(brainId); + if (!brain) return null; + + const db = await IdentityDB.connect({ + client: "sqlite", + filename: config.dbPath, + }); + + const space = await db.getSpaceByName(brain.spaceName); + if (!space) return null; + + return new Brain(db, space, brain); + } +} diff --git a/src/brain/manager.ts b/src/brain/manager.ts new file mode 100644 index 0000000..2b221ea --- /dev/null +++ b/src/brain/manager.ts @@ -0,0 +1,55 @@ +import { config } from "@/config"; +import { readFile, writeFile } from "fs/promises"; + +export interface BrainItem { + brainId: string; + spaceName: string; + displayName: string; + baseSystemPrompt: string; +} +export type BrainDB = Record; + +export class BrainDBManager { + private get db() { + return readFile(config.braindbPath, { encoding: "utf-8" }).then( + (content) => { + return JSON.parse(content) as BrainDB; + }, + ); + } + + private async writeDb(db: BrainDB) { + await writeFile(config.braindbPath, JSON.stringify(db), { + encoding: "utf-8", + }); + } + + async loadBrain(brainId: string): Promise { + const brainOrNot = (await this.db)[brainId]; + return brainOrNot; + } + + async saveBrain(brainId: string, brain: BrainItem) { + const db = await this.db; + db[brainId] = brain; + await this.writeDb(db); + } + + async listBrain() { + return Object.entries(await this.db).map( + ([_, { brainId, displayName }]) => ({ brainId, displayName }), + ); + } + + async deleteBrain(brainId: string) { + const db = await this.db; + delete db[brainId]; + await this.writeDb(db); + } + + async isBrainAvailable(brainId: string) { + return brainId in (await this.db); + } +} + +export const brainManager = new BrainDBManager(); diff --git a/src/config.ts b/src/config.ts index 6121b58..aa7b45d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,15 +1,22 @@ import "dotenv/config"; +import { join } from "path"; export interface Config { openrouterApiKey: string; dbPath: string; + braindbPath: string; } const openrouterApiKey = process.env["OPENROUTER_API_KEY"]; if (!openrouterApiKey) throw new Error("OPENROUTER_API_KEY is missing"); -const dbPath = process.env["DB_PATH"] ?? "sqlite.db"; +const dbPath = join(process.cwd(), process.env["DB_PATH"] ?? "brainbox.db"); +const braindbPath = join( + process.cwd(), + process.env["BRAINDB_PATH"] ?? "brainbox.json", +); export const config: Config = { openrouterApiKey, dbPath, + braindbPath, }; diff --git a/src/openrouter/promptLoader.ts b/src/openrouter/promptLoader.ts new file mode 100644 index 0000000..eff2898 --- /dev/null +++ b/src/openrouter/promptLoader.ts @@ -0,0 +1,16 @@ +import { readFile } from "fs/promises"; +import path from "path"; + +const prompts = ["PERSONA_INIT", "PERSONA_BASE_SYSTEM_PROMPT"] as const; +export type PromptKey = (typeof prompts)[number]; + +function fileName(promptKey: PromptKey): string { + return promptKey.toLowerCase() + ".md"; +} + +const PROMPTS_DIR = path.resolve(import.meta.dir, "../../prompts"); + +export async function loadPrompt(promptKey: PromptKey): Promise { + const filePath = path.join(PROMPTS_DIR, fileName(promptKey)); + return readFile(filePath, "utf-8"); +} diff --git a/src/openrouter/schema.ts b/src/openrouter/schema.ts new file mode 100644 index 0000000..1085113 --- /dev/null +++ b/src/openrouter/schema.ts @@ -0,0 +1,37 @@ +export const extractedFactSchema = { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + statement: { type: "string" }, + summary: { type: "string" }, + source: { type: "string" }, + confidence: { type: "number" }, + metadata: { type: "object", additionalProperties: false }, + topics: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + name: { type: "string" }, + category: { + type: "string", + enum: ["entity", "concept", "temporal", "custom"], + }, + granularity: { + type: "string", + enum: ["abstract", "concrete", "mixed"], + }, + role: { type: "string" }, + description: { type: "string" }, + metadata: { type: "object", additionalProperties: false }, + }, + required: ["name", "category", "granularity", "role", "description"], + }, + }, + }, + required: ["statement", "summary", "source", "confidence", "topics"], + }, +};