diff --git a/.env.local.example b/.env.local.example index ce0b399d7..c4eb0e8e2 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,76 +1,84 @@ -# Required -# The settings below are essential for the basic functionality of the system. +############################################################################### +# Required Configuration +# These settings are essential for the basic functionality of the system. +############################################################################### # 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] +# Search Configuration +TAVILY_API_KEY=[YOUR_TAVILY_API_KEY] # Get your API key at: https://app.tavily.com/home -# Redis Configuration -USE_LOCAL_REDIS=false -LOCAL_REDIS_URL=redis://localhost:6379 # or redis://redis:6379 if you're using docker compose +############################################################################### +# Optional Features +# Enable these features by uncommenting and configuring the settings below +############################################################################### -# Upstash Redis URL and Token retrieved here: https://console.upstash.com/redis -UPSTASH_REDIS_REST_URL=[YOUR_UPSTASH_REDIS_REST_URL] -UPSTASH_REDIS_REST_TOKEN=[YOUR_UPSTASH_REDIS_REST_TOKEN] +#------------------------------------------------------------------------------ +# Chat History Storage +# Enable persistent chat history across sessions +#------------------------------------------------------------------------------ +# NEXT_PUBLIC_ENABLE_SAVE_CHAT_HISTORY=true # enable chat history storage -# SearXNG Configuration -SEARXNG_API_URL=http://localhost:8080 # Replace with your local SearXNG API URL or docker http://searxng:8080 -SEARCH_API=tavily # use searxng, tavily or exa -SEARXNG_SECRET="" # generate a secret key e.g. openssl rand -base64 32 -SEARXNG_PORT=8080 # default port -SEARXNG_BIND_ADDRESS=0.0.0.0 # default address -SEARXNG_IMAGE_PROXY=true # enable image proxy -SEARXNG_LIMITER=false # can be enabled to limit the number of requests per IP address -SEARXNG_DEFAULT_DEPTH=basic # Set to 'basic' or 'advanced', only affects SearXNG searches -SEARXNG_MAX_RESULTS=50 # Maximum number of results to return from SearXNG -SEARXNG_ENGINES=google,bing,duckduckgo,wikipedia # Search engines to use -SEARXNG_TIME_RANGE=None # Time range for search results: day, week, month, year, or None (for all time) -SEARXNG_SAFESEARCH=0 # Safe search setting: 0 (off), 1 (moderate), 2 (strict) +# Redis Configuration (Required if NEXT_PUBLIC_ENABLE_SAVE_CHAT_HISTORY=true) +# Choose between local Redis or Upstash Redis +# OPTION 1: Local Redis +# USE_LOCAL_REDIS=false # use local Redis +# LOCAL_REDIS_URL=redis://localhost:6379 # local Redis URL (if USE_LOCAL_REDIS=true) -#NEXT_PUBLIC_BASE_URL=http://localhost:3000 - -# Optional -# The settings below can be used optionally as needed. +# OPTION 2: Upstash Redis (Recommended for production) +# UPSTASH_REDIS_REST_URL=[YOUR_UPSTASH_REDIS_REST_URL] # Upstash Redis REST URL (if USE_LOCAL_REDIS=false) +# UPSTASH_REDIS_REST_TOKEN=[YOUR_UPSTASH_REDIS_REST_TOKEN] # Upstash Redis REST Token (if USE_LOCAL_REDIS=false) +#------------------------------------------------------------------------------ # Additional AI Providers -# Uncomment and configure the API keys below to enable other AI providers. -# Each provider requires its own API key and configuration. - -# Google Generative AI API key retrieved here: https://aistudio.google.com/app/apikey +# Enable alternative AI models by configuring these providers +#------------------------------------------------------------------------------ +# Google Generative AI # GOOGLE_GENERATIVE_AI_API_KEY=[YOUR_GOOGLE_GENERATIVE_AI_API_KEY] -# Anthropic API key retrieved here: https://console.anthropic.com/settings/keys +# Anthropic (Claude) # ANTHROPIC_API_KEY=[YOUR_ANTHROPIC_API_KEY] -# Groq API key retrieved here: https://console.groq.com/keys +# Groq # GROQ_API_KEY=[YOUR_GROQ_API_KEY] -# If you want to use Ollama, set the base URL here. +# Ollama (Local AI) # OLLAMA_BASE_URL=http://localhost:11434 -# Azure OpenAI API key retrieved here: https://oai.azure.com/resource/deployments/ +# Azure OpenAI # AZURE_API_KEY= -# The resource name is used in the assembled URL: https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}. # AZURE_RESOURCE_NAME= -# The deployment name is the name of the Azure OpenAI deployment you want to use. # NEXT_PUBLIC_AZURE_DEPLOYMENT_NAME= - # OpenAI Compatible Model -# Note: Only models with tool-use capabilities can be used -# NEXT_PUBLIC_OPENAI_COMPATIBLE_MODEL=[YOUR_OPENAI_COMPATIBLE_MODEL] -# OPENAI_COMPATIBLE_API_KEY=[YOUR_OPENAI_COMPATIBLE_API_KEY] -# OPENAI_COMPATIBLE_API_BASE_URL=[YOUR_OPENAI_COMPATIBLE_API_BASE_URL] - -# enable the share feature -# If you enable this feature, separate account management implementation is required. -# ENABLE_SHARE=true - -# enable the video search tool -# Serper API Key retrieved here: https://serper.dev/api-key -# SERPER_API_KEY=[YOUR_SERPER_API_KEY] - -# If you want to use Jina instead of Tavily for retrieve tool, enable the following variables. -# JINA_API_KEY=[YOUR_JINA_API_KEY] \ No newline at end of file +# NEXT_PUBLIC_OPENAI_COMPATIBLE_MODEL= +# OPENAI_COMPATIBLE_API_KEY= +# OPENAI_COMPATIBLE_API_BASE_URL= + +#------------------------------------------------------------------------------ +# Alternative Search Providers +# Configure different search backends (default: Tavily) +#------------------------------------------------------------------------------ +# SEARCH_API=searxng # Available options: tavily, searxng, exa + +# SearXNG Configuration (Required if SEARCH_API=searxng) +# SEARXNG_API_URL=http://localhost:8080 # Replace with your local SearXNG API URL or docker http://searxng:8080 +# SEARXNG_SECRET="" # generate a secret key e.g. openssl rand -base64 32 +# SEARXNG_PORT=8080 +# SEARXNG_BIND_ADDRESS=0.0.0.0 +# SEARXNG_IMAGE_PROXY=true +# SEARXNG_LIMITER=false +# SEARXNG_DEFAULT_DEPTH=basic +# SEARXNG_MAX_RESULTS=50 +# SEARXNG_ENGINES=google,bing,duckduckgo,wikipedia +# SEARXNG_TIME_RANGE=None +# SEARXNG_SAFESEARCH=0 + +#------------------------------------------------------------------------------ +# Additional Features +# Enable extra functionality as needed +#------------------------------------------------------------------------------ +# NEXT_PUBLIC_ENABLE_SHARE=true # Enable sharing of chat conversations +# SERPER_API_KEY=[YOUR_SERPER_API_KEY] # Enable video search capability +# JINA_API_KEY=[YOUR_JINA_API_KEY] # Alternative to Tavily for retrieve tool \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d292e786f..fab9bb987 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "cSpell.words": [ - "openai", - "Tavily" - ] -} \ No newline at end of file + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "cSpell.words": ["openai", "Tavily"], + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..cc13406eb --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,64 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..c70c87a7e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing to Morphic + +Thank you for your interest in contributing to Morphic! This document provides guidelines and instructions for contributing. + +## Code of Conduct + +By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md). + +## How to Contribute + +### Reporting Issues + +- Check if the issue already exists in our [GitHub Issues](https://github.com/miurla/morphic/issues) +- Use the issue templates when creating a new issue +- Provide as much context as possible + +### Pull Requests + +1. Fork the repository +2. Create a new branch from `main`: + ```bash + git checkout -b feat/your-feature-name + ``` +3. Make your changes +4. Commit your changes using conventional commits: + ```bash + git commit -m "feat: add new feature" + ``` +5. Push to your fork +6. Open a Pull Request + +### Commit Convention + +We use conventional commits. Examples: + +- `feat: add new feature` +- `fix: resolve issue with X` +- `docs: update README` +- `chore: update dependencies` +- `refactor: improve code structure` + +### Development Setup + +Follow the [Quickstart](README.md#-quickstart) guide in the README to set up your development environment. + +## License + +By contributing, you agree that your contributions will be licensed under the Apache-2.0 License. diff --git a/README.md b/README.md index 72f064ba7..a4f60fe24 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,7 @@ An AI-powered search engine with a generative UI. -> [!CAUTION] -> Morphic is built with Vercel AI SDK RSC. AI SDK RSC is [experimental](https://sdk.vercel.ai/docs/getting-started/navigating-the-library#when-to-use-ai-sdk-rsc) and has some limitations. When using it in production, it is recommended to [migrate](https://sdk.vercel.ai/docs/ai-sdk-rsc/migrating-to-ui) to SDK UI. - -![capture](/public/capture-240404_blk.png) - -> [!NOTE] -> Please note that there are differences between this repository and the official website [morphic.sh](https://morphic.sh). The official website is a fork of this repository with additional features such as authentication, which are necessary for providing the service online. The core source code of Morphic resides in this repository, and it's designed to be easily built and deployed. +![capture](/public/screenshot-2025-01-15.png) ## 🗂️ Overview @@ -18,123 +12,127 @@ An AI-powered search engine with a generative UI. - 🌐 [Deploy](#-deploy) - 🔎 [Search Engine](#-search-engine) - ✅ [Verified models](#-verified-models) +- ⚡ [AI SDK Implementation](#-ai-sdk-implementation) +- 📦 [Open Source vs Cloud Offering](#-open-source-vs-cloud-offering) +- 👥 [Contributing](#-contributing) ## 🛠 Features -- Search and answer using GenerativeUI -- Understand user's questions -- Search history functionality -- Share search results ([Optional](https://github.com/miurla/morphic/blob/main/.env.local.example)) -- Video search support ([Optional](https://github.com/miurla/morphic/blob/main/.env.local.example)) -- Get answers from specified URLs -- Use as a search engine [※](#-search-engine) -- Support for providers other than OpenAI - - Google Generative AI Provider - - Azure OpenAI Provider [※](https://github.com/miurla/morphic/issues/13) - - Anthropic Provider - - Ollama Provider - - Groq Provider -- Local Redis support -- SearXNG Search API support with customizable depth (basic or advanced) -- Configurable search depth (basic or advanced) -- SearXNG Search API support with customizable depth +### Core Features -## 🧱 Stack +- AI-powered search with GenerativeUI +- Natural language question understanding +- Multiple search providers support (Tavily, SearXNG, Exa) +- Model selection from UI (switch between available AI models) -- 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/) / [Serper](https://serper.dev) / [SearXNG](https://docs.searxng.org/) -- Extract API: [Tavily AI](https://tavily.com/) / [Jina AI](https://jina.ai/) -- Database (Serverless/Local): [Upstash](https://upstash.com/) / [Redis](https://redis.io/) -- Component library: [shadcn/ui](https://ui.shadcn.com/) -- Headless component primitives: [Radix UI](https://www.radix-ui.com/) -- Styling: [Tailwind CSS](https://tailwindcss.com/) +### Chat & History -## 🚀 Quickstart +- Chat history functionality (Optional) +- Share search results (Optional) +- Redis support (Local/Upstash) -### 1. Fork and Clone repo +### AI Providers -Fork the repo to your Github account, then run the following command to clone the repo: +- OpenAI (Default) +- Google Generative AI +- Azure OpenAI +- Anthropic +- Ollama +- Groq +- OpenAI Compatible -``` -git clone git@github.com:[YOUR_GITHUB_ACCOUNT]/morphic.git -``` +### Search Capabilities -### 2. Install dependencies +- URL-specific search +- Video search support (Optional) +- SearXNG integration with: + - Customizable search depth (basic/advanced) + - Configurable engines + - Adjustable results limit + - Safe search options + - Custom time range filtering -``` -cd morphic -bun install -``` +### Additional Features -### 3. Setting up Upstash Redis +- Docker deployment ready +- Browser search engine integration -Follow the guide below to set up Upstash Redis. Create a database and obtain `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. Refer to the [Upstash guide](https://upstash.com/blog/rag-chatbot-upstash#setting-up-upstash-redis) for instructions on how to proceed. +## 🧱 Stack -If you intend to use a local Redis, you can skip this step. +### Core Framework -### 4. Fill out secrets +- [Next.js](https://nextjs.org/) - App Router, React Server Components +- [TypeScript](https://www.typescriptlang.org/) - Type safety +- [Vercel AI SDK](https://sdk.vercel.ai/docs) - Text streaming / Generative UI -``` -cp .env.local.example .env.local -``` +### AI & Search -Your .env.local file should look like this: +- [OpenAI](https://openai.com/) - Default AI provider (Optional: Google AI, Anthropic, Groq, Ollama, Azure OpenAI) +- [Tavily AI](https://tavily.com/) - Default search provider +- Alternative providers: + - [SearXNG](https://docs.searxng.org/) - Self-hosted search + - [Exa](https://exa.ai/) - Neural search -``` -# OpenAI API key retrieved here: https://platform.openai.com/api-keys -OPENAI_API_KEY= +### Data Storage -# Tavily API Key retrieved here: https://app.tavily.com/home -TAVILY_API_KEY= +- [Upstash](https://upstash.com/) - Serverless Redis +- [Redis](https://redis.io/) - Local Redis option -# Upstash Redis URL and Token retrieved here: https://console.upstash.com/redis -UPSTASH_REDIS_REST_URL= -UPSTASH_REDIS_REST_TOKEN= +### UI & Styling -## Redis Configuration +- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework +- [shadcn/ui](https://ui.shadcn.com/) - Re-usable components +- [Radix UI](https://www.radix-ui.com/) - Unstyled, accessible components +- [Lucide Icons](https://lucide.dev/) - Beautiful & consistent icons -This application supports both Upstash Redis and local Redis. To use local Redis: +## 🚀 Quickstart -1. Set `USE_LOCAL_REDIS=true` in your `.env.local` file. -2. Optionally, set `LOCAL_REDIS_URL` if your local Redis is not running on the default `localhost:6379` or `redis://redis:6379` if you're using docker compose. +### 1. Fork and Clone repo -To use Upstash Redis: +Fork the repo to your Github account, then run the following command to clone the repo: -1. Set `USE_LOCAL_REDIS=false` or leave it unset in your `.env.local` file. -2. Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` with your Upstash credentials. +```bash +git clone git@github.com:[YOUR_GITHUB_ACCOUNT]/morphic.git +``` -# SearXNG Configuration -SEARXNG_API_URL=http://localhost:8080 # Replace with your local SearXNG API URL or docker http://searxng:8080 -SEARCH_API=tavily # use searxng, tavily or exa -SEARXNG_SECRET="" # generate a secret key e.g. openssl rand -base64 32 -SEARXNG_PORT=8080 # default port -SEARXNG_BIND_ADDRESS=0.0.0.0 # default address -SEARXNG_IMAGE_PROXY=true # enable image proxy -SEARXNG_LIMITER=false # can be enabled to limit the number of requests per IP address -SEARXNG_DEFAULT_DEPTH=basic # Set to 'basic' or 'advanced', only affects SearXNG searches -SEARXNG_MAX_RESULTS=50 # Maximum number of results to return from SearXNG +### 2. Install dependencies +```bash +cd morphic +bun install ``` -### 5. Run app locally +### 3. Configure environment variables -#### Using Bun +```bash +cp .env.local.example .env.local +``` -To run the application locally using Bun, execute the following command: +Fill in the required environment variables in `.env.local`: -`bun dev` +```bash +# Required +OPENAI_API_KEY= # Get from https://platform.openai.com/api-keys +TAVILY_API_KEY= # Get from https://app.tavily.com/home +``` -You can now visit in your web browser. +For optional features configuration (Redis, SearXNG, etc.), see [CONFIGURATION.md](./docs/CONFIGURATION.md) -#### Using Docker +### 4. Run app locally -To run the application using Docker, use the following command: +#### Using Bun -`docker compose up -d` +```bash +bun dev +``` -This will start the application in detached mode. You can access it at . +#### Using Docker + +```bash +docker compose up -d +``` + +Visit http://localhost:3000 in your browser. ## 🌐 Deploy @@ -163,65 +161,6 @@ If you want to use Morphic as a search engine in your browser, follow these step This will allow you to use Morphic as your default search engine in the browser. -### Using SearXNG as an Alternative Search Backend - -Morphic now supports SearXNG as an alternative search backend with advanced search capabilities. To use SearXNG: - -1. Ensure you have Docker and Docker Compose installed on your system. -2. In your `.env.local` file, set the following variables: - - - NEXT_PUBLIC_BASE_URL= # Base URL for local development - - SEARXNG_API_URL= # Replace with your local SearXNG API URL or docker - - SEARXNG_SECRET=your_secret_key_here - - SEARXNG_PORT=8080 - - SEARXNG_IMAGE_PROXY=true - - SEARCH_API=searxng - - SEARXNG_LIMITER=false # can be enabled to limit the number of requests per IP - - SEARXNG_DEFAULT_DEPTH=basic # Set to 'basic' or 'advanced' - - SEARXNG_MAX_RESULTS=50 # Maximum number of results to return from SearXNG - - SEARXNG_ENGINES=google,bing,duckduckgo,wikipedia # can be overriden in searxng config - - SEARXNG_TIME_RANGE=None # Time range for search results - - SEARXNG_SAFESEARCH=0 # Safe search setting - - SEARXNG_CRAWL_MULTIPLIER=4 # Multiplier for the number of results to crawl in advanced search - -3. Two configuration files are provided in the root directory: - - - `searxng-settings.yml`: This file contains the main configuration for SearXNG, including engine settings and server options. - - `searxng-limiter.toml`: This file configures the rate limiting and bot detection features of SearXNG. - -4. Run `docker-compose up` to start the Morphic stack with SearXNG included. -5. SearXNG will be available at `http://localhost:8080` and Morphic will use it as the search backend. - -#### Advanced Search Configuration - -- `NEXT_PUBLIC_BASE_URL`: Set this to your local development URL () or your production URL when deploying. -- `SEARXNG_DEFAULT_DEPTH`: Set to 'basic' or 'advanced' to control the default search depth. -- `SEARXNG_MAX_RESULTS`: Maximum number of results to return from SearXNG. -- `SEARXNG_CRAWL_MULTIPLIER`: In advanced search mode, this multiplier determines how many results to crawl. For example, if `SEARXNG_MAX_RESULTS=10` and `SEARXNG_CRAWL_MULTIPLIER=4`, up to 40 results will be crawled before filtering and ranking. -- `SEARXNG_ENGINES`: Comma-separated list of search engines to use. -- `SEARXNG_TIME_RANGE`: Time range for search results (e.g., 'day', 'week', 'month', 'year', 'all'). -- `SEARXNG_SAFESEARCH`: Safe search setting (0 for off, 1 for moderate, 2 for strict). - -The advanced search feature includes content crawling, relevance scoring, and filtering to provide more accurate and comprehensive results. - -#### Customizing SearXNG - -- You can modify `searxng-settings.yml` to enable/disable specific search engines, change UI settings, or adjust server options. -- The `searxng-limiter.toml` file allows you to configure rate limiting and bot detection. This is useful if you're exposing SearXNG directly to the internet. -- If you prefer not to use external configuration files, you can set these options using environment variables in the `docker-compose.yml` file or directly in the SearXNG container. - -#### Troubleshooting - -- If you encounter issues with specific search engines (e.g., Wikidata), you can disable them in `searxng-settings.yml`: - -```yaml -engines: - - name: wikidata - disabled: true -``` - -- refer to - ## ✅ Verified models ### List of models applicable to all @@ -241,3 +180,36 @@ engines: - Groq - llama3-groq-8b-8192-tool-use-preview - llama3-groq-70b-8192-tool-use-preview + +## ⚡ AI SDK Implementation + +### Current Version: AI SDK UI + +This version of Morphic uses the AI SDK UI implementation, which is recommended for production use. It provides better streaming performance and more reliable client-side UI updates. + +### Previous Version: AI SDK RSC (v0.2.34 and earlier) + +The React Server Components (RSC) implementation of AI SDK was used in versions up to [v0.2.34](https://github.com/miurla/morphic/releases/tag/v0.2.34) but is now considered experimental and not recommended for production. If you need to reference the RSC implementation, please check the v0.2.34 release tag. + +> Note: v0.2.34 was the final version using RSC implementation before migrating to AI SDK UI. + +For more information about choosing between AI SDK UI and RSC, see the [official documentation](https://sdk.vercel.ai/docs/getting-started/navigating-the-library#when-to-use-ai-sdk-rsc). + +## 📦 Open Source vs Cloud Offering + +Morphic is open source software available under the Apache-2.0 license. + +To maintain sustainable development and provide cloud-ready features, we offer a hosted version of Morphic alongside our open-source offering. The cloud solution makes Morphic accessible to non-technical users and provides additional features while keeping the core functionality open and available for developers. + +For our cloud service, visit [morphic.sh](https://morphic.sh). + +## 👥 Contributing + +We welcome contributions to Morphic! Whether it's bug reports, feature requests, or pull requests, all contributions are appreciated. + +Please see our [Contributing Guide](CONTRIBUTING.md) for details on: + +- How to submit issues +- How to submit pull requests +- Commit message conventions +- Development setup diff --git a/app/actions.tsx b/app/actions.tsx deleted file mode 100644 index d110e773e..000000000 --- a/app/actions.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { - StreamableValue, - createAI, - createStreamableUI, - createStreamableValue, - getAIState, - getMutableAIState -} from 'ai/rsc' -import { CoreMessage, generateId } from 'ai' -import { Section } from '@/components/section' -import { FollowupPanel } from '@/components/followup-panel' -import { saveChat } from '@/lib/actions/chat' -import { Chat } from '@/lib/types' -import { AIMessage } from '@/lib/types' -import { UserMessage } from '@/components/user-message' -import { SearchSection } from '@/components/search-section' -import SearchRelated from '@/components/search-related' -import { CopilotDisplay } from '@/components/copilot-display' -import RetrieveSection from '@/components/retrieve-section' -import { VideoSearchSection } from '@/components/video-search-section' -import { AnswerSection } from '@/components/answer-section' -import { workflow } from '@/lib/actions/workflow' -import { isProviderEnabled } from '@/lib/utils/registry' - -const MAX_MESSAGES = 6 - -async function submit( - formData?: FormData, - skip?: boolean, - retryMessages?: AIMessage[] -) { - 'use server' - - const aiState = getMutableAIState() - const uiStream = createStreamableUI() - const isGenerating = createStreamableValue(true) - const isCollapsed = createStreamableValue(false) - - const aiMessages = [...(retryMessages ?? aiState.get().messages)] - // Get the messages from the state, filter out the tool messages - const messages: CoreMessage[] = aiMessages - .filter( - message => - message.role !== 'tool' && - message.type !== 'followup' && - message.type !== 'related' && - message.type !== 'end' - ) - .map(message => { - const { role, content } = message - return { role, content } as CoreMessage - }) - - // Limit the number of messages to the maximum - messages.splice(0, Math.max(messages.length - MAX_MESSAGES, 0)) - // 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 - const type = skip - ? undefined - : formData?.has('input') - ? 'input' - : formData?.has('related_query') - ? 'input_related' - : 'inquiry' - - // Get the model from the form data (e.g., openai:gpt-4o-mini) - const model = (formData?.get('model') as string) || 'openai:gpt-4o-mini' - const providerId = model.split(':')[0] - console.log(`Using model: ${model}`) - // Check if provider is enabled - if (!isProviderEnabled(providerId)) { - throw new Error( - `Provider ${providerId} is not available (API key not configured or base URL not set)` - ) - } - - // Add the user message to the state - if (content) { - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: generateId(), - role: 'user', - content, - type - } - ] - }) - messages.push({ - role: 'user', - content - }) - } - - // Run the agent workflow - workflow( - { uiStream, isCollapsed, isGenerating }, - aiState, - messages, - skip ?? false, - model - ) - - return { - id: generateId(), - isGenerating: isGenerating.value, - component: uiStream.value, - isCollapsed: isCollapsed.value - } -} - -export type AIState = { - messages: AIMessage[] - chatId: string - isSharePage?: boolean -} - -export type UIState = { - id: string - component: React.ReactNode - isGenerating?: StreamableValue - isCollapsed?: StreamableValue -}[] - -const initialAIState: AIState = { - chatId: generateId(), - messages: [] -} - -const initialUIState: UIState = [] - -// 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 - }, - initialUIState, - initialAIState, - onGetUIState: async () => { - 'use server' - - const aiState = getAIState() - if (aiState) { - const uiState = getUIStateFromAIState(aiState as Chat) - return uiState - } else { - return - } - }, - onSetAIState: async ({ state, done }) => { - 'use server' - - // Check if there is any message of type 'answer' in the state messages - if (!state.messages.some(e => e.type === 'answer')) { - return - } - - const { chatId, messages } = state - const createdAt = new Date() - const userId = 'anonymous' - const path = `/search/${chatId}` - const title = - messages.length > 0 - ? JSON.parse(messages[0].content)?.input?.substring(0, 100) || - 'Untitled' - : 'Untitled' - // Add an 'end' message at the end to determine if the history needs to be reloaded - const updatedMessages: AIMessage[] = [ - ...messages, - { - id: generateId(), - role: 'assistant', - content: `end`, - type: 'end' - } - ] - - const chat: Chat = { - id: chatId, - createdAt, - userId, - path, - title, - messages: updatedMessages - } - await saveChat(chat) - } -}) - -export const getUIStateFromAIState = (aiState: Chat) => { - const chatId = aiState.chatId - const isSharePage = aiState.isSharePage - - // Ensure messages is an array of plain objects - const messages = Array.isArray(aiState.messages) - ? aiState.messages.map(msg => ({ ...msg })) - : [] - - return messages - .map((message, index) => { - const { role, content, id, type, name } = message - - if ( - !type || - type === 'end' || - (isSharePage && type === 'related') || - (isSharePage && type === 'followup') - ) - return null - - switch (role) { - case 'user': - switch (type) { - case 'input': - case 'input_related': - const json = JSON.parse(content) - const value = type === 'input' ? json.input : json.related_query - return { - id, - component: ( - - ) - } - case 'inquiry': - return { - id, - component: - } - } - case 'assistant': - const answer = createStreamableValue() - answer.done(content) - switch (type) { - case 'answer': - return { - id, - component: - } - case 'related': - const relatedQueries = createStreamableValue() - relatedQueries.done(JSON.parse(content)) - return { - id, - component: ( - - ) - } - case 'followup': - return { - id, - component: ( -
- -
- ) - } - } - case 'tool': - try { - const toolOutput = JSON.parse(content) - const isCollapsed = createStreamableValue() - isCollapsed.done(true) - const searchResults = createStreamableValue() - searchResults.done(JSON.stringify(toolOutput)) - switch (name) { - case 'search': - return { - id, - component: , - isCollapsed: isCollapsed.value - } - case 'retrieve': - return { - id, - component: , - isCollapsed: isCollapsed.value - } - case 'videoSearch': - return { - id, - component: ( - - ), - isCollapsed: isCollapsed.value - } - } - } catch (error) { - return { - id, - component: null - } - } - default: - return { - id, - component: null - } - } - }) - .filter(message => message !== null) as UIState -} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 000000000..5d8946e7d --- /dev/null +++ b/app/api/chat/route.ts @@ -0,0 +1,154 @@ +import { getChat, saveChat } from '@/lib/actions/chat' +import { generateRelatedQuestions } from '@/lib/agents/generate-related-questions' +import { researcher } from '@/lib/agents/researcher' +import { ExtendedCoreMessage } from '@/lib/types' +import { convertToExtendedCoreMessages } from '@/lib/utils' +import { isProviderEnabled } from '@/lib/utils/registry' +import { + convertToCoreMessages, + createDataStreamResponse, + JSONValue, + streamText +} from 'ai' +import { cookies } from 'next/headers' + +export const maxDuration = 30 + +const DEFAULT_MODEL = 'openai:gpt-4o-mini' + +export async function POST(req: Request) { + try { + const { messages, id: chatId } = await req.json() + const referer = req.headers.get('referer') + const isSharePage = referer?.includes('/share/') + + if (isSharePage) { + return new Response('Chat API is not available on share pages', { + status: 403, + statusText: 'Forbidden' + }) + } + + const coreMessages = convertToCoreMessages(messages) + const extendedCoreMessages = convertToExtendedCoreMessages(messages) + + const cookieStore = await cookies() + const modelFromCookie = cookieStore.get('selected-model')?.value + const model = modelFromCookie || DEFAULT_MODEL + const provider = model.split(':')[0] + + if (!isProviderEnabled(provider)) { + return new Response(`Selected provider is not enabled ${provider}`, { + status: 404, + statusText: 'Not Found' + }) + } + + return createDataStreamResponse({ + execute: async dataStream => { + try { + let researcherConfig + try { + researcherConfig = await researcher({ + messages: coreMessages, + model + }) + } catch (error) { + console.error('Researcher configuration error:', error) + throw new Error('Failed to initialize researcher configuration') + } + + const result = streamText({ + ...researcherConfig, + onFinish: async event => { + try { + const responseMessages = event.response.messages + + let annotation: JSONValue = { + type: 'related-questions', + data: { + items: [] + } + } + + // Notify related questions loading + dataStream.writeMessageAnnotation(annotation) + + // Generate related questions + const relatedQuestions = await generateRelatedQuestions( + responseMessages, + model + ) + + // Update the annotation with the related questions + annotation = { + ...annotation, + data: relatedQuestions.object + } + + // Send related questions to client + dataStream.writeMessageAnnotation(annotation) + + // Create the message to save + const generatedMessages = [ + ...extendedCoreMessages, + ...responseMessages.slice(0, -1), + { + role: 'data', + content: annotation + }, + responseMessages[responseMessages.length - 1] + ] as ExtendedCoreMessage[] + + // Get the chat from the database if it exists, otherwise create a new one + const savedChat = (await getChat(chatId)) ?? { + messages: [], + createdAt: new Date(), + userId: 'anonymous', + path: `/search/${chatId}`, + title: messages[0].content, + id: chatId + } + + // Save chat with complete response and related questions + await saveChat({ + ...savedChat, + messages: generatedMessages + }).catch(error => { + console.error('Failed to save chat:', error) + throw new Error('Failed to save chat history') + }) + } catch (error) { + console.error('Error in onFinish:', error) + throw error + } + } + }) + + result.mergeIntoDataStream(dataStream) + } catch (error) { + console.error('Stream execution error:', error) + } + }, + onError: error => { + console.error('Stream error:', error) + return error instanceof Error ? error.message : String(error) + } + }) + } catch (error) { + console.error('API route error:', error) + return new Response( + JSON.stringify({ + error: + error instanceof Error + ? error.message + : 'An unexpected error occurred', + status: 500 + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' } + } + ) + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 294382289..be4d88427 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,13 +1,12 @@ -import type { Metadata, Viewport } from 'next' -import { Inter as FontSans } from 'next/font/google' -import './globals.css' -import { cn } from '@/lib/utils' -import { ThemeProvider } from '@/components/theme-provider' -import Header from '@/components/header' import Footer from '@/components/footer' +import Header from '@/components/header' import { Sidebar } from '@/components/sidebar' +import { ThemeProvider } from '@/components/theme-provider' import { Toaster } from '@/components/ui/sonner' -import { AppStateProvider } from '@/lib/utils/app-state' +import { cn } from '@/lib/utils' +import type { Metadata, Viewport } from 'next' +import { Inter as FontSans } from 'next/font/google' +import './globals.css' const fontSans = FontSans({ subsets: ['latin'], @@ -46,6 +45,8 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { + const enableSaveChatHistory = + process.env.NEXT_PUBLIC_ENABLE_SAVE_CHAT_HISTORY === 'true' return ( @@ -55,13 +56,11 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - -
- {children} - -