Add web UI views for agents

- Add a page to view all agents
- Add slugs to manage agents
- Add a view to view single agent
- Display active agent when in chat window
- Fix post-login redirect issue
This commit is contained in:
sabaimran
2024-03-14 00:07:36 +05:30
parent 6ab649312f
commit 290712c3fe
16 changed files with 1003 additions and 61 deletions

View File

@@ -268,6 +268,7 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non
def configure_routes(app): def configure_routes(app):
# Import APIs here to setup search types before while configuring server # Import APIs here to setup search types before while configuring server
from khoj.routers.api import api 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_chat import api_chat
from khoj.routers.api_config import api_config from khoj.routers.api_config import api_config
from khoj.routers.indexer import indexer from khoj.routers.indexer import indexer
@@ -275,6 +276,7 @@ def configure_routes(app):
app.include_router(api, prefix="/api") app.include_router(api, prefix="/api")
app.include_router(api_chat, prefix="/api/chat") 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(api_config, prefix="/api/config")
app.include_router(indexer, prefix="/api/v1/index") app.include_router(indexer, prefix="/api/v1/index")
app.include_router(web_client) app.include_router(web_client)

View File

@@ -402,8 +402,29 @@ class AgentAdapters:
return await Agent.objects.filter(id=agent_id).afirst() return await Agent.objects.filter(id=agent_id).afirst()
@staticmethod @staticmethod
def get_all_acessible_agents(user: KhojUser = None): async def aget_agent_by_slug(agent_slug: str):
return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct() return await Agent.objects.filter(slug__iexact=agent_slug.lower()).afirst()
@staticmethod
def get_agent_by_slug(slug: str, user: KhojUser = None):
agent = Agent.objects.filter(slug=slug).first()
# Check if agent is public or created by the user
if agent and (agent.public or agent.creator == user):
return agent
return None
@staticmethod
def get_all_accessible_agents(user: KhojUser = None):
return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct().order_by("created_at")
@staticmethod
async def aget_all_accessible_agents(user: KhojUser = None) -> List[Agent]:
get_all_accessible_agents = sync_to_async(
lambda: Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct().order_by("created_at").all(),
thread_sensitive=True,
)
agents = await get_all_accessible_agents()
return await sync_to_async(list)(agents)
@staticmethod @staticmethod
def get_conversation_agent_by_id(agent_id: int): def get_conversation_agent_by_id(agent_id: int):
@@ -419,12 +440,16 @@ class AgentAdapters:
@staticmethod @staticmethod
def create_default_agent(): def create_default_agent():
# First delete the existing default
Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).delete()
default_conversation_config = ConversationAdapters.get_default_conversation_config() default_conversation_config = ConversationAdapters.get_default_conversation_config()
default_personality = prompts.personality.format(current_date="placeholder") default_personality = prompts.personality.format(current_date="placeholder")
if Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).exists():
agent = Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
agent.tuning = default_personality
agent.chat_model = default_conversation_config
agent.save()
return agent
# The default agent is public and managed by the admin. It's handled a little differently than other agents. # The default agent is public and managed by the admin. It's handled a little differently than other agents.
return Agent.objects.create( return Agent.objects.create(
name=AgentAdapters.DEFAULT_AGENT_NAME, name=AgentAdapters.DEFAULT_AGENT_NAME,
@@ -482,10 +507,12 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def acreate_conversation_session( async def acreate_conversation_session(
user: KhojUser, client_application: ClientApplication = None, agent_id: int = None user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None
): ):
if agent_id: if agent_slug:
agent = await AgentAdapters.aget_agent_by_id(id) agent = await AgentAdapters.aget_agent_by_slug(agent_slug)
if agent is None:
raise HTTPException(status_code=400, detail="Invalid agent id")
return await Conversation.objects.acreate(user=user, client=client_application, agent=agent) return await Conversation.objects.acreate(user=user, client=client_application, agent=agent)
return await Conversation.objects.acreate(user=user, client=client_application) return await Conversation.objects.acreate(user=user, client=client_application)

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.2.10 on 2024-03-11 05:12 # Generated by Django 4.2.10 on 2024-03-13 07:38
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -23,6 +23,7 @@ class Migration(migrations.Migration):
("tools", models.JSONField(default=list)), ("tools", models.JSONField(default=list)),
("public", models.BooleanField(default=False)), ("public", models.BooleanField(default=False)),
("managed_by_admin", models.BooleanField(default=False)), ("managed_by_admin", models.BooleanField(default=False)),
("slug", models.CharField(blank=True, default=None, max_length=200, null=True)),
( (
"chat_model", "chat_model",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="database.chatmodeloptions"), models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="database.chatmodeloptions"),

View File

@@ -1,4 +1,5 @@
import uuid import uuid
from random import choice
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -92,13 +93,25 @@ class Agent(BaseModel):
public = models.BooleanField(default=False) public = models.BooleanField(default=False)
managed_by_admin = models.BooleanField(default=False) managed_by_admin = models.BooleanField(default=False)
chat_model = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE) chat_model = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE)
slug = models.CharField(max_length=200, default=None, null=True, blank=True)
@receiver(pre_save, sender=Agent) @receiver(pre_save, sender=Agent)
def check_public_name(sender, instance, **kwargs): def verify_agent(sender, instance, **kwargs):
if instance.public: # check if this is a new instance
if instance._state.adding:
if Agent.objects.filter(name=instance.name, public=True).exists(): if Agent.objects.filter(name=instance.name, public=True).exists():
raise ValidationError(f"A public Agent with the name {instance.name} already 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():
random_number = choice([i for i in range(0, 10000) if i not in observed_random_numbers])
observed_random_numbers.add(random_number)
slug = f"{slug}-{random_number}"
instance.slug = slug
class NotionConfig(BaseModel): class NotionConfig(BaseModel):

View 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">Instructions</div>
<div id="agent-tuning">
<p>{{ agent.tuning }}</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;
}
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;
grid-template-rows: auto auto minmax(80px, 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(agentId) {
let response = await fetch(`/api/chat/sessions?agent_slug=${agentId}`, { 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>

View File

@@ -0,0 +1,201 @@
<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.tuning }}</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 minmax(80px, 100%) auto;
}
h1#agents-list-title {
margin: 0;
}
.agent-info p {
height: 50px; /* Adjust this value as needed */
overflow: auto;
margin: 0px;
}
div.agent-info {
font-size: medium;
}
div.agent-info a,
div.agent-info h2 {
margin: 0;
}
div.agent img {
width: 50px;
border-radius: 50%;
}
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: 50%;
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(agentId) {
let response = await fetch(`/api/chat/sessions?agent_slug=${agentId}`, { 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>

View File

@@ -130,7 +130,7 @@ img.khoj-logo {
background-color: var(--background-color); background-color: var(--background-color);
min-width: 160px; min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
right: 15vw; right: 5vw;
top: 64px; top: 64px;
z-index: 1; z-index: 1;
opacity: 0; opacity: 0;

View File

@@ -162,7 +162,7 @@
height: 40px; height: 40px;
} }
.card-title { .card-title {
font-size: 20px; font-size: medium;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@@ -12,15 +12,16 @@
<script type="text/javascript" src="/static/assets/markdown-it.min.js?v={{ khoj_version }}"></script> <script type="text/javascript" src="/static/assets/markdown-it.min.js?v={{ khoj_version }}"></script>
<script> <script>
let welcome_message = ` 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 - 🧠 Answer general knowledge questions
- 💡 Be a sounding board for your ideas - 💡 Be a sounding board for your ideas
- 📜 Chat with your notes & documents - 📜 Chat with your notes & documents
- 🌄 Generate images based on your messages - 🌄 Generate images based on your messages
- 🔎 Search the web for answers to your questions - 🔎 Search the web for answers to your questions
- 🎙️ Listen to your audio messages (use the mic by the input box to speak your message) - 🎙️ Listen to your audio messages (use the mic by the input box to speak your message)
- 📚 Understand files you drag & drop here
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. To get started, just start typing below. You can also type / to see a list of commands.
`.trim() `.trim()
@@ -115,7 +116,6 @@ To get started, just start typing below. You can also type / to see a list of co
linkElement.setAttribute('href', link); linkElement.setAttribute('href', link);
linkElement.setAttribute('target', '_blank'); linkElement.setAttribute('target', '_blank');
linkElement.setAttribute('rel', 'noopener noreferrer'); linkElement.setAttribute('rel', 'noopener noreferrer');
linkElement.classList.add("inline-chat-link");
linkElement.classList.add("reference-link"); linkElement.classList.add("reference-link");
linkElement.setAttribute('title', title); linkElement.setAttribute('title', title);
linkElement.textContent = title; linkElement.textContent = title;
@@ -827,6 +827,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.conversationId = response.conversation_id;
chatBody.dataset.conversationTitle = response.slug || `New conversation 🌱`; 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"); let chatBodyWrapper = document.getElementById("chat-body-wrapper");
const fullChatLog = response.chat || []; const fullChatLog = response.chat || [];
@@ -919,12 +946,100 @@ To get started, just start typing below. You can also type / to see a list of co
} }
function createNewConversation() { function createNewConversation() {
let chatBody = document.getElementById("chat-body"); // Create a modal that appears in the middle of the entire screen. It should have a form to create a new conversation.
chatBody.innerHTML = ""; let modal = document.createElement('div');
flashStatusInChatInput("📝 New conversation started"); modal.classList.add("modal");
chatBody.dataset.conversationId = ""; modal.id = "new-conversation-modal";
chatBody.dataset.conversationTitle = ""; let modalContent = document.createElement('div');
renderMessage(welcome_message, "khoj"); 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 = "&times;";
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() { function refreshChatSessionsPanel() {
@@ -1175,8 +1290,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('new-conversation').classList.toggle('collapsed');
document.getElementById('existing-conversations').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('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> </script>
<body> <body>
@@ -1196,13 +1309,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> <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> </svg>
</button> </button>
<div id="conversation-list-header" style="display: none;">Conversations</div>
</div> </div>
<div id="existing-conversations"> <div id="existing-conversations">
<div id="conversation-list"> <div id="conversation-list">
<div id="conversation-list-header" style="display: none;">Recent Conversations</div>
<div id="conversation-list-body"></div> <div id="conversation-list-body"></div>
</div> </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>
<div id="collapse-side-panel"> <div id="collapse-side-panel">
<button <button
@@ -1278,7 +1405,7 @@ To get started, just start typing below. You can also type / to see a list of co
color: var(--main-text-color); color: var(--main-text-color);
text-align: center; text-align: center;
font-family: var(--font-family); font-family: var(--font-family);
font-size: 20px; font-size: medium;
font-weight: 300; font-weight: 300;
line-height: 1.5em; line-height: 1.5em;
height: 100vh; height: 100vh;
@@ -1429,10 +1556,6 @@ To get started, just start typing below. You can also type / to see a list of co
overflow-y: scroll; overflow-y: scroll;
} }
#chat-section-wrapper.mobile-friendly {
grid-template-columns: auto auto;
}
#chat-body-wrapper { #chat-body-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1445,10 +1568,15 @@ To get started, just start typing below. You can also type / to see a list of co
background: var(--background-color); background: var(--background-color);
border-radius: 5px; border-radius: 5px;
box-shadow: 0 0 11px #aaa; box-shadow: 0 0 11px #aaa;
overflow-y: scroll;
text-align: left; text-align: left;
transition: width 0.3s ease-in-out; 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; overflow-y: auto;
} }
@@ -1470,8 +1598,12 @@ To get started, just start typing below. You can also type / to see a list of co
grid-gap: 8px; grid-gap: 8px;
} }
div#conversation-list {
height: 1;
}
div#side-panel-wrapper { div#side-panel-wrapper {
display: flex display: flex;
} }
#chat-body { #chat-body {
@@ -1861,7 +1993,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) { @media only screen and (min-width: 700px) {
body { 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; grid-template-rows: auto auto minmax(80px, 100%) auto;
} }
body > * { body > * {
@@ -1882,6 +2014,7 @@ To get started, just start typing below. You can also type / to see a list of co
div#new-conversation { div#new-conversation {
text-align: left; text-align: left;
border-bottom: 1px solid var(--main-text-color); border-bottom: 1px solid var(--main-text-color);
margin-top: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -2037,6 +2170,169 @@ To get started, just start typing below. You can also type / to see a list of co
animation-delay: -0.5s; 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 */
}
.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 { @keyframes lds-ripple {
0% { 0% {
top: 36px; top: 36px;

View File

@@ -9,26 +9,28 @@
<a id="search-nav" class="khoj-nav" href="/search">🔎 Search</a> <a id="search-nav" class="khoj-nav" href="/search">🔎 Search</a>
{% endif %} {% endif %}
<!-- Dropdown Menu --> <!-- Dropdown Menu -->
<div id="khoj-nav-menu-container" class="khoj-nav dropdown"> {% if username %}
{% if user_photo and user_photo != "None" %} <div id="khoj-nav-menu-container" class="khoj-nav dropdown">
{% if is_active %} {% if user_photo and user_photo != "None" %}
<img id="profile-picture" class="circle subscribed" src="{{ user_photo }}" alt="{{ username[0].upper() }}" onclick="toggleMenu()" referrerpolicy="no-referrer"> {% 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 %} {% 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 %} {% endif %}
{% else %} <div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
{% if is_active %} <div class="khoj-nav-username"> {{ username }} </div>
<div id="profile-picture" class="circle user-initial subscribed" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div> <a id="settings-nav" class="khoj-nav" href="/config">⚙️ Settings</a>
{% else %} <a class="khoj-nav" href="/auth/logout">🔑 Logout</a>
<div id="profile-picture" class="circle user-initial" alt="{{ username[0].upper() }}" onclick="toggleMenu()">{{ username[0].upper() }}</div> </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> </div>
</div> {% endif %}
</nav> </nav>
</div> </div>
{%- endmacro %} {%- endmacro %}

View File

@@ -114,7 +114,7 @@ class MarkdownToEntries(TextToEntries):
# Append base filename to compiled entry for context to model # Append base filename to compiled entry for context to model
# Increment heading level for heading entries and make filename as its top level heading # Increment heading level for heading entries and make filename as its top level heading
prefix = f"# {stem}\n#" if heading else f"# {stem}\n" 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( entries.append(
Entry( Entry(
compiled=compiled_entry, compiled=compiled_entry,

View File

@@ -23,7 +23,7 @@ Today is {current_date} in UTC.
custom_personality = PromptTemplate.from_template( custom_personality = PromptTemplate.from_template(
""" """
Your are {name}, a personal agent on Khoj. You are {name}, a personal agent on Khoj.
Use your general knowledge and past conversation with the user as context to inform your responses. 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 were created by Khoj Inc. with the following capabilities:

View File

@@ -0,0 +1,39 @@
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,
"tuning": agent.tuning,
"public": agent.public,
"creator": agent.creator.username if agent.creator else None,
"managed_by_admin": agent.managed_by_admin,
}
)
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)

View File

@@ -81,9 +81,22 @@ def chat_history(
status_code=404, 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 = conversation.conversation_log
meta_log.update( 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( update_telemetry_state(
@@ -148,12 +161,12 @@ def chat_sessions(
async def create_chat_session( async def create_chat_session(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
agent_id: Optional[int] = None, agent_slug: Optional[str] = None,
): ):
user = request.user.object user = request.user.object
# Create new Conversation Session # 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} response = {"conversation_id": conversation.id}

View File

@@ -7,6 +7,7 @@ from starlette.authentication import requires
from starlette.config import Config from starlette.config import Config
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse, RedirectResponse, Response from starlette.responses import HTMLResponse, RedirectResponse, Response
from starlette.status import HTTP_302_FOUND
from khoj.database.adapters import ( from khoj.database.adapters import (
create_khoj_token, create_khoj_token,
@@ -90,6 +91,7 @@ async def delete_token(request: Request, token: str) -> str:
@auth_router.post("/redirect") @auth_router.post("/redirect")
async def auth(request: Request): async def auth(request: Request):
form = await request.form() form = await request.form()
next_url = request.query_params.get("next", "/")
credential = form.get("credential") credential = form.get("credential")
csrf_token_cookie = request.cookies.get("g_csrf_token") 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)}, metadata={"user_id": str(khoj_user.uuid)},
) )
logger.log(logging.INFO, f"New User Created: {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") @auth_router.get("/logout")

View File

@@ -115,8 +115,8 @@ def chat_page(request: Request):
@web_client.get("/login", response_class=FileResponse) @web_client.get("/login", response_class=FileResponse)
def login_page(request: Request): def login_page(request: Request):
next_url = request.query_params.get("next", "/")
if request.user.is_authenticated: if request.user.is_authenticated:
next_url = request.query_params.get("next", "/")
return RedirectResponse(url=next_url) return RedirectResponse(url=next_url)
google_client_id = os.environ.get("GOOGLE_CLIENT_ID") google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
redirect_uri = str(request.app.url_path_for("auth")) redirect_uri = str(request.app.url_path_for("auth"))
@@ -125,14 +125,74 @@ def login_page(request: Request):
context={ context={
"request": request, "request": request,
"google_client_id": google_client_id, "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) @web_client.get("/agents", response_class=HTMLResponse)
def agents_page(request: Request): def agents_page(request: Request):
agents = AgentAdapters.get_all_acessible_agents(request.user.object if request.user.is_authenticated else None) 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,
"tuning": agent.tuning,
"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 agents_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)
agent_metadata = {
"slug": agent.slug,
"avatar": agent.avatar,
"name": agent.name,
"tuning": agent.tuning,
"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,
},
)
@web_client.get("/config", response_class=HTMLResponse) @web_client.get("/config", response_class=HTMLResponse)