128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
import { eq, and, desc } from 'drizzle-orm';
|
|
import { scoutFindings, agents, teamMembers, teamResources } from '../db/schema.js';
|
|
import type { Db } from '../db/connection.js';
|
|
|
|
interface Finding {
|
|
source: string;
|
|
category: string;
|
|
title: string;
|
|
summary: string;
|
|
relevanceScore: number;
|
|
url: string | null;
|
|
}
|
|
|
|
export class ScoutAgent {
|
|
constructor(private db: Db) {}
|
|
|
|
async scan(userId: string, teamId: string): Promise<Array<typeof scoutFindings.$inferSelect>> {
|
|
const findings: Finding[] = [];
|
|
|
|
// Source 1: Check team resources for newly shared items
|
|
findings.push(...await this.checkTeamResources(teamId));
|
|
|
|
// Source 2: Mock marketplace check (would check npm/MCP registry in production)
|
|
findings.push(...await this.checkMarketplace(userId));
|
|
|
|
// Score relevance based on user's agent configs and role
|
|
const scored = await this.scoreRelevance(findings, userId, teamId);
|
|
|
|
// Filter out findings with titles already dismissed by this user
|
|
const existingDismissed = await this.db.select().from(scoutFindings)
|
|
.where(and(
|
|
eq(scoutFindings.userId, userId),
|
|
eq(scoutFindings.status, 'dismissed'),
|
|
));
|
|
const dismissedTitles = new Set(existingDismissed.map(f => f.title));
|
|
const filtered = scored.filter(f => !dismissedTitles.has(f.title));
|
|
|
|
// Store findings
|
|
const stored = [];
|
|
for (const finding of filtered) {
|
|
const [entry] = await this.db.insert(scoutFindings).values({
|
|
userId,
|
|
teamId,
|
|
source: finding.source,
|
|
category: finding.category,
|
|
title: finding.title,
|
|
summary: finding.summary,
|
|
relevanceScore: finding.relevanceScore,
|
|
url: finding.url,
|
|
status: 'new',
|
|
}).returning();
|
|
stored.push(entry);
|
|
}
|
|
|
|
return stored;
|
|
}
|
|
|
|
private async checkTeamResources(teamId: string): Promise<Finding[]> {
|
|
const recent = await this.db.select().from(teamResources)
|
|
.where(eq(teamResources.teamId, teamId))
|
|
.orderBy(desc(teamResources.createdAt))
|
|
.limit(5);
|
|
|
|
return recent.map(r => ({
|
|
source: 'team' as const,
|
|
category: r.resourceType === 'skill' ? 'skill' : 'practice',
|
|
title: `New team resource: ${r.name}`,
|
|
summary: r.description ?? `A ${r.resourceType} shared by a team member`,
|
|
relevanceScore: 0.5,
|
|
url: null,
|
|
}));
|
|
}
|
|
|
|
private async checkMarketplace(_userId: string): Promise<Finding[]> {
|
|
// Mock: In production, would check npm registry for MCP packages, skill marketplace, etc.
|
|
return [];
|
|
}
|
|
|
|
private async scoreRelevance(findings: Finding[], userId: string, teamId: string): Promise<Finding[]> {
|
|
// Load user's agent configs for interest matching
|
|
const userAgents = await this.db.select().from(agents)
|
|
.where(eq(agents.userId, userId));
|
|
|
|
// Load member interests
|
|
const [membership] = await this.db.select().from(teamMembers)
|
|
.where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId)));
|
|
|
|
const interests = (membership?.interests as string[]) ?? [];
|
|
const agentTools = userAgents.flatMap(a => (a.tools as string[]) ?? []);
|
|
|
|
return findings.map(f => {
|
|
let score = f.relevanceScore;
|
|
// Boost if matches user interests
|
|
if (interests.some(i => f.title.toLowerCase().includes(i.toLowerCase()))) score += 0.3;
|
|
// Boost if matches agent tool names
|
|
if (agentTools.some(t => f.title.toLowerCase().includes(t.toLowerCase()))) score += 0.2;
|
|
return { ...f, relevanceScore: Math.min(score, 1.0) };
|
|
});
|
|
}
|
|
|
|
async adopt(findingId: string, userId: string) {
|
|
const [updated] = await this.db.update(scoutFindings)
|
|
.set({ status: 'adopted' })
|
|
.where(and(
|
|
eq(scoutFindings.id, findingId),
|
|
eq(scoutFindings.userId, userId),
|
|
))
|
|
.returning();
|
|
return updated ?? null;
|
|
}
|
|
|
|
async dismiss(findingId: string, userId: string) {
|
|
const [updated] = await this.db.update(scoutFindings)
|
|
.set({ status: 'dismissed' })
|
|
.where(and(
|
|
eq(scoutFindings.id, findingId),
|
|
eq(scoutFindings.userId, userId),
|
|
))
|
|
.returning();
|
|
return updated ?? null;
|
|
}
|
|
|
|
async listFindings(userId: string) {
|
|
return this.db.select().from(scoutFindings)
|
|
.where(eq(scoutFindings.userId, userId));
|
|
}
|
|
}
|