Files
network-app-api/src/routes/network.ts
Hammer a4c6ada7de feat: network matching API - rule-based scoring + AI intro suggestions
- /api/network/matches: find all client matches with scoring
- /api/network/matches/:clientId: matches for a specific client
- /api/network/intro: AI-generated introduction suggestions
- /api/network/stats: network analytics (industries, locations, connectors)
- Rule-based scoring: industry, interests, location, tags, complementary roles
- Smart filtering: same company detection, related industry groups
2026-01-29 12:35:27 +00:00

166 lines
5.6 KiB
TypeScript

import { Elysia, t } from 'elysia';
import { db } from '../db';
import { clients } from '../db/schema';
import { eq } from 'drizzle-orm';
import { findMatches, findMatchesForClient, generateIntroSuggestion } from '../services/matching';
import type { ClientProfile, MatchResult } from '../services/matching';
function toClientProfile(c: typeof clients.$inferSelect): ClientProfile {
return {
id: c.id,
firstName: c.firstName,
lastName: c.lastName,
email: c.email,
company: c.company,
role: c.role,
industry: c.industry,
city: c.city,
state: c.state,
interests: c.interests,
tags: c.tags,
notes: c.notes,
};
}
export const networkRoutes = new Elysia({ prefix: '/network' })
// Get all network matches for the user's clients
.get('/matches', async (ctx) => {
const user = (ctx as any).user;
const query = ctx.query;
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
if (allClients.length < 2) {
return { matches: [] as MatchResult[], total: 0, clientCount: allClients.length };
}
const profiles = allClients.map(toClientProfile);
const minScore = parseInt(query.minScore || '20', 10);
const limit = parseInt(query.limit || '50', 10);
const rawMatches = findMatches(profiles, minScore).slice(0, limit);
const matches: MatchResult[] = rawMatches.map(m => ({
...m,
introSuggestion: `Consider introducing ${m.clientA.name} and ${m.clientB.name} — they share common ground.`,
}));
return { matches, total: matches.length, clientCount: allClients.length };
}, {
query: t.Object({
minScore: t.Optional(t.String()),
limit: t.Optional(t.String()),
}),
})
// Get matches for a specific client
.get('/matches/:clientId', async (ctx) => {
const user = (ctx as any).user;
const params = ctx.params;
const query = ctx.query;
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
const target = allClients.find(c => c.id === params.clientId);
if (!target) throw new Error('Client not found');
const profiles = allClients.map(toClientProfile);
const targetProfile = toClientProfile(target);
const minScore = parseInt(query.minScore || '15', 10);
const rawMatches = findMatchesForClient(targetProfile, profiles, minScore);
const matches: MatchResult[] = rawMatches.map(m => ({
...m,
introSuggestion: `Consider introducing ${m.clientA.name} and ${m.clientB.name} — they share common ground.`,
}));
return { matches, client: { id: target.id, name: `${target.firstName} ${target.lastName}` } };
}, {
params: t.Object({
clientId: t.String({ format: 'uuid' }),
}),
query: t.Object({
minScore: t.Optional(t.String()),
}),
})
// Generate AI intro suggestion for a specific match
.post('/intro', async (ctx) => {
const user = (ctx as any).user;
const body = ctx.body;
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
const clientA = allClients.find(c => c.id === body.clientAId);
const clientB = allClients.find(c => c.id === body.clientBId);
if (!clientA || !clientB) throw new Error('Client not found');
const suggestion = await generateIntroSuggestion(
toClientProfile(clientA),
toClientProfile(clientB),
body.reasons,
(body.provider as any) || 'openai',
);
return { introSuggestion: suggestion };
}, {
body: t.Object({
clientAId: t.String({ format: 'uuid' }),
clientBId: t.String({ format: 'uuid' }),
reasons: t.Array(t.String()),
provider: t.Optional(t.String()),
}),
})
// Network stats
.get('/stats', async (ctx) => {
const user = (ctx as any).user;
const allClients = await db.select().from(clients).where(eq(clients.userId, user.id));
const profiles = allClients.map(toClientProfile);
const matches = findMatches(profiles, 20);
// Industry breakdown
const industries: Record<string, number> = {};
for (const c of allClients) {
if (c.industry) {
industries[c.industry] = (industries[c.industry] || 0) + 1;
}
}
// Location breakdown
const locations: Record<string, number> = {};
for (const c of allClients) {
const loc = [c.city, c.state].filter(Boolean).join(', ');
if (loc) {
locations[loc] = (locations[loc] || 0) + 1;
}
}
// Category breakdown
const categories: Record<string, number> = {};
for (const m of matches) {
categories[m.category] = (categories[m.category] || 0) + 1;
}
// Most connected clients
const connectionCount: Record<string, { name: string; count: number }> = {};
for (const m of matches) {
if (!connectionCount[m.clientA.id]) connectionCount[m.clientA.id] = { name: m.clientA.name, count: 0 };
if (!connectionCount[m.clientB.id]) connectionCount[m.clientB.id] = { name: m.clientB.name, count: 0 };
connectionCount[m.clientA.id]!.count++;
connectionCount[m.clientB.id]!.count++;
}
const topConnectors = Object.entries(connectionCount)
.map(([id, { name, count }]) => ({ id, name, matchCount: count }))
.sort((a, b) => b.matchCount - a.matchCount)
.slice(0, 5);
return {
totalClients: allClients.length,
totalMatches: matches.length,
avgScore: matches.length > 0 ? Math.round(matches.reduce((s, m) => s + m.score, 0) / matches.length) : 0,
industries: Object.entries(industries).sort((a, b) => b[1] - a[1]),
locations: Object.entries(locations).sort((a, b) => b[1] - a[1]),
categories,
topConnectors,
};
});