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.
This commit is contained in:
parent
261bd0dba1
commit
4d7450f910
9 changed files with 204 additions and 5 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
package-lock.json
generated
61
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
8
src/agent-niches/agent-niches.module.ts
Normal file
8
src/agent-niches/agent-niches.module.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { NicheRegistryService } from './niche-registry.service';
|
||||
|
||||
@Module({
|
||||
providers: [NicheRegistryService],
|
||||
exports: [NicheRegistryService],
|
||||
})
|
||||
export class AgentNichesModule {}
|
||||
14
src/agent-niches/definitions/business-capability.md
Normal file
14
src/agent-niches/definitions/business-capability.md
Normal file
|
|
@ -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.
|
||||
13
src/agent-niches/definitions/financial.md
Normal file
13
src/agent-niches/definitions/financial.md
Normal file
|
|
@ -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.
|
||||
13
src/agent-niches/definitions/legal.md
Normal file
13
src/agent-niches/definitions/legal.md
Normal file
|
|
@ -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.
|
||||
90
src/agent-niches/niche-registry.service.ts
Normal file
90
src/agent-niches/niche-registry.service.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue