From 4d7450f9108450eb9be74f4c9a8c79643d7d87bf Mon Sep 17 00:00:00 2001 From: valentinbvro Date: Fri, 24 Jul 2026 00:00:42 +0200 Subject: [PATCH] feat: add agent niche registry (SKILL.md-style) grouping AI tools by domain Ports the SKILL.md/frontmatter pattern from skills-claude (fork of anthropics/skills) to define user-facing "niches" (legal, financial, business-capability) that each group one or more AGENT_TOOL_REGISTRY entries. NicheRegistryService loads and validates src/agent-niches/ definitions/*.md at boot, failing loudly on unknown tool references so a niche can never silently grant access to an undeclared capability. This is the data model for the planned dashboard where memberships access Hermes agents scoped by niche instead of by raw tool name. --- nest-cli.json | 6 +- package-lock.json | 61 ++++++++++++- package.json | 2 + src/agent-niches/agent-niches.module.ts | 8 ++ .../definitions/business-capability.md | 14 +++ src/agent-niches/definitions/financial.md | 13 +++ src/agent-niches/definitions/legal.md | 13 +++ src/agent-niches/niche-registry.service.ts | 90 +++++++++++++++++++ src/app.module.ts | 2 + 9 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 src/agent-niches/agent-niches.module.ts create mode 100644 src/agent-niches/definitions/business-capability.md create mode 100644 src/agent-niches/definitions/financial.md create mode 100644 src/agent-niches/definitions/legal.md create mode 100644 src/agent-niches/niche-registry.service.ts diff --git a/nest-cli.json b/nest-cli.json index 2566481..c757fa0 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,5 +1,9 @@ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "src", + "compilerOptions": { + "assets": ["agent-niches/definitions/*.md"], + "watchAssets": true + } } diff --git a/package-lock.json b/package-lock.json index d997287..e9dee0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,14 @@ "@nestjs/swagger": "^8.0.0", "@nestjs/throttler": "^6.2.0", "@supabase/supabase-js": "^2.47.0", + "@types/js-yaml": "^4.0.9", "bullmq": "^5.28.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "drizzle-orm": "^0.36.0", "helmet": "^8.0.0", "ioredis": "^5.4.1", + "js-yaml": "^5.2.1", "nestjs-pino": "^4.1.0", "pg": "^8.13.0", "pino-http": "^10.3.0", @@ -1878,6 +1880,18 @@ } } }, + "node_modules/@nestjs/swagger/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@nestjs/swagger/node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2103,6 +2117,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3106,6 +3126,29 @@ } } }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/cron": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz", @@ -4225,15 +4268,25 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz", + "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/json-parse-even-better-errors": { diff --git a/package.json b/package.json index 7d8fbc0..26bc083 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,14 @@ "@nestjs/swagger": "^8.0.0", "@nestjs/throttler": "^6.2.0", "@supabase/supabase-js": "^2.47.0", + "@types/js-yaml": "^4.0.9", "bullmq": "^5.28.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "drizzle-orm": "^0.36.0", "helmet": "^8.0.0", "ioredis": "^5.4.1", + "js-yaml": "^5.2.1", "nestjs-pino": "^4.1.0", "pg": "^8.13.0", "pino-http": "^10.3.0", diff --git a/src/agent-niches/agent-niches.module.ts b/src/agent-niches/agent-niches.module.ts new file mode 100644 index 0000000..9c83090 --- /dev/null +++ b/src/agent-niches/agent-niches.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { NicheRegistryService } from './niche-registry.service'; + +@Module({ + providers: [NicheRegistryService], + exports: [NicheRegistryService], +}) +export class AgentNichesModule {} diff --git a/src/agent-niches/definitions/business-capability.md b/src/agent-niches/definitions/business-capability.md new file mode 100644 index 0000000..4c96245 --- /dev/null +++ b/src/agent-niches/definitions/business-capability.md @@ -0,0 +1,14 @@ +--- +name: business-capability +description: Ce poate/nu poate face agentul pentru o sarcina data, pe baza ocupatiilor si skill-urilor ESCO +tools: + - capability-reasoning +--- + +Aceasta nisa raspunde la intrebarea "poate agentul sa faca X?" pentru o sarcina descrisa +in text liber, potrivind-o pe ocupatiile si skill-urile ESCO prin unealta +`capability-reasoning`. + +Raspunsul trebuie sa indice explicit ocupatiile/skill-urile ESCO gasite si sa precizeze +clar cand o sarcina nu are potrivire suficienta in registrul de capabilitati, in loc sa +presupuna ca agentul poate face orice. diff --git a/src/agent-niches/definitions/financial.md b/src/agent-niches/definitions/financial.md new file mode 100644 index 0000000..f6dd55f --- /dev/null +++ b/src/agent-niches/definitions/financial.md @@ -0,0 +1,13 @@ +--- +name: financial +description: Date macroeconomice, valutare, inflatie si stiri economice +tools: + - economic-data +--- + +Aceasta nisa acopera intrebari economice/financiare: curs valutar, inflatie, PIB, somaj, +si stiri economice, pe baza surselor World Bank/ECB/Eurostat/FRED/OECD expuse prin +unealta `economic-data`. + +Raspunsurile trebuie sa fie ancorate direct in valorile returnate de unealta, cu sursa si +perioada la care se refera datele, niciodata estimari generate liber de model. diff --git a/src/agent-niches/definitions/legal.md b/src/agent-niches/definitions/legal.md new file mode 100644 index 0000000..c58ed5c --- /dev/null +++ b/src/agent-niches/definitions/legal.md @@ -0,0 +1,13 @@ +--- +name: legal +description: Intrebari despre legislatie si conformitate in UK, Germania si Romania +tools: + - legislation-search +--- + +Aceasta nisa acopera intrebari despre legi si reglementari din UK, Germania si Romania, +pe baza cautarii semantice in corpusul de legislatie vectorizat. + +Raspunsurile trebuie sa citeze intotdeauna sectiunea/sursa legii gasite prin unealta +`legislation-search` si sa includa mentiunea ca informatia este cu caracter informativ, +nu consultanta juridica. diff --git a/src/agent-niches/niche-registry.service.ts b/src/agent-niches/niche-registry.service.ts new file mode 100644 index 0000000..005d689 --- /dev/null +++ b/src/agent-niches/niche-registry.service.ts @@ -0,0 +1,90 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { Injectable } from '@nestjs/common'; +import { load as parseYaml } from 'js-yaml'; +import { AGENT_TOOL_REGISTRY } from '../auth/ability.factory'; + +const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/; +const DEFINITIONS_DIR = join(__dirname, 'definitions'); + +export interface AgentNicheDefinition { + name: string; + description: string; + tools: readonly string[]; + instructions: string; +} + +interface NicheFrontmatter { + name?: unknown; + description?: unknown; + tools?: unknown; +} + +function parseNicheFile(fileName: string, raw: string): AgentNicheDefinition { + const match = FRONTMATTER_PATTERN.exec(raw); + if (!match) { + throw new Error(`Niche definition "${fileName}" is missing YAML frontmatter (---...---).`); + } + + const [, frontmatterBlock, instructions] = match; + const frontmatter = parseYaml(frontmatterBlock) as NicheFrontmatter; + + if ( + typeof frontmatter.name !== 'string' || + typeof frontmatter.description !== 'string' || + !Array.isArray(frontmatter.tools) + ) { + throw new Error( + `Niche definition "${fileName}" must declare string "name", string "description" and array "tools" in frontmatter.`, + ); + } + + const tools = frontmatter.tools.map((tool) => { + if (typeof tool !== 'string') { + throw new Error(`Niche definition "${fileName}" has a non-string entry in "tools".`); + } + return tool; + }); + + const unknownTools = tools.filter((tool) => !AGENT_TOOL_REGISTRY.some((registered) => registered.name === tool)); + if (unknownTools.length > 0) { + throw new Error( + `Niche definition "${fileName}" references unknown tool(s): ${unknownTools.join(', ')}. Add them to AGENT_TOOL_REGISTRY first.`, + ); + } + + return { + name: frontmatter.name, + description: frontmatter.description, + tools, + instructions: instructions.trim(), + }; +} + +/** + * Incarca definitiile de nisa (format SKILL.md -- portat din skills-claude, + * github.com/valentinbvro/skills-claude, fork al anthropics/skills) din + * src/agent-niches/definitions/*.md. Fiecare nisa grupeaza unul sau mai multe + * unelte AI din AGENT_TOOL_REGISTRY sub un nume orientat pe utilizator + * (legal / financial / business-capability), pentru dashboard-ul unde + * membership-urile acceseaza agenti scopati pe nisa. + */ +@Injectable() +export class NicheRegistryService { + private readonly niches: readonly AgentNicheDefinition[]; + + constructor() { + const fileNames = readdirSync(DEFINITIONS_DIR).filter((fileName) => fileName.endsWith('.md')); + this.niches = fileNames.map((fileName) => + parseNicheFile(fileName, readFileSync(join(DEFINITIONS_DIR, fileName), 'utf8')), + ); + } + + findAll(): readonly AgentNicheDefinition[] { + return this.niches; + } + + findByName(name: string): AgentNicheDefinition | undefined { + return this.niches.find((niche) => niche.name === name); + } +} diff --git a/src/app.module.ts b/src/app.module.ts index 181c180..69ee771 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { LoggerModule } from 'nestjs-pino'; import { HealthController } from './health/health.controller'; import { AuthModule } from './auth/auth.module'; import { EventsModule } from './events/events.module'; +import { AgentNichesModule } from './agent-niches/agent-niches.module'; import { TenantGuard } from './auth/tenant.guard'; function parseRedisConnection(redisUrl: string | undefined) { @@ -31,6 +32,7 @@ function parseRedisConnection(redisUrl: string | undefined) { BullModule.forRoot({ connection: parseRedisConnection(process.env.REDIS_URL) }), AuthModule, EventsModule, + AgentNichesModule, ], controllers: [HealthController], providers: [