From be11f666e4bd3b30235a80ca57a44e22c527a0ba Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 19 Jan 2025 12:16:37 -0800 Subject: [PATCH] Initialize the concept in the backend of hidden agents A hidden agent basically allows each individual conversation to maintain custom settings, via an agent that's not exposed to the traditional functionalities allotted for manually created agents (e.g., browsing, maintenance in agents page). This will be hooked up to the front-end such that any conversation that's initiated with the default agent can then be given custom settings, which in the background creates a hidden agent. This allows us to repurpose all of our existing agents infrastructure for chat-level customization. --- src/khoj/database/adapters/__init__.py | 34 +++++++- src/khoj/database/models/__init__.py | 1 - src/khoj/routers/api_agents.py | 107 ++++++++++++++++++++++++- src/khoj/routers/api_chat.py | 12 ++- 4 files changed, 147 insertions(+), 7 deletions(-) diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 31ccd2f2..0390ce23 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -69,6 +69,7 @@ from khoj.search_filter.word_filter import WordFilter from khoj.utils import state from khoj.utils.config import OfflineChatProcessorModel from khoj.utils.helpers import ( + generate_random_internal_agent_name, generate_random_name, in_debug_mode, is_none_or_empty, @@ -806,13 +807,15 @@ class AgentAdapters: privacy_level: str, icon: str, color: str, - chat_model: str, + chat_model: Optional[str], files: List[str], input_tools: List[str], output_modes: List[str], slug: Optional[str] = None, is_hidden: Optional[bool] = False, ): + if not chat_model: + chat_model = ConversationAdapters.get_default_chat_model(user) chat_model_option = await ChatModel.objects.filter(name=chat_model).afirst() # Slug will be None for new agents, which will trigger a new agent creation with a generated, immutable slug @@ -864,6 +867,35 @@ class AgentAdapters: return agent + @staticmethod + @arequire_valid_user + async def aupdate_hidden_agent( + user: KhojUser, + slug: Optional[str] = None, + persona: Optional[str] = None, + chat_model: Optional[str] = None, + input_tools: Optional[List[str]] = None, + output_modes: Optional[List[str]] = None, + ): + random_name = generate_random_internal_agent_name() + + agent = await AgentAdapters.aupdate_agent( + user=user, + name=random_name, + personality=persona, + privacy_level=Agent.PrivacyLevel.PRIVATE, + icon=Agent.StyleIconTypes.LIGHTBULB, + color=Agent.StyleColorTypes.BLUE, + chat_model=chat_model, + files=[], + input_tools=input_tools, + output_modes=output_modes, + slug=slug, + is_hidden=True, + ) + + return agent + class PublicConversationAdapters: @staticmethod diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 97b24510..b88ae3f3 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -285,7 +285,6 @@ class Agent(DbBaseModel): class OutputModeOptions(models.TextChoices): # These map to various ConversationCommand types - TEXT = "text" IMAGE = "image" AUTOMATION = "automation" DIAGRAM = "diagram" diff --git a/src/khoj/routers/api_agents.py b/src/khoj/routers/api_agents.py index 87693f56..432117e1 100644 --- a/src/khoj/routers/api_agents.py +++ b/src/khoj/routers/api_agents.py @@ -9,7 +9,7 @@ from fastapi import APIRouter, Request from fastapi.requests import Request from fastapi.responses import Response from pydantic import BaseModel -from starlette.authentication import requires +from starlette.authentication import has_required_scope, requires from khoj.database.adapters import AgentAdapters, ConversationAdapters from khoj.database.models import Agent, Conversation, KhojUser @@ -41,6 +41,14 @@ class ModifyAgentBody(BaseModel): is_hidden: Optional[bool] = False +class ModifyHiddenAgentBody(BaseModel): + slug: str + persona: Optional[str] = None + chat_model: Optional[str] = None + input_tools: Optional[List[str]] = [] + output_modes: Optional[List[str]] = [] + + @api_agents.get("", response_class=Response) async def all_agents( request: Request, @@ -183,6 +191,93 @@ async def delete_agent( return Response(content=json.dumps({"message": "Agent deleted."}), media_type="application/json", status_code=200) +@api_agents.patch("/hidden", response_class=Response) +@requires(["authenticated"]) +async def update_hidden_agent( + request: Request, + common: CommonQueryParams, + body: ModifyHiddenAgentBody, +) -> Response: + user: KhojUser = request.user.object + + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + + selected_agent = await AgentAdapters.aget_agent_by_slug(body.slug, user) + + if not selected_agent: + return Response( + content=json.dumps({"error": f"Agent with name {body.slug} not found."}), + media_type="application/json", + status_code=404, + ) + + agent = await AgentAdapters.aupdate_hidden_agent( + user, + body.slug, + body.persona, + chat_model, + body.input_tools, + body.output_modes, + ) + + agents_packet = { + "slug": agent.slug, + "name": agent.name, + "persona": agent.personality, + "creator": agent.creator.username if agent.creator else None, + "managed_by_admin": agent.managed_by_admin, + "color": agent.style_color, + "icon": agent.style_icon, + "privacy_level": agent.privacy_level, + "chat_model": agent.chat_model.name, + "files": body.files, + "input_tools": agent.input_tools, + "output_modes": agent.output_modes, + } + + return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) + + +@api_agents.post("/hidden", response_class=Response) +@requires(["authenticated"]) +async def create_hidden_agent( + request: Request, + common: CommonQueryParams, + body: ModifyHiddenAgentBody, +) -> Response: + user: KhojUser = request.user.object + + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + + agent = await AgentAdapters.aupdate_hidden_agent( + user, + body.slug, + body.persona, + chat_model, + body.input_tools, + body.output_modes, + ) + + agents_packet = { + "slug": agent.slug, + "name": agent.name, + "persona": agent.personality, + "creator": agent.creator.username if agent.creator else None, + "managed_by_admin": agent.managed_by_admin, + "color": agent.style_color, + "icon": agent.style_icon, + "privacy_level": agent.privacy_level, + "chat_model": agent.chat_model.name, + "files": body.files, + "input_tools": agent.input_tools, + "output_modes": agent.output_modes, + } + + return Response(content=json.dumps(agents_packet), media_type="application/json", status_code=200) + + @api_agents.post("", response_class=Response) @requires(["authenticated"]) async def create_agent( @@ -203,6 +298,9 @@ async def create_agent( status_code=400, ) + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + agent = await AgentAdapters.aupdate_agent( user, body.name, @@ -210,7 +308,7 @@ async def create_agent( body.privacy_level, body.icon, body.color, - body.chat_model, + chat_model, body.files, body.input_tools, body.output_modes, @@ -266,6 +364,9 @@ async def update_agent( status_code=404, ) + subscribed = has_required_scope(request, ["premium"]) + chat_model = body.chat_model if subscribed else None + agent = await AgentAdapters.aupdate_agent( user, body.name, @@ -273,7 +374,7 @@ async def update_agent( body.privacy_level, body.icon, body.color, - body.chat_model, + chat_model, body.files, body.input_tools, body.output_modes, diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 8d9bd7cc..99f585d0 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -223,13 +223,17 @@ def chat_history( if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user: conversation.agent = None else: + agent_has_files = EntryAdapters.agent_has_entries(conversation.agent) agent_metadata = { "slug": conversation.agent.slug, "name": conversation.agent.name, - "isCreator": conversation.agent.creator == user, + "is_creator": conversation.agent.creator == user, "color": conversation.agent.style_color, "icon": conversation.agent.style_icon, "persona": conversation.agent.personality, + "is_hidden": conversation.agent.is_hidden, + "chat_model": conversation.agent.chat_model.name, + "has_files": agent_has_files, } meta_log = conversation.conversation_log @@ -282,13 +286,17 @@ def get_shared_chat( if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE: conversation.agent = None else: + agent_has_files = EntryAdapters.agent_has_entries(conversation.agent) agent_metadata = { "slug": conversation.agent.slug, "name": conversation.agent.name, - "isCreator": conversation.agent.creator == user, + "is_creator": conversation.agent.creator == user, "color": conversation.agent.style_color, "icon": conversation.agent.style_icon, "persona": conversation.agent.personality, + "is_hidden": conversation.agent.is_hidden, + "chat_model": conversation.agent.chat_model.name, + "has_files": agent_has_files, } meta_log = conversation.conversation_log