-
Notifications
You must be signed in to change notification settings - Fork 8
feat(search): enhance search_entries tool with flexible query parameters #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,27 +7,104 @@ import { BaseToolSchema, createToolClient } from '../../utils/tools.js'; | |
import { summarizeData } from '../../utils/summarizer.js'; | ||
|
||
export const SearchEntriesToolParams = BaseToolSchema.extend({ | ||
query: z.object({ | ||
content_type: z.string().optional().describe('Filter by content type'), | ||
include: z | ||
.number() | ||
.optional() | ||
.describe('Include this many levels of linked entries'), | ||
select: z | ||
.string() | ||
.optional() | ||
.describe('Comma-separated list of fields to return'), | ||
links_to_entry: z | ||
.string() | ||
.optional() | ||
.describe('Find entries that link to the specified entry ID'), | ||
limit: z | ||
.number() | ||
.optional() | ||
.describe('Maximum number of entries to return'), | ||
skip: z.number().optional().describe('Skip this many entries'), | ||
order: z.string().optional().describe('Order entries by this field'), | ||
}), | ||
query: z | ||
.object({ | ||
// Core parameters (maintain backward compatibility) | ||
content_type: z.string().optional().describe('Filter by content type'), | ||
include: z | ||
.number() | ||
.optional() | ||
.describe('Include this many levels of linked entries'), | ||
select: z | ||
.string() | ||
.optional() | ||
.describe('Comma-separated list of fields to return'), | ||
links_to_entry: z | ||
.string() | ||
.optional() | ||
.describe('Find entries that link to the specified entry ID'), | ||
limit: z | ||
.number() | ||
.optional() | ||
.describe( | ||
'Maximum number of entries to return (default: 10, max: 100)', | ||
), | ||
skip: z | ||
.number() | ||
.optional() | ||
.describe('Skip this many entries for pagination'), | ||
order: z.string().optional().describe('Order entries by this field'), | ||
|
||
// Full-text search (like ivo version) | ||
query: z | ||
.string() | ||
.optional() | ||
.describe('Full-text search across all fields'), | ||
|
||
// Common field-based searches (examples - any field is supported via catchall) | ||
'fields.title': z.string().optional().describe('Search by title field'), | ||
'fields.slug': z.string().optional().describe('Search by slug field'), | ||
'fields.internalName': z | ||
.string() | ||
.optional() | ||
.describe('Search by internal name field'), | ||
'fields.text': z | ||
.string() | ||
.optional() | ||
.describe('Search by text field (useful for testimonials)'), | ||
'fields.title[match]': z | ||
.string() | ||
.optional() | ||
.describe('Pattern match on title field'), | ||
'fields.slug[match]': z | ||
.string() | ||
.optional() | ||
.describe('Pattern match on slug field'), | ||
'fields.title[exists]': z | ||
.boolean() | ||
.optional() | ||
.describe('Check if title field exists'), | ||
'fields.slug[exists]': z | ||
.boolean() | ||
.optional() | ||
.describe('Check if slug field exists'), | ||
|
||
// System field searches | ||
'sys.id[in]': z | ||
.array(z.string()) | ||
.optional() | ||
.describe('Search by multiple entry IDs'), | ||
'sys.contentType.sys.id': z | ||
.string() | ||
.optional() | ||
.describe('Filter by content type ID'), | ||
'sys.createdAt[gte]': z | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For ISO 8601 datetimes, you can use the V3 https://v3.zod.dev/?id=strings | V4 https://zod.dev/api#iso-dates |
||
.string() | ||
.optional() | ||
.describe('Created after date (ISO format)'), | ||
'sys.createdAt[lte]': z | ||
.string() | ||
.optional() | ||
.describe('Created before date (ISO format)'), | ||
'sys.updatedAt[gte]': z | ||
.string() | ||
.optional() | ||
.describe('Updated after date (ISO format)'), | ||
'sys.updatedAt[lte]': z | ||
.string() | ||
.optional() | ||
.describe('Updated before date (ISO format)'), | ||
|
||
// Metadata searches | ||
'metadata.tags.sys.id[in]': z | ||
.array(z.string()) | ||
.optional() | ||
.describe('Filter by tag IDs'), | ||
}) | ||
.catchall(z.any()) | ||
.describe( | ||
'Flexible search parameters supporting ANY Contentful API query parameter. Use fields.* for field searches, sys.* for system fields, and any other Contentful API parameter.', | ||
), | ||
}); | ||
|
||
type Params = z.infer<typeof SearchEntriesToolParams>; | ||
|
@@ -44,13 +121,13 @@ async function tool(args: Params) { | |
...params, | ||
query: { | ||
...args.query, | ||
limit: Math.min(args.query.limit || 3, 3), | ||
limit: Math.min(args.query.limit || 10, 100), // Allow up to 100 results, default 10 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move this to a reusable fn so we don't need to duplicate the code below in the summarize data. Something like: function searchLimit(userLimit?: number) {
return Math.min(userLimit || 10, 100)
} |
||
skip: args.query.skip || 0, | ||
}, | ||
}); | ||
|
||
const summarized = summarizeData(entries, { | ||
maxItems: 3, | ||
maxItems: Math.min(args.query.limit || 10, 100), // Match the query limit | ||
remainingMessage: | ||
'To see more entries, please ask me to retrieve the next page.', | ||
}); | ||
|
@@ -62,5 +139,5 @@ async function tool(args: Params) { | |
|
||
export const searchEntriesTool = withErrorHandling( | ||
tool, | ||
'Error deleting dataset', | ||
'Error searching entries', | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just use
// Full-text search
as the comment here