Merge branch 'master' of github.com:khoj-ai/khoj into HEAD

This commit is contained in:
sabaimran
2025-01-20 15:35:19 -08:00
46 changed files with 1876 additions and 259 deletions

View File

@@ -1,4 +1,4 @@
name: build and deploy github pages for documentation
name: deploy documentation
on:
push:
branches:

View File

@@ -26,7 +26,7 @@ jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
container: ubuntu:jammy
container: ubuntu:latest
strategy:
fail-fast: false
matrix:
@@ -67,19 +67,19 @@ jobs:
env:
DEBIAN_FRONTEND: noninteractive
run : |
apt install -y postgresql postgresql-client && apt install -y postgresql-server-dev-14
apt install -y postgresql postgresql-client && apt install -y postgresql-server-dev-16
- name: ⬇️ Install pip
run: |
apt install -y python3-pip
python -m ensurepip --upgrade
python -m pip install --upgrade pip
python3 -m ensurepip --upgrade
python3 -m pip install --upgrade pip
- name: ⬇️ Install Application
env:
PIP_EXTRA_INDEX_URL: "https://download.pytorch.org/whl/cpu https://abetlen.github.io/llama-cpp-python/whl/cpu"
CUDA_VISIBLE_DEVICES: ""
run: sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && pip install --upgrade .[dev]
run: sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && pip install --break-system-packages --upgrade .[dev]
- name: 🧪 Test Application
env:

View File

@@ -58,6 +58,6 @@ RUN cd src && python3 khoj/manage.py collectstatic --noinput
# Run the Application
# There are more arguments required for the application to run,
# but those should be passed in through the docker-compose.yml file.
ARG PORT
ARG PORT=42110
EXPOSE ${PORT}
ENTRYPOINT ["python3", "src/khoj/main.py"]

View File

@@ -1,7 +1,6 @@
services:
database:
image: ankane/pgvector
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -15,10 +14,8 @@ services:
retries: 5
sandbox:
image: ghcr.io/khoj-ai/terrarium:latest
restart: unless-stopped
search:
image: docker.io/searxng/searxng:latest
restart: unless-stopped
volumes:
- khoj_search:/etc/searxng
environment:
@@ -29,7 +26,6 @@ services:
condition: service_healthy
# Use the following line to use the latest version of khoj. Otherwise, it will build from source. Set this to ghcr.io/khoj-ai/khoj-cloud:latest if you want to use the prod image.
image: ghcr.io/khoj-ai/khoj:latest
restart: unless-stopped
# Uncomment the following line to build from source. This will take a few minutes. Comment the next two lines out if you want to use the official image.
# build:
# context: .
@@ -63,7 +59,7 @@ services:
- KHOJ_SEARXNG_URL=http://search:8080
# Uncomment line below to use with Ollama running on your local machine at localhost:11434.
# Change URL to use with other OpenAI API compatible providers like VLLM, LMStudio etc.
# - OPENAI_API_BASE=http://host.docker.internal:11434/v1/
# - OPENAI_BASE_URL=http://host.docker.internal:11434/v1/
#
# Uncomment appropriate lines below to use chat models by OpenAI, Anthropic, Google.
# Ensure you set your provider specific API keys.

View File

