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: [