Skip to content

Commit

Permalink
web and video search tools including Tavily, Exa, and SearXNG integra…
Browse files Browse the repository at this point in the history
…tions for web search, and YouTube video search. Also, implemented PDF processing for converting PDF contents into structured WordPress posts while preserving hyperlinks.

Took 47 seconds
  • Loading branch information
mikepsinn committed Nov 18, 2024
1 parent 1d65047 commit 141e040
Show file tree
Hide file tree
Showing 18 changed files with 1,779 additions and 253 deletions.
28 changes: 16 additions & 12 deletions lib/agents/fdai/fdaiMetaAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { z } from "zod";
import { anthropic } from "@ai-sdk/anthropic";
import { generateObject } from "ai";
import Exa from 'exa-js';
import {getModel} from "@/lib/utils/modelUtils";
import { generateMetaAnalysisQuery } from '@/lib/meta-analysis/metaAnalysisQueries';

const exa = new Exa(process.env.EXA_API_KEY);

Expand Down Expand Up @@ -56,24 +57,24 @@ const MetaAnalysisReportSchema = z.object({
effectivenessComparison: z.array(z.object({
intervention: z.string().describe('Name of the compared intervention'),
relativeEffectiveness: z.string().describe('Relative effectiveness compared to the main drug'),
dalysAvoided: z.number().optional().describe('Estimated Disability-Adjusted Life Years (DALYs) avoided per patient'),
qalysIncreased: z.number().optional().describe('Estimated Quality-Adjusted Life Years (QALYs) increased per patient'),
numberNeededToHarm: z.number().optional().describe('Number Needed to Harm (NNH) to cause a negative health outcome'),
numberNeededToTreat: z.number().optional().describe('Number Needed to Treat (NNT) to achieve a positive health outcome'),
dalysAvoided: z.union([z.number(), z.string()]).optional().describe('Estimated Disability-Adjusted Life Years (DALYs) avoided per patient'),
qalysIncreased: z.union([z.number(), z.string()]).optional().describe('Estimated Quality-Adjusted Life Years (QALYs) increased per patient'),
numberNeededToHarm: z.union([z.number(), z.string()]).optional().describe('Number Needed to Harm (NNH) to cause a negative health outcome'),
numberNeededToTreat: z.union([z.number(), z.string()]).optional().describe('Number Needed to Treat (NNT) to achieve a positive health outcome'),
})).describe('Comparison of effectiveness with other treatments'),
dalysAvoided: z.number().optional().describe('Estimated Disability-Adjusted Life Years (DALYs) avoided per patient'),
qalysIncreased: z.number().optional().describe('Estimated Quality-Adjusted Life Years (QALYs) increased per patient'),
numberNeededToHarm: z.number().optional().describe('Number Needed to Harm (NNH) to cause a negative health outcome'),
numberOfPatients: z.number().optional().describe('Number of patients globally who would benefit from the drug'),
numberNeededToTreat: z.number().optional().describe('Number Needed to Treat (NNT) to achieve a positive health outcome'),
dalysAvoided: z.union([z.number(), z.string()]).optional().describe('Estimated Disability-Adjusted Life Years (DALYs) avoided per patient'),
qalysIncreased: z.union([z.number(), z.string()]).optional().describe('Estimated Quality-Adjusted Life Years (QALYs) increased per patient'),
numberNeededToHarm: z.union([z.number(), z.string()]).optional().describe('Number Needed to Harm (NNH) to cause a negative health outcome'),
numberOfPatients: z.union([z.number(), z.string()]).optional().describe('Number of patients globally who would benefit from the drug'),
numberNeededToTreat: z.union([z.number(), z.string()]).optional().describe('Number Needed to Treat (NNT) to achieve a positive health outcome'),
//referenceSources: z.array(articleSchema).describe('Sources of information used in compiling this report')
});

export type MetaAnalysisReportType = z.infer<typeof MetaAnalysisReportSchema>;


