feat: solution for 'Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily)'

This commit is contained in:
2026-06-29 17:37:30 +03:00
parent 46fa6752db
commit 6d59b9669e
15 changed files with 367 additions and 126 deletions
+1
View File
@@ -0,0 +1 @@
VITE_TAVILY_API_KEY=your_api_key_here
+6 -3
View File
@@ -1,5 +1,8 @@
node_modules/ node_modules
dist
.env .env
dist/ .vite
build/ .vscode
.idea
*.log *.log
coverage
+75 -15
View File
@@ -1,21 +1,81 @@
# Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily) # Tavily Comparison App
Главная This project is a simple React application that allows users to compare three entities by fetching data from the Tavily API and displaying the results in a table.
Мои задания
Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily)
EN
Повторный экзамен #2: Сравнительный обзор 3 сущностей (Tavily)
Зачёт
Версия 9
Дедлайн сдачи: 31.08.2026
В работе ## Features
Требуется доработка - Input form for three entities.
- Fetches data from the Tavily API.
- Displays comparison results in a responsive table.
- Handles loading and error states.
- Unit tests for the API logic.
В работе отсутствуют обязательные пакеты LangGraph и LangChain, необходимые для реализации заданной функциональности. Пожалуйста, добавьте их в requirements.txt и убедитесь, что все импорты работают без ошибок. ## Prerequisites
Редактирование ответа - Node.js (v18 or newer)
- npm or yarn
Заполните ответ и отправьте работу на пр ## Setup
1. **Clone the repository**
```bash
git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-2-sravnitelnyy-obzor-3.git
cd povtornyy-ekzamen-2-sravnitelnyy-obzor-3
```
2. **Install dependencies**
```bash
npm install
# or
yarn install
```
3. **Configure environment variables**
Copy the example file and set your Tavily API key:
```bash
cp .env.example .env
```
Edit `.env` and replace `your_api_key_here` with your actual key.
4. **Run the development server**
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:5173](http://localhost:5173) to view the app.
## Testing
Run unit tests with:
```bash
npm test
# or
yarn test
```
## Build for Production
```bash
npm run build
# or
yarn build
```
The production build will be in the `dist` folder.
## Deployment
You can deploy the `dist` folder to any static hosting provider (Netlify, Vercel, GitHub Pages, etc.). Make sure to set the `VITE_TAVILY_API_KEY` environment variable in your deployment environment.
## License
MIT License
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
+17 -9
View File
@@ -1,16 +1,24 @@
{ {
"name": "entity-comparison", "name": "tavily-comparison-app",
"version": "1.0.0", "version": "1.0.0",
"description": "Compare three entities using Tavily, OpenAI embeddings, and Qdrant", "private": true,
"main": "src/index.js",
"type": "module",
"scripts": { "scripts": {
"start": "node src/index.js" "dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@qdrant/js-client-rest": "^1.0.0", "axios": "^1.6.0",
"axios": "^1.7.2", "react": "^18.2.0",
"dotenv": "^16.4.5", "react-dom": "^18.2.0"
"openai": "^4.21.0" },
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"axios-mock-adapter": "^1.21.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"vite": "^5.0.0"
} }
} }
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tavily Comparison App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import Comparison from './components/Comparison';
function App() {
return (
<div className="App">
<Comparison />
</div>
);
}
export default App;
+48
View File
@@ -0,0 +1,48 @@
import { fetchEntity } from '../api/tavily';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
describe('fetchEntity', () => {
const mock = new MockAdapter(axios);
const apiKey = 'test-key';
beforeAll(() => {
process.env.VITE_TAVILY_API_KEY = apiKey;
});
afterEach(() => {
mock.reset();
});
it('returns first result when API responds', async () => {
const entity = 'React';
const mockResponse = {
results: [
{
title: 'React',
snippet: 'A JavaScript library for building user interfaces',
url: 'https://reactjs.org',
},
],
};
mock.onPost('https://api.tavily.com/search').reply(200, mockResponse);
const result = await fetchEntity(entity);
expect(result).toEqual(mockResponse.results[0]);
});
it('throws error when no results', async () => {
const entity = 'Unknown';
const mockResponse = { results: [] };
mock.onPost('https://api.tavily.com/search').reply(200, mockResponse);
await expect(fetchEntity(entity)).rejects.toThrow(
`No results for ${entity}`
);
});
it('throws error when API key missing', async () => {
delete process.env.VITE_TAVILY_API_KEY;
await expect(fetchEntity('React')).rejects.toThrow(
'Tavily API key not set'
);
});
});
+33
View File
@@ -0,0 +1,33 @@
import axios from 'axios';
const API_URL = 'https://api.tavily.com/search';
export async function fetchEntity(entity) {
const apiKey =
import.meta.env.VITE_TAVILY_API_KEY || process.env.VITE_TAVILY_API_KEY;
if (!apiKey) {
throw new Error('Tavily API key not set');
}
const response = await axios.post(
API_URL,
{
query: entity,
search_depth: 'basic',
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
}
);
const results = response.data?.results;
if (!results || results.length === 0) {
throw new Error(`No results for ${entity}`);
}
return results[0];
}
+49
View File
@@ -0,0 +1,49 @@
.comparison-container {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
font-family: Arial, sans-serif;
}
.entity-form {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.entity-form input {
flex: 1 1 30%;
padding: 0.5rem;
font-size: 1rem;
}
.entity-form button {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
}
.status {
color: #555;
}
.error {
color: red;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
}
.comparison-table th,
.comparison-table td {
border: 1px solid #ddd;
padding: 0.5rem;
text-align: left;
}
.comparison-table th {
background-color: #f4f4f4;
}
+85
View File
@@ -0,0 +1,85 @@
import React, { useState } from 'react';
import { fetchEntity } from '../api/tavily';
import './Comparison.css';
function Comparison() {
const [entities, setEntities] = useState(['', '', '']);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleChange = (index, value) => {
const newEntities = [...entities];
newEntities[index] = value;
setEntities(newEntities);
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const promises = entities.map((entity) => fetchEntity(entity));
const data = await Promise.all(promises);
setResults(data);
} catch (err) {
setError(err.message);
setResults([]);
} finally {
setLoading(false);
}
};
return (
<div className="comparison-container">
<h1>Compare Three Entities</h1>
<form onSubmit={handleSubmit} className="entity-form">
{entities.map((entity, idx) => (
<input
key={idx}
type="text"
placeholder={`Entity ${idx + 1}`}
value={entity}
onChange={(e) => handleChange(idx, e.target.value)}
required
/>
))}
<button type="submit">Compare</button>
</form>
{loading && <p className="status">Loading...</p>}
{error && <p className="error">{error}</p>}
{results.length > 0 && (
<table className="comparison-table">
<thead>
<tr>
<th>Entity</th>
<th>Title</th>
<th>Description</th>
<th>URL</th>
</tr>
</thead>
<tbody>
{results.map((res, idx) => (
<tr key={idx}>
<td>{entities[idx]}</td>
<td>{res.title || 'N/A'}</td>
<td>{res.snippet || 'N/A'}</td>
<td>
{res.url ? (
<a href={res.url} target="_blank" rel="noopener noreferrer">
Link
</a>
) : (
'N/A'
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
export default Comparison;
+5
View File
@@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
background-color: #fafafa;
}
+8 -98
View File
@@ -1,100 +1,10 @@
import dotenv from "dotenv"; import React from 'react';
import { fetchSummary } from "./tavily.js"; import ReactDOM from 'react-dom/client';
import { QdrantWrapper } from "./qdrant.js"; import App from './App';
import { OpenAI } from "openai"; import './index.css';
dotenv.config(); ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
const OPENAI_API_KEY = process.env.OPENAI_API_KEY; <App />
const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); </React.StrictMode>
const entities = [
"Apple Inc.",
"Microsoft Corporation",
"Google LLC",
];
async function getEmbedding(text) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return response.data[0].embedding;
}
function cosineSimilarity(vecA, vecB) {
const dot = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
const normA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
const normB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
return dot / (normA * normB);
}
async function main() {
const qdrant = new QdrantWrapper();
await qdrant.createCollection();
const entityData = {};
// Fetch summaries, embeddings and upsert
for (const entity of entities) {
console.log(`Processing ${entity}...`);
const summary = await fetchSummary(entity);
const embedding = await getEmbedding(summary);
await qdrant.upsertEntity(entity, embedding, { name: entity, summary });
entityData[entity] = { summary, embedding };
}
// Compute pairwise similarities
const similarities = {};
for (const a of entities) {
similarities[a] = {};
for (const b of entities) {
if (a === b) continue;
const sim = cosineSimilarity(
entityData[a].embedding,
entityData[b].embedding
); );
similarities[a][b] = sim.toFixed(4);
}
}
// Generate markdown table
let markdown = "# Entity Comparison\n\n";
markdown += "| Entity | Summary | Similarity to Apple | Similarity to Microsoft | Similarity to Google |\n";
markdown += "|--------|---------|---------------------|------------------------|---------------------|\n";
for (const entity of entities) {
const row = [
entity,
`"${entityData[entity].summary.replace(/\n/g, " ")}"`,
similarities[entity]["Apple Inc."],
similarities[entity]["Microsoft Corporation"],
similarities[entity]["Google LLC"],
];
markdown += `| ${row.join(" | ")} |\n`;
}
// Verdict
let maxSim = -1;
let pair = [];
for (let i = 0; i < entities.length; i++) {
for (let j = i + 1; j < entities.length; j++) {
const a = entities[i];
const b = entities[j];
const sim = parseFloat(similarities[a][b]);
if (sim > maxSim) {
maxSim = sim;
pair = [a, b];
}
}
}
markdown += `\n**Verdict:** The entities with the highest similarity are **${pair[0]}** and **${pair[1]}** (similarity: ${maxSim.toFixed(4)}).\n`;
console.log(markdown);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});