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
Expand Up @@ -27,50 +27,51 @@ import { readCustomerByEmail } from './helpers/read_customer_by_email';
import { readCustomerById } from './helpers/read_customer_by_id';
import { readCustomerList } from './helpers/read_customer_list';

// Use a flat object schema instead of discriminatedUnion to ensure OpenAI-compatible JSON Schema
// (discriminatedUnion produces anyOf/oneOf which some providers reject as "type: None")
const customerReadArgs = z.object({
operation: z
.enum(['get_by_id', 'get_by_email', 'list', 'count'])
.describe(
"Operation to perform: 'get_by_id' (fetch by ID), 'get_by_email' (fetch by email), 'list' (paginate all), or 'count' (count total customers)",
),
// For get_by_id operation
customerId: z
.string()
.optional()
.describe(
'Required for \'get_by_id\': Convex Id<"customers"> (string format) for the target customer',
),
// For get_by_email operation
email: z
.string()
.optional()
.describe(
"Required for 'get_by_email': Customer email address to search for",
),
// Common fields for all operations
fields: z
.array(z.string())
.optional()
.describe(
"Optional list of fields to return. Default: ['_id','name','email','status','source','locale']",
),
// For list operation
cursor: z
.string()
.nullable()
.optional()
.describe(
"For 'list' operation: Pagination cursor from previous response, or null/omitted for first page",
),
numItems: z
.number()
.optional()
.describe(
"For 'list' operation: Number of items per page (default: 200). Fewer fields = more items allowed.",
),
});
const customerReadArgs = z.discriminatedUnion('operation', [
z.object({
operation: z.literal('get_by_id'),
customerId: z
.string()
.describe(
'Convex Id<"customers"> (string format) for the target customer',
),
fields: z
.array(z.string())
.optional()
.describe(
"Fields to return. Default: ['_id','name','email','status','source','locale']",
),
}),
z.object({
operation: z.literal('get_by_email'),
email: z.string().describe('Customer email address to search for'),
fields: z
.array(z.string())
.optional()
.describe(
"Fields to return. Default: ['_id','name','email','status','source','locale']",
),
}),
z.object({
operation: z.literal('list'),
cursor: z
.string()
.nullable()
.optional()
.describe(
'Pagination cursor from previous response, or null/omitted for first page',
),
numItems: z
.number()
.optional()
.describe(
'Number of items per page (default: 200). Fewer fields = more items allowed.',
),
}),
z.object({
operation: z.literal('count'),
}),
]);

