Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { MigrationInterface, QueryRunner, Table, TableIndex, TableUnique } from 'typeorm';

/**
* Per-user read cursors for visible chat sessions (primary + user) in the agent console.
*/
export class CreateUserChatSessionReadStateTable1774100000000 implements MigrationInterface {
name = 'CreateUserChatSessionReadStateTable1774100000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user_chat_session_read_state',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{
name: 'user_id',
type: 'varchar',
length: '64',
isNullable: false,
},
{
name: 'client_id',
type: 'uuid',
isNullable: false,
},
{
name: 'agent_id',
type: 'uuid',
isNullable: false,
},
{
name: 'chat_session_id',
type: 'uuid',
isNullable: false,
},
{
name: 'last_read_at',
type: 'timestamptz',
isNullable: true,
},
{
name: 'last_read_agent_message_id',
type: 'uuid',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
{
name: 'updated_at',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
],
}),
true,
);

await queryRunner.createUniqueConstraint(
'user_chat_session_read_state',
new TableUnique({
name: 'uq_user_chat_session_read_state_user_client_agent_chat',
columnNames: ['user_id', 'client_id', 'agent_id', 'chat_session_id'],
}),
);

await queryRunner.createIndex(
'user_chat_session_read_state',
new TableIndex({
name: 'IDX_user_chat_session_read_state_user_id',
columnNames: ['user_id'],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('user_chat_session_read_state', 'IDX_user_chat_session_read_state_user_id');
await queryRunner.dropUniqueConstraint(
'user_chat_session_read_state',
'uq_user_chat_session_read_state_user_client_agent_chat',
);
await queryRunner.dropTable('user_chat_session_read_state');
}
}
2 changes: 2 additions & 0 deletions apps/agenstra/backend-agent-controller/src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
TicketBodyGenerationSessionEntity,
TicketCommentEntity,
TicketEntity,
UserChatSessionReadStateEntity,
UserEnvironmentReadStateEntity,
} from '@forepath/agenstra/backend/feature-agent-controller';
import { WebhookDeliveryEntity, WebhookEndpointEntity, EmailDeliveryEntity } from '@forepath/shared/backend';
Expand Down Expand Up @@ -106,6 +107,7 @@ export const typeormConfig: DataSourceOptions = {
AgentConsoleRegexFilterRuleEntity,
AgentConsoleRegexFilterRuleClientEntity,
AgentConsoleRegexFilterRuleSyncTargetEntity,
UserChatSessionReadStateEntity,
UserEnvironmentReadStateEntity,
WebhookEndpointEntity,
WebhookDeliveryEntity,
Expand Down
1 change: 1 addition & 0 deletions apps/agenstra/backend-agent-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@bull-board/nestjs": "7.1.5",
"@bull-board/ui": "7.1.5",
"@nestjs/bullmq": "11.0.4",
"@opensearch-project/opensearch": "3.6.0",
"@opentelemetry/sdk-node": "0.220.0",
"axios": "1.12.2",
"bullmq": "5.76.10",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';

/**
* Creates agent_chat_sessions, backfills a primary session per agent,
* and attaches chat_session_id to agent_messages and agent_message_events.
*/
export class CreateAgentChatSessionsTable1781200000000 implements MigrationInterface {
name = 'CreateAgentChatSessionsTable1781200000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'agent_chat_sessions',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{
name: 'agent_id',
type: 'uuid',
isNullable: false,
},
{
name: 'title',
type: 'varchar',
length: '255',
isNullable: true,
},
{
name: 'kind',
type: 'varchar',
length: '16',
isNullable: false,
},
{
name: 'resume_session_suffix',
type: 'varchar',
length: '128',
isNullable: false,
default: "''",
},
{
name: 'last_message_at',
type: 'timestamptz',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
{
name: 'updated_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
],
}),
true,
);

await queryRunner.createForeignKey(
'agent_chat_sessions',
new TableForeignKey({
columnNames: ['agent_id'],
referencedColumnNames: ['id'],
referencedTableName: 'agents',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
}),
);

await queryRunner.createIndex(
'agent_chat_sessions',
new TableIndex({
name: 'IDX_agent_chat_sessions_agent_id',
columnNames: ['agent_id'],
}),
);

await queryRunner.createIndex(
'agent_chat_sessions',
new TableIndex({
name: 'UQ_agent_chat_sessions_agent_suffix',
columnNames: ['agent_id', 'resume_session_suffix'],
isUnique: true,
}),
);

await queryRunner.query(`
CREATE UNIQUE INDEX "UQ_agent_chat_sessions_agent_primary"
ON "agent_chat_sessions" ("agent_id")
WHERE "kind" = 'primary'
`);

await queryRunner.query(`
INSERT INTO "agent_chat_sessions" ("agent_id", "title", "kind", "resume_session_suffix", "created_at", "updated_at")
SELECT "id", 'Chat', 'primary', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM "agents"
`);

