Skip to content

Commit b3d4df4

Browse files
committed
showing multiple cached answers
1 parent 89bc53c commit b3d4df4

File tree

6 files changed

+212
-43
lines changed

6 files changed

+212
-43
lines changed

app/src/app/api/search/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export async function GET(request: NextRequest) {
77
const response = await fetch(`${SEARCH_API}${request.nextUrl.search}`);
88
const data = await response.json();
99

10-
return Response.json({ ...data });
10+
return Response.json(data);
1111
}

app/src/app/page.tsx

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,24 @@ import VideoList, { type VideoDocument } from '@/components/VideoList';
66
import Modal from '@/components/Modal';
77
import UploadButton from '@/components/UploadButton';
88
import VideoForm from '@/components/VideoForm';
9-
import Markdown from '@/components/Markdown';
109
import SettingsIcon from '@/components/SettingsIcon';
1110
import SettingsMenu from '@/components/SettingsMenu';
11+
import Accordion from '@/components/Accordion';
12+
import CircularProgress from '@/components/CircularProgress';
13+
import Markdown from '@/components/Markdown';
1214

1315
interface VideoSearchResult {
1416
videos: VideoDocument[];
1517
answer: string;
18+
question: string;
19+
isOriginal?: boolean;
1620
}
1721

1822
export default function Home() {
19-
const [results, setResults] = useState<VideoSearchResult>();
23+
const [results, setResults] = useState<VideoSearchResult[]>();
24+
const [isSearching, setIsSearching] = useState(false);
2025
const [showModal, setShowModal] = useState(false);
26+
const [currentQuestion, setCurrentQuestion] = useState<string>('');
2127
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
2228
const [selectedOption, setSelectedOption] = useState('OpenAI');
2329

@@ -32,14 +38,22 @@ export default function Home() {
3238
setShowModal(false);
3339
};
3440

35-
const handleSearch = async (question: string) => {
41+
const handleSearch = async (question: string, useCache?: boolean) => {
3642
// Replace with your API call
37-
setResults(undefined);
38-
const response = await fetch(
39-
`/api/search?api=${selectedOption.toLowerCase()}&question=${question}`,
40-
);
41-
const data: VideoSearchResult = await response.json();
43+
setCurrentQuestion(question);
44+
let url = `/api/search?api=${selectedOption.toLowerCase()}&question=${question}`;
45+
46+
if (useCache === false) {
47+
url = `${url}&useCache=false`;
48+
} else {
49+
setResults(undefined);
50+
}
51+
52+
setIsSearching(true);
53+
const response = await fetch(url);
54+
const data: VideoSearchResult[] = await response.json();
4255
setResults(data);
56+
setIsSearching(false);
4357
};
4458

4559
const handleToggleSettings = () => {
@@ -52,6 +66,12 @@ export default function Home() {
5266
setSelectedOption(event.target.value);
5367
};
5468

69+
const haveResults = Array.isArray(results) && results.length > 0;
70+
const haveOriginal =
71+
haveResults &&
72+
typeof results[0].isOriginal !== 'undefined' &&
73+
results[0].isOriginal;
74+
5575
return (
5676
<>
5777
<main className="flex min-h-screen flex-col items-center justify-between p-24">
@@ -60,11 +80,50 @@ export default function Home() {
6080
Ask me about Redis
6181
</h1>
6282
<QuestionForm onSubmit={handleSearch} />
63-
{typeof results !== 'undefined' && (
64-
<div className="py-4">
65-
<Markdown markdown={results?.answer ?? ''} />
66-
<h2>To learn more, check out these videos:</h2>
67-
<VideoList videos={results?.videos ?? []} />
83+
{Array.isArray(results) && !haveResults && !isSearching && (
84+
<p className="mt-4">No results found. Try another question.</p>
85+
)}
86+
{!haveOriginal && (
87+
<>
88+
<h2 className="text-xl font-bold mb-2 mt-4">
89+
Your question has already been asked in a different way, here
90+
are some possible answers:
91+
</h2>
92+
<p>
93+
If none of the answers below are useful, we can always generate
94+
a unique response for you.
95+
</p>
96+
<button
97+
className="mt-2 bg-blue-500 text-white p-2 rounded"
98+
onClick={() => {
99+
if (isSearching) {
100+
return;
101+
}
102+
103+
void handleSearch(currentQuestion, false);
104+
}}>
105+
{isSearching
106+
? (
107+
<CircularProgress />
108+
)
109+
: (
110+
<span>Generate unique response</span>
111+
)}
112+
</button>
113+
<div className="mt-4">
114+
<Accordion items={results ?? []} />
115+
</div>
116+
</>
117+
)}
118+
{haveOriginal && (
119+
<div className="mt-4 rounded-md shadow-lg bg-white">
120+
<div className="p-4">
121+
<Markdown markdown={results[0].answer} />
122+
<h2 className="text-xl font-bold my-4">
123+
To learn more, check out these videos:
124+
</h2>
125+
<VideoList videos={results[0].videos} />
126+
</div>
68127
</div>
69128
)}
70129
</div>

app/src/components/Accordion.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// components/Accordion.jsx
2+
3+
import React, { useState } from 'react';
4+
import VideoList, { type VideoDocument } from './VideoList';
5+
import Markdown from './Markdown';
6+
7+
interface AccordionItemProps {
8+
question: string;
9+
children: React.ReactNode;
10+
}
11+
12+
function AccordionItem({ question, children }: AccordionItemProps) {
13+
const [isOpen, setIsOpen] = useState(false);
14+
15+
return (
16+
<div className="border-b border-gray-200">
17+
<button
18+
className="py-2 px-4 w-full text-left text-black font-semibold text-lg shadow-md"
19+
onClick={() => {
20+
setIsOpen(!isOpen);
21+
}}>
22+
{question}
23+
</button>
24+
{isOpen && <div className="p-4 bg-white">{children}</div>}
25+
</div>
26+
);
27+
}
28+
29+
export interface AccordionProps {
30+
items: Array<{
31+
question: string;
32+
answer: string;
33+
videos: VideoDocument[];
34+
}>;
35+
}
36+
37+
function Accordion({ items }: AccordionProps) {
38+
return (
39+
<div className="bg-blue-100 rounded-md shadow-lg">
40+
{items.map((item, index) => (
41+
<AccordionItem key={index} question={item.question}>
42+
<Markdown markdown={item.answer} />
43+
<h2 className="text-xl font-bold my-4">
44+
To learn more, check out these videos:
45+
</h2>
46+
<VideoList videos={item.videos} />
47+
</AccordionItem>
48+
))}
49+
</div>
50+
);
51+
}
52+
53+
export default Accordion;

services/video-search/src/api/prompt.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { type ApiConfig } from './index.js';
1818
export type AnswerDocument = Document<{
1919
videos: VideoDocument[];
2020
answer: string;
21+
question?: string;
22+
isOriginal?: boolean;
2123
}>;
2224

2325
export interface Prompt {

services/video-search/src/api/search.ts

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,28 @@ import log from '../log.js';
33
import config from '../config.js';
44
import { type RedisVectorStore } from 'langchain/vectorstores/redis';
55
import { type ApiConfig } from './index.js';
6-
import { type Prompt } from './prompt.js';
6+
import { type AnswerDocument, type Prompt } from './prompt.js';
77

88
export interface SearchConfig extends Pick<ApiConfig, 'prefix'> {
99
vectorStore: RedisVectorStore;
1010
answerVectorStore: RedisVectorStore;
1111
prompt: Prompt;
1212
}
1313

14+
export interface VideoSearchOptions {
15+
useCache?: boolean;
16+
}
17+
1418
export interface Search {
1519
searchVideos: (
1620
question: string,
21+
options?: VideoSearchOptions,
1722
) => Promise<
18-
Record<string, any> | { videos: VideoDocument[]; answer: string }
23+
Array<{
24+
videos: VideoDocument[];
25+
answer: string;
26+
isOriginal?: boolean | undefined;
27+
}>
1928
>;
2029
}
2130

@@ -56,41 +65,68 @@ export default function initialize({
5665
/**
5766
* Scores will be between 0 and 1, where 0 is most accurate and 1 is least accurate
5867
*/
59-
const [result] = await answerVectorStore.similaritySearchWithScore(
68+
let results = (await answerVectorStore.similaritySearchWithScore(
6069
question,
61-
1,
62-
);
70+
config.searches.KNN,
71+
)) as Array<[AnswerDocument, number]>;
6372

64-
if (Array.isArray(result) && result.length > 0) {
65-
log.debug(`Found closest answer with score: ${String(result[1])}`, {
66-
location: `${prefix}.search.getAnswer`,
67-
score: result[1],
68-
});
73+
if (Array.isArray(results) && results.length > 0) {
74+
// Filter out results with too high similarity score
75+
results = results.filter(
76+
(result) => result[1] <= config.searches.maxSimilarityScore,
77+
);
6978

70-
if (result[1] < config.searches.maxSimilarityScore) {
71-
log.debug(`Found answer to question ${question}`, {
72-
location: `${prefix}.search.getAnswer`,
73-
});
79+
const inaccurateResults = results.filter(
80+
(result) => result[1] > config.searches.maxSimilarityScore,
81+
);
7482

75-
return result[0].metadata;
83+
if (
84+
Array.isArray(inaccurateResults) &&
85+
inaccurateResults.length > 0
86+
) {
87+
log.debug(
88+
`Rejected ${inaccurateResults.length} similar answers that have a score > ${config.searches.maxSimilarityScore}`,
89+
{
90+
location: `${prefix}.search.getAnswer`,
91+
scores: inaccurateResults.map((result) => result[1]),
92+
},
93+
);
7694
}
95+
}
96+
97+
if (Array.isArray(results) && results.length > 0) {
98+
log.debug(
99+
`Accepted ${results.length} similar answers that have a score <= ${config.searches.maxSimilarityScore}`,
100+
{
101+
location: `${prefix}.search.getAnswer`,
102+
scores: results.map((result) => result[1]),
103+
},
104+
);
77105

78-
log.debug(`Score too low for question ${question}`, {
79-
location: `${prefix}.search.getAnswer`,
80-
score: result[1],
106+
return results.map((result) => {
107+
return {
108+
...result[0].metadata,
109+
question: result[0].pageContent,
110+
isOriginal: false,
111+
};
81112
});
82113
}
83114
}
84115

85-
async function searchVideos(question: string) {
116+
async function searchVideos(
117+
question: string,
118+
{ useCache = config.searches.answerCache }: VideoSearchOptions = {},
119+
) {
86120
log.debug(`Original question: ${question}`, {
87121
location: `${prefix}.search.search`,
88122
});
89123

90-
const existingAnswer = await checkAnswerCache(question);
124+
if (useCache) {
125+
const existingAnswer = await checkAnswerCache(question);
91126

92-
if (typeof existingAnswer !== 'undefined') {
93-
return existingAnswer;
127+
if (typeof existingAnswer !== 'undefined') {
128+
return existingAnswer;
129+
}
94130
}
95131

96132
const semanticQuestion = await prompt.getSemanticQuestion(question);
@@ -115,14 +151,19 @@ export default function initialize({
115151
location: `${prefix}.search.search`,
116152
});
117153

118-
// TODO: modify the prompt to ask both questions
119154
const answerDocument = await prompt.answerQuestion(question, videos);
120155

121156
if (config.searches.answerCache) {
122157
await answerVectorStore.addDocuments([answerDocument]);
123158
}
124159

125-
return answerDocument.metadata;
160+
return [
161+
{
162+
...answerDocument.metadata,
163+
question: answerDocument.pageContent,
164+
isOriginal: true,
165+
},
166+
];
126167
}
127168

128169
return {

services/video-search/src/router.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import getApi from './api/index.js';
33
import * as transcripts from './transcripts/index.js';
44
import log from './log.js';
55
import { client } from './db.js';
6+
import { type VideoSearchOptions } from './api/search.js';
67

78
const router = express.Router();
89

@@ -42,27 +43,40 @@ router.post('/videos', async (req, res) => {
4243
});
4344

4445
router.get('/videos/search', async (req, res) => {
45-
const { question, api: useApi } = req.query as {
46+
const {
47+
question,
48+
api: useApi,
49+
useCache,
50+
} = req.query as {
4651
question: string;
4752
api: 'google' | 'openai';
53+
useCache?: 'true' | 'false';
4854
};
4955

50-
console.log(req.url);
51-
5256
try {
5357
log.debug(`Searching videos using ${useApi}`, {
5458
question,
5559
location: 'router.videos.search',
5660
});
5761
const api = getApi(useApi);
58-
const results = await api.search.searchVideos(question);
62+
const searchOptions: VideoSearchOptions = {};
63+
64+
if (typeof useCache === 'string') {
65+
if (useCache.toLowerCase() === 'true') {
66+
searchOptions.useCache = true;
67+
} else if (useCache.toLowerCase() === 'false') {
68+
searchOptions.useCache = false;
69+
}
70+
}
71+
72+
const results = await api.search.searchVideos(question, searchOptions);
5973

6074
log.info('Results found for question', {
6175
question,
6276
location: 'router.videos.search',
6377
});
6478

65-
res.status(200).json({ ...results });
79+
res.status(200).json(results);
6680
} catch (e) {
6781
log.error('Unexpected error in /videos/search', {
6882
error: {

0 commit comments

Comments
 (0)