feat: add tool call activity backend infrastructure for DB mode

- Add ToolCallActivity TypeORM entity with indexes for efficient querying
- Add ToolCallActivityRepository with pagination, search, and statistics
- Add ToolCallActivityDao interface and DB implementation
- Update DaoFactory and DatabaseDaoFactory to support activity DAO
- Add IToolCallActivity interfaces to types

Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-31 16:30:55 +00:00
parent c06f9ed916
commit 3d667e10f8
9 changed files with 531 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import { UserConfigDao, UserConfigDaoImpl } from './UserConfigDao.js';
import { OAuthClientDao, OAuthClientDaoImpl } from './OAuthClientDao.js'; import { OAuthClientDao, OAuthClientDaoImpl } from './OAuthClientDao.js';
import { OAuthTokenDao, OAuthTokenDaoImpl } from './OAuthTokenDao.js'; import { OAuthTokenDao, OAuthTokenDaoImpl } from './OAuthTokenDao.js';
import { BearerKeyDao, BearerKeyDaoImpl } from './BearerKeyDao.js'; import { BearerKeyDao, BearerKeyDaoImpl } from './BearerKeyDao.js';
import { ToolCallActivityDao } from './ToolCallActivityDao.js';
/** /**
* DAO Factory interface for creating DAO instances * DAO Factory interface for creating DAO instances
@@ -19,6 +20,7 @@ export interface DaoFactory {
getOAuthClientDao(): OAuthClientDao; getOAuthClientDao(): OAuthClientDao;
getOAuthTokenDao(): OAuthTokenDao; getOAuthTokenDao(): OAuthTokenDao;
getBearerKeyDao(): BearerKeyDao; getBearerKeyDao(): BearerKeyDao;
getToolCallActivityDao(): ToolCallActivityDao | null; // Only available in DB mode
} }
/** /**
@@ -106,6 +108,11 @@ export class JsonFileDaoFactory implements DaoFactory {
return this.bearerKeyDao; return this.bearerKeyDao;
} }
getToolCallActivityDao(): ToolCallActivityDao | null {
// Tool call activity is only available in DB mode
return null;
}
/** /**
* Reset all cached DAO instances (useful for testing) * Reset all cached DAO instances (useful for testing)
*/ */
@@ -194,3 +201,14 @@ export function getOAuthTokenDao(): OAuthTokenDao {
export function getBearerKeyDao(): BearerKeyDao { export function getBearerKeyDao(): BearerKeyDao {
return getDaoFactory().getBearerKeyDao(); return getDaoFactory().getBearerKeyDao();
} }
export function getToolCallActivityDao(): ToolCallActivityDao | null {
return getDaoFactory().getToolCallActivityDao();
}
/**
* Check if the application is using database mode
*/
export function isUsingDatabase(): boolean {
return getDaoFactory().getToolCallActivityDao() !== null;
}

View File

@@ -8,6 +8,7 @@ import {
OAuthClientDao, OAuthClientDao,
OAuthTokenDao, OAuthTokenDao,
BearerKeyDao, BearerKeyDao,
ToolCallActivityDao,
} from './index.js'; } from './index.js';
import { UserDaoDbImpl } from './UserDaoDbImpl.js'; import { UserDaoDbImpl } from './UserDaoDbImpl.js';
import { ServerDaoDbImpl } from './ServerDaoDbImpl.js'; import { ServerDaoDbImpl } from './ServerDaoDbImpl.js';
@@ -17,6 +18,7 @@ import { UserConfigDaoDbImpl } from './UserConfigDaoDbImpl.js';
import { OAuthClientDaoDbImpl } from './OAuthClientDaoDbImpl.js'; import { OAuthClientDaoDbImpl } from './OAuthClientDaoDbImpl.js';
import { OAuthTokenDaoDbImpl } from './OAuthTokenDaoDbImpl.js'; import { OAuthTokenDaoDbImpl } from './OAuthTokenDaoDbImpl.js';
import { BearerKeyDaoDbImpl } from './BearerKeyDaoDbImpl.js'; import { BearerKeyDaoDbImpl } from './BearerKeyDaoDbImpl.js';
import { ToolCallActivityDaoDbImpl } from './ToolCallActivityDao.js';
/** /**
* Database-backed DAO factory implementation * Database-backed DAO factory implementation
@@ -32,6 +34,7 @@ export class DatabaseDaoFactory implements DaoFactory {
private oauthClientDao: OAuthClientDao | null = null; private oauthClientDao: OAuthClientDao | null = null;
private oauthTokenDao: OAuthTokenDao | null = null; private oauthTokenDao: OAuthTokenDao | null = null;
private bearerKeyDao: BearerKeyDao | null = null; private bearerKeyDao: BearerKeyDao | null = null;
private toolCallActivityDao: ToolCallActivityDao | null = null;
/** /**
* Get singleton instance * Get singleton instance
@@ -103,6 +106,13 @@ export class DatabaseDaoFactory implements DaoFactory {
return this.bearerKeyDao!; return this.bearerKeyDao!;
} }
getToolCallActivityDao(): ToolCallActivityDao | null {
if (!this.toolCallActivityDao) {
this.toolCallActivityDao = new ToolCallActivityDaoDbImpl();
}
return this.toolCallActivityDao;
}
/** /**
* Reset all cached DAO instances (useful for testing) * Reset all cached DAO instances (useful for testing)
*/ */
@@ -115,5 +125,6 @@ export class DatabaseDaoFactory implements DaoFactory {
this.oauthClientDao = null; this.oauthClientDao = null;
this.oauthTokenDao = null; this.oauthTokenDao = null;
this.bearerKeyDao = null; this.bearerKeyDao = null;
this.toolCallActivityDao = null;
} }
} }