await queryRunner.query(`
ALTER TABLE "agent_messages"
ADD COLUMN "chat_session_id" uuid
`);

await queryRunner.query(`
ALTER TABLE "agent_message_events"
ADD COLUMN "chat_session_id" uuid
`);

await queryRunner.query(`
UPDATE "agent_messages" AS m
SET "chat_session_id" = s."id"
FROM "agent_chat_sessions" AS s
WHERE s."agent_id" = m."agent_id" AND s."kind" = 'primary'
`);

await queryRunner.query(`
UPDATE "agent_message_events" AS e
SET "chat_session_id" = s."id"
FROM "agent_chat_sessions" AS s
WHERE s."agent_id" = e."agent_id" AND s."kind" = 'primary'
`);

await queryRunner.query(`
UPDATE "agent_chat_sessions" AS s
SET "last_message_at" = sub."max_created"
FROM (
SELECT "chat_session_id", MAX("created_at") AS "max_created"
FROM "agent_messages"
WHERE "chat_session_id" IS NOT NULL
GROUP BY "chat_session_id"
) AS sub
WHERE s."id" = sub."chat_session_id"
`);

await queryRunner.query(`
ALTER TABLE "agent_messages"
ALTER COLUMN "chat_session_id" SET NOT NULL
`);

await queryRunner.query(`
ALTER TABLE "agent_message_events"
ALTER COLUMN "chat_session_id" SET NOT NULL
`);

await queryRunner.createForeignKey(
'agent_messages',
new TableForeignKey({
name: 'FK_agent_messages_chat_session_id',
columnNames: ['chat_session_id'],
referencedColumnNames: ['id'],
referencedTableName: 'agent_chat_sessions',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
}),
);

await queryRunner.createForeignKey(
'agent_message_events',
new TableForeignKey({
name: 'FK_agent_message_events_chat_session_id',
columnNames: ['chat_session_id'],
referencedColumnNames: ['id'],
referencedTableName: 'agent_chat_sessions',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
}),
);

await queryRunner.createIndex(
'agent_messages',
new TableIndex({
name: 'IDX_agent_messages_agent_chat_created',
columnNames: ['agent_id', 'chat_session_id', 'created_at'],
}),
);

await queryRunner.createIndex(
'agent_message_events',
new TableIndex({
name: 'IDX_agent_message_events_agent_chat_created',
columnNames: ['agent_id', 'chat_session_id', 'created_at'],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('agent_message_events', 'IDX_agent_message_events_agent_chat_created');
await queryRunner.dropIndex('agent_messages', 'IDX_agent_messages_agent_chat_created');

const messagesTable = await queryRunner.getTable('agent_messages');
const messagesFk = messagesTable?.foreignKeys.find((fk) => fk.columnNames.includes('chat_session_id'));

if (messagesFk) {
await queryRunner.dropForeignKey('agent_messages', messagesFk);
}

const eventsTable = await queryRunner.getTable('agent_message_events');
const eventsFk = eventsTable?.foreignKeys.find((fk) => fk.columnNames.includes('chat_session_id'));

if (eventsFk) {
await queryRunner.dropForeignKey('agent_message_events', eventsFk);
}

await queryRunner.query(`ALTER TABLE "agent_message_events" DROP COLUMN "chat_session_id"`);
await queryRunner.query(`ALTER TABLE "agent_messages" DROP COLUMN "chat_session_id"`);

await queryRunner.query(`DROP INDEX IF EXISTS "UQ_agent_chat_sessions_agent_primary"`);
await queryRunner.dropIndex('agent_chat_sessions', 'UQ_agent_chat_sessions_agent_suffix');
await queryRunner.dropIndex('agent_chat_sessions', 'IDX_agent_chat_sessions_agent_id');

const sessionsTable = await queryRunner.getTable('agent_chat_sessions');
const sessionsFk = sessionsTable?.foreignKeys.find((fk) => fk.columnNames.includes('agent_id'));

if (sessionsFk) {
await queryRunner.dropForeignKey('agent_chat_sessions', sessionsFk);
}

await queryRunner.dropTable('agent_chat_sessions');
}
}
2 changes: 2 additions & 0 deletions apps/agenstra/backend-agent-manager/src/typeorm.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AgentChatSessionEntity,
AgentEntity,
AgentEnvironmentVariableEntity,
AgentMessageEntity,
Expand Down Expand Up @@ -41,6 +42,7 @@ export const typeormConfig: DataSourceOptions = {
database: process.env.DB_DATABASE || 'agent_manager',
entities: [
AgentEntity,
AgentChatSessionEntity,
AgentEnvironmentVariableEntity,
AgentMessageEntity,
AgentMessageEventEntity,
Expand Down
Loading
Loading