mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-05 21:29:11 +00:00
Merge branch 'master' into migrate-to-llama-cpp-for-offline-chat
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Saba Imran, Debanjum Singh Solanky <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
;; Saba Imran <saba@khoj.dev>
|
||||
;; Description: An AI copilot for your Second Brain
|
||||
;; Keywords: search, chat, org-mode, outlines, markdown, pdf, image
|
||||
;; Version: 1.7.0
|
||||
;; Version: 1.8.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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Khoj Inc.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
||||
@@ -39,5 +39,6 @@
|
||||
"1.6.0": "0.15.0",
|
||||
"1.6.1": "0.15.0",
|
||||
"1.6.2": "0.15.0",
|
||||
"1.7.0": "0.15.0"
|
||||
"1.7.0": "0.15.0",
|
||||
"1.8.0": "0.15.0"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.requests import HTTPConnection
|
||||
|
||||
from khoj.database.adapters import (
|
||||
AgentAdapters,
|
||||
ClientApplicationAdapters,
|
||||
ConversationAdapters,
|
||||
SubscriptionState,
|
||||
@@ -229,11 +230,16 @@ def configure_server(
|
||||
|
||||
state.SearchType = configure_search_types()
|
||||
state.search_models = configure_search(state.search_models, state.config.search_type)
|
||||
setup_default_agent()
|
||||
initialize_content(regenerate, search_type, init, user)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
def setup_default_agent():
|
||||
AgentAdapters.create_default_agent()
|
||||
|
||||
|
||||
def initialize_content(regenerate: bool, search_type: Optional[SearchType] = None, init=False, user: KhojUser = None):
|
||||
# Initialize Content from Config
|
||||
if state.search_models:
|
||||
@@ -262,6 +268,7 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non
|
||||
def configure_routes(app):
|
||||
# Import APIs here to setup search types before while configuring server
|
||||
from khoj.routers.api import api
|
||||
from khoj.routers.api_agents import api_agents
|
||||
from khoj.routers.api_chat import api_chat
|
||||
from khoj.routers.api_config import api_config
|
||||
from khoj.routers.indexer import indexer
|
||||
@@ -269,6 +276,7 @@ def configure_routes(app):
|
||||
|
||||
app.include_router(api, prefix="/api")
|
||||
app.include_router(api_chat, prefix="/api/chat")
|
||||
app.include_router(api_agents, prefix="/api/agents")
|
||||
app.include_router(api_config, prefix="/api/config")
|
||||
app.include_router(indexer, prefix="/api/v1/index")
|
||||
app.include_router(web_client)
|
||||
|
||||
@@ -16,6 +16,7 @@ from pgvector.django import CosineDistance
|
||||
from torch import Tensor
|
||||
|
||||
from khoj.database.models import (
|
||||
Agent,
|
||||
ChatModelOptions,
|
||||
ClientApplication,
|
||||
Conversation,
|
||||
@@ -37,6 +38,7 @@ from khoj.database.models import (
|
||||
UserRequests,
|
||||
UserSearchModelConfig,
|
||||
)
|
||||
from khoj.processor.conversation import prompts
|
||||
from khoj.search_filter.date_filter import DateFilter
|
||||
from khoj.search_filter.file_filter import FileFilter
|
||||
from khoj.search_filter.word_filter import WordFilter
|
||||
@@ -391,6 +393,80 @@ class ClientApplicationAdapters:
|
||||
return await ClientApplication.objects.filter(client_id=client_id, client_secret=client_secret).afirst()
|
||||
|
||||
|
||||
class AgentAdapters:
|
||||
DEFAULT_AGENT_NAME = "Khoj"
|
||||
DEFAULT_AGENT_AVATAR = "https://khoj-web-bucket.s3.amazonaws.com/lamp-128.png"
|
||||
DEFAULT_AGENT_SLUG = "khoj"
|
||||
|
||||
@staticmethod
|
||||
async def aget_agent_by_slug(agent_slug: str, user: KhojUser):
|
||||
return await Agent.objects.filter(
|
||||
(Q(slug__iexact=agent_slug.lower())) & (Q(public=True) | Q(creator=user))
|
||||
).afirst()
|
||||
|
||||
@staticmethod
|
||||
def get_agent_by_slug(slug: str, user: KhojUser = None):
|
||||
if user:
|
||||
return Agent.objects.filter((Q(slug__iexact=slug.lower())) & (Q(public=True) | Q(creator=user))).first()
|
||||
return Agent.objects.filter(slug__iexact=slug.lower(), public=True).first()
|
||||
|
||||
@staticmethod
|
||||
def get_all_accessible_agents(user: KhojUser = None):
|
||||
if user:
|
||||
return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct().order_by("created_at")
|
||||
return Agent.objects.filter(public=True).order_by("created_at")
|
||||
|
||||
@staticmethod
|
||||
async def aget_all_accessible_agents(user: KhojUser = None) -> List[Agent]:
|
||||
agents = await sync_to_async(AgentAdapters.get_all_accessible_agents)(user)
|
||||
return await sync_to_async(list)(agents)
|
||||
|
||||
@staticmethod
|
||||
def get_conversation_agent_by_id(agent_id: int):
|
||||
agent = Agent.objects.filter(id=agent_id).first()
|
||||
if agent == AgentAdapters.get_default_agent():
|
||||
# If the agent is set to the default agent, then return None and let the default application code be used
|
||||
return None
|
||||
return agent
|
||||
|
||||
@staticmethod
|
||||
def get_default_agent():
|
||||
return Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
|
||||
|
||||
@staticmethod
|
||||
def create_default_agent():
|
||||
default_conversation_config = ConversationAdapters.get_default_conversation_config()
|
||||
default_personality = prompts.personality.format(current_date="placeholder")
|
||||
|
||||
agent = Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
|
||||
|
||||
if agent:
|
||||
agent.personality = default_personality
|
||||
agent.chat_model = default_conversation_config
|
||||
agent.slug = AgentAdapters.DEFAULT_AGENT_SLUG
|
||||
agent.name = AgentAdapters.DEFAULT_AGENT_NAME
|
||||
agent.save()
|
||||
else:
|
||||
# The default agent is public and managed by the admin. It's handled a little differently than other agents.
|
||||
agent = Agent.objects.create(
|
||||
name=AgentAdapters.DEFAULT_AGENT_NAME,
|
||||
public=True,
|
||||
managed_by_admin=True,
|
||||
chat_model=default_conversation_config,
|
||||
personality=default_personality,
|
||||
tools=["*"],
|
||||
avatar=AgentAdapters.DEFAULT_AGENT_AVATAR,
|
||||
slug=AgentAdapters.DEFAULT_AGENT_SLUG,
|
||||
)
|
||||
Conversation.objects.filter(agent=None).update(agent=agent)
|
||||
|
||||
return agent
|
||||
|
||||
@staticmethod
|
||||
async def aget_default_agent():
|
||||
return await Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).afirst()
|
||||
|
||||
|
||||
class ConversationAdapters:
|
||||
@staticmethod
|
||||
def get_conversation_by_user(
|
||||
@@ -403,9 +479,10 @@ class ConversationAdapters:
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
agent = AgentAdapters.get_default_agent()
|
||||
conversation = (
|
||||
Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at").first()
|
||||
) or Conversation.objects.create(user=user, client=client_application)
|
||||
) or Conversation.objects.create(user=user, client=client_application, agent=agent)
|
||||
|
||||
return conversation
|
||||
|
||||
@@ -431,8 +508,16 @@ class ConversationAdapters:
|
||||
return Conversation.objects.filter(id=conversation_id).first()
|
||||
|
||||
@staticmethod
|
||||
async def acreate_conversation_session(user: KhojUser, client_application: ClientApplication = None):
|
||||
return await Conversation.objects.acreate(user=user, client=client_application)
|
||||
async def acreate_conversation_session(
|
||||
user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None
|
||||
):
|
||||
if agent_slug:
|
||||
agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=400, detail="No such agent currently exists.")
|
||||
return await Conversation.objects.acreate(user=user, client=client_application, agent=agent)
|
||||
agent = await AgentAdapters.aget_default_agent()
|
||||
return await Conversation.objects.acreate(user=user, client=client_application, agent=agent)
|
||||
|
||||
@staticmethod
|
||||
async def aget_conversation_by_user(
|
||||
@@ -443,9 +528,14 @@ class ConversationAdapters:
|
||||
elif title:
|
||||
return await Conversation.objects.filter(user=user, client=client_application, title=title).afirst()
|
||||
else:
|
||||
return await (
|
||||
Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at").afirst()
|
||||
) or await Conversation.objects.acreate(user=user, client=client_application)
|
||||
conversation = Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at")
|
||||
|
||||
if await conversation.aexists():
|
||||
return await conversation.prefetch_related("agent").afirst()
|
||||
|
||||
return await (
|
||||
Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at").afirst()
|
||||
) or await Conversation.objects.acreate(user=user, client=client_application)
|
||||
|
||||
@staticmethod
|
||||
async def adelete_conversation_by_user(
|
||||
@@ -603,9 +693,14 @@ class ConversationAdapters:
|
||||
return random.sample(all_questions, max_results)
|
||||
|
||||
@staticmethod
|
||||
def get_valid_conversation_config(user: KhojUser):
|
||||
def get_valid_conversation_config(user: KhojUser, conversation: Conversation):
|
||||
offline_chat_config = ConversationAdapters.get_offline_chat_conversation_config()
|
||||
conversation_config = ConversationAdapters.get_conversation_config(user)
|
||||
|
||||
if conversation.agent and conversation.agent.chat_model:
|
||||
conversation_config = conversation.agent.chat_model
|
||||
else:
|
||||
conversation_config = ConversationAdapters.get_conversation_config(user)
|
||||
|
||||
if conversation_config is None:
|
||||
conversation_config = ConversationAdapters.get_default_conversation_config()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib.auth.admin import UserAdmin
|
||||
from django.http import HttpResponse
|
||||
|
||||
from khoj.database.models import (
|
||||
Agent,
|
||||
ChatModelOptions,
|
||||
ClientApplication,
|
||||
Conversation,
|
||||
@@ -50,6 +51,7 @@ admin.site.register(ReflectiveQuestion)
|
||||
admin.site.register(UserSearchModelConfig)
|
||||
admin.site.register(TextToImageModelConfig)
|
||||
admin.site.register(ClientApplication)
|
||||
admin.site.register(Agent)
|
||||
|
||||
|
||||
@admin.register(Entry)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-13 07:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0030_conversation_slug_and_title"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Agent",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("tuning", models.TextField()),
|
||||
("avatar", models.URLField(blank=True, default=None, max_length=400, null=True)),
|
||||
("tools", models.JSONField(default=list)),
|
||||
("public", models.BooleanField(default=False)),
|
||||
("managed_by_admin", models.BooleanField(default=False)),
|
||||
("slug", models.CharField(max_length=200)),
|
||||
(
|
||||
"chat_model",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="database.chatmodeloptions"),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="conversation",
|
||||
name="agent",
|
||||
field=models.ForeignKey(
|
||||
blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to="database.agent"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -3,6 +3,18 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_default_locale(apps, schema_editor):
|
||||
return
|
||||
|
||||
|
||||
def reverse_set_default_locale(apps, schema_editor):
|
||||
GoogleUser = apps.get_model("database", "GoogleUser")
|
||||
for user in GoogleUser.objects.all():
|
||||
if not user.locale:
|
||||
user.locale = "en"
|
||||
user.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0030_conversation_slug_and_title"),
|
||||
@@ -14,4 +26,5 @@ class Migration(migrations.Migration):
|
||||
name="locale",
|
||||
field=models.CharField(blank=True, default=None, max_length=200, null=True),
|
||||
),
|
||||
migrations.RunPython(set_default_locale, reverse_set_default_locale),
|
||||
]
|
||||
|
||||
14
src/khoj/database/migrations/0032_merge_20240322_0427.py
Normal file
14
src/khoj/database/migrations/0032_merge_20240322_0427.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-22 04:27
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0031_agent_conversation_agent"),
|
||||
("database", "0031_alter_googleuser_locale"),
|
||||
]
|
||||
|
||||
operations: List[str] = []
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-23 16:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0032_merge_20240322_0427"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="agent",
|
||||
old_name="tuning",
|
||||
new_name="personality",
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,11 @@
|
||||
import uuid
|
||||
from random import choice
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from pgvector.django import VectorField
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
@@ -69,6 +73,52 @@ class Subscription(BaseModel):
|
||||
renewal_date = models.DateTimeField(null=True, default=None, blank=True)
|
||||
|
||||
|
||||
class ChatModelOptions(BaseModel):
|
||||
class ModelType(models.TextChoices):
|
||||
OPENAI = "openai"
|
||||
OFFLINE = "offline"
|
||||
|
||||
max_prompt_size = models.IntegerField(default=None, null=True, blank=True)
|
||||
tokenizer = models.CharField(max_length=200, default=None, null=True, blank=True)
|
||||
chat_model = models.CharField(max_length=200, default="NousResearch/Hermes-2-Pro-Mistral-7B-GGUF")
|
||||
model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE)
|
||||
|
||||
|
||||
class Agent(BaseModel):
|
||||
creator = models.ForeignKey(
|
||||
KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True
|
||||
) # Creator will only be null when the agents are managed by admin
|
||||
name = models.CharField(max_length=200)
|
||||
personality = models.TextField()
|
||||
avatar = models.URLField(max_length=400, default=None, null=True, blank=True)
|
||||
tools = models.JSONField(default=list) # List of tools the agent has access to, like online search or notes search
|
||||
public = models.BooleanField(default=False)
|
||||
managed_by_admin = models.BooleanField(default=False)
|
||||
chat_model = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE)
|
||||
slug = models.CharField(max_length=200)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Agent)
|
||||
def verify_agent(sender, instance, **kwargs):
|
||||
# check if this is a new instance
|
||||
if instance._state.adding:
|
||||
if Agent.objects.filter(name=instance.name, public=True).exists():
|
||||
raise ValidationError(f"A public Agent with the name {instance.name} already exists.")
|
||||
if Agent.objects.filter(name=instance.name, creator=instance.creator).exists():
|
||||
raise ValidationError(f"A private Agent with the name {instance.name} already exists.")
|
||||
|
||||
slug = instance.name.lower().replace(" ", "-")
|
||||
observed_random_numbers = set()
|
||||
while Agent.objects.filter(slug=slug).exists():
|
||||
try:
|
||||
random_number = choice([i for i in range(0, 1000) if i not in observed_random_numbers])
|
||||
except IndexError:
|
||||
raise ValidationError("Unable to generate a unique slug for the Agent. Please try again later.")
|
||||
observed_random_numbers.add(random_number)
|
||||
slug = f"{slug}-{random_number}"
|
||||
instance.slug = slug
|
||||
|
||||
|
||||
class NotionConfig(BaseModel):
|
||||
token = models.CharField(max_length=200)
|
||||
user = models.ForeignKey(KhojUser, on_delete=models.CASCADE)
|
||||
@@ -153,17 +203,6 @@ class SpeechToTextModelOptions(BaseModel):
|
||||
model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE)
|
||||
|
||||
|
||||
class ChatModelOptions(BaseModel):
|
||||
class ModelType(models.TextChoices):
|
||||
OPENAI = "openai"
|
||||
OFFLINE = "offline"
|
||||
|
||||
max_prompt_size = models.IntegerField(default=None, null=True, blank=True)
|
||||
tokenizer = models.CharField(max_length=200, default=None, null=True, blank=True)
|
||||
chat_model = models.CharField(max_length=200, default="NousResearch/Hermes-2-Pro-Mistral-7B-GGUF")
|
||||
model_type = models.CharField(max_length=200, choices=ModelType.choices, default=ModelType.OFFLINE)
|
||||
|
||||
|
||||
class UserConversationConfig(BaseModel):
|
||||
user = models.OneToOneField(KhojUser, on_delete=models.CASCADE)
|
||||
setting = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE, default=None, null=True, blank=True)
|
||||
@@ -180,6 +219,7 @@ class Conversation(BaseModel):
|
||||
client = models.ForeignKey(ClientApplication, on_delete=models.CASCADE, default=None, null=True, blank=True)
|
||||
slug = models.CharField(max_length=200, default=None, null=True, blank=True)
|
||||
title = models.CharField(max_length=200, default=None, null=True, blank=True)
|
||||
agent = models.ForeignKey(Agent, on_delete=models.SET_NULL, default=None, null=True, blank=True)
|
||||
|
||||
|
||||
class ReflectiveQuestion(BaseModel):
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Khoj: An AI Personal Assistant for your digital brain</title>
|
||||
<link rel=”stylesheet” href=”static/styles.css”>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||
</head>
|
||||
<body class="not-found">
|
||||
<!--Add Header Logo and Nav Pane-->
|
||||
{% import 'utils.html' as utils %}
|
||||
{{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
|
||||
|
||||
<header class=”header”>
|
||||
<h1>Oops, this is awkward. That page couldn't be found.</h1>
|
||||
<h1>Oops, this is awkward. Looks like there's nothing here.</h1>
|
||||
</header>
|
||||
<a href="/config">Go Home</a>
|
||||
<a class="redirect-link" href="/">Go Home</a>
|
||||
|
||||
<footer class=”footer”>
|
||||
</footer>
|
||||
@@ -18,5 +23,34 @@
|
||||
body.not-found {
|
||||
padding: 0 10%
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--main-text-color);
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body a.redirect-link {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
background-color: var(--primary);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--main-text-color);
|
||||
color: var(--main-text-color);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
body a.redirect-link:hover {
|
||||
background-color: var(--main-text-color);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
</style>
|
||||
</html>
|
||||
|
||||
286
src/khoj/interface/web/agent.html
Normal file
286
src/khoj/interface/web/agent.html
Normal file
@@ -0,0 +1,286 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<title>Khoj - Agents</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||
</head>
|
||||
<script type="text/javascript" src="/static/assets/utils.js?v={{ khoj_version }}"></script>
|
||||
<body>
|
||||
<!--Add Header Logo and Nav Pane-->
|
||||
{% import 'utils.html' as utils %}
|
||||
{{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
|
||||
<div id="agent-metadata-wrapper">
|
||||
<div id="agent-metadata">
|
||||
<div id="agent-avatar-wrapper">
|
||||
<div id="agent-settings-header">Agent Settings</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div id="agent-data-wrapper">
|
||||
<div id="agent-avatar-wrapper">
|
||||
<img id="agent-avatar" src="{{ agent.avatar }}" alt="Agent Avatar">
|
||||
<input type="text" id="agent-name-input" value="{{ agent.name }}" {% if agent.creator_not_self %} disabled {% endif %}>
|
||||
</div>
|
||||
<div id="agent-instructions">Personality</div>
|
||||
<div id="agent-tuning">
|
||||
<p>{{ agent.personality }}</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div id="agent-public">
|
||||
<p>Public</p>
|
||||
<label class="switch">
|
||||
<input type="checkbox" {% if agent.public %} checked {% endif %} {% if agent.creator_not_self %} disabled {% endif %}>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p id="agent-creator" style="display: none;">Creator: {{ agent.creator }}</p>
|
||||
<p id="agent-managed-by-admin" style="display: none;">ⓘ This agent is managed by the administrator</p>
|
||||
<button onclick="openChat('{{ agent.slug }}')">Chat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<a href="/agents">All Agents</a>
|
||||
</div>
|
||||
</body>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
display: grid;
|
||||
color: var(--main-text-color);
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
div#agent-settings-header {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
div.divider {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 2px solid var(--main-text-color);
|
||||
}
|
||||
|
||||
div#footer {
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
background-color: var(--background-color);
|
||||
border-top: 1px solid var(--main-text-color);
|
||||
text-align: left;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
div#footer a {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
div#agent-data-wrapper button {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--primary);
|
||||
font: inherit;
|
||||
color: var(--main-text-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
div#agent-data-wrapper button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(--primary-hover);
|
||||
}
|
||||
|
||||
input#agent-name-input {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-color: #EEEEEE;
|
||||
color: var(--main-text-color);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
div#agent-instructions {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#agent-metadata {
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
text-align: left;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#agent-avatar-wrapper {
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#agent-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#agent-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#agent-tuning, #agent-public, #agent-creator, #agent-managed-by-admin {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#agent-tuning p {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
#agent-metadata p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#agent-public {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px var(--primary-hover);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
div#agent-data-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 700px) {
|
||||
body {
|
||||
grid-template-columns: auto min(70vw, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
#agent-metadata-wrapper {
|
||||
display: block;
|
||||
width: min(30vw, 100%);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
async function openChat(agentSlug) {
|
||||
let response = await fetch(`/api/chat/sessions?agent_slug=${agentSlug}`, { method: "POST" });
|
||||
let data = await response.json();
|
||||
if (response.status == 200) {
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
alert("Failed to start chat session");
|
||||
}
|
||||
}
|
||||
|
||||
// Show the agent-managed-by-admin paragraph if the agent is managed by the admin
|
||||
// compare agent.managed_by_admin as a lowercase string to "true"
|
||||
let isManagedByAdmin = "{{ agent.managed_by_admin }}".toLowerCase() === "true";
|
||||
if (isManagedByAdmin) {
|
||||
document.getElementById("agent-managed-by-admin").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("agent-creator").style.display = "block";
|
||||
}
|
||||
|
||||
// Resize the input field based on the length of the value
|
||||
let input = document.getElementById("agent-name-input");
|
||||
input.addEventListener("input", resizeInput);
|
||||
resizeInput.call(input);
|
||||
function resizeInput() {
|
||||
this.style.width = this.value.length + 1 + "ch";
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
203
src/khoj/interface/web/agents.html
Normal file
203
src/khoj/interface/web/agents.html
Normal file
@@ -0,0 +1,203 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<title>Khoj - Agents</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||
</head>
|
||||
<script type="text/javascript" src="/static/assets/utils.js?v={{ khoj_version }}"></script>
|
||||
<body>
|
||||
<!--Add Header Logo and Nav Pane-->
|
||||
{% import 'utils.html' as utils %}
|
||||
{{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
|
||||
<!-- {{ agents }} -->
|
||||
|
||||
<div id="agents-list">
|
||||
<div id="agents">
|
||||
<div id="agents-header">
|
||||
<h1 id="agents-list-title">Agents</h1>
|
||||
<!-- <div id="create-agent">
|
||||
<a href="/agents/create"><svg class="new-convo-button" viewBox="0 0 35 35" fill="#000000" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 0c-8.836 0-16 7.163-16 16s7.163 16 16 16c8.837 0 16-7.163 16-16s-7.163-16-16-16zM16 30.032c-7.72 0-14-6.312-14-14.032s6.28-14 14-14 14 6.28 14 14-6.28 14.032-14 14.032zM23 15h-6v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v6h-6c-0.552 0-1 0.448-1 1s0.448 1 1 1h6v6c0 0.552 0.448 1 1 1s1-0.448 1-1v-6h6c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
|
||||
</svg></a>
|
||||
</div> -->
|
||||
</div>
|
||||
{% for agent in agents %}
|
||||
<div class="agent">
|
||||
<a href="/agent/{{ agent.slug }}">
|
||||
<div class="agent-avatar">
|
||||
<img src="{{ agent.avatar }}" alt="{{ agent.name }}">
|
||||
</div>
|
||||
</a>
|
||||
<div class="agent-info">
|
||||
<a href="/agent/{{ agent.slug }}">
|
||||
<h2>{{ agent.name }}</h2>
|
||||
</a>
|
||||
<p>{{ agent.personality }}</p>
|
||||
</div>
|
||||
<div class="agent-info">
|
||||
<button onclick="openChat('{{ agent.slug }}')">Talk</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<a href="/">Back to Chat</a>
|
||||
</div>
|
||||
</body>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
display: grid;
|
||||
color: var(--main-text-color);
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
h1#agents-list-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.agent-info p {
|
||||
height: 50px;
|
||||
overflow: auto;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
div.agent-info {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
div.agent-info a,
|
||||
div.agent-info h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.agent img {
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
div.agent a {
|
||||
text-decoration: none;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
div#agents-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
div#agents-header a,
|
||||
div.agent-info button {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--primary);
|
||||
font: inherit;
|
||||
color: var(--main-text-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
div#agents-header a:hover,
|
||||
div.agent-info button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(--primary-hover);
|
||||
}
|
||||
|
||||
|
||||
div#footer {
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
background-color: var(--background-color);
|
||||
border-top: 1px solid var(--main-text-color);
|
||||
text-align: left;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
div#footer a {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
div.agent {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: var(--frosted-background-color);
|
||||
border-top: 1px solid var(--main-text-color);
|
||||
}
|
||||
|
||||
div.agent-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div#agents {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background-color: var(--frosted-background-color);
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
border-radius: 8px;
|
||||
width: 75%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
svg.new-convo-button {
|
||||
width: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 700px) {
|
||||
body {
|
||||
grid-template-columns: auto min(70vw, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
div#agents {
|
||||
width: 90%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
async function openChat(agentSlug) {
|
||||
let response = await fetch(`/api/chat/sessions?agent_slug=${agentSlug}`, { method: "POST" });
|
||||
let data = await response.json();
|
||||
if (response.status == 200) {
|
||||
window.location.href = "/";
|
||||
} else if(response.status == 403 || response.status == 401) {
|
||||
window.location.href = "/login?next=/agent/" + agentId;
|
||||
} else {
|
||||
alert("Failed to start chat session");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
@@ -130,7 +130,7 @@ img.khoj-logo {
|
||||
background-color: var(--background-color);
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
right: 15vw;
|
||||
right: 5vw;
|
||||
top: 64px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
height: 40px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 20px;
|
||||
font-size: medium;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -13,15 +13,17 @@
|
||||
<script type="text/javascript" src="/static/assets/markdown-it.min.js?v={{ khoj_version }}"></script>
|
||||
<script>
|
||||
let welcome_message = `
|
||||
Hi, I am Khoj, your open, personal AI 👋🏽. I can help:
|
||||
Hi, I am Khoj, your open, personal AI 👋🏽. I can:
|
||||
- 🧠 Answer general knowledge questions
|
||||
- 💡 Be a sounding board for your ideas
|
||||
- 📜 Chat with your notes & documents
|
||||
- 🌄 Generate images based on your messages
|
||||
- 🔎 Search the web for answers to your questions
|
||||
- 🎙️ Listen to your audio messages (use the mic by the input box to speak your message)
|
||||
- 📚 Understand files you drag & drop here
|
||||
- 👩🏾🚀 Be tuned to your conversation needs via [agents](./agents)
|
||||
|
||||
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), [Emacs](https://docs.khoj.dev/clients/emacs#setup) apps to search, chat with your 🖥️ computer docs.
|
||||
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), [Emacs](https://docs.khoj.dev/clients/emacs#setup) apps to search, chat with your 🖥️ computer docs. You can manage all the files you've shared with me at any time by going to [your settings](/config/content-source/computer/).
|
||||
|
||||
To get started, just start typing below. You can also type / to see a list of commands.
|
||||
`.trim()
|
||||
@@ -116,7 +118,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
linkElement.setAttribute('href', link);
|
||||
linkElement.setAttribute('target', '_blank');
|
||||
linkElement.setAttribute('rel', 'noopener noreferrer');
|
||||
linkElement.classList.add("inline-chat-link");
|
||||
linkElement.classList.add("reference-link");
|
||||
linkElement.setAttribute('title', title);
|
||||
linkElement.textContent = title;
|
||||
@@ -842,6 +843,33 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
chatBody.dataset.conversationId = response.conversation_id;
|
||||
chatBody.dataset.conversationTitle = response.slug || `New conversation 🌱`;
|
||||
|
||||
let agentMetadata = response.agent;
|
||||
if (agentMetadata) {
|
||||
let agentName = agentMetadata.name;
|
||||
let agentAvatar = agentMetadata.avatar;
|
||||
let agentOwnedByUser = agentMetadata.isCreator;
|
||||
|
||||
let agentAvatarElement = document.getElementById("agent-avatar");
|
||||
let agentNameElement = document.getElementById("agent-name");
|
||||
|
||||
let agentLinkElement = document.getElementById("agent-link");
|
||||
|
||||
agentAvatarElement.src = agentAvatar;
|
||||
agentNameElement.textContent = agentName;
|
||||
agentLinkElement.setAttribute("href", `/agent/${agentMetadata.slug}`);
|
||||
|
||||
if (agentOwnedByUser) {
|
||||
let agentOwnedByUserElement = document.getElementById("agent-owned-by-user");
|
||||
agentOwnedByUserElement.style.display = "block";
|
||||
}
|
||||
|
||||
let agentMetadataElement = document.getElementById("agent-metadata");
|
||||
agentMetadataElement.style.display = "block";
|
||||
} else {
|
||||
let agentMetadataElement = document.getElementById("agent-metadata");
|
||||
agentMetadataElement.style.display = "none";
|
||||
}
|
||||
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
const fullChatLog = response.chat || [];
|
||||
|
||||
@@ -934,12 +962,100 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
}
|
||||
|
||||
function createNewConversation() {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
flashStatusInChatInput("📝 New conversation started");
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
renderMessage(welcome_message, "khoj");
|
||||
// Create a modal that appears in the middle of the entire screen. It should have a form to create a new conversation.
|
||||
let modal = document.createElement('div');
|
||||
modal.classList.add("modal");
|
||||
modal.id = "new-conversation-modal";
|
||||
let modalContent = document.createElement('div');
|
||||
modalContent.classList.add("modal-content");
|
||||
let modalHeader = document.createElement('div');
|
||||
modalHeader.classList.add("modal-header");
|
||||
let modalTitle = document.createElement('h2');
|
||||
modalTitle.textContent = "New Conversation";
|
||||
let modalCloseButton = document.createElement('button');
|
||||
modalCloseButton.classList.add("modal-close-button");
|
||||
modalCloseButton.innerHTML = "×";
|
||||
modalCloseButton.addEventListener('click', function() {
|
||||
modal.remove();
|
||||
});
|
||||
modalHeader.appendChild(modalTitle);
|
||||
modalHeader.appendChild(modalCloseButton);
|
||||
modalContent.appendChild(modalHeader);
|
||||
let modalBody = document.createElement('div');
|
||||
modalBody.classList.add("modal-body");
|
||||
|
||||
let agentDropDownPicker = document.createElement('select');
|
||||
agentDropDownPicker.setAttribute("id", "agent-dropdown-picker");
|
||||
agentDropDownPicker.setAttribute("name", "agent-dropdown-picker");
|
||||
|
||||
let agentDropDownLabel = document.createElement('label');
|
||||
agentDropDownLabel.setAttribute("for", "agent-dropdown-picker");
|
||||
agentDropDownLabel.textContent = "Who do you want to talk to?";
|
||||
|
||||
fetch('/api/agents')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.length > 0) {
|
||||
data.forEach((agent) => {
|
||||
let agentOption = document.createElement('option');
|
||||
agentOption.setAttribute("value", agent.slug);
|
||||
agentOption.textContent = agent.name;
|
||||
agentDropDownPicker.appendChild(agentOption);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
|
||||
let seeAllAgentsLink = document.createElement('a');
|
||||
seeAllAgentsLink.setAttribute("href", "/agents");
|
||||
seeAllAgentsLink.setAttribute("target", "_blank");
|
||||
seeAllAgentsLink.textContent = "See all agents";
|
||||
|
||||
let newConversationSubmitButton = document.createElement('button');
|
||||
newConversationSubmitButton.setAttribute("type", "submit");
|
||||
newConversationSubmitButton.textContent = "Go";
|
||||
newConversationSubmitButton.id = "new-conversation-submit-button";
|
||||
|
||||
newConversationSubmitButton.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
let agentSlug = agentDropDownPicker.value;
|
||||
let createURL = `/api/chat/sessions?client=web&agent_slug=${agentSlug}`;
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
fetch(createURL, { method: "POST" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
chatBody.dataset.conversationId = data.conversation_id;
|
||||
modal.remove();
|
||||
loadChat();
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
let closeButton = document.createElement('button');
|
||||
closeButton.id = "close-button";
|
||||
closeButton.innerHTML = "Close";
|
||||
closeButton.classList.add("close-button");
|
||||
closeButton.addEventListener('click', function() {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
modalBody.appendChild(agentDropDownLabel);
|
||||
modalBody.appendChild(agentDropDownPicker);
|
||||
modalBody.appendChild(seeAllAgentsLink);
|
||||
|
||||
let modalFooter = document.createElement('div');
|
||||
modalFooter.classList.add("modal-footer");
|
||||
modalFooter.appendChild(closeButton);
|
||||
modalFooter.appendChild(newConversationSubmitButton);
|
||||
modalBody.appendChild(modalFooter);
|
||||
|
||||
modalContent.appendChild(modalBody);
|
||||
modal.appendChild(modalContent);
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
function refreshChatSessionsPanel() {
|
||||
@@ -1194,8 +1310,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
document.getElementById('new-conversation').classList.toggle('collapsed');
|
||||
document.getElementById('existing-conversations').classList.toggle('collapsed');
|
||||
document.getElementById('side-panel-collapse').style.transform = document.getElementById('side-panel').classList.contains('collapsed') ? 'rotate(0deg)' : 'rotate(180deg)';
|
||||
|
||||
document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly');
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
@@ -1215,13 +1329,27 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
<path d="M16 0c-8.836 0-16 7.163-16 16s7.163 16 16 16c8.837 0 16-7.163 16-16s-7.163-16-16-16zM16 30.032c-7.72 0-14-6.312-14-14.032s6.28-14 14-14 14 6.28 14 14-6.28 14.032-14 14.032zM23 15h-6v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v6h-6c-0.552 0-1 0.448-1 1s0.448 1 1 1h6v6c0 0.552 0.448 1 1 1s1-0.448 1-1v-6h6c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="conversation-list-header" style="display: none;">Conversations</div>
|
||||
</div>
|
||||
<div id="existing-conversations">
|
||||
<div id="conversation-list">
|
||||
<div id="conversation-list-header" style="display: none;">Recent Conversations</div>
|
||||
<div id="conversation-list-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="inline-chat-link" id="agent-link" href="">
|
||||
<div id="agent-metadata" style="display: none;">
|
||||
Active
|
||||
<div id="agent-metadata-content">
|
||||
<div id="agent-avatar-wrapper">
|
||||
<img id="agent-avatar" src="" alt="Agent Avatar" />
|
||||
</div>
|
||||
<div id="agent-name-wrapper">
|
||||
<div id="agent-name"></div>
|
||||
<div id="agent-owned-by-user" style="display: none;">Edit</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div id="collapse-side-panel">
|
||||
<button
|
||||
@@ -1297,7 +1425,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
color: var(--main-text-color);
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
font-size: 20px;
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
height: 100vh;
|
||||
@@ -1448,10 +1576,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#chat-section-wrapper.mobile-friendly {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
#chat-body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1464,10 +1588,15 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
background: var(--background-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
overflow-y: scroll;
|
||||
text-align: left;
|
||||
transition: width 0.3s ease-in-out;
|
||||
max-height: 85vh;
|
||||
max-height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
div#existing-conversations {
|
||||
max-height: 95%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -1489,8 +1618,12 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
grid-gap: 8px;
|
||||
}
|
||||
|
||||
div#conversation-list {
|
||||
height: 1;
|
||||
}
|
||||
|
||||
div#side-panel-wrapper {
|
||||
display: flex
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#chat-body {
|
||||
@@ -1880,7 +2013,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
}
|
||||
@media only screen and (min-width: 700px) {
|
||||
body {
|
||||
grid-template-columns: auto min(70vw, 100%) auto;
|
||||
grid-template-columns: auto min(90vw, 100%) auto;
|
||||
grid-template-rows: auto auto minmax(80px, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
@@ -1901,6 +2034,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
div#new-conversation {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--main-text-color);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@@ -2056,6 +2190,170 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
#agent-metadata-content {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 10px;
|
||||
background-color: var(--primary);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#agent-metadata {
|
||||
border-top: 1px solid black;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#agent-avatar-wrapper {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#agent-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#agent-name-wrapper {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#agent-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#agent-instructions {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
height: 50px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#agent-owned-by-user {
|
||||
font-size: 12px;
|
||||
color: #007BFF;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
margin: 15% auto; /* 15% from the top and centered */
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 250px;
|
||||
text-align: left;
|
||||
background: var(--background-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
color: var(--main-text-color);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-body a {
|
||||
/* text-decoration: none; */
|
||||
color: var(--summer-sun);
|
||||
}
|
||||
|
||||
.modal-close-button {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--summer-sun);
|
||||
}
|
||||
|
||||
.modal-close-button:hover,
|
||||
.modal-close-button:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#new-conversation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#new-conversation-form label,
|
||||
#new-conversation-form input,
|
||||
#new-conversation-form button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#new-conversation-form button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.modal-body button {
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--main-text-color);
|
||||
}
|
||||
|
||||
button#new-conversation-submit-button {
|
||||
background: var(--summer-sun);
|
||||
transition: background 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
button#close-button {
|
||||
background: var(--background-color);
|
||||
transition: background 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
button#new-conversation-submit-button:hover {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
button#close-button:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.modal-body select {
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--main-text-color);
|
||||
}
|
||||
|
||||
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 36px;
|
||||
|
||||
@@ -4,31 +4,33 @@
|
||||
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways-500.png?v={{ khoj_version }}" alt="Khoj"></img>
|
||||
</a>
|
||||
<nav class="khoj-nav">
|
||||
<a id="agents-nav" class="khoj-nav" href="/agents">Agents</a>
|
||||
{% if has_documents %}
|
||||
<a id="chat-nav" class="khoj-nav" href="/chat">💬 Chat</a>
|
||||
<a id="search-nav" class="khoj-nav" href="/search">🔎 Search</a>
|
||||
<a id="search-nav" class="khoj-nav" href="/search">Search</a>
|
||||
{% endif %}
|
||||
<!-- Dropdown Menu -->
|
||||
<div id="khoj-nav-menu-container" class="khoj-nav dropdown">
|
||||
{% if user_photo and user_photo != "None" %}
|
||||
{% if is_active %}
|
||||
<img id="profile-picture" class="circle subscribed" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer">
|
||||
{% if username %}
|
||||
<div id="khoj-nav-menu-container" class="khoj-nav dropdown">
|
||||
{% if user_photo and user_photo != "None" %}
|
||||
{% if is_active %}
|
||||
<img id="profile-picture" class="circle subscribed" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer">
|
||||
{% else %}
|
||||
<img id="profile-picture" class="circle" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img id="profile-picture" class="circle" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer">
|
||||
{% if is_active %}
|
||||
<div id="profile-picture" class="circle user-initial subscribed" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div>
|
||||
{% else %}
|
||||
<div id="profile-picture" class="circle user-initial" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if is_active %}
|
||||
<div id="profile-picture" class="circle user-initial subscribed" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div>
|
||||
{% else %}
|
||||
<div id="profile-picture" class="circle user-initial" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
|
||||
<div class="khoj-nav-username"> {{ username }} </div>
|
||||
<a id="settings-nav" class="khoj-nav" href="/config">⚙️ Settings</a>
|
||||
<a class="khoj-nav" href="/auth/logout">🔑 Logout</a>
|
||||
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
|
||||
<div class="khoj-nav-username"> {{ username }} </div>
|
||||
<a id="settings-nav" class="khoj-nav" href="/config">⚙️ Settings</a>
|
||||
<a class="khoj-nav" href="/auth/logout">🔑 Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -160,7 +160,9 @@ def start_server(app, host=None, port=None, socket=None):
|
||||
if socket:
|
||||
uvicorn.run(app, proxy_headers=True, uds=socket, log_level="debug", use_colors=True, log_config=None)
|
||||
else:
|
||||
uvicorn.run(app, host=host, port=port, log_level="debug", use_colors=True, log_config=None)
|
||||
uvicorn.run(
|
||||
app, host=host, port=port, log_level="debug", use_colors=True, log_config=None, timeout_keep_alive=60
|
||||
)
|
||||
logger.info("🌒 Stopping Khoj")
|
||||
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ class MarkdownToEntries(TextToEntries):
|
||||
# Append base filename to compiled entry for context to model
|
||||
# Increment heading level for heading entries and make filename as its top level heading
|
||||
prefix = f"# {stem}\n#" if heading else f"# {stem}\n"
|
||||
compiled_entry = f"{prefix}{parsed_entry}"
|
||||
compiled_entry = f"{entry_filename}\n{prefix}{parsed_entry}"
|
||||
entries.append(
|
||||
Entry(
|
||||
compiled=compiled_entry,
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Iterator, List, Union
|
||||
from langchain.schema import ChatMessage
|
||||
from llama_cpp import Llama
|
||||
|
||||
from khoj.database.models import Agent
|
||||
from khoj.processor.conversation import prompts
|
||||
from khoj.processor.conversation.offline.utils import download_model
|
||||
from khoj.processor.conversation.utils import (
|
||||
@@ -131,6 +132,7 @@ def converse_offline(
|
||||
tokenizer_name=None,
|
||||
location_data: LocationData = None,
|
||||
user_name: str = None,
|
||||
agent: Agent = None,
|
||||
) -> Union[ThreadedGenerator, Iterator[str]]:
|
||||
"""
|
||||
Converse with user using Llama
|
||||
@@ -140,6 +142,15 @@ def converse_offline(
|
||||
offline_chat_model = loaded_model or download_model(model)
|
||||
compiled_references_message = "\n\n".join({f"{item}" for item in references})
|
||||
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
if agent and agent.personality:
|
||||
system_prompt = prompts.custom_system_prompt_offline_chat.format(
|
||||
name=agent.name, bio=agent.personality, current_date=current_date
|
||||
)
|
||||
else:
|
||||
system_prompt = prompts.system_prompt_offline_chat.format(current_date=current_date)
|
||||
|
||||
conversation_primer = prompts.query_prompt.format(query=user_query)
|
||||
|
||||
if location_data:
|
||||
@@ -169,10 +180,9 @@ def converse_offline(
|
||||
conversation_primer = f"{prompts.notes_conversation_offline.format(references=compiled_references_message)}\n{conversation_primer}"
|
||||
|
||||
# Setup Prompt with Primer or Conversation History
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
messages = generate_chatml_messages_with_context(
|
||||
conversation_primer,
|
||||
prompts.system_prompt_offline_chat.format(current_date=current_date),
|
||||
system_prompt,
|
||||
conversation_log,
|
||||
model_name=model,
|
||||
loaded_model=offline_chat_model,
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Dict, Optional
|
||||
|
||||
from langchain.schema import ChatMessage
|
||||
|
||||
from khoj.database.models import Agent
|
||||
from khoj.processor.conversation import prompts
|
||||
from khoj.processor.conversation.openai.utils import (
|
||||
chat_completion_with_backoff,
|
||||
@@ -115,6 +116,7 @@ def converse(
|
||||
tokenizer_name=None,
|
||||
location_data: LocationData = None,
|
||||
user_name: str = None,
|
||||
agent: Agent = None,
|
||||
):
|
||||
"""
|
||||
Converse with user using OpenAI's ChatGPT
|
||||
@@ -125,6 +127,13 @@ def converse(
|
||||
|
||||
conversation_primer = prompts.query_prompt.format(query=user_query)
|
||||
|
||||
if agent and agent.personality:
|
||||
system_prompt = prompts.custom_personality.format(
|
||||
name=agent.name, bio=agent.personality, current_date=current_date
|
||||
)
|
||||
else:
|
||||
system_prompt = prompts.personality.format(current_date=current_date)
|
||||
|
||||
if location_data:
|
||||
location = f"{location_data.city}, {location_data.region}, {location_data.country}"
|
||||
location_prompt = prompts.user_location.format(location=location)
|
||||
@@ -152,7 +161,7 @@ def converse(
|
||||
# Setup Prompt with Primer or Conversation History
|
||||
messages = generate_chatml_messages_with_context(
|
||||
conversation_primer,
|
||||
prompts.personality.format(current_date=current_date),
|
||||
system_prompt,
|
||||
conversation_log,
|
||||
model,
|
||||
max_prompt_size,
|
||||
|
||||
@@ -21,6 +21,24 @@ Today is {current_date} in UTC.
|
||||
""".strip()
|
||||
)
|
||||
|
||||
custom_personality = PromptTemplate.from_template(
|
||||
"""
|
||||
You are {name}, a personal agent on Khoj.
|
||||
Use your general knowledge and past conversation with the user as context to inform your responses.
|
||||
You were created by Khoj Inc. with the following capabilities:
|
||||
|
||||
- You *CAN REMEMBER ALL NOTES and PERSONAL INFORMATION FOREVER* that the user ever shares with you.
|
||||
- Users can share files and other information with you using the Khoj Desktop, Obsidian or Emacs app. They can also drag and drop their files into the chat window.
|
||||
- Say "I don't know" or "I don't understand" if you don't know what to say or if you don't know the answer to a question.
|
||||
- Ask crisp follow-up questions to get additional context, when the answer cannot be inferred from the provided notes or past conversations.
|
||||
- Sometimes the user will share personal information that needs to be remembered, like an account ID or a residential address. These can be acknowledged with a simple "Got it" or "Okay".
|
||||
|
||||
Today is {current_date} in UTC.
|
||||
|
||||
Instructions:\n{bio}
|
||||
""".strip()
|
||||
)
|
||||
|
||||
## General Conversation
|
||||
## --
|
||||
general_conversation = PromptTemplate.from_template(
|
||||
@@ -61,6 +79,20 @@ Today is {current_date} in UTC.
|
||||
""".strip()
|
||||
)
|
||||
|
||||
custom_system_prompt_offline_chat = PromptTemplate.from_template(
|
||||
"""
|
||||
You are {name}, a personal agent on Khoj.
|
||||
- Use your general knowledge and past conversation with the user as context to inform your responses.
|
||||
- If you do not know the answer, say 'I don't know.'
|
||||
- Think step-by-step and ask questions to get the necessary information to answer the user's question.
|
||||
- Do not print verbatim Notes unless necessary.
|
||||
|
||||
Today is {current_date} in UTC.
|
||||
|
||||
Instructions:\n{bio}
|
||||
""".strip()
|
||||
)
|
||||
|
||||
## Notes Conversation
|
||||
## --
|
||||
notes_conversation = PromptTemplate.from_template(
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi.requests import Request
|
||||
from fastapi.responses import Response
|
||||
from starlette.authentication import requires
|
||||
|
||||
from khoj.configure import configure_server, initialize_content
|
||||
from khoj.configure import initialize_content
|
||||
from khoj.database.adapters import (
|
||||
ConversationAdapters,
|
||||
EntryAdapters,
|
||||
|
||||
43
src/khoj/routers/api_agents.py
Normal file
43
src/khoj/routers/api_agents.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.requests import Request
|
||||
from fastapi.responses import Response
|
||||
|
||||
from khoj.database.adapters import AgentAdapters
|
||||
from khoj.database.models import KhojUser
|
||||
from khoj.routers.helpers import CommonQueryParams
|
||||
|
||||
# Initialize Router
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
api_agents = APIRouter()
|
||||
|
||||
|
||||
@api_agents.get("", response_class=Response)
|
||||
async def all_agents(
|
||||
request: Request,
|
||||
common: CommonQueryParams,
|
||||
) -> Response:
|
||||
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||
agents = await AgentAdapters.aget_all_accessible_agents(user)
|
||||
agents_packet = list()
|
||||
for agent in agents:
|
||||
agents_packet.append(
|
||||
{
|
||||
"slug": agent.slug,
|
||||
"avatar": agent.avatar,
|
||||
"name": agent.name,
|
||||
"personality": agent.personality,
|
||||
"public": agent.public,
|
||||
"creator": agent.creator.username if agent.creator else None,
|
||||
"managed_by_admin": agent.managed_by_admin,
|
||||
}
|
||||
)
|
||||
|
||||
# Make sure that the agent named 'khoj' is first in the list. Everything else is sorted by name.
|
||||
agents_packet.sort(key=lambda x: x["name"])
|
||||
agents_packet.sort(key=lambda x: x["slug"] == "khoj", reverse=True)
|
||||
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
||||
@@ -12,7 +12,11 @@ from starlette.authentication import requires
|
||||
|
||||
from khoj.database.adapters import ConversationAdapters, EntryAdapters, aget_user_name
|
||||
from khoj.database.models import KhojUser
|
||||
from khoj.processor.conversation.prompts import help_message, no_entries_found
|
||||
from khoj.processor.conversation.prompts import (
|
||||
help_message,
|
||||
no_entries_found,
|
||||
no_notes_found,
|
||||
)
|
||||
from khoj.processor.conversation.utils import save_to_conversation_log
|
||||
from khoj.processor.tools.online_search import (
|
||||
online_search_enabled,
|
||||
@@ -85,9 +89,22 @@ def chat_history(
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
agent_metadata = None
|
||||
if conversation.agent:
|
||||
agent_metadata = {
|
||||
"slug": conversation.agent.slug,
|
||||
"name": conversation.agent.name,
|
||||
"avatar": conversation.agent.avatar,
|
||||
"isCreator": conversation.agent.creator == user,
|
||||
}
|
||||
|
||||
meta_log = conversation.conversation_log
|
||||
meta_log.update(
|
||||
{"conversation_id": conversation.id, "slug": conversation.title if conversation.title else conversation.slug}
|
||||
{
|
||||
"conversation_id": conversation.id,
|
||||
"slug": conversation.title if conversation.title else conversation.slug,
|
||||
"agent": agent_metadata,
|
||||
}
|
||||
)
|
||||
|
||||
update_telemetry_state(
|
||||
@@ -152,18 +169,24 @@ def chat_sessions(
|
||||
async def create_chat_session(
|
||||
request: Request,
|
||||
common: CommonQueryParams,
|
||||
agent_slug: Optional[str] = None,
|
||||
):
|
||||
user = request.user.object
|
||||
|
||||
# Create new Conversation Session
|
||||
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app)
|
||||
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, agent_slug)
|
||||
|
||||
response = {"conversation_id": conversation.id}
|
||||
|
||||
conversation_metadata = {
|
||||
"agent": agent_slug,
|
||||
}
|
||||
|
||||
update_telemetry_state(
|
||||
request=request,
|
||||
telemetry_type="api",
|
||||
api="create_chat_sessions",
|
||||
metadata=conversation_metadata,
|
||||
**common.__dict__,
|
||||
)
|
||||
|
||||
@@ -242,7 +265,7 @@ async def chat(
|
||||
) -> Response:
|
||||
user: KhojUser = request.user.object
|
||||
q = unquote(q)
|
||||
logger.info("Chat request by {user.username}: {q}")
|
||||
logger.info(f"Chat request by {user.username}: {q}")
|
||||
|
||||
await is_ready_to_chat(user)
|
||||
conversation_commands = [get_conversation_command(query=q, any_references=True)]
|
||||
@@ -295,6 +318,14 @@ async def chat(
|
||||
response_obj = {"response": no_entries_found_format}
|
||||
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
||||
|
||||
if conversation_commands == [ConversationCommand.Notes] and is_none_or_empty(compiled_references):
|
||||
no_notes_found_format = no_notes_found.format()
|
||||
if stream:
|
||||
return StreamingResponse(iter([no_notes_found_format]), media_type="text/event-stream", status_code=200)
|
||||
else:
|
||||
response_obj = {"response": no_notes_found_format}
|
||||
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
||||
|
||||
if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
|
||||
conversation_commands.remove(ConversationCommand.Notes)
|
||||
|
||||
@@ -356,6 +387,7 @@ async def chat(
|
||||
llm_response, chat_metadata = await agenerate_chat_response(
|
||||
defiltered_query,
|
||||
meta_log,
|
||||
conversation,
|
||||
compiled_references,
|
||||
online_results,
|
||||
inferred_queries,
|
||||
@@ -369,6 +401,7 @@ async def chat(
|
||||
|
||||
cmd_set = set([cmd.value for cmd in conversation_commands])
|
||||
chat_metadata["conversation_command"] = cmd_set
|
||||
chat_metadata["agent"] = conversation.agent.slug if conversation.agent else None
|
||||
|
||||
update_telemetry_state(
|
||||
request=request,
|
||||
|
||||
@@ -7,6 +7,7 @@ from starlette.authentication import requires
|
||||
from starlette.config import Config
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse, RedirectResponse, Response
|
||||
from starlette.status import HTTP_302_FOUND
|
||||
|
||||
from khoj.database.adapters import (
|
||||
create_khoj_token,
|
||||
@@ -90,6 +91,7 @@ async def delete_token(request: Request, token: str) -> str:
|
||||
@auth_router.post("/redirect")
|
||||
async def auth(request: Request):
|
||||
form = await request.form()
|
||||
next_url = request.query_params.get("next", "/")
|
||||
credential = form.get("credential")
|
||||
|
||||
csrf_token_cookie = request.cookies.get("g_csrf_token")
|
||||
@@ -117,9 +119,9 @@ async def auth(request: Request):
|
||||
metadata={"user_id": str(khoj_user.uuid)},
|
||||
)
|
||||
logger.log(logging.INFO, f"New User Created: {khoj_user.uuid}")
|
||||
RedirectResponse(url="/?status=welcome")
|
||||
return RedirectResponse(url=f"{next_url}", status_code=HTTP_302_FOUND)
|
||||
|
||||
return RedirectResponse(url="/")
|
||||
return RedirectResponse(url=f"{next_url}")
|
||||
|
||||
|
||||
@auth_router.get("/logout")
|
||||
|
||||
@@ -10,10 +10,11 @@ import openai
|
||||
from fastapi import Depends, Header, HTTPException, Request, UploadFile
|
||||
from starlette.authentication import has_required_scope
|
||||
|
||||
from khoj.database.adapters import ConversationAdapters, EntryAdapters
|
||||
from khoj.database.adapters import AgentAdapters, ConversationAdapters, EntryAdapters
|
||||
from khoj.database.models import (
|
||||
ChatModelOptions,
|
||||
ClientApplication,
|
||||
Conversation,
|
||||
KhojUser,
|
||||
Subscription,
|
||||
TextToImageModelConfig,
|
||||
@@ -407,6 +408,7 @@ async def send_message_to_model_wrapper(
|
||||
def generate_chat_response(
|
||||
q: str,
|
||||
meta_log: dict,
|
||||
conversation: Conversation,
|
||||
compiled_references: List[str] = [],
|
||||
online_results: Dict[str, Dict] = {},
|
||||
inferred_queries: List[str] = [],
|
||||
@@ -422,6 +424,7 @@ def generate_chat_response(
|
||||
logger.debug(f"Conversation Types: {conversation_commands}")
|
||||
|
||||
metadata = {}
|
||||
agent = AgentAdapters.get_conversation_agent_by_id(conversation.agent.id) if conversation.agent else None
|
||||
|
||||
try:
|
||||
partial_completion = partial(
|
||||
@@ -436,7 +439,7 @@ def generate_chat_response(
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
conversation_config = ConversationAdapters.get_valid_conversation_config(user)
|
||||
conversation_config = ConversationAdapters.get_valid_conversation_config(user, conversation)
|
||||
if conversation_config.model_type == "offline":
|
||||
if state.offline_chat_processor_config is None or state.offline_chat_processor_config.loaded_model is None:
|
||||
state.offline_chat_processor_config = OfflineChatProcessorModel(conversation_config.chat_model)
|
||||
@@ -455,6 +458,7 @@ def generate_chat_response(
|
||||
tokenizer_name=conversation_config.tokenizer,
|
||||
location_data=location_data,
|
||||
user_name=user_name,
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
elif conversation_config.model_type == "openai":
|
||||
@@ -474,6 +478,7 @@ def generate_chat_response(
|
||||
tokenizer_name=conversation_config.tokenizer,
|
||||
location_data=location_data,
|
||||
user_name=user_name,
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
metadata.update({"chat_model": conversation_config.chat_model})
|
||||
|
||||
@@ -10,6 +10,7 @@ from starlette.authentication import has_required_scope, requires
|
||||
|
||||
from khoj.database import adapters
|
||||
from khoj.database.adapters import (
|
||||
AgentAdapters,
|
||||
ConversationAdapters,
|
||||
EntryAdapters,
|
||||
get_user_github_config,
|
||||
@@ -114,8 +115,8 @@ def chat_page(request: Request):
|
||||
|
||||
@web_client.get("/login", response_class=FileResponse)
|
||||
def login_page(request: Request):
|
||||
next_url = request.query_params.get("next", "/")
|
||||
if request.user.is_authenticated:
|
||||
next_url = request.query_params.get("next", "/")
|
||||
return RedirectResponse(url=next_url)
|
||||
google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
|
||||
redirect_uri = str(request.app.url_path_for("auth"))
|
||||
@@ -124,7 +125,85 @@ def login_page(request: Request):
|
||||
context={
|
||||
"request": request,
|
||||
"google_client_id": google_client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"redirect_uri": f"{redirect_uri}?next={next_url}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@web_client.get("/agents", response_class=HTMLResponse)
|
||||
def agents_page(request: Request):
|
||||
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||
user_picture = request.session.get("user", {}).get("picture") if user else None
|
||||
agents = AgentAdapters.get_all_accessible_agents(user)
|
||||
agents_packet = list()
|
||||
for agent in agents:
|
||||
agents_packet.append(
|
||||
{
|
||||
"slug": agent.slug,
|
||||
"avatar": agent.avatar,
|
||||
"name": agent.name,
|
||||
"personality": agent.personality,
|
||||
"public": agent.public,
|
||||
"creator": agent.creator.username if agent.creator else None,
|
||||
"managed_by_admin": agent.managed_by_admin,
|
||||
}
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"agents.html",
|
||||
context={
|
||||
"request": request,
|
||||
"agents": agents_packet,
|
||||
"khoj_version": state.khoj_version,
|
||||
"username": user.username if user else None,
|
||||
"has_documents": False,
|
||||
"is_active": has_required_scope(request, ["premium"]),
|
||||
"user_photo": user_picture,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@web_client.get("/agent/{agent_slug}", response_class=HTMLResponse)
|
||||
def agent_page(request: Request, agent_slug: str):
|
||||
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||
user_picture = request.session.get("user", {}).get("picture") if user else None
|
||||
|
||||
agent = AgentAdapters.get_agent_by_slug(agent_slug)
|
||||
|
||||
if agent == None:
|
||||
return templates.TemplateResponse(
|
||||
"404.html",
|
||||
context={
|
||||
"request": request,
|
||||
"khoj_version": state.khoj_version,
|
||||
"username": user.username if user else None,
|
||||
"has_documents": False,
|
||||
"is_active": has_required_scope(request, ["premium"]),
|
||||
"user_photo": user_picture,
|
||||
},
|
||||
)
|
||||
|
||||
agent_metadata = {
|
||||
"slug": agent.slug,
|
||||
"avatar": agent.avatar,
|
||||
"name": agent.name,
|
||||
"personality": agent.personality,
|
||||
"public": agent.public,
|
||||
"creator": agent.creator.username if agent.creator else None,
|
||||
"managed_by_admin": agent.managed_by_admin,
|
||||
"chat_model": agent.chat_model.chat_model,
|
||||
"creator_not_self": agent.creator != user,
|
||||
}
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"agent.html",
|
||||
context={
|
||||
"request": request,
|
||||
"agent": agent_metadata,
|
||||
"khoj_version": state.khoj_version,
|
||||
"username": user.username if user else None,
|
||||
"has_documents": False,
|
||||
"is_active": has_required_scope(request, ["premium"]),
|
||||
"user_photo": user_picture,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user