export const customerReadTool: ToolDefinition = {
name: 'customer_read',
Expand Down Expand Up @@ -126,23 +127,13 @@ BEST PRACTICES:
| CustomerReadCountResult
> => {
if (args.operation === 'get_by_id') {
if (!args.customerId) {
throw new Error(
"Missing required 'customerId' for get_by_id operation",
);
}
return readCustomerById(ctx, {
customerId: args.customerId,
fields: args.fields,
});
}

if (args.operation === 'get_by_email') {
if (!args.email) {
throw new Error(
"Missing required 'email' for get_by_email operation",
);
}
return readCustomerByEmail(ctx, {
email: args.email,
fields: args.fields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,17 @@ import {
type DatabaseSchemaGetTableResult,
} from './helpers/types';

const databaseSchemaArgs = z.object({
operation: z
.enum(['list_tables', 'get_table_schema'])
.describe(
"Operation: 'list_tables' to see all tables, 'get_table_schema' to get fields for a specific table",
),
tableName: z
.string()
.optional()
.describe(
"Required for 'get_table_schema': table name (e.g., 'conversations', 'customers', 'approvals')",
),
});
const databaseSchemaArgs = z.discriminatedUnion('operation', [
z.object({
operation: z.literal('list_tables'),
}),
z.object({
operation: z.literal('get_table_schema'),
tableName: z
.string()
.describe("Table name (e.g., 'conversations', 'customers', 'approvals')"),
}),
]);

export const databaseSchemaTool: ToolDefinition = {
name: 'database_schema',
Expand Down Expand Up @@ -92,12 +90,6 @@ FILTER EXPRESSION EXAMPLES:
}

// operation === 'get_table_schema'
if (!args.tableName) {
throw new Error(
"Missing required 'tableName' for get_table_schema operation",
);
}

const schema = getTableSchema(args.tableName);
if (!schema) {
const availableTables = getSupportedTables()
Expand Down
153 changes: 65 additions & 88 deletions services/platform/convex/agent_tools/files/docx_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,74 +82,68 @@ const sectionSchema = z.object({
.describe('Table rows (2D array)'),
});

const docxArgs = z.object({
operation: z
.enum(['list_templates', 'generate', 'parse'])
.optional()
.describe(
"Operation to perform: 'list_templates', 'generate' (default), or 'parse' (extract text from DOCX).",
),
// For list_templates operation
limit: z
.number()
.optional()
.describe(
"For 'list_templates': Maximum number of DOCX documents/templates to return (default: 50)",
),
// For generate operation (optional template support)
templateStorageId: z
.string()
.optional()
.describe(
'Convex storage ID of a DOCX template. When provided, the template is used as base, preserving headers, footers, fonts, and page setup.',
),
fileName: z
.string()
.optional()
.describe(
"For 'generate': Base name for the DOCX file (without extension). Required for generate.",
),
title: z.string().optional().describe('Document title'),
subtitle: z.string().optional().describe('Document subtitle'),
sections: z
.array(sectionSchema)
.optional()
.describe(
"For 'generate': Content sections. Each section can be a heading, paragraph, bullets, numbered list, table, quote, or code block.",
),
// For generate from markdown/html (same content as PDF tool)
sourceType: z
.enum(['markdown', 'html'])
.optional()
.describe(
"For 'generate': Source type when generating from markdown or HTML content instead of sections. Use this to quickly convert existing markdown/HTML to DOCX.",
),
content: z
.string()
.optional()
.describe(
"For 'generate': Markdown or HTML text content. Use with sourceType. This is the fastest way to generate DOCX from the same content used for PDF generation.",
),
// For parse operation
fileId: z
.string()
.optional()
.describe(
"For 'parse': **REQUIRED** - Convex storage ID (e.g., 'kg2bazp7fbgt9srq63knfagjrd7yfenj'). Get this from the file attachment context.",
),
filename: z
.string()
.optional()
.describe(
"For 'parse': Original filename (e.g., 'document.docx'). Optional — auto-resolved from file metadata if omitted.",
),
user_input: z
.string()
.optional()
.describe(
"For 'parse': **REQUIRED** - The user's question or instruction about the document content",
),
});
const docxArgs = z.discriminatedUnion('operation', [
z.object({
operation: z.literal('list_templates'),
limit: z
.number()
.optional()
.describe(
'Maximum number of DOCX documents/templates to return (default: 50)',
),
}),
z.object({
operation: z.literal('generate'),
templateStorageId: z
.string()
.optional()
.describe(
'Convex storage ID of a DOCX template. When provided, the template is used as base, preserving headers, footers, fonts, and page setup.',
),
fileName: z
.string()
.describe('Base name for the DOCX file (without extension)'),
title: z.string().optional().describe('Document title'),
subtitle: z.string().optional().describe('Document subtitle'),
sections: z
.array(sectionSchema)
.optional()
.describe(
'Content sections. Each section can be a heading, paragraph, bullets, numbered list, table, quote, or code block.',
),
sourceType: z
.enum(['markdown', 'html'])
.optional()
.describe(
'Source type when generating from markdown or HTML content instead of sections. Use this to quickly convert existing markdown/HTML to DOCX.',
),
content: z
.string()
.optional()
.describe(
'Markdown or HTML text content. Use with sourceType. This is the fastest way to generate DOCX from the same content used for PDF generation.',
),
}),
z.object({
operation: z.literal('parse'),
fileId: z
.string()
.describe(
"Convex storage ID (e.g., 'kg2bazp7fbgt9srq63knfagjrd7yfenj'). Get this from the file attachment context.",
),
filename: z
.string()
.optional()
.describe(
"Original filename (e.g., 'document.docx'). Optional — auto-resolved from file metadata if omitted.",
),
user_input: z
.string()
.describe(
"The user's question or instruction about the document content",
),
}),
]);

export const docxTool = {
name: 'docx' as const,
Expand Down Expand Up @@ -204,10 +198,8 @@ AFTER GENERATING: The file automatically appears as a download card in the chat.
args: docxArgs,
handler: async (ctx: ToolCtx, args): Promise<DocxResult> => {
const { organizationId } = ctx;
const operation = args.operation ?? 'generate';

// Handle list_templates operation
if (operation === 'list_templates') {
if (args.operation === 'list_templates') {
if (!organizationId) {
return {
operation: 'list_templates',
Expand Down Expand Up @@ -267,19 +259,7 @@ AFTER GENERATING: The file automatically appears as a download card in the chat.
}
}

// Handle parse operation
if (operation === 'parse') {
if (!args.fileId) {
throw new Error(
"Missing required 'fileId' for parse operation. Get the fileId from the file attachment context.",
);
}
if (!args.user_input) {
throw new Error(
"Missing required 'user_input' for parse operation. Provide the user's question or instruction about the document.",
);
}

if (args.operation === 'parse') {
const model = getAgentModelId(ctx);
const result = await parseFile(
ctx,
Expand All @@ -292,13 +272,10 @@ AFTER GENERATING: The file automatically appears as a download card in the chat.
return { operation: 'parse', ...result };
}

// Default / generate operation
// operation === 'generate'
if (!organizationId) {
throw new Error('organizationId is required to generate a document');
}
if (!args.fileName) {
throw new Error("Missing required 'fileName' for generate operation");
}

// Mode A: Generate from markdown/html content
if (args.sourceType && args.content) {
Expand Down
Loading
Loading