Files
accounted/lib/agent-context/agent-competence.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

100 lines
3.4 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
// Server-side read of the agent's competence (the domain-knowledge "atoms" it
// ships with) and its top learned facts (memory), for the read-only overview
// on the "Vad din agent vet" page. Mirrors GET /api/agent/skills and
// GET /api/agent/memory so the two surfaces stay consistent; the full editable
// management lives in /settings/assistant.
export type AtomTier = 'horizontal' | 'vertical' | 'modifier'
export interface AgentAtom {
id: string
tier: AtomTier
title: string
description: string
/** horizontal atoms apply to every company; vertical/modifier only when the
* composer selected them into this company's profile. */
active: boolean
}
export type FactKind = 'fact' | 'preference' | 'pattern' | 'correction'
export type FactSource = 'composer' | 'user_taught' | 'agent_learned' | 'derived'
export interface AgentFact {
id: string
kind: FactKind
content: string
source: FactSource
is_pinned: boolean
}
export interface AgentCompetence {
atoms: AgentAtom[]
facts: AgentFact[]
/** Total active facts (before the overview cap), so the count is honest. */
factsActiveTotal: number
}
const FACTS_LIMIT = 12
export async function buildAgentCompetence(
supabase: SupabaseClient,
companyId: string,
): Promise<AgentCompetence> {
const [atomsRes, profileRes, factsRes, factsCountRes] = await Promise.all([
// The atom registry is global product content, not tenant data.
supabase
.from('agent_atom_registry')
.select('id, tier, title, description')
.eq('is_active', true)
.eq('mcp_exposed', true)
.is('parent_atom_id', null)
.order('tier', { ascending: true })
.order('title', { ascending: true }),
supabase
.from('agent_profiles')
.select('vertical_atoms, modifier_atoms')
.eq('company_id', companyId)
.maybeSingle(),
supabase
.from('agent_memory')
.select('id, kind, content, source, is_pinned')
.eq('company_id', companyId)
.eq('is_active', true)
.order('is_pinned', { ascending: false })
.order('relevance_score', { ascending: false })
.order('last_accessed_at', { ascending: false, nullsFirst: false })
.order('created_at', { ascending: false })
.limit(FACTS_LIMIT),
supabase
.from('agent_memory')
.select('id', { count: 'exact', head: true })
.eq('company_id', companyId)
.eq('is_active', true),
])
if (atomsRes.error) throw new Error(`agent skills failed: ${atomsRes.error.message}`)
if (factsRes.error) throw new Error(`agent memory failed: ${factsRes.error.message}`)
const verticalActive = new Set((profileRes.data?.vertical_atoms as string[] | null) ?? [])
const modifierActive = new Set((profileRes.data?.modifier_atoms as string[] | null) ?? [])
const atoms: AgentAtom[] = (atomsRes.data ?? []).map((a) => {
const tier = a.tier as AtomTier
const active =
tier === 'horizontal' ? true : tier === 'vertical' ? verticalActive.has(a.id) : modifierActive.has(a.id)
return { id: a.id, tier, title: a.title, description: a.description, active }
})
const facts: AgentFact[] = (factsRes.data ?? []).map((f) => ({
id: f.id,
kind: f.kind as FactKind,
content: f.content,
source: f.source as FactSource,
is_pinned: f.is_pinned,
}))
return { atoms, facts, factsActiveTotal: factsCountRes.count ?? facts.length }
}