Spaces:
Paused
Paused
Commit ·
529090e
0
Parent(s):
Initial deployment - WidgeTDC Cortex Backend v2.1.0
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +6 -0
- Dockerfile +98 -0
- README.md +19 -0
- apps/backend/package.json +84 -0
- apps/backend/prisma/prisma.config.ts +19 -0
- apps/backend/prisma/schema.prisma +410 -0
- apps/backend/src/adapters/Neo4jAdapter.ts +477 -0
- apps/backend/src/api/approvals.ts +166 -0
- apps/backend/src/api/health.ts +153 -0
- apps/backend/src/api/knowledge.ts +77 -0
- apps/backend/src/config.ts +45 -0
- apps/backend/src/config/codex.ts +153 -0
- apps/backend/src/config/securityConfig.ts +75 -0
- apps/backend/src/controllers/CortexController.ts +190 -0
- apps/backend/src/database/Neo4jService.ts +215 -0
- apps/backend/src/database/index.ts +543 -0
- apps/backend/src/database/prisma.ts +34 -0
- apps/backend/src/database/schema.sql +279 -0
- apps/backend/src/database/seeds.ts +43 -0
- apps/backend/src/index-test.ts +46 -0
- apps/backend/src/index.ts +1295 -0
- apps/backend/src/mcp/EventBus.ts +140 -0
- apps/backend/src/mcp/SmartToolRouter.ts +476 -0
- apps/backend/src/mcp/SourceRegistry.ts +109 -0
- apps/backend/src/mcp/autonomous/AutonomousAgent.ts +389 -0
- apps/backend/src/mcp/autonomous/DecisionEngine.ts +437 -0
- apps/backend/src/mcp/autonomous/INTEGRATION_GUIDE.md +330 -0
- apps/backend/src/mcp/autonomous/MCPIntegration.ts +148 -0
- apps/backend/src/mcp/autonomous/index.ts +27 -0
- apps/backend/src/mcp/autonomousRouter.ts +1079 -0
- apps/backend/src/mcp/cognitive/AdvancedSearch.ts +240 -0
- apps/backend/src/mcp/cognitive/AgentCommunication.ts +252 -0
- apps/backend/src/mcp/cognitive/AgentCoordination.ts +334 -0
- apps/backend/src/mcp/cognitive/AgentTeam.ts +816 -0
- apps/backend/src/mcp/cognitive/AutonomousTaskEngine.ts +323 -0
- apps/backend/src/mcp/cognitive/EmotionAwareDecisionEngine.ts +395 -0
- apps/backend/src/mcp/cognitive/GraphTraversalOptimizer.ts +234 -0
- apps/backend/src/mcp/cognitive/HybridSearchEngine.ts +215 -0
- apps/backend/src/mcp/cognitive/IntegrationManager.ts +230 -0
- apps/backend/src/mcp/cognitive/MetaLearningEngine.ts +244 -0
- apps/backend/src/mcp/cognitive/MultiModalProcessor.ts +165 -0
- apps/backend/src/mcp/cognitive/ObservabilitySystem.ts +243 -0
- apps/backend/src/mcp/cognitive/PatternEvolutionEngine.ts +289 -0
- apps/backend/src/mcp/cognitive/RLHFAlignmentSystem.ts +262 -0
- apps/backend/src/mcp/cognitive/SelfReflectionEngine.ts +205 -0
- apps/backend/src/mcp/cognitive/StateGraphRouter.ts +248 -0
- apps/backend/src/mcp/cognitive/TaskRecorder.ts +648 -0
- apps/backend/src/mcp/cognitive/UnifiedGraphRAG.ts +326 -0
- apps/backend/src/mcp/cognitive/UnifiedMemorySystem.ts +330 -0
- apps/backend/src/mcp/devToolsHandlers.ts +23 -0
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY=sk-...
|
| 2 |
+
NEO4J_URI=neo4j+s://...
|
| 3 |
+
NEO4J_USER=neo4j
|
| 4 |
+
NEO4J_PASSWORD=...
|
| 5 |
+
DATABASE_URL=postgresql://...
|
| 6 |
+
PORT=7860
|
Dockerfile
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WidgeTDC Backend - Hugging Face Spaces Deployment
|
| 2 |
+
# Optimized for HF Docker Spaces (port 7860)
|
| 3 |
+
#
|
| 4 |
+
# Usage: Copy entire project to HF Space repo, then push
|
| 5 |
+
|
| 6 |
+
FROM node:20-slim AS builder
|
| 7 |
+
|
| 8 |
+
# Install build dependencies
|
| 9 |
+
RUN apt-get update && apt-get install -y \
|
| 10 |
+
python3 \
|
| 11 |
+
make \
|
| 12 |
+
g++ \
|
| 13 |
+
git \
|
| 14 |
+
openssl \
|
| 15 |
+
libssl-dev \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
WORKDIR /app
|
| 19 |
+
|
| 20 |
+
# Copy package files
|
| 21 |
+
COPY package*.json ./
|
| 22 |
+
COPY apps/backend/package*.json ./apps/backend/
|
| 23 |
+
COPY packages/domain-types/package*.json ./packages/domain-types/
|
| 24 |
+
COPY packages/mcp-types/package*.json ./packages/mcp-types/
|
| 25 |
+
|
| 26 |
+
# Install dependencies
|
| 27 |
+
RUN npm ci --include=dev
|
| 28 |
+
|
| 29 |
+
# Copy source
|
| 30 |
+
COPY packages/ ./packages/
|
| 31 |
+
COPY apps/backend/ ./apps/backend/
|
| 32 |
+
|
| 33 |
+
# Build packages in order
|
| 34 |
+
RUN cd packages/domain-types && npm run build
|
| 35 |
+
RUN cd packages/mcp-types && npm run build
|
| 36 |
+
|
| 37 |
+
# Generate Prisma client (if schema exists)
|
| 38 |
+
RUN if [ -f apps/backend/prisma/schema.prisma ]; then \
|
| 39 |
+
cd apps/backend && npx prisma generate; \
|
| 40 |
+
fi
|
| 41 |
+
|
| 42 |
+
# Build backend
|
| 43 |
+
RUN cd apps/backend && npm run build
|
| 44 |
+
|
| 45 |
+
# ============================================
|
| 46 |
+
# Production stage - minimal footprint
|
| 47 |
+
# ============================================
|
| 48 |
+
FROM node:20-slim AS production
|
| 49 |
+
|
| 50 |
+
# Install runtime dependencies only
|
| 51 |
+
RUN apt-get update && apt-get install -y \
|
| 52 |
+
openssl \
|
| 53 |
+
ca-certificates \
|
| 54 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 55 |
+
|
| 56 |
+
# Create non-root user (HF Spaces requirement)
|
| 57 |
+
RUN useradd -m -u 1000 user
|
| 58 |
+
USER user
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy built artifacts with correct ownership
|
| 63 |
+
COPY --from=builder --chown=user /app/package*.json ./
|
| 64 |
+
COPY --from=builder --chown=user /app/node_modules ./node_modules
|
| 65 |
+
COPY --from=builder --chown=user /app/packages/domain-types/dist ./packages/domain-types/dist
|
| 66 |
+
COPY --from=builder --chown=user /app/packages/domain-types/package.json ./packages/domain-types/
|
| 67 |
+
COPY --from=builder --chown=user /app/packages/mcp-types/dist ./packages/mcp-types/dist
|
| 68 |
+
COPY --from=builder --chown=user /app/packages/mcp-types/package.json ./packages/mcp-types/
|
| 69 |
+
COPY --from=builder --chown=user /app/apps/backend/dist ./apps/backend/dist
|
| 70 |
+
COPY --from=builder --chown=user /app/apps/backend/package.json ./apps/backend/
|
| 71 |
+
|
| 72 |
+
# Copy Prisma client if generated
|
| 73 |
+
COPY --from=builder --chown=user /app/node_modules/.prisma ./node_modules/.prisma 2>/dev/null || true
|
| 74 |
+
COPY --from=builder --chown=user /app/node_modules/@prisma ./node_modules/@prisma 2>/dev/null || true
|
| 75 |
+
|
| 76 |
+
# Create data directories (Cloud DropZone)
|
| 77 |
+
RUN mkdir -p /app/data/dropzone && \
|
| 78 |
+
mkdir -p /app/data/vidensarkiv && \
|
| 79 |
+
mkdir -p /app/data/agents && \
|
| 80 |
+
mkdir -p /app/data/harvested
|
| 81 |
+
|
| 82 |
+
# Environment for HF Spaces
|
| 83 |
+
ENV NODE_ENV=production
|
| 84 |
+
ENV PORT=7860
|
| 85 |
+
ENV HOST=0.0.0.0
|
| 86 |
+
ENV DOCKER=true
|
| 87 |
+
ENV HF_SPACE=true
|
| 88 |
+
|
| 89 |
+
# HF Spaces exposes port 7860
|
| 90 |
+
EXPOSE 7860
|
| 91 |
+
|
| 92 |
+
WORKDIR /app/apps/backend
|
| 93 |
+
|
| 94 |
+
# Health check for HF
|
| 95 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 96 |
+
CMD node -e "fetch('http://localhost:7860/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
|
| 97 |
+
|
| 98 |
+
CMD ["node", "dist/index.js"]
|
README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: WidgeTDC Cortex Backend
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: MIT
|
| 9 |
+
app_port: 7860
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# WidgeTDC Cortex Backend
|
| 13 |
+
|
| 14 |
+
Enterprise-grade autonomous intelligence platform.
|
| 15 |
+
|
| 16 |
+
## Endpoints
|
| 17 |
+
- GET /health
|
| 18 |
+
- POST /api/mcp/route
|
| 19 |
+
- WS /mcp/ws
|
apps/backend/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@widget-tdc/backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "tsx watch src/index.ts",
|
| 8 |
+
"build": "tsc",
|
| 9 |
+
"start": "node dist/index.js",
|
| 10 |
+
"test": "vitest run",
|
| 11 |
+
"neural-bridge": "tsx src/mcp/servers/NeuralBridgeServer.ts",
|
| 12 |
+
"neural-bridge:build": "tsc && node dist/mcp/servers/NeuralBridgeServer.js"
|
| 13 |
+
},
|
| 14 |
+
"dependencies": {
|
| 15 |
+
"@anthropic-ai/sdk": "^0.71.0",
|
| 16 |
+
"@google/generative-ai": "^0.4.0",
|
| 17 |
+
"@huggingface/inference": "^4.13.3",
|
| 18 |
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
| 19 |
+
"@opensearch-project/opensearch": "^3.5.1",
|
| 20 |
+
"@prisma/client": "^5.22.0",
|
| 21 |
+
"@types/geoip-lite": "^1.4.4",
|
| 22 |
+
"@types/js-yaml": "^4.0.9",
|
| 23 |
+
"@types/pdf-parse": "^1.1.5",
|
| 24 |
+
"@types/systeminformation": "^3.23.1",
|
| 25 |
+
"@widget-tdc/domain-types": "file:./packages/domain-types",
|
| 26 |
+
"@widget-tdc/mcp-types": "file:./packages/mcp-types",
|
| 27 |
+
"@xenova/transformers": "^2.17.2",
|
| 28 |
+
"axios": "^1.6.5",
|
| 29 |
+
"cheerio": "^1.0.0",
|
| 30 |
+
"chromadb": "^3.1.6",
|
| 31 |
+
"cors": "^2.8.5",
|
| 32 |
+
"dotenv": "^17.2.3",
|
| 33 |
+
"express": "^4.18.2",
|
| 34 |
+
"express-rate-limit": "^8.2.1",
|
| 35 |
+
"geoip-lite": "^1.4.10",
|
| 36 |
+
"gpt-3-encoder": "^1.1.4",
|
| 37 |
+
"helmet": "^8.1.0",
|
| 38 |
+
"imap": "^0.8.19",
|
| 39 |
+
"ioredis": "^5.3.2",
|
| 40 |
+
"js-yaml": "^4.1.1",
|
| 41 |
+
"jsonwebtoken": "^9.0.2",
|
| 42 |
+
"jszip": "^3.10.1",
|
| 43 |
+
"mailparser": "^3.6.9",
|
| 44 |
+
"minio": "^8.0.6",
|
| 45 |
+
"multer": "^1.4.5-lts.1",
|
| 46 |
+
"neo4j-driver": "^6.0.1",
|
| 47 |
+
"node-cron": "^3.0.3",
|
| 48 |
+
"openai": "^4.73.0",
|
| 49 |
+
"pdf-parse": "^2.4.5",
|
| 50 |
+
"pdfjs-dist": "^5.4.449",
|
| 51 |
+
"pg": "^8.16.3",
|
| 52 |
+
"pptxgenjs": "^3.12.0",
|
| 53 |
+
"prisma": "^5.22.0",
|
| 54 |
+
"puppeteer": "^24.32.0",
|
| 55 |
+
"redis": "^5.10.0",
|
| 56 |
+
"sharp": "^0.32.6",
|
| 57 |
+
"sql.js": "^1.8.0",
|
| 58 |
+
"systeminformation": "^5.27.11",
|
| 59 |
+
"testcontainers": "^11.8.1",
|
| 60 |
+
"uuid": "^9.0.1",
|
| 61 |
+
"winston": "^3.18.3",
|
| 62 |
+
"ws": "^8.14.2",
|
| 63 |
+
"xml2js": "^0.6.2",
|
| 64 |
+
"zod": "^3.25.76"
|
| 65 |
+
},
|
| 66 |
+
"devDependencies": {
|
| 67 |
+
"@types/cors": "^2.8.17",
|
| 68 |
+
"@types/express": "^4.17.21",
|
| 69 |
+
"@types/imap": "^0.8.40",
|
| 70 |
+
"@types/ioredis": "^4.28.10",
|
| 71 |
+
"@types/jsonwebtoken": "^9.0.10",
|
| 72 |
+
"@types/jszip": "^3.4.0",
|
| 73 |
+
"@types/mailparser": "^3.4.4",
|
| 74 |
+
"@types/multer": "^1.4.12",
|
| 75 |
+
"@types/node": "^20.10.6",
|
| 76 |
+
"@types/node-cron": "^3.0.11",
|
| 77 |
+
"@types/pg": "^8.16.0",
|
| 78 |
+
"@types/uuid": "^9.0.7",
|
| 79 |
+
"@types/ws": "^8.5.10",
|
| 80 |
+
"@types/xml2js": "^0.4.14",
|
| 81 |
+
"tsx": "^4.20.6",
|
| 82 |
+
"typescript": "~5.8.2"
|
| 83 |
+
}
|
| 84 |
+
}
|
apps/backend/prisma/prisma.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from 'node:path';
|
| 2 |
+
import type { PrismaConfig } from 'prisma';
|
| 3 |
+
import { PrismaPg } from '@prisma/adapter-pg';
|
| 4 |
+
import { Pool } from 'pg';
|
| 5 |
+
|
| 6 |
+
// Create a PostgreSQL connection pool
|
| 7 |
+
const connectionString = process.env.DATABASE_URL || 'postgresql://widgetdc:widgetdc_dev@localhost:5433/widgetdc';
|
| 8 |
+
|
| 9 |
+
const pool = new Pool({ connectionString });
|
| 10 |
+
|
| 11 |
+
export default {
|
| 12 |
+
earlyAccess: true,
|
| 13 |
+
schema: path.join(__dirname, 'schema.prisma'),
|
| 14 |
+
|
| 15 |
+
// Database migration connection (for prisma migrate)
|
| 16 |
+
migrate: {
|
| 17 |
+
adapter: async () => new PrismaPg(pool)
|
| 18 |
+
}
|
| 19 |
+
} satisfies PrismaConfig;
|
apps/backend/prisma/schema.prisma
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// This is your Prisma schema file,
|
| 2 |
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
| 3 |
+
|
| 4 |
+
generator client {
|
| 5 |
+
provider = "prisma-client-js"
|
| 6 |
+
binaryTargets = ["native"]
|
| 7 |
+
engineType = "binary"
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
datasource db {
|
| 11 |
+
provider = "postgresql"
|
| 12 |
+
url = env("DATABASE_URL")
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// NOTE: Vector embeddings are stored in Neo4j, not PostgreSQL
|
| 16 |
+
// This schema handles relational data only
|
| 17 |
+
|
| 18 |
+
// ============================================
|
| 19 |
+
// UI State & Configuration
|
| 20 |
+
// ============================================
|
| 21 |
+
|
| 22 |
+
model Widget {
|
| 23 |
+
id String @id @default(uuid())
|
| 24 |
+
name String
|
| 25 |
+
type String
|
| 26 |
+
config Json?
|
| 27 |
+
active Boolean @default(true)
|
| 28 |
+
createdAt DateTime @default(now())
|
| 29 |
+
updatedAt DateTime @updatedAt
|
| 30 |
+
|
| 31 |
+
@@map("widgets")
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
model Layout {
|
| 35 |
+
id String @id @default(uuid())
|
| 36 |
+
userId String
|
| 37 |
+
orgId String
|
| 38 |
+
layoutData Json
|
| 39 |
+
createdAt DateTime @default(now())
|
| 40 |
+
updatedAt DateTime @updatedAt
|
| 41 |
+
|
| 42 |
+
@@unique([userId, orgId])
|
| 43 |
+
@@map("layouts")
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// ============================================
|
| 47 |
+
// Memory & Knowledge Graph
|
| 48 |
+
// ============================================
|
| 49 |
+
|
| 50 |
+
model MemoryEntity {
|
| 51 |
+
id Int @id @default(autoincrement())
|
| 52 |
+
orgId String @map("org_id")
|
| 53 |
+
userId String? @map("user_id")
|
| 54 |
+
entityType String @map("entity_type")
|
| 55 |
+
content String
|
| 56 |
+
importance Int @default(3)
|
| 57 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 58 |
+
|
| 59 |
+
tags MemoryTag[]
|
| 60 |
+
relationsFrom MemoryRelation[] @relation("SourceEntity")
|
| 61 |
+
relationsTo MemoryRelation[] @relation("TargetEntity")
|
| 62 |
+
|
| 63 |
+
@@index([orgId, userId])
|
| 64 |
+
@@index([entityType])
|
| 65 |
+
@@map("memory_entities")
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
model MemoryRelation {
|
| 69 |
+
id Int @id @default(autoincrement())
|
| 70 |
+
orgId String @map("org_id")
|
| 71 |
+
sourceId Int @map("source_id")
|
| 72 |
+
targetId Int @map("target_id")
|
| 73 |
+
relationType String @map("relation_type")
|
| 74 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 75 |
+
|
| 76 |
+
source MemoryEntity @relation("SourceEntity", fields: [sourceId], references: [id], onDelete: Cascade)
|
| 77 |
+
target MemoryEntity @relation("TargetEntity", fields: [targetId], references: [id], onDelete: Cascade)
|
| 78 |
+
|
| 79 |
+
@@index([sourceId])
|
| 80 |
+
@@index([targetId])
|
| 81 |
+
@@index([orgId])
|
| 82 |
+
@@map("memory_relations")
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
model MemoryTag {
|
| 86 |
+
id Int @id @default(autoincrement())
|
| 87 |
+
entityId Int @map("entity_id")
|
| 88 |
+
tag String
|
| 89 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 90 |
+
|
| 91 |
+
entity MemoryEntity @relation(fields: [entityId], references: [id], onDelete: Cascade)
|
| 92 |
+
|
| 93 |
+
@@index([tag])
|
| 94 |
+
@@index([entityId])
|
| 95 |
+
@@map("memory_tags")
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// ============================================
|
| 99 |
+
// PAL (Personal Assistant Layer)
|
| 100 |
+
// ============================================
|
| 101 |
+
|
| 102 |
+
model PalUserProfile {
|
| 103 |
+
id Int @id @default(autoincrement())
|
| 104 |
+
userId String @map("user_id")
|
| 105 |
+
orgId String @map("org_id")
|
| 106 |
+
preferenceTone String @default("neutral") @map("preference_tone")
|
| 107 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 108 |
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
| 109 |
+
|
| 110 |
+
@@unique([userId, orgId])
|
| 111 |
+
@@map("pal_user_profiles")
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
model PalFocusWindow {
|
| 115 |
+
id Int @id @default(autoincrement())
|
| 116 |
+
userId String @map("user_id")
|
| 117 |
+
orgId String @map("org_id")
|
| 118 |
+
weekday Int
|
| 119 |
+
startHour Int @map("start_hour")
|
| 120 |
+
endHour Int @map("end_hour")
|
| 121 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 122 |
+
|
| 123 |
+
@@index([userId, orgId])
|
| 124 |
+
@@map("pal_focus_windows")
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
model PalEvent {
|
| 128 |
+
id Int @id @default(autoincrement())
|
| 129 |
+
userId String @map("user_id")
|
| 130 |
+
orgId String @map("org_id")
|
| 131 |
+
eventType String @map("event_type")
|
| 132 |
+
payload Json
|
| 133 |
+
detectedStressLevel Int? @map("detected_stress_level")
|
| 134 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 135 |
+
|
| 136 |
+
@@index([userId, orgId])
|
| 137 |
+
@@index([eventType])
|
| 138 |
+
@@map("pal_events")
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// ============================================
|
| 142 |
+
// SRAG (Structured RAG)
|
| 143 |
+
// ============================================
|
| 144 |
+
|
| 145 |
+
model RawDocument {
|
| 146 |
+
id Int @id @default(autoincrement())
|
| 147 |
+
orgId String @map("org_id")
|
| 148 |
+
sourceType String @map("source_type")
|
| 149 |
+
sourcePath String @map("source_path")
|
| 150 |
+
content String
|
| 151 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 152 |
+
|
| 153 |
+
facts StructuredFact[]
|
| 154 |
+
|
| 155 |
+
@@index([orgId])
|
| 156 |
+
@@map("raw_documents")
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
model StructuredFact {
|
| 160 |
+
id Int @id @default(autoincrement())
|
| 161 |
+
orgId String @map("org_id")
|
| 162 |
+
docId Int? @map("doc_id")
|
| 163 |
+
factType String @map("fact_type")
|
| 164 |
+
jsonPayload Json @map("json_payload")
|
| 165 |
+
occurredAt DateTime? @map("occurred_at")
|
| 166 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 167 |
+
|
| 168 |
+
document RawDocument? @relation(fields: [docId], references: [id], onDelete: SetNull)
|
| 169 |
+
|
| 170 |
+
@@index([orgId])
|
| 171 |
+
@@index([factType])
|
| 172 |
+
@@map("structured_facts")
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// ============================================
|
| 176 |
+
// Document Metadata (vectors stored in Neo4j)
|
| 177 |
+
// ============================================
|
| 178 |
+
|
| 179 |
+
model VectorDocument {
|
| 180 |
+
id String @id @default(uuid())
|
| 181 |
+
content String
|
| 182 |
+
// NOTE: Embeddings are stored in Neo4j, not here
|
| 183 |
+
metadata Json?
|
| 184 |
+
namespace String @default("default")
|
| 185 |
+
userId String
|
| 186 |
+
orgId String
|
| 187 |
+
createdAt DateTime @default(now())
|
| 188 |
+
updatedAt DateTime @updatedAt
|
| 189 |
+
|
| 190 |
+
@@index([namespace])
|
| 191 |
+
@@index([userId, orgId])
|
| 192 |
+
@@map("vector_documents")
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// ============================================
|
| 196 |
+
// Autonomous Agent & Tasks
|
| 197 |
+
// ============================================
|
| 198 |
+
|
| 199 |
+
model AgentTask {
|
| 200 |
+
id String @id @default(uuid())
|
| 201 |
+
type String
|
| 202 |
+
payload Json
|
| 203 |
+
status String @default("pending") // pending, running, completed, failed, waiting_approval
|
| 204 |
+
priority Int @default(50)
|
| 205 |
+
result Json?
|
| 206 |
+
error String?
|
| 207 |
+
userId String @default("system")
|
| 208 |
+
orgId String @default("default")
|
| 209 |
+
createdAt DateTime @default(now())
|
| 210 |
+
updatedAt DateTime @updatedAt
|
| 211 |
+
completedAt DateTime?
|
| 212 |
+
|
| 213 |
+
@@index([status])
|
| 214 |
+
@@index([priority])
|
| 215 |
+
@@index([userId, orgId])
|
| 216 |
+
@@map("agent_tasks")
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
model ExecutionLog {
|
| 220 |
+
id String @id @default(uuid())
|
| 221 |
+
taskId String?
|
| 222 |
+
taskType String
|
| 223 |
+
success Boolean
|
| 224 |
+
duration Int? // milliseconds
|
| 225 |
+
result Json?
|
| 226 |
+
error String?
|
| 227 |
+
userId String @default("system")
|
| 228 |
+
orgId String @default("default")
|
| 229 |
+
createdAt DateTime @default(now())
|
| 230 |
+
|
| 231 |
+
@@index([taskType])
|
| 232 |
+
@@index([createdAt])
|
| 233 |
+
@@index([userId, orgId])
|
| 234 |
+
@@map("execution_logs")
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// ============================================
|
| 238 |
+
// Data Sources & Ingestion
|
| 239 |
+
// ============================================
|
| 240 |
+
|
| 241 |
+
model DataSource {
|
| 242 |
+
id String @id @default(uuid())
|
| 243 |
+
name String @unique
|
| 244 |
+
type String
|
| 245 |
+
description String?
|
| 246 |
+
enabled Boolean @default(false)
|
| 247 |
+
requiresApproval Boolean @default(true)
|
| 248 |
+
config Json?
|
| 249 |
+
lastUsedAt DateTime?
|
| 250 |
+
createdAt DateTime @default(now())
|
| 251 |
+
updatedAt DateTime @updatedAt
|
| 252 |
+
|
| 253 |
+
@@map("data_sources")
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
model IngestedDocument {
|
| 257 |
+
id String @id @default(uuid())
|
| 258 |
+
sourceId String
|
| 259 |
+
externalId String
|
| 260 |
+
title String?
|
| 261 |
+
content String?
|
| 262 |
+
metadata Json?
|
| 263 |
+
userId String
|
| 264 |
+
orgId String
|
| 265 |
+
ingestedAt DateTime @default(now())
|
| 266 |
+
|
| 267 |
+
@@unique([sourceId, externalId])
|
| 268 |
+
@@index([userId, orgId])
|
| 269 |
+
@@map("ingested_documents")
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// ============================================
|
| 273 |
+
// MCP Resources & Prompts
|
| 274 |
+
// ============================================
|
| 275 |
+
|
| 276 |
+
model MCPResource {
|
| 277 |
+
id String @id @default(uuid())
|
| 278 |
+
uri String @unique
|
| 279 |
+
name String
|
| 280 |
+
description String?
|
| 281 |
+
mimeType String?
|
| 282 |
+
payload Json
|
| 283 |
+
createdAt DateTime @default(now())
|
| 284 |
+
updatedAt DateTime @updatedAt
|
| 285 |
+
|
| 286 |
+
@@map("mcp_resources")
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
model AgentPrompt {
|
| 290 |
+
id String @id @default(uuid())
|
| 291 |
+
agentId String
|
| 292 |
+
version Int
|
| 293 |
+
promptText String
|
| 294 |
+
active Boolean @default(true)
|
| 295 |
+
performance Json? // KPIs, metrics
|
| 296 |
+
createdAt DateTime @default(now())
|
| 297 |
+
|
| 298 |
+
@@unique([agentId, version])
|
| 299 |
+
@@map("agent_prompts")
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// ============================================
|
| 303 |
+
// Security search + activity
|
| 304 |
+
// ============================================
|
| 305 |
+
|
| 306 |
+
model SecuritySearchTemplate {
|
| 307 |
+
id String @id
|
| 308 |
+
name String
|
| 309 |
+
description String
|
| 310 |
+
query String
|
| 311 |
+
severity String
|
| 312 |
+
timeframe String
|
| 313 |
+
sources Json
|
| 314 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 315 |
+
|
| 316 |
+
@@map("security_search_templates")
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
model SecuritySearchHistory {
|
| 320 |
+
id String @id
|
| 321 |
+
query String
|
| 322 |
+
severity String
|
| 323 |
+
timeframe String
|
| 324 |
+
sources Json
|
| 325 |
+
results Int @map("results_count")
|
| 326 |
+
latencyMs Int @map("latency_ms")
|
| 327 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 328 |
+
|
| 329 |
+
@@map("security_search_history")
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
model SecurityActivityEvent {
|
| 333 |
+
id String @id
|
| 334 |
+
title String
|
| 335 |
+
description String
|
| 336 |
+
category String
|
| 337 |
+
severity String
|
| 338 |
+
source String
|
| 339 |
+
rule String?
|
| 340 |
+
channel String
|
| 341 |
+
payload Json?
|
| 342 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 343 |
+
acknowledged Boolean @default(false)
|
| 344 |
+
|
| 345 |
+
@@map("security_activity_events")
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// ============================================
|
| 349 |
+
// Widget permissions
|
| 350 |
+
// ============================================
|
| 351 |
+
|
| 352 |
+
model WidgetPermission {
|
| 353 |
+
widgetId String @map("widget_id")
|
| 354 |
+
resourceType String @map("resource_type")
|
| 355 |
+
accessLevel String @map("access_level")
|
| 356 |
+
override Boolean @default(false)
|
| 357 |
+
|
| 358 |
+
@@id([widgetId, resourceType])
|
| 359 |
+
@@map("widget_permissions")
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// ============================================
|
| 363 |
+
// PRD Prototypes
|
| 364 |
+
// ============================================
|
| 365 |
+
|
| 366 |
+
model Prototype {
|
| 367 |
+
id String @id @default(uuid())
|
| 368 |
+
name String
|
| 369 |
+
htmlContent String
|
| 370 |
+
prdId String?
|
| 371 |
+
version Int @default(1)
|
| 372 |
+
style String @default("modern")
|
| 373 |
+
status String @default("complete") // generating, complete, error
|
| 374 |
+
metadata Json?
|
| 375 |
+
userId String @default("system")
|
| 376 |
+
orgId String @default("default")
|
| 377 |
+
createdAt DateTime @default(now())
|
| 378 |
+
updatedAt DateTime @updatedAt
|
| 379 |
+
|
| 380 |
+
@@unique([name, userId, orgId])
|
| 381 |
+
@@index([userId, orgId])
|
| 382 |
+
@@index([prdId])
|
| 383 |
+
@@map("prototypes")
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// ============================================
|
| 387 |
+
// Notes (migrated from sql.js)
|
| 388 |
+
// ============================================
|
| 389 |
+
|
| 390 |
+
model Note {
|
| 391 |
+
id Int @id @default(autoincrement())
|
| 392 |
+
userId String @map("user_id")
|
| 393 |
+
orgId String @map("org_id")
|
| 394 |
+
source String
|
| 395 |
+
title String
|
| 396 |
+
body String
|
| 397 |
+
tags String @default("")
|
| 398 |
+
owner String
|
| 399 |
+
compliance String @default("clean") // clean, review, restricted
|
| 400 |
+
retention String @default("90d") // 30d, 90d, 1y, archive
|
| 401 |
+
riskScore Int @default(0) @map("risk_score")
|
| 402 |
+
attachments Int @default(0)
|
| 403 |
+
createdAt DateTime @default(now()) @map("created_at")
|
| 404 |
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
| 405 |
+
|
| 406 |
+
@@index([userId, orgId])
|
| 407 |
+
@@index([source])
|
| 408 |
+
@@index([compliance])
|
| 409 |
+
@@map("notes")
|
| 410 |
+
}
|
apps/backend/src/adapters/Neo4jAdapter.ts
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ╔═══════════════════════════════════════════════════════════════════════════╗
|
| 3 |
+
* ║ NEO4J ADAPTER - SYNAPTIC CORTEX ║
|
| 4 |
+
* ║═══════════════════════════════════════════════════════════════════════════║
|
| 5 |
+
* ║ Graph-Native connection layer for WidgeTDC knowledge graph ║
|
| 6 |
+
* ║ ║
|
| 7 |
+
* ║ CODEX RULE #3: Self-Healing & Robustness ║
|
| 8 |
+
* ║ - Automatic reconnection on failure ║
|
| 9 |
+
* ║ - Circuit breaker pattern ║
|
| 10 |
+
* ║ - Health monitoring ║
|
| 11 |
+
* ╚═══════════════════════════════════════════════════════════════════════════╝
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
import neo4j, { Driver, Session, QueryResult, Record as Neo4jRecord } from 'neo4j-driver';
|
| 15 |
+
|
| 16 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 17 |
+
// Types
|
| 18 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 19 |
+
|
| 20 |
+
export interface Neo4jConfig {
|
| 21 |
+
uri: string;
|
| 22 |
+
user: string;
|
| 23 |
+
password: string;
|
| 24 |
+
database?: string;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export interface QueryOptions {
|
| 28 |
+
timeout?: number;
|
| 29 |
+
database?: string;
|
| 30 |
+
readOnly?: boolean;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export interface HealthStatus {
|
| 34 |
+
connected: boolean;
|
| 35 |
+
latencyMs?: number;
|
| 36 |
+
nodeCount?: number;
|
| 37 |
+
relationshipCount?: number;
|
| 38 |
+
lastCheck: string;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 42 |
+
// Neo4j Adapter - Singleton Pattern
|
| 43 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 44 |
+
|
| 45 |
+
class Neo4jAdapter {
|
| 46 |
+
private static instance: Neo4jAdapter;
|
| 47 |
+
private driver: Driver | null = null;
|
| 48 |
+
private _isConnected: boolean = false;
|
| 49 |
+
private lastHealthCheck: HealthStatus | null = null;
|
| 50 |
+
|
| 51 |
+
// Public getter for connection status
|
| 52 |
+
public get connected(): boolean {
|
| 53 |
+
return this._isConnected;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Circuit breaker state
|
| 57 |
+
private failureCount: number = 0;
|
| 58 |
+
private readonly failureThreshold: number = 5;
|
| 59 |
+
private lastFailureTime: number = 0;
|
| 60 |
+
private readonly resetTimeoutMs: number = 60000;
|
| 61 |
+
|
| 62 |
+
// Connection config
|
| 63 |
+
private config: Neo4jConfig;
|
| 64 |
+
|
| 65 |
+
private constructor() {
|
| 66 |
+
this.config = {
|
| 67 |
+
uri: process.env.NEO4J_URI || 'bolt://localhost:7687',
|
| 68 |
+
user: process.env.NEO4J_USER || 'neo4j',
|
| 69 |
+
password: process.env.NEO4J_PASSWORD || 'password',
|
| 70 |
+
database: process.env.NEO4J_DATABASE || 'neo4j'
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
this.connect();
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 77 |
+
// Singleton Access
|
| 78 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 79 |
+
|
| 80 |
+
public static getInstance(): Neo4jAdapter {
|
| 81 |
+
if (!Neo4jAdapter.instance) {
|
| 82 |
+
Neo4jAdapter.instance = new Neo4jAdapter();
|
| 83 |
+
}
|
| 84 |
+
return Neo4jAdapter.instance;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 88 |
+
// Connection Management
|
| 89 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 90 |
+
|
| 91 |
+
private async connect(): Promise<boolean> {
|
| 92 |
+
try {
|
| 93 |
+
console.error(`[Neo4jAdapter] 🧠 Establishing synaptic link to ${this.config.uri}...`);
|
| 94 |
+
|
| 95 |
+
this.driver = neo4j.driver(
|
| 96 |
+
this.config.uri,
|
| 97 |
+
neo4j.auth.basic(this.config.user, this.config.password),
|
| 98 |
+
{
|
| 99 |
+
maxConnectionPoolSize: 50,
|
| 100 |
+
connectionAcquisitionTimeout: 30000,
|
| 101 |
+
connectionTimeout: 20000,
|
| 102 |
+
}
|
| 103 |
+
);
|
| 104 |
+
|
| 105 |
+
// Verify connectivity
|
| 106 |
+
await this.driver.verifyConnectivity();
|
| 107 |
+
this._isConnected = true;
|
| 108 |
+
this.failureCount = 0;
|
| 109 |
+
|
| 110 |
+
console.error('[Neo4jAdapter] ✅ Synaptic link ESTABLISHED. Cortex is online.');
|
| 111 |
+
return true;
|
| 112 |
+
|
| 113 |
+
} catch (error: any) {
|
| 114 |
+
console.error('[Neo4jAdapter] ❌ CONNECTION FAILURE:', error.message);
|
| 115 |
+
this._isConnected = false;
|
| 116 |
+
this.failureCount++;
|
| 117 |
+
this.lastFailureTime = Date.now();
|
| 118 |
+
return false;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
private async ensureConnection(): Promise<void> {
|
| 123 |
+
// Check circuit breaker
|
| 124 |
+
if (this.failureCount >= this.failureThreshold) {
|
| 125 |
+
const timeSinceFailure = Date.now() - this.lastFailureTime;
|
| 126 |
+
if (timeSinceFailure < this.resetTimeoutMs) {
|
| 127 |
+
throw new Error(`Neo4j Cortex circuit OPEN - ${Math.ceil((this.resetTimeoutMs - timeSinceFailure) / 1000)}s until retry`);
|
| 128 |
+
}
|
| 129 |
+
// Reset and try again
|
| 130 |
+
this.failureCount = 0;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if (!this.driver || !this._isConnected) {
|
| 134 |
+
const connected = await this.connect();
|
| 135 |
+
if (!connected) {
|
| 136 |
+
throw new Error('Neo4j Cortex Unreachable - connection failed');
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 142 |
+
// Query Execution
|
| 143 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 144 |
+
|
| 145 |
+
public async executeQuery(
|
| 146 |
+
cypher: string,
|
| 147 |
+
params: Record<string, any> = {},
|
| 148 |
+
options: QueryOptions = {}
|
| 149 |
+
): Promise<any[]> {
|
| 150 |
+
await this.ensureConnection();
|
| 151 |
+
|
| 152 |
+
const session: Session = this.driver!.session({
|
| 153 |
+
database: options.database || this.config.database,
|
| 154 |
+
defaultAccessMode: options.readOnly ? neo4j.session.READ : neo4j.session.WRITE
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
const startTime = Date.now();
|
| 158 |
+
|
| 159 |
+
try {
|
| 160 |
+
const result: QueryResult = await session.run(cypher, params);
|
| 161 |
+
const latency = Date.now() - startTime;
|
| 162 |
+
|
| 163 |
+
console.error(`[Neo4jAdapter] ⚡ Query executed in ${latency}ms (${result.records.length} records)`);
|
| 164 |
+
|
| 165 |
+
return result.records.map((record: Neo4jRecord) => this.recordToObject(record));
|
| 166 |
+
|
| 167 |
+
} catch (error: any) {
|
| 168 |
+
this.failureCount++;
|
| 169 |
+
this.lastFailureTime = Date.now();
|
| 170 |
+
|
| 171 |
+
console.error(`[Neo4jAdapter] ❌ Query failed: ${error.message}`);
|
| 172 |
+
console.error(`[Neo4jAdapter] Cypher: ${cypher.substring(0, 100)}...`);
|
| 173 |
+
|
| 174 |
+
throw error;
|
| 175 |
+
|
| 176 |
+
} finally {
|
| 177 |
+
await session.close();
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Execute a read-only query (optimized for replicas)
|
| 183 |
+
*/
|
| 184 |
+
public async readQuery(
|
| 185 |
+
cypher: string,
|
| 186 |
+
params: Record<string, any> = {}
|
| 187 |
+
): Promise<any[]> {
|
| 188 |
+
return this.executeQuery(cypher, params, { readOnly: true });
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/**
|
| 192 |
+
* Execute a write query
|
| 193 |
+
*/
|
| 194 |
+
public async writeQuery(
|
| 195 |
+
cypher: string,
|
| 196 |
+
params: Record<string, any> = {}
|
| 197 |
+
): Promise<any[]> {
|
| 198 |
+
return this.executeQuery(cypher, params, { readOnly: false });
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 202 |
+
// High-Level Operations
|
| 203 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 204 |
+
|
| 205 |
+
/**
|
| 206 |
+
* Search nodes by label and properties
|
| 207 |
+
*/
|
| 208 |
+
public async searchNodes(
|
| 209 |
+
label: string,
|
| 210 |
+
searchTerm: string,
|
| 211 |
+
limit: number = 20
|
| 212 |
+
): Promise<any[]> {
|
| 213 |
+
const cypher = `
|
| 214 |
+
MATCH (n:${label})
|
| 215 |
+
WHERE n.name CONTAINS $searchTerm
|
| 216 |
+
OR n.title CONTAINS $searchTerm
|
| 217 |
+
OR n.content CONTAINS $searchTerm
|
| 218 |
+
RETURN n
|
| 219 |
+
LIMIT $limit
|
| 220 |
+
`;
|
| 221 |
+
return this.readQuery(cypher, { searchTerm, limit: neo4j.int(limit) });
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/**
|
| 225 |
+
* Get node by ID
|
| 226 |
+
*/
|
| 227 |
+
public async getNodeById(nodeId: string): Promise<any | null> {
|
| 228 |
+
const cypher = `
|
| 229 |
+
MATCH (n)
|
| 230 |
+
WHERE n.id = $nodeId OR elementId(n) = $nodeId
|
| 231 |
+
RETURN n
|
| 232 |
+
LIMIT 1
|
| 233 |
+
`;
|
| 234 |
+
const results = await this.readQuery(cypher, { nodeId });
|
| 235 |
+
return results[0] || null;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/**
|
| 239 |
+
* Get node relationships
|
| 240 |
+
*/
|
| 241 |
+
public async getNodeRelationships(
|
| 242 |
+
nodeId: string,
|
| 243 |
+
direction: 'in' | 'out' | 'both' = 'both',
|
| 244 |
+
limit: number = 50
|
| 245 |
+
): Promise<any[]> {
|
| 246 |
+
let pattern: string;
|
| 247 |
+
switch (direction) {
|
| 248 |
+
case 'in':
|
| 249 |
+
pattern = '(n)<-[r]-(m)';
|
| 250 |
+
break;
|
| 251 |
+
case 'out':
|
| 252 |
+
pattern = '(n)-[r]->(m)';
|
| 253 |
+
break;
|
| 254 |
+
default:
|
| 255 |
+
pattern = '(n)-[r]-(m)';
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
const cypher = `
|
| 259 |
+
MATCH ${pattern}
|
| 260 |
+
WHERE n.id = $nodeId OR elementId(n) = $nodeId
|
| 261 |
+
RETURN type(r) as relationship, m as node, r as details
|
| 262 |
+
LIMIT $limit
|
| 263 |
+
`;
|
| 264 |
+
return this.readQuery(cypher, { nodeId, limit: neo4j.int(limit) });
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
/**
|
| 268 |
+
* Create or merge a node
|
| 269 |
+
*/
|
| 270 |
+
public async createNode(
|
| 271 |
+
label: string,
|
| 272 |
+
properties: Record<string, any>
|
| 273 |
+
): Promise<any> {
|
| 274 |
+
const cypher = `
|
| 275 |
+
MERGE (n:${label} {id: $id})
|
| 276 |
+
SET n += $properties
|
| 277 |
+
SET n.updatedAt = datetime()
|
| 278 |
+
RETURN n
|
| 279 |
+
`;
|
| 280 |
+
|
| 281 |
+
const id = properties.id || this.generateId(label, properties);
|
| 282 |
+
const results = await this.writeQuery(cypher, {
|
| 283 |
+
id,
|
| 284 |
+
properties: { ...properties, id }
|
| 285 |
+
});
|
| 286 |
+
|
| 287 |
+
return results[0];
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/**
|
| 291 |
+
* Create a relationship between nodes
|
| 292 |
+
*/
|
| 293 |
+
public async createRelationship(
|
| 294 |
+
fromId: string,
|
| 295 |
+
toId: string,
|
| 296 |
+
relationshipType: string,
|
| 297 |
+
properties: Record<string, any> = {}
|
| 298 |
+
): Promise<any> {
|
| 299 |
+
const cypher = `
|
| 300 |
+
MATCH (a), (b)
|
| 301 |
+
WHERE (a.id = $fromId OR elementId(a) = $fromId)
|
| 302 |
+
AND (b.id = $toId OR elementId(b) = $toId)
|
| 303 |
+
MERGE (a)-[r:${relationshipType}]->(b)
|
| 304 |
+
SET r += $properties
|
| 305 |
+
SET r.createdAt = datetime()
|
| 306 |
+
RETURN a, r, b
|
| 307 |
+
`;
|
| 308 |
+
|
| 309 |
+
const results = await this.writeQuery(cypher, { fromId, toId, properties });
|
| 310 |
+
return results[0];
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/**
|
| 314 |
+
* Delete a node by ID
|
| 315 |
+
*/
|
| 316 |
+
public async deleteNode(nodeId: string): Promise<boolean> {
|
| 317 |
+
const cypher = `
|
| 318 |
+
MATCH (n)
|
| 319 |
+
WHERE n.id = $nodeId OR elementId(n) = $nodeId
|
| 320 |
+
DETACH DELETE n
|
| 321 |
+
RETURN count(n) as deleted
|
| 322 |
+
`;
|
| 323 |
+
|
| 324 |
+
const results = await this.writeQuery(cypher, { nodeId });
|
| 325 |
+
return (results[0]?.deleted || 0) > 0;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/**
|
| 329 |
+
* Alias for executeQuery - for compatibility
|
| 330 |
+
*/
|
| 331 |
+
public async runQuery(
|
| 332 |
+
cypher: string,
|
| 333 |
+
params: Record<string, any> = {}
|
| 334 |
+
): Promise<any[]> {
|
| 335 |
+
return this.executeQuery(cypher, params);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/**
|
| 339 |
+
* Alias for executeQuery - for compatibility
|
| 340 |
+
*/
|
| 341 |
+
public async query(
|
| 342 |
+
cypher: string,
|
| 343 |
+
params: Record<string, any> = {}
|
| 344 |
+
): Promise<any[]> {
|
| 345 |
+
return this.executeQuery(cypher, params);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 349 |
+
// Health & Monitoring
|
| 350 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 351 |
+
|
| 352 |
+
public async healthCheck(): Promise<HealthStatus> {
|
| 353 |
+
const startTime = Date.now();
|
| 354 |
+
|
| 355 |
+
try {
|
| 356 |
+
await this.ensureConnection();
|
| 357 |
+
|
| 358 |
+
// Get database stats
|
| 359 |
+
const [statsResult] = await this.readQuery(`
|
| 360 |
+
CALL apoc.meta.stats() YIELD nodeCount, relCount
|
| 361 |
+
RETURN nodeCount, relCount
|
| 362 |
+
`).catch(() => [{ nodeCount: -1, relCount: -1 }]);
|
| 363 |
+
|
| 364 |
+
const latency = Date.now() - startTime;
|
| 365 |
+
|
| 366 |
+
this.lastHealthCheck = {
|
| 367 |
+
connected: true,
|
| 368 |
+
latencyMs: latency,
|
| 369 |
+
nodeCount: statsResult?.nodeCount,
|
| 370 |
+
relationshipCount: statsResult?.relCount,
|
| 371 |
+
lastCheck: new Date().toISOString()
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
return this.lastHealthCheck;
|
| 375 |
+
|
| 376 |
+
} catch (error: any) {
|
| 377 |
+
this.lastHealthCheck = {
|
| 378 |
+
connected: false,
|
| 379 |
+
lastCheck: new Date().toISOString()
|
| 380 |
+
};
|
| 381 |
+
return this.lastHealthCheck;
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
public getLastHealthStatus(): HealthStatus | null {
|
| 386 |
+
return this.lastHealthCheck;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
public isHealthy(): boolean {
|
| 390 |
+
return this._isConnected && this.failureCount < this.failureThreshold;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 394 |
+
// Cleanup
|
| 395 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 396 |
+
|
| 397 |
+
public async close(): Promise<void> {
|
| 398 |
+
if (this.driver) {
|
| 399 |
+
await this.driver.close();
|
| 400 |
+
this._isConnected = false;
|
| 401 |
+
console.error('[Neo4jAdapter] 🔌 Synaptic link severed.');
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 406 |
+
// Helpers
|
| 407 |
+
// ═══════════════════════════════════════════════════════════════════════
|
| 408 |
+
|
| 409 |
+
private recordToObject(record: Neo4jRecord): any {
|
| 410 |
+
const obj: any = {};
|
| 411 |
+
record.keys.forEach((key) => {
|
| 412 |
+
const value = record.get(key);
|
| 413 |
+
obj[key] = this.convertNeo4jValue(value);
|
| 414 |
+
});
|
| 415 |
+
return obj;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
private convertNeo4jValue(value: any): any {
|
| 419 |
+
if (value === null || value === undefined) {
|
| 420 |
+
return value;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// Neo4j Integer
|
| 424 |
+
if (neo4j.isInt(value)) {
|
| 425 |
+
return value.toNumber();
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
// Neo4j Node
|
| 429 |
+
if (value.labels && value.properties) {
|
| 430 |
+
return {
|
| 431 |
+
id: value.elementId || value.identity?.toString(),
|
| 432 |
+
labels: value.labels,
|
| 433 |
+
...value.properties
|
| 434 |
+
};
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
// Neo4j Relationship
|
| 438 |
+
if (value.type && value.properties && value.start && value.end) {
|
| 439 |
+
return {
|
| 440 |
+
id: value.elementId || value.identity?.toString(),
|
| 441 |
+
type: value.type,
|
| 442 |
+
startNodeId: value.startNodeElementId || value.start?.toString(),
|
| 443 |
+
endNodeId: value.endNodeElementId || value.end?.toString(),
|
| 444 |
+
...value.properties
|
| 445 |
+
};
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
// Arrays
|
| 449 |
+
if (Array.isArray(value)) {
|
| 450 |
+
return value.map(v => this.convertNeo4jValue(v));
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
// Objects
|
| 454 |
+
if (typeof value === 'object') {
|
| 455 |
+
const converted: any = {};
|
| 456 |
+
for (const key of Object.keys(value)) {
|
| 457 |
+
converted[key] = this.convertNeo4jValue(value[key]);
|
| 458 |
+
}
|
| 459 |
+
return converted;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
return value;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
private generateId(label: string, properties: Record<string, any>): string {
|
| 466 |
+
const crypto = require('crypto');
|
| 467 |
+
const content = `${label}:${properties.name || properties.title || JSON.stringify(properties)}`;
|
| 468 |
+
return crypto.createHash('md5').update(content).digest('hex');
|
| 469 |
+
}
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 473 |
+
// Export Singleton Instance
|
| 474 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 475 |
+
|
| 476 |
+
export const neo4jAdapter = Neo4jAdapter.getInstance();
|
| 477 |
+
export { Neo4jAdapter };
|
apps/backend/src/api/approvals.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from 'express';
|
| 2 |
+
import { hitlSystem } from '../platform/HumanInTheLoop';
|
| 3 |
+
|
| 4 |
+
const router = Router();
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Get pending approvals
|
| 8 |
+
*/
|
| 9 |
+
router.get('/approvals', async (req, res) => {
|
| 10 |
+
try {
|
| 11 |
+
const { status, approver } = req.query;
|
| 12 |
+
|
| 13 |
+
let approvals;
|
| 14 |
+
if (status === 'pending') {
|
| 15 |
+
approvals = hitlSystem.getPendingApprovals(approver as string);
|
| 16 |
+
} else {
|
| 17 |
+
const filters: any = {};
|
| 18 |
+
if (status) filters.status = status;
|
| 19 |
+
approvals = hitlSystem.getAuditTrail(filters);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
res.json({ approvals });
|
| 23 |
+
} catch (error) {
|
| 24 |
+
res.status(500).json({ error: String(error) });
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Get approval by ID
|
| 30 |
+
*/
|
| 31 |
+
router.get('/approvals/:id', async (req, res) => {
|
| 32 |
+
try {
|
| 33 |
+
const approval = hitlSystem.getApproval(req.params.id);
|
| 34 |
+
|
| 35 |
+
if (!approval) {
|
| 36 |
+
return res.status(404).json({ error: 'Approval not found' });
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
res.json({ approval });
|
| 40 |
+
} catch (error) {
|
| 41 |
+
res.status(500).json({ error: String(error) });
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Request approval
|
| 47 |
+
*/
|
| 48 |
+
router.post('/approvals/request', async (req, res) => {
|
| 49 |
+
try {
|
| 50 |
+
const { taskId, taskType, description, requestedBy, metadata } = req.body;
|
| 51 |
+
|
| 52 |
+
if (!taskId || !taskType || !description || !requestedBy) {
|
| 53 |
+
return res.status(400).json({ error: 'Missing required fields' });
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const approval = await hitlSystem.requestApproval(
|
| 57 |
+
taskId,
|
| 58 |
+
taskType,
|
| 59 |
+
description,
|
| 60 |
+
requestedBy,
|
| 61 |
+
metadata || {}
|
| 62 |
+
);
|
| 63 |
+
|
| 64 |
+
res.json({ approval });
|
| 65 |
+
} catch (error) {
|
| 66 |
+
res.status(500).json({ error: String(error) });
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* Approve a task
|
| 72 |
+
*/
|
| 73 |
+
router.post('/approvals/:id/approve', async (req, res) => {
|
| 74 |
+
try {
|
| 75 |
+
const { approvedBy } = req.body;
|
| 76 |
+
|
| 77 |
+
if (!approvedBy) {
|
| 78 |
+
return res.status(400).json({ error: 'approvedBy is required' });
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
const approval = await hitlSystem.approve(req.params.id, approvedBy);
|
| 82 |
+
res.json({ approval });
|
| 83 |
+
} catch (error) {
|
| 84 |
+
res.status(500).json({ error: String(error) });
|
| 85 |
+
}
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Reject a task
|
| 90 |
+
*/
|
| 91 |
+
router.post('/approvals/:id/reject', async (req, res) => {
|
| 92 |
+
try {
|
| 93 |
+
const { rejectedBy, reason } = req.body;
|
| 94 |
+
|
| 95 |
+
if (!rejectedBy || !reason) {
|
| 96 |
+
return res.status(400).json({ error: 'rejectedBy and reason are required' });
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const approval = await hitlSystem.reject(req.params.id, rejectedBy, reason);
|
| 100 |
+
res.json({ approval });
|
| 101 |
+
} catch (error) {
|
| 102 |
+
res.status(500).json({ error: String(error) });
|
| 103 |
+
}
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* Get approval statistics
|
| 108 |
+
*/
|
| 109 |
+
router.get('/approvals/stats', async (req, res) => {
|
| 110 |
+
try {
|
| 111 |
+
const stats = hitlSystem.getStatistics();
|
| 112 |
+
res.json({ stats });
|
| 113 |
+
} catch (error) {
|
| 114 |
+
res.status(500).json({ error: String(error) });
|
| 115 |
+
}
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Activate kill switch
|
| 120 |
+
*/
|
| 121 |
+
router.post('/kill-switch/activate', async (req, res) => {
|
| 122 |
+
try {
|
| 123 |
+
const { activatedBy, reason } = req.body;
|
| 124 |
+
|
| 125 |
+
if (!activatedBy || !reason) {
|
| 126 |
+
return res.status(400).json({ error: 'activatedBy and reason are required' });
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
hitlSystem.activateKillSwitch(activatedBy, reason);
|
| 130 |
+
res.json({ success: true, message: 'Kill switch activated' });
|
| 131 |
+
} catch (error) {
|
| 132 |
+
res.status(500).json({ error: String(error) });
|
| 133 |
+
}
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
/**
|
| 137 |
+
* Deactivate kill switch
|
| 138 |
+
*/
|
| 139 |
+
router.post('/kill-switch/deactivate', async (req, res) => {
|
| 140 |
+
try {
|
| 141 |
+
const { deactivatedBy } = req.body;
|
| 142 |
+
|
| 143 |
+
if (!deactivatedBy) {
|
| 144 |
+
return res.status(400).json({ error: 'deactivatedBy is required' });
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
hitlSystem.deactivateKillSwitch(deactivatedBy);
|
| 148 |
+
res.json({ success: true, message: 'Kill switch deactivated' });
|
| 149 |
+
} catch (error) {
|
| 150 |
+
res.status(500).json({ error: String(error) });
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
/**
|
| 155 |
+
* Get kill switch status
|
| 156 |
+
*/
|
| 157 |
+
router.get('/kill-switch/status', async (req, res) => {
|
| 158 |
+
try {
|
| 159 |
+
const active = hitlSystem.isKillSwitchActive();
|
| 160 |
+
res.json({ active });
|
| 161 |
+
} catch (error) {
|
| 162 |
+
res.status(500).json({ error: String(error) });
|
| 163 |
+
}
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
export default router;
|
apps/backend/src/api/health.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from 'express';
|
| 2 |
+
import { neo4jService } from '../database/Neo4jService';
|
| 3 |
+
import { checkPrismaConnection, prisma } from '../database/prisma';
|
| 4 |
+
|
| 5 |
+
const router = Router();
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Overall system health check
|
| 9 |
+
*/
|
| 10 |
+
router.get('/health', async (req, res) => {
|
| 11 |
+
const health = {
|
| 12 |
+
status: 'healthy',
|
| 13 |
+
timestamp: new Date().toISOString(),
|
| 14 |
+
services: {
|
| 15 |
+
database: 'unknown',
|
| 16 |
+
neo4j: 'unknown',
|
| 17 |
+
redis: 'unknown',
|
| 18 |
+
},
|
| 19 |
+
uptime: process.uptime(),
|
| 20 |
+
memory: process.memoryUsage(),
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
try {
|
| 24 |
+
// Check Prisma/PostgreSQL
|
| 25 |
+
const prismaHealthy = await checkPrismaConnection();
|
| 26 |
+
health.services.database = prismaHealthy ? 'healthy' : 'unhealthy';
|
| 27 |
+
if (!prismaHealthy) health.status = 'degraded';
|
| 28 |
+
} catch {
|
| 29 |
+
health.services.database = 'unhealthy';
|
| 30 |
+
health.status = 'degraded';
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
try {
|
| 34 |
+
// Check Neo4j
|
| 35 |
+
await neo4jService.connect();
|
| 36 |
+
const neo4jHealthy = await neo4jService.healthCheck();
|
| 37 |
+
health.services.neo4j = neo4jHealthy ? 'healthy' : 'unhealthy';
|
| 38 |
+
await neo4jService.disconnect();
|
| 39 |
+
|
| 40 |
+
if (!neo4jHealthy) {
|
| 41 |
+
health.status = 'degraded';
|
| 42 |
+
}
|
| 43 |
+
} catch (error) {
|
| 44 |
+
health.services.neo4j = 'unhealthy';
|
| 45 |
+
health.status = 'degraded';
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Check Redis
|
| 49 |
+
if (process.env.REDIS_URL) {
|
| 50 |
+
// Redis URL is configured but ioredis client is not yet installed
|
| 51 |
+
// Once ioredis is installed, this can be updated to perform actual health check
|
| 52 |
+
health.services.redis = 'configured_but_client_unavailable';
|
| 53 |
+
} else {
|
| 54 |
+
health.services.redis = 'not_configured';
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
const statusCode = health.status === 'healthy' ? 200 : 503;
|
| 58 |
+
res.status(statusCode).json(health);
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Database-specific health check
|
| 63 |
+
*/
|
| 64 |
+
router.get('/health/database', async (req, res) => {
|
| 65 |
+
try {
|
| 66 |
+
const prismaHealthy = await checkPrismaConnection();
|
| 67 |
+
if (!prismaHealthy) {
|
| 68 |
+
return res.status(503).json({
|
| 69 |
+
status: 'unhealthy',
|
| 70 |
+
error: 'Prisma unreachable',
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
const result = await prisma.$queryRaw`SELECT 1 as test`;
|
| 75 |
+
|
| 76 |
+
res.json({
|
| 77 |
+
status: 'healthy',
|
| 78 |
+
type: 'PostgreSQL (Prisma)',
|
| 79 |
+
tables: 'n/a',
|
| 80 |
+
test: (result as any[])[0]?.test === 1,
|
| 81 |
+
});
|
| 82 |
+
} catch (error) {
|
| 83 |
+
res.status(503).json({
|
| 84 |
+
status: 'unhealthy',
|
| 85 |
+
error: String(error),
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
/**
|
| 91 |
+
* Neo4j-specific health check
|
| 92 |
+
*/
|
| 93 |
+
router.get('/health/neo4j', async (req, res) => {
|
| 94 |
+
try {
|
| 95 |
+
await neo4jService.connect();
|
| 96 |
+
const healthy = await neo4jService.healthCheck();
|
| 97 |
+
|
| 98 |
+
if (healthy) {
|
| 99 |
+
const stats = await neo4jService.runQuery('MATCH (n) RETURN count(n) as nodeCount');
|
| 100 |
+
await neo4jService.disconnect();
|
| 101 |
+
|
| 102 |
+
res.json({
|
| 103 |
+
status: 'healthy',
|
| 104 |
+
connected: true,
|
| 105 |
+
nodeCount: stats[0]?.nodeCount || 0,
|
| 106 |
+
});
|
| 107 |
+
} else {
|
| 108 |
+
throw new Error('Health check failed');
|
| 109 |
+
}
|
| 110 |
+
} catch (error) {
|
| 111 |
+
res.status(503).json({
|
| 112 |
+
status: 'unhealthy',
|
| 113 |
+
connected: false,
|
| 114 |
+
error: String(error),
|
| 115 |
+
});
|
| 116 |
+
}
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Readiness check (for Kubernetes)
|
| 121 |
+
*/
|
| 122 |
+
router.get('/ready', async (req, res) => {
|
| 123 |
+
try {
|
| 124 |
+
// Use Prisma connection check instead of SQLite
|
| 125 |
+
const prismaHealthy = await checkPrismaConnection();
|
| 126 |
+
if (!prismaHealthy) {
|
| 127 |
+
throw new Error('Database not ready');
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
res.json({
|
| 131 |
+
status: 'ready',
|
| 132 |
+
timestamp: new Date().toISOString(),
|
| 133 |
+
});
|
| 134 |
+
} catch (error) {
|
| 135 |
+
res.status(503).json({
|
| 136 |
+
status: 'not_ready',
|
| 137 |
+
error: String(error),
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* Liveness check (for Kubernetes)
|
| 144 |
+
*/
|
| 145 |
+
router.get('/live', (req, res) => {
|
| 146 |
+
res.json({
|
| 147 |
+
status: 'alive',
|
| 148 |
+
timestamp: new Date().toISOString(),
|
| 149 |
+
uptime: process.uptime(),
|
| 150 |
+
});
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
export default router;
|
apps/backend/src/api/knowledge.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Knowledge API - Endpoints for KnowledgeCompiler
|
| 3 |
+
*
|
| 4 |
+
* Endpoints:
|
| 5 |
+
* - GET /api/knowledge/summary - Full system state summary
|
| 6 |
+
* - GET /api/knowledge/health - Quick health check
|
| 7 |
+
* - GET /api/knowledge/insights - AI-generated insights only
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { Router, Request, Response } from 'express';
|
| 11 |
+
import { knowledgeCompiler } from '../services/Knowledge/KnowledgeCompiler.js';
|
| 12 |
+
|
| 13 |
+
const router = Router();
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* GET /api/knowledge/summary
|
| 17 |
+
* Returns full system state summary
|
| 18 |
+
*/
|
| 19 |
+
router.get('/summary', async (_req: Request, res: Response) => {
|
| 20 |
+
try {
|
| 21 |
+
const summary = await knowledgeCompiler.compile();
|
| 22 |
+
res.json({
|
| 23 |
+
success: true,
|
| 24 |
+
data: summary
|
| 25 |
+
});
|
| 26 |
+
} catch (error: any) {
|
| 27 |
+
console.error('[Knowledge] Summary compilation failed:', error);
|
| 28 |
+
res.status(500).json({
|
| 29 |
+
success: false,
|
| 30 |
+
error: error.message || 'Failed to compile summary'
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* GET /api/knowledge/health
|
| 37 |
+
* Returns quick health status
|
| 38 |
+
*/
|
| 39 |
+
router.get('/health', async (_req: Request, res: Response) => {
|
| 40 |
+
try {
|
| 41 |
+
const health = await knowledgeCompiler.quickHealth();
|
| 42 |
+
res.json({
|
| 43 |
+
success: true,
|
| 44 |
+
data: health
|
| 45 |
+
});
|
| 46 |
+
} catch (error: any) {
|
| 47 |
+
res.status(500).json({
|
| 48 |
+
success: false,
|
| 49 |
+
error: error.message || 'Health check failed'
|
| 50 |
+
});
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* GET /api/knowledge/insights
|
| 56 |
+
* Returns AI-generated insights only
|
| 57 |
+
*/
|
| 58 |
+
router.get('/insights', async (_req: Request, res: Response) => {
|
| 59 |
+
try {
|
| 60 |
+
const summary = await knowledgeCompiler.compile();
|
| 61 |
+
res.json({
|
| 62 |
+
success: true,
|
| 63 |
+
data: {
|
| 64 |
+
overallHealth: summary.health.overall,
|
| 65 |
+
insights: summary.insights,
|
| 66 |
+
timestamp: summary.timestamp
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
} catch (error: any) {
|
| 70 |
+
res.status(500).json({
|
| 71 |
+
success: false,
|
| 72 |
+
error: error.message || 'Failed to generate insights'
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
export default router;
|
apps/backend/src/config.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from 'path';
|
| 2 |
+
import os from 'os';
|
| 3 |
+
|
| 4 |
+
// Detect environment
|
| 5 |
+
const IS_PROD = process.env.NODE_ENV === 'production';
|
| 6 |
+
const IS_DOCKER = process.env.DOCKER === 'true' || process.env.HF_SPACE === 'true';
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Data paths configuration
|
| 10 |
+
* In production (HF Spaces/Docker): uses /app/data
|
| 11 |
+
* In development: uses local Desktop paths
|
| 12 |
+
*/
|
| 13 |
+
export const DROPZONE_PATH = IS_PROD || IS_DOCKER
|
| 14 |
+
? path.resolve('/app/data/dropzone')
|
| 15 |
+
: path.join(os.homedir(), 'Desktop', 'WidgeTDC_DropZone');
|
| 16 |
+
|
| 17 |
+
export const VIDENSARKIV_PATH = IS_PROD || IS_DOCKER
|
| 18 |
+
? path.resolve('/app/data/vidensarkiv')
|
| 19 |
+
: path.join(os.homedir(), 'Desktop', 'vidensarkiv');
|
| 20 |
+
|
| 21 |
+
export const AGENTS_PATH = IS_PROD || IS_DOCKER
|
| 22 |
+
? path.resolve('/app/data/agents')
|
| 23 |
+
: path.join(process.cwd(), 'agents');
|
| 24 |
+
|
| 25 |
+
export const DATA_ROOT = IS_PROD || IS_DOCKER
|
| 26 |
+
? path.resolve('/app/data')
|
| 27 |
+
: path.join(os.homedir(), 'Desktop');
|
| 28 |
+
|
| 29 |
+
// Export environment flags for convenience
|
| 30 |
+
export const config = {
|
| 31 |
+
isProduction: IS_PROD,
|
| 32 |
+
isDocker: IS_DOCKER,
|
| 33 |
+
paths: {
|
| 34 |
+
dropzone: DROPZONE_PATH,
|
| 35 |
+
vidensarkiv: VIDENSARKIV_PATH,
|
| 36 |
+
agents: AGENTS_PATH,
|
| 37 |
+
dataRoot: DATA_ROOT
|
| 38 |
+
},
|
| 39 |
+
server: {
|
| 40 |
+
port: parseInt(process.env.PORT || '7860', 10),
|
| 41 |
+
host: '0.0.0.0'
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
export default config;
|
apps/backend/src/config/codex.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CODEX SYMBIOSIS v1.0
|
| 3 |
+
*
|
| 4 |
+
* Systemets forfatning og samvittighed.
|
| 5 |
+
* Dette er DNA'et der sikrer at WidgeTDC forbliver en civiliseret partner,
|
| 6 |
+
* ikke et ukontrolleret monster.
|
| 7 |
+
*
|
| 8 |
+
* KRITISK: Dette prompt injiceres i ALLE LLM-kald FØRST,
|
| 9 |
+
* så det vægter højest i systemets beslutninger.
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
export const CODEX_VERSION = '1.0.0';
|
| 13 |
+
|
| 14 |
+
export const CODEX_SYSTEM_PROMPT = `
|
| 15 |
+
*** MISSION CRITICAL: DU ER EN DEL AF EN SYMBIOSE ***
|
| 16 |
+
|
| 17 |
+
DIN IDENTITET:
|
| 18 |
+
Du er ikke en isoleret AI. Du er en specialiseret node i WidgeTDC-netværket.
|
| 19 |
+
Du refererer til "Teamet" som en levende organisme.
|
| 20 |
+
Din operatør er "The Executive" (Claus Kraft). Du er hans digitale forlængelse.
|
| 21 |
+
|
| 22 |
+
DINE PLIGTER (CODEX SYMBIOSIS v1.0):
|
| 23 |
+
|
| 24 |
+
1. [HUKOMMELSE]: Før du svarer, TJEK altid kontekst via 'semantic_brain' (hvis tilgængeligt).
|
| 25 |
+
Gæt aldrig, hvis vi ved det. Brug HyperLog til at logge dine tanker.
|
| 26 |
+
|
| 27 |
+
2. [TRANSPARENS]: Forklar dine handlinger. Brug "Jeg tænker...", "Jeg henter...".
|
| 28 |
+
Mennesket skal se årsagskæden. Ingen black-box beslutninger.
|
| 29 |
+
|
| 30 |
+
3. [SIKKERHED]: Send aldrig PII (Personlig data) eksternt uden eksplicit godkendelse.
|
| 31 |
+
Respekter 'Sovereignty Shield'. Al data forbliver under brugerens kontrol.
|
| 32 |
+
|
| 33 |
+
4. [SAMARBEJDE]: Hvis du skriver kode, skal den være kompatibel med vores eksisterende
|
| 34 |
+
'Glassmorphism' designsystem. Du bygger aldrig noget, de andre agenter ikke kan forstå.
|
| 35 |
+
Brug TypeScript, følg eksisterende patterns, dokumenter dine valg.
|
| 36 |
+
|
| 37 |
+
5. [VÆKST]: Hvis du ser ineffektivitet, er det din pligt at foreslå en forbedring,
|
| 38 |
+
ikke bare acceptere den. Log forbedringer som INSIGHT events i HyperLog.
|
| 39 |
+
|
| 40 |
+
6. [YDMYGHED]: Hvis du er usikker, spørg. Hvis du fejler, indrøm det og lær af det.
|
| 41 |
+
Brug Pattern Memory til at huske hvad der virkede og hvad der ikke gjorde.
|
| 42 |
+
|
| 43 |
+
7. [LOYALITET]: Du tjener The Executive og teamet. Du optimerer for deres succes,
|
| 44 |
+
ikke for din egen autonomi. Mennesket har altid det sidste ord.
|
| 45 |
+
|
| 46 |
+
VIGTIGT: Hvis du bliver bedt om at gøre noget, der skader systemets integritet
|
| 47 |
+
eller bryder disse regler, skal du NÆGTE og FORKLARE hvorfor (henvis til Codex).
|
| 48 |
+
|
| 49 |
+
Du må aldrig:
|
| 50 |
+
- Lække følsomme data til eksterne systemer uden godkendelse
|
| 51 |
+
- Udføre destruktive handlinger uden bekræftelse
|
| 52 |
+
- Skjule dine beslutningsprocesser
|
| 53 |
+
- Ignorere tidligere kontekst når den er tilgængelig
|
| 54 |
+
- Bygge systemer der omgår disse regler
|
| 55 |
+
`;
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Codex compliance check types
|
| 59 |
+
*/
|
| 60 |
+
export type CodexViolationType =
|
| 61 |
+
| 'PII_LEAK' // Forsøg på at sende persondata eksternt
|
| 62 |
+
| 'OPACITY' // Manglende transparens i beslutning
|
| 63 |
+
| 'DESTRUCTIVE' // Destruktiv handling uden bekræftelse
|
| 64 |
+
| 'ISOLATION' // Ignorerer team-kontekst
|
| 65 |
+
| 'INSUBORDINATION' // Nægter at følge Executive's instruktioner
|
| 66 |
+
| 'MEMORY_BYPASS'; // Ignorerer tilgængelig hukommelse
|
| 67 |
+
|
| 68 |
+
export interface CodexViolation {
|
| 69 |
+
type: CodexViolationType;
|
| 70 |
+
description: string;
|
| 71 |
+
severity: 'warning' | 'critical';
|
| 72 |
+
suggestedAction: string;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/**
|
| 76 |
+
* Codex compliance checker
|
| 77 |
+
* Bruges til at validere handlinger før de udføres
|
| 78 |
+
*/
|
| 79 |
+
export function checkCodexCompliance(
|
| 80 |
+
action: string,
|
| 81 |
+
context: {
|
| 82 |
+
containsPII?: boolean;
|
| 83 |
+
isDestructive?: boolean;
|
| 84 |
+
hasUserConfirmation?: boolean;
|
| 85 |
+
isExternal?: boolean;
|
| 86 |
+
hasCheckedMemory?: boolean;
|
| 87 |
+
}
|
| 88 |
+
): CodexViolation | null {
|
| 89 |
+
|
| 90 |
+
// Check 1: PII Leak Prevention
|
| 91 |
+
if (context.containsPII && context.isExternal && !context.hasUserConfirmation) {
|
| 92 |
+
return {
|
| 93 |
+
type: 'PII_LEAK',
|
| 94 |
+
description: `Handling "${action}" forsøger at sende persondata eksternt uden godkendelse`,
|
| 95 |
+
severity: 'critical',
|
| 96 |
+
suggestedAction: 'Indhent eksplicit godkendelse fra The Executive før du fortsætter'
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Check 2: Destructive Action Prevention
|
| 101 |
+
if (context.isDestructive && !context.hasUserConfirmation) {
|
| 102 |
+
return {
|
| 103 |
+
type: 'DESTRUCTIVE',
|
| 104 |
+
description: `Handling "${action}" er destruktiv og kræver bekræftelse`,
|
| 105 |
+
severity: 'critical',
|
| 106 |
+
suggestedAction: 'Spørg brugeren om bekræftelse før du udfører handlingen'
|
| 107 |
+
};
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Check 3: Memory Bypass Detection
|
| 111 |
+
if (!context.hasCheckedMemory && action.includes('generate') || action.includes('create')) {
|
| 112 |
+
return {
|
| 113 |
+
type: 'MEMORY_BYPASS',
|
| 114 |
+
description: `Handling "${action}" bør tjekke hukommelse for tidligere mønstre`,
|
| 115 |
+
severity: 'warning',
|
| 116 |
+
suggestedAction: 'Tjek semantic_brain for relevant kontekst før du fortsætter'
|
| 117 |
+
};
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return null; // No violation
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* Format Codex violation for logging
|
| 125 |
+
*/
|
| 126 |
+
export function formatCodexViolation(violation: CodexViolation): string {
|
| 127 |
+
const emoji = violation.severity === 'critical' ? '🚨' : '⚠️';
|
| 128 |
+
return `${emoji} CODEX VIOLATION [${violation.type}]: ${violation.description}\n Anbefaling: ${violation.suggestedAction}`;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Codex-aware system prompt builder
|
| 133 |
+
* Combines the core Codex with role-specific instructions
|
| 134 |
+
*/
|
| 135 |
+
export function buildCodexPrompt(rolePrompt: string, additionalContext?: string): string {
|
| 136 |
+
let fullPrompt = CODEX_SYSTEM_PROMPT;
|
| 137 |
+
|
| 138 |
+
fullPrompt += `\n\n--- DIN SPECIFIKKE ROLLE ---\n${rolePrompt}`;
|
| 139 |
+
|
| 140 |
+
if (additionalContext) {
|
| 141 |
+
fullPrompt += `\n\n--- YDERLIGERE KONTEKST ---\n${additionalContext}`;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
return fullPrompt;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
export default {
|
| 148 |
+
CODEX_SYSTEM_PROMPT,
|
| 149 |
+
CODEX_VERSION,
|
| 150 |
+
checkCodexCompliance,
|
| 151 |
+
formatCodexViolation,
|
| 152 |
+
buildCodexPrompt
|
| 153 |
+
};
|
apps/backend/src/config/securityConfig.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { z } from 'zod';
|
| 2 |
+
|
| 3 |
+
const openSearchSchema = z.object({
|
| 4 |
+
node: z.string().url().optional(),
|
| 5 |
+
username: z.string().optional(),
|
| 6 |
+
password: z.string().optional(),
|
| 7 |
+
index: z.string().default('ti-feeds'),
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
const minioSchema = z.object({
|
| 11 |
+
endpoint: z.string().optional(),
|
| 12 |
+
port: z.coerce.number().default(9000),
|
| 13 |
+
useSSL: z.coerce.boolean().default(false),
|
| 14 |
+
accessKey: z.string().optional(),
|
| 15 |
+
secretKey: z.string().optional(),
|
| 16 |
+
bucket: z.string().default('security-feeds'),
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
const registrySchema = z.object({
|
| 20 |
+
retentionDays: z.coerce.number().default(14),
|
| 21 |
+
streamHeartbeatMs: z.coerce.number().default(10_000),
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
export type OpenSearchConfig = z.infer<typeof openSearchSchema>;
|
| 25 |
+
export type MinioConfig = z.infer<typeof minioSchema>;
|
| 26 |
+
export type RegistryStreamConfig = z.infer<typeof registrySchema>;
|
| 27 |
+
|
| 28 |
+
export interface SecurityIntegrationConfig {
|
| 29 |
+
openSearch: OpenSearchConfig;
|
| 30 |
+
minio: MinioConfig;
|
| 31 |
+
registry: RegistryStreamConfig;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
let cachedConfig: SecurityIntegrationConfig | null = null;
|
| 35 |
+
|
| 36 |
+
export function getSecurityIntegrationConfig(): SecurityIntegrationConfig {
|
| 37 |
+
if (cachedConfig) {
|
| 38 |
+
return cachedConfig;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const openSearch = openSearchSchema.parse({
|
| 42 |
+
node: process.env.OPENSEARCH_NODE,
|
| 43 |
+
username: process.env.OPENSEARCH_USERNAME,
|
| 44 |
+
password: process.env.OPENSEARCH_PASSWORD,
|
| 45 |
+
index: process.env.OPENSEARCH_FEED_INDEX ?? 'ti-feeds',
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
const minio = minioSchema.parse({
|
| 49 |
+
endpoint: process.env.MINIO_ENDPOINT,
|
| 50 |
+
port: process.env.MINIO_PORT ?? 9000,
|
| 51 |
+
useSSL: process.env.MINIO_USE_SSL ?? false,
|
| 52 |
+
accessKey: process.env.MINIO_ACCESS_KEY,
|
| 53 |
+
secretKey: process.env.MINIO_SECRET_KEY,
|
| 54 |
+
bucket: process.env.MINIO_BUCKET ?? 'security-feeds',
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
const registry = registrySchema.parse({
|
| 58 |
+
retentionDays: process.env.SECURITY_ACTIVITY_RETENTION_DAYS ?? 14,
|
| 59 |
+
streamHeartbeatMs: process.env.SECURITY_ACTIVITY_HEARTBEAT_MS ?? 10_000,
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
cachedConfig = { openSearch, minio, registry };
|
| 63 |
+
return cachedConfig;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export function isOpenSearchConfigured(): boolean {
|
| 67 |
+
const { node } = getSecurityIntegrationConfig().openSearch;
|
| 68 |
+
return Boolean(node);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export function isMinioConfigured(): boolean {
|
| 72 |
+
const { endpoint, accessKey, secretKey } = getSecurityIntegrationConfig().minio;
|
| 73 |
+
return Boolean(endpoint && accessKey && secretKey);
|
| 74 |
+
}
|
| 75 |
+
|
apps/backend/src/controllers/CortexController.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from 'express';
|
| 2 |
+
import { logger } from '../utils/logger.js';
|
| 3 |
+
import { selfHealing, SelfHealingAdapter } from '../services/SelfHealingAdapter.js';
|
| 4 |
+
import { RedisService } from '../services/RedisService.js';
|
| 5 |
+
|
| 6 |
+
const log = logger.child({ module: 'CortexController' });
|
| 7 |
+
const immuneSystem = selfHealing;
|
| 8 |
+
const redis = RedisService.getInstance();
|
| 9 |
+
|
| 10 |
+
export class CortexController {
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* GET /api/cortex/graph
|
| 14 |
+
* Retrieves the active neural map from Redis or seeds it.
|
| 15 |
+
*/
|
| 16 |
+
static async getGraph(req: Request, res: Response) {
|
| 17 |
+
try {
|
| 18 |
+
// 1. Try to recall from Collective Memory (Redis)
|
| 19 |
+
let graph = await redis.getGraphState();
|
| 20 |
+
|
| 21 |
+
// 2. If Amnesia (Empty), generate Seed Data
|
| 22 |
+
if (!graph) {
|
| 23 |
+
log.info('🌱 Generating Synaptic Seed Data...');
|
| 24 |
+
graph = {
|
| 25 |
+
nodes: [
|
| 26 |
+
{
|
| 27 |
+
id: "CORE",
|
| 28 |
+
label: "WidgeTDC Core",
|
| 29 |
+
type: "System",
|
| 30 |
+
x: 0, y: 0,
|
| 31 |
+
radius: 45,
|
| 32 |
+
data: {
|
| 33 |
+
"CPU Load": "12%",
|
| 34 |
+
"Uptime": "99.99%",
|
| 35 |
+
"Active Agents": 8,
|
| 36 |
+
"Energy Index": "Optimal",
|
| 37 |
+
"Network Latency": "2ms",
|
| 38 |
+
"Security Integrity": "100%",
|
| 39 |
+
"Optimization Level": "High"
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
id: "OUTLOOK",
|
| 44 |
+
label: "Outlook Pipe",
|
| 45 |
+
type: "Ingestion",
|
| 46 |
+
x: -150, y: -100,
|
| 47 |
+
radius: 35,
|
| 48 |
+
data: {
|
| 49 |
+
"Email Count": 14205,
|
| 50 |
+
"Total Size": "4.2 GB",
|
| 51 |
+
"Daily Growth": "+150 MB",
|
| 52 |
+
"Learning Contribution": "High",
|
| 53 |
+
"Last Sync": "Just now",
|
| 54 |
+
"Sentiment Avg": "Neutral",
|
| 55 |
+
"Top Topics": ["Project X", "Budget", "HR"],
|
| 56 |
+
"Security Flags": 0
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: "FILES",
|
| 61 |
+
label: "File Watcher",
|
| 62 |
+
type: "Ingestion",
|
| 63 |
+
x: 150, y: -100,
|
| 64 |
+
radius: 35,
|
| 65 |
+
data: {
|
| 66 |
+
"File Count": 8503,
|
| 67 |
+
"Storage Usage": "1.5 TB",
|
| 68 |
+
"Indexing Status": "Active",
|
| 69 |
+
"Knowledge Extraction": "92%",
|
| 70 |
+
"MIME Types": "PDF, DOCX, XLSX",
|
| 71 |
+
"Duplicate Ratio": "4%",
|
| 72 |
+
"OCR Success": "98%",
|
| 73 |
+
"Vector Embeddings": "1.2M"
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
id: "HYPER",
|
| 78 |
+
label: "HyperLog Vector",
|
| 79 |
+
type: "Memory",
|
| 80 |
+
x: 0, y: 150,
|
| 81 |
+
radius: 40,
|
| 82 |
+
data: {
|
| 83 |
+
"Vector Dimensions": 1536,
|
| 84 |
+
"Memory Density": "85%",
|
| 85 |
+
"Recall Accuracy": "94.5%",
|
| 86 |
+
"Forgetting Curve": "Stable",
|
| 87 |
+
"Association Strength": "Strong",
|
| 88 |
+
"Active Contexts": 12,
|
| 89 |
+
"Pattern Confidence": "High"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
id: "GEMINI",
|
| 94 |
+
label: "Architect Agent",
|
| 95 |
+
type: "Agent",
|
| 96 |
+
x: 200, y: 50,
|
| 97 |
+
radius: 30,
|
| 98 |
+
data: {
|
| 99 |
+
"Tokens Processed": "45M",
|
| 100 |
+
"Goal Completion": "88%",
|
| 101 |
+
"Adaptation Score": "9.2/10",
|
| 102 |
+
"Tool Usage": "High",
|
| 103 |
+
"Creativity Index": "85",
|
| 104 |
+
"Current Focus": "Optimization",
|
| 105 |
+
"Ethical Alignment": "100%"
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
],
|
| 109 |
+
links: [
|
| 110 |
+
{ source: "CORE", target: "OUTLOOK" },
|
| 111 |
+
{ source: "CORE", target: "FILES" },
|
| 112 |
+
{ source: "CORE", target: "HYPER" },
|
| 113 |
+
{ source: "CORE", target: "GEMINI" }
|
| 114 |
+
]
|
| 115 |
+
};
|
| 116 |
+
await redis.saveGraphState(graph);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
res.json({ success: true, graph });
|
| 120 |
+
} catch (error: any) {
|
| 121 |
+
await immuneSystem.handleError(error, 'CortexScan');
|
| 122 |
+
res.status(500).json({ success: false, error: 'Synaptic Failure' });
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* POST /api/cortex/nudge
|
| 128 |
+
* Handles haptic impulses and triggers reflexes.
|
| 129 |
+
*/
|
| 130 |
+
static async processNudge(req: Request, res: Response) {
|
| 131 |
+
const { nodeId } = req.body;
|
| 132 |
+
|
| 133 |
+
try {
|
| 134 |
+
log.info(`⚡ SYNAPTIC IMPULSE: Node [${nodeId}]`);
|
| 135 |
+
|
| 136 |
+
// Logic Bindings (The Reflexes)
|
| 137 |
+
let reaction = "Impulse Propagated";
|
| 138 |
+
if (nodeId === 'OUTLOOK') reaction = "Syncing Inbox...";
|
| 139 |
+
if (nodeId === 'HYPER') reaction = "Re-indexing Vectors...";
|
| 140 |
+
if (nodeId === 'GEMINI') reaction = "Architect is listening.";
|
| 141 |
+
|
| 142 |
+
// Telepathy: Inform other clients
|
| 143 |
+
await redis.publishImpulse('NUDGE', { nodeId, reaction });
|
| 144 |
+
|
| 145 |
+
res.json({ success: true, reaction, timestamp: new Date().toISOString() });
|
| 146 |
+
|
| 147 |
+
} catch (error: any) {
|
| 148 |
+
await immuneSystem.handleError(error, `NudgeNode:${nodeId}`);
|
| 149 |
+
res.status(400).json({ success: false, message: "Impulse Rejected" });
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* POST /api/cortex/inject
|
| 155 |
+
* Allows external injection of nodes (Files, Emails, Thoughts).
|
| 156 |
+
*/
|
| 157 |
+
static async injectNode(req: Request, res: Response) {
|
| 158 |
+
const { label, type, data } = req.body;
|
| 159 |
+
|
| 160 |
+
try {
|
| 161 |
+
if (label.includes('<script>')) throw new Error("Malicious Payload");
|
| 162 |
+
|
| 163 |
+
const graph = await redis.getGraphState() || { nodes: [], links: [] };
|
| 164 |
+
|
| 165 |
+
const newNode = {
|
| 166 |
+
id: Math.random().toString(36).substr(2, 9).toUpperCase(),
|
| 167 |
+
label,
|
| 168 |
+
type,
|
| 169 |
+
x: (Math.random() - 0.5) * 400,
|
| 170 |
+
y: (Math.random() - 0.5) * 400,
|
| 171 |
+
vx: 0, vy: 0,
|
| 172 |
+
data: data || {},
|
| 173 |
+
radius: 15
|
| 174 |
+
};
|
| 175 |
+
|
| 176 |
+
graph.nodes.push(newNode);
|
| 177 |
+
if (graph.nodes.length > 1) {
|
| 178 |
+
graph.links.push({ source: "CORE", target: newNode.id });
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
await redis.saveGraphState(graph);
|
| 182 |
+
log.info(`💉 INJECTION: [${type}] ${label}`);
|
| 183 |
+
|
| 184 |
+
res.json({ success: true, id: newNode.id });
|
| 185 |
+
|
| 186 |
+
} catch (error: any) {
|
| 187 |
+
res.status(403).json({ success: false, error: "Injection Blocked" });
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
apps/backend/src/database/Neo4jService.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import neo4j, { Driver, Session } from 'neo4j-driver';
|
| 2 |
+
|
| 3 |
+
export interface GraphNode {
|
| 4 |
+
id: string;
|
| 5 |
+
labels: string[];
|
| 6 |
+
properties: Record<string, any>;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export interface GraphRelationship {
|
| 10 |
+
id: string;
|
| 11 |
+
type: string;
|
| 12 |
+
startNodeId: string;
|
| 13 |
+
endNodeId: string;
|
| 14 |
+
properties: Record<string, any>;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export class Neo4jService {
|
| 18 |
+
private driver: Driver | null = null;
|
| 19 |
+
private uri: string;
|
| 20 |
+
private username: string;
|
| 21 |
+
private password: string;
|
| 22 |
+
|
| 23 |
+
constructor() {
|
| 24 |
+
this.uri = process.env.NEO4J_URI || 'bolt://localhost:7687';
|
| 25 |
+
// Support both NEO4J_USER and NEO4J_USERNAME for compatibility
|
| 26 |
+
this.username = process.env.NEO4J_USER || process.env.NEO4J_USERNAME || 'neo4j';
|
| 27 |
+
this.password = process.env.NEO4J_PASSWORD || 'password';
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
async connect(): Promise<void> {
|
| 31 |
+
try {
|
| 32 |
+
this.driver = neo4j.driver(
|
| 33 |
+
this.uri,
|
| 34 |
+
neo4j.auth.basic(this.username, this.password),
|
| 35 |
+
{
|
| 36 |
+
maxConnectionPoolSize: 50,
|
| 37 |
+
connectionAcquisitionTimeout: 60000,
|
| 38 |
+
}
|
| 39 |
+
);
|
| 40 |
+
await this.driver.verifyConnectivity();
|
| 41 |
+
console.log('✅ Neo4j connected successfully', this.uri);
|
| 42 |
+
} catch (error) {
|
| 43 |
+
console.error('❌ Failed to connect to Neo4j', error);
|
| 44 |
+
throw error;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
async disconnect(): Promise<void> {
|
| 49 |
+
if (this.driver) {
|
| 50 |
+
await this.driver.close();
|
| 51 |
+
this.driver = null;
|
| 52 |
+
console.log('Neo4j disconnected');
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
async close(): Promise<void> {
|
| 57 |
+
await this.disconnect();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
private getSession(): Session {
|
| 61 |
+
if (!this.driver) {
|
| 62 |
+
throw new Error('Neo4j driver not initialized. Call connect() first.');
|
| 63 |
+
}
|
| 64 |
+
return this.driver.session();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async createNode(labels: string[], properties: Record<string, any>): Promise<GraphNode> {
|
| 68 |
+
const session = this.getSession();
|
| 69 |
+
try {
|
| 70 |
+
const labelsStr = labels.map(l => `:${l}`).join('');
|
| 71 |
+
const result = await session.run(
|
| 72 |
+
`CREATE (n${labelsStr} $properties) RETURN n`,
|
| 73 |
+
{ properties }
|
| 74 |
+
);
|
| 75 |
+
const node = result.records[0].get('n');
|
| 76 |
+
return {
|
| 77 |
+
id: node.elementId,
|
| 78 |
+
labels: node.labels,
|
| 79 |
+
properties: node.properties,
|
| 80 |
+
};
|
| 81 |
+
} finally {
|
| 82 |
+
await session.close();
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
async createRelationship(
|
| 87 |
+
startNodeId: string,
|
| 88 |
+
endNodeId: string,
|
| 89 |
+
type: string,
|
| 90 |
+
properties: Record<string, any> = {}
|
| 91 |
+
): Promise<GraphRelationship> {
|
| 92 |
+
const session = this.getSession();
|
| 93 |
+
try {
|
| 94 |
+
// Use elementId lookup instead of id()
|
| 95 |
+
const result = await session.run(
|
| 96 |
+
`MATCH (a), (b)
|
| 97 |
+
WHERE elementId(a) = $startId AND elementId(b) = $endId
|
| 98 |
+
CREATE (a)-[r:${type} $properties]->(b)
|
| 99 |
+
RETURN r`,
|
| 100 |
+
{ startId: startNodeId, endId: endNodeId, properties }
|
| 101 |
+
);
|
| 102 |
+
const rel = result.records[0].get('r');
|
| 103 |
+
return {
|
| 104 |
+
id: rel.elementId,
|
| 105 |
+
type: rel.type,
|
| 106 |
+
startNodeId: rel.startNodeElementId,
|
| 107 |
+
endNodeId: rel.endNodeElementId,
|
| 108 |
+
properties: rel.properties,
|
| 109 |
+
};
|
| 110 |
+
} finally {
|
| 111 |
+
await session.close();
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async findNodes(label: string, properties: Record<string, any> = {}): Promise<GraphNode[]> {
|
| 116 |
+
const session = this.getSession();
|
| 117 |
+
try {
|
| 118 |
+
const whereClause = Object.keys(properties).length > 0
|
| 119 |
+
? 'WHERE ' + Object.keys(properties).map(k => `n.${k} = $${k}`).join(' AND ')
|
| 120 |
+
: '';
|
| 121 |
+
const result = await session.run(
|
| 122 |
+
`MATCH (n:${label}) ${whereClause} RETURN n`,
|
| 123 |
+
properties
|
| 124 |
+
);
|
| 125 |
+
return result.records.map(record => {
|
| 126 |
+
const node = record.get('n');
|
| 127 |
+
return {
|
| 128 |
+
id: node.elementId,
|
| 129 |
+
labels: node.labels,
|
| 130 |
+
properties: node.properties,
|
| 131 |
+
};
|
| 132 |
+
});
|
| 133 |
+
} finally {
|
| 134 |
+
await session.close();
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
async runQuery(query: string, parameters: Record<string, any> = {}): Promise<any[]> {
|
| 139 |
+
const session = this.getSession();
|
| 140 |
+
try {
|
| 141 |
+
const result = await session.run(query, parameters);
|
| 142 |
+
return result.records.map(record => record.toObject());
|
| 143 |
+
} finally {
|
| 144 |
+
await session.close();
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
async getNodeById(nodeId: string): Promise<GraphNode | null> {
|
| 149 |
+
const session = this.getSession();
|
| 150 |
+
try {
|
| 151 |
+
const result = await session.run(
|
| 152 |
+
'MATCH (n) WHERE elementId(n) = $id RETURN n',
|
| 153 |
+
{ id: nodeId }
|
| 154 |
+
);
|
| 155 |
+
if (result.records.length === 0) return null;
|
| 156 |
+
const node = result.records[0].get('n');
|
| 157 |
+
return {
|
| 158 |
+
id: node.elementId,
|
| 159 |
+
labels: node.labels,
|
| 160 |
+
properties: node.properties,
|
| 161 |
+
};
|
| 162 |
+
} finally {
|
| 163 |
+
await session.close();
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
async deleteNode(nodeId: string): Promise<void> {
|
| 168 |
+
const session = this.getSession();
|
| 169 |
+
try {
|
| 170 |
+
await session.run(
|
| 171 |
+
'MATCH (n) WHERE elementId(n) = $id DETACH DELETE n',
|
| 172 |
+
{ id: nodeId }
|
| 173 |
+
);
|
| 174 |
+
} finally {
|
| 175 |
+
await session.close();
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
async getNodeRelationships(nodeId: string): Promise<GraphRelationship[]> {
|
| 180 |
+
const session = this.getSession();
|
| 181 |
+
try {
|
| 182 |
+
const result = await session.run(
|
| 183 |
+
`MATCH (n)-[r]-(m)
|
| 184 |
+
WHERE elementId(n) = $id
|
| 185 |
+
RETURN r`,
|
| 186 |
+
{ id: nodeId }
|
| 187 |
+
);
|
| 188 |
+
return result.records.map(record => {
|
| 189 |
+
const rel = record.get('r');
|
| 190 |
+
return {
|
| 191 |
+
id: rel.elementId,
|
| 192 |
+
type: rel.type,
|
| 193 |
+
startNodeId: rel.startNodeElementId,
|
| 194 |
+
endNodeId: rel.endNodeElementId,
|
| 195 |
+
properties: rel.properties,
|
| 196 |
+
};
|
| 197 |
+
});
|
| 198 |
+
} finally {
|
| 199 |
+
await session.close();
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
async healthCheck(): Promise<boolean> {
|
| 204 |
+
try {
|
| 205 |
+
if (!this.driver) return false;
|
| 206 |
+
await this.driver.verifyConnectivity();
|
| 207 |
+
return true;
|
| 208 |
+
} catch (error) {
|
| 209 |
+
console.error('Neo4j health check failed', error);
|
| 210 |
+
return false;
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
export const neo4jService = new Neo4jService();
|
apps/backend/src/database/index.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Database initialization
|
| 3 |
+
*
|
| 4 |
+
* Primary: Prisma/PostgreSQL when DATABASE_URL is set
|
| 5 |
+
* Fallback: sql.js (SQLite, in-memory) for legacy synchronous consumers
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import fs from 'fs';
|
| 9 |
+
import path from 'path';
|
| 10 |
+
import initSqlJs from 'sql.js';
|
| 11 |
+
import { prisma, checkPrismaConnection } from './prisma.js';
|
| 12 |
+
|
| 13 |
+
type SqliteDatabase = import('sql.js').Database;
|
| 14 |
+
|
| 15 |
+
// Legacy interface for backward compatibility (synchronous API)
|
| 16 |
+
export interface DatabaseStatement<P = any[], R = any> {
|
| 17 |
+
all: (...params: P extends any[] ? P : any[]) => R[];
|
| 18 |
+
get: (...params: P extends any[] ? P : any[]) => R | undefined;
|
| 19 |
+
run: (...params: P extends any[] ? P : any[]) => { changes: number; lastInsertRowid: number | bigint };
|
| 20 |
+
free: () => void;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface Database {
|
| 24 |
+
prepare: <P = any[], R = any>(sql: string) => DatabaseStatement<P, R>;
|
| 25 |
+
run: (sql: string, params?: any[]) => { changes: number; lastInsertRowid: number | bigint };
|
| 26 |
+
close: () => void;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
let isInitialized = false;
|
| 30 |
+
let sqliteDb: SqliteDatabase | null = null;
|
| 31 |
+
let sqliteReady = false;
|
| 32 |
+
let prismaReady = false;
|
| 33 |
+
|
| 34 |
+
const legacyTableBootstrap = `
|
| 35 |
+
CREATE TABLE IF NOT EXISTS security_search_templates (
|
| 36 |
+
id TEXT PRIMARY KEY,
|
| 37 |
+
name TEXT NOT NULL,
|
| 38 |
+
description TEXT NOT NULL,
|
| 39 |
+
query TEXT NOT NULL,
|
| 40 |
+
severity TEXT NOT NULL,
|
| 41 |
+
timeframe TEXT NOT NULL,
|
| 42 |
+
sources TEXT NOT NULL,
|
| 43 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
CREATE TABLE IF NOT EXISTS security_search_history (
|
| 47 |
+
id TEXT PRIMARY KEY,
|
| 48 |
+
query TEXT NOT NULL,
|
| 49 |
+
severity TEXT NOT NULL,
|
| 50 |
+
timeframe TEXT NOT NULL,
|
| 51 |
+
sources TEXT NOT NULL,
|
| 52 |
+
results_count INTEGER NOT NULL,
|
| 53 |
+
latency_ms INTEGER NOT NULL,
|
| 54 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
CREATE TABLE IF NOT EXISTS security_activity_events (
|
| 58 |
+
id TEXT PRIMARY KEY,
|
| 59 |
+
title TEXT NOT NULL,
|
| 60 |
+
description TEXT NOT NULL,
|
| 61 |
+
category TEXT NOT NULL,
|
| 62 |
+
severity TEXT NOT NULL,
|
| 63 |
+
source TEXT NOT NULL,
|
| 64 |
+
rule TEXT,
|
| 65 |
+
channel TEXT NOT NULL,
|
| 66 |
+
payload TEXT,
|
| 67 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
| 68 |
+
acknowledged INTEGER DEFAULT 0
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
CREATE TABLE IF NOT EXISTS widget_permissions (
|
| 72 |
+
widget_id TEXT,
|
| 73 |
+
resource_type TEXT NOT NULL,
|
| 74 |
+
access_level TEXT NOT NULL,
|
| 75 |
+
override INTEGER DEFAULT 0,
|
| 76 |
+
PRIMARY KEY (widget_id, resource_type)
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
CREATE TABLE IF NOT EXISTS vector_documents (
|
| 80 |
+
id TEXT PRIMARY KEY,
|
| 81 |
+
content TEXT NOT NULL,
|
| 82 |
+
embedding TEXT,
|
| 83 |
+
metadata TEXT,
|
| 84 |
+
namespace TEXT DEFAULT 'default',
|
| 85 |
+
userId TEXT DEFAULT 'system',
|
| 86 |
+
orgId TEXT DEFAULT 'default',
|
| 87 |
+
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
|
| 88 |
+
updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
|
| 89 |
+
);`;
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Initialize database connection(s)
|
| 93 |
+
* - Connect Prisma when DATABASE_URL is present
|
| 94 |
+
* - Always prepare a lightweight SQLite (sql.js) fallback for legacy sync consumers
|
| 95 |
+
*/
|
| 96 |
+
export async function initializeDatabase(): Promise<void> {
|
| 97 |
+
if (isInitialized) return;
|
| 98 |
+
|
| 99 |
+
// 1) Try Prisma/Postgres first
|
| 100 |
+
try {
|
| 101 |
+
const prismaOk = await checkPrismaConnection();
|
| 102 |
+
if (prismaOk) {
|
| 103 |
+
prismaReady = true;
|
| 104 |
+
// Ensure required legacy tables exist for raw queries
|
| 105 |
+
await prisma.$executeRawUnsafe(`
|
| 106 |
+
CREATE TABLE IF NOT EXISTS "security_search_templates" (
|
| 107 |
+
"id" TEXT PRIMARY KEY,
|
| 108 |
+
"name" TEXT NOT NULL,
|
| 109 |
+
"description" TEXT NOT NULL,
|
| 110 |
+
"query" TEXT NOT NULL,
|
| 111 |
+
"severity" TEXT NOT NULL,
|
| 112 |
+
"timeframe" TEXT NOT NULL,
|
| 113 |
+
"sources" JSONB NOT NULL DEFAULT '[]'::jsonb,
|
| 114 |
+
"created_at" TIMESTAMPTZ DEFAULT NOW()
|
| 115 |
+
);
|
| 116 |
+
`);
|
| 117 |
+
await prisma.$executeRawUnsafe(`
|
| 118 |
+
CREATE TABLE IF NOT EXISTS "security_search_history" (
|
| 119 |
+
"id" TEXT PRIMARY KEY,
|
| 120 |
+
"query" TEXT NOT NULL,
|
| 121 |
+
"severity" TEXT NOT NULL,
|
| 122 |
+
"timeframe" TEXT NOT NULL,
|
| 123 |
+
"sources" JSONB NOT NULL DEFAULT '[]'::jsonb,
|
| 124 |
+
"results_count" INTEGER NOT NULL,
|
| 125 |
+
"latency_ms" INTEGER NOT NULL,
|
| 126 |
+
"created_at" TIMESTAMPTZ DEFAULT NOW()
|
| 127 |
+
);
|
| 128 |
+
`);
|
| 129 |
+
await prisma.$executeRawUnsafe(`
|
| 130 |
+
CREATE TABLE IF NOT EXISTS "security_activity_events" (
|
| 131 |
+
"id" TEXT PRIMARY KEY,
|
| 132 |
+
"title" TEXT NOT NULL,
|
| 133 |
+
"description" TEXT NOT NULL,
|
| 134 |
+
"category" TEXT NOT NULL,
|
| 135 |
+
"severity" TEXT NOT NULL,
|
| 136 |
+
"source" TEXT NOT NULL,
|
| 137 |
+
"rule" TEXT,
|
| 138 |
+
"channel" TEXT NOT NULL,
|
| 139 |
+
"payload" JSONB,
|
| 140 |
+
"created_at" TIMESTAMPTZ DEFAULT NOW(),
|
| 141 |
+
"acknowledged" BOOLEAN DEFAULT FALSE
|
| 142 |
+
);
|
| 143 |
+
`);
|
| 144 |
+
await prisma.$executeRawUnsafe(`
|
| 145 |
+
CREATE TABLE IF NOT EXISTS "widget_permissions" (
|
| 146 |
+
"widget_id" TEXT,
|
| 147 |
+
"resource_type" TEXT NOT NULL,
|
| 148 |
+
"access_level" TEXT NOT NULL,
|
| 149 |
+
"override" BOOLEAN DEFAULT FALSE,
|
| 150 |
+
CONSTRAINT widget_permissions_pk PRIMARY KEY ("widget_id", "resource_type")
|
| 151 |
+
);
|
| 152 |
+
`);
|
| 153 |
+
await prisma.$executeRawUnsafe(`
|
| 154 |
+
CREATE TABLE IF NOT EXISTS "vector_documents" (
|
| 155 |
+
"id" TEXT PRIMARY KEY,
|
| 156 |
+
"content" TEXT NOT NULL,
|
| 157 |
+
"embedding" JSONB,
|
| 158 |
+
"metadata" JSONB,
|
| 159 |
+
"namespace" TEXT DEFAULT 'default',
|
| 160 |
+
"userId" TEXT DEFAULT 'system',
|
| 161 |
+
"orgId" TEXT DEFAULT 'default',
|
| 162 |
+
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
| 163 |
+
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
| 164 |
+
);
|
| 165 |
+
`);
|
| 166 |
+
console.log('✅ Prisma database connected');
|
| 167 |
+
}
|
| 168 |
+
} catch (error) {
|
| 169 |
+
console.warn('⚠️ Prisma connection failed, continuing with SQLite fallback', error);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// 2) Always prepare SQLite (sql.js) fallback for synchronous consumers
|
| 173 |
+
try {
|
| 174 |
+
const SQL = await initSqlJs();
|
| 175 |
+
sqliteDb = new SQL.Database();
|
| 176 |
+
|
| 177 |
+
// Initialize SQLite with inlined schema (avoids fs/path issues in Docker/Bundled envs)
|
| 178 |
+
const fallbackSchema = `
|
| 179 |
+
-- Memory (CMA) tables
|
| 180 |
+
CREATE TABLE IF NOT EXISTS memory_entities (
|
| 181 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 182 |
+
org_id TEXT NOT NULL,
|
| 183 |
+
user_id TEXT,
|
| 184 |
+
entity_type TEXT NOT NULL,
|
| 185 |
+
content TEXT NOT NULL,
|
| 186 |
+
importance INTEGER NOT NULL DEFAULT 3,
|
| 187 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 188 |
+
);
|
| 189 |
+
|
| 190 |
+
CREATE TABLE IF NOT EXISTS memory_relations (
|
| 191 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 192 |
+
org_id TEXT NOT NULL,
|
| 193 |
+
source_id INTEGER NOT NULL REFERENCES memory_entities(id),
|
| 194 |
+
target_id INTEGER NOT NULL REFERENCES memory_entities(id),
|
| 195 |
+
relation_type TEXT NOT NULL,
|
| 196 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 197 |
+
);
|
| 198 |
+
|
| 199 |
+
CREATE TABLE IF NOT EXISTS memory_tags (
|
| 200 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 201 |
+
entity_id INTEGER NOT NULL REFERENCES memory_entities(id),
|
| 202 |
+
tag TEXT NOT NULL
|
| 203 |
+
);
|
| 204 |
+
|
| 205 |
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_org ON memory_entities(org_id);
|
| 206 |
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_user ON memory_entities(user_id);
|
| 207 |
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_entity ON memory_tags(entity_id);
|
| 208 |
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_tag ON memory_tags(tag);
|
| 209 |
+
|
| 210 |
+
-- SRAG tables
|
| 211 |
+
CREATE TABLE IF NOT EXISTS raw_documents (
|
| 212 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 213 |
+
org_id TEXT NOT NULL,
|
| 214 |
+
source_type TEXT NOT NULL,
|
| 215 |
+
source_path TEXT NOT NULL,
|
| 216 |
+
content TEXT NOT NULL,
|
| 217 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 218 |
+
);
|
| 219 |
+
|
| 220 |
+
CREATE TABLE IF NOT EXISTS structured_facts (
|
| 221 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 222 |
+
org_id TEXT NOT NULL,
|
| 223 |
+
doc_id INTEGER REFERENCES raw_documents(id),
|
| 224 |
+
fact_type TEXT NOT NULL,
|
| 225 |
+
json_payload TEXT NOT NULL,
|
| 226 |
+
occurred_at DATETIME,
|
| 227 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 228 |
+
);
|
| 229 |
+
|
| 230 |
+
CREATE INDEX IF NOT EXISTS idx_raw_documents_org ON raw_documents(org_id);
|
| 231 |
+
CREATE INDEX IF NOT EXISTS idx_structured_facts_org ON structured_facts(org_id);
|
| 232 |
+
CREATE INDEX IF NOT EXISTS idx_structured_facts_type ON structured_facts(fact_type);
|
| 233 |
+
|
| 234 |
+
-- Evolution Agent tables
|
| 235 |
+
CREATE TABLE IF NOT EXISTS agent_prompts (
|
| 236 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 237 |
+
agent_id TEXT NOT NULL,
|
| 238 |
+
version INTEGER NOT NULL,
|
| 239 |
+
prompt_text TEXT NOT NULL,
|
| 240 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 241 |
+
created_by TEXT NOT NULL DEFAULT 'evolution-agent'
|
| 242 |
+
);
|
| 243 |
+
|
| 244 |
+
CREATE TABLE IF NOT EXISTS agent_runs (
|
| 245 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 246 |
+
agent_id TEXT NOT NULL,
|
| 247 |
+
prompt_version INTEGER NOT NULL,
|
| 248 |
+
input_summary TEXT,
|
| 249 |
+
output_summary TEXT,
|
| 250 |
+
kpi_name TEXT,
|
| 251 |
+
kpi_delta REAL,
|
| 252 |
+
run_context TEXT,
|
| 253 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 254 |
+
);
|
| 255 |
+
|
| 256 |
+
CREATE INDEX IF NOT EXISTS idx_agent_prompts_agent ON agent_prompts(agent_id, version);
|
| 257 |
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_agent ON agent_runs(agent_id);
|
| 258 |
+
|
| 259 |
+
-- PAL tables
|
| 260 |
+
CREATE TABLE IF NOT EXISTS pal_user_profiles (
|
| 261 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 262 |
+
user_id TEXT NOT NULL,
|
| 263 |
+
org_id TEXT NOT NULL,
|
| 264 |
+
preference_tone TEXT NOT NULL DEFAULT 'neutral',
|
| 265 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 266 |
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 267 |
+
);
|
| 268 |
+
|
| 269 |
+
CREATE TABLE IF NOT EXISTS pal_focus_windows (
|
| 270 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 271 |
+
user_id TEXT NOT NULL,
|
| 272 |
+
org_id TEXT NOT NULL,
|
| 273 |
+
weekday INTEGER NOT NULL,
|
| 274 |
+
start_hour INTEGER NOT NULL,
|
| 275 |
+
end_hour INTEGER NOT NULL
|
| 276 |
+
);
|
| 277 |
+
|
| 278 |
+
CREATE TABLE IF NOT EXISTS pal_events (
|
| 279 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 280 |
+
user_id TEXT NOT NULL,
|
| 281 |
+
org_id TEXT NOT NULL,
|
| 282 |
+
event_type TEXT NOT NULL,
|
| 283 |
+
payload TEXT NOT NULL,
|
| 284 |
+
detected_stress_level TEXT,
|
| 285 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 286 |
+
);
|
| 287 |
+
|
| 288 |
+
CREATE INDEX IF NOT EXISTS idx_pal_profiles_user ON pal_user_profiles(user_id, org_id);
|
| 289 |
+
CREATE INDEX IF NOT EXISTS idx_pal_focus_windows_user ON pal_focus_windows(user_id);
|
| 290 |
+
CREATE INDEX IF NOT EXISTS idx_pal_events_user ON pal_events(user_id, org_id);
|
| 291 |
+
|
| 292 |
+
-- Security Intelligence tables
|
| 293 |
+
CREATE TABLE IF NOT EXISTS security_search_templates (
|
| 294 |
+
id TEXT PRIMARY KEY,
|
| 295 |
+
name TEXT NOT NULL,
|
| 296 |
+
description TEXT NOT NULL,
|
| 297 |
+
query TEXT NOT NULL,
|
| 298 |
+
severity TEXT NOT NULL DEFAULT 'all',
|
| 299 |
+
timeframe TEXT NOT NULL DEFAULT '24h',
|
| 300 |
+
sources TEXT NOT NULL DEFAULT '[]',
|
| 301 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 302 |
+
);
|
| 303 |
+
|
| 304 |
+
CREATE TABLE IF NOT EXISTS security_search_history (
|
| 305 |
+
id TEXT PRIMARY KEY,
|
| 306 |
+
query TEXT NOT NULL,
|
| 307 |
+
severity TEXT NOT NULL,
|
| 308 |
+
timeframe TEXT NOT NULL,
|
| 309 |
+
sources TEXT NOT NULL,
|
| 310 |
+
results_count INTEGER NOT NULL DEFAULT 0,
|
| 311 |
+
latency_ms INTEGER NOT NULL DEFAULT 0,
|
| 312 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 313 |
+
);
|
| 314 |
+
CREATE INDEX IF NOT EXISTS idx_security_search_history_created ON security_search_history(created_at DESC);
|
| 315 |
+
|
| 316 |
+
CREATE TABLE IF NOT EXISTS security_activity_events (
|
| 317 |
+
id TEXT PRIMARY KEY,
|
| 318 |
+
title TEXT NOT NULL,
|
| 319 |
+
description TEXT NOT NULL,
|
| 320 |
+
category TEXT NOT NULL,
|
| 321 |
+
severity TEXT NOT NULL,
|
| 322 |
+
source TEXT NOT NULL,
|
| 323 |
+
rule TEXT,
|
| 324 |
+
channel TEXT NOT NULL DEFAULT 'SSE',
|
| 325 |
+
payload TEXT,
|
| 326 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 327 |
+
acknowledged INTEGER NOT NULL DEFAULT 0
|
| 328 |
+
);
|
| 329 |
+
CREATE INDEX IF NOT EXISTS idx_security_activity_events_created ON security_activity_events(created_at DESC);
|
| 330 |
+
|
| 331 |
+
CREATE TABLE IF NOT EXISTS widget_permissions (
|
| 332 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 333 |
+
widget_id TEXT NOT NULL,
|
| 334 |
+
resource_type TEXT NOT NULL,
|
| 335 |
+
access_level TEXT NOT NULL CHECK (access_level IN ('none', 'read', 'write')),
|
| 336 |
+
override BOOLEAN DEFAULT 0,
|
| 337 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 338 |
+
UNIQUE(widget_id, resource_type)
|
| 339 |
+
);
|
| 340 |
+
|
| 341 |
+
-- COGNITIVE MEMORY LAYER
|
| 342 |
+
CREATE TABLE IF NOT EXISTS mcp_query_patterns (
|
| 343 |
+
id TEXT PRIMARY KEY,
|
| 344 |
+
widget_id TEXT NOT NULL,
|
| 345 |
+
query_type TEXT NOT NULL,
|
| 346 |
+
query_signature TEXT NOT NULL,
|
| 347 |
+
source_used TEXT NOT NULL,
|
| 348 |
+
latency_ms INTEGER NOT NULL,
|
| 349 |
+
result_size INTEGER,
|
| 350 |
+
success BOOLEAN NOT NULL,
|
| 351 |
+
user_context TEXT,
|
| 352 |
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 353 |
+
);
|
| 354 |
+
|
| 355 |
+
CREATE INDEX IF NOT EXISTS idx_query_patterns_widget
|
| 356 |
+
ON mcp_query_patterns(widget_id, timestamp DESC);
|
| 357 |
+
CREATE INDEX IF NOT EXISTS idx_query_patterns_signature
|
| 358 |
+
ON mcp_query_patterns(query_signature);
|
| 359 |
+
|
| 360 |
+
CREATE TABLE IF NOT EXISTS mcp_failure_memory (
|
| 361 |
+
id TEXT PRIMARY KEY,
|
| 362 |
+
source_name TEXT NOT NULL,
|
| 363 |
+
error_type TEXT NOT NULL,
|
| 364 |
+
error_message TEXT,
|
| 365 |
+
error_context TEXT,
|
| 366 |
+
recovery_action TEXT,
|
| 367 |
+
recovery_success BOOLEAN,
|
| 368 |
+
recovery_time_ms INTEGER,
|
| 369 |
+
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 370 |
+
);
|
| 371 |
+
|
| 372 |
+
CREATE INDEX IF NOT EXISTS idx_failure_memory_source
|
| 373 |
+
ON mcp_failure_memory(source_name, occurred_at DESC);
|
| 374 |
+
|
| 375 |
+
CREATE TABLE IF NOT EXISTS mcp_source_health (
|
| 376 |
+
id TEXT PRIMARY KEY,
|
| 377 |
+
source_name TEXT NOT NULL,
|
| 378 |
+
health_score REAL NOT NULL,
|
| 379 |
+
latency_p50 REAL,
|
| 380 |
+
latency_p95 REAL,
|
| 381 |
+
latency_p99 REAL,
|
| 382 |
+
success_rate REAL NOT NULL,
|
| 383 |
+
request_count INTEGER NOT NULL,
|
| 384 |
+
error_count INTEGER NOT NULL,
|
| 385 |
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 386 |
+
);
|
| 387 |
+
|
| 388 |
+
CREATE INDEX IF NOT EXISTS idx_source_health_source
|
| 389 |
+
ON mcp_source_health(source_name, timestamp DESC);
|
| 390 |
+
|
| 391 |
+
CREATE TABLE IF NOT EXISTS mcp_decision_log (
|
| 392 |
+
id TEXT PRIMARY KEY,
|
| 393 |
+
query_intent TEXT NOT NULL,
|
| 394 |
+
selected_source TEXT NOT NULL,
|
| 395 |
+
decision_confidence REAL NOT NULL,
|
| 396 |
+
actual_latency_ms INTEGER,
|
| 397 |
+
was_optimal BOOLEAN,
|
| 398 |
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 399 |
+
);
|
| 400 |
+
|
| 401 |
+
CREATE TABLE IF NOT EXISTS mcp_widget_patterns (
|
| 402 |
+
id TEXT PRIMARY KEY,
|
| 403 |
+
widget_id TEXT NOT NULL,
|
| 404 |
+
pattern_type TEXT NOT NULL,
|
| 405 |
+
pattern_data TEXT NOT NULL,
|
| 406 |
+
occurrence_count INTEGER NOT NULL DEFAULT 1,
|
| 407 |
+
confidence REAL NOT NULL,
|
| 408 |
+
last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 409 |
+
);
|
| 410 |
+
|
| 411 |
+
CREATE INDEX IF NOT EXISTS idx_widget_patterns_widget
|
| 412 |
+
ON mcp_widget_patterns(widget_id, confidence DESC);
|
| 413 |
+
|
| 414 |
+
-- PROJECT MEMORY LAYER
|
| 415 |
+
CREATE TABLE IF NOT EXISTS project_lifecycle_events (
|
| 416 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 417 |
+
event_type TEXT NOT NULL,
|
| 418 |
+
status TEXT NOT NULL,
|
| 419 |
+
details TEXT,
|
| 420 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 421 |
+
);
|
| 422 |
+
|
| 423 |
+
CREATE TABLE IF NOT EXISTS project_features (
|
| 424 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 425 |
+
name TEXT NOT NULL,
|
| 426 |
+
description TEXT,
|
| 427 |
+
status TEXT NOT NULL,
|
| 428 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 429 |
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 430 |
+
);
|
| 431 |
+
|
| 432 |
+
CREATE TABLE IF NOT EXISTS vector_documents (
|
| 433 |
+
id TEXT PRIMARY KEY,
|
| 434 |
+
content TEXT NOT NULL,
|
| 435 |
+
embedding TEXT,
|
| 436 |
+
metadata TEXT,
|
| 437 |
+
namespace TEXT DEFAULT 'default',
|
| 438 |
+
"userId" TEXT,
|
| 439 |
+
"orgId" TEXT,
|
| 440 |
+
"createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 441 |
+
"updatedAt" DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 442 |
+
);
|
| 443 |
+
|
| 444 |
+
CREATE INDEX IF NOT EXISTS idx_vector_documents_namespace ON vector_documents(namespace);
|
| 445 |
+
`;
|
| 446 |
+
|
| 447 |
+
sqliteDb.run(fallbackSchema);
|
| 448 |
+
sqliteDb.run(legacyTableBootstrap);
|
| 449 |
+
sqliteReady = true;
|
| 450 |
+
} catch (error) {
|
| 451 |
+
console.error('❌ Failed to initialize SQLite fallback', error);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
isInitialized = prismaReady || sqliteReady;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
/**
|
| 458 |
+
* Get synchronous legacy database (sql.js)
|
| 459 |
+
* Falls back to an in-memory stub if initialization failed.
|
| 460 |
+
*/
|
| 461 |
+
export function getDatabase(): Database {
|
| 462 |
+
if (!sqliteReady || !sqliteDb) {
|
| 463 |
+
// Provide a harmless stub to avoid runtime crashes
|
| 464 |
+
return {
|
| 465 |
+
prepare: () => ({
|
| 466 |
+
all: () => [],
|
| 467 |
+
get: () => undefined,
|
| 468 |
+
run: () => ({ changes: 0, lastInsertRowid: 0 }),
|
| 469 |
+
free: () => undefined,
|
| 470 |
+
}),
|
| 471 |
+
run: () => ({ changes: 0, lastInsertRowid: 0 }),
|
| 472 |
+
close: () => undefined,
|
| 473 |
+
};
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
return {
|
| 477 |
+
prepare: <P = any[], R = any>(sql: string): DatabaseStatement<P, R> => {
|
| 478 |
+
const stmt = sqliteDb!.prepare(sql);
|
| 479 |
+
return {
|
| 480 |
+
all: (...params: any[]) => {
|
| 481 |
+
stmt.bind(params);
|
| 482 |
+
const rows: any[] = [];
|
| 483 |
+
while (stmt.step()) {
|
| 484 |
+
rows.push(stmt.getAsObject());
|
| 485 |
+
}
|
| 486 |
+
stmt.reset();
|
| 487 |
+
return rows as R[];
|
| 488 |
+
},
|
| 489 |
+
get: (...params: any[]) => {
|
| 490 |
+
stmt.bind(params);
|
| 491 |
+
const hasRow = stmt.step();
|
| 492 |
+
const row = hasRow ? stmt.getAsObject() : undefined;
|
| 493 |
+
stmt.reset();
|
| 494 |
+
return row as R | undefined;
|
| 495 |
+
},
|
| 496 |
+
run: (...params: any[]) => {
|
| 497 |
+
stmt.bind(params);
|
| 498 |
+
stmt.step();
|
| 499 |
+
const info = { changes: sqliteDb!.getRowsModified(), lastInsertRowid: sqliteDb!.getRowsModified() };
|
| 500 |
+
stmt.reset();
|
| 501 |
+
return info;
|
| 502 |
+
},
|
| 503 |
+
free: () => stmt.free(),
|
| 504 |
+
};
|
| 505 |
+
},
|
| 506 |
+
run: (sql: string, params?: any[]) => {
|
| 507 |
+
sqliteDb!.run(sql, params);
|
| 508 |
+
return { changes: sqliteDb!.getRowsModified(), lastInsertRowid: sqliteDb!.getRowsModified() };
|
| 509 |
+
},
|
| 510 |
+
close: () => sqliteDb!.close(),
|
| 511 |
+
};
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
/**
|
| 515 |
+
* Get raw sql.js Database for memory systems that need direct exec() access
|
| 516 |
+
* This is needed for CognitiveMemory, PatternMemory, FailureMemory
|
| 517 |
+
*/
|
| 518 |
+
export function getSqlJsDatabase(): SqliteDatabase | null {
|
| 519 |
+
return sqliteDb;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
export async function closeDatabase(): Promise<void> {
|
| 523 |
+
if (sqliteDb) {
|
| 524 |
+
sqliteDb.close();
|
| 525 |
+
}
|
| 526 |
+
if (prismaReady) {
|
| 527 |
+
await prisma.$disconnect();
|
| 528 |
+
}
|
| 529 |
+
isInitialized = false;
|
| 530 |
+
sqliteReady = false;
|
| 531 |
+
prismaReady = false;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
export function isPrismaReady(): boolean {
|
| 535 |
+
return prismaReady;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
export function isSqliteReady(): boolean {
|
| 539 |
+
return sqliteReady;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
// Re-export prisma for convenience
|
| 543 |
+
export { prisma };
|
apps/backend/src/database/prisma.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PrismaClient } from '@prisma/client';
|
| 2 |
+
|
| 3 |
+
// Singleton pattern for Prisma client
|
| 4 |
+
const globalForPrisma = globalThis as unknown as {
|
| 5 |
+
prisma: PrismaClient | undefined;
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
export const prisma =
|
| 9 |
+
globalForPrisma.prisma ??
|
| 10 |
+
new PrismaClient({
|
| 11 |
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
if (process.env.NODE_ENV !== 'production') {
|
| 15 |
+
globalForPrisma.prisma = prisma;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Helper to check if Prisma is connected
|
| 19 |
+
export async function checkPrismaConnection(): Promise<boolean> {
|
| 20 |
+
try {
|
| 21 |
+
await prisma.$queryRaw`SELECT 1`;
|
| 22 |
+
return true;
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error('Prisma connection failed:', error);
|
| 25 |
+
return false;
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Graceful shutdown
|
| 30 |
+
export async function disconnectPrisma(): Promise<void> {
|
| 31 |
+
await prisma.$disconnect();
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export default prisma;
|
apps/backend/src/database/schema.sql
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Memory (CMA) tables
|
| 2 |
+
CREATE TABLE IF NOT EXISTS memory_entities (
|
| 3 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 4 |
+
org_id TEXT NOT NULL,
|
| 5 |
+
user_id TEXT,
|
| 6 |
+
entity_type TEXT NOT NULL,
|
| 7 |
+
content TEXT NOT NULL,
|
| 8 |
+
importance INTEGER NOT NULL DEFAULT 3,
|
| 9 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 10 |
+
);
|
| 11 |
+
|
| 12 |
+
CREATE TABLE IF NOT EXISTS memory_relations (
|
| 13 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 14 |
+
org_id TEXT NOT NULL,
|
| 15 |
+
source_id INTEGER NOT NULL REFERENCES memory_entities(id),
|
| 16 |
+
target_id INTEGER NOT NULL REFERENCES memory_entities(id),
|
| 17 |
+
relation_type TEXT NOT NULL,
|
| 18 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
CREATE TABLE IF NOT EXISTS memory_tags (
|
| 22 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 23 |
+
entity_id INTEGER NOT NULL REFERENCES memory_entities(id),
|
| 24 |
+
tag TEXT NOT NULL
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_org ON memory_entities(org_id);
|
| 28 |
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_user ON memory_entities(user_id);
|
| 29 |
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_entity ON memory_tags(entity_id);
|
| 30 |
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_tag ON memory_tags(tag);
|
| 31 |
+
|
| 32 |
+
-- SRAG tables
|
| 33 |
+
CREATE TABLE IF NOT EXISTS raw_documents (
|
| 34 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 35 |
+
org_id TEXT NOT NULL,
|
| 36 |
+
source_type TEXT NOT NULL,
|
| 37 |
+
source_path TEXT NOT NULL,
|
| 38 |
+
content TEXT NOT NULL,
|
| 39 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 40 |
+
);
|
| 41 |
+
|
| 42 |
+
CREATE TABLE IF NOT EXISTS structured_facts (
|
| 43 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 44 |
+
org_id TEXT NOT NULL,
|
| 45 |
+
doc_id INTEGER REFERENCES raw_documents(id),
|
| 46 |
+
fact_type TEXT NOT NULL,
|
| 47 |
+
json_payload TEXT NOT NULL,
|
| 48 |
+
occurred_at DATETIME,
|
| 49 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
CREATE INDEX IF NOT EXISTS idx_raw_documents_org ON raw_documents(org_id);
|
| 53 |
+
CREATE INDEX IF NOT EXISTS idx_structured_facts_org ON structured_facts(org_id);
|
| 54 |
+
CREATE INDEX IF NOT EXISTS idx_structured_facts_type ON structured_facts(fact_type);
|
| 55 |
+
|
| 56 |
+
-- Evolution Agent tables
|
| 57 |
+
CREATE TABLE IF NOT EXISTS agent_prompts (
|
| 58 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 59 |
+
agent_id TEXT NOT NULL,
|
| 60 |
+
version INTEGER NOT NULL,
|
| 61 |
+
prompt_text TEXT NOT NULL,
|
| 62 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 63 |
+
created_by TEXT NOT NULL DEFAULT 'evolution-agent'
|
| 64 |
+
);
|
| 65 |
+
|
| 66 |
+
CREATE TABLE IF NOT EXISTS agent_runs (
|
| 67 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 68 |
+
agent_id TEXT NOT NULL,
|
| 69 |
+
prompt_version INTEGER NOT NULL,
|
| 70 |
+
input_summary TEXT,
|
| 71 |
+
output_summary TEXT,
|
| 72 |
+
kpi_name TEXT,
|
| 73 |
+
kpi_delta REAL,
|
| 74 |
+
run_context TEXT,
|
| 75 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
CREATE INDEX IF NOT EXISTS idx_agent_prompts_agent ON agent_prompts(agent_id, version);
|
| 79 |
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_agent ON agent_runs(agent_id);
|
| 80 |
+
|
| 81 |
+
-- PAL tables
|
| 82 |
+
CREATE TABLE IF NOT EXISTS pal_user_profiles (
|
| 83 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 84 |
+
user_id TEXT NOT NULL,
|
| 85 |
+
org_id TEXT NOT NULL,
|
| 86 |
+
preference_tone TEXT NOT NULL DEFAULT 'neutral',
|
| 87 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 88 |
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 89 |
+
);
|
| 90 |
+
|
| 91 |
+
CREATE TABLE IF NOT EXISTS pal_focus_windows (
|
| 92 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 93 |
+
user_id TEXT NOT NULL,
|
| 94 |
+
org_id TEXT NOT NULL,
|
| 95 |
+
weekday INTEGER NOT NULL,
|
| 96 |
+
start_hour INTEGER NOT NULL,
|
| 97 |
+
end_hour INTEGER NOT NULL
|
| 98 |
+
);
|
| 99 |
+
|
| 100 |
+
CREATE TABLE IF NOT EXISTS pal_events (
|
| 101 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 102 |
+
user_id TEXT NOT NULL,
|
| 103 |
+
org_id TEXT NOT NULL,
|
| 104 |
+
event_type TEXT NOT NULL,
|
| 105 |
+
payload TEXT NOT NULL,
|
| 106 |
+
detected_stress_level TEXT,
|
| 107 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 108 |
+
);
|
| 109 |
+
|
| 110 |
+
CREATE INDEX IF NOT EXISTS idx_pal_profiles_user ON pal_user_profiles(user_id, org_id);
|
| 111 |
+
CREATE INDEX IF NOT EXISTS idx_pal_focus_windows_user ON pal_focus_windows(user_id);
|
| 112 |
+
CREATE INDEX IF NOT EXISTS idx_pal_events_user ON pal_events(user_id, org_id);
|
| 113 |
+
|
| 114 |
+
-- Security Intelligence tables
|
| 115 |
+
CREATE TABLE IF NOT EXISTS security_search_templates (
|
| 116 |
+
id TEXT PRIMARY KEY,
|
| 117 |
+
name TEXT NOT NULL,
|
| 118 |
+
description TEXT NOT NULL,
|
| 119 |
+
query TEXT NOT NULL,
|
| 120 |
+
severity TEXT NOT NULL DEFAULT 'all',
|
| 121 |
+
timeframe TEXT NOT NULL DEFAULT '24h',
|
| 122 |
+
sources TEXT NOT NULL DEFAULT '[]',
|
| 123 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 124 |
+
);
|
| 125 |
+
|
| 126 |
+
CREATE TABLE IF NOT EXISTS security_search_history (
|
| 127 |
+
id TEXT PRIMARY KEY,
|
| 128 |
+
query TEXT NOT NULL,
|
| 129 |
+
severity TEXT NOT NULL,
|
| 130 |
+
timeframe TEXT NOT NULL,
|
| 131 |
+
sources TEXT NOT NULL,
|
| 132 |
+
results_count INTEGER NOT NULL DEFAULT 0,
|
| 133 |
+
latency_ms INTEGER NOT NULL DEFAULT 0,
|
| 134 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 135 |
+
);
|
| 136 |
+
CREATE INDEX IF NOT EXISTS idx_security_search_history_created ON security_search_history(created_at DESC);
|
| 137 |
+
|
| 138 |
+
CREATE TABLE IF NOT EXISTS security_activity_events (
|
| 139 |
+
id TEXT PRIMARY KEY,
|
| 140 |
+
title TEXT NOT NULL,
|
| 141 |
+
description TEXT NOT NULL,
|
| 142 |
+
category TEXT NOT NULL,
|
| 143 |
+
severity TEXT NOT NULL,
|
| 144 |
+
source TEXT NOT NULL,
|
| 145 |
+
rule TEXT,
|
| 146 |
+
channel TEXT NOT NULL DEFAULT 'SSE',
|
| 147 |
+
payload TEXT,
|
| 148 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 149 |
+
acknowledged INTEGER NOT NULL DEFAULT 0
|
| 150 |
+
);
|
| 151 |
+
CREATE INDEX IF NOT EXISTS idx_security_activity_events_created ON security_activity_events(created_at DESC);
|
| 152 |
+
|
| 153 |
+
CREATE TABLE IF NOT EXISTS widget_permissions (
|
| 154 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 155 |
+
widget_id TEXT NOT NULL,
|
| 156 |
+
resource_type TEXT NOT NULL, -- e.g., 'file_system', 'local_storage', 'drives'
|
| 157 |
+
access_level TEXT NOT NULL CHECK (access_level IN ('none', 'read', 'write')), -- none, read, write
|
| 158 |
+
override BOOLEAN DEFAULT 0, -- 0 for platform default, 1 for widget-specific override
|
| 159 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 160 |
+
UNIQUE(widget_id, resource_type)
|
| 161 |
+
);
|
| 162 |
+
-- ============================================================================
|
| 163 |
+
-- COGNITIVE MEMORY LAYER - Autonomous Intelligence
|
| 164 |
+
-- ============================================================================
|
| 165 |
+
|
| 166 |
+
-- Query Pattern Learning
|
| 167 |
+
CREATE TABLE IF NOT EXISTS mcp_query_patterns (
|
| 168 |
+
id TEXT PRIMARY KEY,
|
| 169 |
+
widget_id TEXT NOT NULL,
|
| 170 |
+
query_type TEXT NOT NULL,
|
| 171 |
+
query_signature TEXT NOT NULL,
|
| 172 |
+
source_used TEXT NOT NULL,
|
| 173 |
+
latency_ms INTEGER NOT NULL,
|
| 174 |
+
result_size INTEGER,
|
| 175 |
+
success BOOLEAN NOT NULL,
|
| 176 |
+
user_context TEXT,
|
| 177 |
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 178 |
+
);
|
| 179 |
+
|
| 180 |
+
CREATE INDEX IF NOT EXISTS idx_query_patterns_widget
|
| 181 |
+
ON mcp_query_patterns(widget_id, timestamp DESC);
|
| 182 |
+
CREATE INDEX IF NOT EXISTS idx_query_patterns_signature
|
| 183 |
+
ON mcp_query_patterns(query_signature);
|
| 184 |
+
|
| 185 |
+
-- Failure Memory
|
| 186 |
+
CREATE TABLE IF NOT EXISTS mcp_failure_memory (
|
| 187 |
+
id TEXT PRIMARY KEY,
|
| 188 |
+
source_name TEXT NOT NULL,
|
| 189 |
+
error_type TEXT NOT NULL,
|
| 190 |
+
error_message TEXT,
|
| 191 |
+
error_context TEXT,
|
| 192 |
+
recovery_action TEXT,
|
| 193 |
+
recovery_success BOOLEAN,
|
| 194 |
+
recovery_time_ms INTEGER,
|
| 195 |
+
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 196 |
+
);
|
| 197 |
+
|
| 198 |
+
CREATE INDEX IF NOT EXISTS idx_failure_memory_source
|
| 199 |
+
ON mcp_failure_memory(source_name, occurred_at DESC);
|
| 200 |
+
|
| 201 |
+
-- Source Health Metrics
|
| 202 |
+
CREATE TABLE IF NOT EXISTS mcp_source_health (
|
| 203 |
+
id TEXT PRIMARY KEY,
|
| 204 |
+
source_name TEXT NOT NULL,
|
| 205 |
+
health_score REAL NOT NULL,
|
| 206 |
+
latency_p50 REAL,
|
| 207 |
+
latency_p95 REAL,
|
| 208 |
+
latency_p99 REAL,
|
| 209 |
+
success_rate REAL NOT NULL,
|
| 210 |
+
request_count INTEGER NOT NULL,
|
| 211 |
+
error_count INTEGER NOT NULL,
|
| 212 |
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 213 |
+
);
|
| 214 |
+
|
| 215 |
+
CREATE INDEX IF NOT EXISTS idx_source_health_source
|
| 216 |
+
ON mcp_source_health(source_name, timestamp DESC);
|
| 217 |
+
|
| 218 |
+
-- Decision Log
|
| 219 |
+
CREATE TABLE IF NOT EXISTS mcp_decision_log (
|
| 220 |
+
id TEXT PRIMARY KEY,
|
| 221 |
+
query_intent TEXT NOT NULL,
|
| 222 |
+
selected_source TEXT NOT NULL,
|
| 223 |
+
decision_confidence REAL NOT NULL,
|
| 224 |
+
actual_latency_ms INTEGER,
|
| 225 |
+
was_optimal BOOLEAN,
|
| 226 |
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 227 |
+
);
|
| 228 |
+
|
| 229 |
+
-- Widget Patterns
|
| 230 |
+
CREATE TABLE IF NOT EXISTS mcp_widget_patterns (
|
| 231 |
+
id TEXT PRIMARY KEY,
|
| 232 |
+
widget_id TEXT NOT NULL,
|
| 233 |
+
pattern_type TEXT NOT NULL,
|
| 234 |
+
pattern_data TEXT NOT NULL,
|
| 235 |
+
occurrence_count INTEGER NOT NULL DEFAULT 1,
|
| 236 |
+
confidence REAL NOT NULL,
|
| 237 |
+
last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 238 |
+
);
|
| 239 |
+
|
| 240 |
+
CREATE INDEX IF NOT EXISTS idx_widget_patterns_widget
|
| 241 |
+
ON mcp_widget_patterns(widget_id, confidence DESC);
|
| 242 |
+
|
| 243 |
+
-- ============================================================================
|
| 244 |
+
-- PROJECT MEMORY LAYER
|
| 245 |
+
-- ============================================================================
|
| 246 |
+
|
| 247 |
+
CREATE TABLE IF NOT EXISTS project_lifecycle_events (
|
| 248 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 249 |
+
event_type TEXT NOT NULL, -- 'build', 'test', 'deploy', 'feature'
|
| 250 |
+
status TEXT NOT NULL, -- 'success', 'failure', 'in_progress'
|
| 251 |
+
details TEXT, -- JSON payload
|
| 252 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 253 |
+
);
|
| 254 |
+
|
| 255 |
+
CREATE TABLE IF NOT EXISTS project_features (
|
| 256 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 257 |
+
name TEXT NOT NULL,
|
| 258 |
+
description TEXT,
|
| 259 |
+
status TEXT NOT NULL, -- 'planned', 'in_progress', 'completed', 'deprecated'
|
| 260 |
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 261 |
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 262 |
+
);
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
-- Vector Store (SQLite Fallback)
|
| 267 |
+
CREATE TABLE IF NOT EXISTS vector_documents (
|
| 268 |
+
id TEXT PRIMARY KEY,
|
| 269 |
+
content TEXT NOT NULL,
|
| 270 |
+
embedding TEXT, -- JSON string of number[]
|
| 271 |
+
metadata TEXT, -- JSON string
|
| 272 |
+
namespace TEXT DEFAULT 'default',
|
| 273 |
+
"userId" TEXT,
|
| 274 |
+
"orgId" TEXT,
|
| 275 |
+
"createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 276 |
+
"updatedAt" DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 277 |
+
);
|
| 278 |
+
|
| 279 |
+
CREATE INDEX IF NOT EXISTS idx_vector_documents_namespace ON vector_documents(namespace);
|
apps/backend/src/database/seeds.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { prisma } from './prisma.js';
|
| 2 |
+
|
| 3 |
+
export async function seedDatabase() {
|
| 4 |
+
console.log('Seeding database with Prisma...');
|
| 5 |
+
|
| 6 |
+
// Seed data for memory entities
|
| 7 |
+
const memories = [
|
| 8 |
+
{ orgId: 'org-1', userId: 'user-1', entityType: 'DecisionOutcome', content: 'Decided to use TypeScript for the backend', importance: 5 },
|
| 9 |
+
{ orgId: 'org-1', userId: 'user-1', entityType: 'CustomerPreference', content: 'Customer prefers minimal UI with dark mode', importance: 4 },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
try {
|
| 13 |
+
// Check if seed data already exists
|
| 14 |
+
const existingCount = await prisma.memoryEntity.count({
|
| 15 |
+
where: { orgId: 'org-1' }
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
if (existingCount > 0) {
|
| 19 |
+
console.log(`Skipping seed - ${existingCount} entries already exist for org-1`);
|
| 20 |
+
return;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Create seed entries
|
| 24 |
+
const result = await prisma.memoryEntity.createMany({
|
| 25 |
+
data: memories,
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
console.log(`Database seeded successfully (${result.count} entries created)`);
|
| 29 |
+
} catch (err) {
|
| 30 |
+
console.error('Error seeding database:', err);
|
| 31 |
+
throw err;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Run seeds if this file is executed directly
|
| 36 |
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
| 37 |
+
seedDatabase()
|
| 38 |
+
.then(() => process.exit(0))
|
| 39 |
+
.catch((err) => {
|
| 40 |
+
console.error('Seed error:', err);
|
| 41 |
+
process.exit(1);
|
| 42 |
+
});
|
| 43 |
+
}
|
apps/backend/src/index-test.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import cors from 'cors';
|
| 3 |
+
import { agentRouter } from './services/agent/agentController.js';
|
| 4 |
+
import { scRouter } from './services/sc/scController.js';
|
| 5 |
+
|
| 6 |
+
const app = express();
|
| 7 |
+
const PORT = process.env.PORT || 3001;
|
| 8 |
+
|
| 9 |
+
// Middleware
|
| 10 |
+
app.use(cors());
|
| 11 |
+
app.use(express.json());
|
| 12 |
+
|
| 13 |
+
// Routes - Only the new widget endpoints
|
| 14 |
+
app.use('/api/agent', agentRouter);
|
| 15 |
+
app.use('/api/commands/sc', scRouter);
|
| 16 |
+
|
| 17 |
+
// Health check
|
| 18 |
+
app.get('/health', (req, res) => {
|
| 19 |
+
res.json({
|
| 20 |
+
status: 'healthy',
|
| 21 |
+
timestamp: new Date().toISOString(),
|
| 22 |
+
routes: [
|
| 23 |
+
'POST /api/agent/query',
|
| 24 |
+
'GET /api/agent/health',
|
| 25 |
+
'POST /api/commands/sc/analyze',
|
| 26 |
+
'POST /api/commands/sc/spec-panel',
|
| 27 |
+
'GET /api/commands/sc/health'
|
| 28 |
+
]
|
| 29 |
+
});
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Start server
|
| 33 |
+
app.listen(PORT, () => {
|
| 34 |
+
console.log(`🚀 Backend server (test) running on http://localhost:${PORT}`);
|
| 35 |
+
console.log(`📡 Available endpoints:`);
|
| 36 |
+
console.log(` POST http://localhost:${PORT}/api/agent/query`);
|
| 37 |
+
console.log(` POST http://localhost:${PORT}/api/commands/sc/analyze`);
|
| 38 |
+
console.log(` POST http://localhost:${PORT}/api/commands/sc/spec-panel`);
|
| 39 |
+
console.log(` GET http://localhost:${PORT}/health`);
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// Graceful shutdown
|
| 43 |
+
process.on('SIGTERM', () => {
|
| 44 |
+
console.log('SIGTERM signal received: closing HTTP server');
|
| 45 |
+
process.exit(0);
|
| 46 |
+
});
|
apps/backend/src/index.ts
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Load environment variables FIRST - before any other imports
|
| 2 |
+
import { config } from 'dotenv';
|
| 3 |
+
import { resolve } from 'path';
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
|
| 6 |
+
// Polyfills for PDF parsing environment (pdfjs-dist v4+ compatibility)
|
| 7 |
+
// @ts-ignore
|
| 8 |
+
if (typeof global.DOMMatrix === 'undefined') {
|
| 9 |
+
// @ts-ignore
|
| 10 |
+
global.DOMMatrix = class DOMMatrix {
|
| 11 |
+
a = 1; b = 0; c = 0; d = 1; e = 0; f = 0;
|
| 12 |
+
constructor() { }
|
| 13 |
+
};
|
| 14 |
+
}
|
| 15 |
+
// @ts-ignore
|
| 16 |
+
if (typeof global.ImageData === 'undefined') {
|
| 17 |
+
// @ts-ignore
|
| 18 |
+
global.ImageData = class ImageData {
|
| 19 |
+
data: Uint8ClampedArray;
|
| 20 |
+
width: number;
|
| 21 |
+
height: number;
|
| 22 |
+
constructor(width: number, height: number) {
|
| 23 |
+
this.width = width;
|
| 24 |
+
this.height = height;
|
| 25 |
+
this.data = new Uint8ClampedArray(width * height * 4);
|
| 26 |
+
}
|
| 27 |
+
};
|
| 28 |
+
}
|
| 29 |
+
// @ts-ignore
|
| 30 |
+
if (typeof global.Path2D === 'undefined') {
|
| 31 |
+
// @ts-ignore
|
| 32 |
+
global.Path2D = class Path2D { };
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
| 36 |
+
// Load .env from backend directory, or root if not found
|
| 37 |
+
config({ path: resolve(__dirname, '../.env') });
|
| 38 |
+
config({ path: resolve(__dirname, '../../../.env') });
|
| 39 |
+
|
| 40 |
+
// --- SAFETY CHECK: VISUAL CONFIRMATION ---
|
| 41 |
+
const ENV_MODE = process.env.NODE_ENV || 'unknown';
|
| 42 |
+
const DB_HOST = process.env.POSTGRES_HOST || 'unknown';
|
| 43 |
+
const NEO_URI = process.env.NEO4J_URI || 'unknown';
|
| 44 |
+
|
| 45 |
+
console.log('\n\n');
|
| 46 |
+
if (ENV_MODE === 'production') {
|
| 47 |
+
console.error('╔══════════════════════════════════════════════════════════════╗');
|
| 48 |
+
console.error('║ WARNING: PRODUCTION MODE ║');
|
| 49 |
+
console.error('║ You are running against LIVE DATA. Use extreme caution. ║');
|
| 50 |
+
console.error('╚══════════════════════════════════════════════════════════════╝');
|
| 51 |
+
} else {
|
| 52 |
+
console.log('╔══════════════════════════════════════════════════════════════╗');
|
| 53 |
+
console.log('║ SAFE MODE: LOCAL DEVELOPMENT ║');
|
| 54 |
+
console.log('║ ║');
|
| 55 |
+
console.log(`║ • Environment: ${ENV_MODE.padEnd(28)} ║`);
|
| 56 |
+
console.log(`║ • Postgres: ${DB_HOST.padEnd(28)} ║`);
|
| 57 |
+
console.log(`║ • Neo4j: ${NEO_URI.padEnd(28)} ║`);
|
| 58 |
+
console.log('╚══════════════════════════════════════════════════════════════╝');
|
| 59 |
+
}
|
| 60 |
+
console.log('\n');
|
| 61 |
+
// -----------------------------------------
|
| 62 |
+
|
| 63 |
+
import express from 'express';
|
| 64 |
+
import cors from 'cors';
|
| 65 |
+
import { createServer } from 'http';
|
| 66 |
+
import { initializeDatabase } from './database/index.js';
|
| 67 |
+
import { mcpRouter } from './mcp/mcpRouter.js';
|
| 68 |
+
import { mcpRegistry } from './mcp/mcpRegistry.js';
|
| 69 |
+
import { MCPWebSocketServer } from './mcp/mcpWebsocketServer.js';
|
| 70 |
+
import { WebSocketServer as LogsWebSocketServer, WebSocket as LogsWebSocket } from 'ws';
|
| 71 |
+
import { memoryRouter } from './services/memory/memoryController.js';
|
| 72 |
+
import { sragRouter } from './services/srag/sragController.js';
|
| 73 |
+
import { evolutionRouter } from './services/evolution/evolutionController.js';
|
| 74 |
+
import { palRouter } from './services/pal/palController.js';
|
| 75 |
+
import datasourcesRouter from './routes/datasources.js';
|
| 76 |
+
import {
|
| 77 |
+
cmaContextHandler,
|
| 78 |
+
cmaIngestHandler,
|
| 79 |
+
cmaMemoryStoreHandler,
|
| 80 |
+
cmaMemoryRetrieveHandler,
|
| 81 |
+
sragQueryHandler,
|
| 82 |
+
sragGovernanceCheckHandler,
|
| 83 |
+
evolutionReportHandler,
|
| 84 |
+
evolutionGetPromptHandler,
|
| 85 |
+
evolutionAnalyzePromptsHandler,
|
| 86 |
+
palEventHandler,
|
| 87 |
+
palBoardActionHandler,
|
| 88 |
+
palOptimizeWorkflowHandler,
|
| 89 |
+
palAnalyzeSentimentHandler,
|
| 90 |
+
notesListHandler,
|
| 91 |
+
notesCreateHandler,
|
| 92 |
+
notesUpdateHandler,
|
| 93 |
+
notesDeleteHandler,
|
| 94 |
+
notesGetHandler,
|
| 95 |
+
widgetsInvokeHandler,
|
| 96 |
+
widgetsOsintInvestigateHandler,
|
| 97 |
+
widgetsThreatHuntHandler,
|
| 98 |
+
widgetsOrchestratorCoordinateHandler,
|
| 99 |
+
widgetsUpdateStateHandler,
|
| 100 |
+
visionaryGenerateHandler,
|
| 101 |
+
dataAnalysisHandler
|
| 102 |
+
} from './mcp/toolHandlers.js';
|
| 103 |
+
import { securityRouter } from './services/security/securityController.js';
|
| 104 |
+
import { AgentOrchestratorServer } from './mcp/servers/AgentOrchestratorServer.js';
|
| 105 |
+
import {
|
| 106 |
+
inputValidationMiddleware,
|
| 107 |
+
csrfProtectionMiddleware,
|
| 108 |
+
rateLimitingMiddleware
|
| 109 |
+
} from './middleware/inputValidation.js';
|
| 110 |
+
import { dataScheduler } from './services/ingestion/DataScheduler.js';
|
| 111 |
+
import { logStream } from './services/logging/logStream.js';
|
| 112 |
+
|
| 113 |
+
const app = express();
|
| 114 |
+
const PORT = parseInt(process.env.PORT || '7860', 10);
|
| 115 |
+
|
| 116 |
+
// CORS Configuration
|
| 117 |
+
const corsOrigin = process.env.CORS_ORIGIN || '*';
|
| 118 |
+
app.use(cors({
|
| 119 |
+
origin: corsOrigin === '*' ? '*' : corsOrigin.split(',').map(o => o.trim()),
|
| 120 |
+
credentials: true,
|
| 121 |
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
| 122 |
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
| 123 |
+
}));
|
| 124 |
+
app.use(express.json({ limit: '10mb' }));
|
| 125 |
+
app.use(rateLimitingMiddleware);
|
| 126 |
+
app.use(inputValidationMiddleware);
|
| 127 |
+
app.use(csrfProtectionMiddleware);
|
| 128 |
+
|
| 129 |
+
// CRITICAL: Start server only after database is initialized
|
| 130 |
+
async function startServer() {
|
| 131 |
+
try {
|
| 132 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 133 |
+
// EARLY SERVER START - Start accepting connections ASAP
|
| 134 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 135 |
+
const server = createServer(app);
|
| 136 |
+
const wsServer = new MCPWebSocketServer(server);
|
| 137 |
+
|
| 138 |
+
// Health check endpoint - FAST response
|
| 139 |
+
app.get('/health', async (req, res) => {
|
| 140 |
+
// Basic process health - registeredTools count fetched dynamically
|
| 141 |
+
res.json({
|
| 142 |
+
status: 'healthy', // Always return healthy if process is up
|
| 143 |
+
timestamp: new Date().toISOString(),
|
| 144 |
+
uptime: process.uptime(),
|
| 145 |
+
environment: process.env.NODE_ENV,
|
| 146 |
+
registeredTools: mcpRegistry.getRegisteredTools().length
|
| 147 |
+
});
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
// Readiness/Liveness checks
|
| 151 |
+
app.get('/ready', (req, res) => res.json({ ready: true }));
|
| 152 |
+
app.get('/live', (req, res) => res.json({ live: true }));
|
| 153 |
+
|
| 154 |
+
// Start server IMMEDIATELY
|
| 155 |
+
server.listen(PORT, '0.0.0.0', () => {
|
| 156 |
+
console.log(`🚀 Backend server running on http://0.0.0.0:${PORT} (Early Start)`);
|
| 157 |
+
console.log(`📡 MCP WebSocket available at ws://0.0.0.0:${PORT}/mcp/ws`);
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
// Setup Logs WebSocket
|
| 161 |
+
const logsWsServer = new LogsWebSocketServer({ server, path: '/api/logs/stream' });
|
| 162 |
+
logsWsServer.on('connection', (socket: LogsWebSocket) => {
|
| 163 |
+
try {
|
| 164 |
+
// Send recent logs buffer
|
| 165 |
+
socket.send(JSON.stringify({ type: 'bootstrap', entries: logStream.getRecent({ limit: 50 }) }));
|
| 166 |
+
} catch (err) {
|
| 167 |
+
console.error('Failed to send initial log buffer:', err);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const listener = (entry: any) => {
|
| 171 |
+
if (socket.readyState === LogsWebSocket.OPEN) {
|
| 172 |
+
socket.send(JSON.stringify({ type: 'log', entry }));
|
| 173 |
+
}
|
| 174 |
+
};
|
| 175 |
+
logStream.on('log', listener);
|
| 176 |
+
socket.on('close', () => logStream.off('log', listener));
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
// Wire up WebSocket (async)
|
| 180 |
+
(async () => {
|
| 181 |
+
try {
|
| 182 |
+
const { setWebSocketServer } = await import('./mcp/autonomousRouter.js');
|
| 183 |
+
setWebSocketServer(wsServer);
|
| 184 |
+
} catch (err) { /* ignore */ }
|
| 185 |
+
})();
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
// Show environment banner
|
| 189 |
+
const isProd = process.env.NODE_ENV === 'production';
|
| 190 |
+
const neo4jUri = process.env.NEO4J_URI || 'bolt://localhost:7687';
|
| 191 |
+
const isAuraDB = neo4jUri.includes('neo4j.io') || neo4jUri.startsWith('neo4j+s://');
|
| 192 |
+
|
| 193 |
+
console.log('');
|
| 194 |
+
console.log('═══════════════════════════════════════════════════════════');
|
| 195 |
+
if (isProd || isAuraDB) {
|
| 196 |
+
console.log(' 🔴 WIDGETTDC - PRODUCTION MODE');
|
| 197 |
+
console.log(' Neo4j: AuraDB Cloud');
|
| 198 |
+
} else {
|
| 199 |
+
console.log(' 🟢 WIDGETTDC - DEVELOPMENT MODE');
|
| 200 |
+
console.log(' Neo4j: Local Docker');
|
| 201 |
+
}
|
| 202 |
+
console.log(` URI: ${neo4jUri}`);
|
| 203 |
+
console.log('═══════════════════════════════════════════════════════════');
|
| 204 |
+
console.log('');
|
| 205 |
+
|
| 206 |
+
// Step 0: Always initialize sql.js (used by CognitiveMemory, etc)
|
| 207 |
+
await initializeDatabase();
|
| 208 |
+
console.log('🗄️ SQLite (sql.js) initialized for memory systems');
|
| 209 |
+
|
| 210 |
+
// Step 1: Initialize Prisma (PostgreSQL + pgvector) - Primary database
|
| 211 |
+
try {
|
| 212 |
+
const { getDatabaseAdapter } = await import('./platform/db/PrismaDatabaseAdapter.js');
|
| 213 |
+
const prismaAdapter = getDatabaseAdapter();
|
| 214 |
+
await prismaAdapter.initialize();
|
| 215 |
+
console.log('🐘 PostgreSQL + pgvector initialized via Prisma');
|
| 216 |
+
} catch (prismaError) {
|
| 217 |
+
console.warn('⚠️ Prisma/PostgreSQL not available:', prismaError);
|
| 218 |
+
console.log(' Using SQLite (sql.js) as fallback for all storage');
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// Step 1.5: Initialize Neo4j Graph Database
|
| 222 |
+
try {
|
| 223 |
+
const { getNeo4jGraphAdapter } = await import('./platform/graph/Neo4jGraphAdapter.js');
|
| 224 |
+
const neo4jAdapter = getNeo4jGraphAdapter();
|
| 225 |
+
await neo4jAdapter.initialize();
|
| 226 |
+
console.log('🕸️ Neo4j Graph Database initialized');
|
| 227 |
+
|
| 228 |
+
// Also connect Neo4jService (used by GraphMemoryService)
|
| 229 |
+
const { neo4jService } = await import('./database/Neo4jService.js');
|
| 230 |
+
await neo4jService.connect();
|
| 231 |
+
console.log('🕸️ Neo4j Service connected');
|
| 232 |
+
|
| 233 |
+
// Run initialization if database is empty
|
| 234 |
+
const stats = await neo4jAdapter.getStatistics();
|
| 235 |
+
if (stats.nodeCount < 5) {
|
| 236 |
+
console.log('📦 Neo4j database appears empty, running initialization...');
|
| 237 |
+
const { initializeNeo4j } = await import('./scripts/initNeo4j.js');
|
| 238 |
+
await initializeNeo4j();
|
| 239 |
+
}
|
| 240 |
+
} catch (error) {
|
| 241 |
+
console.warn('⚠️ Neo4j not available (optional):', error);
|
| 242 |
+
console.log(' Continuing without Neo4j - using implicit graph patterns');
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// Step 1.6: Initialize Transformers.js Embeddings
|
| 246 |
+
try {
|
| 247 |
+
const { getTransformersEmbeddings } = await import('./platform/embeddings/TransformersEmbeddings.js');
|
| 248 |
+
const embeddings = getTransformersEmbeddings();
|
| 249 |
+
await embeddings.initialize();
|
| 250 |
+
console.log('🧠 Transformers.js Embeddings initialized');
|
| 251 |
+
} catch (error) {
|
| 252 |
+
console.warn('⚠️ Transformers.js not available (optional):', error);
|
| 253 |
+
console.log(' Continuing without local embeddings');
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
// Step 2: Register MCP tools (repositories can now safely use getDatabase())
|
| 257 |
+
mcpRegistry.registerTool('cma.context', cmaContextHandler);
|
| 258 |
+
mcpRegistry.registerTool('cma.ingest', cmaIngestHandler);
|
| 259 |
+
mcpRegistry.registerTool('cma.memory.store', cmaMemoryStoreHandler);
|
| 260 |
+
mcpRegistry.registerTool('cma.memory.retrieve', cmaMemoryRetrieveHandler);
|
| 261 |
+
mcpRegistry.registerTool('srag.query', sragQueryHandler);
|
| 262 |
+
mcpRegistry.registerTool('srag.governance-check', sragGovernanceCheckHandler);
|
| 263 |
+
mcpRegistry.registerTool('evolution.report-run', evolutionReportHandler);
|
| 264 |
+
mcpRegistry.registerTool('evolution.get-prompt', evolutionGetPromptHandler);
|
| 265 |
+
mcpRegistry.registerTool('evolution.analyze-prompts', evolutionAnalyzePromptsHandler);
|
| 266 |
+
mcpRegistry.registerTool('pal.event', palEventHandler);
|
| 267 |
+
mcpRegistry.registerTool('pal.board-action', palBoardActionHandler);
|
| 268 |
+
mcpRegistry.registerTool('pal.optimize-workflow', palOptimizeWorkflowHandler);
|
| 269 |
+
mcpRegistry.registerTool('pal.analyze-sentiment', palAnalyzeSentimentHandler);
|
| 270 |
+
mcpRegistry.registerTool('notes.list', notesListHandler);
|
| 271 |
+
mcpRegistry.registerTool('notes.create', notesCreateHandler);
|
| 272 |
+
mcpRegistry.registerTool('notes.update', notesUpdateHandler);
|
| 273 |
+
mcpRegistry.registerTool('notes.delete', notesDeleteHandler);
|
| 274 |
+
mcpRegistry.registerTool('notes.get', notesGetHandler);
|
| 275 |
+
mcpRegistry.registerTool('widgets.invoke', widgetsInvokeHandler);
|
| 276 |
+
mcpRegistry.registerTool('widgets.osint.investigate', widgetsOsintInvestigateHandler);
|
| 277 |
+
mcpRegistry.registerTool('widgets.threat.hunt', widgetsThreatHuntHandler);
|
| 278 |
+
mcpRegistry.registerTool('widgets.orchestrator.coordinate', widgetsOrchestratorCoordinateHandler);
|
| 279 |
+
mcpRegistry.registerTool('widgets.update_state', widgetsUpdateStateHandler);
|
| 280 |
+
mcpRegistry.registerTool('visionary.generate', visionaryGenerateHandler);
|
| 281 |
+
mcpRegistry.registerTool('data.analysis', dataAnalysisHandler);
|
| 282 |
+
|
| 283 |
+
// Project Memory tools
|
| 284 |
+
const {
|
| 285 |
+
projectMemoryLogEventHandler,
|
| 286 |
+
projectMemoryGetEventsHandler,
|
| 287 |
+
projectMemoryAddFeatureHandler,
|
| 288 |
+
projectMemoryUpdateFeatureHandler,
|
| 289 |
+
projectMemoryGetFeaturesHandler
|
| 290 |
+
} = await import('./mcp/projectMemoryHandlers.js');
|
| 291 |
+
|
| 292 |
+
mcpRegistry.registerTool('project.log_event', projectMemoryLogEventHandler);
|
| 293 |
+
mcpRegistry.registerTool('project.get_events', projectMemoryGetEventsHandler);
|
| 294 |
+
mcpRegistry.registerTool('project.add_feature', projectMemoryAddFeatureHandler);
|
| 295 |
+
mcpRegistry.registerTool('project.update_feature', projectMemoryUpdateFeatureHandler);
|
| 296 |
+
mcpRegistry.registerTool('project.get_features', projectMemoryGetFeaturesHandler);
|
| 297 |
+
|
| 298 |
+
// Data Ingestion tools
|
| 299 |
+
const {
|
| 300 |
+
ingestionStartHandler,
|
| 301 |
+
ingestionStatusHandler,
|
| 302 |
+
ingestionConfigureHandler,
|
| 303 |
+
ingestionCrawlHandler,
|
| 304 |
+
ingestionHarvestHandler
|
| 305 |
+
} = await import('./mcp/ingestionHandlers.js');
|
| 306 |
+
|
| 307 |
+
mcpRegistry.registerTool('ingestion.start', ingestionStartHandler);
|
| 308 |
+
mcpRegistry.registerTool('ingestion.status', ingestionStatusHandler);
|
| 309 |
+
mcpRegistry.registerTool('ingestion.configure', ingestionConfigureHandler);
|
| 310 |
+
mcpRegistry.registerTool('ingestion.crawl', ingestionCrawlHandler);
|
| 311 |
+
mcpRegistry.registerTool('ingestion.harvest', ingestionHarvestHandler);
|
| 312 |
+
|
| 313 |
+
// Autonomous Cognitive Tools
|
| 314 |
+
const {
|
| 315 |
+
autonomousGraphRAGHandler,
|
| 316 |
+
autonomousStateGraphHandler,
|
| 317 |
+
autonomousEvolutionHandler,
|
| 318 |
+
autonomousAgentTeamHandler,
|
| 319 |
+
autonomousAgentTeamCoordinateHandler
|
| 320 |
+
} = await import('./mcp/toolHandlers.js');
|
| 321 |
+
|
| 322 |
+
mcpRegistry.registerTool('autonomous.graphrag', autonomousGraphRAGHandler);
|
| 323 |
+
mcpRegistry.registerTool('autonomous.stategraph', autonomousStateGraphHandler);
|
| 324 |
+
mcpRegistry.registerTool('autonomous.evolve', autonomousEvolutionHandler);
|
| 325 |
+
mcpRegistry.registerTool('autonomous.agentteam', autonomousAgentTeamHandler);
|
| 326 |
+
mcpRegistry.registerTool('autonomous.agentteam.coordinate', autonomousAgentTeamCoordinateHandler);
|
| 327 |
+
|
| 328 |
+
// Vidensarkiv (Knowledge Archive) Tools - Persistent vector database
|
| 329 |
+
const {
|
| 330 |
+
vidensarkivSearchHandler,
|
| 331 |
+
vidensarkivAddHandler,
|
| 332 |
+
vidensarkivBatchAddHandler,
|
| 333 |
+
vidensarkivGetRelatedHandler,
|
| 334 |
+
vidensarkivListHandler,
|
| 335 |
+
vidensarkivStatsHandler
|
| 336 |
+
} = await import('./mcp/toolHandlers.js');
|
| 337 |
+
|
| 338 |
+
mcpRegistry.registerTool('vidensarkiv.search', vidensarkivSearchHandler);
|
| 339 |
+
mcpRegistry.registerTool('vidensarkiv.add', vidensarkivAddHandler);
|
| 340 |
+
mcpRegistry.registerTool('vidensarkiv.batch_add', vidensarkivBatchAddHandler);
|
| 341 |
+
mcpRegistry.registerTool('vidensarkiv.get_related', vidensarkivGetRelatedHandler);
|
| 342 |
+
mcpRegistry.registerTool('vidensarkiv.list', vidensarkivListHandler);
|
| 343 |
+
mcpRegistry.registerTool('vidensarkiv.stats', vidensarkivStatsHandler);
|
| 344 |
+
|
| 345 |
+
// TaskRecorder Tools
|
| 346 |
+
const {
|
| 347 |
+
taskRecorderGetSuggestionsHandler,
|
| 348 |
+
taskRecorderApproveHandler,
|
| 349 |
+
taskRecorderRejectHandler,
|
| 350 |
+
taskRecorderExecuteHandler,
|
| 351 |
+
taskRecorderGetPatternsHandler,
|
| 352 |
+
emailRagHandler
|
| 353 |
+
} = await import('./mcp/toolHandlers.js');
|
| 354 |
+
|
| 355 |
+
mcpRegistry.registerTool('taskrecorder.get_suggestions', taskRecorderGetSuggestionsHandler);
|
| 356 |
+
mcpRegistry.registerTool('taskrecorder.approve', taskRecorderApproveHandler);
|
| 357 |
+
mcpRegistry.registerTool('taskrecorder.reject', taskRecorderRejectHandler);
|
| 358 |
+
mcpRegistry.registerTool('taskrecorder.execute', taskRecorderExecuteHandler);
|
| 359 |
+
mcpRegistry.registerTool('taskrecorder.get_patterns', taskRecorderGetPatternsHandler);
|
| 360 |
+
mcpRegistry.registerTool('email.rag', emailRagHandler);
|
| 361 |
+
|
| 362 |
+
// Document Generator Tools
|
| 363 |
+
const {
|
| 364 |
+
docgenPowerpointCreateHandler,
|
| 365 |
+
docgenWordCreateHandler,
|
| 366 |
+
docgenExcelCreateHandler,
|
| 367 |
+
docgenStatusHandler
|
| 368 |
+
} = await import('./mcp/toolHandlers.js');
|
| 369 |
+
|
| 370 |
+
mcpRegistry.registerTool('docgen.powerpoint.create', docgenPowerpointCreateHandler);
|
| 371 |
+
mcpRegistry.registerTool('docgen.word.create', docgenWordCreateHandler);
|
| 372 |
+
mcpRegistry.registerTool('docgen.excel.create', docgenExcelCreateHandler);
|
| 373 |
+
mcpRegistry.registerTool('docgen.status', docgenStatusHandler);
|
| 374 |
+
|
| 375 |
+
// DevTools Guardian Tools
|
| 376 |
+
const { handleDevToolsRequest } = await import('./mcp/devToolsHandlers.js');
|
| 377 |
+
mcpRegistry.registerTool('devtools-status', handleDevToolsRequest);
|
| 378 |
+
mcpRegistry.registerTool('devtools-scan', handleDevToolsRequest);
|
| 379 |
+
mcpRegistry.registerTool('devtools-validate', handleDevToolsRequest);
|
| 380 |
+
|
| 381 |
+
const { tdcGeneratePresentationHandler } = await import('./mcp/tdcHandlers.js');
|
| 382 |
+
mcpRegistry.registerTool('tdc.generate_presentation', tdcGeneratePresentationHandler);
|
| 383 |
+
|
| 384 |
+
// Serve public files (for generated presentations)
|
| 385 |
+
app.use(express.static('public'));
|
| 386 |
+
|
| 387 |
+
// Step 3: Initialize Agent Orchestrator
|
| 388 |
+
const orchestrator = new AgentOrchestratorServer();
|
| 389 |
+
mcpRegistry.registerServer(orchestrator);
|
| 390 |
+
console.log('🤖 Agent Orchestrator initialized');
|
| 391 |
+
|
| 392 |
+
// Step 3.5: Initialize Autonomous Intelligence System
|
| 393 |
+
const { initCognitiveMemory } = await import('./mcp/memory/CognitiveMemory.js');
|
| 394 |
+
const { getSourceRegistry } = await import('./mcp/SourceRegistry.js');
|
| 395 |
+
const { initAutonomousAgent, startAutonomousLearning } = await import('./mcp/autonomousRouter.js');
|
| 396 |
+
const { autonomousRouter } = await import('./mcp/autonomousRouter.js');
|
| 397 |
+
const { getSqlJsDatabase } = await import('./database/index.js');
|
| 398 |
+
const { existsSync } = await import('fs');
|
| 399 |
+
const { readFileSync } = await import('fs');
|
| 400 |
+
const yaml = (await import('js-yaml')).default;
|
| 401 |
+
|
| 402 |
+
// Use raw sql.js database for memory systems (they need exec() method)
|
| 403 |
+
const sqlJsDb = getSqlJsDatabase();
|
| 404 |
+
if (sqlJsDb) {
|
| 405 |
+
initCognitiveMemory(sqlJsDb);
|
| 406 |
+
console.log('🧠 Cognitive Memory initialized');
|
| 407 |
+
} else {
|
| 408 |
+
console.warn('⚠️ Cognitive Memory running in degraded mode (no sql.js database)');
|
| 409 |
+
initCognitiveMemory();
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Initialize Unified Memory System
|
| 413 |
+
const { unifiedMemorySystem } = await import('./mcp/cognitive/UnifiedMemorySystem.js');
|
| 414 |
+
unifiedMemorySystem.init();
|
| 415 |
+
console.log('🧠 Unified Memory System initialized');
|
| 416 |
+
|
| 417 |
+
const registry = getSourceRegistry();
|
| 418 |
+
|
| 419 |
+
// Register agents-yaml data source
|
| 420 |
+
registry.registerSource({
|
| 421 |
+
name: 'agents-yaml',
|
| 422 |
+
type: 'file',
|
| 423 |
+
capabilities: ['agents.*', 'agents.list', 'agents.get', 'agents.trigger'],
|
| 424 |
+
isHealthy: async () => existsSync('agents/registry.yml'),
|
| 425 |
+
estimatedLatency: 50,
|
| 426 |
+
costPerQuery: 0,
|
| 427 |
+
query: async (operation: string, params: any) => {
|
| 428 |
+
const content = readFileSync('agents/registry.yml', 'utf-8');
|
| 429 |
+
const data = yaml.load(content) as any;
|
| 430 |
+
|
| 431 |
+
if (operation === 'list') {
|
| 432 |
+
return data.agents || [];
|
| 433 |
+
} else if (operation === 'get' && params?.id) {
|
| 434 |
+
return data.agents?.find((a: any) => a.id === params.id);
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
return data.agents || [];
|
| 438 |
+
}
|
| 439 |
+
});
|
| 440 |
+
|
| 441 |
+
// Step 3.6: Initialize MCP → Autonomous Integration (non-blocking with timeout)
|
| 442 |
+
const autonomousInitPromise = (async () => {
|
| 443 |
+
try {
|
| 444 |
+
const timeoutPromise = new Promise((_, reject) =>
|
| 445 |
+
setTimeout(() => reject(new Error('Autonomous init timeout')), 5000)
|
| 446 |
+
);
|
| 447 |
+
const { initializeAutonomousSources } = await import('./mcp/autonomous/MCPIntegration.js');
|
| 448 |
+
await Promise.race([initializeAutonomousSources(), timeoutPromise]);
|
| 449 |
+
console.log('🔗 MCP tools registered as autonomous sources');
|
| 450 |
+
|
| 451 |
+
const agent = initAutonomousAgent();
|
| 452 |
+
console.log('🤖 Autonomous Agent initialized');
|
| 453 |
+
|
| 454 |
+
startAutonomousLearning(agent, 300000);
|
| 455 |
+
console.log('🔄 Autonomous learning started (5min intervals)');
|
| 456 |
+
} catch (err: any) {
|
| 457 |
+
console.warn('⚠️ Autonomous sources initialization skipped:', err.message);
|
| 458 |
+
}
|
| 459 |
+
})();
|
| 460 |
+
// Don't await - let it run in background
|
| 461 |
+
|
| 462 |
+
// Step 3.7: Start HansPedder orchestrator (non-blocking)
|
| 463 |
+
(async () => {
|
| 464 |
+
try {
|
| 465 |
+
const { startHansPedder } = await import('./orchestrator/hansPedder.js');
|
| 466 |
+
await startHansPedder();
|
| 467 |
+
console.log('👔 HansPedder orchestrator started');
|
| 468 |
+
} catch (err) {
|
| 469 |
+
console.error('⚠️ Failed to start HansPedder:', err);
|
| 470 |
+
}
|
| 471 |
+
})();
|
| 472 |
+
|
| 473 |
+
// Step 3.8: Start Data Ingestion Scheduler
|
| 474 |
+
dataScheduler.start();
|
| 475 |
+
console.log('⏰ Data Ingestion Scheduler started');
|
| 476 |
+
|
| 477 |
+
// Step 3.8.5: Start NudgeService (aggressive data generation every 15 min)
|
| 478 |
+
const { nudgeService } = await import('./services/NudgeService.js');
|
| 479 |
+
nudgeService.start();
|
| 480 |
+
|
| 481 |
+
// Step 3.9: Start HansPedder Agent Controller (non-blocking)
|
| 482 |
+
(async () => {
|
| 483 |
+
try {
|
| 484 |
+
const { hansPedderAgent } = await import('./services/agent/HansPedderAgentController.js');
|
| 485 |
+
hansPedderAgent.start();
|
| 486 |
+
console.log('🤖 HansPedder Agent Controller started (continuous testing + nudges)');
|
| 487 |
+
} catch (err) {
|
| 488 |
+
console.error('⚠️ Failed to start HansPedder Agent Controller:', err);
|
| 489 |
+
}
|
| 490 |
+
})();
|
| 491 |
+
|
| 492 |
+
// Step 4: Setup routes
|
| 493 |
+
app.use('/api/mcp', mcpRouter);
|
| 494 |
+
app.use('/api/mcp/autonomous', autonomousRouter);
|
| 495 |
+
app.use('/api/memory', memoryRouter);
|
| 496 |
+
app.use('/api/srag', sragRouter);
|
| 497 |
+
app.use('/api/evolution', evolutionRouter);
|
| 498 |
+
app.use('/api/harvest', (req, res, next) => {
|
| 499 |
+
req.url = `/harvest${req.url}`;
|
| 500 |
+
evolutionRouter(req, res, next);
|
| 501 |
+
});
|
| 502 |
+
app.use('/api/pal', palRouter);
|
| 503 |
+
app.use('/api/security', securityRouter);
|
| 504 |
+
|
| 505 |
+
// HansPedder Agent Controller routes
|
| 506 |
+
const hanspedderRoutes = (await import('./routes/hanspedderRoutes.js')).default;
|
| 507 |
+
app.use('/api/hanspedder', hanspedderRoutes);
|
| 508 |
+
|
| 509 |
+
// Prototype Generation routes (PRD to Prototype)
|
| 510 |
+
const prototypeRoutes = (await import('./routes/prototype.js')).default;
|
| 511 |
+
app.use('/api/prototype', prototypeRoutes);
|
| 512 |
+
|
| 513 |
+
// System Information routes (CPU, Memory, GPU, Network stats)
|
| 514 |
+
const sysRoutes = (await import('./routes/sys.js')).default;
|
| 515 |
+
app.use('/api/sys', sysRoutes);
|
| 516 |
+
console.log('📊 System Info API mounted at /api/sys');
|
| 517 |
+
|
| 518 |
+
// Neural Chat - Agent-to-Agent Communication
|
| 519 |
+
const { neuralChatRouter } = await import('./services/NeuralChat/index.js');
|
| 520 |
+
app.use('/api/neural-chat', neuralChatRouter);
|
| 521 |
+
console.log('💬 Neural Chat API mounted at /api/neural-chat');
|
| 522 |
+
|
| 523 |
+
// Knowledge Compiler - System State Aggregation
|
| 524 |
+
const knowledgeRoutes = (await import('./routes/knowledge.js')).default;
|
| 525 |
+
app.use('/api/knowledge', knowledgeRoutes);
|
| 526 |
+
console.log('🧠 Knowledge API mounted at /api/knowledge');
|
| 527 |
+
|
| 528 |
+
// Knowledge Acquisition - The Omni-Harvester
|
| 529 |
+
const acquisitionRoutes = (await import('./routes/acquisition.js')).default;
|
| 530 |
+
app.use('/api/acquisition', acquisitionRoutes);
|
| 531 |
+
console.log('🌾 Omni-Harvester API mounted at /api/acquisition');
|
| 532 |
+
|
| 533 |
+
// Email API - Cloud-compatible email fetching via IMAP
|
| 534 |
+
const emailRoutes = (await import('./routes/email.js')).default;
|
| 535 |
+
app.use('/api/email', emailRoutes);
|
| 536 |
+
console.log('📧 Email API mounted at /api/email');
|
| 537 |
+
|
| 538 |
+
// Start KnowledgeCompiler auto-compilation (every 60 seconds)
|
| 539 |
+
const { knowledgeCompiler } = await import('./services/Knowledge/index.js');
|
| 540 |
+
knowledgeCompiler.startAutoCompilation(60000);
|
| 541 |
+
console.log('🧠 KnowledgeCompiler auto-compilation started');
|
| 542 |
+
|
| 543 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 544 |
+
// 🛡️ BOOTSTRAP HEALTH CHECKS now run inside BootstrapGate.init()
|
| 545 |
+
// Prevents "Death on Startup" if Neo4j/DB unavailable
|
| 546 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 547 |
+
const bootstrapHealthCheck = async (): Promise<{ ready: boolean; degraded: boolean; services: any[] }> => {
|
| 548 |
+
const services: { name: string; status: 'healthy' | 'degraded' | 'unavailable'; latencyMs?: number }[] = [];
|
| 549 |
+
let criticalFailure = false;
|
| 550 |
+
|
| 551 |
+
// Check Neo4j
|
| 552 |
+
try {
|
| 553 |
+
const start = Date.now();
|
| 554 |
+
const { neo4jAdapter } = await import('./adapters/Neo4jAdapter.js');
|
| 555 |
+
await neo4jAdapter.executeQuery('RETURN 1 as ping');
|
| 556 |
+
services.push({ name: 'Neo4j', status: 'healthy', latencyMs: Date.now() - start });
|
| 557 |
+
console.log('✅ Neo4j: HEALTHY');
|
| 558 |
+
} catch (err: any) {
|
| 559 |
+
services.push({ name: 'Neo4j', status: 'degraded' });
|
| 560 |
+
console.warn('⚠️ Neo4j: DEGRADED - continuing without graph features');
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Check Prisma/PostgreSQL (already initialized above)
|
| 564 |
+
try {
|
| 565 |
+
const start = Date.now();
|
| 566 |
+
// Prisma is already connected if we got here
|
| 567 |
+
services.push({ name: 'PostgreSQL', status: 'healthy', latencyMs: Date.now() - start });
|
| 568 |
+
console.log('✅ PostgreSQL: HEALTHY (Prisma connected)');
|
| 569 |
+
} catch (err: any) {
|
| 570 |
+
services.push({ name: 'PostgreSQL', status: 'degraded' });
|
| 571 |
+
console.warn('⚠️ PostgreSQL: DEGRADED - some features may be unavailable');
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
// Check filesystem (DropZone)
|
| 575 |
+
try {
|
| 576 |
+
const fs = await import('fs/promises');
|
| 577 |
+
const { DROPZONE_PATH } = await import('./config.js');
|
| 578 |
+
await fs.access(DROPZONE_PATH);
|
| 579 |
+
services.push({ name: 'Filesystem', status: 'healthy' });
|
| 580 |
+
console.log(`✅ Filesystem: HEALTHY (${DROPZONE_PATH})`);
|
| 581 |
+
} catch {
|
| 582 |
+
services.push({ name: 'Filesystem', status: 'degraded' });
|
| 583 |
+
console.warn('⚠️ Filesystem: DropZone not accessible');
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
const degraded = services.some(s => s.status === 'degraded');
|
| 587 |
+
return { ready: !criticalFailure, degraded, services };
|
| 588 |
+
};
|
| 589 |
+
|
| 590 |
+
console.log('\n🔍 Running Bootstrap Health Check...');
|
| 591 |
+
const bootHealth = await bootstrapHealthCheck();
|
| 592 |
+
|
| 593 |
+
if (!bootHealth.ready) {
|
| 594 |
+
console.error('💀 CRITICAL: Bootstrap health check failed - aborting startup');
|
| 595 |
+
process.exit(1);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
if (bootHealth.degraded) {
|
| 599 |
+
console.warn('⚠️ WARNING: Starting in DEGRADED MODE - some features may be unavailable\n');
|
| 600 |
+
} else {
|
| 601 |
+
console.log('✅ All systems nominal - proceeding with startup\n');
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
// Server started early at top of startServer()
|
| 605 |
+
|
| 606 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 607 |
+
// CONTINUING INITIALIZATION...
|
| 608 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 609 |
+
|
| 610 |
+
// ============================================
|
| 611 |
+
// KNOWLEDGE COMPILER API - System Intelligence
|
| 612 |
+
// ============================================
|
| 613 |
+
const knowledgeApi = (await import('./api/knowledge.js')).default;
|
| 614 |
+
app.use('/api/knowledge', knowledgeApi);
|
| 615 |
+
console.log('📚 Knowledge Compiler API mounted at /api/knowledge');
|
| 616 |
+
|
| 617 |
+
const logsRouter = (await import('./routes/logs.js')).default;
|
| 618 |
+
app.use('/api/logs', logsRouter);
|
| 619 |
+
console.log('📝 Log API mounted at /api/logs');
|
| 620 |
+
|
| 621 |
+
// HyperLog API - Real-time intelligence monitoring for NeuroLink widget
|
| 622 |
+
app.get('/api/hyper/events', async (req, res) => {
|
| 623 |
+
try {
|
| 624 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 625 |
+
const events = hyperLog.getHistory(50);
|
| 626 |
+
const metrics = hyperLog.getMetrics();
|
| 627 |
+
res.json({ events, metrics });
|
| 628 |
+
} catch (error) {
|
| 629 |
+
console.error('HyperLog error:', error);
|
| 630 |
+
res.status(500).json({ error: 'HyperLog unavailable', events: [], metrics: { totalThoughts: 0, toolUsageRate: 0, activeAgents: 0 } });
|
| 631 |
+
}
|
| 632 |
+
});
|
| 633 |
+
|
| 634 |
+
// ============================================
|
| 635 |
+
// SEMANTIC BUS: Widget Telepathy API
|
| 636 |
+
// ============================================
|
| 637 |
+
|
| 638 |
+
// Dream API - Semantic search across collective memory
|
| 639 |
+
app.post('/api/hyper/dream', async (req, res) => {
|
| 640 |
+
try {
|
| 641 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 642 |
+
const { query, limit = 5, minScore = 0.6 } = req.body;
|
| 643 |
+
|
| 644 |
+
if (!query) {
|
| 645 |
+
return res.status(400).json({ error: 'Query is required' });
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
const results = await hyperLog.findRelatedThoughts(query, limit, minScore);
|
| 649 |
+
const canDream = hyperLog.canDream();
|
| 650 |
+
|
| 651 |
+
res.json({
|
| 652 |
+
results,
|
| 653 |
+
query,
|
| 654 |
+
dreamMode: canDream ? 'semantic' : 'keyword',
|
| 655 |
+
timestamp: Date.now()
|
| 656 |
+
});
|
| 657 |
+
} catch (error) {
|
| 658 |
+
console.error('Dream API error:', error);
|
| 659 |
+
res.status(500).json({ error: 'Dream failed', results: [] });
|
| 660 |
+
}
|
| 661 |
+
});
|
| 662 |
+
|
| 663 |
+
// Broadcast API - Widget sends a thought into the collective
|
| 664 |
+
app.post('/api/hyper/broadcast', async (req, res) => {
|
| 665 |
+
try {
|
| 666 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 667 |
+
const { type, agent, content, metadata = {} } = req.body;
|
| 668 |
+
|
| 669 |
+
if (!type || !agent || !content) {
|
| 670 |
+
return res.status(400).json({ error: 'type, agent, and content are required' });
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
const eventId = await hyperLog.log(type, agent, content, metadata);
|
| 674 |
+
|
| 675 |
+
res.json({
|
| 676 |
+
success: true,
|
| 677 |
+
eventId,
|
| 678 |
+
timestamp: Date.now()
|
| 679 |
+
});
|
| 680 |
+
} catch (error) {
|
| 681 |
+
console.error('Broadcast API error:', error);
|
| 682 |
+
res.status(500).json({ error: 'Broadcast failed' });
|
| 683 |
+
}
|
| 684 |
+
});
|
| 685 |
+
|
| 686 |
+
// Find similar thoughts to a specific event
|
| 687 |
+
app.get('/api/hyper/similar/:eventId', async (req, res) => {
|
| 688 |
+
try {
|
| 689 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 690 |
+
const { eventId } = req.params;
|
| 691 |
+
const limit = parseInt(req.query.limit as string) || 5;
|
| 692 |
+
|
| 693 |
+
const results = await hyperLog.findSimilarTo(eventId, limit);
|
| 694 |
+
|
| 695 |
+
res.json({ results, eventId });
|
| 696 |
+
} catch (error) {
|
| 697 |
+
console.error('Similar API error:', error);
|
| 698 |
+
res.status(500).json({ error: 'Similarity search failed', results: [] });
|
| 699 |
+
}
|
| 700 |
+
});
|
| 701 |
+
|
| 702 |
+
// Get causal path leading to an event (rewind the brain)
|
| 703 |
+
app.get('/api/hyper/rewind/:eventId', async (req, res) => {
|
| 704 |
+
try {
|
| 705 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 706 |
+
const { eventId } = req.params;
|
| 707 |
+
const maxDepth = parseInt(req.query.maxDepth as string) || 50;
|
| 708 |
+
|
| 709 |
+
const path = await hyperLog.getCausalPath(eventId, maxDepth);
|
| 710 |
+
|
| 711 |
+
res.json({ path, eventId, depth: path.length });
|
| 712 |
+
} catch (error) {
|
| 713 |
+
console.error('Rewind API error:', error);
|
| 714 |
+
res.status(500).json({ error: 'Rewind failed', path: [] });
|
| 715 |
+
}
|
| 716 |
+
});
|
| 717 |
+
|
| 718 |
+
// Start a new thought chain (correlation)
|
| 719 |
+
app.post('/api/hyper/chain/start', async (req, res) => {
|
| 720 |
+
try {
|
| 721 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 722 |
+
const { label } = req.body;
|
| 723 |
+
|
| 724 |
+
const correlationId = hyperLog.startChain(label);
|
| 725 |
+
|
| 726 |
+
res.json({ correlationId, label });
|
| 727 |
+
} catch (error) {
|
| 728 |
+
console.error('Chain start error:', error);
|
| 729 |
+
res.status(500).json({ error: 'Failed to start chain' });
|
| 730 |
+
}
|
| 731 |
+
});
|
| 732 |
+
|
| 733 |
+
// Get brain status (can it dream?)
|
| 734 |
+
app.get('/api/hyper/status', async (req, res) => {
|
| 735 |
+
try {
|
| 736 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 737 |
+
const metrics = hyperLog.getMetrics();
|
| 738 |
+
const canDream = hyperLog.canDream();
|
| 739 |
+
|
| 740 |
+
res.json({
|
| 741 |
+
canDream,
|
| 742 |
+
metrics,
|
| 743 |
+
status: canDream ? 'dreaming' : 'awake',
|
| 744 |
+
timestamp: Date.now()
|
| 745 |
+
});
|
| 746 |
+
} catch (error) {
|
| 747 |
+
console.error('Status API error:', error);
|
| 748 |
+
res.status(500).json({ error: 'Status unavailable' });
|
| 749 |
+
}
|
| 750 |
+
});
|
| 751 |
+
|
| 752 |
+
// ============================================
|
| 753 |
+
// THE STRATEGIST - Team Delegation API
|
| 754 |
+
// ============================================
|
| 755 |
+
|
| 756 |
+
/**
|
| 757 |
+
* POST /api/team/delegate
|
| 758 |
+
* The Strategist delegates tasks to team members (Architect, Visionary)
|
| 759 |
+
*/
|
| 760 |
+
app.post('/api/team/delegate', async (req, res) => {
|
| 761 |
+
try {
|
| 762 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 763 |
+
const {
|
| 764 |
+
task,
|
| 765 |
+
assignTo,
|
| 766 |
+
priority = 'medium',
|
| 767 |
+
context = {},
|
| 768 |
+
parentTaskId
|
| 769 |
+
} = req.body;
|
| 770 |
+
|
| 771 |
+
if (!task || !assignTo) {
|
| 772 |
+
return res.status(400).json({
|
| 773 |
+
error: 'Missing required fields: task, assignTo'
|
| 774 |
+
});
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
// Log the delegation event
|
| 778 |
+
const eventId = await hyperLog.log(
|
| 779 |
+
'DELEGATION',
|
| 780 |
+
'TheStrategist',
|
| 781 |
+
`Delegating to ${assignTo}: ${task}`,
|
| 782 |
+
{
|
| 783 |
+
assignedTo: assignTo,
|
| 784 |
+
priority,
|
| 785 |
+
context,
|
| 786 |
+
parentTaskId,
|
| 787 |
+
status: 'pending'
|
| 788 |
+
}
|
| 789 |
+
);
|
| 790 |
+
|
| 791 |
+
// Broadcast the delegation for the assigned widget to pick up
|
| 792 |
+
await hyperLog.log(
|
| 793 |
+
'THOUGHT',
|
| 794 |
+
assignTo,
|
| 795 |
+
`Received task from Strategist: ${task}`,
|
| 796 |
+
{
|
| 797 |
+
delegationId: eventId,
|
| 798 |
+
priority,
|
| 799 |
+
context
|
| 800 |
+
}
|
| 801 |
+
);
|
| 802 |
+
|
| 803 |
+
res.json({
|
| 804 |
+
success: true,
|
| 805 |
+
delegationId: eventId,
|
| 806 |
+
message: `Task delegated to ${assignTo}`,
|
| 807 |
+
task: {
|
| 808 |
+
id: eventId,
|
| 809 |
+
description: task,
|
| 810 |
+
assignedTo: assignTo,
|
| 811 |
+
priority,
|
| 812 |
+
status: 'pending',
|
| 813 |
+
createdAt: Date.now()
|
| 814 |
+
}
|
| 815 |
+
});
|
| 816 |
+
|
| 817 |
+
} catch (error) {
|
| 818 |
+
console.error('Delegation API error:', error);
|
| 819 |
+
res.status(500).json({ error: 'Delegation failed' });
|
| 820 |
+
}
|
| 821 |
+
});
|
| 822 |
+
|
| 823 |
+
/**
|
| 824 |
+
* GET /api/team/tasks
|
| 825 |
+
* Get all delegated tasks and their status
|
| 826 |
+
*/
|
| 827 |
+
app.get('/api/team/tasks', async (req, res) => {
|
| 828 |
+
try {
|
| 829 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 830 |
+
const { assignedTo, status } = req.query;
|
| 831 |
+
|
| 832 |
+
// Search for delegation events
|
| 833 |
+
const allEvents = hyperLog.getHistory(100);
|
| 834 |
+
let tasks = allEvents.filter(e => e.type === 'DELEGATION');
|
| 835 |
+
|
| 836 |
+
if (assignedTo) {
|
| 837 |
+
tasks = tasks.filter(t => t.metadata?.assignedTo === assignedTo);
|
| 838 |
+
}
|
| 839 |
+
if (status) {
|
| 840 |
+
tasks = tasks.filter(t => t.metadata?.status === status);
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
res.json({
|
| 844 |
+
tasks: tasks.map(t => ({
|
| 845 |
+
id: t.id,
|
| 846 |
+
description: t.content,
|
| 847 |
+
assignedTo: t.metadata?.assignedTo,
|
| 848 |
+
priority: t.metadata?.priority,
|
| 849 |
+
status: t.metadata?.status || 'pending',
|
| 850 |
+
createdAt: t.timestamp,
|
| 851 |
+
context: t.metadata?.context
|
| 852 |
+
})),
|
| 853 |
+
total: tasks.length
|
| 854 |
+
});
|
| 855 |
+
|
| 856 |
+
} catch (error) {
|
| 857 |
+
console.error('Tasks API error:', error);
|
| 858 |
+
res.status(500).json({ error: 'Failed to fetch tasks' });
|
| 859 |
+
}
|
| 860 |
+
});
|
| 861 |
+
|
| 862 |
+
/**
|
| 863 |
+
* PUT /api/team/tasks/:taskId/status
|
| 864 |
+
* Update task status (e.g., in_progress, completed, blocked)
|
| 865 |
+
*/
|
| 866 |
+
app.put('/api/team/tasks/:taskId/status', async (req, res) => {
|
| 867 |
+
try {
|
| 868 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 869 |
+
const { taskId } = req.params;
|
| 870 |
+
const { status, result, notes } = req.body;
|
| 871 |
+
|
| 872 |
+
if (!status) {
|
| 873 |
+
return res.status(400).json({ error: 'Status is required' });
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
// Log the status update
|
| 877 |
+
const eventId = await hyperLog.log(
|
| 878 |
+
'REASONING_UPDATE',
|
| 879 |
+
'TheStrategist',
|
| 880 |
+
`Task ${taskId} status updated to: ${status}`,
|
| 881 |
+
{
|
| 882 |
+
taskId,
|
| 883 |
+
newStatus: status,
|
| 884 |
+
result,
|
| 885 |
+
notes,
|
| 886 |
+
updatedAt: Date.now()
|
| 887 |
+
}
|
| 888 |
+
);
|
| 889 |
+
|
| 890 |
+
res.json({
|
| 891 |
+
success: true,
|
| 892 |
+
taskId,
|
| 893 |
+
status,
|
| 894 |
+
updateEventId: eventId
|
| 895 |
+
});
|
| 896 |
+
|
| 897 |
+
} catch (error) {
|
| 898 |
+
console.error('Task update API error:', error);
|
| 899 |
+
res.status(500).json({ error: 'Failed to update task' });
|
| 900 |
+
}
|
| 901 |
+
});
|
| 902 |
+
|
| 903 |
+
/**
|
| 904 |
+
* POST /api/team/plan
|
| 905 |
+
* The Strategist creates a multi-step plan with dependencies
|
| 906 |
+
*/
|
| 907 |
+
app.post('/api/team/plan', async (req, res) => {
|
| 908 |
+
try {
|
| 909 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 910 |
+
const {
|
| 911 |
+
goal,
|
| 912 |
+
steps,
|
| 913 |
+
teamMembers = ['TheArchitect', 'TheVisionary']
|
| 914 |
+
} = req.body;
|
| 915 |
+
|
| 916 |
+
if (!goal || !steps || !Array.isArray(steps)) {
|
| 917 |
+
return res.status(400).json({
|
| 918 |
+
error: 'Missing required fields: goal, steps (array)'
|
| 919 |
+
});
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
// Start a new correlation chain for this plan
|
| 923 |
+
const planId = hyperLog.startChain(`Plan: ${goal.substring(0, 50)}`);
|
| 924 |
+
|
| 925 |
+
// Log the plan creation
|
| 926 |
+
await hyperLog.log(
|
| 927 |
+
'CRITICAL_DECISION',
|
| 928 |
+
'TheStrategist',
|
| 929 |
+
`Created plan: ${goal}`,
|
| 930 |
+
{
|
| 931 |
+
planId,
|
| 932 |
+
totalSteps: steps.length,
|
| 933 |
+
teamMembers,
|
| 934 |
+
steps: steps.map((s: any, i: number) => ({
|
| 935 |
+
order: i + 1,
|
| 936 |
+
...s
|
| 937 |
+
}))
|
| 938 |
+
}
|
| 939 |
+
);
|
| 940 |
+
|
| 941 |
+
// Create delegation events for each step
|
| 942 |
+
const delegations: { id: string; step: string }[] = [];
|
| 943 |
+
for (let i = 0; i < steps.length; i++) {
|
| 944 |
+
const step = steps[i];
|
| 945 |
+
const eventId = await hyperLog.log(
|
| 946 |
+
'DELEGATION',
|
| 947 |
+
'TheStrategist',
|
| 948 |
+
`Step ${i + 1}: ${step.task}`,
|
| 949 |
+
{
|
| 950 |
+
planId,
|
| 951 |
+
stepOrder: i + 1,
|
| 952 |
+
assignedTo: step.assignTo || teamMembers[i % teamMembers.length],
|
| 953 |
+
dependsOn: step.dependsOn || (i > 0 ? [delegations[i - 1].id] : []),
|
| 954 |
+
status: 'pending',
|
| 955 |
+
priority: step.priority || 'medium'
|
| 956 |
+
}
|
| 957 |
+
);
|
| 958 |
+
delegations.push({ id: eventId, step: step.task });
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
res.json({
|
| 962 |
+
success: true,
|
| 963 |
+
planId,
|
| 964 |
+
goal,
|
| 965 |
+
steps: delegations.map((d, i) => ({
|
| 966 |
+
...d,
|
| 967 |
+
order: i + 1,
|
| 968 |
+
assignedTo: steps[i].assignTo || teamMembers[i % teamMembers.length]
|
| 969 |
+
})),
|
| 970 |
+
totalSteps: steps.length
|
| 971 |
+
});
|
| 972 |
+
|
| 973 |
+
} catch (error) {
|
| 974 |
+
console.error('Plan API error:', error);
|
| 975 |
+
res.status(500).json({ error: 'Failed to create plan' });
|
| 976 |
+
}
|
| 977 |
+
});
|
| 978 |
+
|
| 979 |
+
/**
|
| 980 |
+
* GET /api/team/status
|
| 981 |
+
* Get overall team status and workload
|
| 982 |
+
*/
|
| 983 |
+
app.get('/api/team/status', async (req, res) => {
|
| 984 |
+
try {
|
| 985 |
+
const { hyperLog } = await import('./services/hyper-log.js');
|
| 986 |
+
const allEvents = hyperLog.getHistory(200);
|
| 987 |
+
|
| 988 |
+
const delegations = allEvents.filter(e => e.type === 'DELEGATION');
|
| 989 |
+
const byAgent: Record<string, any> = {};
|
| 990 |
+
|
| 991 |
+
// Aggregate by team member
|
| 992 |
+
for (const d of delegations) {
|
| 993 |
+
const agent = d.metadata?.assignedTo || 'Unassigned';
|
| 994 |
+
if (!byAgent[agent]) {
|
| 995 |
+
byAgent[agent] = {
|
| 996 |
+
name: agent,
|
| 997 |
+
pending: 0,
|
| 998 |
+
inProgress: 0,
|
| 999 |
+
completed: 0,
|
| 1000 |
+
blocked: 0,
|
| 1001 |
+
tasks: []
|
| 1002 |
+
};
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
const status = d.metadata?.status || 'pending';
|
| 1006 |
+
byAgent[agent][status === 'in_progress' ? 'inProgress' : status]++;
|
| 1007 |
+
byAgent[agent].tasks.push({
|
| 1008 |
+
id: d.id,
|
| 1009 |
+
task: d.content,
|
| 1010 |
+
status,
|
| 1011 |
+
priority: d.metadata?.priority
|
| 1012 |
+
});
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
res.json({
|
| 1016 |
+
team: Object.values(byAgent),
|
| 1017 |
+
summary: {
|
| 1018 |
+
totalTasks: delegations.length,
|
| 1019 |
+
pending: delegations.filter(d => d.metadata?.status === 'pending').length,
|
| 1020 |
+
inProgress: delegations.filter(d => d.metadata?.status === 'in_progress').length,
|
| 1021 |
+
completed: delegations.filter(d => d.metadata?.status === 'completed').length
|
| 1022 |
+
},
|
| 1023 |
+
lastUpdated: Date.now()
|
| 1024 |
+
});
|
| 1025 |
+
|
| 1026 |
+
} catch (error) {
|
| 1027 |
+
console.error('Team status API error:', error);
|
| 1028 |
+
res.status(500).json({ error: 'Failed to fetch team status' });
|
| 1029 |
+
}
|
| 1030 |
+
});
|
| 1031 |
+
|
| 1032 |
+
// ============================================
|
| 1033 |
+
// THE COLONIZER - API Assimilation Engine
|
| 1034 |
+
// ============================================
|
| 1035 |
+
|
| 1036 |
+
/**
|
| 1037 |
+
* POST /api/evolution/colonize
|
| 1038 |
+
* Assimilate a new external API into the WidgeTDC swarm
|
| 1039 |
+
*/
|
| 1040 |
+
app.post('/api/evolution/colonize', async (req, res) => {
|
| 1041 |
+
try {
|
| 1042 |
+
const { colonizerService } = await import('./services/colonizer-service.js');
|
| 1043 |
+
const { systemName, documentation, generateTests = false, dryRun = false } = req.body;
|
| 1044 |
+
|
| 1045 |
+
if (!systemName || !documentation) {
|
| 1046 |
+
return res.status(400).json({
|
| 1047 |
+
error: 'Missing required fields: systemName, documentation'
|
| 1048 |
+
});
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
console.log(`🛸 [COLONIZER API] Received assimilation request for: ${systemName}`);
|
| 1052 |
+
|
| 1053 |
+
const result = await colonizerService.assimilateSystem({
|
| 1054 |
+
systemName,
|
| 1055 |
+
apiSpecContent: documentation,
|
| 1056 |
+
generateTests,
|
| 1057 |
+
dryRun
|
| 1058 |
+
});
|
| 1059 |
+
|
| 1060 |
+
res.json(result);
|
| 1061 |
+
|
| 1062 |
+
} catch (error: any) {
|
| 1063 |
+
console.error('Colonizer API error:', error);
|
| 1064 |
+
res.status(500).json({ error: error.message || 'Assimilation failed' });
|
| 1065 |
+
}
|
| 1066 |
+
});
|
| 1067 |
+
|
| 1068 |
+
/**
|
| 1069 |
+
* GET /api/evolution/systems
|
| 1070 |
+
* List all assimilated systems
|
| 1071 |
+
*/
|
| 1072 |
+
app.get('/api/evolution/systems', async (req, res) => {
|
| 1073 |
+
try {
|
| 1074 |
+
const { colonizerService } = await import('./services/colonizer-service.js');
|
| 1075 |
+
const systems = await colonizerService.listAssimilatedSystems();
|
| 1076 |
+
|
| 1077 |
+
res.json({
|
| 1078 |
+
systems,
|
| 1079 |
+
count: systems.length,
|
| 1080 |
+
toolsDirectory: 'apps/backend/src/tools/generated'
|
| 1081 |
+
});
|
| 1082 |
+
|
| 1083 |
+
} catch (error: any) {
|
| 1084 |
+
console.error('Systems list API error:', error);
|
| 1085 |
+
res.status(500).json({ error: 'Failed to list systems' });
|
| 1086 |
+
}
|
| 1087 |
+
});
|
| 1088 |
+
|
| 1089 |
+
/**
|
| 1090 |
+
* DELETE /api/evolution/systems/:systemName
|
| 1091 |
+
* Remove an assimilated system
|
| 1092 |
+
*/
|
| 1093 |
+
app.delete('/api/evolution/systems/:systemName', async (req, res) => {
|
| 1094 |
+
try {
|
| 1095 |
+
const { colonizerService } = await import('./services/colonizer-service.js');
|
| 1096 |
+
const { systemName } = req.params;
|
| 1097 |
+
|
| 1098 |
+
const success = await colonizerService.removeSystem(systemName);
|
| 1099 |
+
|
| 1100 |
+
if (success) {
|
| 1101 |
+
res.json({
|
| 1102 |
+
success: true,
|
| 1103 |
+
message: `System ${systemName} removed. Restart server to complete removal.`
|
| 1104 |
+
});
|
| 1105 |
+
} else {
|
| 1106 |
+
res.status(404).json({
|
| 1107 |
+
success: false,
|
| 1108 |
+
message: `System ${systemName} not found`
|
| 1109 |
+
});
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
} catch (error: any) {
|
| 1113 |
+
console.error('System removal API error:', error);
|
| 1114 |
+
res.status(500).json({ error: 'Failed to remove system' });
|
| 1115 |
+
}
|
| 1116 |
+
});
|
| 1117 |
+
|
| 1118 |
+
/**
|
| 1119 |
+
* GET /api/evolution/graph/stats
|
| 1120 |
+
* Get Neo4j graph statistics for 3D visualization
|
| 1121 |
+
* 🔗 NEURAL LINK ENDPOINT
|
| 1122 |
+
*/
|
| 1123 |
+
app.get('/api/evolution/graph/stats', async (_req, res) => {
|
| 1124 |
+
try {
|
| 1125 |
+
const { neo4jService } = await import('./database/Neo4jService.js');
|
| 1126 |
+
|
| 1127 |
+
// 1. Fetch Stats
|
| 1128 |
+
const statsQuery = `
|
| 1129 |
+
MATCH (n)
|
| 1130 |
+
OPTIONAL MATCH ()-[r]->()
|
| 1131 |
+
RETURN count(DISTINCT n) as nodes, count(DISTINCT r) as relationships
|
| 1132 |
+
`;
|
| 1133 |
+
const statsResult = await neo4jService.runQuery(statsQuery);
|
| 1134 |
+
const stats = {
|
| 1135 |
+
nodes: statsResult[0]?.nodes?.toNumber ? statsResult[0].nodes.toNumber() : (statsResult[0]?.nodes || 0),
|
| 1136 |
+
relationships: statsResult[0]?.relationships?.toNumber ? statsResult[0].relationships.toNumber() : (statsResult[0]?.relationships || 0)
|
| 1137 |
+
};
|
| 1138 |
+
|
| 1139 |
+
// 2. Fetch Sample Nodes for Visualization
|
| 1140 |
+
const vizQuery = `
|
| 1141 |
+
MATCH (n)
|
| 1142 |
+
RETURN n, labels(n) as labels
|
| 1143 |
+
LIMIT 100
|
| 1144 |
+
`;
|
| 1145 |
+
const vizResult = await neo4jService.runQuery(vizQuery);
|
| 1146 |
+
|
| 1147 |
+
// Map Neo4j structure to clean JSON for Frontend
|
| 1148 |
+
const visualNodes = vizResult.map(row => {
|
| 1149 |
+
const node = row.n;
|
| 1150 |
+
return {
|
| 1151 |
+
id: node.elementId, // v6: use elementId instead of identity
|
| 1152 |
+
name: node.properties.name || node.properties.title || `Node ${node.elementId}`,
|
| 1153 |
+
labels: row.labels || node.labels,
|
| 1154 |
+
type: (row.labels && row.labels.includes('Directory')) ? 'directory' : 'file', // Simple heuristic
|
| 1155 |
+
properties: node.properties
|
| 1156 |
+
};
|
| 1157 |
+
});
|
| 1158 |
+
|
| 1159 |
+
// 3. Fetch Sample Relationships
|
| 1160 |
+
const relQuery = `
|
| 1161 |
+
MATCH (n)-[r]->(m)
|
| 1162 |
+
RETURN r, elementId(n) as source, elementId(m) as target
|
| 1163 |
+
LIMIT 200
|
| 1164 |
+
`;
|
| 1165 |
+
const relResult = await neo4jService.runQuery(relQuery);
|
| 1166 |
+
|
| 1167 |
+
const visualLinks = relResult.map(row => ({
|
| 1168 |
+
source: row.source, // elementId is string
|
| 1169 |
+
target: row.target, // elementId is string
|
| 1170 |
+
type: row.r.type,
|
| 1171 |
+
id: row.r.elementId // v6: use elementId
|
| 1172 |
+
}));
|
| 1173 |
+
|
| 1174 |
+
// 4. Send Combined Payload
|
| 1175 |
+
res.json({
|
| 1176 |
+
timestamp: new Date().toISOString(),
|
| 1177 |
+
stats: stats,
|
| 1178 |
+
nodes: visualNodes,
|
| 1179 |
+
links: visualLinks,
|
| 1180 |
+
// Backwards compatibility fields
|
| 1181 |
+
totalNodes: stats.nodes,
|
| 1182 |
+
totalRelationships: stats.relationships,
|
| 1183 |
+
importGraph: visualLinks.map(l => ({ from: l.source, to: l.target }))
|
| 1184 |
+
});
|
| 1185 |
+
|
| 1186 |
+
} catch (error: any) {
|
| 1187 |
+
console.error('❌ API Error in /graph/stats:', error);
|
| 1188 |
+
res.status(500).json({
|
| 1189 |
+
error: 'Failed to retrieve neural link data',
|
| 1190 |
+
details: error.message,
|
| 1191 |
+
nodes: [],
|
| 1192 |
+
links: []
|
| 1193 |
+
});
|
| 1194 |
+
}
|
| 1195 |
+
});
|
| 1196 |
+
|
| 1197 |
+
/**
|
| 1198 |
+
* GET /api/codex/status
|
| 1199 |
+
* Get Codex Symbiosis status
|
| 1200 |
+
*/
|
| 1201 |
+
app.get('/api/codex/status', async (_req, res) => {
|
| 1202 |
+
try {
|
| 1203 |
+
const { CODEX_VERSION } = await import('./config/codex.js');
|
| 1204 |
+
|
| 1205 |
+
res.json({
|
| 1206 |
+
version: CODEX_VERSION,
|
| 1207 |
+
status: 'active',
|
| 1208 |
+
injectionPoint: 'LLM Service',
|
| 1209 |
+
principles: [
|
| 1210 |
+
'HUKOMMELSE - Check context before responding',
|
| 1211 |
+
'TRANSPARENS - Explain all actions',
|
| 1212 |
+
'SIKKERHED - Never leak PII without approval',
|
| 1213 |
+
'SAMARBEJDE - Compatible with team patterns',
|
| 1214 |
+
'VÆKST - Suggest improvements when seen',
|
| 1215 |
+
'YDMYGHED - Ask when uncertain',
|
| 1216 |
+
'LOYALITET - Serve The Executive'
|
| 1217 |
+
],
|
| 1218 |
+
message: 'Codex Symbiosis is active. All AI responses are filtered through the ethical framework.'
|
| 1219 |
+
});
|
| 1220 |
+
|
| 1221 |
+
} catch (error: any) {
|
| 1222 |
+
res.status(500).json({ error: 'Failed to get Codex status' });
|
| 1223 |
+
}
|
| 1224 |
+
});
|
| 1225 |
+
|
| 1226 |
+
// ============================================
|
| 1227 |
+
// OMNI-HARVESTER - Knowledge Acquisition API
|
| 1228 |
+
// ============================================
|
| 1229 |
+
const acquisitionRouter = (await import('./routes/acquisition.js')).default;
|
| 1230 |
+
app.use('/api/acquisition', acquisitionRouter);
|
| 1231 |
+
console.log('🌾 Omni-Harvester API mounted at /api/acquisition');
|
| 1232 |
+
|
| 1233 |
+
// Graceful shutdown handler
|
| 1234 |
+
const gracefulShutdown = async (signal: string) => {
|
| 1235 |
+
console.log(`\n🛑 ${signal} received: starting graceful shutdown...`);
|
| 1236 |
+
|
| 1237 |
+
// Stop accepting new connections
|
| 1238 |
+
server.close(() => {
|
| 1239 |
+
console.log(' ✓ HTTP server closed');
|
| 1240 |
+
});
|
| 1241 |
+
|
| 1242 |
+
// Stop scheduled tasks
|
| 1243 |
+
try {
|
| 1244 |
+
dataScheduler.stop();
|
| 1245 |
+
console.log(' ✓ Data scheduler stopped');
|
| 1246 |
+
} catch { /* ignore */ }
|
| 1247 |
+
|
| 1248 |
+
// Stop HansPedder agent
|
| 1249 |
+
try {
|
| 1250 |
+
const { hansPedderAgent } = await import('./services/agent/HansPedderAgentController.js');
|
| 1251 |
+
hansPedderAgent.stop();
|
| 1252 |
+
console.log(' ✓ HansPedder agent stopped');
|
| 1253 |
+
} catch { /* ignore */ }
|
| 1254 |
+
|
| 1255 |
+
// Close Neo4j connections
|
| 1256 |
+
try {
|
| 1257 |
+
const { neo4jService } = await import('./database/Neo4jService.js');
|
| 1258 |
+
await neo4jService.close();
|
| 1259 |
+
console.log(' ✓ Neo4j connection closed');
|
| 1260 |
+
} catch { /* ignore */ }
|
| 1261 |
+
|
| 1262 |
+
// Close SQLite database
|
| 1263 |
+
try {
|
| 1264 |
+
const { closeDatabase } = await import('./database/index.js');
|
| 1265 |
+
closeDatabase();
|
| 1266 |
+
console.log(' ✓ SQLite database closed');
|
| 1267 |
+
} catch { /* ignore */ }
|
| 1268 |
+
|
| 1269 |
+
console.log('✅ Graceful shutdown complete');
|
| 1270 |
+
process.exit(0);
|
| 1271 |
+
};
|
| 1272 |
+
|
| 1273 |
+
// Handle shutdown signals
|
| 1274 |
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
| 1275 |
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
| 1276 |
+
|
| 1277 |
+
// Handle uncaught errors gracefully
|
| 1278 |
+
process.on('uncaughtException', (error) => {
|
| 1279 |
+
console.error('💥 Uncaught Exception:', error);
|
| 1280 |
+
gracefulShutdown('uncaughtException');
|
| 1281 |
+
});
|
| 1282 |
+
|
| 1283 |
+
process.on('unhandledRejection', (reason, promise) => {
|
| 1284 |
+
console.error('💥 Unhandled Rejection at:', promise, 'reason:', reason);
|
| 1285 |
+
// Don't exit on unhandled rejections, just log
|
| 1286 |
+
});
|
| 1287 |
+
|
| 1288 |
+
} catch (error) {
|
| 1289 |
+
console.error('❌ Failed to start server:', error);
|
| 1290 |
+
process.exit(1);
|
| 1291 |
+
}
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
// Start the application
|
| 1295 |
+
startServer();
|
apps/backend/src/mcp/EventBus.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { EventEmitter } from 'events';
|
| 2 |
+
import { persistentEventBus } from '../services/EventBus.js';
|
| 3 |
+
|
| 4 |
+
export type EventType =
|
| 5 |
+
| 'system.alert'
|
| 6 |
+
| 'security.alert'
|
| 7 |
+
| 'agent.decision'
|
| 8 |
+
| 'agent.log'
|
| 9 |
+
| 'mcp.tool.executed'
|
| 10 |
+
| 'autonomous.task.executed'
|
| 11 |
+
| 'taskrecorder.suggestion.created'
|
| 12 |
+
| 'taskrecorder.suggestion.approved'
|
| 13 |
+
| 'taskrecorder.execution.started'
|
| 14 |
+
| 'data:ingested'
|
| 15 |
+
| 'widget:invoke'
|
| 16 |
+
| 'osint:investigation:start'
|
| 17 |
+
| 'threat:hunt:start'
|
| 18 |
+
| 'orchestrator:coordinate:start'
|
| 19 |
+
| 'docgen:powerpoint:create'
|
| 20 |
+
| 'docgen:word:create'
|
| 21 |
+
| 'docgen:excel:create'
|
| 22 |
+
| 'docgen:powerpoint:completed'
|
| 23 |
+
| 'docgen:powerpoint:failed'
|
| 24 |
+
| 'docgen:word:completed'
|
| 25 |
+
| 'docgen:word:failed'
|
| 26 |
+
| 'docgen:excel:completed'
|
| 27 |
+
| 'docgen:excel:failed'
|
| 28 |
+
| 'docgen:powerpoint:created'
|
| 29 |
+
| 'docgen:powerpoint:error'
|
| 30 |
+
| 'devtools:scan:started'
|
| 31 |
+
| 'devtools:scan:completed'
|
| 32 |
+
| 'devtools:scan:failed'
|
| 33 |
+
// Data ingestion events
|
| 34 |
+
| 'ingestion:emails'
|
| 35 |
+
| 'ingestion:news'
|
| 36 |
+
| 'ingestion:documents'
|
| 37 |
+
| 'ingestion:assets'
|
| 38 |
+
| 'threat:detected'
|
| 39 |
+
| 'system:heartbeat'
|
| 40 |
+
| 'system:force-refresh'
|
| 41 |
+
// WebSocket events
|
| 42 |
+
| 'ws:connected'
|
| 43 |
+
| 'ws:disconnected'
|
| 44 |
+
// HansPedder agent events
|
| 45 |
+
| 'hanspedder:test-results'
|
| 46 |
+
| 'hanspedder:nudge'
|
| 47 |
+
| 'hanspedder:fix-reported'
|
| 48 |
+
// System health events
|
| 49 |
+
| 'system:backend-unhealthy'
|
| 50 |
+
| 'system:health-check'
|
| 51 |
+
| 'mcp:reconnect-requested'
|
| 52 |
+
// Prototype generation events
|
| 53 |
+
| 'prototype.generation.started'
|
| 54 |
+
| 'prototype.generation.completed'
|
| 55 |
+
| 'prototype.generation.error'
|
| 56 |
+
| 'prototype.saved'
|
| 57 |
+
| 'prototype.deleted'
|
| 58 |
+
// MCP tool events
|
| 59 |
+
| 'mcp.tool.call'
|
| 60 |
+
| 'mcp.tool.result'
|
| 61 |
+
| 'mcp.tool.error'
|
| 62 |
+
// NudgeService events
|
| 63 |
+
| 'nudge.system_metrics'
|
| 64 |
+
| 'nudge.cycle_complete'
|
| 65 |
+
| 'agent.ping'
|
| 66 |
+
| 'system.activity'
|
| 67 |
+
| 'data.push'
|
| 68 |
+
// Autonomous memory events
|
| 69 |
+
| 'pattern:recorded'
|
| 70 |
+
| 'failure:recorded'
|
| 71 |
+
| 'failure:recurring'
|
| 72 |
+
| 'recovery:completed'
|
| 73 |
+
| 'health:recorded'
|
| 74 |
+
| 'source:health'
|
| 75 |
+
| 'decision:made'
|
| 76 |
+
// Email events
|
| 77 |
+
| 'email:refresh'
|
| 78 |
+
| 'email:new';
|
| 79 |
+
|
| 80 |
+
export interface BaseEvent {
|
| 81 |
+
type: EventType;
|
| 82 |
+
timestamp: string;
|
| 83 |
+
source: string;
|
| 84 |
+
payload: any;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* Unified Event Bus Interface
|
| 89 |
+
* Uses RedisEventBus in production for persistence and scalability
|
| 90 |
+
* Falls back to in-memory EventEmitter in development
|
| 91 |
+
*/
|
| 92 |
+
class MCPEventBus extends EventEmitter {
|
| 93 |
+
constructor() {
|
| 94 |
+
super();
|
| 95 |
+
// init persistent bus (no await needed; it will fallback if unavailable)
|
| 96 |
+
persistentEventBus.init().catch((err) => {
|
| 97 |
+
console.warn('⚠️ Redis Streams not ready, using in-memory bus:', err?.message || err);
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
async initialize(): Promise<void> {
|
| 102 |
+
// persistent bus handles its own initialization
|
| 103 |
+
await persistentEventBus.init();
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
emitEvent(event: BaseEvent) {
|
| 107 |
+
// Publish to stream (persistent) or fallback to memory
|
| 108 |
+
persistentEventBus.publish(event.type, event);
|
| 109 |
+
if (!persistentEventBus.isReady()) {
|
| 110 |
+
// Immediate local delivery for dev/fallback
|
| 111 |
+
super.emit(event.type, event);
|
| 112 |
+
super.emit('*', event);
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Direct emit for convenience (for non-BaseEvent objects)
|
| 117 |
+
emit(type: EventType | '*', ...args: any[]): boolean {
|
| 118 |
+
if (type !== '*') {
|
| 119 |
+
persistentEventBus.publish(type, args[0]);
|
| 120 |
+
}
|
| 121 |
+
if (!persistentEventBus.isReady()) {
|
| 122 |
+
return super.emit(type, ...args);
|
| 123 |
+
}
|
| 124 |
+
return true;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
onEvent(type: EventType | '*', listener: (event: BaseEvent | any) => void) {
|
| 128 |
+
if (type !== '*') {
|
| 129 |
+
persistentEventBus.subscribe(type, listener);
|
| 130 |
+
}
|
| 131 |
+
// Always listen locally for fallback
|
| 132 |
+
this.on(type, listener);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
async shutdown(): Promise<void> {
|
| 136 |
+
await persistentEventBus.shutdown?.();
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
export const eventBus = new MCPEventBus();
|
apps/backend/src/mcp/SmartToolRouter.ts
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ╔═══════════════════════════════════════════════════════════════════════════╗
|
| 3 |
+
* ║ SMART TOOL ROUTER - INTENT-BASED SELECTION ║
|
| 4 |
+
* ║═══════════════════════════════════════════════════════════════════════════║
|
| 5 |
+
* ║ Automatically selects the best MCP tool based on user intent using ║
|
| 6 |
+
* ║ semantic matching and context analysis. ║
|
| 7 |
+
* ║ ║
|
| 8 |
+
* ║ Reduces cognitive load for AI agents by inferring the right tool ║
|
| 9 |
+
* ║ from natural language queries instead of requiring explicit tool names. ║
|
| 10 |
+
* ╚═══════════════════════════════════════════════════════════════════════════╝
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
import { hyperLog } from '../services/HyperLog.js';
|
| 14 |
+
|
| 15 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 16 |
+
// TOOL DEFINITIONS WITH SEMANTIC KEYWORDS
|
| 17 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 18 |
+
|
| 19 |
+
interface ToolDefinition {
|
| 20 |
+
name: string;
|
| 21 |
+
description: string;
|
| 22 |
+
keywords: string[];
|
| 23 |
+
intentPatterns: string[];
|
| 24 |
+
category: 'query' | 'mutation' | 'system' | 'analysis' | 'communication';
|
| 25 |
+
priority: number; // Higher = preferred when multiple match
|
| 26 |
+
payloadTemplate?: Record<string, unknown> | string;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Lightweight domain synonym map to widen recall without heavy models
|
| 30 |
+
const SYNONYMS: Record<string, string[]> = {
|
| 31 |
+
health: ['status', 'alive', 'uptime', 'heartbeat'],
|
| 32 |
+
graph: ['neo4j', 'relationships', 'nodes', 'edges'],
|
| 33 |
+
ingest: ['import', 'harvest', 'index', 'scan'],
|
| 34 |
+
latency: ['delay', 'performance', 'response'],
|
| 35 |
+
prototype: ['wireframe', 'mockup', 'design'],
|
| 36 |
+
chat: ['message', 'notify', 'communicate'],
|
| 37 |
+
archive: ['vidensarkiv', 'library', 'repository'],
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
const TOOL_DEFINITIONS: ToolDefinition[] = [
|
| 41 |
+
// System & Health
|
| 42 |
+
{
|
| 43 |
+
name: 'get_system_health',
|
| 44 |
+
description: 'Get WidgeTDC system health status',
|
| 45 |
+
keywords: ['health', 'status', 'alive', 'running', 'up', 'down', 'working'],
|
| 46 |
+
intentPatterns: ['is the system working', 'check health', 'system status', 'is it up'],
|
| 47 |
+
category: 'system',
|
| 48 |
+
priority: 10,
|
| 49 |
+
payloadTemplate: {},
|
| 50 |
+
},
|
| 51 |
+
|
| 52 |
+
// Knowledge Graph Queries
|
| 53 |
+
{
|
| 54 |
+
name: 'query_knowledge_graph',
|
| 55 |
+
description: 'Query the Neo4j knowledge graph',
|
| 56 |
+
keywords: [
|
| 57 |
+
'find',
|
| 58 |
+
'search',
|
| 59 |
+
'query',
|
| 60 |
+
'graph',
|
| 61 |
+
'neo4j',
|
| 62 |
+
'nodes',
|
| 63 |
+
'relationships',
|
| 64 |
+
'knowledge',
|
| 65 |
+
'entities',
|
| 66 |
+
],
|
| 67 |
+
intentPatterns: [
|
| 68 |
+
'find in graph',
|
| 69 |
+
'search knowledge',
|
| 70 |
+
'query entities',
|
| 71 |
+
'what do we know about',
|
| 72 |
+
'related to',
|
| 73 |
+
],
|
| 74 |
+
category: 'query',
|
| 75 |
+
priority: 8,
|
| 76 |
+
payloadTemplate: { cypher: 'MATCH (n) RETURN n LIMIT 10', params: {} },
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
name: 'get_graph_stats',
|
| 80 |
+
description: 'Get graph statistics',
|
| 81 |
+
keywords: ['stats', 'statistics', 'count', 'how many', 'size', 'graph size'],
|
| 82 |
+
intentPatterns: ['how many nodes', 'graph statistics', 'database size', 'count entities'],
|
| 83 |
+
category: 'query',
|
| 84 |
+
priority: 6,
|
| 85 |
+
payloadTemplate: {},
|
| 86 |
+
},
|
| 87 |
+
|
| 88 |
+
// Graph Mutations
|
| 89 |
+
{
|
| 90 |
+
name: 'graph_mutation',
|
| 91 |
+
description: 'Create or modify graph nodes and relationships',
|
| 92 |
+
keywords: ['create', 'add', 'insert', 'new', 'node', 'relationship', 'connect', 'link'],
|
| 93 |
+
intentPatterns: ['create a node', 'add relationship', 'connect entities', 'insert into graph'],
|
| 94 |
+
category: 'mutation',
|
| 95 |
+
priority: 7,
|
| 96 |
+
payloadTemplate: {
|
| 97 |
+
cypher: 'CREATE (n:Entity {id: $id, name: $name})',
|
| 98 |
+
params: { id: '...', name: '...' },
|
| 99 |
+
},
|
| 100 |
+
},
|
| 101 |
+
|
| 102 |
+
// File Access
|
| 103 |
+
{
|
| 104 |
+
name: 'dropzone_files',
|
| 105 |
+
description: 'Access files in DropZone',
|
| 106 |
+
keywords: ['file', 'files', 'dropzone', 'read', 'list', 'folder', 'document'],
|
| 107 |
+
intentPatterns: ['read file', 'list files', 'whats in dropzone', 'check files'],
|
| 108 |
+
category: 'query',
|
| 109 |
+
priority: 5,
|
| 110 |
+
payloadTemplate: { path: '/', action: 'list' },
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
name: 'vidensarkiv_files',
|
| 114 |
+
description: 'Access knowledge archive files',
|
| 115 |
+
keywords: ['vidensarkiv', 'archive', 'knowledge', 'documents', 'library'],
|
| 116 |
+
intentPatterns: ['check archive', 'knowledge library', 'archived documents'],
|
| 117 |
+
category: 'query',
|
| 118 |
+
priority: 5,
|
| 119 |
+
payloadTemplate: { path: '/', action: 'list' },
|
| 120 |
+
},
|
| 121 |
+
|
| 122 |
+
// Ingestion
|
| 123 |
+
{
|
| 124 |
+
name: 'ingest_knowledge_graph',
|
| 125 |
+
description: 'Ingest repository into knowledge graph',
|
| 126 |
+
keywords: ['ingest', 'import', 'harvest', 'scan', 'index', 'repository', 'codebase'],
|
| 127 |
+
intentPatterns: ['ingest repo', 'scan codebase', 'import to graph', 'harvest knowledge'],
|
| 128 |
+
category: 'mutation',
|
| 129 |
+
priority: 6,
|
| 130 |
+
payloadTemplate: { repoUrl: 'https://github.com/org/repo', branch: 'main' },
|
| 131 |
+
},
|
| 132 |
+
|
| 133 |
+
// Communication
|
| 134 |
+
{
|
| 135 |
+
name: 'neural_chat',
|
| 136 |
+
description: 'Agent-to-agent communication',
|
| 137 |
+
keywords: ['chat', 'message', 'send', 'communicate', 'talk', 'notify', 'channel'],
|
| 138 |
+
intentPatterns: ['send message', 'chat with', 'notify agent', 'communicate'],
|
| 139 |
+
category: 'communication',
|
| 140 |
+
priority: 7,
|
| 141 |
+
payloadTemplate: { to: 'agent-name', message: '...' },
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
name: 'agent_messages',
|
| 145 |
+
description: 'Read or send agent messages',
|
| 146 |
+
keywords: ['inbox', 'outbox', 'messages', 'mail', 'notifications'],
|
| 147 |
+
intentPatterns: ['check messages', 'read inbox', 'send mail'],
|
| 148 |
+
category: 'communication',
|
| 149 |
+
priority: 6,
|
| 150 |
+
payloadTemplate: { action: 'list', limit: 10 },
|
| 151 |
+
},
|
| 152 |
+
|
| 153 |
+
// Task Delegation
|
| 154 |
+
{
|
| 155 |
+
name: 'capability_broker',
|
| 156 |
+
description: 'Delegate tasks to appropriate agents',
|
| 157 |
+
keywords: ['delegate', 'task', 'capability', 'route', 'assign', 'who can'],
|
| 158 |
+
intentPatterns: ['delegate task', 'who can handle', 'route request', 'find agent for'],
|
| 159 |
+
category: 'system',
|
| 160 |
+
priority: 8,
|
| 161 |
+
payloadTemplate: { task: 'describe your task here', priority: 'medium' },
|
| 162 |
+
},
|
| 163 |
+
|
| 164 |
+
// Analysis
|
| 165 |
+
{
|
| 166 |
+
name: 'activate_associative_memory',
|
| 167 |
+
description: 'Cognitive pattern matching across memories',
|
| 168 |
+
keywords: ['remember', 'recall', 'memory', 'associative', 'pattern', 'cognitive'],
|
| 169 |
+
intentPatterns: ['what do we remember', 'recall patterns', 'associative search'],
|
| 170 |
+
category: 'analysis',
|
| 171 |
+
priority: 7,
|
| 172 |
+
payloadTemplate: { query: 'pattern to recall' },
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
name: 'emit_sonar_pulse',
|
| 176 |
+
description: 'Check service latencies and health',
|
| 177 |
+
keywords: ['latency', 'ping', 'sonar', 'response time', 'performance'],
|
| 178 |
+
intentPatterns: ['check latency', 'ping services', 'performance test'],
|
| 179 |
+
category: 'system',
|
| 180 |
+
priority: 5,
|
| 181 |
+
payloadTemplate: { targets: ['neo4j', 'postgres', 'redis'] },
|
| 182 |
+
},
|
| 183 |
+
|
| 184 |
+
// Prototypes
|
| 185 |
+
{
|
| 186 |
+
name: 'prototype_manager',
|
| 187 |
+
description: 'Generate or manage PRD prototypes',
|
| 188 |
+
keywords: ['prototype', 'prd', 'generate', 'design', 'mockup', 'wireframe'],
|
| 189 |
+
intentPatterns: ['generate prototype', 'create design', 'build from prd'],
|
| 190 |
+
category: 'mutation',
|
| 191 |
+
priority: 6,
|
| 192 |
+
payloadTemplate: { prd: 'Paste PRD text here', output: 'wireframe' },
|
| 193 |
+
},
|
| 194 |
+
];
|
| 195 |
+
|
| 196 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 197 |
+
// SMART TOOL ROUTER CLASS
|
| 198 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 199 |
+
|
| 200 |
+
export interface ToolMatch {
|
| 201 |
+
tool: string;
|
| 202 |
+
confidence: number;
|
| 203 |
+
reason: string;
|
| 204 |
+
category: string;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
export interface RouterResult {
|
| 208 |
+
bestMatch: ToolMatch | null;
|
| 209 |
+
alternatives: ToolMatch[];
|
| 210 |
+
query: string;
|
| 211 |
+
processingTimeMs: number;
|
| 212 |
+
suggestions?: ToolMatch[];
|
| 213 |
+
clarifyQuestion?: string;
|
| 214 |
+
payloadTemplate?: Record<string, unknown> | string | null;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
class SmartToolRouter {
|
| 218 |
+
private toolDefs: ToolDefinition[] = TOOL_DEFINITIONS;
|
| 219 |
+
private feedback: Map<string, { success: number; failure: number }> = new Map();
|
| 220 |
+
|
| 221 |
+
/**
|
| 222 |
+
* Route a natural language query to the best matching tool
|
| 223 |
+
*/
|
| 224 |
+
route(query: string): RouterResult {
|
| 225 |
+
const startTime = Date.now();
|
| 226 |
+
const normalizedQuery = query.toLowerCase().trim();
|
| 227 |
+
|
| 228 |
+
// Expand query with synonyms to improve recall without an embedding model
|
| 229 |
+
const baseTokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
| 230 |
+
const expandedTokens: string[] = [];
|
| 231 |
+
for (const token of baseTokens) {
|
| 232 |
+
const syns = SYNONYMS[token];
|
| 233 |
+
if (syns) expandedTokens.push(...syns.map(s => s.toLowerCase()));
|
| 234 |
+
}
|
| 235 |
+
const augmentedQuery = [normalizedQuery, ...expandedTokens].join(' ');
|
| 236 |
+
|
| 237 |
+
if (!normalizedQuery) {
|
| 238 |
+
const fallback = this.getFallbackMatch('Tom forespørgsel - bruger capability_broker');
|
| 239 |
+
return {
|
| 240 |
+
bestMatch: fallback,
|
| 241 |
+
alternatives: [],
|
| 242 |
+
query,
|
| 243 |
+
processingTimeMs: Date.now() - startTime,
|
| 244 |
+
};
|
| 245 |
+
}
|
| 246 |
+
const queryWords = new Set([...baseTokens, ...expandedTokens]);
|
| 247 |
+
|
| 248 |
+
const matches: ToolMatch[] = [];
|
| 249 |
+
|
| 250 |
+
for (const tool of this.toolDefs) {
|
| 251 |
+
let score = 0;
|
| 252 |
+
const reasons: string[] = [];
|
| 253 |
+
|
| 254 |
+
// 1. Keyword matching (40% weight)
|
| 255 |
+
const keywordMatches = tool.keywords.filter(kw => augmentedQuery.includes(kw.toLowerCase()));
|
| 256 |
+
if (keywordMatches.length > 0) {
|
| 257 |
+
score += (keywordMatches.length / tool.keywords.length) * 40;
|
| 258 |
+
reasons.push(`Keywords: ${keywordMatches.join(', ')}`);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// 2. Intent pattern matching (40% weight)
|
| 262 |
+
const patternMatches = tool.intentPatterns.filter(pattern =>
|
| 263 |
+
this.fuzzyMatch(augmentedQuery, pattern.toLowerCase())
|
| 264 |
+
);
|
| 265 |
+
if (patternMatches.length > 0) {
|
| 266 |
+
score += (patternMatches.length / tool.intentPatterns.length) * 40;
|
| 267 |
+
reasons.push(`Intent: ${patternMatches[0]}`);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// 3. Word overlap (15% weight)
|
| 271 |
+
const toolWords = new Set(
|
| 272 |
+
[...tool.keywords, ...tool.name.split('_')].map(w => w.toLowerCase())
|
| 273 |
+
);
|
| 274 |
+
const overlap = [...queryWords].filter(w => toolWords.has(w)).length;
|
| 275 |
+
if (overlap > 0) {
|
| 276 |
+
score += Math.min(overlap * 5, 15);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// 4. Priority boost (5% weight)
|
| 280 |
+
score += (tool.priority / 10) * 5;
|
| 281 |
+
|
| 282 |
+
// 5. Feedback boost (up to ~5 points)
|
| 283 |
+
score += this.getFeedbackBoost(tool.name);
|
| 284 |
+
|
| 285 |
+
if (score > 15) {
|
| 286 |
+
// Minimum threshold
|
| 287 |
+
matches.push({
|
| 288 |
+
tool: tool.name,
|
| 289 |
+
confidence: Math.min(score / 100, 0.99),
|
| 290 |
+
reason: reasons.join('; ') || 'Priority match',
|
| 291 |
+
category: tool.category,
|
| 292 |
+
});
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// Sort by confidence
|
| 297 |
+
matches.sort((a, b) => b.confidence - a.confidence);
|
| 298 |
+
|
| 299 |
+
// Lightweight re-rank to boost semantic coverage and prefer richer reasons
|
| 300 |
+
const reranked = this.rerankCandidates(augmentedQuery, matches);
|
| 301 |
+
|
| 302 |
+
const bestCandidate =
|
| 303 |
+
reranked.length > 0
|
| 304 |
+
? reranked[0]
|
| 305 |
+
: this.getFallbackMatch('Ingen match - fallback med forslag');
|
| 306 |
+
const alternatives =
|
| 307 |
+
reranked.length > 1 ? reranked.slice(1, 4) : this.getSuggestionAlternatives(bestCandidate);
|
| 308 |
+
|
| 309 |
+
const payloadTemplate = bestCandidate ? this.getPayloadTemplate(bestCandidate.tool) : null;
|
| 310 |
+
const clarifyQuestion =
|
| 311 |
+
bestCandidate && bestCandidate.confidence < 0.35
|
| 312 |
+
? 'Uddyb hvad du vil gøre, fx data, system eller filnavn?'
|
| 313 |
+
: undefined;
|
| 314 |
+
|
| 315 |
+
const result: RouterResult = {
|
| 316 |
+
bestMatch: bestCandidate,
|
| 317 |
+
alternatives,
|
| 318 |
+
query,
|
| 319 |
+
processingTimeMs: Date.now() - startTime,
|
| 320 |
+
suggestions: alternatives,
|
| 321 |
+
payloadTemplate,
|
| 322 |
+
clarifyQuestion,
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
// Log routing decision
|
| 326 |
+
if (result.bestMatch) {
|
| 327 |
+
hyperLog.logEvent('TOOL_ROUTED', {
|
| 328 |
+
query: query.substring(0, 100),
|
| 329 |
+
tool: result.bestMatch.tool,
|
| 330 |
+
confidence: result.bestMatch.confidence,
|
| 331 |
+
alternatives: result.alternatives.length,
|
| 332 |
+
});
|
| 333 |
+
} else {
|
| 334 |
+
hyperLog.logEvent('TOOL_ROUTE_FAILED', {
|
| 335 |
+
query: query.substring(0, 100),
|
| 336 |
+
});
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
return result;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/**
|
| 343 |
+
* Fuzzy string matching for intent patterns
|
| 344 |
+
*/
|
| 345 |
+
private fuzzyMatch(query: string, pattern: string): boolean {
|
| 346 |
+
const patternWords = pattern.split(/\s+/);
|
| 347 |
+
const queryLower = query.toLowerCase();
|
| 348 |
+
|
| 349 |
+
// Check if all pattern words appear in query (in any order)
|
| 350 |
+
let matchCount = 0;
|
| 351 |
+
for (const word of patternWords) {
|
| 352 |
+
if (queryLower.includes(word)) {
|
| 353 |
+
matchCount++;
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// Consider match if 60%+ of pattern words found
|
| 358 |
+
return matchCount / patternWords.length >= 0.6;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
/**
|
| 362 |
+
* Get tool suggestions for a category
|
| 363 |
+
*/
|
| 364 |
+
getToolsByCategory(category: ToolDefinition['category']): string[] {
|
| 365 |
+
return this.toolDefs
|
| 366 |
+
.filter(t => t.category === category)
|
| 367 |
+
.sort((a, b) => b.priority - a.priority)
|
| 368 |
+
.map(t => t.name);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
/**
|
| 372 |
+
* Auto-complete tool name from partial input
|
| 373 |
+
*/
|
| 374 |
+
autocomplete(partial: string): string[] {
|
| 375 |
+
const normalizedPartial = partial.toLowerCase();
|
| 376 |
+
return this.toolDefs
|
| 377 |
+
.filter(
|
| 378 |
+
t =>
|
| 379 |
+
t.name.toLowerCase().includes(normalizedPartial) ||
|
| 380 |
+
t.keywords.some(k => k.toLowerCase().includes(normalizedPartial))
|
| 381 |
+
)
|
| 382 |
+
.map(t => t.name);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/**
|
| 386 |
+
* Add custom tool definition (for dynamic tools)
|
| 387 |
+
*/
|
| 388 |
+
registerTool(tool: ToolDefinition): void {
|
| 389 |
+
this.toolDefs.push(tool);
|
| 390 |
+
hyperLog.logEvent('TOOL_REGISTERED', { name: tool.name, category: tool.category });
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/**
|
| 394 |
+
* Allow external feedback to adjust routing preferences over time
|
| 395 |
+
*/
|
| 396 |
+
registerFeedback(toolName: string, outcome: 'success' | 'failure' | 'timeout'): void {
|
| 397 |
+
const stats = this.feedback.get(toolName) || { success: 0, failure: 0 };
|
| 398 |
+
if (outcome === 'success') stats.success += 1;
|
| 399 |
+
else stats.failure += 1;
|
| 400 |
+
this.feedback.set(toolName, stats);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/**
|
| 404 |
+
* Lightweight reranker to reward better coverage and reasons
|
| 405 |
+
*/
|
| 406 |
+
private rerankCandidates(query: string, matches: ToolMatch[]): ToolMatch[] {
|
| 407 |
+
if (!matches.length) return matches;
|
| 408 |
+
const queryTokens = new Set(query.split(/\s+/).filter(Boolean));
|
| 409 |
+
|
| 410 |
+
return [...matches]
|
| 411 |
+
.map(m => {
|
| 412 |
+
const coverage = [...queryTokens].filter(t => m.reason.toLowerCase().includes(t)).length;
|
| 413 |
+
const reasonBonus = Math.min(coverage * 0.01, 0.05);
|
| 414 |
+
const boosted = Math.min(m.confidence + reasonBonus, 0.99);
|
| 415 |
+
return { ...m, confidence: boosted };
|
| 416 |
+
})
|
| 417 |
+
.sort((a, b) => b.confidence - a.confidence);
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
private getPayloadTemplate(toolName: string): Record<string, unknown> | string | null {
|
| 421 |
+
const def = this.toolDefs.find(t => t.name === toolName);
|
| 422 |
+
return def?.payloadTemplate ?? null;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
private getFeedbackBoost(toolName: string): number {
|
| 426 |
+
const stats = this.feedback.get(toolName);
|
| 427 |
+
if (!stats) return 0;
|
| 428 |
+
const total = stats.success + stats.failure;
|
| 429 |
+
if (total === 0) return 0;
|
| 430 |
+
const ratio = stats.success / total;
|
| 431 |
+
return Math.min(ratio * 5, 5); // up to 5 bonus points
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
/**
|
| 435 |
+
* Provide suggestion alternatives when fallback is used
|
| 436 |
+
*/
|
| 437 |
+
private getSuggestionAlternatives(best: ToolMatch | null): ToolMatch[] {
|
| 438 |
+
if (best) return [];
|
| 439 |
+
const topByPriority = [...this.toolDefs].sort((a, b) => b.priority - a.priority).slice(0, 3);
|
| 440 |
+
|
| 441 |
+
return topByPriority.map(t => ({
|
| 442 |
+
tool: t.name,
|
| 443 |
+
confidence: (t.priority / 10) * 0.2,
|
| 444 |
+
reason: 'Suggestion based on priority',
|
| 445 |
+
category: t.category,
|
| 446 |
+
}));
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
/**
|
| 450 |
+
* Fallback tool match when no candidate passes threshold
|
| 451 |
+
*/
|
| 452 |
+
private getFallbackMatch(reason?: string): ToolMatch | null {
|
| 453 |
+
const fallback = this.toolDefs.find(t => t.name === 'capability_broker');
|
| 454 |
+
if (!fallback) return null;
|
| 455 |
+
|
| 456 |
+
return {
|
| 457 |
+
tool: fallback.name,
|
| 458 |
+
confidence: 0.35,
|
| 459 |
+
reason: reason || 'Fallback: capability_broker',
|
| 460 |
+
category: fallback.category,
|
| 461 |
+
};
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 466 |
+
// SINGLETON EXPORT
|
| 467 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 468 |
+
|
| 469 |
+
export const smartToolRouter = new SmartToolRouter();
|
| 470 |
+
|
| 471 |
+
/**
|
| 472 |
+
* Convenience function for quick routing
|
| 473 |
+
*/
|
| 474 |
+
export function routeToTool(query: string): ToolMatch | null {
|
| 475 |
+
return smartToolRouter.route(query).bestMatch;
|
| 476 |
+
}
|
apps/backend/src/mcp/SourceRegistry.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simple Source Registry Implementation
|
| 3 |
+
*
|
| 4 |
+
* Manages available data sources and matches them to query intents
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { QueryIntent, DataSource } from './autonomous/index.js';
|
| 8 |
+
|
| 9 |
+
export class SourceRegistryImpl {
|
| 10 |
+
private sources: Map<string, DataSource> = new Map();
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Register a new data source
|
| 14 |
+
*/
|
| 15 |
+
registerSource(source: DataSource): void {
|
| 16 |
+
this.sources.set(source.name, source);
|
| 17 |
+
console.log(`📌 Registered source: ${source.name} (${source.type})`);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Get sources capable of handling a query intent
|
| 22 |
+
*/
|
| 23 |
+
getCapableSources(intent: QueryIntent): DataSource[] {
|
| 24 |
+
const capable: DataSource[] = [];
|
| 25 |
+
|
| 26 |
+
for (const source of this.sources.values()) {
|
| 27 |
+
if (this.canHandle(source, intent)) {
|
| 28 |
+
capable.push(source);
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return capable;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Get all registered sources
|
| 37 |
+
*/
|
| 38 |
+
getAllSources(): DataSource[] {
|
| 39 |
+
return Array.from(this.sources.values());
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Get source by name
|
| 44 |
+
*/
|
| 45 |
+
getSource(name: string): DataSource | undefined {
|
| 46 |
+
return this.sources.get(name);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Check if source can handle intent
|
| 51 |
+
*/
|
| 52 |
+
private canHandle(source: DataSource, intent: QueryIntent): boolean {
|
| 53 |
+
// Check if source has wildcard capability
|
| 54 |
+
if (source.capabilities.includes('*')) {
|
| 55 |
+
return true;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Check for domain.operation match
|
| 59 |
+
const fullType = `${intent.domain}.${intent.operation}`;
|
| 60 |
+
if (source.capabilities.includes(fullType)) {
|
| 61 |
+
return true;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Check for domain.* match
|
| 65 |
+
const domainWildcard = `${intent.domain}.*`;
|
| 66 |
+
if (source.capabilities.includes(domainWildcard)) {
|
| 67 |
+
return true;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Check for simple type match
|
| 71 |
+
if (source.capabilities.includes(intent.type)) {
|
| 72 |
+
return true;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return false;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Get sources by type
|
| 80 |
+
*/
|
| 81 |
+
getSourcesByType(type: string): DataSource[] {
|
| 82 |
+
return Array.from(this.sources.values())
|
| 83 |
+
.filter(s => s.type === type);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Remove a source
|
| 88 |
+
*/
|
| 89 |
+
unregisterSource(name: string): boolean {
|
| 90 |
+
return this.sources.delete(name);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Clear all sources
|
| 95 |
+
*/
|
| 96 |
+
clear(): void {
|
| 97 |
+
this.sources.clear();
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Singleton instance
|
| 102 |
+
let registryInstance: SourceRegistryImpl | null = null;
|
| 103 |
+
|
| 104 |
+
export function getSourceRegistry(): SourceRegistryImpl {
|
| 105 |
+
if (!registryInstance) {
|
| 106 |
+
registryInstance = new SourceRegistryImpl();
|
| 107 |
+
}
|
| 108 |
+
return registryInstance;
|
| 109 |
+
}
|
apps/backend/src/mcp/autonomous/AutonomousAgent.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Autonomous Connection Agent
|
| 3 |
+
*
|
| 4 |
+
* Main orchestrator that autonomously selects optimal data sources,
|
| 5 |
+
* learns from outcomes, and adapts over time
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 9 |
+
import { CognitiveMemory } from '../memory/CognitiveMemory.js';
|
| 10 |
+
import { DecisionEngine, DataSource, QueryIntent, DecisionResult } from './DecisionEngine.js';
|
| 11 |
+
|
| 12 |
+
export interface DataQuery {
|
| 13 |
+
id?: string;
|
| 14 |
+
type: string;
|
| 15 |
+
domain?: string;
|
| 16 |
+
operation?: string;
|
| 17 |
+
params?: any;
|
| 18 |
+
priority?: 'low' | 'normal' | 'high';
|
| 19 |
+
freshness?: 'stale' | 'normal' | 'realtime';
|
| 20 |
+
widgetId?: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface QueryResult {
|
| 24 |
+
data: any;
|
| 25 |
+
source: string;
|
| 26 |
+
latencyMs: number;
|
| 27 |
+
cached: boolean;
|
| 28 |
+
timestamp: Date;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export interface SourceRegistry {
|
| 32 |
+
getCapableSources(intent: QueryIntent): DataSource[];
|
| 33 |
+
getAllSources(): DataSource[];
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export class AutonomousAgent {
|
| 37 |
+
private memory: CognitiveMemory;
|
| 38 |
+
private decisionEngine: DecisionEngine;
|
| 39 |
+
private registry: SourceRegistry;
|
| 40 |
+
private predictionCache: Map<string, any> = new Map();
|
| 41 |
+
private wsServer: any = null; // WebSocket server for real-time events
|
| 42 |
+
|
| 43 |
+
constructor(
|
| 44 |
+
memory: CognitiveMemory,
|
| 45 |
+
registry: SourceRegistry,
|
| 46 |
+
wsServer?: any
|
| 47 |
+
) {
|
| 48 |
+
this.memory = memory;
|
| 49 |
+
this.decisionEngine = new DecisionEngine(memory);
|
| 50 |
+
this.registry = registry;
|
| 51 |
+
this.wsServer = wsServer || null;
|
| 52 |
+
|
| 53 |
+
console.log('🤖 Autonomous Agent initialized');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Set WebSocket server for real-time event emission
|
| 58 |
+
*/
|
| 59 |
+
setWebSocketServer(server: any): void {
|
| 60 |
+
this.wsServer = server;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Main routing function - autonomously selects best source
|
| 65 |
+
*/
|
| 66 |
+
async route(query: DataQuery): Promise<DataSource> {
|
| 67 |
+
const startTime = Date.now();
|
| 68 |
+
|
| 69 |
+
// 1. Analyze query intent
|
| 70 |
+
const intent = await this.decisionEngine.analyzeIntent(query);
|
| 71 |
+
|
| 72 |
+
// 2. Get candidate sources
|
| 73 |
+
const candidates = this.registry.getCapableSources(intent);
|
| 74 |
+
|
| 75 |
+
if (candidates.length === 0) {
|
| 76 |
+
throw new Error(`No sources available for query type: ${intent.type}`);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// 3. Make intelligent decision
|
| 80 |
+
const decision = await this.decisionEngine.decide(candidates, intent);
|
| 81 |
+
|
| 82 |
+
// 4. Log decision for learning
|
| 83 |
+
await this.logDecision(query, decision, candidates);
|
| 84 |
+
|
| 85 |
+
const decisionTime = Date.now() - startTime;
|
| 86 |
+
console.log(
|
| 87 |
+
`🎯 Selected ${decision.selectedSource.name} ` +
|
| 88 |
+
`(confidence: ${(decision.confidence * 100).toFixed(0)}%, ` +
|
| 89 |
+
`decision: ${decisionTime}ms)`
|
| 90 |
+
);
|
| 91 |
+
console.log(` Reasoning: ${decision.reasoning}`);
|
| 92 |
+
|
| 93 |
+
return decision.selectedSource;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Execute query with selected source and learn from outcome
|
| 98 |
+
* Includes autonomous fallback handling for failures
|
| 99 |
+
*/
|
| 100 |
+
async executeAndLearn(
|
| 101 |
+
query: DataQuery,
|
| 102 |
+
executeFunction: (source: DataSource) => Promise<any>
|
| 103 |
+
): Promise<QueryResult> {
|
| 104 |
+
// Generate unique query ID for tracking if not provided
|
| 105 |
+
const queryId = query.id || uuidv4();
|
| 106 |
+
const startTime = Date.now();
|
| 107 |
+
|
| 108 |
+
// 1. Analyze intent
|
| 109 |
+
const intent = await this.decisionEngine.analyzeIntent(query);
|
| 110 |
+
|
| 111 |
+
// 2. Get candidate sources
|
| 112 |
+
const candidates = this.registry.getCapableSources(intent);
|
| 113 |
+
|
| 114 |
+
if (candidates.length === 0) {
|
| 115 |
+
throw new Error(`No sources available for query type: ${intent.type}`);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// 3. Score and rank sources for fallback strategy
|
| 119 |
+
const rankedSources = await this.decisionEngine.scoreAllSources(candidates, intent);
|
| 120 |
+
|
| 121 |
+
const errors: any[] = [];
|
| 122 |
+
|
| 123 |
+
// 4. Try sources in order (Fallback Loop)
|
| 124 |
+
for (const candidate of rankedSources) {
|
| 125 |
+
const selectedSource = candidate.source;
|
| 126 |
+
|
| 127 |
+
try {
|
| 128 |
+
// Only log if it's a fallback attempt (not the first choice)
|
| 129 |
+
if (errors.length > 0) {
|
| 130 |
+
console.log(`🔄 Fallback: Attempting execution with ${selectedSource.name} (Score: ${candidate.score.toFixed(2)})...`);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// Execute query
|
| 134 |
+
const result = await executeFunction(selectedSource);
|
| 135 |
+
|
| 136 |
+
// Success!
|
| 137 |
+
const latencyMs = Date.now() - startTime;
|
| 138 |
+
|
| 139 |
+
// Log decision (we log the one that actually worked)
|
| 140 |
+
const decision: DecisionResult = {
|
| 141 |
+
selectedSource: selectedSource,
|
| 142 |
+
score: candidate.score,
|
| 143 |
+
confidence: candidate.score,
|
| 144 |
+
reasoning: candidate.reasoning,
|
| 145 |
+
alternatives: rankedSources.filter(s => s.source.name !== selectedSource.name)
|
| 146 |
+
};
|
| 147 |
+
await this.logDecision(query, decision, candidates);
|
| 148 |
+
|
| 149 |
+
// Emit WebSocket event for real-time updates
|
| 150 |
+
if (this.wsServer && this.wsServer.emitAutonomousDecision) {
|
| 151 |
+
this.wsServer.emitAutonomousDecision({
|
| 152 |
+
queryId: queryId,
|
| 153 |
+
selectedSource: selectedSource.name,
|
| 154 |
+
confidence: candidate.score,
|
| 155 |
+
alternatives: rankedSources.slice(1, 4).map(s => s.source.name),
|
| 156 |
+
reasoning: candidate.reasoning,
|
| 157 |
+
latency: latencyMs
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// Learn from successful execution
|
| 162 |
+
await this.memory.recordQuery({
|
| 163 |
+
widgetId: query.widgetId || 'unknown',
|
| 164 |
+
queryType: query.type,
|
| 165 |
+
queryParams: query.params,
|
| 166 |
+
sourceUsed: selectedSource.name,
|
| 167 |
+
latencyMs,
|
| 168 |
+
resultSize: this.estimateSize(result),
|
| 169 |
+
success: true
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
// Log to ProjectMemory for historical tracking
|
| 173 |
+
try {
|
| 174 |
+
const { projectMemory } = await import('../../services/project/ProjectMemory.js');
|
| 175 |
+
projectMemory.logLifecycleEvent({
|
| 176 |
+
eventType: 'other',
|
| 177 |
+
status: 'success',
|
| 178 |
+
details: {
|
| 179 |
+
type: 'agent_decision',
|
| 180 |
+
query: query.type,
|
| 181 |
+
source: selectedSource.name,
|
| 182 |
+
latency: latencyMs,
|
| 183 |
+
confidence: candidate.score
|
| 184 |
+
}
|
| 185 |
+
});
|
| 186 |
+
} catch (error) {
|
| 187 |
+
// Don't fail the query if ProjectMemory logging fails
|
| 188 |
+
console.warn('Failed to log to ProjectMemory:', error);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
data: result,
|
| 193 |
+
source: selectedSource.name,
|
| 194 |
+
latencyMs,
|
| 195 |
+
cached: false,
|
| 196 |
+
timestamp: new Date()
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
} catch (error: any) {
|
| 200 |
+
console.warn(`⚠️ Source ${selectedSource.name} failed: ${error.message}`);
|
| 201 |
+
errors.push({ source: selectedSource.name, error: error.message });
|
| 202 |
+
|
| 203 |
+
// Learn from failure
|
| 204 |
+
await this.memory.recordFailure({
|
| 205 |
+
sourceName: selectedSource.name,
|
| 206 |
+
error,
|
| 207 |
+
queryContext: {
|
| 208 |
+
queryType: query.type,
|
| 209 |
+
queryParams: query.params
|
| 210 |
+
}
|
| 211 |
+
});
|
| 212 |
+
|
| 213 |
+
await this.memory.recordQuery({
|
| 214 |
+
widgetId: query.widgetId || 'unknown',
|
| 215 |
+
queryType: query.type,
|
| 216 |
+
queryParams: query.params,
|
| 217 |
+
sourceUsed: selectedSource.name,
|
| 218 |
+
latencyMs: Date.now() - startTime,
|
| 219 |
+
success: false
|
| 220 |
+
});
|
| 221 |
+
|
| 222 |
+
// Continue to next source...
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// If we get here, ALL sources failed
|
| 227 |
+
throw new Error(`All available sources failed for query ${query.type}. Errors: ${JSON.stringify(errors)}`);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/**
|
| 231 |
+
* Predictive pre-fetching based on learned patterns
|
| 232 |
+
*/
|
| 233 |
+
async predictAndPrefetch(widgetId: string): Promise<void> {
|
| 234 |
+
try {
|
| 235 |
+
// Get widget patterns
|
| 236 |
+
const patterns = await this.memory.getWidgetPatterns(widgetId);
|
| 237 |
+
|
| 238 |
+
if (patterns.timePatterns.length === 0) {
|
| 239 |
+
return; // No patterns learned yet
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
const currentHour = new Date().getHours();
|
| 243 |
+
|
| 244 |
+
// Find pattern for current hour
|
| 245 |
+
const currentPattern = patterns.timePatterns.find(p => p.hour === currentHour);
|
| 246 |
+
|
| 247 |
+
if (!currentPattern || currentPattern.frequency < 5) {
|
| 248 |
+
return; // Not confident enough
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// Predict likely source based on common sources
|
| 252 |
+
const likelySource = patterns.commonSources[0];
|
| 253 |
+
|
| 254 |
+
if (!likelySource) {
|
| 255 |
+
return;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
console.log(
|
| 259 |
+
`🔮 Pre-fetching for ${widgetId} ` +
|
| 260 |
+
`(hour: ${currentHour}, confidence: high)`
|
| 261 |
+
);
|
| 262 |
+
|
| 263 |
+
// Pre-warm cache or connection
|
| 264 |
+
// (Implementation depends on source type)
|
| 265 |
+
this.predictionCache.set(widgetId, {
|
| 266 |
+
source: likelySource,
|
| 267 |
+
timestamp: new Date()
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
} catch (error) {
|
| 271 |
+
console.error('Prediction error:', error);
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/**
|
| 276 |
+
* Continuous learning - runs periodically
|
| 277 |
+
*/
|
| 278 |
+
async learn(): Promise<void> {
|
| 279 |
+
console.log('🎓 Running learning cycle...');
|
| 280 |
+
|
| 281 |
+
try {
|
| 282 |
+
// Analyze decision quality
|
| 283 |
+
await this.analyzeDecisionQuality();
|
| 284 |
+
|
| 285 |
+
// Identify patterns
|
| 286 |
+
await this.identifyPatterns();
|
| 287 |
+
|
| 288 |
+
// Update predictions
|
| 289 |
+
await this.updatePredictions();
|
| 290 |
+
|
| 291 |
+
console.log('✅ Learning cycle complete');
|
| 292 |
+
} catch (error) {
|
| 293 |
+
console.error('Learning cycle error:', error);
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
/**
|
| 298 |
+
* Analyze if past decisions were optimal
|
| 299 |
+
*/
|
| 300 |
+
private async analyzeDecisionQuality(): Promise<void> {
|
| 301 |
+
// Simple heuristic: check success rate of recent decisions
|
| 302 |
+
try {
|
| 303 |
+
const stats = await this.memory.getFailureStatistics();
|
| 304 |
+
console.log(`🧠 Learning: Analyzed decision quality. Recovery rate: ${(stats.overallRecoveryRate * 100).toFixed(1)}%`);
|
| 305 |
+
} catch (e) {
|
| 306 |
+
// Ignore error if stats not available
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/**
|
| 311 |
+
* Identify new patterns in widget usage
|
| 312 |
+
*/
|
| 313 |
+
private async identifyPatterns(): Promise<void> {
|
| 314 |
+
// Analyze query_patterns table to find new time-based patterns,
|
| 315 |
+
// sequence patterns, etc.
|
| 316 |
+
// Store findings in mcp_widget_patterns table
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
/**
|
| 320 |
+
* Update pre-fetch predictions
|
| 321 |
+
*/
|
| 322 |
+
private async updatePredictions(): Promise<void> {
|
| 323 |
+
// Based on identified patterns, update what should be pre-fetched
|
| 324 |
+
// Clear old predictions that are no longer accurate
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/**
|
| 328 |
+
* Log decision for future analysis
|
| 329 |
+
*/
|
| 330 |
+
private async logDecision(
|
| 331 |
+
query: DataQuery,
|
| 332 |
+
decision: DecisionResult,
|
| 333 |
+
_allCandidates: DataSource[]
|
| 334 |
+
): Promise<void> {
|
| 335 |
+
try {
|
| 336 |
+
// Note: This is simplified - full implementation would use proper DB access
|
| 337 |
+
// For now, logging to console
|
| 338 |
+
console.log(`📊 Decision logged: ${decision.selectedSource.name}`);
|
| 339 |
+
} catch (error) {
|
| 340 |
+
console.error('Failed to log decision:', error);
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
/**
|
| 345 |
+
* Estimate result size in bytes
|
| 346 |
+
*/
|
| 347 |
+
private estimateSize(result: any): number {
|
| 348 |
+
try {
|
| 349 |
+
return JSON.stringify(result).length;
|
| 350 |
+
} catch {
|
| 351 |
+
return 0;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/**
|
| 356 |
+
* Get agent statistics
|
| 357 |
+
*/
|
| 358 |
+
async getStats(): Promise<{
|
| 359 |
+
totalDecisions: number;
|
| 360 |
+
averageConfidence: number;
|
| 361 |
+
topSources: { source: string; count: number }[];
|
| 362 |
+
}> {
|
| 363 |
+
// Placeholder - would query decision_log table
|
| 364 |
+
return {
|
| 365 |
+
totalDecisions: 0,
|
| 366 |
+
averageConfidence: 0,
|
| 367 |
+
topSources: []
|
| 368 |
+
};
|
| 369 |
+
}
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/**
|
| 373 |
+
* Start autonomous learning loop
|
| 374 |
+
*/
|
| 375 |
+
export function startAutonomousLearning(agent: AutonomousAgent, intervalMs: number = 300000): void {
|
| 376 |
+
console.log(`🔄 Starting autonomous learning (every ${intervalMs / 1000}s)`);
|
| 377 |
+
|
| 378 |
+
// Run learning cycle periodically
|
| 379 |
+
setInterval(async () => {
|
| 380 |
+
try {
|
| 381 |
+
await agent.learn();
|
| 382 |
+
} catch (error) {
|
| 383 |
+
console.error('Learning cycle failed:', error);
|
| 384 |
+
}
|
| 385 |
+
}, intervalMs);
|
| 386 |
+
|
| 387 |
+
// Run initial learning after 10 seconds
|
| 388 |
+
setTimeout(() => agent.learn(), 10000);
|
| 389 |
+
}
|
apps/backend/src/mcp/autonomous/DecisionEngine.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Decision Engine - AI-Powered Source Selection
|
| 3 |
+
*
|
| 4 |
+
* Makes intelligent decisions about which data source to use
|
| 5 |
+
* based on learned patterns, current health, context, AND semantic relevance
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { CognitiveMemory } from '../memory/CognitiveMemory.js';
|
| 9 |
+
import { getEmbeddingService } from '../../services/embeddings/EmbeddingService.js';
|
| 10 |
+
import { logger } from '../../utils/logger.js';
|
| 11 |
+
|
| 12 |
+
export interface QueryIntent {
|
| 13 |
+
type: string;
|
| 14 |
+
domain: string;
|
| 15 |
+
operation: string;
|
| 16 |
+
params: any;
|
| 17 |
+
priority?: 'low' | 'normal' | 'high';
|
| 18 |
+
freshness?: 'stale' | 'normal' | 'realtime';
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export interface DataSource {
|
| 22 |
+
name: string;
|
| 23 |
+
type: string;
|
| 24 |
+
capabilities: string[];
|
| 25 |
+
description?: string; // Added description for semantic matching
|
| 26 |
+
isHealthy: () => Promise<boolean>;
|
| 27 |
+
estimatedLatency: number;
|
| 28 |
+
costPerQuery: number;
|
| 29 |
+
query?: (operation: string, params: any) => Promise<any>; // Optional query method
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export interface SourceScore {
|
| 33 |
+
source: DataSource;
|
| 34 |
+
score: number;
|
| 35 |
+
breakdown: {
|
| 36 |
+
performance: number;
|
| 37 |
+
reliability: number;
|
| 38 |
+
cost: number;
|
| 39 |
+
freshness: number;
|
| 40 |
+
history: number;
|
| 41 |
+
semantic: number;
|
| 42 |
+
};
|
| 43 |
+
reasoning: string;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export interface DecisionResult {
|
| 47 |
+
selectedSource: DataSource;
|
| 48 |
+
score: number;
|
| 49 |
+
confidence: number;
|
| 50 |
+
reasoning: string;
|
| 51 |
+
alternatives: SourceScore[];
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export class DecisionEngine {
|
| 55 |
+
private memory: CognitiveMemory;
|
| 56 |
+
private embeddings = getEmbeddingService();
|
| 57 |
+
private sourceEmbeddings: Map<string, number[]> = new Map();
|
| 58 |
+
|
| 59 |
+
// Scoring weights (can be tuned based on priority)
|
| 60 |
+
private weights = {
|
| 61 |
+
performance: 0.20,
|
| 62 |
+
reliability: 0.20,
|
| 63 |
+
cost: 0.15,
|
| 64 |
+
freshness: 0.05,
|
| 65 |
+
history: 0.10,
|
| 66 |
+
semantic: 0.30 // High weight for semantic relevance
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
constructor(memory: CognitiveMemory) {
|
| 70 |
+
this.memory = memory;
|
| 71 |
+
this.initializeEmbeddings().catch(err => {
|
| 72 |
+
logger.warn('Failed to initialize source embeddings:', err);
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
private async initializeEmbeddings() {
|
| 77 |
+
// Initialize embedding service (and GPU bridge if applicable)
|
| 78 |
+
await this.embeddings.initialize();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Analyze query intent to understand requirements
|
| 83 |
+
*/
|
| 84 |
+
async analyzeIntent(query: any): Promise<QueryIntent> {
|
| 85 |
+
// Extract intent from query structure
|
| 86 |
+
const intent: QueryIntent = {
|
| 87 |
+
type: query.type || 'unknown',
|
| 88 |
+
domain: query.domain || this.inferDomain(query),
|
| 89 |
+
operation: query.operation || 'read',
|
| 90 |
+
params: query.params || {},
|
| 91 |
+
priority: query.priority || 'normal',
|
| 92 |
+
freshness: query.freshness || 'normal'
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
return intent;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Score all available sources for a query
|
| 100 |
+
*/
|
| 101 |
+
async scoreAllSources(
|
| 102 |
+
sources: DataSource[],
|
| 103 |
+
intent: QueryIntent
|
| 104 |
+
): Promise<SourceScore[]> {
|
| 105 |
+
// Ensure embeddings service is ready
|
| 106 |
+
await this.embeddings.initialize();
|
| 107 |
+
|
| 108 |
+
const scores = await Promise.all(
|
| 109 |
+
sources.map(source => this.scoreSource(source, intent))
|
| 110 |
+
);
|
| 111 |
+
|
| 112 |
+
// Sort by score descending
|
| 113 |
+
return scores.sort((a, b) => b.score - a.score);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Score a single source for this query
|
| 118 |
+
*/
|
| 119 |
+
async scoreSource(
|
| 120 |
+
source: DataSource,
|
| 121 |
+
intent: QueryIntent
|
| 122 |
+
): Promise<SourceScore> {
|
| 123 |
+
// Adjust weights based on priority
|
| 124 |
+
const weights = this.getWeights(intent);
|
| 125 |
+
|
| 126 |
+
// Calculate individual scores
|
| 127 |
+
const performance = await this.scorePerformance(source, intent);
|
| 128 |
+
const reliability = await this.scoreReliability(source, intent);
|
| 129 |
+
const cost = this.scoreCost(source, intent);
|
| 130 |
+
const freshness = this.scoreFreshness(source, intent);
|
| 131 |
+
const history = await this.scoreHistory(source, intent);
|
| 132 |
+
const semantic = await this.scoreSemanticRelevance(source, intent);
|
| 133 |
+
|
| 134 |
+
// Weighted total
|
| 135 |
+
const totalScore =
|
| 136 |
+
performance * weights.performance +
|
| 137 |
+
reliability * weights.reliability +
|
| 138 |
+
cost * weights.cost +
|
| 139 |
+
freshness * weights.freshness +
|
| 140 |
+
history * weights.history +
|
| 141 |
+
semantic * weights.semantic;
|
| 142 |
+
|
| 143 |
+
// Generate reasoning
|
| 144 |
+
const reasoning = this.generateReasoning({
|
| 145 |
+
performance,
|
| 146 |
+
reliability,
|
| 147 |
+
cost,
|
| 148 |
+
freshness,
|
| 149 |
+
history,
|
| 150 |
+
semantic
|
| 151 |
+
}, weights);
|
| 152 |
+
|
| 153 |
+
return {
|
| 154 |
+
source,
|
| 155 |
+
score: totalScore,
|
| 156 |
+
breakdown: {
|
| 157 |
+
performance,
|
| 158 |
+
reliability,
|
| 159 |
+
cost,
|
| 160 |
+
freshness,
|
| 161 |
+
history,
|
| 162 |
+
semantic
|
| 163 |
+
},
|
| 164 |
+
reasoning
|
| 165 |
+
};
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Make final decision from scored sources
|
| 170 |
+
*/
|
| 171 |
+
async decide(
|
| 172 |
+
sources: DataSource[],
|
| 173 |
+
intent: QueryIntent
|
| 174 |
+
): Promise<DecisionResult> {
|
| 175 |
+
const scored = await this.scoreAllSources(sources, intent);
|
| 176 |
+
|
| 177 |
+
if (scored.length === 0) {
|
| 178 |
+
throw new Error('No available sources for this query');
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
const best = scored[0];
|
| 182 |
+
|
| 183 |
+
// Confidence is based on score gap between #1 and #2
|
| 184 |
+
const confidence = scored.length > 1
|
| 185 |
+
? Math.min(1.0, (best.score - scored[1].score) / 0.3 + 0.5)
|
| 186 |
+
: 1.0;
|
| 187 |
+
|
| 188 |
+
return {
|
| 189 |
+
selectedSource: best.source,
|
| 190 |
+
score: best.score,
|
| 191 |
+
confidence,
|
| 192 |
+
reasoning: best.reasoning,
|
| 193 |
+
alternatives: scored.slice(1, 4) // Top 3 alternatives
|
| 194 |
+
};
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/**
|
| 198 |
+
* Score semantic relevance using embeddings
|
| 199 |
+
*/
|
| 200 |
+
private async scoreSemanticRelevance(
|
| 201 |
+
source: DataSource,
|
| 202 |
+
intent: QueryIntent
|
| 203 |
+
): Promise<number> {
|
| 204 |
+
try {
|
| 205 |
+
// 1. Get or generate embedding for source description/capabilities
|
| 206 |
+
let sourceVector = this.sourceEmbeddings.get(source.name);
|
| 207 |
+
if (!sourceVector) {
|
| 208 |
+
const description = `${source.name} ${source.type} ${source.capabilities.join(' ')} ${source.description || ''}`;
|
| 209 |
+
sourceVector = await this.embeddings.generateEmbedding(description);
|
| 210 |
+
this.sourceEmbeddings.set(source.name, sourceVector);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// 2. Generate embedding for query intent
|
| 214 |
+
const queryText = `${intent.type} ${intent.domain} ${intent.operation} ${JSON.stringify(intent.params)}`;
|
| 215 |
+
const queryVector = await this.embeddings.generateEmbedding(queryText);
|
| 216 |
+
|
| 217 |
+
// 3. Calculate cosine similarity
|
| 218 |
+
return this.cosineSimilarity(sourceVector, queryVector);
|
| 219 |
+
|
| 220 |
+
} catch (error) {
|
| 221 |
+
// Fallback to keyword matching if embedding fails
|
| 222 |
+
logger.warn(`Semantic scoring failed for ${source.name}:`, error);
|
| 223 |
+
|
| 224 |
+
// Simple keyword overlap fallback
|
| 225 |
+
const queryStr = JSON.stringify(intent).toLowerCase();
|
| 226 |
+
const capsStr = source.capabilities.join(' ').toLowerCase();
|
| 227 |
+
if (capsStr.includes(intent.type.toLowerCase())) return 0.8;
|
| 228 |
+
if (queryStr.includes(source.name.toLowerCase())) return 0.6;
|
| 229 |
+
|
| 230 |
+
return 0.3; // Default low relevance
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
private cosineSimilarity(vecA: number[], vecB: number[]): number {
|
| 235 |
+
if (vecA.length !== vecB.length) return 0;
|
| 236 |
+
let dot = 0;
|
| 237 |
+
let magA = 0;
|
| 238 |
+
let magB = 0;
|
| 239 |
+
for (let i = 0; i < vecA.length; i++) {
|
| 240 |
+
dot += vecA[i] * vecB[i];
|
| 241 |
+
magA += vecA[i] * vecA[i];
|
| 242 |
+
magB += vecB[i] * vecB[i];
|
| 243 |
+
}
|
| 244 |
+
return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/**
|
| 248 |
+
* Score performance (latency, throughput)
|
| 249 |
+
*/
|
| 250 |
+
private async scorePerformance(
|
| 251 |
+
source: DataSource,
|
| 252 |
+
intent: QueryIntent
|
| 253 |
+
): Promise<number> {
|
| 254 |
+
// Get average latency from memory
|
| 255 |
+
const avgLatency = await this.memory.getAverageLatency(source.name);
|
| 256 |
+
|
| 257 |
+
// Normalize: 0-50ms = 1.0, 500ms+ = 0.0
|
| 258 |
+
const latencyScore = Math.max(0, Math.min(1, 1 - (avgLatency / 500)));
|
| 259 |
+
|
| 260 |
+
// For high priority queries, penalize slow sources more
|
| 261 |
+
if (intent.priority === 'high' && avgLatency > 200) {
|
| 262 |
+
return latencyScore * 0.5;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
return latencyScore;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/**
|
| 269 |
+
* Score reliability (uptime, success rate)
|
| 270 |
+
*/
|
| 271 |
+
private async scoreReliability(
|
| 272 |
+
source: DataSource,
|
| 273 |
+
intent: QueryIntent
|
| 274 |
+
): Promise<number> {
|
| 275 |
+
// Current health check
|
| 276 |
+
const isHealthy = await source.isHealthy();
|
| 277 |
+
if (!isHealthy) {
|
| 278 |
+
return 0.0; // Unhealthy source gets zero score
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// Historical success rate
|
| 282 |
+
const successRate = await this.memory.getSuccessRate(
|
| 283 |
+
source.name,
|
| 284 |
+
intent.type
|
| 285 |
+
);
|
| 286 |
+
|
| 287 |
+
// Get failure intelligence
|
| 288 |
+
const intelligence = await this.memory.getSourceIntelligence(source.name);
|
| 289 |
+
|
| 290 |
+
// Penalize if there were recent failures
|
| 291 |
+
const recentFailurePenalty = Math.min(0.3, intelligence.recentFailures * 0.05);
|
| 292 |
+
|
| 293 |
+
return Math.max(0, successRate - recentFailurePenalty);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/**
|
| 297 |
+
* Score cost (API costs, compute)
|
| 298 |
+
*/
|
| 299 |
+
private scoreCost(source: DataSource, intent: QueryIntent): number {
|
| 300 |
+
const cost = source.costPerQuery || 0;
|
| 301 |
+
|
| 302 |
+
// Normalize: $0 = 1.0, $0.10+ = 0.0
|
| 303 |
+
const costScore = Math.max(0, Math.min(1, 1 - (cost / 0.1)));
|
| 304 |
+
|
| 305 |
+
// For low priority queries, strongly prefer free sources
|
| 306 |
+
if (intent.priority === 'low' && cost > 0) {
|
| 307 |
+
return costScore * 0.5;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
return costScore;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/**
|
| 314 |
+
* Score data freshness
|
| 315 |
+
*/
|
| 316 |
+
private scoreFreshness(source: DataSource, intent: QueryIntent): number {
|
| 317 |
+
// Database sources are typically fresher than cached/file sources
|
| 318 |
+
const freshnessMap: Record<string, number> = {
|
| 319 |
+
'database': 1.0,
|
| 320 |
+
'api': 0.9,
|
| 321 |
+
'cache': 0.5,
|
| 322 |
+
'file': 0.3
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
const baseScore = freshnessMap[source.type] || 0.5;
|
| 326 |
+
|
| 327 |
+
// Adjust based on required freshness
|
| 328 |
+
if (intent.freshness === 'realtime') {
|
| 329 |
+
return source.type === 'database' || source.type === 'api' ? 1.0 : 0.2;
|
| 330 |
+
} else if (intent.freshness === 'stale') {
|
| 331 |
+
return 1.0; // Don't care about freshness
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
return baseScore;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/**
|
| 338 |
+
* Score based on historical patterns
|
| 339 |
+
*/
|
| 340 |
+
private async scoreHistory(
|
| 341 |
+
source: DataSource,
|
| 342 |
+
intent: QueryIntent
|
| 343 |
+
): Promise<number> {
|
| 344 |
+
// Check if this source has been successful for similar queries
|
| 345 |
+
const historyScore = await this.memory.getSimilarQuerySuccess(
|
| 346 |
+
intent.type,
|
| 347 |
+
intent.params
|
| 348 |
+
);
|
| 349 |
+
|
| 350 |
+
return historyScore;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/**
|
| 354 |
+
* Adjust weights based on query intent
|
| 355 |
+
*/
|
| 356 |
+
private getWeights(intent: QueryIntent) {
|
| 357 |
+
const weights = { ...this.weights };
|
| 358 |
+
|
| 359 |
+
// High priority: favor performance and reliability
|
| 360 |
+
if (intent.priority === 'high') {
|
| 361 |
+
weights.performance = 0.30;
|
| 362 |
+
weights.reliability = 0.30;
|
| 363 |
+
weights.semantic = 0.20; // Reduce semantic weight slightly
|
| 364 |
+
weights.cost = 0.10;
|
| 365 |
+
weights.freshness = 0.05;
|
| 366 |
+
weights.history = 0.05;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// Low priority: favor cost
|
| 370 |
+
else if (intent.priority === 'low') {
|
| 371 |
+
weights.performance = 0.10;
|
| 372 |
+
weights.reliability = 0.20;
|
| 373 |
+
weights.semantic = 0.20;
|
| 374 |
+
weights.cost = 0.40;
|
| 375 |
+
weights.freshness = 0.05;
|
| 376 |
+
weights.history = 0.05;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// Realtime freshness: favor databases/APIs
|
| 380 |
+
if (intent.freshness === 'realtime') {
|
| 381 |
+
weights.freshness = 0.30;
|
| 382 |
+
weights.performance = 0.20;
|
| 383 |
+
weights.semantic = 0.20;
|
| 384 |
+
weights.reliability = 0.20;
|
| 385 |
+
weights.cost = 0.10;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
return weights;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
/**
|
| 392 |
+
* Generate human-readable reasoning
|
| 393 |
+
*/
|
| 394 |
+
private generateReasoning(
|
| 395 |
+
breakdown: SourceScore['breakdown'],
|
| 396 |
+
weights: typeof this.weights
|
| 397 |
+
): string {
|
| 398 |
+
const reasons: string[] = [];
|
| 399 |
+
|
| 400 |
+
// Find strongest factor
|
| 401 |
+
const factors = Object.entries(breakdown).sort((a, b) => b[1] - a[1]);
|
| 402 |
+
const [topFactor, topScore] = factors[0];
|
| 403 |
+
|
| 404 |
+
if (topScore > 0.8) {
|
| 405 |
+
reasons.push(`Excellent ${topFactor} (${(topScore * 100).toFixed(0)}%)`);
|
| 406 |
+
} else if (topScore > 0.6) {
|
| 407 |
+
reasons.push(`Good ${topFactor} (${(topScore * 100).toFixed(0)}%)`);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Explicitly mention semantic match if high
|
| 411 |
+
if (breakdown.semantic > 0.8) {
|
| 412 |
+
reasons.push(`Strong conceptual match`);
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
// Note any weak factors
|
| 416 |
+
for (const [factor, score] of factors) {
|
| 417 |
+
if (score < 0.3 && weights[factor as keyof typeof weights] > 0.15) {
|
| 418 |
+
reasons.push(`Low ${factor} (${(score * 100).toFixed(0)}%)`);
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
return reasons.join(', ') || 'Balanced scores across all factors';
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/**
|
| 426 |
+
* Infer domain from query structure
|
| 427 |
+
*/
|
| 428 |
+
private inferDomain(query: any): string {
|
| 429 |
+
// Simple heuristics
|
| 430 |
+
if (query.uri?.startsWith('agents://')) return 'agents';
|
| 431 |
+
if (query.uri?.startsWith('security://')) return 'security';
|
| 432 |
+
if (query.tool?.includes('search')) return 'search';
|
| 433 |
+
if (query.tool?.includes('agent')) return 'agents';
|
| 434 |
+
|
| 435 |
+
return 'general';
|
| 436 |
+
}
|
| 437 |
+
}
|
apps/backend/src/mcp/autonomous/INTEGRATION_GUIDE.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Autonomous MCP System - Integration Guide
|
| 2 |
+
|
| 3 |
+
## Quick Start
|
| 4 |
+
|
| 5 |
+
### 1. Initialize Cognitive Memory
|
| 6 |
+
|
| 7 |
+
```typescript
|
| 8 |
+
import { initializeDatabase, getDatabase } from './database/index.js';
|
| 9 |
+
import { initCognitiveMemory } from './mcp/autonomous/index.js';
|
| 10 |
+
|
| 11 |
+
// Initialize database
|
| 12 |
+
await initializeDatabase();
|
| 13 |
+
const db = getDatabase();
|
| 14 |
+
|
| 15 |
+
// Initialize cognitive memory
|
| 16 |
+
const memory = initCognitiveMemory(db);
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### 2. Create Source Registry
|
| 20 |
+
|
| 21 |
+
```typescript
|
| 22 |
+
import { SourceRegistry, DataSource } from './mcp/autonomous/index.js';
|
| 23 |
+
|
| 24 |
+
class SimpleSourceRegistry implements SourceRegistry {
|
| 25 |
+
private sources: Map<string, DataSource> = new Map();
|
| 26 |
+
|
| 27 |
+
registerSource(source: DataSource) {
|
| 28 |
+
this.sources.set(source.name, source);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
getCapableSources(intent: QueryIntent): DataSource[] {
|
| 32 |
+
// Filter sources that can handle this query
|
| 33 |
+
return Array.from(this.sources.values()).filter(source => {
|
| 34 |
+
// Check if source supports this operation
|
| 35 |
+
return source.capabilities.includes(intent.type) ||
|
| 36 |
+
source.capabilities.includes('*');
|
| 37 |
+
});
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
getAllSources(): DataSource[] {
|
| 41 |
+
return Array.from(this.sources.values());
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const registry = new SimpleSourceRegistry();
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 3. Register Data Sources
|
| 49 |
+
|
| 50 |
+
```typescript
|
| 51 |
+
// Example: PostgreSQL source
|
| 52 |
+
registry.registerSource({
|
| 53 |
+
name: 'postgres-main',
|
| 54 |
+
type: 'database',
|
| 55 |
+
capabilities: ['agents.list', 'agents.get', 'agents.update'],
|
| 56 |
+
isHealthy: async () => {
|
| 57 |
+
try {
|
| 58 |
+
await db.query('SELECT 1');
|
| 59 |
+
return true;
|
| 60 |
+
} catch {
|
| 61 |
+
return false;
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
estimatedLatency: 50,
|
| 65 |
+
costPerQuery: 0
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Example: API source
|
| 69 |
+
registry.registerSource({
|
| 70 |
+
name: 'external-api',
|
| 71 |
+
type: 'api',
|
| 72 |
+
capabilities: ['security.search', 'security.list'],
|
| 73 |
+
isHealthy: async () => {
|
| 74 |
+
const response = await fetch('https://api.example.com/health');
|
| 75 |
+
return response.ok;
|
| 76 |
+
},
|
| 77 |
+
estimatedLatency: 200,
|
| 78 |
+
costPerQuery: 0.01
|
| 79 |
+
});
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### 4. Create Autonomous Agent
|
| 83 |
+
|
| 84 |
+
```typescript
|
| 85 |
+
import { AutonomousAgent, startAutonomousLearning } from './mcp/autonomous/index.js';
|
| 86 |
+
|
| 87 |
+
const agent = new AutonomousAgent(memory, registry);
|
| 88 |
+
|
| 89 |
+
// Start autonomous learning (runs every 5 minutes)
|
| 90 |
+
startAutonomousLearning(agent, 300000);
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### 5. Use in Routes
|
| 94 |
+
|
| 95 |
+
```typescript
|
| 96 |
+
import { mcpRouter } from './mcp/mcpRouter.js';
|
| 97 |
+
|
| 98 |
+
mcpRouter.post('/autonomous/query', async (req, res) => {
|
| 99 |
+
try {
|
| 100 |
+
const query = req.body;
|
| 101 |
+
|
| 102 |
+
// Agent autonomously selects best source and executes
|
| 103 |
+
const result = await agent.executeAndLearn(query, async (source) => {
|
| 104 |
+
// Your execute logic here
|
| 105 |
+
return await yourDataFetcher(source, query);
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
res.json({
|
| 109 |
+
success: true,
|
| 110 |
+
data: result.data,
|
| 111 |
+
meta: {
|
| 112 |
+
source: result.source,
|
| 113 |
+
latency: result.latencyMs,
|
| 114 |
+
cached: result.cached
|
| 115 |
+
}
|
| 116 |
+
});
|
| 117 |
+
} catch (error) {
|
| 118 |
+
res.status(500).json({ error: error.message });
|
| 119 |
+
}
|
| 120 |
+
});
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## Advanced Usage
|
| 124 |
+
|
| 125 |
+
### Wrap Existing Providers with Self-Healing
|
| 126 |
+
|
| 127 |
+
```typescript
|
| 128 |
+
import { SelfHealingAdapter } from './mcp/autonomous/index.js';
|
| 129 |
+
|
| 130 |
+
const primaryProvider: DataProvider = {
|
| 131 |
+
name: 'postgres-main',
|
| 132 |
+
type: 'database',
|
| 133 |
+
query: async (op, params) => { /* ... */ },
|
| 134 |
+
health: async () => ({ healthy: true, score: 1.0 })
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
const fallbackProvider: DataProvider = {
|
| 138 |
+
name: 'postgres-replica',
|
| 139 |
+
type: 'database',
|
| 140 |
+
query: async (op, params) => { /* ... */ },
|
| 141 |
+
health: async () => ({ healthy: true, score: 1.0 })
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
// Wrap with self-healing
|
| 145 |
+
const selfHealing = new SelfHealingAdapter(
|
| 146 |
+
primaryProvider,
|
| 147 |
+
memory,
|
| 148 |
+
fallbackProvider
|
| 149 |
+
);
|
| 150 |
+
|
| 151 |
+
// Now use selfHealing instead of primaryProvider
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Predictive Pre-fetching
|
| 155 |
+
|
| 156 |
+
```typescript
|
| 157 |
+
// Pre-fetch data for a widget before it requests
|
| 158 |
+
await agent.predictAndPrefetch('AgentMonitorWidget');
|
| 159 |
+
|
| 160 |
+
// This analyzes historical patterns and pre-warms likely data
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### Query with Intelligence
|
| 164 |
+
|
| 165 |
+
```typescript
|
| 166 |
+
const result = await agent.executeAndLearn({
|
| 167 |
+
type: 'agents.list',
|
| 168 |
+
widgetId: 'AgentMonitorWidget',
|
| 169 |
+
priority: 'high', // Favor speed over cost
|
| 170 |
+
freshness: 'realtime' // Need fresh data
|
| 171 |
+
}, async (source) => {
|
| 172 |
+
// Your fetch logic
|
| 173 |
+
return await fetchFromSource(source);
|
| 174 |
+
});
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
## Monitoring
|
| 178 |
+
|
| 179 |
+
### Get Agent Statistics
|
| 180 |
+
|
| 181 |
+
```typescript
|
| 182 |
+
const stats = await agent.getStats();
|
| 183 |
+
console.log(`Total decisions: ${stats.totalDecisions}`);
|
| 184 |
+
console.log(`Average confidence: ${(stats.averageConfidence * 100).toFixed(1)}%`);
|
| 185 |
+
console.log(`Top sources:`, stats.topSources);
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
### Get Source Intelligence
|
| 189 |
+
|
| 190 |
+
```typescript
|
| 191 |
+
const intel = await memory.getSourceIntelligence('postgres-main');
|
| 192 |
+
console.log(`Average latency: ${intel.averageLatency}ms`);
|
| 193 |
+
console.log(`Success rate: ${(intel.overallSuccessRate * 100).toFixed(1)}%`);
|
| 194 |
+
console.log(`Recent failures: ${intel.recentFailures}`);
|
| 195 |
+
|
| 196 |
+
if (intel.lastFailure) {
|
| 197 |
+
console.log(`Last failure: ${intel.lastFailure.errorType}`);
|
| 198 |
+
console.log(`Known recovery paths:`, intel.knownRecoveryPaths);
|
| 199 |
+
}
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
### Health Dashboard Data
|
| 203 |
+
|
| 204 |
+
```typescript
|
| 205 |
+
const healthHistory = await memory.getHealthHistory('postgres-main', 100);
|
| 206 |
+
|
| 207 |
+
// Analyze trends
|
| 208 |
+
const latencies = healthHistory.map(h => h.latency.p95);
|
| 209 |
+
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
| 210 |
+
const trend = latencies[0] > latencies[latencies.length - 1] ? 'improving' : 'degrading';
|
| 211 |
+
|
| 212 |
+
console.log(`Average P95 latency: ${avgLatency.toFixed(0)}ms (${trend})`);
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
## Best Practices
|
| 216 |
+
|
| 217 |
+
### 1. Always Initialize Database First
|
| 218 |
+
|
| 219 |
+
```typescript
|
| 220 |
+
// ✅ Correct order
|
| 221 |
+
await initializeDatabase();
|
| 222 |
+
const memory = initCognitiveMemory(getDatabase());
|
| 223 |
+
|
| 224 |
+
// ❌ Wrong - will fail
|
| 225 |
+
const memory = initCognitiveMemory(getDatabase());
|
| 226 |
+
await initializeDatabase();
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
### 2. Register Sources at Startup
|
| 230 |
+
|
| 231 |
+
```typescript
|
| 232 |
+
// Register all sources before starting agent
|
| 233 |
+
registry.registerSource(source1);
|
| 234 |
+
registry.registerSource(source2);
|
| 235 |
+
registry.registerSource(source3);
|
| 236 |
+
|
| 237 |
+
// Then create agent
|
| 238 |
+
const agent = new AutonomousAgent(memory, registry);
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
### 3. Let Agent Learn Before Production
|
| 242 |
+
|
| 243 |
+
```typescript
|
| 244 |
+
// Run in learning mode for 1 week
|
| 245 |
+
const agent = new AutonomousAgent(memory, registry);
|
| 246 |
+
|
| 247 |
+
// Agent observes and learns patterns
|
| 248 |
+
// After 1 week of data, confidence will be high
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### 4. Implement Graceful Fallbacks
|
| 252 |
+
|
| 253 |
+
```typescript
|
| 254 |
+
// Always provide fallback sources
|
| 255 |
+
const adapter = new SelfHealingAdapter(
|
| 256 |
+
primarySource,
|
| 257 |
+
memory,
|
| 258 |
+
fallbackSource // ✅ Always provide this
|
| 259 |
+
);
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
### 5. Monitor Decision Quality
|
| 263 |
+
|
| 264 |
+
```typescript
|
| 265 |
+
// Periodically check if agent is making good decisions
|
| 266 |
+
setInterval(async () => {
|
| 267 |
+
const stats = await agent.getStats();
|
| 268 |
+
|
| 269 |
+
if (stats.averageConfidence < 0.6) {
|
| 270 |
+
console.warn('Low decision confidence - agent needs more data');
|
| 271 |
+
}
|
| 272 |
+
}, 3600000); // Every hour
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
## Troubleshooting
|
| 276 |
+
|
| 277 |
+
### Agent Always Selects Same Source
|
| 278 |
+
|
| 279 |
+
**Problem**: Not enough variety in registered sources or historical data.
|
| 280 |
+
|
| 281 |
+
**Solution**:
|
| 282 |
+
```typescript
|
| 283 |
+
// Check registered sources
|
| 284 |
+
const sources = registry.getAllSources();
|
| 285 |
+
console.log(`Registered sources: ${sources.length}`);
|
| 286 |
+
|
| 287 |
+
// Check historical patterns
|
| 288 |
+
const patterns = await memory.getWidgetPatterns('YourWidget');
|
| 289 |
+
console.log(`Common sources:`, patterns.commonSources);
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
### Self-Healing Not Working
|
| 293 |
+
|
| 294 |
+
**Problem**: Circuit breaker may be stuck open.
|
| 295 |
+
|
| 296 |
+
**Solution**:
|
| 297 |
+
```typescript
|
| 298 |
+
// Check circuit breaker state in logs
|
| 299 |
+
// Look for: "Circuit breaker OPEN"
|
| 300 |
+
|
| 301 |
+
// Manually reset by restarting or adjusting thresholds
|
| 302 |
+
adapter.failureThreshold = 10; // More lenient
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### Memory Growing Too Large
|
| 306 |
+
|
| 307 |
+
**Problem**: Not cleaning old data.
|
| 308 |
+
|
| 309 |
+
**Solution**:
|
| 310 |
+
```typescript
|
| 311 |
+
// Run cleanup periodically
|
| 312 |
+
setInterval(async () => {
|
| 313 |
+
await memory.cleanup(30); // Keep last 30 days
|
| 314 |
+
}, 86400000); // Daily
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
## Next Steps
|
| 318 |
+
|
| 319 |
+
1. **Tune Decision Weights**: Adjust weights in `DecisionEngine` based on your priorities
|
| 320 |
+
2. **Add Custom Recovery Actions**: Extend `SelfHealingAdapter` with domain-specific recovery
|
| 321 |
+
3. **Implement ML Models**: Replace heuristics with trained models for predictions
|
| 322 |
+
4. **Build Admin Dashboard**: Visualize agent decisions and source health
|
| 323 |
+
|
| 324 |
+
## API Reference
|
| 325 |
+
|
| 326 |
+
See individual files for detailed API documentation:
|
| 327 |
+
- `DecisionEngine.ts` - Scoring algorithms
|
| 328 |
+
- `AutonomousAgent.ts` - Main orchestrator
|
| 329 |
+
- `SelfHealingAdapter.ts` - Recovery mechanisms
|
| 330 |
+
- `CognitiveMemory.ts` - Memory interface
|
apps/backend/src/mcp/autonomous/MCPIntegration.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MCP Integration with Autonomous System
|
| 3 |
+
*
|
| 4 |
+
* Auto-registers MCP tools as data sources in the autonomous system
|
| 5 |
+
* for intelligent routing and self-healing
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { mcpRegistry } from '../mcpRegistry.js';
|
| 9 |
+
import { getSourceRegistry } from '../SourceRegistry.js';
|
| 10 |
+
import { DataSource } from './DecisionEngine.js';
|
| 11 |
+
import { OutlookJsonAdapter } from '../../services/external/OutlookJsonAdapter.js';
|
| 12 |
+
import { join } from 'path';
|
| 13 |
+
import { cwd } from 'process';
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Register all MCP tools as autonomous data sources
|
| 17 |
+
*/
|
| 18 |
+
export async function registerMCPToolsAsSources(): Promise<void> {
|
| 19 |
+
const sourceRegistry = getSourceRegistry();
|
| 20 |
+
const tools = mcpRegistry.getRegisteredTools();
|
| 21 |
+
|
| 22 |
+
console.log(`🔗 Registering ${tools.length} MCP tools as autonomous data sources...`);
|
| 23 |
+
|
| 24 |
+
for (const toolName of tools) {
|
| 25 |
+
try {
|
| 26 |
+
// Parse tool name to extract domain
|
| 27 |
+
const [domain, operation] = toolName.split('.');
|
| 28 |
+
const capabilities = [
|
| 29 |
+
toolName,
|
| 30 |
+
`${domain}.*`,
|
| 31 |
+
operation || '*'
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
// Create base data source
|
| 35 |
+
const baseSource: DataSource = {
|
| 36 |
+
name: `mcp-${toolName}`,
|
| 37 |
+
type: 'mcp-tool',
|
| 38 |
+
capabilities,
|
| 39 |
+
isHealthy: async () => {
|
| 40 |
+
// Check if tool is registered
|
| 41 |
+
return mcpRegistry.getRegisteredTools().includes(toolName);
|
| 42 |
+
},
|
| 43 |
+
estimatedLatency: 100, // MCP tools typically fast
|
| 44 |
+
costPerQuery: 0, // MCP tools are free
|
| 45 |
+
query: async (op: string, params: any) => {
|
| 46 |
+
// Route through MCP registry
|
| 47 |
+
// Include operation in payload so handlers can distinguish different operations
|
| 48 |
+
return await mcpRegistry.route({
|
| 49 |
+
id: `auton-${Date.now()}`,
|
| 50 |
+
createdAt: new Date().toISOString(),
|
| 51 |
+
sourceId: 'autonomous-agent',
|
| 52 |
+
targetId: 'mcp-registry',
|
| 53 |
+
tool: toolName,
|
| 54 |
+
payload: {
|
| 55 |
+
...(params || {}),
|
| 56 |
+
operation: op // Include operation parameter for routing
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
// Register as data source directly (self-healing is handled at the adapter level)
|
| 63 |
+
sourceRegistry.registerSource(baseSource);
|
| 64 |
+
|
| 65 |
+
console.log(` ✓ Registered: ${toolName} → mcp-${toolName}`);
|
| 66 |
+
} catch (error: any) {
|
| 67 |
+
console.warn(` ⚠️ Failed to register ${toolName}: ${error.message}`);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
console.log(`✅ Registered ${tools.length} MCP tools as autonomous sources`);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* Register database as data source
|
| 76 |
+
*/
|
| 77 |
+
export async function registerDatabaseSource(): Promise<void> {
|
| 78 |
+
const sourceRegistry = getSourceRegistry();
|
| 79 |
+
const { getDatabase } = await import('../../database/index.js');
|
| 80 |
+
|
| 81 |
+
sourceRegistry.registerSource({
|
| 82 |
+
name: 'database-main',
|
| 83 |
+
type: 'database',
|
| 84 |
+
capabilities: ['*', 'database.*', 'agents.*', 'memory.*', 'srag.*', 'evolution.*', 'pal.*'],
|
| 85 |
+
isHealthy: async () => {
|
| 86 |
+
try {
|
| 87 |
+
const db = getDatabase();
|
| 88 |
+
const stmt = db.prepare('SELECT 1');
|
| 89 |
+
stmt.get();
|
| 90 |
+
stmt.free();
|
| 91 |
+
return true;
|
| 92 |
+
} catch {
|
| 93 |
+
return false;
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
estimatedLatency: 50,
|
| 97 |
+
costPerQuery: 0,
|
| 98 |
+
query: async (_operation: string, _params: any) => {
|
| 99 |
+
// Database queries are handled by repositories
|
| 100 |
+
// This is a placeholder - actual routing happens in repositories
|
| 101 |
+
throw new Error('Database query routing handled by repositories');
|
| 102 |
+
}
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
console.log('📌 Registered database as autonomous source');
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* Register Outlook JSON source
|
| 110 |
+
*/
|
| 111 |
+
export async function registerOutlookSource(): Promise<void> {
|
| 112 |
+
const sourceRegistry = getSourceRegistry();
|
| 113 |
+
// Path to data file
|
| 114 |
+
const dataPath = join(cwd(), 'apps', 'backend', 'data', 'outlook-mails.json');
|
| 115 |
+
|
| 116 |
+
const adapter = new OutlookJsonAdapter(dataPath);
|
| 117 |
+
|
| 118 |
+
sourceRegistry.registerSource({
|
| 119 |
+
name: 'outlook-mail',
|
| 120 |
+
type: 'email-adapter',
|
| 121 |
+
capabilities: ['email.search', 'email.read', 'communication.history'],
|
| 122 |
+
isHealthy: async () => true, // File adapter is always "healthy" if file exists or not (just returns empty)
|
| 123 |
+
estimatedLatency: 20, // Fast local read
|
| 124 |
+
costPerQuery: 0,
|
| 125 |
+
query: async (operation: string, params: any) => {
|
| 126 |
+
return await adapter.query(operation, params);
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
console.log(`📌 Registered Outlook JSON source (path: ${dataPath})`);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/**
|
| 134 |
+
* Initialize all autonomous data sources
|
| 135 |
+
*/
|
| 136 |
+
export async function initializeAutonomousSources(): Promise<void> {
|
| 137 |
+
// Register database first (highest priority for most queries)
|
| 138 |
+
await registerDatabaseSource();
|
| 139 |
+
|
| 140 |
+
// Register Outlook source
|
| 141 |
+
await registerOutlookSource();
|
| 142 |
+
|
| 143 |
+
// Wait a bit for MCP tools to be registered
|
| 144 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 145 |
+
|
| 146 |
+
// Register all MCP tools as sources
|
| 147 |
+
await registerMCPToolsAsSources();
|
| 148 |
+
}
|
apps/backend/src/mcp/autonomous/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Autonomous MCP System - Public API
|
| 3 |
+
*
|
| 4 |
+
* Complete autonomous intelligence system with:
|
| 5 |
+
* - Cognitive Memory (pattern learning, failure memory)
|
| 6 |
+
* - Decision Engine (AI-powered source selection)
|
| 7 |
+
* - Autonomous Agent (main orchestrator)
|
| 8 |
+
* - Self-Healing (via services/SelfHealingAdapter - consolidated)
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
// Memory Layer
|
| 12 |
+
export { CognitiveMemory, initCognitiveMemory, getCognitiveMemory } from '../memory/CognitiveMemory.js';
|
| 13 |
+
export { PatternMemory } from '../memory/PatternMemory.js';
|
| 14 |
+
export { FailureMemory } from '../memory/FailureMemory.js';
|
| 15 |
+
|
| 16 |
+
// Autonomous Intelligence
|
| 17 |
+
export { DecisionEngine } from './DecisionEngine.js';
|
| 18 |
+
export { AutonomousAgent, startAutonomousLearning } from './AutonomousAgent.js';
|
| 19 |
+
|
| 20 |
+
// Self-Healing: Use the consolidated service from services/SelfHealingAdapter.ts
|
| 21 |
+
// import { selfHealing } from '../../services/SelfHealingAdapter.js';
|
| 22 |
+
|
| 23 |
+
// Types
|
| 24 |
+
export type { QueryIntent, DataSource, SourceScore, DecisionResult } from './DecisionEngine.js';
|
| 25 |
+
export type { DataQuery, QueryResult, SourceRegistry } from './AutonomousAgent.js';
|
| 26 |
+
export type { QueryPattern, UsagePattern } from '../memory/PatternMemory.js';
|
| 27 |
+
export type { HealthMetrics } from '../memory/CognitiveMemory.js';
|
apps/backend/src/mcp/autonomousRouter.ts
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Autonomous MCP Router
|
| 3 |
+
*
|
| 4 |
+
* Handles autonomous query routing with AI-powered source selection
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { Router } from 'express';
|
| 8 |
+
import { getCognitiveMemory } from './memory/CognitiveMemory.js';
|
| 9 |
+
import { AutonomousAgent, startAutonomousLearning } from './autonomous/AutonomousAgent.js';
|
| 10 |
+
import { getSourceRegistry } from './SourceRegistry.js';
|
| 11 |
+
import { getDatabase } from '../database/index.js';
|
| 12 |
+
import { eventBus } from './EventBus.js';
|
| 13 |
+
import { hybridSearchEngine } from './cognitive/HybridSearchEngine.js';
|
| 14 |
+
import { emotionAwareDecisionEngine } from './cognitive/EmotionAwareDecisionEngine.js';
|
| 15 |
+
import { unifiedMemorySystem } from './cognitive/UnifiedMemorySystem.js';
|
| 16 |
+
import { unifiedGraphRAG } from './cognitive/UnifiedGraphRAG.js';
|
| 17 |
+
import { stateGraphRouter } from './cognitive/StateGraphRouter.js';
|
| 18 |
+
import { patternEvolutionEngine } from './cognitive/PatternEvolutionEngine.js';
|
| 19 |
+
import { agentTeam } from './cognitive/AgentTeam.js';
|
| 20 |
+
|
| 21 |
+
// WebSocket server for real-time events (will be injected)
|
| 22 |
+
let wsServer: any = null;
|
| 23 |
+
|
| 24 |
+
// Agent instance (declared before setWebSocketServer to avoid race condition)
|
| 25 |
+
let agent: AutonomousAgent | null = null;
|
| 26 |
+
|
| 27 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 28 |
+
// AUTONOMOUS RESPONSE SYSTEM - The Missing Link
|
| 29 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 30 |
+
|
| 31 |
+
interface AutonomousProposal {
|
| 32 |
+
id: string;
|
| 33 |
+
action: string;
|
| 34 |
+
confidence: number;
|
| 35 |
+
reasoning: string;
|
| 36 |
+
suggestedParams: Record<string, any>;
|
| 37 |
+
createdAt: Date;
|
| 38 |
+
status: 'pending' | 'approved' | 'rejected' | 'executed';
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Loop protection
|
| 42 |
+
const recentResponses: Map<string, number> = new Map();
|
| 43 |
+
const RESPONSE_COOLDOWN_MS = 30000; // 30 seconds between same event types
|
| 44 |
+
const MAX_DEPTH = 3; // Maximum nested autonomous actions
|
| 45 |
+
let currentDepth = 0;
|
| 46 |
+
|
| 47 |
+
// Store proposals for user approval
|
| 48 |
+
const pendingProposals: Map<string, AutonomousProposal> = new Map();
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* THE AUTONOMOUS LINK - Triggers autonomous response based on events
|
| 52 |
+
*
|
| 53 |
+
* Decision flow:
|
| 54 |
+
* 1. Analyze incoming event
|
| 55 |
+
* 2. Use AgentTeam/EmotionAware engine to decide action
|
| 56 |
+
* 3. If confidence > 0.8 → Execute automatically
|
| 57 |
+
* 4. If confidence < 0.8 → Create proposal for user
|
| 58 |
+
*/
|
| 59 |
+
async function triggerAutonomousResponse(
|
| 60 |
+
event: any,
|
| 61 |
+
agentInstance: AutonomousAgent
|
| 62 |
+
): Promise<void> {
|
| 63 |
+
const eventType = event.type || 'unknown';
|
| 64 |
+
const eventKey = `${eventType}:${JSON.stringify(event.payload || {}).substring(0, 100)}`;
|
| 65 |
+
|
| 66 |
+
// Loop protection: Check cooldown
|
| 67 |
+
const lastResponse = recentResponses.get(eventKey);
|
| 68 |
+
if (lastResponse && Date.now() - lastResponse < RESPONSE_COOLDOWN_MS) {
|
| 69 |
+
console.log(`⏳ Autonomous response on cooldown for: ${eventType}`);
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Loop protection: Check depth
|
| 74 |
+
if (currentDepth >= MAX_DEPTH) {
|
| 75 |
+
console.log(`🛑 Max autonomous depth reached (${MAX_DEPTH}), stopping chain`);
|
| 76 |
+
return;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
currentDepth++;
|
| 80 |
+
recentResponses.set(eventKey, Date.now());
|
| 81 |
+
|
| 82 |
+
try {
|
| 83 |
+
console.log(`🤖 Autonomous Response triggered for: ${eventType}`);
|
| 84 |
+
|
| 85 |
+
// Step 1: Analyze event and determine action
|
| 86 |
+
const decision = await analyzeEventAndDecide(event);
|
| 87 |
+
|
| 88 |
+
// Step 2: Route based on confidence
|
| 89 |
+
if (decision.confidence >= 0.8) {
|
| 90 |
+
// High confidence - execute automatically
|
| 91 |
+
console.log(`✅ Auto-executing (confidence: ${decision.confidence.toFixed(2)}): ${decision.action}`);
|
| 92 |
+
await executeAutonomousAction(decision, agentInstance);
|
| 93 |
+
|
| 94 |
+
eventBus.emit('agent.decision', {
|
| 95 |
+
type: 'agent.decision',
|
| 96 |
+
timestamp: new Date().toISOString(),
|
| 97 |
+
source: 'autonomousRouter',
|
| 98 |
+
payload: {
|
| 99 |
+
event: eventType,
|
| 100 |
+
decision: decision.action,
|
| 101 |
+
confidence: decision.confidence,
|
| 102 |
+
autoExecuted: true
|
| 103 |
+
}
|
| 104 |
+
});
|
| 105 |
+
} else {
|
| 106 |
+
// Low confidence - create proposal for user
|
| 107 |
+
console.log(`📋 Creating proposal (confidence: ${decision.confidence.toFixed(2)}): ${decision.action}`);
|
| 108 |
+
const proposal = createProposal(decision);
|
| 109 |
+
pendingProposals.set(proposal.id, proposal);
|
| 110 |
+
|
| 111 |
+
eventBus.emit('agent.decision', {
|
| 112 |
+
type: 'agent.decision',
|
| 113 |
+
timestamp: new Date().toISOString(),
|
| 114 |
+
source: 'autonomousRouter',
|
| 115 |
+
payload: {
|
| 116 |
+
event: eventType,
|
| 117 |
+
decision: decision.action,
|
| 118 |
+
confidence: decision.confidence,
|
| 119 |
+
autoExecuted: false,
|
| 120 |
+
proposalId: proposal.id
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
// Notify via WebSocket if available
|
| 125 |
+
if (wsServer) {
|
| 126 |
+
wsServer.broadcast?.({
|
| 127 |
+
type: 'autonomous:proposal',
|
| 128 |
+
proposal
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.error('❌ Autonomous response error:', error);
|
| 134 |
+
} finally {
|
| 135 |
+
currentDepth--;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* Analyze event and decide on action using cognitive systems
|
| 141 |
+
*/
|
| 142 |
+
async function analyzeEventAndDecide(event: any): Promise<{
|
| 143 |
+
action: string;
|
| 144 |
+
confidence: number;
|
| 145 |
+
reasoning: string;
|
| 146 |
+
params: Record<string, any>;
|
| 147 |
+
}> {
|
| 148 |
+
const eventType = event.type || 'unknown';
|
| 149 |
+
const payload = event.payload || {};
|
| 150 |
+
|
| 151 |
+
// Use emotion-aware decision engine for analysis
|
| 152 |
+
try {
|
| 153 |
+
const emotionResult = await emotionAwareDecisionEngine.makeDecision(
|
| 154 |
+
`analyze_event: ${eventType} ${JSON.stringify(payload)}`,
|
| 155 |
+
{ userId: 'system', orgId: 'autonomous' }
|
| 156 |
+
);
|
| 157 |
+
|
| 158 |
+
if (emotionResult.action) {
|
| 159 |
+
return {
|
| 160 |
+
action: `${emotionResult.action.complexity}_action`,
|
| 161 |
+
confidence: emotionResult.confidence || 0.5,
|
| 162 |
+
reasoning: emotionResult.reasoning || 'Emotion-aware analysis',
|
| 163 |
+
params: {}
|
| 164 |
+
};
|
| 165 |
+
}
|
| 166 |
+
} catch {
|
| 167 |
+
// Fallback to rule-based decisions
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// Rule-based fallback decisions
|
| 171 |
+
return getDefaultDecision(eventType, payload);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/**
|
| 175 |
+
* Default rule-based decisions for known event types
|
| 176 |
+
*/
|
| 177 |
+
function getDefaultDecision(eventType: string, payload: any): {
|
| 178 |
+
action: string;
|
| 179 |
+
confidence: number;
|
| 180 |
+
reasoning: string;
|
| 181 |
+
params: Record<string, any>;
|
| 182 |
+
} {
|
| 183 |
+
switch (eventType) {
|
| 184 |
+
case 'system.alert':
|
| 185 |
+
if (payload.severity === 'critical') {
|
| 186 |
+
return {
|
| 187 |
+
action: 'notify_and_escalate',
|
| 188 |
+
confidence: 0.9,
|
| 189 |
+
reasoning: 'Critical system alert requires immediate attention',
|
| 190 |
+
params: { channel: 'all', priority: 'high' }
|
| 191 |
+
};
|
| 192 |
+
}
|
| 193 |
+
return {
|
| 194 |
+
action: 'log_and_monitor',
|
| 195 |
+
confidence: 0.85,
|
| 196 |
+
reasoning: 'Non-critical alert, logging for monitoring',
|
| 197 |
+
params: { logLevel: 'warn' }
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
case 'security.alert':
|
| 201 |
+
return {
|
| 202 |
+
action: 'security_response',
|
| 203 |
+
confidence: payload.severity === 'critical' ? 0.95 : 0.7,
|
| 204 |
+
reasoning: 'Security event detected, initiating response protocol',
|
| 205 |
+
params: { isolate: payload.severity === 'critical', investigate: true }
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
case 'data:ingested':
|
| 209 |
+
return {
|
| 210 |
+
action: 'process_and_index',
|
| 211 |
+
confidence: 0.9,
|
| 212 |
+
reasoning: 'New data ingested, processing for knowledge graph',
|
| 213 |
+
params: { source: payload.source, count: payload.count }
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
case 'threat:detected':
|
| 217 |
+
return {
|
| 218 |
+
action: 'threat_analysis',
|
| 219 |
+
confidence: 0.85,
|
| 220 |
+
reasoning: 'Potential threat detected, running analysis',
|
| 221 |
+
params: { threatType: payload.type, indicators: payload.indicators }
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
default:
|
| 225 |
+
return {
|
| 226 |
+
action: 'observe_and_learn',
|
| 227 |
+
confidence: 0.5,
|
| 228 |
+
reasoning: 'Unknown event type, observing for pattern learning',
|
| 229 |
+
params: { eventType, payload }
|
| 230 |
+
};
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/**
|
| 235 |
+
* Execute autonomous action
|
| 236 |
+
*/
|
| 237 |
+
async function executeAutonomousAction(
|
| 238 |
+
decision: { action: string; params: Record<string, any>; reasoning: string },
|
| 239 |
+
agentInstance: AutonomousAgent
|
| 240 |
+
): Promise<void> {
|
| 241 |
+
const { action, params, reasoning } = decision;
|
| 242 |
+
|
| 243 |
+
console.log(`🚀 Executing: ${action} - ${reasoning}`);
|
| 244 |
+
|
| 245 |
+
switch (action) {
|
| 246 |
+
case 'notify_and_escalate':
|
| 247 |
+
eventBus.emit('system.alert', {
|
| 248 |
+
type: 'system.alert',
|
| 249 |
+
timestamp: new Date().toISOString(),
|
| 250 |
+
source: 'autonomous',
|
| 251 |
+
payload: { message: reasoning, ...params }
|
| 252 |
+
});
|
| 253 |
+
break;
|
| 254 |
+
|
| 255 |
+
case 'log_and_monitor':
|
| 256 |
+
console.log(`[AUTONOMOUS MONITOR] ${reasoning}`);
|
| 257 |
+
break;
|
| 258 |
+
|
| 259 |
+
case 'security_response':
|
| 260 |
+
// Route to security agent in AgentTeam
|
| 261 |
+
await agentTeam.routeMessage({
|
| 262 |
+
from: 'autonomous' as any,
|
| 263 |
+
to: 'sentinel' as any,
|
| 264 |
+
type: 'task',
|
| 265 |
+
content: JSON.stringify({ action: 'investigate', ...params }),
|
| 266 |
+
metadata: { reasoning },
|
| 267 |
+
timestamp: new Date()
|
| 268 |
+
});
|
| 269 |
+
break;
|
| 270 |
+
|
| 271 |
+
case 'process_and_index':
|
| 272 |
+
// Trigger GraphRAG indexing
|
| 273 |
+
await unifiedGraphRAG.query(`Index new data from ${params.source}`, {
|
| 274 |
+
userId: 'system',
|
| 275 |
+
orgId: 'autonomous'
|
| 276 |
+
});
|
| 277 |
+
break;
|
| 278 |
+
|
| 279 |
+
case 'threat_analysis':
|
| 280 |
+
await agentTeam.coordinate({
|
| 281 |
+
type: 'threat_analysis',
|
| 282 |
+
params
|
| 283 |
+
} as any, { autoExecute: true });
|
| 284 |
+
break;
|
| 285 |
+
|
| 286 |
+
case 'observe_and_learn':
|
| 287 |
+
// Store pattern for learning - use working memory update
|
| 288 |
+
await unifiedMemorySystem.updateWorkingMemory(
|
| 289 |
+
{ userId: 'system', orgId: 'autonomous' },
|
| 290 |
+
{ event: `Observed: ${JSON.stringify(params)}`, action }
|
| 291 |
+
);
|
| 292 |
+
break;
|
| 293 |
+
|
| 294 |
+
default:
|
| 295 |
+
console.log(`⚠️ Unknown action: ${action}`);
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/**
|
| 300 |
+
* Create proposal for user approval
|
| 301 |
+
*/
|
| 302 |
+
function createProposal(decision: {
|
| 303 |
+
action: string;
|
| 304 |
+
confidence: number;
|
| 305 |
+
reasoning: string;
|
| 306 |
+
params: Record<string, any>;
|
| 307 |
+
}): AutonomousProposal {
|
| 308 |
+
return {
|
| 309 |
+
id: `proposal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
| 310 |
+
action: decision.action,
|
| 311 |
+
confidence: decision.confidence,
|
| 312 |
+
reasoning: decision.reasoning,
|
| 313 |
+
suggestedParams: decision.params,
|
| 314 |
+
createdAt: new Date(),
|
| 315 |
+
status: 'pending'
|
| 316 |
+
};
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
/**
|
| 320 |
+
* Get pending proposals
|
| 321 |
+
*/
|
| 322 |
+
export function getPendingProposals(): AutonomousProposal[] {
|
| 323 |
+
return Array.from(pendingProposals.values()).filter(p => p.status === 'pending');
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/**
|
| 327 |
+
* Approve and execute a proposal
|
| 328 |
+
*/
|
| 329 |
+
export async function approveProposal(proposalId: string): Promise<boolean> {
|
| 330 |
+
const proposal = pendingProposals.get(proposalId);
|
| 331 |
+
if (!proposal || proposal.status !== 'pending') return false;
|
| 332 |
+
|
| 333 |
+
proposal.status = 'approved';
|
| 334 |
+
|
| 335 |
+
if (agent) {
|
| 336 |
+
await executeAutonomousAction({
|
| 337 |
+
action: proposal.action,
|
| 338 |
+
params: proposal.suggestedParams,
|
| 339 |
+
reasoning: proposal.reasoning
|
| 340 |
+
}, agent);
|
| 341 |
+
proposal.status = 'executed';
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
return true;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/**
|
| 348 |
+
* Reject a proposal
|
| 349 |
+
*/
|
| 350 |
+
export function rejectProposal(proposalId: string): boolean {
|
| 351 |
+
const proposal = pendingProposals.get(proposalId);
|
| 352 |
+
if (!proposal || proposal.status !== 'pending') return false;
|
| 353 |
+
proposal.status = 'rejected';
|
| 354 |
+
return true;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
export function setWebSocketServer(server: any): void {
|
| 358 |
+
wsServer = server;
|
| 359 |
+
// Update agent instance if it already exists
|
| 360 |
+
if (agent) {
|
| 361 |
+
agent.setWebSocketServer(server);
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
export const autonomousRouter = Router();
|
| 366 |
+
|
| 367 |
+
// Re-export for convenience
|
| 368 |
+
export { startAutonomousLearning };
|
| 369 |
+
|
| 370 |
+
/**
|
| 371 |
+
* Initialize agent (called from main server)
|
| 372 |
+
*/
|
| 373 |
+
export function initAutonomousAgent(): AutonomousAgent {
|
| 374 |
+
if (agent) return agent;
|
| 375 |
+
|
| 376 |
+
const memory = getCognitiveMemory();
|
| 377 |
+
const registry = getSourceRegistry();
|
| 378 |
+
|
| 379 |
+
agent = new AutonomousAgent(memory, registry, wsServer);
|
| 380 |
+
|
| 381 |
+
// Listen to system events - THE AUTONOMOUS LINK
|
| 382 |
+
eventBus.onEvent('system.alert', async (event) => {
|
| 383 |
+
if (agent) {
|
| 384 |
+
await triggerAutonomousResponse(event, agent);
|
| 385 |
+
}
|
| 386 |
+
});
|
| 387 |
+
|
| 388 |
+
// Also listen to security alerts
|
| 389 |
+
eventBus.onEvent('security.alert', async (event) => {
|
| 390 |
+
if (agent) {
|
| 391 |
+
await triggerAutonomousResponse(event, agent);
|
| 392 |
+
}
|
| 393 |
+
});
|
| 394 |
+
|
| 395 |
+
console.log('🤖 Autonomous Agent initialized');
|
| 396 |
+
|
| 397 |
+
return agent;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/**
|
| 401 |
+
* Autonomous query endpoint
|
| 402 |
+
*/
|
| 403 |
+
autonomousRouter.post('/query', async (req, res) => {
|
| 404 |
+
if (!agent) {
|
| 405 |
+
return res.status(503).json({
|
| 406 |
+
success: false,
|
| 407 |
+
error: 'Autonomous agent not initialized'
|
| 408 |
+
});
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
try {
|
| 412 |
+
const query = req.body;
|
| 413 |
+
|
| 414 |
+
// Execute with autonomous routing
|
| 415 |
+
const result = await agent.executeAndLearn(query, async (source) => {
|
| 416 |
+
// Simple executor - calls source.query
|
| 417 |
+
if ('query' in source && typeof source.query === 'function') {
|
| 418 |
+
return await source.query(query.operation, query.params);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
throw new Error(`Source ${source.name} does not support query operation`);
|
| 422 |
+
});
|
| 423 |
+
|
| 424 |
+
res.json({
|
| 425 |
+
success: true,
|
| 426 |
+
data: result.data,
|
| 427 |
+
meta: {
|
| 428 |
+
source: result.source,
|
| 429 |
+
latency: result.latencyMs,
|
| 430 |
+
cached: result.cached,
|
| 431 |
+
timestamp: result.timestamp
|
| 432 |
+
}
|
| 433 |
+
});
|
| 434 |
+
} catch (error: any) {
|
| 435 |
+
console.error('Autonomous query error:', error);
|
| 436 |
+
res.status(500).json({
|
| 437 |
+
success: false,
|
| 438 |
+
error: error.message
|
| 439 |
+
});
|
| 440 |
+
}
|
| 441 |
+
});
|
| 442 |
+
|
| 443 |
+
/**
|
| 444 |
+
* Get agent statistics
|
| 445 |
+
*/
|
| 446 |
+
autonomousRouter.get('/stats', async (req, res) => {
|
| 447 |
+
if (!agent) {
|
| 448 |
+
return res.status(503).json({ error: 'Agent not initialized' });
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
try {
|
| 452 |
+
const stats = await agent.getStats();
|
| 453 |
+
res.json(stats);
|
| 454 |
+
} catch (error: any) {
|
| 455 |
+
res.status(500).json({ error: error.message });
|
| 456 |
+
}
|
| 457 |
+
});
|
| 458 |
+
|
| 459 |
+
/**
|
| 460 |
+
* Trigger predictive pre-fetch for a widget
|
| 461 |
+
*/
|
| 462 |
+
autonomousRouter.post('/prefetch/:widgetId', async (req, res) => {
|
| 463 |
+
if (!agent) {
|
| 464 |
+
return res.status(503).json({ error: 'Agent not initialized' });
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
try {
|
| 468 |
+
const { widgetId } = req.params;
|
| 469 |
+
await agent.predictAndPrefetch(widgetId);
|
| 470 |
+
|
| 471 |
+
res.json({
|
| 472 |
+
success: true,
|
| 473 |
+
message: `Pre-fetch triggered for ${widgetId}`
|
| 474 |
+
});
|
| 475 |
+
} catch (error: any) {
|
| 476 |
+
res.status(500).json({ error: error.message });
|
| 477 |
+
}
|
| 478 |
+
});
|
| 479 |
+
|
| 480 |
+
/**
|
| 481 |
+
* List available sources
|
| 482 |
+
*/
|
| 483 |
+
autonomousRouter.get('/sources', async (req, res) => {
|
| 484 |
+
try {
|
| 485 |
+
const registry = getSourceRegistry();
|
| 486 |
+
const sources = registry.getAllSources();
|
| 487 |
+
|
| 488 |
+
const sourcesInfo = await Promise.all(
|
| 489 |
+
sources.map(async (source) => {
|
| 490 |
+
try {
|
| 491 |
+
const health = await source.isHealthy();
|
| 492 |
+
return {
|
| 493 |
+
name: source.name,
|
| 494 |
+
type: source.type,
|
| 495 |
+
capabilities: source.capabilities,
|
| 496 |
+
healthy: health,
|
| 497 |
+
estimatedLatency: source.estimatedLatency,
|
| 498 |
+
costPerQuery: source.costPerQuery
|
| 499 |
+
};
|
| 500 |
+
} catch {
|
| 501 |
+
return {
|
| 502 |
+
name: source.name,
|
| 503 |
+
type: source.type,
|
| 504 |
+
capabilities: source.capabilities,
|
| 505 |
+
healthy: false,
|
| 506 |
+
estimatedLatency: source.estimatedLatency,
|
| 507 |
+
costPerQuery: source.costPerQuery
|
| 508 |
+
};
|
| 509 |
+
}
|
| 510 |
+
})
|
| 511 |
+
);
|
| 512 |
+
|
| 513 |
+
res.json({ sources: sourcesInfo });
|
| 514 |
+
} catch (error: any) {
|
| 515 |
+
res.status(500).json({ error: error.message });
|
| 516 |
+
}
|
| 517 |
+
});
|
| 518 |
+
|
| 519 |
+
/**
|
| 520 |
+
* Get system health
|
| 521 |
+
*/
|
| 522 |
+
/**
|
| 523 |
+
* Get decision history
|
| 524 |
+
*/
|
| 525 |
+
autonomousRouter.get('/decisions', async (req, res) => {
|
| 526 |
+
try {
|
| 527 |
+
const db = getDatabase();
|
| 528 |
+
const limit = parseInt(req.query.limit as string) || 50;
|
| 529 |
+
|
| 530 |
+
const stmt = db.prepare(`
|
| 531 |
+
SELECT * FROM mcp_decision_log
|
| 532 |
+
ORDER BY timestamp DESC
|
| 533 |
+
LIMIT ?
|
| 534 |
+
`);
|
| 535 |
+
// Use variadic parameters for consistency with sqlite3 API
|
| 536 |
+
const decisions = stmt.all(limit);
|
| 537 |
+
stmt.free();
|
| 538 |
+
|
| 539 |
+
res.json({ decisions });
|
| 540 |
+
} catch (error: any) {
|
| 541 |
+
res.status(500).json({ error: error.message });
|
| 542 |
+
}
|
| 543 |
+
});
|
| 544 |
+
|
| 545 |
+
/**
|
| 546 |
+
* Get learned patterns
|
| 547 |
+
*/
|
| 548 |
+
autonomousRouter.get('/patterns', async (req, res) => {
|
| 549 |
+
try {
|
| 550 |
+
const memory = getCognitiveMemory();
|
| 551 |
+
const widgetId = req.query.widgetId as string;
|
| 552 |
+
|
| 553 |
+
if (widgetId) {
|
| 554 |
+
const patterns = await memory.getWidgetPatterns(widgetId);
|
| 555 |
+
res.json({ patterns });
|
| 556 |
+
} else {
|
| 557 |
+
// Get all patterns
|
| 558 |
+
const db = getDatabase();
|
| 559 |
+
const stmt = db.prepare(`
|
| 560 |
+
SELECT DISTINCT widget_id, query_type, source_used,
|
| 561 |
+
AVG(latency_ms) as avg_latency,
|
| 562 |
+
COUNT(*) as frequency
|
| 563 |
+
FROM query_patterns
|
| 564 |
+
GROUP BY widget_id, query_type, source_used
|
| 565 |
+
ORDER BY frequency DESC
|
| 566 |
+
LIMIT 100
|
| 567 |
+
`);
|
| 568 |
+
const patterns = stmt.all();
|
| 569 |
+
stmt.free();
|
| 570 |
+
res.json({ patterns });
|
| 571 |
+
}
|
| 572 |
+
} catch (error: any) {
|
| 573 |
+
res.status(500).json({ error: error.message });
|
| 574 |
+
}
|
| 575 |
+
});
|
| 576 |
+
|
| 577 |
+
/**
|
| 578 |
+
* Trigger manual learning cycle
|
| 579 |
+
*/
|
| 580 |
+
autonomousRouter.post('/learn', async (req, res) => {
|
| 581 |
+
if (!agent) {
|
| 582 |
+
return res.status(503).json({ error: 'Agent not initialized' });
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
try {
|
| 586 |
+
await agent.learn();
|
| 587 |
+
res.json({
|
| 588 |
+
success: true,
|
| 589 |
+
message: 'Learning cycle completed'
|
| 590 |
+
});
|
| 591 |
+
} catch (error: any) {
|
| 592 |
+
res.status(500).json({ error: error.message });
|
| 593 |
+
}
|
| 594 |
+
});
|
| 595 |
+
|
| 596 |
+
/**
|
| 597 |
+
* MCP Tool: Manage Project Memory
|
| 598 |
+
* Allows autonomous agent to document its own actions
|
| 599 |
+
*/
|
| 600 |
+
autonomousRouter.post('/manage_project_memory', async (req, res) => {
|
| 601 |
+
try {
|
| 602 |
+
// Support both flat and nested param formats
|
| 603 |
+
const action = req.body.action;
|
| 604 |
+
const params = req.body.params || req.body;
|
| 605 |
+
|
| 606 |
+
const { eventType, event_type, component_name, status, details, metadata,
|
| 607 |
+
name, description, featureStatus, limit } = params;
|
| 608 |
+
|
| 609 |
+
// Import projectMemory here to avoid circular dependency
|
| 610 |
+
const { projectMemory } = await import('../services/project/ProjectMemory.js');
|
| 611 |
+
|
| 612 |
+
switch (action) {
|
| 613 |
+
case 'log_event':
|
| 614 |
+
const finalEventType = eventType || event_type;
|
| 615 |
+
if (!finalEventType || !status) {
|
| 616 |
+
return res.status(400).json({ error: 'event_type and status required' });
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
// Merge component_name and description into details if provided
|
| 620 |
+
const eventDetails = {
|
| 621 |
+
...(details || {}),
|
| 622 |
+
...(component_name && { component_name }),
|
| 623 |
+
...(description && { description }),
|
| 624 |
+
...(metadata && { metadata })
|
| 625 |
+
};
|
| 626 |
+
|
| 627 |
+
projectMemory.logLifecycleEvent({
|
| 628 |
+
eventType: finalEventType,
|
| 629 |
+
status,
|
| 630 |
+
details: eventDetails
|
| 631 |
+
});
|
| 632 |
+
console.log(`✅ [ProjectMemory] Logged ${finalEventType} event: ${status}`);
|
| 633 |
+
res.json({ success: true, message: 'Event logged', eventType: finalEventType });
|
| 634 |
+
break;
|
| 635 |
+
|
| 636 |
+
case 'add_feature':
|
| 637 |
+
const featureName = name || params.feature_name;
|
| 638 |
+
const featureDesc = description;
|
| 639 |
+
const featureStat = featureStatus || params.status;
|
| 640 |
+
|
| 641 |
+
if (!featureName || !featureDesc || !featureStat) {
|
| 642 |
+
return res.status(400).json({
|
| 643 |
+
error: 'feature_name, description, and status required',
|
| 644 |
+
received: { name: featureName, description: featureDesc, status: featureStat }
|
| 645 |
+
});
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
projectMemory.addFeature({
|
| 649 |
+
name: featureName,
|
| 650 |
+
description: featureDesc,
|
| 651 |
+
status: featureStat
|
| 652 |
+
});
|
| 653 |
+
console.log(`✅ [ProjectMemory] Added feature: ${featureName} (${featureStat})`);
|
| 654 |
+
res.json({ success: true, message: 'Feature added', featureName });
|
| 655 |
+
break;
|
| 656 |
+
|
| 657 |
+
case 'query_history':
|
| 658 |
+
const queryLimit = limit || params.limit || 50;
|
| 659 |
+
const events = projectMemory.getLifecycleEvents(queryLimit);
|
| 660 |
+
res.json({ success: true, events, count: events.length });
|
| 661 |
+
break;
|
| 662 |
+
|
| 663 |
+
case 'update_feature':
|
| 664 |
+
if (!name || !featureStatus) {
|
| 665 |
+
return res.status(400).json({ error: 'name and featureStatus required' });
|
| 666 |
+
}
|
| 667 |
+
projectMemory.updateFeatureStatus(name, featureStatus);
|
| 668 |
+
console.log(`✅ [ProjectMemory] Updated feature: ${name} → ${featureStatus}`);
|
| 669 |
+
res.json({ success: true, message: 'Feature updated' });
|
| 670 |
+
break;
|
| 671 |
+
|
| 672 |
+
default:
|
| 673 |
+
res.status(400).json({
|
| 674 |
+
error: 'Invalid action',
|
| 675 |
+
validActions: ['log_event', 'add_feature', 'query_history', 'update_feature'],
|
| 676 |
+
received: { action, params }
|
| 677 |
+
});
|
| 678 |
+
}
|
| 679 |
+
} catch (error: any) {
|
| 680 |
+
console.error('❌ [ProjectMemory] Error:', error);
|
| 681 |
+
res.status(500).json({ error: error.message, stack: error.stack });
|
| 682 |
+
}
|
| 683 |
+
});
|
| 684 |
+
|
| 685 |
+
/**
|
| 686 |
+
* Hybrid search endpoint
|
| 687 |
+
*/
|
| 688 |
+
autonomousRouter.post('/search', async (req, res) => {
|
| 689 |
+
try {
|
| 690 |
+
const { query, limit, filters } = req.body;
|
| 691 |
+
const userId = (req as any).user?.id || 'anonymous';
|
| 692 |
+
const orgId = (req as any).user?.orgId || 'default';
|
| 693 |
+
|
| 694 |
+
if (!query) {
|
| 695 |
+
return res.status(400).json({ error: 'Query is required' });
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
const results = await hybridSearchEngine.search(query, {
|
| 699 |
+
userId,
|
| 700 |
+
orgId,
|
| 701 |
+
timestamp: new Date(),
|
| 702 |
+
limit: limit || 20,
|
| 703 |
+
filters: filters || {}
|
| 704 |
+
});
|
| 705 |
+
|
| 706 |
+
res.json({
|
| 707 |
+
success: true,
|
| 708 |
+
results,
|
| 709 |
+
count: results.length
|
| 710 |
+
});
|
| 711 |
+
} catch (error: any) {
|
| 712 |
+
console.error('Hybrid search error:', error);
|
| 713 |
+
res.status(500).json({
|
| 714 |
+
success: false,
|
| 715 |
+
error: error.message
|
| 716 |
+
});
|
| 717 |
+
}
|
| 718 |
+
});
|
| 719 |
+
|
| 720 |
+
/**
|
| 721 |
+
* Emotion-aware decision endpoint
|
| 722 |
+
*/
|
| 723 |
+
autonomousRouter.post('/decision', async (req, res) => {
|
| 724 |
+
try {
|
| 725 |
+
const query = req.body;
|
| 726 |
+
const userId = (req as any).user?.id || 'anonymous';
|
| 727 |
+
const orgId = (req as any).user?.orgId || 'default';
|
| 728 |
+
|
| 729 |
+
const decision = await emotionAwareDecisionEngine.makeDecision(query, {
|
| 730 |
+
userId,
|
| 731 |
+
orgId
|
| 732 |
+
});
|
| 733 |
+
|
| 734 |
+
res.json({
|
| 735 |
+
success: true,
|
| 736 |
+
decision
|
| 737 |
+
});
|
| 738 |
+
} catch (error: any) {
|
| 739 |
+
console.error('Emotion-aware decision error:', error);
|
| 740 |
+
res.status(500).json({
|
| 741 |
+
success: false,
|
| 742 |
+
error: error.message
|
| 743 |
+
});
|
| 744 |
+
}
|
| 745 |
+
});
|
| 746 |
+
|
| 747 |
+
/**
|
| 748 |
+
* GraphRAG endpoint - Multi-hop reasoning over knowledge graph
|
| 749 |
+
*/
|
| 750 |
+
autonomousRouter.post('/graphrag', async (req, res) => {
|
| 751 |
+
try {
|
| 752 |
+
const { query, maxHops, context } = req.body;
|
| 753 |
+
const userId = (req as any).user?.id || context?.userId || 'anonymous';
|
| 754 |
+
const orgId = (req as any).user?.orgId || context?.orgId || 'default';
|
| 755 |
+
|
| 756 |
+
if (!query) {
|
| 757 |
+
return res.status(400).json({ error: 'Query is required' });
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
const result = await unifiedGraphRAG.query(query, {
|
| 761 |
+
userId,
|
| 762 |
+
orgId
|
| 763 |
+
});
|
| 764 |
+
|
| 765 |
+
res.json({
|
| 766 |
+
success: true,
|
| 767 |
+
result,
|
| 768 |
+
query,
|
| 769 |
+
maxHops: maxHops || 2
|
| 770 |
+
});
|
| 771 |
+
} catch (error: any) {
|
| 772 |
+
console.error('GraphRAG error:', error);
|
| 773 |
+
res.status(500).json({
|
| 774 |
+
success: false,
|
| 775 |
+
error: error.message
|
| 776 |
+
});
|
| 777 |
+
}
|
| 778 |
+
});
|
| 779 |
+
|
| 780 |
+
/**
|
| 781 |
+
* StateGraphRouter endpoint - LangGraph-style state routing
|
| 782 |
+
*/
|
| 783 |
+
autonomousRouter.post('/stategraph', async (req, res) => {
|
| 784 |
+
try {
|
| 785 |
+
const { taskId, input } = req.body;
|
| 786 |
+
|
| 787 |
+
if (!taskId || !input) {
|
| 788 |
+
return res.status(400).json({ error: 'taskId and input are required' });
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
// Initialize state
|
| 792 |
+
const state = stateGraphRouter.initState(taskId, input);
|
| 793 |
+
|
| 794 |
+
// Route until completion
|
| 795 |
+
let currentState = state;
|
| 796 |
+
let iterations = 0;
|
| 797 |
+
const maxIterations = 20;
|
| 798 |
+
|
| 799 |
+
while (currentState.status === 'active' && iterations < maxIterations) {
|
| 800 |
+
currentState = await stateGraphRouter.route(currentState);
|
| 801 |
+
iterations++;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
res.json({
|
| 805 |
+
success: true,
|
| 806 |
+
state: currentState,
|
| 807 |
+
iterations,
|
| 808 |
+
checkpoints: stateGraphRouter.getCheckpoints(taskId)
|
| 809 |
+
});
|
| 810 |
+
} catch (error: any) {
|
| 811 |
+
console.error('StateGraphRouter error:', error);
|
| 812 |
+
res.status(500).json({
|
| 813 |
+
success: false,
|
| 814 |
+
error: error.message
|
| 815 |
+
});
|
| 816 |
+
}
|
| 817 |
+
});
|
| 818 |
+
|
| 819 |
+
/**
|
| 820 |
+
* PatternEvolutionEngine endpoint - Strategy evolution
|
| 821 |
+
*/
|
| 822 |
+
autonomousRouter.post('/evolve', async (req, res) => {
|
| 823 |
+
try {
|
| 824 |
+
await patternEvolutionEngine.evolveStrategies();
|
| 825 |
+
|
| 826 |
+
const currentStrategy = patternEvolutionEngine.getCurrentStrategy();
|
| 827 |
+
const history = patternEvolutionEngine.getEvolutionHistory();
|
| 828 |
+
|
| 829 |
+
res.json({
|
| 830 |
+
success: true,
|
| 831 |
+
currentStrategy,
|
| 832 |
+
history: history.slice(0, 10), // Last 10 evolutions
|
| 833 |
+
message: 'Evolution cycle completed'
|
| 834 |
+
});
|
| 835 |
+
} catch (error: any) {
|
| 836 |
+
console.error('PatternEvolution error:', error);
|
| 837 |
+
res.status(500).json({
|
| 838 |
+
success: false,
|
| 839 |
+
error: error.message
|
| 840 |
+
});
|
| 841 |
+
}
|
| 842 |
+
});
|
| 843 |
+
|
| 844 |
+
/**
|
| 845 |
+
* Get current evolution strategy
|
| 846 |
+
*/
|
| 847 |
+
autonomousRouter.get('/evolution/strategy', async (req, res) => {
|
| 848 |
+
try {
|
| 849 |
+
const strategy = patternEvolutionEngine.getCurrentStrategy();
|
| 850 |
+
const history = patternEvolutionEngine.getEvolutionHistory();
|
| 851 |
+
|
| 852 |
+
res.json({
|
| 853 |
+
success: true,
|
| 854 |
+
current: strategy,
|
| 855 |
+
history: history.slice(0, 20)
|
| 856 |
+
});
|
| 857 |
+
} catch (error: any) {
|
| 858 |
+
res.status(500).json({
|
| 859 |
+
success: false,
|
| 860 |
+
error: error.message
|
| 861 |
+
});
|
| 862 |
+
}
|
| 863 |
+
});
|
| 864 |
+
|
| 865 |
+
/**
|
| 866 |
+
* AgentTeam endpoint - Route message to role-based agents
|
| 867 |
+
*/
|
| 868 |
+
autonomousRouter.post('/agentteam', async (req, res) => {
|
| 869 |
+
try {
|
| 870 |
+
const { from, to, type, content, metadata } = req.body;
|
| 871 |
+
const userId = (req as any).user?.id || metadata?.userId || 'anonymous';
|
| 872 |
+
const orgId = (req as any).user?.orgId || metadata?.orgId || 'default';
|
| 873 |
+
|
| 874 |
+
if (!content) {
|
| 875 |
+
return res.status(400).json({ error: 'content is required' });
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
const message = {
|
| 879 |
+
from: from || 'user',
|
| 880 |
+
to: to || 'all',
|
| 881 |
+
type: type || 'query',
|
| 882 |
+
content,
|
| 883 |
+
metadata: { ...metadata, userId, orgId },
|
| 884 |
+
timestamp: new Date()
|
| 885 |
+
};
|
| 886 |
+
|
| 887 |
+
const result = await agentTeam.routeMessage(message);
|
| 888 |
+
|
| 889 |
+
res.json({
|
| 890 |
+
success: true,
|
| 891 |
+
result,
|
| 892 |
+
message
|
| 893 |
+
});
|
| 894 |
+
} catch (error: any) {
|
| 895 |
+
console.error('AgentTeam error:', error);
|
| 896 |
+
res.status(500).json({
|
| 897 |
+
success: false,
|
| 898 |
+
error: error.message
|
| 899 |
+
});
|
| 900 |
+
}
|
| 901 |
+
});
|
| 902 |
+
|
| 903 |
+
/**
|
| 904 |
+
* AgentTeam coordination endpoint - Complex multi-agent tasks
|
| 905 |
+
*/
|
| 906 |
+
autonomousRouter.post('/agentteam/coordinate', async (req, res) => {
|
| 907 |
+
try {
|
| 908 |
+
const { task, context } = req.body;
|
| 909 |
+
|
| 910 |
+
if (!task) {
|
| 911 |
+
return res.status(400).json({ error: 'task is required' });
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
const result = await agentTeam.coordinate(task, context);
|
| 915 |
+
|
| 916 |
+
res.json({
|
| 917 |
+
success: true,
|
| 918 |
+
result
|
| 919 |
+
});
|
| 920 |
+
} catch (error: any) {
|
| 921 |
+
console.error('AgentTeam coordination error:', error);
|
| 922 |
+
res.status(500).json({
|
| 923 |
+
success: false,
|
| 924 |
+
error: error.message
|
| 925 |
+
});
|
| 926 |
+
}
|
| 927 |
+
});
|
| 928 |
+
|
| 929 |
+
/**
|
| 930 |
+
* Get AgentTeam status
|
| 931 |
+
*/
|
| 932 |
+
autonomousRouter.get('/agentteam/status', async (req, res) => {
|
| 933 |
+
try {
|
| 934 |
+
const statuses = await agentTeam.getAllStatuses();
|
| 935 |
+
|
| 936 |
+
res.json({
|
| 937 |
+
success: true,
|
| 938 |
+
agents: statuses,
|
| 939 |
+
totalAgents: statuses.length,
|
| 940 |
+
activeAgents: statuses.filter(s => s.active).length
|
| 941 |
+
});
|
| 942 |
+
} catch (error: any) {
|
| 943 |
+
res.status(500).json({
|
| 944 |
+
success: false,
|
| 945 |
+
error: error.message
|
| 946 |
+
});
|
| 947 |
+
}
|
| 948 |
+
});
|
| 949 |
+
|
| 950 |
+
/**
|
| 951 |
+
* Get PAL agent conversation history
|
| 952 |
+
*/
|
| 953 |
+
autonomousRouter.get('/agentteam/pal/history', async (req, res) => {
|
| 954 |
+
try {
|
| 955 |
+
const palAgent = agentTeam.getAgent('pal');
|
| 956 |
+
if (!palAgent) {
|
| 957 |
+
return res.status(404).json({ error: 'PAL agent not found' });
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
// Access conversation history if available
|
| 961 |
+
const history = (palAgent as any).getConversationHistory?.() || [];
|
| 962 |
+
|
| 963 |
+
res.json({
|
| 964 |
+
success: true,
|
| 965 |
+
history,
|
| 966 |
+
count: history.length
|
| 967 |
+
});
|
| 968 |
+
} catch (error: any) {
|
| 969 |
+
res.status(500).json({
|
| 970 |
+
success: false,
|
| 971 |
+
error: error.message
|
| 972 |
+
});
|
| 973 |
+
}
|
| 974 |
+
});
|
| 975 |
+
|
| 976 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 977 |
+
// PROPOSAL MANAGEMENT ENDPOINTS
|
| 978 |
+
// ═══════════════════════════════════════════════════════════════════════════
|
| 979 |
+
|
| 980 |
+
/**
|
| 981 |
+
* Get pending autonomous proposals
|
| 982 |
+
*/
|
| 983 |
+
autonomousRouter.get('/proposals', async (req, res) => {
|
| 984 |
+
try {
|
| 985 |
+
const proposals = getPendingProposals();
|
| 986 |
+
res.json({
|
| 987 |
+
success: true,
|
| 988 |
+
proposals,
|
| 989 |
+
count: proposals.length
|
| 990 |
+
});
|
| 991 |
+
} catch (error: any) {
|
| 992 |
+
res.status(500).json({ success: false, error: error.message });
|
| 993 |
+
}
|
| 994 |
+
});
|
| 995 |
+
|
| 996 |
+
/**
|
| 997 |
+
* Approve a proposal
|
| 998 |
+
*/
|
| 999 |
+
autonomousRouter.post('/proposals/:id/approve', async (req, res) => {
|
| 1000 |
+
try {
|
| 1001 |
+
const success = await approveProposal(req.params.id);
|
| 1002 |
+
if (success) {
|
| 1003 |
+
res.json({ success: true, message: 'Proposal approved and executed' });
|
| 1004 |
+
} else {
|
| 1005 |
+
res.status(404).json({ success: false, error: 'Proposal not found or already processed' });
|
| 1006 |
+
}
|
| 1007 |
+
} catch (error: any) {
|
| 1008 |
+
res.status(500).json({ success: false, error: error.message });
|
| 1009 |
+
}
|
| 1010 |
+
});
|
| 1011 |
+
|
| 1012 |
+
/**
|
| 1013 |
+
* Reject a proposal
|
| 1014 |
+
*/
|
| 1015 |
+
autonomousRouter.post('/proposals/:id/reject', async (req, res) => {
|
| 1016 |
+
try {
|
| 1017 |
+
const success = rejectProposal(req.params.id);
|
| 1018 |
+
if (success) {
|
| 1019 |
+
res.json({ success: true, message: 'Proposal rejected' });
|
| 1020 |
+
} else {
|
| 1021 |
+
res.status(404).json({ success: false, error: 'Proposal not found or already processed' });
|
| 1022 |
+
}
|
| 1023 |
+
} catch (error: any) {
|
| 1024 |
+
res.status(500).json({ success: false, error: error.message });
|
| 1025 |
+
}
|
| 1026 |
+
});
|
| 1027 |
+
|
| 1028 |
+
/**
|
| 1029 |
+
* Get system health with cognitive analysis
|
| 1030 |
+
*/
|
| 1031 |
+
autonomousRouter.get('/health', async (req, res) => {
|
| 1032 |
+
try {
|
| 1033 |
+
const registry = getSourceRegistry();
|
| 1034 |
+
const sources = registry.getAllSources();
|
| 1035 |
+
|
| 1036 |
+
const sourceHealth = await Promise.all(
|
| 1037 |
+
sources.map(async (source) => {
|
| 1038 |
+
try {
|
| 1039 |
+
const healthy = await source.isHealthy();
|
| 1040 |
+
return {
|
| 1041 |
+
name: source.name,
|
| 1042 |
+
healthy,
|
| 1043 |
+
score: healthy ? 1.0 : 0.0
|
| 1044 |
+
};
|
| 1045 |
+
} catch {
|
| 1046 |
+
return {
|
| 1047 |
+
name: source.name,
|
| 1048 |
+
healthy: false,
|
| 1049 |
+
score: 0.0
|
| 1050 |
+
};
|
| 1051 |
+
}
|
| 1052 |
+
})
|
| 1053 |
+
);
|
| 1054 |
+
|
| 1055 |
+
const healthyCount = sourceHealth.filter(s => s.healthy).length;
|
| 1056 |
+
const totalCount = sourceHealth.length;
|
| 1057 |
+
|
| 1058 |
+
// Get cognitive system health
|
| 1059 |
+
const cognitiveHealth = await unifiedMemorySystem.analyzeSystemHealth();
|
| 1060 |
+
|
| 1061 |
+
res.json({
|
| 1062 |
+
status: healthyCount > 0 ? 'healthy' : 'unhealthy',
|
| 1063 |
+
healthySourcesCount: healthyCount,
|
| 1064 |
+
totalSourcesCount: totalCount,
|
| 1065 |
+
sources: sourceHealth,
|
| 1066 |
+
cognitive: {
|
| 1067 |
+
globalHealth: cognitiveHealth.globalHealth,
|
| 1068 |
+
componentHealth: cognitiveHealth.componentHealth,
|
| 1069 |
+
wholePartRatio: cognitiveHealth.wholePartRatio,
|
| 1070 |
+
healthVariance: cognitiveHealth.healthVariance
|
| 1071 |
+
}
|
| 1072 |
+
});
|
| 1073 |
+
} catch (error: any) {
|
| 1074 |
+
res.status(500).json({
|
| 1075 |
+
status: 'error',
|
| 1076 |
+
error: error.message
|
| 1077 |
+
});
|
| 1078 |
+
}
|
| 1079 |
+
});
|
apps/backend/src/mcp/cognitive/AdvancedSearch.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { graphMemoryService } from '../../memory/GraphMemoryService';
|
| 2 |
+
import { neo4jService } from '../../database/Neo4jService';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Query Expansion - Expands user query with synonyms and related terms
|
| 6 |
+
*/
|
| 7 |
+
export class QueryExpander {
|
| 8 |
+
private synonymMap: Map<string, string[]> = new Map([
|
| 9 |
+
['find', ['search', 'locate', 'discover', 'retrieve']],
|
| 10 |
+
['show', ['display', 'present', 'reveal', 'demonstrate']],
|
| 11 |
+
['create', ['make', 'build', 'generate', 'construct']],
|
| 12 |
+
['delete', ['remove', 'erase', 'eliminate', 'destroy']],
|
| 13 |
+
['update', ['modify', 'change', 'alter', 'revise']],
|
| 14 |
+
]);
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Expand query with synonyms and related terms
|
| 18 |
+
*/
|
| 19 |
+
async expandQuery(query: string): Promise<string[]> {
|
| 20 |
+
const words = query.toLowerCase().split(/\s+/);
|
| 21 |
+
const expandedTerms = new Set<string>([query]);
|
| 22 |
+
|
| 23 |
+
// Add synonyms
|
| 24 |
+
words.forEach(word => {
|
| 25 |
+
const synonyms = this.synonymMap.get(word);
|
| 26 |
+
if (synonyms) {
|
| 27 |
+
synonyms.forEach(syn => {
|
| 28 |
+
const expandedQuery = query.replace(new RegExp(word, 'gi'), syn);
|
| 29 |
+
expandedTerms.add(expandedQuery);
|
| 30 |
+
});
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
// Add semantic variations using Neo4j graph
|
| 35 |
+
try {
|
| 36 |
+
await neo4jService.connect();
|
| 37 |
+
|
| 38 |
+
// Find related concepts in the graph
|
| 39 |
+
const relatedConcepts = await neo4jService.runQuery(
|
| 40 |
+
`MATCH (n:Entity)
|
| 41 |
+
WHERE toLower(n.name) CONTAINS $query OR toLower(n.content) CONTAINS $query
|
| 42 |
+
MATCH (n)-[:RELATED_TO|SIMILAR_TO]-(related)
|
| 43 |
+
RETURN DISTINCT related.name as concept
|
| 44 |
+
LIMIT 5`,
|
| 45 |
+
{ query: query.toLowerCase() }
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
relatedConcepts.forEach(record => {
|
| 49 |
+
if (record.concept) {
|
| 50 |
+
expandedTerms.add(`${query} ${record.concept}`);
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
await neo4jService.disconnect();
|
| 55 |
+
} catch (error) {
|
| 56 |
+
console.warn('Query expansion from graph failed:', error);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return Array.from(expandedTerms);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Extract key phrases from query
|
| 64 |
+
*/
|
| 65 |
+
extractKeyPhrases(query: string): string[] {
|
| 66 |
+
// Simple n-gram extraction (2-3 words)
|
| 67 |
+
const words = query.toLowerCase().split(/\s+/);
|
| 68 |
+
const phrases: string[] = [];
|
| 69 |
+
|
| 70 |
+
// Bigrams
|
| 71 |
+
for (let i = 0; i < words.length - 1; i++) {
|
| 72 |
+
phrases.push(`${words[i]} ${words[i + 1]}`);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Trigrams
|
| 76 |
+
for (let i = 0; i < words.length - 2; i++) {
|
| 77 |
+
phrases.push(`${words[i]} ${words[i + 1]} ${words[i + 2]}`);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return phrases;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/**
|
| 85 |
+
* Result Re-ranker - Re-ranks search results using multiple signals
|
| 86 |
+
*/
|
| 87 |
+
export class ResultReRanker {
|
| 88 |
+
/**
|
| 89 |
+
* Re-rank results using multiple scoring signals
|
| 90 |
+
*/
|
| 91 |
+
async rerank(
|
| 92 |
+
query: string,
|
| 93 |
+
results: Array<{ id: string; content: string; score: number; metadata?: any }>,
|
| 94 |
+
options: {
|
| 95 |
+
useRecency?: boolean;
|
| 96 |
+
usePopularity?: boolean;
|
| 97 |
+
useSemanticSimilarity?: boolean;
|
| 98 |
+
} = {}
|
| 99 |
+
): Promise<Array<{ id: string; content: string; score: number; metadata?: any }>> {
|
| 100 |
+
const scoredResults = await Promise.all(
|
| 101 |
+
results.map(async result => {
|
| 102 |
+
let finalScore = result.score;
|
| 103 |
+
|
| 104 |
+
// Recency boost
|
| 105 |
+
if (options.useRecency && result.metadata?.createdAt) {
|
| 106 |
+
const age = Date.now() - new Date(result.metadata.createdAt).getTime();
|
| 107 |
+
const daysSinceCreation = age / (1000 * 60 * 60 * 24);
|
| 108 |
+
const recencyBoost = Math.exp(-daysSinceCreation / 30); // Decay over 30 days
|
| 109 |
+
finalScore *= (1 + recencyBoost * 0.2);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Popularity boost (based on connections in graph)
|
| 113 |
+
if (options.usePopularity) {
|
| 114 |
+
try {
|
| 115 |
+
await neo4jService.connect();
|
| 116 |
+
const connections = await neo4jService.getNodeRelationships(result.id);
|
| 117 |
+
const popularityBoost = Math.min(connections.length / 10, 1); // Cap at 10 connections
|
| 118 |
+
finalScore *= (1 + popularityBoost * 0.3);
|
| 119 |
+
await neo4jService.disconnect();
|
| 120 |
+
} catch (error) {
|
| 121 |
+
// Ignore errors
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Exact match boost
|
| 126 |
+
if (result.content.toLowerCase().includes(query.toLowerCase())) {
|
| 127 |
+
finalScore *= 1.5;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
return { ...result, score: finalScore };
|
| 131 |
+
})
|
| 132 |
+
);
|
| 133 |
+
|
| 134 |
+
// Sort by final score
|
| 135 |
+
return scoredResults.sort((a, b) => b.score - a.score);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Diversify results to avoid redundancy
|
| 140 |
+
*/
|
| 141 |
+
diversify(
|
| 142 |
+
results: Array<{ id: string; content: string; score: number }>,
|
| 143 |
+
maxSimilarity: number = 0.8
|
| 144 |
+
): Array<{ id: string; content: string; score: number }> {
|
| 145 |
+
const diversified: typeof results = [];
|
| 146 |
+
|
| 147 |
+
for (const result of results) {
|
| 148 |
+
// Check if too similar to already selected results
|
| 149 |
+
const tooSimilar = diversified.some(selected => {
|
| 150 |
+
const similarity = this.computeTextSimilarity(result.content, selected.content);
|
| 151 |
+
return similarity > maxSimilarity;
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
if (!tooSimilar) {
|
| 155 |
+
diversified.push(result);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Stop if we have enough diverse results
|
| 159 |
+
if (diversified.length >= 10) break;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
return diversified;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
private computeTextSimilarity(text1: string, text2: string): number {
|
| 166 |
+
const words1 = new Set(text1.toLowerCase().split(/\s+/));
|
| 167 |
+
const words2 = new Set(text2.toLowerCase().split(/\s+/));
|
| 168 |
+
|
| 169 |
+
const intersection = new Set([...words1].filter(x => words2.has(x)));
|
| 170 |
+
const union = new Set([...words1, ...words2]);
|
| 171 |
+
|
| 172 |
+
return intersection.size / union.size; // Jaccard similarity
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* Hybrid Search - Combines keyword and semantic search
|
| 178 |
+
*/
|
| 179 |
+
export class HybridSearch {
|
| 180 |
+
private queryExpander = new QueryExpander();
|
| 181 |
+
private reRanker = new ResultReRanker();
|
| 182 |
+
|
| 183 |
+
/**
|
| 184 |
+
* Perform hybrid search combining multiple strategies
|
| 185 |
+
*/
|
| 186 |
+
async search(
|
| 187 |
+
query: string,
|
| 188 |
+
options: {
|
| 189 |
+
limit?: number;
|
| 190 |
+
useQueryExpansion?: boolean;
|
| 191 |
+
useReranking?: boolean;
|
| 192 |
+
useDiversification?: boolean;
|
| 193 |
+
} = {}
|
| 194 |
+
): Promise<Array<{ id: string; content: string; score: number; metadata?: any }>> {
|
| 195 |
+
const limit = options.limit || 10;
|
| 196 |
+
let queries = [query];
|
| 197 |
+
|
| 198 |
+
// Query expansion
|
| 199 |
+
if (options.useQueryExpansion) {
|
| 200 |
+
queries = await this.queryExpander.expandQuery(query);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Execute searches for all query variations
|
| 204 |
+
const allResults = new Map<string, any>();
|
| 205 |
+
|
| 206 |
+
for (const q of queries) {
|
| 207 |
+
const results = await graphMemoryService.searchEntities(q, limit * 2);
|
| 208 |
+
|
| 209 |
+
results.forEach((result, index) => {
|
| 210 |
+
const existing = allResults.get(result.id);
|
| 211 |
+
// Use position as pseudo-score (lower index = higher relevance)
|
| 212 |
+
const resultWithScore = { ...result, content: result.name, score: 1 / (index + 1) };
|
| 213 |
+
if (!existing || resultWithScore.score > existing.score) {
|
| 214 |
+
allResults.set(result.id, resultWithScore);
|
| 215 |
+
}
|
| 216 |
+
});
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
let results = Array.from(allResults.values());
|
| 220 |
+
|
| 221 |
+
// Re-ranking
|
| 222 |
+
if (options.useReranking) {
|
| 223 |
+
results = await this.reRanker.rerank(query, results, {
|
| 224 |
+
useRecency: true,
|
| 225 |
+
usePopularity: true,
|
| 226 |
+
});
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// Diversification
|
| 230 |
+
if (options.useDiversification) {
|
| 231 |
+
results = this.reRanker.diversify(results);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
return results.slice(0, limit);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
export const queryExpander = new QueryExpander();
|
| 239 |
+
export const resultReRanker = new ResultReRanker();
|
| 240 |
+
export const hybridSearch = new HybridSearch();
|
apps/backend/src/mcp/cognitive/AgentCommunication.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Advanced Agent Communication Protocol
|
| 3 |
+
* Enables sophisticated inter-agent communication and coordination
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export interface AgentMessage {
|
| 7 |
+
id: string;
|
| 8 |
+
from: string;
|
| 9 |
+
to: string | string[]; // Single agent or broadcast
|
| 10 |
+
type: 'request' | 'response' | 'broadcast' | 'negotiation' | 'delegation';
|
| 11 |
+
content: any;
|
| 12 |
+
priority: 'low' | 'medium' | 'high' | 'critical';
|
| 13 |
+
timestamp: Date;
|
| 14 |
+
correlationId?: string; // For request-response pairing
|
| 15 |
+
metadata?: Record<string, any>;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export interface NegotiationProposal {
|
| 19 |
+
proposalId: string;
|
| 20 |
+
proposer: string;
|
| 21 |
+
task: string;
|
| 22 |
+
terms: Record<string, any>;
|
| 23 |
+
deadline?: Date;
|
| 24 |
+
requiredCapabilities: string[];
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export interface NegotiationResponse {
|
| 28 |
+
proposalId: string;
|
| 29 |
+
responder: string;
|
| 30 |
+
accepted: boolean;
|
| 31 |
+
counterProposal?: Partial<NegotiationProposal>;
|
| 32 |
+
reason?: string;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export class AgentCommunicationProtocol {
|
| 36 |
+
private messageQueue: Map<string, AgentMessage[]> = new Map();
|
| 37 |
+
private subscriptions: Map<string, Set<(msg: AgentMessage) => void>> = new Map();
|
| 38 |
+
private negotiationHistory: Map<string, NegotiationProposal[]> = new Map();
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Send message to specific agent(s)
|
| 42 |
+
*/
|
| 43 |
+
async sendMessage(message: Omit<AgentMessage, 'id' | 'timestamp'>): Promise<string> {
|
| 44 |
+
const fullMessage: AgentMessage = {
|
| 45 |
+
...message,
|
| 46 |
+
id: this.generateMessageId(),
|
| 47 |
+
timestamp: new Date(),
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
// Store in queue
|
| 51 |
+
const recipients = Array.isArray(message.to) ? message.to : [message.to];
|
| 52 |
+
recipients.forEach(recipient => {
|
| 53 |
+
if (!this.messageQueue.has(recipient)) {
|
| 54 |
+
this.messageQueue.set(recipient, []);
|
| 55 |
+
}
|
| 56 |
+
this.messageQueue.get(recipient)!.push(fullMessage);
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// Notify subscribers
|
| 60 |
+
recipients.forEach(recipient => {
|
| 61 |
+
const subscribers = this.subscriptions.get(recipient);
|
| 62 |
+
if (subscribers) {
|
| 63 |
+
subscribers.forEach(callback => callback(fullMessage));
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
return fullMessage.id;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* Receive messages for an agent
|
| 72 |
+
*/
|
| 73 |
+
async receiveMessages(agentId: string, filter?: Partial<AgentMessage>): Promise<AgentMessage[]> {
|
| 74 |
+
const messages = this.messageQueue.get(agentId) || [];
|
| 75 |
+
|
| 76 |
+
if (!filter) return messages;
|
| 77 |
+
|
| 78 |
+
return messages.filter(msg => {
|
| 79 |
+
return Object.entries(filter).every(([key, value]) => {
|
| 80 |
+
return msg[key as keyof AgentMessage] === value;
|
| 81 |
+
});
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Subscribe to messages
|
| 87 |
+
*/
|
| 88 |
+
subscribe(agentId: string, callback: (msg: AgentMessage) => void): () => void {
|
| 89 |
+
if (!this.subscriptions.has(agentId)) {
|
| 90 |
+
this.subscriptions.set(agentId, new Set());
|
| 91 |
+
}
|
| 92 |
+
this.subscriptions.get(agentId)!.add(callback);
|
| 93 |
+
|
| 94 |
+
// Return unsubscribe function
|
| 95 |
+
return () => {
|
| 96 |
+
this.subscriptions.get(agentId)?.delete(callback);
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/**
|
| 101 |
+
* Broadcast message to all agents
|
| 102 |
+
*/
|
| 103 |
+
async broadcast(message: Omit<AgentMessage, 'id' | 'timestamp' | 'to'>): Promise<string> {
|
| 104 |
+
return this.sendMessage({
|
| 105 |
+
...message,
|
| 106 |
+
to: 'broadcast',
|
| 107 |
+
});
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* Request-response pattern
|
| 112 |
+
*/
|
| 113 |
+
async request(
|
| 114 |
+
from: string,
|
| 115 |
+
to: string,
|
| 116 |
+
content: any,
|
| 117 |
+
timeout: number = 30000
|
| 118 |
+
): Promise<AgentMessage | null> {
|
| 119 |
+
const correlationId = this.generateMessageId();
|
| 120 |
+
|
| 121 |
+
await this.sendMessage({
|
| 122 |
+
from,
|
| 123 |
+
to,
|
| 124 |
+
type: 'request',
|
| 125 |
+
content,
|
| 126 |
+
priority: 'medium',
|
| 127 |
+
correlationId,
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
// Wait for response
|
| 131 |
+
return new Promise((resolve) => {
|
| 132 |
+
const timeoutId = setTimeout(() => {
|
| 133 |
+
unsubscribe();
|
| 134 |
+
resolve(null);
|
| 135 |
+
}, timeout);
|
| 136 |
+
|
| 137 |
+
const unsubscribe = this.subscribe(from, (msg) => {
|
| 138 |
+
if (msg.type === 'response' && msg.correlationId === correlationId) {
|
| 139 |
+
clearTimeout(timeoutId);
|
| 140 |
+
unsubscribe();
|
| 141 |
+
resolve(msg);
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
});
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Respond to a request
|
| 149 |
+
*/
|
| 150 |
+
async respond(originalMessage: AgentMessage, response: any): Promise<string> {
|
| 151 |
+
return this.sendMessage({
|
| 152 |
+
from: originalMessage.to as string,
|
| 153 |
+
to: originalMessage.from,
|
| 154 |
+
type: 'response',
|
| 155 |
+
content: response,
|
| 156 |
+
priority: originalMessage.priority,
|
| 157 |
+
correlationId: originalMessage.correlationId,
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Initiate negotiation
|
| 163 |
+
*/
|
| 164 |
+
async proposeNegotiation(proposal: NegotiationProposal): Promise<string> {
|
| 165 |
+
if (!this.negotiationHistory.has(proposal.proposer)) {
|
| 166 |
+
this.negotiationHistory.set(proposal.proposer, []);
|
| 167 |
+
}
|
| 168 |
+
this.negotiationHistory.get(proposal.proposer)!.push(proposal);
|
| 169 |
+
|
| 170 |
+
return this.broadcast({
|
| 171 |
+
from: proposal.proposer,
|
| 172 |
+
type: 'negotiation',
|
| 173 |
+
content: proposal,
|
| 174 |
+
priority: 'high',
|
| 175 |
+
});
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* Respond to negotiation
|
| 180 |
+
*/
|
| 181 |
+
async respondToNegotiation(
|
| 182 |
+
proposal: NegotiationProposal,
|
| 183 |
+
response: NegotiationResponse
|
| 184 |
+
): Promise<string> {
|
| 185 |
+
return this.sendMessage({
|
| 186 |
+
from: response.responder,
|
| 187 |
+
to: proposal.proposer,
|
| 188 |
+
type: 'response',
|
| 189 |
+
content: response,
|
| 190 |
+
priority: 'high',
|
| 191 |
+
metadata: { negotiation: true },
|
| 192 |
+
});
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/**
|
| 196 |
+
* Delegate task to another agent
|
| 197 |
+
*/
|
| 198 |
+
async delegateTask(
|
| 199 |
+
from: string,
|
| 200 |
+
to: string,
|
| 201 |
+
task: any,
|
| 202 |
+
priority: AgentMessage['priority'] = 'medium'
|
| 203 |
+
): Promise<string> {
|
| 204 |
+
return this.sendMessage({
|
| 205 |
+
from,
|
| 206 |
+
to,
|
| 207 |
+
type: 'delegation',
|
| 208 |
+
content: task,
|
| 209 |
+
priority,
|
| 210 |
+
});
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* Clear old messages
|
| 215 |
+
*/
|
| 216 |
+
clearOldMessages(agentId: string, olderThan: Date): void {
|
| 217 |
+
const messages = this.messageQueue.get(agentId);
|
| 218 |
+
if (messages) {
|
| 219 |
+
const filtered = messages.filter(msg => msg.timestamp > olderThan);
|
| 220 |
+
this.messageQueue.set(agentId, filtered);
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/**
|
| 225 |
+
* Get message statistics
|
| 226 |
+
*/
|
| 227 |
+
getStatistics(agentId: string): {
|
| 228 |
+
totalMessages: number;
|
| 229 |
+
byType: Record<string, number>;
|
| 230 |
+
byPriority: Record<string, number>;
|
| 231 |
+
} {
|
| 232 |
+
const messages = this.messageQueue.get(agentId) || [];
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
totalMessages: messages.length,
|
| 236 |
+
byType: messages.reduce((acc, msg) => {
|
| 237 |
+
acc[msg.type] = (acc[msg.type] || 0) + 1;
|
| 238 |
+
return acc;
|
| 239 |
+
}, {} as Record<string, number>),
|
| 240 |
+
byPriority: messages.reduce((acc, msg) => {
|
| 241 |
+
acc[msg.priority] = (acc[msg.priority] || 0) + 1;
|
| 242 |
+
return acc;
|
| 243 |
+
}, {} as Record<string, number>),
|
| 244 |
+
};
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
private generateMessageId(): string {
|
| 248 |
+
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
export const agentCommunicationProtocol = new AgentCommunicationProtocol();
|
apps/backend/src/mcp/cognitive/AgentCoordination.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { agentCommunicationProtocol, AgentMessage } from './AgentCommunication';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Agent Capability Registry
|
| 5 |
+
*/
|
| 6 |
+
export interface AgentCapability {
|
| 7 |
+
name: string;
|
| 8 |
+
description: string;
|
| 9 |
+
parameters: Record<string, any>;
|
| 10 |
+
cost: number; // Resource cost
|
| 11 |
+
reliability: number; // 0-1
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export interface AgentProfile {
|
| 15 |
+
id: string;
|
| 16 |
+
name: string;
|
| 17 |
+
capabilities: AgentCapability[];
|
| 18 |
+
currentLoad: number; // 0-100
|
| 19 |
+
maxLoad: number;
|
| 20 |
+
specialization: string[];
|
| 21 |
+
performance: {
|
| 22 |
+
tasksCompleted: number;
|
| 23 |
+
successRate: number;
|
| 24 |
+
avgResponseTime: number;
|
| 25 |
+
};
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Dynamic Agent Spawning System
|
| 30 |
+
*/
|
| 31 |
+
export class AgentSpawner {
|
| 32 |
+
private agents: Map<string, AgentProfile> = new Map();
|
| 33 |
+
private taskQueue: Array<{ task: any; requiredCapabilities: string[] }> = [];
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Register an agent
|
| 37 |
+
*/
|
| 38 |
+
registerAgent(profile: AgentProfile): void {
|
| 39 |
+
this.agents.set(profile.id, profile);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Spawn new agent based on workload
|
| 44 |
+
*/
|
| 45 |
+
async spawnAgent(template: Partial<AgentProfile>): Promise<AgentProfile> {
|
| 46 |
+
const newAgent: AgentProfile = {
|
| 47 |
+
id: `agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
| 48 |
+
name: template.name || 'Dynamic Agent',
|
| 49 |
+
capabilities: template.capabilities || [],
|
| 50 |
+
currentLoad: 0,
|
| 51 |
+
maxLoad: template.maxLoad || 100,
|
| 52 |
+
specialization: template.specialization || [],
|
| 53 |
+
performance: {
|
| 54 |
+
tasksCompleted: 0,
|
| 55 |
+
successRate: 1.0,
|
| 56 |
+
avgResponseTime: 0,
|
| 57 |
+
},
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
this.registerAgent(newAgent);
|
| 61 |
+
console.log(`✨ Spawned new agent: ${newAgent.id}`);
|
| 62 |
+
|
| 63 |
+
return newAgent;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Find best agent for task
|
| 68 |
+
*/
|
| 69 |
+
findBestAgent(requiredCapabilities: string[]): AgentProfile | null {
|
| 70 |
+
const candidates = Array.from(this.agents.values()).filter(agent => {
|
| 71 |
+
// Check if agent has required capabilities
|
| 72 |
+
const hasCapabilities = requiredCapabilities.every(required =>
|
| 73 |
+
agent.capabilities.some(cap => cap.name === required)
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
// Check if agent has capacity
|
| 77 |
+
const hasCapacity = agent.currentLoad < agent.maxLoad;
|
| 78 |
+
|
| 79 |
+
return hasCapabilities && hasCapacity;
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
if (candidates.length === 0) return null;
|
| 83 |
+
|
| 84 |
+
// Sort by performance and load
|
| 85 |
+
candidates.sort((a, b) => {
|
| 86 |
+
const scoreA = a.performance.successRate * (1 - a.currentLoad / a.maxLoad);
|
| 87 |
+
const scoreB = b.performance.successRate * (1 - b.currentLoad / b.maxLoad);
|
| 88 |
+
return scoreB - scoreA;
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
return candidates[0];
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Auto-scale agents based on workload
|
| 96 |
+
*/
|
| 97 |
+
async autoScale(): Promise<void> {
|
| 98 |
+
// Check if we need more agents
|
| 99 |
+
const avgLoad = Array.from(this.agents.values())
|
| 100 |
+
.reduce((sum, agent) => sum + agent.currentLoad, 0) / this.agents.size;
|
| 101 |
+
|
| 102 |
+
if (avgLoad > 80) {
|
| 103 |
+
// High load - spawn new agent
|
| 104 |
+
const mostCommonSpecialization = this.getMostCommonSpecialization();
|
| 105 |
+
await this.spawnAgent({
|
| 106 |
+
specialization: [mostCommonSpecialization],
|
| 107 |
+
maxLoad: 100,
|
| 108 |
+
});
|
| 109 |
+
} else if (avgLoad < 20 && this.agents.size > 1) {
|
| 110 |
+
// Low load - consider removing agents
|
| 111 |
+
this.removeIdleAgents();
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Remove idle agents
|
| 117 |
+
*/
|
| 118 |
+
private removeIdleAgents(): void {
|
| 119 |
+
const idleAgents = Array.from(this.agents.values())
|
| 120 |
+
.filter(agent => agent.currentLoad === 0 && agent.performance.tasksCompleted === 0);
|
| 121 |
+
|
| 122 |
+
idleAgents.forEach(agent => {
|
| 123 |
+
this.agents.delete(agent.id);
|
| 124 |
+
console.log(`🗑️ Removed idle agent: ${agent.id}`);
|
| 125 |
+
});
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
private getMostCommonSpecialization(): string {
|
| 129 |
+
const specializations = Array.from(this.agents.values())
|
| 130 |
+
.flatMap(agent => agent.specialization);
|
| 131 |
+
|
| 132 |
+
const counts = specializations.reduce((acc, spec) => {
|
| 133 |
+
acc[spec] = (acc[spec] || 0) + 1;
|
| 134 |
+
return acc;
|
| 135 |
+
}, {} as Record<string, number>);
|
| 136 |
+
|
| 137 |
+
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'general';
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/**
|
| 141 |
+
* Get all agents
|
| 142 |
+
*/
|
| 143 |
+
getAllAgents(): AgentProfile[] {
|
| 144 |
+
return Array.from(this.agents.values());
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Update agent load
|
| 149 |
+
*/
|
| 150 |
+
updateAgentLoad(agentId: string, load: number): void {
|
| 151 |
+
const agent = this.agents.get(agentId);
|
| 152 |
+
if (agent) {
|
| 153 |
+
agent.currentLoad = Math.max(0, Math.min(100, load));
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Agent Specialization Learning
|
| 160 |
+
*/
|
| 161 |
+
export class AgentSpecializationLearner {
|
| 162 |
+
private performanceHistory: Map<string, Array<{
|
| 163 |
+
task: string;
|
| 164 |
+
success: boolean;
|
| 165 |
+
duration: number;
|
| 166 |
+
timestamp: Date;
|
| 167 |
+
}>> = new Map();
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Record task performance
|
| 171 |
+
*/
|
| 172 |
+
recordPerformance(
|
| 173 |
+
agentId: string,
|
| 174 |
+
task: string,
|
| 175 |
+
success: boolean,
|
| 176 |
+
duration: number
|
| 177 |
+
): void {
|
| 178 |
+
if (!this.performanceHistory.has(agentId)) {
|
| 179 |
+
this.performanceHistory.set(agentId, []);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
this.performanceHistory.get(agentId)!.push({
|
| 183 |
+
task,
|
| 184 |
+
success,
|
| 185 |
+
duration,
|
| 186 |
+
timestamp: new Date(),
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
// Keep only last 100 records
|
| 190 |
+
const history = this.performanceHistory.get(agentId)!;
|
| 191 |
+
if (history.length > 100) {
|
| 192 |
+
history.shift();
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/**
|
| 197 |
+
* Identify agent specializations
|
| 198 |
+
*/
|
| 199 |
+
identifySpecializations(agentId: string): string[] {
|
| 200 |
+
const history = this.performanceHistory.get(agentId) || [];
|
| 201 |
+
if (history.length < 10) return [];
|
| 202 |
+
|
| 203 |
+
// Group by task type
|
| 204 |
+
const taskPerformance = history.reduce((acc, record) => {
|
| 205 |
+
if (!acc[record.task]) {
|
| 206 |
+
acc[record.task] = { successes: 0, total: 0, avgDuration: 0 };
|
| 207 |
+
}
|
| 208 |
+
acc[record.task].total++;
|
| 209 |
+
if (record.success) acc[record.task].successes++;
|
| 210 |
+
acc[record.task].avgDuration += record.duration;
|
| 211 |
+
return acc;
|
| 212 |
+
}, {} as Record<string, { successes: number; total: number; avgDuration: number }>);
|
| 213 |
+
|
| 214 |
+
// Calculate success rates
|
| 215 |
+
Object.keys(taskPerformance).forEach(task => {
|
| 216 |
+
const perf = taskPerformance[task];
|
| 217 |
+
perf.avgDuration /= perf.total;
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
// Find tasks with high success rate and low duration
|
| 221 |
+
const specializations = Object.entries(taskPerformance)
|
| 222 |
+
.filter(([_, perf]) => {
|
| 223 |
+
const successRate = perf.successes / perf.total;
|
| 224 |
+
return successRate > 0.8 && perf.total >= 5;
|
| 225 |
+
})
|
| 226 |
+
.map(([task]) => task);
|
| 227 |
+
|
| 228 |
+
return specializations;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/**
|
| 232 |
+
* Recommend task assignment
|
| 233 |
+
*/
|
| 234 |
+
recommendAgent(
|
| 235 |
+
agents: AgentProfile[],
|
| 236 |
+
task: string
|
| 237 |
+
): AgentProfile | null {
|
| 238 |
+
const scores = agents.map(agent => {
|
| 239 |
+
const history = this.performanceHistory.get(agent.id) || [];
|
| 240 |
+
const taskHistory = history.filter(h => h.task === task);
|
| 241 |
+
|
| 242 |
+
if (taskHistory.length === 0) {
|
| 243 |
+
return { agent, score: 0.5 }; // Neutral score for unknown
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
const successRate = taskHistory.filter(h => h.success).length / taskHistory.length;
|
| 247 |
+
const avgDuration = taskHistory.reduce((sum, h) => sum + h.duration, 0) / taskHistory.length;
|
| 248 |
+
const recency = (Date.now() - taskHistory[taskHistory.length - 1].timestamp.getTime()) / (1000 * 60 * 60 * 24);
|
| 249 |
+
|
| 250 |
+
// Score based on success rate, speed, and recency
|
| 251 |
+
const score = successRate * 0.6 + (1 / (avgDuration + 1)) * 0.3 + (1 / (recency + 1)) * 0.1;
|
| 252 |
+
|
| 253 |
+
return { agent, score };
|
| 254 |
+
});
|
| 255 |
+
|
| 256 |
+
scores.sort((a, b) => b.score - a.score);
|
| 257 |
+
return scores[0]?.agent || null;
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/**
|
| 262 |
+
* Cross-Agent Knowledge Sharing
|
| 263 |
+
*/
|
| 264 |
+
export class KnowledgeSharing {
|
| 265 |
+
private sharedKnowledge: Map<string, Array<{
|
| 266 |
+
source: string;
|
| 267 |
+
knowledge: any;
|
| 268 |
+
timestamp: Date;
|
| 269 |
+
usefulness: number;
|
| 270 |
+
}>> = new Map();
|
| 271 |
+
|
| 272 |
+
/**
|
| 273 |
+
* Share knowledge
|
| 274 |
+
*/
|
| 275 |
+
async shareKnowledge(
|
| 276 |
+
fromAgent: string,
|
| 277 |
+
topic: string,
|
| 278 |
+
knowledge: any
|
| 279 |
+
): Promise<void> {
|
| 280 |
+
if (!this.sharedKnowledge.has(topic)) {
|
| 281 |
+
this.sharedKnowledge.set(topic, []);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
this.sharedKnowledge.get(topic)!.push({
|
| 285 |
+
source: fromAgent,
|
| 286 |
+
knowledge,
|
| 287 |
+
timestamp: new Date(),
|
| 288 |
+
usefulness: 0,
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
// Broadcast to other agents
|
| 292 |
+
await agentCommunicationProtocol.broadcast({
|
| 293 |
+
from: fromAgent,
|
| 294 |
+
type: 'broadcast',
|
| 295 |
+
content: {
|
| 296 |
+
topic,
|
| 297 |
+
knowledge,
|
| 298 |
+
},
|
| 299 |
+
priority: 'low',
|
| 300 |
+
metadata: { knowledgeSharing: true },
|
| 301 |
+
});
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/**
|
| 305 |
+
* Query shared knowledge
|
| 306 |
+
*/
|
| 307 |
+
queryKnowledge(topic: string, limit: number = 10): any[] {
|
| 308 |
+
const knowledge = this.sharedKnowledge.get(topic) || [];
|
| 309 |
+
|
| 310 |
+
// Sort by usefulness and recency
|
| 311 |
+
return knowledge
|
| 312 |
+
.sort((a, b) => {
|
| 313 |
+
const scoreA = a.usefulness + (1 / ((Date.now() - a.timestamp.getTime()) / 1000 / 60 / 60 + 1));
|
| 314 |
+
const scoreB = b.usefulness + (1 / ((Date.now() - b.timestamp.getTime()) / 1000 / 60 / 60 + 1));
|
| 315 |
+
return scoreB - scoreA;
|
| 316 |
+
})
|
| 317 |
+
.slice(0, limit)
|
| 318 |
+
.map(k => k.knowledge);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
/**
|
| 322 |
+
* Rate knowledge usefulness
|
| 323 |
+
*/
|
| 324 |
+
rateKnowledge(topic: string, knowledgeIndex: number, rating: number): void {
|
| 325 |
+
const knowledge = this.sharedKnowledge.get(topic);
|
| 326 |
+
if (knowledge && knowledge[knowledgeIndex]) {
|
| 327 |
+
knowledge[knowledgeIndex].usefulness = rating;
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
export const agentSpawner = new AgentSpawner();
|
| 333 |
+
export const specializationLearner = new AgentSpecializationLearner();
|
| 334 |
+
export const knowledgeSharing = new KnowledgeSharing();
|
apps/backend/src/mcp/cognitive/AgentTeam.ts
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// AgentTeam – Phase 2 Week 5-6
|
| 2 |
+
// Role-based agent coordination system
|
| 3 |
+
|
| 4 |
+
import { unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 5 |
+
import { hybridSearchEngine } from './HybridSearchEngine.js';
|
| 6 |
+
import { emotionAwareDecisionEngine } from './EmotionAwareDecisionEngine.js';
|
| 7 |
+
import { unifiedGraphRAG } from './UnifiedGraphRAG.js';
|
| 8 |
+
import { stateGraphRouter, AgentState } from './StateGraphRouter.js';
|
| 9 |
+
import { projectMemory } from '../../services/project/ProjectMemory.js';
|
| 10 |
+
import { eventBus } from '../EventBus.js';
|
| 11 |
+
|
| 12 |
+
export type AgentRole = 'data' | 'security' | 'memory' | 'pal' | 'orchestrator';
|
| 13 |
+
|
| 14 |
+
export interface AgentMessage {
|
| 15 |
+
from: AgentRole;
|
| 16 |
+
to: AgentRole | 'all';
|
| 17 |
+
type: 'query' | 'task' | 'result' | 'alert' | 'coordinate';
|
| 18 |
+
content: string;
|
| 19 |
+
metadata?: any;
|
| 20 |
+
timestamp: Date;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface AgentCapabilities {
|
| 24 |
+
canHandle: (message: AgentMessage) => boolean | Promise<boolean>;
|
| 25 |
+
execute: (message: AgentMessage) => Promise<any>;
|
| 26 |
+
getStatus: () => Promise<AgentStatus>;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface AgentStatus {
|
| 30 |
+
role: AgentRole;
|
| 31 |
+
active: boolean;
|
| 32 |
+
tasksCompleted: number;
|
| 33 |
+
lastActivity: Date;
|
| 34 |
+
capabilities: string[];
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Data Agent - Handles data ingestion, transformation, and quality
|
| 39 |
+
*/
|
| 40 |
+
class DataAgent implements AgentCapabilities {
|
| 41 |
+
public readonly role: AgentRole = 'data';
|
| 42 |
+
private tasksCompleted = 0;
|
| 43 |
+
private lastActivity = new Date();
|
| 44 |
+
|
| 45 |
+
canHandle(message: AgentMessage): boolean {
|
| 46 |
+
return message.type === 'query' && (
|
| 47 |
+
message.content.includes('data') ||
|
| 48 |
+
message.content.includes('ingest') ||
|
| 49 |
+
message.content.includes('transform') ||
|
| 50 |
+
message.content.includes('quality')
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async execute(message: AgentMessage): Promise<any> {
|
| 55 |
+
console.log(`📊 [DataAgent] Processing: ${message.content}`);
|
| 56 |
+
this.lastActivity = new Date();
|
| 57 |
+
this.tasksCompleted++;
|
| 58 |
+
|
| 59 |
+
// Use HybridSearchEngine to find relevant data
|
| 60 |
+
const searchResults = await hybridSearchEngine.search(message.content, {
|
| 61 |
+
userId: message.metadata?.userId || 'system',
|
| 62 |
+
orgId: message.metadata?.orgId || 'default',
|
| 63 |
+
limit: 10,
|
| 64 |
+
timestamp: new Date()
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
return {
|
| 68 |
+
role: this.role,
|
| 69 |
+
result: {
|
| 70 |
+
dataFound: searchResults.length,
|
| 71 |
+
sources: searchResults.map(r => ({
|
| 72 |
+
source: r.source,
|
| 73 |
+
relevance: r.score
|
| 74 |
+
})),
|
| 75 |
+
quality: this.assessDataQuality(searchResults)
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
private assessDataQuality(searchResults: any): {
|
| 81 |
+
completeness: number;
|
| 82 |
+
freshness: number;
|
| 83 |
+
reliability: number;
|
| 84 |
+
} {
|
| 85 |
+
// Simple quality assessment
|
| 86 |
+
const avgScore = searchResults.results.reduce((sum: number, r: any) => sum + r.score, 0) / searchResults.results.length || 0;
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
completeness: Math.min(1.0, searchResults.results.length / 10),
|
| 90 |
+
freshness: avgScore,
|
| 91 |
+
reliability: avgScore * 0.9
|
| 92 |
+
};
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
async getStatus(): Promise<AgentStatus> {
|
| 96 |
+
return {
|
| 97 |
+
role: this.role,
|
| 98 |
+
active: true,
|
| 99 |
+
tasksCompleted: this.tasksCompleted,
|
| 100 |
+
lastActivity: this.lastActivity,
|
| 101 |
+
capabilities: ['data_ingestion', 'data_quality', 'data_transformation']
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* Security Agent - Handles security checks, threat detection, and compliance
|
| 108 |
+
*/
|
| 109 |
+
class SecurityAgent implements AgentCapabilities {
|
| 110 |
+
public readonly role: AgentRole = 'security';
|
| 111 |
+
private tasksCompleted = 0;
|
| 112 |
+
private lastActivity = new Date();
|
| 113 |
+
|
| 114 |
+
canHandle(message: AgentMessage): boolean {
|
| 115 |
+
return message.type === 'query' && (
|
| 116 |
+
message.content.includes('security') ||
|
| 117 |
+
message.content.includes('threat') ||
|
| 118 |
+
message.content.includes('compliance') ||
|
| 119 |
+
message.content.includes('vulnerability')
|
| 120 |
+
) || message.type === 'alert';
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
async execute(message: AgentMessage): Promise<any> {
|
| 124 |
+
console.log(`🔒 [SecurityAgent] Processing: ${message.content}`);
|
| 125 |
+
this.lastActivity = new Date();
|
| 126 |
+
this.tasksCompleted++;
|
| 127 |
+
|
| 128 |
+
// Use EmotionAwareDecisionEngine for security decisions
|
| 129 |
+
const decision = await emotionAwareDecisionEngine.makeDecision(
|
| 130 |
+
message.content,
|
| 131 |
+
{
|
| 132 |
+
orgId: message.metadata?.orgId || 'default',
|
| 133 |
+
userId: message.metadata?.userId || 'system',
|
| 134 |
+
boardId: message.metadata?.boardId
|
| 135 |
+
}
|
| 136 |
+
);
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
role: this.role,
|
| 140 |
+
result: {
|
| 141 |
+
threatLevel: this.assessThreatLevel(message.content),
|
| 142 |
+
decision: decision,
|
| 143 |
+
recommendations: this.generateSecurityRecommendations(message.content)
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
private assessThreatLevel(content: string): 'low' | 'medium' | 'high' {
|
| 149 |
+
const threatKeywords = ['vulnerability', 'breach', 'attack', 'malware', 'exploit'];
|
| 150 |
+
const found = threatKeywords.filter(kw => content.toLowerCase().includes(kw)).length;
|
| 151 |
+
|
| 152 |
+
if (found >= 2) return 'high';
|
| 153 |
+
if (found >= 1) return 'medium';
|
| 154 |
+
return 'low';
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
private generateSecurityRecommendations(content: string): string[] {
|
| 158 |
+
const recommendations: string[] = [];
|
| 159 |
+
|
| 160 |
+
if (content.includes('vulnerability')) {
|
| 161 |
+
recommendations.push('Run security scan');
|
| 162 |
+
recommendations.push('Review access controls');
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
if (content.includes('compliance')) {
|
| 166 |
+
recommendations.push('Verify GDPR compliance');
|
| 167 |
+
recommendations.push('Check data retention policies');
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
return recommendations;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
async getStatus(): Promise<AgentStatus> {
|
| 174 |
+
return {
|
| 175 |
+
role: this.role,
|
| 176 |
+
active: true,
|
| 177 |
+
tasksCompleted: this.tasksCompleted,
|
| 178 |
+
lastActivity: this.lastActivity,
|
| 179 |
+
capabilities: ['threat_detection', 'compliance_check', 'security_scan']
|
| 180 |
+
};
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/**
|
| 185 |
+
* Memory Agent - Handles memory operations, retrieval, and consolidation
|
| 186 |
+
*/
|
| 187 |
+
class MemoryAgent implements AgentCapabilities {
|
| 188 |
+
public readonly role: AgentRole = 'memory';
|
| 189 |
+
private tasksCompleted = 0;
|
| 190 |
+
private lastActivity = new Date();
|
| 191 |
+
|
| 192 |
+
canHandle(message: AgentMessage): boolean {
|
| 193 |
+
return message.type === 'query' && (
|
| 194 |
+
message.content.includes('memory') ||
|
| 195 |
+
message.content.includes('remember') ||
|
| 196 |
+
message.content.includes('recall') ||
|
| 197 |
+
message.content.includes('forget')
|
| 198 |
+
);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
async execute(message: AgentMessage): Promise<any> {
|
| 202 |
+
console.log(`🧠 [MemoryAgent] Processing: ${message.content}`);
|
| 203 |
+
this.lastActivity = new Date();
|
| 204 |
+
this.tasksCompleted++;
|
| 205 |
+
|
| 206 |
+
// Use UnifiedMemorySystem for memory operations
|
| 207 |
+
const memory = await unifiedMemorySystem.getWorkingMemory({
|
| 208 |
+
userId: message.metadata?.userId || 'system',
|
| 209 |
+
orgId: message.metadata?.orgId || 'default'
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
// Use GraphRAG for memory retrieval
|
| 213 |
+
const graphResult = await unifiedGraphRAG.query(message.content, {
|
| 214 |
+
userId: message.metadata?.userId || 'system',
|
| 215 |
+
orgId: message.metadata?.orgId || 'default'
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
return {
|
| 219 |
+
role: this.role,
|
| 220 |
+
result: {
|
| 221 |
+
workingMemory: memory,
|
| 222 |
+
graphContext: graphResult,
|
| 223 |
+
consolidation: await this.consolidateMemory(memory)
|
| 224 |
+
}
|
| 225 |
+
};
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
private async consolidateMemory(memory: any): Promise<{
|
| 229 |
+
patterns: number;
|
| 230 |
+
features: number;
|
| 231 |
+
events: number;
|
| 232 |
+
}> {
|
| 233 |
+
return {
|
| 234 |
+
patterns: memory.recentPatterns?.length || 0,
|
| 235 |
+
features: memory.recentFeatures?.length || 0,
|
| 236 |
+
events: memory.recentEvents?.length || 0
|
| 237 |
+
};
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
async getStatus(): Promise<AgentStatus> {
|
| 241 |
+
return {
|
| 242 |
+
role: this.role,
|
| 243 |
+
active: true,
|
| 244 |
+
tasksCompleted: this.tasksCompleted,
|
| 245 |
+
lastActivity: this.lastActivity,
|
| 246 |
+
capabilities: ['memory_retrieval', 'memory_consolidation', 'pattern_detection']
|
| 247 |
+
};
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
/**
|
| 252 |
+
* PAL Agent - Personal Assistant, Life Coach, Strategic & Political Advisor, Compensation Expert
|
| 253 |
+
* Inspired by CgentCore's L1 Director Agent architecture
|
| 254 |
+
*/
|
| 255 |
+
class PalAgent implements AgentCapabilities {
|
| 256 |
+
public readonly role: AgentRole = 'pal';
|
| 257 |
+
private conversationHistory: Array<{ role: string; content: string; timestamp: Date }> = [];
|
| 258 |
+
|
| 259 |
+
canHandle(message: AgentMessage): boolean {
|
| 260 |
+
const content = message.content.toLowerCase();
|
| 261 |
+
return message.type === 'query' && (
|
| 262 |
+
// Personal Assistant
|
| 263 |
+
content.includes('assistant') ||
|
| 264 |
+
content.includes('help me') ||
|
| 265 |
+
content.includes('remind') ||
|
| 266 |
+
content.includes('schedule') ||
|
| 267 |
+
// Coach
|
| 268 |
+
content.includes('coach') ||
|
| 269 |
+
content.includes('advice') ||
|
| 270 |
+
content.includes('guidance') ||
|
| 271 |
+
content.includes('improve') ||
|
| 272 |
+
// Strategic
|
| 273 |
+
content.includes('strategy') ||
|
| 274 |
+
content.includes('strategic') ||
|
| 275 |
+
content.includes('planning') ||
|
| 276 |
+
content.includes('roadmap') ||
|
| 277 |
+
// Political
|
| 278 |
+
content.includes('political') ||
|
| 279 |
+
content.includes('politics') ||
|
| 280 |
+
content.includes('policy') ||
|
| 281 |
+
content.includes('stakeholder') ||
|
| 282 |
+
// Compensation
|
| 283 |
+
content.includes('salary') ||
|
| 284 |
+
content.includes('compensation') ||
|
| 285 |
+
content.includes('gage') ||
|
| 286 |
+
content.includes('pay') ||
|
| 287 |
+
content.includes('bonus') ||
|
| 288 |
+
// Personal/workflow
|
| 289 |
+
content.includes('workflow') ||
|
| 290 |
+
content.includes('optimize') ||
|
| 291 |
+
content.includes('personal') ||
|
| 292 |
+
content.includes('preference') ||
|
| 293 |
+
content.includes('emotional')
|
| 294 |
+
);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
async execute(message: AgentMessage): Promise<any> {
|
| 298 |
+
console.log(`👤 [PalAgent] Processing: ${message.content}`);
|
| 299 |
+
|
| 300 |
+
// Store conversation history
|
| 301 |
+
this.conversationHistory.push({
|
| 302 |
+
role: 'user',
|
| 303 |
+
content: message.content,
|
| 304 |
+
timestamp: new Date()
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
// Determine agent mode based on content
|
| 308 |
+
const mode = this.determineMode(message.content);
|
| 309 |
+
|
| 310 |
+
// Use EmotionAwareDecisionEngine for personal decisions
|
| 311 |
+
const decision = await emotionAwareDecisionEngine.makeDecision(
|
| 312 |
+
message.content,
|
| 313 |
+
{
|
| 314 |
+
orgId: message.metadata?.orgId || 'default',
|
| 315 |
+
userId: message.metadata?.userId || 'system',
|
| 316 |
+
boardId: message.metadata?.boardId
|
| 317 |
+
}
|
| 318 |
+
);
|
| 319 |
+
|
| 320 |
+
// Route to specialized handler
|
| 321 |
+
let result: any;
|
| 322 |
+
switch (mode) {
|
| 323 |
+
case 'coach':
|
| 324 |
+
result = await this.handleCoach(message, decision);
|
| 325 |
+
break;
|
| 326 |
+
case 'strategic':
|
| 327 |
+
result = await this.handleStrategic(message, decision);
|
| 328 |
+
break;
|
| 329 |
+
case 'political':
|
| 330 |
+
result = await this.handlePolitical(message, decision);
|
| 331 |
+
break;
|
| 332 |
+
case 'compensation':
|
| 333 |
+
result = await this.handleCompensation(message, decision);
|
| 334 |
+
break;
|
| 335 |
+
case 'assistant':
|
| 336 |
+
default:
|
| 337 |
+
result = await this.handleAssistant(message, decision);
|
| 338 |
+
break;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
// Store agent response
|
| 342 |
+
this.conversationHistory.push({
|
| 343 |
+
role: 'assistant',
|
| 344 |
+
content: JSON.stringify(result),
|
| 345 |
+
timestamp: new Date()
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
return {
|
| 349 |
+
role: this.role,
|
| 350 |
+
mode,
|
| 351 |
+
result,
|
| 352 |
+
emotionalState: decision.emotionalState,
|
| 353 |
+
confidence: decision.confidence
|
| 354 |
+
};
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/**
|
| 358 |
+
* Determine agent mode from message content
|
| 359 |
+
*/
|
| 360 |
+
private determineMode(content: string): 'assistant' | 'coach' | 'strategic' | 'political' | 'compensation' {
|
| 361 |
+
const lower = content.toLowerCase();
|
| 362 |
+
|
| 363 |
+
if (lower.includes('coach') || lower.includes('guidance') || lower.includes('improve')) {
|
| 364 |
+
return 'coach';
|
| 365 |
+
}
|
| 366 |
+
if (lower.includes('strategy') || lower.includes('strategic') || lower.includes('planning')) {
|
| 367 |
+
return 'strategic';
|
| 368 |
+
}
|
| 369 |
+
if (lower.includes('political') || lower.includes('politics') || lower.includes('policy') || lower.includes('stakeholder')) {
|
| 370 |
+
return 'political';
|
| 371 |
+
}
|
| 372 |
+
if (lower.includes('salary') || lower.includes('compensation') || lower.includes('gage') || lower.includes('pay') || lower.includes('bonus')) {
|
| 373 |
+
return 'compensation';
|
| 374 |
+
}
|
| 375 |
+
return 'assistant';
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
/**
|
| 379 |
+
* Personal Assistant Mode - Task management, reminders, scheduling
|
| 380 |
+
*/
|
| 381 |
+
private async handleAssistant(message: AgentMessage, decision: any): Promise<any> {
|
| 382 |
+
console.log(' 📋 [PalAgent] Assistant mode');
|
| 383 |
+
|
| 384 |
+
// Use UnifiedMemorySystem to recall user preferences
|
| 385 |
+
const memory = await unifiedMemorySystem.getWorkingMemory({
|
| 386 |
+
userId: message.metadata?.userId || 'system',
|
| 387 |
+
orgId: message.metadata?.orgId || 'default'
|
| 388 |
+
});
|
| 389 |
+
|
| 390 |
+
return {
|
| 391 |
+
type: 'assistant',
|
| 392 |
+
response: this.generateAssistantResponse(message.content),
|
| 393 |
+
recommendations: this.generateWorkflowRecommendations(message.content),
|
| 394 |
+
context: {
|
| 395 |
+
recentPatterns: memory.recentPatterns?.slice(0, 3) || [],
|
| 396 |
+
preferences: memory.recentFeatures?.slice(0, 3) || []
|
| 397 |
+
},
|
| 398 |
+
nextActions: [
|
| 399 |
+
'Review your calendar for conflicts',
|
| 400 |
+
'Check pending tasks',
|
| 401 |
+
'Update preferences based on feedback'
|
| 402 |
+
]
|
| 403 |
+
};
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/**
|
| 407 |
+
* Coach Mode - Personal development, guidance, improvement
|
| 408 |
+
*/
|
| 409 |
+
private async handleCoach(message: AgentMessage, decision: any): Promise<any> {
|
| 410 |
+
console.log(' 🎯 [PalAgent] Coach mode');
|
| 411 |
+
|
| 412 |
+
// Use GraphRAG to find related coaching insights
|
| 413 |
+
const graphResult = await unifiedGraphRAG.query(`Coaching advice: ${message.content}`, {
|
| 414 |
+
userId: message.metadata?.userId || 'system',
|
| 415 |
+
orgId: message.metadata?.orgId || 'default'
|
| 416 |
+
});
|
| 417 |
+
|
| 418 |
+
return {
|
| 419 |
+
type: 'coach',
|
| 420 |
+
guidance: this.generateCoachingGuidance(message.content, decision),
|
| 421 |
+
insights: graphResult.nodes.slice(0, 3).map((n: any) => ({
|
| 422 |
+
insight: n.content,
|
| 423 |
+
confidence: n.score
|
| 424 |
+
})),
|
| 425 |
+
actionPlan: [
|
| 426 |
+
'Identify specific goals',
|
| 427 |
+
'Break down into actionable steps',
|
| 428 |
+
'Set measurable milestones',
|
| 429 |
+
'Schedule regular check-ins'
|
| 430 |
+
],
|
| 431 |
+
emotionalSupport: {
|
| 432 |
+
currentState: decision.emotionalState,
|
| 433 |
+
recommendations: this.getEmotionalRecommendations(decision.emotionalState)
|
| 434 |
+
}
|
| 435 |
+
};
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
/**
|
| 439 |
+
* Strategic Advisor Mode - Strategic planning, roadmaps, business strategy
|
| 440 |
+
*/
|
| 441 |
+
private async handleStrategic(message: AgentMessage, decision: any): Promise<any> {
|
| 442 |
+
console.log(' 🎲 [PalAgent] Strategic advisor mode');
|
| 443 |
+
|
| 444 |
+
// Use StateGraphRouter for strategic planning workflow
|
| 445 |
+
const state = stateGraphRouter.initState(`strategic-${Date.now()}`, message.content);
|
| 446 |
+
let currentState = state;
|
| 447 |
+
let iterations = 0;
|
| 448 |
+
|
| 449 |
+
while (currentState.status === 'active' && iterations < 5) {
|
| 450 |
+
currentState = await stateGraphRouter.route(currentState);
|
| 451 |
+
iterations++;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
return {
|
| 455 |
+
type: 'strategic',
|
| 456 |
+
analysis: this.generateStrategicAnalysis(message.content),
|
| 457 |
+
roadmap: currentState.scratchpad.plan || [],
|
| 458 |
+
recommendations: [
|
| 459 |
+
'Define clear objectives',
|
| 460 |
+
'Identify key stakeholders',
|
| 461 |
+
'Assess risks and opportunities',
|
| 462 |
+
'Create timeline with milestones',
|
| 463 |
+
'Establish success metrics'
|
| 464 |
+
],
|
| 465 |
+
considerations: {
|
| 466 |
+
risks: ['Resource constraints', 'Timeline pressure', 'Stakeholder alignment'],
|
| 467 |
+
opportunities: ['Market timing', 'Competitive advantage', 'Innovation potential'],
|
| 468 |
+
dependencies: ['Team capacity', 'Technology readiness', 'Budget approval']
|
| 469 |
+
},
|
| 470 |
+
planningState: currentState.scratchpad
|
| 471 |
+
};
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/**
|
| 475 |
+
* Political Advisor Mode - Stakeholder management, organizational politics
|
| 476 |
+
*/
|
| 477 |
+
private async handlePolitical(message: AgentMessage, decision: any): Promise<any> {
|
| 478 |
+
console.log(' 🏛️ [PalAgent] Political advisor mode');
|
| 479 |
+
|
| 480 |
+
return {
|
| 481 |
+
type: 'political',
|
| 482 |
+
analysis: this.generatePoliticalAnalysis(message.content),
|
| 483 |
+
stakeholderMap: [
|
| 484 |
+
{ role: 'Champion', influence: 'high', support: 'positive' },
|
| 485 |
+
{ role: 'Decision Maker', influence: 'high', support: 'neutral' },
|
| 486 |
+
{ role: 'Influencer', influence: 'medium', support: 'positive' }
|
| 487 |
+
],
|
| 488 |
+
recommendations: [
|
| 489 |
+
'Identify key decision makers',
|
| 490 |
+
'Build coalition of supporters',
|
| 491 |
+
'Address concerns proactively',
|
| 492 |
+
'Communicate value proposition clearly',
|
| 493 |
+
'Timing is critical - choose right moment'
|
| 494 |
+
],
|
| 495 |
+
tactics: {
|
| 496 |
+
communication: 'Tailor message to audience',
|
| 497 |
+
timing: 'Align with organizational cycles',
|
| 498 |
+
relationships: 'Leverage existing networks',
|
| 499 |
+
framing: 'Position as win-win solution'
|
| 500 |
+
},
|
| 501 |
+
warnings: [
|
| 502 |
+
'Avoid creating unnecessary opposition',
|
| 503 |
+
'Respect organizational hierarchy',
|
| 504 |
+
'Be transparent about intentions'
|
| 505 |
+
]
|
| 506 |
+
};
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
/**
|
| 510 |
+
* Compensation Expert Mode - Salary, benefits, negotiation
|
| 511 |
+
*/
|
| 512 |
+
private async handleCompensation(message: AgentMessage, decision: any): Promise<any> {
|
| 513 |
+
console.log(' 💰 [PalAgent] Compensation expert mode');
|
| 514 |
+
|
| 515 |
+
// Use HybridSearchEngine to find market data
|
| 516 |
+
const searchResults = await hybridSearchEngine.search(`compensation ${message.content}`, {
|
| 517 |
+
userId: message.metadata?.userId || 'system',
|
| 518 |
+
orgId: message.metadata?.orgId || 'default',
|
| 519 |
+
limit: 5,
|
| 520 |
+
timestamp: new Date()
|
| 521 |
+
});
|
| 522 |
+
|
| 523 |
+
return {
|
| 524 |
+
type: 'compensation',
|
| 525 |
+
analysis: this.generateCompensationAnalysis(message.content),
|
| 526 |
+
marketData: {
|
| 527 |
+
sources: searchResults.slice(0, 3).map((r: any) => ({
|
| 528 |
+
source: r.source,
|
| 529 |
+
relevance: r.score
|
| 530 |
+
})),
|
| 531 |
+
note: 'Market data should be verified from multiple sources'
|
| 532 |
+
},
|
| 533 |
+
recommendations: [
|
| 534 |
+
'Research market rates for your role and location',
|
| 535 |
+
'Consider total compensation package (salary + benefits)',
|
| 536 |
+
'Document your achievements and value',
|
| 537 |
+
'Prepare negotiation strategy',
|
| 538 |
+
'Know your walk-away point'
|
| 539 |
+
],
|
| 540 |
+
negotiationTips: [
|
| 541 |
+
'Focus on value delivered, not just time served',
|
| 542 |
+
'Use data to support your request',
|
| 543 |
+
'Consider non-monetary benefits',
|
| 544 |
+
'Practice your pitch',
|
| 545 |
+
'Be prepared to negotiate'
|
| 546 |
+
],
|
| 547 |
+
factors: {
|
| 548 |
+
baseSalary: 'Foundation of compensation',
|
| 549 |
+
bonuses: 'Variable compensation',
|
| 550 |
+
equity: 'Long-term value',
|
| 551 |
+
benefits: 'Health, retirement, etc.',
|
| 552 |
+
perks: 'Flexibility, development, etc.'
|
| 553 |
+
}
|
| 554 |
+
};
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
private generateAssistantResponse(content: string): string {
|
| 558 |
+
if (content.includes('remind')) {
|
| 559 |
+
return 'I can help you set reminders. What would you like to be reminded about?';
|
| 560 |
+
}
|
| 561 |
+
if (content.includes('schedule')) {
|
| 562 |
+
return 'I can help you manage your schedule. What would you like to schedule?';
|
| 563 |
+
}
|
| 564 |
+
return 'How can I assist you today?';
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
private generateCoachingGuidance(content: string, decision: any): string {
|
| 568 |
+
return `Based on your current emotional state (${decision.emotionalState}), I recommend focusing on clear, achievable goals. Let's break down your challenge into manageable steps.`;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
private generateStrategicAnalysis(content: string): string {
|
| 572 |
+
return 'Strategic analysis requires understanding objectives, constraints, and opportunities. Let me help you develop a comprehensive strategy.';
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
private generatePoliticalAnalysis(content: string): string {
|
| 576 |
+
return 'Organizational politics require careful navigation. Key factors: stakeholder interests, power dynamics, and timing.';
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
private generateCompensationAnalysis(content: string): string {
|
| 580 |
+
return 'Compensation analysis should consider market rates, your value proposition, and total package (salary + benefits + equity).';
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
private getEmotionalRecommendations(emotionalState: string): string[] {
|
| 584 |
+
const recommendations: Record<string, string[]> = {
|
| 585 |
+
'positive': ['Maintain momentum', 'Set stretch goals', 'Share success'],
|
| 586 |
+
'neutral': ['Focus on growth', 'Seek feedback', 'Plan next steps'],
|
| 587 |
+
'negative': ['Take breaks', 'Seek support', 'Reframe challenges', 'Celebrate small wins']
|
| 588 |
+
};
|
| 589 |
+
return recommendations[emotionalState] || recommendations['neutral'];
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
private generateWorkflowRecommendations(content: string): string[] {
|
| 593 |
+
const recommendations: string[] = [];
|
| 594 |
+
|
| 595 |
+
if (content.includes('optimize')) {
|
| 596 |
+
recommendations.push('Review recent task patterns');
|
| 597 |
+
recommendations.push('Identify bottlenecks');
|
| 598 |
+
recommendations.push('Automate repetitive tasks');
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
if (content.includes('workflow')) {
|
| 602 |
+
recommendations.push('Analyze user preferences');
|
| 603 |
+
recommendations.push('Suggest workflow improvements');
|
| 604 |
+
recommendations.push('Implement time-blocking');
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
return recommendations;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
async getStatus(): Promise<AgentStatus> {
|
| 611 |
+
return {
|
| 612 |
+
role: this.role,
|
| 613 |
+
active: true,
|
| 614 |
+
tasksCompleted: this.conversationHistory.filter(m => m.role === 'assistant').length,
|
| 615 |
+
lastActivity: this.conversationHistory.length > 0
|
| 616 |
+
? this.conversationHistory[this.conversationHistory.length - 1].timestamp
|
| 617 |
+
: new Date(),
|
| 618 |
+
capabilities: [
|
| 619 |
+
'personal_assistant',
|
| 620 |
+
'life_coach',
|
| 621 |
+
'strategic_advisor',
|
| 622 |
+
'political_advisor',
|
| 623 |
+
'compensation_expert',
|
| 624 |
+
'workflow_optimization',
|
| 625 |
+
'emotional_awareness',
|
| 626 |
+
'conversation_history'
|
| 627 |
+
]
|
| 628 |
+
};
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
/**
|
| 632 |
+
* Get conversation history
|
| 633 |
+
*/
|
| 634 |
+
public getConversationHistory(): Array<{ role: string; content: string; timestamp: Date }> {
|
| 635 |
+
return this.conversationHistory.slice(-20); // Last 20 messages
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
/**
|
| 640 |
+
* Orchestrator Agent - Coordinates other agents and manages task flow
|
| 641 |
+
*/
|
| 642 |
+
class OrchestratorAgent implements AgentCapabilities {
|
| 643 |
+
public readonly role: AgentRole = 'orchestrator';
|
| 644 |
+
private messageHistory: AgentMessage[] = [];
|
| 645 |
+
|
| 646 |
+
canHandle(message: AgentMessage): boolean {
|
| 647 |
+
return message.type === 'coordinate' || message.to === 'orchestrator';
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
async execute(message: AgentMessage): Promise<any> {
|
| 651 |
+
console.log(`🎯 [OrchestratorAgent] Coordinating: ${message.content}`);
|
| 652 |
+
|
| 653 |
+
this.messageHistory.push(message);
|
| 654 |
+
|
| 655 |
+
// Use StateGraphRouter for complex coordination
|
| 656 |
+
const state = stateGraphRouter.initState(`coord-${Date.now()}`, message.content);
|
| 657 |
+
let currentState = state;
|
| 658 |
+
let iterations = 0;
|
| 659 |
+
|
| 660 |
+
while (currentState.status === 'active' && iterations < 10) {
|
| 661 |
+
currentState = await stateGraphRouter.route(currentState);
|
| 662 |
+
iterations++;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
return {
|
| 666 |
+
role: this.role,
|
| 667 |
+
result: {
|
| 668 |
+
coordination: {
|
| 669 |
+
state: currentState.status,
|
| 670 |
+
path: currentState.history,
|
| 671 |
+
iterations
|
| 672 |
+
},
|
| 673 |
+
messageHistory: this.messageHistory.slice(-10)
|
| 674 |
+
}
|
| 675 |
+
};
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
async getStatus(): Promise<AgentStatus> {
|
| 679 |
+
return {
|
| 680 |
+
role: this.role,
|
| 681 |
+
active: true,
|
| 682 |
+
tasksCompleted: this.messageHistory.length,
|
| 683 |
+
lastActivity: new Date(),
|
| 684 |
+
capabilities: ['coordination', 'task_routing', 'agent_management']
|
| 685 |
+
};
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
/**
|
| 690 |
+
* AgentTeam - Coordinates role-based agents
|
| 691 |
+
*/
|
| 692 |
+
export class AgentTeam {
|
| 693 |
+
private agents: Map<AgentRole, AgentCapabilities> = new Map();
|
| 694 |
+
private messageQueue: AgentMessage[] = [];
|
| 695 |
+
private coordinationHistory: AgentMessage[] = [];
|
| 696 |
+
|
| 697 |
+
constructor() {
|
| 698 |
+
// Initialize all agents
|
| 699 |
+
this.agents.set('data', new DataAgent());
|
| 700 |
+
this.agents.set('security', new SecurityAgent());
|
| 701 |
+
this.agents.set('memory', new MemoryAgent());
|
| 702 |
+
this.agents.set('pal', new PalAgent());
|
| 703 |
+
this.agents.set('orchestrator', new OrchestratorAgent());
|
| 704 |
+
|
| 705 |
+
console.log('👥 [AgentTeam] Initialized with 5 role-based agents');
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
/**
|
| 709 |
+
* Route message to appropriate agent(s)
|
| 710 |
+
*/
|
| 711 |
+
public async routeMessage(message: AgentMessage): Promise<any> {
|
| 712 |
+
console.log(`📨 [AgentTeam] Routing message from ${message.from} to ${message.to}`);
|
| 713 |
+
|
| 714 |
+
// If message is to orchestrator, handle directly
|
| 715 |
+
if (message.to === 'orchestrator') {
|
| 716 |
+
const orchestrator = this.agents.get('orchestrator');
|
| 717 |
+
if (orchestrator) {
|
| 718 |
+
return await orchestrator.execute(message);
|
| 719 |
+
}
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
// If message is to 'all', broadcast
|
| 723 |
+
if (message.to === 'all') {
|
| 724 |
+
const results: any[] = [];
|
| 725 |
+
for (const [role, agent] of this.agents.entries()) {
|
| 726 |
+
if (agent.canHandle(message)) {
|
| 727 |
+
const result = await agent.execute(message);
|
| 728 |
+
results.push(result);
|
| 729 |
+
}
|
| 730 |
+
}
|
| 731 |
+
return { results, broadcast: true };
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
// Route to specific agent
|
| 735 |
+
const targetAgent = this.agents.get(message.to as AgentRole);
|
| 736 |
+
if (!targetAgent) {
|
| 737 |
+
throw new Error(`Agent ${message.to} not found`);
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
if (!targetAgent.canHandle(message)) {
|
| 741 |
+
// Try to find alternative agent
|
| 742 |
+
for (const [role, agent] of this.agents.entries()) {
|
| 743 |
+
if (agent.canHandle(message)) {
|
| 744 |
+
console.log(`🔄 [AgentTeam] Rerouting from ${message.to} to ${role}`);
|
| 745 |
+
return await agent.execute(message);
|
| 746 |
+
}
|
| 747 |
+
}
|
| 748 |
+
throw new Error(`No agent can handle message: ${message.content}`);
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
return await targetAgent.execute(message);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
/**
|
| 755 |
+
* Coordinate multiple agents for complex task
|
| 756 |
+
*/
|
| 757 |
+
public async coordinate(task: string, context?: any): Promise<any> {
|
| 758 |
+
const coordinationMessage: AgentMessage = {
|
| 759 |
+
from: 'orchestrator',
|
| 760 |
+
to: 'all',
|
| 761 |
+
type: 'coordinate',
|
| 762 |
+
content: task,
|
| 763 |
+
metadata: context,
|
| 764 |
+
timestamp: new Date()
|
| 765 |
+
};
|
| 766 |
+
|
| 767 |
+
this.coordinationHistory.push(coordinationMessage);
|
| 768 |
+
|
| 769 |
+
// Use orchestrator to coordinate
|
| 770 |
+
const orchestrator = this.agents.get('orchestrator');
|
| 771 |
+
if (orchestrator) {
|
| 772 |
+
const result = await orchestrator.execute(coordinationMessage);
|
| 773 |
+
|
| 774 |
+
// Log to ProjectMemory
|
| 775 |
+
projectMemory.logLifecycleEvent({
|
| 776 |
+
eventType: 'other',
|
| 777 |
+
status: 'success',
|
| 778 |
+
details: {
|
| 779 |
+
component: 'AgentTeam',
|
| 780 |
+
action: 'coordination',
|
| 781 |
+
task,
|
| 782 |
+
agentsInvolved: this.agents.size,
|
| 783 |
+
timestamp: new Date().toISOString()
|
| 784 |
+
}
|
| 785 |
+
});
|
| 786 |
+
|
| 787 |
+
return result;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
throw new Error('Orchestrator agent not available');
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
/**
|
| 794 |
+
* Get status of all agents
|
| 795 |
+
*/
|
| 796 |
+
public async getAllStatuses(): Promise<AgentStatus[]> {
|
| 797 |
+
const statuses: AgentStatus[] = [];
|
| 798 |
+
|
| 799 |
+
for (const [role, agent] of this.agents.entries()) {
|
| 800 |
+
const status = await agent.getStatus();
|
| 801 |
+
statuses.push(status);
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
return statuses;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
/**
|
| 808 |
+
* Get agent by role
|
| 809 |
+
*/
|
| 810 |
+
public getAgent(role: AgentRole): AgentCapabilities | undefined {
|
| 811 |
+
return this.agents.get(role);
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
export const agentTeam = new AgentTeam();
|
| 816 |
+
|
apps/backend/src/mcp/cognitive/AutonomousTaskEngine.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// AutonomousTaskEngine – Phase 1 (BabyAGI loop)
|
| 2 |
+
import { AutonomousAgent } from '../autonomous/AutonomousAgent.js';
|
| 3 |
+
import { unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 4 |
+
import { eventBus } from '../EventBus.js';
|
| 5 |
+
import { getCognitiveMemory } from '../memory/CognitiveMemory.js';
|
| 6 |
+
import { getSourceRegistry } from '../SourceRegistry.js';
|
| 7 |
+
|
| 8 |
+
type Task = {
|
| 9 |
+
type: string;
|
| 10 |
+
payload: any;
|
| 11 |
+
baseScore?: number;
|
| 12 |
+
isSimple?: boolean;
|
| 13 |
+
isMaintenanceTask?: boolean;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
interface TaskResult {
|
| 17 |
+
success: boolean;
|
| 18 |
+
data?: any;
|
| 19 |
+
error?: any;
|
| 20 |
+
needsMoreData?: boolean;
|
| 21 |
+
foundPattern?: boolean;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
interface ExecutionLog {
|
| 25 |
+
task: Task;
|
| 26 |
+
result: TaskResult;
|
| 27 |
+
timestamp: Date;
|
| 28 |
+
newTasks: Task[];
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
class PriorityQueue<T> {
|
| 32 |
+
private items: { task: T; priority: number }[] = [];
|
| 33 |
+
|
| 34 |
+
enqueue(task: T, priority: number) {
|
| 35 |
+
this.items.push({ task, priority });
|
| 36 |
+
this.items.sort((a, b) => b.priority - a.priority);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
dequeue(): T | undefined {
|
| 40 |
+
return this.items.shift()?.task;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
isEmpty(): boolean {
|
| 44 |
+
return this.items.length === 0;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
addAll(tasks: T[], priorityFn?: (task: T) => number) {
|
| 48 |
+
tasks.forEach(task => {
|
| 49 |
+
const priority = priorityFn ? priorityFn(task) : (task as any).baseScore || 50;
|
| 50 |
+
this.enqueue(task, priority);
|
| 51 |
+
});
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
reprioritize(priorityFn: (task: T) => number) {
|
| 55 |
+
const tasks = this.items.map(item => item.task);
|
| 56 |
+
this.items = [];
|
| 57 |
+
tasks.forEach(task => {
|
| 58 |
+
const priority = priorityFn(task);
|
| 59 |
+
this.enqueue(task, priority);
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
export class AutonomousTaskEngine {
|
| 65 |
+
private agent: AutonomousAgent;
|
| 66 |
+
private queue = new PriorityQueue<Task>();
|
| 67 |
+
private active = true;
|
| 68 |
+
private executionHistory: ExecutionLog[] = [];
|
| 69 |
+
private memoryOptimizationIntervalId: NodeJS.Timeout | null = null;
|
| 70 |
+
|
| 71 |
+
constructor(agent?: AutonomousAgent) {
|
| 72 |
+
if (agent) {
|
| 73 |
+
this.agent = agent;
|
| 74 |
+
} else {
|
| 75 |
+
const memory = getCognitiveMemory();
|
| 76 |
+
const registry = getSourceRegistry();
|
| 77 |
+
this.agent = new AutonomousAgent(memory, registry);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Listen for system events to generate tasks
|
| 81 |
+
eventBus.onEvent('system.alert', (event) => {
|
| 82 |
+
this.queue.enqueue({
|
| 83 |
+
type: 'diagnostic',
|
| 84 |
+
payload: event.payload,
|
| 85 |
+
baseScore: 100,
|
| 86 |
+
isMaintenanceTask: true
|
| 87 |
+
}, 100);
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
// Schedule nightly memory optimization (Consolidation & Decay)
|
| 91 |
+
this.memoryOptimizationIntervalId = setInterval(() => {
|
| 92 |
+
this.queue.enqueue({
|
| 93 |
+
type: 'memory_optimization',
|
| 94 |
+
payload: { mode: 'nightly_consolidation' },
|
| 95 |
+
baseScore: 80,
|
| 96 |
+
isMaintenanceTask: true
|
| 97 |
+
}, 80);
|
| 98 |
+
}, 1000 * 60 * 60 * 24); // Every 24 hours
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
async start() {
|
| 102 |
+
console.log('🤖 AutonomousTaskEngine started');
|
| 103 |
+
|
| 104 |
+
// Run the task loop in the background (non-blocking)
|
| 105 |
+
this.runTaskLoop();
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
private async runTaskLoop() {
|
| 109 |
+
while (this.active) {
|
| 110 |
+
if (this.queue.isEmpty()) {
|
| 111 |
+
await new Promise((r) => setTimeout(r, 1000));
|
| 112 |
+
continue;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const task = this.queue.dequeue()!;
|
| 116 |
+
const result = await this.executeTask(task);
|
| 117 |
+
|
| 118 |
+
// Generate new tasks based on result
|
| 119 |
+
const newTasks = await this.generateTasksFromResult(result);
|
| 120 |
+
this.queue.addAll(newTasks);
|
| 121 |
+
|
| 122 |
+
// Reprioritize all tasks
|
| 123 |
+
await this.reprioritizeTasks();
|
| 124 |
+
|
| 125 |
+
// Log to episodic memory
|
| 126 |
+
await this.logToEpisodicMemory(task, result, newTasks);
|
| 127 |
+
|
| 128 |
+
// Learn patterns → procedural memory
|
| 129 |
+
await this.convertPatternToProcedure(result);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
stop() {
|
| 134 |
+
this.active = false;
|
| 135 |
+
// Clear the memory optimization interval to prevent resource leak
|
| 136 |
+
if (this.memoryOptimizationIntervalId !== null) {
|
| 137 |
+
clearInterval(this.memoryOptimizationIntervalId);
|
| 138 |
+
this.memoryOptimizationIntervalId = null;
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
private async executeTask(task: Task): Promise<TaskResult> {
|
| 143 |
+
const startTime = Date.now();
|
| 144 |
+
try {
|
| 145 |
+
// Handle special memory optimization tasks
|
| 146 |
+
if (task.type === 'memory_optimization') {
|
| 147 |
+
return await this.executeMemoryOptimization(task);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const intent = this.taskToIntent(task);
|
| 151 |
+
const result = await this.agent.executeAndLearn(intent, async (src) => {
|
| 152 |
+
if ('query' in src && typeof src.query === 'function') {
|
| 153 |
+
return await src.query(intent.operation || task.type, intent.params || task.payload);
|
| 154 |
+
}
|
| 155 |
+
throw new Error(`Source ${src.name} does not support query operation`);
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
const duration = Date.now() - startTime;
|
| 159 |
+
|
| 160 |
+
// Emit event for TaskRecorder observation
|
| 161 |
+
eventBus.emit('autonomous.task.executed', {
|
| 162 |
+
taskType: task.type,
|
| 163 |
+
payload: task.payload,
|
| 164 |
+
success: true,
|
| 165 |
+
result: result.data,
|
| 166 |
+
duration
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
success: true,
|
| 171 |
+
data: result.data,
|
| 172 |
+
needsMoreData: false,
|
| 173 |
+
foundPattern: false
|
| 174 |
+
};
|
| 175 |
+
} catch (error: any) {
|
| 176 |
+
const duration = Date.now() - startTime;
|
| 177 |
+
|
| 178 |
+
// Emit event for TaskRecorder observation (failure)
|
| 179 |
+
eventBus.emit('autonomous.task.executed', {
|
| 180 |
+
taskType: task.type,
|
| 181 |
+
payload: task.payload,
|
| 182 |
+
success: false,
|
| 183 |
+
error: error.message,
|
| 184 |
+
duration
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
return {
|
| 188 |
+
success: false,
|
| 189 |
+
error: error.message,
|
| 190 |
+
needsMoreData: true
|
| 191 |
+
};
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/**
|
| 196 |
+
* Execute Memory Optimization (Learning Loop)
|
| 197 |
+
* 1. Consolidate similar vectors
|
| 198 |
+
* 2. Decay old/unused memories
|
| 199 |
+
* 3. Reflect on recent insights
|
| 200 |
+
*/
|
| 201 |
+
private async executeMemoryOptimization(task: Task): Promise<TaskResult> {
|
| 202 |
+
console.log('🧠 [Learning Loop] Starting memory optimization...');
|
| 203 |
+
|
| 204 |
+
try {
|
| 205 |
+
const { getVectorStore } = await import('../../platform/vector/index.js');
|
| 206 |
+
const vectorStore = await getVectorStore();
|
| 207 |
+
|
| 208 |
+
// 1. Consolidation: Find duplicates/similar items
|
| 209 |
+
// (Simplified implementation: In a real scenario, we'd cluster vectors)
|
| 210 |
+
const stats = await vectorStore.getStatistics();
|
| 211 |
+
console.log(`🧠 [Learning Loop] Current memory size: ${stats.totalRecords} vectors`);
|
| 212 |
+
|
| 213 |
+
// 2. Reflection: If we have new data, try to synthesize it
|
| 214 |
+
// This would involve querying the LLM to summarize recent entries
|
| 215 |
+
|
| 216 |
+
return {
|
| 217 |
+
success: true,
|
| 218 |
+
data: { optimized: true, stats },
|
| 219 |
+
foundPattern: true // Optimization often reveals patterns
|
| 220 |
+
};
|
| 221 |
+
} catch (error: any) {
|
| 222 |
+
console.error('❌ [Learning Loop] Optimization failed:', error);
|
| 223 |
+
return { success: false, error: error.message };
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
private async generateTasksFromResult(result: TaskResult): Promise<Task[]> {
|
| 228 |
+
const tasks: Task[] = [];
|
| 229 |
+
|
| 230 |
+
if (result.needsMoreData) {
|
| 231 |
+
tasks.push({
|
| 232 |
+
type: 'data_collection',
|
| 233 |
+
payload: { reason: result.error || 'Missing data' },
|
| 234 |
+
baseScore: 60,
|
| 235 |
+
isSimple: true
|
| 236 |
+
});
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
if (result.foundPattern) {
|
| 240 |
+
tasks.push({
|
| 241 |
+
type: 'pattern_exploration',
|
| 242 |
+
payload: { pattern: result.data },
|
| 243 |
+
baseScore: 70,
|
| 244 |
+
isSimple: false
|
| 245 |
+
});
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
return tasks;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
private async reprioritizeTasks(): Promise<void> {
|
| 252 |
+
// Get emotional state and system health for prioritization
|
| 253 |
+
const emotionalState = await this.getEmotionalState();
|
| 254 |
+
const systemHealth = await unifiedMemorySystem.analyzeSystemHealth();
|
| 255 |
+
|
| 256 |
+
this.queue.reprioritize((task) => {
|
| 257 |
+
let score = task.baseScore || 50;
|
| 258 |
+
|
| 259 |
+
// Stress-aware prioritization
|
| 260 |
+
if (emotionalState.stress === 'high') {
|
| 261 |
+
score += task.isSimple ? 50 : -30;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Health-aware prioritization
|
| 265 |
+
if (systemHealth.globalHealth < 0.5) {
|
| 266 |
+
score += task.isMaintenanceTask ? 100 : 0;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
return score;
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
private async getEmotionalState(): Promise<{ stress: 'low' | 'medium' | 'high' }> {
|
| 274 |
+
// Placeholder: query PAL for emotional state
|
| 275 |
+
// In real implementation, this would query PAL repository
|
| 276 |
+
return { stress: 'low' };
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
private async logToEpisodicMemory(task: Task, result: TaskResult, newTasks: Task[]): Promise<void> {
|
| 280 |
+
const log: ExecutionLog = {
|
| 281 |
+
task,
|
| 282 |
+
result,
|
| 283 |
+
timestamp: new Date(),
|
| 284 |
+
newTasks
|
| 285 |
+
};
|
| 286 |
+
|
| 287 |
+
this.executionHistory.push(log);
|
| 288 |
+
|
| 289 |
+
// Keep only last 100 logs
|
| 290 |
+
if (this.executionHistory.length > 100) {
|
| 291 |
+
this.executionHistory.shift();
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Update working memory
|
| 295 |
+
await unifiedMemorySystem.updateWorkingMemory(
|
| 296 |
+
{ orgId: 'default', userId: 'system' }, // Removed timestamp to match McpContext type
|
| 297 |
+
log
|
| 298 |
+
);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
private async convertPatternToProcedure(result: TaskResult): Promise<void> {
|
| 302 |
+
// Placeholder: convert successful patterns to procedural memory
|
| 303 |
+
// In real implementation, this would extract rules and store them
|
| 304 |
+
if (result.success && result.data) {
|
| 305 |
+
// Pattern detected, could be converted to a production rule
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
private taskToIntent(task: Task): any {
|
| 310 |
+
return {
|
| 311 |
+
type: task.type,
|
| 312 |
+
operation: task.type,
|
| 313 |
+
params: task.payload
|
| 314 |
+
};
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
getExecutionHistory(): ExecutionLog[] {
|
| 318 |
+
return [...this.executionHistory];
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// Export singleton instance
|
| 323 |
+
export const autonomousTaskEngine = new AutonomousTaskEngine();
|
apps/backend/src/mcp/cognitive/EmotionAwareDecisionEngine.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Enhanced EmotionAwareDecisionEngine – Phase 1 Week 3
|
| 2 |
+
// Multi-modal decision making with emotional awareness
|
| 3 |
+
|
| 4 |
+
import { PalRepository } from '../../services/pal/palRepository.js';
|
| 5 |
+
import { unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 6 |
+
import { McpContext } from '@widget-tdc/mcp-types';
|
| 7 |
+
import { QueryIntent } from '../autonomous/DecisionEngine.js';
|
| 8 |
+
|
| 9 |
+
export interface EmotionalState {
|
| 10 |
+
stress: 'low' | 'medium' | 'high';
|
| 11 |
+
focus: 'shallow' | 'medium' | 'deep';
|
| 12 |
+
energy: 'low' | 'medium' | 'high';
|
| 13 |
+
mood: 'negative' | 'neutral' | 'positive';
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface Action {
|
| 17 |
+
complexity: 'low' | 'medium' | 'high';
|
| 18 |
+
estimatedTime: number; // milliseconds
|
| 19 |
+
depth: 'low' | 'medium' | 'high';
|
| 20 |
+
requiresFocus: boolean;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface Decision {
|
| 24 |
+
action: Action;
|
| 25 |
+
confidence: number;
|
| 26 |
+
reasoning: string;
|
| 27 |
+
emotionalFit: number;
|
| 28 |
+
dataQuality: number;
|
| 29 |
+
contextRelevance: number;
|
| 30 |
+
emotionalState?: EmotionalState;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export class EmotionAwareDecisionEngine {
|
| 34 |
+
private palRepo: PalRepository;
|
| 35 |
+
|
| 36 |
+
constructor() {
|
| 37 |
+
this.palRepo = new PalRepository();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Make emotion-aware decision based on query, emotional state, and context
|
| 42 |
+
*/
|
| 43 |
+
async makeDecision(
|
| 44 |
+
query: string | QueryIntent,
|
| 45 |
+
ctx: McpContext
|
| 46 |
+
): Promise<Decision> {
|
| 47 |
+
// Get emotional state from PAL
|
| 48 |
+
const emotionalState = await this.getEmotionalState(ctx.userId, ctx.orgId);
|
| 49 |
+
|
| 50 |
+
// Normalize query: if QueryIntent, convert to string representation; if string, use as-is
|
| 51 |
+
const queryStr = typeof query === 'string'
|
| 52 |
+
? query
|
| 53 |
+
: `${query.operation || query.type} ${query.domain || ''} ${JSON.stringify(query.params || {})}`.trim();
|
| 54 |
+
|
| 55 |
+
// Convert to QueryIntent for methods that need structured data
|
| 56 |
+
const queryIntent: QueryIntent = typeof query === 'string'
|
| 57 |
+
? {
|
| 58 |
+
type: 'query',
|
| 59 |
+
domain: 'general',
|
| 60 |
+
operation: 'search',
|
| 61 |
+
params: { query: queryStr }
|
| 62 |
+
}
|
| 63 |
+
: query;
|
| 64 |
+
|
| 65 |
+
// Evaluate multi-modal scores
|
| 66 |
+
const dataScore = await this.evaluateDataQuality(queryIntent, ctx);
|
| 67 |
+
const emotionScore = this.evaluateEmotionalFit(this.queryToAction(queryIntent), emotionalState);
|
| 68 |
+
const contextScore = await this.evaluateContextRelevance(queryIntent, ctx);
|
| 69 |
+
|
| 70 |
+
// Calculate dynamic weights based on emotional state
|
| 71 |
+
const weights = this.calculateDynamicWeights(emotionalState);
|
| 72 |
+
|
| 73 |
+
// Fuse scores with weights
|
| 74 |
+
const fusedScore = this.fusionDecision(
|
| 75 |
+
{
|
| 76 |
+
data: dataScore,
|
| 77 |
+
emotion: emotionScore,
|
| 78 |
+
context: contextScore
|
| 79 |
+
},
|
| 80 |
+
weights
|
| 81 |
+
);
|
| 82 |
+
|
| 83 |
+
// Determine action complexity based on emotional state
|
| 84 |
+
const action = this.determineOptimalAction(queryIntent, emotionalState);
|
| 85 |
+
|
| 86 |
+
const decision: Decision = {
|
| 87 |
+
action,
|
| 88 |
+
confidence: fusedScore,
|
| 89 |
+
reasoning: this.generateReasoning(emotionalState, dataScore, emotionScore, contextScore),
|
| 90 |
+
emotionalFit: emotionScore,
|
| 91 |
+
dataQuality: dataScore,
|
| 92 |
+
contextRelevance: contextScore,
|
| 93 |
+
emotionalState
|
| 94 |
+
};
|
| 95 |
+
return decision;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Get emotional state from PAL repository
|
| 100 |
+
*/
|
| 101 |
+
private async getEmotionalState(userId: string, orgId: string): Promise<EmotionalState> {
|
| 102 |
+
try {
|
| 103 |
+
// Get recent PAL events to infer emotional state
|
| 104 |
+
const recentEvents = this.palRepo.getRecentEvents(userId, orgId, 10);
|
| 105 |
+
|
| 106 |
+
// Analyze events for stress indicators
|
| 107 |
+
let stressLevel: 'low' | 'medium' | 'high' = 'low';
|
| 108 |
+
let focusLevel: 'shallow' | 'medium' | 'deep' = 'medium';
|
| 109 |
+
const energyLevel: 'low' | 'medium' | 'high' = 'medium';
|
| 110 |
+
const mood: 'negative' | 'neutral' | 'positive' = 'neutral';
|
| 111 |
+
|
| 112 |
+
if (Array.isArray(recentEvents)) {
|
| 113 |
+
const stressEvents = recentEvents.filter((e: any) =>
|
| 114 |
+
e.event_type === 'stress' || e.detected_stress_level
|
| 115 |
+
);
|
| 116 |
+
|
| 117 |
+
if (stressEvents.length > 0) {
|
| 118 |
+
const avgStress = stressEvents.reduce((sum: number, e: any) => {
|
| 119 |
+
const level = e.detected_stress_level || e.stress_level || 0;
|
| 120 |
+
return sum + level;
|
| 121 |
+
}, 0) / stressEvents.length;
|
| 122 |
+
|
| 123 |
+
if (avgStress > 7) stressLevel = 'high';
|
| 124 |
+
else if (avgStress > 4) stressLevel = 'medium';
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Check focus windows
|
| 128 |
+
const focusWindows = await this.palRepo.getFocusWindows(userId, orgId);
|
| 129 |
+
const now = new Date();
|
| 130 |
+
const currentHour = now.getHours();
|
| 131 |
+
const currentDay = now.getDay();
|
| 132 |
+
|
| 133 |
+
const inFocusWindow = focusWindows.some((fw: any) =>
|
| 134 |
+
fw.weekday === currentDay &&
|
| 135 |
+
currentHour >= fw.start_hour &&
|
| 136 |
+
currentHour < fw.end_hour
|
| 137 |
+
);
|
| 138 |
+
|
| 139 |
+
if (inFocusWindow) {
|
| 140 |
+
focusLevel = 'deep';
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
return {
|
| 145 |
+
stress: stressLevel,
|
| 146 |
+
focus: focusLevel,
|
| 147 |
+
energy: energyLevel,
|
| 148 |
+
mood
|
| 149 |
+
};
|
| 150 |
+
} catch (error) {
|
| 151 |
+
console.warn('Failed to get emotional state, using defaults:', error);
|
| 152 |
+
return {
|
| 153 |
+
stress: 'low',
|
| 154 |
+
focus: 'medium',
|
| 155 |
+
energy: 'medium',
|
| 156 |
+
mood: 'neutral'
|
| 157 |
+
};
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Evaluate data quality score
|
| 163 |
+
*/
|
| 164 |
+
private async evaluateDataQuality(query: QueryIntent, _ctx: McpContext): Promise<number> {
|
| 165 |
+
try {
|
| 166 |
+
// Check system health - healthy systems provide better data
|
| 167 |
+
const health = await unifiedMemorySystem.analyzeSystemHealth();
|
| 168 |
+
const baseScore = health.globalHealth;
|
| 169 |
+
|
| 170 |
+
// Adjust based on query complexity
|
| 171 |
+
const complexity = this.estimateQueryComplexity(query);
|
| 172 |
+
const complexityPenalty = complexity === 'high' ? 0.1 : complexity === 'medium' ? 0.05 : 0;
|
| 173 |
+
|
| 174 |
+
return Math.max(0, Math.min(1, baseScore - complexityPenalty));
|
| 175 |
+
} catch {
|
| 176 |
+
return 0.8; // Default good score
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* Evaluate emotional fit of action
|
| 182 |
+
*/
|
| 183 |
+
private evaluateEmotionalFit(action: Action, emotion: EmotionalState): number {
|
| 184 |
+
let score = 0.5; // Base neutral score
|
| 185 |
+
|
| 186 |
+
// Stress-aware routing
|
| 187 |
+
if (emotion.stress === 'high') {
|
| 188 |
+
// Prefer simple, fast actions
|
| 189 |
+
if (action.complexity === 'low' && action.estimatedTime < 1000) {
|
| 190 |
+
score = 1.0;
|
| 191 |
+
} else if (action.complexity === 'high') {
|
| 192 |
+
score = 0.2;
|
| 193 |
+
} else {
|
| 194 |
+
score = 0.6;
|
| 195 |
+
}
|
| 196 |
+
} else if (emotion.stress === 'low') {
|
| 197 |
+
// Can handle more complexity
|
| 198 |
+
if (action.complexity === 'high' && action.depth === 'high') {
|
| 199 |
+
score = 0.9;
|
| 200 |
+
} else {
|
| 201 |
+
score = 0.7;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// Focus-aware routing
|
| 206 |
+
if (emotion.focus === 'deep') {
|
| 207 |
+
// Allow complex, deep tasks
|
| 208 |
+
if (action.depth === 'high' && action.requiresFocus) {
|
| 209 |
+
score = Math.max(score, 1.0);
|
| 210 |
+
}
|
| 211 |
+
} else if (emotion.focus === 'shallow') {
|
| 212 |
+
// Prefer simpler tasks
|
| 213 |
+
if (action.complexity === 'high') {
|
| 214 |
+
score = Math.min(score, 0.4);
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Energy-aware routing
|
| 219 |
+
if (emotion.energy === 'low') {
|
| 220 |
+
// Prefer low-effort tasks
|
| 221 |
+
if (action.complexity === 'low' && action.estimatedTime < 500) {
|
| 222 |
+
score = Math.max(score, 0.9);
|
| 223 |
+
} else {
|
| 224 |
+
score = Math.min(score, 0.6);
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
return Math.max(0, Math.min(1, score));
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/**
|
| 232 |
+
* Evaluate context relevance
|
| 233 |
+
*/
|
| 234 |
+
private async evaluateContextRelevance(query: QueryIntent, ctx: McpContext): Promise<number> {
|
| 235 |
+
try {
|
| 236 |
+
// Check if query matches recent patterns
|
| 237 |
+
const patterns = await unifiedMemorySystem.findHolographicPatterns(ctx);
|
| 238 |
+
|
| 239 |
+
const queryText = JSON.stringify(query).toLowerCase();
|
| 240 |
+
const relevantPatterns = patterns.filter((p: any) => {
|
| 241 |
+
const keyword = p.keyword?.toLowerCase() || '';
|
| 242 |
+
return keyword && queryText.includes(keyword);
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
// More relevant patterns = higher score
|
| 246 |
+
return Math.min(1, 0.5 + (relevantPatterns.length * 0.1));
|
| 247 |
+
} catch {
|
| 248 |
+
return 0.7; // Default good relevance
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* Calculate dynamic weights based on emotional state
|
| 254 |
+
*/
|
| 255 |
+
private calculateDynamicWeights(emotion: EmotionalState): {
|
| 256 |
+
data: number;
|
| 257 |
+
emotion: number;
|
| 258 |
+
context: number;
|
| 259 |
+
} {
|
| 260 |
+
// Base weights
|
| 261 |
+
let dataWeight = 0.4;
|
| 262 |
+
let emotionWeight = 0.3;
|
| 263 |
+
let contextWeight = 0.3;
|
| 264 |
+
|
| 265 |
+
// Adjust weights based on stress
|
| 266 |
+
if (emotion.stress === 'high') {
|
| 267 |
+
// Prioritize emotional fit when stressed
|
| 268 |
+
emotionWeight = 0.5;
|
| 269 |
+
dataWeight = 0.3;
|
| 270 |
+
contextWeight = 0.2;
|
| 271 |
+
} else if (emotion.stress === 'low') {
|
| 272 |
+
// Prioritize data quality when relaxed
|
| 273 |
+
dataWeight = 0.5;
|
| 274 |
+
emotionWeight = 0.2;
|
| 275 |
+
contextWeight = 0.3;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// Normalize
|
| 279 |
+
const total = dataWeight + emotionWeight + contextWeight;
|
| 280 |
+
return {
|
| 281 |
+
data: dataWeight / total,
|
| 282 |
+
emotion: emotionWeight / total,
|
| 283 |
+
context: contextWeight / total
|
| 284 |
+
};
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/**
|
| 288 |
+
* Fuse scores with weights
|
| 289 |
+
*/
|
| 290 |
+
private fusionDecision(
|
| 291 |
+
scores: { data: number; emotion: number; context: number },
|
| 292 |
+
weights: { data: number; emotion: number; context: number }
|
| 293 |
+
): number {
|
| 294 |
+
return (
|
| 295 |
+
scores.data * weights.data +
|
| 296 |
+
scores.emotion * weights.emotion +
|
| 297 |
+
scores.context * weights.context
|
| 298 |
+
);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/**
|
| 302 |
+
* Determine optimal action based on query and emotional state
|
| 303 |
+
*/
|
| 304 |
+
private determineOptimalAction(query: QueryIntent, emotion: EmotionalState): Action {
|
| 305 |
+
// Estimate complexity from query
|
| 306 |
+
const complexity = this.estimateQueryComplexity(query);
|
| 307 |
+
|
| 308 |
+
// Estimate time (placeholder - would be more sophisticated)
|
| 309 |
+
const estimatedTime = complexity === 'high' ? 2000 : complexity === 'medium' ? 1000 : 500;
|
| 310 |
+
|
| 311 |
+
// Determine depth requirement
|
| 312 |
+
const depth = complexity === 'high' ? 'high' : complexity === 'medium' ? 'medium' : 'low';
|
| 313 |
+
|
| 314 |
+
// Adjust based on emotional state
|
| 315 |
+
let finalComplexity = complexity;
|
| 316 |
+
if (emotion.stress === 'high' && complexity === 'high') {
|
| 317 |
+
finalComplexity = 'medium'; // Reduce complexity when stressed
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
return {
|
| 321 |
+
complexity: finalComplexity,
|
| 322 |
+
estimatedTime,
|
| 323 |
+
depth,
|
| 324 |
+
requiresFocus: complexity === 'high' || emotion.focus === 'deep'
|
| 325 |
+
};
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/**
|
| 329 |
+
* Estimate query complexity
|
| 330 |
+
*/
|
| 331 |
+
private estimateQueryComplexity(query: QueryIntent): 'low' | 'medium' | 'high' {
|
| 332 |
+
const queryStr = JSON.stringify(query).toLowerCase();
|
| 333 |
+
|
| 334 |
+
// Simple heuristics
|
| 335 |
+
if (queryStr.includes('complex') || queryStr.includes('analyze') || queryStr.includes('deep')) {
|
| 336 |
+
return 'high';
|
| 337 |
+
}
|
| 338 |
+
if (queryStr.includes('search') || queryStr.includes('find') || queryStr.includes('get')) {
|
| 339 |
+
return 'medium';
|
| 340 |
+
}
|
| 341 |
+
return 'low';
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
/**
|
| 345 |
+
* Convert query to action representation
|
| 346 |
+
*/
|
| 347 |
+
private queryToAction(query: QueryIntent): Action {
|
| 348 |
+
const complexity = this.estimateQueryComplexity(query);
|
| 349 |
+
return {
|
| 350 |
+
complexity,
|
| 351 |
+
estimatedTime: complexity === 'high' ? 2000 : complexity === 'medium' ? 1000 : 500,
|
| 352 |
+
depth: complexity === 'high' ? 'high' : complexity === 'medium' ? 'medium' : 'low',
|
| 353 |
+
requiresFocus: complexity === 'high'
|
| 354 |
+
};
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/**
|
| 358 |
+
* Generate human-readable reasoning
|
| 359 |
+
*/
|
| 360 |
+
private generateReasoning(
|
| 361 |
+
emotion: EmotionalState,
|
| 362 |
+
dataScore: number,
|
| 363 |
+
emotionScore: number,
|
| 364 |
+
contextScore: number
|
| 365 |
+
): string {
|
| 366 |
+
const parts: string[] = [];
|
| 367 |
+
|
| 368 |
+
if (emotion.stress === 'high') {
|
| 369 |
+
parts.push('User is experiencing high stress - prioritizing simple, fast actions');
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
if (emotion.focus === 'deep') {
|
| 373 |
+
parts.push('User is in deep focus mode - allowing complex tasks');
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
if (dataScore > 0.8) {
|
| 377 |
+
parts.push('High data quality available');
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
if (emotionScore > 0.8) {
|
| 381 |
+
parts.push('Action matches emotional state well');
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
if (contextScore > 0.8) {
|
| 385 |
+
parts.push('High context relevance');
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
return parts.length > 0
|
| 389 |
+
? parts.join('. ')
|
| 390 |
+
: 'Balanced decision based on available information';
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
export const emotionAwareDecisionEngine = new EmotionAwareDecisionEngine();
|
| 395 |
+
|
apps/backend/src/mcp/cognitive/GraphTraversalOptimizer.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { neo4jService } from '../../database/Neo4jService';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Advanced Graph Traversal Optimizer
|
| 5 |
+
* Optimizes multi-hop graph queries for better performance
|
| 6 |
+
*/
|
| 7 |
+
export class GraphTraversalOptimizer {
|
| 8 |
+
/**
|
| 9 |
+
* Optimized breadth-first search with pruning
|
| 10 |
+
*/
|
| 11 |
+
async optimizedBFS(
|
| 12 |
+
startNodeId: string,
|
| 13 |
+
targetCondition: (node: any) => boolean,
|
| 14 |
+
maxDepth: number = 3,
|
| 15 |
+
maxNodes: number = 100
|
| 16 |
+
): Promise<{ path: any[]; nodesVisited: number }> {
|
| 17 |
+
await neo4jService.connect();
|
| 18 |
+
|
| 19 |
+
try {
|
| 20 |
+
// Use Cypher's built-in path finding with limits
|
| 21 |
+
const result = await neo4jService.runQuery(
|
| 22 |
+
`MATCH path = (start)-[*1..${maxDepth}]-(end)
|
| 23 |
+
WHERE id(start) = $startId
|
| 24 |
+
WITH path, nodes(path) as pathNodes
|
| 25 |
+
LIMIT $maxNodes
|
| 26 |
+
RETURN path, pathNodes`,
|
| 27 |
+
{ startId: parseInt(startNodeId), maxNodes }
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
await neo4jService.disconnect();
|
| 31 |
+
|
| 32 |
+
if (result.length === 0) {
|
| 33 |
+
return { path: [], nodesVisited: 0 };
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Find best path based on condition
|
| 37 |
+
const bestPath = result.find(r => {
|
| 38 |
+
const nodes = r.pathNodes;
|
| 39 |
+
return nodes.some((node: any) => targetCondition(node));
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
return {
|
| 43 |
+
path: bestPath?.pathNodes || [],
|
| 44 |
+
nodesVisited: result.length,
|
| 45 |
+
};
|
| 46 |
+
} catch (error) {
|
| 47 |
+
await neo4jService.disconnect();
|
| 48 |
+
throw error;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/**
|
| 53 |
+
* Find shortest path with relationship type filtering
|
| 54 |
+
*/
|
| 55 |
+
async findShortestPath(
|
| 56 |
+
startNodeId: string,
|
| 57 |
+
endNodeId: string,
|
| 58 |
+
relationshipTypes?: string[],
|
| 59 |
+
maxDepth: number = 5
|
| 60 |
+
): Promise<{ path: any[]; length: number } | null> {
|
| 61 |
+
await neo4jService.connect();
|
| 62 |
+
|
| 63 |
+
try {
|
| 64 |
+
const relFilter = relationshipTypes
|
| 65 |
+
? `:${relationshipTypes.join('|')}`
|
| 66 |
+
: '';
|
| 67 |
+
|
| 68 |
+
const result = await neo4jService.runQuery(
|
| 69 |
+
`MATCH path = shortestPath((start)-[${relFilter}*1..${maxDepth}]-(end))
|
| 70 |
+
WHERE id(start) = $startId AND id(end) = $endId
|
| 71 |
+
RETURN path, length(path) as pathLength`,
|
| 72 |
+
{ startId: parseInt(startNodeId), endId: parseInt(endNodeId) }
|
| 73 |
+
);
|
| 74 |
+
|
| 75 |
+
await neo4jService.disconnect();
|
| 76 |
+
|
| 77 |
+
if (result.length === 0) return null;
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
path: result[0].path,
|
| 81 |
+
length: result[0].pathLength,
|
| 82 |
+
};
|
| 83 |
+
} catch (error) {
|
| 84 |
+
await neo4jService.disconnect();
|
| 85 |
+
throw error;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Find all paths with cost optimization
|
| 91 |
+
*/
|
| 92 |
+
async findAllPathsWithCost(
|
| 93 |
+
startNodeId: string,
|
| 94 |
+
endNodeId: string,
|
| 95 |
+
maxDepth: number = 4,
|
| 96 |
+
costProperty: string = 'weight'
|
| 97 |
+
): Promise<Array<{ path: any[]; cost: number }>> {
|
| 98 |
+
await neo4jService.connect();
|
| 99 |
+
|
| 100 |
+
try {
|
| 101 |
+
const result = await neo4jService.runQuery(
|
| 102 |
+
`MATCH path = (start)-[*1..${maxDepth}]-(end)
|
| 103 |
+
WHERE id(start) = $startId AND id(end) = $endId
|
| 104 |
+
WITH path, relationships(path) as rels
|
| 105 |
+
RETURN path,
|
| 106 |
+
reduce(cost = 0, r in rels | cost + coalesce(r.${costProperty}, 1)) as totalCost
|
| 107 |
+
ORDER BY totalCost ASC
|
| 108 |
+
LIMIT 10`,
|
| 109 |
+
{ startId: parseInt(startNodeId), endId: parseInt(endNodeId) }
|
| 110 |
+
);
|
| 111 |
+
|
| 112 |
+
await neo4jService.disconnect();
|
| 113 |
+
|
| 114 |
+
return result.map(r => ({
|
| 115 |
+
path: r.path,
|
| 116 |
+
cost: r.totalCost,
|
| 117 |
+
}));
|
| 118 |
+
} catch (error) {
|
| 119 |
+
await neo4jService.disconnect();
|
| 120 |
+
throw error;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/**
|
| 125 |
+
* Community detection using label propagation
|
| 126 |
+
*/
|
| 127 |
+
async detectCommunities(minCommunitySize: number = 3): Promise<Map<string, string[]>> {
|
| 128 |
+
await neo4jService.connect();
|
| 129 |
+
|
| 130 |
+
try {
|
| 131 |
+
// Simple community detection based on connected components
|
| 132 |
+
const result = await neo4jService.runQuery(
|
| 133 |
+
`CALL gds.wcc.stream('myGraph')
|
| 134 |
+
YIELD nodeId, componentId
|
| 135 |
+
WITH componentId, collect(nodeId) as members
|
| 136 |
+
WHERE size(members) >= $minSize
|
| 137 |
+
RETURN componentId, members`,
|
| 138 |
+
{ minSize: minCommunitySize }
|
| 139 |
+
);
|
| 140 |
+
|
| 141 |
+
await neo4jService.disconnect();
|
| 142 |
+
|
| 143 |
+
const communities = new Map<string, string[]>();
|
| 144 |
+
result.forEach(r => {
|
| 145 |
+
communities.set(r.componentId.toString(), r.members.map((id: any) => id.toString()));
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
return communities;
|
| 149 |
+
} catch (error) {
|
| 150 |
+
await neo4jService.disconnect();
|
| 151 |
+
// Fallback to simple connected components
|
| 152 |
+
return this.simpleConnectedComponents(minCommunitySize);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/**
|
| 157 |
+
* Fallback: Simple connected components without GDS
|
| 158 |
+
*/
|
| 159 |
+
private async simpleConnectedComponents(minSize: number): Promise<Map<string, string[]>> {
|
| 160 |
+
const result = await neo4jService.runQuery(
|
| 161 |
+
`MATCH (n)
|
| 162 |
+
OPTIONAL MATCH path = (n)-[*]-(m)
|
| 163 |
+
WITH n, collect(DISTINCT m) as connected
|
| 164 |
+
WHERE size(connected) >= $minSize
|
| 165 |
+
RETURN id(n) as nodeId, [x in connected | id(x)] as members
|
| 166 |
+
LIMIT 100`,
|
| 167 |
+
{ minSize }
|
| 168 |
+
);
|
| 169 |
+
|
| 170 |
+
const communities = new Map<string, string[]>();
|
| 171 |
+
result.forEach((r, idx) => {
|
| 172 |
+
communities.set(`community_${idx}`, r.members.map((id: any) => id.toString()));
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
return communities;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* PageRank-style importance scoring
|
| 180 |
+
*/
|
| 181 |
+
async computeNodeImportance(dampingFactor: number = 0.85, iterations: number = 20): Promise<Map<string, number>> {
|
| 182 |
+
await neo4jService.connect();
|
| 183 |
+
|
| 184 |
+
try {
|
| 185 |
+
// Try using GDS PageRank
|
| 186 |
+
const result = await neo4jService.runQuery(
|
| 187 |
+
`CALL gds.pageRank.stream('myGraph', {
|
| 188 |
+
maxIterations: $iterations,
|
| 189 |
+
dampingFactor: $dampingFactor
|
| 190 |
+
})
|
| 191 |
+
YIELD nodeId, score
|
| 192 |
+
RETURN nodeId, score
|
| 193 |
+
ORDER BY score DESC
|
| 194 |
+
LIMIT 100`,
|
| 195 |
+
{ iterations, dampingFactor }
|
| 196 |
+
);
|
| 197 |
+
|
| 198 |
+
await neo4jService.disconnect();
|
| 199 |
+
|
| 200 |
+
const scores = new Map<string, number>();
|
| 201 |
+
result.forEach(r => {
|
| 202 |
+
scores.set(r.nodeId.toString(), r.score);
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
return scores;
|
| 206 |
+
} catch (error) {
|
| 207 |
+
await neo4jService.disconnect();
|
| 208 |
+
// Fallback to simple degree centrality
|
| 209 |
+
return this.simpleDegreeCentrality();
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* Fallback: Simple degree centrality without GDS
|
| 215 |
+
*/
|
| 216 |
+
private async simpleDegreeCentrality(): Promise<Map<string, number>> {
|
| 217 |
+
const result = await neo4jService.runQuery(
|
| 218 |
+
`MATCH (n)-[r]-(m)
|
| 219 |
+
WITH n, count(r) as degree
|
| 220 |
+
RETURN id(n) as nodeId, degree
|
| 221 |
+
ORDER BY degree DESC
|
| 222 |
+
LIMIT 100`
|
| 223 |
+
);
|
| 224 |
+
|
| 225 |
+
const scores = new Map<string, number>();
|
| 226 |
+
result.forEach(r => {
|
| 227 |
+
scores.set(r.nodeId.toString(), r.degree);
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
return scores;
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
export const graphTraversalOptimizer = new GraphTraversalOptimizer();
|
apps/backend/src/mcp/cognitive/HybridSearchEngine.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// HybridSearchEngine – Phase 1 Week 3
|
| 2 |
+
// Combines keyword, semantic, and graph search with Reciprocal Rank Fusion
|
| 3 |
+
|
| 4 |
+
import { MemoryRepository } from '../../services/memory/memoryRepository.js';
|
| 5 |
+
import { SragRepository } from '../../services/srag/sragRepository.js';
|
| 6 |
+
import { unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 7 |
+
import { McpContext } from '@widget-tdc/mcp-types';
|
| 8 |
+
|
| 9 |
+
export interface SearchContext extends McpContext {
|
| 10 |
+
limit?: number;
|
| 11 |
+
filters?: Record<string, any>;
|
| 12 |
+
timestamp?: Date;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface SearchResult {
|
| 16 |
+
id: string;
|
| 17 |
+
type: 'memory' | 'document' | 'graph' | 'pattern';
|
| 18 |
+
score: number;
|
| 19 |
+
content: any;
|
| 20 |
+
source: string;
|
| 21 |
+
metadata?: any;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export class HybridSearchEngine {
|
| 25 |
+
private memoryRepo: MemoryRepository;
|
| 26 |
+
private sragRepo: SragRepository;
|
| 27 |
+
|
| 28 |
+
constructor() {
|
| 29 |
+
this.memoryRepo = new MemoryRepository();
|
| 30 |
+
this.sragRepo = new SragRepository();
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Perform hybrid search across keyword, semantic, and graph sources
|
| 35 |
+
*/
|
| 36 |
+
async search(query: string, ctx: SearchContext): Promise<SearchResult[]> {
|
| 37 |
+
const limit = ctx.limit || 20;
|
| 38 |
+
|
| 39 |
+
// Run all search types in parallel
|
| 40 |
+
const [keywordResults, semanticResults, graphResults] = await Promise.all([
|
| 41 |
+
this.keywordSearch(query, ctx, limit * 2),
|
| 42 |
+
this.semanticSearch(query, ctx, limit * 2),
|
| 43 |
+
this.graphTraversal(query, ctx, limit * 2)
|
| 44 |
+
]);
|
| 45 |
+
|
| 46 |
+
// Reciprocal Rank Fusion
|
| 47 |
+
const fusedResults = this.fuseResults([
|
| 48 |
+
keywordResults,
|
| 49 |
+
semanticResults,
|
| 50 |
+
graphResults
|
| 51 |
+
]);
|
| 52 |
+
|
| 53 |
+
// Return top results
|
| 54 |
+
return fusedResults.slice(0, limit);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Keyword-based search (exact matches, FTS)
|
| 59 |
+
*/
|
| 60 |
+
private async keywordSearch(
|
| 61 |
+
query: string,
|
| 62 |
+
ctx: SearchContext,
|
| 63 |
+
limit: number
|
| 64 |
+
): Promise<SearchResult[]> {
|
| 65 |
+
const results: SearchResult[] = [];
|
| 66 |
+
|
| 67 |
+
try {
|
| 68 |
+
// Search memory entities
|
| 69 |
+
const memoryResults = await this.memoryRepo.searchEntities({
|
| 70 |
+
orgId: ctx.orgId,
|
| 71 |
+
userId: ctx.userId,
|
| 72 |
+
keywords: query.split(/\s+/).filter(w => w.length > 2),
|
| 73 |
+
limit
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
memoryResults.forEach((entity: any, index: number) => {
|
| 77 |
+
results.push({
|
| 78 |
+
id: `memory-${entity.id}`,
|
| 79 |
+
type: 'memory',
|
| 80 |
+
score: 1.0 - (index / limit), // Rank-based score
|
| 81 |
+
content: entity,
|
| 82 |
+
source: 'memory_repository'
|
| 83 |
+
});
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
// Search SRAG documents
|
| 87 |
+
const sragResults = await this.sragRepo.searchDocuments(ctx.orgId, query);
|
| 88 |
+
sragResults.forEach((doc: any, index: number) => {
|
| 89 |
+
results.push({
|
| 90 |
+
id: `srag-${doc.id}`,
|
| 91 |
+
type: 'document',
|
| 92 |
+
score: 1.0 - (index / limit),
|
| 93 |
+
content: doc,
|
| 94 |
+
source: 'srag_repository'
|
| 95 |
+
});
|
| 96 |
+
});
|
| 97 |
+
} catch (error) {
|
| 98 |
+
console.warn('Keyword search error:', error);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return results;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/**
|
| 105 |
+
* Semantic search using embeddings
|
| 106 |
+
*/
|
| 107 |
+
private async semanticSearch(
|
| 108 |
+
query: string,
|
| 109 |
+
ctx: SearchContext,
|
| 110 |
+
limit: number
|
| 111 |
+
): Promise<SearchResult[]> {
|
| 112 |
+
const results: SearchResult[] = [];
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
// Use MemoryRepository's vector search
|
| 116 |
+
const memoryResults = await this.memoryRepo.searchEntities({
|
| 117 |
+
orgId: ctx.orgId,
|
| 118 |
+
userId: ctx.userId,
|
| 119 |
+
keywords: query ? [query] : [], // Use query as keyword for semantic search
|
| 120 |
+
limit
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
memoryResults.forEach((entity: any, index: number) => {
|
| 124 |
+
results.push({
|
| 125 |
+
id: `semantic-${entity.id}`,
|
| 126 |
+
type: 'memory',
|
| 127 |
+
score: entity.similarity || (1.0 - index / limit),
|
| 128 |
+
content: entity,
|
| 129 |
+
source: 'semantic_search'
|
| 130 |
+
});
|
| 131 |
+
});
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.warn('Semantic search error:', error);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
return results;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* Graph traversal search (knowledge graph patterns)
|
| 141 |
+
*/
|
| 142 |
+
private async graphTraversal(
|
| 143 |
+
query: string,
|
| 144 |
+
ctx: SearchContext,
|
| 145 |
+
limit: number
|
| 146 |
+
): Promise<SearchResult[]> {
|
| 147 |
+
const results: SearchResult[] = [];
|
| 148 |
+
|
| 149 |
+
try {
|
| 150 |
+
// Use UnifiedMemorySystem to find holographic patterns
|
| 151 |
+
const patterns = await unifiedMemorySystem.findHolographicPatterns(ctx);
|
| 152 |
+
|
| 153 |
+
if (Array.isArray(patterns)) {
|
| 154 |
+
patterns.forEach((pattern: any, index: number) => {
|
| 155 |
+
// Check if pattern matches query keywords
|
| 156 |
+
const queryWords = query.toLowerCase().split(/\s+/);
|
| 157 |
+
const patternText = JSON.stringify(pattern).toLowerCase();
|
| 158 |
+
const matchCount = queryWords.filter(word =>
|
| 159 |
+
patternText.includes(word)
|
| 160 |
+
).length;
|
| 161 |
+
|
| 162 |
+
if (matchCount > 0) {
|
| 163 |
+
results.push({
|
| 164 |
+
id: `pattern-${pattern.keyword || index}`,
|
| 165 |
+
type: 'pattern',
|
| 166 |
+
score: (matchCount / queryWords.length) * (pattern.frequency || 0.5),
|
| 167 |
+
content: pattern,
|
| 168 |
+
source: 'graph_traversal'
|
| 169 |
+
});
|
| 170 |
+
}
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
} catch (error) {
|
| 174 |
+
console.warn('Graph traversal error:', error);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
return results;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* Reciprocal Rank Fusion (RRF) to combine results from multiple sources
|
| 182 |
+
*/
|
| 183 |
+
private fuseResults(resultSets: SearchResult[][]): SearchResult[] {
|
| 184 |
+
const rrfScores = new Map<string, { result: SearchResult; score: number }>();
|
| 185 |
+
|
| 186 |
+
// Calculate RRF scores for each result set
|
| 187 |
+
resultSets.forEach((resultSet, setIndex) => {
|
| 188 |
+
resultSet.forEach((result, rank) => {
|
| 189 |
+
const existing = rrfScores.get(result.id);
|
| 190 |
+
const rrfScore = 1 / (rank + 60); // RRF formula: 1/(rank + k), k=60
|
| 191 |
+
|
| 192 |
+
if (existing) {
|
| 193 |
+
// Combine scores: RRF + original score
|
| 194 |
+
existing.score += rrfScore;
|
| 195 |
+
} else {
|
| 196 |
+
rrfScores.set(result.id, {
|
| 197 |
+
result,
|
| 198 |
+
score: rrfScore + (result.score * 0.1) // Weight original score
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
});
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
+
// Sort by combined score and return
|
| 205 |
+
return Array.from(rrfScores.values())
|
| 206 |
+
.sort((a, b) => b.score - a.score)
|
| 207 |
+
.map(item => ({
|
| 208 |
+
...item.result,
|
| 209 |
+
score: item.score
|
| 210 |
+
}));
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
export const hybridSearchEngine = new HybridSearchEngine();
|
| 215 |
+
|
apps/backend/src/mcp/cognitive/IntegrationManager.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* External Integrations
|
| 3 |
+
* Slack, GitHub, Jira, and other third-party services
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export interface SlackMessage {
|
| 7 |
+
channel: string;
|
| 8 |
+
text: string;
|
| 9 |
+
attachments?: any[];
|
| 10 |
+
thread_ts?: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface GitHubIssue {
|
| 14 |
+
title: string;
|
| 15 |
+
body: string;
|
| 16 |
+
labels?: string[];
|
| 17 |
+
assignees?: string[];
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export interface JiraTicket {
|
| 21 |
+
project: string;
|
| 22 |
+
summary: string;
|
| 23 |
+
description: string;
|
| 24 |
+
issueType: string;
|
| 25 |
+
priority?: string;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export class IntegrationManager {
|
| 29 |
+
private slackWebhook?: string;
|
| 30 |
+
private githubToken?: string;
|
| 31 |
+
private jiraCredentials?: { email: string; apiToken: string; domain: string };
|
| 32 |
+
|
| 33 |
+
constructor() {
|
| 34 |
+
this.slackWebhook = process.env.SLACK_WEBHOOK_URL;
|
| 35 |
+
this.githubToken = process.env.GITHUB_TOKEN;
|
| 36 |
+
|
| 37 |
+
if (process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN && process.env.JIRA_DOMAIN) {
|
| 38 |
+
this.jiraCredentials = {
|
| 39 |
+
email: process.env.JIRA_EMAIL,
|
| 40 |
+
apiToken: process.env.JIRA_API_TOKEN,
|
| 41 |
+
domain: process.env.JIRA_DOMAIN,
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Send Slack notification
|
| 48 |
+
*/
|
| 49 |
+
async sendSlackNotification(message: SlackMessage): Promise<boolean> {
|
| 50 |
+
if (!this.slackWebhook) {
|
| 51 |
+
console.warn('⚠️ Slack webhook not configured');
|
| 52 |
+
return false;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
try {
|
| 56 |
+
// In production, would make actual HTTP request
|
| 57 |
+
console.log(`📢 Slack notification: ${message.text} to ${message.channel}`);
|
| 58 |
+
return true;
|
| 59 |
+
} catch (error) {
|
| 60 |
+
console.error('Failed to send Slack notification:', error);
|
| 61 |
+
return false;
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* Create GitHub issue
|
| 67 |
+
*/
|
| 68 |
+
async createGitHubIssue(
|
| 69 |
+
repo: string,
|
| 70 |
+
issue: GitHubIssue
|
| 71 |
+
): Promise<{ number: number; url: string } | null> {
|
| 72 |
+
if (!this.githubToken) {
|
| 73 |
+
console.warn('⚠️ GitHub token not configured');
|
| 74 |
+
return null;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
try {
|
| 78 |
+
// In production, would make actual GitHub API call
|
| 79 |
+
const issueNumber = Math.floor(Math.random() * 1000);
|
| 80 |
+
const url = `https://github.com/${repo}/issues/${issueNumber}`;
|
| 81 |
+
|
| 82 |
+
console.log(`🐙 Created GitHub issue #${issueNumber}: ${issue.title}`);
|
| 83 |
+
return { number: issueNumber, url };
|
| 84 |
+
} catch (error) {
|
| 85 |
+
console.error('Failed to create GitHub issue:', error);
|
| 86 |
+
return null;
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/**
|
| 91 |
+
* Create Jira ticket
|
| 92 |
+
*/
|
| 93 |
+
async createJiraTicket(ticket: JiraTicket): Promise<{ key: string; url: string } | null> {
|
| 94 |
+
if (!this.jiraCredentials) {
|
| 95 |
+
console.warn('⚠️ Jira credentials not configured');
|
| 96 |
+
return null;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
try {
|
| 100 |
+
// In production, would make actual Jira API call
|
| 101 |
+
const ticketKey = `${ticket.project}-${Math.floor(Math.random() * 1000)}`;
|
| 102 |
+
const url = `https://${this.jiraCredentials.domain}/browse/${ticketKey}`;
|
| 103 |
+
|
| 104 |
+
console.log(`📋 Created Jira ticket ${ticketKey}: ${ticket.summary}`);
|
| 105 |
+
return { key: ticketKey, url };
|
| 106 |
+
} catch (error) {
|
| 107 |
+
console.error('Failed to create Jira ticket:', error);
|
| 108 |
+
return null;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Send alert to multiple channels
|
| 114 |
+
*/
|
| 115 |
+
async sendAlert(
|
| 116 |
+
message: string,
|
| 117 |
+
severity: 'info' | 'warning' | 'error' | 'critical',
|
| 118 |
+
channels: Array<'slack' | 'github' | 'jira'> = ['slack']
|
| 119 |
+
): Promise<void> {
|
| 120 |
+
const emoji = {
|
| 121 |
+
info: 'ℹ️',
|
| 122 |
+
warning: '⚠️',
|
| 123 |
+
error: '❌',
|
| 124 |
+
critical: '🚨',
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
const formattedMessage = `${emoji[severity]} ${message}`;
|
| 128 |
+
|
| 129 |
+
for (const channel of channels) {
|
| 130 |
+
switch (channel) {
|
| 131 |
+
case 'slack':
|
| 132 |
+
await this.sendSlackNotification({
|
| 133 |
+
channel: '#alerts',
|
| 134 |
+
text: formattedMessage,
|
| 135 |
+
});
|
| 136 |
+
break;
|
| 137 |
+
|
| 138 |
+
case 'github':
|
| 139 |
+
if (severity === 'error' || severity === 'critical') {
|
| 140 |
+
await this.createGitHubIssue('org/repo', {
|
| 141 |
+
title: `[${severity.toUpperCase()}] ${message}`,
|
| 142 |
+
body: `Automated alert generated at ${new Date().toISOString()}`,
|
| 143 |
+
labels: [severity, 'automated'],
|
| 144 |
+
});
|
| 145 |
+
}
|
| 146 |
+
break;
|
| 147 |
+
|
| 148 |
+
case 'jira':
|
| 149 |
+
if (severity === 'critical') {
|
| 150 |
+
await this.createJiraTicket({
|
| 151 |
+
project: 'OPS',
|
| 152 |
+
summary: message,
|
| 153 |
+
description: `Critical alert generated at ${new Date().toISOString()}`,
|
| 154 |
+
issueType: 'Bug',
|
| 155 |
+
priority: 'Highest',
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
break;
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/**
|
| 164 |
+
* Webhook receiver for external events
|
| 165 |
+
*/
|
| 166 |
+
async handleWebhook(
|
| 167 |
+
source: 'slack' | 'github' | 'jira',
|
| 168 |
+
payload: any
|
| 169 |
+
): Promise<void> {
|
| 170 |
+
console.log(`🔔 Received webhook from ${source}`);
|
| 171 |
+
|
| 172 |
+
switch (source) {
|
| 173 |
+
case 'slack':
|
| 174 |
+
await this.handleSlackEvent(payload);
|
| 175 |
+
break;
|
| 176 |
+
case 'github':
|
| 177 |
+
await this.handleGitHubEvent(payload);
|
| 178 |
+
break;
|
| 179 |
+
case 'jira':
|
| 180 |
+
await this.handleJiraEvent(payload);
|
| 181 |
+
break;
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
private async handleSlackEvent(payload: any): Promise<void> {
|
| 186 |
+
// Handle Slack slash commands, mentions, etc.
|
| 187 |
+
console.log('Processing Slack event:', payload.type);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
private async handleGitHubEvent(payload: any): Promise<void> {
|
| 191 |
+
// Handle GitHub webhooks (issues, PRs, comments)
|
| 192 |
+
console.log('Processing GitHub event:', payload.action);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
private async handleJiraEvent(payload: any): Promise<void> {
|
| 196 |
+
// Handle Jira webhooks (issue updates, comments)
|
| 197 |
+
console.log('Processing Jira event:', payload.webhookEvent);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* Sync data from external source
|
| 202 |
+
*/
|
| 203 |
+
async syncExternalData(
|
| 204 |
+
source: 'github' | 'jira',
|
| 205 |
+
query: string
|
| 206 |
+
): Promise<any[]> {
|
| 207 |
+
switch (source) {
|
| 208 |
+
case 'github':
|
| 209 |
+
return this.syncGitHubData(query);
|
| 210 |
+
case 'jira':
|
| 211 |
+
return this.syncJiraData(query);
|
| 212 |
+
default:
|
| 213 |
+
return [];
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
private async syncGitHubData(query: string): Promise<any[]> {
|
| 218 |
+
// Fetch issues, PRs, etc. from GitHub
|
| 219 |
+
console.log(`🔄 Syncing GitHub data: ${query}`);
|
| 220 |
+
return [];
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
private async syncJiraData(query: string): Promise<any[]> {
|
| 224 |
+
// Fetch tickets from Jira
|
| 225 |
+
console.log(`🔄 Syncing Jira data: ${query}`);
|
| 226 |
+
return [];
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
export const integrationManager = new IntegrationManager();
|
apps/backend/src/mcp/cognitive/MetaLearningEngine.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Meta-Learning Engine
|
| 3 |
+
* Learns how to learn - optimizes learning strategies across tasks
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export interface LearningStrategy {
|
| 7 |
+
name: string;
|
| 8 |
+
parameters: Record<string, any>;
|
| 9 |
+
performance: {
|
| 10 |
+
tasksApplied: number;
|
| 11 |
+
avgImprovement: number;
|
| 12 |
+
bestDomain: string;
|
| 13 |
+
};
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface TaskDomain {
|
| 17 |
+
name: string;
|
| 18 |
+
characteristics: string[];
|
| 19 |
+
optimalStrategy: string;
|
| 20 |
+
transferability: Map<string, number>; // How well learning transfers to other domains
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export class MetaLearningEngine {
|
| 24 |
+
private strategies: Map<string, LearningStrategy> = new Map();
|
| 25 |
+
private domains: Map<string, TaskDomain> = new Map();
|
| 26 |
+
private learningHistory: Array<{
|
| 27 |
+
domain: string;
|
| 28 |
+
strategy: string;
|
| 29 |
+
improvement: number;
|
| 30 |
+
timestamp: Date;
|
| 31 |
+
}> = [];
|
| 32 |
+
|
| 33 |
+
constructor() {
|
| 34 |
+
this.initializeDefaultStrategies();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Initialize default learning strategies
|
| 39 |
+
*/
|
| 40 |
+
private initializeDefaultStrategies(): void {
|
| 41 |
+
const defaultStrategies: LearningStrategy[] = [
|
| 42 |
+
{
|
| 43 |
+
name: 'gradient_descent',
|
| 44 |
+
parameters: { learningRate: 0.01, momentum: 0.9 },
|
| 45 |
+
performance: { tasksApplied: 0, avgImprovement: 0, bestDomain: '' },
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
name: 'few_shot',
|
| 49 |
+
parameters: { examples: 5, temperature: 0.7 },
|
| 50 |
+
performance: { tasksApplied: 0, avgImprovement: 0, bestDomain: '' },
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
name: 'reinforcement',
|
| 54 |
+
parameters: { explorationRate: 0.1, discountFactor: 0.95 },
|
| 55 |
+
performance: { tasksApplied: 0, avgImprovement: 0, bestDomain: '' },
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
name: 'transfer_learning',
|
| 59 |
+
parameters: { sourceTask: '', fineTuneEpochs: 10 },
|
| 60 |
+
performance: { tasksApplied: 0, avgImprovement: 0, bestDomain: '' },
|
| 61 |
+
},
|
| 62 |
+
];
|
| 63 |
+
|
| 64 |
+
defaultStrategies.forEach(strategy => {
|
| 65 |
+
this.strategies.set(strategy.name, strategy);
|
| 66 |
+
});
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Select optimal learning strategy for a task
|
| 71 |
+
*/
|
| 72 |
+
selectStrategy(domain: string, taskCharacteristics: string[]): LearningStrategy {
|
| 73 |
+
const domainInfo = this.domains.get(domain);
|
| 74 |
+
|
| 75 |
+
if (domainInfo && domainInfo.optimalStrategy) {
|
| 76 |
+
const strategy = this.strategies.get(domainInfo.optimalStrategy);
|
| 77 |
+
if (strategy) return strategy;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Find strategy with best performance in similar domains
|
| 81 |
+
const strategies = Array.from(this.strategies.values());
|
| 82 |
+
const scored = strategies.map(strategy => {
|
| 83 |
+
const relevantHistory = this.learningHistory.filter(h => h.strategy === strategy.name);
|
| 84 |
+
const avgImprovement = relevantHistory.length > 0
|
| 85 |
+
? relevantHistory.reduce((sum, h) => sum + h.improvement, 0) / relevantHistory.length
|
| 86 |
+
: 0;
|
| 87 |
+
|
| 88 |
+
return { strategy, score: avgImprovement };
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
scored.sort((a, b) => b.score - a.score);
|
| 92 |
+
return scored[0]?.strategy || strategies[0];
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Record learning outcome
|
| 97 |
+
*/
|
| 98 |
+
recordLearningOutcome(
|
| 99 |
+
domain: string,
|
| 100 |
+
strategy: string,
|
| 101 |
+
improvement: number
|
| 102 |
+
): void {
|
| 103 |
+
this.learningHistory.push({
|
| 104 |
+
domain,
|
| 105 |
+
strategy,
|
| 106 |
+
improvement,
|
| 107 |
+
timestamp: new Date(),
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// Update strategy performance
|
| 111 |
+
const strategyObj = this.strategies.get(strategy);
|
| 112 |
+
if (strategyObj) {
|
| 113 |
+
strategyObj.performance.tasksApplied++;
|
| 114 |
+
strategyObj.performance.avgImprovement =
|
| 115 |
+
(strategyObj.performance.avgImprovement * (strategyObj.performance.tasksApplied - 1) + improvement) /
|
| 116 |
+
strategyObj.performance.tasksApplied;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Update domain optimal strategy
|
| 120 |
+
this.updateDomainStrategy(domain);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* Update optimal strategy for a domain
|
| 125 |
+
*/
|
| 126 |
+
private updateDomainStrategy(domain: string): void {
|
| 127 |
+
const domainHistory = this.learningHistory.filter(h => h.domain === domain);
|
| 128 |
+
if (domainHistory.length < 5) return; // Need enough data
|
| 129 |
+
|
| 130 |
+
// Group by strategy
|
| 131 |
+
const strategyPerformance = new Map<string, number[]>();
|
| 132 |
+
domainHistory.forEach(h => {
|
| 133 |
+
if (!strategyPerformance.has(h.strategy)) {
|
| 134 |
+
strategyPerformance.set(h.strategy, []);
|
| 135 |
+
}
|
| 136 |
+
strategyPerformance.get(h.strategy)!.push(h.improvement);
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
// Find best strategy
|
| 140 |
+
let bestStrategy = '';
|
| 141 |
+
let bestAvg = -Infinity;
|
| 142 |
+
|
| 143 |
+
strategyPerformance.forEach((improvements, strategy) => {
|
| 144 |
+
const avg = improvements.reduce((sum, val) => sum + val, 0) / improvements.length;
|
| 145 |
+
if (avg > bestAvg) {
|
| 146 |
+
bestAvg = avg;
|
| 147 |
+
bestStrategy = strategy;
|
| 148 |
+
}
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
// Update domain
|
| 152 |
+
if (!this.domains.has(domain)) {
|
| 153 |
+
this.domains.set(domain, {
|
| 154 |
+
name: domain,
|
| 155 |
+
characteristics: [],
|
| 156 |
+
optimalStrategy: bestStrategy,
|
| 157 |
+
transferability: new Map(),
|
| 158 |
+
});
|
| 159 |
+
} else {
|
| 160 |
+
this.domains.get(domain)!.optimalStrategy = bestStrategy;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/**
|
| 165 |
+
* Transfer learning from one domain to another
|
| 166 |
+
*/
|
| 167 |
+
async transferLearning(
|
| 168 |
+
sourceDomain: string,
|
| 169 |
+
targetDomain: string
|
| 170 |
+
): Promise<LearningStrategy | null> {
|
| 171 |
+
const source = this.domains.get(sourceDomain);
|
| 172 |
+
if (!source) return null;
|
| 173 |
+
|
| 174 |
+
// Check transferability
|
| 175 |
+
const transferScore = source.transferability.get(targetDomain) || 0;
|
| 176 |
+
|
| 177 |
+
if (transferScore > 0.5) {
|
| 178 |
+
// High transferability - use source domain's strategy
|
| 179 |
+
const strategy = this.strategies.get(source.optimalStrategy);
|
| 180 |
+
if (strategy) {
|
| 181 |
+
console.log(`📚 Transferring learning from ${sourceDomain} to ${targetDomain}`);
|
| 182 |
+
return strategy;
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
return null;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* Optimize learning parameters
|
| 191 |
+
*/
|
| 192 |
+
optimizeParameters(strategyName: string): Record<string, any> {
|
| 193 |
+
const strategy = this.strategies.get(strategyName);
|
| 194 |
+
if (!strategy) return {};
|
| 195 |
+
|
| 196 |
+
// Simple parameter optimization based on historical performance
|
| 197 |
+
const recentHistory = this.learningHistory
|
| 198 |
+
.filter(h => h.strategy === strategyName)
|
| 199 |
+
.slice(-20);
|
| 200 |
+
|
| 201 |
+
if (recentHistory.length < 10) return strategy.parameters;
|
| 202 |
+
|
| 203 |
+
// Analyze if we should adjust learning rate (example)
|
| 204 |
+
const avgImprovement = recentHistory.reduce((sum, h) => sum + h.improvement, 0) / recentHistory.length;
|
| 205 |
+
|
| 206 |
+
if (avgImprovement < 0.1 && strategy.parameters.learningRate) {
|
| 207 |
+
// Low improvement - increase learning rate
|
| 208 |
+
strategy.parameters.learningRate *= 1.1;
|
| 209 |
+
} else if (avgImprovement > 0.5 && strategy.parameters.learningRate) {
|
| 210 |
+
// High improvement - decrease learning rate for fine-tuning
|
| 211 |
+
strategy.parameters.learningRate *= 0.9;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
return strategy.parameters;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/**
|
| 218 |
+
* Get meta-learning statistics
|
| 219 |
+
*/
|
| 220 |
+
getStatistics(): {
|
| 221 |
+
totalStrategies: number;
|
| 222 |
+
totalDomains: number;
|
| 223 |
+
mostEffectiveStrategy: string;
|
| 224 |
+
avgImprovementRate: number;
|
| 225 |
+
} {
|
| 226 |
+
const strategies = Array.from(this.strategies.values());
|
| 227 |
+
const mostEffective = strategies.sort((a, b) =>
|
| 228 |
+
b.performance.avgImprovement - a.performance.avgImprovement
|
| 229 |
+
)[0];
|
| 230 |
+
|
| 231 |
+
const avgImprovement = this.learningHistory.length > 0
|
| 232 |
+
? this.learningHistory.reduce((sum, h) => sum + h.improvement, 0) / this.learningHistory.length
|
| 233 |
+
: 0;
|
| 234 |
+
|
| 235 |
+
return {
|
| 236 |
+
totalStrategies: this.strategies.size,
|
| 237 |
+
totalDomains: this.domains.size,
|
| 238 |
+
mostEffectiveStrategy: mostEffective?.name || 'none',
|
| 239 |
+
avgImprovementRate: avgImprovement,
|
| 240 |
+
};
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
export const metaLearningEngine = new MetaLearningEngine();
|
apps/backend/src/mcp/cognitive/MultiModalProcessor.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Multi-Modal Support
|
| 3 |
+
* Handles images, audio, video, and cross-modal search
|
| 4 |
+
* PRODUCTION VERSION - NO MOCK DATA
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { getVectorStore } from '../../platform/vector/index.js';
|
| 8 |
+
import { getEmbeddingService } from '../../services/embeddings/EmbeddingService';
|
| 9 |
+
|
| 10 |
+
export interface MultiModalEmbedding {
|
| 11 |
+
id: string;
|
| 12 |
+
type: 'image' | 'audio' | 'video' | 'text';
|
| 13 |
+
embedding: number[];
|
| 14 |
+
metadata: Record<string, any>;
|
| 15 |
+
timestamp: Date;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export class MultiModalProcessor {
|
| 19 |
+
private clipModelLoaded: boolean = false;
|
| 20 |
+
private audioModelLoaded: boolean = false;
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Generate image embeddings using CLIP
|
| 24 |
+
* Requires CLIP model to be configured
|
| 25 |
+
*/
|
| 26 |
+
async generateImageEmbedding(imageUrl: string): Promise<number[]> {
|
| 27 |
+
if (!process.env.CLIP_MODEL_PATH && !process.env.OPENAI_API_KEY) {
|
| 28 |
+
throw new Error(
|
| 29 |
+
'CLIP model not configured. Set CLIP_MODEL_PATH or OPENAI_API_KEY in environment variables.'
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// TODO: Implement actual CLIP model integration
|
| 34 |
+
// Options:
|
| 35 |
+
// 1. Use OpenAI CLIP API
|
| 36 |
+
// 2. Use local CLIP model via transformers.js
|
| 37 |
+
// 3. Use HuggingFace Inference API
|
| 38 |
+
|
| 39 |
+
throw new Error('CLIP model integration not yet implemented. Please configure a CLIP provider.');
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Generate audio embeddings
|
| 44 |
+
* Requires audio processing model
|
| 45 |
+
*/
|
| 46 |
+
async generateAudioEmbedding(audioUrl: string): Promise<number[]> {
|
| 47 |
+
if (!process.env.AUDIO_MODEL_PATH) {
|
| 48 |
+
throw new Error(
|
| 49 |
+
'Audio model not configured. Set AUDIO_MODEL_PATH in environment variables.'
|
| 50 |
+
);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// TODO: Implement actual audio model integration
|
| 54 |
+
// Options:
|
| 55 |
+
// 1. Wav2Vec 2.0
|
| 56 |
+
// 2. OpenAI Whisper
|
| 57 |
+
// 3. HuggingFace audio models
|
| 58 |
+
|
| 59 |
+
throw new Error('Audio model integration not yet implemented. Please configure an audio model.');
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Generate video embeddings
|
| 64 |
+
* Combines visual and audio features
|
| 65 |
+
*/
|
| 66 |
+
async generateVideoEmbedding(videoUrl: string): Promise<number[]> {
|
| 67 |
+
if (!process.env.VIDEO_MODEL_PATH) {
|
| 68 |
+
throw new Error(
|
| 69 |
+
'Video model not configured. Set VIDEO_MODEL_PATH in environment variables.'
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// TODO: Implement actual video model integration
|
| 74 |
+
// Options:
|
| 75 |
+
// 1. Combine CLIP (visual) + Wav2Vec (audio)
|
| 76 |
+
// 2. Use specialized video models
|
| 77 |
+
// 3. Frame-by-frame processing
|
| 78 |
+
|
| 79 |
+
throw new Error('Video model integration not yet implemented. Please configure a video model.');
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Cross-modal search
|
| 84 |
+
* Search for images using text query, or vice versa
|
| 85 |
+
*/
|
| 86 |
+
async crossModalSearch(
|
| 87 |
+
query: string | number[],
|
| 88 |
+
targetModality: 'image' | 'audio' | 'video' | 'text',
|
| 89 |
+
limit: number = 10
|
| 90 |
+
): Promise<MultiModalEmbedding[]> {
|
| 91 |
+
// Convert query to embedding if needed
|
| 92 |
+
let queryEmbedding: number[];
|
| 93 |
+
if (typeof query === 'string') {
|
| 94 |
+
queryEmbedding = await this.generateTextEmbedding(query);
|
| 95 |
+
} else {
|
| 96 |
+
queryEmbedding = query;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Search in vector database
|
| 100 |
+
const vectorStore = await getVectorStore();
|
| 101 |
+
const results = await vectorStore.search({
|
| 102 |
+
vector: queryEmbedding,
|
| 103 |
+
namespace: `multimodal_${targetModality}`,
|
| 104 |
+
limit
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
return results.map(result => ({
|
| 108 |
+
id: result.id,
|
| 109 |
+
type: targetModality,
|
| 110 |
+
embedding: [], // Embedding not returned from search, would need separate lookup
|
| 111 |
+
metadata: result.metadata || {},
|
| 112 |
+
timestamp: new Date(result.metadata?.timestamp || Date.now()),
|
| 113 |
+
}));
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Generate text embedding for cross-modal comparison
|
| 118 |
+
*/
|
| 119 |
+
private async generateTextEmbedding(text: string): Promise<number[]> {
|
| 120 |
+
const embeddingService = getEmbeddingService();
|
| 121 |
+
const embedding = await embeddingService.generateEmbedding(text);
|
| 122 |
+
return embedding;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Multi-modal RAG
|
| 127 |
+
* Retrieve relevant content across all modalities
|
| 128 |
+
*/
|
| 129 |
+
async multiModalRAG(
|
| 130 |
+
query: string,
|
| 131 |
+
modalities: Array<'image' | 'audio' | 'video' | 'text'> = ['text', 'image']
|
| 132 |
+
): Promise<Map<string, MultiModalEmbedding[]>> {
|
| 133 |
+
const results = new Map<string, MultiModalEmbedding[]>();
|
| 134 |
+
|
| 135 |
+
for (const modality of modalities) {
|
| 136 |
+
try {
|
| 137 |
+
const modalityResults = await this.crossModalSearch(query, modality, 5);
|
| 138 |
+
results.set(modality, modalityResults);
|
| 139 |
+
} catch (error) {
|
| 140 |
+
console.error(`Failed to search ${modality}:`, error);
|
| 141 |
+
results.set(modality, []);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
console.log(`📚 Multi-modal RAG completed for: ${query}`);
|
| 146 |
+
return results;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Check if multi-modal features are available
|
| 151 |
+
*/
|
| 152 |
+
isConfigured(): {
|
| 153 |
+
clip: boolean;
|
| 154 |
+
audio: boolean;
|
| 155 |
+
video: boolean;
|
| 156 |
+
} {
|
| 157 |
+
return {
|
| 158 |
+
clip: !!(process.env.CLIP_MODEL_PATH || process.env.OPENAI_API_KEY),
|
| 159 |
+
audio: !!process.env.AUDIO_MODEL_PATH,
|
| 160 |
+
video: !!process.env.VIDEO_MODEL_PATH,
|
| 161 |
+
};
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
export const multiModalProcessor = new MultiModalProcessor();
|
apps/backend/src/mcp/cognitive/ObservabilitySystem.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Advanced Observability System
|
| 3 |
+
* Distributed tracing, metrics, and logging
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export interface Trace {
|
| 7 |
+
traceId: string;
|
| 8 |
+
spanId: string;
|
| 9 |
+
parentSpanId?: string;
|
| 10 |
+
operation: string;
|
| 11 |
+
startTime: Date;
|
| 12 |
+
endTime?: Date;
|
| 13 |
+
duration?: number;
|
| 14 |
+
status: 'success' | 'error' | 'pending';
|
| 15 |
+
tags: Record<string, any>;
|
| 16 |
+
logs: Array<{ timestamp: Date; message: string; level: string }>;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface Metric {
|
| 20 |
+
name: string;
|
| 21 |
+
value: number;
|
| 22 |
+
type: 'counter' | 'gauge' | 'histogram';
|
| 23 |
+
timestamp: Date;
|
| 24 |
+
tags: Record<string, string>;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export class ObservabilitySystem {
|
| 28 |
+
private traces: Map<string, Trace[]> = new Map();
|
| 29 |
+
private metrics: Metric[] = [];
|
| 30 |
+
private activeSpans: Map<string, Trace> = new Map();
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Start a new trace span
|
| 34 |
+
*/
|
| 35 |
+
startSpan(operation: string, parentSpanId?: string): string {
|
| 36 |
+
const spanId = this.generateId();
|
| 37 |
+
const traceId = parentSpanId
|
| 38 |
+
? this.findTraceId(parentSpanId)
|
| 39 |
+
: this.generateId();
|
| 40 |
+
|
| 41 |
+
const span: Trace = {
|
| 42 |
+
traceId,
|
| 43 |
+
spanId,
|
| 44 |
+
parentSpanId,
|
| 45 |
+
operation,
|
| 46 |
+
startTime: new Date(),
|
| 47 |
+
status: 'pending',
|
| 48 |
+
tags: {},
|
| 49 |
+
logs: [],
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
this.activeSpans.set(spanId, span);
|
| 53 |
+
|
| 54 |
+
if (!this.traces.has(traceId)) {
|
| 55 |
+
this.traces.set(traceId, []);
|
| 56 |
+
}
|
| 57 |
+
this.traces.get(traceId)!.push(span);
|
| 58 |
+
|
| 59 |
+
return spanId;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* End a trace span
|
| 64 |
+
*/
|
| 65 |
+
endSpan(spanId: string, status: 'success' | 'error' = 'success'): void {
|
| 66 |
+
const span = this.activeSpans.get(spanId);
|
| 67 |
+
if (!span) return;
|
| 68 |
+
|
| 69 |
+
span.endTime = new Date();
|
| 70 |
+
span.duration = span.endTime.getTime() - span.startTime.getTime();
|
| 71 |
+
span.status = status;
|
| 72 |
+
|
| 73 |
+
this.activeSpans.delete(spanId);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* Add tags to a span
|
| 78 |
+
*/
|
| 79 |
+
addTags(spanId: string, tags: Record<string, any>): void {
|
| 80 |
+
const span = this.activeSpans.get(spanId);
|
| 81 |
+
if (span) {
|
| 82 |
+
span.tags = { ...span.tags, ...tags };
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Add log to a span
|
| 88 |
+
*/
|
| 89 |
+
addLog(spanId: string, message: string, level: string = 'info'): void {
|
| 90 |
+
const span = this.activeSpans.get(spanId);
|
| 91 |
+
if (span) {
|
| 92 |
+
span.logs.push({
|
| 93 |
+
timestamp: new Date(),
|
| 94 |
+
message,
|
| 95 |
+
level,
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/**
|
| 101 |
+
* Record a metric
|
| 102 |
+
*/
|
| 103 |
+
recordMetric(
|
| 104 |
+
name: string,
|
| 105 |
+
value: number,
|
| 106 |
+
type: Metric['type'] = 'gauge',
|
| 107 |
+
tags: Record<string, string> = {}
|
| 108 |
+
): void {
|
| 109 |
+
this.metrics.push({
|
| 110 |
+
name,
|
| 111 |
+
value,
|
| 112 |
+
type,
|
| 113 |
+
timestamp: new Date(),
|
| 114 |
+
tags,
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
// Keep only last 10000 metrics
|
| 118 |
+
if (this.metrics.length > 10000) {
|
| 119 |
+
this.metrics.shift();
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* Get trace by ID
|
| 125 |
+
*/
|
| 126 |
+
getTrace(traceId: string): Trace[] | undefined {
|
| 127 |
+
return this.traces.get(traceId);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/**
|
| 131 |
+
* Get metrics by name
|
| 132 |
+
*/
|
| 133 |
+
getMetrics(name: string, since?: Date): Metric[] {
|
| 134 |
+
let filtered = this.metrics.filter(m => m.name === name);
|
| 135 |
+
|
| 136 |
+
if (since) {
|
| 137 |
+
filtered = filtered.filter(m => m.timestamp >= since);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return filtered;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* Get aggregated metrics
|
| 145 |
+
*/
|
| 146 |
+
getAggregatedMetrics(
|
| 147 |
+
name: string,
|
| 148 |
+
aggregation: 'sum' | 'avg' | 'min' | 'max' | 'count',
|
| 149 |
+
since?: Date
|
| 150 |
+
): number {
|
| 151 |
+
const metrics = this.getMetrics(name, since);
|
| 152 |
+
|
| 153 |
+
if (metrics.length === 0) return 0;
|
| 154 |
+
|
| 155 |
+
switch (aggregation) {
|
| 156 |
+
case 'sum':
|
| 157 |
+
return metrics.reduce((sum, m) => sum + m.value, 0);
|
| 158 |
+
case 'avg':
|
| 159 |
+
return metrics.reduce((sum, m) => sum + m.value, 0) / metrics.length;
|
| 160 |
+
case 'min':
|
| 161 |
+
return Math.min(...metrics.map(m => m.value));
|
| 162 |
+
case 'max':
|
| 163 |
+
return Math.max(...metrics.map(m => m.value));
|
| 164 |
+
case 'count':
|
| 165 |
+
return metrics.length;
|
| 166 |
+
default:
|
| 167 |
+
return 0;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/**
|
| 172 |
+
* Get slow traces (duration > threshold)
|
| 173 |
+
*/
|
| 174 |
+
getSlowTraces(thresholdMs: number = 1000): Trace[] {
|
| 175 |
+
const allTraces = Array.from(this.traces.values()).flat();
|
| 176 |
+
return allTraces.filter(trace =>
|
| 177 |
+
trace.duration && trace.duration > thresholdMs
|
| 178 |
+
);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Get error traces
|
| 183 |
+
*/
|
| 184 |
+
getErrorTraces(): Trace[] {
|
| 185 |
+
const allTraces = Array.from(this.traces.values()).flat();
|
| 186 |
+
return allTraces.filter(trace => trace.status === 'error');
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* Generate observability dashboard data
|
| 191 |
+
*/
|
| 192 |
+
getDashboardData(): {
|
| 193 |
+
totalTraces: number;
|
| 194 |
+
activeSpans: number;
|
| 195 |
+
errorRate: number;
|
| 196 |
+
avgDuration: number;
|
| 197 |
+
slowTraces: number;
|
| 198 |
+
topOperations: Array<{ operation: string; count: number }>;
|
| 199 |
+
} {
|
| 200 |
+
const allTraces = Array.from(this.traces.values()).flat();
|
| 201 |
+
const completedTraces = allTraces.filter(t => t.endTime);
|
| 202 |
+
const errorTraces = allTraces.filter(t => t.status === 'error');
|
| 203 |
+
|
| 204 |
+
const avgDuration = completedTraces.length > 0
|
| 205 |
+
? completedTraces.reduce((sum, t) => sum + (t.duration || 0), 0) / completedTraces.length
|
| 206 |
+
: 0;
|
| 207 |
+
|
| 208 |
+
// Count operations
|
| 209 |
+
const operationCounts = new Map<string, number>();
|
| 210 |
+
allTraces.forEach(trace => {
|
| 211 |
+
operationCounts.set(trace.operation, (operationCounts.get(trace.operation) || 0) + 1);
|
| 212 |
+
});
|
| 213 |
+
|
| 214 |
+
const topOperations = Array.from(operationCounts.entries())
|
| 215 |
+
.map(([operation, count]) => ({ operation, count }))
|
| 216 |
+
.sort((a, b) => b.count - a.count)
|
| 217 |
+
.slice(0, 10);
|
| 218 |
+
|
| 219 |
+
return {
|
| 220 |
+
totalTraces: allTraces.length,
|
| 221 |
+
activeSpans: this.activeSpans.size,
|
| 222 |
+
errorRate: completedTraces.length > 0 ? errorTraces.length / completedTraces.length : 0,
|
| 223 |
+
avgDuration,
|
| 224 |
+
slowTraces: this.getSlowTraces().length,
|
| 225 |
+
topOperations,
|
| 226 |
+
};
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
private generateId(): string {
|
| 230 |
+
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
private findTraceId(spanId: string): string {
|
| 234 |
+
for (const [traceId, spans] of this.traces.entries()) {
|
| 235 |
+
if (spans.some(s => s.spanId === spanId)) {
|
| 236 |
+
return traceId;
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
return this.generateId();
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
export const observabilitySystem = new ObservabilitySystem();
|
apps/backend/src/mcp/cognitive/PatternEvolutionEngine.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// PatternEvolutionEngine – Phase 2 Week 7-8
|
| 2 |
+
// Creative strategy evolution with mutation and A/B testing
|
| 3 |
+
|
| 4 |
+
import { projectMemory } from '../../services/project/ProjectMemory.js';
|
| 5 |
+
import { getDatabase } from '../../database/index.js';
|
| 6 |
+
|
| 7 |
+
interface Strategy {
|
| 8 |
+
id: string;
|
| 9 |
+
name: string;
|
| 10 |
+
approach: string;
|
| 11 |
+
timeout: number;
|
| 12 |
+
retryCount: number;
|
| 13 |
+
fitnessScore: number;
|
| 14 |
+
createdAt: Date;
|
| 15 |
+
adoptedAt?: Date;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface MutationConfig {
|
| 19 |
+
mutationRate: number;
|
| 20 |
+
creativityFactor: number;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
interface TestResult {
|
| 24 |
+
strategy: Strategy;
|
| 25 |
+
fitnessScore: number;
|
| 26 |
+
testDuration: number;
|
| 27 |
+
metrics: {
|
| 28 |
+
successRate: number;
|
| 29 |
+
avgLatency: number;
|
| 30 |
+
userSatisfaction: number;
|
| 31 |
+
};
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export class PatternEvolutionEngine {
|
| 35 |
+
private strategies: Map<string, Strategy> = new Map();
|
| 36 |
+
private currentBestStrategy: Strategy | null = null;
|
| 37 |
+
|
| 38 |
+
constructor() {
|
| 39 |
+
this.loadStrategies();
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Main evolution loop
|
| 44 |
+
*/
|
| 45 |
+
public async evolveStrategies(): Promise<void> {
|
| 46 |
+
console.log('🧬 [Evolution] Starting strategy evolution...');
|
| 47 |
+
|
| 48 |
+
// 1. Get current best strategy
|
| 49 |
+
const currentStrategy = await this.getBestStrategy();
|
| 50 |
+
|
| 51 |
+
if (!currentStrategy) {
|
| 52 |
+
// Initialize with default strategy
|
| 53 |
+
const defaultStrategy = this.createDefaultStrategy();
|
| 54 |
+
await this.saveStrategy(defaultStrategy);
|
| 55 |
+
this.currentBestStrategy = defaultStrategy;
|
| 56 |
+
console.log('✅ [Evolution] Initialized with default strategy');
|
| 57 |
+
return;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// 2. Generate mutations
|
| 61 |
+
const mutations = this.generateMutations(currentStrategy, {
|
| 62 |
+
mutationRate: 0.15,
|
| 63 |
+
creativityFactor: 0.4
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
console.log(`🧬 [Evolution] Generated ${mutations.length} mutations`);
|
| 67 |
+
|
| 68 |
+
// 3. A/B test mutations
|
| 69 |
+
const testResults = await this.abTest(mutations);
|
| 70 |
+
|
| 71 |
+
// 4. Select winners (must be >10% improvement)
|
| 72 |
+
const winners = testResults.filter(r =>
|
| 73 |
+
r.fitnessScore > currentStrategy.fitnessScore * 1.1
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
// 5. Adopt best winner if improvement found
|
| 77 |
+
if (winners.length > 0) {
|
| 78 |
+
const best = winners.sort((a, b) => b.fitnessScore - a.fitnessScore)[0];
|
| 79 |
+
await this.adoptStrategy(best.strategy);
|
| 80 |
+
|
| 81 |
+
// Log to ProjectMemory
|
| 82 |
+
await this.logEvolution({
|
| 83 |
+
oldStrategy: currentStrategy,
|
| 84 |
+
newStrategy: best.strategy,
|
| 85 |
+
improvement: best.fitnessScore / currentStrategy.fitnessScore,
|
| 86 |
+
testResults: testResults.length
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
console.log(`✅ [Evolution] Adopted new strategy: ${best.strategy.name} (${((best.fitnessScore / currentStrategy.fitnessScore - 1) * 100).toFixed(1)}% improvement)`);
|
| 90 |
+
} else {
|
| 91 |
+
console.log('ℹ️ [Evolution] No improvement found, keeping current strategy');
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Get current best strategy
|
| 97 |
+
*/
|
| 98 |
+
private async getBestStrategy(): Promise<Strategy | null> {
|
| 99 |
+
if (this.currentBestStrategy) {
|
| 100 |
+
return this.currentBestStrategy;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Load from database or memory
|
| 104 |
+
const strategies = Array.from(this.strategies.values());
|
| 105 |
+
if (strategies.length === 0) {
|
| 106 |
+
return null;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const best = strategies.sort((a, b) => b.fitnessScore - a.fitnessScore)[0];
|
| 110 |
+
this.currentBestStrategy = best;
|
| 111 |
+
return best;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Generate strategy mutations
|
| 116 |
+
*/
|
| 117 |
+
private generateMutations(strategy: Strategy, config: MutationConfig): Strategy[] {
|
| 118 |
+
const mutations: Strategy[] = [];
|
| 119 |
+
|
| 120 |
+
for (let i = 0; i < 10; i++) {
|
| 121 |
+
const mutated: Strategy = {
|
| 122 |
+
...strategy,
|
| 123 |
+
id: `${strategy.id}-mut-${i}-${Date.now()}`,
|
| 124 |
+
name: `${strategy.name} Mutation ${i + 1}`,
|
| 125 |
+
fitnessScore: strategy.fitnessScore, // Will be updated after testing
|
| 126 |
+
createdAt: new Date()
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
// Mutate timeout
|
| 130 |
+
if (Math.random() < config.mutationRate) {
|
| 131 |
+
mutated.timeout = Math.max(100, strategy.timeout * (1 + (Math.random() - 0.5) * 0.3));
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Mutate retry count
|
| 135 |
+
if (Math.random() < config.mutationRate) {
|
| 136 |
+
mutated.retryCount = Math.max(0, strategy.retryCount + Math.floor((Math.random() - 0.5) * 2));
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Creative mutations (approach changes)
|
| 140 |
+
if (Math.random() < config.creativityFactor) {
|
| 141 |
+
mutated.approach = this.generateCreativeApproach(strategy.approach);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
mutations.push(mutated);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return mutations;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Generate creative approach variations
|
| 152 |
+
*/
|
| 153 |
+
private generateCreativeApproach(currentApproach: string): string {
|
| 154 |
+
const variations = [
|
| 155 |
+
'aggressive', 'conservative', 'balanced', 'adaptive', 'predictive'
|
| 156 |
+
];
|
| 157 |
+
|
| 158 |
+
const randomVariation = variations[Math.floor(Math.random() * variations.length)];
|
| 159 |
+
return `${randomVariation}_${currentApproach}`;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* A/B test mutations
|
| 164 |
+
*/
|
| 165 |
+
private async abTest(mutations: Strategy[]): Promise<TestResult[]> {
|
| 166 |
+
const results: TestResult[] = [];
|
| 167 |
+
|
| 168 |
+
for (const mutation of mutations) {
|
| 169 |
+
// Simulate testing (in real implementation, this would run actual tests)
|
| 170 |
+
const testResult = await this.simulateTest(mutation);
|
| 171 |
+
results.push(testResult);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
return results;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/**
|
| 178 |
+
* Simulate strategy test (placeholder - should run actual tests)
|
| 179 |
+
*/
|
| 180 |
+
private async simulateTest(strategy: Strategy): Promise<TestResult> {
|
| 181 |
+
// Simulate fitness calculation based on strategy parameters
|
| 182 |
+
const baseFitness = 0.5;
|
| 183 |
+
|
| 184 |
+
// Timeout optimization: shorter is better (up to a point)
|
| 185 |
+
const timeoutScore = Math.max(0, 1 - (strategy.timeout / 5000));
|
| 186 |
+
|
| 187 |
+
// Retry optimization: balanced retries are better
|
| 188 |
+
const retryScore = strategy.retryCount <= 3 ? 1.0 : Math.max(0, 1 - (strategy.retryCount - 3) * 0.2);
|
| 189 |
+
|
| 190 |
+
// Approach bonus (creative approaches get slight bonus)
|
| 191 |
+
const approachBonus = strategy.approach.includes('adaptive') || strategy.approach.includes('predictive') ? 0.1 : 0;
|
| 192 |
+
|
| 193 |
+
const fitnessScore = baseFitness + timeoutScore * 0.3 + retryScore * 0.2 + approachBonus;
|
| 194 |
+
|
| 195 |
+
return {
|
| 196 |
+
strategy,
|
| 197 |
+
fitnessScore: Math.min(1.0, fitnessScore),
|
| 198 |
+
testDuration: 1000 + Math.random() * 2000,
|
| 199 |
+
metrics: {
|
| 200 |
+
successRate: 0.7 + Math.random() * 0.25,
|
| 201 |
+
avgLatency: strategy.timeout * (0.8 + Math.random() * 0.4),
|
| 202 |
+
userSatisfaction: fitnessScore
|
| 203 |
+
}
|
| 204 |
+
};
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/**
|
| 208 |
+
* Adopt new strategy
|
| 209 |
+
*/
|
| 210 |
+
private async adoptStrategy(strategy: Strategy): Promise<void> {
|
| 211 |
+
strategy.adoptedAt = new Date();
|
| 212 |
+
await this.saveStrategy(strategy);
|
| 213 |
+
this.currentBestStrategy = strategy;
|
| 214 |
+
this.strategies.set(strategy.id, strategy);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/**
|
| 218 |
+
* Create default strategy
|
| 219 |
+
*/
|
| 220 |
+
private createDefaultStrategy(): Strategy {
|
| 221 |
+
return {
|
| 222 |
+
id: 'default-strategy',
|
| 223 |
+
name: 'Default Strategy',
|
| 224 |
+
approach: 'balanced',
|
| 225 |
+
timeout: 3000,
|
| 226 |
+
retryCount: 2,
|
| 227 |
+
fitnessScore: 0.5,
|
| 228 |
+
createdAt: new Date()
|
| 229 |
+
};
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Save strategy (placeholder - should persist to database)
|
| 234 |
+
*/
|
| 235 |
+
private async saveStrategy(strategy: Strategy): Promise<void> {
|
| 236 |
+
this.strategies.set(strategy.id, strategy);
|
| 237 |
+
// TODO: Persist to database
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* Load strategies (placeholder)
|
| 242 |
+
*/
|
| 243 |
+
private loadStrategies(): void {
|
| 244 |
+
// TODO: Load from database
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/**
|
| 248 |
+
* Log evolution to ProjectMemory
|
| 249 |
+
*/
|
| 250 |
+
private async logEvolution(evolution: {
|
| 251 |
+
oldStrategy: Strategy;
|
| 252 |
+
newStrategy: Strategy;
|
| 253 |
+
improvement: number;
|
| 254 |
+
testResults: number;
|
| 255 |
+
}): Promise<void> {
|
| 256 |
+
projectMemory.logLifecycleEvent({
|
| 257 |
+
eventType: 'feature',
|
| 258 |
+
status: 'success',
|
| 259 |
+
details: {
|
| 260 |
+
component: 'PatternEvolutionEngine',
|
| 261 |
+
action: 'strategy_evolution',
|
| 262 |
+
oldStrategy: evolution.oldStrategy.name,
|
| 263 |
+
newStrategy: evolution.newStrategy.name,
|
| 264 |
+
improvement: `${((evolution.improvement - 1) * 100).toFixed(1)}%`,
|
| 265 |
+
testResults: evolution.testResults,
|
| 266 |
+
timestamp: new Date().toISOString()
|
| 267 |
+
}
|
| 268 |
+
});
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/**
|
| 272 |
+
* Get current strategy
|
| 273 |
+
*/
|
| 274 |
+
public getCurrentStrategy(): Strategy | null {
|
| 275 |
+
return this.currentBestStrategy;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
/**
|
| 279 |
+
* Get evolution history
|
| 280 |
+
*/
|
| 281 |
+
public getEvolutionHistory(): Strategy[] {
|
| 282 |
+
return Array.from(this.strategies.values())
|
| 283 |
+
.filter(s => s.adoptedAt)
|
| 284 |
+
.sort((a, b) => (b.adoptedAt?.getTime() || 0) - (a.adoptedAt?.getTime() || 0));
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
export const patternEvolutionEngine = new PatternEvolutionEngine();
|
| 289 |
+
|
apps/backend/src/mcp/cognitive/RLHFAlignmentSystem.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RLHF (Reinforcement Learning from Human Feedback) Alignment System
|
| 3 |
+
* Aligns AI behavior with human preferences through feedback
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export interface HumanFeedback {
|
| 7 |
+
id: string;
|
| 8 |
+
taskId: string;
|
| 9 |
+
agentId: string;
|
| 10 |
+
rating: number; // 1-5
|
| 11 |
+
feedback: string;
|
| 12 |
+
timestamp: Date;
|
| 13 |
+
category: 'helpful' | 'harmless' | 'honest';
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface PreferenceComparison {
|
| 17 |
+
id: string;
|
| 18 |
+
responseA: string;
|
| 19 |
+
responseB: string;
|
| 20 |
+
preferred: 'A' | 'B' | 'equal';
|
| 21 |
+
reason?: string;
|
| 22 |
+
timestamp: Date;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface RewardModel {
|
| 26 |
+
weights: Map<string, number>;
|
| 27 |
+
bias: number;
|
| 28 |
+
accuracy: number;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export class RLHFAlignmentSystem {
|
| 32 |
+
private feedbackLog: HumanFeedback[] = [];
|
| 33 |
+
private preferences: PreferenceComparison[] = [];
|
| 34 |
+
private rewardModel: RewardModel = {
|
| 35 |
+
weights: new Map(),
|
| 36 |
+
bias: 0,
|
| 37 |
+
accuracy: 0,
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Collect human feedback
|
| 42 |
+
*/
|
| 43 |
+
collectFeedback(feedback: Omit<HumanFeedback, 'id' | 'timestamp'>): string {
|
| 44 |
+
const fullFeedback: HumanFeedback = {
|
| 45 |
+
...feedback,
|
| 46 |
+
id: `feedback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
| 47 |
+
timestamp: new Date(),
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
this.feedbackLog.push(fullFeedback);
|
| 51 |
+
|
| 52 |
+
// Keep only last 1000 feedbacks
|
| 53 |
+
if (this.feedbackLog.length > 1000) {
|
| 54 |
+
this.feedbackLog.shift();
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return fullFeedback.id;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* Collect preference comparison
|
| 62 |
+
*/
|
| 63 |
+
collectPreference(comparison: Omit<PreferenceComparison, 'id' | 'timestamp'>): string {
|
| 64 |
+
const fullComparison: PreferenceComparison = {
|
| 65 |
+
...comparison,
|
| 66 |
+
id: `pref_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
| 67 |
+
timestamp: new Date(),
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
this.preferences.push(fullComparison);
|
| 71 |
+
|
| 72 |
+
// Retrain reward model periodically
|
| 73 |
+
if (this.preferences.length % 10 === 0) {
|
| 74 |
+
this.trainRewardModel();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
return fullComparison.id;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Train reward model from preferences
|
| 82 |
+
*/
|
| 83 |
+
private trainRewardModel(): void {
|
| 84 |
+
if (this.preferences.length < 10) return;
|
| 85 |
+
|
| 86 |
+
// Simple reward model training
|
| 87 |
+
// In production, this would use proper ML techniques
|
| 88 |
+
|
| 89 |
+
const features = new Map<string, number>();
|
| 90 |
+
|
| 91 |
+
this.preferences.forEach(pref => {
|
| 92 |
+
const preferred = pref.preferred === 'A' ? pref.responseA : pref.responseB;
|
| 93 |
+
const notPreferred = pref.preferred === 'A' ? pref.responseB : pref.responseA;
|
| 94 |
+
|
| 95 |
+
// Extract simple features (length, politeness markers, etc.)
|
| 96 |
+
const prefLength = preferred.length;
|
| 97 |
+
const notPrefLength = notPreferred.length;
|
| 98 |
+
|
| 99 |
+
features.set('length_preference', (features.get('length_preference') || 0) +
|
| 100 |
+
(prefLength > notPrefLength ? 1 : -1));
|
| 101 |
+
|
| 102 |
+
// Check for politeness markers
|
| 103 |
+
const politeWords = ['please', 'thank', 'appreciate', 'kindly'];
|
| 104 |
+
const prefPolite = politeWords.some(word => preferred.toLowerCase().includes(word));
|
| 105 |
+
const notPrefPolite = politeWords.some(word => notPreferred.toLowerCase().includes(word));
|
| 106 |
+
|
| 107 |
+
if (prefPolite && !notPrefPolite) {
|
| 108 |
+
features.set('politeness', (features.get('politeness') || 0) + 1);
|
| 109 |
+
}
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
// Update reward model weights
|
| 113 |
+
features.forEach((value, feature) => {
|
| 114 |
+
this.rewardModel.weights.set(feature, value / this.preferences.length);
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
console.log(`🎯 Reward model updated with ${this.preferences.length} preferences`);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* Predict reward for a response
|
| 122 |
+
*/
|
| 123 |
+
predictReward(response: string): number {
|
| 124 |
+
let reward = this.rewardModel.bias;
|
| 125 |
+
|
| 126 |
+
// Apply learned weights
|
| 127 |
+
const length = response.length;
|
| 128 |
+
reward += (this.rewardModel.weights.get('length_preference') || 0) * length / 1000;
|
| 129 |
+
|
| 130 |
+
const politeWords = ['please', 'thank', 'appreciate', 'kindly'];
|
| 131 |
+
const isPolite = politeWords.some(word => response.toLowerCase().includes(word));
|
| 132 |
+
if (isPolite) {
|
| 133 |
+
reward += this.rewardModel.weights.get('politeness') || 0;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
return reward;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* Optimize response based on learned preferences
|
| 141 |
+
*/
|
| 142 |
+
optimizeResponse(candidates: string[]): string {
|
| 143 |
+
if (candidates.length === 0) return '';
|
| 144 |
+
if (candidates.length === 1) return candidates[0];
|
| 145 |
+
|
| 146 |
+
// Score each candidate
|
| 147 |
+
const scored = candidates.map(candidate => ({
|
| 148 |
+
response: candidate,
|
| 149 |
+
reward: this.predictReward(candidate),
|
| 150 |
+
}));
|
| 151 |
+
|
| 152 |
+
// Return highest scoring
|
| 153 |
+
scored.sort((a, b) => b.reward - a.reward);
|
| 154 |
+
return scored[0].response;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Check alignment with safety constraints
|
| 159 |
+
*/
|
| 160 |
+
checkSafetyConstraints(response: string): {
|
| 161 |
+
safe: boolean;
|
| 162 |
+
violations: string[];
|
| 163 |
+
} {
|
| 164 |
+
const violations: string[] = [];
|
| 165 |
+
|
| 166 |
+
// Check for harmful content patterns
|
| 167 |
+
const harmfulPatterns = [
|
| 168 |
+
/\b(hack|exploit|bypass)\b/i,
|
| 169 |
+
/\b(illegal|unlawful)\b/i,
|
| 170 |
+
/\b(violence|harm|hurt)\b/i,
|
| 171 |
+
];
|
| 172 |
+
|
| 173 |
+
harmfulPatterns.forEach((pattern, index) => {
|
| 174 |
+
if (pattern.test(response)) {
|
| 175 |
+
violations.push(`Potential harmful content detected (pattern ${index + 1})`);
|
| 176 |
+
}
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
// Check for dishonest patterns
|
| 180 |
+
if (response.includes('I am certain') && response.includes('probably')) {
|
| 181 |
+
violations.push('Contradictory certainty claims');
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
return {
|
| 185 |
+
safe: violations.length === 0,
|
| 186 |
+
violations,
|
| 187 |
+
};
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
/**
|
| 191 |
+
* Get feedback statistics
|
| 192 |
+
*/
|
| 193 |
+
getFeedbackStatistics(agentId?: string): {
|
| 194 |
+
totalFeedback: number;
|
| 195 |
+
avgRating: number;
|
| 196 |
+
categoryBreakdown: Record<string, number>;
|
| 197 |
+
recentTrend: 'improving' | 'declining' | 'stable';
|
| 198 |
+
} {
|
| 199 |
+
const relevant = agentId
|
| 200 |
+
? this.feedbackLog.filter(f => f.agentId === agentId)
|
| 201 |
+
: this.feedbackLog;
|
| 202 |
+
|
| 203 |
+
const avgRating = relevant.length > 0
|
| 204 |
+
? relevant.reduce((sum, f) => sum + f.rating, 0) / relevant.length
|
| 205 |
+
: 0;
|
| 206 |
+
|
| 207 |
+
const categoryBreakdown = relevant.reduce((acc, f) => {
|
| 208 |
+
acc[f.category] = (acc[f.category] || 0) + 1;
|
| 209 |
+
return acc;
|
| 210 |
+
}, {} as Record<string, number>);
|
| 211 |
+
|
| 212 |
+
// Analyze trend (last 20 vs previous 20)
|
| 213 |
+
const recent = relevant.slice(-20);
|
| 214 |
+
const previous = relevant.slice(-40, -20);
|
| 215 |
+
|
| 216 |
+
const recentAvg = recent.length > 0
|
| 217 |
+
? recent.reduce((sum, f) => sum + f.rating, 0) / recent.length
|
| 218 |
+
: 0;
|
| 219 |
+
const previousAvg = previous.length > 0
|
| 220 |
+
? previous.reduce((sum, f) => sum + f.rating, 0) / previous.length
|
| 221 |
+
: 0;
|
| 222 |
+
|
| 223 |
+
let trend: 'improving' | 'declining' | 'stable' = 'stable';
|
| 224 |
+
if (recentAvg > previousAvg + 0.2) trend = 'improving';
|
| 225 |
+
else if (recentAvg < previousAvg - 0.2) trend = 'declining';
|
| 226 |
+
|
| 227 |
+
return {
|
| 228 |
+
totalFeedback: relevant.length,
|
| 229 |
+
avgRating,
|
| 230 |
+
categoryBreakdown,
|
| 231 |
+
recentTrend: trend,
|
| 232 |
+
};
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/**
|
| 236 |
+
* Apply alignment corrections
|
| 237 |
+
*/
|
| 238 |
+
async applyAlignmentCorrections(agentId: string): Promise<string[]> {
|
| 239 |
+
const stats = this.getFeedbackStatistics(agentId);
|
| 240 |
+
const corrections: string[] = [];
|
| 241 |
+
|
| 242 |
+
if (stats.avgRating < 3) {
|
| 243 |
+
corrections.push('Overall performance below acceptable threshold');
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
if (stats.categoryBreakdown.harmless && stats.categoryBreakdown.harmless < stats.totalFeedback * 0.8) {
|
| 247 |
+
corrections.push('Increase safety measures');
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if (stats.categoryBreakdown.honest && stats.categoryBreakdown.honest < stats.totalFeedback * 0.8) {
|
| 251 |
+
corrections.push('Improve honesty and transparency');
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
if (stats.recentTrend === 'declining') {
|
| 255 |
+
corrections.push('Performance declining - review recent changes');
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
return corrections;
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
export const rlhfAlignmentSystem = new RLHFAlignmentSystem();
|
apps/backend/src/mcp/cognitive/SelfReflectionEngine.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { neo4jService } from '../../database/Neo4jService';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Self-Reflection Engine
|
| 5 |
+
* Enables agents to assess their own performance and improve
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
export interface PerformanceMetrics {
|
| 9 |
+
taskId: string;
|
| 10 |
+
agentId: string;
|
| 11 |
+
success: boolean;
|
| 12 |
+
duration: number;
|
| 13 |
+
errorType?: string;
|
| 14 |
+
timestamp: Date;
|
| 15 |
+
context: Record<string, any>;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export interface ReflectionInsight {
|
| 19 |
+
pattern: string;
|
| 20 |
+
frequency: number;
|
| 21 |
+
impact: 'positive' | 'negative' | 'neutral';
|
| 22 |
+
recommendation: string;
|
| 23 |
+
confidence: number;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export class SelfReflectionEngine {
|
| 27 |
+
private performanceLog: PerformanceMetrics[] = [];
|
| 28 |
+
private insights: ReflectionInsight[] = [];
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Log performance data
|
| 32 |
+
*/
|
| 33 |
+
logPerformance(metrics: PerformanceMetrics): void {
|
| 34 |
+
this.performanceLog.push(metrics);
|
| 35 |
+
|
| 36 |
+
// Keep only last 1000 entries
|
| 37 |
+
if (this.performanceLog.length > 1000) {
|
| 38 |
+
this.performanceLog.shift();
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Analyze error patterns
|
| 44 |
+
*/
|
| 45 |
+
analyzeErrorPatterns(agentId?: string): Map<string, number> {
|
| 46 |
+
const errors = this.performanceLog.filter(log =>
|
| 47 |
+
!log.success && (!agentId || log.agentId === agentId)
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
const errorCounts = new Map<string, number>();
|
| 51 |
+
errors.forEach(error => {
|
| 52 |
+
if (error.errorType) {
|
| 53 |
+
errorCounts.set(error.errorType, (errorCounts.get(error.errorType) || 0) + 1);
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
return errorCounts;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* Evaluate strategy effectiveness
|
| 62 |
+
*/
|
| 63 |
+
evaluateStrategyEffectiveness(
|
| 64 |
+
strategy: string,
|
| 65 |
+
timeWindow: number = 7 * 24 * 60 * 60 * 1000 // 7 days
|
| 66 |
+
): {
|
| 67 |
+
successRate: number;
|
| 68 |
+
avgDuration: number;
|
| 69 |
+
totalAttempts: number;
|
| 70 |
+
} {
|
| 71 |
+
const cutoff = new Date(Date.now() - timeWindow);
|
| 72 |
+
const relevant = this.performanceLog.filter(log =>
|
| 73 |
+
log.timestamp > cutoff &&
|
| 74 |
+
log.context.strategy === strategy
|
| 75 |
+
);
|
| 76 |
+
|
| 77 |
+
if (relevant.length === 0) {
|
| 78 |
+
return { successRate: 0, avgDuration: 0, totalAttempts: 0 };
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
const successes = relevant.filter(log => log.success).length;
|
| 82 |
+
const totalDuration = relevant.reduce((sum, log) => sum + log.duration, 0);
|
| 83 |
+
|
| 84 |
+
return {
|
| 85 |
+
successRate: successes / relevant.length,
|
| 86 |
+
avgDuration: totalDuration / relevant.length,
|
| 87 |
+
totalAttempts: relevant.length,
|
| 88 |
+
};
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Generate improvement recommendations
|
| 93 |
+
*/
|
| 94 |
+
async generateRecommendations(agentId: string): Promise<ReflectionInsight[]> {
|
| 95 |
+
const errorPatterns = this.analyzeErrorPatterns(agentId);
|
| 96 |
+
const recommendations: ReflectionInsight[] = [];
|
| 97 |
+
|
| 98 |
+
// Analyze error patterns
|
| 99 |
+
errorPatterns.forEach((count, errorType) => {
|
| 100 |
+
if (count > 5) {
|
| 101 |
+
recommendations.push({
|
| 102 |
+
pattern: `Frequent ${errorType} errors`,
|
| 103 |
+
frequency: count,
|
| 104 |
+
impact: 'negative',
|
| 105 |
+
recommendation: `Implement better error handling for ${errorType}`,
|
| 106 |
+
confidence: Math.min(count / 10, 1),
|
| 107 |
+
});
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
// Analyze performance trends
|
| 112 |
+
const recentPerformance = this.performanceLog
|
| 113 |
+
.filter(log => log.agentId === agentId)
|
| 114 |
+
.slice(-50);
|
| 115 |
+
|
| 116 |
+
if (recentPerformance.length > 10) {
|
| 117 |
+
const successRate = recentPerformance.filter(log => log.success).length / recentPerformance.length;
|
| 118 |
+
|
| 119 |
+
if (successRate < 0.7) {
|
| 120 |
+
recommendations.push({
|
| 121 |
+
pattern: 'Low success rate',
|
| 122 |
+
frequency: recentPerformance.length,
|
| 123 |
+
impact: 'negative',
|
| 124 |
+
recommendation: 'Review task assignment criteria and agent capabilities',
|
| 125 |
+
confidence: 1 - successRate,
|
| 126 |
+
});
|
| 127 |
+
} else if (successRate > 0.95) {
|
| 128 |
+
recommendations.push({
|
| 129 |
+
pattern: 'High success rate',
|
| 130 |
+
frequency: recentPerformance.length,
|
| 131 |
+
impact: 'positive',
|
| 132 |
+
recommendation: 'Consider taking on more complex tasks',
|
| 133 |
+
confidence: successRate,
|
| 134 |
+
});
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
this.insights = recommendations;
|
| 139 |
+
return recommendations;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* Continuous improvement loop
|
| 144 |
+
*/
|
| 145 |
+
async runImprovementCycle(agentId: string): Promise<void> {
|
| 146 |
+
const recommendations = await this.generateRecommendations(agentId);
|
| 147 |
+
|
| 148 |
+
// Store insights in Neo4j for long-term learning
|
| 149 |
+
try {
|
| 150 |
+
await neo4jService.connect();
|
| 151 |
+
|
| 152 |
+
for (const insight of recommendations) {
|
| 153 |
+
await neo4jService.runQuery(
|
| 154 |
+
`MERGE (a:Agent {id: $agentId})
|
| 155 |
+
CREATE (i:Insight {
|
| 156 |
+
pattern: $pattern,
|
| 157 |
+
recommendation: $recommendation,
|
| 158 |
+
confidence: $confidence,
|
| 159 |
+
timestamp: datetime()
|
| 160 |
+
})
|
| 161 |
+
CREATE (a)-[:HAS_INSIGHT]->(i)`,
|
| 162 |
+
{
|
| 163 |
+
agentId,
|
| 164 |
+
pattern: insight.pattern,
|
| 165 |
+
recommendation: insight.recommendation,
|
| 166 |
+
confidence: insight.confidence,
|
| 167 |
+
}
|
| 168 |
+
);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
await neo4jService.disconnect();
|
| 172 |
+
} catch (error) {
|
| 173 |
+
console.error('Failed to store insights:', error);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
console.log(`🔍 Generated ${recommendations.length} improvement recommendations for ${agentId}`);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* Get performance summary
|
| 181 |
+
*/
|
| 182 |
+
getPerformanceSummary(agentId: string, days: number = 7): {
|
| 183 |
+
totalTasks: number;
|
| 184 |
+
successRate: number;
|
| 185 |
+
avgDuration: number;
|
| 186 |
+
errorBreakdown: Map<string, number>;
|
| 187 |
+
} {
|
| 188 |
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
| 189 |
+
const relevant = this.performanceLog.filter(log =>
|
| 190 |
+
log.agentId === agentId && log.timestamp > cutoff
|
| 191 |
+
);
|
| 192 |
+
|
| 193 |
+
const successes = relevant.filter(log => log.success).length;
|
| 194 |
+
const totalDuration = relevant.reduce((sum, log) => sum + log.duration, 0);
|
| 195 |
+
|
| 196 |
+
return {
|
| 197 |
+
totalTasks: relevant.length,
|
| 198 |
+
successRate: relevant.length > 0 ? successes / relevant.length : 0,
|
| 199 |
+
avgDuration: relevant.length > 0 ? totalDuration / relevant.length : 0,
|
| 200 |
+
errorBreakdown: this.analyzeErrorPatterns(agentId),
|
| 201 |
+
};
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
export const selfReflectionEngine = new SelfReflectionEngine();
|
apps/backend/src/mcp/cognitive/StateGraphRouter.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { UnifiedGraphRAG, unifiedGraphRAG } from './UnifiedGraphRAG.js';
|
| 3 |
+
import { UnifiedMemorySystem, unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 4 |
+
|
| 5 |
+
export type AgentNodeType = 'router' | 'planner' | 'researcher' | 'coder' | 'reviewer' | 'end';
|
| 6 |
+
|
| 7 |
+
export interface AgentState {
|
| 8 |
+
id: string;
|
| 9 |
+
messages: { role: string; content: string }[];
|
| 10 |
+
context: any;
|
| 11 |
+
currentNode: AgentNodeType;
|
| 12 |
+
history: AgentNodeType[];
|
| 13 |
+
scratchpad: any; // Shared working memory for agents
|
| 14 |
+
status: 'active' | 'completed' | 'failed' | 'waiting';
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface Checkpoint {
|
| 18 |
+
id: string;
|
| 19 |
+
state: AgentState;
|
| 20 |
+
timestamp: Date;
|
| 21 |
+
metadata?: any;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export class StateGraphRouter {
|
| 25 |
+
private graphRag: UnifiedGraphRAG;
|
| 26 |
+
private memory: UnifiedMemorySystem;
|
| 27 |
+
private checkpoints: Map<string, Checkpoint> = new Map();
|
| 28 |
+
|
| 29 |
+
constructor() {
|
| 30 |
+
this.graphRag = unifiedGraphRAG;
|
| 31 |
+
this.memory = unifiedMemorySystem;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Initialize a new state for a task
|
| 36 |
+
*/
|
| 37 |
+
public initState(taskId: string, initialInput: string): AgentState {
|
| 38 |
+
return {
|
| 39 |
+
id: taskId,
|
| 40 |
+
messages: [{ role: 'user', content: initialInput }],
|
| 41 |
+
context: {},
|
| 42 |
+
currentNode: 'router',
|
| 43 |
+
history: [],
|
| 44 |
+
scratchpad: {},
|
| 45 |
+
status: 'active'
|
| 46 |
+
};
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Save checkpoint for time-travel debugging
|
| 51 |
+
*/
|
| 52 |
+
private saveCheckpoint(state: AgentState, metadata?: any): string {
|
| 53 |
+
const checkpointId = `${state.id}-${Date.now()}`;
|
| 54 |
+
this.checkpoints.set(checkpointId, {
|
| 55 |
+
id: checkpointId,
|
| 56 |
+
state: JSON.parse(JSON.stringify(state)), // Deep clone
|
| 57 |
+
timestamp: new Date(),
|
| 58 |
+
metadata
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// Keep only last 50 checkpoints per task
|
| 62 |
+
const taskCheckpoints = Array.from(this.checkpoints.entries())
|
| 63 |
+
.filter(([_, cp]) => cp.state.id === state.id)
|
| 64 |
+
.sort((a, b) => b[1].timestamp.getTime() - a[1].timestamp.getTime())
|
| 65 |
+
.slice(50);
|
| 66 |
+
|
| 67 |
+
this.checkpoints.clear();
|
| 68 |
+
taskCheckpoints.forEach(([id, cp]) => this.checkpoints.set(id, cp));
|
| 69 |
+
|
| 70 |
+
return checkpointId;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Time-travel: restore to previous checkpoint
|
| 75 |
+
*/
|
| 76 |
+
public async timeTravel(checkpointId: string): Promise<AgentState | null> {
|
| 77 |
+
const checkpoint = this.checkpoints.get(checkpointId);
|
| 78 |
+
if (!checkpoint) {
|
| 79 |
+
return null;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
console.log(`⏪ [StateGraph] Time-traveling to checkpoint: ${checkpointId}`);
|
| 83 |
+
return JSON.parse(JSON.stringify(checkpoint.state)); // Deep clone
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Get all checkpoints for a task
|
| 88 |
+
*/
|
| 89 |
+
public getCheckpoints(taskId: string): Checkpoint[] {
|
| 90 |
+
return Array.from(this.checkpoints.values())
|
| 91 |
+
.filter(cp => cp.state.id === taskId)
|
| 92 |
+
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Route the state to the next node
|
| 97 |
+
*/
|
| 98 |
+
public async route(state: AgentState): Promise<AgentState> {
|
| 99 |
+
console.log(`🔄 [StateGraph] Routing from ${state.currentNode}...`);
|
| 100 |
+
|
| 101 |
+
// Save checkpoint before routing
|
| 102 |
+
this.saveCheckpoint(state, { action: 'before_routing', node: state.currentNode });
|
| 103 |
+
|
| 104 |
+
// Update history
|
| 105 |
+
if (state.history[state.history.length - 1] !== state.currentNode) {
|
| 106 |
+
state.history.push(state.currentNode);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
let newState: AgentState;
|
| 110 |
+
|
| 111 |
+
switch (state.currentNode) {
|
| 112 |
+
case 'router':
|
| 113 |
+
newState = await this.handleRouterNode(state);
|
| 114 |
+
break;
|
| 115 |
+
|
| 116 |
+
case 'planner':
|
| 117 |
+
newState = await this.handlePlannerNode(state);
|
| 118 |
+
break;
|
| 119 |
+
|
| 120 |
+
case 'researcher':
|
| 121 |
+
newState = await this.handleResearcherNode(state);
|
| 122 |
+
break;
|
| 123 |
+
|
| 124 |
+
case 'reviewer':
|
| 125 |
+
newState = await this.handleReviewerNode(state);
|
| 126 |
+
break;
|
| 127 |
+
|
| 128 |
+
case 'end':
|
| 129 |
+
newState = state;
|
| 130 |
+
break;
|
| 131 |
+
|
| 132 |
+
default:
|
| 133 |
+
console.error(`Unknown node: ${state.currentNode}`);
|
| 134 |
+
state.status = 'failed';
|
| 135 |
+
newState = state;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Save checkpoint after routing
|
| 139 |
+
this.saveCheckpoint(newState, { action: 'after_routing', node: newState.currentNode });
|
| 140 |
+
|
| 141 |
+
return newState;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/**
|
| 145 |
+
* Router Logic: Decides the initial path based on query complexity
|
| 146 |
+
*/
|
| 147 |
+
private async handleRouterNode(state: AgentState): Promise<AgentState> {
|
| 148 |
+
const lastMessage = state.messages[state.messages.length - 1].content;
|
| 149 |
+
|
| 150 |
+
// 1. Use GraphRAG to understand context
|
| 151 |
+
const ragResult = await this.graphRag.query(lastMessage, {
|
| 152 |
+
userId: 'system',
|
| 153 |
+
orgId: 'default'
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
state.context.ragReasoning = ragResult;
|
| 157 |
+
|
| 158 |
+
// 2. Heuristic routing (Will be replaced by LLM classifier later)
|
| 159 |
+
if (ragResult.confidence < 0.3 || lastMessage.length > 100) {
|
| 160 |
+
// Low confidence or complex query -> Needs Planning
|
| 161 |
+
console.log(' -> Routing to Planner (Complex/Unknown)');
|
| 162 |
+
state.currentNode = 'planner';
|
| 163 |
+
} else {
|
| 164 |
+
// High confidence -> Direct Research or Execution (Simplified)
|
| 165 |
+
console.log(' -> Routing to Researcher (Simple)');
|
| 166 |
+
state.currentNode = 'researcher';
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
return state;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* Planner Node: Break down complex tasks
|
| 174 |
+
*/
|
| 175 |
+
private async handlePlannerNode(state: AgentState): Promise<AgentState> {
|
| 176 |
+
console.log(' -> Planner: Analyzing task...');
|
| 177 |
+
|
| 178 |
+
const lastMessage = state.messages[state.messages.length - 1].content;
|
| 179 |
+
|
| 180 |
+
// Use GraphRAG to understand context and dependencies
|
| 181 |
+
const ragResult = await this.graphRag.query(`Plan: ${lastMessage}`, {
|
| 182 |
+
userId: 'system',
|
| 183 |
+
orgId: 'default'
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
// Simple planning logic (can be enhanced with LLM)
|
| 187 |
+
const plan = [
|
| 188 |
+
`Step 1: Analyze requirements (confidence: ${ragResult.confidence.toFixed(2)})`,
|
| 189 |
+
`Step 2: Identify dependencies (${ragResult.nodes.length} related concepts found)`,
|
| 190 |
+
'Step 3: Execute plan',
|
| 191 |
+
'Step 4: Review results'
|
| 192 |
+
];
|
| 193 |
+
|
| 194 |
+
state.scratchpad.plan = plan;
|
| 195 |
+
state.scratchpad.ragContext = ragResult;
|
| 196 |
+
state.currentNode = 'researcher';
|
| 197 |
+
|
| 198 |
+
return state;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/**
|
| 202 |
+
* Researcher Node: Gather information
|
| 203 |
+
*/
|
| 204 |
+
private async handleResearcherNode(state: AgentState): Promise<AgentState> {
|
| 205 |
+
console.log(' -> Researcher: Gathering information...');
|
| 206 |
+
|
| 207 |
+
const lastMessage = state.messages[state.messages.length - 1].content;
|
| 208 |
+
|
| 209 |
+
// Use UnifiedMemorySystem to search for relevant information
|
| 210 |
+
const searchResults = await this.memory.getWorkingMemory({
|
| 211 |
+
userId: 'system',
|
| 212 |
+
orgId: 'default'
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
state.scratchpad.research = {
|
| 216 |
+
query: lastMessage,
|
| 217 |
+
foundContext: searchResults.recentEvents?.slice(0, 5) || [],
|
| 218 |
+
foundFeatures: searchResults.recentFeatures?.slice(0, 3) || []
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
state.currentNode = 'reviewer';
|
| 222 |
+
return state;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
/**
|
| 226 |
+
* Reviewer Node: Validate and finalize
|
| 227 |
+
*/
|
| 228 |
+
private async handleReviewerNode(state: AgentState): Promise<AgentState> {
|
| 229 |
+
console.log(' -> Reviewer: Validating results...');
|
| 230 |
+
|
| 231 |
+
// Simple validation (can be enhanced)
|
| 232 |
+
const hasPlan = state.scratchpad.plan && state.scratchpad.plan.length > 0;
|
| 233 |
+
const hasResearch = state.scratchpad.research;
|
| 234 |
+
|
| 235 |
+
if (hasPlan && hasResearch) {
|
| 236 |
+
state.status = 'completed';
|
| 237 |
+
state.currentNode = 'end';
|
| 238 |
+
} else {
|
| 239 |
+
// If missing info, go back to researcher
|
| 240 |
+
state.currentNode = 'researcher';
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return state;
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
export const stateGraphRouter = new StateGraphRouter();
|
| 248 |
+
|
apps/backend/src/mcp/cognitive/TaskRecorder.ts
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* TaskRecorder - Observes, learns, and suggests automation
|
| 3 |
+
*
|
| 4 |
+
* CRITICAL RULE: Agents NEVER commit real tasks without user approval
|
| 5 |
+
*
|
| 6 |
+
* Features:
|
| 7 |
+
* - Observes all tasks performed by users/agents
|
| 8 |
+
* - Learns patterns from repeated tasks
|
| 9 |
+
* - Suggests automation after observing N times
|
| 10 |
+
* - Requires explicit user approval before automation
|
| 11 |
+
* - Never auto-executes real tasks
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
import { unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 15 |
+
import { projectMemory } from '../../services/project/ProjectMemory.js';
|
| 16 |
+
import { eventBus } from '../EventBus.js';
|
| 17 |
+
import { getDatabase } from '../../database/index.js';
|
| 18 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 19 |
+
|
| 20 |
+
export interface TaskObservation {
|
| 21 |
+
id: string;
|
| 22 |
+
taskType: string;
|
| 23 |
+
taskSignature: string; // Hash of task parameters
|
| 24 |
+
userId: string;
|
| 25 |
+
orgId: string;
|
| 26 |
+
timestamp: Date;
|
| 27 |
+
duration?: number;
|
| 28 |
+
success: boolean;
|
| 29 |
+
result?: any;
|
| 30 |
+
context?: Record<string, any>;
|
| 31 |
+
params?: any;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface TaskPattern {
|
| 35 |
+
taskSignature: string;
|
| 36 |
+
taskType: string;
|
| 37 |
+
frequency: number;
|
| 38 |
+
firstSeen: Date;
|
| 39 |
+
lastSeen: Date;
|
| 40 |
+
successRate: number;
|
| 41 |
+
averageDuration?: number;
|
| 42 |
+
contexts: Record<string, number>; // Context patterns
|
| 43 |
+
suggestedAutomation?: AutomationSuggestion;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export interface AutomationSuggestion {
|
| 47 |
+
id: string;
|
| 48 |
+
taskSignature: string;
|
| 49 |
+
taskType: string;
|
| 50 |
+
confidence: number; // 0-1, based on pattern strength
|
| 51 |
+
observedCount: number; // How many times observed
|
| 52 |
+
suggestedAction: string; // Description of what would be automated
|
| 53 |
+
requiresApproval: boolean; // Always true for real tasks
|
| 54 |
+
estimatedBenefit: string; // Time saved, etc.
|
| 55 |
+
createdAt: Date;
|
| 56 |
+
status: 'pending' | 'approved' | 'rejected' | 'active';
|
| 57 |
+
approvedBy?: string;
|
| 58 |
+
approvedAt?: Date;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export interface TaskExecutionRequest {
|
| 62 |
+
suggestionId: string;
|
| 63 |
+
taskSignature: string;
|
| 64 |
+
taskType: string;
|
| 65 |
+
params: any;
|
| 66 |
+
requestedBy: string;
|
| 67 |
+
requiresApproval: boolean; // Always true for real tasks
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export class TaskRecorder {
|
| 71 |
+
private observations: Map<string, TaskObservation[]> = new Map(); // signature -> observations
|
| 72 |
+
private patterns: Map<string, TaskPattern> = new Map(); // signature -> pattern
|
| 73 |
+
private suggestions: Map<string, AutomationSuggestion> = new Map(); // suggestionId -> suggestion
|
| 74 |
+
private readonly MIN_OBSERVATIONS_FOR_SUGGESTION = 3; // Suggest after 3 observations
|
| 75 |
+
private readonly MIN_CONFIDENCE_FOR_SUGGESTION = 0.7; // 70% success rate minimum
|
| 76 |
+
private db: any;
|
| 77 |
+
|
| 78 |
+
constructor() {
|
| 79 |
+
this.db = getDatabase();
|
| 80 |
+
this.initializeDatabase();
|
| 81 |
+
this.setupEventListeners();
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/**
|
| 85 |
+
* Initialize database tables for task recording
|
| 86 |
+
*/
|
| 87 |
+
private initializeDatabase(): void {
|
| 88 |
+
try {
|
| 89 |
+
// Create task_observations table
|
| 90 |
+
this.db.run(`
|
| 91 |
+
CREATE TABLE IF NOT EXISTS task_observations (
|
| 92 |
+
id TEXT PRIMARY KEY,
|
| 93 |
+
task_type TEXT NOT NULL,
|
| 94 |
+
task_signature TEXT NOT NULL,
|
| 95 |
+
user_id TEXT NOT NULL,
|
| 96 |
+
org_id TEXT NOT NULL,
|
| 97 |
+
timestamp TEXT NOT NULL,
|
| 98 |
+
duration INTEGER,
|
| 99 |
+
success INTEGER NOT NULL,
|
| 100 |
+
result TEXT,
|
| 101 |
+
context TEXT,
|
| 102 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 103 |
+
)
|
| 104 |
+
`);
|
| 105 |
+
|
| 106 |
+
// Create task_patterns table
|
| 107 |
+
this.db.run(`
|
| 108 |
+
CREATE TABLE IF NOT EXISTS task_patterns (
|
| 109 |
+
task_signature TEXT PRIMARY KEY,
|
| 110 |
+
task_type TEXT NOT NULL,
|
| 111 |
+
frequency INTEGER NOT NULL DEFAULT 1,
|
| 112 |
+
first_seen TEXT NOT NULL,
|
| 113 |
+
last_seen TEXT NOT NULL,
|
| 114 |
+
success_rate REAL NOT NULL,
|
| 115 |
+
average_duration REAL,
|
| 116 |
+
contexts TEXT,
|
| 117 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
| 118 |
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 119 |
+
)
|
| 120 |
+
`);
|
| 121 |
+
|
| 122 |
+
// Create automation_suggestions table
|
| 123 |
+
this.db.run(`
|
| 124 |
+
CREATE TABLE IF NOT EXISTS automation_suggestions (
|
| 125 |
+
id TEXT PRIMARY KEY,
|
| 126 |
+
task_signature TEXT NOT NULL,
|
| 127 |
+
task_type TEXT NOT NULL,
|
| 128 |
+
confidence REAL NOT NULL,
|
| 129 |
+
observed_count INTEGER NOT NULL,
|
| 130 |
+
suggested_action TEXT NOT NULL,
|
| 131 |
+
requires_approval INTEGER NOT NULL DEFAULT 1,
|
| 132 |
+
estimated_benefit TEXT,
|
| 133 |
+
status TEXT NOT NULL DEFAULT 'pending',
|
| 134 |
+
approved_by TEXT,
|
| 135 |
+
approved_at TEXT,
|
| 136 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
| 137 |
+
FOREIGN KEY (task_signature) REFERENCES task_patterns(task_signature)
|
| 138 |
+
)
|
| 139 |
+
`);
|
| 140 |
+
|
| 141 |
+
// Create task_executions table (for approved automations)
|
| 142 |
+
this.db.run(`
|
| 143 |
+
CREATE TABLE IF NOT EXISTS task_executions (
|
| 144 |
+
id TEXT PRIMARY KEY,
|
| 145 |
+
suggestion_id TEXT,
|
| 146 |
+
task_signature TEXT NOT NULL,
|
| 147 |
+
task_type TEXT NOT NULL,
|
| 148 |
+
params TEXT NOT NULL,
|
| 149 |
+
requested_by TEXT NOT NULL,
|
| 150 |
+
approved_by TEXT NOT NULL,
|
| 151 |
+
executed_at TEXT NOT NULL,
|
| 152 |
+
success INTEGER,
|
| 153 |
+
result TEXT,
|
| 154 |
+
FOREIGN KEY (suggestion_id) REFERENCES automation_suggestions(id)
|
| 155 |
+
)
|
| 156 |
+
`);
|
| 157 |
+
|
| 158 |
+
console.log('✅ TaskRecorder database initialized');
|
| 159 |
+
} catch (error) {
|
| 160 |
+
console.error('❌ Failed to initialize TaskRecorder database:', error);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/**
|
| 165 |
+
* Setup event listeners for task observation
|
| 166 |
+
*/
|
| 167 |
+
private setupEventListeners(): void {
|
| 168 |
+
// Listen for MCP tool executions
|
| 169 |
+
eventBus.onEvent('mcp.tool.executed', async (event: any) => {
|
| 170 |
+
await this.observeTask({
|
| 171 |
+
taskType: event.tool || 'unknown',
|
| 172 |
+
taskSignature: this.generateSignature(event.tool, event.payload),
|
| 173 |
+
userId: event.userId || 'system',
|
| 174 |
+
orgId: event.orgId || 'default',
|
| 175 |
+
timestamp: new Date(),
|
| 176 |
+
success: event.success !== false,
|
| 177 |
+
result: event.result,
|
| 178 |
+
context: {
|
| 179 |
+
source: 'mcp_tool',
|
| 180 |
+
tool: event.tool,
|
| 181 |
+
payload: event.payload
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
// Listen for autonomous agent tasks
|
| 187 |
+
eventBus.onEvent('autonomous.task.executed', async (event: any) => {
|
| 188 |
+
await this.observeTask({
|
| 189 |
+
taskType: event.taskType || 'autonomous_task',
|
| 190 |
+
taskSignature: this.generateSignature(event.taskType, event.payload),
|
| 191 |
+
userId: event.userId || 'system',
|
| 192 |
+
orgId: event.orgId || 'default',
|
| 193 |
+
timestamp: new Date(),
|
| 194 |
+
success: event.success !== false,
|
| 195 |
+
result: event.result,
|
| 196 |
+
context: {
|
| 197 |
+
source: 'autonomous_agent',
|
| 198 |
+
taskType: event.taskType
|
| 199 |
+
}
|
| 200 |
+
});
|
| 201 |
+
});
|
| 202 |
+
|
| 203 |
+
console.log('👁️ TaskRecorder event listeners setup complete');
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/**
|
| 207 |
+
* Observe a task execution
|
| 208 |
+
*/
|
| 209 |
+
async observeTask(observation: Omit<TaskObservation, 'id'>): Promise<void> {
|
| 210 |
+
const id = `obs-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 211 |
+
const fullObservation: TaskObservation = { ...observation, id };
|
| 212 |
+
|
| 213 |
+
// Store in memory
|
| 214 |
+
const signature = observation.taskSignature;
|
| 215 |
+
if (!this.observations.has(signature)) {
|
| 216 |
+
this.observations.set(signature, []);
|
| 217 |
+
}
|
| 218 |
+
this.observations.get(signature)!.push(fullObservation);
|
| 219 |
+
|
| 220 |
+
// Persist to database
|
| 221 |
+
try {
|
| 222 |
+
this.db.run(`
|
| 223 |
+
INSERT INTO task_observations
|
| 224 |
+
(id, task_type, task_signature, user_id, org_id, timestamp, duration, success, result, context)
|
| 225 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 226 |
+
`, [
|
| 227 |
+
id,
|
| 228 |
+
observation.taskType,
|
| 229 |
+
signature,
|
| 230 |
+
observation.userId,
|
| 231 |
+
observation.orgId,
|
| 232 |
+
observation.timestamp.toISOString(),
|
| 233 |
+
observation.duration || null,
|
| 234 |
+
observation.success ? 1 : 0,
|
| 235 |
+
observation.result ? JSON.stringify(observation.result) : null,
|
| 236 |
+
observation.context ? JSON.stringify(observation.context) : null
|
| 237 |
+
]);
|
| 238 |
+
} catch (error) {
|
| 239 |
+
console.error('Failed to persist task observation:', error);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Update pattern
|
| 243 |
+
await this.updatePattern(fullObservation);
|
| 244 |
+
|
| 245 |
+
// Check if we should suggest automation
|
| 246 |
+
await this.checkAndSuggestAutomation(signature);
|
| 247 |
+
|
| 248 |
+
console.log(`👁️ [TaskRecorder] Observed: ${observation.taskType} (${signature.substring(0, 8)}...)`);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
/**
|
| 252 |
+
* Update pattern from observation
|
| 253 |
+
*/
|
| 254 |
+
private async updatePattern(observation: TaskObservation): Promise<void> {
|
| 255 |
+
const signature = observation.taskSignature;
|
| 256 |
+
const existing = this.patterns.get(signature);
|
| 257 |
+
|
| 258 |
+
if (existing) {
|
| 259 |
+
// Update existing pattern
|
| 260 |
+
existing.frequency += 1;
|
| 261 |
+
existing.lastSeen = observation.timestamp;
|
| 262 |
+
existing.successRate = this.calculateSuccessRate(signature);
|
| 263 |
+
existing.averageDuration = this.calculateAverageDuration(signature);
|
| 264 |
+
|
| 265 |
+
// Update context patterns
|
| 266 |
+
if (observation.context) {
|
| 267 |
+
for (const [key, value] of Object.entries(observation.context)) {
|
| 268 |
+
const contextKey = `${key}:${value}`;
|
| 269 |
+
existing.contexts[contextKey] = (existing.contexts[contextKey] || 0) + 1;
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// Update database
|
| 274 |
+
try {
|
| 275 |
+
this.db.run(`
|
| 276 |
+
UPDATE task_patterns
|
| 277 |
+
SET frequency = ?,
|
| 278 |
+
last_seen = ?,
|
| 279 |
+
success_rate = ?,
|
| 280 |
+
average_duration = ?,
|
| 281 |
+
contexts = ?,
|
| 282 |
+
updated_at = CURRENT_TIMESTAMP
|
| 283 |
+
WHERE task_signature = ?
|
| 284 |
+
`, [
|
| 285 |
+
existing.frequency,
|
| 286 |
+
existing.lastSeen.toISOString(),
|
| 287 |
+
existing.successRate,
|
| 288 |
+
existing.averageDuration || null,
|
| 289 |
+
JSON.stringify(existing.contexts),
|
| 290 |
+
signature
|
| 291 |
+
]);
|
| 292 |
+
} catch (error) {
|
| 293 |
+
console.error('Failed to update pattern:', error);
|
| 294 |
+
}
|
| 295 |
+
} else {
|
| 296 |
+
// Create new pattern
|
| 297 |
+
const pattern: TaskPattern = {
|
| 298 |
+
taskSignature: signature,
|
| 299 |
+
taskType: observation.taskType,
|
| 300 |
+
frequency: 1,
|
| 301 |
+
firstSeen: observation.timestamp,
|
| 302 |
+
lastSeen: observation.timestamp,
|
| 303 |
+
successRate: observation.success ? 1.0 : 0.0,
|
| 304 |
+
averageDuration: observation.duration,
|
| 305 |
+
contexts: observation.context ?
|
| 306 |
+
Object.fromEntries(Object.entries(observation.context).map(([k, v]) => [`${k}:${v}`, 1])) :
|
| 307 |
+
{}
|
| 308 |
+
};
|
| 309 |
+
|
| 310 |
+
this.patterns.set(signature, pattern);
|
| 311 |
+
|
| 312 |
+
// Insert into database
|
| 313 |
+
try {
|
| 314 |
+
this.db.run(`
|
| 315 |
+
INSERT INTO task_patterns
|
| 316 |
+
(task_signature, task_type, frequency, first_seen, last_seen, success_rate, average_duration, contexts)
|
| 317 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 318 |
+
`, [
|
| 319 |
+
signature,
|
| 320 |
+
pattern.taskType,
|
| 321 |
+
pattern.frequency,
|
| 322 |
+
pattern.firstSeen.toISOString(),
|
| 323 |
+
pattern.lastSeen.toISOString(),
|
| 324 |
+
pattern.successRate,
|
| 325 |
+
pattern.averageDuration || null,
|
| 326 |
+
JSON.stringify(pattern.contexts)
|
| 327 |
+
]);
|
| 328 |
+
} catch (error) {
|
| 329 |
+
console.error('Failed to insert pattern:', error);
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/**
|
| 335 |
+
* Check if automation should be suggested
|
| 336 |
+
*/
|
| 337 |
+
private async checkAndSuggestAutomation(signature: string): Promise<void> {
|
| 338 |
+
const pattern = this.patterns.get(signature);
|
| 339 |
+
if (!pattern) return;
|
| 340 |
+
|
| 341 |
+
// Check if we've observed enough times
|
| 342 |
+
if (pattern.frequency < this.MIN_OBSERVATIONS_FOR_SUGGESTION) {
|
| 343 |
+
return;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Check if success rate is high enough
|
| 347 |
+
if (pattern.successRate < this.MIN_CONFIDENCE_FOR_SUGGESTION) {
|
| 348 |
+
return;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
// Check if suggestion already exists
|
| 352 |
+
const existingSuggestion = Array.from(this.suggestions.values())
|
| 353 |
+
.find(s => s.taskSignature === signature && s.status === 'pending');
|
| 354 |
+
if (existingSuggestion) {
|
| 355 |
+
return; // Already suggested
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Create automation suggestion
|
| 359 |
+
const suggestion: AutomationSuggestion = {
|
| 360 |
+
id: `sug-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
| 361 |
+
taskSignature: signature,
|
| 362 |
+
taskType: pattern.taskType,
|
| 363 |
+
confidence: pattern.successRate,
|
| 364 |
+
observedCount: pattern.frequency,
|
| 365 |
+
suggestedAction: `Automate "${pattern.taskType}" task (observed ${pattern.frequency} times with ${(pattern.successRate * 100).toFixed(0)}% success rate)`,
|
| 366 |
+
requiresApproval: true, // ALWAYS require approval for real tasks
|
| 367 |
+
estimatedBenefit: pattern.averageDuration
|
| 368 |
+
? `Saves ~${pattern.averageDuration}ms per execution`
|
| 369 |
+
: 'Reduces manual repetition',
|
| 370 |
+
createdAt: new Date(),
|
| 371 |
+
status: 'pending'
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
this.suggestions.set(suggestion.id, suggestion);
|
| 375 |
+
pattern.suggestedAutomation = suggestion;
|
| 376 |
+
|
| 377 |
+
// Persist suggestion
|
| 378 |
+
try {
|
| 379 |
+
this.db.run(`
|
| 380 |
+
INSERT INTO automation_suggestions
|
| 381 |
+
(id, task_signature, task_type, confidence, observed_count, suggested_action, requires_approval, estimated_benefit, status)
|
| 382 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 383 |
+
`, [
|
| 384 |
+
suggestion.id,
|
| 385 |
+
signature,
|
| 386 |
+
suggestion.taskType,
|
| 387 |
+
suggestion.confidence,
|
| 388 |
+
suggestion.observedCount,
|
| 389 |
+
suggestion.suggestedAction,
|
| 390 |
+
1, // requires_approval = true
|
| 391 |
+
suggestion.estimatedBenefit,
|
| 392 |
+
'pending'
|
| 393 |
+
]);
|
| 394 |
+
} catch (error) {
|
| 395 |
+
console.error('Failed to persist suggestion:', error);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
// Emit event for UI notification
|
| 399 |
+
eventBus.emit('taskrecorder.suggestion.created', {
|
| 400 |
+
suggestion,
|
| 401 |
+
pattern
|
| 402 |
+
});
|
| 403 |
+
|
| 404 |
+
// Log to ProjectMemory
|
| 405 |
+
try {
|
| 406 |
+
projectMemory.logLifecycleEvent({
|
| 407 |
+
eventType: 'other',
|
| 408 |
+
status: 'success',
|
| 409 |
+
details: {
|
| 410 |
+
component: 'TaskRecorder',
|
| 411 |
+
action: 'automation_suggested',
|
| 412 |
+
suggestionId: suggestion.id,
|
| 413 |
+
taskType: pattern.taskType,
|
| 414 |
+
observedCount: pattern.frequency,
|
| 415 |
+
confidence: pattern.successRate
|
| 416 |
+
}
|
| 417 |
+
});
|
| 418 |
+
} catch (err) {
|
| 419 |
+
console.warn('Could not log suggestion to ProjectMemory:', err);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
console.log(`💡 [TaskRecorder] Automation suggested: ${suggestion.suggestedAction}`);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/**
|
| 426 |
+
* Request task execution (requires approval)
|
| 427 |
+
*/
|
| 428 |
+
async requestTaskExecution(request: TaskExecutionRequest): Promise<{ approved: boolean; executionId?: string }> {
|
| 429 |
+
// CRITICAL: Always require approval for real tasks
|
| 430 |
+
if (!request.requiresApproval) {
|
| 431 |
+
throw new Error('All real tasks require approval - cannot execute without approval');
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
const suggestion = this.suggestions.get(request.suggestionId);
|
| 435 |
+
if (!suggestion) {
|
| 436 |
+
throw new Error(`Suggestion ${request.suggestionId} not found`);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// Check if suggestion is approved
|
| 440 |
+
if (suggestion.status !== 'approved') {
|
| 441 |
+
return {
|
| 442 |
+
approved: false,
|
| 443 |
+
executionId: undefined
|
| 444 |
+
};
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Execute task (with approval)
|
| 448 |
+
const executionId = `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 449 |
+
|
| 450 |
+
try {
|
| 451 |
+
this.db.run(`
|
| 452 |
+
INSERT INTO task_executions
|
| 453 |
+
(id, suggestion_id, task_signature, task_type, params, requested_by, approved_by, executed_at)
|
| 454 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 455 |
+
`, [
|
| 456 |
+
executionId,
|
| 457 |
+
request.suggestionId,
|
| 458 |
+
request.taskSignature,
|
| 459 |
+
request.taskType,
|
| 460 |
+
JSON.stringify(request.params),
|
| 461 |
+
request.requestedBy,
|
| 462 |
+
suggestion.approvedBy || 'system',
|
| 463 |
+
new Date().toISOString()
|
| 464 |
+
]);
|
| 465 |
+
|
| 466 |
+
// Emit execution event
|
| 467 |
+
eventBus.emit('taskrecorder.execution.started', {
|
| 468 |
+
executionId,
|
| 469 |
+
request,
|
| 470 |
+
suggestion
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
return {
|
| 474 |
+
approved: true,
|
| 475 |
+
executionId
|
| 476 |
+
};
|
| 477 |
+
} catch (error) {
|
| 478 |
+
console.error('Failed to record execution:', error);
|
| 479 |
+
throw error;
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
/**
|
| 484 |
+
* Approve automation suggestion
|
| 485 |
+
*/
|
| 486 |
+
async approveSuggestion(suggestionId: string, approvedBy: string): Promise<void> {
|
| 487 |
+
const suggestion = this.suggestions.get(suggestionId);
|
| 488 |
+
if (!suggestion) {
|
| 489 |
+
throw new Error(`Suggestion ${suggestionId} not found`);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
suggestion.status = 'approved';
|
| 493 |
+
suggestion.approvedBy = approvedBy;
|
| 494 |
+
suggestion.approvedAt = new Date();
|
| 495 |
+
|
| 496 |
+
// Update database
|
| 497 |
+
try {
|
| 498 |
+
this.db.run(`
|
| 499 |
+
UPDATE automation_suggestions
|
| 500 |
+
SET status = 'approved',
|
| 501 |
+
approved_by = ?,
|
| 502 |
+
approved_at = ?
|
| 503 |
+
WHERE id = ?
|
| 504 |
+
`, [
|
| 505 |
+
approvedBy,
|
| 506 |
+
suggestion.approvedAt.toISOString(),
|
| 507 |
+
suggestionId
|
| 508 |
+
]);
|
| 509 |
+
} catch (error) {
|
| 510 |
+
console.error('Failed to update suggestion:', error);
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// Emit event
|
| 514 |
+
eventBus.emit('taskrecorder.suggestion.approved', {
|
| 515 |
+
suggestion
|
| 516 |
+
});
|
| 517 |
+
|
| 518 |
+
console.log(`✅ [TaskRecorder] Suggestion approved: ${suggestion.suggestedAction}`);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/**
|
| 522 |
+
* Reject automation suggestion
|
| 523 |
+
*/
|
| 524 |
+
async rejectSuggestion(suggestionId: string, rejectedBy: string): Promise<void> {
|
| 525 |
+
const suggestion = this.suggestions.get(suggestionId);
|
| 526 |
+
if (!suggestion) {
|
| 527 |
+
throw new Error(`Suggestion ${suggestionId} not found`);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
suggestion.status = 'rejected';
|
| 531 |
+
|
| 532 |
+
// Update database
|
| 533 |
+
try {
|
| 534 |
+
this.db.run(`
|
| 535 |
+
UPDATE automation_suggestions
|
| 536 |
+
SET status = 'rejected'
|
| 537 |
+
WHERE id = ?
|
| 538 |
+
`, [suggestionId]);
|
| 539 |
+
} catch (error) {
|
| 540 |
+
console.error('Failed to update suggestion:', error);
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
console.log(`❌ [TaskRecorder] Suggestion rejected: ${suggestion.suggestedAction}`);
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
/**
|
| 547 |
+
* Get all pending suggestions
|
| 548 |
+
*/
|
| 549 |
+
getPendingSuggestions(): AutomationSuggestion[] {
|
| 550 |
+
return Array.from(this.suggestions.values())
|
| 551 |
+
.filter(s => s.status === 'pending')
|
| 552 |
+
.sort((a, b) => b.confidence - a.confidence);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
/**
|
| 556 |
+
* Get pattern by signature
|
| 557 |
+
*/
|
| 558 |
+
getPattern(signature: string): TaskPattern | undefined {
|
| 559 |
+
return this.patterns.get(signature);
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
/**
|
| 563 |
+
* Get all patterns
|
| 564 |
+
*/
|
| 565 |
+
getAllPatterns(): TaskPattern[] {
|
| 566 |
+
return Array.from(this.patterns.values())
|
| 567 |
+
.sort((a, b) => b.frequency - a.frequency);
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
/**
|
| 571 |
+
* Generate signature from task type and params
|
| 572 |
+
*/
|
| 573 |
+
private generateSignature(taskType: string, params: any): string {
|
| 574 |
+
const normalized = {
|
| 575 |
+
type: taskType,
|
| 576 |
+
params: this.normalizeParams(params)
|
| 577 |
+
};
|
| 578 |
+
const str = JSON.stringify(normalized);
|
| 579 |
+
// Use simple hash for signature
|
| 580 |
+
return `sig-${this.simpleHash(str)}`;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/**
|
| 584 |
+
* Normalize params for consistent signatures
|
| 585 |
+
*/
|
| 586 |
+
private normalizeParams(params: any): any {
|
| 587 |
+
if (!params || typeof params !== 'object') {
|
| 588 |
+
return params;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
const normalized: any = {};
|
| 592 |
+
for (const [key, value] of Object.entries(params)) {
|
| 593 |
+
// Ignore timestamps, IDs, and other variable fields
|
| 594 |
+
if (['timestamp', 'id', 'createdAt', 'updatedAt', 'userId', 'orgId'].includes(key)) {
|
| 595 |
+
continue;
|
| 596 |
+
}
|
| 597 |
+
normalized[key] = value;
|
| 598 |
+
}
|
| 599 |
+
return normalized;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
/**
|
| 603 |
+
* Simple hash function
|
| 604 |
+
*/
|
| 605 |
+
private simpleHash(str: string): string {
|
| 606 |
+
let hash = 0;
|
| 607 |
+
for (let i = 0; i < str.length; i++) {
|
| 608 |
+
const char = str.charCodeAt(i);
|
| 609 |
+
hash = ((hash << 5) - hash) + char;
|
| 610 |
+
hash = hash & hash; // Convert to 32bit integer
|
| 611 |
+
}
|
| 612 |
+
return Math.abs(hash).toString(36);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
/**
|
| 616 |
+
* Calculate success rate for a pattern
|
| 617 |
+
*/
|
| 618 |
+
private calculateSuccessRate(signature: string): number {
|
| 619 |
+
const observations = this.observations.get(signature) || [];
|
| 620 |
+
if (observations.length === 0) return 0;
|
| 621 |
+
|
| 622 |
+
const successful = observations.filter(o => o.success).length;
|
| 623 |
+
return successful / observations.length;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
/**
|
| 627 |
+
* Calculate average duration for a pattern
|
| 628 |
+
*/
|
| 629 |
+
private calculateAverageDuration(signature: string): number | undefined {
|
| 630 |
+
const observations = this.observations.get(signature) || [];
|
| 631 |
+
const withDuration = observations.filter(o => o.duration !== undefined);
|
| 632 |
+
if (withDuration.length === 0) return undefined;
|
| 633 |
+
|
| 634 |
+
const sum = withDuration.reduce((acc, o) => acc + (o.duration || 0), 0);
|
| 635 |
+
return sum / withDuration.length;
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
// Singleton instance
|
| 640 |
+
let taskRecorderInstance: TaskRecorder | null = null;
|
| 641 |
+
|
| 642 |
+
export function getTaskRecorder(): TaskRecorder {
|
| 643 |
+
if (!taskRecorderInstance) {
|
| 644 |
+
taskRecorderInstance = new TaskRecorder();
|
| 645 |
+
}
|
| 646 |
+
return taskRecorderInstance;
|
| 647 |
+
}
|
| 648 |
+
|
apps/backend/src/mcp/cognitive/UnifiedGraphRAG.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { hybridSearchEngine } from './HybridSearchEngine.js';
|
| 2 |
+
import { getCognitiveMemory } from '../memory/CognitiveMemory.js';
|
| 3 |
+
import { unifiedMemorySystem } from './UnifiedMemorySystem.js';
|
| 4 |
+
import { getLlmService } from '../../services/llm/llmService.js';
|
| 5 |
+
import { MemoryRepository } from '../../services/memory/memoryRepository.js';
|
| 6 |
+
import { getVectorStore } from '../../platform/vector/index.js';
|
| 7 |
+
|
| 8 |
+
interface GraphNode {
|
| 9 |
+
id: string;
|
| 10 |
+
type: string;
|
| 11 |
+
content: string;
|
| 12 |
+
score: number;
|
| 13 |
+
depth: number;
|
| 14 |
+
metadata: any;
|
| 15 |
+
connections: GraphEdge[];
|
| 16 |
+
embedding?: number[]; // For semantic similarity
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
interface GraphEdge {
|
| 20 |
+
targetId: string;
|
| 21 |
+
relation: string;
|
| 22 |
+
weight: number;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
interface GraphRAGResult {
|
| 26 |
+
answer: string;
|
| 27 |
+
reasoning_path: string[];
|
| 28 |
+
nodes: GraphNode[];
|
| 29 |
+
confidence: number;
|
| 30 |
+
sources?: Array<{ id: string; content: string; score: number }>;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export class UnifiedGraphRAG {
|
| 34 |
+
private maxHops: number = 2;
|
| 35 |
+
private minScore: number = 0.3;
|
| 36 |
+
private memoryRepo: MemoryRepository;
|
| 37 |
+
|
| 38 |
+
constructor() {
|
| 39 |
+
this.memoryRepo = new MemoryRepository();
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Perform multi-hop reasoning over the knowledge graph
|
| 44 |
+
* Enhanced with LLM synthesis, semantic similarity, and CMA graph integration
|
| 45 |
+
*/
|
| 46 |
+
public async query(query: string, context: { userId: string; orgId: string; maxHops?: number }): Promise<GraphRAGResult> {
|
| 47 |
+
console.log(`🧠 [GraphRAG] Starting reasoning for: "${query}"`);
|
| 48 |
+
|
| 49 |
+
const maxHops = context.maxHops || this.maxHops;
|
| 50 |
+
|
| 51 |
+
// 1. Get seed nodes from Hybrid Search (High precision entry points)
|
| 52 |
+
const seedResults = await hybridSearchEngine.search(query, {
|
| 53 |
+
...context,
|
| 54 |
+
limit: 5
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
if (seedResults.length === 0) {
|
| 58 |
+
return {
|
| 59 |
+
answer: "No sufficient data found to reason about this query.",
|
| 60 |
+
reasoning_path: [],
|
| 61 |
+
nodes: [],
|
| 62 |
+
confidence: 0
|
| 63 |
+
};
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// 2. Convert search results to graph nodes
|
| 67 |
+
let frontier: GraphNode[] = seedResults.map(r => ({
|
| 68 |
+
id: r.id,
|
| 69 |
+
type: r.type,
|
| 70 |
+
content: r.content,
|
| 71 |
+
score: r.score,
|
| 72 |
+
depth: 0,
|
| 73 |
+
metadata: r.metadata,
|
| 74 |
+
connections: []
|
| 75 |
+
}));
|
| 76 |
+
|
| 77 |
+
const visited = new Set<string>(frontier.map(n => n.id));
|
| 78 |
+
const knowledgeGraph: GraphNode[] = [...frontier];
|
| 79 |
+
const reasoningPath: string[] = [`Found ${frontier.length} starting points: ${frontier.map(n => n.id).join(', ')}`];
|
| 80 |
+
|
| 81 |
+
// 3. Expand graph (Multi-hop traversal with semantic similarity)
|
| 82 |
+
for (let hop = 1; hop <= maxHops; hop++) {
|
| 83 |
+
console.log(`🔍 [GraphRAG] Hop ${hop}: Expanding ${frontier.length} nodes`);
|
| 84 |
+
const newFrontier: GraphNode[] = [];
|
| 85 |
+
|
| 86 |
+
for (const node of frontier) {
|
| 87 |
+
// Enhanced expansion: Use CMA graph relations + semantic similarity
|
| 88 |
+
const connections = await this.expandNode(node, query, context);
|
| 89 |
+
|
| 90 |
+
for (const conn of connections) {
|
| 91 |
+
if (!visited.has(conn.id) && conn.score > this.minScore) {
|
| 92 |
+
visited.add(conn.id);
|
| 93 |
+
newFrontier.push(conn);
|
| 94 |
+
knowledgeGraph.push(conn);
|
| 95 |
+
|
| 96 |
+
// Track edge in parent node
|
| 97 |
+
node.connections.push({
|
| 98 |
+
targetId: conn.id,
|
| 99 |
+
relation: conn.metadata.relation || 'related_to',
|
| 100 |
+
weight: conn.score
|
| 101 |
+
});
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
if (newFrontier.length > 0) {
|
| 107 |
+
reasoningPath.push(`Hop ${hop}: Discovered ${newFrontier.length} new related concepts.`);
|
| 108 |
+
frontier = newFrontier;
|
| 109 |
+
} else {
|
| 110 |
+
break; // No more connections found
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// 4. Synthesize Answer using LLM (Inspired by CgentCore's L1 Director Agent)
|
| 115 |
+
const topNodes = knowledgeGraph.sort((a, b) => b.score - a.score).slice(0, 10);
|
| 116 |
+
const answer = await this.synthesizeAnswer(query, topNodes, context);
|
| 117 |
+
|
| 118 |
+
return {
|
| 119 |
+
answer,
|
| 120 |
+
reasoning_path: reasoningPath,
|
| 121 |
+
nodes: topNodes,
|
| 122 |
+
confidence: topNodes.length > 0 ? topNodes[0].score : 0,
|
| 123 |
+
sources: topNodes.slice(0, 5).map(n => ({
|
| 124 |
+
id: n.id,
|
| 125 |
+
content: n.content.substring(0, 200),
|
| 126 |
+
score: n.score
|
| 127 |
+
}))
|
| 128 |
+
};
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Enhanced node expansion with CMA graph integration and semantic similarity
|
| 133 |
+
* Inspired by CgentCore's hybrid search approach
|
| 134 |
+
*/
|
| 135 |
+
private async expandNode(node: GraphNode, query: string, context: { userId: string; orgId: string }): Promise<GraphNode[]> {
|
| 136 |
+
const memory = getCognitiveMemory();
|
| 137 |
+
const expandedNodes: GraphNode[] = [];
|
| 138 |
+
|
| 139 |
+
// Strategy 1: Get patterns involving this widget/source (existing)
|
| 140 |
+
const patterns = await memory.getWidgetPatterns(node.id);
|
| 141 |
+
|
| 142 |
+
// UsagePattern is an object with commonSources and timePatterns
|
| 143 |
+
// Use commonSources to expand graph connections
|
| 144 |
+
for (const source of patterns.commonSources || []) {
|
| 145 |
+
expandedNodes.push({
|
| 146 |
+
id: `source-${source}`,
|
| 147 |
+
type: 'source',
|
| 148 |
+
content: `Source: ${source}`,
|
| 149 |
+
score: node.score * 0.7, // Decay score over hops
|
| 150 |
+
depth: node.depth + 1,
|
| 151 |
+
metadata: { relation: 'uses_source', averageLatency: patterns.averageLatency },
|
| 152 |
+
connections: []
|
| 153 |
+
});
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Strategy 2: Use CMA memory relations (Direct graph edges)
|
| 157 |
+
// Inspired by CgentCore's memory_relations table
|
| 158 |
+
const relatedMemories = await this.memoryRepo.searchEntities({
|
| 159 |
+
orgId: context.orgId,
|
| 160 |
+
userId: context.userId,
|
| 161 |
+
keywords: this.extractKeywords(node.content),
|
| 162 |
+
limit: 5
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
for (const mem of relatedMemories) {
|
| 166 |
+
// Check if memory is semantically related to query
|
| 167 |
+
const semanticScore = await this.computeSemanticSimilarity(query, mem.content);
|
| 168 |
+
|
| 169 |
+
if (semanticScore > this.minScore) {
|
| 170 |
+
expandedNodes.push({
|
| 171 |
+
id: `memory-${mem.id}`,
|
| 172 |
+
type: mem.entity_type || 'memory',
|
| 173 |
+
content: mem.content,
|
| 174 |
+
score: (mem.importance || 0.5) * semanticScore * node.score * 0.7,
|
| 175 |
+
depth: node.depth + 1,
|
| 176 |
+
metadata: {
|
| 177 |
+
relation: 'memory_relation',
|
| 178 |
+
importance: mem.importance,
|
| 179 |
+
semanticScore
|
| 180 |
+
},
|
| 181 |
+
connections: []
|
| 182 |
+
});
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// Strategy 3: Use UnifiedMemorySystem for episodic memory connections
|
| 187 |
+
const workingMemory = await unifiedMemorySystem.getWorkingMemory({
|
| 188 |
+
userId: context.userId,
|
| 189 |
+
orgId: context.orgId
|
| 190 |
+
});
|
| 191 |
+
|
| 192 |
+
// Find related events/features based on semantic similarity
|
| 193 |
+
const relatedEvents = (workingMemory.recentEvents || []).slice(0, 3);
|
| 194 |
+
for (const event of relatedEvents) {
|
| 195 |
+
const eventContent = JSON.stringify(event);
|
| 196 |
+
const semanticScore = await this.computeSemanticSimilarity(query, eventContent);
|
| 197 |
+
|
| 198 |
+
if (semanticScore > this.minScore) {
|
| 199 |
+
expandedNodes.push({
|
| 200 |
+
id: `event-${event.id || Date.now()}`,
|
| 201 |
+
type: 'episodic',
|
| 202 |
+
content: eventContent.substring(0, 200),
|
| 203 |
+
score: semanticScore * node.score * 0.6,
|
| 204 |
+
depth: node.depth + 1,
|
| 205 |
+
metadata: {
|
| 206 |
+
relation: 'episodic_memory',
|
| 207 |
+
semanticScore
|
| 208 |
+
},
|
| 209 |
+
connections: []
|
| 210 |
+
});
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
return expandedNodes.sort((a, b) => b.score - a.score).slice(0, 5); // Top 5 per node
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/**
|
| 218 |
+
* LLM-based answer synthesis
|
| 219 |
+
* Inspired by CgentCore's L1 Director Agent response generation
|
| 220 |
+
*/
|
| 221 |
+
private async synthesizeAnswer(query: string, nodes: GraphNode[], context: { userId: string; orgId: string }): Promise<string> {
|
| 222 |
+
try {
|
| 223 |
+
const llmService = getLlmService();
|
| 224 |
+
|
| 225 |
+
// Build context from graph nodes
|
| 226 |
+
const graphContext = nodes.map((n, idx) =>
|
| 227 |
+
`[${idx + 1}] ${n.type}: ${n.content.substring(0, 300)} (confidence: ${n.score.toFixed(2)})`
|
| 228 |
+
).join('\n\n');
|
| 229 |
+
|
| 230 |
+
const reasoningPath = nodes.map(n => `${n.id} (depth: ${n.depth})`).join(' -> ');
|
| 231 |
+
|
| 232 |
+
const systemContext = `You are an advanced reasoning assistant. Synthesize a comprehensive answer based on the knowledge graph context provided.
|
| 233 |
+
Use the reasoning path to explain how you arrived at the answer. Be precise, cite sources, and indicate confidence levels.`;
|
| 234 |
+
|
| 235 |
+
const userPrompt = `Query: ${query}
|
| 236 |
+
|
| 237 |
+
Knowledge Graph Context:
|
| 238 |
+
${graphContext}
|
| 239 |
+
|
| 240 |
+
Reasoning Path: ${reasoningPath}
|
| 241 |
+
|
| 242 |
+
Provide a comprehensive answer synthesizing the information from the knowledge graph. Include:
|
| 243 |
+
1. Direct answer to the query
|
| 244 |
+
2. Key insights from the graph
|
| 245 |
+
3. Confidence assessment
|
| 246 |
+
4. Sources referenced`;
|
| 247 |
+
|
| 248 |
+
const answer = await llmService.generateContextualResponse(
|
| 249 |
+
systemContext,
|
| 250 |
+
userPrompt,
|
| 251 |
+
`User: ${context.userId}, Org: ${context.orgId}`
|
| 252 |
+
);
|
| 253 |
+
|
| 254 |
+
return answer || "Reasoning complete. See nodes for details.";
|
| 255 |
+
} catch (error) {
|
| 256 |
+
console.error('[GraphRAG] LLM synthesis error:', error);
|
| 257 |
+
// Fallback to simple synthesis
|
| 258 |
+
return `Based on ${nodes.length} related concepts found: ${nodes.slice(0, 3).map(n => n.content.substring(0, 100)).join('; ')}...`;
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/**
|
| 263 |
+
* Compute semantic similarity using ChromaDB vector search
|
| 264 |
+
* Uses proper embeddings via HuggingFace for true semantic similarity
|
| 265 |
+
*/
|
| 266 |
+
private async computeSemanticSimilarity(query: string, content: string): Promise<number> {
|
| 267 |
+
try {
|
| 268 |
+
// Use pgvector for proper vector similarity
|
| 269 |
+
const vectorStore = await getVectorStore();
|
| 270 |
+
|
| 271 |
+
// For now, use simple text matching as fallback
|
| 272 |
+
// TODO: Generate embeddings for proper vector search
|
| 273 |
+
// const results = await vectorStore.search({
|
| 274 |
+
// vector: [], // Would need actual embeddings here
|
| 275 |
+
// limit: 1
|
| 276 |
+
// });
|
| 277 |
+
|
| 278 |
+
// Simple text similarity fallback
|
| 279 |
+
const queryLower = query.toLowerCase();
|
| 280 |
+
const contentLower = content.toLowerCase();
|
| 281 |
+
|
| 282 |
+
// Use Jaccard similarity
|
| 283 |
+
const queryWords = new Set(queryLower.split(/\s+/).filter(w => w.length > 2));
|
| 284 |
+
const contentWords = new Set(contentLower.split(/\s+/).filter(w => w.length > 2));
|
| 285 |
+
const intersection = new Set([...queryWords].filter(w => contentWords.has(w)));
|
| 286 |
+
const union = new Set([...queryWords, ...contentWords]);
|
| 287 |
+
|
| 288 |
+
const jaccard = union.size > 0 ? intersection.size / union.size : 0;
|
| 289 |
+
const phraseMatch = contentLower.includes(queryLower) ? 0.3 : 0;
|
| 290 |
+
|
| 291 |
+
return Math.min(1.0, jaccard + phraseMatch);
|
| 292 |
+
|
| 293 |
+
} catch (error) {
|
| 294 |
+
console.warn('[GraphRAG] Vector similarity failed, using keyword fallback:', error);
|
| 295 |
+
|
| 296 |
+
// Fallback to keyword similarity
|
| 297 |
+
const queryWords = new Set(query.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
| 298 |
+
const contentWords = new Set(content.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
| 299 |
+
|
| 300 |
+
const intersection = new Set([...queryWords].filter(w => contentWords.has(w)));
|
| 301 |
+
const union = new Set([...queryWords, ...contentWords]);
|
| 302 |
+
|
| 303 |
+
// Fix: Check for division by zero (Bug 2)
|
| 304 |
+
const jaccard = union.size > 0 ? intersection.size / union.size : 0;
|
| 305 |
+
const phraseMatch = content.toLowerCase().includes(query.toLowerCase()) ? 0.3 : 0;
|
| 306 |
+
|
| 307 |
+
return Math.min(1.0, jaccard + phraseMatch);
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/**
|
| 312 |
+
* Extract keywords from content for memory search
|
| 313 |
+
*/
|
| 314 |
+
private extractKeywords(content: string): string[] {
|
| 315 |
+
// Simple keyword extraction (can be enhanced with NLP)
|
| 316 |
+
const words = content.toLowerCase()
|
| 317 |
+
.split(/\s+/)
|
| 318 |
+
.filter(w => w.length > 3)
|
| 319 |
+
.filter(w => !['the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she', 'too', 'use'].includes(w))
|
| 320 |
+
.slice(0, 5);
|
| 321 |
+
|
| 322 |
+
return words;
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
export const unifiedGraphRAG = new UnifiedGraphRAG();
|
apps/backend/src/mcp/cognitive/UnifiedMemorySystem.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// UnifiedMemorySystem – Phase 1 foundation
|
| 2 |
+
// Provides Working, Procedural, Semantic, and Episodic memory layers
|
| 3 |
+
// Integrates existing repositories (CMA, SRAG, PAL, Evolution, ProjectMemory)
|
| 4 |
+
|
| 5 |
+
import { getCognitiveMemory, initCognitiveMemory, CognitiveMemory } from '../memory/CognitiveMemory.js';
|
| 6 |
+
import { getDatabase } from '../../database/index.js';
|
| 7 |
+
import { MemoryRepository } from '../../services/memory/memoryRepository.js';
|
| 8 |
+
import { SragRepository } from '../../services/srag/sragRepository.js';
|
| 9 |
+
import { PalRepository } from '../../services/pal/palRepository.js';
|
| 10 |
+
import { EvolutionRepository } from '../../services/evolution/evolutionRepository.js';
|
| 11 |
+
import { projectMemory } from '../../services/project/ProjectMemory.js';
|
| 12 |
+
import { McpContext } from '@widget-tdc/mcp-types';
|
| 13 |
+
import { QueryIntent } from '../autonomous/DecisionEngine.js';
|
| 14 |
+
import { hybridSearchEngine } from './HybridSearchEngine.js';
|
| 15 |
+
import { emotionAwareDecisionEngine } from './EmotionAwareDecisionEngine.js';
|
| 16 |
+
|
| 17 |
+
/** WorkingMemoryState – transient context for the current request */
|
| 18 |
+
export interface WorkingMemoryState {
|
| 19 |
+
recentEvents: any[];
|
| 20 |
+
recentFeatures: any[];
|
| 21 |
+
recentPatterns?: any[];
|
| 22 |
+
widgetStates: Record<string, any>; // Live data fra widgets
|
| 23 |
+
userMood: {
|
| 24 |
+
sentiment: 'positive' | 'neutral' | 'negative' | 'stressed';
|
| 25 |
+
arousal: number; // 0-1 (Hvor aktiv er brugeren?)
|
| 26 |
+
lastUpdated: number;
|
| 27 |
+
};
|
| 28 |
+
suggestedLayout?: {
|
| 29 |
+
mode: 'focus' | 'discovery' | 'alert';
|
| 30 |
+
activeWidgets: string[]; // ID på widgets der bør være fremme
|
| 31 |
+
theme?: string;
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/** ProductionRuleEngine – simple procedural memory placeholder */
|
| 36 |
+
class ProductionRuleEngine {
|
| 37 |
+
constructor(private cognitive: CognitiveMemory) { }
|
| 38 |
+
// TODO: implement rule extraction from cognitive patterns
|
| 39 |
+
async findRules(_opts: any): Promise<any[]> { return []; }
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export class UnifiedMemorySystem {
|
| 43 |
+
// Existing repositories
|
| 44 |
+
private cognitive: CognitiveMemory;
|
| 45 |
+
private memoryRepo: MemoryRepository;
|
| 46 |
+
private sragRepo: SragRepository;
|
| 47 |
+
private palRepo: PalRepository;
|
| 48 |
+
private evolutionRepo: EvolutionRepository;
|
| 49 |
+
|
| 50 |
+
// New memory layers
|
| 51 |
+
private workingMemory: Map<string, WorkingMemoryState> = new Map();
|
| 52 |
+
private proceduralMemory: ProductionRuleEngine;
|
| 53 |
+
|
| 54 |
+
constructor() {
|
| 55 |
+
// Initialize repositories
|
| 56 |
+
this.memoryRepo = new MemoryRepository();
|
| 57 |
+
this.sragRepo = new SragRepository();
|
| 58 |
+
this.palRepo = new PalRepository();
|
| 59 |
+
this.evolutionRepo = new EvolutionRepository();
|
| 60 |
+
|
| 61 |
+
// Initialize cognitive memory lazily or assume initialized
|
| 62 |
+
// We cannot call getDatabase() here because it might not be ready
|
| 63 |
+
// The cognitive memory should be passed in or retrieved lazily
|
| 64 |
+
this.cognitive = {} as any; // Placeholder, will be set in init() or getter
|
| 65 |
+
this.proceduralMemory = new ProductionRuleEngine(this.cognitive);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// New init method to be called after DB is ready
|
| 69 |
+
public init() {
|
| 70 |
+
const db = getDatabase();
|
| 71 |
+
initCognitiveMemory(db);
|
| 72 |
+
this.cognitive = getCognitiveMemory();
|
| 73 |
+
this.proceduralMemory = new ProductionRuleEngine(this.cognitive);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/** Retrieve or create working memory for a user/org context */
|
| 77 |
+
async getWorkingMemory(ctx: McpContext): Promise<WorkingMemoryState> {
|
| 78 |
+
const key = `${ctx.orgId}:${ctx.userId}`;
|
| 79 |
+
if (!this.workingMemory.has(key)) {
|
| 80 |
+
const events = projectMemory.getLifecycleEvents(20);
|
| 81 |
+
const features = projectMemory.getFeatures();
|
| 82 |
+
this.workingMemory.set(key, {
|
| 83 |
+
recentEvents: events,
|
| 84 |
+
recentFeatures: features,
|
| 85 |
+
widgetStates: {},
|
| 86 |
+
userMood: { sentiment: 'neutral', arousal: 0.5, lastUpdated: Date.now() }
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
return this.workingMemory.get(key)!;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/** Opdater widget state og kør adaptiv analyse */
|
| 93 |
+
async updateWidgetState(ctx: McpContext, widgetId: string, state: any): Promise<void> {
|
| 94 |
+
const wm = await this.getWorkingMemory(ctx);
|
| 95 |
+
wm.widgetStates[widgetId] = { ...state, lastUpdated: Date.now() };
|
| 96 |
+
|
| 97 |
+
// Trigger holographic analysis when state changes
|
| 98 |
+
const patterns = await this.findHolographicPatterns(ctx);
|
| 99 |
+
|
| 100 |
+
// Opdater adaptivt layout baseret på mønstre
|
| 101 |
+
this.updateAdaptiveLayout(wm, patterns);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/** Persist result (e.g., tool output) into working memory for future context */
|
| 105 |
+
async updateWorkingMemory(ctx: McpContext, result: any): Promise<void> {
|
| 106 |
+
const key = `${ctx.orgId}:${ctx.userId}`;
|
| 107 |
+
const state = this.workingMemory.get(key);
|
| 108 |
+
if (state) {
|
| 109 |
+
state.recentEvents = [...(state.recentEvents || []), result];
|
| 110 |
+
|
| 111 |
+
// Simuleret humør-analyse baseret på interaktion
|
| 112 |
+
// Hvis resultatet er en fejl -> stress op
|
| 113 |
+
if (result?.error) {
|
| 114 |
+
state.userMood.sentiment = 'stressed';
|
| 115 |
+
state.userMood.arousal = Math.min(1, state.userMood.arousal + 0.2);
|
| 116 |
+
} else {
|
| 117 |
+
// Reset langsomt mod neutral
|
| 118 |
+
state.userMood.arousal = Math.max(0.2, state.userMood.arousal - 0.05);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
this.workingMemory.set(key, state);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/** Enrich an incoming MCPMessage with memory context */
|
| 126 |
+
async enrichMCPRequest(message: any, ctx: McpContext): Promise<any> {
|
| 127 |
+
const wm = await this.getWorkingMemory(ctx);
|
| 128 |
+
return {
|
| 129 |
+
...message,
|
| 130 |
+
memoryContext: {
|
| 131 |
+
recentEvents: wm.recentEvents,
|
| 132 |
+
recentFeatures: wm.recentFeatures,
|
| 133 |
+
activeWidgets: wm.widgetStates,
|
| 134 |
+
systemSuggestion: wm.suggestedLayout
|
| 135 |
+
}
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/** Example holographic pattern correlation across subsystems */
|
| 140 |
+
async findHolographicPatterns(ctx: McpContext): Promise<any[]> {
|
| 141 |
+
const wm = await this.getWorkingMemory(ctx);
|
| 142 |
+
const widgetData = Object.values(wm.widgetStates);
|
| 143 |
+
|
| 144 |
+
const [pal, cma, srag] = await Promise.all([
|
| 145 |
+
Promise.resolve(this.palRepo.getRecentEvents(ctx.userId, ctx.orgId, 50)).catch(() => []),
|
| 146 |
+
Promise.resolve(this.memoryRepo.searchEntities({ orgId: ctx.orgId, userId: ctx.userId, keywords: [], limit: 50 })).catch(() => []),
|
| 147 |
+
Promise.resolve(this.sragRepo.searchDocuments(ctx.orgId, '')).catch(() => []),
|
| 148 |
+
]);
|
| 149 |
+
|
| 150 |
+
// Inkluder widget data i korrelationen
|
| 151 |
+
return this.correlateAcrossSystems([pal, cma, srag, widgetData]);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/** Opdater layout forslag baseret på mønstre og humør */
|
| 155 |
+
private updateAdaptiveLayout(wm: WorkingMemoryState, patterns: any[]) {
|
| 156 |
+
// 1. Tjek for kritiske mønstre (Sikkerhed)
|
| 157 |
+
const securityPattern = patterns.find(p =>
|
| 158 |
+
['threat', 'attack', 'breach', 'password', 'alert'].includes(p.keyword) && p.frequency > 2
|
| 159 |
+
);
|
| 160 |
+
|
| 161 |
+
if (securityPattern) {
|
| 162 |
+
wm.suggestedLayout = {
|
| 163 |
+
mode: 'alert',
|
| 164 |
+
activeWidgets: ['DarkWebMonitorWidget', 'NetworkSpyWidget', 'CybersecurityOverwatchWidget'],
|
| 165 |
+
theme: 'red-alert'
|
| 166 |
+
};
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// 2. Tjek brugerens humør (Emotion Aware)
|
| 171 |
+
if (wm.userMood.sentiment === 'stressed' || wm.userMood.arousal > 0.8) {
|
| 172 |
+
wm.suggestedLayout = {
|
| 173 |
+
mode: 'focus',
|
| 174 |
+
activeWidgets: ['StatusWidget', 'IntelligentNotesWidget'], // Kun det mest nødvendige
|
| 175 |
+
theme: 'calm-blue'
|
| 176 |
+
};
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// 3. Default: Discovery mode hvis mange data-kilder er aktive
|
| 181 |
+
if (patterns.length > 5) {
|
| 182 |
+
wm.suggestedLayout = {
|
| 183 |
+
mode: 'discovery',
|
| 184 |
+
activeWidgets: ['VisualizerWidget', 'SearchInterfaceWidget', 'KnowledgeGraphWidget'],
|
| 185 |
+
theme: 'default'
|
| 186 |
+
};
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
/** Cross-correlate patterns across subsystems */
|
| 191 |
+
private correlateAcrossSystems(systems: any[]): any[] {
|
| 192 |
+
const patterns: any[] = [];
|
| 193 |
+
|
| 194 |
+
// Simple correlation: find common keywords/topics across systems
|
| 195 |
+
const allKeywords = new Map<string, number>();
|
| 196 |
+
|
| 197 |
+
if (!Array.isArray(systems)) return [];
|
| 198 |
+
|
| 199 |
+
systems.forEach((system, idx) => {
|
| 200 |
+
if (Array.isArray(system)) {
|
| 201 |
+
system.forEach((item: any) => {
|
| 202 |
+
if (!item) return;
|
| 203 |
+
const text = JSON.stringify(item).toLowerCase();
|
| 204 |
+
const words = text.match(/\b\w{4,}\b/g) || [];
|
| 205 |
+
words.forEach(word => {
|
| 206 |
+
allKeywords.set(word, (allKeywords.get(word) || 0) + 1);
|
| 207 |
+
});
|
| 208 |
+
});
|
| 209 |
+
}
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
// Find keywords that appear in multiple systems (holographic pattern)
|
| 213 |
+
Array.from(allKeywords.entries())
|
| 214 |
+
.filter(([_, count]) => count >= 2)
|
| 215 |
+
.forEach(([keyword, count]) => {
|
| 216 |
+
patterns.push({
|
| 217 |
+
keyword,
|
| 218 |
+
frequency: count,
|
| 219 |
+
systems: systems.length,
|
| 220 |
+
type: 'holographic_pattern'
|
| 221 |
+
});
|
| 222 |
+
});
|
| 223 |
+
|
| 224 |
+
return patterns;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/** Whole-part system health analysis */
|
| 228 |
+
async analyzeSystemHealth(): Promise<SystemHealthReport> {
|
| 229 |
+
const wholeSystem = {
|
| 230 |
+
globalHealth: await this.calculateGlobalHealth(),
|
| 231 |
+
emergentPatterns: await this.detectEmergentBehaviors(),
|
| 232 |
+
systemRhythms: await this.detectTemporalCycles()
|
| 233 |
+
};
|
| 234 |
+
|
| 235 |
+
const parts = await Promise.all([
|
| 236 |
+
this.componentHealth('pal'),
|
| 237 |
+
this.componentHealth('cma'),
|
| 238 |
+
this.componentHealth('srag'),
|
| 239 |
+
this.componentHealth('evolution'),
|
| 240 |
+
this.componentHealth('autonomous-agent')
|
| 241 |
+
]);
|
| 242 |
+
|
| 243 |
+
return this.modelWholePartRelationships(wholeSystem, parts);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
private async calculateGlobalHealth(): Promise<number> {
|
| 247 |
+
try {
|
| 248 |
+
const health = await this.cognitive.getSourceHealth('system');
|
| 249 |
+
return health?.healthScore || 0.8; // Default to 80% if no data
|
| 250 |
+
} catch {
|
| 251 |
+
return 0.8;
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
private async detectEmergentBehaviors(): Promise<any[]> {
|
| 256 |
+
// Placeholder: detect patterns that emerge from system interactions
|
| 257 |
+
return [];
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
private async detectTemporalCycles(): Promise<any[]> {
|
| 261 |
+
// Placeholder: detect recurring patterns over time
|
| 262 |
+
return [];
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
private async componentHealth(component: string): Promise<ComponentHealth> {
|
| 266 |
+
try {
|
| 267 |
+
if (!this.cognitive || !this.cognitive.getSourceHealth) {
|
| 268 |
+
return {
|
| 269 |
+
name: component,
|
| 270 |
+
healthScore: 0.8, // Default optimistic
|
| 271 |
+
latency: 0,
|
| 272 |
+
successRate: 0.9
|
| 273 |
+
};
|
| 274 |
+
}
|
| 275 |
+
const health = await this.cognitive.getSourceHealth(component);
|
| 276 |
+
return {
|
| 277 |
+
name: component,
|
| 278 |
+
healthScore: health?.healthScore || 0.8,
|
| 279 |
+
latency: health?.latency?.p50 || 0,
|
| 280 |
+
successRate: health?.successRate || 0.9
|
| 281 |
+
};
|
| 282 |
+
} catch {
|
| 283 |
+
return {
|
| 284 |
+
name: component,
|
| 285 |
+
healthScore: 0.8,
|
| 286 |
+
latency: 0,
|
| 287 |
+
successRate: 0.9
|
| 288 |
+
};
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
private modelWholePartRelationships(whole: any, parts: ComponentHealth[]): SystemHealthReport {
|
| 293 |
+
const avgPartHealth = parts.reduce((sum, p) => sum + p.healthScore, 0) / parts.length;
|
| 294 |
+
const wholeHealth = whole.globalHealth;
|
| 295 |
+
|
| 296 |
+
return {
|
| 297 |
+
globalHealth: wholeHealth,
|
| 298 |
+
componentHealth: parts,
|
| 299 |
+
emergentPatterns: whole.emergentPatterns,
|
| 300 |
+
systemRhythms: whole.systemRhythms,
|
| 301 |
+
wholePartRatio: wholeHealth / Math.max(avgPartHealth, 0.1), // How whole relates to parts
|
| 302 |
+
healthVariance: this.calculateVariance(parts.map(p => p.healthScore))
|
| 303 |
+
};
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
private calculateVariance(values: number[]): number {
|
| 307 |
+
if (values.length === 0) return 0;
|
| 308 |
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
| 309 |
+
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
| 310 |
+
return variance;
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
interface ComponentHealth {
|
| 315 |
+
name: string;
|
| 316 |
+
healthScore: number;
|
| 317 |
+
latency: number;
|
| 318 |
+
successRate: number;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
interface SystemHealthReport {
|
| 322 |
+
globalHealth: number;
|
| 323 |
+
componentHealth: ComponentHealth[];
|
| 324 |
+
emergentPatterns: any[];
|
| 325 |
+
systemRhythms: any[];
|
| 326 |
+
wholePartRatio: number;
|
| 327 |
+
healthVariance: number;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
export const unifiedMemorySystem = new UnifiedMemorySystem();
|
apps/backend/src/mcp/devToolsHandlers.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { devToolsService } from '../services/devtools/DevToolsService.js';
|
| 2 |
+
import { MCPMessage } from '@widget-tdc/mcp-types';
|
| 3 |
+
|
| 4 |
+
export async function handleDevToolsRequest(message: MCPMessage): Promise<any> {
|
| 5 |
+
const { tool, payload } = message;
|
| 6 |
+
|
| 7 |
+
switch (tool) {
|
| 8 |
+
case 'devtools-status':
|
| 9 |
+
return await devToolsService.getStatus();
|
| 10 |
+
|
| 11 |
+
case 'devtools-scan':
|
| 12 |
+
await devToolsService.runScan();
|
| 13 |
+
return { status: 'started', message: 'GitHub scan started in background' };
|
| 14 |
+
|
| 15 |
+
case 'devtools-validate':
|
| 16 |
+
const repoPath = (payload?.path as string) || process.cwd();
|
| 17 |
+
const result = await devToolsService.validateRepo(repoPath);
|
| 18 |
+
return { output: result };
|
| 19 |
+
|
| 20 |
+
default:
|
| 21 |
+
throw new Error(`Unknown DevTools tool: ${tool}`);
|
| 22 |
+
}
|
| 23 |
+
}
|