diff --git a/src/khoj/configure.py b/src/khoj/configure.py
index 4cc2ff3a..ab0b2649 100644
--- a/src/khoj/configure.py
+++ b/src/khoj/configure.py
@@ -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)
diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py
index 54cce808..87314726 100644
--- a/src/khoj/database/adapters/__init__.py
+++ b/src/khoj/database/adapters/__init__.py
@@ -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)
diff --git a/src/khoj/database/migrations/0031_agent_conversation_agent.py b/src/khoj/database/migrations/0031_agent_conversation_agent.py
index 16586499..e3742dbc 100644
--- a/src/khoj/database/migrations/0031_agent_conversation_agent.py
+++ b/src/khoj/database/migrations/0031_agent_conversation_agent.py
@@ -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"),
diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py
index 5272a1f6..846b3318 100644
--- a/src/khoj/database/models/__init__.py
+++ b/src/khoj/database/models/__init__.py
@@ -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):
diff --git a/src/khoj/interface/web/agent.html b/src/khoj/interface/web/agent.html
new file mode 100644
index 00000000..e64e6076
--- /dev/null
+++ b/src/khoj/interface/web/agent.html
@@ -0,0 +1,286 @@
+
+
+
+
+ Khoj - Agents
+
+
+
+
+
+
+
+
+ {% import 'utils.html' as utils %}
+ {{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
+
+
+
+
+
+
diff --git a/src/khoj/interface/web/agents.html b/src/khoj/interface/web/agents.html
new file mode 100644
index 00000000..cbb7757c
--- /dev/null
+++ b/src/khoj/interface/web/agents.html
@@ -0,0 +1,201 @@
+
+
+
+
+ Khoj - Agents
+
+
+
+
+
+
+
+
+ {% import 'utils.html' as utils %}
+ {{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
+
+
+
+
+
+ {% for agent in agents %}
+
+ {% endfor %}
+
+
+
+
+
+
+
diff --git a/src/khoj/interface/web/assets/khoj.css b/src/khoj/interface/web/assets/khoj.css
index 7ba93c6a..3d7e7d4a 100644
--- a/src/khoj/interface/web/assets/khoj.css
+++ b/src/khoj/interface/web/assets/khoj.css
@@ -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;
diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html
index 5c26e060..870f4eb9 100644
--- a/src/khoj/interface/web/base_config.html
+++ b/src/khoj/interface/web/base_config.html
@@ -162,7 +162,7 @@
height: 40px;
}
.card-title {
- font-size: 20px;
+ font-size: medium;
font-weight: normal;
margin: 0;
padding: 0;
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html
index ef45b0db..7bbb0ebb 100644
--- a/src/khoj/interface/web/chat.html
+++ b/src/khoj/interface/web/chat.html
@@ -12,15 +12,16 @@
@@ -1196,13 +1309,27 @@ To get started, just start typing below. You can also type / to see a list of co
+
+
+
+