async function getWebResults(drugName: string, conditionName: string, numResults: number = 10): Promise<Article[]> {
const query = `${drugName} for ${conditionName} meta-analysis safety efficacy "clinical trials" "systematic review"`;
const query = generateMetaAnalysisQuery(drugName, conditionName);
const searchResponse = await exa.searchAndContents(query, {
numResults,
useAutoprompt: false,
Expand Down Expand Up @@ -111,8 +112,11 @@ export async function doMetaAnalysis(drugName: string, conditionName: string): P
Web search results:
${webResultsText}`;

//const model = getModel("gemini-1.5-flash")
const model = getModel()

const result = await generateObject({
model: anthropic('claude-3-5-sonnet-20240620'),
model,
schema: MetaAnalysisReportSchema,
prompt,
});
Expand Down
4 changes: 4 additions & 0 deletions lib/agents/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './task-manager'
export * from './inquire'
export * from './query-suggestor'
export * from './researcher'
70 changes: 70 additions & 0 deletions lib/agents/inquire.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Copilot } from '@/app/search/components/copilot'
import { createStreamableUI, createStreamableValue } from 'ai/rsc'
import { CoreMessage, streamObject } from 'ai'
import { PartialInquiry, inquirySchema } from '@/lib/schema/inquiry'
import { getModel } from '../utils/index'

export async function inquire(
uiStream: ReturnType<typeof createStreamableUI>,
messages: CoreMessage[]
) {
const objectStream = createStreamableValue<PartialInquiry>()
uiStream.update(<Copilot inquiry={objectStream.value} />)

let finalInquiry: PartialInquiry = {}
await streamObject({
model: getModel(),
system: `As a professional web researcher, your role is to deepen your understanding of the user's input by conducting further inquiries when necessary.
After receiving an initial response from the user, carefully assess whether additional questions are absolutely essential to provide a comprehensive and accurate answer. Only proceed with further inquiries if the available information is insufficient or ambiguous.
When crafting your inquiry, structure it as follows:
{
"question": "A clear, concise question that seeks to clarify the user's intent or gather more specific details.",
"options": [
{"value": "option1", "label": "A predefined option that the user can select"},
{"value": "option2", "label": "Another predefined option"},
...
],
"allowsInput": true/false, // Indicates whether the user can provide a free-form input
"inputLabel": "A label for the free-form input field, if allowed",
"inputPlaceholder": "A placeholder text to guide the user's free-form input"
}
Important: The "value" field in the options must always be in English, regardless of the user's language.
For example:
{
"question": "What specific information are you seeking about Rivian?",
"options": [
{"value": "history", "label": "History"},
{"value": "products", "label": "Products"},
{"value": "investors", "label": "Investors"},
{"value": "partnerships", "label": "Partnerships"},
{"value": "competitors", "label": "Competitors"}
],
"allowsInput": true,
"inputLabel": "If other, please specify",
"inputPlaceholder": "e.g., Specifications"
}
By providing predefined options, you guide the user towards the most relevant aspects of their query, while the free-form input allows them to provide additional context or specific details not covered by the options.
Remember, your goal is to gather the necessary information to deliver a thorough and accurate response.
Please match the language of the response (question, labels, inputLabel, and inputPlaceholder) to the user's language, but keep the "value" field in English.
`,
messages,
schema: inquirySchema
})
.then(async result => {
for await (const obj of result.partialObjectStream) {
if (obj) {
objectStream.update(obj)
finalInquiry = obj
}
}
})
.finally(() => {
objectStream.done()
})

return finalInquiry
}
70 changes: 70 additions & 0 deletions lib/agents/pdfDownloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fetch from 'node-fetch';
import fs from 'fs/promises';
import path from 'path';

export class PDFDownloader {
private downloadDir: string;

constructor(downloadDir: string) {
this.downloadDir = downloadDir;
}

private async ensureDirectoryExists(): Promise<void> {
try {
await fs.access(this.downloadDir);
} catch {
await fs.mkdir(this.downloadDir, { recursive: true });
}
}

private async downloadPDF(pdfUrl: string): Promise<void> {
const response = await fetch(pdfUrl);
if (!response.ok) {
throw new Error(`Failed to download PDF: ${response.statusText}`);
}

const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

// Extract filename from URL or use a fallback
const urlParts = new URL(pdfUrl);
const fileName = path.basename(urlParts.pathname) || 'document.pdf';
const filePath = path.join(this.downloadDir, fileName);

await fs.writeFile(filePath, buffer);
console.log(`Downloaded: ${fileName}`);
}

public async downloadPDFs(pdfUrls: string[]): Promise<{
downloaded: number;
errors: string[];
}> {
const results = {
downloaded: 0,
errors: [] as string[]
};

try {
await this.ensureDirectoryExists();

console.log(`Starting download of ${pdfUrls.length} PDFs...`);

for (const pdfUrl of pdfUrls) {
try {
await this.downloadPDF(pdfUrl);
results.downloaded++;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.errors.push(`Error downloading ${pdfUrl}: ${errorMessage}`);
console.error(`Failed to download ${pdfUrl}: ${errorMessage}`);
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.errors.push(`Error in download process: ${errorMessage}`);
console.error(`Error in download process: ${errorMessage}`);
}

return results;
}
}
Loading

0 comments on commit 141e040

Please sign in to comment.