@unified-llm/core
Version:
Unified LLM interface (in-memory).
451 lines • 21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.threadRepository = exports.ThreadRepository = void 0;
const drizzle_orm_1 = require("drizzle-orm");
const connection_1 = require("./connection");
const schema_1 = require("./schema");
const uuid_1 = require("uuid");
class ThreadRepository {
constructor(dbPath) {
this.dbPromise = (0, connection_1.getDatabase)(dbPath); // Promise<LibSQLDatabase | null>
}
/* ────────────── ヘルパ ────────────── */
async getDb() {
return await this.dbPromise;
}
async isDisabled() {
return (await this.getDb()) === null;
}
/* ========== スレッド管理 ========== */
/** 新規作成 */
async createThread(config = {}) {
var _a, _b, _c, _d, _e, _f, _g;
const now = new Date();
const newThread = {
id: (_a = config.threadId) !== null && _a !== void 0 ? _a : `thread_${(0, uuid_1.v4)()}`,
title: config.title,
description: config.description,
createdAt: now,
updatedAt: now,
createdBy: config.createdBy,
isActive: true,
tags: config.tags ? JSON.stringify(config.tags) : null,
metadata: config.metadata ? JSON.stringify(config.metadata) : null,
};
if (await this.isDisabled()) {
return {
...newThread,
title: (_b = newThread.title) !== null && _b !== void 0 ? _b : null,
description: (_c = newThread.description) !== null && _c !== void 0 ? _c : null,
createdBy: (_d = newThread.createdBy) !== null && _d !== void 0 ? _d : null,
tags: (_e = newThread.tags) !== null && _e !== void 0 ? _e : null,
metadata: (_f = newThread.metadata) !== null && _f !== void 0 ? _f : null,
isActive: (_g = newThread.isActive) !== null && _g !== void 0 ? _g : null,
};
}
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const [thread] = await db.insert(schema_1.threads).values(newThread).returning();
return thread;
}
/** 取得 */
async getThread(threadId) {
if (await this.isDisabled())
return null;
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const [thread] = await db
.select()
.from(schema_1.threads)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.threads.id, threadId), (0, drizzle_orm_1.eq)(schema_1.threads.isActive, true)));
return thread !== null && thread !== void 0 ? thread : null;
}
/** 更新 */
async updateThread(threadId, updates) {
if (await this.isDisabled())
return null;
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const updateData = { ...updates, updatedAt: new Date() };
if (updates.tags)
updateData.tags = JSON.stringify(updates.tags);
if (updates.metadata)
updateData.metadata = JSON.stringify(updates.metadata);
const [updated] = await db
.update(schema_1.threads)
.set(updateData)
.where((0, drizzle_orm_1.eq)(schema_1.threads.id, threadId))
.returning();
return updated !== null && updated !== void 0 ? updated : null;
}
/** 論理削除 */
async deleteThread(threadId) {
var _a;
if (await this.isDisabled())
return false;
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const res = await db
.update(schema_1.threads)
.set({ isActive: false, updatedAt: new Date() })
.where((0, drizzle_orm_1.eq)(schema_1.threads.id, threadId))
.run();
const affected = typeof res.rowsAffected === 'bigint'
? Number(res.rowsAffected)
: (_a = res.rowsAffected) !== null && _a !== void 0 ? _a : 0;
return affected > 0;
}
/** スレッド一覧 */
async listThreads(options) {
var _a, _b, _c, _d, _e, _f;
if (await this.isDisabled())
return [];
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 50;
const offset = (_b = options === null || options === void 0 ? void 0 : options.offset) !== null && _b !== void 0 ? _b : 0;
const conditions = [(0, drizzle_orm_1.eq)(schema_1.threads.isActive, true)];
if (options === null || options === void 0 ? void 0 : options.createdBy)
conditions.push((0, drizzle_orm_1.eq)(schema_1.threads.createdBy, options.createdBy));
const base = await db
.select({
id: schema_1.threads.id,
title: schema_1.threads.title,
description: schema_1.threads.description,
updatedAt: schema_1.threads.updatedAt,
tags: schema_1.threads.tags,
messageCount: (0, drizzle_orm_1.sql) `count(distinct ${schema_1.messages.id})`,
})
.from(schema_1.threads)
.leftJoin(schema_1.messages, (0, drizzle_orm_1.eq)(schema_1.messages.threadId, schema_1.threads.id))
.where((0, drizzle_orm_1.and)(...conditions))
.groupBy(schema_1.threads.id)
.orderBy((0, drizzle_orm_1.desc)(schema_1.threads.updatedAt))
.limit(limit)
.offset(offset);
const summaries = [];
for (const row of base) {
const participants = await this.getActiveParticipants(row.id);
summaries.push({
id: row.id,
title: (_c = row.title) !== null && _c !== void 0 ? _c : undefined,
description: (_d = row.description) !== null && _d !== void 0 ? _d : undefined,
messageCount: (_e = row.messageCount) !== null && _e !== void 0 ? _e : 0,
participantCount: participants.length,
lastActivity: row.updatedAt,
activeParticipants: participants.map((p) => p.clientId),
tags: row.tags ? JSON.parse(row.tags) : undefined,
});
}
if ((_f = options === null || options === void 0 ? void 0 : options.tags) === null || _f === void 0 ? void 0 : _f.length) {
return summaries.filter((s) => { var _a; return (_a = s.tags) === null || _a === void 0 ? void 0 : _a.some((t) => { var _a, _b; return (_b = (_a = options.tags) === null || _a === void 0 ? void 0 : _a.includes(t)) !== null && _b !== void 0 ? _b : false; }); });
}
return summaries;
}
/* ========== 参加者管理 ========== */
/** 参加 */
async joinThread(threadId, clientId, options = {}) {
var _a, _b, _c, _d, _e;
const now = new Date();
const newP = {
id: `participant_${(0, uuid_1.v4)()}`,
threadId,
clientId,
joinedAt: now,
leftAt: null,
role: (_a = options.role) !== null && _a !== void 0 ? _a : 'participant',
nickname: options.nickname,
metadata: options.metadata ? JSON.stringify(options.metadata) : null,
};
if (await this.isDisabled()) {
return {
...newP,
nickname: (_b = newP.nickname) !== null && _b !== void 0 ? _b : null,
metadata: (_c = newP.metadata) !== null && _c !== void 0 ? _c : null,
role: (_d = newP.role) !== null && _d !== void 0 ? _d : null,
leftAt: (_e = newP.leftAt) !== null && _e !== void 0 ? _e : null,
};
}
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const existing = await db
.select()
.from(schema_1.threadParticipants)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.threadParticipants.threadId, threadId), (0, drizzle_orm_1.eq)(schema_1.threadParticipants.clientId, clientId), (0, drizzle_orm_1.isNull)(schema_1.threadParticipants.leftAt)));
if (existing.length) {
throw new Error(`LLMClient ${clientId} is already participating in thread ${threadId}`);
}
const [participant] = await db
.insert(schema_1.threadParticipants)
.values(newP)
.returning();
await db
.update(schema_1.threads)
.set({ updatedAt: now })
.where((0, drizzle_orm_1.eq)(schema_1.threads.id, threadId))
.run();
return participant;
}
/** 離脱 */
async leaveThread(threadId, clientId) {
var _a;
if (await this.isDisabled())
return false;
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const now = new Date();
const res = await db
.update(schema_1.threadParticipants)
.set({ leftAt: now })
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.threadParticipants.threadId, threadId), (0, drizzle_orm_1.eq)(schema_1.threadParticipants.clientId, clientId), (0, drizzle_orm_1.isNull)(schema_1.threadParticipants.leftAt)))
.run();
const affected = typeof res.rowsAffected === 'bigint'
? Number(res.rowsAffected)
: (_a = res.rowsAffected) !== null && _a !== void 0 ? _a : 0;
if (affected > 0) {
await db
.update(schema_1.threads)
.set({ updatedAt: now })
.where((0, drizzle_orm_1.eq)(schema_1.threads.id, threadId))
.run();
}
return affected > 0;
}
/** アクティブ参加者 */
async getActiveParticipants(threadId) {
if (await this.isDisabled())
return [];
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const rows = await db
.select({
participant: schema_1.threadParticipants,
client: schema_1.llmClients,
})
.from(schema_1.threadParticipants)
.innerJoin(schema_1.llmClients, (0, drizzle_orm_1.eq)(schema_1.threadParticipants.clientId, schema_1.llmClients.id))
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.threadParticipants.threadId, threadId), (0, drizzle_orm_1.isNull)(schema_1.threadParticipants.leftAt)))
.orderBy((0, drizzle_orm_1.asc)(schema_1.threadParticipants.joinedAt));
return rows.map((r) => ({ ...r.participant, client: r.client }));
}
/** ある時点以降の参加者 */
async getParticipantsSince(threadId, since) {
if (await this.isDisabled())
return [];
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const rows = await db
.select({
participant: schema_1.threadParticipants,
client: schema_1.llmClients,
})
.from(schema_1.threadParticipants)
.innerJoin(schema_1.llmClients, (0, drizzle_orm_1.eq)(schema_1.threadParticipants.clientId, schema_1.llmClients.id))
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.threadParticipants.threadId, threadId), (0, drizzle_orm_1.sql) `${schema_1.threadParticipants.joinedAt} >= ${since.getTime()}`, (0, drizzle_orm_1.isNull)(schema_1.threadParticipants.leftAt)))
.orderBy((0, drizzle_orm_1.asc)(schema_1.threadParticipants.joinedAt));
return rows.map((r) => ({ ...r.participant, client: r.client }));
}
/* ========== メッセージ管理 ========== */
/** 追加 */
async addMessage(data) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const newMsg = {
id: `msg_${(0, uuid_1.v4)()}`,
threadId: data.threadId,
clientId: data.clientId,
role: data.role,
content: data.content,
toolCalls: data.toolCalls,
toolResults: data.toolResults,
timestamp: new Date(),
sequence: 1,
parentMessageId: data.parentMessageId,
isEdited: false,
editedAt: null,
tokens: data.tokens,
cost: data.cost,
metadata: data.metadata,
};
if (await this.isDisabled()) {
return {
...newMsg,
parentMessageId: (_a = newMsg.parentMessageId) !== null && _a !== void 0 ? _a : null,
metadata: (_b = newMsg.metadata) !== null && _b !== void 0 ? _b : null,
toolCalls: (_c = newMsg.toolCalls) !== null && _c !== void 0 ? _c : null,
toolResults: (_d = newMsg.toolResults) !== null && _d !== void 0 ? _d : null,
tokens: (_e = newMsg.tokens) !== null && _e !== void 0 ? _e : null,
cost: (_f = newMsg.cost) !== null && _f !== void 0 ? _f : null,
sequence: (_g = newMsg.sequence) !== null && _g !== void 0 ? _g : null,
threadId: (_h = newMsg.threadId) !== null && _h !== void 0 ? _h : null,
clientId: (_j = newMsg.clientId) !== null && _j !== void 0 ? _j : null,
editedAt: (_k = newMsg.editedAt) !== null && _k !== void 0 ? _k : null,
isEdited: (_l = newMsg.isEdited) !== null && _l !== void 0 ? _l : null,
};
}
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const [cnt] = await db
.select({ count: (0, drizzle_orm_1.sql) `count(*)` })
.from(schema_1.messages)
.where((0, drizzle_orm_1.eq)(schema_1.messages.threadId, data.threadId));
newMsg.sequence = ((_m = cnt.count) !== null && _m !== void 0 ? _m : 0) + 1;
const [msg] = await db.insert(schema_1.messages).values(newMsg).returning();
await db
.update(schema_1.threads)
.set({ updatedAt: new Date() })
.where((0, drizzle_orm_1.eq)(schema_1.threads.id, data.threadId))
.run();
return msg;
}
/** スレッドのメッセージ取得 */
async getThreadMessages(threadId, options) {
var _a, _b;
if (await this.isDisabled())
return [];
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const cond = [(0, drizzle_orm_1.eq)(schema_1.messages.threadId, threadId)];
if (options === null || options === void 0 ? void 0 : options.since) {
cond.push((0, drizzle_orm_1.sql) `${schema_1.messages.timestamp} >= ${options.since.getTime()}`);
}
return await db
.select()
.from(schema_1.messages)
.where((0, drizzle_orm_1.and)(...cond))
.orderBy((0, drizzle_orm_1.asc)(schema_1.messages.sequence))
.limit((_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 1000)
.offset((_b = options === null || options === void 0 ? void 0 : options.offset) !== null && _b !== void 0 ? _b : 0);
}
/** 参加以降に見えるメッセージ */
async getVisibleMessages(threadId, clientId, options) {
var _a;
if (await this.isDisabled())
return [];
const db = await this.getDb();
if (!db)
throw new Error('Database connection is not available');
const [participant] = await db
.select()
.from(schema_1.threadParticipants)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.threadParticipants.threadId, threadId), (0, drizzle_orm_1.eq)(schema_1.threadParticipants.clientId, clientId)))
.orderBy((0, drizzle_orm_1.asc)(schema_1.threadParticipants.joinedAt))
.limit(1);
if (!participant) {
throw new Error(`LLMClient ${clientId} is not a participant in thread ${threadId}`);
}
const sinceTs = participant.joinedAt.getTime();
let msgs;
if (options === null || options === void 0 ? void 0 : options.includeContext) {
const after = await db
.select()
.from(schema_1.messages)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.messages.threadId, threadId), (0, drizzle_orm_1.sql) `${schema_1.messages.timestamp} >= ${sinceTs}`))
.orderBy((0, drizzle_orm_1.asc)(schema_1.messages.sequence));
const before = await db
.select()
.from(schema_1.messages)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.messages.threadId, threadId), (0, drizzle_orm_1.sql) `${schema_1.messages.timestamp} < ${sinceTs}`))
.orderBy((0, drizzle_orm_1.desc)(schema_1.messages.sequence))
.limit((_a = options.contextLimit) !== null && _a !== void 0 ? _a : 20);
msgs = [...before.reverse(), ...after];
}
else {
msgs = await db
.select()
.from(schema_1.messages)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.messages.threadId, threadId), (0, drizzle_orm_1.sql) `${schema_1.messages.timestamp} >= ${sinceTs}`))
.orderBy((0, drizzle_orm_1.asc)(schema_1.messages.sequence));
}
return msgs;
}
/* ========== 統一変換 / 統計 ========== */
async getConversationThread(threadId) {
var _a;
if (await this.isDisabled())
return null;
const thread = await this.getThread(threadId);
if (!thread)
return null;
const msgs = await this.getThreadMessages(threadId);
return {
id: thread.id,
title: (_a = thread.title) !== null && _a !== void 0 ? _a : undefined,
messages: msgs.map(this.convertToUnifiedMessage),
created_at: thread.createdAt,
updated_at: thread.updatedAt,
metadata: {
description: thread.description,
created_by: thread.createdBy,
tags: thread.tags ? JSON.parse(thread.tags) : undefined,
...(thread.metadata ? JSON.parse(thread.metadata) : {}),
},
};
}
convertToUnifiedMessage(db) {
var _a;
return {
id: db.id,
role: db.role,
content: db.content,
created_at: db.timestamp,
metadata: {
client_id: db.clientId,
tokens: db.tokens,
cost: db.cost,
tool_calls: db.toolCalls,
tool_results: db.toolResults,
sequence: db.sequence,
parent_message_id: db.parentMessageId,
is_edited: db.isEdited,
edited_at: db.editedAt,
...((_a = db.metadata) !== null && _a !== void 0 ? _a : {}),
},
};
}
/** 統計 */
async getThreadStats(threadId) {
if (await this.isDisabled()) {
return {
messageCount: 0,
participantCount: 0,
totalTokens: 0,
totalCost: 0,
participants: [],
};
}
const msgs = await this.getThreadMessages(threadId);
const parts = await this.getActiveParticipants(threadId);
const base = msgs.reduce((acc, m) => {
var _a, _b;
acc.messageCount++;
acc.totalTokens += (_a = m.tokens) !== null && _a !== void 0 ? _a : 0;
acc.totalCost += (_b = m.cost) !== null && _b !== void 0 ? _b : 0;
return acc;
}, { messageCount: 0, totalTokens: 0, totalCost: 0 });
const perParticipant = parts.map((p) => ({
clientId: p.clientId,
messageCount: msgs.filter((m) => m.clientId === p.clientId).length,
joinedAt: p.joinedAt,
}));
return {
...base,
participantCount: parts.length,
participants: perParticipant,
};
}
}
exports.ThreadRepository = ThreadRepository;
/* シングルトン */
exports.threadRepository = new ThreadRepository();
//# sourceMappingURL=thread-repository.js.map