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):
# 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
@ -275,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)

View file

@ -402,8 +402,29 @@ class AgentAdapters:
return await Agent.objects.filter(id=agent_id).afirst()
@staticmethod
def get_all_acessible_agents(user: KhojUser = None):
return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct()
async def aget_agent_by_slug(agent_slug: str):
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
def get_conversation_agent_by_id(agent_id: int):
@ -419,12 +440,16 @@ class AgentAdapters:
@staticmethod
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_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.
return Agent.objects.create(
name=AgentAdapters.DEFAULT_AGENT_NAME,
@ -482,10 +507,12 @@ class ConversationAdapters:
@staticmethod
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:
agent = await AgentAdapters.aget_agent_by_id(id)
if agent_slug:
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)

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
from django.conf import settings
@ -23,6 +23,7 @@ class Migration(migrations.Migration):
("tools", models.JSONField(default=list)),
("public", 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",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="database.chatmodeloptions"),

View file

@ -1,4 +1,5 @@
import uuid
from random import choice
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
@ -92,13 +93,25 @@ class Agent(BaseModel):
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, default=None, null=True, blank=True)
@receiver(pre_save, sender=Agent)
def check_public_name(sender, instance, **kwargs):
if instance.public:
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():
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):

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);
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;

View file

@ -162,7 +162,7 @@
height: 40px;
}
.card-title {
font-size: 20px;
font-size: medium;
font-weight: normal;
margin: 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>
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
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()
@ -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('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;
@ -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.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 || [];
@ -919,12 +946,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 = "&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() {
@ -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('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>
@ -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>
</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
@ -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);
text-align: center;
font-family: var(--font-family);
font-size: 20px;
font-size: medium;
font-weight: 300;
line-height: 1.5em;
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;
}
#chat-section-wrapper.mobile-friendly {
grid-template-columns: auto auto;
}
#chat-body-wrapper {
display: flex;
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);
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;
}
@ -1470,8 +1598,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 {
@ -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) {
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 > * {
@ -1882,6 +2014,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;
}
@ -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;
}
#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 {
0% {
top: 36px;

View file

@ -9,26 +9,28 @@
<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 %}

View file

@ -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,

View file

@ -23,7 +23,7 @@ Today is {current_date} in UTC.
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.
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,
)
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(
@ -148,12 +161,12 @@ def chat_sessions(
async def create_chat_session(
request: Request,
common: CommonQueryParams,
agent_id: Optional[int] = None,
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}

View file

@ -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")

View file

@ -115,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"))
@ -125,14 +125,74 @@ 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):
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)