View File

@@ -0,0 +1,186 @@
import {
IToolCallActivity,
IToolCallActivitySearchParams,
IToolCallActivityPage,
IToolCallActivityStats,
} from '../types/index.js';
import { ToolCallActivityRepository } from '../db/repositories/ToolCallActivityRepository.js';
/**
* Tool Call Activity DAO interface (DB mode only)
*/
export interface ToolCallActivityDao {
/**
* Create a new tool call activity
*/
create(activity: Omit<IToolCallActivity, 'id' | 'createdAt'>): Promise<IToolCallActivity>;
/**
* Find activity by ID
*/
findById(id: string): Promise<IToolCallActivity | null>;
/**
* Update an existing activity
*/
update(id: string, updates: Partial<IToolCallActivity>): Promise<IToolCallActivity | null>;
/**
* Delete an activity
*/
delete(id: string): Promise<boolean>;
/**
* Find activities with pagination and filtering
*/
findWithPagination(
page: number,
pageSize: number,
params?: IToolCallActivitySearchParams,
): Promise<IToolCallActivityPage>;
/**
* Get recent activities
*/
findRecent(limit: number): Promise<IToolCallActivity[]>;
/**
* Get activity statistics
*/
getStats(): Promise<IToolCallActivityStats>;
/**
* Delete old activities (cleanup)
*/
deleteOlderThan(date: Date): Promise<number>;
/**
* Count total activities
*/
count(): Promise<number>;
}
/**
* Database-backed implementation of ToolCallActivityDao
*/
export class ToolCallActivityDaoDbImpl implements ToolCallActivityDao {
private repository: ToolCallActivityRepository;
constructor() {
this.repository = new ToolCallActivityRepository();
}
async create(activity: Omit<IToolCallActivity, 'id' | 'createdAt'>): Promise<IToolCallActivity> {
const created = await this.repository.create({
serverName: activity.serverName,
toolName: activity.toolName,
keyId: activity.keyId,
keyName: activity.keyName,
status: activity.status,
request: activity.request,
response: activity.response,
errorMessage: activity.errorMessage,
durationMs: activity.durationMs,
clientIp: activity.clientIp,
sessionId: activity.sessionId,
groupName: activity.groupName,
});
return this.mapToInterface(created);
}
async findById(id: string): Promise<IToolCallActivity | null> {
const activity = await this.repository.findById(id);
return activity ? this.mapToInterface(activity) : null;
}
async update(
id: string,
updates: Partial<IToolCallActivity>,
): Promise<IToolCallActivity | null> {
const updated = await this.repository.update(id, {
serverName: updates.serverName,
toolName: updates.toolName,
keyId: updates.keyId,
keyName: updates.keyName,
status: updates.status,
request: updates.request,
response: updates.response,
errorMessage: updates.errorMessage,
durationMs: updates.durationMs,
clientIp: updates.clientIp,
sessionId: updates.sessionId,
groupName: updates.groupName,
});
return updated ? this.mapToInterface(updated) : null;
}
async delete(id: string): Promise<boolean> {
return await this.repository.delete(id);
}
async findWithPagination(
page: number = 1,
pageSize: number = 20,
params: IToolCallActivitySearchParams = {},
): Promise<IToolCallActivityPage> {
const result = await this.repository.findWithPagination(page, pageSize, params);
return {
items: result.items.map((item) => this.mapToInterface(item)),
total: result.total,
page: result.page,
pageSize: result.pageSize,
totalPages: result.totalPages,
};
}
async findRecent(limit: number = 10): Promise<IToolCallActivity[]> {
const activities = await this.repository.findRecent(limit);
return activities.map((activity) => this.mapToInterface(activity));
}
async getStats(): Promise<IToolCallActivityStats> {
return await this.repository.getStats();
}
async deleteOlderThan(date: Date): Promise<number> {
return await this.repository.deleteOlderThan(date);
}
async count(): Promise<number> {
return await this.repository.count();
}
private mapToInterface(activity: {
id: string;
serverName: string;
toolName: string;
keyId?: string;
keyName?: string;
status: 'pending' | 'success' | 'error';
request?: string;
response?: string;
errorMessage?: string;
durationMs?: number;
clientIp?: string;
sessionId?: string;
groupName?: string;
createdAt: Date;
}): IToolCallActivity {
return {
id: activity.id,
serverName: activity.serverName,
toolName: activity.toolName,
keyId: activity.keyId,
keyName: activity.keyName,
status: activity.status,
request: activity.request,
response: activity.response,
errorMessage: activity.errorMessage,
durationMs: activity.durationMs,
clientIp: activity.clientIp,
sessionId: activity.sessionId,
groupName: activity.groupName,
createdAt: activity.createdAt,
};
}
}

