Skip to content
Draft
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
39 changes: 27 additions & 12 deletions app/api/sandbox/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -102,6 +105,8 @@ async function runGroundedAnswerAgent({
Customer question:
${question}

${webContext ? `Web search results:\n${webContext}\n` : ''}

Retrieved knowledge base sections:
${retrievedContext}

Expand Down Expand Up @@ -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<{
Expand All @@ -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
Expand Down Expand Up @@ -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: '<p>No relevant information found in the knowledge base for this question.</p>',
Expand All @@ -252,6 +266,7 @@ export async function POST(request: Request) {
subject,
question,
retrievedContext,
webContext,
tonePolicy,
});

Expand Down
19 changes: 14 additions & 5 deletions app/api/webhooks/reply/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -225,13 +226,15 @@ async function runGroundedAnswerAgent({
question,
retrievedContext,
apiContext,
webContext,
conversationHistory,
tonePolicy,
}: {
subject: string;
question: string;
retrievedContext: string;
apiContext?: string;
webContext?: string;
conversationHistory?: string;
tonePolicy?: string | null;
}) {
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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 --------------------------- */

Expand All @@ -376,6 +384,7 @@ export async function POST(request: Request) {
question: record.cleaned_body,
retrievedContext,
apiContext,
webContext,
conversationHistory,
tonePolicy: org?.tone_policy,
});
Expand Down
1 change: 1 addition & 0 deletions app/org/[slug]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
);
}
1 change: 1 addition & 0 deletions app/org/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
);
}
11 changes: 11 additions & 0 deletions components/organization/WelcomeDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface Props {
initialTonePolicy: string | null;
initialMcpServers: Tables<'mcp_server'>[];
workflowsCount: number;
webSearchEnabled: boolean;
}

// ─── Stat chip ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -211,6 +212,7 @@ export function WelcomeDashboard({
initialTonePolicy,
initialMcpServers,
workflowsCount,
webSearchEnabled,
}: Props) {
const { copied, copyToClipboard } = useCopyToClipboard();
const [, setAddDataSource] = useAddDataSource();
Expand Down Expand Up @@ -318,6 +320,15 @@ export function WelcomeDashboard({
<StatChip value={sources.length} label="Data sources" />
<StatChip value={mcpServers.length} label="MCP servers" />
<StatChip value={workflowsCount} label="Workflows" />
<div className="group relative flex items-center gap-3 border border-[#FF4500]/20 bg-muted/50 px-4 py-3 transition-all duration-200 hover:border-[#FF4500]/60 hover:bg-[#FF4500]/5 overflow-hidden">
<span className="absolute left-0 top-0 h-full w-0.5 bg-[#FF4500] scale-y-0 origin-bottom transition-transform duration-200 group-hover:scale-y-100" />
<span className={cn(
'font-mono text-xs uppercase tracking-[0.2em]',
webSearchEnabled ? 'text-emerald-500' : 'text-muted-foreground'
)}>
🌐 Web Search {webSearchEnabled ? 'On' : 'Off'}
</span>
</div>
</div>
</div>

Expand Down
1 change: 1 addition & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ SUPABASE_SERVICE_KEY=
NEXT_PUBLIC_BASE_URL=http://localhost:3000
RESEND_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=
EXA_API_KEY=
48 changes: 48 additions & 0 deletions lib/exa-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Exa from 'exa-js';

/** Maximum number of Exa search results to include as context. */
const EXA_MAX_RESULTS = 3;

/** Maximum characters to include from each result's text. */
const EXA_MAX_TEXT_CHARS = 1500;

function formatSearchResult(
r: { title?: string | null; url?: string | null; text?: string | null },
index: number
): string {
const title = r.title ? `[${index + 1}] ${r.title}` : `[${index + 1}]`;
const url = r.url ? `Source: ${r.url}` : '';
const text = r.text?.trim() ?? '';
return [title, url, text].filter(Boolean).join('\n');
}

/**
* Perform an Exa neural web search and return a plain-text summary of the
* top results suitable for inclusion in the grounded answer agent prompt.
*
* Returns an empty string when EXA_API_KEY is not set or the search fails.
*/
export async function searchWeb(query: string): Promise<string> {
const apiKey = process.env.EXA_API_KEY;
if (!apiKey) return '';

try {
const exa = new Exa(apiKey);

const result = await exa.searchAndContents(query, {
numResults: EXA_MAX_RESULTS,
type: 'neural',
useAutoprompt: true,
text: { maxCharacters: EXA_MAX_TEXT_CHARS },
});

if (!result.results?.length) return '';

return result.results
.map((r, i) => formatSearchResult(r, i))
.join('\n\n---\n\n');
} catch (err) {
console.error('[exa-search] Web search failed:', err);
return '';
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"clsx": "^2.1.1",
"common-tags": "^1.8.2",
"date-fns": "^3.6.0",
"exa-js": "^2.7.0",
"geist": "^1.7.0",
"isomorphic-dompurify": "3.0.0-rc.2",
"jotai": "^2.18.0",
Expand Down