@unified-llm/core
Version:
Unified LLM interface.
487 lines • 20.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Thread = void 0;
const llm_client_1 = require("./llm-client");
const thread_repository_1 = require("./database/thread-repository");
const client_repository_1 = require("./database/client-repository");
const uuid_1 = require("uuid");
/**
* スレッドベースのチャットセッション
* 複数のLLMクライアントが途中から参加可能な永続化された会話スレッド
*/
class Thread {
constructor(config = {}) {
var _a;
this.clients = new Map();
this.messages = [];
this._isLoaded = false;
this._runtimeClientIds = new Set(); // 実行時クライアントIDの追跡
this.id = config.threadId || `thread_${(0, uuid_1.v4)()}`;
this.title = config.title;
this.description = config.description;
this._createdBy = config.createdBy;
this._tags = config.tags;
this.autoSave = (_a = config.autoSave) !== null && _a !== void 0 ? _a : true;
this.repository = new thread_repository_1.ThreadRepository(config.dbPath);
}
/**
* 既存のスレッドをロード
*/
async load() {
var _a, _b, _c;
try {
const thread = await this.repository.getConversationThread(this.id);
if (thread) {
this.title = thread.title;
this.description = (_a = thread.metadata) === null || _a === void 0 ? void 0 : _a.description;
this._createdBy = (_b = thread.metadata) === null || _b === void 0 ? void 0 : _b.created_by;
this._tags = (_c = thread.metadata) === null || _c === void 0 ? void 0 : _c.tags;
this.messages = thread.messages;
this._isLoaded = true;
return true;
}
return false;
}
catch (error) {
console.error('Failed to load thread session:', error);
return false;
}
}
/**
* スレッドを保存
*/
async save() {
var _a, _b, _c, _d, _e, _f;
try {
// Skip saving if persistence is disabled
if (this.repository.isDisabled) {
return true;
}
// スレッドが存在しない場合は作成
let thread = await this.repository.getThread(this.id);
if (!thread) {
const ThreadConfig = {
title: this.title,
description: this.description,
createdBy: this._createdBy,
tags: this._tags,
};
thread = await this.repository.createThread(ThreadConfig);
this.id = thread.id;
}
else if (this.title !== thread.title || this.description !== thread.description) {
await this.repository.updateThread(this.id, {
title: this.title,
description: this.description,
tags: this._tags,
});
}
// 新しいメッセージを保存
const existingMessages = await this.repository.getThreadMessages(this.id);
const existingMessageIds = new Set(existingMessages.map(m => m.id));
for (const message of this.messages) {
if (!existingMessageIds.has(message.id)) {
// clientIdが存在する場合のみ設定、存在しない場合はundefinedにする
let clientId = (_a = message.metadata) === null || _a === void 0 ? void 0 : _a.client_id;
if (clientId) {
try {
const { ClientRepository } = await Promise.resolve().then(() => __importStar(require('./database/client-repository')));
const assistantRepo = new ClientRepository();
const assistant = await assistantRepo.findById(clientId);
if (!assistant) {
clientId = undefined; // 存在しないアシスタントIDの場合はundefinedに
}
}
catch (_error) {
clientId = undefined; // エラーの場合もundefinedに
}
}
await this.repository.addMessage({
threadId: this.id,
clientId: clientId,
role: message.role,
content: message.content,
toolCalls: (_b = message.metadata) === null || _b === void 0 ? void 0 : _b.tool_calls,
toolResults: (_c = message.metadata) === null || _c === void 0 ? void 0 : _c.tool_results,
parentMessageId: (_d = message.metadata) === null || _d === void 0 ? void 0 : _d.parent_message_id,
tokens: (_e = message.metadata) === null || _e === void 0 ? void 0 : _e.tokens,
cost: (_f = message.metadata) === null || _f === void 0 ? void 0 : _f.cost,
metadata: message.metadata,
});
}
}
this._isLoaded = true;
return true;
}
catch (error) {
console.error('Failed to save thread session:', error);
return false;
}
}
/**
* 保存されたLLMクライアントをスレッドに参加させる
*/
async addAssistantById(clientId, nickname, options = {}) {
// 保存されたLLMクライアント設定を取得
const storedAssistant = await client_repository_1.clientRepository.findById(clientId);
if (!storedAssistant) {
throw new Error(`LLM client with ID ${clientId} not found`);
}
// APIキーを環境変数から取得
const apiKey = storedAssistant.apiKey || process.env[`${storedAssistant.provider.toUpperCase()}_API_KEY`];
if (!apiKey) {
throw new Error(`API key not found for provider ${storedAssistant.provider}`);
}
// LLMClientインスタンスを作成
const assistant = await llm_client_1.LLMClient.fromSaved(clientId, apiKey);
// スレッドに参加
await this.repository.joinThread(this.id, clientId, {
role: options.role || 'participant',
nickname: nickname || storedAssistant.name,
metadata: {
...options.metadata,
includeContext: options.includeContext,
contextLimit: options.contextLimit,
}
});
// メモリに追加
const displayName = nickname || storedAssistant.name;
this.clients.set(displayName, assistant);
// 自動保存
if (this.autoSave) {
await this.save();
}
}
/**
* 実行時LLMクライアントをスレッドに参加させる(実行時作成対応)
*/
async addAssistant(assistant, name, options = {}) {
let clientId = options.clientId;
// clientIdが指定されていない場合、一時的なIDを生成
if (!clientId) {
clientId = `runtime_${(0, uuid_1.v4)()}`;
this._runtimeClientIds.add(clientId);
}
// カスタムIDも含めて、すべての実行時アシスタントをDBに保存
if (!clientId.startsWith('runtime_') || options.clientId) {
this._runtimeClientIds.add(clientId);
}
// 実行時LLMクライアントの情報を一時的にDBに保存
const runtimeAssistantConfig = {
id: clientId,
name: name,
description: `Runtime LLM client: ${name}`,
provider: options.provider || 'openai', // デフォルトプロバイダー
model: options.model,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
metadata: {
isRuntime: true, // 実行時LLMクライアントマーク
sessionId: this.id
}
};
try {
await client_repository_1.clientRepository.save(runtimeAssistantConfig);
}
catch (error) {
console.warn('Failed to save runtime LLM client to DB, proceeding with memory-only mode:', error);
}
// スレッドに参加
await this.repository.joinThread(this.id, clientId, {
role: options.role || 'participant',
nickname: name,
metadata: {
...options.metadata,
isRuntime: !options.clientId, // 実行時LLMクライアントかどうか
includeContext: options.includeContext,
contextLimit: options.contextLimit,
},
});
// メモリに追加
this.clients.set(name, assistant);
// 自動保存
if (this.autoSave) {
await this.save();
}
}
/**
* LLMクライアントをスレッドから離脱させる
*/
async removeAssistant(nameOrId) {
// メモリから削除
let removed = false;
if (this.clients.has(nameOrId)) {
this.clients.delete(nameOrId);
removed = true;
}
// データベースから離脱
// nameOrIdがニックネームの場合、対応するLLMクライアントIDを見つける必要がある
const participants = await this.repository.getActiveParticipants(this.id);
const participant = participants.find(p => p.clientId === nameOrId || p.nickname === nameOrId);
if (participant) {
await this.repository.leaveThread(this.id, participant.clientId);
// 実行時LLMクライアントの場合はDBからも削除
if (this._runtimeClientIds.has(participant.clientId)) {
try {
await client_repository_1.clientRepository.hardDelete(participant.clientId);
this._runtimeClientIds.delete(participant.clientId);
}
catch (error) {
console.warn(`Failed to cleanup runtime LLM client ${participant.clientId}:`, error);
}
}
removed = true;
}
// 自動保存
if (this.autoSave && removed) {
await this.save();
}
return removed;
}
/**
* スレッドに参加しているLLMクライアントの一覧を取得
*/
async getParticipants() {
return await this.repository.getActiveParticipants(this.id);
}
/**
* 特定の時点以降に参加したLLMクライアントを取得
*/
async getNewParticipantsSince(since) {
return await this.repository.getParticipantsSince(this.id, since);
}
/**
* メッセージを送信(指定したLLMクライアントまたは全LLMクライアントが応答)
*/
async sendMessage(content, targetAssistant) {
var _a;
// ユーザーメッセージを追加
const userMessage = {
id: (0, uuid_1.v4)(),
role: 'user',
content,
created_at: new Date(),
};
this.messages.push(userMessage);
const responses = [];
// 対象LLMクライアントを決定
const targetAssistants = targetAssistant
? [this.clients.get(targetAssistant)].filter(Boolean)
: Array.from(this.clients.values());
// 各LLMクライアントから応答を取得
for (const assistant of targetAssistants) {
try {
const assistantName = this.getAssistantName(assistant);
const participant = await this.getParticipantByName(assistantName);
if (!participant) {
console.warn(`LLM client ${assistantName} is not a participant in this thread`);
continue;
}
// LLMクライアントが見ることができるメッセージを取得
// participantのメタデータから文脈設定を取得
const participantMetadata = participant.metadata ? JSON.parse(participant.metadata) : {};
const includeContext = (_a = participantMetadata.includeContext) !== null && _a !== void 0 ? _a : false;
const contextLimit = participantMetadata.contextLimit;
const visibleMessages = await this.repository.getVisibleMessages(this.id, participant.clientId, { includeContext, contextLimit });
const contextMessages = visibleMessages.map(msg => ({
id: msg.id,
role: msg.role,
content: msg.content,
created_at: msg.timestamp,
metadata: msg.metadata,
}));
// 現在のメッセージを含む完全なコンテキストを構築
const fullContext = [...contextMessages, userMessage];
const response = await assistant.chat({
messages: fullContext,
model: participant.client.model || 'gpt-4-turbo-preview',
});
responses.push(response);
// LLMクライアントの応答をメッセージに追加
const assistantMessage = {
id: response.id,
role: 'assistant',
content: response.message.content,
created_at: response.created_at,
metadata: {
client_id: participant.clientId,
client_name: assistantName,
provider: response.provider,
model: response.model,
usage: response.usage,
finish_reason: response.finish_reason,
},
};
this.messages.push(assistantMessage);
}
catch (error) {
console.error('Failed to get response from LLM client:', error);
}
}
// 自動保存
if (this.autoSave) {
await this.save();
}
return responses;
}
/**
* LLMクライアントのニックネームから参加者情報を取得
*/
async getParticipantByName(name) {
const participants = await this.repository.getActiveParticipants(this.id);
return participants.find(p => p.nickname === name || p.clientId === name);
}
/**
* LLMクライアントインスタンスからニックネームを取得
*/
getAssistantName(assistant) {
for (const [name, ass] of this.clients.entries()) {
if (ass === assistant)
return name;
}
return 'unknown';
}
/**
* 統一形式のスレッドとして取得
*/
toConversationThread() {
return {
id: this.id,
title: this.title,
messages: this.messages,
created_at: new Date(), // これはDBから取得すべき
updated_at: new Date(),
metadata: {
description: this.description,
created_by: this._createdBy,
tags: this._tags,
},
};
}
/**
* メッセージをクリア
*/
clearMessages() {
this.messages = [];
}
/**
* スレッドの統計情報を取得
*/
async getStats() {
if (this._isLoaded || this.autoSave) {
return await this.repository.getThreadStats(this.id);
}
// メモリ内のメッセージから統計を計算
const clientIds = new Set();
let totalTokens = 0;
this.messages.forEach(msg => {
var _a, _b;
if ((_a = msg.metadata) === null || _a === void 0 ? void 0 : _a.client_id) {
clientIds.add(msg.metadata.client_id);
}
totalTokens += ((_b = msg.metadata) === null || _b === void 0 ? void 0 : _b.tokens) || 0;
});
return {
messageCount: this.messages.length,
participantCount: this.clients.size,
totalTokens,
totalCost: 0, // 正確な計算にはDBアクセスが必要
participants: Array.from(clientIds).map(id => ({
clientId: id,
messageCount: this.messages.filter(m => { var _a; return ((_a = m.metadata) === null || _a === void 0 ? void 0 : _a.client_id) === id; }).length,
joinedAt: new Date(), // 正確な情報にはDBアクセスが必要
})),
};
}
/**
* セッション終了時のクリーンアップ(実行時LLMクライアントをDBから削除)
*/
async cleanup() {
for (const runtimeId of this._runtimeClientIds) {
try {
await client_repository_1.clientRepository.hardDelete(runtimeId);
}
catch (error) {
console.warn(`Failed to cleanup runtime LLM client ${runtimeId}:`, error);
}
}
this._runtimeClientIds.clear();
}
// ========== 静的メソッド ==========
/**
* 新しいスレッドを作成
*/
static async createThread(config = {}) {
const session = new Thread(config);
if (config.autoSave !== false) {
await session.save();
}
return session;
}
/**
* 既存のスレッドをロード
*/
static async loadThread(threadId, config = {}) {
const session = new Thread({ ...config, threadId });
const loaded = await session.load();
return loaded ? session : null;
}
/**
* アクティブなスレッドの一覧を取得
*/
static async listThreads(options) {
const repository = new thread_repository_1.ThreadRepository(options === null || options === void 0 ? void 0 : options.dbPath);
return await repository.listThreads(options);
}
/**
* スレッドを削除
*/
static async deleteThread(threadId, dbPath) {
const repository = new thread_repository_1.ThreadRepository(dbPath);
return await repository.deleteThread(threadId);
}
/**
* 既存のチャットをロード(loadThreadのエイリアス)
* テストの互換性のために追加
*/
static async loadChat(threadId, config = {}) {
return Thread.loadThread(threadId, config);
}
}
exports.Thread = Thread;
exports.default = Thread;
//# sourceMappingURL=thread.js.map