View File

@@ -9,6 +9,7 @@ export * from './UserConfigDao.js';
export * from './OAuthClientDao.js'; export * from './OAuthClientDao.js';
export * from './OAuthTokenDao.js'; export * from './OAuthTokenDao.js';
export * from './BearerKeyDao.js'; export * from './BearerKeyDao.js';
export * from './ToolCallActivityDao.js';
// Export database implementations // Export database implementations
export * from './UserDaoDbImpl.js'; export * from './UserDaoDbImpl.js';

View File

@@ -0,0 +1,62 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
Index,
} from 'typeorm';
/**
* Tool call activity entity for logging tool invocations (DB mode only)
*/
@Entity({ name: 'tool_call_activities' })
export class ToolCallActivity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ type: 'varchar', length: 255, name: 'server_name' })
serverName: string;
@Index()
@Column({ type: 'varchar', length: 255, name: 'tool_name' })
toolName: string;
@Index()
@Column({ type: 'varchar', length: 255, name: 'key_id', nullable: true })
keyId?: string;
@Column({ type: 'varchar', length: 255, name: 'key_name', nullable: true })
keyName?: string;
@Index()
@Column({ type: 'varchar', length: 50, default: 'pending' })
status: 'pending' | 'success' | 'error';
@Column({ type: 'text', nullable: true })
request?: string;
@Column({ type: 'text', nullable: true })
response?: string;
@Column({ type: 'text', name: 'error_message', nullable: true })
errorMessage?: string;
@Column({ type: 'int', name: 'duration_ms', nullable: true })
durationMs?: number;
@Column({ type: 'varchar', length: 100, name: 'client_ip', nullable: true })
clientIp?: string;
@Column({ type: 'varchar', length: 255, name: 'session_id', nullable: true })
sessionId?: string;
@Column({ type: 'varchar', length: 255, name: 'group_name', nullable: true })
groupName?: string;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
}
export default ToolCallActivity;

