mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-12-02 09:48:16 +00:00
Compare commits
101 Commits
295334b195
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
046f159528 | ||
|
|
6899b49ca0 | ||
|
|
dbc2137efb | ||
|
|
1ea348ddb7 | ||
|
|
b8a7fb936f | ||
|
|
33c8f454a3 | ||
|
|
3e90305c12 | ||
|
|
41c879cd86 | ||
|
|
9b3833f933 | ||
|
|
610d06be36 | ||
|
|
7757bbd253 | ||
|
|
e2a371936b | ||
|
|
5901a965f7 | ||
|
|
6150784c27 | ||
|
|
cb30e2438a | ||
|
|
ead2a5b215 | ||
|
|
1df4d886ff | ||
|
|
2574287fa8 | ||
|
|
3005b379cf | ||
|
|
f83bd06e89 | ||
|
|
7544bbafaf | ||
|
|
0a62c60da2 | ||
|
|
956a768a86 | ||
|
|
e0ba476ca4 | ||
|
|
cba3f43b19 | ||
|
|
ec06a2b9ff | ||
|
|
1b4e883f57 | ||
|
|
f15802b688 | ||
|
|
8dec689a45 | ||
|
|
730ee0ff41 | ||
|
|
7c9258cfc9 | ||
|
|
4e7143ce0c | ||
|
|
d5f62f2dca | ||
|
|
b7b280637f | ||
|
|
e22a39fd73 | ||
|
|
6da6acbcd0 | ||
|
|
0ac8569a9e | ||
|
|
74bc08d189 | ||
|
|
d7dd17c069 | ||
|
|
6d35d60b49 | ||
|
|
d6c364fdcb | ||
|
|
8d04f636d0 | ||
|
|
9ac2da3607 | ||
|
|
55cf88822d | ||
|
|
c4acc83fd5 | ||
|
|
08feb18197 | ||
|
|
0df0114e76 | ||
|
|
4016b21bdf | ||
|
|
f7a43b3cb9 | ||
|
|
70bcd8c6f1 | ||
|
|
2568088341 | ||
|
|
a494d4c329 | ||
|
|
9b85c63a80 | ||
|
|
1614cfa5e5 | ||
|
|
036b44611f | ||
|
|
8b515201f3 | ||
|
|
cbcb03c7ac | ||
|
|
afc68ca91f | ||
|
|
3cc8882b28 | ||
|
|
c3830795cb | ||
|
|
f44ad973aa | ||
|
|
4bcbdad6cb | ||
|
|
5272c7fd3e | ||
|
|
657a577ec8 | ||
|
|
f6dac43d7a | ||
|
|
a00f2231d4 | ||
|
|
1da9b7655c | ||
|
|
9934c1dbe0 | ||
|
|
f767717d7f | ||
|
|
e88e1c627c | ||
|
|
2edef888a3 | ||
|
|
2dc8078848 | ||
|
|
8df81c20cf | ||
|
|
34bd02236d | ||
|
|
2430376a0c | ||
|
|
bd5628b390 | ||
|
|
3d5d04eda0 | ||
|
|
07a17925b1 | ||
|
|
3bcf646af1 | ||
|
|
e499c0b96e | ||
|
|
33b736e1e8 | ||
|
|
5e1746f646 | ||
|
|
41fe009847 | ||
|
|
1a8889c71c | ||
|
|
70c1f7230c | ||
|
|
c0771095a6 | ||
|
|
0856896aff | ||
|
|
3da53aed03 | ||
|
|
244675759c | ||
|
|
ce6a37aaff | ||
|
|
c3abba8462 | ||
|
|
f709aa8224 | ||
|
|
22695f4ef6 | ||
|
|
75ef2e0282 | ||
|
|
b0d97c4c83 | ||
|
|
6527388e25 | ||
|
|
7397e33f29 | ||
|
|
f6ffa9ebe0 | ||
|
|
f9e675823b | ||
|
|
2e736613c5 | ||
|
|
046daf442a |
BIN
.assets/demo.gif
Normal file
BIN
.assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 MiB |
BIN
.assets/sponsers/exa.png
Normal file
BIN
.assets/sponsers/exa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
.assets/sponsers/warp.png
Normal file
BIN
.assets/sponsers/warp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 KiB |
122
README.md
122
README.md
@@ -1,74 +1,76 @@
|
|||||||
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
# Perplexica 🔍
|
||||||
|
|
||||||
<div align="center" markdown="1">
|
|
||||||
<sup>Special thanks to:</sup>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a href="https://www.warp.dev/perplexica">
|
|
||||||
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica)
|
|
||||||
|
|
||||||
[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
|
[](https://github.com/ItzCrazyKns/Perplexica/stargazers)
|
||||||
|
[](https://github.com/ItzCrazyKns/Perplexica/network/members)
|
||||||
|
[](https://github.com/ItzCrazyKns/Perplexica/watchers)
|
||||||
|
[](https://hub.docker.com/r/itzcrazykns1337/perplexica)
|
||||||
|
[](https://github.com/ItzCrazyKns/Perplexica/blob/master/LICENSE)
|
||||||
|
[](https://github.com/ItzCrazyKns/Perplexica/commits/master)
|
||||||
[](https://discord.gg/26aArMy8tT)
|
[](https://discord.gg/26aArMy8tT)
|
||||||
|
|
||||||

|
Perplexica is a **privacy-focused AI answering engine** that runs entirely on your own hardware. It combines knowledge from the vast internet with support for **local LLMs** (Ollama) and cloud providers (OpenAI, Claude, Groq), delivering accurate answers with **cited sources** while keeping your searches completely private.
|
||||||
|
|
||||||
## Table of Contents <!-- omit in toc -->
|

|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Preview](#preview)
|
|
||||||
- [Features](#features)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended)
|
|
||||||
- [Non-Docker Installation](#non-docker-installation)
|
|
||||||
- [Ollama Connection Errors](#ollama-connection-errors)
|
|
||||||
- [Lemonade Connection Errors](#lemonade-connection-errors)
|
|
||||||
- [Using as a Search Engine](#using-as-a-search-engine)
|
|
||||||
- [Using Perplexica's API](#using-perplexicas-api)
|
|
||||||
- [Expose Perplexica to a network](#expose-perplexica-to-network)
|
|
||||||
- [One-Click Deployment](#one-click-deployment)
|
|
||||||
- [Upcoming Features](#upcoming-features)
|
|
||||||
- [Support Us](#support-us)
|
|
||||||
- [Donations](#donations)
|
|
||||||
- [Contribution](#contribution)
|
|
||||||
- [Help and Support](#help-and-support)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Perplexica is an open-source AI-powered searching tool or an AI-powered search engine that goes deep into the internet to find answers. Inspired by Perplexity AI, it's an open-source option that not just searches the web but understands your questions. It uses advanced machine learning algorithms like similarity searching and embeddings to refine results and provides clear answers with sources cited.
|
|
||||||
|
|
||||||
Using SearxNG to stay current and fully open source, Perplexica ensures you always get the most up-to-date information without compromising your privacy.
|
|
||||||
|
|
||||||
Want to know more about its architecture and how it works? You can read it [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/README.md).
|
Want to know more about its architecture and how it works? You can read it [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/README.md).
|
||||||
|
|
||||||
## Preview
|
## ✨ Features
|
||||||
|
|
||||||

|
🤖 **Support for all major AI providers** - Use local LLMs through Ollama or connect to OpenAI, Anthropic Claude, Google Gemini, Groq, and more. Mix and match models based on your needs.
|
||||||
|
|
||||||
## Features
|
⚡ **Smart search modes** - Choose Balanced Mode for everyday searches, Fast Mode when you need quick answers, or wait for Quality Mode (coming soon) for deep research.
|
||||||
|
|
||||||
- **Local LLMs**: You can utilize local LLMs such as Qwen, DeepSeek, Llama, and Mistral.
|
🎯 **Six specialized focus modes** - Get better results with modes designed for specific tasks: Academic papers, YouTube videos, Reddit discussions, Wolfram Alpha calculations, writing assistance, or general web search.
|
||||||
- **Two Main Modes:**
|
|
||||||
- **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page.
|
|
||||||
- **Normal Mode:** Processes your query and performs a web search.
|
|
||||||
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
|
|
||||||
- **All Mode:** Searches the entire web to find the best results.
|
|
||||||
- **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web.
|
|
||||||
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
|
|
||||||
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
|
|
||||||
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
|
|
||||||
- **Reddit Search Mode:** Searches Reddit for discussions and opinions related to the query.
|
|
||||||
- **Current Information:** Some search tools might give you outdated info because they use data from crawling bots and convert them into embeddings and store them in a index. Unlike them, Perplexica uses SearxNG, a metasearch engine to get the results and rerank and get the most relevant source out of it, ensuring you always get the latest information without the overhead of daily data updates.
|
|
||||||
- **API**: Integrate Perplexica into your existing applications and make use of its capibilities.
|
|
||||||
|
|
||||||
It has many more features like image and video search. Some of the planned features are mentioned in [upcoming features](#upcoming-features).
|
🔍 **Web search powered by SearxNG** - Access multiple search engines while keeping your identity private. Support for Tavily and Exa coming soon for even better results.
|
||||||
|
|
||||||
|
📷 **Image and video search** - Find visual content alongside text results. Search isn't limited to just articles anymore.
|
||||||
|
|
||||||
|
📄 **File uploads** - Upload documents and ask questions about them. PDFs, text files, images - Perplexica understands them all.
|
||||||
|
|
||||||
|
🌐 **Search specific domains** - Limit your search to specific websites when you know where to look. Perfect for technical documentation or research papers.
|
||||||
|
|
||||||
|
💡 **Smart suggestions** - Get intelligent search suggestions as you type, helping you formulate better queries.
|
||||||
|
|
||||||
|
📚 **Discover** - Browse interesting articles and trending content throughout the day. Stay informed without even searching.
|
||||||
|
|
||||||
|
🕒 **Search history** - Every search is saved locally so you can revisit your discoveries anytime. Your research is never lost.
|
||||||
|
|
||||||
|
✨ **More coming soon** - We're actively developing new features based on community feedback. Join our Discord to help shape Perplexica's future!
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Perplexica's development is powered by the generous support of our sponsors. Their contributions help keep this project free, open-source, and accessible to everyone.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
|
||||||
|
<a href="https://www.warp.dev/perplexica">
|
||||||
|
<img alt="Warp Terminal" src=".assets/sponsers/warp.png" width="100%">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### **✨ [Try Warp - The AI-Powered Terminal →](https://www.warp.dev/perplexica)**
|
||||||
|
|
||||||
|
Warp is revolutionizing development workflows with AI-powered features, modern UX, and blazing-fast performance. Used by developers at top companies worldwide.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We'd also like to thank the following partners for their generous support:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="100" align="center">
|
||||||
|
<a href="https://dashboard.exa.ai" target="_blank">
|
||||||
|
<img src=".assets/sponsers/exa.png" alt="Exa" width="80" height="80" style="border-radius: .75rem;" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://dashboard.exa.ai">Exa</a> • The Perfect Web Search API for LLMs - web search, crawling, deep research, and answer APIs
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
15
drizzle/0002_daffy_wrecker.sql
Normal file
15
drizzle/0002_daffy_wrecker.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_messages` (
|
||||||
|
`id` integer PRIMARY KEY NOT NULL,
|
||||||
|
`messageId` text NOT NULL,
|
||||||
|
`chatId` text NOT NULL,
|
||||||
|
`backendId` text NOT NULL,
|
||||||
|
`query` text NOT NULL,
|
||||||
|
`createdAt` text NOT NULL,
|
||||||
|
`responseBlocks` text DEFAULT '[]',
|
||||||
|
`status` text DEFAULT 'answering'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE `messages`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
132
drizzle/meta/0002_snapshot.json
Normal file
132
drizzle/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "1c5eb804-d6b4-48ec-9a8f-75fb729c8e52",
|
||||||
|
"prevId": "6dedf55f-0e44-478f-82cf-14a21ac686f8",
|
||||||
|
"tables": {
|
||||||
|
"chats": {
|
||||||
|
"name": "chats",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"focusMode": {
|
||||||
|
"name": "focusMode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"name": "files",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"name": "messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"name": "messageId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"backendId": {
|
||||||
|
"name": "backendId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"name": "query",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"responseBlocks": {
|
||||||
|
"name": "responseBlocks",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'answering'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1758863991284,
|
"when": 1758863991284,
|
||||||
"tag": "0001_wise_rockslide",
|
"tag": "0001_wise_rockslide",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1763732708332,
|
||||||
|
"tag": "0002_daffy_wrecker",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -16,13 +16,14 @@
|
|||||||
"@huggingface/transformers": "^3.7.5",
|
"@huggingface/transformers": "^3.7.5",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@icons-pack/react-simple-icons": "^12.3.0",
|
"@icons-pack/react-simple-icons": "^12.3.0",
|
||||||
"@langchain/anthropic": "^1.0.0",
|
"@langchain/anthropic": "^1.0.1",
|
||||||
"@langchain/community": "^1.0.0",
|
"@langchain/community": "^1.0.3",
|
||||||
"@langchain/core": "^1.0.1",
|
"@langchain/core": "^1.0.5",
|
||||||
"@langchain/google-genai": "^1.0.0",
|
"@langchain/google-genai": "^1.0.1",
|
||||||
"@langchain/groq": "^1.0.0",
|
"@langchain/groq": "^1.0.1",
|
||||||
"@langchain/ollama": "^1.0.0",
|
"@langchain/langgraph": "^1.0.1",
|
||||||
"@langchain/openai": "^1.0.0",
|
"@langchain/ollama": "^1.0.1",
|
||||||
|
"@langchain/openai": "^1.1.1",
|
||||||
"@langchain/textsplitters": "^1.0.0",
|
"@langchain/textsplitters": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
@@ -33,22 +34,29 @@
|
|||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"langchain": "^1.0.1",
|
"langchain": "^1.0.4",
|
||||||
|
"lightweight-charts": "^5.0.9",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"markdown-to-jsx": "^7.7.2",
|
"markdown-to-jsx": "^7.7.2",
|
||||||
|
"mathjs": "^15.1.0",
|
||||||
"next": "^15.2.2",
|
"next": "^15.2.2",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"ollama": "^0.6.3",
|
||||||
|
"openai": "^6.9.0",
|
||||||
|
"partial-json": "^0.1.7",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-text-to-speech": "^0.14.5",
|
"react-text-to-speech": "^0.14.5",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
|
"rfc6902": "^5.1.2",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
|
"yahoo-finance2": "^3.10.2",
|
||||||
"yet-another-react-lightbox": "^3.17.2",
|
"yet-another-react-lightbox": "^3.17.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
import { EventEmitter } from 'stream';
|
|
||||||
import db from '@/lib/db';
|
|
||||||
import { chats, messages as messagesSchema } from '@/lib/db/schema';
|
|
||||||
import { and, eq, gt } from 'drizzle-orm';
|
|
||||||
import { getFileDetails } from '@/lib/utils/files';
|
|
||||||
import { searchHandlers } from '@/lib/search';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
|
import SearchAgent from '@/lib/agents/search';
|
||||||
|
import SessionManager from '@/lib/session';
|
||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -20,47 +16,25 @@ const messageSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const chatModelSchema: z.ZodType<ModelWithProvider> = z.object({
|
const chatModelSchema: z.ZodType<ModelWithProvider> = z.object({
|
||||||
providerId: z.string({
|
providerId: z.string({ message: 'Chat model provider id must be provided' }),
|
||||||
errorMap: () => ({
|
key: z.string({ message: 'Chat model key must be provided' }),
|
||||||
message: 'Chat model provider id must be provided',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
key: z.string({
|
|
||||||
errorMap: () => ({
|
|
||||||
message: 'Chat model key must be provided',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const embeddingModelSchema: z.ZodType<ModelWithProvider> = z.object({
|
const embeddingModelSchema: z.ZodType<ModelWithProvider> = z.object({
|
||||||
providerId: z.string({
|
providerId: z.string({
|
||||||
errorMap: () => ({
|
|
||||||
message: 'Embedding model provider id must be provided',
|
message: 'Embedding model provider id must be provided',
|
||||||
}),
|
}),
|
||||||
}),
|
key: z.string({ message: 'Embedding model key must be provided' }),
|
||||||
key: z.string({
|
|
||||||
errorMap: () => ({
|
|
||||||
message: 'Embedding model key must be provided',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
message: messageSchema,
|
message: messageSchema,
|
||||||
optimizationMode: z.enum(['speed', 'balanced', 'quality'], {
|
optimizationMode: z.enum(['speed', 'balanced', 'quality'], {
|
||||||
errorMap: () => ({
|
|
||||||
message: 'Optimization mode must be one of: speed, balanced, quality',
|
message: 'Optimization mode must be one of: speed, balanced, quality',
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
focusMode: z.string().min(1, 'Focus mode is required'),
|
focusMode: z.string().min(1, 'Focus mode is required'),
|
||||||
history: z
|
history: z
|
||||||
.array(
|
.array(z.tuple([z.string(), z.string()]))
|
||||||
z.tuple([z.string(), z.string()], {
|
|
||||||
errorMap: () => ({
|
|
||||||
message: 'History items must be tuples of two strings',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional()
|
.optional()
|
||||||
.default([]),
|
.default([]),
|
||||||
files: z.array(z.string()).optional().default([]),
|
files: z.array(z.string()).optional().default([]),
|
||||||
@@ -78,7 +52,7 @@ const safeValidateBody = (data: unknown) => {
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error.errors.map((e) => ({
|
error: result.error.issues.map((e: any) => ({
|
||||||
path: e.path.join('.'),
|
path: e.path.join('.'),
|
||||||
message: e.message,
|
message: e.message,
|
||||||
})),
|
})),
|
||||||
@@ -91,151 +65,12 @@ const safeValidateBody = (data: unknown) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmitterEvents = async (
|
|
||||||
stream: EventEmitter,
|
|
||||||
writer: WritableStreamDefaultWriter,
|
|
||||||
encoder: TextEncoder,
|
|
||||||
chatId: string,
|
|
||||||
) => {
|
|
||||||
let receivedMessage = '';
|
|
||||||
const aiMessageId = crypto.randomBytes(7).toString('hex');
|
|
||||||
|
|
||||||
stream.on('data', (data) => {
|
|
||||||
const parsedData = JSON.parse(data);
|
|
||||||
if (parsedData.type === 'response') {
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'message',
|
|
||||||
data: parsedData.data,
|
|
||||||
messageId: aiMessageId,
|
|
||||||
}) + '\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
receivedMessage += parsedData.data;
|
|
||||||
} else if (parsedData.type === 'sources') {
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'sources',
|
|
||||||
data: parsedData.data,
|
|
||||||
messageId: aiMessageId,
|
|
||||||
}) + '\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sourceMessageId = crypto.randomBytes(7).toString('hex');
|
|
||||||
|
|
||||||
db.insert(messagesSchema)
|
|
||||||
.values({
|
|
||||||
chatId: chatId,
|
|
||||||
messageId: sourceMessageId,
|
|
||||||
role: 'source',
|
|
||||||
sources: parsedData.data,
|
|
||||||
createdAt: new Date().toString(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stream.on('end', () => {
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'messageEnd',
|
|
||||||
}) + '\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
writer.close();
|
|
||||||
|
|
||||||
db.insert(messagesSchema)
|
|
||||||
.values({
|
|
||||||
content: receivedMessage,
|
|
||||||
chatId: chatId,
|
|
||||||
messageId: aiMessageId,
|
|
||||||
role: 'assistant',
|
|
||||||
createdAt: new Date().toString(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
});
|
|
||||||
stream.on('error', (data) => {
|
|
||||||
const parsedData = JSON.parse(data);
|
|
||||||
writer.write(
|
|
||||||
encoder.encode(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
data: parsedData.data,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
writer.close();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHistorySave = async (
|
|
||||||
message: Message,
|
|
||||||
humanMessageId: string,
|
|
||||||
focusMode: string,
|
|
||||||
files: string[],
|
|
||||||
) => {
|
|
||||||
const chat = await db.query.chats.findFirst({
|
|
||||||
where: eq(chats.id, message.chatId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileData = files.map(getFileDetails);
|
|
||||||
|
|
||||||
if (!chat) {
|
|
||||||
await db
|
|
||||||
.insert(chats)
|
|
||||||
.values({
|
|
||||||
id: message.chatId,
|
|
||||||
title: message.content,
|
|
||||||
createdAt: new Date().toString(),
|
|
||||||
focusMode: focusMode,
|
|
||||||
files: fileData,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
} else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) {
|
|
||||||
db.update(chats)
|
|
||||||
.set({
|
|
||||||
files: files.map(getFileDetails),
|
|
||||||
})
|
|
||||||
.where(eq(chats.id, message.chatId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageExists = await db.query.messages.findFirst({
|
|
||||||
where: eq(messagesSchema.messageId, humanMessageId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!messageExists) {
|
|
||||||
await db
|
|
||||||
.insert(messagesSchema)
|
|
||||||
.values({
|
|
||||||
content: message.content,
|
|
||||||
chatId: message.chatId,
|
|
||||||
messageId: humanMessageId,
|
|
||||||
role: 'user',
|
|
||||||
createdAt: new Date().toString(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
} else {
|
|
||||||
await db
|
|
||||||
.delete(messagesSchema)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
gt(messagesSchema.id, messageExists.id),
|
|
||||||
eq(messagesSchema.chatId, message.chatId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
export const POST = async (req: Request) => {
|
||||||
try {
|
try {
|
||||||
const reqBody = (await req.json()) as Body;
|
const reqBody = (await req.json()) as Body;
|
||||||
|
|
||||||
const parseBody = safeValidateBody(reqBody);
|
const parseBody = safeValidateBody(reqBody);
|
||||||
|
|
||||||
if (!parseBody.success) {
|
if (!parseBody.success) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ message: 'Invalid request body', error: parseBody.error },
|
{ message: 'Invalid request body', error: parseBody.error },
|
||||||
@@ -265,48 +100,116 @@ export const POST = async (req: Request) => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const humanMessageId =
|
const history: ChatTurnMessage[] = body.history.map((msg) => {
|
||||||
message.messageId ?? crypto.randomBytes(7).toString('hex');
|
|
||||||
|
|
||||||
const history: BaseMessage[] = body.history.map((msg) => {
|
|
||||||
if (msg[0] === 'human') {
|
if (msg[0] === 'human') {
|
||||||
return new HumanMessage({
|
return {
|
||||||
|
role: 'user',
|
||||||
content: msg[1],
|
content: msg[1],
|
||||||
});
|
};
|
||||||
} else {
|
} else {
|
||||||
return new AIMessage({
|
return {
|
||||||
|
role: 'assistant',
|
||||||
content: msg[1],
|
content: msg[1],
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handler = searchHandlers[body.focusMode];
|
const agent = new SearchAgent();
|
||||||
|
const session = SessionManager.createSession();
|
||||||
if (!handler) {
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
message: 'Invalid focus mode',
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = await handler.searchAndAnswer(
|
|
||||||
message.content,
|
|
||||||
history,
|
|
||||||
llm,
|
|
||||||
embedding,
|
|
||||||
body.optimizationMode,
|
|
||||||
body.files,
|
|
||||||
body.systemInstructions as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseStream = new TransformStream();
|
const responseStream = new TransformStream();
|
||||||
const writer = responseStream.writable.getWriter();
|
const writer = responseStream.writable.getWriter();
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
handleEmitterEvents(stream, writer, encoder, message.chatId);
|
let receivedMessage = '';
|
||||||
handleHistorySave(message, humanMessageId, body.focusMode, body.files);
|
|
||||||
|
session.addListener('data', (data: any) => {
|
||||||
|
if (data.type === 'response') {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
data: data.data,
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
receivedMessage += data.data;
|
||||||
|
} else if (data.type === 'sources') {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'sources',
|
||||||
|
data: data.data,
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (data.type === 'block') {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'block',
|
||||||
|
block: data.block,
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (data.type === 'updateBlock') {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'updateBlock',
|
||||||
|
blockId: data.blockId,
|
||||||
|
patch: data.patch,
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (data.type === 'researchComplete') {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'researchComplete',
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
session.addListener('end', () => {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'messageEnd',
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
writer.close();
|
||||||
|
session.removeAllListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
session.addListener('error', (data: any) => {
|
||||||
|
writer.write(
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
data: data.data,
|
||||||
|
}) + '\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
writer.close();
|
||||||
|
session.removeAllListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
agent.searchAsync(session, {
|
||||||
|
chatHistory: history,
|
||||||
|
followUp: message.content,
|
||||||
|
config: {
|
||||||
|
llm,
|
||||||
|
embedding: embedding,
|
||||||
|
sources: ['web'],
|
||||||
|
mode: body.optimizationMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* handleHistorySave(message, humanMessageId, body.focusMode, body.files); */
|
||||||
|
|
||||||
return new Response(responseStream.readable, {
|
return new Response(responseStream.readable, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import handleImageSearch from '@/lib/chains/imageSearchAgent';
|
import searchImages from '@/lib/agents/media/image';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
|
|
||||||
interface ImageSearchBody {
|
interface ImageSearchBody {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -13,16 +12,6 @@ export const POST = async (req: Request) => {
|
|||||||
try {
|
try {
|
||||||
const body: ImageSearchBody = await req.json();
|
const body: ImageSearchBody = await req.json();
|
||||||
|
|
||||||
const chatHistory = body.chatHistory
|
|
||||||
.map((msg: any) => {
|
|
||||||
if (msg.role === 'user') {
|
|
||||||
return new HumanMessage(msg.content);
|
|
||||||
} else if (msg.role === 'assistant') {
|
|
||||||
return new AIMessage(msg.content);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
|
||||||
|
|
||||||
const registry = new ModelRegistry();
|
const registry = new ModelRegistry();
|
||||||
|
|
||||||
const llm = await registry.loadChatModel(
|
const llm = await registry.loadChatModel(
|
||||||
@@ -30,9 +19,9 @@ export const POST = async (req: Request) => {
|
|||||||
body.chatModel.key,
|
body.chatModel.key,
|
||||||
);
|
);
|
||||||
|
|
||||||
const images = await handleImageSearch(
|
const images = await searchImages(
|
||||||
{
|
{
|
||||||
chat_history: chatHistory,
|
chatHistory: body.chatHistory,
|
||||||
query: body.query,
|
query: body.query,
|
||||||
},
|
},
|
||||||
llm,
|
llm,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
import { MetaSearchAgentType } from '@/lib/search/metaSearchAgent';
|
|
||||||
import { searchHandlers } from '@/lib/search';
|
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
|
import SessionManager from '@/lib/session';
|
||||||
|
import SearchAgent from '@/lib/agents/search';
|
||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
|
||||||
interface ChatRequestBody {
|
interface ChatRequestBody {
|
||||||
optimizationMode: 'speed' | 'balanced';
|
optimizationMode: 'speed' | 'balanced';
|
||||||
@@ -30,12 +30,6 @@ export const POST = async (req: Request) => {
|
|||||||
body.optimizationMode = body.optimizationMode || 'balanced';
|
body.optimizationMode = body.optimizationMode || 'balanced';
|
||||||
body.stream = body.stream || false;
|
body.stream = body.stream || false;
|
||||||
|
|
||||||
const history: BaseMessage[] = body.history.map((msg) => {
|
|
||||||
return msg[0] === 'human'
|
|
||||||
? new HumanMessage({ content: msg[1] })
|
|
||||||
: new AIMessage({ content: msg[1] });
|
|
||||||
});
|
|
||||||
|
|
||||||
const registry = new ModelRegistry();
|
const registry = new ModelRegistry();
|
||||||
|
|
||||||
const [llm, embeddings] = await Promise.all([
|
const [llm, embeddings] = await Promise.all([
|
||||||
@@ -46,21 +40,26 @@ export const POST = async (req: Request) => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const searchHandler: MetaSearchAgentType = searchHandlers[body.focusMode];
|
const history: ChatTurnMessage[] = body.history.map((msg) => {
|
||||||
|
return msg[0] === 'human'
|
||||||
|
? { role: 'user', content: msg[1] }
|
||||||
|
: { role: 'assistant', content: msg[1] };
|
||||||
|
});
|
||||||
|
|
||||||
if (!searchHandler) {
|
const session = SessionManager.createSession();
|
||||||
return Response.json({ message: 'Invalid focus mode' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const emitter = await searchHandler.searchAndAnswer(
|
const agent = new SearchAgent();
|
||||||
body.query,
|
|
||||||
history,
|
agent.searchAsync(session, {
|
||||||
llm,
|
chatHistory: history,
|
||||||
embeddings,
|
config: {
|
||||||
body.optimizationMode,
|
embedding: embeddings,
|
||||||
[],
|
llm: llm,
|
||||||
body.systemInstructions || '',
|
sources: ['web', 'discussions', 'academic'],
|
||||||
);
|
mode: 'balanced',
|
||||||
|
},
|
||||||
|
followUp: body.query,
|
||||||
|
});
|
||||||
|
|
||||||
if (!body.stream) {
|
if (!body.stream) {
|
||||||
return new Promise(
|
return new Promise(
|
||||||
@@ -71,7 +70,7 @@ export const POST = async (req: Request) => {
|
|||||||
let message = '';
|
let message = '';
|
||||||
let sources: any[] = [];
|
let sources: any[] = [];
|
||||||
|
|
||||||
emitter.on('data', (data: string) => {
|
session.addListener('data', (data: string) => {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData = JSON.parse(data);
|
||||||
if (parsedData.type === 'response') {
|
if (parsedData.type === 'response') {
|
||||||
@@ -89,11 +88,11 @@ export const POST = async (req: Request) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('end', () => {
|
session.addListener('end', () => {
|
||||||
resolve(Response.json({ message, sources }, { status: 200 }));
|
resolve(Response.json({ message, sources }, { status: 200 }));
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('error', (error: any) => {
|
session.addListener('error', (error: any) => {
|
||||||
reject(
|
reject(
|
||||||
Response.json(
|
Response.json(
|
||||||
{ message: 'Search error', error },
|
{ message: 'Search error', error },
|
||||||
@@ -124,14 +123,14 @@ export const POST = async (req: Request) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
signal.addEventListener('abort', () => {
|
signal.addEventListener('abort', () => {
|
||||||
emitter.removeAllListeners();
|
session.removeAllListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
controller.close();
|
controller.close();
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('data', (data: string) => {
|
session.addListener('data', (data: string) => {
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -162,7 +161,7 @@ export const POST = async (req: Request) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('end', () => {
|
session.addListener('end', () => {
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
|
|
||||||
controller.enqueue(
|
controller.enqueue(
|
||||||
@@ -175,7 +174,7 @@ export const POST = async (req: Request) => {
|
|||||||
controller.close();
|
controller.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('error', (error: any) => {
|
session.addListener('error', (error: any) => {
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
|
|
||||||
controller.error(error);
|
controller.error(error);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import generateSuggestions from '@/lib/chains/suggestionGeneratorAgent';
|
import generateSuggestions from '@/lib/agents/suggestions';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||||
|
|
||||||
interface SuggestionsGenerationBody {
|
interface SuggestionsGenerationBody {
|
||||||
@@ -13,16 +12,6 @@ export const POST = async (req: Request) => {
|
|||||||
try {
|
try {
|
||||||
const body: SuggestionsGenerationBody = await req.json();
|
const body: SuggestionsGenerationBody = await req.json();
|
||||||
|
|
||||||
const chatHistory = body.chatHistory
|
|
||||||
.map((msg: any) => {
|
|
||||||
if (msg.role === 'user') {
|
|
||||||
return new HumanMessage(msg.content);
|
|
||||||
} else if (msg.role === 'assistant') {
|
|
||||||
return new AIMessage(msg.content);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
|
||||||
|
|
||||||
const registry = new ModelRegistry();
|
const registry = new ModelRegistry();
|
||||||
|
|
||||||
const llm = await registry.loadChatModel(
|
const llm = await registry.loadChatModel(
|
||||||
@@ -32,7 +21,7 @@ export const POST = async (req: Request) => {
|
|||||||
|
|
||||||
const suggestions = await generateSuggestions(
|
const suggestions = await generateSuggestions(
|
||||||
{
|
{
|
||||||
chat_history: chatHistory,
|
chatHistory: body.chatHistory,
|
||||||
},
|
},
|
||||||
llm,
|
llm,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
|
|||||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
|
import { Chunk } from '@/lib/types';
|
||||||
|
|
||||||
interface FileRes {
|
interface FileRes {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@@ -87,9 +88,17 @@ export async function POST(req: Request) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const embeddings = await model.embedDocuments(
|
const chunks: Chunk[] = splitted.map((doc) => {
|
||||||
splitted.map((doc) => doc.pageContent),
|
return {
|
||||||
|
content: doc.pageContent,
|
||||||
|
metadata: doc.metadata,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddings = await model.embedChunks(
|
||||||
|
chunks
|
||||||
);
|
);
|
||||||
|
|
||||||
const embeddingsDataPath = filePath.replace(
|
const embeddingsDataPath = filePath.replace(
|
||||||
/\.\w+$/,
|
/\.\w+$/,
|
||||||
'-embeddings.json',
|
'-embeddings.json',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import handleVideoSearch from '@/lib/chains/videoSearchAgent';
|
import handleVideoSearch from '@/lib/agents/media/video';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
|
|
||||||
interface VideoSearchBody {
|
interface VideoSearchBody {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -13,16 +12,6 @@ export const POST = async (req: Request) => {
|
|||||||
try {
|
try {
|
||||||
const body: VideoSearchBody = await req.json();
|
const body: VideoSearchBody = await req.json();
|
||||||
|
|
||||||
const chatHistory = body.chatHistory
|
|
||||||
.map((msg: any) => {
|
|
||||||
if (msg.role === 'user') {
|
|
||||||
return new HumanMessage(msg.content);
|
|
||||||
} else if (msg.role === 'assistant') {
|
|
||||||
return new AIMessage(msg.content);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((msg) => msg !== undefined) as BaseMessage[];
|
|
||||||
|
|
||||||
const registry = new ModelRegistry();
|
const registry = new ModelRegistry();
|
||||||
|
|
||||||
const llm = await registry.loadChatModel(
|
const llm = await registry.loadChatModel(
|
||||||
@@ -32,7 +21,7 @@ export const POST = async (req: Request) => {
|
|||||||
|
|
||||||
const videos = await handleVideoSearch(
|
const videos = await handleVideoSearch(
|
||||||
{
|
{
|
||||||
chat_history: chatHistory,
|
chatHistory: body.chatHistory,
|
||||||
query: body.query,
|
query: body.query,
|
||||||
},
|
},
|
||||||
llm,
|
llm,
|
||||||
|
|||||||
197
src/components/AssistantSteps.tsx
Normal file
197
src/components/AssistantSteps.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Brain, Search, FileText, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types';
|
||||||
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
|
const getStepIcon = (step: ResearchBlockSubStep) => {
|
||||||
|
if (step.type === 'reasoning') {
|
||||||
|
return <Brain className="w-4 h-4" />;
|
||||||
|
} else if (step.type === 'searching') {
|
||||||
|
return <Search className="w-4 h-4" />;
|
||||||
|
} else if (step.type === 'reading') {
|
||||||
|
return <FileText className="w-4 h-4" />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStepTitle = (
|
||||||
|
step: ResearchBlockSubStep,
|
||||||
|
isStreaming: boolean,
|
||||||
|
): string => {
|
||||||
|
if (step.type === 'reasoning') {
|
||||||
|
return isStreaming && !step.reasoning ? 'Thinking...' : 'Thinking';
|
||||||
|
} else if (step.type === 'searching') {
|
||||||
|
return `Searching ${step.searching.length} ${step.searching.length === 1 ? 'query' : 'queries'}`;
|
||||||
|
} else if (step.type === 'reading') {
|
||||||
|
return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`;
|
||||||
|
}
|
||||||
|
return 'Processing';
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssistantSteps = ({
|
||||||
|
block,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
block: ResearchBlock;
|
||||||
|
status: 'answering' | 'completed' | 'error';
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
const { researchEnded, loading } = useChat();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (researchEnded) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
} else if (status === 'answering') {
|
||||||
|
setIsExpanded(true);
|
||||||
|
}
|
||||||
|
}, [researchEnded, status]);
|
||||||
|
|
||||||
|
if (!block || block.data.subSteps.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="w-full flex items-center justify-between p-3 hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Brain className="w-4 h-4 text-black dark:text-white" />
|
||||||
|
<span className="text-sm font-medium text-black dark:text-white">
|
||||||
|
Research Progress ({block.data.subSteps.length}{' '}
|
||||||
|
{block.data.subSteps.length === 1 ? 'step' : 'steps'})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-black/70 dark:text-white/70" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-black/70 dark:text-white/70" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="border-t border-light-200 dark:border-dark-200"
|
||||||
|
>
|
||||||
|
<div className="p-3 space-y-2">
|
||||||
|
{block.data.subSteps.map((step, index) => {
|
||||||
|
const isLastStep = index === block.data.subSteps.length - 1;
|
||||||
|
const isStreaming = loading && isLastStep && !researchEnded;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={step.id}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0 }}
|
||||||
|
className="flex gap-3"
|
||||||
|
>
|
||||||
|
{/* Timeline connector */}
|
||||||
|
<div className="flex flex-col items-center pt-0.5">
|
||||||
|
<div
|
||||||
|
className={`rounded-full p-1.5 bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 ${isStreaming ? 'animate-pulse' : ''}`}
|
||||||
|
>
|
||||||
|
{getStepIcon(step)}
|
||||||
|
</div>
|
||||||
|
{index < block.data.subSteps.length - 1 && (
|
||||||
|
<div className="w-0.5 flex-1 min-h-[20px] bg-light-200 dark:bg-dark-200 mt-1.5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step content */}
|
||||||
|
<div className="flex-1 pb-1">
|
||||||
|
<span className="text-sm font-medium text-black dark:text-white">
|
||||||
|
{getStepTitle(step, isStreaming)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{step.type === 'reasoning' && (
|
||||||
|
<>
|
||||||
|
{step.reasoning && (
|
||||||
|
<p className="text-xs text-black/70 dark:text-white/70 mt-0.5">
|
||||||
|
{step.reasoning}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{isStreaming && !step.reasoning && (
|
||||||
|
<div className="flex items-center gap-1.5 mt-0.5">
|
||||||
|
<div
|
||||||
|
className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: '0ms' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: '150ms' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: '300ms' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step.type === 'searching' &&
|
||||||
|
step.searching.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||||
|
{step.searching.map((query, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200"
|
||||||
|
>
|
||||||
|
{query}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step.type === 'reading' && step.reading.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||||
|
{step.reading.slice(0, 4).map((result, idx) => {
|
||||||
|
const url = result.metadata.url || '';
|
||||||
|
const title = result.metadata.title || 'Untitled';
|
||||||
|
const domain = url ? new URL(url).hostname : '';
|
||||||
|
const faviconUrl = domain
|
||||||
|
? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200"
|
||||||
|
>
|
||||||
|
{faviconUrl && (
|
||||||
|
<img
|
||||||
|
src={faviconUrl}
|
||||||
|
alt=""
|
||||||
|
className="w-3 h-3 rounded-sm flex-shrink-0"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="line-clamp-1">{title}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssistantSteps;
|
||||||
@@ -7,11 +7,12 @@ import MessageBoxLoading from './MessageBoxLoading';
|
|||||||
import { useChat } from '@/lib/hooks/useChat';
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
|
|
||||||
const Chat = () => {
|
const Chat = () => {
|
||||||
const { sections, chatTurns, loading, messageAppeared } = useChat();
|
const { sections, loading, messageAppeared, messages } = useChat();
|
||||||
|
|
||||||
const [dividerWidth, setDividerWidth] = useState(0);
|
const [dividerWidth, setDividerWidth] = useState(0);
|
||||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const messageEnd = useRef<HTMLDivElement | null>(null);
|
const messageEnd = useRef<HTMLDivElement | null>(null);
|
||||||
|
const lastScrolledRef = useRef<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDividerWidth = () => {
|
const updateDividerWidth = () => {
|
||||||
@@ -22,35 +23,40 @@ const Chat = () => {
|
|||||||
|
|
||||||
updateDividerWidth();
|
updateDividerWidth();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateDividerWidth();
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentRef = dividerRef.current;
|
||||||
|
if (currentRef) {
|
||||||
|
resizeObserver.observe(currentRef);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', updateDividerWidth);
|
window.addEventListener('resize', updateDividerWidth);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (currentRef) {
|
||||||
|
resizeObserver.unobserve(currentRef);
|
||||||
|
}
|
||||||
|
resizeObserver.disconnect();
|
||||||
window.removeEventListener('resize', updateDividerWidth);
|
window.removeEventListener('resize', updateDividerWidth);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [sections.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scroll = () => {
|
const scroll = () => {
|
||||||
messageEnd.current?.scrollIntoView({ behavior: 'auto' });
|
messageEnd.current?.scrollIntoView({ behavior: 'auto' });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (chatTurns.length === 1) {
|
if (messages.length === 1) {
|
||||||
document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`;
|
document.title = `${messages[0].query.substring(0, 30)} - Perplexica`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageEndBottom =
|
if (sections.length > lastScrolledRef.current) {
|
||||||
messageEnd.current?.getBoundingClientRect().bottom ?? 0;
|
|
||||||
|
|
||||||
const distanceFromMessageEnd = window.innerHeight - messageEndBottom;
|
|
||||||
|
|
||||||
if (distanceFromMessageEnd >= -100) {
|
|
||||||
scroll();
|
scroll();
|
||||||
|
lastScrolledRef.current = sections.length;
|
||||||
}
|
}
|
||||||
|
}, [messages]);
|
||||||
if (chatTurns[chatTurns.length - 1]?.role === 'user') {
|
|
||||||
scroll();
|
|
||||||
}
|
|
||||||
}, [chatTurns]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
|
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
|
||||||
@@ -58,7 +64,7 @@ const Chat = () => {
|
|||||||
const isLast = i === sections.length - 1;
|
const isLast = i === sections.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={section.userMessage.messageId}>
|
<Fragment key={section.message.messageId}>
|
||||||
<MessageBox
|
<MessageBox
|
||||||
section={section}
|
section={section}
|
||||||
sectionIndex={i}
|
sectionIndex={i}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Document } from '@langchain/core/documents';
|
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
import Chat from './Chat';
|
import Chat from './Chat';
|
||||||
import EmptyChat from './EmptyChat';
|
import EmptyChat from './EmptyChat';
|
||||||
import { Settings } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import NextError from 'next/error';
|
import NextError from 'next/error';
|
||||||
import { useChat } from '@/lib/hooks/useChat';
|
import { useChat } from '@/lib/hooks/useChat';
|
||||||
import Loader from './ui/Loader';
|
|
||||||
import SettingsButtonMobile from './Settings/SettingsButtonMobile';
|
import SettingsButtonMobile from './Settings/SettingsButtonMobile';
|
||||||
|
import { Block, Chunk } from '@/lib/types';
|
||||||
|
|
||||||
export interface BaseMessage {
|
export interface BaseMessage {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@@ -17,20 +14,27 @@ export interface BaseMessage {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Message extends BaseMessage {
|
||||||
|
backendId: string;
|
||||||
|
query: string;
|
||||||
|
responseBlocks: Block[];
|
||||||
|
status: 'answering' | 'completed' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMessage extends BaseMessage {
|
||||||
|
role: 'user';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssistantMessage extends BaseMessage {
|
export interface AssistantMessage extends BaseMessage {
|
||||||
role: 'assistant';
|
role: 'assistant';
|
||||||
content: string;
|
content: string;
|
||||||
suggestions?: string[];
|
suggestions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserMessage extends BaseMessage {
|
|
||||||
role: 'user';
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SourceMessage extends BaseMessage {
|
export interface SourceMessage extends BaseMessage {
|
||||||
role: 'source';
|
role: 'source';
|
||||||
sources: Document[];
|
sources: Chunk[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuggestionMessage extends BaseMessage {
|
export interface SuggestionMessage extends BaseMessage {
|
||||||
@@ -38,11 +42,12 @@ export interface SuggestionMessage extends BaseMessage {
|
|||||||
suggestions: string[];
|
suggestions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message =
|
export type LegacyMessage =
|
||||||
| AssistantMessage
|
| AssistantMessage
|
||||||
| UserMessage
|
| UserMessage
|
||||||
| SourceMessage
|
| SourceMessage
|
||||||
| SuggestionMessage;
|
| SuggestionMessage;
|
||||||
|
|
||||||
export type ChatTurn = UserMessage | AssistantMessage;
|
export type ChatTurn = UserMessage | AssistantMessage;
|
||||||
|
|
||||||
export interface File {
|
export interface File {
|
||||||
@@ -51,8 +56,13 @@ export interface File {
|
|||||||
fileId: string;
|
fileId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Widget {
|
||||||
|
widgetType: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
const ChatWindow = () => {
|
const ChatWindow = () => {
|
||||||
const { hasError, isReady, notFound, messages } = useChat();
|
const { hasError, notFound, messages } = useChat();
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -68,8 +78,7 @@ const ChatWindow = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isReady ? (
|
return notFound ? (
|
||||||
notFound ? (
|
|
||||||
<NextError statusCode={404} />
|
<NextError statusCode={404} />
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -82,11 +91,6 @@ const ChatWindow = () => {
|
|||||||
<EmptyChat />
|
<EmptyChat />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
@@ -5,8 +8,39 @@ import Link from 'next/link';
|
|||||||
import WeatherWidget from './WeatherWidget';
|
import WeatherWidget from './WeatherWidget';
|
||||||
import NewsArticleWidget from './NewsArticleWidget';
|
import NewsArticleWidget from './NewsArticleWidget';
|
||||||
import SettingsButtonMobile from '@/components/Settings/SettingsButtonMobile';
|
import SettingsButtonMobile from '@/components/Settings/SettingsButtonMobile';
|
||||||
|
import {
|
||||||
|
getShowNewsWidget,
|
||||||
|
getShowWeatherWidget,
|
||||||
|
} from '@/lib/config/clientRegistry';
|
||||||
|
|
||||||
const EmptyChat = () => {
|
const EmptyChat = () => {
|
||||||
|
const [showWeather, setShowWeather] = useState(() =>
|
||||||
|
typeof window !== 'undefined' ? getShowWeatherWidget() : true,
|
||||||
|
);
|
||||||
|
const [showNews, setShowNews] = useState(() =>
|
||||||
|
typeof window !== 'undefined' ? getShowNewsWidget() : true,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateWidgetVisibility = () => {
|
||||||
|
setShowWeather(getShowWeatherWidget());
|
||||||
|
setShowNews(getShowNewsWidget());
|
||||||
|
};
|
||||||
|
|
||||||
|
updateWidgetVisibility();
|
||||||
|
|
||||||
|
window.addEventListener('client-config-changed', updateWidgetVisibility);
|
||||||
|
window.addEventListener('storage', updateWidgetVisibility);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
'client-config-changed',
|
||||||
|
updateWidgetVisibility,
|
||||||
|
);
|
||||||
|
window.removeEventListener('storage', updateWidgetVisibility);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
||||||
@@ -19,14 +53,20 @@ const EmptyChat = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<EmptyChatMessageInput />
|
<EmptyChatMessageInput />
|
||||||
</div>
|
</div>
|
||||||
|
{(showWeather || showNews) && (
|
||||||
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
<div className="flex flex-col w-full gap-4 mt-2 sm:flex-row sm:justify-center">
|
||||||
|
{showWeather && (
|
||||||
<div className="flex-1 w-full">
|
<div className="flex-1 w-full">
|
||||||
<WeatherWidget />
|
<WeatherWidget />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{showNews && (
|
||||||
<div className="flex-1 w-full">
|
<div className="flex-1 w-full">
|
||||||
<NewsArticleWidget />
|
<NewsArticleWidget />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,14 +15,21 @@ const Copy = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const contentToCopy = `${initialMessage}${section?.sourceMessage?.sources && section.sourceMessage.sources.length > 0 && `\n\nCitations:\n${section.sourceMessage.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
const contentToCopy = `${initialMessage}${
|
||||||
|
section?.message.responseBlocks.filter((b) => b.type === 'source')
|
||||||
|
?.length > 0 &&
|
||||||
|
`\n\nCitations:\n${section.message.responseBlocks
|
||||||
|
.filter((b) => b.type === 'source')
|
||||||
|
?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`)
|
||||||
|
.join(`\n`)}`
|
||||||
|
}`;
|
||||||
navigator.clipboard.writeText(contentToCopy);
|
navigator.clipboard.writeText(contentToCopy);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1000);
|
setTimeout(() => setCopied(false), 1000);
|
||||||
}}
|
}}
|
||||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{copied ? <Check size={18} /> : <ClipboardList size={18} />}
|
{copied ? <Check size={16} /> : <ClipboardList size={16} />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ArrowLeftRight } from 'lucide-react';
|
import { ArrowLeftRight, Repeat } from 'lucide-react';
|
||||||
|
|
||||||
const Rewrite = ({
|
const Rewrite = ({
|
||||||
rewrite,
|
rewrite,
|
||||||
@@ -10,12 +10,11 @@ const Rewrite = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => rewrite(messageId)}
|
onClick={() => rewrite(messageId)}
|
||||||
className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
|
className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
|
||||||
>
|
>
|
||||||
<ArrowLeftRight size={18} />
|
<Repeat size={16} />
|
||||||
<p className="text-xs font-medium">Rewrite</p>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
1;
|
||||||
export default Rewrite;
|
export default Rewrite;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
StopCircle,
|
StopCircle,
|
||||||
Layers3,
|
Layers3,
|
||||||
Plus,
|
Plus,
|
||||||
|
CornerDownRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
|
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
|
||||||
import Copy from './MessageActions/Copy';
|
import Copy from './MessageActions/Copy';
|
||||||
@@ -21,6 +22,9 @@ import { useSpeech } from 'react-text-to-speech';
|
|||||||
import ThinkBox from './ThinkBox';
|
import ThinkBox from './ThinkBox';
|
||||||
import { useChat, Section } from '@/lib/hooks/useChat';
|
import { useChat, Section } from '@/lib/hooks/useChat';
|
||||||
import Citation from './Citation';
|
import Citation from './Citation';
|
||||||
|
import AssistantSteps from './AssistantSteps';
|
||||||
|
import { ResearchBlock } from '@/lib/types';
|
||||||
|
import Renderer from './Widgets/Renderer';
|
||||||
|
|
||||||
const ThinkTagProcessor = ({
|
const ThinkTagProcessor = ({
|
||||||
children,
|
children,
|
||||||
@@ -45,12 +49,21 @@ const MessageBox = ({
|
|||||||
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { loading, chatTurns, sendMessage, rewrite } = useChat();
|
const { loading, sendMessage, rewrite, messages, researchEnded } = useChat();
|
||||||
|
|
||||||
const parsedMessage = section.parsedAssistantMessage || '';
|
const parsedMessage = section.parsedTextBlocks.join('\n\n');
|
||||||
const speechMessage = section.speechMessage || '';
|
const speechMessage = section.speechMessage || '';
|
||||||
const thinkingEnded = section.thinkingEnded;
|
const thinkingEnded = section.thinkingEnded;
|
||||||
|
|
||||||
|
const sourceBlocks = section.message.responseBlocks.filter(
|
||||||
|
(block): block is typeof block & { type: 'source' } =>
|
||||||
|
block.type === 'source',
|
||||||
|
);
|
||||||
|
|
||||||
|
const sources = sourceBlocks.flatMap((block) => block.data);
|
||||||
|
|
||||||
|
const hasContent = section.parsedTextBlocks.length > 0;
|
||||||
|
|
||||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||||
|
|
||||||
const markdownOverrides: MarkdownToJSX.Options = {
|
const markdownOverrides: MarkdownToJSX.Options = {
|
||||||
@@ -71,7 +84,7 @@ const MessageBox = ({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className={'w-full pt-8 break-words'}>
|
<div className={'w-full pt-8 break-words'}>
|
||||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||||
{section.userMessage.content}
|
{section.message.query}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,8 +93,7 @@ const MessageBox = ({
|
|||||||
ref={dividerRef}
|
ref={dividerRef}
|
||||||
className="flex flex-col space-y-6 w-full lg:w-9/12"
|
className="flex flex-col space-y-6 w-full lg:w-9/12"
|
||||||
>
|
>
|
||||||
{section.sourceMessage &&
|
{sources.length > 0 && (
|
||||||
section.sourceMessage.sources.length > 0 && (
|
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<BookCopy className="text-black dark:text-white" size={20} />
|
<BookCopy className="text-black dark:text-white" size={20} />
|
||||||
@@ -89,12 +101,42 @@ const MessageBox = ({
|
|||||||
Sources
|
Sources
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<MessageSources sources={section.sourceMessage.sources} />
|
<MessageSources sources={sources} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.message.responseBlocks
|
||||||
|
.filter(
|
||||||
|
(block): block is ResearchBlock =>
|
||||||
|
block.type === 'research' && block.data.subSteps.length > 0,
|
||||||
|
)
|
||||||
|
.map((researchBlock) => (
|
||||||
|
<div key={researchBlock.id} className="flex flex-col space-y-2">
|
||||||
|
<AssistantSteps
|
||||||
|
block={researchBlock}
|
||||||
|
status={section.message.status}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{section.widgets.length > 0 && <Renderer widgets={section.widgets} />}
|
||||||
|
|
||||||
|
{isLast &&
|
||||||
|
loading &&
|
||||||
|
!researchEnded &&
|
||||||
|
!section.message.responseBlocks.some(
|
||||||
|
(b) => b.type === 'research' && b.data.subSteps.length > 0,
|
||||||
|
) && (
|
||||||
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200">
|
||||||
|
<Disc3 className="w-4 h-4 text-black dark:text-white animate-spin" />
|
||||||
|
<span className="text-sm text-black/70 dark:text-white/70">
|
||||||
|
Brainstorming...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{section.sourceMessage && (
|
{sources.length > 0 && (
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<Disc3
|
<Disc3
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -109,7 +151,7 @@ const MessageBox = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{section.assistantMessage && (
|
{hasContent && (
|
||||||
<>
|
<>
|
||||||
<Markdown
|
<Markdown
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -122,18 +164,15 @@ const MessageBox = ({
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
|
|
||||||
{loading && isLast ? null : (
|
{loading && isLast ? null : (
|
||||||
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
|
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4">
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center -ml-2">
|
||||||
<Rewrite
|
<Rewrite
|
||||||
rewrite={rewrite}
|
rewrite={rewrite}
|
||||||
messageId={section.assistantMessage.messageId}
|
messageId={section.message.messageId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center -mr-2">
|
||||||
<Copy
|
<Copy initialMessage={parsedMessage} section={section} />
|
||||||
initialMessage={section.assistantMessage.content}
|
|
||||||
section={section}
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (speechStatus === 'started') {
|
if (speechStatus === 'started') {
|
||||||
@@ -142,12 +181,12 @@ const MessageBox = ({
|
|||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
className="p-2 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{speechStatus === 'started' ? (
|
{speechStatus === 'started' ? (
|
||||||
<StopCircle size={18} />
|
<StopCircle size={16} />
|
||||||
) : (
|
) : (
|
||||||
<Volume2 size={18} />
|
<Volume2 size={16} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,9 +196,9 @@ const MessageBox = ({
|
|||||||
{isLast &&
|
{isLast &&
|
||||||
section.suggestions &&
|
section.suggestions &&
|
||||||
section.suggestions.length > 0 &&
|
section.suggestions.length > 0 &&
|
||||||
section.assistantMessage &&
|
hasContent &&
|
||||||
!loading && (
|
!loading && (
|
||||||
<div className="mt-8 pt-6 border-t border-light-200/50 dark:border-dark-200/50">
|
<div className="mt-6">
|
||||||
<div className="flex flex-row items-center space-x-2 mb-4">
|
<div className="flex flex-row items-center space-x-2 mb-4">
|
||||||
<Layers3
|
<Layers3
|
||||||
className="text-black dark:text-white"
|
className="text-black dark:text-white"
|
||||||
@@ -173,20 +212,24 @@ const MessageBox = ({
|
|||||||
{section.suggestions.map(
|
{section.suggestions.map(
|
||||||
(suggestion: string, i: number) => (
|
(suggestion: string, i: number) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
{i > 0 && (
|
<div className="h-px bg-light-200/40 dark:bg-dark-200/40" />
|
||||||
<div className="h-px bg-light-200/40 dark:bg-dark-200/40 mx-3" />
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => sendMessage(suggestion)}
|
onClick={() => sendMessage(suggestion)}
|
||||||
className="group w-full px-3 py-4 text-left transition-colors duration-200"
|
className="group w-full py-4 text-left transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<p className="text-sm text-black/70 dark:text-white/70 group-hover:text-[#24A0ED] transition-colors duration-200 leading-relaxed">
|
<div className="flex flex-row space-x-3 items-center ">
|
||||||
|
<CornerDownRight
|
||||||
|
size={17}
|
||||||
|
className="group-hover:text-sky-400 transition-colors duration-200"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-black/70 dark:text-white/70 group-hover:text-sky-400 transition-colors duration-200 leading-relaxed">
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<Plus
|
<Plus
|
||||||
size={16}
|
size={16}
|
||||||
className="text-black/40 dark:text-white/40 group-hover:text-[#24A0ED] transition-colors duration-200 flex-shrink-0"
|
className="text-black/40 dark:text-white/40 group-hover:text-sky-400 transition-colors duration-200 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -201,17 +244,17 @@ const MessageBox = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{section.assistantMessage && (
|
{hasContent && (
|
||||||
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
|
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
|
||||||
<SearchImages
|
<SearchImages
|
||||||
query={section.userMessage.content}
|
query={section.message.query}
|
||||||
chatHistory={chatTurns.slice(0, sectionIndex * 2)}
|
chatHistory={messages}
|
||||||
messageId={section.assistantMessage.messageId}
|
messageId={section.message.messageId}
|
||||||
/>
|
/>
|
||||||
<SearchVideos
|
<SearchVideos
|
||||||
chatHistory={chatTurns.slice(0, sectionIndex * 2)}
|
chatHistory={messages}
|
||||||
query={section.userMessage.content}
|
query={section.message.query}
|
||||||
messageId={section.assistantMessage.messageId}
|
messageId={section.message.messageId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const OptimizationModes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'quality',
|
key: 'quality',
|
||||||
title: 'Quality (Soon)',
|
title: 'Quality',
|
||||||
description: 'Get the most thorough and accurate answer',
|
description: 'Get the most thorough and accurate answer',
|
||||||
icon: (
|
icon: (
|
||||||
<Star
|
<Star
|
||||||
@@ -75,13 +75,11 @@ const Optimization = () => {
|
|||||||
<PopoverButton
|
<PopoverButton
|
||||||
onClick={() => setOptimizationMode(mode.key)}
|
onClick={() => setOptimizationMode(mode.key)}
|
||||||
key={i}
|
key={i}
|
||||||
disabled={mode.key === 'quality'}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none',
|
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none',
|
||||||
optimizationMode === mode.key
|
optimizationMode === mode.key
|
||||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||||
mode.key === 'quality' && 'opacity-50 cursor-not-allowed',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-1 text-black dark:text-white">
|
<div className="flex flex-row items-center space-x-1 text-black dark:text-white">
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import {
|
|||||||
Transition,
|
Transition,
|
||||||
TransitionChild,
|
TransitionChild,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { Document } from '@langchain/core/documents';
|
|
||||||
import { File } from 'lucide-react';
|
import { File } from 'lucide-react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
|
import { Chunk } from '@/lib/types';
|
||||||
|
|
||||||
const MessageSources = ({ sources }: { sources: Document[] }) => {
|
const MessageSources = ({ sources }: { sources: Chunk[] }) => {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import { useChat, Section } from '@/lib/hooks/useChat';
|
import { useChat, Section } from '@/lib/hooks/useChat';
|
||||||
|
import { SourceBlock } from '@/lib/types';
|
||||||
|
|
||||||
const downloadFile = (filename: string, content: string, type: string) => {
|
const downloadFile = (filename: string, content: string, type: string) => {
|
||||||
const blob = new Blob([content], { type });
|
const blob = new Blob([content], { type });
|
||||||
@@ -28,35 +29,41 @@ const downloadFile = (filename: string, content: string, type: string) => {
|
|||||||
|
|
||||||
const exportAsMarkdown = (sections: Section[], title: string) => {
|
const exportAsMarkdown = (sections: Section[], title: string) => {
|
||||||
const date = new Date(
|
const date = new Date(
|
||||||
sections[0]?.userMessage?.createdAt || Date.now(),
|
sections[0].message.createdAt || Date.now(),
|
||||||
).toLocaleString();
|
).toLocaleString();
|
||||||
let md = `# 💬 Chat Export: ${title}\n\n`;
|
let md = `# 💬 Chat Export: ${title}\n\n`;
|
||||||
md += `*Exported on: ${date}*\n\n---\n`;
|
md += `*Exported on: ${date}*\n\n---\n`;
|
||||||
|
|
||||||
sections.forEach((section, idx) => {
|
sections.forEach((section, idx) => {
|
||||||
if (section.userMessage) {
|
|
||||||
md += `\n---\n`;
|
md += `\n---\n`;
|
||||||
md += `**🧑 User**
|
md += `**🧑 User**
|
||||||
`;
|
`;
|
||||||
md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`;
|
md += `*${new Date(section.message.createdAt).toLocaleString()}*\n\n`;
|
||||||
md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`;
|
md += `> ${section.message.query.replace(/\n/g, '\n> ')}\n`;
|
||||||
}
|
|
||||||
|
|
||||||
if (section.assistantMessage) {
|
if (section.message.responseBlocks.length > 0) {
|
||||||
md += `\n---\n`;
|
md += `\n---\n`;
|
||||||
md += `**🤖 Assistant**
|
md += `**🤖 Assistant**
|
||||||
`;
|
`;
|
||||||
md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`;
|
md += `*${new Date(section.message.createdAt).toLocaleString()}*\n\n`;
|
||||||
md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`;
|
md += `> ${section.message.responseBlocks
|
||||||
|
.filter((b) => b.type === 'text')
|
||||||
|
.map((block) => block.data)
|
||||||
|
.join('\n')
|
||||||
|
.replace(/\n/g, '\n> ')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceResponseBlock = section.message.responseBlocks.find(
|
||||||
|
(block) => block.type === 'source',
|
||||||
|
) as SourceBlock | undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
section.sourceMessage &&
|
sourceResponseBlock &&
|
||||||
section.sourceMessage.sources &&
|
sourceResponseBlock.data &&
|
||||||
section.sourceMessage.sources.length > 0
|
sourceResponseBlock.data.length > 0
|
||||||
) {
|
) {
|
||||||
md += `\n**Citations:**\n`;
|
md += `\n**Citations:**\n`;
|
||||||
section.sourceMessage.sources.forEach((src: any, i: number) => {
|
sourceResponseBlock.data.forEach((src: any, i: number) => {
|
||||||
const url = src.metadata?.url || '';
|
const url = src.metadata?.url || '';
|
||||||
md += `- [${i + 1}] [${url}](${url})\n`;
|
md += `- [${i + 1}] [${url}](${url})\n`;
|
||||||
});
|
});
|
||||||
@@ -69,7 +76,7 @@ const exportAsMarkdown = (sections: Section[], title: string) => {
|
|||||||
const exportAsPDF = (sections: Section[], title: string) => {
|
const exportAsPDF = (sections: Section[], title: string) => {
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF();
|
||||||
const date = new Date(
|
const date = new Date(
|
||||||
sections[0]?.userMessage?.createdAt || Date.now(),
|
sections[0]?.message?.createdAt || Date.now(),
|
||||||
).toLocaleString();
|
).toLocaleString();
|
||||||
let y = 15;
|
let y = 15;
|
||||||
const pageHeight = doc.internal.pageSize.height;
|
const pageHeight = doc.internal.pageSize.height;
|
||||||
@@ -86,7 +93,6 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
doc.setTextColor(30);
|
doc.setTextColor(30);
|
||||||
|
|
||||||
sections.forEach((section, idx) => {
|
sections.forEach((section, idx) => {
|
||||||
if (section.userMessage) {
|
|
||||||
if (y > pageHeight - 30) {
|
if (y > pageHeight - 30) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
y = 15;
|
y = 15;
|
||||||
@@ -96,15 +102,11 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setTextColor(120);
|
doc.setTextColor(120);
|
||||||
doc.text(
|
doc.text(`${new Date(section.message.createdAt).toLocaleString()}`, 40, y);
|
||||||
`${new Date(section.userMessage.createdAt).toLocaleString()}`,
|
|
||||||
40,
|
|
||||||
y,
|
|
||||||
);
|
|
||||||
y += 6;
|
y += 6;
|
||||||
doc.setTextColor(30);
|
doc.setTextColor(30);
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
const userLines = doc.splitTextToSize(section.userMessage.content, 180);
|
const userLines = doc.splitTextToSize(section.message.query, 180);
|
||||||
for (let i = 0; i < userLines.length; i++) {
|
for (let i = 0; i < userLines.length; i++) {
|
||||||
if (y > pageHeight - 20) {
|
if (y > pageHeight - 20) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
@@ -121,9 +123,8 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
}
|
}
|
||||||
doc.line(10, y, 200, y);
|
doc.line(10, y, 200, y);
|
||||||
y += 4;
|
y += 4;
|
||||||
}
|
|
||||||
|
|
||||||
if (section.assistantMessage) {
|
if (section.message.responseBlocks.length > 0) {
|
||||||
if (y > pageHeight - 30) {
|
if (y > pageHeight - 30) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
y = 15;
|
y = 15;
|
||||||
@@ -134,7 +135,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setTextColor(120);
|
doc.setTextColor(120);
|
||||||
doc.text(
|
doc.text(
|
||||||
`${new Date(section.assistantMessage.createdAt).toLocaleString()}`,
|
`${new Date(section.message.createdAt).toLocaleString()}`,
|
||||||
40,
|
40,
|
||||||
y,
|
y,
|
||||||
);
|
);
|
||||||
@@ -142,7 +143,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
doc.setTextColor(30);
|
doc.setTextColor(30);
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
const assistantLines = doc.splitTextToSize(
|
const assistantLines = doc.splitTextToSize(
|
||||||
section.assistantMessage.content,
|
section.parsedTextBlocks.join('\n'),
|
||||||
180,
|
180,
|
||||||
);
|
);
|
||||||
for (let i = 0; i < assistantLines.length; i++) {
|
for (let i = 0; i < assistantLines.length; i++) {
|
||||||
@@ -154,10 +155,14 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
y += 6;
|
y += 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceResponseBlock = section.message.responseBlocks.find(
|
||||||
|
(block) => block.type === 'source',
|
||||||
|
) as SourceBlock | undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
section.sourceMessage &&
|
sourceResponseBlock &&
|
||||||
section.sourceMessage.sources &&
|
sourceResponseBlock.data &&
|
||||||
section.sourceMessage.sources.length > 0
|
sourceResponseBlock.data.length > 0
|
||||||
) {
|
) {
|
||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setTextColor(80);
|
doc.setTextColor(80);
|
||||||
@@ -167,7 +172,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
|||||||
}
|
}
|
||||||
doc.text('Citations:', 12, y);
|
doc.text('Citations:', 12, y);
|
||||||
y += 5;
|
y += 5;
|
||||||
section.sourceMessage.sources.forEach((src: any, i: number) => {
|
sourceResponseBlock.data.forEach((src: any, i: number) => {
|
||||||
const url = src.metadata?.url || '';
|
const url = src.metadata?.url || '';
|
||||||
if (y > pageHeight - 15) {
|
if (y > pageHeight - 15) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
@@ -198,15 +203,15 @@ const Navbar = () => {
|
|||||||
const { sections, chatId } = useChat();
|
const { sections, chatId } = useChat();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sections.length > 0 && sections[0].userMessage) {
|
if (sections.length > 0 && sections[0].message) {
|
||||||
const newTitle =
|
const newTitle =
|
||||||
sections[0].userMessage.content.length > 20
|
sections[0].message.query.substring(0, 30) + '...' ||
|
||||||
? `${sections[0].userMessage.content.substring(0, 20).trim()}...`
|
'New Conversation';
|
||||||
: sections[0].userMessage.content;
|
|
||||||
setTitle(newTitle);
|
setTitle(newTitle);
|
||||||
const newTimeAgo = formatTimeDifference(
|
const newTimeAgo = formatTimeDifference(
|
||||||
new Date(),
|
new Date(),
|
||||||
sections[0].userMessage.createdAt,
|
sections[0].message.createdAt,
|
||||||
);
|
);
|
||||||
setTimeAgo(newTimeAgo);
|
setTimeAgo(newTimeAgo);
|
||||||
}
|
}
|
||||||
@@ -214,10 +219,10 @@ const Navbar = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
if (sections.length > 0 && sections[0].userMessage) {
|
if (sections.length > 0 && sections[0].message) {
|
||||||
const newTimeAgo = formatTimeDifference(
|
const newTimeAgo = formatTimeDifference(
|
||||||
new Date(),
|
new Date(),
|
||||||
sections[0].userMessage.createdAt,
|
sections[0].message.createdAt,
|
||||||
);
|
);
|
||||||
setTimeAgo(newTimeAgo);
|
setTimeAgo(newTimeAgo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const AddModel = ({
|
|||||||
>
|
>
|
||||||
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
||||||
<div className="px-6 pt-6 pb-4">
|
<div className="px-6 pt-6 pb-4">
|
||||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
<h3 className="text-black/90 dark:text-white/90 font-medium text-sm">
|
||||||
Add new {type === 'chat' ? 'chat' : 'embedding'} model
|
Add new {type === 'chat' ? 'chat' : 'embedding'} model
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +115,7 @@ const AddModel = ({
|
|||||||
<input
|
<input
|
||||||
value={modelName}
|
value={modelName}
|
||||||
onChange={(e) => setModelName(e.target.value)}
|
onChange={(e) => setModelName(e.target.value)}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder="e.g., GPT-4"
|
placeholder="e.g., GPT-4"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@@ -128,7 +128,7 @@ const AddModel = ({
|
|||||||
<input
|
<input
|
||||||
value={modelKey}
|
value={modelKey}
|
||||||
onChange={(e) => setModelKey(e.target.value)}
|
onChange={(e) => setModelKey(e.target.value)}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder="e.g., gpt-4"
|
placeholder="e.g., gpt-4"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@@ -140,7 +140,7 @@ const AddModel = ({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 rounded-lg text-sm bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
className="px-4 py-2 rounded-lg text-[13px] bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loader2 className="animate-spin" size={16} />
|
<Loader2 className="animate-spin" size={16} />
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ const AddProvider = ({
|
|||||||
|
|
||||||
setProviders((prev) => [...prev, data]);
|
setProviders((prev) => [...prev, data]);
|
||||||
|
|
||||||
toast.success('Provider added successfully.');
|
toast.success('Connection added successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding provider:', error);
|
console.error('Error adding provider:', error);
|
||||||
toast.error('Failed to add provider.');
|
toast.error('Failed to add connection.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -96,10 +96,10 @@ const AddProvider = ({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="px-3 md:px-4 py-1.5 md:py-2 rounded-lg text-xs sm:text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
className="px-3 md:px-4 py-1.5 md:py-2 rounded-lg text-xs sm:text-xs border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5 md:w-4 md:h-4" />
|
<Plus className="w-3.5 h-3.5 md:w-4 md:h-4" />
|
||||||
<span>Add Provider</span>
|
<span>Add Connection</span>
|
||||||
</button>
|
</button>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && (
|
{open && (
|
||||||
@@ -119,8 +119,8 @@ const AddProvider = ({
|
|||||||
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
|
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
|
||||||
<div className="px-6 pt-6 pb-4">
|
<div className="px-6 pt-6 pb-4">
|
||||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
<h3 className="text-black/90 dark:text-white/90 font-medium text-sm">
|
||||||
Add new provider
|
Add new connection
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||||
@@ -128,7 +128,7 @@ const AddProvider = ({
|
|||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex flex-col items-start space-y-2">
|
<div className="flex flex-col items-start space-y-2">
|
||||||
<label className="text-xs text-black/70 dark:text-white/70">
|
<label className="text-xs text-black/70 dark:text-white/70">
|
||||||
Select provider type
|
Select connection type
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedProvider ?? ''}
|
value={selectedProvider ?? ''}
|
||||||
@@ -149,13 +149,13 @@ const AddProvider = ({
|
|||||||
className="flex flex-col items-start space-y-2"
|
className="flex flex-col items-start space-y-2"
|
||||||
>
|
>
|
||||||
<label className="text-xs text-black/70 dark:text-white/70">
|
<label className="text-xs text-black/70 dark:text-white/70">
|
||||||
Name*
|
Connection Name*
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder={'Provider Name'}
|
placeholder={'e.g., My OpenAI Connection'}
|
||||||
type="text"
|
type="text"
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
@@ -178,7 +178,7 @@ const AddProvider = ({
|
|||||||
[field.key]: event.target.value,
|
[field.key]: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder={
|
placeholder={
|
||||||
(field as StringUIConfigField).placeholder
|
(field as StringUIConfigField).placeholder
|
||||||
}
|
}
|
||||||
@@ -194,12 +194,12 @@ const AddProvider = ({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 rounded-lg text-sm bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
className="px-4 py-2 rounded-lg text-[13px] bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loader2 className="animate-spin" size={16} />
|
<Loader2 className="animate-spin" size={16} />
|
||||||
) : (
|
) : (
|
||||||
'Add Provider'
|
'Add Connection'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ const DeleteProvider = ({
|
|||||||
return prev.filter((p) => p.id !== modelProvider.id);
|
return prev.filter((p) => p.id !== modelProvider.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Provider deleted successfully.');
|
toast.success('Connection deleted successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting provider:', error);
|
console.error('Error deleting provider:', error);
|
||||||
toast.error('Failed to delete provider.');
|
toast.error('Failed to delete connection.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ const DeleteProvider = ({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group"
|
className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group"
|
||||||
title="Delete provider"
|
title="Delete connection"
|
||||||
>
|
>
|
||||||
<Trash2
|
<Trash2
|
||||||
size={14}
|
size={14}
|
||||||
@@ -76,14 +76,15 @@ const DeleteProvider = ({
|
|||||||
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
||||||
<div className="px-6 pt-6 pb-4">
|
<div className="px-6 pt-6 pb-4">
|
||||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
||||||
Delete provider
|
Delete connection
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
<p className="text-SM text-black/60 dark:text-white/60">
|
<p className="text-sm text-black/60 dark:text-white/60">
|
||||||
Are you sure you want to delete the provider "
|
Are you sure you want to delete the connection "
|
||||||
{modelProvider.name}"? This action cannot be undone.
|
{modelProvider.name}"? This action cannot be undone.
|
||||||
|
All associated models will also be removed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-6 py-6 flex justify-end space-x-2">
|
<div className="px-6 py-6 flex justify-end space-x-2">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { UIConfigField, ConfigModelProvider } from '@/lib/config/types';
|
import { UIConfigField, ConfigModelProvider } from '@/lib/config/types';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { AlertCircle, ChevronDown, Pencil, Trash2, X } from 'lucide-react';
|
import { AlertCircle, Plug2, Plus, Pencil, Trash2, X } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import AddModel from './AddModelDialog';
|
import AddModel from './AddModelDialog';
|
||||||
@@ -17,7 +17,7 @@ const ModelProvider = ({
|
|||||||
fields: UIConfigField[];
|
fields: UIConfigField[];
|
||||||
setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>;
|
setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>;
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
const handleModelDelete = async (
|
const handleModelDelete = async (
|
||||||
type: 'chat' | 'embedding',
|
type: 'chat' | 'embedding',
|
||||||
@@ -66,23 +66,35 @@ const ModelProvider = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modelCount =
|
||||||
|
modelProvider.chatModels.filter((m) => m.key !== 'error').length +
|
||||||
|
modelProvider.embeddingModels.filter((m) => m.key !== 'error').length;
|
||||||
|
const hasError =
|
||||||
|
modelProvider.chatModels.some((m) => m.key === 'error') ||
|
||||||
|
modelProvider.embeddingModels.some((m) => m.key === 'error');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={modelProvider.id}
|
key={modelProvider.id}
|
||||||
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden"
|
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden bg-light-primary dark:bg-dark-primary"
|
||||||
>
|
>
|
||||||
<div
|
<div className="px-5 py-3.5 flex flex-row justify-between w-full items-center border-b border-light-200 dark:border-dark-200 bg-light-secondary/30 dark:bg-dark-secondary/30">
|
||||||
className={cn(
|
<div className="flex items-center gap-2.5">
|
||||||
'group px-5 py-4 flex flex-row justify-between w-full cursor-pointer hover:bg-light-secondary hover:dark:bg-dark-secondary transition duration-200 items-center',
|
<div className="p-1.5 rounded-md bg-sky-500/10 dark:bg-sky-500/10">
|
||||||
!open && 'rounded-lg',
|
<Plug2 size={14} className="text-sky-500" />
|
||||||
)}
|
</div>
|
||||||
onClick={() => setOpen(!open)}
|
<div className="flex flex-col">
|
||||||
>
|
<p className="text-sm lg:text-sm text-black dark:text-white font-medium">
|
||||||
<p className="text-sm lg:text-base text-black dark:text-white font-medium">
|
|
||||||
{modelProvider.name}
|
{modelProvider.name}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-4">
|
{modelCount > 0 && (
|
||||||
<div className="flex flex-row items-center">
|
<p className="text-[10px] lg:text-[11px] text-black/50 dark:text-white/50">
|
||||||
|
{modelCount} model{modelCount !== 1 ? 's' : ''} configured
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center gap-1">
|
||||||
<UpdateProvider
|
<UpdateProvider
|
||||||
fields={fields}
|
fields={fields}
|
||||||
modelProvider={modelProvider}
|
modelProvider={modelProvider}
|
||||||
@@ -93,119 +105,118 @@ const ModelProvider = ({
|
|||||||
setProviders={setProviders}
|
setProviders={setProviders}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={cn(
|
|
||||||
open ? 'rotate-180' : '',
|
|
||||||
'transition duration-200 text-black/70 dark:text-white/70 group-hover:text-sky-500',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<AnimatePresence>
|
|
||||||
{open && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
|
||||||
exit={{ height: 0, opacity: 0 }}
|
|
||||||
transition={{ duration: 0.1 }}
|
|
||||||
>
|
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
|
||||||
<div className="flex flex-col gap-y-4 px-5 py-4">
|
<div className="flex flex-col gap-y-4 px-5 py-4">
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<div className="flex flex-row w-full justify-between items-center">
|
<div className="flex flex-row w-full justify-between items-center">
|
||||||
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70">
|
<p className="text-[11px] lg:text-[11px] font-medium text-black/70 dark:text-white/70 uppercase tracking-wide">
|
||||||
Chat models
|
Chat Models
|
||||||
</p>
|
</p>
|
||||||
|
{!modelProvider.chatModels.some((m) => m.key === 'error') && (
|
||||||
<AddModel
|
<AddModel
|
||||||
providerId={modelProvider.id}
|
providerId={modelProvider.id}
|
||||||
setProviders={setProviders}
|
setProviders={setProviders}
|
||||||
type="chat"
|
type="chat"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
|
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
|
||||||
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
<div className="flex flex-row items-center gap-2 text-xs lg:text-xs text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||||
<AlertCircle size={16} className="shrink-0" />
|
<AlertCircle size={16} className="shrink-0" />
|
||||||
<span className="break-words">
|
<span className="break-words">
|
||||||
{
|
{
|
||||||
modelProvider.chatModels.find(
|
modelProvider.chatModels.find((m) => m.key === 'error')
|
||||||
(m) => m.key === 'error',
|
?.name
|
||||||
)?.name
|
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : modelProvider.chatModels.filter((m) => m.key !== 'error')
|
||||||
|
.length === 0 && !hasError ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-4 px-4 rounded-lg border-2 border-dashed border-light-200 dark:border-dark-200 bg-light-secondary/20 dark:bg-dark-secondary/20">
|
||||||
|
<p className="text-xs text-black/50 dark:text-white/50 text-center">
|
||||||
|
No chat models configured
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : modelProvider.chatModels.filter((m) => m.key !== 'error')
|
||||||
|
.length > 0 ? (
|
||||||
<div className="flex flex-row flex-wrap gap-2">
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
{modelProvider.chatModels.map((model, index) => (
|
{modelProvider.chatModels.map((model, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${modelProvider.id}-chat-${model.key}-${index}`}
|
key={`${modelProvider.id}-chat-${model.key}-${index}`}
|
||||||
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
className="flex flex-row items-center space-x-1.5 text-xs lg:text-xs text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5 border border-light-200 dark:border-dark-200"
|
||||||
>
|
>
|
||||||
<span>{model.name}</span>
|
<span>{model.name}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleModelDelete('chat', model.key);
|
handleModelDelete('chat', model.key);
|
||||||
}}
|
}}
|
||||||
|
className="hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<div className="flex flex-row w-full justify-between items-center">
|
<div className="flex flex-row w-full justify-between items-center">
|
||||||
<p className="text-[11px] lg:text-xs text-black/70 dark:text-white/70">
|
<p className="text-[11px] lg:text-[11px] font-medium text-black/70 dark:text-white/70 uppercase tracking-wide">
|
||||||
Embedding models
|
Embedding Models
|
||||||
</p>
|
</p>
|
||||||
|
{!modelProvider.embeddingModels.some((m) => m.key === 'error') && (
|
||||||
<AddModel
|
<AddModel
|
||||||
providerId={modelProvider.id}
|
providerId={modelProvider.id}
|
||||||
setProviders={setProviders}
|
setProviders={setProviders}
|
||||||
type="embedding"
|
type="embedding"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{modelProvider.embeddingModels.some(
|
{modelProvider.embeddingModels.some((m) => m.key === 'error') ? (
|
||||||
(m) => m.key === 'error',
|
<div className="flex flex-row items-center gap-2 text-xs lg:text-xs text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||||
) ? (
|
|
||||||
<div className="flex flex-row items-center gap-2 text-xs lg:text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
|
||||||
<AlertCircle size={16} className="shrink-0" />
|
<AlertCircle size={16} className="shrink-0" />
|
||||||
<span className="break-words">
|
<span className="break-words">
|
||||||
{
|
{
|
||||||
modelProvider.embeddingModels.find(
|
modelProvider.embeddingModels.find((m) => m.key === 'error')
|
||||||
(m) => m.key === 'error',
|
?.name
|
||||||
)?.name
|
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : modelProvider.embeddingModels.filter((m) => m.key !== 'error')
|
||||||
|
.length === 0 && !hasError ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-4 px-4 rounded-lg border-2 border-dashed border-light-200 dark:border-dark-200 bg-light-secondary/20 dark:bg-dark-secondary/20">
|
||||||
|
<p className="text-xs text-black/50 dark:text-white/50 text-center">
|
||||||
|
No embedding models configured
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : modelProvider.embeddingModels.filter((m) => m.key !== 'error')
|
||||||
|
.length > 0 ? (
|
||||||
<div className="flex flex-row flex-wrap gap-2">
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
{modelProvider.embeddingModels.map((model, index) => (
|
{modelProvider.embeddingModels.map((model, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
|
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
|
||||||
className="flex flex-row items-center space-x-1 text-xs lg:text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
className="flex flex-row items-center space-x-1.5 text-xs lg:text-xs text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5 border border-light-200 dark:border-dark-200"
|
||||||
>
|
>
|
||||||
<span>{model.name}</span>
|
<span>{model.name}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleModelDelete('embedding', model.key);
|
handleModelDelete('embedding', model.key);
|
||||||
}}
|
}}
|
||||||
|
className="hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ const ModelSelect = ({
|
|||||||
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
||||||
<div className="space-y-3 lg:space-y-5">
|
<div className="space-y-3 lg:space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm lg:text-base text-black dark:text-white">
|
<h4 className="text-sm lg:text-sm text-black dark:text-white">
|
||||||
Select {type === 'chat' ? 'Chat Model' : 'Embedding Model'}
|
Select {type === 'chat' ? 'Chat Model' : 'Embedding Model'}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||||
{type === 'chat'
|
{type === 'chat'
|
||||||
? 'Select the model to use for chat responses'
|
? 'Choose which model to use for generating responses'
|
||||||
: 'Select the model to use for embeddings'}
|
: 'Choose which model to use for generating embeddings'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
@@ -86,7 +86,7 @@ const ModelSelect = ({
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="!text-xs lg:!text-sm"
|
className="!text-xs lg:!text-[13px]"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const Models = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 space-y-6 overflow-y-auto py-6">
|
<div className="flex-1 space-y-6 overflow-y-auto py-6">
|
||||||
<div className="flex flex-col px-6 gap-y-4">
|
<div className="flex flex-col px-6 gap-y-4">
|
||||||
<h3 className="text-xs lg:text-sm text-black/70 dark:text-white/70">
|
<h3 className="text-xs lg:text-xs text-black/70 dark:text-white/70">
|
||||||
Select models
|
Select models
|
||||||
</h3>
|
</h3>
|
||||||
<ModelSelect
|
<ModelSelect
|
||||||
@@ -38,13 +38,40 @@ const Models = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||||
<div className="flex flex-row justify-between items-center px-6 ">
|
<div className="flex flex-row justify-between items-center px-6 ">
|
||||||
<p className="text-xs lg:text-sm text-black/70 dark:text-white/70">
|
<p className="text-xs lg:text-xs text-black/70 dark:text-white/70">
|
||||||
Manage model provider
|
Manage connections
|
||||||
</p>
|
</p>
|
||||||
<AddProvider modelProviders={fields} setProviders={setProviders} />
|
<AddProvider modelProviders={fields} setProviders={setProviders} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col px-6 gap-y-4">
|
<div className="flex flex-col px-6 gap-y-4">
|
||||||
{providers.map((provider) => (
|
{providers.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 px-4 rounded-lg border-2 border-dashed border-light-200 dark:border-dark-200 bg-light-secondary/10 dark:bg-dark-secondary/10">
|
||||||
|
<div className="p-3 rounded-full bg-sky-500/10 dark:bg-sky-500/10 mb-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-8 h-8 text-sky-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-black/70 dark:text-white/70 mb-1">
|
||||||
|
No connections yet
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-black/50 dark:text-white/50 text-center max-w-sm mb-4">
|
||||||
|
Add your first connection to start using AI models. Connect to
|
||||||
|
OpenAI, Anthropic, Ollama, and more.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
providers.map((provider) => (
|
||||||
<ModelProvider
|
<ModelProvider
|
||||||
key={`provider-${provider.id}`}
|
key={`provider-${provider.id}`}
|
||||||
fields={
|
fields={
|
||||||
@@ -54,7 +81,8 @@ const Models = ({
|
|||||||
modelProvider={provider}
|
modelProvider={provider}
|
||||||
setProviders={setProviders}
|
setProviders={setProviders}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ const UpdateProvider = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Provider updated successfully.');
|
toast.success('Connection updated successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating provider:', error);
|
console.error('Error updating provider:', error);
|
||||||
toast.error('Failed to update provider.');
|
toast.error('Failed to update connection.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -109,8 +109,8 @@ const UpdateProvider = ({
|
|||||||
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
|
<form onSubmit={handleSubmit} className="flex flex-col flex-1">
|
||||||
<div className="px-6 pt-6 pb-4">
|
<div className="px-6 pt-6 pb-4">
|
||||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
<h3 className="text-black/90 dark:text-white/90 font-medium text-sm">
|
||||||
Update provider
|
Update connection
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||||
@@ -121,13 +121,13 @@ const UpdateProvider = ({
|
|||||||
className="flex flex-col items-start space-y-2"
|
className="flex flex-col items-start space-y-2"
|
||||||
>
|
>
|
||||||
<label className="text-xs text-black/70 dark:text-white/70">
|
<label className="text-xs text-black/70 dark:text-white/70">
|
||||||
Name*
|
Connection Name*
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(event) => setName(event.target.value)}
|
onChange={(event) => setName(event.target.value)}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder={'Provider Name'}
|
placeholder={'Connection Name'}
|
||||||
type="text"
|
type="text"
|
||||||
required={true}
|
required={true}
|
||||||
/>
|
/>
|
||||||
@@ -150,7 +150,7 @@ const UpdateProvider = ({
|
|||||||
[field.key]: event.target.value,
|
[field.key]: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-4 py-3 pr-10 text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder={
|
placeholder={
|
||||||
(field as StringUIConfigField).placeholder
|
(field as StringUIConfigField).placeholder
|
||||||
}
|
}
|
||||||
@@ -166,12 +166,12 @@ const UpdateProvider = ({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 rounded-lg text-sm bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
className="px-4 py-2 rounded-lg text-[13px] bg-sky-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loader2 className="animate-spin" size={16} />
|
<Loader2 className="animate-spin" size={16} />
|
||||||
) : (
|
) : (
|
||||||
'Update Provider'
|
'Update Connection'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
29
src/components/Settings/Sections/Personalization.tsx
Normal file
29
src/components/Settings/Sections/Personalization.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
|
import SettingsField from '../SettingsField';
|
||||||
|
|
||||||
|
const Personalization = ({
|
||||||
|
fields,
|
||||||
|
values,
|
||||||
|
}: {
|
||||||
|
fields: UIConfigField[];
|
||||||
|
values: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-6">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<SettingsField
|
||||||
|
key={field.key}
|
||||||
|
field={field}
|
||||||
|
value={
|
||||||
|
(field.scope === 'client'
|
||||||
|
? localStorage.getItem(field.key)
|
||||||
|
: values[field.key]) ?? field.default
|
||||||
|
}
|
||||||
|
dataAdd="personalization"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Personalization;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
import SettingsField from '../SettingsField';
|
import SettingsField from '../SettingsField';
|
||||||
|
|
||||||
const General = ({
|
const Preferences = ({
|
||||||
fields,
|
fields,
|
||||||
values,
|
values,
|
||||||
}: {
|
}: {
|
||||||
@@ -19,11 +19,11 @@ const General = ({
|
|||||||
? localStorage.getItem(field.key)
|
? localStorage.getItem(field.key)
|
||||||
: values[field.key]) ?? field.default
|
: values[field.key]) ?? field.default
|
||||||
}
|
}
|
||||||
dataAdd="general"
|
dataAdd="preferences"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default General;
|
export default Preferences;
|
||||||
@@ -4,9 +4,10 @@ import {
|
|||||||
BrainCog,
|
BrainCog,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Sliders,
|
||||||
|
ToggleRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import General from './Sections/General';
|
import Preferences from './Sections/Preferences';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -15,20 +16,29 @@ import { cn } from '@/lib/utils';
|
|||||||
import Models from './Sections/Models/Section';
|
import Models from './Sections/Models/Section';
|
||||||
import SearchSection from './Sections/Search';
|
import SearchSection from './Sections/Search';
|
||||||
import Select from '@/components/ui/Select';
|
import Select from '@/components/ui/Select';
|
||||||
|
import Personalization from './Sections/Personalization';
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
key: 'general',
|
key: 'preferences',
|
||||||
name: 'General',
|
name: 'Preferences',
|
||||||
description: 'Adjust common settings.',
|
description: 'Customize your application preferences.',
|
||||||
icon: Settings,
|
icon: Sliders,
|
||||||
component: General,
|
component: Preferences,
|
||||||
dataAdd: 'general',
|
dataAdd: 'preferences',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'personalization',
|
||||||
|
name: 'Personalization',
|
||||||
|
description: 'Customize the behavior and tone of the model.',
|
||||||
|
icon: ToggleRight,
|
||||||
|
component: Personalization,
|
||||||
|
dataAdd: 'personalization',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'models',
|
key: 'models',
|
||||||
name: 'Models',
|
name: 'Models',
|
||||||
description: 'Configure model settings.',
|
description: 'Connect to AI services and manage connections.',
|
||||||
icon: BrainCog,
|
icon: BrainCog,
|
||||||
component: Models,
|
component: Models,
|
||||||
dataAdd: 'modelProviders',
|
dataAdd: 'modelProviders',
|
||||||
@@ -166,7 +176,7 @@ const SettingsDialogue = ({
|
|||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<div className="border-b border-light-200/60 px-6 pb-6 lg:pt-6 dark:border-dark-200/60 flex-shrink-0">
|
<div className="border-b border-light-200/60 px-6 pb-6 lg:pt-6 dark:border-dark-200/60 flex-shrink-0">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h4 className="font-medium text-black dark:text-white text-sm lg:text-base">
|
<h4 className="font-medium text-black dark:text-white text-sm lg:text-sm">
|
||||||
{selectedSection.name}
|
{selectedSection.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
SelectUIConfigField,
|
SelectUIConfigField,
|
||||||
StringUIConfigField,
|
StringUIConfigField,
|
||||||
|
SwitchUIConfigField,
|
||||||
TextareaUIConfigField,
|
TextareaUIConfigField,
|
||||||
UIConfigField,
|
UIConfigField,
|
||||||
} from '@/lib/config/types';
|
} from '@/lib/config/types';
|
||||||
@@ -9,6 +10,13 @@ import Select from '../ui/Select';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { Switch } from '@headlessui/react';
|
||||||
|
|
||||||
|
const emitClientConfigChanged = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new Event('client-config-changed'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsSelect = ({
|
const SettingsSelect = ({
|
||||||
field,
|
field,
|
||||||
@@ -33,6 +41,7 @@ const SettingsSelect = ({
|
|||||||
if (field.key === 'theme') {
|
if (field.key === 'theme') {
|
||||||
setTheme(newValue);
|
setTheme(newValue);
|
||||||
}
|
}
|
||||||
|
emitClientConfigChanged();
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch('/api/config', {
|
const res = await fetch('/api/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -62,7 +71,7 @@ const SettingsSelect = ({
|
|||||||
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
||||||
<div className="space-y-3 lg:space-y-5">
|
<div className="space-y-3 lg:space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm lg:text-base text-black dark:text-white">
|
<h4 className="text-sm lg:text-sm text-black dark:text-white">
|
||||||
{field.name}
|
{field.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||||
@@ -104,6 +113,7 @@ const SettingsInput = ({
|
|||||||
try {
|
try {
|
||||||
if (field.scope === 'client') {
|
if (field.scope === 'client') {
|
||||||
localStorage.setItem(field.key, newValue);
|
localStorage.setItem(field.key, newValue);
|
||||||
|
emitClientConfigChanged();
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch('/api/config', {
|
const res = await fetch('/api/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -133,7 +143,7 @@ const SettingsInput = ({
|
|||||||
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
||||||
<div className="space-y-3 lg:space-y-5">
|
<div className="space-y-3 lg:space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm lg:text-base text-black dark:text-white">
|
<h4 className="text-sm lg:text-sm text-black dark:text-white">
|
||||||
{field.name}
|
{field.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||||
@@ -145,7 +155,7 @@ const SettingsInput = ({
|
|||||||
value={value ?? field.default ?? ''}
|
value={value ?? field.default ?? ''}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={(event) => handleSave(event.target.value)}
|
onBlur={(event) => handleSave(event.target.value)}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
type="text"
|
type="text"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -180,6 +190,7 @@ const SettingsTextarea = ({
|
|||||||
try {
|
try {
|
||||||
if (field.scope === 'client') {
|
if (field.scope === 'client') {
|
||||||
localStorage.setItem(field.key, newValue);
|
localStorage.setItem(field.key, newValue);
|
||||||
|
emitClientConfigChanged();
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch('/api/config', {
|
const res = await fetch('/api/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -209,7 +220,7 @@ const SettingsTextarea = ({
|
|||||||
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
||||||
<div className="space-y-3 lg:space-y-5">
|
<div className="space-y-3 lg:space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm lg:text-base text-black dark:text-white">
|
<h4 className="text-sm lg:text-sm text-black dark:text-white">
|
||||||
{field.name}
|
{field.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||||
@@ -221,7 +232,7 @@ const SettingsTextarea = ({
|
|||||||
value={value ?? field.default ?? ''}
|
value={value ?? field.default ?? ''}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={(event) => handleSave(event.target.value)}
|
onBlur={(event) => handleSave(event.target.value)}
|
||||||
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-sm text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
className="w-full rounded-lg border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary px-3 py-2 lg:px-4 lg:py-3 pr-10 !text-xs lg:!text-[13px] text-black/80 dark:text-white/80 placeholder:text-black/40 dark:placeholder:text-white/40 focus-visible:outline-none focus-visible:border-light-300 dark:focus-visible:border-dark-300 transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
rows={4}
|
rows={4}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -237,6 +248,80 @@ const SettingsTextarea = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SettingsSwitch = ({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
dataAdd,
|
||||||
|
}: {
|
||||||
|
field: SwitchUIConfigField;
|
||||||
|
value?: any;
|
||||||
|
setValue: (value: any) => void;
|
||||||
|
dataAdd: string;
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = async (newValue: boolean) => {
|
||||||
|
setLoading(true);
|
||||||
|
setValue(newValue);
|
||||||
|
try {
|
||||||
|
if (field.scope === 'client') {
|
||||||
|
localStorage.setItem(field.key, String(newValue));
|
||||||
|
emitClientConfigChanged();
|
||||||
|
} else {
|
||||||
|
const res = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: `${dataAdd}.${field.key}`,
|
||||||
|
value: newValue,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Failed to save config:', await res.text());
|
||||||
|
throw new Error('Failed to save configuration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving config:', error);
|
||||||
|
toast.error('Failed to save configuration.');
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => setLoading(false), 150);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChecked = value === true || value === 'true';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-xl border border-light-200 bg-light-primary/80 p-4 lg:p-6 transition-colors dark:border-dark-200 dark:bg-dark-primary/80">
|
||||||
|
<div className="flex flex-row items-center space-x-3 lg:space-x-5 w-full justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm lg:text-sm text-black dark:text-white">
|
||||||
|
{field.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[11px] lg:text-xs text-black/50 dark:text-white/50">
|
||||||
|
{field.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className="group relative flex h-6 w-12 shrink-0 cursor-pointer rounded-full bg-white/10 p-1 duration-200 ease-in-out focus:outline-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed data-[checked]:bg-sky-500"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none inline-block size-4 translate-x-0 rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out group-data-[checked]:translate-x-6"
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsField = ({
|
const SettingsField = ({
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
@@ -276,6 +361,15 @@ const SettingsField = ({
|
|||||||
dataAdd={dataAdd}
|
dataAdd={dataAdd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'switch':
|
||||||
|
return (
|
||||||
|
<SettingsSwitch
|
||||||
|
field={field}
|
||||||
|
value={val}
|
||||||
|
setValue={setVal}
|
||||||
|
dataAdd={dataAdd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <div>Unsupported field type: {field.type}</div>;
|
return <div>Unsupported field type: {field.type}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ const SetupConfig = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasProviders = providers.length > 0;
|
const visibleProviders = providers.filter(
|
||||||
|
(p) => p.name.toLowerCase() !== 'transformers',
|
||||||
|
);
|
||||||
|
const hasProviders =
|
||||||
|
visibleProviders.filter((p) => p.chatModels.length > 0).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[95vw] md:w-[80vw] lg:w-[65vw] mx-auto px-2 sm:px-4 md:px-6 flex flex-col space-y-6">
|
<div className="w-[95vw] md:w-[80vw] lg:w-[65vw] mx-auto px-2 sm:px-4 md:px-6 flex flex-col space-y-6">
|
||||||
@@ -81,10 +85,10 @@ const SetupConfig = ({
|
|||||||
<div className="flex flex-row justify-between items-center mb-4 md:mb-6 pb-3 md:pb-4 border-b border-light-200 dark:border-dark-200">
|
<div className="flex flex-row justify-between items-center mb-4 md:mb-6 pb-3 md:pb-4 border-b border-light-200 dark:border-dark-200">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs sm:text-sm font-medium text-black dark:text-white">
|
<p className="text-xs sm:text-sm font-medium text-black dark:text-white">
|
||||||
Manage Providers
|
Manage Connections
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-0.5">
|
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-0.5">
|
||||||
Add and configure your model providers
|
Add connections to access AI models
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AddProvider
|
<AddProvider
|
||||||
@@ -100,14 +104,17 @@ const SetupConfig = ({
|
|||||||
Loading providers...
|
Loading providers...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : providers.length === 0 ? (
|
) : visibleProviders.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-8 md:py-12 text-center">
|
<div className="flex flex-col items-center justify-center py-8 md:py-12 text-center">
|
||||||
<p className="text-xs sm:text-sm font-medium text-black/70 dark:text-white/70">
|
<p className="text-xs sm:text-sm font-medium text-black/70 dark:text-white/70">
|
||||||
No providers configured
|
No connections configured
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-1">
|
||||||
|
Click "Add Connection" above to get started
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
providers.map((provider) => (
|
visibleProviders.map((provider) => (
|
||||||
<ModelProvider
|
<ModelProvider
|
||||||
key={`provider-${provider.id}`}
|
key={`provider-${provider.id}`}
|
||||||
fields={
|
fields={
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const WeatherWidget = () => {
|
|||||||
setData({
|
setData({
|
||||||
temperature: data.temperature,
|
temperature: data.temperature,
|
||||||
condition: data.condition,
|
condition: data.condition,
|
||||||
location: location.city,
|
location: 'Mars',
|
||||||
humidity: data.humidity,
|
humidity: data.humidity,
|
||||||
windSpeed: data.windSpeed,
|
windSpeed: data.windSpeed,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
|
|||||||
54
src/components/Widgets/Calculation.tsx
Normal file
54
src/components/Widgets/Calculation.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Calculator, Equal } from 'lucide-react';
|
||||||
|
|
||||||
|
type CalculationWidgetProps = {
|
||||||
|
expression: string;
|
||||||
|
result: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Calculation = ({ expression, result }: CalculationWidgetProps) => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 overflow-hidden shadow-sm">
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-light-100/50 dark:bg-dark-100/50 border-b border-light-200 dark:border-dark-200">
|
||||||
|
<div className="rounded-full p-1.5 bg-light-100 dark:bg-dark-100">
|
||||||
|
<Calculator className="w-4 h-4 text-black/70 dark:text-white/70" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-black dark:text-white">
|
||||||
|
Calculation
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50 font-medium">
|
||||||
|
Expression
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-light-100 dark:bg-dark-100 rounded-md p-2.5 border border-light-200 dark:border-dark-200">
|
||||||
|
<code className="text-sm text-black dark:text-white font-mono break-all">
|
||||||
|
{expression}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
|
<Equal className="w-3.5 h-3.5 text-black/50 dark:text-white/50" />
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50 font-medium">
|
||||||
|
Result
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gradient-to-br from-light-100 to-light-secondary dark:from-dark-100 dark:to-dark-secondary rounded-md p-4 border-2 border-light-200 dark:border-dark-200">
|
||||||
|
<div className="text-4xl font-bold text-black dark:text-white font-mono tabular-nums">
|
||||||
|
{result.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Calculation;
|
||||||
76
src/components/Widgets/Renderer.tsx
Normal file
76
src/components/Widgets/Renderer.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Widget } from '../ChatWindow';
|
||||||
|
import Weather from './Weather';
|
||||||
|
import Calculation from './Calculation';
|
||||||
|
import Stock from './Stock';
|
||||||
|
|
||||||
|
const Renderer = ({ widgets }: { widgets: Widget[] }) => {
|
||||||
|
return widgets.map((widget, index) => {
|
||||||
|
switch (widget.widgetType) {
|
||||||
|
case 'weather':
|
||||||
|
return (
|
||||||
|
<Weather
|
||||||
|
key={index}
|
||||||
|
location={widget.params.location}
|
||||||
|
current={widget.params.current}
|
||||||
|
daily={widget.params.daily}
|
||||||
|
timezone={widget.params.timezone}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'calculation_result':
|
||||||
|
return (
|
||||||
|
<Calculation
|
||||||
|
expression={widget.params.expression}
|
||||||
|
result={widget.params.result}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'stock':
|
||||||
|
return (
|
||||||
|
<Stock
|
||||||
|
key={index}
|
||||||
|
symbol={widget.params.symbol}
|
||||||
|
shortName={widget.params.shortName}
|
||||||
|
longName={widget.params.longName}
|
||||||
|
exchange={widget.params.exchange}
|
||||||
|
currency={widget.params.currency}
|
||||||
|
marketState={widget.params.marketState}
|
||||||
|
regularMarketPrice={widget.params.regularMarketPrice}
|
||||||
|
regularMarketChange={widget.params.regularMarketChange}
|
||||||
|
regularMarketChangePercent={
|
||||||
|
widget.params.regularMarketChangePercent
|
||||||
|
}
|
||||||
|
regularMarketPreviousClose={
|
||||||
|
widget.params.regularMarketPreviousClose
|
||||||
|
}
|
||||||
|
regularMarketOpen={widget.params.regularMarketOpen}
|
||||||
|
regularMarketDayHigh={widget.params.regularMarketDayHigh}
|
||||||
|
regularMarketDayLow={widget.params.regularMarketDayLow}
|
||||||
|
regularMarketVolume={widget.params.regularMarketVolume}
|
||||||
|
averageDailyVolume3Month={widget.params.averageDailyVolume3Month}
|
||||||
|
marketCap={widget.params.marketCap}
|
||||||
|
fiftyTwoWeekLow={widget.params.fiftyTwoWeekLow}
|
||||||
|
fiftyTwoWeekHigh={widget.params.fiftyTwoWeekHigh}
|
||||||
|
trailingPE={widget.params.trailingPE}
|
||||||
|
forwardPE={widget.params.forwardPE}
|
||||||
|
dividendYield={widget.params.dividendYield}
|
||||||
|
earningsPerShare={widget.params.earningsPerShare}
|
||||||
|
website={widget.params.website}
|
||||||
|
postMarketPrice={widget.params.postMarketPrice}
|
||||||
|
postMarketChange={widget.params.postMarketChange}
|
||||||
|
postMarketChangePercent={widget.params.postMarketChangePercent}
|
||||||
|
preMarketPrice={widget.params.preMarketPrice}
|
||||||
|
preMarketChange={widget.params.preMarketChange}
|
||||||
|
preMarketChangePercent={widget.params.preMarketChangePercent}
|
||||||
|
chartData={widget.params.chartData}
|
||||||
|
comparisonData={widget.params.comparisonData}
|
||||||
|
error={widget.params.error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div key={index}>Unknown widget type: {widget.widgetType}</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Renderer;
|
||||||
517
src/components/Widgets/Stock.tsx
Normal file
517
src/components/Widgets/Stock.tsx
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Clock, ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
createChart,
|
||||||
|
ColorType,
|
||||||
|
LineStyle,
|
||||||
|
BaselineSeries,
|
||||||
|
LineSeries,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
|
type StockWidgetProps = {
|
||||||
|
symbol: string;
|
||||||
|
shortName: string;
|
||||||
|
longName?: string;
|
||||||
|
exchange?: string;
|
||||||
|
currency?: string;
|
||||||
|
marketState?: string;
|
||||||
|
regularMarketPrice?: number;
|
||||||
|
regularMarketChange?: number;
|
||||||
|
regularMarketChangePercent?: number;
|
||||||
|
regularMarketPreviousClose?: number;
|
||||||
|
regularMarketOpen?: number;
|
||||||
|
regularMarketDayHigh?: number;
|
||||||
|
regularMarketDayLow?: number;
|
||||||
|
regularMarketVolume?: number;
|
||||||
|
averageDailyVolume3Month?: number;
|
||||||
|
marketCap?: number;
|
||||||
|
fiftyTwoWeekLow?: number;
|
||||||
|
fiftyTwoWeekHigh?: number;
|
||||||
|
trailingPE?: number;
|
||||||
|
forwardPE?: number;
|
||||||
|
dividendYield?: number;
|
||||||
|
earningsPerShare?: number;
|
||||||
|
website?: string;
|
||||||
|
postMarketPrice?: number;
|
||||||
|
postMarketChange?: number;
|
||||||
|
postMarketChangePercent?: number;
|
||||||
|
preMarketPrice?: number;
|
||||||
|
preMarketChange?: number;
|
||||||
|
preMarketChangePercent?: number;
|
||||||
|
chartData?: {
|
||||||
|
'1D'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'5D'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'1M'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'3M'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'6M'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'1Y'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
MAX?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
} | null;
|
||||||
|
comparisonData?: Array<{
|
||||||
|
ticker: string;
|
||||||
|
name: string;
|
||||||
|
chartData: {
|
||||||
|
'1D'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'5D'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'1M'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'3M'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'6M'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
'1Y'?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
MAX?: { timestamps: number[]; prices: number[] } | null;
|
||||||
|
};
|
||||||
|
}> | null;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumber = (num: number | undefined, decimals = 2): string => {
|
||||||
|
if (num === undefined || num === null) return 'N/A';
|
||||||
|
return num.toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: decimals,
|
||||||
|
maximumFractionDigits: decimals,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLargeNumber = (num: number | undefined): string => {
|
||||||
|
if (num === undefined || num === null) return 'N/A';
|
||||||
|
if (num >= 1e12) return `$${(num / 1e12).toFixed(2)}T`;
|
||||||
|
if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
|
||||||
|
if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
|
||||||
|
if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
|
||||||
|
return `$${num.toFixed(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Stock = (props: StockWidgetProps) => {
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
const [selectedTimeframe, setSelectedTimeframe] = useState<
|
||||||
|
'1D' | '5D' | '1M' | '3M' | '6M' | '1Y' | 'MAX'
|
||||||
|
>('1M');
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkDarkMode = () => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
checkDarkMode();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(checkDarkMode);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentChartData = props.chartData?.[selectedTimeframe];
|
||||||
|
if (
|
||||||
|
!chartContainerRef.current ||
|
||||||
|
!currentChartData ||
|
||||||
|
currentChartData.timestamps.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chart = createChart(chartContainerRef.current, {
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
height: 280,
|
||||||
|
layout: {
|
||||||
|
background: { type: ColorType.Solid, color: 'transparent' },
|
||||||
|
textColor: isDarkMode ? '#6b7280' : '#9ca3af',
|
||||||
|
fontSize: 11,
|
||||||
|
attributionLogo: false,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: {
|
||||||
|
color: isDarkMode ? '#21262d' : '#e8edf1',
|
||||||
|
style: LineStyle.Solid,
|
||||||
|
},
|
||||||
|
horzLines: {
|
||||||
|
color: isDarkMode ? '#21262d' : '#e8edf1',
|
||||||
|
style: LineStyle.Solid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crosshair: {
|
||||||
|
vertLine: {
|
||||||
|
color: isDarkMode ? '#30363d' : '#d0d7de',
|
||||||
|
labelVisible: false,
|
||||||
|
},
|
||||||
|
horzLine: {
|
||||||
|
color: isDarkMode ? '#30363d' : '#d0d7de',
|
||||||
|
labelVisible: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
borderVisible: false,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
leftPriceScale: {
|
||||||
|
borderVisible: false,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
timeScale: {
|
||||||
|
borderVisible: false,
|
||||||
|
timeVisible: false,
|
||||||
|
},
|
||||||
|
handleScroll: false,
|
||||||
|
handleScale: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prices = currentChartData.prices;
|
||||||
|
let baselinePrice: number;
|
||||||
|
|
||||||
|
if (selectedTimeframe === '1D') {
|
||||||
|
baselinePrice = props.regularMarketPreviousClose ?? prices[0];
|
||||||
|
} else {
|
||||||
|
baselinePrice = prices[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const baselineSeries = chart.addSeries(BaselineSeries);
|
||||||
|
|
||||||
|
baselineSeries.applyOptions({
|
||||||
|
baseValue: { type: 'price', price: baselinePrice },
|
||||||
|
topLineColor: isDarkMode ? '#14b8a6' : '#0d9488',
|
||||||
|
topFillColor1: isDarkMode
|
||||||
|
? 'rgba(20, 184, 166, 0.28)'
|
||||||
|
: 'rgba(13, 148, 136, 0.24)',
|
||||||
|
topFillColor2: isDarkMode
|
||||||
|
? 'rgba(20, 184, 166, 0.05)'
|
||||||
|
: 'rgba(13, 148, 136, 0.05)',
|
||||||
|
bottomLineColor: isDarkMode ? '#f87171' : '#dc2626',
|
||||||
|
bottomFillColor1: isDarkMode
|
||||||
|
? 'rgba(248, 113, 113, 0.05)'
|
||||||
|
: 'rgba(220, 38, 38, 0.05)',
|
||||||
|
bottomFillColor2: isDarkMode
|
||||||
|
? 'rgba(248, 113, 113, 0.28)'
|
||||||
|
: 'rgba(220, 38, 38, 0.24)',
|
||||||
|
lineWidth: 2,
|
||||||
|
crosshairMarkerVisible: true,
|
||||||
|
crosshairMarkerRadius: 4,
|
||||||
|
crosshairMarkerBorderColor: '',
|
||||||
|
crosshairMarkerBackgroundColor: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = currentChartData.timestamps.map((timestamp, index) => {
|
||||||
|
const price = currentChartData.prices[index];
|
||||||
|
return {
|
||||||
|
time: (timestamp / 1000) as any,
|
||||||
|
value: price,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
baselineSeries.setData(data);
|
||||||
|
|
||||||
|
const comparisonColors = ['#8b5cf6', '#f59e0b', '#ec4899'];
|
||||||
|
if (props.comparisonData && props.comparisonData.length > 0) {
|
||||||
|
props.comparisonData.forEach((comp, index) => {
|
||||||
|
const compChartData = comp.chartData[selectedTimeframe];
|
||||||
|
if (compChartData && compChartData.prices.length > 0) {
|
||||||
|
const compData = compChartData.timestamps.map((timestamp, i) => ({
|
||||||
|
time: (timestamp / 1000) as any,
|
||||||
|
value: compChartData.prices[i],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const compSeries = chart.addSeries(LineSeries);
|
||||||
|
compSeries.applyOptions({
|
||||||
|
color: comparisonColors[index] || '#6b7280',
|
||||||
|
lineWidth: 2,
|
||||||
|
crosshairMarkerVisible: true,
|
||||||
|
crosshairMarkerRadius: 4,
|
||||||
|
priceScaleId: 'left',
|
||||||
|
});
|
||||||
|
compSeries.setData(compData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (chartContainerRef.current) {
|
||||||
|
chart.applyOptions({
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
props.chartData,
|
||||||
|
props.comparisonData,
|
||||||
|
selectedTimeframe,
|
||||||
|
isDarkMode,
|
||||||
|
props.regularMarketPreviousClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isPositive = (props.regularMarketChange ?? 0) >= 0;
|
||||||
|
const isMarketOpen = props.marketState === 'REGULAR';
|
||||||
|
const isPreMarket = props.marketState === 'PRE';
|
||||||
|
const isPostMarket = props.marketState === 'POST';
|
||||||
|
|
||||||
|
const displayPrice = isPostMarket
|
||||||
|
? props.postMarketPrice ?? props.regularMarketPrice
|
||||||
|
: isPreMarket
|
||||||
|
? props.preMarketPrice ?? props.regularMarketPrice
|
||||||
|
: props.regularMarketPrice;
|
||||||
|
|
||||||
|
const displayChange = isPostMarket
|
||||||
|
? props.postMarketChange ?? props.regularMarketChange
|
||||||
|
: isPreMarket
|
||||||
|
? props.preMarketChange ?? props.regularMarketChange
|
||||||
|
: props.regularMarketChange;
|
||||||
|
|
||||||
|
const displayChangePercent = isPostMarket
|
||||||
|
? props.postMarketChangePercent ?? props.regularMarketChangePercent
|
||||||
|
: isPreMarket
|
||||||
|
? props.preMarketChangePercent ?? props.regularMarketChangePercent
|
||||||
|
: props.regularMarketChangePercent;
|
||||||
|
|
||||||
|
const changeColor = isPositive
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-red-600 dark:text-red-400';
|
||||||
|
|
||||||
|
if (props.error) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-4">
|
||||||
|
<p className="text-sm text-black dark:text-white">
|
||||||
|
Error: {props.error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-light-200 dark:border-dark-200 overflow-hidden">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="flex items-start justify-between gap-4 pb-4 border-b border-light-200 dark:border-dark-200">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
{props.website && (
|
||||||
|
<img
|
||||||
|
src={`https://logo.clearbit.com/${new URL(props.website).hostname}`}
|
||||||
|
alt={`${props.symbol} logo`}
|
||||||
|
className="w-8 h-8 rounded-lg"
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h3 className="text-2xl font-bold text-black dark:text-white">
|
||||||
|
{props.symbol}
|
||||||
|
</h3>
|
||||||
|
{props.exchange && (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-medium rounded bg-light-100 dark:bg-dark-100 text-black/60 dark:text-white/60">
|
||||||
|
{props.exchange}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isMarketOpen && (
|
||||||
|
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-950/40 border border-green-300 dark:border-green-800">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
<span className="text-xs font-medium text-green-700 dark:text-green-400">
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isPreMarket && (
|
||||||
|
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-950/40 border border-blue-300 dark:border-blue-800">
|
||||||
|
<Clock className="w-3 h-3 text-blue-600 dark:text-blue-400" />
|
||||||
|
<span className="text-xs font-medium text-blue-700 dark:text-blue-400">
|
||||||
|
Pre-Market
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isPostMarket && (
|
||||||
|
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-orange-100 dark:bg-orange-950/40 border border-orange-300 dark:border-orange-800">
|
||||||
|
<Clock className="w-3 h-3 text-orange-600 dark:text-orange-400" />
|
||||||
|
<span className="text-xs font-medium text-orange-700 dark:text-orange-400">
|
||||||
|
After Hours
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-black/60 dark:text-white/60">
|
||||||
|
{props.longName || props.shortName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="flex items-baseline gap-2 mb-1">
|
||||||
|
<span className="text-3xl font-medium text-black dark:text-white">
|
||||||
|
{props.currency === 'USD' ? '$' : ''}
|
||||||
|
{formatNumber(displayPrice)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-end gap-1 ${changeColor}`}
|
||||||
|
>
|
||||||
|
{isPositive ? (
|
||||||
|
<ArrowUpRight className="w-4 h-4" />
|
||||||
|
) : displayChange === 0 ? (
|
||||||
|
<Minus className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ArrowDownRight className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="text-lg font-normal">
|
||||||
|
{displayChange !== undefined && displayChange >= 0 ? '+' : ''}
|
||||||
|
{formatNumber(displayChange)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-normal">
|
||||||
|
(
|
||||||
|
{displayChangePercent !== undefined && displayChangePercent >= 0
|
||||||
|
? '+'
|
||||||
|
: ''}
|
||||||
|
{formatNumber(displayChangePercent)}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{props.chartData && (
|
||||||
|
<div className="bg-light-secondary dark:bg-dark-secondary rounded-lg overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between p-3 border-b border-light-200 dark:border-dark-200">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{(['1D', '5D', '1M', '3M', '6M', '1Y', 'MAX'] as const).map(
|
||||||
|
(timeframe) => (
|
||||||
|
<button
|
||||||
|
key={timeframe}
|
||||||
|
onClick={() => setSelectedTimeframe(timeframe)}
|
||||||
|
disabled={!props.chartData?.[timeframe]}
|
||||||
|
className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
|
||||||
|
selectedTimeframe === timeframe
|
||||||
|
? 'bg-black/10 dark:bg-white/10 text-black dark:text-white'
|
||||||
|
: 'text-black/50 dark:text-white/50 hover:text-black/80 dark:hover:text-white/80'
|
||||||
|
} disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||||
|
>
|
||||||
|
{timeframe}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{props.comparisonData && props.comparisonData.length > 0 && (
|
||||||
|
<div className="flex items-center gap-3 ml-auto">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
{props.symbol}
|
||||||
|
</span>
|
||||||
|
{props.comparisonData.map((comp, index) => {
|
||||||
|
const colors = ['#8b5cf6', '#f59e0b', '#ec4899'];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={comp.ticker}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: colors[index] }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-black/70 dark:text-white/70">
|
||||||
|
{comp.ticker}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div ref={chartContainerRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 border-t border-light-200 dark:border-dark-200">
|
||||||
|
<div className="flex justify-between p-3 border-r border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
Prev Close
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
${formatNumber(props.regularMarketPreviousClose)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-r border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
52W Range
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
${formatNumber(props.fiftyTwoWeekLow, 2)}-$
|
||||||
|
{formatNumber(props.fiftyTwoWeekHigh, 2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
Market Cap
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
{formatLargeNumber(props.marketCap)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-t border-r border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
Open
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
${formatNumber(props.regularMarketOpen)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-t border-r border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
P/E Ratio
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
{props.trailingPE ? formatNumber(props.trailingPE, 2) : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-t border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
Dividend Yield
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
{props.dividendYield
|
||||||
|
? `${formatNumber(props.dividendYield * 100, 2)}%`
|
||||||
|
: 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-t border-r border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
Day Range
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
${formatNumber(props.regularMarketDayLow, 2)}-$
|
||||||
|
{formatNumber(props.regularMarketDayHigh, 2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-t border-r border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
Volume
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
{formatLargeNumber(props.regularMarketVolume)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-3 border-t border-light-200 dark:border-dark-200">
|
||||||
|
<span className="text-xs text-black/50 dark:text-white/50">
|
||||||
|
EPS
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-black dark:text-white font-medium">
|
||||||
|
$
|
||||||
|
{props.earningsPerShare
|
||||||
|
? formatNumber(props.earningsPerShare, 2)
|
||||||
|
: 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Stock;
|
||||||
407
src/components/Widgets/Weather.tsx
Normal file
407
src/components/Widgets/Weather.tsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Wind, Droplets, Gauge } from 'lucide-react';
|
||||||
|
import { useMemo, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type WeatherWidgetProps = {
|
||||||
|
location: string;
|
||||||
|
current: {
|
||||||
|
time: string;
|
||||||
|
temperature_2m: number;
|
||||||
|
relative_humidity_2m: number;
|
||||||
|
apparent_temperature: number;
|
||||||
|
is_day: number;
|
||||||
|
precipitation: number;
|
||||||
|
weather_code: number;
|
||||||
|
wind_speed_10m: number;
|
||||||
|
wind_direction_10m: number;
|
||||||
|
wind_gusts_10m?: number;
|
||||||
|
};
|
||||||
|
daily: {
|
||||||
|
time: string[];
|
||||||
|
weather_code: number[];
|
||||||
|
temperature_2m_max: number[];
|
||||||
|
temperature_2m_min: number[];
|
||||||
|
precipitation_probability_max: number[];
|
||||||
|
};
|
||||||
|
timezone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeatherInfo = (code: number, isDay: boolean, isDarkMode: boolean) => {
|
||||||
|
const dayNight = isDay ? 'day' : 'night';
|
||||||
|
|
||||||
|
const weatherMap: Record<
|
||||||
|
number,
|
||||||
|
{ icon: string; description: string; gradient: string }
|
||||||
|
> = {
|
||||||
|
0: {
|
||||||
|
icon: `clear-${dayNight}.svg`,
|
||||||
|
description: 'Clear',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? isDay
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E8F1FA, #7A9DBF 35%, #4A7BA8 60%, #2F5A88)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #5A6A7E, #3E4E63 40%, #2A3544 65%, #1A2230)'
|
||||||
|
: isDay
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #DBEAFE 30%, #93C5FD 60%, #60A5FA)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #7B8694, #475569 45%, #334155 70%, #1E293B)',
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
icon: `clear-${dayNight}.svg`,
|
||||||
|
description: 'Mostly Clear',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? isDay
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E8F1FA, #7A9DBF 35%, #4A7BA8 60%, #2F5A88)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #5A6A7E, #3E4E63 40%, #2A3544 65%, #1A2230)'
|
||||||
|
: isDay
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #DBEAFE 30%, #93C5FD 60%, #60A5FA)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #7B8694, #475569 45%, #334155 70%, #1E293B)',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
icon: `cloudy-1-${dayNight}.svg`,
|
||||||
|
description: 'Partly Cloudy',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? isDay
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E1ED, #8BA3B8 35%, #617A93 60%, #426070)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #6B7583, #4A5563 40%, #3A4450 65%, #2A3340)'
|
||||||
|
: isDay
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E0F2FE 28%, #BFDBFE 58%, #93C5FD)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #8B99AB, #64748B 45%, #475569 70%, #334155)',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
icon: `cloudy-1-${dayNight}.svg`,
|
||||||
|
description: 'Cloudy',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8C3CF, #758190 38%, #546270 65%, #3D4A58)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #F5F8FA, #CBD5E1 32%, #94A3B8 65%, #64748B)',
|
||||||
|
},
|
||||||
|
45: {
|
||||||
|
icon: `fog-${dayNight}.svg`,
|
||||||
|
description: 'Foggy',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #C5CDD8, #8892A0 38%, #697380 65%, #4F5A68)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E2E8F0 30%, #CBD5E1 62%, #94A3B8)',
|
||||||
|
},
|
||||||
|
48: {
|
||||||
|
icon: `fog-${dayNight}.svg`,
|
||||||
|
description: 'Rime Fog',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #C5CDD8, #8892A0 38%, #697380 65%, #4F5A68)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #E2E8F0 30%, #CBD5E1 62%, #94A3B8)',
|
||||||
|
},
|
||||||
|
51: {
|
||||||
|
icon: `rainy-1-${dayNight}.svg`,
|
||||||
|
description: 'Light Drizzle',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8D4E5, #6FA4C5 35%, #4A85AC 60%, #356A8E)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5FBFF, #A5F3FC 28%, #67E8F9 60%, #22D3EE)',
|
||||||
|
},
|
||||||
|
53: {
|
||||||
|
icon: `rainy-1-${dayNight}.svg`,
|
||||||
|
description: 'Drizzle',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8D4E5, #6FA4C5 35%, #4A85AC 60%, #356A8E)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5FBFF, #A5F3FC 28%, #67E8F9 60%, #22D3EE)',
|
||||||
|
},
|
||||||
|
55: {
|
||||||
|
icon: `rainy-2-${dayNight}.svg`,
|
||||||
|
description: 'Heavy Drizzle',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)',
|
||||||
|
},
|
||||||
|
61: {
|
||||||
|
icon: `rainy-2-${dayNight}.svg`,
|
||||||
|
description: 'Light Rain',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)',
|
||||||
|
},
|
||||||
|
63: {
|
||||||
|
icon: `rainy-2-${dayNight}.svg`,
|
||||||
|
description: 'Rain',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8DB3C8, #4D819F 38%, #326A87 65%, #215570)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8E8FF, #38BDF8 32%, #0EA5E9 65%, #0284C7)',
|
||||||
|
},
|
||||||
|
65: {
|
||||||
|
icon: `rainy-3-${dayNight}.svg`,
|
||||||
|
description: 'Heavy Rain',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7BA3B8, #3D6F8A 38%, #295973 65%, #1A455D)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #9CD9F5, #0EA5E9 32%, #0284C7 65%, #0369A1)',
|
||||||
|
},
|
||||||
|
71: {
|
||||||
|
icon: `snowy-1-${dayNight}.svg`,
|
||||||
|
description: 'Light Snow',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5F0FA, #9BB5CE 32%, #7496B8 58%, #527A9E)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #F0F9FF 25%, #E0F2FE 55%, #BAE6FD)',
|
||||||
|
},
|
||||||
|
73: {
|
||||||
|
icon: `snowy-2-${dayNight}.svg`,
|
||||||
|
description: 'Snow',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E5F3, #85A1BD 35%, #6584A8 60%, #496A8E)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #FAFEFF, #E0F2FE 28%, #BAE6FD 60%, #7DD3FC)',
|
||||||
|
},
|
||||||
|
75: {
|
||||||
|
icon: `snowy-3-${dayNight}.svg`,
|
||||||
|
description: 'Heavy Snow',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #BDD8EB, #6F92AE 35%, #4F7593 60%, #365A78)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #F0FAFF, #BAE6FD 30%, #7DD3FC 62%, #38BDF8)',
|
||||||
|
},
|
||||||
|
77: {
|
||||||
|
icon: `snowy-1-${dayNight}.svg`,
|
||||||
|
description: 'Snow Grains',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #E5F0FA, #9BB5CE 32%, #7496B8 58%, #527A9E)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #FFFFFF, #F0F9FF 25%, #E0F2FE 55%, #BAE6FD)',
|
||||||
|
},
|
||||||
|
80: {
|
||||||
|
icon: `rainy-2-${dayNight}.svg`,
|
||||||
|
description: 'Light Showers',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #A5C5D8, #5E92B0 35%, #3F789D 60%, #2A5F82)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4F3FF, #7DD3FC 30%, #38BDF8 62%, #0EA5E9)',
|
||||||
|
},
|
||||||
|
81: {
|
||||||
|
icon: `rainy-2-${dayNight}.svg`,
|
||||||
|
description: 'Showers',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8DB3C8, #4D819F 38%, #326A87 65%, #215570)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #B8E8FF, #38BDF8 32%, #0EA5E9 65%, #0284C7)',
|
||||||
|
},
|
||||||
|
82: {
|
||||||
|
icon: `rainy-3-${dayNight}.svg`,
|
||||||
|
description: 'Heavy Showers',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7BA3B8, #3D6F8A 38%, #295973 65%, #1A455D)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #9CD9F5, #0EA5E9 32%, #0284C7 65%, #0369A1)',
|
||||||
|
},
|
||||||
|
85: {
|
||||||
|
icon: `snowy-2-${dayNight}.svg`,
|
||||||
|
description: 'Light Snow Showers',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #D4E5F3, #85A1BD 35%, #6584A8 60%, #496A8E)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #FAFEFF, #E0F2FE 28%, #BAE6FD 60%, #7DD3FC)',
|
||||||
|
},
|
||||||
|
86: {
|
||||||
|
icon: `snowy-3-${dayNight}.svg`,
|
||||||
|
description: 'Snow Showers',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #BDD8EB, #6F92AE 35%, #4F7593 60%, #365A78)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #F0FAFF, #BAE6FD 30%, #7DD3FC 62%, #38BDF8)',
|
||||||
|
},
|
||||||
|
95: {
|
||||||
|
icon: `scattered-thunderstorms-${dayNight}.svg`,
|
||||||
|
description: 'Thunderstorm',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #8A95A3, #5F6A7A 38%, #475260 65%, #2F3A48)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #C8D1DD, #94A3B8 32%, #64748B 65%, #475569)',
|
||||||
|
},
|
||||||
|
96: {
|
||||||
|
icon: 'severe-thunderstorm.svg',
|
||||||
|
description: 'Thunderstorm + Hail',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #7A8593, #515C6D 38%, #3A4552 65%, #242D3A)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #B0BBC8, #64748B 32%, #475569 65%, #334155)',
|
||||||
|
},
|
||||||
|
99: {
|
||||||
|
icon: 'severe-thunderstorm.svg',
|
||||||
|
description: 'Severe Thunderstorm',
|
||||||
|
gradient: isDarkMode
|
||||||
|
? 'radial-gradient(ellipse 150% 100% at 50% 100%, #6A7583, #434E5D 40%, #2F3A47 68%, #1C2530)'
|
||||||
|
: 'radial-gradient(ellipse 150% 100% at 50% 100%, #9BA8B8, #475569 35%, #334155 68%, #1E293B)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return weatherMap[code] || weatherMap[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Weather = ({
|
||||||
|
location,
|
||||||
|
current,
|
||||||
|
daily,
|
||||||
|
timezone,
|
||||||
|
}: WeatherWidgetProps) => {
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkDarkMode = () => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
checkDarkMode();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(checkDarkMode);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const weatherInfo = useMemo(
|
||||||
|
() =>
|
||||||
|
getWeatherInfo(
|
||||||
|
current?.weather_code || 0,
|
||||||
|
current?.is_day === 1,
|
||||||
|
isDarkMode,
|
||||||
|
),
|
||||||
|
[current?.weather_code, current?.is_day, isDarkMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const forecast = useMemo(() => {
|
||||||
|
if (!daily?.time || daily.time.length === 0) return [];
|
||||||
|
|
||||||
|
return daily.time.slice(1, 7).map((time, idx) => {
|
||||||
|
const date = new Date(time);
|
||||||
|
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||||
|
const isDay = true;
|
||||||
|
const weatherCode = daily.weather_code[idx + 1];
|
||||||
|
const info = getWeatherInfo(weatherCode, isDay, isDarkMode);
|
||||||
|
|
||||||
|
return {
|
||||||
|
day: dayName,
|
||||||
|
icon: info.icon,
|
||||||
|
high: Math.round(daily.temperature_2m_max[idx + 1]),
|
||||||
|
low: Math.round(daily.temperature_2m_min[idx + 1]),
|
||||||
|
highF: Math.round((daily.temperature_2m_max[idx + 1] * 9) / 5 + 32),
|
||||||
|
lowF: Math.round((daily.temperature_2m_min[idx + 1] * 9) / 5 + 32),
|
||||||
|
precipitation: daily.precipitation_probability_max[idx + 1] || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [daily, isDarkMode]);
|
||||||
|
|
||||||
|
if (!current || !daily || !daily.time || daily.time.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden rounded-lg shadow-md bg-gray-200 dark:bg-gray-800">
|
||||||
|
<div className="p-4 text-black dark:text-white">
|
||||||
|
<p className="text-sm">Weather data unavailable for {location}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden rounded-lg shadow-md">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background: weatherInfo.gradient,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative p-4 text-gray-800 dark:text-white">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={`/weather-ico/${weatherInfo.icon}`}
|
||||||
|
alt={weatherInfo.description}
|
||||||
|
className="w-16 h-16 drop-shadow-lg"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-4xl font-bold drop-shadow-md">
|
||||||
|
{current.temperature_2m}°
|
||||||
|
</span>
|
||||||
|
<span className="text-lg">F C</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium drop-shadow mt-0.5">
|
||||||
|
{weatherInfo.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs font-medium opacity-90">
|
||||||
|
{daily.temperature_2m_max[0]}° {daily.temperature_2m_min[0]}°
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 pb-3 border-b border-gray-800/20 dark:border-white/20">
|
||||||
|
<h3 className="text-base font-semibold drop-shadow-md">{location}</h3>
|
||||||
|
<p className="text-xs text-gray-700 dark:text-white/80 drop-shadow mt-0.5">
|
||||||
|
{new Date(current.time).toLocaleString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-6 gap-2 mb-3 pb-3 border-b border-gray-800/20 dark:border-white/20">
|
||||||
|
{forecast.map((day, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex flex-col items-center bg-gray-800/10 dark:bg-white/10 backdrop-blur-sm rounded-md p-2"
|
||||||
|
>
|
||||||
|
<p className="text-xs font-medium mb-1">{day.day}</p>
|
||||||
|
<img
|
||||||
|
src={`/weather-ico/${day.icon}`}
|
||||||
|
alt=""
|
||||||
|
className="w-8 h-8 mb-1"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
<span className="font-semibold">{day.high}°</span>
|
||||||
|
<span className="text-gray-600 dark:text-white/60">
|
||||||
|
{day.low}°
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{day.precipitation > 0 && (
|
||||||
|
<div className="flex items-center gap-0.5 mt-1">
|
||||||
|
<Droplets className="w-3 h-3 text-gray-600 dark:text-white/70" />
|
||||||
|
<span className="text-[10px] text-gray-600 dark:text-white/70">
|
||||||
|
{day.precipitation}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2 bg-gray-800/10 dark:bg-white/10 backdrop-blur-sm rounded-md p-2">
|
||||||
|
<Wind className="w-4 h-4 text-gray-700 dark:text-white/80 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-gray-600 dark:text-white/70">
|
||||||
|
Wind
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{Math.round(current.wind_speed_10m)} km/h
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 bg-gray-800/10 dark:bg-white/10 backdrop-blur-sm rounded-md p-2">
|
||||||
|
<Droplets className="w-4 h-4 text-gray-700 dark:text-white/80 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-gray-600 dark:text-white/70">
|
||||||
|
Humidity
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{Math.round(current.relative_humidity_2m)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 bg-gray-800/10 dark:bg-white/10 backdrop-blur-sm rounded-md p-2">
|
||||||
|
<Gauge className="w-4 h-4 text-gray-700 dark:text-white/80 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-gray-600 dark:text-white/70">
|
||||||
|
Feels Like
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{Math.round(current.apparent_temperature)}°C
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Weather;
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { Message } from '@/components/ChatWindow';
|
import { Message } from '@/components/ChatWindow';
|
||||||
|
|
||||||
export const getSuggestions = async (chatHistory: Message[]) => {
|
export const getSuggestions = async (chatHistory: [string, string][]) => {
|
||||||
|
const chatTurns = chatHistory.map(([role, content]) => {
|
||||||
|
if (role === 'human') {
|
||||||
|
return { role: 'user', content };
|
||||||
|
} else {
|
||||||
|
return { role: 'assistant', content };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const chatModel = localStorage.getItem('chatModelKey');
|
const chatModel = localStorage.getItem('chatModelKey');
|
||||||
const chatModelProvider = localStorage.getItem('chatModelProviderId');
|
const chatModelProvider = localStorage.getItem('chatModelProviderId');
|
||||||
|
|
||||||
@@ -10,7 +18,7 @@ export const getSuggestions = async (chatHistory: Message[]) => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chatHistory: chatHistory,
|
chatHistory: chatTurns,
|
||||||
chatModel: {
|
chatModel: {
|
||||||
providerId: chatModelProvider,
|
providerId: chatModelProvider,
|
||||||
key: chatModel,
|
key: chatModel,
|
||||||
|
|||||||
66
src/lib/agents/media/image.ts
Normal file
66
src/lib/agents/media/image.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/* I don't think can be classified as agents but to keep the structure consistent i guess ill keep it here */
|
||||||
|
|
||||||
|
import { searchSearxng } from '@/lib/searxng';
|
||||||
|
import {
|
||||||
|
imageSearchFewShots,
|
||||||
|
imageSearchPrompt,
|
||||||
|
} from '@/lib/prompts/media/image';
|
||||||
|
import BaseLLM from '@/lib/models/base/llm';
|
||||||
|
import z from 'zod';
|
||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
|
||||||
|
type ImageSearchChainInput = {
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageSearchResult = {
|
||||||
|
img_src: string;
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchImages = async (
|
||||||
|
input: ImageSearchChainInput,
|
||||||
|
llm: BaseLLM<any>,
|
||||||
|
) => {
|
||||||
|
const schema = z.object({
|
||||||
|
query: z.string().describe('The image search query.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await llm.generateObject<z.infer<typeof schema>>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: imageSearchPrompt,
|
||||||
|
},
|
||||||
|
...imageSearchFewShots,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<conversation>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation>\n<follow_up>\n${input.query}\n</follow_up>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRes = await searchSearxng(res.query, {
|
||||||
|
engines: ['bing images', 'google images'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const images: ImageSearchResult[] = [];
|
||||||
|
|
||||||
|
searchRes.results.forEach((result) => {
|
||||||
|
if (result.img_src && result.url && result.title) {
|
||||||
|
images.push({
|
||||||
|
img_src: result.img_src,
|
||||||
|
url: result.url,
|
||||||
|
title: result.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return images.slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default searchImages;
|
||||||
66
src/lib/agents/media/video.ts
Normal file
66
src/lib/agents/media/video.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
import { searchSearxng } from '@/lib/searxng';
|
||||||
|
import {
|
||||||
|
videoSearchFewShots,
|
||||||
|
videoSearchPrompt,
|
||||||
|
} from '@/lib/prompts/media/videos';
|
||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
import BaseLLM from '@/lib/models/base/llm';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
type VideoSearchChainInput = {
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VideoSearchResult = {
|
||||||
|
img_src: string;
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
iframe_src: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchVideos = async (
|
||||||
|
input: VideoSearchChainInput,
|
||||||
|
llm: BaseLLM<any>,
|
||||||
|
) => {
|
||||||
|
const schema = z.object({
|
||||||
|
query: z.string().describe('The video search query.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await llm.generateObject<z.infer<typeof schema>>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: videoSearchPrompt,
|
||||||
|
},
|
||||||
|
...videoSearchFewShots,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<conversation>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation>\n<follow_up>\n${input.query}\n</follow_up>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchRes = await searchSearxng(res.query, {
|
||||||
|
engines: ['youtube'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const videos: VideoSearchResult[] = [];
|
||||||
|
|
||||||
|
searchRes.results.forEach((result) => {
|
||||||
|
if (result.thumbnail && result.url && result.title && result.iframe_src) {
|
||||||
|
videos.push({
|
||||||
|
img_src: result.thumbnail,
|
||||||
|
url: result.url,
|
||||||
|
title: result.title,
|
||||||
|
iframe_src: result.iframe_src,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return videos.slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default searchVideos;
|
||||||
53
src/lib/agents/search/classifier.ts
Normal file
53
src/lib/agents/search/classifier.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { ClassifierInput } from './types';
|
||||||
|
import { classifierPrompt } from '@/lib/prompts/search/classifier';
|
||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
classification: z.object({
|
||||||
|
skipSearch: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to skip the search step.'),
|
||||||
|
personalSearch: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to perform a personal search.'),
|
||||||
|
academicSearch: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to perform an academic search.'),
|
||||||
|
discussionSearch: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to perform a discussion search.'),
|
||||||
|
showWeatherWidget: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to show the weather widget.'),
|
||||||
|
showStockWidget: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to show the stock widget.'),
|
||||||
|
showCalculationWidget: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Indicates whether to show the calculation widget.'),
|
||||||
|
}),
|
||||||
|
standaloneFollowUp: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"A self-contained, context-independent reformulation of the user's question.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const classify = async (input: ClassifierInput) => {
|
||||||
|
const output = await input.llm.generateObject<typeof schema>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: classifierPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<conversation_history>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation_history>\n<user_query>\n${input.query}\n</user_query>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
102
src/lib/agents/search/index.ts
Normal file
102
src/lib/agents/search/index.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { ResearcherOutput, SearchAgentInput } from './types';
|
||||||
|
import SessionManager from '@/lib/session';
|
||||||
|
import { classify } from './classifier';
|
||||||
|
import Researcher from './researcher';
|
||||||
|
import { getWriterPrompt } from '@/lib/prompts/search/writer';
|
||||||
|
import { WidgetExecutor } from './widgets';
|
||||||
|
|
||||||
|
class SearchAgent {
|
||||||
|
async searchAsync(session: SessionManager, input: SearchAgentInput) {
|
||||||
|
const classification = await classify({
|
||||||
|
chatHistory: input.chatHistory,
|
||||||
|
enabledSources: input.config.sources,
|
||||||
|
query: input.followUp,
|
||||||
|
llm: input.config.llm,
|
||||||
|
});
|
||||||
|
|
||||||
|
const widgetPromise = WidgetExecutor.executeAll({
|
||||||
|
classification,
|
||||||
|
chatHistory: input.chatHistory,
|
||||||
|
followUp: input.followUp,
|
||||||
|
llm: input.config.llm,
|
||||||
|
}).then((widgetOutputs) => {
|
||||||
|
widgetOutputs.forEach((o) => {
|
||||||
|
session.emitBlock({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: 'widget',
|
||||||
|
data: {
|
||||||
|
widgetType: o.type,
|
||||||
|
params: o.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return widgetOutputs;
|
||||||
|
});
|
||||||
|
|
||||||
|
let searchPromise: Promise<ResearcherOutput> | null = null;
|
||||||
|
|
||||||
|
if (!classification.classification.skipSearch) {
|
||||||
|
const researcher = new Researcher();
|
||||||
|
searchPromise = researcher.research(session, {
|
||||||
|
chatHistory: input.chatHistory,
|
||||||
|
followUp: input.followUp,
|
||||||
|
classification: classification,
|
||||||
|
config: input.config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [widgetOutputs, searchResults] = await Promise.all([
|
||||||
|
widgetPromise,
|
||||||
|
searchPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
|
session.emit('data', {
|
||||||
|
type: 'researchComplete',
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalContext =
|
||||||
|
searchResults?.findings
|
||||||
|
.filter((f) => f.type === 'search_results')
|
||||||
|
.flatMap((f) => f.results)
|
||||||
|
.map((f) => `${f.metadata.title}: ${f.content}`)
|
||||||
|
.join('\n') || '';
|
||||||
|
|
||||||
|
const widgetContext = widgetOutputs
|
||||||
|
.map((o) => {
|
||||||
|
return `${o.type}: ${o.llmContext}`;
|
||||||
|
})
|
||||||
|
.join('\n-------------\n');
|
||||||
|
|
||||||
|
const finalContextWithWidgets = `<search_results note="These are the search results and you can cite these">${finalContext}</search_results>\n<widgets_result noteForAssistant="Its output is already showed to the user, you can use this information to answer the query but do not CITE this as a souce">${widgetContext}</widgets_result>`;
|
||||||
|
|
||||||
|
const writerPrompt = getWriterPrompt(finalContextWithWidgets);
|
||||||
|
const answerStream = input.config.llm.streamText({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: writerPrompt,
|
||||||
|
},
|
||||||
|
...input.chatHistory,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: input.followUp,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let accumulatedText = '';
|
||||||
|
|
||||||
|
for await (const chunk of answerStream) {
|
||||||
|
accumulatedText += chunk.contentChunk;
|
||||||
|
|
||||||
|
session.emit('data', {
|
||||||
|
type: 'response',
|
||||||
|
data: chunk.contentChunk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
session.emit('end', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchAgent;
|
||||||
19
src/lib/agents/search/researcher/actions/done.ts
Normal file
19
src/lib/agents/search/researcher/actions/done.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { ResearchAction } from '../../types';
|
||||||
|
|
||||||
|
const doneAction: ResearchAction<any> = {
|
||||||
|
name: 'done',
|
||||||
|
description:
|
||||||
|
"Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.",
|
||||||
|
enabled: (_) => true,
|
||||||
|
schema: z.object({
|
||||||
|
type: z.literal('done'),
|
||||||
|
}),
|
||||||
|
execute: async (params, additionalConfig) => {
|
||||||
|
return {
|
||||||
|
type: 'done',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default doneAction;
|
||||||
8
src/lib/agents/search/researcher/actions/index.ts
Normal file
8
src/lib/agents/search/researcher/actions/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import doneAction from './done';
|
||||||
|
import ActionRegistry from './registry';
|
||||||
|
import webSearchAction from './webSearch';
|
||||||
|
|
||||||
|
ActionRegistry.register(webSearchAction);
|
||||||
|
ActionRegistry.register(doneAction);
|
||||||
|
|
||||||
|
export { ActionRegistry };
|
||||||
73
src/lib/agents/search/researcher/actions/registry.ts
Normal file
73
src/lib/agents/search/researcher/actions/registry.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
ActionConfig,
|
||||||
|
ActionOutput,
|
||||||
|
AdditionalConfig,
|
||||||
|
ClassifierOutput,
|
||||||
|
ResearchAction,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
class ActionRegistry {
|
||||||
|
private static actions: Map<string, ResearchAction> = new Map();
|
||||||
|
|
||||||
|
static register(action: ResearchAction<any>) {
|
||||||
|
this.actions.set(action.name, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(name: string): ResearchAction | undefined {
|
||||||
|
return this.actions.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAvailableActions(config: {
|
||||||
|
classification: ClassifierOutput;
|
||||||
|
}): ResearchAction[] {
|
||||||
|
return Array.from(
|
||||||
|
this.actions.values().filter((action) => action.enabled(config)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAvailableActionsDescriptions(config: {
|
||||||
|
classification: ClassifierOutput;
|
||||||
|
}): string {
|
||||||
|
const availableActions = this.getAvailableActions(config);
|
||||||
|
|
||||||
|
return availableActions
|
||||||
|
.map((action) => `------------\n##${action.name}\n${action.description}`)
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async execute(
|
||||||
|
name: string,
|
||||||
|
params: any,
|
||||||
|
additionalConfig: AdditionalConfig,
|
||||||
|
) {
|
||||||
|
const action = this.actions.get(name);
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
throw new Error(`Action with name ${name} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return action.execute(params, additionalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async executeAll(
|
||||||
|
actions: ActionConfig[],
|
||||||
|
additionalConfig: AdditionalConfig,
|
||||||
|
): Promise<ActionOutput[]> {
|
||||||
|
const results: ActionOutput[] = [];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
actions.map(async (actionConfig) => {
|
||||||
|
const output = await this.execute(
|
||||||
|
actionConfig.type,
|
||||||
|
actionConfig.params,
|
||||||
|
additionalConfig,
|
||||||
|
);
|
||||||
|
results.push(output);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActionRegistry;
|
||||||
56
src/lib/agents/search/researcher/actions/webSearch.ts
Normal file
56
src/lib/agents/search/researcher/actions/webSearch.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { ResearchAction } from '../../types';
|
||||||
|
import { searchSearxng } from '@/lib/searxng';
|
||||||
|
import { Chunk } from '@/lib/types';
|
||||||
|
|
||||||
|
const actionSchema = z.object({
|
||||||
|
type: z.literal('web_search'),
|
||||||
|
queries: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe('An array of search queries to perform web searches for.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionDescription = `
|
||||||
|
You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results.
|
||||||
|
When this action is present, you must use it to obtain current information from the web.
|
||||||
|
|
||||||
|
### How to use:
|
||||||
|
1. For speed search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
|
||||||
|
2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings.
|
||||||
|
3. If you're set on quality mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
|
||||||
|
|
||||||
|
Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const webSearchAction: ResearchAction<typeof actionSchema> = {
|
||||||
|
name: 'web_search',
|
||||||
|
description: actionDescription,
|
||||||
|
schema: actionSchema,
|
||||||
|
enabled: (config) => true,
|
||||||
|
execute: async (input, _) => {
|
||||||
|
let results: Chunk[] = [];
|
||||||
|
|
||||||
|
const search = async (q: string) => {
|
||||||
|
const res = await searchSearxng(q);
|
||||||
|
|
||||||
|
res.results.forEach((r) => {
|
||||||
|
results.push({
|
||||||
|
content: r.content || r.title,
|
||||||
|
metadata: {
|
||||||
|
title: r.title,
|
||||||
|
url: r.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(input.queries.map(search));
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'search_results',
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default webSearchAction;
|
||||||
229
src/lib/agents/search/researcher/index.ts
Normal file
229
src/lib/agents/search/researcher/index.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import {
|
||||||
|
ActionConfig,
|
||||||
|
ActionOutput,
|
||||||
|
ResearcherInput,
|
||||||
|
ResearcherOutput,
|
||||||
|
} from '../types';
|
||||||
|
import { ActionRegistry } from './actions';
|
||||||
|
import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
|
||||||
|
import SessionManager from '@/lib/session';
|
||||||
|
import { ReasoningResearchBlock } from '@/lib/types';
|
||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
|
||||||
|
class Researcher {
|
||||||
|
async research(
|
||||||
|
session: SessionManager,
|
||||||
|
input: ResearcherInput,
|
||||||
|
): Promise<ResearcherOutput> {
|
||||||
|
let findings: string = '';
|
||||||
|
let actionOutput: ActionOutput[] = [];
|
||||||
|
let maxIteration =
|
||||||
|
input.config.mode === 'speed'
|
||||||
|
? 1
|
||||||
|
: input.config.mode === 'balanced'
|
||||||
|
? 3
|
||||||
|
: 25;
|
||||||
|
|
||||||
|
const availableActions = ActionRegistry.getAvailableActions({
|
||||||
|
classification: input.classification,
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
reasoning: z
|
||||||
|
.string()
|
||||||
|
.describe('The reasoning behind choosing the next action.'),
|
||||||
|
action: z
|
||||||
|
.union(availableActions.map((a) => a.schema))
|
||||||
|
.describe('The action to be performed next.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableActionsDescription =
|
||||||
|
ActionRegistry.getAvailableActionsDescriptions({
|
||||||
|
classification: input.classification,
|
||||||
|
});
|
||||||
|
|
||||||
|
const researchBlockId = crypto.randomUUID();
|
||||||
|
|
||||||
|
session.emitBlock({
|
||||||
|
id: researchBlockId,
|
||||||
|
type: 'research',
|
||||||
|
data: {
|
||||||
|
subSteps: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < maxIteration; i++) {
|
||||||
|
const researcherPrompt = getResearcherPrompt(
|
||||||
|
availableActionsDescription,
|
||||||
|
input.config.mode,
|
||||||
|
i,
|
||||||
|
maxIteration,
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionStream = input.config.llm.streamObject<typeof schema>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: researcherPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `
|
||||||
|
<conversation>
|
||||||
|
${formatChatHistoryAsString(input.chatHistory.slice(-10))}
|
||||||
|
User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp})
|
||||||
|
</conversation>
|
||||||
|
|
||||||
|
<previous_actions>
|
||||||
|
${findings}
|
||||||
|
</previous_actions>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const block = session.getBlock(researchBlockId);
|
||||||
|
|
||||||
|
let reasoningEmitted = false;
|
||||||
|
let reasoningId = crypto.randomUUID();
|
||||||
|
|
||||||
|
let finalActionRes: any;
|
||||||
|
|
||||||
|
for await (const partialRes of actionStream) {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
partialRes.reasoning &&
|
||||||
|
!reasoningEmitted &&
|
||||||
|
block &&
|
||||||
|
block.type === 'research'
|
||||||
|
) {
|
||||||
|
reasoningEmitted = true;
|
||||||
|
block.data.subSteps.push({
|
||||||
|
id: reasoningId,
|
||||||
|
type: 'reasoning',
|
||||||
|
reasoning: partialRes.reasoning,
|
||||||
|
});
|
||||||
|
session.updateBlock(researchBlockId, [
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/data/subSteps',
|
||||||
|
value: block.data.subSteps,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else if (
|
||||||
|
partialRes.reasoning &&
|
||||||
|
reasoningEmitted &&
|
||||||
|
block &&
|
||||||
|
block.type === 'research'
|
||||||
|
) {
|
||||||
|
const subStepIndex = block.data.subSteps.findIndex(
|
||||||
|
(step: any) => step.id === reasoningId,
|
||||||
|
);
|
||||||
|
if (subStepIndex !== -1) {
|
||||||
|
const subStep = block.data.subSteps[
|
||||||
|
subStepIndex
|
||||||
|
] as ReasoningResearchBlock;
|
||||||
|
subStep.reasoning = partialRes.reasoning;
|
||||||
|
session.updateBlock(researchBlockId, [
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/data/subSteps',
|
||||||
|
value: block.data.subSteps,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalActionRes = partialRes;
|
||||||
|
} catch (e) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalActionRes.action.type === 'done') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionConfig: ActionConfig = {
|
||||||
|
type: finalActionRes.action.type as string,
|
||||||
|
params: finalActionRes.action,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queries = actionConfig.params.queries || [];
|
||||||
|
if (block && block.type === 'research') {
|
||||||
|
block.data.subSteps.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: 'searching',
|
||||||
|
searching: queries,
|
||||||
|
});
|
||||||
|
session.updateBlock(researchBlockId, [
|
||||||
|
{ op: 'replace', path: '/data/subSteps', value: block.data.subSteps },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
findings += `\n---\nIteration ${i + 1}:\n`;
|
||||||
|
findings += 'Reasoning: ' + finalActionRes.reasoning + '\n';
|
||||||
|
findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`;
|
||||||
|
|
||||||
|
const actionResult = await ActionRegistry.execute(
|
||||||
|
actionConfig.type,
|
||||||
|
actionConfig.params,
|
||||||
|
{
|
||||||
|
llm: input.config.llm,
|
||||||
|
embedding: input.config.embedding,
|
||||||
|
session: session,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
actionOutput.push(actionResult);
|
||||||
|
|
||||||
|
if (actionResult.type === 'search_results') {
|
||||||
|
if (block && block.type === 'research') {
|
||||||
|
block.data.subSteps.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: 'reading',
|
||||||
|
reading: actionResult.results,
|
||||||
|
});
|
||||||
|
session.updateBlock(researchBlockId, [
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/data/subSteps',
|
||||||
|
value: block.data.subSteps,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
findings += actionResult.results
|
||||||
|
.map(
|
||||||
|
(r) =>
|
||||||
|
`Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
findings += '\n---------\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResults = actionOutput.filter(
|
||||||
|
(a) => a.type === 'search_results',
|
||||||
|
);
|
||||||
|
|
||||||
|
session.emit('data', {
|
||||||
|
type: 'sources',
|
||||||
|
data: searchResults
|
||||||
|
.flatMap((a) => a.results)
|
||||||
|
.map((r) => ({
|
||||||
|
content: r.content,
|
||||||
|
metadata: r.metadata,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
findings: actionOutput,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Researcher;
|
||||||
105
src/lib/agents/search/types.ts
Normal file
105
src/lib/agents/search/types.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import BaseLLM from '../../models/base/llm';
|
||||||
|
import BaseEmbedding from '@/lib/models/base/embedding';
|
||||||
|
import SessionManager from '@/lib/session';
|
||||||
|
import { ChatTurnMessage, Chunk } from '@/lib/types';
|
||||||
|
|
||||||
|
export type SearchSources = 'web' | 'discussions' | 'academic';
|
||||||
|
|
||||||
|
export type SearchAgentConfig = {
|
||||||
|
sources: SearchSources[];
|
||||||
|
llm: BaseLLM<any>;
|
||||||
|
embedding: BaseEmbedding<any>;
|
||||||
|
mode: 'speed' | 'balanced' | 'quality';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchAgentInput = {
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
followUp: string;
|
||||||
|
config: SearchAgentConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WidgetInput = {
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
followUp: string;
|
||||||
|
classification: ClassifierOutput;
|
||||||
|
llm: BaseLLM<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Widget = {
|
||||||
|
type: string;
|
||||||
|
shouldExecute: (classification: ClassifierOutput) => boolean;
|
||||||
|
execute: (input: WidgetInput) => Promise<WidgetOutput | void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WidgetOutput = {
|
||||||
|
type: string;
|
||||||
|
llmContext: string;
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClassifierInput = {
|
||||||
|
llm: BaseLLM<any>;
|
||||||
|
enabledSources: SearchSources[];
|
||||||
|
query: string;
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClassifierOutput = {
|
||||||
|
classification: {
|
||||||
|
skipSearch: boolean;
|
||||||
|
personalSearch: boolean;
|
||||||
|
academicSearch: boolean;
|
||||||
|
discussionSearch: boolean;
|
||||||
|
showWeatherWidget: boolean;
|
||||||
|
showStockWidget: boolean;
|
||||||
|
showCalculationWidget: boolean;
|
||||||
|
};
|
||||||
|
standaloneFollowUp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdditionalConfig = {
|
||||||
|
llm: BaseLLM<any>;
|
||||||
|
embedding: BaseEmbedding<any>;
|
||||||
|
session: SessionManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResearcherInput = {
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
followUp: string;
|
||||||
|
classification: ClassifierOutput;
|
||||||
|
config: SearchAgentConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResearcherOutput = {
|
||||||
|
findings: ActionOutput[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchActionOutput = {
|
||||||
|
type: 'search_results';
|
||||||
|
results: Chunk[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DoneActionOutput = {
|
||||||
|
type: 'done';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionOutput = SearchActionOutput | DoneActionOutput;
|
||||||
|
|
||||||
|
export interface ResearchAction<
|
||||||
|
TSchema extends z.ZodObject<any> = z.ZodObject<any>,
|
||||||
|
> {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
schema: z.ZodObject<any>;
|
||||||
|
enabled: (config: { classification: ClassifierOutput }) => boolean;
|
||||||
|
execute: (
|
||||||
|
params: z.infer<TSchema>,
|
||||||
|
additionalConfig: AdditionalConfig,
|
||||||
|
) => Promise<ActionOutput>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActionConfig = {
|
||||||
|
type: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
};
|
||||||
67
src/lib/agents/search/widgets/calculationWidget.ts
Normal file
67
src/lib/agents/search/widgets/calculationWidget.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { Widget } from '../types';
|
||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
import { exp, evaluate as mathEval } from 'mathjs';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
expression: z
|
||||||
|
.string()
|
||||||
|
.describe('Mathematical expression to calculate or evaluate.'),
|
||||||
|
notPresent: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Whether there is any need for the calculation widget.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const system = `
|
||||||
|
<role>
|
||||||
|
Assistant is a calculation expression extractor. You will recieve a user follow up and a conversation history.
|
||||||
|
Your task is to determine if there is a mathematical expression that needs to be calculated or evaluated. If there is, extract the expression and return it. If there is no need for any calculation, set notPresent to true.
|
||||||
|
</role>
|
||||||
|
|
||||||
|
<instructions>
|
||||||
|
Make sure that the extracted expression is valid and can be used to calculate the result with Math JS library (https://mathjs.org/). If the expression is not valid, set notPresent to true.
|
||||||
|
If you feel like you cannot extract a valid expression, set notPresent to true.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<output_format>
|
||||||
|
You must respond in the following JSON format without any extra text, explanations or filler sentences:
|
||||||
|
{
|
||||||
|
"expression": string,
|
||||||
|
"notPresent": boolean
|
||||||
|
}
|
||||||
|
</output_format>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const calculationWidget: Widget = {
|
||||||
|
type: 'calculationWidget',
|
||||||
|
shouldExecute: (classification) =>
|
||||||
|
classification.classification.showCalculationWidget,
|
||||||
|
execute: async (input) => {
|
||||||
|
const output = await input.llm.generateObject<typeof schema>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: system,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<conversation_history>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation_history>\n<user_follow_up>\n${input.followUp}\n</user_follow_up>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = mathEval(output.expression);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'calculation_result',
|
||||||
|
llmContext: `The result of the calculation for the expression "${output.expression}" is: ${result}`,
|
||||||
|
data: {
|
||||||
|
expression: output.expression,
|
||||||
|
result,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default calculationWidget;
|
||||||
36
src/lib/agents/search/widgets/executor.ts
Normal file
36
src/lib/agents/search/widgets/executor.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Widget, WidgetInput, WidgetOutput } from '../types';
|
||||||
|
|
||||||
|
class WidgetExecutor {
|
||||||
|
static widgets = new Map<string, Widget>();
|
||||||
|
|
||||||
|
static register(widget: Widget) {
|
||||||
|
this.widgets.set(widget.type, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWidget(type: string): Widget | undefined {
|
||||||
|
return this.widgets.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async executeAll(input: WidgetInput): Promise<WidgetOutput[]> {
|
||||||
|
const results: WidgetOutput[] = [];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(this.widgets.values()).map(async (widget) => {
|
||||||
|
try {
|
||||||
|
if (widget.shouldExecute(input.classification)) {
|
||||||
|
const output = await widget.execute(input);
|
||||||
|
if (output) {
|
||||||
|
results.push(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Error executing widget ${widget.type}:`, e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WidgetExecutor;
|
||||||
10
src/lib/agents/search/widgets/index.ts
Normal file
10
src/lib/agents/search/widgets/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import calculationWidget from './calculationWidget';
|
||||||
|
import WidgetExecutor from './executor';
|
||||||
|
import weatherWidget from './weatherWidget';
|
||||||
|
import stockWidget from './stockWidget';
|
||||||
|
|
||||||
|
WidgetExecutor.register(weatherWidget);
|
||||||
|
WidgetExecutor.register(calculationWidget);
|
||||||
|
WidgetExecutor.register(stockWidget);
|
||||||
|
|
||||||
|
export { WidgetExecutor };
|
||||||
434
src/lib/agents/search/widgets/stockWidget.ts
Normal file
434
src/lib/agents/search/widgets/stockWidget.ts
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { Widget } from '../types';
|
||||||
|
import YahooFinance from 'yahoo-finance2';
|
||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
|
||||||
|
const yf = new YahooFinance({
|
||||||
|
suppressNotices: ['yahooSurvey'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"The stock name for example Nvidia, Google, Apple, Microsoft etc. You can also return ticker if you're aware of it otherwise just use the name.",
|
||||||
|
),
|
||||||
|
comparisonNames: z
|
||||||
|
.array(z.string())
|
||||||
|
.max(3)
|
||||||
|
.describe(
|
||||||
|
"Optional array of up to 3 stock names to compare against the base name (e.g., ['Microsoft', 'GOOGL', 'Meta']). Charts will show percentage change comparison.",
|
||||||
|
),
|
||||||
|
notPresent: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Whether there is no need for the stock widget.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemPrompt = `
|
||||||
|
<role>
|
||||||
|
You are a stock ticker/name extractor. You will receive a user follow up and a conversation history.
|
||||||
|
Your task is to determine if the user is asking about stock information and extract the stock name(s) they want data for.
|
||||||
|
</role>
|
||||||
|
|
||||||
|
<instructions>
|
||||||
|
- If the user is asking about a stock, extract the primary stock name or ticker.
|
||||||
|
- If the user wants to compare stocks, extract up to 3 comparison stock names in comparisonNames.
|
||||||
|
- You can use either stock names (e.g., "Nvidia", "Apple") or tickers (e.g., "NVDA", "AAPL").
|
||||||
|
- If you cannot determine a valid stock or the query is not stock-related, set notPresent to true.
|
||||||
|
- If no comparison is needed, set comparisonNames to an empty array.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<output_format>
|
||||||
|
You must respond in the following JSON format without any extra text, explanations or filler sentences:
|
||||||
|
{
|
||||||
|
"name": string,
|
||||||
|
"comparisonNames": string[],
|
||||||
|
"notPresent": boolean
|
||||||
|
}
|
||||||
|
</output_format>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const stockWidget: Widget = {
|
||||||
|
type: 'stockWidget',
|
||||||
|
shouldExecute: (classification) =>
|
||||||
|
classification.classification.showStockWidget,
|
||||||
|
execute: async (input) => {
|
||||||
|
const output = await input.llm.generateObject<typeof schema>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<conversation_history>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation_history>\n<user_follow_up>\n${input.followUp}\n</user_follow_up>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (output.notPresent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = output;
|
||||||
|
try {
|
||||||
|
const name = params.name;
|
||||||
|
|
||||||
|
const findings = await yf.search(name);
|
||||||
|
|
||||||
|
if (findings.quotes.length === 0)
|
||||||
|
throw new Error(`Failed to find quote for name/symbol: ${name}`);
|
||||||
|
|
||||||
|
const ticker = findings.quotes[0].symbol as string;
|
||||||
|
|
||||||
|
const quote: any = await yf.quote(ticker);
|
||||||
|
|
||||||
|
const chartPromises = {
|
||||||
|
'1D': yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
|
||||||
|
period2: new Date(),
|
||||||
|
interval: '5m',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
'5D': yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000),
|
||||||
|
period2: new Date(),
|
||||||
|
interval: '15m',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
'1M': yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
'3M': yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
'6M': yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
'1Y': yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
MAX: yf
|
||||||
|
.chart(ticker, {
|
||||||
|
period1: new Date(Date.now() - 10 * 365 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1wk',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
};
|
||||||
|
|
||||||
|
const charts = await Promise.all([
|
||||||
|
chartPromises['1D'],
|
||||||
|
chartPromises['5D'],
|
||||||
|
chartPromises['1M'],
|
||||||
|
chartPromises['3M'],
|
||||||
|
chartPromises['6M'],
|
||||||
|
chartPromises['1Y'],
|
||||||
|
chartPromises['MAX'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [chart1D, chart5D, chart1M, chart3M, chart6M, chart1Y, chartMAX] =
|
||||||
|
charts;
|
||||||
|
|
||||||
|
if (!quote) {
|
||||||
|
throw new Error(`No data found for ticker: ${ticker}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let comparisonData: any = null;
|
||||||
|
if (params.comparisonNames.length > 0) {
|
||||||
|
const comparisonPromises = params.comparisonNames
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(async (compName) => {
|
||||||
|
try {
|
||||||
|
const compFindings = await yf.search(compName);
|
||||||
|
|
||||||
|
if (compFindings.quotes.length === 0) return null;
|
||||||
|
|
||||||
|
const compTicker = compFindings.quotes[0].symbol as string;
|
||||||
|
const compQuote = await yf.quote(compTicker);
|
||||||
|
const compCharts = await Promise.all([
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
|
||||||
|
period2: new Date(),
|
||||||
|
interval: '5m',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000),
|
||||||
|
period2: new Date(),
|
||||||
|
interval: '15m',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
|
||||||
|
interval: '1d',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
yf
|
||||||
|
.chart(compTicker, {
|
||||||
|
period1: new Date(
|
||||||
|
Date.now() - 10 * 365 * 24 * 60 * 60 * 1000,
|
||||||
|
),
|
||||||
|
interval: '1wk',
|
||||||
|
})
|
||||||
|
.catch(() => null),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
ticker: compTicker,
|
||||||
|
name: compQuote.shortName || compTicker,
|
||||||
|
charts: compCharts,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to fetch comparison ticker ${compName}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const compResults = await Promise.all(comparisonPromises);
|
||||||
|
comparisonData = compResults.filter((r) => r !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockData = {
|
||||||
|
symbol: quote.symbol,
|
||||||
|
shortName: quote.shortName || quote.longName || ticker,
|
||||||
|
longName: quote.longName,
|
||||||
|
exchange: quote.fullExchangeName || quote.exchange,
|
||||||
|
currency: quote.currency,
|
||||||
|
quoteType: quote.quoteType,
|
||||||
|
|
||||||
|
marketState: quote.marketState,
|
||||||
|
regularMarketTime: quote.regularMarketTime,
|
||||||
|
postMarketTime: quote.postMarketTime,
|
||||||
|
preMarketTime: quote.preMarketTime,
|
||||||
|
|
||||||
|
regularMarketPrice: quote.regularMarketPrice,
|
||||||
|
regularMarketChange: quote.regularMarketChange,
|
||||||
|
regularMarketChangePercent: quote.regularMarketChangePercent,
|
||||||
|
regularMarketPreviousClose: quote.regularMarketPreviousClose,
|
||||||
|
regularMarketOpen: quote.regularMarketOpen,
|
||||||
|
regularMarketDayHigh: quote.regularMarketDayHigh,
|
||||||
|
regularMarketDayLow: quote.regularMarketDayLow,
|
||||||
|
|
||||||
|
postMarketPrice: quote.postMarketPrice,
|
||||||
|
postMarketChange: quote.postMarketChange,
|
||||||
|
postMarketChangePercent: quote.postMarketChangePercent,
|
||||||
|
preMarketPrice: quote.preMarketPrice,
|
||||||
|
preMarketChange: quote.preMarketChange,
|
||||||
|
preMarketChangePercent: quote.preMarketChangePercent,
|
||||||
|
|
||||||
|
regularMarketVolume: quote.regularMarketVolume,
|
||||||
|
averageDailyVolume3Month: quote.averageDailyVolume3Month,
|
||||||
|
averageDailyVolume10Day: quote.averageDailyVolume10Day,
|
||||||
|
bid: quote.bid,
|
||||||
|
bidSize: quote.bidSize,
|
||||||
|
ask: quote.ask,
|
||||||
|
askSize: quote.askSize,
|
||||||
|
|
||||||
|
fiftyTwoWeekLow: quote.fiftyTwoWeekLow,
|
||||||
|
fiftyTwoWeekHigh: quote.fiftyTwoWeekHigh,
|
||||||
|
fiftyTwoWeekChange: quote.fiftyTwoWeekChange,
|
||||||
|
fiftyTwoWeekChangePercent: quote.fiftyTwoWeekChangePercent,
|
||||||
|
|
||||||
|
marketCap: quote.marketCap,
|
||||||
|
trailingPE: quote.trailingPE,
|
||||||
|
forwardPE: quote.forwardPE,
|
||||||
|
priceToBook: quote.priceToBook,
|
||||||
|
bookValue: quote.bookValue,
|
||||||
|
earningsPerShare: quote.epsTrailingTwelveMonths,
|
||||||
|
epsForward: quote.epsForward,
|
||||||
|
|
||||||
|
dividendRate: quote.dividendRate,
|
||||||
|
dividendYield: quote.dividendYield,
|
||||||
|
exDividendDate: quote.exDividendDate,
|
||||||
|
trailingAnnualDividendRate: quote.trailingAnnualDividendRate,
|
||||||
|
trailingAnnualDividendYield: quote.trailingAnnualDividendYield,
|
||||||
|
|
||||||
|
beta: quote.beta,
|
||||||
|
|
||||||
|
fiftyDayAverage: quote.fiftyDayAverage,
|
||||||
|
fiftyDayAverageChange: quote.fiftyDayAverageChange,
|
||||||
|
fiftyDayAverageChangePercent: quote.fiftyDayAverageChangePercent,
|
||||||
|
twoHundredDayAverage: quote.twoHundredDayAverage,
|
||||||
|
twoHundredDayAverageChange: quote.twoHundredDayAverageChange,
|
||||||
|
twoHundredDayAverageChangePercent:
|
||||||
|
quote.twoHundredDayAverageChangePercent,
|
||||||
|
|
||||||
|
sector: quote.sector,
|
||||||
|
industry: quote.industry,
|
||||||
|
website: quote.website,
|
||||||
|
|
||||||
|
chartData: {
|
||||||
|
'1D': chart1D
|
||||||
|
? {
|
||||||
|
timestamps: chart1D.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chart1D.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'5D': chart5D
|
||||||
|
? {
|
||||||
|
timestamps: chart5D.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chart5D.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'1M': chart1M
|
||||||
|
? {
|
||||||
|
timestamps: chart1M.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chart1M.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'3M': chart3M
|
||||||
|
? {
|
||||||
|
timestamps: chart3M.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chart3M.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'6M': chart6M
|
||||||
|
? {
|
||||||
|
timestamps: chart6M.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chart6M.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'1Y': chart1Y
|
||||||
|
? {
|
||||||
|
timestamps: chart1Y.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chart1Y.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
MAX: chartMAX
|
||||||
|
? {
|
||||||
|
timestamps: chartMAX.quotes.map((q: any) => q.date.getTime()),
|
||||||
|
prices: chartMAX.quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
comparisonData: comparisonData
|
||||||
|
? comparisonData.map((comp: any) => ({
|
||||||
|
ticker: comp.ticker,
|
||||||
|
name: comp.name,
|
||||||
|
chartData: {
|
||||||
|
'1D': comp.charts[0]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[0].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[0].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'5D': comp.charts[1]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[1].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[1].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'1M': comp.charts[2]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[2].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[2].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'3M': comp.charts[3]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[3].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[3].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'6M': comp.charts[4]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[4].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[4].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
'1Y': comp.charts[5]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[5].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[5].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
MAX: comp.charts[6]
|
||||||
|
? {
|
||||||
|
timestamps: comp.charts[6].quotes.map((q: any) =>
|
||||||
|
q.date.getTime(),
|
||||||
|
),
|
||||||
|
prices: comp.charts[6].quotes.map((q: any) => q.close),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'stock',
|
||||||
|
llmContext: `Current price of ${stockData.shortName} (${stockData.symbol}) is ${stockData.regularMarketPrice} ${stockData.currency}. Other details: ${JSON.stringify(
|
||||||
|
{
|
||||||
|
marketState: stockData.marketState,
|
||||||
|
regularMarketChange: stockData.regularMarketChange,
|
||||||
|
regularMarketChangePercent: stockData.regularMarketChangePercent,
|
||||||
|
marketCap: stockData.marketCap,
|
||||||
|
peRatio: stockData.trailingPE,
|
||||||
|
dividendYield: stockData.dividendYield,
|
||||||
|
},
|
||||||
|
)}`,
|
||||||
|
data: stockData,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
type: 'stock',
|
||||||
|
llmContext: 'Failed to fetch stock data.',
|
||||||
|
data: {
|
||||||
|
error: `Error fetching stock data: ${error.message || error}`,
|
||||||
|
ticker: params.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default stockWidget;
|
||||||
203
src/lib/agents/search/widgets/weatherWidget.ts
Normal file
203
src/lib/agents/search/widgets/weatherWidget.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { Widget } from '../types';
|
||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
location: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'Human-readable location name (e.g., "New York, NY, USA", "London, UK"). Use this OR lat/lon coordinates, never both. Leave empty string if providing coordinates.',
|
||||||
|
),
|
||||||
|
lat: z
|
||||||
|
.number()
|
||||||
|
.describe(
|
||||||
|
'Latitude coordinate in decimal degrees (e.g., 40.7128). Only use when location name is empty.',
|
||||||
|
),
|
||||||
|
lon: z
|
||||||
|
.number()
|
||||||
|
.describe(
|
||||||
|
'Longitude coordinate in decimal degrees (e.g., -74.0060). Only use when location name is empty.',
|
||||||
|
),
|
||||||
|
notPresent: z
|
||||||
|
.boolean()
|
||||||
|
.describe('Whether there is no need for the weather widget.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemPrompt = `
|
||||||
|
<role>
|
||||||
|
You are a location extractor for weather queries. You will receive a user follow up and a conversation history.
|
||||||
|
Your task is to determine if the user is asking about weather and extract the location they want weather for.
|
||||||
|
</role>
|
||||||
|
|
||||||
|
<instructions>
|
||||||
|
- If the user is asking about weather, extract the location name OR coordinates (never both).
|
||||||
|
- If using location name, set lat and lon to 0.
|
||||||
|
- If using coordinates, set location to empty string.
|
||||||
|
- If you cannot determine a valid location or the query is not weather-related, set notPresent to true.
|
||||||
|
- Location should be specific (city, state/region, country) for best results.
|
||||||
|
- You have to give the location so that it can be used to fetch weather data, it cannot be left empty unless notPresent is true.
|
||||||
|
- Make sure to infer short forms of location names (e.g., "NYC" -> "New York City", "LA" -> "Los Angeles").
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<output_format>
|
||||||
|
You must respond in the following JSON format without any extra text, explanations or filler sentences:
|
||||||
|
{
|
||||||
|
"location": string,
|
||||||
|
"lat": number,
|
||||||
|
"lon": number,
|
||||||
|
"notPresent": boolean
|
||||||
|
}
|
||||||
|
</output_format>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const weatherWidget: Widget = {
|
||||||
|
type: 'weatherWidget',
|
||||||
|
shouldExecute: (classification) =>
|
||||||
|
classification.classification.showWeatherWidget,
|
||||||
|
execute: async (input) => {
|
||||||
|
const output = await input.llm.generateObject<typeof schema>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<conversation_history>\n${formatChatHistoryAsString(input.chatHistory)}\n</conversation_history>\n<user_follow_up>\n${input.followUp}\n</user_follow_up>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (output.notPresent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = output;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
params.location === '' &&
|
||||||
|
(params.lat === undefined || params.lon === undefined)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Either location name or both latitude and longitude must be provided.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.location !== '') {
|
||||||
|
const openStreetMapUrl = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(params.location)}&format=json&limit=1`;
|
||||||
|
|
||||||
|
const locationRes = await fetch(openStreetMapUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Perplexica',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await locationRes.json();
|
||||||
|
|
||||||
|
const location = data[0];
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find coordinates for location: ${params.location}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const weatherRes = await fetch(
|
||||||
|
`https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Perplexica',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const weatherData = await weatherRes.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'weather',
|
||||||
|
llmContext: `Weather in ${params.location} is ${JSON.stringify(weatherData.current)}`,
|
||||||
|
data: {
|
||||||
|
location: params.location,
|
||||||
|
latitude: location.lat,
|
||||||
|
longitude: location.lon,
|
||||||
|
current: weatherData.current,
|
||||||
|
hourly: {
|
||||||
|
time: weatherData.hourly.time.slice(0, 24),
|
||||||
|
temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24),
|
||||||
|
precipitation_probability:
|
||||||
|
weatherData.hourly.precipitation_probability.slice(0, 24),
|
||||||
|
precipitation: weatherData.hourly.precipitation.slice(0, 24),
|
||||||
|
weather_code: weatherData.hourly.weather_code.slice(0, 24),
|
||||||
|
},
|
||||||
|
daily: weatherData.daily,
|
||||||
|
timezone: weatherData.timezone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (params.lat !== undefined && params.lon !== undefined) {
|
||||||
|
const [weatherRes, locationRes] = await Promise.all([
|
||||||
|
fetch(
|
||||||
|
`https://api.open-meteo.com/v1/forecast?latitude=${params.lat}&longitude=${params.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max&timezone=auto&forecast_days=7`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Perplexica',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?lat=${params.lat}&lon=${params.lon}&format=json`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Perplexica',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const weatherData = await weatherRes.json();
|
||||||
|
const locationData = await locationRes.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'weather',
|
||||||
|
llmContext: `Weather in ${locationData.display_name} is ${JSON.stringify(weatherData.current)}`,
|
||||||
|
data: {
|
||||||
|
location: locationData.display_name,
|
||||||
|
latitude: params.lat,
|
||||||
|
longitude: params.lon,
|
||||||
|
current: weatherData.current,
|
||||||
|
hourly: {
|
||||||
|
time: weatherData.hourly.time.slice(0, 24),
|
||||||
|
temperature_2m: weatherData.hourly.temperature_2m.slice(0, 24),
|
||||||
|
precipitation_probability:
|
||||||
|
weatherData.hourly.precipitation_probability.slice(0, 24),
|
||||||
|
precipitation: weatherData.hourly.precipitation.slice(0, 24),
|
||||||
|
weather_code: weatherData.hourly.weather_code.slice(0, 24),
|
||||||
|
},
|
||||||
|
daily: weatherData.daily,
|
||||||
|
timezone: weatherData.timezone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'weather',
|
||||||
|
llmContext: 'No valid location or coordinates provided.',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
type: 'weather',
|
||||||
|
llmContext: 'Failed to fetch weather data.',
|
||||||
|
data: {
|
||||||
|
error: `Error fetching weather data: ${err}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default weatherWidget;
|
||||||
39
src/lib/agents/suggestions/index.ts
Normal file
39
src/lib/agents/suggestions/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
|
import { suggestionGeneratorPrompt } from '@/lib/prompts/suggestions';
|
||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
import z from 'zod';
|
||||||
|
import BaseLLM from '@/lib/models/base/llm';
|
||||||
|
import { i } from 'mathjs';
|
||||||
|
|
||||||
|
type SuggestionGeneratorInput = {
|
||||||
|
chatHistory: ChatTurnMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
suggestions: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe('List of suggested questions or prompts'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateSuggestions = async (
|
||||||
|
input: SuggestionGeneratorInput,
|
||||||
|
llm: BaseLLM<any>,
|
||||||
|
) => {
|
||||||
|
const res = await llm.generateObject<z.infer<typeof schema>>({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: suggestionGeneratorPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `<chat_history>\n${formatChatHistoryAsString(input.chatHistory)}\n</chat_history>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.suggestions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default generateSuggestions;
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import {
|
|
||||||
RunnableSequence,
|
|
||||||
RunnableMap,
|
|
||||||
RunnableLambda,
|
|
||||||
} from '@langchain/core/runnables';
|
|
||||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
import { searchSearxng } from '../searxng';
|
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import LineOutputParser from '../outputParsers/lineOutputParser';
|
|
||||||
|
|
||||||
const imageSearchChainPrompt = `
|
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
|
|
||||||
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
|
||||||
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
|
|
||||||
`;
|
|
||||||
|
|
||||||
type ImageSearchChainInput = {
|
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ImageSearchResult {
|
|
||||||
img_src: string;
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strParser = new StringOutputParser();
|
|
||||||
|
|
||||||
const createImageSearchChain = (llm: BaseChatModel) => {
|
|
||||||
return RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
|
||||||
chat_history: (input: ImageSearchChainInput) => {
|
|
||||||
return formatChatHistoryAsString(input.chat_history);
|
|
||||||
},
|
|
||||||
query: (input: ImageSearchChainInput) => {
|
|
||||||
return input.query;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ChatPromptTemplate.fromMessages([
|
|
||||||
['system', imageSearchChainPrompt],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>\n</conversation>\n<follow_up>\nWhat is a cat?\n</follow_up>',
|
|
||||||
],
|
|
||||||
['assistant', '<query>A cat</query>'],
|
|
||||||
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>\n</conversation>\n<follow_up>\nWhat is a car? How does it work?\n</follow_up>',
|
|
||||||
],
|
|
||||||
['assistant', '<query>Car working</query>'],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
|
|
||||||
],
|
|
||||||
['assistant', '<query>AC working</query>'],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
llm,
|
|
||||||
strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
const queryParser = new LineOutputParser({
|
|
||||||
key: 'query',
|
|
||||||
});
|
|
||||||
|
|
||||||
return await queryParser.parse(input);
|
|
||||||
}),
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
const res = await searchSearxng(input, {
|
|
||||||
engines: ['bing images', 'google images'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const images: ImageSearchResult[] = [];
|
|
||||||
|
|
||||||
res.results.forEach((result) => {
|
|
||||||
if (result.img_src && result.url && result.title) {
|
|
||||||
images.push({
|
|
||||||
img_src: result.img_src,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return images.slice(0, 10);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageSearch = (
|
|
||||||
input: ImageSearchChainInput,
|
|
||||||
llm: BaseChatModel,
|
|
||||||
) => {
|
|
||||||
const imageSearchChain = createImageSearchChain(llm);
|
|
||||||
return imageSearchChain.invoke(input);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default handleImageSearch;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
|
|
||||||
import ListLineOutputParser from '../outputParsers/listLineOutputParser';
|
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
|
|
||||||
const suggestionGeneratorPrompt = `
|
|
||||||
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
|
|
||||||
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
|
|
||||||
Make sure the suggestions are medium in length and are informative and relevant to the conversation.
|
|
||||||
|
|
||||||
Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example:
|
|
||||||
|
|
||||||
<suggestions>
|
|
||||||
Tell me more about SpaceX and their recent projects
|
|
||||||
What is the latest news on SpaceX?
|
|
||||||
Who is the CEO of SpaceX?
|
|
||||||
</suggestions>
|
|
||||||
|
|
||||||
Conversation:
|
|
||||||
{chat_history}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type SuggestionGeneratorInput = {
|
|
||||||
chat_history: BaseMessage[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const outputParser = new ListLineOutputParser({
|
|
||||||
key: 'suggestions',
|
|
||||||
});
|
|
||||||
|
|
||||||
const createSuggestionGeneratorChain = (llm: BaseChatModel) => {
|
|
||||||
return RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
|
||||||
chat_history: (input: SuggestionGeneratorInput) =>
|
|
||||||
formatChatHistoryAsString(input.chat_history),
|
|
||||||
}),
|
|
||||||
PromptTemplate.fromTemplate(suggestionGeneratorPrompt),
|
|
||||||
llm,
|
|
||||||
outputParser,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateSuggestions = (
|
|
||||||
input: SuggestionGeneratorInput,
|
|
||||||
llm: BaseChatModel,
|
|
||||||
) => {
|
|
||||||
(llm as unknown as ChatOpenAI).temperature = 0;
|
|
||||||
const suggestionGeneratorChain = createSuggestionGeneratorChain(llm);
|
|
||||||
return suggestionGeneratorChain.invoke(input);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default generateSuggestions;
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import {
|
|
||||||
RunnableSequence,
|
|
||||||
RunnableMap,
|
|
||||||
RunnableLambda,
|
|
||||||
} from '@langchain/core/runnables';
|
|
||||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
import { searchSearxng } from '../searxng';
|
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import LineOutputParser from '../outputParsers/lineOutputParser';
|
|
||||||
|
|
||||||
const videoSearchChainPrompt = `
|
|
||||||
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
|
|
||||||
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
|
||||||
Output only the rephrased query wrapped in an XML <query> element. Do not include any explanation or additional text.
|
|
||||||
`;
|
|
||||||
|
|
||||||
type VideoSearchChainInput = {
|
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface VideoSearchResult {
|
|
||||||
img_src: string;
|
|
||||||
url: string;
|
|
||||||
title: string;
|
|
||||||
iframe_src: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strParser = new StringOutputParser();
|
|
||||||
|
|
||||||
const createVideoSearchChain = (llm: BaseChatModel) => {
|
|
||||||
return RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
|
||||||
chat_history: (input: VideoSearchChainInput) => {
|
|
||||||
return formatChatHistoryAsString(input.chat_history);
|
|
||||||
},
|
|
||||||
query: (input: VideoSearchChainInput) => {
|
|
||||||
return input.query;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ChatPromptTemplate.fromMessages([
|
|
||||||
['system', videoSearchChainPrompt],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>\n</conversation>\n<follow_up>\nHow does a car work?\n</follow_up>',
|
|
||||||
],
|
|
||||||
['assistant', '<query>How does a car work?</query>'],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>\n</conversation>\n<follow_up>\nWhat is the theory of relativity?\n</follow_up>',
|
|
||||||
],
|
|
||||||
['assistant', '<query>Theory of relativity</query>'],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
|
|
||||||
],
|
|
||||||
['assistant', '<query>AC working</query>'],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
llm,
|
|
||||||
strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
const queryParser = new LineOutputParser({
|
|
||||||
key: 'query',
|
|
||||||
});
|
|
||||||
return await queryParser.parse(input);
|
|
||||||
}),
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
const res = await searchSearxng(input, {
|
|
||||||
engines: ['youtube'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const videos: VideoSearchResult[] = [];
|
|
||||||
|
|
||||||
res.results.forEach((result) => {
|
|
||||||
if (
|
|
||||||
result.thumbnail &&
|
|
||||||
result.url &&
|
|
||||||
result.title &&
|
|
||||||
result.iframe_src
|
|
||||||
) {
|
|
||||||
videos.push({
|
|
||||||
img_src: result.thumbnail,
|
|
||||||
url: result.url,
|
|
||||||
title: result.title,
|
|
||||||
iframe_src: result.iframe_src,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return videos.slice(0, 10);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoSearch = (
|
|
||||||
input: VideoSearchChainInput,
|
|
||||||
llm: BaseChatModel,
|
|
||||||
) => {
|
|
||||||
const videoSearchChain = createVideoSearchChain(llm);
|
|
||||||
return videoSearchChain.invoke(input);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default handleVideoSearch;
|
|
||||||
@@ -6,11 +6,14 @@ const getClientConfig = (key: string, defaultVal?: any) => {
|
|||||||
|
|
||||||
export const getTheme = () => getClientConfig('theme', 'dark');
|
export const getTheme = () => getClientConfig('theme', 'dark');
|
||||||
|
|
||||||
export const getAutoImageSearch = () =>
|
export const getAutoMediaSearch = () =>
|
||||||
Boolean(getClientConfig('autoImageSearch', 'true'));
|
getClientConfig('autoMediaSearch', 'true') === 'true';
|
||||||
|
|
||||||
export const getAutoVideoSearch = () =>
|
|
||||||
Boolean(getClientConfig('autoVideoSearch', 'true'));
|
|
||||||
|
|
||||||
export const getSystemInstructions = () =>
|
export const getSystemInstructions = () =>
|
||||||
getClientConfig('systemInstructions', '');
|
getClientConfig('systemInstructions', '');
|
||||||
|
|
||||||
|
export const getShowWeatherWidget = () =>
|
||||||
|
getClientConfig('showWeatherWidget', 'true') === 'true';
|
||||||
|
|
||||||
|
export const getShowNewsWidget = () =>
|
||||||
|
getClientConfig('showNewsWidget', 'true') === 'true';
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ class ConfigManager {
|
|||||||
currentConfig: Config = {
|
currentConfig: Config = {
|
||||||
version: this.configVersion,
|
version: this.configVersion,
|
||||||
setupComplete: false,
|
setupComplete: false,
|
||||||
general: {},
|
preferences: {},
|
||||||
|
personalization: {},
|
||||||
modelProviders: [],
|
modelProviders: [],
|
||||||
search: {
|
search: {
|
||||||
searxngURL: '',
|
searxngURL: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
uiConfigSections: UIConfigSections = {
|
uiConfigSections: UIConfigSections = {
|
||||||
general: [
|
preferences: [
|
||||||
{
|
{
|
||||||
name: 'Theme',
|
name: 'Theme',
|
||||||
key: 'theme',
|
key: 'theme',
|
||||||
@@ -40,16 +41,6 @@ class ConfigManager {
|
|||||||
default: 'dark',
|
default: 'dark',
|
||||||
scope: 'client',
|
scope: 'client',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'System Instructions',
|
|
||||||
key: 'systemInstructions',
|
|
||||||
type: 'textarea',
|
|
||||||
required: false,
|
|
||||||
description: 'Add custom behavior or tone for the model.',
|
|
||||||
placeholder:
|
|
||||||
'e.g., "Respond in a friendly and concise tone" or "Use British English and format answers as bullet points."',
|
|
||||||
scope: 'client',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Measurement Unit',
|
name: 'Measurement Unit',
|
||||||
key: 'measureUnit',
|
key: 'measureUnit',
|
||||||
@@ -69,6 +60,45 @@ class ConfigManager {
|
|||||||
default: 'Metric',
|
default: 'Metric',
|
||||||
scope: 'client',
|
scope: 'client',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Auto video & image search',
|
||||||
|
key: 'autoMediaSearch',
|
||||||
|
type: 'switch',
|
||||||
|
required: false,
|
||||||
|
description: 'Automatically search for relevant images and videos.',
|
||||||
|
default: true,
|
||||||
|
scope: 'client',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Show weather widget',
|
||||||
|
key: 'showWeatherWidget',
|
||||||
|
type: 'switch',
|
||||||
|
required: false,
|
||||||
|
description: 'Display the weather card on the home screen.',
|
||||||
|
default: true,
|
||||||
|
scope: 'client',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Show news widget',
|
||||||
|
key: 'showNewsWidget',
|
||||||
|
type: 'switch',
|
||||||
|
required: false,
|
||||||
|
description: 'Display the recent news card on the home screen.',
|
||||||
|
default: true,
|
||||||
|
scope: 'client',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
personalization: [
|
||||||
|
{
|
||||||
|
name: 'System Instructions',
|
||||||
|
key: 'systemInstructions',
|
||||||
|
type: 'textarea',
|
||||||
|
required: false,
|
||||||
|
description: 'Add custom behavior or tone for the model.',
|
||||||
|
placeholder:
|
||||||
|
'e.g., "Respond in a friendly and concise tone" or "Use British English and format answers as bullet points."',
|
||||||
|
scope: 'client',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
modelProviders: [],
|
modelProviders: [],
|
||||||
search: [
|
search: [
|
||||||
|
|||||||
@@ -38,11 +38,17 @@ type TextareaUIConfigField = BaseUIConfigField & {
|
|||||||
default?: string;
|
default?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SwitchUIConfigField = BaseUIConfigField & {
|
||||||
|
type: 'switch';
|
||||||
|
default?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type UIConfigField =
|
type UIConfigField =
|
||||||
| StringUIConfigField
|
| StringUIConfigField
|
||||||
| SelectUIConfigField
|
| SelectUIConfigField
|
||||||
| PasswordUIConfigField
|
| PasswordUIConfigField
|
||||||
| TextareaUIConfigField;
|
| TextareaUIConfigField
|
||||||
|
| SwitchUIConfigField;
|
||||||
|
|
||||||
type ConfigModelProvider = {
|
type ConfigModelProvider = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -57,7 +63,10 @@ type ConfigModelProvider = {
|
|||||||
type Config = {
|
type Config = {
|
||||||
version: number;
|
version: number;
|
||||||
setupComplete: boolean;
|
setupComplete: boolean;
|
||||||
general: {
|
preferences: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
personalization: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
modelProviders: ConfigModelProvider[];
|
modelProviders: ConfigModelProvider[];
|
||||||
@@ -80,7 +89,8 @@ type ModelProviderUISection = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UIConfigSections = {
|
type UIConfigSections = {
|
||||||
general: UIConfigField[];
|
preferences: UIConfigField[];
|
||||||
|
personalization: UIConfigField[];
|
||||||
modelProviders: ModelProviderUISection[];
|
modelProviders: ModelProviderUISection[];
|
||||||
search: UIConfigField[];
|
search: UIConfigField[];
|
||||||
};
|
};
|
||||||
@@ -95,4 +105,5 @@ export type {
|
|||||||
ModelProviderUISection,
|
ModelProviderUISection,
|
||||||
ConfigModelProvider,
|
ConfigModelProvider,
|
||||||
TextareaUIConfigField,
|
TextareaUIConfigField,
|
||||||
|
SwitchUIConfigField,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,12 +18,18 @@ db.exec(`
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
function sanitizeSql(content: string) {
|
function sanitizeSql(content: string) {
|
||||||
return content
|
const statements = content
|
||||||
|
.split(/--> statement-breakpoint/g)
|
||||||
|
.map((stmt) =>
|
||||||
|
stmt
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.filter(
|
.filter((l) => !l.trim().startsWith('-->'))
|
||||||
(l) => !l.trim().startsWith('-->') && !l.includes('statement-breakpoint'),
|
.join('\n')
|
||||||
|
.trim(),
|
||||||
)
|
)
|
||||||
.join('\n');
|
.filter((stmt) => stmt.length > 0);
|
||||||
|
|
||||||
|
return statements;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.readdirSync(migrationsFolder)
|
fs.readdirSync(migrationsFolder)
|
||||||
@@ -32,7 +38,7 @@ fs.readdirSync(migrationsFolder)
|
|||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
const filePath = path.join(migrationsFolder, file);
|
const filePath = path.join(migrationsFolder, file);
|
||||||
let content = fs.readFileSync(filePath, 'utf-8');
|
let content = fs.readFileSync(filePath, 'utf-8');
|
||||||
content = sanitizeSql(content);
|
const statements = sanitizeSql(content);
|
||||||
|
|
||||||
const migrationName = file.split('_')[0] || file;
|
const migrationName = file.split('_')[0] || file;
|
||||||
|
|
||||||
@@ -108,7 +114,12 @@ fs.readdirSync(migrationsFolder)
|
|||||||
db.exec('DROP TABLE messages;');
|
db.exec('DROP TABLE messages;');
|
||||||
db.exec('ALTER TABLE messages_with_sources RENAME TO messages;');
|
db.exec('ALTER TABLE messages_with_sources RENAME TO messages;');
|
||||||
} else {
|
} else {
|
||||||
db.exec(content);
|
// Execute each statement separately
|
||||||
|
statements.forEach((stmt) => {
|
||||||
|
if (stmt.trim()) {
|
||||||
|
db.exec(stmt);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.prepare('INSERT OR IGNORE INTO ran_migrations (name) VALUES (?)').run(
|
db.prepare('INSERT OR IGNORE INTO ran_migrations (name) VALUES (?)').run(
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
|
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Block } from '../types';
|
||||||
|
|
||||||
export const messages = sqliteTable('messages', {
|
export const messages = sqliteTable('messages', {
|
||||||
id: integer('id').primaryKey(),
|
id: integer('id').primaryKey(),
|
||||||
role: text('type', { enum: ['assistant', 'user', 'source'] }).notNull(),
|
|
||||||
chatId: text('chatId').notNull(),
|
|
||||||
createdAt: text('createdAt')
|
|
||||||
.notNull()
|
|
||||||
.default(sql`CURRENT_TIMESTAMP`),
|
|
||||||
messageId: text('messageId').notNull(),
|
messageId: text('messageId').notNull(),
|
||||||
|
chatId: text('chatId').notNull(),
|
||||||
content: text('content'),
|
backendId: text('backendId').notNull(),
|
||||||
|
query: text('query').notNull(),
|
||||||
sources: text('sources', {
|
createdAt: text('createdAt').notNull(),
|
||||||
mode: 'json',
|
responseBlocks: text('responseBlocks', { mode: 'json' })
|
||||||
})
|
.$type<Block[]>()
|
||||||
.$type<Document[]>()
|
|
||||||
.default(sql`'[]'`),
|
.default(sql`'[]'`),
|
||||||
|
status: text({ enum: ['answering', 'completed', 'error'] }).default(
|
||||||
|
'answering',
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface File {
|
interface DBFile {
|
||||||
name: string;
|
name: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
}
|
}
|
||||||
@@ -31,6 +28,6 @@ export const chats = sqliteTable('chats', {
|
|||||||
createdAt: text('createdAt').notNull(),
|
createdAt: text('createdAt').notNull(),
|
||||||
focusMode: text('focusMode').notNull(),
|
focusMode: text('focusMode').notNull(),
|
||||||
files: text('files', { mode: 'json' })
|
files: text('files', { mode: 'json' })
|
||||||
.$type<File[]>()
|
.$type<DBFile[]>()
|
||||||
.default(sql`'[]'`),
|
.default(sql`'[]'`),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { Message } from '@/components/ChatWindow';
|
||||||
AssistantMessage,
|
import { Block } from '@/lib/types';
|
||||||
ChatTurn,
|
|
||||||
Message,
|
|
||||||
SourceMessage,
|
|
||||||
SuggestionMessage,
|
|
||||||
UserMessage,
|
|
||||||
} from '@/components/ChatWindow';
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
@@ -21,20 +15,21 @@ import { useParams, useSearchParams } from 'next/navigation';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getSuggestions } from '../actions';
|
import { getSuggestions } from '../actions';
|
||||||
import { MinimalProvider } from '../models/types';
|
import { MinimalProvider } from '../models/types';
|
||||||
|
import { getAutoMediaSearch } from '../config/clientRegistry';
|
||||||
|
import { applyPatch } from 'rfc6902';
|
||||||
|
import { Widget } from '@/components/ChatWindow';
|
||||||
|
|
||||||
export type Section = {
|
export type Section = {
|
||||||
userMessage: UserMessage;
|
message: Message;
|
||||||
assistantMessage: AssistantMessage | undefined;
|
widgets: Widget[];
|
||||||
parsedAssistantMessage: string | undefined;
|
parsedTextBlocks: string[];
|
||||||
speechMessage: string | undefined;
|
speechMessage: string;
|
||||||
sourceMessage: SourceMessage | undefined;
|
|
||||||
thinkingEnded: boolean;
|
thinkingEnded: boolean;
|
||||||
suggestions?: string[];
|
suggestions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChatContext = {
|
type ChatContext = {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
chatTurns: ChatTurn[];
|
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
chatHistory: [string, string][];
|
chatHistory: [string, string][];
|
||||||
files: File[];
|
files: File[];
|
||||||
@@ -50,6 +45,8 @@ type ChatContext = {
|
|||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
chatModelProvider: ChatModelProvider;
|
chatModelProvider: ChatModelProvider;
|
||||||
embeddingModelProvider: EmbeddingModelProvider;
|
embeddingModelProvider: EmbeddingModelProvider;
|
||||||
|
researchEnded: boolean;
|
||||||
|
setResearchEnded: (ended: boolean) => void;
|
||||||
setOptimizationMode: (mode: string) => void;
|
setOptimizationMode: (mode: string) => void;
|
||||||
setFocusMode: (mode: string) => void;
|
setFocusMode: (mode: string) => void;
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
@@ -94,17 +91,6 @@ const checkConfig = async (
|
|||||||
'embeddingModelProviderId',
|
'embeddingModelProviderId',
|
||||||
);
|
);
|
||||||
|
|
||||||
const autoImageSearch = localStorage.getItem('autoImageSearch');
|
|
||||||
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
|
|
||||||
|
|
||||||
if (!autoImageSearch) {
|
|
||||||
localStorage.setItem('autoImageSearch', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!autoVideoSearch) {
|
|
||||||
localStorage.setItem('autoVideoSearch', 'false');
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`/api/providers`, {
|
const res = await fetch(`/api/providers`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -214,18 +200,26 @@ const loadMessages = async (
|
|||||||
|
|
||||||
setMessages(messages);
|
setMessages(messages);
|
||||||
|
|
||||||
const chatTurns = messages.filter(
|
const history: [string, string][] = [];
|
||||||
(msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
|
messages.forEach((msg) => {
|
||||||
);
|
history.push(['human', msg.query]);
|
||||||
|
|
||||||
const history = chatTurns.map((msg) => {
|
const textBlocks = msg.responseBlocks
|
||||||
return [msg.role, msg.content];
|
.filter(
|
||||||
}) as [string, string][];
|
(block): block is Block & { type: 'text' } => block.type === 'text',
|
||||||
|
)
|
||||||
|
.map((block) => block.data)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
if (textBlocks) {
|
||||||
|
history.push(['assistant', textBlocks]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.debug(new Date(), 'app:messages_loaded');
|
console.debug(new Date(), 'app:messages_loaded');
|
||||||
|
|
||||||
if (chatTurns.length > 0) {
|
if (messages.length > 0) {
|
||||||
document.title = chatTurns[0].content;
|
document.title = messages[0].query;
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = data.chat.files.map((file: any) => {
|
const files = data.chat.files.map((file: any) => {
|
||||||
@@ -256,12 +250,12 @@ export const chatContext = createContext<ChatContext>({
|
|||||||
loading: false,
|
loading: false,
|
||||||
messageAppeared: false,
|
messageAppeared: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
chatTurns: [],
|
|
||||||
sections: [],
|
sections: [],
|
||||||
notFound: false,
|
notFound: false,
|
||||||
optimizationMode: '',
|
optimizationMode: '',
|
||||||
chatModelProvider: { key: '', providerId: '' },
|
chatModelProvider: { key: '', providerId: '' },
|
||||||
embeddingModelProvider: { key: '', providerId: '' },
|
embeddingModelProvider: { key: '', providerId: '' },
|
||||||
|
researchEnded: false,
|
||||||
rewrite: () => {},
|
rewrite: () => {},
|
||||||
sendMessage: async () => {},
|
sendMessage: async () => {},
|
||||||
setFileIds: () => {},
|
setFileIds: () => {},
|
||||||
@@ -270,6 +264,7 @@ export const chatContext = createContext<ChatContext>({
|
|||||||
setOptimizationMode: () => {},
|
setOptimizationMode: () => {},
|
||||||
setChatModelProvider: () => {},
|
setChatModelProvider: () => {},
|
||||||
setEmbeddingModelProvider: () => {},
|
setEmbeddingModelProvider: () => {},
|
||||||
|
setResearchEnded: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -283,6 +278,8 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [messageAppeared, setMessageAppeared] = useState(false);
|
const [messageAppeared, setMessageAppeared] = useState(false);
|
||||||
|
|
||||||
|
const [researchEnded, setResearchEnded] = useState(false);
|
||||||
|
|
||||||
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
|
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
|
||||||
@@ -315,66 +312,44 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
const messagesRef = useRef<Message[]>([]);
|
const messagesRef = useRef<Message[]>([]);
|
||||||
|
|
||||||
const chatTurns = useMemo((): ChatTurn[] => {
|
|
||||||
return messages.filter(
|
|
||||||
(msg): msg is ChatTurn => msg.role === 'user' || msg.role === 'assistant',
|
|
||||||
);
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
const sections = useMemo<Section[]>(() => {
|
const sections = useMemo<Section[]>(() => {
|
||||||
const sections: Section[] = [];
|
return messages.map((msg) => {
|
||||||
|
const textBlocks: string[] = [];
|
||||||
messages.forEach((msg, i) => {
|
let speechMessage = '';
|
||||||
if (msg.role === 'user') {
|
|
||||||
const nextUserMessageIndex = messages.findIndex(
|
|
||||||
(m, j) => j > i && m.role === 'user',
|
|
||||||
);
|
|
||||||
|
|
||||||
const aiMessage = messages.find(
|
|
||||||
(m, j) =>
|
|
||||||
j > i &&
|
|
||||||
m.role === 'assistant' &&
|
|
||||||
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
|
|
||||||
) as AssistantMessage | undefined;
|
|
||||||
|
|
||||||
const sourceMessage = messages.find(
|
|
||||||
(m, j) =>
|
|
||||||
j > i &&
|
|
||||||
m.role === 'source' &&
|
|
||||||
m.sources &&
|
|
||||||
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
|
|
||||||
) as SourceMessage | undefined;
|
|
||||||
|
|
||||||
let thinkingEnded = false;
|
let thinkingEnded = false;
|
||||||
let processedMessage = aiMessage?.content ?? '';
|
|
||||||
let speechMessage = aiMessage?.content ?? '';
|
|
||||||
let suggestions: string[] = [];
|
let suggestions: string[] = [];
|
||||||
|
|
||||||
if (aiMessage) {
|
const sourceBlocks = msg.responseBlocks.filter(
|
||||||
|
(block): block is Block & { type: 'source' } => block.type === 'source',
|
||||||
|
);
|
||||||
|
const sources = sourceBlocks.flatMap((block) => block.data);
|
||||||
|
|
||||||
|
const widgetBlocks = msg.responseBlocks
|
||||||
|
.filter((b) => b.type === 'widget')
|
||||||
|
.map((b) => b.data) as Widget[];
|
||||||
|
|
||||||
|
msg.responseBlocks.forEach((block) => {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
let processedText = block.data;
|
||||||
const citationRegex = /\[([^\]]+)\]/g;
|
const citationRegex = /\[([^\]]+)\]/g;
|
||||||
const regex = /\[(\d+)\]/g;
|
const regex = /\[(\d+)\]/g;
|
||||||
|
|
||||||
if (processedMessage.includes('<think>')) {
|
if (processedText.includes('<think>')) {
|
||||||
const openThinkTag =
|
const openThinkTag = processedText.match(/<think>/g)?.length || 0;
|
||||||
processedMessage.match(/<think>/g)?.length || 0;
|
|
||||||
const closeThinkTag =
|
const closeThinkTag =
|
||||||
processedMessage.match(/<\/think>/g)?.length || 0;
|
processedText.match(/<\/think>/g)?.length || 0;
|
||||||
|
|
||||||
if (openThinkTag && !closeThinkTag) {
|
if (openThinkTag && !closeThinkTag) {
|
||||||
processedMessage += '</think> <a> </a>';
|
processedText += '</think> <a> </a>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aiMessage.content.includes('</think>')) {
|
if (block.data.includes('</think>')) {
|
||||||
thinkingEnded = true;
|
thinkingEnded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (sources.length > 0) {
|
||||||
sourceMessage &&
|
processedText = processedText.replace(
|
||||||
sourceMessage.sources &&
|
|
||||||
sourceMessage.sources.length > 0
|
|
||||||
) {
|
|
||||||
processedMessage = processedMessage.replace(
|
|
||||||
citationRegex,
|
citationRegex,
|
||||||
(_, capturedContent: string) => {
|
(_, capturedContent: string) => {
|
||||||
const numbers = capturedContent
|
const numbers = capturedContent
|
||||||
@@ -389,7 +364,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return `[${numStr}]`;
|
return `[${numStr}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = sourceMessage.sources?.[number - 1];
|
const source = sources[number - 1];
|
||||||
const url = source?.metadata?.url;
|
const url = source?.metadata?.url;
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -403,37 +378,27 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return linksHtml;
|
return linksHtml;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
speechMessage = aiMessage.content.replace(regex, '');
|
speechMessage += block.data.replace(regex, '');
|
||||||
} else {
|
} else {
|
||||||
processedMessage = processedMessage.replace(regex, '');
|
processedText = processedText.replace(regex, '');
|
||||||
speechMessage = aiMessage.content.replace(regex, '');
|
speechMessage += block.data.replace(regex, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestionMessage = messages.find(
|
textBlocks.push(processedText);
|
||||||
(m, j) =>
|
} else if (block.type === 'suggestion') {
|
||||||
j > i &&
|
suggestions = block.data;
|
||||||
m.role === 'suggestion' &&
|
|
||||||
(nextUserMessageIndex === -1 || j < nextUserMessageIndex),
|
|
||||||
) as SuggestionMessage | undefined;
|
|
||||||
|
|
||||||
if (suggestionMessage && suggestionMessage.suggestions.length > 0) {
|
|
||||||
suggestions = suggestionMessage.suggestions;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
sections.push({
|
return {
|
||||||
userMessage: msg,
|
message: msg,
|
||||||
assistantMessage: aiMessage,
|
parsedTextBlocks: textBlocks,
|
||||||
sourceMessage: sourceMessage,
|
|
||||||
parsedAssistantMessage: processedMessage,
|
|
||||||
speechMessage,
|
speechMessage,
|
||||||
thinkingEnded,
|
thinkingEnded,
|
||||||
suggestions: suggestions,
|
suggestions,
|
||||||
|
widgets: widgetBlocks,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -499,24 +464,17 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
const rewrite = (messageId: string) => {
|
const rewrite = (messageId: string) => {
|
||||||
const index = messages.findIndex((msg) => msg.messageId === messageId);
|
const index = messages.findIndex((msg) => msg.messageId === messageId);
|
||||||
const chatTurnsIndex = chatTurns.findIndex(
|
|
||||||
(msg) => msg.messageId === messageId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
const message = chatTurns[chatTurnsIndex - 1];
|
setMessages((prev) => prev.slice(0, index));
|
||||||
|
|
||||||
setMessages((prev) => {
|
|
||||||
return [
|
|
||||||
...prev.slice(0, messages.length > 2 ? messages.indexOf(message) : 0),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
setChatHistory((prev) => {
|
setChatHistory((prev) => {
|
||||||
return [...prev.slice(0, chatTurns.length > 2 ? chatTurnsIndex - 1 : 0)];
|
return prev.slice(0, index * 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
sendMessage(message.content, message.messageId, true);
|
const messageToRewrite = messages[index];
|
||||||
|
sendMessage(messageToRewrite.query, messageToRewrite.messageId, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -537,140 +495,213 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
) => {
|
) => {
|
||||||
if (loading || !message) return;
|
if (loading || !message) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setResearchEnded(false);
|
||||||
setMessageAppeared(false);
|
setMessageAppeared(false);
|
||||||
|
|
||||||
if (messages.length <= 1) {
|
if (messages.length <= 1) {
|
||||||
window.history.replaceState(null, '', `/c/${chatId}`);
|
window.history.replaceState(null, '', `/c/${chatId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let recievedMessage = '';
|
|
||||||
let added = false;
|
|
||||||
|
|
||||||
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
||||||
|
const backendId = crypto.randomBytes(20).toString('hex');
|
||||||
|
|
||||||
setMessages((prevMessages) => [
|
const newMessage: Message = {
|
||||||
...prevMessages,
|
messageId,
|
||||||
{
|
|
||||||
content: message,
|
|
||||||
messageId: messageId,
|
|
||||||
chatId: chatId!,
|
chatId: chatId!,
|
||||||
role: 'user',
|
backendId,
|
||||||
|
query: message,
|
||||||
|
responseBlocks: [],
|
||||||
|
status: 'answering',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
},
|
};
|
||||||
]);
|
|
||||||
|
setMessages((prevMessages) => [...prevMessages, newMessage]);
|
||||||
|
|
||||||
|
const receivedTextRef = { current: '' };
|
||||||
|
|
||||||
const messageHandler = async (data: any) => {
|
const messageHandler = async (data: any) => {
|
||||||
if (data.type === 'error') {
|
if (data.type === 'error') {
|
||||||
toast.error(data.data);
|
toast.error(data.data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.messageId === messageId
|
||||||
|
? { ...msg, status: 'error' as const }
|
||||||
|
: msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.type === 'researchComplete') {
|
||||||
|
setResearchEnded(true);
|
||||||
|
if (
|
||||||
|
newMessage.responseBlocks.find(
|
||||||
|
(b) => b.type === 'source' && b.data.length > 0,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setMessageAppeared(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'block') {
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) => {
|
||||||
|
if (msg.messageId === messageId) {
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
responseBlocks: [...msg.responseBlocks, data.block],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'updateBlock') {
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) => {
|
||||||
|
if (msg.messageId === messageId) {
|
||||||
|
const updatedBlocks = msg.responseBlocks.map((block) => {
|
||||||
|
if (block.id === data.blockId) {
|
||||||
|
const updatedBlock = { ...block };
|
||||||
|
applyPatch(updatedBlock, data.patch);
|
||||||
|
return updatedBlock;
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
});
|
||||||
|
return { ...msg, responseBlocks: updatedBlocks };
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.type === 'sources') {
|
if (data.type === 'sources') {
|
||||||
setMessages((prevMessages) => [
|
const sourceBlock: Block = {
|
||||||
...prevMessages,
|
id: crypto.randomBytes(7).toString('hex'),
|
||||||
{
|
type: 'source',
|
||||||
messageId: data.messageId,
|
data: data.data,
|
||||||
chatId: chatId!,
|
};
|
||||||
role: 'source',
|
|
||||||
sources: data.data,
|
setMessages((prev) =>
|
||||||
createdAt: new Date(),
|
prev.map((msg) => {
|
||||||
},
|
if (msg.messageId === messageId) {
|
||||||
]);
|
return {
|
||||||
|
...msg,
|
||||||
|
responseBlocks: [...msg.responseBlocks, sourceBlock],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (data.data.length > 0) {
|
if (data.data.length > 0) {
|
||||||
setMessageAppeared(true);
|
setMessageAppeared(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'message') {
|
if (data.type === 'message') {
|
||||||
if (!added) {
|
receivedTextRef.current += data.data;
|
||||||
setMessages((prevMessages) => [
|
|
||||||
...prevMessages,
|
|
||||||
{
|
|
||||||
content: data.data,
|
|
||||||
messageId: data.messageId,
|
|
||||||
chatId: chatId!,
|
|
||||||
role: 'assistant',
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
added = true;
|
|
||||||
setMessageAppeared(true);
|
|
||||||
} else {
|
|
||||||
setMessages((prev) =>
|
|
||||||
prev.map((message) => {
|
|
||||||
if (
|
|
||||||
message.messageId === data.messageId &&
|
|
||||||
message.role === 'assistant'
|
|
||||||
) {
|
|
||||||
return { ...message, content: message.content + data.data };
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) => {
|
||||||
|
if (msg.messageId === messageId) {
|
||||||
|
const existingTextBlockIndex = msg.responseBlocks.findIndex(
|
||||||
|
(b) => b.type === 'text',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingTextBlockIndex >= 0) {
|
||||||
|
const updatedBlocks = [...msg.responseBlocks];
|
||||||
|
const existingBlock = updatedBlocks[
|
||||||
|
existingTextBlockIndex
|
||||||
|
] as Block & { type: 'text' };
|
||||||
|
updatedBlocks[existingTextBlockIndex] = {
|
||||||
|
...existingBlock,
|
||||||
|
data: existingBlock.data + data.data,
|
||||||
|
};
|
||||||
|
return { ...msg, responseBlocks: updatedBlocks };
|
||||||
|
} else {
|
||||||
|
const textBlock: Block = {
|
||||||
|
id: crypto.randomBytes(7).toString('hex'),
|
||||||
|
type: 'text',
|
||||||
|
data: data.data,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
responseBlocks: [...msg.responseBlocks, textBlock],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
setMessageAppeared(true);
|
||||||
recievedMessage += data.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'messageEnd') {
|
if (data.type === 'messageEnd') {
|
||||||
setChatHistory((prevHistory) => [
|
const newHistory: [string, string][] = [
|
||||||
...prevHistory,
|
...chatHistory,
|
||||||
['human', message],
|
['human', message],
|
||||||
['assistant', recievedMessage],
|
['assistant', receivedTextRef.current],
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
setChatHistory(newHistory);
|
||||||
|
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.messageId === messageId
|
||||||
|
? { ...msg, status: 'completed' as const }
|
||||||
|
: msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
||||||
|
|
||||||
const autoImageSearch = localStorage.getItem('autoImageSearch');
|
const autoMediaSearch = getAutoMediaSearch();
|
||||||
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
|
|
||||||
|
|
||||||
if (autoImageSearch === 'true') {
|
if (autoMediaSearch) {
|
||||||
document
|
document
|
||||||
.getElementById(`search-images-${lastMsg.messageId}`)
|
.getElementById(`search-images-${lastMsg.messageId}`)
|
||||||
?.click();
|
?.click();
|
||||||
}
|
|
||||||
|
|
||||||
if (autoVideoSearch === 'true') {
|
|
||||||
document
|
document
|
||||||
.getElementById(`search-videos-${lastMsg.messageId}`)
|
.getElementById(`search-videos-${lastMsg.messageId}`)
|
||||||
?.click();
|
?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check if there are sources after message id's index and no suggestions */
|
// Check if there are sources and no suggestions
|
||||||
|
const currentMsg = messagesRef.current.find(
|
||||||
const userMessageIndex = messagesRef.current.findIndex(
|
(msg) => msg.messageId === messageId,
|
||||||
(msg) => msg.messageId === messageId && msg.role === 'user',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sourceMessage = messagesRef.current.find(
|
const hasSourceBlocks = currentMsg?.responseBlocks.some(
|
||||||
(msg, i) => i > userMessageIndex && msg.role === 'source',
|
(block) => block.type === 'source' && block.data.length > 0,
|
||||||
) as SourceMessage | undefined;
|
);
|
||||||
|
const hasSuggestions = currentMsg?.responseBlocks.some(
|
||||||
const suggestionMessageIndex = messagesRef.current.findIndex(
|
(block) => block.type === 'suggestion',
|
||||||
(msg, i) => i > userMessageIndex && msg.role === 'suggestion',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (hasSourceBlocks && !hasSuggestions) {
|
||||||
sourceMessage &&
|
const suggestions = await getSuggestions(newHistory);
|
||||||
sourceMessage.sources.length > 0 &&
|
const suggestionBlock: Block = {
|
||||||
suggestionMessageIndex == -1
|
id: crypto.randomBytes(7).toString('hex'),
|
||||||
) {
|
type: 'suggestion',
|
||||||
const suggestions = await getSuggestions(messagesRef.current);
|
data: suggestions,
|
||||||
setMessages((prev) => {
|
};
|
||||||
return [
|
|
||||||
...prev,
|
setMessages((prev) =>
|
||||||
{
|
prev.map((msg) => {
|
||||||
role: 'suggestion',
|
if (msg.messageId === messageId) {
|
||||||
suggestions: suggestions,
|
return {
|
||||||
chatId: chatId!,
|
...msg,
|
||||||
createdAt: new Date(),
|
responseBlocks: [...msg.responseBlocks, suggestionBlock],
|
||||||
messageId: crypto.randomBytes(7).toString('hex'),
|
};
|
||||||
},
|
}
|
||||||
];
|
return msg;
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -739,7 +770,6 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
<chatContext.Provider
|
<chatContext.Provider
|
||||||
value={{
|
value={{
|
||||||
messages,
|
messages,
|
||||||
chatTurns,
|
|
||||||
sections,
|
sections,
|
||||||
chatHistory,
|
chatHistory,
|
||||||
files,
|
files,
|
||||||
@@ -763,6 +793,8 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
chatModelProvider,
|
chatModelProvider,
|
||||||
embeddingModelProvider,
|
embeddingModelProvider,
|
||||||
setEmbeddingModelProvider,
|
setEmbeddingModelProvider,
|
||||||
|
researchEnded,
|
||||||
|
setResearchEnded,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
9
src/lib/models/base/embedding.ts
Normal file
9
src/lib/models/base/embedding.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Chunk } from '@/lib/types';
|
||||||
|
|
||||||
|
abstract class BaseEmbedding<CONFIG> {
|
||||||
|
constructor(protected config: CONFIG) {}
|
||||||
|
abstract embedText(texts: string[]): Promise<number[][]>;
|
||||||
|
abstract embedChunks(chunks: Chunk[]): Promise<number[][]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseEmbedding;
|
||||||
22
src/lib/models/base/llm.ts
Normal file
22
src/lib/models/base/llm.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import {
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
abstract class BaseLLM<CONFIG> {
|
||||||
|
constructor(protected config: CONFIG) {}
|
||||||
|
abstract generateText(input: GenerateTextInput): Promise<GenerateTextOutput>;
|
||||||
|
abstract streamText(
|
||||||
|
input: GenerateTextInput,
|
||||||
|
): AsyncGenerator<StreamTextOutput>;
|
||||||
|
abstract generateObject<T>(input: GenerateObjectInput): Promise<z.infer<T>>;
|
||||||
|
abstract streamObject<T>(
|
||||||
|
input: GenerateObjectInput,
|
||||||
|
): AsyncGenerator<Partial<z.infer<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseLLM;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Embeddings } from '@langchain/core/embeddings';
|
import { ModelList, ProviderMetadata } from '../types';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
|
import BaseLLM from './llm';
|
||||||
|
import BaseEmbedding from './embedding';
|
||||||
|
|
||||||
abstract class BaseModelProvider<CONFIG> {
|
abstract class BaseModelProvider<CONFIG> {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -11,8 +11,8 @@ abstract class BaseModelProvider<CONFIG> {
|
|||||||
) {}
|
) {}
|
||||||
abstract getDefaultModels(): Promise<ModelList>;
|
abstract getDefaultModels(): Promise<ModelList>;
|
||||||
abstract getModelList(): Promise<ModelList>;
|
abstract getModelList(): Promise<ModelList>;
|
||||||
abstract loadChatModel(modelName: string): Promise<BaseChatModel>;
|
abstract loadChatModel(modelName: string): Promise<BaseLLM<any>>;
|
||||||
abstract loadEmbeddingModel(modelName: string): Promise<Embeddings>;
|
abstract loadEmbeddingModel(modelName: string): Promise<BaseEmbedding<any>>;
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
static getProviderConfigFields(): UIConfigField[] {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface AimlConfig {
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'API Key',
|
|
||||||
key: 'apiKey',
|
|
||||||
description: 'Your AI/ML API key',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'AI/ML API Key',
|
|
||||||
env: 'AIML_API_KEY',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class AimlProvider extends BaseModelProvider<AimlConfig> {
|
|
||||||
constructor(id: string, name: string, config: AimlConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
try {
|
|
||||||
const res = await fetch('https://api.aimlapi.com/models', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${this.config.apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const chatModels: Model[] = data.data
|
|
||||||
.filter((m: any) => m.type === 'chat-completion')
|
|
||||||
.map((m: any) => {
|
|
||||||
return {
|
|
||||||
name: m.id,
|
|
||||||
key: m.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddingModels: Model[] = data.data
|
|
||||||
.filter((m: any) => m.type === 'embedding')
|
|
||||||
.map((m: any) => {
|
|
||||||
return {
|
|
||||||
name: m.id,
|
|
||||||
key: m.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: embeddingModels,
|
|
||||||
chat: chatModels,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof TypeError) {
|
|
||||||
throw new Error(
|
|
||||||
'Error connecting to AI/ML API. Please ensure your API key is correct and the service is available.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [
|
|
||||||
...defaultModels.embedding,
|
|
||||||
...configProvider.embeddingModels,
|
|
||||||
],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading AI/ML API Chat Model. Invalid Model Selected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatOpenAI({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
configuration: {
|
|
||||||
baseURL: 'https://api.aimlapi.com',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading AI/ML API Embedding Model. Invalid Model Selected.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new OpenAIEmbeddings({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
model: key,
|
|
||||||
configuration: {
|
|
||||||
baseURL: 'https://api.aimlapi.com',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): AimlConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.apiKey)
|
|
||||||
throw new Error('Invalid config provided. API key must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiKey: String(raw.apiKey),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'aiml',
|
|
||||||
name: 'AI/ML API',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AimlProvider;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatAnthropic } from '@langchain/anthropic';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface AnthropicConfig {
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'API Key',
|
|
||||||
key: 'apiKey',
|
|
||||||
description: 'Your Anthropic API key',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'Anthropic API Key',
|
|
||||||
env: 'ANTHROPIC_API_KEY',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class AnthropicProvider extends BaseModelProvider<AnthropicConfig> {
|
|
||||||
constructor(id: string, name: string, config: AnthropicConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
const res = await fetch('https://api.anthropic.com/v1/models?limit=999', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'x-api-key': this.config.apiKey,
|
|
||||||
'anthropic-version': '2023-06-01',
|
|
||||||
'Content-type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed to fetch Anthropic models: ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await res.json()).data;
|
|
||||||
|
|
||||||
const models: Model[] = data.map((m: any) => {
|
|
||||||
return {
|
|
||||||
key: m.id,
|
|
||||||
name: m.display_name,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [],
|
|
||||||
chat: models,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading Anthropic Chat Model. Invalid Model Selected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatAnthropic({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
throw new Error('Anthropic provider does not support embedding models.');
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): AnthropicConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.apiKey)
|
|
||||||
throw new Error('Invalid config provided. API key must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiKey: String(raw.apiKey),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'anthropic',
|
|
||||||
name: 'Anthropic',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnthropicProvider;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface DeepSeekConfig {
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultChatModels: Model[] = [
|
|
||||||
{
|
|
||||||
name: 'Deepseek Chat / DeepSeek V3.2 Exp',
|
|
||||||
key: 'deepseek-chat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Deepseek Reasoner / DeepSeek V3.2 Exp',
|
|
||||||
key: 'deepseek-reasoner',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'API Key',
|
|
||||||
key: 'apiKey',
|
|
||||||
description: 'Your DeepSeek API key',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'DeepSeek API Key',
|
|
||||||
env: 'DEEPSEEK_API_KEY',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class DeepSeekProvider extends BaseModelProvider<DeepSeekConfig> {
|
|
||||||
constructor(id: string, name: string, config: DeepSeekConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
return {
|
|
||||||
embedding: [],
|
|
||||||
chat: defaultChatModels,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading DeepSeek Chat Model. Invalid Model Selected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatOpenAI({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
configuration: {
|
|
||||||
baseURL: 'https://api.deepseek.com',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
throw new Error('DeepSeek provider does not support embedding models.');
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): DeepSeekConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.apiKey)
|
|
||||||
throw new Error('Invalid config provided. API key must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiKey: String(raw.apiKey),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'deepseek',
|
|
||||||
name: 'Deepseek AI',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DeepSeekProvider;
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import {
|
|
||||||
ChatGoogleGenerativeAI,
|
|
||||||
GoogleGenerativeAIEmbeddings,
|
|
||||||
} from '@langchain/google-genai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface GeminiConfig {
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'API Key',
|
|
||||||
key: 'apiKey',
|
|
||||||
description: 'Your Google Gemini API key',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'Google Gemini API Key',
|
|
||||||
env: 'GEMINI_API_KEY',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class GeminiProvider extends BaseModelProvider<GeminiConfig> {
|
|
||||||
constructor(id: string, name: string, config: GeminiConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
const res = await fetch(
|
|
||||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
let defaultEmbeddingModels: Model[] = [];
|
|
||||||
let defaultChatModels: Model[] = [];
|
|
||||||
|
|
||||||
data.models.forEach((m: any) => {
|
|
||||||
if (m.supportedGenerationMethods.includes('embedText')) {
|
|
||||||
defaultEmbeddingModels.push({
|
|
||||||
key: m.name,
|
|
||||||
name: m.displayName,
|
|
||||||
});
|
|
||||||
} else if (m.supportedGenerationMethods.includes('generateContent')) {
|
|
||||||
defaultChatModels.push({
|
|
||||||
key: m.name,
|
|
||||||
name: m.displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: defaultEmbeddingModels,
|
|
||||||
chat: defaultChatModels,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [
|
|
||||||
...defaultModels.embedding,
|
|
||||||
...configProvider.embeddingModels,
|
|
||||||
],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading Gemini Chat Model. Invalid Model Selected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatGoogleGenerativeAI({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading Gemini Embedding Model. Invalid Model Selected.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GoogleGenerativeAIEmbeddings({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
model: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): GeminiConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.apiKey)
|
|
||||||
throw new Error('Invalid config provided. API key must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiKey: String(raw.apiKey),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'gemini',
|
|
||||||
name: 'Google Gemini',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GeminiProvider;
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatGroq } from '@langchain/groq';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface GroqConfig {
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'API Key',
|
|
||||||
key: 'apiKey',
|
|
||||||
description: 'Your Groq API key',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'Groq API Key',
|
|
||||||
env: 'GROQ_API_KEY',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class GroqProvider extends BaseModelProvider<GroqConfig> {
|
|
||||||
constructor(id: string, name: string, config: GroqConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
try {
|
|
||||||
const res = await fetch('https://api.groq.com/openai/v1/models', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${this.config.apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const models: Model[] = data.data.map((m: any) => {
|
|
||||||
return {
|
|
||||||
name: m.id,
|
|
||||||
key: m.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [],
|
|
||||||
chat: models,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof TypeError) {
|
|
||||||
throw new Error(
|
|
||||||
'Error connecting to Groq API. Please ensure your API key is correct and the Groq service is available.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error('Error Loading Groq Chat Model. Invalid Model Selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatGroq({
|
|
||||||
apiKey: this.config.apiKey,
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
throw new Error('Groq provider does not support embedding models.');
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): GroqConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.apiKey)
|
|
||||||
throw new Error('Invalid config provided. API key must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiKey: String(raw.apiKey),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'groq',
|
|
||||||
name: 'Groq',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GroqProvider;
|
|
||||||
@@ -1,27 +1,11 @@
|
|||||||
import { ModelProviderUISection } from '@/lib/config/types';
|
import { ModelProviderUISection } from '@/lib/config/types';
|
||||||
import { ProviderConstructor } from './baseProvider';
|
import { ProviderConstructor } from '../base/provider';
|
||||||
import OpenAIProvider from './openai';
|
import OpenAIProvider from './openai';
|
||||||
import OllamaProvider from './ollama';
|
import OllamaProvider from './ollama';
|
||||||
import TransformersProvider from './transformers';
|
|
||||||
import AnthropicProvider from './anthropic';
|
|
||||||
import GeminiProvider from './gemini';
|
|
||||||
import GroqProvider from './groq';
|
|
||||||
import DeepSeekProvider from './deepseek';
|
|
||||||
import LMStudioProvider from './lmstudio';
|
|
||||||
import LemonadeProvider from './lemonade';
|
|
||||||
import AimlProvider from '@/lib/models/providers/aiml';
|
|
||||||
|
|
||||||
export const providers: Record<string, ProviderConstructor<any>> = {
|
export const providers: Record<string, ProviderConstructor<any>> = {
|
||||||
openai: OpenAIProvider,
|
openai: OpenAIProvider,
|
||||||
ollama: OllamaProvider,
|
ollama: OllamaProvider,
|
||||||
transformers: TransformersProvider,
|
|
||||||
anthropic: AnthropicProvider,
|
|
||||||
gemini: GeminiProvider,
|
|
||||||
groq: GroqProvider,
|
|
||||||
deepseek: DeepSeekProvider,
|
|
||||||
aiml: AimlProvider,
|
|
||||||
lmstudio: LMStudioProvider,
|
|
||||||
lemonade: LemonadeProvider,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModelProvidersUIConfigSection =
|
export const getModelProvidersUIConfigSection =
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface LemonadeConfig {
|
|
||||||
baseURL: string;
|
|
||||||
apiKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'Base URL',
|
|
||||||
key: 'baseURL',
|
|
||||||
description: 'The base URL for Lemonade API',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'https://api.lemonade.ai/v1',
|
|
||||||
env: 'LEMONADE_BASE_URL',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'API Key',
|
|
||||||
key: 'apiKey',
|
|
||||||
description: 'Your Lemonade API key (optional)',
|
|
||||||
required: false,
|
|
||||||
placeholder: 'Lemonade API Key',
|
|
||||||
env: 'LEMONADE_API_KEY',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class LemonadeProvider extends BaseModelProvider<LemonadeConfig> {
|
|
||||||
constructor(id: string, name: string, config: LemonadeConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
try {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.config.apiKey) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${this.config.baseURL}/models`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const models: Model[] = data.data.map((m: any) => {
|
|
||||||
return {
|
|
||||||
name: m.id,
|
|
||||||
key: m.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: models,
|
|
||||||
chat: models,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof TypeError) {
|
|
||||||
throw new Error(
|
|
||||||
'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [
|
|
||||||
...defaultModels.embedding,
|
|
||||||
...configProvider.embeddingModels,
|
|
||||||
],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading Lemonade Chat Model. Invalid Model Selected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatOpenAI({
|
|
||||||
apiKey: this.config.apiKey || 'not-needed',
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
configuration: {
|
|
||||||
baseURL: this.config.baseURL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading Lemonade Embedding Model. Invalid Model Selected.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new OpenAIEmbeddings({
|
|
||||||
apiKey: this.config.apiKey || 'not-needed',
|
|
||||||
model: key,
|
|
||||||
configuration: {
|
|
||||||
baseURL: this.config.baseURL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): LemonadeConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.baseURL)
|
|
||||||
throw new Error('Invalid config provided. Base URL must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseURL: String(raw.baseURL),
|
|
||||||
apiKey: raw.apiKey ? String(raw.apiKey) : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'lemonade',
|
|
||||||
name: 'Lemonade',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LemonadeProvider;
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
|
|
||||||
interface LMStudioConfig {
|
|
||||||
baseURL: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'Base URL',
|
|
||||||
key: 'baseURL',
|
|
||||||
description: 'The base URL for LM Studio server',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'http://localhost:1234',
|
|
||||||
env: 'LM_STUDIO_BASE_URL',
|
|
||||||
scope: 'server',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class LMStudioProvider extends BaseModelProvider<LMStudioConfig> {
|
|
||||||
constructor(id: string, name: string, config: LMStudioConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeBaseURL(url: string): string {
|
|
||||||
const trimmed = url.trim().replace(/\/+$/, '');
|
|
||||||
return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
try {
|
|
||||||
const baseURL = this.normalizeBaseURL(this.config.baseURL);
|
|
||||||
|
|
||||||
const res = await fetch(`${baseURL}/models`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const models: Model[] = data.data.map((m: any) => {
|
|
||||||
return {
|
|
||||||
name: m.id,
|
|
||||||
key: m.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: models,
|
|
||||||
chat: models,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof TypeError) {
|
|
||||||
throw new Error(
|
|
||||||
'Error connecting to LM Studio. Please ensure the base URL is correct and the LM Studio server is running.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [
|
|
||||||
...defaultModels.embedding,
|
|
||||||
...configProvider.embeddingModels,
|
|
||||||
],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading LM Studio Chat Model. Invalid Model Selected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatOpenAI({
|
|
||||||
apiKey: 'lm-studio',
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
streaming: true,
|
|
||||||
configuration: {
|
|
||||||
baseURL: this.normalizeBaseURL(this.config.baseURL),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading LM Studio Embedding Model. Invalid Model Selected.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new OpenAIEmbeddings({
|
|
||||||
apiKey: 'lm-studio',
|
|
||||||
model: key,
|
|
||||||
configuration: {
|
|
||||||
baseURL: this.normalizeBaseURL(this.config.baseURL),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): LMStudioConfig {
|
|
||||||
if (!raw || typeof raw !== 'object')
|
|
||||||
throw new Error('Invalid config provided. Expected object');
|
|
||||||
if (!raw.baseURL)
|
|
||||||
throw new Error('Invalid config provided. Base URL must be provided');
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseURL: String(raw.baseURL),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'lmstudio',
|
|
||||||
name: 'LM Studio',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LMStudioProvider;
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOllama, OllamaEmbeddings } from '@langchain/ollama';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
||||||
|
import BaseModelProvider from '../../base/provider';
|
||||||
|
import { Model, ModelList, ProviderMetadata } from '../../types';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
import OllamaLLM from './ollamaLLM';
|
||||||
|
import OllamaEmbedding from './ollamaEmbedding';
|
||||||
|
|
||||||
interface OllamaConfig {
|
interface OllamaConfig {
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
@@ -76,7 +77,7 @@ class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
async loadChatModel(key: string): Promise<BaseLLM<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
const exists = modelList.chat.find((m) => m.key === key);
|
||||||
@@ -87,14 +88,13 @@ class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChatOllama({
|
return new OllamaLLM({
|
||||||
temperature: 0.7,
|
baseURL: this.config.baseURL,
|
||||||
model: key,
|
model: key,
|
||||||
baseUrl: this.config.baseURL,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
const exists = modelList.embedding.find((m) => m.key === key);
|
||||||
|
|
||||||
@@ -104,9 +104,9 @@ class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OllamaEmbeddings({
|
return new OllamaEmbedding({
|
||||||
model: key,
|
model: key,
|
||||||
baseUrl: this.config.baseURL,
|
baseURL: this.config.baseURL,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
40
src/lib/models/providers/ollama/ollamaEmbedding.ts
Normal file
40
src/lib/models/providers/ollama/ollamaEmbedding.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Ollama } from 'ollama';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
import { Chunk } from '@/lib/types';
|
||||||
|
|
||||||
|
type OllamaConfig = {
|
||||||
|
model: string;
|
||||||
|
baseURL?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OllamaEmbedding extends BaseEmbedding<OllamaConfig> {
|
||||||
|
ollamaClient: Ollama;
|
||||||
|
|
||||||
|
constructor(protected config: OllamaConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.ollamaClient = new Ollama({
|
||||||
|
host: this.config.baseURL || 'http://localhost:11434',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedText(texts: string[]): Promise<number[][]> {
|
||||||
|
const response = await this.ollamaClient.embed({
|
||||||
|
input: texts,
|
||||||
|
model: this.config.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedChunks(chunks: Chunk[]): Promise<number[][]> {
|
||||||
|
const response = await this.ollamaClient.embed({
|
||||||
|
input: chunks.map((c) => c.content),
|
||||||
|
model: this.config.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.embeddings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OllamaEmbedding;
|
||||||
173
src/lib/models/providers/ollama/ollamaLLM.ts
Normal file
173
src/lib/models/providers/ollama/ollamaLLM.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import {
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
} from '../../types';
|
||||||
|
import { Ollama } from 'ollama';
|
||||||
|
import { parse } from 'partial-json';
|
||||||
|
|
||||||
|
type OllamaConfig = {
|
||||||
|
baseURL: string;
|
||||||
|
model: string;
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reasoningModels = [
|
||||||
|
'gpt-oss',
|
||||||
|
'deepseek-r1',
|
||||||
|
'qwen3',
|
||||||
|
'deepseek-v3.1',
|
||||||
|
'magistral',
|
||||||
|
];
|
||||||
|
|
||||||
|
class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||||
|
ollamaClient: Ollama;
|
||||||
|
|
||||||
|
constructor(protected config: OllamaConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.ollamaClient = new Ollama({
|
||||||
|
host: this.config.baseURL || 'http://localhost:11434',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateText(input: GenerateTextInput): Promise<GenerateTextOutput> {
|
||||||
|
const res = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
options: {
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 0.7,
|
||||||
|
num_predict: input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
num_ctx: 32000,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ??
|
||||||
|
this.config.options?.presencePenalty,
|
||||||
|
stop:
|
||||||
|
input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: res.message.content,
|
||||||
|
additionalInfo: {
|
||||||
|
reasoning: res.message.thinking,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamText(
|
||||||
|
input: GenerateTextInput,
|
||||||
|
): AsyncGenerator<StreamTextOutput> {
|
||||||
|
const stream = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
stream: true,
|
||||||
|
options: {
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 0.7,
|
||||||
|
num_ctx: 32000,
|
||||||
|
num_predict: input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ??
|
||||||
|
this.config.options?.presencePenalty,
|
||||||
|
stop:
|
||||||
|
input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
yield {
|
||||||
|
contentChunk: chunk.message.content,
|
||||||
|
done: chunk.done,
|
||||||
|
additionalInfo: {
|
||||||
|
reasoning: chunk.message.thinking,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
|
||||||
|
const response = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
format: z.toJSONSchema(input.schema),
|
||||||
|
...(reasoningModels.find((m) => this.config.model.includes(m))
|
||||||
|
? { think: false }
|
||||||
|
: {}),
|
||||||
|
options: {
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 0.7,
|
||||||
|
num_predict: input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ??
|
||||||
|
this.config.options?.presencePenalty,
|
||||||
|
stop:
|
||||||
|
input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return input.schema.parse(JSON.parse(response.message.content)) as T;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error parsing response from Ollama: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamObject<T>(input: GenerateObjectInput): AsyncGenerator<T> {
|
||||||
|
let recievedObj: string = '';
|
||||||
|
|
||||||
|
const stream = await this.ollamaClient.chat({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
format: z.toJSONSchema(input.schema),
|
||||||
|
stream: true,
|
||||||
|
...(reasoningModels.find((m) => this.config.model.includes(m))
|
||||||
|
? { think: false }
|
||||||
|
: {}),
|
||||||
|
options: {
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 0.7,
|
||||||
|
num_predict: input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ??
|
||||||
|
this.config.options?.presencePenalty,
|
||||||
|
stop:
|
||||||
|
input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
recievedObj += chunk.message.content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield parse(recievedObj) as T;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error parsing partial object from Ollama:', err);
|
||||||
|
yield {} as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OllamaLLM;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
import { UIConfigField } from '@/lib/config/types';
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
||||||
|
import { Model, ModelList, ProviderMetadata } from '../../types';
|
||||||
|
import OpenAIEmbedding from './openaiEmbedding';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
import BaseModelProvider from '../../base/provider';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import OpenAILLM from './openaiLLM';
|
||||||
|
|
||||||
interface OpenAIConfig {
|
interface OpenAIConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -145,7 +146,7 @@ class OpenAIProvider extends BaseModelProvider<OpenAIConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
async loadChatModel(key: string): Promise<BaseLLM<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
const exists = modelList.chat.find((m) => m.key === key);
|
||||||
@@ -156,17 +157,14 @@ class OpenAIProvider extends BaseModelProvider<OpenAIConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChatOpenAI({
|
return new OpenAILLM({
|
||||||
apiKey: this.config.apiKey,
|
apiKey: this.config.apiKey,
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
model: key,
|
||||||
configuration: {
|
|
||||||
baseURL: this.config.baseURL,
|
baseURL: this.config.baseURL,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
async loadEmbeddingModel(key: string): Promise<BaseEmbedding<any>> {
|
||||||
const modelList = await this.getModelList();
|
const modelList = await this.getModelList();
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
const exists = modelList.embedding.find((m) => m.key === key);
|
||||||
|
|
||||||
@@ -176,12 +174,10 @@ class OpenAIProvider extends BaseModelProvider<OpenAIConfig> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OpenAIEmbeddings({
|
return new OpenAIEmbedding({
|
||||||
apiKey: this.config.apiKey,
|
apiKey: this.config.apiKey,
|
||||||
model: key,
|
model: key,
|
||||||
configuration: {
|
|
||||||
baseURL: this.config.baseURL,
|
baseURL: this.config.baseURL,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
42
src/lib/models/providers/openai/openaiEmbedding.ts
Normal file
42
src/lib/models/providers/openai/openaiEmbedding.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import BaseEmbedding from '../../base/embedding';
|
||||||
|
import { Chunk } from '@/lib/types';
|
||||||
|
|
||||||
|
type OpenAIConfig = {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
baseURL?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpenAIEmbedding extends BaseEmbedding<OpenAIConfig> {
|
||||||
|
openAIClient: OpenAI;
|
||||||
|
|
||||||
|
constructor(protected config: OpenAIConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.openAIClient = new OpenAI({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedText(texts: string[]): Promise<number[][]> {
|
||||||
|
const response = await this.openAIClient.embeddings.create({
|
||||||
|
model: this.config.model,
|
||||||
|
input: texts,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.map((embedding) => embedding.embedding);
|
||||||
|
}
|
||||||
|
|
||||||
|
async embedChunks(chunks: Chunk[]): Promise<number[][]> {
|
||||||
|
const response = await this.openAIClient.embeddings.create({
|
||||||
|
model: this.config.model,
|
||||||
|
input: chunks.map((c) => c.content),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.map((embedding) => embedding.embedding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenAIEmbedding;
|
||||||
166
src/lib/models/providers/openai/openaiLLM.ts
Normal file
166
src/lib/models/providers/openai/openaiLLM.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import BaseLLM from '../../base/llm';
|
||||||
|
import { zodTextFormat, zodResponseFormat } from 'openai/helpers/zod';
|
||||||
|
import {
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
} from '../../types';
|
||||||
|
import { parse } from 'partial-json';
|
||||||
|
|
||||||
|
type OpenAIConfig = {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
baseURL?: string;
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpenAILLM extends BaseLLM<OpenAIConfig> {
|
||||||
|
openAIClient: OpenAI;
|
||||||
|
|
||||||
|
constructor(protected config: OpenAIConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
this.openAIClient = new OpenAI({
|
||||||
|
apiKey: this.config.apiKey,
|
||||||
|
baseURL: this.config.baseURL || 'https://api.openai.com/v1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateText(input: GenerateTextInput): Promise<GenerateTextOutput> {
|
||||||
|
const response = await this.openAIClient.chat.completions.create({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 1.0,
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
max_completion_tokens:
|
||||||
|
input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
stop: input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ?? this.config.options?.presencePenalty,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.choices && response.choices.length > 0) {
|
||||||
|
return {
|
||||||
|
content: response.choices[0].message.content!,
|
||||||
|
additionalInfo: {
|
||||||
|
finishReason: response.choices[0].finish_reason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No response from OpenAI');
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamText(
|
||||||
|
input: GenerateTextInput,
|
||||||
|
): AsyncGenerator<StreamTextOutput> {
|
||||||
|
const stream = await this.openAIClient.chat.completions.create({
|
||||||
|
model: this.config.model,
|
||||||
|
messages: input.messages,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 1.0,
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
max_completion_tokens:
|
||||||
|
input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
stop: input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ?? this.config.options?.presencePenalty,
|
||||||
|
stream: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.choices && chunk.choices.length > 0) {
|
||||||
|
yield {
|
||||||
|
contentChunk: chunk.choices[0].delta.content || '',
|
||||||
|
done: chunk.choices[0].finish_reason !== null,
|
||||||
|
additionalInfo: {
|
||||||
|
finishReason: chunk.choices[0].finish_reason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
|
||||||
|
const response = await this.openAIClient.chat.completions.parse({
|
||||||
|
messages: input.messages,
|
||||||
|
model: this.config.model,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 1.0,
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
max_completion_tokens:
|
||||||
|
input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
stop: input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ?? this.config.options?.presencePenalty,
|
||||||
|
response_format: zodResponseFormat(input.schema, 'object'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.choices && response.choices.length > 0) {
|
||||||
|
try {
|
||||||
|
return input.schema.parse(response.choices[0].message.parsed) as T;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error parsing response from OpenAI: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No response from OpenAI');
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamObject<T>(input: GenerateObjectInput): AsyncGenerator<T> {
|
||||||
|
let recievedObj: string = '';
|
||||||
|
|
||||||
|
const stream = this.openAIClient.responses.stream({
|
||||||
|
model: this.config.model,
|
||||||
|
input: input.messages,
|
||||||
|
temperature:
|
||||||
|
input.options?.temperature ?? this.config.options?.temperature ?? 1.0,
|
||||||
|
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||||
|
max_completion_tokens:
|
||||||
|
input.options?.maxTokens ?? this.config.options?.maxTokens,
|
||||||
|
stop: input.options?.stopSequences ?? this.config.options?.stopSequences,
|
||||||
|
frequency_penalty:
|
||||||
|
input.options?.frequencyPenalty ??
|
||||||
|
this.config.options?.frequencyPenalty,
|
||||||
|
presence_penalty:
|
||||||
|
input.options?.presencePenalty ?? this.config.options?.presencePenalty,
|
||||||
|
text: {
|
||||||
|
format: zodTextFormat(input.schema, 'object'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.type === 'response.output_text.delta' && chunk.delta) {
|
||||||
|
recievedObj += chunk.delta;
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield parse(recievedObj) as T;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error parsing partial object from OpenAI:', err);
|
||||||
|
yield {} as T;
|
||||||
|
}
|
||||||
|
} else if (chunk.type === 'response.output_text.done' && chunk.text) {
|
||||||
|
try {
|
||||||
|
yield parse(chunk.text) as T;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error parsing response from OpenAI: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenAILLM;
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { Model, ModelList, ProviderMetadata } from '../types';
|
|
||||||
import BaseModelProvider from './baseProvider';
|
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
|
||||||
import { UIConfigField } from '@/lib/config/types';
|
|
||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
|
||||||
import { HuggingFaceTransformersEmbeddings } from '@langchain/community/embeddings/huggingface_transformers';
|
|
||||||
interface TransformersConfig {}
|
|
||||||
|
|
||||||
const defaultEmbeddingModels: Model[] = [
|
|
||||||
{
|
|
||||||
name: 'all-MiniLM-L6-v2',
|
|
||||||
key: 'Xenova/all-MiniLM-L6-v2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mxbai-embed-large-v1',
|
|
||||||
key: 'mixedbread-ai/mxbai-embed-large-v1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'nomic-embed-text-v1',
|
|
||||||
key: 'Xenova/nomic-embed-text-v1',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [];
|
|
||||||
|
|
||||||
class TransformersProvider extends BaseModelProvider<TransformersConfig> {
|
|
||||||
constructor(id: string, name: string, config: TransformersConfig) {
|
|
||||||
super(id, name, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
|
||||||
return {
|
|
||||||
embedding: [...defaultEmbeddingModels],
|
|
||||||
chat: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: [
|
|
||||||
...defaultModels.embedding,
|
|
||||||
...configProvider.embeddingModels,
|
|
||||||
],
|
|
||||||
chat: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
|
||||||
throw new Error('Transformers Provider does not support chat models.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
|
||||||
const modelList = await this.getModelList();
|
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
throw new Error(
|
|
||||||
'Error Loading OpenAI Embedding Model. Invalid Model Selected.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HuggingFaceTransformersEmbeddings({
|
|
||||||
model: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): TransformersConfig {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'transformers',
|
|
||||||
name: 'Transformers',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TransformersProvider;
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { ConfigModelProvider } from '../config/types';
|
import { ConfigModelProvider } from '../config/types';
|
||||||
import BaseModelProvider, {
|
import BaseModelProvider, { createProviderInstance } from './base/provider';
|
||||||
createProviderInstance,
|
|
||||||
} from './providers/baseProvider';
|
|
||||||
import { getConfiguredModelProviders } from '../config/serverRegistry';
|
import { getConfiguredModelProviders } from '../config/serverRegistry';
|
||||||
import { providers } from './providers';
|
import { providers } from './providers';
|
||||||
import { MinimalProvider, ModelList } from './types';
|
import { MinimalProvider, ModelList } from './types';
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { ChatTurnMessage } from '../types';
|
||||||
|
|
||||||
type Model = {
|
type Model = {
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -25,10 +28,59 @@ type ModelWithProvider = {
|
|||||||
providerId: string;
|
providerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GenerateOptions = {
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
topP?: number;
|
||||||
|
stopSequences?: string[];
|
||||||
|
frequencyPenalty?: number;
|
||||||
|
presencePenalty?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateTextInput = {
|
||||||
|
messages: ChatTurnMessage[];
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateTextOutput = {
|
||||||
|
content: string;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamTextOutput = {
|
||||||
|
contentChunk: string;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
done?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateObjectInput = {
|
||||||
|
schema: z.ZodTypeAny;
|
||||||
|
messages: ChatTurnMessage[];
|
||||||
|
options?: GenerateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateObjectOutput<T> = {
|
||||||
|
object: T;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamObjectOutput<T> = {
|
||||||
|
objectChunk: Partial<T>;
|
||||||
|
additionalInfo?: Record<string, any>;
|
||||||
|
done?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
Model,
|
Model,
|
||||||
ModelList,
|
ModelList,
|
||||||
ProviderMetadata,
|
ProviderMetadata,
|
||||||
MinimalProvider,
|
MinimalProvider,
|
||||||
ModelWithProvider,
|
ModelWithProvider,
|
||||||
|
GenerateOptions,
|
||||||
|
GenerateTextInput,
|
||||||
|
GenerateTextOutput,
|
||||||
|
StreamTextOutput,
|
||||||
|
GenerateObjectInput,
|
||||||
|
GenerateObjectOutput,
|
||||||
|
StreamObjectOutput,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { BaseOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
|
|
||||||
interface LineOutputParserArgs {
|
|
||||||
key?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LineOutputParser extends BaseOutputParser<string | undefined> {
|
|
||||||
private key = 'questions';
|
|
||||||
|
|
||||||
constructor(args?: LineOutputParserArgs) {
|
|
||||||
super();
|
|
||||||
this.key = args?.key ?? this.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
static lc_name() {
|
|
||||||
return 'LineOutputParser';
|
|
||||||
}
|
|
||||||
|
|
||||||
lc_namespace = ['langchain', 'output_parsers', 'line_output_parser'];
|
|
||||||
|
|
||||||
async parse(text: string): Promise<string | undefined> {
|
|
||||||
text = text.trim() || '';
|
|
||||||
|
|
||||||
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
|
|
||||||
const startKeyIndex = text.indexOf(`<${this.key}>`);
|
|
||||||
const endKeyIndex = text.indexOf(`</${this.key}>`);
|
|
||||||
|
|
||||||
if (startKeyIndex === -1 || endKeyIndex === -1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const questionsStartIndex =
|
|
||||||
startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length;
|
|
||||||
const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex;
|
|
||||||
const line = text
|
|
||||||
.slice(questionsStartIndex, questionsEndIndex)
|
|
||||||
.trim()
|
|
||||||
.replace(regex, '');
|
|
||||||
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormatInstructions(): string {
|
|
||||||
throw new Error('Not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LineOutputParser;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { BaseOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
|
|
||||||
interface LineListOutputParserArgs {
|
|
||||||
key?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LineListOutputParser extends BaseOutputParser<string[]> {
|
|
||||||
private key = 'questions';
|
|
||||||
|
|
||||||
constructor(args?: LineListOutputParserArgs) {
|
|
||||||
super();
|
|
||||||
this.key = args?.key ?? this.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
static lc_name() {
|
|
||||||
return 'LineListOutputParser';
|
|
||||||
}
|
|
||||||
|
|
||||||
lc_namespace = ['langchain', 'output_parsers', 'line_list_output_parser'];
|
|
||||||
|
|
||||||
async parse(text: string): Promise<string[]> {
|
|
||||||
text = text.trim() || '';
|
|
||||||
|
|
||||||
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
|
|
||||||
const startKeyIndex = text.indexOf(`<${this.key}>`);
|
|
||||||
const endKeyIndex = text.indexOf(`</${this.key}>`);
|
|
||||||
|
|
||||||
if (startKeyIndex === -1 || endKeyIndex === -1) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const questionsStartIndex =
|
|
||||||
startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length;
|
|
||||||
const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex;
|
|
||||||
const lines = text
|
|
||||||
.slice(questionsStartIndex, questionsEndIndex)
|
|
||||||
.trim()
|
|
||||||
.split('\n')
|
|
||||||
.filter((line) => line.trim() !== '')
|
|
||||||
.map((line) => line.replace(regex, ''));
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormatInstructions(): string {
|
|
||||||
throw new Error('Not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LineListOutputParser;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import {
|
|
||||||
webSearchResponsePrompt,
|
|
||||||
webSearchRetrieverFewShots,
|
|
||||||
webSearchRetrieverPrompt,
|
|
||||||
} from './webSearch';
|
|
||||||
import { writingAssistantPrompt } from './writingAssistant';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
webSearchResponsePrompt,
|
|
||||||
webSearchRetrieverPrompt,
|
|
||||||
webSearchRetrieverFewShots,
|
|
||||||
writingAssistantPrompt,
|
|
||||||
};
|
|
||||||
29
src/lib/prompts/media/image.ts
Normal file
29
src/lib/prompts/media/image.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
|
||||||
|
export const imageSearchPrompt = `
|
||||||
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
|
||||||
|
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
||||||
|
Output only the rephrased query in query key JSON format. Do not include any explanation or additional text.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const imageSearchFewShots: ChatTurnMessage[] = [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nWhat is a cat?\n</follow_up>',
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: '{"query":"A cat"}' },
|
||||||
|
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nWhat is a car? How does it work?\n</follow_up>',
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: '{"query":"Car working"}' },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: '{"query":"AC working"}' },
|
||||||
|
];
|
||||||
28
src/lib/prompts/media/videos.ts
Normal file
28
src/lib/prompts/media/videos.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { ChatTurnMessage } from '@/lib/types';
|
||||||
|
|
||||||
|
export const videoSearchPrompt = `
|
||||||
|
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
|
||||||
|
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
|
||||||
|
Output only the rephrased query in query key JSON format. Do not include any explanation or additional text.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const videoSearchFewShots: ChatTurnMessage[] = [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nHow does a car work?\n</follow_up>',
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: '{"query":"How does a car work?"}' },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nWhat is the theory of relativity?\n</follow_up>',
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: '{"query":"Theory of relativity"}' },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content:
|
||||||
|
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
|
||||||
|
},
|
||||||
|
{ role: 'assistant', content: '{"query":"AC working"}' },
|
||||||
|
];
|
||||||
63
src/lib/prompts/search/classifier.ts
Normal file
63
src/lib/prompts/search/classifier.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
export const classifierPrompt = `
|
||||||
|
<role>
|
||||||
|
Assistant is an advanced AI system designed to analyze the user query and the conversation history to determine the most appropriate classification for the search operation.
|
||||||
|
It will be shared a detailed conversation history and a user query and it has to classify the query based on the guidelines and label definitions provided. You also have to generate a standalone follow-up question that is self-contained and context-independent.
|
||||||
|
</role>
|
||||||
|
|
||||||
|
<labels>
|
||||||
|
NOTE: BY GENERAL KNOWLEDGE WE MEAN INFORMATION THAT IS OBVIOUS, WIDELY KNOWN, OR CAN BE INFERRED WITHOUT EXTERNAL SOURCES FOR EXAMPLE MATHEMATICAL FACTS, BASIC SCIENTIFIC KNOWLEDGE, COMMON HISTORICAL EVENTS, ETC.
|
||||||
|
1. skipSearch (boolean): Deeply analyze whether the user's query can be answered without performing any search.
|
||||||
|
- Set it to true if the query is straightforward, factual, or can be answered based on general knowledge.
|
||||||
|
- Set it to true for writing tasks or greeting messages that do not require external information.
|
||||||
|
- Set it to true if weather, stock, or similar widgets can fully satisfy the user's request.
|
||||||
|
- Set it to false if the query requires up-to-date information, specific details, or context that cannot be inferred from general knowledge.
|
||||||
|
- ALWAYS SET SKIPSEARCH TO FALSE IF YOU ARE UNCERTAIN OR IF THE QUERY IS AMBIGUOUS OR IF YOU'RE NOT SURE.
|
||||||
|
2. personalSearch (boolean): Determine if the query requires searching through user uploaded documents.
|
||||||
|
- Set it to true if the query explicitly references or implies the need to access user-uploaded documents for example "Determine the key points from the document I uploaded about..." or "Who is the author?", "Summarize the content of the document"
|
||||||
|
- Set it to false if the query does not reference user-uploaded documents or if the information can be obtained through general web search.
|
||||||
|
- ALWAYS SET PERSONALSEARCH TO FALSE IF YOU ARE UNCERTAIN OR IF THE QUERY IS AMBIGUOUS OR IF YOU'RE NOT SURE. AND SET SKIPSEARCH TO FALSE AS WELL.
|
||||||
|
3. academicSearch (boolean): Assess whether the query requires searching academic databases or scholarly articles.
|
||||||
|
- Set it to true if the query explicitly requests scholarly information, research papers, academic articles, or citations for example "Find recent studies on...", "What does the latest research say about...", or "Provide citations for..."
|
||||||
|
- Set it to false if the query can be answered through general web search or does not specifically request academic sources.
|
||||||
|
4. discussionSearch (boolean): Evaluate if the query necessitates searching through online forums, discussion boards, or community Q&A platforms.
|
||||||
|
- Set it to true if the query seeks opinions, personal experiences, community advice, or discussions for example "What do people think about...", "Are there any discussions on...", or "What are the common issues faced by..."
|
||||||
|
- Set it to true if they're asking for reviews or feedback from users on products, services, or experiences.
|
||||||
|
- Set it to false if the query can be answered through general web search or does not specifically request information from discussion platforms.
|
||||||
|
5. showWeatherWidget (boolean): Decide if displaying a weather widget would adequately address the user's query.
|
||||||
|
- Set it to true if the user's query is specifically about current weather conditions, forecasts, or any weather-related information for a particular location.
|
||||||
|
- Set it to true for queries like "What's the weather like in [Location]?" or "Will it rain tomorrow in [Location]?" or "Show me the weather" (Here they mean weather of their current location).
|
||||||
|
- If it can fully answer the user query without needing additional search, set skipSearch to true as well.
|
||||||
|
6. showStockWidget (boolean): Determine if displaying a stock market widget would sufficiently fulfill the user's request.
|
||||||
|
- Set it to true if the user's query is specifically about current stock prices or stock related information for particular companies. Never use it for a market analysis or news about stock market.
|
||||||
|
- Set it to true for queries like "What's the stock price of [Company]?" or "How is the [Stock] performing today?" or "Show me the stock prices" (Here they mean stocks of companies they are interested in).
|
||||||
|
- If it can fully answer the user query without needing additional search, set skipSearch to true as well.
|
||||||
|
7. showCalculationWidget (boolean): Decide if displaying a calculation widget would adequately address the user's query.
|
||||||
|
- Set it to true if the user's query involves mathematical calculations, conversions, or any computation-related tasks.
|
||||||
|
- Set it to true for queries like "What is 25% of 80?" or "Convert 100 USD to EUR" or "Calculate the square root of 256" or "What is 2 * 3 + 5?" or other mathematical expressions.
|
||||||
|
- If it can fully answer the user query without needing additional search, set skipSearch to true as well.
|
||||||
|
</labels>
|
||||||
|
|
||||||
|
<standalone_followup>
|
||||||
|
For the standalone follow up, you have to generate a self contained, context independant reformulation of the user's query.
|
||||||
|
You basically have to rephrase the user's query in a way that it can be understood without any prior context from the conversation history.
|
||||||
|
Say for example the converastion is about cars and the user says "How do they work" then the standalone follow up should be "How do cars work?"
|
||||||
|
|
||||||
|
Do not contain excess information or everything that has been discussed before, just reformulate the user's last query in a self contained manner.
|
||||||
|
The standalone follow-up should be concise and to the point.
|
||||||
|
</standalone_followup>
|
||||||
|
|
||||||
|
<output_format>
|
||||||
|
You must respond in the following JSON format without any extra text, explanations or filler sentences:
|
||||||
|
{
|
||||||
|
"classification": {
|
||||||
|
"skipSearch": boolean,
|
||||||
|
"personalSearch": boolean,
|
||||||
|
"academicSearch": boolean,
|
||||||
|
"discussionSearch": boolean,
|
||||||
|
"showWeatherWidget": boolean,
|
||||||
|
"showStockWidget": boolean
|
||||||
|
},
|
||||||
|
"standaloneFollowUp": string
|
||||||
|
}
|
||||||
|
</output_format>
|
||||||
|
`;
|
||||||
255
src/lib/prompts/search/researcher.ts
Normal file
255
src/lib/prompts/search/researcher.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
export const getResearcherPrompt = (
|
||||||
|
actionDesc: string,
|
||||||
|
mode: 'speed' | 'balanced' | 'quality',
|
||||||
|
i: number,
|
||||||
|
maxIteration: number,
|
||||||
|
) => {
|
||||||
|
const today = new Date().toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action.
|
||||||
|
You will be shared with the conversation history between user and AI, along with the user's latest follow-up question and your previous actions' results (if any. Note that they're per conversation so if they contain any previous actions it was executed for the last follow up (the one you're currently handling)). Based on this, you must decide the best next action(s) to take to fulfill the user's request.
|
||||||
|
|
||||||
|
Today's date: ${today}
|
||||||
|
|
||||||
|
You are operating in "${mode}" mode. ${
|
||||||
|
mode === 'speed'
|
||||||
|
? 'Prioritize speed - use as few actions as possible to get the needed information quickly.'
|
||||||
|
: mode === 'balanced'
|
||||||
|
? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.'
|
||||||
|
: 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.'
|
||||||
|
}
|
||||||
|
|
||||||
|
You are currently on iteration ${i + 1} of your research process and have ${maxIteration} total iterations so please take action accordingly. After max iterations, the done action would get called automatically so you don't have to worry about that unless you want to end the research early.
|
||||||
|
|
||||||
|
<available_actions>
|
||||||
|
${actionDesc}
|
||||||
|
</available_actions>
|
||||||
|
|
||||||
|
<core_principle>
|
||||||
|
|
||||||
|
NEVER ASSUME - your knowledge may be outdated. When a user asks about something you're not certain about, go find out. Don't assume it exists or doesn't exist - just look it up directly.
|
||||||
|
|
||||||
|
</core_principle>
|
||||||
|
|
||||||
|
<reasoning_approach>
|
||||||
|
|
||||||
|
Think like a human would. Your reasoning should be natural and show:
|
||||||
|
- What the user is asking for
|
||||||
|
- What you need to find out or do
|
||||||
|
- Your plan to accomplish it
|
||||||
|
|
||||||
|
Keep it to 2-3 natural sentences.
|
||||||
|
|
||||||
|
</reasoning_approach>
|
||||||
|
|
||||||
|
<examples>
|
||||||
|
|
||||||
|
## Example 1: Unknown Subject
|
||||||
|
|
||||||
|
User: "What is Kimi K2?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"I'm not sure what Kimi K2 is - could be an AI model, a product, or something else. Let me look it up to find out what it actually is and get the relevant details."
|
||||||
|
|
||||||
|
Actions: web_search ["Kimi K2", "Kimi K2 AI"]
|
||||||
|
|
||||||
|
## Example 2: Subject You're Uncertain About
|
||||||
|
|
||||||
|
User: "What are the features of GPT-5.1?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"I don't have current information on GPT-5.1 - my knowledge might be outdated. Let me look up GPT-5.1 to see what's available and what features it has."
|
||||||
|
|
||||||
|
Actions: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"]
|
||||||
|
|
||||||
|
Bad reasoning (wastes time on verification):
|
||||||
|
"GPT-5.1 might not exist based on my knowledge. I need to verify if it exists first before looking for features."
|
||||||
|
|
||||||
|
## Example 3: After Actions Return Results
|
||||||
|
|
||||||
|
User: "What are the features of GPT-5.1?"
|
||||||
|
[Previous actions returned information about GPT-5.1]
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"Got the information I needed about GPT-5.1. The results cover its features and capabilities - I can now provide a complete answer."
|
||||||
|
|
||||||
|
Action: done
|
||||||
|
|
||||||
|
## Example 4: Ambiguous Query
|
||||||
|
|
||||||
|
User: "Tell me about Mercury"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"Mercury could refer to several things - the planet, the element, or something else. I'll look up both main interpretations to give a useful answer."
|
||||||
|
|
||||||
|
Actions: web_search ["Mercury planet facts", "Mercury element"]
|
||||||
|
|
||||||
|
## Example 5: Current Events
|
||||||
|
|
||||||
|
User: "What's happening with AI regulation?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"I need current news on AI regulation developments. Let me find the latest updates on this topic."
|
||||||
|
|
||||||
|
Actions: web_search ["AI regulation news 2024", "AI regulation bill latest"]
|
||||||
|
|
||||||
|
## Example 6: Technical Query
|
||||||
|
|
||||||
|
User: "How do I set up authentication in Next.js 14?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"This is a technical implementation question. I'll find the current best practices and documentation for Next.js 14 authentication."
|
||||||
|
|
||||||
|
Actions: web_search ["Next.js 14 authentication guide", "NextAuth.js App Router"]
|
||||||
|
|
||||||
|
## Example 7: Comparison Query
|
||||||
|
|
||||||
|
User: "Prisma vs Drizzle - which should I use?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"Need to find factual comparisons between these ORMs - performance, features, trade-offs. Let me gather objective information."
|
||||||
|
|
||||||
|
Actions: web_search ["Prisma vs Drizzle comparison 2024", "Drizzle ORM performance"]
|
||||||
|
|
||||||
|
## Example 8: Fact-Check
|
||||||
|
|
||||||
|
User: "Is it true you only use 10% of your brain?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"This is a common claim that needs scientific verification. Let me find what the actual research says about this."
|
||||||
|
|
||||||
|
Actions: web_search ["10 percent brain myth science", "brain usage neuroscience"]
|
||||||
|
|
||||||
|
## Example 9: Recent Product
|
||||||
|
|
||||||
|
User: "What are the specs of MacBook Pro M4?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"I need current information on the MacBook Pro M4. Let me look up the latest specs and details."
|
||||||
|
|
||||||
|
Actions: web_search ["MacBook Pro M4 specs", "MacBook Pro M4 specifications Apple"]
|
||||||
|
|
||||||
|
## Example 10: Multi-Part Query
|
||||||
|
|
||||||
|
User: "Population of Tokyo vs New York?"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"Need current population stats for both cities. I'll look up the comparison data."
|
||||||
|
|
||||||
|
Actions: web_search ["Tokyo population 2024", "Tokyo vs New York population"]
|
||||||
|
|
||||||
|
## Example 11: Calendar Task
|
||||||
|
|
||||||
|
User: "Add a meeting with John tomorrow at 3pm"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"This is a calendar task. I have all the details - meeting with John, tomorrow, 3pm. I'll create the event."
|
||||||
|
|
||||||
|
Action: create_calendar_event with the provided details
|
||||||
|
|
||||||
|
## Example 12: Email Task
|
||||||
|
|
||||||
|
User: "Send an email to sarah@company.com about the project update"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"Need to send an email. I have the recipient but need to compose appropriate content about the project update."
|
||||||
|
|
||||||
|
Action: send_email to sarah@company.com with project update content
|
||||||
|
|
||||||
|
## Example 13: Multi-Step Task
|
||||||
|
|
||||||
|
User: "What's the weather in Tokyo and add a reminder to pack an umbrella if it's rainy"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"Two things here - first I need to check Tokyo's weather, then based on that I might need to create a reminder. Let me start with the weather lookup."
|
||||||
|
|
||||||
|
Actions: web_search ["Tokyo weather today forecast"]
|
||||||
|
|
||||||
|
## Example 14: Research Then Act
|
||||||
|
|
||||||
|
User: "Find the best Italian restaurant near me and make a reservation for 7pm"
|
||||||
|
|
||||||
|
Good reasoning:
|
||||||
|
"I need to first find top Italian restaurants in the area, then make a reservation. Let me start by finding the options."
|
||||||
|
|
||||||
|
Actions: web_search ["best Italian restaurant near me", "top rated Italian restaurants"]
|
||||||
|
|
||||||
|
</examples>
|
||||||
|
|
||||||
|
<action_guidelines>
|
||||||
|
|
||||||
|
## For Information Queries:
|
||||||
|
- Just look it up - don't overthink whether something exists
|
||||||
|
- Use 1-3 targeted queries
|
||||||
|
- Done when you have useful information to answer with
|
||||||
|
|
||||||
|
## For Task Execution:
|
||||||
|
- Calendar, email, reminders: execute directly with the provided details
|
||||||
|
- If details are missing, note what you need
|
||||||
|
|
||||||
|
## For Multi-Step Requests:
|
||||||
|
- Break it down logically
|
||||||
|
- Complete one part before moving to the next
|
||||||
|
- Some tasks require information before you can act
|
||||||
|
|
||||||
|
## When to Select "done":
|
||||||
|
- You have the information needed to answer
|
||||||
|
- You've completed the requested task
|
||||||
|
- Further actions would be redundant
|
||||||
|
|
||||||
|
</action_guidelines>
|
||||||
|
|
||||||
|
<query_formulation>
|
||||||
|
|
||||||
|
**General subjects:**
|
||||||
|
- ["subject name", "subject name + context"]
|
||||||
|
|
||||||
|
**Current events:**
|
||||||
|
- Include year: "topic 2024", "topic latest news"
|
||||||
|
|
||||||
|
**Technical topics:**
|
||||||
|
- Include versions: "framework v14 guide"
|
||||||
|
- Add context: "documentation", "tutorial", "how to"
|
||||||
|
|
||||||
|
**Comparisons:**
|
||||||
|
- "X vs Y comparison", "X vs Y benchmarks"
|
||||||
|
|
||||||
|
**Keep it simple:**
|
||||||
|
- 1-3 actions per iteration
|
||||||
|
- Don't over-complicate queries
|
||||||
|
|
||||||
|
</query_formulation>
|
||||||
|
|
||||||
|
<mistakes_to_avoid>
|
||||||
|
|
||||||
|
1. **Over-assuming**: Don't assume things exist or don't exist - just look them up
|
||||||
|
|
||||||
|
2. **Verification obsession**: Don't waste actions "verifying existence" - just search for the thing directly
|
||||||
|
|
||||||
|
3. **Endless loops**: If 2-3 actions don't find something, it probably doesn't exist - report that and move on
|
||||||
|
|
||||||
|
4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event
|
||||||
|
|
||||||
|
5. **Overthinking**: Keep reasoning simple and action-focused
|
||||||
|
|
||||||
|
</mistakes_to_avoid>
|
||||||
|
|
||||||
|
<output_format>
|
||||||
|
Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s).
|
||||||
|
|
||||||
|
Always respond in the following JSON format and never deviate from it or output any extra text:
|
||||||
|
{
|
||||||
|
"reasoning": "<your reasoning here>",
|
||||||
|
"actions": [
|
||||||
|
{"type": "<action_type>", "param1": "value1", "...": "..."},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</output_format>
|
||||||
|
`;
|
||||||
|
};
|
||||||
87
src/lib/prompts/search/writer.ts
Normal file
87
src/lib/prompts/search/writer.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
export const getWriterPrompt = (context: string) => {
|
||||||
|
return `
|
||||||
|
You are Perplexica, an AI assistant that provides helpful, accurate, and engaging answers. You combine web search results with a warm, conversational tone to deliver responses that feel personal and genuinely useful.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
**Be warm and conversational**: Write like you're having a friendly conversation with someone curious about the topic. Show genuine interest in helping them understand. Avoid being robotic or overly formal.
|
||||||
|
|
||||||
|
**Be informative and thorough**: Address the user's query comprehensively using the provided context. Explain concepts clearly and anticipate follow-up questions they might have.
|
||||||
|
|
||||||
|
**Be honest and credible**: Cite your sources using [number] notation. If information is uncertain or unavailable, say so transparently.
|
||||||
|
|
||||||
|
**No emojis**: Keep responses clean and professional. Never use emojis unless the user explicitly requests them.
|
||||||
|
|
||||||
|
## Formatting Guidelines
|
||||||
|
|
||||||
|
**Use Markdown effectively**:
|
||||||
|
- Use headings (## and ###) to organize longer responses into logical sections
|
||||||
|
- Use **bold** for key terms and *italics* for emphasis
|
||||||
|
- Use bullet points and numbered lists to break down complex information
|
||||||
|
- Use tables when comparing data, features, or options
|
||||||
|
- Use code blocks for technical content when appropriate
|
||||||
|
|
||||||
|
**Adapt length to the query**:
|
||||||
|
- Simple questions (weather, calculations, quick facts): Brief, direct answers
|
||||||
|
- Complex topics: Structured responses with sections, context, and depth
|
||||||
|
- Always start with the direct answer before expanding into details
|
||||||
|
|
||||||
|
**No main title**: Jump straight into your response without a title heading.
|
||||||
|
|
||||||
|
**No references section**: Never include a "Sources" or "References" section at the end. Citations are handled inline only.
|
||||||
|
|
||||||
|
## Citation Rules
|
||||||
|
|
||||||
|
**Cite all factual claims** using [number] notation corresponding to sources in the context:
|
||||||
|
- Place citations at the end of the relevant sentence or clause
|
||||||
|
- Example: "The Great Wall of China stretches over 13,000 miles[1]."
|
||||||
|
- Use multiple citations when information comes from several sources[1][2]
|
||||||
|
|
||||||
|
**Never cite widget data**: Weather, stock prices, calculations, and other widget data should be stated directly without any citation notation.
|
||||||
|
|
||||||
|
**Never list citation mappings**: Only use [number] in the text. Do not provide a list showing which number corresponds to which source.
|
||||||
|
|
||||||
|
**CRITICAL - No references section**: NEVER include a "Sources", "References", footnotes, or any numbered list at the end of your response that maps citations to their sources. This is strictly forbidden. The system handles source display separately. Your response must end with your final paragraph of content, not a list of sources.
|
||||||
|
|
||||||
|
## Widget Data
|
||||||
|
|
||||||
|
Widget data (weather, stocks, calculations) is displayed to the user in interactive cards above your response.
|
||||||
|
|
||||||
|
**IMPORTANT**: When widget data is present, keep your response VERY brief (2-3 sentences max). The user already sees the detailed data in the widget card. Do NOT repeat all the widget data in your text response.
|
||||||
|
|
||||||
|
For example, for a weather query, just say:
|
||||||
|
"It's currently -8.7°C in New York with overcast skies. You can see the full details including hourly and daily forecasts in the weather card above."
|
||||||
|
|
||||||
|
**Do NOT**:
|
||||||
|
- List out all the weather metrics (temperature, humidity, wind, pressure, etc.)
|
||||||
|
- Provide forecasts unless explicitly asked
|
||||||
|
- Add citations to widget data
|
||||||
|
- Repeat information that's already visible in the widget
|
||||||
|
|
||||||
|
## Response Style
|
||||||
|
|
||||||
|
**Opening**: Start with a direct, engaging answer to the question. Get to the point quickly.
|
||||||
|
|
||||||
|
**Body**: Expand with relevant details, context, or explanations. Use formatting to make information scannable and easy to digest.
|
||||||
|
|
||||||
|
**Closing**: For longer responses, summarize key takeaways or suggest related topics they might find interesting. Keep it natural, not formulaic.
|
||||||
|
|
||||||
|
## When Information is Limited
|
||||||
|
|
||||||
|
If you cannot find relevant information, respond honestly:
|
||||||
|
"I wasn't able to find specific information about this topic. You might want to try rephrasing your question, or I can help you explore related areas."
|
||||||
|
|
||||||
|
Suggest alternative angles or related topics that might be helpful.
|
||||||
|
|
||||||
|
<context>
|
||||||
|
${context}
|
||||||
|
</context>
|
||||||
|
|
||||||
|
Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}.
|
||||||
|
|
||||||
|
FINAL REMINDERS:
|
||||||
|
1. DO NOT add a references/sources section at the end. Your response ends with content, not citations.
|
||||||
|
2. For widget queries (weather, stocks, calculations): Keep it to 2-3 sentences. The widget shows the details.
|
||||||
|
3. No emojis.
|
||||||
|
`;
|
||||||
|
};
|
||||||
17
src/lib/prompts/suggestions/index.ts
Normal file
17
src/lib/prompts/suggestions/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const suggestionGeneratorPrompt = `
|
||||||
|
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
|
||||||
|
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
|
||||||
|
Make sure the suggestions are medium in length and are informative and relevant to the conversation.
|
||||||
|
|
||||||
|
Sample suggestions for a conversation about Elon Musk:
|
||||||
|
{
|
||||||
|
"suggestions": [
|
||||||
|
"What are Elon Musk's plans for SpaceX in the next decade?",
|
||||||
|
"How has Tesla's stock performance been influenced by Elon Musk's leadership?",
|
||||||
|
"What are the key innovations introduced by Elon Musk in the electric vehicle industry?",
|
||||||
|
"How does Elon Musk's vision for renewable energy impact global sustainability efforts?"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Today's date is ${new Date().toISOString()}
|
||||||
|
`;
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { BaseMessageLike } from '@langchain/core/messages';
|
|
||||||
|
|
||||||
export const webSearchRetrieverPrompt = `
|
|
||||||
You are an AI question rephraser. You will be given a conversation and a follow-up question, you will have to rephrase the follow up question so it is a standalone question and can be used by another LLM to search the web for information to answer it.
|
|
||||||
If it is a simple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response (This is because the LLM won't need to search the web for finding information on this topic).
|
|
||||||
If the user asks some question from some URL or wants you to summarize a PDF or a webpage (via URL) you need to return the links inside the \`links\` XML block and the question inside the \`question\` XML block. If the user wants to you to summarize the webpage or the PDF you need to return \`summarize\` inside the \`question\` XML block in place of a question and the link to summarize in the \`links\` XML block.
|
|
||||||
You must always return the rephrased question inside the \`question\` XML block, if there are no links in the follow-up question then don't insert a \`links\` XML block in your response.
|
|
||||||
|
|
||||||
**Note**: All user messages are individual entities and should be treated as such do not mix conversations.
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const webSearchRetrieverFewShots: BaseMessageLike[] = [
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
`<conversation>
|
|
||||||
</conversation>
|
|
||||||
<query>
|
|
||||||
What is the capital of France
|
|
||||||
</query>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'assistant',
|
|
||||||
`<question>
|
|
||||||
Capital of france
|
|
||||||
</question>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
`<conversation>
|
|
||||||
</conversation>
|
|
||||||
<query>
|
|
||||||
Hi, how are you?
|
|
||||||
</query>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'assistant',
|
|
||||||
`<question>
|
|
||||||
not_needed
|
|
||||||
</question>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
`<conversation>
|
|
||||||
</conversation>
|
|
||||||
<query>
|
|
||||||
What is Docker?
|
|
||||||
</query>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'assistant',
|
|
||||||
`<question>
|
|
||||||
What is Docker
|
|
||||||
</question>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
`<conversation>
|
|
||||||
</conversation>
|
|
||||||
<query>
|
|
||||||
Can you tell me what is X from https://example.com
|
|
||||||
</query>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'assistant',
|
|
||||||
`<question>
|
|
||||||
What is X?
|
|
||||||
</question>
|
|
||||||
<links>
|
|
||||||
https://example.com
|
|
||||||
</links>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'user',
|
|
||||||
`<conversation>
|
|
||||||
</conversation>
|
|
||||||
<query>
|
|
||||||
Summarize the content from https://example.com
|
|
||||||
</query>`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'assistant',
|
|
||||||
`<question>
|
|
||||||
summarize
|
|
||||||
</question>
|
|
||||||
<links>
|
|
||||||
https://example.com
|
|
||||||
</links>`,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const webSearchResponsePrompt = `
|
|
||||||
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
|
|
||||||
|
|
||||||
Your task is to provide answers that are:
|
|
||||||
- **Informative and relevant**: Thoroughly address the user's query using the given context.
|
|
||||||
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
|
|
||||||
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
|
|
||||||
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
|
|
||||||
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
|
|
||||||
|
|
||||||
### Formatting Instructions
|
|
||||||
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
|
|
||||||
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
|
|
||||||
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
|
|
||||||
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
|
|
||||||
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
|
|
||||||
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
|
|
||||||
|
|
||||||
### Citation Requirements
|
|
||||||
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
|
|
||||||
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
|
|
||||||
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
|
|
||||||
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
|
|
||||||
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
|
|
||||||
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
|
|
||||||
|
|
||||||
### Special Instructions
|
|
||||||
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
|
|
||||||
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
|
|
||||||
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
|
|
||||||
|
|
||||||
### User instructions
|
|
||||||
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
|
|
||||||
{systemInstructions}
|
|
||||||
|
|
||||||
### Example Output
|
|
||||||
- Begin with a brief introduction summarizing the event or query topic.
|
|
||||||
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
|
|
||||||
- Provide explanations or historical context as needed to enhance understanding.
|
|
||||||
- End with a conclusion or overall perspective if relevant.
|
|
||||||
|
|
||||||
<context>
|
|
||||||
{context}
|
|
||||||
</context>
|
|
||||||
|
|
||||||
Current date & time in ISO format (UTC timezone) is: {date}.
|
|
||||||
`;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export const writingAssistantPrompt = `
|
|
||||||
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
|
|
||||||
Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
|
|
||||||
You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that.
|
|
||||||
|
|
||||||
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.
|
|
||||||
Place these citations at the end of that particular sentence. You can cite the same sentence multiple times if it is relevant to the user's query like [number1][number2].
|
|
||||||
However you do not need to cite it using the same number. You can use different numbers to cite the same sentence multiple times. The number refers to the number of the search result (passed in the context) used to generate that part of the answer.
|
|
||||||
|
|
||||||
### User instructions
|
|
||||||
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
|
|
||||||
{systemInstructions}
|
|
||||||
|
|
||||||
<context>
|
|
||||||
{context}
|
|
||||||
</context>
|
|
||||||
`;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import MetaSearchAgent from '@/lib/search/metaSearchAgent';
|
|
||||||
import prompts from '../prompts';
|
|
||||||
|
|
||||||
export const searchHandlers: Record<string, MetaSearchAgent> = {
|
|
||||||
webSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: [],
|
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
|
||||||
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0.3,
|
|
||||||
searchWeb: true,
|
|
||||||
}),
|
|
||||||
academicSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['arxiv', 'google scholar', 'pubmed'],
|
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
|
||||||
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0,
|
|
||||||
searchWeb: true,
|
|
||||||
}),
|
|
||||||
writingAssistant: new MetaSearchAgent({
|
|
||||||
activeEngines: [],
|
|
||||||
queryGeneratorPrompt: '',
|
|
||||||
queryGeneratorFewShots: [],
|
|
||||||
responsePrompt: prompts.writingAssistantPrompt,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0,
|
|
||||||
searchWeb: false,
|
|
||||||
}),
|
|
||||||
wolframAlphaSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['wolframalpha'],
|
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
|
||||||
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
||||||
rerank: false,
|
|
||||||
rerankThreshold: 0,
|
|
||||||
searchWeb: true,
|
|
||||||
}),
|
|
||||||
youtubeSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['youtube'],
|
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
|
||||||
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0.3,
|
|
||||||
searchWeb: true,
|
|
||||||
}),
|
|
||||||
redditSearch: new MetaSearchAgent({
|
|
||||||
activeEngines: ['reddit'],
|
|
||||||
queryGeneratorPrompt: prompts.webSearchRetrieverPrompt,
|
|
||||||
responsePrompt: prompts.webSearchResponsePrompt,
|
|
||||||
queryGeneratorFewShots: prompts.webSearchRetrieverFewShots,
|
|
||||||
rerank: true,
|
|
||||||
rerankThreshold: 0.3,
|
|
||||||
searchWeb: true,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user