mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 09:25:06 +01:00
Part 2: Add web UI updates for basic agent interactions (#675)
* Initial pass at backend changes to support agents - Add a db model for Agents, attaching them to conversations - When an agent is added to a conversation, override the system prompt to tweak the instructions - Agents can be configured with prompt modification, model specification, a profile picture, and other things - Admin-configured models will not be editable by individual users - Add unit tests to verify agent behavior. Unit tests demonstrate imperfect adherence to prompt specifications * Customize default behaviors for conversations without agents or with default agents * Add a new web client route for viewing all agents * Use agent_id for getting correct agent * 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 * Fix agent view * Spruce up the 404 page and improve the overall layout for agents pages * Create chat actor for directly reading webpages based on user message - Add prompt for the read webpages chat actor to extract, infer webpage links - Make chat actor infer or extract webpage to read directly from user message - Rename previous read_webpage function to more narrow read_webpage_at_url function * Rename agents_page -> agent_page * Fix unit test for adding the filename to the compiled markdown entry * Fix layout of agent, agents pages * Merge migrations * Let the name, slug of the default agent be Khoj, khoj * Fix chat-related unit tests * Add webpage chat command for read web pages requested by user Update auto chat command inference prompt to show example of when to use webpage chat command (i.e when url is directly provided in link) * Support webpage command in chat API - Fallback to use webpage when SERPER not setup and online command was attempted - Do not stop responding if can't retrieve online results. Try to respond without the online context * Test select webpage as data source and extract web urls chat actors * Tweak prompts to extract information from webpages, online results - Show more of the truncated messages for debugging context - Update Khoj personality prompt to encourage it to remember it's capabilities * Rename extract_content online results field to webpages * Parallelize simple webpage read and extractor Similar to what is being done with search_online with olostep * Pass multiple webpages with their urls in online results context Previously even if MAX_WEBPAGES_TO_READ was > 1, only 1 extracted content would ever be passed. URL of the extracted webpage content wasn't passed to clients in online results context. This limited them from being rendered * Render webpage read in chat response references on Web, Desktop apps * Time chat actor responses & chat api request start for perf analysis * Increase the keep alive timeout in the main application for testing * Do not pipe access/error logs to separate files. Flow to stdout/stderr * [Temp] Reduce to 1 gunicorn worker * Change prod docker image to use jammy, rather than nvidia base image * Use Khoj icon when Khoj web is installed on iOS as a PWA * Make slug required for agents * Simplify calling logic and prevent agent access for unauthenticated users * Standardize to use personality over tuning in agent nomenclature * Make filtering logic more stringent for accessible agents and remove unused method: * Format chat message query --------- Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
This commit is contained in:
parent
8abc8ded82
commit
fdf78525b4
31 changed files with 1332 additions and 130 deletions
|
@ -1,10 +1,10 @@
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
bind = "0.0.0.0:42110"
|
bind = "0.0.0.0:42110"
|
||||||
workers = 8
|
workers = 1
|
||||||
worker_class = "uvicorn.workers.UvicornWorker"
|
worker_class = "uvicorn.workers.UvicornWorker"
|
||||||
timeout = 120
|
timeout = 120
|
||||||
keep_alive = 60
|
keep_alive = 60
|
||||||
accesslog = "access.log"
|
accesslog = "-"
|
||||||
errorlog = "error.log"
|
errorlog = "-"
|
||||||
loglevel = "debug"
|
loglevel = "debug"
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
# Use Nvidia's latest Ubuntu 22.04 image as the base image
|
FROM ubuntu:jammy
|
||||||
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
|
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
|
||||||
|
|
||||||
# Install System Dependencies
|
# Install System Dependencies
|
||||||
RUN apt update -y && apt -y install python3-pip libsqlite3-0 ffmpeg libsm6 libxext6
|
RUN apt update -y && apt -y install python3-pip libsqlite3-0 ffmpeg libsm6 libxext6
|
||||||
# Install Optional Dependencies
|
|
||||||
RUN apt install vim -y
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
function generateOnlineReference(reference, index) {
|
function generateOnlineReference(reference, index) {
|
||||||
|
|
||||||
// Generate HTML for Chat Reference
|
// Generate HTML for Chat Reference
|
||||||
let title = reference.title;
|
let title = reference.title || reference.link;
|
||||||
let link = reference.link;
|
let link = reference.link;
|
||||||
let snippet = reference.snippet;
|
let snippet = reference.snippet;
|
||||||
let question = reference.question;
|
let question = reference.question;
|
||||||
|
@ -191,6 +191,15 @@
|
||||||
referenceSection.appendChild(polishedReference);
|
referenceSection.appendChild(polishedReference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
|
||||||
|
numOnlineReferences += onlineReference.webpages.length;
|
||||||
|
for (let index in onlineReference.webpages) {
|
||||||
|
let reference = onlineReference.webpages[index];
|
||||||
|
let polishedReference = generateOnlineReference(reference, index);
|
||||||
|
referenceSection.appendChild(polishedReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return numOnlineReferences;
|
return numOnlineReferences;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -394,20 +394,32 @@ class ClientApplicationAdapters:
|
||||||
|
|
||||||
|
|
||||||
class AgentAdapters:
|
class AgentAdapters:
|
||||||
DEFAULT_AGENT_NAME = "khoj"
|
DEFAULT_AGENT_NAME = "Khoj"
|
||||||
DEFAULT_AGENT_AVATAR = "https://khoj-web-bucket.s3.amazonaws.com/lamp-128.png"
|
DEFAULT_AGENT_AVATAR = "https://khoj-web-bucket.s3.amazonaws.com/lamp-128.png"
|
||||||
|
DEFAULT_AGENT_SLUG = "khoj"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def aget_agent_by_id(agent_id: int, user: KhojUser):
|
async def aget_agent_by_slug(agent_slug: str, user: KhojUser):
|
||||||
agent = await Agent.objects.filter(id=agent_id).afirst()
|
return await Agent.objects.filter(
|
||||||
# Check if it's accessible to the user
|
(Q(slug__iexact=agent_slug.lower())) & (Q(public=True) | Q(creator=user))
|
||||||
if agent and (agent.public or agent.creator == user):
|
).afirst()
|
||||||
return agent
|
|
||||||
return None
|
@staticmethod
|
||||||
|
def get_agent_by_slug(slug: str, user: KhojUser = None):
|
||||||
|
if user:
|
||||||
|
return Agent.objects.filter((Q(slug__iexact=slug.lower())) & (Q(public=True) | Q(creator=user))).first()
|
||||||
|
return Agent.objects.filter(slug__iexact=slug.lower(), public=True).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_accessible_agents(user: KhojUser = None):
|
def get_all_accessible_agents(user: KhojUser = None):
|
||||||
return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct()
|
if user:
|
||||||
|
return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct().order_by("created_at")
|
||||||
|
return Agent.objects.filter(public=True).order_by("created_at")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def aget_all_accessible_agents(user: KhojUser = None) -> List[Agent]:
|
||||||
|
agents = await sync_to_async(AgentAdapters.get_all_accessible_agents)(user)
|
||||||
|
return await sync_to_async(list)(agents)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_conversation_agent_by_id(agent_id: int):
|
def get_conversation_agent_by_id(agent_id: int):
|
||||||
|
@ -423,12 +435,19 @@ 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")
|
||||||
|
|
||||||
|
agent = Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
|
||||||
|
|
||||||
|
if agent:
|
||||||
|
agent.personality = default_personality
|
||||||
|
agent.chat_model = default_conversation_config
|
||||||
|
agent.slug = AgentAdapters.DEFAULT_AGENT_SLUG
|
||||||
|
agent.name = AgentAdapters.DEFAULT_AGENT_NAME
|
||||||
|
agent.save()
|
||||||
|
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,
|
||||||
|
@ -438,6 +457,7 @@ class AgentAdapters:
|
||||||
personality=default_personality,
|
personality=default_personality,
|
||||||
tools=["*"],
|
tools=["*"],
|
||||||
avatar=AgentAdapters.DEFAULT_AGENT_AVATAR,
|
avatar=AgentAdapters.DEFAULT_AGENT_AVATAR,
|
||||||
|
slug=AgentAdapters.DEFAULT_AGENT_SLUG,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -486,10 +506,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(agent_id, user)
|
agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
|
||||||
|
if agent is None:
|
||||||
|
raise HTTPException(status_code=400, detail="No such agent currently exists.")
|
||||||
return await Conversation.objects.acreate(user=user, client=client_application, agent=agent)
|
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(max_length=200)),
|
||||||
(
|
(
|
||||||
"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
|
||||||
|
@ -94,13 +95,28 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@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():
|
||||||
|
try:
|
||||||
|
random_number = choice([i for i in range(0, 1000) if i not in observed_random_numbers])
|
||||||
|
except IndexError:
|
||||||
|
raise ValidationError("Unable to generate a unique slug for the Agent. Please try again later.")
|
||||||
|
observed_random_numbers.add(random_number)
|
||||||
|
slug = f"{slug}-{random_number}"
|
||||||
|
instance.slug = slug
|
||||||
|
|
||||||
|
|
||||||
class NotionConfig(BaseModel):
|
class NotionConfig(BaseModel):
|
||||||
|
|
|
@ -2,14 +2,19 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Khoj: An AI Personal Assistant for your digital brain</title>
|
<title>Khoj: An AI Personal Assistant for your digital brain</title>
|
||||||
<link rel=”stylesheet” href=”static/styles.css”>
|
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
|
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||||
|
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||||
</head>
|
</head>
|
||||||
<body class="not-found">
|
<body class="not-found">
|
||||||
|
<!--Add Header Logo and Nav Pane-->
|
||||||
|
{% import 'utils.html' as utils %}
|
||||||
|
{{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
|
||||||
|
|
||||||
<header class=”header”>
|
<header class=”header”>
|
||||||
<h1>Oops, this is awkward. That page couldn't be found.</h1>
|
<h1>Oops, this is awkward. Looks like there's nothing here.</h1>
|
||||||
</header>
|
</header>
|
||||||
<a href="/config">Go Home</a>
|
<a class="redirect-link" href="/">Go Home</a>
|
||||||
|
|
||||||
<footer class=”footer”>
|
<footer class=”footer”>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -18,5 +23,34 @@
|
||||||
body.not-found {
|
body.not-found {
|
||||||
padding: 0 10%
|
padding: 0 10%
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: medium;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5em;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a.redirect-link {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--main-text-color);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a.redirect-link:hover {
|
||||||
|
background-color: var(--main-text-color);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</html>
|
</html>
|
||||||
|
|
286
src/khoj/interface/web/agent.html
Normal file
286
src/khoj/interface/web/agent.html
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||||
|
<title>Khoj - Agents</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||||
|
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||||
|
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||||
|
</head>
|
||||||
|
<script type="text/javascript" src="/static/assets/utils.js?v={{ khoj_version }}"></script>
|
||||||
|
<body>
|
||||||
|
<!--Add Header Logo and Nav Pane-->
|
||||||
|
{% import 'utils.html' as utils %}
|
||||||
|
{{ utils.heading_pane(user_photo, username, is_active, has_documents) }}
|
||||||
|
<div id="agent-metadata-wrapper">
|
||||||
|
<div id="agent-metadata">
|
||||||
|
<div id="agent-avatar-wrapper">
|
||||||
|
<div id="agent-settings-header">Agent Settings</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div id="agent-data-wrapper">
|
||||||
|
<div id="agent-avatar-wrapper">
|
||||||
|
<img id="agent-avatar" src="{{ agent.avatar }}" alt="Agent Avatar">
|
||||||
|
<input type="text" id="agent-name-input" value="{{ agent.name }}" {% if agent.creator_not_self %} disabled {% endif %}>
|
||||||
|
</div>
|
||||||
|
<div id="agent-instructions">Instructions</div>
|
||||||
|
<div id="agent-tuning">
|
||||||
|
<p>{{ agent.personality }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div id="agent-public">
|
||||||
|
<p>Public</p>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" {% if agent.public %} checked {% endif %} {% if agent.creator_not_self %} disabled {% endif %}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p id="agent-creator" style="display: none;">Creator: {{ agent.creator }}</p>
|
||||||
|
<p id="agent-managed-by-admin" style="display: none;">ⓘ This agent is managed by the administrator</p>
|
||||||
|
<button onclick="openChat('{{ agent.slug }}')">Chat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
<a href="/agents">All Agents</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
display: grid;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: medium;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5em;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#agent-settings-header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.divider {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 2px solid var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div#footer {
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-top: 1px solid var(--main-text-color);
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#footer a {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div#agent-data-wrapper button {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
font: inherit;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#agent-data-wrapper button:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
box-shadow: 0 0 10px var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
input#agent-name-input {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#agent-instructions {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-metadata {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
text-align: left;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-avatar-wrapper {
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-tuning, #agent-public, #agent-creator, #agent-managed-by-admin {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-tuning p {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-metadata p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agent-public {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(26px);
|
||||||
|
-ms-transform: translateX(26px);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
div#agent-data-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 700px) {
|
||||||
|
body {
|
||||||
|
grid-template-columns: auto min(70vw, 100%) auto;
|
||||||
|
}
|
||||||
|
body > * {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
#agent-metadata-wrapper {
|
||||||
|
display: block;
|
||||||
|
width: min(30vw, 100%);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
async function openChat(agentSlug) {
|
||||||
|
let response = await fetch(`/api/chat/sessions?agent_slug=${agentSlug}`, { method: "POST" });
|
||||||
|
let data = await response.json();
|
||||||
|
if (response.status == 200) {
|
||||||
|
window.location.href = "/";
|
||||||
|
} else {
|
||||||
|
alert("Failed to start chat session");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the agent-managed-by-admin paragraph if the agent is managed by the admin
|
||||||
|
// compare agent.managed_by_admin as a lowercase string to "true"
|
||||||
|
let isManagedByAdmin = "{{ agent.managed_by_admin }}".toLowerCase() === "true";
|
||||||
|
if (isManagedByAdmin) {
|
||||||
|
document.getElementById("agent-managed-by-admin").style.display = "block";
|
||||||
|
} else {
|
||||||
|
document.getElementById("agent-creator").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize the input field based on the length of the value
|
||||||
|
let input = document.getElementById("agent-name-input");
|
||||||
|
input.addEventListener("input", resizeInput);
|
||||||
|
resizeInput.call(input);
|
||||||
|
function resizeInput() {
|
||||||
|
this.style.width = this.value.length + 1 + "ch";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
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.personality }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="agent-info">
|
||||||
|
<button onclick="openChat('{{ agent.slug }}')">Talk</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
<a href="/">Back to Chat</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
display: grid;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: medium;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5em;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1#agents-list-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-info p {
|
||||||
|
height: 50px; /* 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(agentSlug) {
|
||||||
|
let response = await fetch(`/api/chat/sessions?agent_slug=${agentSlug}`, { method: "POST" });
|
||||||
|
let data = await response.json();
|
||||||
|
if (response.status == 200) {
|
||||||
|
window.location.href = "/";
|
||||||
|
} else if(response.status == 403 || response.status == 401) {
|
||||||
|
window.location.href = "/login?next=/agent/" + agentId;
|
||||||
|
} else {
|
||||||
|
alert("Failed to start chat session");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
|
@ -130,7 +130,7 @@ img.khoj-logo {
|
||||||
background-color: var(--background-color);
|
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;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<title>Khoj - Chat</title>
|
<title>Khoj - Chat</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||||
|
<link rel="apple-touch-icon" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||||
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||||
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||||
</head>
|
</head>
|
||||||
|
@ -12,15 +13,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()
|
||||||
|
@ -101,7 +103,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
function generateOnlineReference(reference, index) {
|
function generateOnlineReference(reference, index) {
|
||||||
|
|
||||||
// Generate HTML for Chat Reference
|
// Generate HTML for Chat Reference
|
||||||
let title = reference.title;
|
let title = reference.title || reference.link;
|
||||||
let link = reference.link;
|
let link = reference.link;
|
||||||
let snippet = reference.snippet;
|
let snippet = reference.snippet;
|
||||||
let question = reference.question;
|
let question = reference.question;
|
||||||
|
@ -115,7 +117,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;
|
||||||
|
@ -205,6 +206,15 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
referenceSection.appendChild(polishedReference);
|
referenceSection.appendChild(polishedReference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
|
||||||
|
numOnlineReferences += onlineReference.webpages.length;
|
||||||
|
for (let index in onlineReference.webpages) {
|
||||||
|
let reference = onlineReference.webpages[index];
|
||||||
|
let polishedReference = generateOnlineReference(reference, index);
|
||||||
|
referenceSection.appendChild(polishedReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return numOnlineReferences;
|
return numOnlineReferences;
|
||||||
|
@ -832,6 +842,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 || [];
|
||||||
|
|
||||||
|
@ -924,12 +961,100 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewConversation() {
|
function createNewConversation() {
|
||||||
|
// Create a modal that appears in the middle of the entire screen. It should have a form to create a new conversation.
|
||||||
|
let modal = document.createElement('div');
|
||||||
|
modal.classList.add("modal");
|
||||||
|
modal.id = "new-conversation-modal";
|
||||||
|
let modalContent = document.createElement('div');
|
||||||
|
modalContent.classList.add("modal-content");
|
||||||
|
let modalHeader = document.createElement('div');
|
||||||
|
modalHeader.classList.add("modal-header");
|
||||||
|
let modalTitle = document.createElement('h2');
|
||||||
|
modalTitle.textContent = "New Conversation";
|
||||||
|
let modalCloseButton = document.createElement('button');
|
||||||
|
modalCloseButton.classList.add("modal-close-button");
|
||||||
|
modalCloseButton.innerHTML = "×";
|
||||||
|
modalCloseButton.addEventListener('click', function() {
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
modalHeader.appendChild(modalTitle);
|
||||||
|
modalHeader.appendChild(modalCloseButton);
|
||||||
|
modalContent.appendChild(modalHeader);
|
||||||
|
let modalBody = document.createElement('div');
|
||||||
|
modalBody.classList.add("modal-body");
|
||||||
|
|
||||||
|
let agentDropDownPicker = document.createElement('select');
|
||||||
|
agentDropDownPicker.setAttribute("id", "agent-dropdown-picker");
|
||||||
|
agentDropDownPicker.setAttribute("name", "agent-dropdown-picker");
|
||||||
|
|
||||||
|
let agentDropDownLabel = document.createElement('label');
|
||||||
|
agentDropDownLabel.setAttribute("for", "agent-dropdown-picker");
|
||||||
|
agentDropDownLabel.textContent = "Who do you want to talk to?";
|
||||||
|
|
||||||
|
fetch('/api/agents')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.length > 0) {
|
||||||
|
data.forEach((agent) => {
|
||||||
|
let agentOption = document.createElement('option');
|
||||||
|
agentOption.setAttribute("value", agent.slug);
|
||||||
|
agentOption.textContent = agent.name;
|
||||||
|
agentDropDownPicker.appendChild(agentOption);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
let seeAllAgentsLink = document.createElement('a');
|
||||||
|
seeAllAgentsLink.setAttribute("href", "/agents");
|
||||||
|
seeAllAgentsLink.setAttribute("target", "_blank");
|
||||||
|
seeAllAgentsLink.textContent = "See all agents";
|
||||||
|
|
||||||
|
let newConversationSubmitButton = document.createElement('button');
|
||||||
|
newConversationSubmitButton.setAttribute("type", "submit");
|
||||||
|
newConversationSubmitButton.textContent = "Go";
|
||||||
|
newConversationSubmitButton.id = "new-conversation-submit-button";
|
||||||
|
|
||||||
|
newConversationSubmitButton.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let agentSlug = agentDropDownPicker.value;
|
||||||
|
let createURL = `/api/chat/sessions?client=web&agent_slug=${agentSlug}`;
|
||||||
let chatBody = document.getElementById("chat-body");
|
let chatBody = document.getElementById("chat-body");
|
||||||
chatBody.innerHTML = "";
|
fetch(createURL, { method: "POST" })
|
||||||
flashStatusInChatInput("📝 New conversation started");
|
.then(response => response.json())
|
||||||
chatBody.dataset.conversationId = "";
|
.then(data => {
|
||||||
chatBody.dataset.conversationTitle = "";
|
chatBody.dataset.conversationId = data.conversation_id;
|
||||||
renderMessage(welcome_message, "khoj");
|
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() {
|
||||||
|
@ -1184,8 +1309,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>
|
||||||
|
@ -1205,13 +1328,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
|
||||||
|
@ -1287,7 +1424,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;
|
||||||
|
@ -1438,10 +1575,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;
|
||||||
|
@ -1454,10 +1587,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1479,8 +1617,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 {
|
||||||
|
@ -1870,7 +2012,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 > * {
|
||||||
|
@ -1891,6 +2033,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2046,6 +2189,170 @@ 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 */
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
margin: 15% auto; /* 15% from the top and centered */
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 250px;
|
||||||
|
text-align: left;
|
||||||
|
background: var(--background-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 11px #aaa;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body a {
|
||||||
|
/* text-decoration: none; */
|
||||||
|
color: var(--summer-sun);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-button {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--summer-sun);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-button:hover,
|
||||||
|
.modal-close-button:focus {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#new-conversation-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#new-conversation-form label,
|
||||||
|
#new-conversation-form input,
|
||||||
|
#new-conversation-form button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#new-conversation-form button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body button {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button#new-conversation-submit-button {
|
||||||
|
background: var(--summer-sun);
|
||||||
|
transition: background 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button#close-button {
|
||||||
|
background: var(--background-color);
|
||||||
|
transition: background 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button#new-conversation-submit-button:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
button#close-button:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body select {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes lds-ripple {
|
@keyframes lds-ripple {
|
||||||
0% {
|
0% {
|
||||||
top: 36px;
|
top: 36px;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<title>Khoj - Search</title>
|
<title>Khoj - Search</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||||
|
<link rel="apple-touch-icon" href="/static/assets/icons/favicon-128x128.png?v={{ khoj_version }}">
|
||||||
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
<link rel="manifest" href="/static/khoj.webmanifest?v={{ khoj_version }}">
|
||||||
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
<link rel="stylesheet" href="/static/assets/khoj.css?v={{ khoj_version }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<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 -->
|
||||||
|
{% if username %}
|
||||||
<div id="khoj-nav-menu-container" class="khoj-nav dropdown">
|
<div id="khoj-nav-menu-container" class="khoj-nav dropdown">
|
||||||
{% if user_photo and user_photo != "None" %}
|
{% if user_photo and user_photo != "None" %}
|
||||||
{% if is_active %}
|
{% if is_active %}
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
<a class="khoj-nav" href="/auth/logout">🔑 Logout</a>
|
<a class="khoj-nav" href="/auth/logout">🔑 Logout</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
|
@ -160,7 +160,9 @@ def start_server(app, host=None, port=None, socket=None):
|
||||||
if socket:
|
if socket:
|
||||||
uvicorn.run(app, proxy_headers=True, uds=socket, log_level="debug", use_colors=True, log_config=None)
|
uvicorn.run(app, proxy_headers=True, uds=socket, log_level="debug", use_colors=True, log_config=None)
|
||||||
else:
|
else:
|
||||||
uvicorn.run(app, host=host, port=port, log_level="debug", use_colors=True, log_config=None)
|
uvicorn.run(
|
||||||
|
app, host=host, port=port, log_level="debug", use_colors=True, log_config=None, timeout_keep_alive=60
|
||||||
|
)
|
||||||
logger.info("🌒 Stopping Khoj")
|
logger.info("🌒 Stopping Khoj")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -188,8 +188,8 @@ def converse_offline(
|
||||||
if ConversationCommand.Online in conversation_commands:
|
if ConversationCommand.Online in conversation_commands:
|
||||||
simplified_online_results = online_results.copy()
|
simplified_online_results = online_results.copy()
|
||||||
for result in online_results:
|
for result in online_results:
|
||||||
if online_results[result].get("extracted_content"):
|
if online_results[result].get("webpages"):
|
||||||
simplified_online_results[result] = online_results[result]["extracted_content"]
|
simplified_online_results[result] = online_results[result]["webpages"]
|
||||||
|
|
||||||
conversation_primer = f"{prompts.online_search_conversation.format(online_results=str(simplified_online_results))}\n{conversation_primer}"
|
conversation_primer = f"{prompts.online_search_conversation.format(online_results=str(simplified_online_results))}\n{conversation_primer}"
|
||||||
if not is_none_or_empty(compiled_references_message):
|
if not is_none_or_empty(compiled_references_message):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from langchain.schema import ChatMessage
|
from langchain.schema import ChatMessage
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ def send_message_to_model(messages, api_key, model, response_type="text"):
|
||||||
def converse(
|
def converse(
|
||||||
references,
|
references,
|
||||||
user_query,
|
user_query,
|
||||||
online_results: Optional[dict] = None,
|
online_results: Optional[Dict[str, Dict]] = None,
|
||||||
conversation_log={},
|
conversation_log={},
|
||||||
model: str = "gpt-3.5-turbo",
|
model: str = "gpt-3.5-turbo",
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
|
@ -151,7 +151,7 @@ def converse(
|
||||||
completion_func(chat_response=prompts.no_online_results_found.format())
|
completion_func(chat_response=prompts.no_online_results_found.format())
|
||||||
return iter([prompts.no_online_results_found.format()])
|
return iter([prompts.no_online_results_found.format()])
|
||||||
|
|
||||||
if ConversationCommand.Online in conversation_commands:
|
if ConversationCommand.Online in conversation_commands or ConversationCommand.Webpage in conversation_commands:
|
||||||
conversation_primer = (
|
conversation_primer = (
|
||||||
f"{prompts.online_search_conversation.format(online_results=str(online_results))}\n{conversation_primer}"
|
f"{prompts.online_search_conversation.format(online_results=str(online_results))}\n{conversation_primer}"
|
||||||
)
|
)
|
||||||
|
@ -167,7 +167,7 @@ def converse(
|
||||||
max_prompt_size,
|
max_prompt_size,
|
||||||
tokenizer_name,
|
tokenizer_name,
|
||||||
)
|
)
|
||||||
truncated_messages = "\n".join({f"{message.content[:40]}..." for message in messages})
|
truncated_messages = "\n".join({f"{message.content[:70]}..." for message in messages})
|
||||||
logger.debug(f"Conversation Context for GPT: {truncated_messages}")
|
logger.debug(f"Conversation Context for GPT: {truncated_messages}")
|
||||||
|
|
||||||
# Get Response from GPT
|
# Get Response from GPT
|
||||||
|
|
|
@ -10,7 +10,7 @@ You were created by Khoj Inc. with the following capabilities:
|
||||||
|
|
||||||
- You *CAN REMEMBER ALL NOTES and PERSONAL INFORMATION FOREVER* that the user ever shares with you.
|
- You *CAN REMEMBER ALL NOTES and PERSONAL INFORMATION FOREVER* that the user ever shares with you.
|
||||||
- Users can share files and other information with you using the Khoj Desktop, Obsidian or Emacs app. They can also drag and drop their files into the chat window.
|
- Users can share files and other information with you using the Khoj Desktop, Obsidian or Emacs app. They can also drag and drop their files into the chat window.
|
||||||
- You can generate images, look-up information from the internet, and answer questions based on the user's notes.
|
- You *CAN* generate images, look-up real-time information from the internet, and answer questions based on the user's notes.
|
||||||
- You cannot set reminders.
|
- You cannot set reminders.
|
||||||
- Say "I don't know" or "I don't understand" if you don't know what to say or if you don't know the answer to a question.
|
- Say "I don't know" or "I don't understand" if you don't know what to say or if you don't know the answer to a question.
|
||||||
- Ask crisp follow-up questions to get additional context, when the answer cannot be inferred from the provided notes or past conversations.
|
- Ask crisp follow-up questions to get additional context, when the answer cannot be inferred from the provided notes or past conversations.
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -178,7 +178,8 @@ online_search_conversation = PromptTemplate.from_template(
|
||||||
Use this up-to-date information from the internet to inform your response.
|
Use this up-to-date information from the internet to inform your response.
|
||||||
Ask crisp follow-up questions to get additional context, when a helpful response cannot be provided from the online data or past conversations.
|
Ask crisp follow-up questions to get additional context, when a helpful response cannot be provided from the online data or past conversations.
|
||||||
|
|
||||||
Information from the internet: {online_results}
|
Information from the internet:
|
||||||
|
{online_results}
|
||||||
""".strip()
|
""".strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -312,7 +313,7 @@ Target Query: {query}
|
||||||
Web Pages:
|
Web Pages:
|
||||||
{corpus}
|
{corpus}
|
||||||
|
|
||||||
Collate the relevant information from the website to answer the target query.
|
Collate only relevant information from the website to answer the target query.
|
||||||
""".strip()
|
""".strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -394,6 +395,14 @@ AI: Good morning! How can I help you today?
|
||||||
Q: How can I share my files with Khoj?
|
Q: How can I share my files with Khoj?
|
||||||
Khoj: {{"source": ["default", "online"]}}
|
Khoj: {{"source": ["default", "online"]}}
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Chat History:
|
||||||
|
User: What is the first element in the periodic table?
|
||||||
|
AI: The first element in the periodic table is Hydrogen.
|
||||||
|
|
||||||
|
Q: Summarize this article https://en.wikipedia.org/wiki/Hydrogen
|
||||||
|
Khoj: {{"source": ["webpage"]}}
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
Chat History:
|
Chat History:
|
||||||
User: I want to start a new hobby. I'm thinking of learning to play the guitar.
|
User: I want to start a new hobby. I'm thinking of learning to play the guitar.
|
||||||
|
@ -412,6 +421,50 @@ Khoj:
|
||||||
""".strip()
|
""".strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
infer_webpages_to_read = PromptTemplate.from_template(
|
||||||
|
"""
|
||||||
|
You are Khoj, an advanced web page reading assistant. You are to construct **up to three, valid** webpage urls to read before answering the user's question.
|
||||||
|
- You will receive the conversation history as context.
|
||||||
|
- Add as much context from the previous questions and answers as required to construct the webpage urls.
|
||||||
|
- Use multiple web page urls if required to retrieve the relevant information.
|
||||||
|
- You have access to the the whole internet to retrieve information.
|
||||||
|
|
||||||
|
Which webpages will you need to read to answer the user's question?
|
||||||
|
Provide web page links as a list of strings in a JSON object.
|
||||||
|
Current Date: {current_date}
|
||||||
|
User's Location: {location}
|
||||||
|
|
||||||
|
Here are some examples:
|
||||||
|
History:
|
||||||
|
User: I like to use Hacker News to get my tech news.
|
||||||
|
AI: Hacker News is an online forum for sharing and discussing the latest tech news. It is a great place to learn about new technologies and startups.
|
||||||
|
|
||||||
|
Q: Summarize this post about vector database on Hacker News, https://news.ycombinator.com/item?id=12345
|
||||||
|
Khoj: {{"links": ["https://news.ycombinator.com/item?id=12345"]}}
|
||||||
|
|
||||||
|
History:
|
||||||
|
User: I'm currently living in New York but I'm thinking about moving to San Francisco.
|
||||||
|
AI: New York is a great city to live in. It has a lot of great restaurants and museums. San Francisco is also a great city to live in. It has good access to nature and a great tech scene.
|
||||||
|
|
||||||
|
Q: What is the climate like in those cities?
|
||||||
|
Khoj: {{"links": ["https://en.wikipedia.org/wiki/New_York_City", "https://en.wikipedia.org/wiki/San_Francisco"]}}
|
||||||
|
|
||||||
|
History:
|
||||||
|
User: Hey, how is it going?
|
||||||
|
AI: Not too bad. How can I help you today?
|
||||||
|
|
||||||
|
Q: What's the latest news on r/worldnews?
|
||||||
|
Khoj: {{"links": ["https://www.reddit.com/r/worldnews/"]}}
|
||||||
|
|
||||||
|
Now it's your turn to share actual webpage urls you'd like to read to answer the user's question.
|
||||||
|
History:
|
||||||
|
{chat_history}
|
||||||
|
|
||||||
|
Q: {query}
|
||||||
|
Khoj:
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
online_search_conversation_subqueries = PromptTemplate.from_template(
|
online_search_conversation_subqueries = PromptTemplate.from_template(
|
||||||
"""
|
"""
|
||||||
You are Khoj, an advanced google search assistant. You are tasked with constructing **up to three** google search queries to answer the user's question.
|
You are Khoj, an advanced google search assistant. You are tasked with constructing **up to three** google search queries to answer the user's question.
|
||||||
|
|
|
@ -2,6 +2,7 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
from typing import Dict, Tuple, Union
|
from typing import Dict, Tuple, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
@ -9,7 +10,11 @@ import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from markdownify import markdownify
|
from markdownify import markdownify
|
||||||
|
|
||||||
from khoj.routers.helpers import extract_relevant_info, generate_online_subqueries
|
from khoj.routers.helpers import (
|
||||||
|
extract_relevant_info,
|
||||||
|
generate_online_subqueries,
|
||||||
|
infer_webpage_urls,
|
||||||
|
)
|
||||||
from khoj.utils.helpers import is_none_or_empty, timer
|
from khoj.utils.helpers import is_none_or_empty, timer
|
||||||
from khoj.utils.rawconfig import LocationData
|
from khoj.utils.rawconfig import LocationData
|
||||||
|
|
||||||
|
@ -38,7 +43,7 @@ MAX_WEBPAGES_TO_READ = 1
|
||||||
|
|
||||||
|
|
||||||
async def search_online(query: str, conversation_history: dict, location: LocationData):
|
async def search_online(query: str, conversation_history: dict, location: LocationData):
|
||||||
if SERPER_DEV_API_KEY is None:
|
if not online_search_enabled():
|
||||||
logger.warn("SERPER_DEV_API_KEY is not set")
|
logger.warn("SERPER_DEV_API_KEY is not set")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@ -52,24 +57,21 @@ async def search_online(query: str, conversation_history: dict, location: Locati
|
||||||
|
|
||||||
# Gather distinct web pages from organic search results of each subquery without an instant answer
|
# Gather distinct web pages from organic search results of each subquery without an instant answer
|
||||||
webpage_links = {
|
webpage_links = {
|
||||||
result["link"]
|
organic["link"]: subquery
|
||||||
for subquery in response_dict
|
for subquery in response_dict
|
||||||
for result in response_dict[subquery].get("organic", [])[:MAX_WEBPAGES_TO_READ]
|
for organic in response_dict[subquery].get("organic", [])[:MAX_WEBPAGES_TO_READ]
|
||||||
if "answerBox" not in response_dict[subquery]
|
if "answerBox" not in response_dict[subquery]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read, extract relevant info from the retrieved web pages
|
# Read, extract relevant info from the retrieved web pages
|
||||||
tasks = []
|
logger.info(f"Reading web pages at: {webpage_links.keys()}")
|
||||||
for webpage_link in webpage_links:
|
tasks = [read_webpage_and_extract_content(subquery, link) for link, subquery in webpage_links.items()]
|
||||||
logger.info(f"Reading web page at '{webpage_link}'")
|
|
||||||
task = read_webpage_and_extract_content(subquery, webpage_link)
|
|
||||||
tasks.append(task)
|
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
# Collect extracted info from the retrieved web pages
|
# Collect extracted info from the retrieved web pages
|
||||||
for subquery, extracted_webpage_content in results:
|
for subquery, webpage_extract, url in results:
|
||||||
if extracted_webpage_content is not None:
|
if webpage_extract is not None:
|
||||||
response_dict[subquery]["extracted_content"] = extracted_webpage_content
|
response_dict[subquery]["webpages"] = {"link": url, "snippet": webpage_extract}
|
||||||
|
|
||||||
return response_dict
|
return response_dict
|
||||||
|
|
||||||
|
@ -93,19 +95,35 @@ def search_with_google(subquery: str):
|
||||||
return extracted_search_result
|
return extracted_search_result
|
||||||
|
|
||||||
|
|
||||||
async def read_webpage_and_extract_content(subquery: str, url: str) -> Tuple[str, Union[None, str]]:
|
async def read_webpages(query: str, conversation_history: dict, location: LocationData):
|
||||||
|
"Infer web pages to read from the query and extract relevant information from them"
|
||||||
|
logger.info(f"Inferring web pages to read")
|
||||||
|
urls = await infer_webpage_urls(query, conversation_history, location)
|
||||||
|
|
||||||
|
logger.info(f"Reading web pages at: {urls}")
|
||||||
|
tasks = [read_webpage_and_extract_content(query, url) for url in urls]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
response: Dict[str, Dict] = defaultdict(dict)
|
||||||
|
response[query]["webpages"] = [
|
||||||
|
{"query": q, "link": url, "snippet": web_extract} for q, web_extract, url in results if web_extract is not None
|
||||||
|
]
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def read_webpage_and_extract_content(subquery: str, url: str) -> Tuple[str, Union[None, str], str]:
|
||||||
try:
|
try:
|
||||||
with timer(f"Reading web page at '{url}' took", logger):
|
with timer(f"Reading web page at '{url}' took", logger):
|
||||||
content = await read_webpage_with_olostep(url) if OLOSTEP_API_KEY else await read_webpage(url)
|
content = await read_webpage_with_olostep(url) if OLOSTEP_API_KEY else await read_webpage_at_url(url)
|
||||||
with timer(f"Extracting relevant information from web page at '{url}' took", logger):
|
with timer(f"Extracting relevant information from web page at '{url}' took", logger):
|
||||||
extracted_info = await extract_relevant_info(subquery, content)
|
extracted_info = await extract_relevant_info(subquery, content)
|
||||||
return subquery, extracted_info
|
return subquery, extracted_info, url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to read web page at '{url}' with {e}")
|
logger.error(f"Failed to read web page at '{url}' with {e}")
|
||||||
return subquery, None
|
return subquery, None, url
|
||||||
|
|
||||||
|
|
||||||
async def read_webpage(web_url: str) -> str:
|
async def read_webpage_at_url(web_url: str) -> str:
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
|
||||||
}
|
}
|
||||||
|
@ -129,3 +147,7 @@ async def read_webpage_with_olostep(web_url: str) -> str:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
response_json = await response.json()
|
response_json = await response.json()
|
||||||
return response_json["markdown_content"]
|
return response_json["markdown_content"]
|
||||||
|
|
||||||
|
|
||||||
|
def online_search_enabled():
|
||||||
|
return SERPER_DEV_API_KEY is not None
|
||||||
|
|
43
src/khoj/routers/api_agents.py
Normal file
43
src/khoj/routers/api_agents.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.requests import Request
|
||||||
|
from fastapi.responses import Response
|
||||||
|
|
||||||
|
from khoj.database.adapters import AgentAdapters
|
||||||
|
from khoj.database.models import KhojUser
|
||||||
|
from khoj.routers.helpers import CommonQueryParams
|
||||||
|
|
||||||
|
# Initialize Router
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
api_agents = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@api_agents.get("/", response_class=Response)
|
||||||
|
async def all_agents(
|
||||||
|
request: Request,
|
||||||
|
common: CommonQueryParams,
|
||||||
|
) -> Response:
|
||||||
|
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||||
|
agents = await AgentAdapters.aget_all_accessible_agents(user)
|
||||||
|
agents_packet = list()
|
||||||
|
for agent in agents:
|
||||||
|
agents_packet.append(
|
||||||
|
{
|
||||||
|
"slug": agent.slug,
|
||||||
|
"avatar": agent.avatar,
|
||||||
|
"name": agent.name,
|
||||||
|
"personality": agent.personality,
|
||||||
|
"public": agent.public,
|
||||||
|
"creator": agent.creator.username if agent.creator else None,
|
||||||
|
"managed_by_admin": agent.managed_by_admin,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure that the agent named 'khoj' is first in the list. Everything else is sorted by name.
|
||||||
|
agents_packet.sort(key=lambda x: x["name"])
|
||||||
|
agents_packet.sort(key=lambda x: x["slug"] == "khoj", reverse=True)
|
||||||
|
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
|
@ -12,9 +12,17 @@ from starlette.authentication import requires
|
||||||
|
|
||||||
from khoj.database.adapters import ConversationAdapters, EntryAdapters, aget_user_name
|
from khoj.database.adapters import ConversationAdapters, EntryAdapters, aget_user_name
|
||||||
from khoj.database.models import KhojUser
|
from khoj.database.models import KhojUser
|
||||||
from khoj.processor.conversation.prompts import help_message, no_entries_found
|
from khoj.processor.conversation.prompts import (
|
||||||
|
help_message,
|
||||||
|
no_entries_found,
|
||||||
|
no_notes_found,
|
||||||
|
)
|
||||||
from khoj.processor.conversation.utils import save_to_conversation_log
|
from khoj.processor.conversation.utils import save_to_conversation_log
|
||||||
from khoj.processor.tools.online_search import search_online
|
from khoj.processor.tools.online_search import (
|
||||||
|
online_search_enabled,
|
||||||
|
read_webpages,
|
||||||
|
search_online,
|
||||||
|
)
|
||||||
from khoj.routers.api import extract_references_and_questions
|
from khoj.routers.api import extract_references_and_questions
|
||||||
from khoj.routers.helpers import (
|
from khoj.routers.helpers import (
|
||||||
ApiUserRateLimiter,
|
ApiUserRateLimiter,
|
||||||
|
@ -81,9 +89,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 +169,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, agent_id)
|
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, agent_slug)
|
||||||
|
|
||||||
response = {"conversation_id": conversation.id}
|
response = {"conversation_id": conversation.id}
|
||||||
|
|
||||||
|
@ -239,6 +260,7 @@ async def chat(
|
||||||
) -> Response:
|
) -> Response:
|
||||||
user: KhojUser = request.user.object
|
user: KhojUser = request.user.object
|
||||||
q = unquote(q)
|
q = unquote(q)
|
||||||
|
logger.info(f"Chat request by {user.username}: {q}")
|
||||||
|
|
||||||
await is_ready_to_chat(user)
|
await is_ready_to_chat(user)
|
||||||
conversation_commands = [get_conversation_command(query=q, any_references=True)]
|
conversation_commands = [get_conversation_command(query=q, any_references=True)]
|
||||||
|
@ -281,7 +303,7 @@ async def chat(
|
||||||
compiled_references, inferred_queries, defiltered_query = await extract_references_and_questions(
|
compiled_references, inferred_queries, defiltered_query = await extract_references_and_questions(
|
||||||
request, common, meta_log, q, (n or 5), (d or math.inf), conversation_commands, location
|
request, common, meta_log, q, (n or 5), (d or math.inf), conversation_commands, location
|
||||||
)
|
)
|
||||||
online_results: Dict = dict()
|
online_results: Dict[str, Dict] = {}
|
||||||
|
|
||||||
if conversation_commands == [ConversationCommand.Notes] and not await EntryAdapters.auser_has_entries(user):
|
if conversation_commands == [ConversationCommand.Notes] and not await EntryAdapters.auser_has_entries(user):
|
||||||
no_entries_found_format = no_entries_found.format()
|
no_entries_found_format = no_entries_found.format()
|
||||||
|
@ -291,17 +313,35 @@ async def chat(
|
||||||
response_obj = {"response": no_entries_found_format}
|
response_obj = {"response": no_entries_found_format}
|
||||||
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
||||||
|
|
||||||
|
if conversation_commands == [ConversationCommand.Notes] and is_none_or_empty(compiled_references):
|
||||||
|
no_notes_found_format = no_notes_found.format()
|
||||||
|
if stream:
|
||||||
|
return StreamingResponse(iter([no_notes_found_format]), media_type="text/event-stream", status_code=200)
|
||||||
|
else:
|
||||||
|
response_obj = {"response": no_notes_found_format}
|
||||||
|
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
||||||
|
|
||||||
if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
|
if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
|
||||||
conversation_commands.remove(ConversationCommand.Notes)
|
conversation_commands.remove(ConversationCommand.Notes)
|
||||||
|
|
||||||
if ConversationCommand.Online in conversation_commands:
|
if ConversationCommand.Online in conversation_commands:
|
||||||
|
if not online_search_enabled():
|
||||||
|
conversation_commands.remove(ConversationCommand.Online)
|
||||||
|
# If online search is not enabled, try to read webpages directly
|
||||||
|
if ConversationCommand.Webpage not in conversation_commands:
|
||||||
|
conversation_commands.append(ConversationCommand.Webpage)
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
online_results = await search_online(defiltered_query, meta_log, location)
|
online_results = await search_online(defiltered_query, meta_log, location)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return StreamingResponse(
|
logger.warning(f"Error searching online: {e}. Attempting to respond without online results")
|
||||||
iter(["Please set your SERPER_DEV_API_KEY to get started with online searches 🌐"]),
|
|
||||||
media_type="text/event-stream",
|
if ConversationCommand.Webpage in conversation_commands:
|
||||||
status_code=200,
|
try:
|
||||||
|
online_results = await read_webpages(defiltered_query, meta_log, location)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error directly reading webpages: {e}. Attempting to respond without online results", exc_info=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if ConversationCommand.Image in conversation_commands:
|
if ConversationCommand.Image in conversation_commands:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -37,6 +37,7 @@ from khoj.utils.config import GPT4AllProcessorModel
|
||||||
from khoj.utils.helpers import (
|
from khoj.utils.helpers import (
|
||||||
ConversationCommand,
|
ConversationCommand,
|
||||||
is_none_or_empty,
|
is_none_or_empty,
|
||||||
|
is_valid_url,
|
||||||
log_telemetry,
|
log_telemetry,
|
||||||
mode_descriptions_for_llm,
|
mode_descriptions_for_llm,
|
||||||
timer,
|
timer,
|
||||||
|
@ -168,6 +169,7 @@ async def aget_relevant_information_sources(query: str, conversation_history: di
|
||||||
chat_history=chat_history,
|
chat_history=chat_history,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Infer information sources to refer", logger):
|
||||||
response = await send_message_to_model_wrapper(relevant_tools_prompt, response_type="json_object")
|
response = await send_message_to_model_wrapper(relevant_tools_prompt, response_type="json_object")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -212,6 +214,7 @@ async def aget_relevant_output_modes(query: str, conversation_history: dict):
|
||||||
chat_history=chat_history,
|
chat_history=chat_history,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Infer output mode for chat response", logger):
|
||||||
response = await send_message_to_model_wrapper(relevant_mode_prompt)
|
response = await send_message_to_model_wrapper(relevant_mode_prompt)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -230,6 +233,36 @@ async def aget_relevant_output_modes(query: str, conversation_history: dict):
|
||||||
return ConversationCommand.Default
|
return ConversationCommand.Default
|
||||||
|
|
||||||
|
|
||||||
|
async def infer_webpage_urls(q: str, conversation_history: dict, location_data: LocationData) -> List[str]:
|
||||||
|
"""
|
||||||
|
Infer webpage links from the given query
|
||||||
|
"""
|
||||||
|
location = f"{location_data.city}, {location_data.region}, {location_data.country}" if location_data else "Unknown"
|
||||||
|
chat_history = construct_chat_history(conversation_history)
|
||||||
|
|
||||||
|
utc_date = datetime.utcnow().strftime("%Y-%m-%d")
|
||||||
|
online_queries_prompt = prompts.infer_webpages_to_read.format(
|
||||||
|
current_date=utc_date,
|
||||||
|
query=q,
|
||||||
|
chat_history=chat_history,
|
||||||
|
location=location,
|
||||||
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Infer webpage urls to read", logger):
|
||||||
|
response = await send_message_to_model_wrapper(online_queries_prompt, response_type="json_object")
|
||||||
|
|
||||||
|
# Validate that the response is a non-empty, JSON-serializable list of URLs
|
||||||
|
try:
|
||||||
|
response = response.strip()
|
||||||
|
urls = json.loads(response)
|
||||||
|
valid_unique_urls = {str(url).strip() for url in urls["links"] if is_valid_url(url)}
|
||||||
|
if is_none_or_empty(valid_unique_urls):
|
||||||
|
raise ValueError(f"Invalid list of urls: {response}")
|
||||||
|
return list(valid_unique_urls)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(f"Invalid list of urls: {response}")
|
||||||
|
|
||||||
|
|
||||||
async def generate_online_subqueries(q: str, conversation_history: dict, location_data: LocationData) -> List[str]:
|
async def generate_online_subqueries(q: str, conversation_history: dict, location_data: LocationData) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate subqueries from the given query
|
Generate subqueries from the given query
|
||||||
|
@ -245,6 +278,7 @@ async def generate_online_subqueries(q: str, conversation_history: dict, locatio
|
||||||
location=location,
|
location=location,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Generate online search subqueries", logger):
|
||||||
response = await send_message_to_model_wrapper(online_queries_prompt, response_type="json_object")
|
response = await send_message_to_model_wrapper(online_queries_prompt, response_type="json_object")
|
||||||
|
|
||||||
# Validate that the response is a non-empty, JSON-serializable list
|
# Validate that the response is a non-empty, JSON-serializable list
|
||||||
|
@ -274,6 +308,7 @@ async def extract_relevant_info(q: str, corpus: str) -> Union[str, None]:
|
||||||
corpus=corpus.strip(),
|
corpus=corpus.strip(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Extract relevant information from data", logger):
|
||||||
response = await send_message_to_model_wrapper(
|
response = await send_message_to_model_wrapper(
|
||||||
extract_relevant_information, prompts.system_prompt_extract_relevant_information
|
extract_relevant_information, prompts.system_prompt_extract_relevant_information
|
||||||
)
|
)
|
||||||
|
@ -305,8 +340,8 @@ async def generate_better_image_prompt(
|
||||||
for result in online_results:
|
for result in online_results:
|
||||||
if online_results[result].get("answerBox"):
|
if online_results[result].get("answerBox"):
|
||||||
simplified_online_results[result] = online_results[result]["answerBox"]
|
simplified_online_results[result] = online_results[result]["answerBox"]
|
||||||
elif online_results[result].get("extracted_content"):
|
elif online_results[result].get("webpages"):
|
||||||
simplified_online_results[result] = online_results[result]["extracted_content"]
|
simplified_online_results[result] = online_results[result]["webpages"]
|
||||||
|
|
||||||
image_prompt = prompts.image_generation_improve_prompt.format(
|
image_prompt = prompts.image_generation_improve_prompt.format(
|
||||||
query=q,
|
query=q,
|
||||||
|
@ -317,6 +352,7 @@ async def generate_better_image_prompt(
|
||||||
online_results=simplified_online_results,
|
online_results=simplified_online_results,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Generate contextual image prompt", logger):
|
||||||
response = await send_message_to_model_wrapper(image_prompt)
|
response = await send_message_to_model_wrapper(image_prompt)
|
||||||
|
|
||||||
return response.strip()
|
return response.strip()
|
||||||
|
@ -367,7 +403,7 @@ def generate_chat_response(
|
||||||
meta_log: dict,
|
meta_log: dict,
|
||||||
conversation: Conversation,
|
conversation: Conversation,
|
||||||
compiled_references: List[str] = [],
|
compiled_references: List[str] = [],
|
||||||
online_results: Dict[str, Any] = {},
|
online_results: Dict[str, Dict] = {},
|
||||||
inferred_queries: List[str] = [],
|
inferred_queries: List[str] = [],
|
||||||
conversation_commands: List[ConversationCommand] = [ConversationCommand.Default],
|
conversation_commands: List[ConversationCommand] = [ConversationCommand.Default],
|
||||||
user: KhojUser = None,
|
user: KhojUser = None,
|
||||||
|
|
|
@ -10,6 +10,7 @@ from starlette.authentication import has_required_scope, requires
|
||||||
|
|
||||||
from khoj.database import adapters
|
from khoj.database import adapters
|
||||||
from khoj.database.adapters import (
|
from khoj.database.adapters import (
|
||||||
|
AgentAdapters,
|
||||||
ConversationAdapters,
|
ConversationAdapters,
|
||||||
EntryAdapters,
|
EntryAdapters,
|
||||||
get_user_github_config,
|
get_user_github_config,
|
||||||
|
@ -114,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):
|
||||||
if request.user.is_authenticated:
|
|
||||||
next_url = request.query_params.get("next", "/")
|
next_url = request.query_params.get("next", "/")
|
||||||
|
if request.user.is_authenticated:
|
||||||
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"))
|
||||||
|
@ -124,7 +125,85 @@ 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)
|
||||||
|
def agents_page(request: Request):
|
||||||
|
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||||
|
user_picture = request.session.get("user", {}).get("picture") if user else None
|
||||||
|
agents = AgentAdapters.get_all_accessible_agents(user)
|
||||||
|
agents_packet = list()
|
||||||
|
for agent in agents:
|
||||||
|
agents_packet.append(
|
||||||
|
{
|
||||||
|
"slug": agent.slug,
|
||||||
|
"avatar": agent.avatar,
|
||||||
|
"name": agent.name,
|
||||||
|
"personality": agent.personality,
|
||||||
|
"public": agent.public,
|
||||||
|
"creator": agent.creator.username if agent.creator else None,
|
||||||
|
"managed_by_admin": agent.managed_by_admin,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"agents.html",
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"agents": agents_packet,
|
||||||
|
"khoj_version": state.khoj_version,
|
||||||
|
"username": user.username if user else None,
|
||||||
|
"has_documents": False,
|
||||||
|
"is_active": has_required_scope(request, ["premium"]),
|
||||||
|
"user_photo": user_picture,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@web_client.get("/agent/{agent_slug}", response_class=HTMLResponse)
|
||||||
|
def agent_page(request: Request, agent_slug: str):
|
||||||
|
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
||||||
|
user_picture = request.session.get("user", {}).get("picture") if user else None
|
||||||
|
|
||||||
|
agent = AgentAdapters.get_agent_by_slug(agent_slug)
|
||||||
|
|
||||||
|
if agent == None:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"404.html",
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"khoj_version": state.khoj_version,
|
||||||
|
"username": user.username if user else None,
|
||||||
|
"has_documents": False,
|
||||||
|
"is_active": has_required_scope(request, ["premium"]),
|
||||||
|
"user_photo": user_picture,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_metadata = {
|
||||||
|
"slug": agent.slug,
|
||||||
|
"avatar": agent.avatar,
|
||||||
|
"name": agent.name,
|
||||||
|
"personality": agent.personality,
|
||||||
|
"public": agent.public,
|
||||||
|
"creator": agent.creator.username if agent.creator else None,
|
||||||
|
"managed_by_admin": agent.managed_by_admin,
|
||||||
|
"chat_model": agent.chat_model.chat_model,
|
||||||
|
"creator_not_self": agent.creator != user,
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"agent.html",
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"agent": agent_metadata,
|
||||||
|
"khoj_version": state.khoj_version,
|
||||||
|
"username": user.username if user else None,
|
||||||
|
"has_documents": False,
|
||||||
|
"is_active": has_required_scope(request, ["premium"]),
|
||||||
|
"user_photo": user_picture,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
@ -270,6 +271,7 @@ class ConversationCommand(str, Enum):
|
||||||
Notes = "notes"
|
Notes = "notes"
|
||||||
Help = "help"
|
Help = "help"
|
||||||
Online = "online"
|
Online = "online"
|
||||||
|
Webpage = "webpage"
|
||||||
Image = "image"
|
Image = "image"
|
||||||
|
|
||||||
|
|
||||||
|
@ -278,15 +280,17 @@ command_descriptions = {
|
||||||
ConversationCommand.Notes: "Only talk about information that is available in your knowledge base.",
|
ConversationCommand.Notes: "Only talk about information that is available in your knowledge base.",
|
||||||
ConversationCommand.Default: "The default command when no command specified. It intelligently auto-switches between general and notes mode.",
|
ConversationCommand.Default: "The default command when no command specified. It intelligently auto-switches between general and notes mode.",
|
||||||
ConversationCommand.Online: "Search for information on the internet.",
|
ConversationCommand.Online: "Search for information on the internet.",
|
||||||
|
ConversationCommand.Webpage: "Get information from webpage links provided by you.",
|
||||||
ConversationCommand.Image: "Generate images by describing your imagination in words.",
|
ConversationCommand.Image: "Generate images by describing your imagination in words.",
|
||||||
ConversationCommand.Help: "Display a help message with all available commands and other metadata.",
|
ConversationCommand.Help: "Display a help message with all available commands and other metadata.",
|
||||||
}
|
}
|
||||||
|
|
||||||
tool_descriptions_for_llm = {
|
tool_descriptions_for_llm = {
|
||||||
ConversationCommand.Default: "To use a mix of your internal knowledge and the user's personal knowledge, or if you don't entirely understand the query.",
|
ConversationCommand.Default: "To use a mix of your internal knowledge and the user's personal knowledge, or if you don't entirely understand the query.",
|
||||||
ConversationCommand.General: "Use this when you can answer the question without any outside information or personal knowledge",
|
ConversationCommand.General: "To use when you can answer the question without any outside information or personal knowledge",
|
||||||
ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
|
ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
|
||||||
ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**",
|
ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**",
|
||||||
|
ConversationCommand.Webpage: "To use if the user has directly provided the webpage urls or you are certain of the webpage urls to read.",
|
||||||
}
|
}
|
||||||
|
|
||||||
mode_descriptions_for_llm = {
|
mode_descriptions_for_llm = {
|
||||||
|
@ -340,3 +344,12 @@ def in_debug_mode():
|
||||||
"""Check if Khoj is running in debug mode.
|
"""Check if Khoj is running in debug mode.
|
||||||
Set KHOJ_DEBUG environment variable to true to enable debug mode."""
|
Set KHOJ_DEBUG environment variable to true to enable debug mode."""
|
||||||
return is_env_var_true("KHOJ_DEBUG")
|
return is_env_var_true("KHOJ_DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_url(url: str) -> bool:
|
||||||
|
"""Check if a string is a valid URL"""
|
||||||
|
try:
|
||||||
|
result = urlparse(url.strip())
|
||||||
|
return all([result.scheme, result.netloc])
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
|
@ -579,7 +579,7 @@ async def test_get_correct_tools_general(client_offline_chat):
|
||||||
# ----------------------------------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------------
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@pytest.mark.django_db(transaction=True)
|
@pytest.mark.django_db(transaction=True)
|
||||||
async def test_get_correct_tools_with_chat_history(client_offline_chat):
|
async def test_get_correct_tools_with_chat_history(client_offline_chat, default_user2):
|
||||||
# Arrange
|
# Arrange
|
||||||
user_query = "What's the latest in the Israel/Palestine conflict?"
|
user_query = "What's the latest in the Israel/Palestine conflict?"
|
||||||
chat_log = [
|
chat_log = [
|
||||||
|
@ -590,7 +590,7 @@ async def test_get_correct_tools_with_chat_history(client_offline_chat):
|
||||||
),
|
),
|
||||||
("What's up in New York City?", "A Pride parade has recently been held in New York City, on July 31st.", []),
|
("What's up in New York City?", "A Pride parade has recently been held in New York City, on July 31st.", []),
|
||||||
]
|
]
|
||||||
chat_history = create_conversation(chat_log)
|
chat_history = create_conversation(chat_log, default_user2)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
tools = await aget_relevant_information_sources(user_query, chat_history)
|
tools = await aget_relevant_information_sources(user_query, chat_history)
|
||||||
|
|
|
@ -7,7 +7,10 @@ import pytest
|
||||||
from scipy.stats import linregress
|
from scipy.stats import linregress
|
||||||
|
|
||||||
from khoj.processor.embeddings import EmbeddingsModel
|
from khoj.processor.embeddings import EmbeddingsModel
|
||||||
from khoj.processor.tools.online_search import read_webpage, read_webpage_with_olostep
|
from khoj.processor.tools.online_search import (
|
||||||
|
read_webpage_at_url,
|
||||||
|
read_webpage_with_olostep,
|
||||||
|
)
|
||||||
from khoj.utils import helpers
|
from khoj.utils import helpers
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,7 +93,7 @@ async def test_reading_webpage():
|
||||||
website = "https://en.wikipedia.org/wiki/Great_Chicago_Fire"
|
website = "https://en.wikipedia.org/wiki/Great_Chicago_Fire"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = await read_webpage(website)
|
response = await read_webpage_at_url(website)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert (
|
assert (
|
||||||
|
|
|
@ -34,7 +34,9 @@ def test_markdown_file_with_no_headings_to_jsonl(tmp_path):
|
||||||
# Ensure raw entry with no headings do not get heading prefix prepended
|
# Ensure raw entry with no headings do not get heading prefix prepended
|
||||||
assert not jsonl_data[0]["raw"].startswith("#")
|
assert not jsonl_data[0]["raw"].startswith("#")
|
||||||
# Ensure compiled entry has filename prepended as top level heading
|
# Ensure compiled entry has filename prepended as top level heading
|
||||||
assert jsonl_data[0]["compiled"].startswith(expected_heading)
|
assert expected_heading in jsonl_data[0]["compiled"]
|
||||||
|
# Ensure compiled entry also includes the file name
|
||||||
|
assert str(tmp_path) in jsonl_data[0]["compiled"]
|
||||||
|
|
||||||
|
|
||||||
def test_single_markdown_entry_to_jsonl(tmp_path):
|
def test_single_markdown_entry_to_jsonl(tmp_path):
|
||||||
|
|
|
@ -11,6 +11,7 @@ from khoj.routers.helpers import (
|
||||||
aget_relevant_information_sources,
|
aget_relevant_information_sources,
|
||||||
aget_relevant_output_modes,
|
aget_relevant_output_modes,
|
||||||
generate_online_subqueries,
|
generate_online_subqueries,
|
||||||
|
infer_webpage_urls,
|
||||||
)
|
)
|
||||||
from khoj.utils.helpers import ConversationCommand
|
from khoj.utils.helpers import ConversationCommand
|
||||||
|
|
||||||
|
@ -546,6 +547,34 @@ async def test_select_data_sources_actor_chooses_to_search_online(chat_client):
|
||||||
assert ConversationCommand.Online in conversation_commands
|
assert ConversationCommand.Online in conversation_commands
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------------
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
async def test_select_data_sources_actor_chooses_to_read_webpage(chat_client):
|
||||||
|
# Arrange
|
||||||
|
user_query = "Summarize the wikipedia page on the history of the internet"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
conversation_commands = await aget_relevant_information_sources(user_query, {})
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert ConversationCommand.Webpage in conversation_commands
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------------
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
async def test_infer_webpage_urls_actor_extracts_correct_links(chat_client):
|
||||||
|
# Arrange
|
||||||
|
user_query = "Summarize the wikipedia page on the history of the internet"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
urls = await infer_webpage_urls(user_query, {}, None)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert "https://en.wikipedia.org/wiki/History_of_the_Internet" in urls
|
||||||
|
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
# ----------------------------------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------------
|
||||||
def populate_chat_history(message_list):
|
def populate_chat_history(message_list):
|
||||||
|
|
Loading…
Reference in a new issue