199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
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),
|
|
};
|
|
},
|
|
};
|
|
}
|