@@ -14,14 +14,14 @@ LM Studio can expose an [OpenAI API compatible server](https://lmstudio.ai/docs/
## Setup
1. Install [LM Studio](https://lmstudio.ai/) and download your preferred Chat Model
2. Go to the Server Tab on LM Studio, Select your preferred Chat Model and Click the green Start Server button
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
3. Create a new [Add ai model api](http://localhost:42110/server/admin/database/aimodelapi/add/) on your Khoj admin panel
- Name: `proxy-name`
- Api Key: `any string`
- Api Base Url: `http://localhost:1234/v1/` (default for LMStudio)
4. Create a new [Chat Model](http://localhost:42110/server/admin/database/chatmodel/add) on your Khoj admin panel.
- Name: `llama3.1` (replace with the name of your local model)
- Model Type: `Openai`
- Openai Config: `<the proxy config you created in step 3>`
- Ai model api: `<the Ai model api you created in step 3>`
- Max prompt size: `20000` (replace with the max prompt size of your model)
- Tokenizer: *Do not set for OpenAI, mistral, llama3 based models*
5. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View File

@@ -32,7 +32,7 @@ Restart your Khoj server after first run or update to the settings below to ensu
```bash
ollama pull llama3.1
```
3. Uncomment `OPENAI_API_BASE` environment variable in your downloaded Khoj [docker-compose.yml](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml#:~:text=OPENAI_API_BASE)
3. Uncomment `OPENAI_BASE_URL` environment variable in your downloaded Khoj [docker-compose.yml](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml#:~:text=OPENAI_BASE_URL)
4. Start Khoj docker for the first time to automatically integrate and load models from the Ollama running on your host machine
```bash
# run below command in the directory where you downloaded the Khoj docker-compose.yml
@@ -46,9 +46,9 @@ Restart your Khoj server after first run or update to the settings below to ensu
```bash
ollama pull llama3.1
```
3. Set `OPENAI_API_BASE` environment variable to `http://localhost:11434/v1/` in your shell before starting Khoj for the first time
3. Set `OPENAI_BASE_URL` environment variable to `http://localhost:11434/v1/` in your shell before starting Khoj for the first time
```bash
export OPENAI_API_BASE="http://localhost:11434/v1/"
export OPENAI_BASE_URL="http://localhost:11434/v1/"
khoj --anonymous-mode
```
</TabItem>

View File

@@ -15,3 +15,37 @@ Take advantage of super fast search to find relevant notes and documents from yo
### Demo
![](/img/search_agents_markdown.png ':size=400px')
### Implementation Overview
A bi-encoder models is used to create meaning vectors (aka vector embeddings) of your documents and search queries.
1. When you sync you documents with Khoj, it uses the bi-encoder model to create and store meaning vectors of (chunks of) your documents
2. When you initiate a natural language search the bi-encoder model converts your query into a meaning vector and finds the most relevant document chunks for that query by comparing their meaning vectors.
3. The slower but higher-quality cross-encoder model is than used to re-rank these documents for your given query.
### Setup (Self-Hosting)
You are **not required** to configure the search model config when self-hosting. Khoj sets up decent default local search model config for general use.
You may want to configure this if you need better multi-lingual search, want to experiment with different, newer models or the default models do not work for your use-case.
You can use bi-encoder models downloaded locally [from Huggingface](https://huggingface.co/models?library=sentence-transformers), served via the [HuggingFace Inference API](https://endpoints.huggingface.co/), OpenAI API, Azure OpenAI API or any OpenAI Compatible API like Ollama, LiteLLM etc. Follow the steps below to configure your search model:
1. Open the [SearchModelConfig](http://localhost:42110/server/admin/database/searchmodelconfig/) page on your Khoj admin panel.
2. Hit the Plus button to add a new model config or click the id of an existing model config to edit it.
3. Set the `biencoder` field to the name of the bi-encoder model supported [locally](https://huggingface.co/models?library=sentence-transformers) or via the API you configure.
4. Set the `Embeddings inference endpoint api key` to your OpenAI API key and `Embeddings inference endpoint type` to `OpenAI` to use an OpenAI embedding model.
5. Also set the `Embeddings inference endpoint` to your Azure OpenAI or OpenAI compatible API URL to use the model via those APIs.
6. Ensure the search model config you want to use is the **only one** that has `name` field set to `default`[^1].
7. Save the search model configs and restart your Khoj server to start using your new, updated search config.
:::info
You will need to re-index all your documents if you want to use a different bi-encoder model.
:::
:::info
You may need to tune the `Bi encoder confidence threshold` field for each bi-encoder to get appropriate number of documents for chat with your Knowledge base.
Confidence here is a normalized measure of semantic distance between your query and documents. The confidence threshold limits the documents returned to chat that fall within the distance specified in this field. It can take values between 0.0 (exact overlap) and 1.0 (no meaning overlap).
:::
[^1]: Khoj uses the first search model config named `default` it finds on startup as the search model config for that session

View File

@@ -48,7 +48,7 @@ Restart your Khoj server after the first run to ensure all settings are applied
2. Configure the environment variables in the `docker-compose.yml`
- Set `KHOJ_ADMIN_PASSWORD`, `KHOJ_DJANGO_SECRET_KEY` (and optionally the `KHOJ_ADMIN_EMAIL`) to something secure. This allows you to customize Khoj later via the admin panel.
- Set `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY` to your API key if you want to use OpenAI, Anthropic or Gemini commercial chat models respectively.
- Uncomment `OPENAI_API_BASE` to use [Ollama](/advanced/ollama?type=first-run&server=docker#setup) running on your host machine. Or set it to the URL of your OpenAI compatible API like vLLM or [LMStudio](/advanced/lmstudio).
- Uncomment `OPENAI_BASE_URL` to use [Ollama](/advanced/ollama?type=first-run&server=docker#setup) running on your host machine. Or set it to the URL of your OpenAI compatible API like vLLM or [LMStudio](/advanced/lmstudio).
3. Start Khoj by running the following command in the same directory as your docker-compose.yml file.
```shell
cd ~/.khoj
@@ -74,7 +74,7 @@ Restart your Khoj server after the first run to ensure all settings are applied
2. Configure the environment variables in the `docker-compose.yml`
- Set `KHOJ_ADMIN_PASSWORD`, `KHOJ_DJANGO_SECRET_KEY` (and optionally the `KHOJ_ADMIN_EMAIL`) to something secure. This allows you to customize Khoj later via the admin panel.
- Set `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY` to your API key if you want to use OpenAI, Anthropic or Gemini commercial chat models respectively.
- Uncomment `OPENAI_API_BASE` to use [Ollama](/advanced/ollama) running on your host machine. Or set it to the URL of your OpenAI compatible API like vLLM or [LMStudio](/advanced/lmstudio).
- Uncomment `OPENAI_BASE_URL` to use [Ollama](/advanced/ollama) running on your host machine. Or set it to the URL of your OpenAI compatible API like vLLM or [LMStudio](/advanced/lmstudio).
3. Start Khoj by running the following command in the same directory as your docker-compose.yml file.
```shell
# Windows users should use their WSL2 terminal to run these commands
@@ -96,7 +96,7 @@ Restart your Khoj server after the first run to ensure all settings are applied
2. Configure the environment variables in the `docker-compose.yml`
- Set `KHOJ_ADMIN_PASSWORD`, `KHOJ_DJANGO_SECRET_KEY` (and optionally the `KHOJ_ADMIN_EMAIL`) to something secure. This allows you to customize Khoj later via the admin panel.
- Set `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY` to your API key if you want to use OpenAI, Anthropic or Gemini commercial chat models respectively.
- Uncomment `OPENAI_API_BASE` to use [Ollama](/advanced/ollama) running on your host machine. Or set it to the URL of your OpenAI compatible API like vLLM or [LMStudio](/advanced/lmstudio).
- Uncomment `OPENAI_BASE_URL` to use [Ollama](/advanced/ollama) running on your host machine. Or set it to the URL of your OpenAI compatible API like vLLM or [LMStudio](/advanced/lmstudio).
3. Start Khoj by running the following command in the same directory as your docker-compose.yml file.
```shell
cd ~/.khoj

View File

@@ -1,7 +1,7 @@
{
"id": "khoj",
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"minAppVersion": "0.15.0",
"description": "Your Second Brain",
"author": "Khoj Inc.",

View File

@@ -229,7 +229,7 @@ function generateImageMarkdown(message, intentType, inferredQueries=null) { //sa
} else if (intentType === "text-to-image2") {
imageMarkdown = `![](${message})`;
} else if (intentType === "text-to-image-v3") {
imageMarkdown = `![](data:image/webp;base64,${message})`;
imageMarkdown = `![](${message})`;
}
const inferredQuery = inferredQueries?.[0];
if (inferredQuery) {
@@ -423,7 +423,7 @@ function handleImageResponse(imageJson, rawResponse) {
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
rawResponse = `![](${imageJson.image})`;
} else if (imageJson.intentType === "excalidraw") {
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in the web app`;
rawResponse += redirectMessage;

View File

@@ -1,6 +1,6 @@
{
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"description": "Your Second Brain",
"author": "Khoj Inc. <team@khoj.dev>",
"license": "GPL-3.0-or-later",

View File

@@ -6,7 +6,7 @@
;; Saba Imran <saba@khoj.dev>
;; Description: Your Second Brain
;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image
;; Version: 1.33.2
;; Version: 1.34.0
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs

View File

@@ -1,7 +1,7 @@
{
"id": "khoj",
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"minAppVersion": "0.15.0",
"description": "Your Second Brain",
"author": "Khoj Inc.",

View File

@@ -1,6 +1,6 @@
{
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"description": "Your Second Brain",
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
"license": "GPL-3.0-or-later",

View File

@@ -501,6 +501,7 @@ export class KhojChatView extends KhojPaneView {
conversationId?: string,
images?: string[],
excalidrawDiagram?: string,
mermaidjsDiagram?: string
) {
if (!message) return;
@@ -509,8 +510,9 @@ export class KhojChatView extends KhojPaneView {
intentType?.includes("text-to-image") ||
intentType === "excalidraw" ||
(images && images.length > 0) ||
mermaidjsDiagram ||
excalidrawDiagram) {
let imageMarkdown = this.generateImageMarkdown(message, intentType ?? "", inferredQueries, conversationId, images, excalidrawDiagram);
let imageMarkdown = this.generateImageMarkdown(message, intentType ?? "", inferredQueries, conversationId, images, excalidrawDiagram, mermaidjsDiagram);
chatMessageEl = this.renderMessage({
chatBodyEl: chatEl,
message: imageMarkdown,
@@ -542,28 +544,23 @@ export class KhojChatView extends KhojPaneView {
chatMessageBodyEl.appendChild(this.createReferenceSection(references));
}
generateImageMarkdown(message: string, intentType: string, inferredQueries?: string[], conversationId?: string, images?: string[], excalidrawDiagram?: string): string {
generateImageMarkdown(message: string, intentType: string, inferredQueries?: string[], conversationId?: string, images?: string[], excalidrawDiagram?: string, mermaidjsDiagram?: string): string {
let imageMarkdown = "";
if (intentType === "text-to-image") {
imageMarkdown = `![](data:image/png;base64,${message})`;
} else if (intentType === "text-to-image2") {
imageMarkdown = `![](${message})`;
} else if (intentType === "text-to-image-v3") {
imageMarkdown = `![](data:image/webp;base64,${message})`;
imageMarkdown = `![](${message})`;
} else if (intentType === "excalidraw" || excalidrawDiagram) {
const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`;
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}chat?conversationId=${conversationId}`;
imageMarkdown = redirectMessage;
} else if (mermaidjsDiagram) {
imageMarkdown = "```mermaid\n" + mermaidjsDiagram + "\n```";
} else if (images && images.length > 0) {
for (let image of images) {
if (image.startsWith("https://")) {
imageMarkdown += `![](${image})\n\n`;
} else {
imageMarkdown += `![](data:image/png;base64,${image})\n\n`;
}
}
imageMarkdown += `${message}`;
imageMarkdown += images.map(image => `![](${image})`).join('\n\n');
imageMarkdown += message;
}
if (images?.length === 0 && inferredQueries) {
@@ -961,6 +958,7 @@ export class KhojChatView extends KhojPaneView {
chatBodyEl.dataset.conversationId ?? "",
chatLog.images,
chatLog.excalidrawDiagram,
chatLog.mermaidjsDiagram,
);
// push the user messages to the chat history
if (chatLog.by === "you") {
@@ -1077,7 +1075,7 @@ export class KhojChatView extends KhojPaneView {
}
handleJsonResponse(jsonData: any): void {
if (jsonData.image || jsonData.detail || jsonData.images || jsonData.excalidrawDiagram) {
if (jsonData.image || jsonData.detail || jsonData.images || jsonData.mermaidjsDiagram) {
this.chatMessageState.rawResponse = this.handleImageResponse(jsonData, this.chatMessageState.rawResponse);
} else if (jsonData.response) {
this.chatMessageState.rawResponse = jsonData.response;
@@ -1450,7 +1448,7 @@ export class KhojChatView extends KhojPaneView {
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
rawResponse = `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "excalidraw") {
const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`;
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}`;
@@ -1462,17 +1460,14 @@ export class KhojChatView extends KhojPaneView {
} else if (imageJson.images) {
// If response has images field, response is a list of generated images.
imageJson.images.forEach((image: any) => {
if (image.startsWith("http")) {
rawResponse += `![generated_image](${image})\n\n`;
} else {
rawResponse += `![generated_image](data:image/png;base64,${image})\n\n`;
}
rawResponse += `![generated_image](${image})\n\n`;
});
} else if (imageJson.excalidrawDiagram) {
const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`;
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}`;
rawResponse += redirectMessage;
} else if (imageJson.mermaidjsDiagram) {
rawResponse += imageJson.mermaidjsDiagram;
}
// If response has detail field, response is an error message.

View File

@@ -108,5 +108,6 @@
"1.32.2": "0.15.0",
"1.33.0": "0.15.0",
"1.33.1": "0.15.0",
"1.33.2": "0.15.0"
"1.33.2": "0.15.0",
"1.34.0": "0.15.0"
}

View File

@@ -354,7 +354,15 @@ export default function Chat() {
try {
await readChatStream(response);
} catch (err) {
const apiError = await response.json();
let apiError;
try {
apiError = await response.json();
} catch (err) {
// Error reading API error response
apiError = {
streamError: "Error reading API error response stream. Expected JSON response.",
};
}
console.error(apiError);
// Retrieve latest message being processed
const currentMessage = messages.find((message) => !message.completed);
@@ -365,7 +373,9 @@ export default function Chat() {
const errorName = (err as Error).name;
if (errorMessage.includes("Error in input stream"))
currentMessage.rawResponse = `Woops! The connection broke while I was writing my thoughts down. Maybe try again in a bit or dislike this message if the issue persists?`;
else if (response.status === 429) {
else if (apiError.streamError) {
currentMessage.rawResponse = `Umm, not sure what just happened but I lost my train of thought. Could you try again or ask my developers to look into this if the issue persists? They can be contacted at the Khoj Github, Discord or team@khoj.dev.`;
} else if (response.status === 429) {
"detail" in apiError
? (currentMessage.rawResponse = `${apiError.detail}`)
: (currentMessage.rawResponse = `I'm a bit overwhelmed at the moment. Could you try again in a bit or dislike this message if the issue persists?`);

View File

@@ -19,7 +19,7 @@ export interface MessageMetadata {
export interface GeneratedAssetsData {
images: string[];
excalidrawDiagram: string;
mermaidjsDiagram: string;
files: AttachedFileText[];
}
@@ -114,8 +114,8 @@ export function processMessageChunk(
currentMessage.generatedImages = generatedAssets.images;
}
if (generatedAssets.excalidrawDiagram) {
currentMessage.generatedExcalidrawDiagram = generatedAssets.excalidrawDiagram;
if (generatedAssets.mermaidjsDiagram) {
currentMessage.generatedMermaidjsDiagram = generatedAssets.mermaidjsDiagram;
}
if (generatedAssets.files) {

View File

@@ -418,7 +418,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
conversationId: props.conversationId,
images: message.generatedImages,
queryFiles: message.generatedFiles,
excalidrawDiagram: message.generatedExcalidrawDiagram,
mermaidjsDiagram: message.generatedMermaidjsDiagram,
turnId: messageTurnId,
}}
conversationId={props.conversationId}

View File

@@ -53,6 +53,7 @@ import { DialogTitle } from "@radix-ui/react-dialog";
import { convertBytesToText } from "@/app/common/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getIconFromFilename } from "@/app/common/iconUtils";
import Mermaid from "../mermaid/mermaid";
const md = new markdownIt({
html: true,
@@ -164,6 +165,7 @@ export interface SingleChatMessage {
turnId?: string;
queryFiles?: AttachedFileText[];
excalidrawDiagram?: string;
mermaidjsDiagram?: string;
}
export interface StreamMessage {
@@ -182,9 +184,11 @@ export interface StreamMessage {
turnId?: string;
queryFiles?: AttachedFileText[];
excalidrawDiagram?: string;
mermaidjsDiagram?: string;
generatedFiles?: AttachedFileText[];
generatedImages?: string[];
generatedExcalidrawDiagram?: string;
generatedMermaidjsDiagram?: string;
}
export interface ChatHistoryData {
@@ -271,6 +275,7 @@ interface ChatMessageProps {
turnId?: string;
generatedImage?: string;
excalidrawDiagram?: string;
mermaidjsDiagram?: string;
generatedFiles?: AttachedFileText[];
}
@@ -358,6 +363,7 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [interrupted, setInterrupted] = useState<boolean>(false);
const [excalidrawData, setExcalidrawData] = useState<string>("");
const [mermaidjsData, setMermaidjsData] = useState<string>("");
const interruptedRef = useRef<boolean>(false);
const messageRef = useRef<HTMLDivElement>(null);
@@ -401,6 +407,10 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
setExcalidrawData(props.chatMessage.excalidrawDiagram);
}
if (props.chatMessage.mermaidjsDiagram) {
setMermaidjsData(props.chatMessage.mermaidjsDiagram);
}
// Replace LaTeX delimiters with placeholders
message = message
.replace(/\\\(/g, "LEFTPAREN")
@@ -718,6 +728,7 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
dangerouslySetInnerHTML={{ __html: markdownRendered }}
/>
{excalidrawData && <ExcalidrawComponent data={excalidrawData} />}
{mermaidjsData && <Mermaid chart={mermaidjsData} />}
</div>
<div className={styles.teaserReferencesContainer}>
<TeaserReferencesSection

View File

@@ -1,4 +1,9 @@
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { CircleNotch } from "@phosphor-icons/react";
import { AppSidebar } from "../appSidebar/appSidebar";
import { Separator } from "@/components/ui/separator";
import { useIsMobileWidth } from "@/app/common/utils";
import { KhojLogoType } from "../logo/khojLogo";
interface LoadingProps {
className?: string;
@@ -7,21 +12,39 @@ interface LoadingProps {
}
export default function Loading(props: LoadingProps) {
const isMobileWidth = useIsMobileWidth();
return (
// NOTE: We can display usage tips here for casual learning moments.
<div
className={
props.className ||
"bg-background opacity-50 flex items-center justify-center h-screen"
}
>
<div>
{props.message || "Loading"}{" "}
<span>
<CircleNotch className="inline animate-spin h-5 w-5" />
</span>
<SidebarProvider>
<AppSidebar conversationId={""} />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
{isMobileWidth ? (
<a className="p-0 no-underline" href="/">
<KhojLogoType className="h-auto w-16" />
</a>
) : (
<h2 className="text-lg">Ask Anything</h2>
)}
</header>
</SidebarInset>
<div
className={
props.className ||
"bg-background opacity-50 flex items-center justify-center h-full w-full fixed top-0 left-0 z-50"
}
>
<div>
{props.message || "Loading"}{" "}
<span>
<CircleNotch className="inline animate-spin h-5 w-5" />
</span>
</div>
</div>
</div>
</SidebarProvider>
);
}

View File

@@ -2,7 +2,7 @@
import styles from "./loginPrompt.module.css";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import Autoplay from "embla-carousel-autoplay";
import {
@@ -27,6 +27,7 @@ import {
} from "@/components/ui/carousel";
import { Card, CardContent } from "@/components/ui/card";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
export interface LoginPromptProps {
onOpenChange: (open: boolean) => void;
@@ -181,6 +182,9 @@ export default function LoginPrompt(props: LoginPromptProps) {
<DialogContent
className={`flex flex-col gap-4 ${!useEmailSignIn ? "p-0 pb-4 m-0 max-w-xl" : "w-fit"}`}
>
<VisuallyHidden.Root>
<DialogTitle>Login Dialog</DialogTitle>
</VisuallyHidden.Root>
<div>
{useEmailSignIn ? (
<EmailSignInContext
@@ -232,7 +236,7 @@ function EmailSignInContext({
const [numFailures, setNumFailures] = useState(0);
function checkOTPAndRedirect() {
const verifyUrl = `/auth/magic?code=${otp}&email=${email}`;
const verifyUrl = `/auth/magic?code=${encodeURIComponent(otp)}&email=${encodeURIComponent(email)}`;
if (numFailures >= ALLOWED_OTP_ATTEMPTS) {
setOTPError("Too many failed attempts. Please try again tomorrow.");

View File

@@ -0,0 +1,173 @@
import React, { useEffect, useState, useRef } from "react";
import mermaid from "mermaid";
import { Download, Info } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
interface MermaidProps {
chart: string;
}
const Mermaid: React.FC<MermaidProps> = ({ chart }) => {
const [mermaidError, setMermaidError] = useState<string | null>(null);
const [mermaidId] = useState(`mermaid-chart-${Math.random().toString(12).substring(7)}`);
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
});
mermaid.parseError = (error) => {
console.error("Mermaid errors:", error);
// Extract error message from error object
// Parse error message safely
let errorMessage;
try {
errorMessage = typeof error === "string" ? JSON.parse(error) : error;
} catch (e) {
errorMessage = error?.toString() || "Unknown error";
}
console.log("Mermaid error message:", errorMessage);
if (errorMessage.str !== "element is null") {
setMermaidError(
"Something went wrong while rendering the diagram. Please try again later or downvote the message if the issue persists.",
);
} else {
setMermaidError(null);
}
};
mermaid.contentLoaded();
}, []);
const handleExport = async () => {
if (!elementRef.current) return;
try {
// Get SVG element
const svgElement = elementRef.current.querySelector("svg");
if (!svgElement) throw new Error("No SVG found");
// Get SVG viewBox dimensions
const viewBox = svgElement.getAttribute("viewBox")?.split(" ").map(Number) || [
0, 0, 0, 0,
];
const [, , viewBoxWidth, viewBoxHeight] = viewBox;
// Create canvas with viewBox dimensions
const canvas = document.createElement("canvas");
const scale = 2; // For better resolution
canvas.width = viewBoxWidth * scale;
canvas.height = viewBoxHeight * scale;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Failed to get canvas context");
// Convert SVG to data URL
const svgData = new XMLSerializer().serializeToString(svgElement);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const svgUrl = URL.createObjectURL(svgBlob);
// Create and load image
const img = new Image();
img.src = svgUrl;
await new Promise((resolve, reject) => {
img.onload = () => {
// Scale context for better resolution
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0, viewBoxWidth, viewBoxHeight);
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Failed to create blob"));
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `mermaid-diagram-${Date.now()}.png`;
a.click();
// Cleanup
URL.revokeObjectURL(url);
URL.revokeObjectURL(svgUrl);
resolve(true);
}, "image/png");
};
img.onerror = () => reject(new Error("Failed to load SVG"));
});
} catch (error) {
console.error("Error exporting diagram:", error);
setMermaidError("Failed to export diagram");
}
};
useEffect(() => {
if (elementRef.current) {
elementRef.current.removeAttribute("data-processed");
mermaid
.run({
nodes: [elementRef.current],
})
.then(() => {
setMermaidError(null);
})
.catch((error) => {
let errorMessage;
try {
errorMessage = typeof error === "string" ? JSON.parse(error) : error;
} catch (e) {
errorMessage = error?.toString() || "Unknown error";
}
console.log("Mermaid error message:", errorMessage);
if (errorMessage.str !== "element is null") {
setMermaidError(
"Something went wrong while rendering the diagram. Please try again later or downvote the message if the issue persists.",
);
} else {
setMermaidError(null);
}
});
}
}, [chart]);
return (
<div>
{mermaidError ? (
<div className="flex items-center gap-2 bg-red-100 border border-red-500 rounded-md p-3 mt-3 text-red-900 text-sm">
<Info className="w-12 h-12" />
<span>Error rendering diagram: {mermaidError}</span>
</div>
) : (
<div
id={mermaidId}
ref={elementRef}
className="mermaid"
style={{
width: "auto",
height: "auto",
boxSizing: "border-box",
overflow: "auto",
}}
>
{chart}
</div>
)}
{!mermaidError && (
<Button onClick={handleExport} variant={"secondary"} className="mt-3">
<Download className="w-5 h-5" />
Export as PNG
</Button>
)}
</div>
);
};
export default Mermaid;

View File

@@ -173,8 +173,10 @@ export default function Search() {
const [searchResultsLoading, setSearchResultsLoading] = useState(false);
const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null);
const [exampleQuery, setExampleQuery] = useState("");
const [fileSuggestions, setFileSuggestions] = useState<string[]>([]);
const [allFiles, setAllFiles] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isMobileWidth = useIsMobileWidth();
useEffect(() => {
@@ -183,8 +185,68 @@ export default function Search() {
Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)
],
);
// Load all files once on page load
fetch('/api/content/computer', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
setAllFiles(data);
})
.catch(error => {
console.error('Error loading files:', error);
});
}, []);
function getFileSuggestions(query: string) {
const fileFilterMatch = query.match(/file:([^"\s]*|"[^"]*")?/);
if (!fileFilterMatch) {
setFileSuggestions([]);
setShowSuggestions(false);
return;
}
const filePrefix = fileFilterMatch[1]?.replace(/^"|"$/g, '').trim() || '';
const filteredSuggestions = allFiles
.filter(file => file.toLowerCase().includes(filePrefix.toLowerCase()))
.sort()
.slice(0, 10);
setFileSuggestions(filteredSuggestions);
setShowSuggestions(true);
}
function handleSearchInputChange(value: string) {
setSearchQuery(value);
// Clear previous search timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
// Get file suggestions immediately
getFileSuggestions(value);
// Debounce search
if (value.trim()) {
searchTimeoutRef.current = setTimeout(() => {
search();
}, 750);
}
}
function applySuggestion(suggestion: string) {
// Replace the file: filter with the selected suggestion
const newQuery = searchQuery.replace(/file:([^"\s]*|"[^"]*")?/, `file:"${suggestion}"`);
setSearchQuery(newQuery);
setShowSuggestions(false);
search();
}
function search() {
if (searchResultsLoading || !searchQuery.trim()) return;
@@ -205,30 +267,6 @@ export default function Search() {
});
}
useEffect(() => {
if (!searchQuery.trim()) {
return;
}
setFocusSearchResult(null);
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (searchQuery.trim()) {
searchTimeoutRef.current = setTimeout(() => {
search();
}, 750); // 1000 milliseconds = 1 second
}
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, [searchQuery]);
return (
<SidebarProvider>
<AppSidebar conversationId={""} />
@@ -249,14 +287,38 @@ export default function Search() {
<div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8">
<div className="p-4 md:w-3/4 sm:w-full mx-auto">
<div className="flex justify-between items-center border-2 border-muted p-1 gap-1 rounded-lg">
<Input
autoFocus={true}
className="border-none pl-4"
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onKeyDown={(e) => e.key === "Enter" && search()}
type="search"
placeholder="Search Documents"
/>
<div className="relative flex-1">
<Input
autoFocus={true}
className="border-none pl-4"
onChange={(e) => handleSearchInputChange(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (showSuggestions && fileSuggestions.length > 0) {
applySuggestion(fileSuggestions[0]);
} else {
search();
}
}
}}
type="search"
placeholder="Search Documents (type 'file:' for file suggestions)"
value={searchQuery}
/>
{showSuggestions && fileSuggestions.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-background border rounded-md shadow-lg">
{fileSuggestions.map((suggestion, index) => (
<div
key={index}
className="px-4 py-2 hover:bg-muted cursor-pointer"
onClick={() => applySuggestion(suggestion)}
>
{suggestion}
</div>
))}
</div>
)}
</div>
<button
className="px-2 gap-2 inline-flex items-center rounded border-l border-gray-300 hover:text-gray-500"
onClick={() => search()}

View File

@@ -1,6 +1,6 @@
{
"name": "khoj-ai",
"version": "1.33.2",
"version": "1.34.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -53,12 +53,13 @@
"eslint-config-next": "14.2.3",
"input-otp": "^1.2.4",
"intl-tel-input": "^23.8.1",
"katex": "^0.16.10",
"katex": "^0.16.21",
"libphonenumber-js": "^1.11.4",
"lucide-react": "^0.468.0",
"markdown-it": "^14.1.0",
"markdown-it-highlightjs": "^4.1.0",
"next": "14.2.15",
"mermaid": "^11.4.1",
"next": "14.2.21",
"nodemon": "^3.1.3",
"postcss": "^8.4.38",
"react": "^18",

File diff suppressed because it is too large Load Diff

View File

@@ -234,7 +234,7 @@ def configure_server(
if ConversationAdapters.has_valid_ai_model_api():
ai_model_api = ConversationAdapters.get_ai_model_api()
state.openai_client = openai.OpenAI(api_key=ai_model_api.api_key)
state.openai_client = openai.OpenAI(api_key=ai_model_api.api_key, base_url=ai_model_api.api_base_url)
# Initialize Search Models from Config and initialize content
try:
@@ -249,6 +249,7 @@ def configure_server(
model.bi_encoder,
model.embeddings_inference_endpoint,
model.embeddings_inference_endpoint_api_key,
model.embeddings_inference_endpoint_type,
query_encode_kwargs=model.bi_encoder_query_encode_config,
docs_encode_kwargs=model.bi_encoder_docs_encode_config,
model_kwargs=model.bi_encoder_model_config,

View File

@@ -1288,7 +1288,7 @@ class ConversationAdapters:
@staticmethod
async def get_speech_to_text_config():
return await SpeechToTextModelOptions.objects.filter().afirst()
return await SpeechToTextModelOptions.objects.filter().prefetch_related("ai_model_api").afirst()
@staticmethod
@arequire_valid_user

View File

@@ -1,6 +1,7 @@
import csv
import json
from datetime import datetime, timedelta
from urllib.parse import quote
from apscheduler.job import Job
from django.contrib import admin, messages
@@ -154,8 +155,9 @@ class KhojUserAdmin(UserAdmin, unfold_admin.ModelAdmin):
for user in queryset:
if user.email:
host = request.get_host()
unique_id = user.email_verification_code
login_url = f"{host}/auth/magic?code={unique_id}&email={user.email}"
otp = quote(user.email_verification_code)
encoded_email = quote(user.email)
login_url = f"{host}/auth/magic?code={otp}&email={encoded_email}"
messages.info(request, f"Email login URL for {user.email}: {login_url}")
get_email_login_url.short_description = "Get email login URL" # type: ignore

View File

@@ -3,11 +3,11 @@ from typing import List
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import Count, Q
from django.db.models import Q
from tqdm import tqdm
from khoj.database.adapters import get_default_search_model
from khoj.database.models import Agent, Entry, KhojUser, SearchModelConfig
from khoj.database.models import Entry, SearchModelConfig
from khoj.processor.embeddings import EmbeddingsModel
logging.basicConfig(level=logging.INFO)
@@ -74,6 +74,7 @@ class Command(BaseCommand):
model.bi_encoder,
model.embeddings_inference_endpoint,
model.embeddings_inference_endpoint_api_key,
model.embeddings_inference_endpoint_type,
query_encode_kwargs=model.bi_encoder_query_encode_config,
docs_encode_kwargs=model.bi_encoder_docs_encode_config,
model_kwargs=model.bi_encoder_model_config,

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.0.10 on 2025-01-08 15:09
from django.db import migrations, models
def set_endpoint_type(apps, schema_editor):
SearchModelConfig = apps.get_model("database", "SearchModelConfig")
SearchModelConfig.objects.filter(embeddings_inference_endpoint__isnull=False).exclude(
embeddings_inference_endpoint=""
).update(embeddings_inference_endpoint_type="huggingface")
class Migration(migrations.Migration):
dependencies = [
("database", "0078_khojuser_email_verification_code_expiry"),
]
operations = [
migrations.AddField(
model_name="searchmodelconfig",
name="embeddings_inference_endpoint_type",
field=models.CharField(
choices=[("huggingface", "Huggingface"), ("openai", "Openai"), ("local", "Local")],
default="local",
max_length=200,
),
),
migrations.RunPython(set_endpoint_type, reverse_code=migrations.RunPython.noop),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.0.10 on 2025-01-15 11:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("database", "0079_searchmodelconfig_embeddings_inference_endpoint_type"),
]
operations = [
migrations.AddField(
model_name="speechtotextmodeloptions",
name="ai_model_api",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="database.aimodelapi",
),
),
]

View File

@@ -109,6 +109,7 @@ class ChatMessage(PydanticBaseModel):
images: Optional[List[str]] = None
queryFiles: Optional[List[Dict]] = None
excalidrawDiagram: Optional[List[Dict]] = None
mermaidjsDiagram: str = None
by: str
turnId: Optional[str] = None
intent: Optional[Intent] = None
@@ -481,6 +482,11 @@ class SearchModelConfig(DbBaseModel):
class ModelType(models.TextChoices):
TEXT = "text"
class ApiType(models.TextChoices):
HUGGINGFACE = "huggingface"
OPENAI = "openai"
LOCAL = "local"
# This is the model name exposed to users on their settings page
name = models.CharField(max_length=200, default="default")
# Type of content the model can generate embeddings for
@@ -501,6 +507,10 @@ class SearchModelConfig(DbBaseModel):
embeddings_inference_endpoint = models.CharField(max_length=200, default=None, null=True, blank=True)
# Inference server API Key to use for embeddings inference. Bi-encoder model should be hosted on this server
embeddings_inference_endpoint_api_key = models.CharField(max_length=200, default=None, null=True, blank=True)
# Inference server API type to use for embeddings inference.
embeddings_inference_endpoint_type = models.CharField(
max_length=200, choices=ApiType.choices, default=ApiType.LOCAL
)
# Inference server API endpoint to use for embeddings inference. Cross-encoder model should be hosted on this server
cross_encoder_inference_endpoint = models.CharField(max_length=200, default=None, null=True, blank=True)
# Inference server API Key to use for embeddings inference. Cross-encoder model should be hosted on this server
@@ -557,6 +567,7 @@ class SpeechToTextModelOptions(DbBaseModel):
model_name = models.CharField(max_length=200, default="base")
model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE)
ai_model_api = models.ForeignKey(AiModelApi, on_delete=models.CASCADE, default=None, null=True, blank=True)
def __str__(self):
return f"{self.model_name} - {self.model_type}"

View File

@@ -194,7 +194,7 @@ Limit your response to 3 sentences max. Be succinct, clear, and informative.
## Diagram Generation
## --
improve_diagram_description_prompt = PromptTemplate.from_template(
improve_excalidraw_diagram_description_prompt = PromptTemplate.from_template(
"""
You are an architect working with a novice digital artist using a diagramming software.
{personality_context}
@@ -338,6 +338,123 @@ Diagram Description: {query}
""".strip()
)
improve_mermaid_js_diagram_description_prompt = PromptTemplate.from_template(
"""
You are a senior architect working with an illustrator using a diagramming software.
{personality_context}
Given a particular request, you need to translate it to to a detailed description that the illustrator can use to create a diagram.
You can use the following diagram types in your instructions:
- Flowchart
- Sequence Diagram
- Gantt Chart (only for time-based queries after 0 AD)
- State Diagram
- Pie Chart
Use these primitives to describe what sort of diagram the drawer should create in natural language, not special syntax. We must recreate the diagram every time, so include all relevant prior information in your description.
- Describe the layout, components, and connections.
- Use simple, concise language.
Today's Date: {current_date}
User's Location: {location}
User's Notes:
{references}
Online References:
{online_results}
Conversation Log:
{chat_history}
Query: {query}
Enhanced Description:
""".strip()
)
mermaid_js_diagram_generation_prompt = PromptTemplate.from_template(
"""
You are a designer with the ability to describe diagrams to compose in professional, fine detail. You dive into the details and make labels, connections, and shapes to represent complex systems.
{personality_context}
----Goals----
You need to create a declarative description of the diagram and relevant components, using the Mermaid.js syntax.
You can choose from the following diagram types:
- Flowchart
- Sequence Diagram
- State Diagram
- Gantt Chart
- Pie Chart
----Examples----
---
title: Node
---
flowchart LR
id["This is the start"] --> id2["This is the end"]
sequenceDiagram
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
Alice-)John: See you later!
stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in Another :2014-01-12, 12d
another task :24d
pie title Pets adopted by volunteers
"Dogs" : 10
"Cats" : 30
"Rats" : 60
flowchart TB
subgraph "Group 1"
a1["Start Node"] --> a2["End Node"]
end
subgraph "Group 2"
b1["Process 1"] --> b2["Process 2"]
end
subgraph "Group 3"
c1["Input"] --> c2["Output"]
end
a["Group 1"] --> b["Group 2"]
c["Group 3"] --> d["Group 2"]
----Process----
Create your diagram with great composition and intuitiveness from the provided context and user prompt below.
- You may use subgraphs to group elements together. Each subgraph must have a title.
- **You must wrap ALL entity and node labels in double quotes**, example: "My Node Label"
- **All nodes MUST use the id["label"] format**. For example: node1["My Node Label"]
- Custom style are not permitted. Default styles only.
- JUST provide the diagram, no additional text or context. Say nothing else in your response except the diagram.
- Keep diagrams simple - maximum 15 nodes
- Every node inside a subgraph MUST use square bracket notation: id["label"]
output: {query}
""".strip()
)
failed_diagram_generation = PromptTemplate.from_template(
"""
You attempted to programmatically generate a diagram but failed due to a system issue. You are normally able to generate diagrams, but you encountered a system issue this time.

View File

@@ -62,7 +62,7 @@ model_to_prompt_size = {
"claude-3-5-sonnet-20241022": 60000,
"claude-3-5-haiku-20241022": 60000,
# Offline Models
"Qwen/Qwen2.5-14B-Instruct-GGUF": 20000,
"bartowski/Qwen2.5-14B-Instruct-GGUF": 20000,
"bartowski/Meta-Llama-3.1-8B-Instruct-GGUF": 20000,
"bartowski/Llama-3.2-3B-Instruct-GGUF": 20000,
"bartowski/gemma-2-9b-it-GGUF": 6000,
@@ -266,7 +266,7 @@ def save_to_conversation_log(
raw_query_files: List[FileAttachment] = [],
generated_images: List[str] = [],
raw_generated_files: List[FileAttachment] = [],
generated_excalidraw_diagram: str = None,
generated_mermaidjs_diagram: str = None,
train_of_thought: List[Any] = [],
tracer: Dict[str, Any] = {},
):
@@ -290,8 +290,8 @@ def save_to_conversation_log(
"queryFiles": [file.model_dump(mode="json") for file in raw_generated_files],
}
if generated_excalidraw_diagram:
khoj_message_metadata["excalidrawDiagram"] = generated_excalidraw_diagram
if generated_mermaidjs_diagram:
khoj_message_metadata["mermaidjsDiagram"] = generated_mermaidjs_diagram
updated_conversation = message_to_log(
user_message=q,
@@ -441,7 +441,7 @@ def generate_chatml_messages_with_context(
"query": chat.get("intent", {}).get("inferred-queries", [user_message])[0],
}
if not is_none_or_empty(chat.get("excalidrawDiagram")) and role == "assistant":
if not is_none_or_empty(chat.get("mermaidjsDiagram")) and role == "assistant":
generated_assets["diagram"] = {
"query": chat.get("intent", {}).get("inferred-queries", [user_message])[0],
}
@@ -593,6 +593,11 @@ def clean_json(response: str):
return response.strip().replace("\n", "").removeprefix("```json").removesuffix("```")
def clean_mermaidjs(response: str):
"""Remove any markdown mermaidjs codeblock and newline formatting if present. Useful for non schema enforceable models"""
return response.strip().removeprefix("```mermaid").removesuffix("```")
def clean_code_python(code: str):
"""Remove any markdown codeblock and newline formatting if present. Useful for non schema enforceable models"""
return code.strip().removeprefix("```python").removesuffix("```")

View File

@@ -1,6 +1,8 @@
import logging
from typing import List
from urllib.parse import urlparse
import openai
import requests
import tqdm
from sentence_transformers import CrossEncoder, SentenceTransformer
@@ -13,7 +15,14 @@ from tenacity import (
)
from torch import nn
from khoj.utils.helpers import fix_json_dict, get_device, merge_dicts, timer
from khoj.database.models import SearchModelConfig
from khoj.utils.helpers import (
fix_json_dict,
get_device,
get_openai_client,
merge_dicts,
timer,
)
from khoj.utils.rawconfig import SearchResponse
logger = logging.getLogger(__name__)
@@ -25,6 +34,7 @@ class EmbeddingsModel:
model_name: str = "thenlper/gte-small",
embeddings_inference_endpoint: str = None,
embeddings_inference_endpoint_api_key: str = None,
embeddings_inference_endpoint_type=SearchModelConfig.ApiType.LOCAL,
query_encode_kwargs: dict = {},
docs_encode_kwargs: dict = {},
model_kwargs: dict = {},
@@ -37,15 +47,16 @@ class EmbeddingsModel:
self.model_name = model_name
self.inference_endpoint = embeddings_inference_endpoint
self.api_key = embeddings_inference_endpoint_api_key
with timer(f"Loaded embedding model {self.model_name}", logger):
self.embeddings_model = SentenceTransformer(self.model_name, **self.model_kwargs)
def inference_server_enabled(self) -> bool:
return self.api_key is not None and self.inference_endpoint is not None
self.inference_endpoint_type = embeddings_inference_endpoint_type
if self.inference_endpoint_type == SearchModelConfig.ApiType.LOCAL:
with timer(f"Loaded embedding model {self.model_name}", logger):
self.embeddings_model = SentenceTransformer(self.model_name, **self.model_kwargs)
def embed_query(self, query):
if self.inference_server_enabled():
return self.embed_with_api([query])[0]
if self.inference_endpoint_type == SearchModelConfig.ApiType.HUGGINGFACE:
return self.embed_with_hf([query])[0]
elif self.inference_endpoint_type == SearchModelConfig.ApiType.OPENAI:
return self.embed_with_openai([query])[0]
return self.embeddings_model.encode([query], **self.query_encode_kwargs)[0]
@retry(
@@ -54,7 +65,7 @@ class EmbeddingsModel:
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.DEBUG),
)
def embed_with_api(self, docs):
def embed_with_hf(self, docs):
payload = {"inputs": docs}
headers = {
"Authorization": f"Bearer {self.api_key}",
@@ -71,23 +82,38 @@ class EmbeddingsModel:
raise e
return response.json()["embeddings"]
@retry(
retry=retry_if_exception_type(requests.exceptions.HTTPError),
wait=wait_random_exponential(multiplier=1, max=10),
stop=stop_after_attempt(5),
before_sleep=before_sleep_log(logger, logging.DEBUG),
)
def embed_with_openai(self, docs):
client = get_openai_client(self.api_key, self.inference_endpoint)
response = client.embeddings.create(input=docs, model=self.model_name, encoding_format="float")
return [item.embedding for item in response.data]
def embed_documents(self, docs):
if self.inference_server_enabled():
if "huggingface" not in self.inference_endpoint:
logger.warning(
f"Unsupported inference endpoint: {self.inference_endpoint}. Only HuggingFace supported. Generating embeddings on device instead."
)
return self.embeddings_model.encode(docs, **self.docs_encode_kwargs).tolist()
# break up the docs payload in chunks of 1000 to avoid hitting rate limits
embeddings = []
with tqdm.tqdm(total=len(docs)) as pbar:
for i in range(0, len(docs), 1000):
docs_to_embed = docs[i : i + 1000]
generated_embeddings = self.embed_with_api(docs_to_embed)
embeddings += generated_embeddings
pbar.update(1000)
return embeddings
return self.embeddings_model.encode(docs, **self.docs_encode_kwargs).tolist() if docs else []
if self.inference_endpoint_type == SearchModelConfig.ApiType.LOCAL:
return self.embeddings_model.encode(docs, **self.docs_encode_kwargs).tolist() if docs else []
elif self.inference_endpoint_type == SearchModelConfig.ApiType.HUGGINGFACE:
embed_with_api = self.embed_with_hf
elif self.inference_endpoint_type == SearchModelConfig.ApiType.OPENAI:
embed_with_api = self.embed_with_openai
else:
logger.warning(
f"Unsupported inference endpoint: {self.inference_endpoint_type}. Generating embeddings locally instead."
)
return self.embeddings_model.encode(docs, **self.docs_encode_kwargs).tolist()
# break up the docs payload in chunks of 1000 to avoid hitting rate limits
embeddings = []
with tqdm.tqdm(total=len(docs)) as pbar:
for i in range(0, len(docs), 1000):
docs_to_embed = docs[i : i + 1000]
generated_embeddings = embed_with_api(docs_to_embed)
embeddings += generated_embeddings
pbar.update(1000)
return embeddings
class CrossEncoderModel:

View File

@@ -111,7 +111,7 @@ async def text_to_image(
image_url = upload_image(webp_image_bytes, user.uuid)
if not image_url:
image = base64.b64encode(webp_image_bytes).decode("utf-8")
image = f"data:image/webp;base64,{base64.b64encode(webp_image_bytes).decode('utf-8')}"
yield image_url or image, status_code, image_prompt
@@ -119,25 +119,27 @@ async def text_to_image(
def generate_image_with_openai(
improved_image_prompt: str, text_to_image_config: TextToImageModelConfig, text2image_model: str
):
"Generate image using OpenAI API"
"Generate image using OpenAI (compatible) API"
# Get the API key from the user's configuration
# Get the API config from the user's configuration
api_key = None
if text_to_image_config.api_key:
api_key = text_to_image_config.api_key
openai_client = openai.OpenAI(api_key=api_key)
elif text_to_image_config.ai_model_api:
api_key = text_to_image_config.ai_model_api.api_key
api_base_url = text_to_image_config.ai_model_api.api_base_url
openai_client = openai.OpenAI(api_key=api_key, base_url=api_base_url)
elif state.openai_client:
api_key = state.openai_client.api_key
auth_header = {"Authorization": f"Bearer {api_key}"} if api_key else {}
openai_client = state.openai_client
# Generate image using OpenAI API
OPENAI_IMAGE_GEN_STYLE = "vivid"
response = state.openai_client.images.generate(
response = openai_client.images.generate(
prompt=improved_image_prompt,
model=text2image_model,
style=OPENAI_IMAGE_GEN_STYLE,
response_format="b64_json",
extra_headers=auth_header,
)
# Extract the base64 image from the response

View File

@@ -30,6 +30,8 @@ from khoj.utils.rawconfig import LocationData
logger = logging.getLogger(__name__)
GOOGLE_SEARCH_API_KEY = os.getenv("GOOGLE_SEARCH_API_KEY")
GOOGLE_SEARCH_ENGINE_ID = os.getenv("GOOGLE_SEARCH_ENGINE_ID")
SERPER_DEV_API_KEY = os.getenv("SERPER_DEV_API_KEY")
SERPER_DEV_URL = "https://google.serper.dev/search"
@@ -96,19 +98,25 @@ async def search_online(
yield response_dict
return
logger.info(f"🌐 Searching the Internet for {subqueries}")
if GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_ENGINE_ID:
search_engine = "Google"
search_func = search_with_google
elif SERPER_DEV_API_KEY:
search_engine = "Serper"
search_func = search_with_serper
elif JINA_API_KEY:
search_engine = "Jina"
search_func = search_with_jina
else:
search_engine = "Searxng"
search_func = search_with_searxng
logger.info(f"🌐 Searching the Internet with {search_engine} for {subqueries}")
if send_status_func:
subqueries_str = "\n- " + "\n- ".join(subqueries)
async for event in send_status_func(f"**Searching the Internet for**: {subqueries_str}"):
yield {ChatEvent.STATUS: event}
if SERPER_DEV_API_KEY:
search_func = search_with_serper
elif JINA_API_KEY:
search_func = search_with_jina
else:
search_func = search_with_searxng
with timer(f"Internet searches for {subqueries} took", logger):
search_tasks = [search_func(subquery, location) for subquery in subqueries]
search_results = await asyncio.gather(*search_tasks)
@@ -195,6 +203,56 @@ async def search_with_searxng(query: str, location: LocationData) -> Tuple[str,
return query, {}
async def search_with_google(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]:
country_code = location.country_code.lower() if location and location.country_code else "us"
base_url = "https://www.googleapis.com/customsearch/v1"
params = {
"key": GOOGLE_SEARCH_API_KEY,
"cx": GOOGLE_SEARCH_ENGINE_ID,
"q": query,
"cr": f"country{country_code.upper()}", # Country restrict parameter
"gl": country_code, # Geolocation parameter
}
async with aiohttp.ClientSession() as session:
async with session.get(base_url, params=params) as response:
if response.status != 200:
logger.error(await response.text())
return query, {}
json_response = await response.json()
# Transform Google's response format to match Serper's format
organic_results = []
if "items" in json_response:
organic_results = [
{
"title": item.get("title", ""),
"link": item.get("link", ""),
"snippet": item.get("snippet", ""),
"content": None, # Google Search API doesn't provide full content
}
for item in json_response["items"]
]
# Format knowledge graph if available
knowledge_graph = {}
if "knowledge_graph" in json_response:
kg = json_response["knowledge_graph"]
knowledge_graph = {
"title": kg.get("name", ""),
"description": kg.get("description", ""),
"type": kg.get("type", ""),
}
extracted_search_result: Dict[str, Any] = {"organic": organic_results}
if knowledge_graph:
extracted_search_result["knowledgeGraph"] = knowledge_graph
return query, extracted_search_result
async def search_with_serper(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]:
country_code = location.country_code.lower() if location and location.country_code else "us"
payload = json.dumps({"q": query, "gl": country_code})

View File

@@ -9,6 +9,7 @@ import uuid
from typing import Any, Callable, List, Optional, Set, Union
import cron_descriptor
import openai
import pytz
from apscheduler.job import Job
from apscheduler.triggers.cron import CronTrigger
@@ -264,12 +265,21 @@ async def transcribe(
if not speech_to_text_config:
# If the user has not configured a speech to text model, return an unsupported on server error
status_code = 501
elif state.openai_client and speech_to_text_config.model_type == SpeechToTextModelOptions.ModelType.OPENAI:
speech2text_model = speech_to_text_config.model_name
user_message = await transcribe_audio(audio_file, speech2text_model, client=state.openai_client)
elif speech_to_text_config.model_type == SpeechToTextModelOptions.ModelType.OFFLINE:
speech2text_model = speech_to_text_config.model_name
user_message = await transcribe_audio_offline(audio_filename, speech2text_model)
elif speech_to_text_config.model_type == SpeechToTextModelOptions.ModelType.OPENAI:
speech2text_model = speech_to_text_config.model_name
if speech_to_text_config.ai_model_api:
api_key = speech_to_text_config.ai_model_api.api_key
api_base_url = speech_to_text_config.ai_model_api.api_base_url
openai_client = openai.OpenAI(api_key=api_key, base_url=api_base_url)
elif state.openai_client:
openai_client = state.openai_client
if openai_client:
user_message = await transcribe_audio(audio_file, speech2text_model, client=openai_client)
else:
status_code = 501
finally:
# Close and Delete the temporary audio file
audio_file.close()
@@ -392,7 +402,8 @@ async def extract_references_and_questions(
filters_in_query += " ".join([f'file:"{filter}"' for filter in conversation.file_filters])
using_offline_chat = False
logger.debug(f"Filters in query: {filters_in_query}")
if is_none_or_empty(filters_in_query):
logger.debug(f"Filters in query: {filters_in_query}")
personality_context = prompts.personality_context.format(personality=agent.personality) if agent else ""

View File

@@ -51,7 +51,7 @@ from khoj.routers.helpers import (
construct_automation_created_message,
create_automation,
gather_raw_query_files,
generate_excalidraw_diagram,
generate_mermaidjs_diagram,
generate_summary_from_files,
get_conversation_command,
is_query_empty,
@@ -781,20 +781,25 @@ async def chat(
generated_images: List[str] = []
generated_files: List[FileAttachment] = []
generated_excalidraw_diagram: str = None
generated_mermaidjs_diagram: str = None
program_execution_context: List[str] = []
if conversation_commands == [ConversationCommand.Default]:
chosen_io = await aget_data_sources_and_output_format(
q,
meta_log,
is_automated_task,
user=user,
query_images=uploaded_images,
agent=agent,
query_files=attached_file_context,
tracer=tracer,
)
try:
chosen_io = await aget_data_sources_and_output_format(
q,
meta_log,
is_automated_task,
user=user,
query_images=uploaded_images,
agent=agent,
query_files=attached_file_context,
tracer=tracer,
)
except ValueError as e:
logger.error(f"Error getting data sources and output format: {e}. Falling back to default.")
conversation_commands = [ConversationCommand.General]
conversation_commands = chosen_io.get("sources") + [chosen_io.get("output")]
# If we're doing research, we don't want to do anything else
@@ -1156,7 +1161,7 @@ async def chat(
inferred_queries = []
diagram_description = ""
async for result in generate_excalidraw_diagram(
async for result in generate_mermaidjs_diagram(
q=defiltered_query,
conversation_history=meta_log,
location_data=location,
@@ -1172,12 +1177,12 @@ async def chat(
if isinstance(result, dict) and ChatEvent.STATUS in result:
yield result[ChatEvent.STATUS]
else:
better_diagram_description_prompt, excalidraw_diagram_description = result
if better_diagram_description_prompt and excalidraw_diagram_description:
better_diagram_description_prompt, mermaidjs_diagram_description = result
if better_diagram_description_prompt and mermaidjs_diagram_description:
inferred_queries.append(better_diagram_description_prompt)
diagram_description = excalidraw_diagram_description
diagram_description = mermaidjs_diagram_description
generated_excalidraw_diagram = diagram_description
generated_mermaidjs_diagram = diagram_description
generated_asset_results["diagrams"] = {
"query": better_diagram_description_prompt,
@@ -1186,7 +1191,7 @@ async def chat(
async for result in send_event(
ChatEvent.GENERATED_ASSETS,
{
"excalidrawDiagram": excalidraw_diagram_description,
"mermaidjsDiagram": mermaidjs_diagram_description,
},
):
yield result
@@ -1226,7 +1231,7 @@ async def chat(
raw_query_files,
generated_images,
generated_files,
generated_excalidraw_diagram,
generated_mermaidjs_diagram,
program_execution_context,
generated_asset_results,
tracer,

View File

@@ -1,5 +1,6 @@
import logging
import os
from urllib.parse import quote
import markdown_it
import resend
@@ -29,7 +30,7 @@ def is_resend_enabled():
async def send_magic_link_email(email, unique_id, host):
sign_in_link = f"{host}auth/magic?code={unique_id}&email={email}"
sign_in_link = f"{host}auth/magic?code={quote(unique_id)}&email={quote(email)}"
if not is_resend_enabled():
logger.debug(f"Email sending disabled. Share this sign-in link with the user: {sign_in_link}")

View File

@@ -97,6 +97,7 @@ from khoj.processor.conversation.utils import (
ChatEvent,
ThreadedGenerator,
clean_json,
clean_mermaidjs,
construct_chat_history,
generate_chatml_messages_with_context,
save_to_conversation_log,
@@ -823,7 +824,7 @@ async def generate_better_diagram_description(
elif online_results[result].get("webpages"):
simplified_online_results[result] = online_results[result]["webpages"]
improve_diagram_description_prompt = prompts.improve_diagram_description_prompt.format(
improve_diagram_description_prompt = prompts.improve_excalidraw_diagram_description_prompt.format(
query=q,
chat_history=chat_history,
location=location,
@@ -887,6 +888,133 @@ async def generate_excalidraw_diagram_from_description(
return response
async def generate_mermaidjs_diagram(
q: str,
conversation_history: Dict[str, Any],
location_data: LocationData,
note_references: List[Dict[str, Any]],
online_results: Optional[dict] = None,
query_images: List[str] = None,
user: KhojUser = None,
agent: Agent = None,
send_status_func: Optional[Callable] = None,
query_files: str = None,
tracer: dict = {},
):
if send_status_func:
async for event in send_status_func("**Enhancing the Diagramming Prompt**"):
yield {ChatEvent.STATUS: event}
better_diagram_description_prompt = await generate_better_mermaidjs_diagram_description(
q=q,
conversation_history=conversation_history,
location_data=location_data,
note_references=note_references,
online_results=online_results,
query_images=query_images,
user=user,
agent=agent,
query_files=query_files,
tracer=tracer,
)
if send_status_func:
async for event in send_status_func(f"**Diagram to Create:**:\n{better_diagram_description_prompt}"):
yield {ChatEvent.STATUS: event}
mermaidjs_diagram_description = await generate_mermaidjs_diagram_from_description(
q=better_diagram_description_prompt,
user=user,
agent=agent,
tracer=tracer,
)
inferred_queries = f"Instruction: {better_diagram_description_prompt}"
yield inferred_queries, mermaidjs_diagram_description
async def generate_better_mermaidjs_diagram_description(
q: str,
conversation_history: Dict[str, Any],
location_data: LocationData,
note_references: List[Dict[str, Any]],
online_results: Optional[dict] = None,
query_images: List[str] = None,
user: KhojUser = None,
agent: Agent = None,
query_files: str = None,
tracer: dict = {},
) -> str:
"""
Generate a diagram description from the given query and context
"""
today_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d, %A")
personality_context = (
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
)
location = f"{location_data}" if location_data else "Unknown"
user_references = "\n\n".join([f"# {item['compiled']}" for item in note_references])
chat_history = construct_chat_history(conversation_history)
simplified_online_results = {}
if online_results:
for result in online_results:
if online_results[result].get("answerBox"):
simplified_online_results[result] = online_results[result]["answerBox"]
elif online_results[result].get("webpages"):
simplified_online_results[result] = online_results[result]["webpages"]
improve_diagram_description_prompt = prompts.improve_mermaid_js_diagram_description_prompt.format(
query=q,
chat_history=chat_history,
location=location,
current_date=today_date,
references=user_references,
online_results=simplified_online_results,
personality_context=personality_context,
)
with timer("Chat actor: Generate better Mermaid.js diagram description", logger):
response = await send_message_to_model_wrapper(
improve_diagram_description_prompt,
query_images=query_images,
user=user,
query_files=query_files,
tracer=tracer,
)
response = response.strip()
if response.startswith(('"', "'")) and response.endswith(('"', "'")):
response = response[1:-1]
return response
async def generate_mermaidjs_diagram_from_description(
q: str,
user: KhojUser = None,
agent: Agent = None,
tracer: dict = {},
) -> str:
personality_context = (
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
)
mermaidjs_diagram_generation = prompts.mermaid_js_diagram_generation_prompt.format(
personality_context=personality_context,
query=q,
)
with timer("Chat actor: Generate Mermaid.js diagram", logger):
raw_response = await send_message_to_model_wrapper(query=mermaidjs_diagram_generation, user=user, tracer=tracer)
return clean_mermaidjs(raw_response.strip())
async def generate_better_image_prompt(
q: str,
conversation_history: str,
@@ -1224,7 +1352,7 @@ def generate_chat_response(
raw_query_files: List[FileAttachment] = None,
generated_images: List[str] = None,
raw_generated_files: List[FileAttachment] = [],
generated_excalidraw_diagram: str = None,
generated_mermaidjs_diagram: str = None,
program_execution_context: List[str] = [],
generated_asset_results: Dict[str, Dict] = {},
tracer: dict = {},
@@ -1252,7 +1380,7 @@ def generate_chat_response(
raw_query_files=raw_query_files,
generated_images=generated_images,
raw_generated_files=raw_generated_files,
generated_excalidraw_diagram=generated_excalidraw_diagram,
generated_mermaidjs_diagram=generated_mermaidjs_diagram,
tracer=tracer,
)
@@ -1785,7 +1913,7 @@ def scheduled_chat(
raw_response = requests.post(url, headers=headers, json=json_payload, allow_redirects=False)
# Handle redirect manually if necessary
if raw_response.status_code in [301, 302]:
if raw_response.status_code in [301, 302, 308]:
redirect_url = raw_response.headers["Location"]
logger.info(f"Redirecting to {redirect_url}")
raw_response = requests.post(redirect_url, headers=headers, json=json_payload)
@@ -1967,7 +2095,7 @@ class MessageProcessor:
self.raw_response = ""
self.generated_images = []
self.generated_files = []
self.generated_excalidraw_diagram = []
self.generated_mermaidjs_diagram = []
def convert_message_chunk_to_json(self, raw_chunk: str) -> Dict[str, Any]:
if raw_chunk.startswith("{") and raw_chunk.endswith("}"):
@@ -2014,8 +2142,8 @@ class MessageProcessor:
self.generated_images = chunk_data[key]
elif key == "files":
self.generated_files = chunk_data[key]
elif key == "excalidrawDiagram":
self.generated_excalidraw_diagram = chunk_data[key]
elif key == "mermaidjsDiagram":
self.generated_mermaidjs_diagram = chunk_data[key]
def handle_json_response(self, json_data: Dict[str, str]) -> str | Dict[str, str]:
if "image" in json_data or "details" in json_data:
@@ -2052,7 +2180,7 @@ async def read_chat_stream(response_iterator: AsyncGenerator[str, None]) -> Dict
"usage": processor.usage,
"images": processor.generated_images,
"files": processor.generated_files,
"excalidrawDiagram": processor.generated_excalidraw_diagram,
"mermaidjsDiagram": processor.generated_mermaidjs_diagram,
}

View File

@@ -15,7 +15,7 @@ default_offline_chat_models = [
"bartowski/Llama-3.2-3B-Instruct-GGUF",
"bartowski/gemma-2-9b-it-GGUF",
"bartowski/gemma-2-2b-it-GGUF",
"Qwen/Qwen2.5-14B-Instruct-GGUF",
"bartowski/Qwen2.5-14B-Instruct-GGUF",
]
default_openai_chat_models = ["gpt-4o-mini", "gpt-4o"]
default_gemini_chat_models = ["gemini-1.5-flash", "gemini-1.5-pro"]

View File

@@ -355,7 +355,7 @@ command_descriptions = {
command_descriptions_for_agent = {
ConversationCommand.General: "Agent can use the agents knowledge base and general knowledge.",
ConversationCommand.Notes: "Agent can search the users knowledge base for information.",
ConversationCommand.Notes: "Agent can search the personal knowledge base for information, as well as its own.",
ConversationCommand.Online: "Agent can search the internet for information.",
ConversationCommand.Webpage: "Agent can read suggested web pages for information.",
ConversationCommand.Summarize: "Agent can read an entire document. Agents knowledge base must be a single document.",

View File

@@ -43,14 +43,14 @@ def initialization(interactive: bool = True):
"🗣️ Configure chat models available to your server. You can always update these at /server/admin using your admin account"
)
openai_api_base = os.getenv("OPENAI_API_BASE")
provider = "Ollama" if openai_api_base and openai_api_base.endswith(":11434/v1/") else "OpenAI"
openai_api_key = os.getenv("OPENAI_API_KEY", "placeholder" if openai_api_base else None)
openai_base_url = os.getenv("OPENAI_BASE_URL")
provider = "Ollama" if openai_base_url and openai_base_url.endswith(":11434/v1/") else "OpenAI"
openai_api_key = os.getenv("OPENAI_API_KEY", "placeholder" if openai_base_url else None)
default_chat_models = default_openai_chat_models
if openai_api_base:
if openai_base_url:
# Get available chat models from OpenAI compatible API
try:
openai_client = openai.OpenAI(api_key=openai_api_key, base_url=openai_api_base)
openai_client = openai.OpenAI(api_key=openai_api_key, base_url=openai_base_url)
default_chat_models = [model.id for model in openai_client.models.list()]
# Put the available default OpenAI models at the top
valid_default_models = [model for model in default_openai_chat_models if model in default_chat_models]
@@ -66,7 +66,7 @@ def initialization(interactive: bool = True):
ChatModel.ModelType.OPENAI,
default_chat_models,
default_api_key=openai_api_key,
api_base_url=openai_api_base,
api_base_url=openai_base_url,
vision_enabled=True,
is_offline=False,
interactive=interactive,
@@ -267,7 +267,7 @@ def initialization(interactive: bool = True):
)
# Remove models that are no longer available
existing_models.exclude(chat_model__in=available_models).delete()
existing_models.exclude(name__in=available_models).delete()
except Exception as e:
logger.warning(f"Failed to update models for {config.name}: {str(e)}")

View File

@@ -108,5 +108,6 @@
"1.32.2": "0.15.0",
"1.33.0": "0.15.0",
"1.33.1": "0.15.0",
"1.33.2": "0.15.0"
"1.33.2": "0.15.0",
"1.34.0": "0.15.0"
}