diff --git a/app/api/sandbox/route.ts b/app/api/sandbox/route.ts index f1fb599..dbf0efc 100644 --- a/app/api/sandbox/route.ts +++ b/app/api/sandbox/route.ts @@ -4,6 +4,7 @@ import { codeBlock } from 'common-tags'; import { textModel } from '@/lib/ai'; import { URL_CONTEXT_FALLBACK_CONFIDENCE } from '@/lib/autopilot'; import { generateEmbedding, serializeEmbedding } from '@/lib/embeddings'; +import { searchWeb } from '@/lib/exa-search'; import { parseLLMJSON } from '@/lib/parse-llm-json'; import { createServerClient } from '@/lib/supabase/server'; @@ -61,11 +62,13 @@ async function runGroundedAnswerAgent({ subject, question, retrievedContext, + webContext, tonePolicy, }: { subject: string; question: string; retrievedContext: string; + webContext?: string; tonePolicy?: string | null; }) { const systemPrompt = codeBlock` @@ -102,6 +105,8 @@ async function runGroundedAnswerAgent({ Customer question: ${question} + ${webContext ? `Web search results:\n${webContext}\n` : ''} + Retrieved knowledge base sections: ${retrievedContext} @@ -147,6 +152,18 @@ export async function POST(request: Request) { const tonePolicy = org?.tone_policy ?? null; const questionText = `${subject}\n${question}`; + /* ---------------------- WEB SEARCH + VECTOR RETRIEVAL ------------------ */ + + const embeddingPromise = + datasources && datasources.length > 0 + ? generateEmbedding(questionText) + : Promise.resolve([] as number[]); + + const [webContext, embeddingResult] = await Promise.all([ + searchWeb(questionText), + embeddingPromise, + ]); + /* -------------------------- VECTOR RETRIEVAL --------------------------- */ const matchedSections: Array<{ @@ -158,17 +175,14 @@ export async function POST(request: Request) { similarity: number; }> = []; - if (datasources && datasources.length > 0) { - const embedding = await generateEmbedding(questionText); - if (embedding.length > 0) { - const { data } = await supabase.rpc('match_sections', { - embedding: serializeEmbedding(embedding), - match_threshold: 0.1, - p_organization_id: orgId, - match_count: 8, - }); - if (data) matchedSections.push(...data); - } + if (embeddingResult.length > 0) { + const { data } = await supabase.rpc('match_sections', { + embedding: serializeEmbedding(embeddingResult), + match_threshold: 0.1, + p_organization_id: orgId, + match_count: 8, + }); + if (data) matchedSections.push(...data); } // Expand context by fetching immediate neighbors (position ± 1) of each @@ -232,7 +246,7 @@ export async function POST(request: Request) { datasourceUrl: datasources?.find((d) => d.id === s.datasource_id)?.url, })); - if (!retrievedContext.trim()) { + if (!retrievedContext.trim() && !webContext.trim()) { return new Response( JSON.stringify({ html: '
No relevant information found in the knowledge base for this question.
', @@ -252,6 +266,7 @@ export async function POST(request: Request) { subject, question, retrievedContext, + webContext, tonePolicy, }); diff --git a/app/api/webhooks/reply/route.ts b/app/api/webhooks/reply/route.ts index 500c653..776cf7a 100644 --- a/app/api/webhooks/reply/route.ts +++ b/app/api/webhooks/reply/route.ts @@ -7,6 +7,7 @@ import { textModel } from '@/lib/ai'; import { URL_CONTEXT_FALLBACK_CONFIDENCE } from '@/lib/autopilot'; import { cleanBody } from '@/lib/cleanBody'; import { generateEmbedding, serializeEmbedding } from '@/lib/embeddings'; +import { searchWeb } from '@/lib/exa-search'; import { parseLLMJSON } from '@/lib/parse-llm-json'; import { createServiceClient } from '@/lib/supabase/service'; @@ -225,6 +226,7 @@ async function runGroundedAnswerAgent({ question, retrievedContext, apiContext, + webContext, conversationHistory, tonePolicy, }: { @@ -232,6 +234,7 @@ async function runGroundedAnswerAgent({ question: string; retrievedContext: string; apiContext?: string; + webContext?: string; conversationHistory?: string; tonePolicy?: string | null; }) { @@ -273,6 +276,8 @@ async function runGroundedAnswerAgent({ ${apiContext ? `Live API data:\n${apiContext}\n` : ''} + ${webContext ? `Web search results:\n${webContext}\n` : ''} + Retrieved knowledge base sections: ${retrievedContext} @@ -347,11 +352,14 @@ export async function POST(request: Request) { /* --------------------------- MCP TOOL GATHERING ------------------------- */ - const apiContext = await gatherContextViaMcp( - thread?.subject ?? '', - record.cleaned_body, - mcpServers ?? [] - ); + const [apiContext, webContext] = await Promise.all([ + gatherContextViaMcp( + thread?.subject ?? '', + record.cleaned_body, + mcpServers ?? [] + ), + searchWeb(questionText), + ]); /* -------------------------- VECTOR RETRIEVAL --------------------------- */ @@ -376,6 +384,7 @@ export async function POST(request: Request) { question: record.cleaned_body, retrievedContext, apiContext, + webContext, conversationHistory, tonePolicy: org?.tone_policy, }); diff --git a/app/org/[slug]/dashboard/page.tsx b/app/org/[slug]/dashboard/page.tsx index cf1320c..eeac0ad 100644 --- a/app/org/[slug]/dashboard/page.tsx +++ b/app/org/[slug]/dashboard/page.tsx @@ -58,6 +58,7 @@ export default async function DashboardPage({ params }: Props) { initialTonePolicy={org.tone_policy ?? null} initialMcpServers={mcpServers ?? []} workflowsCount={workflows?.length ?? 0} + webSearchEnabled={!!process.env.EXA_API_KEY} /> ); } diff --git a/app/org/[slug]/page.tsx b/app/org/[slug]/page.tsx index e647d31..cb11ad0 100644 --- a/app/org/[slug]/page.tsx +++ b/app/org/[slug]/page.tsx @@ -58,6 +58,7 @@ export default async function OrgPage({ params }: Props) { initialTonePolicy={org.tone_policy ?? null} initialMcpServers={mcpServers ?? []} workflowsCount={workflows?.length ?? 0} + webSearchEnabled={!!process.env.EXA_API_KEY} /> ); } diff --git a/components/organization/WelcomeDashboard.tsx b/components/organization/WelcomeDashboard.tsx index 7745ed7..701ed8b 100644 --- a/components/organization/WelcomeDashboard.tsx +++ b/components/organization/WelcomeDashboard.tsx @@ -54,6 +54,7 @@ interface Props { initialTonePolicy: string | null; initialMcpServers: Tables<'mcp_server'>[]; workflowsCount: number; + webSearchEnabled: boolean; } // ─── Stat chip ──────────────────────────────────────────────────────────────── @@ -211,6 +212,7 @@ export function WelcomeDashboard({ initialTonePolicy, initialMcpServers, workflowsCount, + webSearchEnabled, }: Props) { const { copied, copyToClipboard } = useCopyToClipboard(); const [, setAddDataSource] = useAddDataSource(); @@ -318,6 +320,15 @@ export function WelcomeDashboard({