Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/backend-agent-controller/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
}
}
},
"reindex-knowledge-embeddings": {
"executor": "nx:run-commands",
"options": {
"command": "npx ts-node -r tsconfig-paths/register --project apps/backend-agent-controller/tsconfig.app.json apps/backend-agent-controller/src/scripts/reindex-knowledge-embeddings.ts"
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddKnowledgeEmbeddingsAndAutoEnrichment1770900000000 implements MigrationInterface {
name = 'AddKnowledgeEmbeddingsAndAutoEnrichment1770900000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS vector`);

await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "knowledge_node_embeddings" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"client_id" uuid NOT NULL,
"knowledge_node_id" uuid NOT NULL,
"chunk_index" int NOT NULL,
"chunk_text" text NOT NULL,
"embedding" vector(768) NOT NULL,
"embedding_model" varchar(128) NOT NULL,
"embedding_provider" varchar(64) NOT NULL,
"content_hash" varchar(64) NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "PK_knowledge_node_embeddings_id" PRIMARY KEY ("id"),
CONSTRAINT "FK_knowledge_node_embeddings_client_id" FOREIGN KEY ("client_id") REFERENCES "clients"("id") ON DELETE CASCADE,
CONSTRAINT "FK_knowledge_node_embeddings_node_id" FOREIGN KEY ("knowledge_node_id") REFERENCES "knowledge_nodes"("id") ON DELETE CASCADE,
CONSTRAINT "UQ_knowledge_node_embeddings_client_node_chunk" UNIQUE ("client_id", "knowledge_node_id", "chunk_index")
)
`);

await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_knowledge_node_embeddings_client_id"
ON "knowledge_node_embeddings" ("client_id")
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_knowledge_node_embeddings_node_id"
ON "knowledge_node_embeddings" ("knowledge_node_id")
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_knowledge_node_embeddings_content_hash"
ON "knowledge_node_embeddings" ("content_hash")
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_knowledge_node_embeddings_embedding_ivfflat"
ON "knowledge_node_embeddings" USING ivfflat ("embedding" vector_cosine_ops)
WITH (lists = 100)
`);

await queryRunner.query(`
ALTER TABLE "ticket_automation"
ADD COLUMN IF NOT EXISTS "auto_enrichment_enabled" boolean NOT NULL DEFAULT true
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "ticket_automation" DROP COLUMN IF EXISTS "auto_enrichment_enabled"
`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_knowledge_node_embeddings_embedding_ivfflat"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_knowledge_node_embeddings_content_hash"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_knowledge_node_embeddings_node_id"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_knowledge_node_embeddings_client_id"`);
await queryRunner.query(`DROP TABLE IF EXISTS "knowledge_node_embeddings"`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

/**
* Adds auto_context_enrichment interaction kind for auto-enrichment telemetry.
*/
export class AddAutoContextEnrichmentInteractionKind1771000000000 implements MigrationInterface {
name = 'AddAutoContextEnrichmentInteractionKind1771000000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TYPE "statistics_interaction_kind_enum" ADD VALUE IF NOT EXISTS 'auto_context_enrichment'
`);
}

