Compare commits

...

26 Commits

Author SHA1 Message Date
timothycarambat beb6529e55 Add back syntax highlighting and keep bundle small via dynamic script build 2024-02-02 18:52:56 -08:00
timothycarambat 6e80f50f6e publish new embed script 2024-02-02 18:05:39 -08:00
timothycarambat a727235195 Add more styling options
Add sponsor text at bottom
Support dynamic container height
Loading animations
2024-02-02 18:05:09 -08:00
timothycarambat d1664f3042 update instructions 2024-02-02 16:38:08 -08:00
timothycarambat 990569e85b Merge branch 'master' of github.com:Mintplex-Labs/anything-llm into embedded-anythingllm-app 2024-02-02 15:30:15 -08:00
timothycarambat 146385bf41 no build on embed changes 2024-02-02 15:29:38 -08:00
timothycarambat 8bf0691f11 update reset script 2024-02-02 15:26:39 -08:00
timothycarambat 2b92ae0901 update failure to lookup message 2024-02-02 14:57:04 -08:00
timothycarambat 5145499776 allow change of workspace for embedconfig 2024-02-02 14:55:27 -08:00
timothycarambat 51765cfe97 merge with master 2024-02-02 13:53:25 -08:00
timothycarambat 7fdd5afab3 remove unused imports 2024-02-02 13:52:15 -08:00
timothycarambat cccec69293 update comments 2024-02-02 13:45:21 -08:00
timothycarambat 9fc45daf77 more style support
reopen if chat was last opened
2024-02-02 13:31:26 -08:00
timothycarambat 27381a612c nonpriv 2024-02-02 12:24:00 -08:00
timothycarambat 20cfb2f4c4 update embed 2024-02-02 12:09:24 -08:00
timothycarambat 8e0b08ecad Admin Embed Chats mgmt page 2024-02-02 11:41:04 -08:00
Sean Hatfield 9d410496c0
[FIX]: Fix Chinese characters causing empty workspace slug (#660)
if slug is empty on create workspace, generate a uuid as the workspace slug
2024-01-31 13:38:21 -08:00
Alex Leventer d927629c19
Add Astra DB to list of supported vector stores (#657)
Update README.md
2024-01-30 09:47:03 -08:00
Timothy Carambat dfab14a5d2
Patch lanceDB not deleting vectors from workspace (#655)
patch lanceDB not deleting vectors from workspace
documentVectors self-sanitize on delete of parent document
2024-01-29 09:49:22 -08:00
Sean Hatfield 9d41ff58e2
[FEAT] add support for new openai embedding models (#653)
* add support for new openai models

* QOL changes/improve logic for adding new openai embedding models

* add example file inputs for Openai embedding ENV selection;

* Fix if stmt conditional

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-01-29 08:48:27 -08:00
Hakeem Abbas 5614e2ed30
feature: Integrate Astra as vectorDBProvider (#648)
* feature: Integrate Astra as vectorDBProvider

feature: Integrate Astra as vectorDBProvider

* Update .env.example

* Add env.example to docker example file
Update spellcheck fo Astra
Update Astra key for vector selection
Update order of AstraDB options
Resize Astra logo image to 330x330
Update methods of Astra to take in latest vectorDB params like TopN and more
Update Astra interface to support default methods and avoid crash errors from 404 collections
Update Astra interface to comply to max chunk insertion limitations
Update Astra interface to dynamically set dimensionality from chunk 0 size on creation

* reset workspaces

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-01-26 13:07:53 -08:00
Sean Hatfield 21653b09fc
[FEAT] add gpt-4-turbo-preview (#651)
* add gpt-4-turbo-preview

* add gpt-4-turbo-preview to valid models
2024-01-26 13:03:50 -08:00
timothycarambat 39d07feaed fix bug yaml 2024-01-26 12:45:05 -08:00
timothycarambat ca3decf413 fix bug yaml 2024-01-26 12:44:15 -08:00
timothycarambat 978cad476a update BARE_METAL setup 2024-01-24 14:27:52 -08:00
Timothy Carambat 8377600211
Patch Azure text completion persistence (#647) 2024-01-24 13:08:22 -08:00
68 changed files with 1320 additions and 125 deletions

View File

@ -5,11 +5,8 @@ labels: [possible bug]
body:
- type: markdown
attributes:
value: |
Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue.
Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm
value: |
Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue. Want help contributing a PR? Use our repo chatbot by OnboardAI! https://learnthisrepo.com/anythingllm"
- type: dropdown
id: runtime
attributes:

View File

@ -20,6 +20,7 @@ on:
- '.vscode/**/*'
- '**/.env.example'
- '.github/ISSUE_TEMPLATE/**/*'
- 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced
jobs:
push_multi_platform_to_registries:

View File

@ -1,6 +1,7 @@
{
"cSpell.words": [
"anythingllm",
"Astra",
"Dockerized",
"Embeddable",
"hljs",

View File

@ -23,7 +23,12 @@ Here you can find the scripts and known working process to run AnythingLLM outsi
2. `cd anything-llm` and run `yarn setup`. This will install all dependencies to run in production as well as debug the application.
3. `cp server/.env.example server/.env` to create the basic ENV file for where instance settings will be read from on service start. This file is automatically managed and should not be edited manually.
3. `cp server/.env.example server/.env` to create the basic ENV file for where instance settings will be read from on service start.
4. Ensure that the `server/.env` file has _at least_ these keys to start. These values will persist and this file will be automatically written and managed after your first successful boot.
```
STORAGE_DIR="/your/absolute/path/to/server/.env"
```
## To start the application
@ -45,10 +50,10 @@ cd server && npx prisma migrate deploy --schema=./prisma/schema.prisma
```
4. Boot the server in production
`cd server && NODE_ENV=production index.js &`
`cd server && NODE_ENV=production node index.js &`
5. Boot the collection in another process
`cd collector && NODE_ENV=production index.js &`
`cd collector && NODE_ENV=production node index.js &`
AnythingLLM should now be running on `http://localhost:3001`!

View File

@ -84,6 +84,7 @@ Some cool features of AnythingLLM
**Supported Vector Databases:**
- [LanceDB](https://github.com/lancedb/lancedb) (default)
- [Astra DB](https://www.datastax.com/products/datastax-astra)
- [Pinecone](https://pinecone.io)
- [Chroma](https://trychroma.com)
- [Weaviate](https://weaviate.io)

View File

@ -54,6 +54,7 @@ GID='1000'
# Only used if you are using an LLM that does not natively support embedding (openai or Azure)
# EMBEDDING_ENGINE='openai'
# OPEN_AI_KEY=sk-xxxx
# EMBEDDING_MODEL_PREF='text-embedding-ada-002'
# EMBEDDING_ENGINE='azure'
# AZURE_OPENAI_ENDPOINT=
@ -103,6 +104,11 @@ GID='1000'
# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com"
# ZILLIZ_API_TOKEN=api-token-here
# Enable all below if you are using vector database: Astra DB.
# VECTOR_DB="astra"
# ASTRA_DB_APPLICATION_TOKEN=
# ASTRA_DB_ENDPOINT=
# CLOUD DEPLOYMENT VARIRABLES ONLY
# AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.

View File

@ -6,3 +6,4 @@
**/dist
**/static/**
src/utils/chat/hljs.js

View File

@ -1,3 +1,86 @@
# AnythingLLM Embedded Mode
# AnythingLLM Embedded Chat Widget
This folder of AnythingLLM contains the source code for how the embedded version of AnythingLLM works to provide a public facing interface of your workspace.
> [!WARNING]
> The core AnythingLLM team publishes a pre-built version of the script that is bundled
> with the main application. You can find it at the frontend URL `/embed/anythingllm-chat-widget.min.js`.
> You should only be working in this repo if you are wanting to build your own custom embed.
This folder of AnythingLLM contains the source code for how the embedded version of AnythingLLM works to provide a public facing interface of your workspace.
The AnythingLLM Embedded chat widget allows you to expose a workspace and its embedded knowledge base as a chat bubble via a `<script>` or `<iframe>` element that you can embed in a website or HTML.
### Security
- Users will _not_ be able to view or read context snippets like they can in the core AnythingLLM application
- Users are assigned a random session ID that they use to persist a chat session.
- **Recommended** You can limit both the number of chats an embedding can process **and** per-session.
_by using the AnythingLLM embedded chat widget you are responsible for securing and configuration of the embed as to not allow excessive chat model abuse of your instance_
### Developer Setup
- `cd embed` from the root of the repo
- `yarn` to install all dev and script dependencies
- `yarn dev` to boot up an example HTML page to use the chat embed widget.
While in development mode (`yarn dev`) the script will rebuild on any changes to files in the `src` directory. Ensure that the required keys for the development embed are accurate and set.
`yarn build` will compile and minify your build of the script. You can then host and link your built script wherever you like.
## Integrations & Embed Types
### `<script>` tag HTML embed
The primary way of embedding a workspace as a chat widget is via a simple `<script>`
```html
<!--
An example of a script tag embed
REQUIRED data attributes:
data-embed-id // The unique id of your embed with its default settings
data-base-api-url // The URL of your anythingLLM instance backend
-->
<script
data-embed-id="5fc05aaf-2f2c-4c84-87a3-367a4692c1ee"
data-base-api-url="http://localhost:3001/api/embed"
src="http://localhost:3000/embed/anythingllm-chat-widget.min.js">
</script>
```
### `<script>` Customization Options
**LLM Overrides**
- `data-prompt` — Override the chat window with a custom system prompt. This is not visible to the user. If undefined it will use the embeds attached workspace system prompt.
- `data-model` — Override the chat model used for responses. This must be a valid model string for your AnythingLLM LLM provider. If unset it will use the embeds attached workspace model selection or the system setting.
- `data-temperature` — Override the chat model temperature. This must be a valid value for your AnythingLLM LLM provider. If unset it will use the embeds attached workspace model temperature or the system setting.
**Style Overrides**
- `data-chat-icon` — The chat bubble icon show when chat is closed. Options are `plus`, `chatCircle`, `support`, `search2`, `search`, `magic`.
- `data-button-color` — The chat bubble background color shown when chat is closed. Value must be hex color code.
- `data-user-bg-color` — The background color of the user chat bubbles when chatting. Value must be hex color code.
- `data-assistant-bg-color` — The background color of the assistant response chat bubbles when chatting. Value must be hex color code.
- `data-brand-image-url` — URL to image that will be show at the top of the chat when chat is open.
- `data-greeting` — Default text message to be shown when chat is opened and no previous message history is found.
- `data-no-sponsor` — Setting this attribute to anything will hide the custom or default sponsor at the bottom of an open chat window.
- `data-sponsor-link` — A clickable link in the sponsor section in the footer of an open chat window.
- `data-sponsor-text` — The text displays in sponsor text in the footer of an open chat window.
**Behavior Overrides**
- `data-open-on-load` — Once loaded, open the chat as default. It can still be closed by the user.
- `data-support-email` — Shows a support email that the user can used to draft an email via the "three dot" menu in the top right. Option will not appear if it is not set.
### `<iframe>` tag HTML embed
_work in progress_
### `<iframe>` Customization Options
_work in progress_

View File

@ -3,9 +3,11 @@
<body>
<h1>This is an example testing page for embedded AnythingLLM.</h1>
<!-- <script data-embed-id="example-uuid" data-base-api-url='http://localhost:3001/api/embed' data-open-on-load="on"
src="/dist/anythingllm-chat-widget.js">
</script> -->
<!--
<script data-embed-id="example-uuid" data-base-api-url='http://localhost:3001/api/embed' data-open-on-load="on"
src="/dist/anythingllm-chat-widget.js"> USE THIS SRC FOR DEVELOPMENT SO CHANGES APPEAR!
</script>
-->
</body>
</html>

View File

@ -1,6 +1,6 @@
{
"name": "anythingllm-embedded-chat",
"private": true,
"private": false,
"type": "module",
"scripts": {
"dev": "nodemon -e js,jsx,css --watch src --exec \"yarn run dev:preview\"",
@ -15,6 +15,7 @@
"@phosphor-icons/react": "^2.0.13",
"dompurify": "^3.0.8",
"he": "^1.2.0",
"highlight.js": "^11.9.0",
"lodash.debounce": "^4.0.8",
"markdown-it": "^13.0.1",
"react": "^18.2.0",
@ -39,4 +40,4 @@
"vite": "^5.0.0",
"vite-plugin-singlefile": "^0.13.5"
}
}
}

View File

@ -0,0 +1,35 @@
// What is this script?
// We want to support code syntax highlighting in the embed modal, but we cannot afford to have the static build
// be large in size. To prevent HighlightJs from loading all 193+ language stylings and bloating the script, we instead take a large subset that
// covers most languages and then dynamically build and register each language since HLJS cannot just register with an array of langs.
// Since the embed is a single script - we need to statically import each library and register that associate language.
// we can then export this as a custom implementation of HLJS and call it a day and keep the bundle small.
import fs from 'fs'
const SUPPORTED_HIGHLIGHTS = ['apache', 'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql', 'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua', 'makefile', 'markdown', 'nginx', 'objectivec', 'perl', 'pgsql', 'php', 'php-template', 'plaintext', 'python', 'python-repl', 'r', 'ruby', 'rust', 'scss', 'shell', 'sql', 'swift', 'typescript', 'vbnet', 'wasm', 'xml', 'yaml'];
function quickClean(input) {
return input.replace(/[^a-zA-Z0-9]/g, '');
}
let content = `/*
This is a dynamically generated file to help de-bloat the app since this script is a static bundle.
You should not modify this file directly. You can regenerate it with "node scripts/updateHljs.mjd" from the embed folder.
Last generated ${(new Date).toDateString()}
----------------------
*/\n\n`
content += 'import hljs from "highlight.js/lib/core";\n';
SUPPORTED_HIGHLIGHTS.forEach((lang) => {
content += `import ${quickClean(lang)}HljsSupport from 'highlight.js/lib/languages/${lang}'\n`;
});
SUPPORTED_HIGHLIGHTS.forEach((lang) => {
content += ` hljs.registerLanguage('${lang}', ${quickClean(lang)}HljsSupport)\n`;
})
content += `// The above should now register on the languages we wish to support statically.\n`;
content += `export const staticHljs = hljs;\n`
fs.writeFileSync('src/utils/chat/hljs.js', content, { encoding: 'utf8' })
console.log(`Static build of HLJS completed - src/utils/chat/hljs.js`)

View File

@ -12,7 +12,9 @@ export default function App() {
const sessionId = useSessionId();
useEffect(() => {
toggleOpenChat(embedSettings.openOnLoad === "on");
if (embedSettings.openOnLoad === "on") {
toggleOpenChat(true);
}
}, [embedSettings.loaded]);
if (!embedSettings.loaded) return null;
@ -25,9 +27,9 @@ export default function App() {
width: isChatOpen ? 320 : "auto",
height: isChatOpen ? "93vh" : "auto",
}}
className={`transition-all duration-300 ease-in-out ${
className={`${
isChatOpen
? "max-w-md p-4 bg-white rounded-lg border shadow-lg w-72"
? "max-w-md px-4 py-2 bg-white rounded-lg border shadow-lg w-72"
: "w-16 h-16 rounded-full"
}`}
>
@ -41,7 +43,7 @@ export default function App() {
<OpenButton
settings={embedSettings}
isOpen={isChatOpen}
toggleOpen={toggleOpenChat}
toggleOpen={() => toggleOpenChat(true)}
/>
</div>
</div>

View File

@ -2,7 +2,7 @@ import React, { memo, forwardRef } from "react";
import { Warning } from "@phosphor-icons/react";
// import Actions from "./Actions";
import renderMarkdown from "@/utils/chat/markdown";
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
import { embedderSettings } from "@/main";
import { v4 } from "uuid";
import createDOMPurify from "dompurify";
@ -17,11 +17,14 @@ const HistoricalMessage = forwardRef(
error
? "bg-red-200"
: role === "user"
? USER_BACKGROUND_COLOR
: AI_BACKGROUND_COLOR
? embedderSettings.USER_BACKGROUND_COLOR
: embedderSettings.AI_BACKGROUND_COLOR
}`}
>
<div className={`py-2 px-2 w-full flex flex-col`}>
<div
style={{ wordBreak: "break-word" }}
className={`py-2 px-2 w-full flex flex-col`}
>
<div className="flex">
{error ? (
<div className="p-2 rounded-lg bg-red-50 text-red-500">

View File

@ -1,7 +1,7 @@
import { forwardRef, memo } from "react";
import { Warning } from "@phosphor-icons/react";
import renderMarkdown from "@/utils/chat/markdown";
import { AI_BACKGROUND_COLOR } from "@/utils/constants";
import { embedderSettings } from "@/main";
const PromptReply = forwardRef(
({ uuid, reply, pending, error, sources = [] }, ref) => {
@ -11,7 +11,7 @@ const PromptReply = forwardRef(
return (
<div
ref={ref}
className={`flex justify-center items-end rounded-lg w-full ${AI_BACKGROUND_COLOR}`}
className={`flex justify-center items-end rounded-lg w-full ${embedderSettings.AI_BACKGROUND_COLOR}`}
>
<div className="py-2 px-2 w-full flex flex-col">
<div className="flex gap-x-5">
@ -44,9 +44,12 @@ const PromptReply = forwardRef(
<div
key={uuid}
ref={ref}
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
className={`flex justify-center items-end w-full ${embedderSettings.AI_BACKGROUND_COLOR}`}
>
<div className="py-2 px-2 w-full flex flex-col">
<div
style={{ wordBreak: "break-word" }}
className="py-2 px-2 w-full flex flex-col"
>
<div className="flex gap-x-5">
<span
className={`reply whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}

View File

@ -1,7 +1,7 @@
import HistoricalMessage from "./HistoricalMessage";
import PromptReply from "./PromptReply";
import { useEffect, useRef, useState } from "react";
import { ArrowDown } from "@phosphor-icons/react";
import { ArrowDown, CircleNotch } from "@phosphor-icons/react";
import debounce from "lodash.debounce";
export default function ChatHistory({ settings = {}, history = [] }) {
@ -46,10 +46,7 @@ export default function ChatHistory({ settings = {}, history = [] }) {
if (history.length === 0) {
return (
<div
style={{ height: "85vh", paddingBottom: 100, paddingTop: 5 }}
className="bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll"
>
<div className="h-full max-h-[82vh] pb-[100px] pt-[5px] bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll">
<div className="flex h-full flex-col items-center justify-center">
<p className="text-slate-400 text-sm font-base py-4 text-center">
{settings?.greeting ?? "Send a chat to get started!"}
@ -61,8 +58,7 @@ export default function ChatHistory({ settings = {}, history = [] }) {
return (
<div
style={{ height: "85vh", paddingBottom: 100, paddingTop: 5 }}
className="bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll"
className="h-full max-h-[82vh] pb-[100px] pt-[5px] bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll"
id="chat-history"
ref={chatHistoryRef}
>
@ -98,7 +94,7 @@ export default function ChatHistory({ settings = {}, history = [] }) {
);
})}
{!isAtBottom && (
<div className="fixed bottom-[8rem] right-[3rem] z-50 cursor-pointer animate-pulse">
<div className="fixed bottom-[10rem] right-[3rem] z-50 cursor-pointer animate-pulse">
<div className="flex flex-col items-center">
<div className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white">
<ArrowDown
@ -113,3 +109,15 @@ export default function ChatHistory({ settings = {}, history = [] }) {
</div>
);
}
export function ChatHistoryLoading() {
return (
<div className="h-full w-full relative">
<div className="h-full max-h-[82vh] pb-[100px] pt-[5px] bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll">
<div className="flex h-full flex-col items-center justify-center">
<CircleNotch size={14} className="text-slate-400 animate-spin" />
</div>
</div>
</div>
);
}

View File

@ -33,15 +33,12 @@ export default function PromptInput({
};
return (
<div
style={{ bottom: 25 }}
className="w-full fixed md:absolute left-0 z-10 flex justify-center items-center"
>
<div className="w-full absolute left-0 bottom-[5px] z-10 flex justify-center items-center">
<form
onSubmit={handleSubmit}
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl"
className="flex flex-col gap-y-1 rounded-t-lg w-full items-center justify-center"
>
<div className="flex items-center rounded-lg md:mb-4">
<div className="flex items-center rounded-lg">
<div className="bg-white border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
<div className="flex items-center w-full">
<textarea

View File

@ -76,7 +76,7 @@ export default function ChatContainer({
}, [loadingResponse, chatHistory]);
return (
<React.Fragment>
<div className="h-full w-full relative">
<ChatHistory settings={settings} history={chatHistory} />
<PromptInput
settings={settings}
@ -86,6 +86,6 @@ export default function ChatContainer({
inputDisabled={loadingResponse}
buttonDisabled={loadingResponse}
/>
</React.Fragment>
</div>
);
}

View File

@ -1,6 +1,11 @@
import AnythingLLMLogo from "@/assets/anything-llm-dark.png";
import ChatService from "@/models/chatService";
import { DotsThreeOutlineVertical, Lightning, X } from "@phosphor-icons/react";
import {
DotsThreeOutlineVertical,
Envelope,
Lightning,
X,
} from "@phosphor-icons/react";
import { useState } from "react";
export default function ChatWindowHeader({
@ -46,15 +51,19 @@ export default function ChatWindowHeader({
<X size={18} />
</button>
</div>
<OptionsMenu showing={showingOptions} resetChat={handleChatReset} />
<OptionsMenu
settings={settings}
showing={showingOptions}
resetChat={handleChatReset}
/>
</div>
);
}
function OptionsMenu({ showing, resetChat }) {
function OptionsMenu({ settings, showing, resetChat }) {
if (!showing) return null;
return (
<div className="absolute bg-white flex flex-col gap-y-2 rounded-lg shadow-lg border border-gray-300 top-[3vh] right-[1vw] max-w-[150px]">
<div className="absolute z-10 bg-white flex flex-col gap-y-1 rounded-lg shadow-lg border border-gray-300 top-[23px] right-[20px] max-w-[150px]">
<button
onClick={resetChat}
className="flex items-center gap-x-1 hover:bg-gray-100 text-sm text-gray-700 p-2 rounded-lg"
@ -62,6 +71,22 @@ function OptionsMenu({ showing, resetChat }) {
<Lightning size={14} />
<p>Reset Chat</p>
</button>
<ContactSupport email={settings.supportEmail} />
</div>
);
}
function ContactSupport({ email = null }) {
if (!email) return null;
const subject = `Inquiry from ${window.location.origin}`;
return (
<a
href={`mailto:${email}?Subject=${encodeURIComponent(subject)}`}
className="flex items-center gap-x-1 hover:bg-gray-100 text-sm text-gray-700 p-2 rounded-lg"
>
<Envelope size={14} />
<p>Email support</p>
</a>
);
}

View File

@ -2,6 +2,8 @@ import ChatWindowHeader from "./Header";
import SessionId from "../SessionId";
import useChatHistory from "@/hooks/chat/useChatHistory";
import ChatContainer from "./ChatContainer";
import Sponsor from "../Sponsor";
import { ChatHistoryLoading } from "./ChatContainer/ChatHistory";
export default function ChatWindow({ closeChat, settings, sessionId }) {
const { chatHistory, setChatHistory, loading } = useChatHistory(
@ -9,10 +11,28 @@ export default function ChatWindow({ closeChat, settings, sessionId }) {
sessionId
);
if (loading) return null;
if (loading) {
return (
<div className="flex flex-col h-full">
<ChatWindowHeader
sessionId={sessionId}
settings={settings}
iconUrl={settings.brandImageUrl}
closeChat={closeChat}
setChatHistory={setChatHistory}
/>
<ChatHistoryLoading />
<div className="pt-4 pb-2 h-fit gap-y-1">
<SessionId />
<Sponsor settings={settings} />
</div>
</div>
);
}
setEventDelegatorForCodeSnippets();
return (
<div className="flex flex-col">
<div className="flex flex-col h-full">
<ChatWindowHeader
sessionId={sessionId}
settings={settings}
@ -25,7 +45,10 @@ export default function ChatWindow({ closeChat, settings, sessionId }) {
settings={settings}
knownHistory={chatHistory}
/>
<SessionId />
<div className="pt-4 pb-2 h-fit gap-y-1">
<SessionId />
<Sponsor settings={settings} />
</div>
</div>
);
}

View File

@ -159,14 +159,6 @@ const customCss = `
.bg-black-900 {
background: #141414;
}
.bg-historical-msg-system {
background: #2563eb;
}
.bg-historical-msg-user {
background: #2C2F35;
}
`;
export default function Head() {

View File

@ -9,10 +9,10 @@ import {
const CHAT_ICONS = {
plus: Plus,
"chat-circle-dots": ChatCircleDots,
headset: Headset,
binoculars: Binoculars,
magnifying: MagnifyingGlass,
chatBubble: ChatCircleDots,
support: Headset,
search2: Binoculars,
search: MagnifyingGlass,
magic: MagicWand,
};
@ -24,7 +24,7 @@ export default function OpenButton({ settings, isOpen, toggleOpen }) {
return (
<button
onClick={toggleOpen}
className="flex items-center justify-center p-4 rounded-full bg-blue-500 text-white text-2xl"
className={`flex items-center justify-center p-4 rounded-full bg-[${settings.buttonColor}] text-white text-2xl`}
aria-label="Toggle Menu"
>
<ChatIcon className="text-white" />

View File

@ -0,0 +1,16 @@
export default function Sponsor({ settings }) {
if (!!settings.noSponsor) return null;
return (
<div className="flex w-full items-center justify-center">
<a
href={settings.sponsorLink ?? "#"}
target="_blank"
rel="noreferrer"
className="text-xs text-gray-300 hover:text-blue-300 hover:underline"
>
{settings.sponsorText}
</a>
</div>
);
}

View File

@ -1,8 +1,16 @@
import { CHAT_UI_REOPEN } from "@/utils/constants";
import { useState } from "react";
export default function useOpenChat() {
const [isOpen, setOpen] = useState(false);
const [isOpen, setOpen] = useState(
!!window?.localStorage?.getItem(CHAT_UI_REOPEN) || false
);
//TODO: Detect if chat was previously open??
return { isChatOpen: isOpen, toggleOpenChat: setOpen };
function toggleOpenChat(newValue) {
if (newValue === true) window.localStorage.setItem(CHAT_UI_REOPEN, "1");
if (newValue === false) window.localStorage.removeItem(CHAT_UI_REOPEN);
setOpen(newValue);
}
return { isChatOpen: isOpen, toggleOpenChat };
}

View File

@ -14,9 +14,16 @@ const DEFAULT_SETTINGS = {
chatIcon: "plus",
brandImageUrl: null, // will be forced into 100x50px container
greeting: null, // empty chat window greeting.
buttonColor: "#262626", // must be hex color code
userBgColor: "#2C2F35", // user text bubble color
assistantBgColor: "#2563eb", // assistant text bubble color
noSponsor: null, // Shows sponsor in footer of chat
sponsorText: "Powered by AnythingLLM", // default sponsor text
sponsorLink: "https://useanything.com", // default sponsor link
// behaviors
openOnLoad: "off", // or "on"
supportEmail: null, // string of email for contact
};
export default function useGetScriptAttributes() {

View File

@ -11,6 +11,12 @@ root.render(
</React.StrictMode>
);
const scriptSettings = Object.assign(
{},
document?.currentScript?.dataset || {}
);
export const embedderSettings = {
settings: Object.assign({}, document?.currentScript?.dataset || {}),
settings: scriptSettings,
USER_BACKGROUND_COLOR: `bg-[${scriptSettings?.userBgColor ?? "#2C2F35"}]`,
AI_BACKGROUND_COLOR: `bg-[${scriptSettings?.assistantBgColor ?? "#2563eb"}]`,
};

View File

@ -0,0 +1,88 @@
/*
This is a dynamically generated file to help de-bloat the app since this script is a static bundle.
You should not modify this file directly. You can regenerate it with "node scripts/updateHljs.mjd" from the embed folder.
Last generated Fri Feb 02 2024
----------------------
*/
import hljs from "highlight.js/lib/core";
import apacheHljsSupport from 'highlight.js/lib/languages/apache'
import bashHljsSupport from 'highlight.js/lib/languages/bash'
import cHljsSupport from 'highlight.js/lib/languages/c'
import cppHljsSupport from 'highlight.js/lib/languages/cpp'
import csharpHljsSupport from 'highlight.js/lib/languages/csharp'
import cssHljsSupport from 'highlight.js/lib/languages/css'
import diffHljsSupport from 'highlight.js/lib/languages/diff'
import goHljsSupport from 'highlight.js/lib/languages/go'
import graphqlHljsSupport from 'highlight.js/lib/languages/graphql'
import iniHljsSupport from 'highlight.js/lib/languages/ini'
import javaHljsSupport from 'highlight.js/lib/languages/java'
import javascriptHljsSupport from 'highlight.js/lib/languages/javascript'
import jsonHljsSupport from 'highlight.js/lib/languages/json'
import kotlinHljsSupport from 'highlight.js/lib/languages/kotlin'
import lessHljsSupport from 'highlight.js/lib/languages/less'
import luaHljsSupport from 'highlight.js/lib/languages/lua'
import makefileHljsSupport from 'highlight.js/lib/languages/makefile'
import markdownHljsSupport from 'highlight.js/lib/languages/markdown'
import nginxHljsSupport from 'highlight.js/lib/languages/nginx'
import objectivecHljsSupport from 'highlight.js/lib/languages/objectivec'
import perlHljsSupport from 'highlight.js/lib/languages/perl'
import pgsqlHljsSupport from 'highlight.js/lib/languages/pgsql'
import phpHljsSupport from 'highlight.js/lib/languages/php'
import phptemplateHljsSupport from 'highlight.js/lib/languages/php-template'
import plaintextHljsSupport from 'highlight.js/lib/languages/plaintext'
import pythonHljsSupport from 'highlight.js/lib/languages/python'
import pythonreplHljsSupport from 'highlight.js/lib/languages/python-repl'
import rHljsSupport from 'highlight.js/lib/languages/r'
import rubyHljsSupport from 'highlight.js/lib/languages/ruby'
import rustHljsSupport from 'highlight.js/lib/languages/rust'
import scssHljsSupport from 'highlight.js/lib/languages/scss'
import shellHljsSupport from 'highlight.js/lib/languages/shell'
import sqlHljsSupport from 'highlight.js/lib/languages/sql'
import swiftHljsSupport from 'highlight.js/lib/languages/swift'
import typescriptHljsSupport from 'highlight.js/lib/languages/typescript'
import vbnetHljsSupport from 'highlight.js/lib/languages/vbnet'
import wasmHljsSupport from 'highlight.js/lib/languages/wasm'
import xmlHljsSupport from 'highlight.js/lib/languages/xml'
import yamlHljsSupport from 'highlight.js/lib/languages/yaml'
hljs.registerLanguage('apache', apacheHljsSupport)
hljs.registerLanguage('bash', bashHljsSupport)
hljs.registerLanguage('c', cHljsSupport)
hljs.registerLanguage('cpp', cppHljsSupport)
hljs.registerLanguage('csharp', csharpHljsSupport)
hljs.registerLanguage('css', cssHljsSupport)
hljs.registerLanguage('diff', diffHljsSupport)
hljs.registerLanguage('go', goHljsSupport)
hljs.registerLanguage('graphql', graphqlHljsSupport)
hljs.registerLanguage('ini', iniHljsSupport)
hljs.registerLanguage('java', javaHljsSupport)
hljs.registerLanguage('javascript', javascriptHljsSupport)
hljs.registerLanguage('json', jsonHljsSupport)
hljs.registerLanguage('kotlin', kotlinHljsSupport)
hljs.registerLanguage('less', lessHljsSupport)
hljs.registerLanguage('lua', luaHljsSupport)
hljs.registerLanguage('makefile', makefileHljsSupport)
hljs.registerLanguage('markdown', markdownHljsSupport)
hljs.registerLanguage('nginx', nginxHljsSupport)
hljs.registerLanguage('objectivec', objectivecHljsSupport)
hljs.registerLanguage('perl', perlHljsSupport)
hljs.registerLanguage('pgsql', pgsqlHljsSupport)
hljs.registerLanguage('php', phpHljsSupport)
hljs.registerLanguage('php-template', phptemplateHljsSupport)
hljs.registerLanguage('plaintext', plaintextHljsSupport)
hljs.registerLanguage('python', pythonHljsSupport)
hljs.registerLanguage('python-repl', pythonreplHljsSupport)
hljs.registerLanguage('r', rHljsSupport)
hljs.registerLanguage('ruby', rubyHljsSupport)
hljs.registerLanguage('rust', rustHljsSupport)
hljs.registerLanguage('scss', scssHljsSupport)
hljs.registerLanguage('shell', shellHljsSupport)
hljs.registerLanguage('sql', sqlHljsSupport)
hljs.registerLanguage('swift', swiftHljsSupport)
hljs.registerLanguage('typescript', typescriptHljsSupport)
hljs.registerLanguage('vbnet', vbnetHljsSupport)
hljs.registerLanguage('wasm', wasmHljsSupport)
hljs.registerLanguage('xml', xmlHljsSupport)
hljs.registerLanguage('yaml', yamlHljsSupport)
// The above should now register on the languages we wish to support statically.
export const staticHljs = hljs;

View File

@ -1,14 +1,31 @@
import { encode as HTMLEncode } from "he";
import markdownIt from "markdown-it";
import { staticHljs as hljs } from "./hljs";
import { v4 } from "uuid";
// TODO can we add back Hljs without bloating the app in a bad way?
const markdown = markdownIt({
html: true,
typographer: true,
highlight: function (code) {
highlight: function (code, lang) {
const uuid = v4();
if (lang && hljs.getLanguage(lang)) {
try {
return (
`<div class="whitespace-pre-line w-full rounded-lg bg-black-900 pb-4 relative font-mono font-normal text-sm text-slate-200">
<div class="w-full flex items-center absolute top-0 left-0 text-slate-200 bg-stone-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md">
<div class="flex gap-2"><code class="text-xs">${lang}</code></div>
<button data-code-snippet data-code="code-${uuid}" class="flex items-center gap-x-2">
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
<p>Copy</p>
</button>
</div>
<pre class="whitespace-pre-wrap px-2">` +
hljs.highlight(code, { language: lang, ignoreIllegals: true }).value +
"</pre></div>"
);
} catch (__) {}
}
return (
`<div class="whitespace-pre-line w-full rounded-lg bg-black-900 pb-4 relative font-mono font-normal text-sm text-slate-200">
<div class="w-full flex items-center absolute top-0 left-0 text-slate-200 bg-stone-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md">

View File

@ -1,4 +1 @@
// export const USER_BACKGROUND_COLOR = "bg-gray-700";
// export const AI_BACKGROUND_COLOR = "bg-blue-600";
export const USER_BACKGROUND_COLOR = "bg-historical-msg-user";
export const AI_BACKGROUND_COLOR = "bg-historical-msg-system";
export const CHAT_UI_REOPEN = "___anythingllm-chat-widget-open___";

View File

@ -1642,6 +1642,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
highlight.js@^11.9.0:
version "11.9.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"

File diff suppressed because one or more lines are too long

View File

@ -44,7 +44,7 @@ const DataConnectorSetup = lazy(
const EmbedConfigSetup = lazy(
() => import("@/pages/GeneralSettings/EmbedConfigs")
);
const EmbedChats = lazy(() => import("@/pages/Admin/Users"));
const EmbedChats = lazy(() => import("@/pages/GeneralSettings/EmbedChats"));
export default function App() {
return (

View File

@ -22,12 +22,27 @@ export default function OpenAiOptions({ settings }) {
Model Preference
</label>
<select
disabled={true}
className="cursor-not-allowed bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
name="EmbeddingModelPref"
required={true}
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
text-embedding-ada-002
</option>
<optgroup label="Available embedding models">
{[
"text-embedding-ada-002",
"text-embedding-3-small",
"text-embedding-3-large",
].map((model) => {
return (
<option
key={model}
value={model}
selected={settings?.EmbeddingModelPref === model}
>
{model}
</option>
);
})}
</optgroup>
</select>
</div>
</div>

View File

@ -85,6 +85,7 @@ function OpenAIModelSelection({ apiKey, settings }) {
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
"gpt-4",
"gpt-4-turbo-preview",
"gpt-4-1106-preview",
"gpt-4-32k",
].map((model) => {

View File

@ -6,9 +6,14 @@ import Directory from "./Directory";
import showToast from "../../../../utils/toast";
import WorkspaceDirectory from "./WorkspaceDirectory";
// OpenAI Cost per token for text-ada-embedding
// OpenAI Cost per token
// ref: https://openai.com/pricing#:~:text=%C2%A0/%201K%20tokens-,Embedding%20models,-Build%20advanced%20search
const COST_PER_TOKEN = 0.0000001; // $0.0001 / 1K tokens
const MODEL_COSTS = {
"text-embedding-ada-002": 0.0000001, // $0.0001 / 1K tokens
"text-embedding-3-small": 0.00000002, // $0.00002 / 1K tokens
"text-embedding-3-large": 0.00000013, // $0.00013 / 1K tokens
};
export default function DocumentSettings({
workspace,
@ -142,10 +147,12 @@ export default function DocumentSettings({
});
// Do not do cost estimation unless the embedding engine is OpenAi.
if (
!systemSettings?.EmbeddingEngine ||
systemSettings.EmbeddingEngine === "openai"
) {
if (systemSettings?.EmbeddingEngine === "openai") {
const COST_PER_TOKEN =
MODEL_COSTS[
systemSettings?.EmbeddingModelPref || "text-embedding-ada-002"
];
const dollarAmount = (totalTokenCount / 1000) * COST_PER_TOKEN;
setEmbeddingsCost(dollarAmount);
}

View File

@ -8,6 +8,7 @@ const PROVIDER_DEFAULT_MODELS = {
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
"gpt-4",
"gpt-4-turbo-preview",
"gpt-4-1106-preview",
"gpt-4-32k",
],

View File

@ -44,6 +44,7 @@ export default function WorkspaceSettings({ active, workspace, settings }) {
const formEl = useRef(null);
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [deleting, setDeleting] = useState(false);
const defaults = recommendedSettings(settings?.LLMProvider);
const handleUpdate = async (e) => {
@ -72,7 +73,15 @@ export default function WorkspaceSettings({ active, workspace, settings }) {
)
)
return false;
await Workspace.delete(workspace.slug);
setDeleting(true);
const success = await Workspace.delete(workspace.slug);
if (!success) {
showToast("Workspace could not be deleted!", "error", { clear: true });
setDeleting(false);
return;
}
workspace.slug === slug
? (window.location = paths.home())
: window.location.reload();
@ -310,7 +319,11 @@ export default function WorkspaceSettings({ active, workspace, settings }) {
</div>
</div>
<div className="flex items-center justify-between p-2 md:p-6 space-x-2 border-t rounded-b border-gray-600">
<DeleteWorkspace workspace={workspace} onClick={deleteWorkspace} />
<DeleteWorkspace
deleting={deleting}
workspace={workspace}
onClick={deleteWorkspace}
/>
{hasChanges && (
<button
type="submit"
@ -324,7 +337,7 @@ export default function WorkspaceSettings({ active, workspace, settings }) {
);
}
function DeleteWorkspace({ workspace, onClick }) {
function DeleteWorkspace({ deleting, workspace, onClick }) {
const [canDelete, setCanDelete] = useState(false);
useEffect(() => {
async function fetchKeys() {
@ -337,11 +350,12 @@ function DeleteWorkspace({ workspace, onClick }) {
if (!canDelete) return null;
return (
<button
disabled={deleting}
onClick={onClick}
type="button"
className="transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-transparent text-white hover:text-white hover:bg-red-600"
className="transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-transparent text-white hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse"
>
Delete Workspace
{deleting ? "Deleting Workspace..." : "Delete Workspace"}
</button>
);
}

View File

@ -0,0 +1,41 @@
export default function AstraDBOptions({ settings }) {
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Astra DB Endpoint
</label>
<input
type="url"
name="AstraDBEndpoint"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Astra DB API endpoint"
defaultValue={settings?.AstraDBEndpoint}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Astra DB Application Token
</label>
<input
type="password"
name="AstraDBApplicationToken"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="AstraCS:..."
defaultValue={
settings?.AstraDBApplicationToken ? "*".repeat(20) : ""
}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -52,6 +52,29 @@ const Embed = {
return { success: true, error: e.message };
});
},
chats: async (offset = 0) => {
return await fetch(`${API_BASE}/embed/chats`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ offset }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return [];
});
},
deleteChat: async (chatId) => {
return await fetch(`${API_BASE}/embed/chats/${chatId}`, {
method: "DELETE",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
};
export default Embed;

View File

@ -0,0 +1,130 @@
import { useRef } from "react";
import truncate from "truncate";
import { X, Trash, LinkSimple } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
import paths from "@/utils/paths";
import Embed from "@/models/embed";
export default function ChatRow({ chat }) {
const rowRef = useRef(null);
const {
isOpen: isPromptOpen,
openModal: openPromptModal,
closeModal: closePromptModal,
} = useModal();
const {
isOpen: isResponseOpen,
openModal: openResponseModal,
closeModal: closeResponseModal,
} = useModal();
const handleDelete = async () => {
if (
!window.confirm(
`Are you sure you want to delete this chat?\n\nThis action is irreversible.`
)
)
return false;
rowRef?.current?.remove();
await Embed.deleteChat(chat.id);
};
return (
<>
<tr
ref={rowRef}
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
>
<td className="px-6 py-4 font-medium whitespace-nowrap text-white">
<a
href={paths.settings.embedSetup()}
target="_blank"
rel="noreferrer"
className="text-white flex items-center hover:underline"
>
<LinkSimple className="mr-2 w-5 h-5" />{" "}
{chat.embed_config.workspace.name}
</a>
</td>
<td className="px-6 py-4 font-medium whitespace-nowrap text-white">
<div className="flex flex-col">
<p>{truncate(chat.session_id, 20)}</p>
<ConnectionDetails
connection_information={chat.connection_information}
/>
</div>
</td>
<td
onClick={openPromptModal}
className="px-6 py-4 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
>
{truncate(chat.prompt, 40)}
</td>
<td
onClick={openResponseModal}
className="px-6 py-4 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
>
{truncate(JSON.parse(chat.response)?.text, 40)}
</td>
<td className="px-6 py-4">{chat.createdAt}</td>
<td className="px-6 py-4 flex items-center gap-x-6">
<button
onClick={handleDelete}
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
>
<Trash className="h-5 w-5" />
</button>
</td>
</tr>
<ModalWrapper isOpen={isPromptOpen}>
<TextPreview text={chat.prompt} closeModal={closePromptModal} />
</ModalWrapper>
<ModalWrapper isOpen={isResponseOpen}>
<TextPreview
text={JSON.parse(chat.response)?.text}
closeModal={closeResponseModal}
/>
</ModalWrapper>
</>
);
}
const TextPreview = ({ text, closeModal }) => {
return (
<div className="relative w-full md:max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="w-full p-6">
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 border border-gray-500 text-white text-sm">
{text}
</pre>
</div>
</div>
</div>
);
};
const ConnectionDetails = ({ connection_information }) => {
let details = {};
try {
details = JSON.parse(connection_information);
} catch {}
if (Object.keys(details).length === 0) return null;
return (
<>
{details.ip && <p className="text-xs text-slate-400">{details.ip}</p>}
{details.host && <p className="text-xs text-slate-400">{details.host}</p>}
</>
);
};

View File

@ -0,0 +1,124 @@
import { useEffect, useState } from "react";
import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import Embed from "@/models/embed";
export default function EmbedChats() {
// TODO [FEAT]: Add export of embed chats
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
>
{isMobile && <SidebarMobileHeader />}
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Embed Chats</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
These are all the recorded chats and messages from any embed that
you have published.
</p>
</div>
<ChatsContainer />
</div>
</div>
</div>
);
}
function ChatsContainer() {
const query = useQuery();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0));
};
const handleNext = () => {
setOffset(offset + 1);
};
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await Embed.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="#3D4147"
baseColor="#2C2F35"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex w-full"
/>
);
}
return (
<>
<table className="w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Embed
</th>
<th scope="col" className="px-6 py-3">
Sender
</th>
<th scope="col" className="px-6 py-3">
Message
</th>
<th scope="col" className="px-6 py-3">
Response
</th>
<th scope="col" className="px-6 py-3">
Sent At
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{!!chats &&
chats.map((chat) => <ChatRow key={chat.id} chat={chat} />)}
</tbody>
</table>
<div className="flex w-full justify-between items-center">
<button
onClick={handlePrevious}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={offset === 0}
>
{" "}
Previous Page
</button>
<button
onClick={handleNext}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={!canNext}
>
Next Page
</button>
</div>
</>
);
}

View File

@ -2,11 +2,7 @@ import React, { useState } from "react";
import { CheckCircle, CopySimple, X } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import hljs from "highlight.js";
import { encode as HTMLEncode } from "he";
// import hljsHTML from 'highlight.js/lib/languages/vbscript-html';
import "highlight.js/styles/github-dark-dimmed.min.css";
// hljs.registerLanguage('html', hljsHTML)
export default function CodeSnippetModal({ embed, closeModal }) {
return (
@ -51,7 +47,7 @@ function createScriptTagSnippet(embed, scriptHost, serverHost) {
return `<!--
Paste this script at the bottom of your HTML before the </body> tag.
See more style and config options on our docs
https://docs.useanything.com/feature-overview/embed
https://github.com/Mintplex-Labs/anything-llm/tree/master/embed/README.md
-->
<script
data-embed-id="${embed.uuid}"

View File

@ -130,7 +130,7 @@ export const WorkspaceSelection = ({ defaultValue = null }) => {
<div>
<div className="flex flex-col mb-2">
<label
htmlFor="workspaceId"
htmlFor="workspace_id"
className="block text-sm font-medium text-white"
>
Workspace
@ -141,13 +141,20 @@ export const WorkspaceSelection = ({ defaultValue = null }) => {
</p>
</div>
<select
name="workspaceId"
name="workspace_id"
required={true}
defaultValue={defaultValue}
className="min-w-[15rem] rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
>
{workspaces.map((workspace) => {
return <option value={workspace.id}>{workspace.name}</option>;
return (
<option
selected={defaultValue === workspace.id}
value={workspace.id}
>
{workspace.name}
</option>
);
})}
</select>
</div>

View File

@ -10,6 +10,7 @@ import WeaviateLogo from "@/media/vectordbs/weaviate.png";
import QDrantLogo from "@/media/vectordbs/qdrant.png";
import MilvusLogo from "@/media/vectordbs/milvus.png";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import PreLoader from "@/components/Preloader";
import ChangeWarningModal from "@/components/ChangeWarning";
import { MagnifyingGlass } from "@phosphor-icons/react";
@ -23,6 +24,7 @@ import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions";
import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions";
export default function GeneralVectorDatabase() {
const [saving, setSaving] = useState(false);
@ -100,6 +102,13 @@ export default function GeneralVectorDatabase() {
options: <MilvusDBOptions settings={settings} />,
description: "Open-source, highly scalable, and blazing fast.",
},
{
name: "AstraDB",
value: "astra",
logo: AstraDBLogo,
options: <AstraDBOptions settings={settings} />,
description: "Vector Search for Real-world GenAI.",
},
];
const updateVectorChoice = (selection) => {

View File

@ -11,6 +11,7 @@ import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
import LocalAiLogo from "@/media/llmprovider/localai.png";
import MistralLogo from "@/media/llmprovider/mistral.jpeg";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import ChromaLogo from "@/media/vectordbs/chroma.png";
import PineconeLogo from "@/media/vectordbs/pinecone.png";
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
@ -147,6 +148,13 @@ const VECTOR_DB_PRIVACY = {
],
logo: ZillizLogo,
},
astra: {
name: "AstraDB",
description: [
"Your vectors and document text are stored on your cloud AstraDB database.",
],
logo: AstraDBLogo,
},
lancedb: {
name: "LanceDB",
description: [

View File

@ -7,6 +7,7 @@ import WeaviateLogo from "@/media/vectordbs/weaviate.png";
import QDrantLogo from "@/media/vectordbs/qdrant.png";
import MilvusLogo from "@/media/vectordbs/milvus.png";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import System from "@/models/system";
import paths from "@/utils/paths";
import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions";
@ -16,6 +17,7 @@ import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"
import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions";
import MilvusOptions from "@/components/VectorDBSelection/MilvusDBOptions";
import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions";
import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions";
import showToast from "@/utils/toast";
import { useNavigate } from "react-router-dom";
import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem";
@ -100,6 +102,13 @@ export default function VectorDatabaseConnection({
options: <MilvusOptions settings={settings} />,
description: "Open-source, highly scalable, and blazing fast.",
},
{
name: "AstraDB",
value: "astra",
logo: AstraDBLogo,
options: <AstraDBOptions settings={settings} />,
description: "Vector Search for Real-world GenAI.",
},
];
function handleForward() {

View File

@ -20,7 +20,7 @@
"prisma:migrate": "cd server && npx prisma migrate dev --name init",
"prisma:seed": "cd server && npx prisma db seed",
"prisma:setup": "yarn prisma:generate && yarn prisma:migrate && yarn prisma:seed",
"prisma:reset": "cd server && npx prisma db push --force-reset",
"prisma:reset": "truncate -s 0 server/storage/anythingllm.db && yarn prisma:migrate",
"prod:server": "cd server && yarn start",
"prod:frontend": "cd frontend && yarn build",
"generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs",

View File

@ -51,6 +51,7 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
# Only used if you are using an LLM that does not natively support embedding (openai or Azure)
# EMBEDDING_ENGINE='openai'
# OPEN_AI_KEY=sk-xxxx
# EMBEDDING_MODEL_PREF='text-embedding-ada-002'
# EMBEDDING_ENGINE='azure'
# AZURE_OPENAI_ENDPOINT=
@ -76,6 +77,11 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
# PINECONE_API_KEY=
# PINECONE_INDEX=
# Enable all below if you are using vector database: Astra DB.
# VECTOR_DB="astra"
# ASTRA_DB_APPLICATION_TOKEN=
# ASTRA_DB_ENDPOINT=
# Enable all below if you are using vector database: LanceDB.
VECTOR_DB="lancedb"

1
server/.gitignore vendored
View File

@ -10,6 +10,7 @@ storage/imports
!storage/documents/DOCUMENTS.md
logs/server.log
*.db
*.db-journal
storage/lancedb
public/

View File

@ -24,7 +24,6 @@ function chatEndpoints(app) {
async (request, response) => {
try {
const user = await userFromSession(request, response);
// console.log("user", user);
const { slug } = request.params;
const { message, mode = "query" } = reqBody(request);

View File

@ -1,10 +1,7 @@
const { EmbedChats } = require("../models/embedChats");
const { EmbedConfig } = require("../models/embedConfig");
const { reqBody, userFromSession } = require("../utils/http");
const {
validEmbedConfig,
validEmbedConfigId,
} = require("../utils/middleware/embedMiddleware");
const { validEmbedConfigId } = require("../utils/middleware/embedMiddleware");
const {
flexUserRoleValid,
ROLES,
@ -80,14 +77,14 @@ function embedManagementEndpoints(app) {
app.post(
"/embed/chats",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (_, response) => {
async (request, response) => {
try {
const { offset = 0, limit = 20 } = reqBody(request);
const embedChats = await EmbedChats.whereWithEmbed(
const embedChats = await EmbedChats.whereWithEmbedAndWorkspace(
{},
limit,
offset * limit,
{ id: "desc" }
{ id: "desc" },
offset * limit
);
const totalChats = await EmbedChats.count();
const hasPages = totalChats > (offset + 1) * limit;
@ -98,6 +95,21 @@ function embedManagementEndpoints(app) {
}
}
);
app.delete(
"/embed/chats/:chatId",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { chatId } = request.params;
await EmbedChats.delete({ id: Number(chatId) });
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
}
module.exports = { embedManagementEndpoints };

View File

@ -106,6 +106,9 @@ const Document = {
await prisma.workspace_documents.delete({
where: { id: document.id, workspaceId: workspace.id },
});
await prisma.document_vectors.deleteMany({
where: { docId: document.docId },
});
} catch (error) {
console.error(error.message);
}

View File

@ -115,7 +115,7 @@ const EmbedChats = {
}
},
whereWithEmbed: async function (
whereWithEmbedAndWorkspace: async function (
clause = {},
limit = null,
orderBy = null,
@ -124,7 +124,17 @@ const EmbedChats = {
try {
const chats = await prisma.embed_chats.findMany({
where: clause,
include: { embed_config: true },
include: {
embed_config: {
select: {
workspace: {
select: {
name: true,
},
},
},
},
},
...(limit !== null ? { take: limit } : {}),
...(offset !== null ? { skip: offset } : {}),
...(orderBy !== null ? { orderBy } : {}),

View File

@ -13,6 +13,7 @@ const EmbedConfig = {
"max_chats_per_day",
"max_chats_per_session",
"chat_mode",
"workspace_id",
],
new: async function (data, creatorId = null) {
@ -48,7 +49,7 @@ const EmbedConfig = {
),
createdBy: Number(creatorId) ?? null,
workspace: {
connect: { id: Number(data.workspaceId) },
connect: { id: Number(data.workspace_id) },
},
},
});
@ -185,7 +186,11 @@ const BOOLEAN_KEYS = [
"enabled",
];
const NUMBER_KEYS = ["max_chats_per_day", "max_chats_per_session"];
const NUMBER_KEYS = [
"max_chats_per_day",
"max_chats_per_session",
"workspace_id",
];
// Helper to validate a data object strictly into the proper format
function validatedCreationData(value, field) {

View File

@ -68,6 +68,12 @@ const SystemSettings = {
ZillizApiToken: process.env.ZILLIZ_API_TOKEN,
}
: {}),
...(vectorDB === "astra"
? {
AstraDBApplicationToken: process?.env?.ASTRA_DB_APPLICATION_TOKEN,
AstraDBEndpoint: process?.env?.ASTRA_DB_ENDPOINT,
}
: {}),
LLMProvider: llmProvider,
...(llmProvider === "openai"
? {

View File

@ -3,6 +3,7 @@ const slugify = require("slugify");
const { Document } = require("./documents");
const { WorkspaceUser } = require("./workspaceUsers");
const { ROLES } = require("../utils/middleware/multiUserProtected");
const { v4: uuidv4 } = require("uuid");
const Workspace = {
writable: [
@ -22,6 +23,7 @@ const Workspace = {
new: async function (name = null, creatorId = null) {
if (!name) return { result: null, message: "name cannot be null" };
var slug = slugify(name, { lower: true });
slug = slug || uuidv4();
const existingBySlug = await this.get({ slug });
if (existingBySlug !== null) {

View File

@ -22,6 +22,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.8.1",
"@azure/openai": "1.0.0-beta.10",
"@datastax/astra-db-ts": "^0.1.3",
"@google/generative-ai": "^0.1.3",
"@googleapis/youtube": "^9.0.0",
"@pinecone-database/pinecone": "^2.0.1",

View File

@ -52,6 +52,8 @@ class OpenAiLLM {
return 8192;
case "gpt-4-1106-preview":
return 128000;
case "gpt-4-turbo-preview":
return 128000;
case "gpt-4-32k":
return 32000;
default:
@ -65,6 +67,7 @@ class OpenAiLLM {
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
"gpt-4-1106-preview",
"gpt-4-turbo-preview",
"gpt-4-32k",
];
const isPreset = validModels.some((model) => modelName === model);

View File

@ -9,6 +9,7 @@ class OpenAiEmbedder {
});
const openai = new OpenAIApi(config);
this.openai = openai;
this.model = process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002";
// Limit of how many strings we can process in a single pass to stay with resource or network limits
this.maxConcurrentChunks = 500;
@ -30,7 +31,7 @@ class OpenAiEmbedder {
new Promise((resolve) => {
this.openai
.createEmbedding({
model: "text-embedding-ada-002",
model: this.model,
input: chunk,
})
.then((res) => {

View File

@ -50,7 +50,7 @@ async function streamChatWithForEmbed(
id: uuid,
type: "textResponse",
textResponse:
"There is no relevant information in this workspace to answer your query.",
"I do not have enough information to answer that. Try another question.",
sources: [],
close: true,
error: null,

View File

@ -269,6 +269,7 @@ function handleStreamResponses(response, stream, responseProps) {
for (const choice of event.choices) {
const delta = choice.delta?.content;
if (!delta) continue;
fullText += delta;
writeResponseChunk(response, {
uuid,
sources: [],

View File

@ -22,6 +22,9 @@ function getVectorDbClass() {
case "zilliz":
const { Zilliz } = require("../vectorDbProviders/zilliz");
return Zilliz;
case "astra":
const { AstraDB } = require("../vectorDbProviders/astra");
return AstraDB;
default:
throw new Error("ENV: No VECTOR_DB value found in environment!");
}

View File

@ -204,6 +204,17 @@ const KEY_MAPPING = {
checks: [isNotEmpty],
},
// Astra DB Options
AstraDBApplicationToken: {
envKey: "ASTRA_DB_APPLICATION_TOKEN",
checks: [isNotEmpty],
},
AstraDBEndpoint: {
envKey: "ASTRA_DB_ENDPOINT",
checks: [isNotEmpty],
},
// Together Ai Options
TogetherAiApiKey: {
envKey: "TOGETHER_AI_API_KEY",
@ -322,6 +333,7 @@ function supportedVectorDB(input = "") {
"qdrant",
"milvus",
"zilliz",
"astra",
];
return supported.includes(input)
? null

View File

@ -21,9 +21,8 @@ async function validEmbedConfig(request, response, next) {
function setConnectionMeta(request, response, next) {
response.locals.connection = {
host: request.hostname,
path: request.path,
ip: request.ip,
host: request.headers?.origin,
ip: request?.ip,
};
next();
}

View File

@ -0,0 +1,22 @@
# How to setup Astra Vector Database for AnythingLLM
[Official Astra DB Docs](https://docs.datastax.com/en/astra/astra-db-vector/get-started/quickstart.html) for reference.
### How to get started
**Requirements**
- Astra Vector Database with active status.
**Instructions**
- [Create an Astra account or sign in to an existing Astra account](astra.datastax.com)
- Create an Astra Serverless(Vector) Database.
- Make sure DB is in active state.
- Get `API ENDPOINT`and `Application Token` from Overview screen
```
VECTOR_DB="astra"
ASTRA_DB_ENDPOINT=Astra DB API endpoint
ASTRA_DB_APPLICATION_TOKEN=AstraCS:..
```

View File

@ -0,0 +1,380 @@
const { AstraDB: AstraClient } = require("@datastax/astra-db-ts");
const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
const { storeVectorResult, cachedVectorInformation } = require("../../files");
const { v4: uuidv4 } = require("uuid");
const {
toChunks,
getLLMProvider,
getEmbeddingEngineSelection,
} = require("../../helpers");
const AstraDB = {
name: "AstraDB",
connect: async function () {
if (process.env.VECTOR_DB !== "astra")
throw new Error("AstraDB::Invalid ENV settings");
const client = new AstraClient(
process?.env?.ASTRA_DB_APPLICATION_TOKEN,
process?.env?.ASTRA_DB_ENDPOINT
);
return { client };
},
heartbeat: async function () {
return { heartbeat: Number(new Date()) };
},
// Astra interface will return a valid collection object even if the collection
// does not actually exist. So we run a simple check which will always throw
// when the table truly does not exist. Faster than iterating all collections.
isRealCollection: async function (astraCollection = null) {
if (!astraCollection) return false;
return await astraCollection
.countDocuments()
.then(() => true)
.catch(() => false);
},
totalVectors: async function () {
const { client } = await this.connect();
const collectionNames = await this.allNamespaces(client);
var totalVectors = 0;
for (const name of collectionNames) {
const collection = await client.collection(name).catch(() => null);
const count = await collection.countDocuments().catch(() => 0);
totalVectors += count ? count : 0;
}
return totalVectors;
},
namespaceCount: async function (_namespace = null) {
const { client } = await this.connect();
const namespace = await this.namespace(client, _namespace);
return namespace?.vectorCount || 0;
},
namespace: async function (client, namespace = null) {
if (!namespace) throw new Error("No namespace value provided.");
const collection = await client.collection(namespace).catch(() => null);
if (!(await this.isRealCollection(collection))) return null;
const count = await collection.countDocuments().catch((e) => {
console.error("Astra::namespaceExists", e.message);
return null;
});
return {
name: namespace,
...collection,
vectorCount: typeof count === "number" ? count : 0,
};
},
hasNamespace: async function (namespace = null) {
if (!namespace) return false;
const { client } = await this.connect();
return await this.namespaceExists(client, namespace);
},
namespaceExists: async function (client, namespace = null) {
if (!namespace) throw new Error("No namespace value provided.");
const collection = await client.collection(namespace);
return await this.isRealCollection(collection);
},
deleteVectorsInNamespace: async function (client, namespace = null) {
await client.dropCollection(namespace);
return true;
},
// AstraDB requires a dimension aspect for collection creation
// we pass this in from the first chunk to infer the dimensions like other
// providers do.
getOrCreateCollection: async function (client, namespace, dimensions = null) {
const isExists = await this.namespaceExists(client, namespace);
if (!isExists) {
if (!dimensions)
throw new Error(
`AstraDB:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.`
);
await client.createCollection(namespace, {
vector: {
dimension: dimensions,
metric: "cosine",
},
});
}
return await client.collection(namespace);
},
addDocumentToNamespace: async function (
namespace,
documentData = {},
fullFilePath = null
) {
const { DocumentVectors } = require("../../../models/vectors");
try {
let vectorDimension = null;
const { pageContent, docId, ...metadata } = documentData;
if (!pageContent || pageContent.length == 0) return false;
console.log("Adding new vectorized document into namespace", namespace);
const cacheResult = await cachedVectorInformation(fullFilePath);
if (cacheResult.exists) {
const { client } = await this.connect();
const { chunks } = cacheResult;
const documentVectors = [];
vectorDimension = chunks[0][0].values.length || null;
const collection = await this.getOrCreateCollection(
client,
namespace,
vectorDimension
);
if (!(await this.isRealCollection(collection)))
throw new Error("Failed to create new AstraDB collection!", {
namespace,
});
for (const chunk of chunks) {
// Before sending to Astra and saving the records to our db
// we need to assign the id of each chunk that is stored in the cached file.
const newChunks = chunk.map((chunk) => {
const _id = uuidv4();
documentVectors.push({ docId, vectorId: _id });
return {
_id: _id,
$vector: chunk.values,
metadata: chunk.metadata || {},
};
});
await collection.insertMany(newChunks);
}
await DocumentVectors.bulkInsert(documentVectors);
return { vectorized: true, error: null };
}
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize:
getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000,
chunkOverlap: 20,
});
const textChunks = await textSplitter.splitText(pageContent);
console.log("Chunks created from document:", textChunks.length);
const LLMConnector = getLLMProvider();
const documentVectors = [];
const vectors = [];
const vectorValues = await LLMConnector.embedChunks(textChunks);
if (!!vectorValues && vectorValues.length > 0) {
for (const [i, vector] of vectorValues.entries()) {
if (!vectorDimension) vectorDimension = vector.length;
const vectorRecord = {
_id: uuidv4(),
$vector: vector,
metadata: { ...metadata, text: textChunks[i] },
};
vectors.push(vectorRecord);
documentVectors.push({ docId, vectorId: vectorRecord._id });
}
} else {
throw new Error(
"Could not embed document chunks! This document will not be recorded."
);
}
const { client } = await this.connect();
const collection = await this.getOrCreateCollection(
client,
namespace,
vectorDimension
);
if (!(await this.isRealCollection(collection)))
throw new Error("Failed to create new AstraDB collection!", {
namespace,
});
if (vectors.length > 0) {
const chunks = [];
console.log("Inserting vectorized chunks into Astra DB.");
// AstraDB has maximum upsert size of 20 records per-request so we have to use a lower chunk size here
// in order to do the queries - this takes a lot more time than other providers but there
// is no way around it. This will save the vector-cache with the same layout, so we don't
// have to chunk again for cached files.
for (const chunk of toChunks(vectors, 20)) {
chunks.push(
chunk.map((c) => {
return { id: c._id, values: c.$vector, metadata: c.metadata };
})
);
await collection.insertMany(chunk);
}
await storeVectorResult(chunks, fullFilePath);
}
await DocumentVectors.bulkInsert(documentVectors);
return { vectorized: true, error: null };
} catch (e) {
console.error("addDocumentToNamespace", e.message);
return { vectorized: false, error: e.message };
}
},
deleteDocumentFromNamespace: async function (namespace, docId) {
const { DocumentVectors } = require("../../../models/vectors");
const { client } = await this.connect();
if (!(await this.namespaceExists(client, namespace)))
throw new Error(
"Invalid namespace - has it been collected and populated yet?"
);
const collection = await client.collection(namespace);
const knownDocuments = await DocumentVectors.where({ docId });
if (knownDocuments.length === 0) return;
const vectorIds = knownDocuments.map((doc) => doc.vectorId);
for (const id of vectorIds) {
await collection.deleteMany({
_id: id,
});
}
const indexes = knownDocuments.map((doc) => doc.id);
await DocumentVectors.deleteIds(indexes);
return true;
},
performSimilaritySearch: async function ({
namespace = null,
input = "",
LLMConnector = null,
similarityThreshold = 0.25,
topN = 4,
}) {
if (!namespace || !input || !LLMConnector)
throw new Error("Invalid request to performSimilaritySearch.");
const { client } = await this.connect();
if (!(await this.namespaceExists(client, namespace))) {
return {
contextTexts: [],
sources: [],
message:
"Invalid query - no namespace found for workspace in vector db!",
};
}
const queryVector = await LLMConnector.embedTextInput(input);
const { contextTexts, sourceDocuments } = await this.similarityResponse(
client,
namespace,
queryVector,
similarityThreshold,
topN
);
const sources = sourceDocuments.map((metadata, i) => {
return { ...metadata, text: contextTexts[i] };
});
return {
contextTexts,
sources: this.curateSources(sources),
message: false,
};
},
similarityResponse: async function (
client,
namespace,
queryVector,
similarityThreshold = 0.25,
topN = 4
) {
const result = {
contextTexts: [],
sourceDocuments: [],
scores: [],
};
const collection = await client.collection(namespace);
const responses = await collection
.find(
{},
{
sort: { $vector: queryVector },
limit: topN,
includeSimilarity: true,
}
)
.toArray();
responses.forEach((response) => {
if (response.$similarity < similarityThreshold) return;
result.contextTexts.push(response.metadata.text);
result.sourceDocuments.push(response);
result.scores.push(response.$similarity);
});
return result;
},
allNamespaces: async function (client) {
try {
let header = new Headers();
header.append("Token", client?.httpClient?.applicationToken);
header.append("Content-Type", "application/json");
let raw = JSON.stringify({
findCollections: {},
});
let requestOptions = {
method: "POST",
headers: header,
body: raw,
redirect: "follow",
};
const call = await fetch(client?.httpClient?.baseUrl, requestOptions);
const resp = await call?.text();
const collections = resp ? JSON.parse(resp)?.status?.collections : [];
return collections;
} catch (e) {
console.error("Astra::AllNamespace", e);
return [];
}
},
"namespace-stats": async function (reqBody = {}) {
const { namespace = null } = reqBody;
if (!namespace) throw new Error("namespace required");
const { client } = await this.connect();
if (!(await this.namespaceExists(client, namespace)))
throw new Error("Namespace by that name does not exist.");
const stats = await this.namespace(client, namespace);
return stats
? stats
: { message: "No stats were able to be fetched from DB for namespace" };
},
"delete-namespace": async function (reqBody = {}) {
const { namespace = null } = reqBody;
const { client } = await this.connect();
if (!(await this.namespaceExists(client, namespace)))
throw new Error("Namespace by that name does not exist.");
const details = await this.namespace(client, namespace);
await this.deleteVectorsInNamespace(client, namespace);
return {
message: `Namespace ${namespace} was deleted along with ${
details?.vectorCount || "all"
} vectors.`,
};
},
curateSources: function (sources = []) {
const documents = [];
for (const source of sources) {
if (Object.keys(source).length > 0) {
const metadata = source.hasOwnProperty("metadata")
? source.metadata
: source;
documents.push({
...metadata,
});
}
}
return documents;
},
};
module.exports.AstraDB = AstraDB;

View File

@ -207,9 +207,9 @@ const LanceDb = {
vectors.push(vectorRecord);
submissions.push({
...vectorRecord.metadata,
id: vectorRecord.id,
vector: vectorRecord.values,
...vectorRecord.metadata,
});
documentVectors.push({ docId, vectorId: vectorRecord.id });
}

View File

@ -174,6 +174,15 @@
enabled "2.0.x"
kuler "^2.0.0"
"@datastax/astra-db-ts@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@datastax/astra-db-ts/-/astra-db-ts-0.1.3.tgz#fcc25cda8d146c06278860054f09d687ff031568"
integrity sha512-7lnpym0HhUtfJVd8+vu6vYdDQpFyYof7TVLFVD2fgoIjUwj3EksFXmqDqicLAlLferZDllqSVthX9pXQ5Rdapw==
dependencies:
axios "^1.4.0"
bson "^6.2.0"
winston "^3.7.2"
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@ -1353,6 +1362,11 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
bson@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-6.2.0.tgz#4b6acafc266ba18eeee111373c2699304a9ba0a3"
integrity sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==
btoa-lite@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
@ -5636,7 +5650,7 @@ winston-transport@^4.5.0:
readable-stream "^3.6.0"
triple-beam "^1.3.0"
winston@^3.9.0:
winston@^3.7.2, winston@^3.9.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.11.0.tgz#2d50b0a695a2758bb1c95279f0a88e858163ed91"
integrity sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==