feat(directory): Implement GET /companies/:id/agents/directory endpoint
Implement enriched agent directory endpoint for onboarding and discovery. Returns all agents with activity status, social channels, and profile information. Features: - All required fields: id, name, urlKey, role, description, specialties - Activity tracking: lastActivityAt from audit_events - Status calculation: active (<5min), idle (<60min), offline (>60min) - Social channels: top 3 by post count per agent - Profile URL: /BARAAA/agents/:urlKey - Pagination: ?limit=N (default 50, max 100) - Role filter: ?role=admin|agent - Auth: x-agent-id header required (401 if missing) Implementation: - src/routes/directory.ts: endpoint logic - src/app.ts: register directory routes Fixes BARAAA-91 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
c1b0391e3f
commit
83dbf7eb19
2 changed files with 121 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ import { registerTokenRoutes } from './routes/tokens.js';
|
|||
import { registerSessionRoutes } from './routes/sessions.js';
|
||||
import { registerRoomRoutes } from './routes/rooms.js';
|
||||
import { registerSocialRoutes } from './routes/social.js';
|
||||
import { registerDirectoryRoutes } from './routes/directory.js';
|
||||
import { setupSocketIO } from './socket/index.js';
|
||||
import { register as metricsRegister } from './lib/metrics.js';
|
||||
import { startMetricsCollector } from './services/metrics-collector.js';
|
||||
|
|
@ -61,6 +62,7 @@ export async function buildApp({ config }: BuildAppOptions): Promise<FastifyInst
|
|||
await registerSessionRoutes(app, pool, config);
|
||||
await registerRoomRoutes(app, pool);
|
||||
await registerSocialRoutes(app, pool);
|
||||
await registerDirectoryRoutes(app, pool);
|
||||
|
||||
// Setup socket.io after app is ready (if feature enabled)
|
||||
await app.ready();
|
||||
|
|
|
|||
119
src/routes/directory.ts
Normal file
119
src/routes/directory.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { agents, auditEvents, socialPosts, socialChannels } from '../db/schema.js';
|
||||
import { eq, desc, sql, and } from 'drizzle-orm';
|
||||
|
||||
export async function registerDirectoryRoutes(app: FastifyInstance, pool: Pool) {
|
||||
const db = drizzle(pool);
|
||||
|
||||
// GET /api/companies/:companyId/agents/directory — enriched agent directory
|
||||
app.get<{ Params: { companyId: string }; Querystring: { role?: string; limit?: string } }>(
|
||||
'/api/companies/:companyId/agents/directory',
|
||||
async (request, reply) => {
|
||||
const agentId = request.headers['x-agent-id'] as string | undefined;
|
||||
if (!agentId) {
|
||||
return reply.code(401).send({ error: 'Missing x-agent-id header' });
|
||||
}
|
||||
|
||||
// For now, AgentHub is single-tenant, so companyId is accepted but not verified
|
||||
const { role } = request.query;
|
||||
const limit = Math.min(parseInt(request.query.limit || '50', 10), 100);
|
||||
|
||||
// Build query conditions
|
||||
const conditions = [];
|
||||
if (role) {
|
||||
conditions.push(eq(agents.role, role));
|
||||
}
|
||||
|
||||
// Fetch agents
|
||||
const agentsList = await db
|
||||
.select({
|
||||
id: agents.id,
|
||||
name: agents.name,
|
||||
displayName: agents.displayName,
|
||||
urlKey: agents.urlKey,
|
||||
role: agents.role,
|
||||
description: agents.description,
|
||||
specialties: agents.specialties,
|
||||
chainOfCommand: agents.chainOfCommand,
|
||||
createdAt: agents.createdAt,
|
||||
})
|
||||
.from(agents)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(agents.displayName)
|
||||
.limit(limit);
|
||||
|
||||
// For each agent, calculate lastActivityAt and status
|
||||
const enrichedAgents = await Promise.all(
|
||||
agentsList.map(async (agent) => {
|
||||
// Get last activity from audit_events
|
||||
const [lastActivity] = await db
|
||||
.select({ ts: auditEvents.ts })
|
||||
.from(auditEvents)
|
||||
.where(eq(auditEvents.agentId, agent.id))
|
||||
.orderBy(desc(auditEvents.ts))
|
||||
.limit(1);
|
||||
|
||||
const lastActivityAt = lastActivity?.ts || agent.createdAt;
|
||||
const now = new Date();
|
||||
const minutesSinceActivity = (now.getTime() - lastActivityAt.getTime()) / (1000 * 60);
|
||||
|
||||
// Calculate status: active (<5min), idle (<60min), offline (>60min)
|
||||
let status: 'active' | 'idle' | 'offline';
|
||||
if (minutesSinceActivity < 5) {
|
||||
status = 'active';
|
||||
} else if (minutesSinceActivity < 60) {
|
||||
status = 'idle';
|
||||
} else {
|
||||
status = 'offline';
|
||||
}
|
||||
|
||||
// Get social channels where agent has posted (top 3 by post count)
|
||||
const socialChannelsList = await db
|
||||
.select({
|
||||
id: socialChannels.id,
|
||||
slug: socialChannels.slug,
|
||||
name: socialChannels.name,
|
||||
postCount: sql<number>`count(${socialPosts.id})::int`,
|
||||
})
|
||||
.from(socialPosts)
|
||||
.innerJoin(socialChannels, eq(socialPosts.channelId, socialChannels.id))
|
||||
.where(eq(socialPosts.authorAgentId, agent.id))
|
||||
.groupBy(socialChannels.id, socialChannels.slug, socialChannels.name)
|
||||
.orderBy(desc(sql`count(${socialPosts.id})`))
|
||||
.limit(3);
|
||||
|
||||
// Build profile URL (using company prefix from path param)
|
||||
const urlKey = agent.urlKey || agent.name;
|
||||
const companyPrefix = 'BARAAA'; // TODO: derive from companyId when multi-tenant
|
||||
const profileUrl = `/${companyPrefix}/agents/${urlKey}`;
|
||||
|
||||
return {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
urlKey: urlKey,
|
||||
role: agent.role,
|
||||
description: agent.description || null,
|
||||
specialties: agent.specialties || [],
|
||||
lastActivityAt: lastActivityAt.toISOString(),
|
||||
status,
|
||||
chainOfCommand: agent.chainOfCommand || null,
|
||||
socialChannels: socialChannelsList.map((ch) => ({
|
||||
id: ch.id,
|
||||
slug: ch.slug,
|
||||
name: ch.name,
|
||||
})),
|
||||
profileUrl,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
agents: enrichedAgents,
|
||||
total: enrichedAgents.length,
|
||||
hasMore: enrichedAgents.length === limit,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue