refactor(api): extract assistant comments slice
This commit is contained in:
@@ -8,8 +8,6 @@ import {
|
||||
CreateAssignmentSchema,
|
||||
AllocationStatus,
|
||||
EstimateStatus,
|
||||
type CommentEntityType,
|
||||
COMMENT_ENTITY_TYPE_VALUES,
|
||||
PermissionKey,
|
||||
SystemRole,
|
||||
} from "@capakraken/shared";
|
||||
@@ -129,6 +127,11 @@ import {
|
||||
createDashboardInsightsReportsExecutors,
|
||||
dashboardInsightsReportsToolDefinitions,
|
||||
} from "./assistant-tools/dashboard-insights-reports.js";
|
||||
import {
|
||||
commentMutationToolDefinitions,
|
||||
commentReadToolDefinitions,
|
||||
createCommentExecutors,
|
||||
} from "./assistant-tools/comments.js";
|
||||
import {
|
||||
withToolAccess,
|
||||
type ToolAccessRequirements,
|
||||
@@ -136,7 +139,6 @@ import {
|
||||
type ToolDef,
|
||||
type ToolExecutor,
|
||||
} from "./assistant-tools/shared.js";
|
||||
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.js";
|
||||
|
||||
export type { ToolContext } from "./assistant-tools/shared.js";
|
||||
|
||||
@@ -2319,22 +2321,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
||||
|
||||
// ── TASK MANAGEMENT ──
|
||||
...notificationTaskToolDefinitions,
|
||||
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_comments",
|
||||
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
},
|
||||
required: ["entityType", "entityId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
...commentReadToolDefinitions,
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -2399,37 +2386,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_comment",
|
||||
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
|
||||
},
|
||||
required: ["entityType", "entityId", "body"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_comment",
|
||||
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
commentId: { type: "string", description: "Comment ID to resolve" },
|
||||
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
|
||||
},
|
||||
required: ["commentId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
...commentMutationToolDefinitions,
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -2861,6 +2818,12 @@ const executors = {
|
||||
createReportCaller,
|
||||
createScopedCallerContext,
|
||||
}),
|
||||
...createCommentExecutors({
|
||||
createCommentCaller,
|
||||
createScopedCallerContext,
|
||||
toAssistantCommentCreationError,
|
||||
toAssistantCommentResolveError,
|
||||
}),
|
||||
|
||||
async search_estimates(params: {
|
||||
projectCode?: string; query?: string; status?: string; limit?: number;
|
||||
@@ -3324,30 +3287,6 @@ const executors = {
|
||||
toAssistantNotificationCreationError,
|
||||
}),
|
||||
|
||||
async list_comments(params: { entityType: CommentEntityType; entityId: string }, ctx: ToolContext) {
|
||||
const caller = createCommentCaller(createScopedCallerContext(ctx));
|
||||
const comments = await caller.list({
|
||||
entityType: params.entityType,
|
||||
entityId: params.entityId,
|
||||
});
|
||||
|
||||
return comments.map((c) => ({
|
||||
id: c.id,
|
||||
author: c.author.name ?? c.author.email,
|
||||
body: c.body,
|
||||
resolved: c.resolved,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
replyCount: c.replies.length,
|
||||
replies: c.replies.map((r) => ({
|
||||
id: r.id,
|
||||
author: r.author.name ?? r.author.email,
|
||||
body: r.body,
|
||||
resolved: r.resolved,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
async lookup_rate(params: {
|
||||
clientId?: string;
|
||||
chapter?: string;
|
||||
@@ -3425,74 +3364,6 @@ const executors = {
|
||||
return caller.generateProjectNarrative({ projectId: params.projectId });
|
||||
},
|
||||
|
||||
async create_comment(params: {
|
||||
entityType: CommentEntityType;
|
||||
entityId: string;
|
||||
body: string;
|
||||
}, ctx: ToolContext) {
|
||||
if (params.body.length === 0) {
|
||||
return { error: "Comment body is required." };
|
||||
}
|
||||
|
||||
if (params.body.length > 10_000) {
|
||||
return { error: "Comment body must be at most 10000 characters." };
|
||||
}
|
||||
|
||||
const caller = createCommentCaller(createScopedCallerContext(ctx));
|
||||
let comment;
|
||||
try {
|
||||
comment = await caller.create({
|
||||
entityType: params.entityType,
|
||||
entityId: params.entityId,
|
||||
body: params.body,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantCommentCreationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["comment"],
|
||||
id: comment.id,
|
||||
author: comment.author.name ?? comment.author.email,
|
||||
body: comment.body,
|
||||
createdAt: comment.createdAt.toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async resolve_comment(params: {
|
||||
commentId: string;
|
||||
resolved?: boolean;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = createCommentCaller(createScopedCallerContext(ctx));
|
||||
let updated;
|
||||
try {
|
||||
updated = await caller.resolve({
|
||||
id: params.commentId,
|
||||
resolved: params.resolved !== false,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantCommentResolveError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["comment"],
|
||||
id: updated.id,
|
||||
resolved: updated.resolved,
|
||||
author: updated.author.name ?? updated.author.email,
|
||||
body: updated.body.slice(0, 100),
|
||||
};
|
||||
},
|
||||
|
||||
async query_change_history(params: {
|
||||
entityType?: string;
|
||||
search?: string;
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { COMMENT_ENTITY_TYPE_VALUES, type CommentEntityType } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../../lib/comment-entity-registry.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
type CommentRecord = {
|
||||
id: string;
|
||||
body: string;
|
||||
resolved: boolean;
|
||||
createdAt: Date;
|
||||
author: {
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CommentListRecord = CommentRecord & {
|
||||
replies: CommentRecord[];
|
||||
};
|
||||
|
||||
type CommentsDeps = {
|
||||
createCommentCaller: (ctx: TRPCContext) => {
|
||||
list: (params: {
|
||||
entityType: CommentEntityType;
|
||||
entityId: string;
|
||||
}) => Promise<CommentListRecord[]>;
|
||||
create: (params: {
|
||||
entityType: CommentEntityType;
|
||||
entityId: string;
|
||||
body: string;
|
||||
}) => Promise<CommentRecord>;
|
||||
resolve: (params: {
|
||||
id: string;
|
||||
resolved: boolean;
|
||||
}) => Promise<CommentRecord>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
toAssistantCommentCreationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantCommentResolveError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const commentReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_comments",
|
||||
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
},
|
||||
required: ["entityType", "entityId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {});
|
||||
|
||||
export const commentMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_comment",
|
||||
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
|
||||
entityId: { type: "string", description: "Entity ID" },
|
||||
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
|
||||
},
|
||||
required: ["entityType", "entityId", "body"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "resolve_comment",
|
||||
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
commentId: { type: "string", description: "Comment ID to resolve" },
|
||||
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
|
||||
},
|
||||
required: ["commentId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {});
|
||||
|
||||
export function createCommentExecutors(
|
||||
deps: CommentsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_comments(
|
||||
params: { entityType: CommentEntityType; entityId: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||
const comments = await caller.list({
|
||||
entityType: params.entityType,
|
||||
entityId: params.entityId,
|
||||
});
|
||||
|
||||
return comments.map((comment) => ({
|
||||
id: comment.id,
|
||||
author: comment.author.name ?? comment.author.email,
|
||||
body: comment.body,
|
||||
resolved: comment.resolved,
|
||||
createdAt: comment.createdAt.toISOString(),
|
||||
replyCount: comment.replies.length,
|
||||
replies: comment.replies.map((reply) => ({
|
||||
id: reply.id,
|
||||
author: reply.author.name ?? reply.author.email,
|
||||
body: reply.body,
|
||||
resolved: reply.resolved,
|
||||
createdAt: reply.createdAt.toISOString(),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
async create_comment(
|
||||
params: { entityType: CommentEntityType; entityId: string; body: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
if (params.body.length === 0) {
|
||||
return { error: "Comment body is required." };
|
||||
}
|
||||
|
||||
if (params.body.length > 10_000) {
|
||||
return { error: "Comment body must be at most 10000 characters." };
|
||||
}
|
||||
|
||||
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||
let comment;
|
||||
try {
|
||||
comment = await caller.create({
|
||||
entityType: params.entityType,
|
||||
entityId: params.entityId,
|
||||
body: params.body,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantCommentCreationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["comment"],
|
||||
id: comment.id,
|
||||
author: comment.author.name ?? comment.author.email,
|
||||
body: comment.body,
|
||||
createdAt: comment.createdAt.toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async resolve_comment(
|
||||
params: { commentId: string; resolved?: boolean },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const caller = deps.createCommentCaller(deps.createScopedCallerContext(ctx));
|
||||
let updated;
|
||||
try {
|
||||
updated = await caller.resolve({
|
||||
id: params.commentId,
|
||||
resolved: params.resolved !== false,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantCommentResolveError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["comment"],
|
||||
id: updated.id,
|
||||
resolved: updated.resolved,
|
||||
author: updated.author.name ?? updated.author.email,
|
||||
body: updated.body.slice(0, 100),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user