mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 21:29:11 +00:00
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:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
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">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>
|
||||||
201
src/khoj/interface/web/agents.html
Normal file
201
src/khoj/interface/web/agents.html
Normal 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>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = "×";
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
39
src/khoj/routers/api_agents.py
Normal file
39
src/khoj/routers/api_agents.py
Normal 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)
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user