khoj 1.24.2.dev3__py3-none-any.whl → 1.24.2.dev16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- khoj/database/adapters/__init__.py +139 -16
- khoj/database/admin.py +2 -0
- khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
- khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
- khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
- khoj/database/models/__init__.py +60 -18
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/1269-2e52d48e7d0e5c61.js +1 -0
- khoj/interface/compiled/_next/static/chunks/1603-67a89278e2c5dbe6.js +1 -0
- khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/4086-2c74808ba38a5a0f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +1 -0
- khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9417-29502e39c3e7d60c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9479-7eed36fc954ef804.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/page-df26b497b7356151.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/page-1688dead2f21270d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/page-91abcb71846922b7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/factchecker/page-7ab093711c27041c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/page-fada198096eab47f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/page-a7e036689b6507ff.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/page-fa11cafaec7ab39f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-c5d2b9076e5390b2.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{webpack-d4781cada9b58e75.js → webpack-f52083d548d804fa.js} +1 -1
- khoj/interface/compiled/_next/static/css/50d972a8c787730b.css +25 -0
- khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +1 -0
- khoj/interface/compiled/_next/static/css/dfb67a9287720a2b.css +1 -0
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +2 -2
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +2 -2
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/factchecker/index.html +1 -1
- khoj/interface/compiled/factchecker/index.txt +2 -2
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +2 -2
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +2 -2
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/processor/content/notion/notion_to_entries.py +2 -1
- khoj/processor/conversation/anthropic/anthropic_chat.py +2 -0
- khoj/processor/conversation/google/gemini_chat.py +2 -0
- khoj/processor/conversation/offline/chat_model.py +3 -1
- khoj/processor/conversation/openai/gpt.py +2 -0
- khoj/processor/conversation/prompts.py +56 -5
- khoj/processor/image/generate.py +3 -1
- khoj/processor/tools/online_search.py +9 -7
- khoj/routers/api.py +34 -5
- khoj/routers/api_agents.py +232 -4
- khoj/routers/api_chat.py +46 -17
- khoj/routers/api_content.py +14 -0
- khoj/routers/helpers.py +113 -13
- khoj/search_type/text_search.py +4 -1
- khoj/utils/helpers.py +15 -2
- {khoj-1.24.2.dev3.dist-info → khoj-1.24.2.dev16.dist-info}/METADATA +1 -8
- {khoj-1.24.2.dev3.dist-info → khoj-1.24.2.dev16.dist-info}/RECORD +67 -64
- khoj/interface/compiled/_next/static/chunks/1603-3e2e1528e3b6ea1d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/2697-a29cb9191a9e339c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/6648-ee109f4ea33a74e2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/7071-b4711cecca6619a8.js +0 -1
- khoj/interface/compiled/_next/static/chunks/743-1a64254447cda71f.js +0 -1
- khoj/interface/compiled/_next/static/chunks/8423-62ac6c832be2461b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9162-0be016519a18568b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9178-7e815211edcb3657.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9417-5d14ac74aaab2c66.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9984-e410179c6fac7cf1.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-d302911777a3e027.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-0a5de8c254c29a1c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-d96bf6a84bb05290.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/factchecker/page-32e61af29e6b431d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/page-96cab08c985716f4.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/page-b3193d46c65571c5.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-0db9b708366606ec.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-f06ac16cfe5b5a16.js +0 -1
- khoj/interface/compiled/_next/static/css/24f141a6e37cd204.css +0 -25
- khoj/interface/compiled/_next/static/css/3e1f1fdd70775091.css +0 -1
- khoj/interface/compiled/_next/static/css/f768dddada62459d.css +0 -1
- /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → MyYNlmGMz32TGV_-febR4}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → MyYNlmGMz32TGV_-febR4}/_ssgManifest.js +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.24.2.dev16.dist-info}/WHEEL +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.24.2.dev16.dist-info}/entry_points.txt +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.24.2.dev16.dist-info}/licenses/LICENSE +0 -0
@@ -10,7 +10,7 @@ import aiohttp
|
|
10
10
|
from bs4 import BeautifulSoup
|
11
11
|
from markdownify import markdownify
|
12
12
|
|
13
|
-
from khoj.database.models import KhojUser
|
13
|
+
from khoj.database.models import Agent, KhojUser
|
14
14
|
from khoj.routers.helpers import (
|
15
15
|
ChatEvent,
|
16
16
|
extract_relevant_info,
|
@@ -57,16 +57,17 @@ async def search_online(
|
|
57
57
|
send_status_func: Optional[Callable] = None,
|
58
58
|
custom_filters: List[str] = [],
|
59
59
|
uploaded_image_url: str = None,
|
60
|
+
agent: Agent = None,
|
60
61
|
):
|
61
62
|
query += " ".join(custom_filters)
|
62
63
|
if not is_internet_connected():
|
63
|
-
logger.
|
64
|
+
logger.warning("Cannot search online as not connected to internet")
|
64
65
|
yield {}
|
65
66
|
return
|
66
67
|
|
67
68
|
# Breakdown the query into subqueries to get the correct answer
|
68
69
|
subqueries = await generate_online_subqueries(
|
69
|
-
query, conversation_history, location, user, uploaded_image_url=uploaded_image_url
|
70
|
+
query, conversation_history, location, user, uploaded_image_url=uploaded_image_url, agent=agent
|
70
71
|
)
|
71
72
|
response_dict = {}
|
72
73
|
|
@@ -101,7 +102,7 @@ async def search_online(
|
|
101
102
|
async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"):
|
102
103
|
yield {ChatEvent.STATUS: event}
|
103
104
|
tasks = [
|
104
|
-
read_webpage_and_extract_content(subquery, link, content, subscribed=subscribed)
|
105
|
+
read_webpage_and_extract_content(subquery, link, content, subscribed=subscribed, agent=agent)
|
105
106
|
for link, subquery, content in webpages
|
106
107
|
]
|
107
108
|
results = await asyncio.gather(*tasks)
|
@@ -143,6 +144,7 @@ async def read_webpages(
|
|
143
144
|
subscribed: bool = False,
|
144
145
|
send_status_func: Optional[Callable] = None,
|
145
146
|
uploaded_image_url: str = None,
|
147
|
+
agent: Agent = None,
|
146
148
|
):
|
147
149
|
"Infer web pages to read from the query and extract relevant information from them"
|
148
150
|
logger.info(f"Inferring web pages to read")
|
@@ -156,7 +158,7 @@ async def read_webpages(
|
|
156
158
|
webpage_links_str = "\n- " + "\n- ".join(list(urls))
|
157
159
|
async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"):
|
158
160
|
yield {ChatEvent.STATUS: event}
|
159
|
-
tasks = [read_webpage_and_extract_content(query, url, subscribed=subscribed) for url in urls]
|
161
|
+
tasks = [read_webpage_and_extract_content(query, url, subscribed=subscribed, agent=agent) for url in urls]
|
160
162
|
results = await asyncio.gather(*tasks)
|
161
163
|
|
162
164
|
response: Dict[str, Dict] = defaultdict(dict)
|
@@ -167,14 +169,14 @@ async def read_webpages(
|
|
167
169
|
|
168
170
|
|
169
171
|
async def read_webpage_and_extract_content(
|
170
|
-
subquery: str, url: str, content: str = None, subscribed: bool = False
|
172
|
+
subquery: str, url: str, content: str = None, subscribed: bool = False, agent: Agent = None
|
171
173
|
) -> Tuple[str, Union[None, str], str]:
|
172
174
|
try:
|
173
175
|
if is_none_or_empty(content):
|
174
176
|
with timer(f"Reading web page at '{url}' took", logger):
|
175
177
|
content = await read_webpage_with_olostep(url) if OLOSTEP_API_KEY else await read_webpage_with_jina(url)
|
176
178
|
with timer(f"Extracting relevant information from web page at '{url}' took", logger):
|
177
|
-
extracted_info = await extract_relevant_info(subquery, content, subscribed=subscribed)
|
179
|
+
extracted_info = await extract_relevant_info(subquery, content, subscribed=subscribed, agent=agent)
|
178
180
|
return subquery, extracted_info, url
|
179
181
|
except Exception as e:
|
180
182
|
logger.error(f"Failed to read web page at '{url}' with {e}")
|
khoj/routers/api.py
CHANGED
@@ -27,7 +27,13 @@ from khoj.database.adapters import (
|
|
27
27
|
get_user_photo,
|
28
28
|
get_user_search_model_or_default,
|
29
29
|
)
|
30
|
-
from khoj.database.models import
|
30
|
+
from khoj.database.models import (
|
31
|
+
Agent,
|
32
|
+
ChatModelOptions,
|
33
|
+
KhojUser,
|
34
|
+
SpeechToTextModelOptions,
|
35
|
+
)
|
36
|
+
from khoj.processor.conversation import prompts
|
31
37
|
from khoj.processor.conversation.anthropic.anthropic_chat import (
|
32
38
|
extract_questions_anthropic,
|
33
39
|
)
|
@@ -106,6 +112,7 @@ async def execute_search(
|
|
106
112
|
r: Optional[bool] = False,
|
107
113
|
max_distance: Optional[Union[float, None]] = None,
|
108
114
|
dedupe: Optional[bool] = True,
|
115
|
+
agent: Optional[Agent] = None,
|
109
116
|
):
|
110
117
|
start_time = time.time()
|
111
118
|
|
@@ -157,6 +164,7 @@ async def execute_search(
|
|
157
164
|
t,
|
158
165
|
question_embedding=encoded_asymmetric_query,
|
159
166
|
max_distance=max_distance,
|
167
|
+
agent=agent,
|
160
168
|
)
|
161
169
|
]
|
162
170
|
|
@@ -333,6 +341,7 @@ async def extract_references_and_questions(
|
|
333
341
|
location_data: LocationData = None,
|
334
342
|
send_status_func: Optional[Callable] = None,
|
335
343
|
uploaded_image_url: Optional[str] = None,
|
344
|
+
agent: Agent = None,
|
336
345
|
):
|
337
346
|
user = request.user.object if request.user.is_authenticated else None
|
338
347
|
|
@@ -340,17 +349,30 @@ async def extract_references_and_questions(
|
|
340
349
|
compiled_references: List[Any] = []
|
341
350
|
inferred_queries: List[str] = []
|
342
351
|
|
352
|
+
agent_has_entries = False
|
353
|
+
|
354
|
+
if agent:
|
355
|
+
agent_has_entries = await sync_to_async(EntryAdapters.agent_has_entries)(agent=agent)
|
356
|
+
|
343
357
|
if (
|
344
358
|
not ConversationCommand.Notes in conversation_commands
|
345
359
|
and not ConversationCommand.Default in conversation_commands
|
360
|
+
and not agent_has_entries
|
346
361
|
):
|
347
362
|
yield compiled_references, inferred_queries, q
|
348
363
|
return
|
349
364
|
|
365
|
+
# If Notes or Default is not in the conversation command, then the search should be restricted to the agent's knowledge base
|
366
|
+
should_limit_to_agent_knowledge = (
|
367
|
+
ConversationCommand.Notes not in conversation_commands
|
368
|
+
and ConversationCommand.Default not in conversation_commands
|
369
|
+
)
|
370
|
+
|
350
371
|
if not await sync_to_async(EntryAdapters.user_has_entries)(user=user):
|
351
|
-
|
352
|
-
|
353
|
-
|
372
|
+
if not agent_has_entries:
|
373
|
+
logger.debug("No documents in knowledge base. Use a Khoj client to sync and chat with your docs.")
|
374
|
+
yield compiled_references, inferred_queries, q
|
375
|
+
return
|
354
376
|
|
355
377
|
# Extract filter terms from user message
|
356
378
|
defiltered_query = q
|
@@ -368,6 +390,8 @@ async def extract_references_and_questions(
|
|
368
390
|
using_offline_chat = False
|
369
391
|
logger.debug(f"Filters in query: {filters_in_query}")
|
370
392
|
|
393
|
+
personality_context = prompts.personality_context.format(personality=agent.personality) if agent else ""
|
394
|
+
|
371
395
|
# Infer search queries from user message
|
372
396
|
with timer("Extracting search queries took", logger):
|
373
397
|
# If we've reached here, either the user has enabled offline chat or the openai model is enabled.
|
@@ -392,6 +416,7 @@ async def extract_references_and_questions(
|
|
392
416
|
location_data=location_data,
|
393
417
|
user=user,
|
394
418
|
max_prompt_size=conversation_config.max_prompt_size,
|
419
|
+
personality_context=personality_context,
|
395
420
|
)
|
396
421
|
elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
|
397
422
|
openai_chat_config = conversation_config.openai_config
|
@@ -408,6 +433,7 @@ async def extract_references_and_questions(
|
|
408
433
|
user=user,
|
409
434
|
uploaded_image_url=uploaded_image_url,
|
410
435
|
vision_enabled=vision_enabled,
|
436
|
+
personality_context=personality_context,
|
411
437
|
)
|
412
438
|
elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
|
413
439
|
api_key = conversation_config.openai_config.api_key
|
@@ -419,6 +445,7 @@ async def extract_references_and_questions(
|
|
419
445
|
conversation_log=meta_log,
|
420
446
|
location_data=location_data,
|
421
447
|
user=user,
|
448
|
+
personality_context=personality_context,
|
422
449
|
)
|
423
450
|
elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
|
424
451
|
api_key = conversation_config.openai_config.api_key
|
@@ -431,6 +458,7 @@ async def extract_references_and_questions(
|
|
431
458
|
location_data=location_data,
|
432
459
|
max_tokens=conversation_config.max_prompt_size,
|
433
460
|
user=user,
|
461
|
+
personality_context=personality_context,
|
434
462
|
)
|
435
463
|
|
436
464
|
# Collate search results as context for GPT
|
@@ -445,13 +473,14 @@ async def extract_references_and_questions(
|
|
445
473
|
n_items = min(n, 3) if using_offline_chat else n
|
446
474
|
search_results.extend(
|
447
475
|
await execute_search(
|
448
|
-
user,
|
476
|
+
user if not should_limit_to_agent_knowledge else None,
|
449
477
|
f"{query} {filters_in_query}",
|
450
478
|
n=n_items,
|
451
479
|
t=SearchType.All,
|
452
480
|
r=True,
|
453
481
|
max_distance=d,
|
454
482
|
dedupe=False,
|
483
|
+
agent=agent,
|
455
484
|
)
|
456
485
|
)
|
457
486
|
search_results = text_search.deduplicated_search_responses(search_results)
|
khoj/routers/api_agents.py
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
+
from typing import Dict, List, Optional
|
3
4
|
|
5
|
+
from asgiref.sync import sync_to_async
|
4
6
|
from fastapi import APIRouter, Request
|
5
7
|
from fastapi.requests import Request
|
6
8
|
from fastapi.responses import Response
|
9
|
+
from pydantic import BaseModel
|
10
|
+
from starlette.authentication import requires
|
7
11
|
|
8
12
|
from khoj.database.adapters import AgentAdapters
|
9
|
-
from khoj.database.models import KhojUser
|
10
|
-
from khoj.routers.helpers import CommonQueryParams
|
13
|
+
from khoj.database.models import Agent, KhojUser
|
14
|
+
from khoj.routers.helpers import CommonQueryParams, acheck_if_safe_prompt
|
15
|
+
from khoj.utils.helpers import (
|
16
|
+
ConversationCommand,
|
17
|
+
command_descriptions_for_agent,
|
18
|
+
mode_descriptions_for_agent,
|
19
|
+
)
|
11
20
|
|
12
21
|
# Initialize Router
|
13
22
|
logger = logging.getLogger(__name__)
|
@@ -16,6 +25,18 @@ logger = logging.getLogger(__name__)
|
|
16
25
|
api_agents = APIRouter()
|
17
26
|
|
18
27
|
|
28
|
+
class ModifyAgentBody(BaseModel):
|
29
|
+
name: str
|
30
|
+
persona: str
|
31
|
+
privacy_level: str
|
32
|
+
icon: str
|
33
|
+
color: str
|
34
|
+
chat_model: str
|
35
|
+
files: Optional[List[str]] = []
|
36
|
+
input_tools: Optional[List[str]] = []
|
37
|
+
output_modes: Optional[List[str]] = []
|
38
|
+
|
39
|
+
|
19
40
|
@api_agents.get("", response_class=Response)
|
20
41
|
async def all_agents(
|
21
42
|
request: Request,
|
@@ -25,17 +46,22 @@ async def all_agents(
|
|
25
46
|
agents = await AgentAdapters.aget_all_accessible_agents(user)
|
26
47
|
agents_packet = list()
|
27
48
|
for agent in agents:
|
49
|
+
files = agent.fileobject_set.all()
|
50
|
+
file_names = [file.file_name for file in files]
|
28
51
|
agents_packet.append(
|
29
52
|
{
|
30
53
|
"slug": agent.slug,
|
31
|
-
"avatar": agent.avatar,
|
32
54
|
"name": agent.name,
|
33
55
|
"persona": agent.personality,
|
34
|
-
"public": agent.public,
|
35
56
|
"creator": agent.creator.username if agent.creator else None,
|
36
57
|
"managed_by_admin": agent.managed_by_admin,
|
37
58
|
"color": agent.style_color,
|
38
59
|
"icon": agent.style_icon,
|
60
|
+
"privacy_level": agent.privacy_level,
|
61
|
+
"chat_model": agent.chat_model.chat_model,
|
62
|
+
"files": file_names,
|
63
|
+
"input_tools": agent.input_tools,
|
64
|
+
"output_modes": agent.output_modes,
|
39
65
|
}
|
40
66
|
)
|
41
67
|
|
@@ -43,3 +69,205 @@ async def all_agents(
|
|
43
69
|
agents_packet.sort(key=lambda x: x["name"])
|
44
70
|
agents_packet.sort(key=lambda x: x["slug"] == "khoj", reverse=True)
|
45
71
|
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
72
|
+
|
73
|
+
|
74
|
+
@api_agents.get("/options", response_class=Response)
|
75
|
+
async def get_agent_configuration_options(
|
76
|
+
request: Request,
|
77
|
+
common: CommonQueryParams,
|
78
|
+
) -> Response:
|
79
|
+
agent_input_tools = [key for key, _ in Agent.InputToolOptions.choices]
|
80
|
+
agent_output_modes = [key for key, _ in Agent.OutputModeOptions.choices]
|
81
|
+
|
82
|
+
agent_input_tool_with_descriptions: Dict[str, str] = {}
|
83
|
+
for key in agent_input_tools:
|
84
|
+
conversation_command = ConversationCommand(key)
|
85
|
+
agent_input_tool_with_descriptions[key] = command_descriptions_for_agent[conversation_command]
|
86
|
+
|
87
|
+
agent_output_modes_with_descriptions: Dict[str, str] = {}
|
88
|
+
for key in agent_output_modes:
|
89
|
+
conversation_command = ConversationCommand(key)
|
90
|
+
agent_output_modes_with_descriptions[key] = mode_descriptions_for_agent[conversation_command]
|
91
|
+
|
92
|
+
return Response(
|
93
|
+
content=json.dumps(
|
94
|
+
{
|
95
|
+
"input_tools": agent_input_tool_with_descriptions,
|
96
|
+
"output_modes": agent_output_modes_with_descriptions,
|
97
|
+
}
|
98
|
+
),
|
99
|
+
media_type="application/json",
|
100
|
+
status_code=200,
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
@api_agents.get("/{agent_slug}", response_class=Response)
|
105
|
+
async def get_agent(
|
106
|
+
request: Request,
|
107
|
+
common: CommonQueryParams,
|
108
|
+
agent_slug: str,
|
109
|
+
) -> Response:
|
110
|
+
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
111
|
+
agent = await AgentAdapters.aget_readonly_agent_by_slug(agent_slug, user)
|
112
|
+
|
113
|
+
if not agent:
|
114
|
+
return Response(
|
115
|
+
content=json.dumps({"error": f"Agent with name {agent_slug} not found."}),
|
116
|
+
media_type="application/json",
|
117
|
+
status_code=404,
|
118
|
+
)
|
119
|
+
|
120
|
+
files = agent.fileobject_set.all()
|
121
|
+
file_names = [file.file_name for file in files]
|
122
|
+
agents_packet = {
|
123
|
+
"slug": agent.slug,
|
124
|
+
"name": agent.name,
|
125
|
+
"persona": agent.personality,
|
126
|
+
"creator": agent.creator.username if agent.creator else None,
|
127
|
+
"managed_by_admin": agent.managed_by_admin,
|
128
|
+
"color": agent.style_color,
|
129
|
+
"icon": agent.style_icon,
|
130
|
+
"privacy_level": agent.privacy_level,
|
131
|
+
"chat_model": agent.chat_model.chat_model,
|
132
|
+
"files": file_names,
|
133
|
+
"input_tools": agent.input_tools,
|
134
|
+
"output_modes": agent.output_modes,
|
135
|
+
}
|
136
|
+
|
137
|
+
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
138
|
+
|
139
|
+
|
140
|
+
@api_agents.delete("/{agent_slug}", response_class=Response)
|
141
|
+
@requires(["authenticated"])
|
142
|
+
async def delete_agent(
|
143
|
+
request: Request,
|
144
|
+
common: CommonQueryParams,
|
145
|
+
agent_slug: str,
|
146
|
+
) -> Response:
|
147
|
+
user: KhojUser = request.user.object
|
148
|
+
|
149
|
+
agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
|
150
|
+
|
151
|
+
if not agent:
|
152
|
+
return Response(
|
153
|
+
content=json.dumps({"error": f"Agent with name {agent_slug} not found."}),
|
154
|
+
media_type="application/json",
|
155
|
+
status_code=404,
|
156
|
+
)
|
157
|
+
|
158
|
+
await AgentAdapters.adelete_agent_by_slug(agent_slug, user)
|
159
|
+
|
160
|
+
return Response(content=json.dumps({"message": "Agent deleted."}), media_type="application/json", status_code=200)
|
161
|
+
|
162
|
+
|
163
|
+
@api_agents.post("", response_class=Response)
|
164
|
+
@requires(["authenticated"])
|
165
|
+
async def create_agent(
|
166
|
+
request: Request,
|
167
|
+
common: CommonQueryParams,
|
168
|
+
body: ModifyAgentBody,
|
169
|
+
) -> Response:
|
170
|
+
user: KhojUser = request.user.object
|
171
|
+
|
172
|
+
is_safe_prompt, reason = True, ""
|
173
|
+
|
174
|
+
if body.privacy_level != Agent.PrivacyLevel.PRIVATE:
|
175
|
+
is_safe_prompt, reason = await acheck_if_safe_prompt(body.persona)
|
176
|
+
|
177
|
+
if not is_safe_prompt:
|
178
|
+
return Response(
|
179
|
+
content=json.dumps({"error": f"{reason}"}),
|
180
|
+
media_type="application/json",
|
181
|
+
status_code=400,
|
182
|
+
)
|
183
|
+
|
184
|
+
agent = await AgentAdapters.aupdate_agent(
|
185
|
+
user,
|
186
|
+
body.name,
|
187
|
+
body.persona,
|
188
|
+
body.privacy_level,
|
189
|
+
body.icon,
|
190
|
+
body.color,
|
191
|
+
body.chat_model,
|
192
|
+
body.files,
|
193
|
+
body.input_tools,
|
194
|
+
body.output_modes,
|
195
|
+
)
|
196
|
+
|
197
|
+
agents_packet = {
|
198
|
+
"slug": agent.slug,
|
199
|
+
"name": agent.name,
|
200
|
+
"persona": agent.personality,
|
201
|
+
"creator": agent.creator.username if agent.creator else None,
|
202
|
+
"managed_by_admin": agent.managed_by_admin,
|
203
|
+
"color": agent.style_color,
|
204
|
+
"icon": agent.style_icon,
|
205
|
+
"privacy_level": agent.privacy_level,
|
206
|
+
"chat_model": agent.chat_model.chat_model,
|
207
|
+
"files": body.files,
|
208
|
+
"input_tools": agent.input_tools,
|
209
|
+
"output_modes": agent.output_modes,
|
210
|
+
}
|
211
|
+
|
212
|
+
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
213
|
+
|
214
|
+
|
215
|
+
@api_agents.patch("", response_class=Response)
|
216
|
+
@requires(["authenticated"])
|
217
|
+
async def update_agent(
|
218
|
+
request: Request,
|
219
|
+
common: CommonQueryParams,
|
220
|
+
body: ModifyAgentBody,
|
221
|
+
) -> Response:
|
222
|
+
user: KhojUser = request.user.object
|
223
|
+
|
224
|
+
is_safe_prompt, reason = True, ""
|
225
|
+
|
226
|
+
if body.privacy_level != Agent.PrivacyLevel.PRIVATE:
|
227
|
+
is_safe_prompt, reason = await acheck_if_safe_prompt(body.persona)
|
228
|
+
|
229
|
+
if not is_safe_prompt:
|
230
|
+
return Response(
|
231
|
+
content=json.dumps({"error": f"{reason}"}),
|
232
|
+
media_type="application/json",
|
233
|
+
status_code=400,
|
234
|
+
)
|
235
|
+
|
236
|
+
selected_agent = await AgentAdapters.aget_agent_by_name(body.name, user)
|
237
|
+
|
238
|
+
if not selected_agent:
|
239
|
+
return Response(
|
240
|
+
content=json.dumps({"error": f"Agent with name {body.name} not found."}),
|
241
|
+
media_type="application/json",
|
242
|
+
status_code=404,
|
243
|
+
)
|
244
|
+
|
245
|
+
agent = await AgentAdapters.aupdate_agent(
|
246
|
+
user,
|
247
|
+
body.name,
|
248
|
+
body.persona,
|
249
|
+
body.privacy_level,
|
250
|
+
body.icon,
|
251
|
+
body.color,
|
252
|
+
body.chat_model,
|
253
|
+
body.files,
|
254
|
+
body.input_tools,
|
255
|
+
body.output_modes,
|
256
|
+
)
|
257
|
+
|
258
|
+
agents_packet = {
|
259
|
+
"slug": agent.slug,
|
260
|
+
"name": agent.name,
|
261
|
+
"persona": agent.personality,
|
262
|
+
"creator": agent.creator.username if agent.creator else None,
|
263
|
+
"managed_by_admin": agent.managed_by_admin,
|
264
|
+
"color": agent.style_color,
|
265
|
+
"icon": agent.style_icon,
|
266
|
+
"privacy_level": agent.privacy_level,
|
267
|
+
"chat_model": agent.chat_model.chat_model,
|
268
|
+
"files": body.files,
|
269
|
+
"input_tools": agent.input_tools,
|
270
|
+
"output_modes": agent.output_modes,
|
271
|
+
}
|
272
|
+
|
273
|
+
return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200)
|
khoj/routers/api_chat.py
CHANGED
@@ -17,13 +17,14 @@ from starlette.authentication import has_required_scope, requires
|
|
17
17
|
|
18
18
|
from khoj.app.settings import ALLOWED_HOSTS
|
19
19
|
from khoj.database.adapters import (
|
20
|
+
AgentAdapters,
|
20
21
|
ConversationAdapters,
|
21
22
|
EntryAdapters,
|
22
23
|
FileObjectAdapters,
|
23
24
|
PublicConversationAdapters,
|
24
25
|
aget_user_name,
|
25
26
|
)
|
26
|
-
from khoj.database.models import KhojUser
|
27
|
+
from khoj.database.models import Agent, KhojUser
|
27
28
|
from khoj.processor.conversation.prompts import help_message, no_entries_found
|
28
29
|
from khoj.processor.conversation.utils import save_to_conversation_log
|
29
30
|
from khoj.processor.image.generate import text_to_image
|
@@ -65,7 +66,7 @@ from khoj.utils.rawconfig import FileFilterRequest, FilesFilterRequest, Location
|
|
65
66
|
# Initialize Router
|
66
67
|
logger = logging.getLogger(__name__)
|
67
68
|
conversation_command_rate_limiter = ConversationCommandRateLimiter(
|
68
|
-
trial_rate_limit=100, subscribed_rate_limit=
|
69
|
+
trial_rate_limit=100, subscribed_rate_limit=6000, slug="command"
|
69
70
|
)
|
70
71
|
|
71
72
|
|
@@ -211,7 +212,6 @@ def chat_history(
|
|
211
212
|
agent_metadata = {
|
212
213
|
"slug": conversation.agent.slug,
|
213
214
|
"name": conversation.agent.name,
|
214
|
-
"avatar": conversation.agent.avatar,
|
215
215
|
"isCreator": conversation.agent.creator == user,
|
216
216
|
"color": conversation.agent.style_color,
|
217
217
|
"icon": conversation.agent.style_icon,
|
@@ -268,7 +268,6 @@ def get_shared_chat(
|
|
268
268
|
agent_metadata = {
|
269
269
|
"slug": conversation.agent.slug,
|
270
270
|
"name": conversation.agent.name,
|
271
|
-
"avatar": conversation.agent.avatar,
|
272
271
|
"isCreator": conversation.agent.creator == user,
|
273
272
|
"color": conversation.agent.style_color,
|
274
273
|
"icon": conversation.agent.style_icon,
|
@@ -418,7 +417,7 @@ def chat_sessions(
|
|
418
417
|
conversations = conversations[:8]
|
419
418
|
|
420
419
|
sessions = conversations.values_list(
|
421
|
-
"id", "slug", "title", "agent__slug", "agent__name", "
|
420
|
+
"id", "slug", "title", "agent__slug", "agent__name", "created_at", "updated_at"
|
422
421
|
)
|
423
422
|
|
424
423
|
session_values = [
|
@@ -426,9 +425,8 @@ def chat_sessions(
|
|
426
425
|
"conversation_id": str(session[0]),
|
427
426
|
"slug": session[2] or session[1],
|
428
427
|
"agent_name": session[4],
|
429
|
-
"
|
430
|
-
"
|
431
|
-
"updated": session[7].strftime("%Y-%m-%d %H:%M:%S"),
|
428
|
+
"created": session[5].strftime("%Y-%m-%d %H:%M:%S"),
|
429
|
+
"updated": session[6].strftime("%Y-%m-%d %H:%M:%S"),
|
432
430
|
}
|
433
431
|
for session in sessions
|
434
432
|
]
|
@@ -590,7 +588,7 @@ async def chat(
|
|
590
588
|
nonlocal connection_alive, ttft
|
591
589
|
if not connection_alive or await request.is_disconnected():
|
592
590
|
connection_alive = False
|
593
|
-
logger.
|
591
|
+
logger.warning(f"User {user} disconnected from {common.client} client")
|
594
592
|
return
|
595
593
|
try:
|
596
594
|
if event_type == ChatEvent.END_LLM_RESPONSE:
|
@@ -658,6 +656,16 @@ async def chat(
|
|
658
656
|
return
|
659
657
|
conversation_id = conversation.id
|
660
658
|
|
659
|
+
agent: Agent | None = None
|
660
|
+
default_agent = await AgentAdapters.aget_default_agent()
|
661
|
+
if conversation.agent and conversation.agent != default_agent:
|
662
|
+
agent = conversation.agent
|
663
|
+
|
664
|
+
if not conversation.agent:
|
665
|
+
conversation.agent = default_agent
|
666
|
+
await conversation.asave()
|
667
|
+
agent = default_agent
|
668
|
+
|
661
669
|
await is_ready_to_chat(user)
|
662
670
|
|
663
671
|
user_name = await aget_user_name(user)
|
@@ -677,7 +685,12 @@ async def chat(
|
|
677
685
|
|
678
686
|
if conversation_commands == [ConversationCommand.Default] or is_automated_task:
|
679
687
|
conversation_commands = await aget_relevant_information_sources(
|
680
|
-
q,
|
688
|
+
q,
|
689
|
+
meta_log,
|
690
|
+
is_automated_task,
|
691
|
+
subscribed=subscribed,
|
692
|
+
uploaded_image_url=uploaded_image_url,
|
693
|
+
agent=agent,
|
681
694
|
)
|
682
695
|
conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
|
683
696
|
async for result in send_event(
|
@@ -685,7 +698,7 @@ async def chat(
|
|
685
698
|
):
|
686
699
|
yield result
|
687
700
|
|
688
|
-
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url)
|
701
|
+
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url, agent)
|
689
702
|
async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"):
|
690
703
|
yield result
|
691
704
|
if mode not in conversation_commands:
|
@@ -709,19 +722,30 @@ async def chat(
|
|
709
722
|
conversation_commands.remove(ConversationCommand.Summarize)
|
710
723
|
elif ConversationCommand.Summarize in conversation_commands:
|
711
724
|
response_log = ""
|
712
|
-
|
725
|
+
agent_has_entries = await EntryAdapters.aagent_has_entries(agent)
|
726
|
+
if len(file_filters) == 0 and not agent_has_entries:
|
713
727
|
response_log = "No files selected for summarization. Please add files using the section on the left."
|
714
728
|
async for result in send_llm_response(response_log):
|
715
729
|
yield result
|
716
|
-
elif len(file_filters) > 1:
|
730
|
+
elif len(file_filters) > 1 and not agent_has_entries:
|
717
731
|
response_log = "Only one file can be selected for summarization."
|
718
732
|
async for result in send_llm_response(response_log):
|
719
733
|
yield result
|
720
734
|
else:
|
721
735
|
try:
|
722
|
-
file_object =
|
736
|
+
file_object = None
|
737
|
+
if await EntryAdapters.aagent_has_entries(agent):
|
738
|
+
file_names = await EntryAdapters.aget_agent_entry_filepaths(agent)
|
739
|
+
if len(file_names) > 0:
|
740
|
+
file_object = await FileObjectAdapters.async_get_file_objects_by_name(
|
741
|
+
None, file_names[0], agent
|
742
|
+
)
|
743
|
+
|
744
|
+
if len(file_filters) > 0:
|
745
|
+
file_object = await FileObjectAdapters.async_get_file_objects_by_name(user, file_filters[0])
|
746
|
+
|
723
747
|
if len(file_object) == 0:
|
724
|
-
response_log = "Sorry,
|
748
|
+
response_log = "Sorry, I couldn't find the full text of this file. Please re-upload the document and try again."
|
725
749
|
async for result in send_llm_response(response_log):
|
726
750
|
yield result
|
727
751
|
return
|
@@ -734,13 +758,13 @@ async def chat(
|
|
734
758
|
yield result
|
735
759
|
|
736
760
|
response = await extract_relevant_summary(
|
737
|
-
q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url
|
761
|
+
q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url, agent=agent
|
738
762
|
)
|
739
763
|
response_log = str(response)
|
740
764
|
async for result in send_llm_response(response_log):
|
741
765
|
yield result
|
742
766
|
except Exception as e:
|
743
|
-
response_log = "Error summarizing file."
|
767
|
+
response_log = "Error summarizing file. Please try again, or contact support."
|
744
768
|
logger.error(f"Error summarizing file for {user.email}: {e}", exc_info=True)
|
745
769
|
async for result in send_llm_response(response_log):
|
746
770
|
yield result
|
@@ -816,6 +840,7 @@ async def chat(
|
|
816
840
|
location,
|
817
841
|
partial(send_event, ChatEvent.STATUS),
|
818
842
|
uploaded_image_url=uploaded_image_url,
|
843
|
+
agent=agent,
|
819
844
|
):
|
820
845
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
821
846
|
yield result[ChatEvent.STATUS]
|
@@ -853,6 +878,7 @@ async def chat(
|
|
853
878
|
partial(send_event, ChatEvent.STATUS),
|
854
879
|
custom_filters,
|
855
880
|
uploaded_image_url=uploaded_image_url,
|
881
|
+
agent=agent,
|
856
882
|
):
|
857
883
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
858
884
|
yield result[ChatEvent.STATUS]
|
@@ -876,6 +902,7 @@ async def chat(
|
|
876
902
|
subscribed,
|
877
903
|
partial(send_event, ChatEvent.STATUS),
|
878
904
|
uploaded_image_url=uploaded_image_url,
|
905
|
+
agent=agent,
|
879
906
|
):
|
880
907
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
881
908
|
yield result[ChatEvent.STATUS]
|
@@ -922,6 +949,7 @@ async def chat(
|
|
922
949
|
subscribed=subscribed,
|
923
950
|
send_status_func=partial(send_event, ChatEvent.STATUS),
|
924
951
|
uploaded_image_url=uploaded_image_url,
|
952
|
+
agent=agent,
|
925
953
|
):
|
926
954
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
927
955
|
yield result[ChatEvent.STATUS]
|
@@ -1132,6 +1160,7 @@ async def get_chat(
|
|
1132
1160
|
yield result
|
1133
1161
|
return
|
1134
1162
|
conversation_id = conversation.id
|
1163
|
+
agent = conversation.agent if conversation.agent else None
|
1135
1164
|
|
1136
1165
|
await is_ready_to_chat(user)
|
1137
1166
|
|