public async down(): Promise<void> {
// PostgreSQL does not support removing enum values safely; keep value.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { KnowledgeEmbeddingIndexService } from '@forepath/framework/backend';
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';

import { AppModule } from '../app/app.module';

function readClientIdArg(args: string[]): string | undefined {
const clientIdArg = args.find((arg) => arg.startsWith('--client-id='));

if (clientIdArg) {
return clientIdArg.split('=')[1];
}

const clientIdIdx = args.findIndex((arg) => arg === '--client-id');

if (clientIdIdx >= 0 && args[clientIdIdx + 1]) {
return args[clientIdIdx + 1];
}

return undefined;
}

async function main(): Promise<void> {
const logger = new Logger('ReindexKnowledgeEmbeddingsCli');
const args = process.argv.slice(2);
const clientId = readClientIdArg(args);

if (!clientId) {
throw new Error('Missing required argument --client-id <uuid>');
}

const app = await NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn', 'log'] });

try {
const knowledgeEmbeddingIndexService = app.get(KnowledgeEmbeddingIndexService);
const result = await knowledgeEmbeddingIndexService.reindexAllPages(clientId);

logger.log(`Reindexed ${result.processed} page embeddings for client ${clientId}`);
} finally {
await app.close();
}
}

main().catch((error: unknown) => {
const logger = new Logger('ReindexKnowledgeEmbeddingsCli');
const message = error instanceof Error ? error.message : String(error);

logger.error(`Embedding reindex failed: ${message}`);
process.exit(1);
});
2 changes: 2 additions & 0 deletions apps/backend-agent-controller/src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
StatisticsUserEntity,
ClientAgentAutonomyEntity,
KnowledgeNodeEntity,
KnowledgeNodeEmbeddingEntity,
KnowledgePageActivityEntity,
KnowledgeRelationEntity,
TicketActivityEntity,
Expand Down Expand Up @@ -68,6 +69,7 @@ export const typeormConfig: DataSourceOptions = {
TicketAutomationRunStepEntity,
ClientAgentAutonomyEntity,
KnowledgeNodeEntity,
KnowledgeNodeEmbeddingEntity,
KnowledgePageActivityEntity,
KnowledgeRelationEntity,
AgentConsoleRegexFilterRuleEntity,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
sequenceDiagram
participant UI as AgentConsoleChatUI
participant Ctl as ClientsGateway
participant Res as AutoContextResolver
participant Tix as TicketsService
participant KTree as KnowledgeTreeService
participant Vec as KnowledgeNodeEmbeddingsPgvector
participant Mgr as AgentManagerGateway
participant Comp as PromptContextComposer
participant Prov as AgentProvider

UI->>Ctl: forward(event=chat, payload.contextInjection)
Ctl->>Ctl: normalize manual context (ticketShas, knowledgeShas)
alt autoEnrichmentEnabled=true and global feature enabled
Ctl->>Res: resolve(clientId, prompt, contextInjection)
Res->>Tix: resolveTicketIdByClientSha(manual ticketShas)
Res->>KTree: findNodeBySha(manual knowledgeShas)
Res->>Vec: topK chunks by cosine distance (client scoped)
Res->>KTree: relation context expansion (ticket/page sources)
Res->>Res: dedupe by canonical entity id + apply section/char budgets
Res-->>Ctl: merged contextInjection (manual + auto knowledgeContexts)
else auto enrichment disabled
Ctl->>Ctl: keep manual-only contextInjection
end
Ctl->>Mgr: emit chat with enriched contextInjection
Mgr->>Comp: composeChatMessage(message, contextInjection)
Comp-->>Mgr: hidden preamble + user message
Mgr->>Prov: sendMessage/sendMessageStream
Prov-->>Mgr: response payload
Mgr-->>Ctl: chatMessage/chatEvent
Ctl-->>UI: forwardAck + response events
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ sequenceDiagram
end
Note over Orch,Vcs: Default strategy reuses `automation/ticket-{ticketId}`; `new_per_run` or one-shot force uses ephemeral `automation/{runPrefix}`.
loop maxIterations
Orch->>Chat: sendChatSync(sync, ephemeral)
Orch->>Chat: sendChatSync(sync, ephemeral, contextInjection)
Chat->>Mgr: chat
Mgr->>Prov: sendMessage(continue, resumeSessionSuffix)
Prov-->>Mgr: NDJSON / text
Mgr-->>Chat: chatMessage
Chat-->>Orch: assistant text
Orch->>DB: append run step
end
Note over Orch,Chat: contextInjection carries includeWorkspace, environmentIds, and autoEnrichmentEnabled.
Orch->>Vcs: runVerifierCommands
Orch->>Vcs: getStatus; if dirty: stage all
Orch->>Chat: sendChatSync (conventional commit subject; autonomous_ticket_commit_message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ components:
When event is "closeTerminal", the payload should contain {sessionId: string}.
When event is "enhanceChat", the payload should contain { message: string, correlationId: string, model?: string }.
When event is "generateTicketBody", the payload should contain { title: string, correlationId: string, model?: string }.
When event is "chat", the payload may include { message, correlationId?, model?, responseMode?: "single"|"stream"|"sync", ephemeral?: boolean, continue?: boolean, resumeSessionSuffix?: string, contextInjection?: { includeWorkspace?: boolean, environmentIds?: string[] } }.
When event is "chat", the payload may include { message, correlationId?, model?, responseMode?: "single"|"stream"|"sync", ephemeral?: boolean, continue?: boolean, resumeSessionSuffix?: string, contextInjection?: { includeWorkspace?: boolean, environmentIds?: string[], ticketShas?: string[], knowledgeShas?: string[], autoEnrichmentEnabled?: boolean } }.
Response events forwarded from the agent-manager include "containerStats" (payload has status.running and optional stats).
agentId:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3064,6 +3064,40 @@ paths:
$ref: '#/components/schemas/StatisticsSummaryDto'
'403':
description: Access denied to this client
/clients/{id}/knowledge/embeddings/reindex:
post:
summary: Manually reindex workspace knowledge embeddings
operationId: reindexClientKnowledgeEmbeddings
description: |
Triggers a synchronous embedding backfill for all knowledge pages in the workspace.
Requires workspace management permissions.
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
description: The UUID of the client
responses:
'200':
description: Reindex completed
content:
application/json:
schema:
type: object
required: [status, clientId, pagesReindexed]
properties:
status:
type: string
enum: [started]
clientId:
type: string
format: uuid
pagesReindexed:
type: integer
'403':
description: Access denied to this client
/clients/{id}/statistics/chat-io:
get:
summary: Get chat I/O records for a client
Expand Down Expand Up @@ -3099,7 +3133,16 @@ paths:
name: interactionKind
schema:
type: string
enum: [chat, prompt_enhancement]
enum:
[
chat,
prompt_enhancement,
ticket_body_generation,
auto_context_enrichment,
autonomous_ticket_run,
autonomous_ticket_run_turn,
autonomous_ticket_commit_message,
]
description: Filter by interaction type (normal chat vs prompt enhancement metrics)
- in: query
name: limit
Expand Down Expand Up @@ -3363,7 +3406,16 @@ paths:
name: interactionKind
schema:
type: string
enum: [chat, prompt_enhancement]
enum:
[
chat,
prompt_enhancement,
ticket_body_generation,
auto_context_enrichment,
autonomous_ticket_run,
autonomous_ticket_run_turn,
autonomous_ticket_commit_message,
]
description: Filter by interaction type (normal chat vs prompt enhancement metrics)
- in: query
name: limit
Expand Down Expand Up @@ -4424,6 +4476,9 @@ components:
totalWords,
totalChars,
avgWordsPerMessage,
autoEnrichmentRuns,
autoEnrichmentContexts,
autoEnrichmentChars,
filterDropCount,
filterTypesBreakdown,
]
Expand All @@ -4436,6 +4491,15 @@ components:
type: integer
avgWordsPerMessage:
type: number
autoEnrichmentRuns:
type: integer
description: Number of turns where auto-enrichment injected at least one context section
autoEnrichmentContexts:
type: integer
description: Total number of auto-injected context sections
autoEnrichmentChars:
type: integer
description: Total character volume of auto-injected context sections
filterDropCount:
type: integer
filterTypesBreakdown:
Expand Down Expand Up @@ -4518,7 +4582,16 @@ components:
enum: [input, output]
interactionKind:
type: string
enum: [chat, prompt_enhancement]
enum:
[
chat,
prompt_enhancement,
ticket_body_generation,
auto_context_enrichment,
autonomous_ticket_run,
autonomous_ticket_run_turn,
autonomous_ticket_commit_message,
]
description: Normal chat vs prompt enhancement (magic wand) metrics
wordCount:
type: integer
Expand Down Expand Up @@ -5014,6 +5087,8 @@ components:
items:
type: string
format: uuid
autoEnrichmentEnabled:
type: boolean
verifierProfile:
$ref: '#/components/schemas/TicketVerifierProfileDto'
requiresApproval:
Expand All @@ -5040,6 +5115,7 @@ components:
- allowedAgentIds
- includeWorkspaceContext
- contextEnvironmentIds
- autoEnrichmentEnabled
- verifierProfile
- requiresApproval
- approvedAt
Expand Down Expand Up @@ -5070,6 +5146,8 @@ components:
items:
type: string
format: uuid
autoEnrichmentEnabled:
type: boolean
verifierProfile:
oneOf:
- $ref: '#/components/schemas/TicketVerifierProfileDto'
Expand Down Expand Up @@ -5286,6 +5364,16 @@ components:
type: array
items:
type: string
ticketShas:
type: array
items:
type: string
knowledgeShas:
type: array
items:
type: string
autoEnrichmentEnabled:
type: boolean
TicketAutomationRunChatEventDto:
type: object
description: >
Expand Down
Loading
Loading