-
Notifications
You must be signed in to change notification settings - Fork 5
feat(platform): improve web tool modes, search UX, and context builder #1551
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 |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * Format available website summaries for display in no-results messages. | ||
| * | ||
| * Queries the organization's indexed websites and returns a formatted | ||
| * bullet list, or undefined if no websites are configured. | ||
| */ | ||
|
|
||
| import type { ToolCtx } from '@convex-dev/agent'; | ||
|
|
||
| import { internal } from '../../../_generated/api'; | ||
|
|
||
| const MAX_LISTED_WEBSITES = 15; | ||
|
|
||
| /** | ||
| * Query and format website summaries for the given organization. | ||
| * Returns a formatted string like: | ||
| * - docs.convex.dev — Convex documentation (245 pages) | ||
| * - example.com (18 pages) | ||
| * | ||
| * Returns undefined if no websites are configured. | ||
| */ | ||
| export async function formatWebsiteSummaries( | ||
| ctx: ToolCtx, | ||
| organizationId: string, | ||
| ): Promise<string | undefined> { | ||
| const websites = await ctx.runQuery( | ||
| internal.websites.internal_queries.listWebsiteSummaries, | ||
| { organizationId }, | ||
| ); | ||
|
|
||
| if (!websites || websites.length === 0) return undefined; | ||
|
|
||
| const listed = websites.slice(0, MAX_LISTED_WEBSITES); | ||
| const lines = listed.map((w) => { | ||
| const parts = [w.domain]; | ||
| if (w.title || w.description) { | ||
| parts.push(` — ${w.title ?? w.description}`); | ||
| } | ||
| if (w.pageCount != null) { | ||
| parts.push(` (${w.pageCount} pages)`); | ||
| } | ||
| return `- ${parts.join('')}`; | ||
| }); | ||
|
|
||
| if (websites.length > MAX_LISTED_WEBSITES) { | ||
| lines.push(`- ... and ${websites.length - MAX_LISTED_WEBSITES} more`); | ||
| } | ||
|
|
||
| return lines.join('\n'); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Convex Tool: Web | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Two modes: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. **URL fetch**: when the user provides a specific URL, fetch and extract | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Two modes (discriminated union on `mode`): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. **fetch**: when the user provides a specific URL, fetch and extract | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * its content directly (web pages, PDFs, images, DOCX, PPTX, etc.). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. **Semantic search**: when the user asks a question without a URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * search crawled website pages via vector embeddings. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Works with any public URL. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. **search**: search crawled website pages via semantic similarity. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Only covers websites added to the organization's knowledge base. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createTool, type ToolCtx } from '@convex-dev/agent'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -15,14 +16,8 @@ import type { ToolDefinition } from '../types'; | |||||||||||||||||||||||||||||||||||||||||||||||||
| import { fetchAndExtract } from './helpers/fetch_and_extract'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { searchPages } from './helpers/search_pages'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const URL_REGEX = /https?:\/\/[^\s"'<>]+/i; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const FILE_EXTENSIONS = /\.(pdf|docx|pptx|png|jpe?g|gif|webp|bmp|tiff?|svg)$/i; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function extractUrl(text: string): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const match = text.match(URL_REGEX); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return match ? match[0] : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function isFileUrl(url: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = new URL(url).pathname; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -32,54 +27,64 @@ function isFileUrl(url: string): boolean { | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const webToolArgs = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| query: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'The user request or question. Used as extraction instruction when fetching a URL, or as a semantic search query over crawled pages.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Explicit URL to fetch and extract content from. When provided, the tool fetches and extracts the URL content directly instead of searching.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| domain: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Optional domain to restrict search to (e.g., "docs.convex.dev"). Only applies in search mode, ignored when fetching a URL.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const webToolArgs = z.discriminatedUnion('mode', [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mode: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .literal('fetch') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Fetch and extract content from a specific URL. Works with any public URL.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'The URL to fetch (web page, PDF, DOCX, PPTX, or image such as PNG, JPG, GIF, WebP, etc.)', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| query: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Optional extraction instruction to guide what content to focus on.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mode: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .literal('search') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Search through websites added to the knowledge base using semantic similarity.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| query: z.string().describe('The search query.'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| domain: z | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Optional domain to restrict search to (e.g., "docs.convex.dev").', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export const webTool: ToolDefinition = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'web', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tool: createTool({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: `Search crawled website pages or fetch content from a specific URL. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: `Access web content in two modes: | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| **Mode 1 — Fetch URL**: When the user provides a specific URL (via the \`url\` parameter, or a URL detected in \`query\`), fetch and extract its content directly. Supports web pages, PDFs, DOCX, PPTX, and images (PNG, JPG, GIF, WebP, etc.). The \`query\` is used as the extraction instruction to guide what content to focus on. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| **fetch**: Fetch and extract content from any public URL. Supports web pages, PDFs, DOCX, PPTX, and images (PNG, JPG, GIF, WebP, etc.). Use the \`query\` parameter as an extraction instruction to guide what content to focus on. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| **Mode 2 — Search**: When no URL is provided, search through previously crawled and indexed website content using semantic similarity. Returns ranked results with page URL, title, and relevant content excerpts. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| **search**: Search through websites that have been added to the organization's knowledge base. Only content from indexed knowledge base websites is searchable — this does NOT search the open internet. If the website you need isn't indexed, use fetch mode with a direct URL instead, or suggest the user add the website to their knowledge base. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| IMPORTANT: Always cite the source URL for every piece of information you present from the results. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| EXAMPLES: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { url: "https://example.com/report.pdf", query: "Summarize the key findings" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { url: "https://example.com/pricing", query: "Extract all pricing tiers" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { query: "https://example.com/page" } — URL detected in query, fetches directly | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { query: "shipping policy" } — no URL, searches crawled pages | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { query: "product pricing details" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { query: "workflow patterns", domain: "docs.convex.dev" } — searches only docs.convex.dev`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { mode: "fetch", url: "https://example.com/report.pdf", query: "Summarize the key findings" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { mode: "fetch", url: "https://example.com/pricing" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { mode: "search", query: "shipping policy" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| - { mode: "search", query: "workflow patterns", domain: "docs.convex.dev" }`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: webToolArgs, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async (ctx: ToolCtx, args) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const targetUrl = args.url || extractUrl(args.query); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (targetUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const instruction = | ||||||||||||||||||||||||||||||||||||||||||||||||||
| args.url && isFileUrl(targetUrl) ? args.query : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (args.mode === 'fetch') { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const instruction = isFileUrl(args.url) ? args.query : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await fetchAndExtract(ctx, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: targetUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: args.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| instruction, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -101,11 +106,23 @@ EXAMPLES: | |||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .join(' | '); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const responseText = `${meta}\n\n${result.content}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const citationHeader = `[1] (Relevance: 100.0%) [Source: ${result.title ?? result.url}] [URL: ${result.url}]`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const responseText = `${citationHeader}\n${meta}\n\n${result.content}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const citations = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'web' as const, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| source: result.title ?? result.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: result.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| relevance: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+112
to
+120
Contributor
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. 🧩 Analysis chain🏁 Script executed: # First, let's look at the file and understand the context
cat -n services/platform/convex/agent_tools/web/web_tool.ts | head -150Repository: tale-project/tale Length of output: 5793 🏁 Script executed: # Check if there are type definitions for citation objects
rg -A 5 -B 5 "citations" services/platform/convex/agent_tools/web/web_tool.tsRepository: tale-project/tale Length of output: 962 🏁 Script executed: # Search for type definitions related to citations in the codebase
fd -t f "\.ts$" -path "*/agent_tools/*" -o -path "*/types/*" | xargs rg -l "citation" -i 2>/dev/null | head -20Repository: tale-project/tale Length of output: 1398 🏁 Script executed: # Look for ToolDefinition type to understand expected return structure
rg -A 10 "type ToolDefinition" services/platform/convex/agent_tools/Repository: tale-project/tale Length of output: 43 🏁 Script executed: # Check the searchPages function to see what citation type it returns
cat -n services/platform/convex/agent_tools/web/helpers/search_pages.ts | head -80Repository: tale-project/tale Length of output: 2565 🏁 Script executed: # Look for citation type definitions
cat -n services/platform/convex/openai_compat/citations.tsRepository: tale-project/tale Length of output: 5149 🏁 Script executed: # Verify the return type of the tool to ensure explicit annotation will work
rg -B 5 -A 15 "createTool" services/platform/convex/agent_tools/web/web_tool.ts | head -40Repository: tale-project/tale Length of output: 1768 🏁 Script executed: # Check if Citation interface is exported from search_pages.ts
rg "export.*Citation" services/platform/convex/agent_tools/web/helpers/search_pages.tsRepository: tale-project/tale Length of output: 43 🏁 Script executed: # Check the full exports and structure of search_pages.ts
tail -50 services/platform/convex/agent_tools/web/helpers/search_pages.tsRepository: tale-project/tale Length of output: 1499 🏁 Script executed: # Check if Citation is defined as interface in search_pages.ts and whether it's exported
grep -n "interface Citation\|export.*Citation" services/platform/convex/agent_tools/web/helpers/search_pages.tsRepository: tale-project/tale Length of output: 84 Use explicit type annotation for the The ♻️ Proposed refactor- const citations = [
- {
- index: 1,
- type: 'web' as const,
- source: result.title ?? result.url,
- url: result.url,
- relevance: 1,
- },
- ];
+ const citations: Array<{
+ index: number;
+ type: 'web';
+ source: string;
+ url: string;
+ relevance: number;
+ }> = [
+ {
+ index: 1,
+ type: 'web',
+ source: result.title ?? result.url,
+ url: result.url,
+ relevance: 1,
+ },
+ ];📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| response: responseText, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| citations, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(result.usage && { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| usage: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputTokens: result.usage.input_tokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -117,6 +134,7 @@ EXAMPLES: | |||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // mode === 'search' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { text: searchResult, citations } = await searchPages(ctx, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| query: args.query, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| domain: args.domain, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
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.
Don’t drop fetch instructions for normal web pages.
queryis documented as the extraction instruction for fetch mode, but Line 84 only forwards it when the URL looks like a file. A call like{ mode: "fetch", url: "https://example.com/pricing", query: "extract the enterprise limits" }will ignore the instruction and return an unguided extraction. Passargs.querythrough for all fetches, or narrow the fetch-mode contract to file-only instructions.🔧 Proposed fix
🤖 Prompt for AI Agents