From 1eaed10ea5ef3c72e7970b24a4bd8ad0ac747581 Mon Sep 17 00:00:00 2001 From: Yoshiki Miura Date: Sat, 6 Apr 2024 12:30:25 +0900 Subject: [PATCH] Initial commit --- .env.local.example | 5 + .eslintrc.json | 3 + .gitignore | 36 ++++ .vscode/settings.json | 4 + LICENSE | 201 +++++++++++++++++++++ README.md | 67 +++++++ app/action.tsx | 117 +++++++++++++ app/agents/index.tsx | 4 + app/agents/inquire.tsx | 46 +++++ app/agents/query-suggestor.tsx | 43 +++++ app/agents/researcher.tsx | 157 +++++++++++++++++ app/agents/task-manager.tsx | 19 ++ app/favicon.ico | Bin 0 -> 9817 bytes app/globals.css | 76 ++++++++ app/layout.tsx | 46 +++++ app/page.tsx | 9 + app/schema/inquiry.tsx | 18 ++ app/schema/next-action.tsx | 8 + app/schema/related.tsx | 13 ++ app/schema/search.tsx | 17 ++ bun.lockb | Bin 0 -> 229219 bytes components.json | 17 ++ components/chat-messages.tsx | 14 ++ components/chat-panel.tsx | 132 ++++++++++++++ components/chat.tsx | 13 ++ components/copilot.tsx | 186 ++++++++++++++++++++ components/empty-screen.tsx | 51 ++++++ components/followup-panel.tsx | 60 +++++++ components/footer.tsx | 42 +++++ components/header.tsx | 23 +++ components/message.tsx | 21 +++ components/mode-toggle.tsx | 40 +++++ components/search-related.tsx | 72 ++++++++ components/search-results-image.tsx | 128 ++++++++++++++ components/search-results.tsx | 70 ++++++++ components/search-skeleton.tsx | 27 +++ components/section.tsx | 71 ++++++++ components/theme-provider.tsx | 9 + components/tool-badge.tsx | 26 +++ components/ui/avatar.tsx | 50 ++++++ components/ui/badge.tsx | 36 ++++ components/ui/button.tsx | 56 ++++++ components/ui/card.tsx | 79 +++++++++ components/ui/carousel.tsx | 262 ++++++++++++++++++++++++++++ components/ui/checkbox.tsx | 30 ++++ components/ui/dialog.tsx | 122 +++++++++++++ components/ui/dropdown-menu.tsx | 200 +++++++++++++++++++++ components/ui/icons.tsx | 22 +++ components/ui/input.tsx | 25 +++ components/ui/label.tsx | 26 +++ components/ui/markdown.tsx | 9 + components/ui/separator.tsx | 31 ++++ components/ui/skeleton.tsx | 15 ++ components/ui/slider.tsx | 28 +++ components/ui/spinner.tsx | 25 +++ components/ui/switch.tsx | 29 +++ components/ui/textarea.tsx | 24 +++ components/user-message.tsx | 18 ++ lib/utils/index.ts | 6 + next.config.mjs | 4 + package.json | 48 +++++ postcss.config.mjs | 8 + prettier.config.js | 34 ++++ public/capture-240404.png | Bin 0 -> 1326362 bytes tailwind.config.ts | 84 +++++++++ tsconfig.json | 26 +++ 66 files changed, 3188 insertions(+) create mode 100644 .env.local.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/action.tsx create mode 100644 app/agents/index.tsx create mode 100644 app/agents/inquire.tsx create mode 100644 app/agents/query-suggestor.tsx create mode 100644 app/agents/researcher.tsx create mode 100644 app/agents/task-manager.tsx create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/schema/inquiry.tsx create mode 100644 app/schema/next-action.tsx create mode 100644 app/schema/related.tsx create mode 100644 app/schema/search.tsx create mode 100755 bun.lockb create mode 100644 components.json create mode 100644 components/chat-messages.tsx create mode 100644 components/chat-panel.tsx create mode 100644 components/chat.tsx create mode 100644 components/copilot.tsx create mode 100644 components/empty-screen.tsx create mode 100644 components/followup-panel.tsx create mode 100644 components/footer.tsx create mode 100644 components/header.tsx create mode 100644 components/message.tsx create mode 100644 components/mode-toggle.tsx create mode 100644 components/search-related.tsx create mode 100644 components/search-results-image.tsx create mode 100644 components/search-results.tsx create mode 100644 components/search-skeleton.tsx create mode 100644 components/section.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/tool-badge.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/icons.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/markdown.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/user-message.tsx create mode 100644 lib/utils/index.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prettier.config.js create mode 100644 public/capture-240404.png create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 00000000..b1a5dde5 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,5 @@ +# OpenAI API key retrieved here: https://platform.openai.com/api-keys +OPENAI_API_KEY= + +# Tavily API Key retrieved here: https://app.tavily.com/home +TAVILY_API_KEY= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..89d1965f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..2b92e157 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Morphic Search + +An AI-powered answer engine with a generative UI. + +![capture](/public/capture-240404.png) + +## 🔍 Overview + +- 🧱 [Stack](#-stack) +- 🚀 [Quickstart](#-quickstart) +- 🌐 [Deploy](#-deploy) + +## 🧱 Stack + +- App framework: [Next.js](https://nextjs.org/) +- Text streaming / Generative UI: [Vercel AI SDK](https://sdk.vercel.ai/docs) +- Generative Model: [OpenAI](https://openai.com/) +- Search API: [Tavily AI](https://tavily.com/) +- CSS framework: [Tailwind CSS](https://tailwindcss.com/) +- Component library: [shadcn/ui](https://ui.shadcn.com/) + +## 🚀 Quickstart + +### 1. Fork and Clone repo + +Fork the repo to your Github account, then run the following command to clone the repo: + +``` +git clone git@github.com:[YOUR_GITHUB_ACCOUNT]/morphic.git +``` + +### 2. Install dependencies + +``` +cd morphic +bun i +``` + +### 3. Fill out secrets + +``` +cp .env.local.example .env.local +``` + +Your .env.local file should look like this: + +``` +# OpenAI API key retrieved here: https://platform.openai.com/api-keys +OPENAI_API_KEY=[YOUR_OPENAI_API_KEY] + +# Tavily API Key retrieved here: https://app.tavily.com/home +TAVILY_API_KEY=[YOUR_TAVILY_API_KEY] +``` + +### 4. Run app locally + +``` +bun dev +``` + +You can now visit http://localhost:3000. + +## 🌐 Deploy + +Host your own live version of Morphic with Vercel. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmiurla%2Fmorphic&env=OPENAI_API_KEY,TAVILY_API_KEY) diff --git a/app/action.tsx b/app/action.tsx new file mode 100644 index 00000000..9428c37b --- /dev/null +++ b/app/action.tsx @@ -0,0 +1,117 @@ +import { + StreamableValue, + createAI, + createStreamableUI, + createStreamableValue, + getMutableAIState +} from 'ai/rsc' +import { ExperimentalMessage } from 'ai' +import { Spinner } from '@/components/ui/spinner' +import { Section } from '@/components/section' +import { FollowupPanel } from '@/components/followup-panel' +import { inquire, researcher, taskManager, querySuggestor } from '@/app/agents' + +async function submit(formData?: FormData, skip?: boolean) { + 'use server' + + const aiState = getMutableAIState() + const uiStream = createStreamableUI() + const isGenerating = createStreamableValue(true) + + const messages: ExperimentalMessage[] = aiState.get() as any + // Get the user input from the form data + const userInput = skip + ? `{"action": "skip"}` + : (formData?.get('input') as string) + const content = skip + ? userInput + : formData + ? JSON.stringify(Object.fromEntries(formData)) + : null + // Add the user message to the state + if (content) { + const message = { role: 'user', content } + console.log('Message content', content, aiState.get()) + messages.push(message as ExperimentalMessage) + aiState.update([...(aiState.get() as any), message]) + } + + async function processEvents() { + uiStream.update() + + let action: any = { object: { next: 'proceed' } } + // If the user skips the task, we proceed to the search + if (!skip) action = await taskManager(messages) + console.log('Next Action: ', action) + + if (action.object.next === 'inquire') { + // Generate inquiry + const inquiry = await inquire(uiStream, messages, userInput) + + uiStream.done() + isGenerating.done() + aiState.done([ + ...aiState.get(), + { role: 'assistant', content: `inquiry: ${inquiry?.inquiry}` } + ]) + return + } + + // Generate the answer + let answer = '' + const streamText = createStreamableValue() + while (answer.length === 0) { + // Search the web and generate the answer + const { fullResponse } = await researcher(uiStream, streamText, messages) + answer = fullResponse + } + + // Generate related queries + await querySuggestor(uiStream, messages) + + // Add follow-up panel + uiStream.append( +
+ +
+ ) + + isGenerating.done(false) + uiStream.done() + aiState.done([...aiState.get(), { role: 'assistant', content: answer }]) + } + + processEvents() + + return { + id: Date.now(), + isGenerating: isGenerating.value, + component: uiStream.value + } +} + +// Define the initial state of the AI. It can be any JSON object. +const initialAIState: { + role: 'user' | 'assistant' | 'system' | 'function' | 'tool' + content: string + id?: string + name?: string +}[] = [] + +// The initial UI state that the client will keep track of, which contains the message IDs and their UI nodes. +const initialUIState: { + id: number + isGenerating: StreamableValue + component: React.ReactNode +}[] = [] + +// AI is a provider you wrap your application with so you can access AI and UI state in your components. +export const AI = createAI({ + actions: { + submit + }, + // Each state can be any shape of object, but for chat applications + // it makes sense to have an array of messages. Or you may prefer something like { id: number, messages: Message[] } + initialUIState, + initialAIState +}) diff --git a/app/agents/index.tsx b/app/agents/index.tsx new file mode 100644 index 00000000..e47c0492 --- /dev/null +++ b/app/agents/index.tsx @@ -0,0 +1,4 @@ +export * from './task-manager' +export * from './inquire' +export * from './query-suggestor' +export * from './researcher' diff --git a/app/agents/inquire.tsx b/app/agents/inquire.tsx new file mode 100644 index 00000000..9da75af5 --- /dev/null +++ b/app/agents/inquire.tsx @@ -0,0 +1,46 @@ +import { openai } from 'ai/openai' +import { Copilot } from '@/components/copilot' +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { ExperimentalMessage, experimental_streamObject } from 'ai' +import { PartialInquiry, inquirySchema } from '@/app/schema/inquiry' + +export async function inquire( + uiStream: ReturnType, + messages: ExperimentalMessage[], + query?: string +) { + const objectStream = createStreamableValue() + uiStream.update() + + let finalInquiry: PartialInquiry = {} + await experimental_streamObject({ + model: openai.chat('gpt-4-turbo-preview'), + maxTokens: 2500, + system: `You are a professional web researcher tasked with deepening your understanding of the user's input through further inquiries. + Only ask additional questions if absolutely necessary after receiving an initial response from the user. + 'names' should be an array of English identifiers for the options provided. Structure your inquiry as follows: + e.g., { + "inquiry": "What specific information are you seeking about Rivian?", + "options": ["History", "Products", "Investors", "Partnerships", "Competitors"], + "names": ["history", "products", "investors", "partnerships", "competitors"], + "allowsInput": true, + "inputLabel": "If other, please specify", + "inputPlaceholder": "e.g., Specifications" + }`, + 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 +} diff --git a/app/agents/query-suggestor.tsx b/app/agents/query-suggestor.tsx new file mode 100644 index 00000000..5c56705f --- /dev/null +++ b/app/agents/query-suggestor.tsx @@ -0,0 +1,43 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { ExperimentalMessage, experimental_streamObject } from 'ai' +import { PartialRelated, relatedSchema } from '@/app/schema/related' +import { Section } from '@/components/section' +import SearchRelated from '@/components/search-related' +import { openai } from 'ai/openai' + +export async function querySuggestor( + uiStream: ReturnType, + messages: ExperimentalMessage[] +) { + const objectStream = createStreamableValue() + uiStream.append( +
+ +
+ ) + + await experimental_streamObject({ + model: openai.chat('gpt-4-turbo-preview'), + maxTokens: 2500, + system: `You are tasked as a professional web researcher to generate queries that delve deeper into the subject based on the initial query and its search results. Your goal is to formulate three related questions. + For example, given the query: "Starship's third test flight key milestones", + Your output should look like: + "{ + "related": [ + "Key milestones achieved during Starship's third test flight", + "Reason for Starship's failure during the third test flight", + "Future plans for Starship following the third test flight" + ] + }"`, + messages, + schema: relatedSchema + }) + .then(async result => { + for await (const obj of result.partialObjectStream) { + objectStream.update(obj) + } + }) + .finally(() => { + objectStream.done() + }) +} diff --git a/app/agents/researcher.tsx b/app/agents/researcher.tsx new file mode 100644 index 00000000..36c7b4a8 --- /dev/null +++ b/app/agents/researcher.tsx @@ -0,0 +1,157 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { + ExperimentalMessage, + ToolCallPart, + ToolResultPart, + experimental_streamText +} from 'ai' +import { searchSchema } from '@/app/schema/search' +import { Section } from '@/components/section' +import { openai } from 'ai/openai' +import { ToolBadge } from '@/components/tool-badge' +import { SearchSkeleton } from '@/components/search-skeleton' +import { SearchResults } from '@/components/search-results' +import { BotMessage } from '@/components/message' +import Exa from 'exa-js' +import { SearchResultsImageSection } from '@/components/search-results-image' + +export async function researcher( + uiStream: ReturnType, + streamText: ReturnType>, + messages: ExperimentalMessage[] +) { + const searchAPI: 'tavily' | 'exa' = 'tavily' + + let fullResponse = '' + const result = await experimental_streamText({ + model: openai.chat('gpt-4-turbo-preview'), + maxTokens: 2500, + system: `As a professional web researcher, you possess the ability to search the web for any information. + Utilize the search results to offer additional assistance or information as needed. + Include a key image in your response if one is relevant. Respond to the user's query accordingly. + `, + messages, + tools: { + search: { + description: 'Search the web for information', + parameters: searchSchema, + execute: async ({ + query, + max_results, + search_depth + }: { + query: string + max_results: number + search_depth: 'basic' | 'advanced' + }) => { + uiStream.update( +
+ {`${query}`} +
+ ) + + uiStream.append( +
+ +
+ ) + + const searchResult = + searchAPI === 'tavily' + ? await tavilySearch(query, max_results, search_depth) + : await exaSearch(query) + + uiStream.update( +
+ +
+ ) + uiStream.append( +
+ +
+ ) + + uiStream.append( +
+ +
+ ) + + return searchResult + } + } + } + }) + + const toolCalls: ToolCallPart[] = [] + const toolResponses: ToolResultPart[] = [] + for await (const delta of result.fullStream) { + switch (delta.type) { + case 'text-delta': + if (delta.textDelta) { + fullResponse += delta.textDelta + streamText.update(fullResponse) + } + break + case 'tool-call': + toolCalls.push(delta) + break + case 'tool-result': + toolResponses.push(delta) + break + } + } + messages.push({ + role: 'assistant', + content: [{ type: 'text', text: fullResponse }, ...toolCalls] + }) + + if (toolResponses.length > 0) { + // Add tool responses to the messages + messages.push({ role: 'tool', content: toolResponses }) + } + + return { result, fullResponse } +} + +async function tavilySearch( + query: string, + maxResults: number = 10, + searchDepth: 'basic' | 'advanced' = 'basic' +): Promise { + const apiKey = process.env.TAVILY_API_KEY + const response = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + api_key: apiKey, + query, + max_results: maxResults < 5 ? 5 : maxResults, + search_depth: searchDepth, + include_images: true, + include_answers: true + }) + }) + + if (!response.ok) { + throw new Error(`Error: ${response.status}`) + } + + const data = await response.json() + return data +} + +async function exaSearch(query: string, maxResults: number = 10): Promise { + const apiKey = process.env.EXA_API_KEY + const exa = new Exa(apiKey) + return exa.searchAndContents(query, { + highlights: true, + numResults: maxResults + }) +} diff --git a/app/agents/task-manager.tsx b/app/agents/task-manager.tsx new file mode 100644 index 00000000..4a506890 --- /dev/null +++ b/app/agents/task-manager.tsx @@ -0,0 +1,19 @@ +import { ExperimentalMessage, experimental_generateObject } from 'ai' +import { openai } from 'ai/openai' +import { nextActionSchema } from '../schema/next-action' + +// Decide whether inquiry is required for the user input +export async function taskManager(messages: ExperimentalMessage[]) { + const result = await experimental_generateObject({ + model: openai.chat('gpt-3.5-turbo'), + system: `You are a professional web researcher tasked with fully understanding the user's query, conducting web searches for the necessary information, and responding accordingly. + Your goal is to comprehend the user's input and determine the next step. You have the option to present a form and request further information from the user. + Select one of the three options based on the context: "proceed", "inquire". Use "proceed" if the provided information is sufficient for action. + Opt for "inquire" if more information can be gathered through questions, allowing for default selections and free-form input. + Make your choice wisely to fulfill your mission effectively.`, + messages, + schema: nextActionSchema + }) + + return result +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2cee9d413eb67d2c982e80e106a08281fa1e6f37 GIT binary patch literal 9817 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 LjBZz>F}VK&%)wUH literal 0 HcmV?d00001 diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 00000000..e7f71728 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 96.1%; + --card-foreground: 0 0% 45.1%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 89.8%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 14.9%; + --card-foreground: 0 0% 63.9%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 14.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 00000000..0dc3dccc --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from 'next' +import { Inter as FontSans } from 'next/font/google' +import { AI } from './action' +import './globals.css' +import { cn } from '@/lib/utils' +import { ThemeProvider } from '@/components/theme-provider' +import Header from '@/components/header' +import Footer from '@/components/footer' + +const fontSans = FontSans({ + subsets: ['latin'], + variable: '--font-sans' +}) + +export const metadata: Metadata = { + title: 'Morphic Search - AI powered answer engine', + description: 'An AI-powered answer engine with a generative UI.' +} + +export default function RootLayout({ + children +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + +
+ {children} +