diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 0c041351..03ab3f94 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -266,7 +266,7 @@ function ChatInputInner({ return () => window.removeEventListener('models-config-changed', handler) }, [loadModelConfig]) - // Check search tool availability (brave or exa, or signed-in via gateway) + // Check search tool availability (exa or signed-in via gateway) useEffect(() => { const checkSearch = async () => { if (isRowboatConnected) { @@ -275,17 +275,10 @@ function ChatInputInner({ } let available = false try { - const raw = await window.ipc.invoke('workspace:readFile', { path: 'config/brave-search.json' }) + const raw = await window.ipc.invoke('workspace:readFile', { path: 'config/exa-search.json' }) const config = JSON.parse(raw.data) if (config.apiKey) available = true } catch { /* not configured */ } - if (!available) { - try { - const raw = await window.ipc.invoke('workspace:readFile', { path: 'config/exa-search.json' }) - const config = JSON.parse(raw.data) - if (config.apiKey) available = true - } catch { /* not configured */ } - } setSearchAvailable(available) } checkSearch() diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts index 256de6d8..d92c124d 100644 --- a/apps/x/apps/renderer/src/lib/chat-conversation.ts +++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts @@ -115,35 +115,27 @@ export type WebSearchCardData = { export const getWebSearchCardData = (tool: ToolCall): WebSearchCardData | null => { if (tool.name === 'web-search') { - const input = normalizeToolInput(tool.input) as Record | undefined - const result = tool.result as Record | undefined - return { - query: (input?.query as string) || '', - results: (result?.results as WebSearchCardResult[]) || [], - } - } - - if (tool.name === 'research-search') { const input = normalizeToolInput(tool.input) as Record | undefined const result = tool.result as Record | undefined const rawResults = (result?.results as Array<{ title: string url: string + description?: string highlights?: string[] text?: string }>) || [] const mapped = rawResults.map((entry) => ({ title: entry.title, url: entry.url, - description: entry.highlights?.[0] || (entry.text ? entry.text.slice(0, 200) : ''), + description: entry.description || entry.highlights?.[0] || (entry.text ? entry.text.slice(0, 200) : ''), })) const category = input?.category as string | undefined return { query: (input?.query as string) || '', results: mapped, - title: category - ? `${category.charAt(0).toUpperCase() + category.slice(1)} search` - : 'Researched the web', + title: (!category || category === 'general') + ? 'Web search' + : `${category.charAt(0).toUpperCase() + category.slice(1)} search`, } } diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index b7ebdfdf..b597a8d6 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -1059,7 +1059,7 @@ export async function* streamAgent({ } if (searchEnabled) { loopLogger.log('search enabled, injecting search prompt'); - instructionsWithDateTime += `\n\n# Search\nThe user has requested a search. Load the search skill and use web search or research search as needed to answer their query.`; + instructionsWithDateTime += `\n\n# Search\nThe user has requested a search. Use the web-search tool to answer their query.`; } let streamError: string | null = null; for await (const event of streamLlm( diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index d39a0d63..a29b225f 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -209,7 +209,7 @@ ${runtimeContextPrompt} - \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution - \`loadSkill\` - Skill loading - \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them. -- \`web-search\` and \`research-search\` - Web and research search tools (available when configured). **You MUST load the \`web-search\` skill before using either of these tools.** It tells you which tool to pick and how many searches to do. +- \`web-search\` - Search the web. Returns rich results with full text, highlights, and metadata. The \`category\` parameter defaults to \`general\` (full web search) — only use a specific category like \`news\`, \`company\`, \`research paper\` etc. when the query is clearly about that type. For everyday queries (weather, restaurants, prices, how-to), use \`general\`. - \`app-navigation\` - Control the app UI: open notes, switch views, filter/search the knowledge base, manage saved views. **Load the \`app-navigation\` skill before using this tool.** - \`save-to-memory\` - Save observations about the user to the agent memory system. Use this proactively during conversations. diff --git a/apps/x/packages/core/src/application/assistant/skills/index.ts b/apps/x/packages/core/src/application/assistant/skills/index.ts index f0b9186f..44774d6e 100644 --- a/apps/x/packages/core/src/application/assistant/skills/index.ts +++ b/apps/x/packages/core/src/application/assistant/skills/index.ts @@ -10,7 +10,7 @@ import organizeFilesSkill from "./organize-files/skill.js"; import slackSkill from "./slack/skill.js"; import backgroundAgentsSkill from "./background-agents/skill.js"; import createPresentationsSkill from "./create-presentations/skill.js"; -import webSearchSkill from "./web-search/skill.js"; + import appNavigationSkill from "./app-navigation/skill.js"; const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)); @@ -84,12 +84,6 @@ const definitions: SkillDefinition[] = [ summary: "Discovering, executing, and integrating MCP tools. Use this to check what external capabilities are available and execute MCP tools on behalf of users.", content: mcpIntegrationSkill, }, - { - id: "web-search", - title: "Web Search", - summary: "Searching the web or researching a topic. Guidance on when to use web-search vs research-search, and how many searches to do.", - content: webSearchSkill, - }, { id: "deletion-guardrails", title: "Deletion Guardrails", diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 5275aaa9..069cf7ef 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -1026,123 +1026,15 @@ export const BuiltinTools: z.infer = { }, // ============================================================================ - // Web Search (Brave Search API) + // Web Search (Exa Search API) // ============================================================================ 'web-search': { - description: 'Search the web using Brave Search. Returns web results with titles, URLs, and descriptions.', - inputSchema: z.object({ - query: z.string().describe('The search query'), - count: z.number().optional().describe('Number of results to return (default: 5, max: 20)'), - freshness: z.string().optional().describe('Filter by freshness: pd (past day), pw (past week), pm (past month), py (past year)'), - }), - isAvailable: async () => { - if (await isSignedIn()) return true; - try { - const braveConfigPath = path.join(WorkDir, 'config', 'brave-search.json'); - const raw = await fs.readFile(braveConfigPath, 'utf8'); - const config = JSON.parse(raw); - return !!config.apiKey; - } catch { - return false; - } - }, - execute: async ({ query, count, freshness }: { query: string; count?: number; freshness?: string }) => { - try { - const resultCount = Math.min(Math.max(count || 5, 1), 20); - const params = new URLSearchParams({ - q: query, - count: String(resultCount), - }); - if (freshness) { - params.set('freshness', freshness); - } - - let response: Response; - - if (await isSignedIn()) { - // Use proxy - const accessToken = await getAccessToken(); - response = await fetch(`${API_URL}/v1/search/brave?${params.toString()}`, { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Accept': 'application/json', - }, - }); - } else { - // Read API key from config - const braveConfigPath = path.join(WorkDir, 'config', 'brave-search.json'); - - let apiKey: string; - try { - const raw = await fs.readFile(braveConfigPath, 'utf8'); - const config = JSON.parse(raw); - apiKey = config.apiKey; - } catch { - return { - success: false, - error: 'Brave Search API key not configured. Create ~/.rowboat/config/brave-search.json with { "apiKey": "" }', - }; - } - - if (!apiKey) { - return { - success: false, - error: 'Brave Search API key is empty. Set "apiKey" in ~/.rowboat/config/brave-search.json', - }; - } - - response = await fetch(`https://api.search.brave.com/res/v1/web/search?${params.toString()}`, { - headers: { - 'X-Subscription-Token': apiKey, - 'Accept': 'application/json', - }, - }); - } - - if (!response.ok) { - const body = await response.text(); - return { - success: false, - error: `Brave Search API error (${response.status}): ${body}`, - }; - } - - const data = await response.json() as { - web?: { results?: Array<{ title?: string; url?: string; description?: string }> }; - }; - - const results = (data.web?.results || []).map((r: { title?: string; url?: string; description?: string }) => ({ - title: r.title || '', - url: r.url || '', - description: r.description || '', - })); - - return { - success: true, - query, - results, - count: results.length, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }, - }, - - // ============================================================================ - // Research Search (Exa Search API) - // ============================================================================ - - 'research-search': { - description: 'Use this for finding articles, blog posts, papers, companies, people, or exploring a topic in depth. Best for discovery and research where you need quality sources, not a quick fact.', + description: 'Search the web for articles, blog posts, papers, companies, people, news, or explore a topic in depth. Returns rich results with full text, highlights, and metadata.', inputSchema: z.object({ query: z.string().describe('The search query'), numResults: z.number().optional().describe('Number of results to return (default: 5, max: 20)'), - category: z.enum(['company', 'research paper', 'news', 'tweet', 'personal site', 'financial report', 'people']).optional().describe('Filter results by category'), + category: z.enum(['general', 'company', 'research paper', 'news', 'tweet', 'personal site', 'financial report', 'people']).optional().describe('Search category. Defaults to "general" which searches the entire web. Only use a specific category when the query is clearly about that type (e.g. "research paper" for academic papers, "company" for company info). For everyday queries like weather, restaurants, prices, how-to, etc., use "general" or omit entirely.'), }), isAvailable: async () => { if (await isSignedIn()) return true; @@ -1168,7 +1060,7 @@ export const BuiltinTools: z.infer = { highlights: true, }, }; - if (category) { + if (category && category !== 'general') { reqBody.category = category; }