From aefd370d78f12405afdb4e1438f9e645036dfd6e Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 12:32:02 +0000 Subject: [PATCH] feat(CC-054): add ExternalDataService (legislation + AnythingLLM + capability) --- src/external-data/external-data.service.ts | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/external-data/external-data.service.ts diff --git a/src/external-data/external-data.service.ts b/src/external-data/external-data.service.ts new file mode 100644 index 0000000..89e4991 --- /dev/null +++ b/src/external-data/external-data.service.ts @@ -0,0 +1,81 @@ +import { BadGatewayException, Injectable } from '@nestjs/common'; + +const TIMEOUT_MS = 10_000; + +async function jsonFetch(url: string, opts: RequestInit = {}): Promise { + const ctrl = new AbortController(); + const id = setTimeout(() => ctrl.abort(), TIMEOUT_MS); + try { + const res = await fetch(url, { + ...opts, + headers: { 'Content-Type': 'application/json', ...opts.headers }, + signal: ctrl.signal, + }); + clearTimeout(id); + if (!res.ok) throw new BadGatewayException(`upstream returned ${res.status}`); + return (await res.json()) as T; + } catch (err) { + clearTimeout(id); + if (err instanceof BadGatewayException) throw err; + throw new BadGatewayException('upstream service unreachable'); + } +} + +@Injectable() +export class ExternalDataService { + /** ceo-os-legislation-api: vector search over imported legislation corpus */ + private readonly legislationUrl = + process.env.LEGISLATION_API_URL ?? 'http://91.98.39.120:9404'; + + /** AnythingLLM: AI-interpreted legislation answers via agent workspace */ + private readonly anythingLlmUrl = + process.env.ANYTHINGLLM_URL ?? 'http://152.53.112.35:3010'; + + private readonly anythingLlmKey = + process.env.ANYTHINGLLM_API_KEY ?? ''; + + private readonly legislationWorkspace = + process.env.ANYTHINGLLM_LEGISLATION_WORKSPACE ?? 'legislation'; + + /** + * Raw vector search: returns matching legislation sections (fast, no AI). + * The 'scenario' field must be a full situation description, not just a keyword. + */ + async searchLegislation(scenario: string, country?: string, topK = 8) { + return jsonFetch(`${this.legislationUrl}/search_legislation`, { + method: 'POST', + body: JSON.stringify({ scenario, country: country ?? null, top_k: topK }), + }); + } + + /** + * AI-interpreted legislation answer via AnythingLLM agent workspace. + * Returns a conversational answer grounded in the legislation corpus. + */ + async askLegislationAgent(message: string, sessionId?: string) { + if (!this.anythingLlmKey) { + throw new BadGatewayException('ANYTHINGLLM_API_KEY not configured'); + } + return jsonFetch( + `${this.anythingLlmUrl}/api/v1/workspace/${this.legislationWorkspace}/chat`, + { + method: 'POST', + headers: { Authorization: `Bearer ${this.anythingLlmKey}` }, + body: JSON.stringify({ + message, + mode: 'chat', + sessionId: sessionId ?? 'ceo-os-legislation', + attachments: [], + }), + }, + ); + } + + async checkCapability(capability: string, context?: string) { + const capUrl = process.env.CAPABILITY_API_URL ?? 'http://91.98.39.120:9402'; + return jsonFetch(`${capUrl}/check_capability`, { + method: 'POST', + body: JSON.stringify({ capability, context }), + }); + } +}