View File

@@ -7,6 +7,7 @@ import UserConfig from './UserConfig.js';
import OAuthClient from './OAuthClient.js'; import OAuthClient from './OAuthClient.js';
import OAuthToken from './OAuthToken.js'; import OAuthToken from './OAuthToken.js';
import BearerKey from './BearerKey.js'; import BearerKey from './BearerKey.js';
import ToolCallActivity from './ToolCallActivity.js';
// Export all entities // Export all entities
export default [ export default [
@@ -19,6 +20,7 @@ export default [
OAuthClient, OAuthClient,
OAuthToken, OAuthToken,
BearerKey, BearerKey,
ToolCallActivity,
]; ];
// Export individual entities for direct use // Export individual entities for direct use
@@ -32,4 +34,5 @@ export {
OAuthClient, OAuthClient,
OAuthToken, OAuthToken,
BearerKey, BearerKey,
ToolCallActivity,
}; };

View File

@@ -0,0 +1,200 @@
import { Repository, FindOptionsWhere, ILike, Between } from 'typeorm';
import { ToolCallActivity } from '../entities/ToolCallActivity.js';
import { getAppDataSource } from '../connection.js';
/**
* Search parameters for filtering tool call activities
*/
export interface ToolCallActivitySearchParams {
serverName?: string;
toolName?: string;
keyId?: string;
status?: 'pending' | 'success' | 'error';
groupName?: string;
startDate?: Date;
endDate?: Date;
searchQuery?: string;
}
/**
* Pagination result for tool call activities
*/
export interface ToolCallActivityPage {
items: ToolCallActivity[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/**
* Repository for ToolCallActivity entity
*/
export class ToolCallActivityRepository {
private repository: Repository<ToolCallActivity>;
constructor() {
this.repository = getAppDataSource().getRepository(ToolCallActivity);
}
/**
* Create a new tool call activity
*/
async create(
activity: Omit<ToolCallActivity, 'id' | 'createdAt'>,
): Promise<ToolCallActivity> {
const newActivity = this.repository.create(activity);
return await this.repository.save(newActivity);
}
/**
* Find activity by ID
*/
async findById(id: string): Promise<ToolCallActivity | null> {
return await this.repository.findOne({ where: { id } });
}
/**
* Update an existing activity
*/
async update(
id: string,
updates: Partial<ToolCallActivity>,
): Promise<ToolCallActivity | null> {
const activity = await this.findById(id);
if (!activity) {
return null;
}
const updated = this.repository.merge(activity, updates);
return await this.repository.save(updated);
}
/**
* Delete an activity
*/
async delete(id: string): Promise<boolean> {
const result = await this.repository.delete({ id });
return (result.affected ?? 0) > 0;
}
/**
* Find activities with pagination and filtering
*/
async findWithPagination(
page: number = 1,
pageSize: number = 20,
params: ToolCallActivitySearchParams = {},
): Promise<ToolCallActivityPage> {
const where: FindOptionsWhere<ToolCallActivity>[] = [];
const baseWhere: FindOptionsWhere<ToolCallActivity> = {};
// Add filters
if (params.serverName) {
baseWhere.serverName = params.serverName;
}
if (params.toolName) {
baseWhere.toolName = params.toolName;
}
if (params.keyId) {
baseWhere.keyId = params.keyId;
}
if (params.status) {
baseWhere.status = params.status;
}
if (params.groupName) {
baseWhere.groupName = params.groupName;
}
if (params.startDate && params.endDate) {
baseWhere.createdAt = Between(params.startDate, params.endDate);
}
// Handle search query - search across multiple fields
if (params.searchQuery) {
const searchPattern = `%${params.searchQuery}%`;
where.push(
{ ...baseWhere, serverName: ILike(searchPattern) },
{ ...baseWhere, toolName: ILike(searchPattern) },
{ ...baseWhere, keyName: ILike(searchPattern) },
{ ...baseWhere, groupName: ILike(searchPattern) },
);
} else {
where.push(baseWhere);
}
const [items, total] = await this.repository.findAndCount({
where: where.length > 0 ? where : undefined,
order: { createdAt: 'DESC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
/**
* Get recent activities
*/
async findRecent(limit: number = 10): Promise<ToolCallActivity[]> {
return await this.repository.find({
order: { createdAt: 'DESC' },
take: limit,
});
}
/**
* Get activity statistics
*/
async getStats(): Promise<{
total: number;
success: number;
error: number;
pending: number;
avgDurationMs: number;
}> {
const stats = await this.repository
.createQueryBuilder('activity')
.select([
'COUNT(*) as total',
'SUM(CASE WHEN status = \'success\' THEN 1 ELSE 0 END) as success',
'SUM(CASE WHEN status = \'error\' THEN 1 ELSE 0 END) as error',
'SUM(CASE WHEN status = \'pending\' THEN 1 ELSE 0 END) as pending',
'AVG(duration_ms) as avgDurationMs',
])
.getRawOne();
return {
total: parseInt(stats?.total || '0', 10),
success: parseInt(stats?.success || '0', 10),
error: parseInt(stats?.error || '0', 10),
pending: parseInt(stats?.pending || '0', 10),
avgDurationMs: parseFloat(stats?.avgDurationMs || '0'),
};
}
/**
* Delete old activities (cleanup)
*/
async deleteOlderThan(date: Date): Promise<number> {
const result = await this.repository
.createQueryBuilder()
.delete()
.where('created_at < :date', { date })
.execute();
return result.affected ?? 0;
}
/**
* Count total activities
*/
async count(): Promise<number> {
return await this.repository.count();
}
}
export default ToolCallActivityRepository;

View File

@@ -7,6 +7,7 @@ import { UserConfigRepository } from './UserConfigRepository.js';
import { OAuthClientRepository } from './OAuthClientRepository.js'; import { OAuthClientRepository } from './OAuthClientRepository.js';
import { OAuthTokenRepository } from './OAuthTokenRepository.js'; import { OAuthTokenRepository } from './OAuthTokenRepository.js';
import { BearerKeyRepository } from './BearerKeyRepository.js'; import { BearerKeyRepository } from './BearerKeyRepository.js';
import { ToolCallActivityRepository } from './ToolCallActivityRepository.js';
// Export all repositories // Export all repositories
export { export {
@@ -19,4 +20,5 @@ export {
OAuthClientRepository, OAuthClientRepository,
OAuthTokenRepository, OAuthTokenRepository,
BearerKeyRepository, BearerKeyRepository,
ToolCallActivityRepository,
}; };

View File

@@ -481,3 +481,51 @@ export interface BatchCreateGroupsResponse {
failureCount: number; // Number of groups that failed failureCount: number; // Number of groups that failed
results: BatchGroupResult[]; // Detailed results for each group results: BatchGroupResult[]; // Detailed results for each group
} }
// Tool call activity interface for logging tool invocations (DB mode only)
export interface IToolCallActivity {
id?: string;
serverName: string;
toolName: string;
keyId?: string;
keyName?: string;
status: 'pending' | 'success' | 'error';
request?: string;
response?: string;
errorMessage?: string;
durationMs?: number;
clientIp?: string;
sessionId?: string;
groupName?: string;
createdAt?: Date;
}
// Tool call activity search parameters
export interface IToolCallActivitySearchParams {
serverName?: string;
toolName?: string;
keyId?: string;
status?: 'pending' | 'success' | 'error';
groupName?: string;
startDate?: Date;
endDate?: Date;
searchQuery?: string;
}
// Tool call activity pagination result
export interface IToolCallActivityPage {
items: IToolCallActivity[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Tool call activity statistics
export interface IToolCallActivityStats {
total: number;
success: number;
error: number;
pending: number;
avgDurationMs: number;
}