diff --git a/manifest.json b/manifest.json index 62d18281..05b35dd3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.24.1", + "version": "1.25.0", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index b3bc7f07..092272b1 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.24.1", + "version": "1.25.0", "description": "Your Second Brain", "author": "Khoj Inc. ", "license": "GPL-3.0-or-later", diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 25c12871..84141553 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Saba Imran ;; Description: Your Second Brain ;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image -;; Version: 1.24.1 +;; Version: 1.25.0 ;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs diff --git a/src/interface/obsidian/manifest.json b/src/interface/obsidian/manifest.json index 62d18281..05b35dd3 100644 --- a/src/interface/obsidian/manifest.json +++ b/src/interface/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.24.1", + "version": "1.25.0", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index 5300845e..5fcda33d 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.24.1", + "version": "1.25.0", "description": "Your Second Brain", "author": "Debanjum Singh Solanky, Saba Imran ", "license": "GPL-3.0-or-later", diff --git a/src/interface/obsidian/versions.json b/src/interface/obsidian/versions.json index 59c69190..24ed5a84 100644 --- a/src/interface/obsidian/versions.json +++ b/src/interface/obsidian/versions.json @@ -76,5 +76,6 @@ "1.23.2": "0.15.0", "1.23.3": "0.15.0", "1.24.0": "0.15.0", - "1.24.1": "0.15.0" + "1.24.1": "0.15.0", + "1.25.0": "0.15.0" } diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index 59d9d545..800dcb16 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -32,7 +32,6 @@ import { Globe, LockOpen, FloppyDisk, - DotsThreeCircleVertical, DotsThreeVertical, Pencil, Trash, @@ -46,16 +45,6 @@ import { DialogHeader, DialogTrigger, } from "@/components/ui/dialog"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer"; import LoginPrompt from "../components/loginPrompt/loginPrompt"; import { InlineLoading } from "../components/loading/loading"; import SidePanel from "../components/sidePanel/chatHistorySidePanel"; @@ -340,281 +329,149 @@ function AgentCard(props: AgentCardProps) { )} - {!props.isMobileWidth ? ( - { - setShowModal(!showModal); - window.history.pushState({}, `Khoj AI - Agents`, `/agents`); - }} - > - -
- {getIconFromIconName(props.data.icon, props.data.color)} - {props.data.name} -
-
-
- {props.editCard && ( -
- - - - - { + setShowModal(!showModal); + window.history.pushState({}, `Khoj AI - Agents`, `/agents`); + }} + > + +
+ {getIconFromIconName(props.data.icon, props.data.color)} + {props.data.name} +
+
+
+ {props.editCard && ( +
+ + + + + + + {props.editCard && + props.data.privacy_level !== "private" && ( + + )} + {props.data.creator === userData?.username && ( - {props.editCard && - props.data.privacy_level !== "private" && ( - - )} - {props.data.creator === userData?.username && ( - - )} - - -
- )} -
- {props.userProfile ? ( - - ) : ( - - )} + )} + +
-
- {props.editCard ? ( - - - Edit {props.data.name} - - - - ) : ( - - -
- {getIconFromIconName(props.data.icon, props.data.color)} -

{props.data.name}

-
-
-
- {props.data.persona} -
-
- {makeBadgeFooter()} -
- - - -
)} -
- ) : ( - { - setShowModal(open); - window.history.pushState({}, `Khoj AI - Agents`, `/agents`); - }} - > - -
- {getIconFromIconName(props.data.icon, props.data.color)} - {props.data.name} -
-
-
- {props.editCard && ( -
- - - - - - - {props.editCard && - props.data.privacy_level !== "private" && ( - - )} - {props.data.creator === userData?.username && ( - - )} - - -
+
+ {props.userProfile ? ( + + ) : ( + )} -
- {props.userProfile ? ( - - ) : ( - - )} -
- {props.editCard ? ( - - - - ) : ( - - - {props.data.name} - Persona - +
+ {props.editCard ? ( + + + Edit {props.data.name} + + + + ) : ( + + +
+ {getIconFromIconName(props.data.icon, props.data.color)} +

{props.data.name}

+
+
+
{props.data.persona} -
- {makeBadgeFooter()} -
- - Done - - - )} - - )} +
+
+ {makeBadgeFooter()} +
+ + + +
+ )} +
@@ -930,7 +787,7 @@ function AgentModificationForm(props: AgentModificationFormProps) { />
Look & Feel -
+
- -
- - Create Agent -
-
- - - Create Agent - - {!props.userProfile && showLoginPrompt && ( - - )} - - - Dismiss - - - - ); - } - return ( diff --git a/src/interface/web/package.json b/src/interface/web/package.json index 15c2ecf7..143d372b 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -1,6 +1,6 @@ { "name": "khoj-ai", - "version": "1.24.1", + "version": "1.25.0", "private": true, "scripts": { "dev": "next dev", diff --git a/src/khoj/configure.py b/src/khoj/configure.py index 53e19f71..b60c00d1 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -42,6 +42,7 @@ from khoj.database.adapters import ( from khoj.database.models import ClientApplication, KhojUser, ProcessLock, Subscription from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel from khoj.routers.api_content import configure_content, configure_search +from khoj.routers.helpers import update_telemetry_state from khoj.routers.twilio import is_twilio_enabled from khoj.utils import constants, state from khoj.utils.config import SearchType @@ -165,7 +166,15 @@ class UserAuthenticationBackend(AuthenticationBackend): create_if_not_exists = request.query_params.get("create_if_not_exists") if create_if_not_exists: - user = await aget_or_create_user_by_phone_number(phone_number) + user, is_new = await aget_or_create_user_by_phone_number(phone_number) + if user and is_new: + update_telemetry_state( + request=request, + telemetry_type="api", + api="create_user", + metadata={"user_id": str(user.uuid)}, + ) + logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}") else: user = await aget_user_by_phone_number(phone_number) @@ -244,7 +253,7 @@ def configure_server( state.SearchType = configure_search_types() state.search_models = configure_search(state.search_models, state.config.search_type) - setup_default_agent() + setup_default_agent(user) message = "📡 Telemetry disabled" if telemetry_disabled(state.config.app) else "📡 Telemetry enabled" logger.info(message) @@ -256,8 +265,8 @@ def configure_server( raise e -def setup_default_agent(): - AgentAdapters.create_default_agent() +def setup_default_agent(user: KhojUser): + AgentAdapters.create_default_agent(user) def initialize_content(regenerate: bool, search_type: Optional[SearchType] = None, user: KhojUser = None): diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 027490be..c67fa91a 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -113,13 +113,15 @@ async def get_or_create_user(token: dict) -> KhojUser: return user -async def aget_or_create_user_by_phone_number(phone_number: str) -> KhojUser: +async def aget_or_create_user_by_phone_number(phone_number: str) -> tuple[KhojUser, bool]: + is_new = False if is_none_or_empty(phone_number): - return None + return None, is_new user = await aget_user_by_phone_number(phone_number) if not user: user = await acreate_user_by_phone_number(phone_number) - return user + is_new = True + return user, is_new async def aset_user_phone_number(user: KhojUser, phone_number: str) -> KhojUser: @@ -165,8 +167,10 @@ async def acreate_user_by_phone_number(phone_number: str) -> KhojUser: return user -async def aget_or_create_user_by_email(email: str) -> KhojUser: - user, _ = await KhojUser.objects.filter(email=email).aupdate_or_create(defaults={"username": email, "email": email}) +async def aget_or_create_user_by_email(email: str) -> tuple[KhojUser, bool]: + user, is_new = await KhojUser.objects.filter(email=email).aupdate_or_create( + defaults={"username": email, "email": email} + ) await user.asave() if user: @@ -177,7 +181,7 @@ async def aget_or_create_user_by_email(email: str) -> KhojUser: if not user_subscription: await Subscription.objects.acreate(user=user, type="trial") - return user + return user, is_new async def aget_user_validated_by_email_verification_code(code: str) -> KhojUser: @@ -248,9 +252,9 @@ def get_user_subscription(email: str) -> Optional[Subscription]: async def set_user_subscription( email: str, is_recurring=None, renewal_date=None, type="standard" -) -> Optional[Subscription]: +) -> tuple[Optional[Subscription], bool]: # Get or create the user object and their subscription - user = await aget_or_create_user_by_email(email) + user, is_new = await aget_or_create_user_by_email(email) user_subscription = await Subscription.objects.filter(user=user).afirst() # Update the user subscription state @@ -262,7 +266,7 @@ async def set_user_subscription( elif renewal_date is not None: user_subscription.renewal_date = renewal_date await user_subscription.asave() - return user_subscription + return user_subscription, is_new def subscription_to_state(subscription: Subscription) -> str: @@ -643,8 +647,8 @@ class AgentAdapters: return Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first() @staticmethod - def create_default_agent(): - default_conversation_config = ConversationAdapters.get_default_conversation_config() + def create_default_agent(user: KhojUser): + default_conversation_config = ConversationAdapters.get_default_conversation_config(user) if default_conversation_config is None: logger.info("No default conversation config found, skipping default agent creation") return None @@ -968,29 +972,51 @@ class ConversationAdapters: return VoiceModelOption.objects.first() @staticmethod - def get_default_conversation_config(): + def get_default_conversation_config(user: KhojUser = None): + """Get default conversation config. Prefer chat model by server admin > user > first created chat model""" + # Get the server chat settings server_chat_settings = ServerChatSettings.objects.first() - if server_chat_settings is None or server_chat_settings.chat_default is None: - return ChatModelOptions.objects.filter().first() - return server_chat_settings.chat_default + if server_chat_settings is not None and server_chat_settings.chat_default is not None: + return server_chat_settings.chat_default + + # Get the user's chat settings, if the server chat settings are not set + user_chat_settings = UserConversationConfig.objects.filter(user=user).first() if user else None + if user_chat_settings is not None and user_chat_settings.setting is not None: + return user_chat_settings.setting + + # Get the first chat model if even the user chat settings are not set + return ChatModelOptions.objects.filter().first() @staticmethod - async def aget_default_conversation_config(): + async def aget_default_conversation_config(user: KhojUser = None): + """Get default conversation config. Prefer chat model by server admin > user > first created chat model""" + # Get the server chat settings server_chat_settings: ServerChatSettings = ( await ServerChatSettings.objects.filter() .prefetch_related("chat_default", "chat_default__openai_config") .afirst() ) - if server_chat_settings is None or server_chat_settings.chat_default is None: - return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst() - return server_chat_settings.chat_default + if server_chat_settings is not None and server_chat_settings.chat_default is not None: + return server_chat_settings.chat_default + + # Get the user's chat settings, if the server chat settings are not set + user_chat_settings = ( + (await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__openai_config").afirst()) + if user + else None + ) + if user_chat_settings is not None and user_chat_settings.setting is not None: + return user_chat_settings.setting + + # Get the first chat model if even the user chat settings are not set + return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst() @staticmethod def get_advanced_conversation_config(): server_chat_settings = ServerChatSettings.objects.first() - if server_chat_settings is None or server_chat_settings.chat_advanced is None: - return ConversationAdapters.get_default_conversation_config() - return server_chat_settings.chat_advanced + if server_chat_settings is not None and server_chat_settings.chat_advanced is not None: + return server_chat_settings.chat_advanced + return ConversationAdapters.get_default_conversation_config() @staticmethod async def aget_advanced_conversation_config(): @@ -999,9 +1025,9 @@ class ConversationAdapters: .prefetch_related("chat_advanced", "chat_advanced__openai_config") .afirst() ) - if server_chat_settings is None or server_chat_settings.chat_advanced is None: - return await ConversationAdapters.aget_default_conversation_config() - return server_chat_settings.chat_advanced + if server_chat_settings is not None or server_chat_settings.chat_advanced is not None: + return server_chat_settings.chat_advanced + return await ConversationAdapters.aget_default_conversation_config() @staticmethod def create_conversation_from_public_conversation( diff --git a/src/khoj/processor/image/generate.py b/src/khoj/processor/image/generate.py index ef7105ca..59073731 100644 --- a/src/khoj/processor/image/generate.py +++ b/src/khoj/processor/image/generate.py @@ -25,7 +25,6 @@ async def text_to_image( location_data: LocationData, references: List[Dict[str, Any]], online_results: Dict[str, Any], - subscribed: bool = False, send_status_func: Optional[Callable] = None, uploaded_image_url: Optional[str] = None, agent: Agent = None, @@ -66,8 +65,8 @@ async def text_to_image( note_references=references, online_results=online_results, model_type=text_to_image_config.model_type, - subscribed=subscribed, uploaded_image_url=uploaded_image_url, + user=user, agent=agent, ) diff --git a/src/khoj/processor/tools/online_search.py b/src/khoj/processor/tools/online_search.py index c2e051d6..d040946b 100644 --- a/src/khoj/processor/tools/online_search.py +++ b/src/khoj/processor/tools/online_search.py @@ -104,7 +104,7 @@ async def search_online( async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"): yield {ChatEvent.STATUS: event} tasks = [ - read_webpage_and_extract_content(subquery, link, content, subscribed=subscribed, agent=agent) + read_webpage_and_extract_content(subquery, link, content, user=user, agent=agent) for link, subquery, content in webpages ] results = await asyncio.gather(*tasks) @@ -160,7 +160,7 @@ async def read_webpages( webpage_links_str = "\n- " + "\n- ".join(list(urls)) async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"): yield {ChatEvent.STATUS: event} - tasks = [read_webpage_and_extract_content(query, url, subscribed=subscribed, agent=agent) for url in urls] + tasks = [read_webpage_and_extract_content(query, url, user=user, agent=agent) for url in urls] results = await asyncio.gather(*tasks) response: Dict[str, Dict] = defaultdict(dict) @@ -171,14 +171,14 @@ async def read_webpages( async def read_webpage_and_extract_content( - subquery: str, url: str, content: str = None, subscribed: bool = False, agent: Agent = None + subquery: str, url: str, content: str = None, user: KhojUser = None, agent: Agent = None ) -> Tuple[str, Union[None, str], str]: try: if is_none_or_empty(content): 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_with_jina(url) with timer(f"Extracting relevant information from web page at '{url}' took", logger): - extracted_info = await extract_relevant_info(subquery, content, subscribed=subscribed, agent=agent) + extracted_info = await extract_relevant_info(subquery, content, user=user, agent=agent) return subquery, extracted_info, url except Exception as e: logger.error(f"Failed to read web page at '{url}' with {e}") diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 6a30e194..f8cc15b5 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -394,7 +394,7 @@ async def extract_references_and_questions( # Infer search queries from user message with timer("Extracting search queries took", logger): # If we've reached here, either the user has enabled offline chat or the openai model is enabled. - conversation_config = await ConversationAdapters.aget_default_conversation_config() + conversation_config = await ConversationAdapters.aget_default_conversation_config(user) vision_enabled = conversation_config.vision_enabled if conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE: diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 271a5d52..a1db041b 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -198,7 +198,7 @@ def chat_history( n: Optional[int] = None, ): user = request.user.object - validate_conversation_config() + validate_conversation_config(user) # Load Conversation History conversation = ConversationAdapters.get_conversation_by_user( @@ -309,7 +309,7 @@ def get_shared_chat( update_telemetry_state( request=request, telemetry_type="api", - api="chat_history", + api="get_shared_chat_history", **common.__dict__, ) @@ -742,12 +742,12 @@ async def chat( q, meta_log, is_automated_task, - subscribed=subscribed, + user=user, uploaded_image_url=uploaded_image_url, agent=agent, ) - mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url, agent) + mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, user, uploaded_image_url, agent) async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"): yield result if mode not in conversation_commands: @@ -1001,7 +1001,6 @@ async def chat( location_data=location, references=compiled_references, online_results=online_results, - subscribed=subscribed, send_status_func=partial(send_event, ChatEvent.STATUS), uploaded_image_url=uploaded_image_url, agent=agent, diff --git a/src/khoj/routers/api_model.py b/src/khoj/routers/api_model.py index d5af4ba0..fc6be626 100644 --- a/src/khoj/routers/api_model.py +++ b/src/khoj/routers/api_model.py @@ -40,7 +40,7 @@ def get_user_chat_model( chat_model = ConversationAdapters.get_conversation_config(user) if chat_model is None: - chat_model = ConversationAdapters.get_default_conversation_config() + chat_model = ConversationAdapters.get_default_conversation_config(user) return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.chat_model})) diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py index 56116d25..98702041 100644 --- a/src/khoj/routers/auth.py +++ b/src/khoj/routers/auth.py @@ -80,11 +80,19 @@ async def login_magic_link(request: Request, form: MagicLinkForm): request.session.pop("user", None) email = form.email - user = await aget_or_create_user_by_email(email) + user, is_new = await aget_or_create_user_by_email(email) unique_id = user.email_verification_code if user: await send_magic_link_email(email, unique_id, request.base_url) + if is_new: + update_telemetry_state( + request=request, + telemetry_type="api", + api="create_user", + metadata={"user_id": str(user.uuid)}, + ) + logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}") return Response(status_code=200) diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 4814d390..bd363c2b 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -124,20 +124,20 @@ def is_query_empty(query: str) -> bool: return is_none_or_empty(query.strip()) -def validate_conversation_config(): - default_config = ConversationAdapters.get_default_conversation_config() +def validate_conversation_config(user: KhojUser): + default_config = ConversationAdapters.get_default_conversation_config(user) if default_config is None: - raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.") + raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.") if default_config.model_type == "openai" and not default_config.openai_config: - raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.") + raise HTTPException(status_code=500, detail="Contact the server administrator to add a chat model.") async def is_ready_to_chat(user: KhojUser): - user_conversation_config = (await ConversationAdapters.aget_user_conversation_config(user)) or ( - await ConversationAdapters.aget_default_conversation_config() - ) + user_conversation_config = await ConversationAdapters.aget_user_conversation_config(user) + if user_conversation_config == None: + user_conversation_config = await ConversationAdapters.aget_default_conversation_config() if user_conversation_config and user_conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE: chat_model = user_conversation_config.chat_model @@ -239,19 +239,19 @@ async def agenerate_chat_response(*args): return await loop.run_in_executor(executor, generate_chat_response, *args) -async def acreate_title_from_query(query: str) -> str: +async def acreate_title_from_query(query: str, user: KhojUser = None) -> str: """ Create a title from the given query """ title_generation_prompt = prompts.subject_generation.format(query=query) with timer("Chat actor: Generate title from query", logger): - response = await send_message_to_model_wrapper(title_generation_prompt) + response = await send_message_to_model_wrapper(title_generation_prompt, user=user) return response.strip() -async def acheck_if_safe_prompt(system_prompt: str) -> Tuple[bool, str]: +async def acheck_if_safe_prompt(system_prompt: str, user: KhojUser = None) -> Tuple[bool, str]: """ Check if the system prompt is safe to use """ @@ -260,7 +260,7 @@ async def acheck_if_safe_prompt(system_prompt: str) -> Tuple[bool, str]: reason = "" with timer("Chat actor: Check if safe prompt", logger): - response = await send_message_to_model_wrapper(safe_prompt_check) + response = await send_message_to_model_wrapper(safe_prompt_check, user=user) response = response.strip() try: @@ -281,7 +281,7 @@ async def aget_relevant_information_sources( query: str, conversation_history: dict, is_task: bool, - subscribed: bool, + user: KhojUser, uploaded_image_url: str = None, agent: Agent = None, ): @@ -319,7 +319,7 @@ async def aget_relevant_information_sources( response = await send_message_to_model_wrapper( relevant_tools_prompt, response_type="json_object", - subscribed=subscribed, + user=user, ) try: @@ -355,7 +355,12 @@ async def aget_relevant_information_sources( async def aget_relevant_output_modes( - query: str, conversation_history: dict, is_task: bool = False, uploaded_image_url: str = None, agent: Agent = None + query: str, + conversation_history: dict, + is_task: bool = False, + user: KhojUser = None, + uploaded_image_url: str = None, + agent: Agent = None, ): """ Given a query, determine which of the available tools the agent should use in order to answer appropriately. @@ -391,7 +396,7 @@ async def aget_relevant_output_modes( ) with timer("Chat actor: Infer output mode for chat response", logger): - response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object") + response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object", user=user) try: response = response.strip() @@ -446,7 +451,7 @@ async def infer_webpage_urls( with timer("Chat actor: Infer webpage urls to read", logger): response = await send_message_to_model_wrapper( - online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object" + online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user ) # Validate that the response is a non-empty, JSON-serializable list of URLs @@ -493,7 +498,7 @@ async def generate_online_subqueries( with timer("Chat actor: Generate online search subqueries", logger): response = await send_message_to_model_wrapper( - online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object" + online_queries_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user ) # Validate that the response is a non-empty, JSON-serializable list @@ -511,7 +516,9 @@ async def generate_online_subqueries( return [q] -async def schedule_query(q: str, conversation_history: dict, uploaded_image_url: str = None) -> Tuple[str, ...]: +async def schedule_query( + q: str, conversation_history: dict, user: KhojUser, uploaded_image_url: str = None +) -> Tuple[str, ...]: """ Schedule the date, time to run the query. Assume the server timezone is UTC. """ @@ -523,7 +530,7 @@ async def schedule_query(q: str, conversation_history: dict, uploaded_image_url: ) raw_response = await send_message_to_model_wrapper( - crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object" + crontime_prompt, uploaded_image_url=uploaded_image_url, response_type="json_object", user=user ) # Validate that the response is a non-empty, JSON-serializable list @@ -537,7 +544,7 @@ async def schedule_query(q: str, conversation_history: dict, uploaded_image_url: raise AssertionError(f"Invalid response for scheduling query: {raw_response}") -async def extract_relevant_info(q: str, corpus: str, subscribed: bool, agent: Agent = None) -> Union[str, None]: +async def extract_relevant_info(q: str, corpus: str, user: KhojUser = None, agent: Agent = None) -> Union[str, None]: """ Extract relevant information for a given query from the target corpus """ @@ -555,14 +562,11 @@ async def extract_relevant_info(q: str, corpus: str, subscribed: bool, agent: Ag personality_context=personality_context, ) - chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config() - with timer("Chat actor: Extract relevant information from data", logger): response = await send_message_to_model_wrapper( extract_relevant_information, prompts.system_prompt_extract_relevant_information, - chat_model_option=chat_model, - subscribed=subscribed, + user=user, ) return response.strip() @@ -571,8 +575,8 @@ async def extract_relevant_summary( q: str, corpus: str, conversation_history: dict, - subscribed: bool = False, uploaded_image_url: str = None, + user: KhojUser = None, agent: Agent = None, ) -> Union[str, None]: """ @@ -595,14 +599,11 @@ async def extract_relevant_summary( personality_context=personality_context, ) - chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config() - with timer("Chat actor: Extract relevant information from data", logger): response = await send_message_to_model_wrapper( extract_relevant_information, prompts.system_prompt_extract_relevant_summary, - chat_model_option=chat_model, - subscribed=subscribed, + user=user, uploaded_image_url=uploaded_image_url, ) return response.strip() @@ -667,8 +668,8 @@ async def generate_better_image_prompt( note_references: List[Dict[str, Any]], online_results: Optional[dict] = None, model_type: Optional[str] = None, - subscribed: bool = False, uploaded_image_url: Optional[str] = None, + user: KhojUser = None, agent: Agent = None, ) -> str: """ @@ -718,12 +719,8 @@ async def generate_better_image_prompt( personality_context=personality_context, ) - chat_model: ChatModelOptions = await ConversationAdapters.aget_default_conversation_config() - with timer("Chat actor: Generate contextual image prompt", logger): - response = await send_message_to_model_wrapper( - image_prompt, chat_model_option=chat_model, subscribed=subscribed, uploaded_image_url=uploaded_image_url - ) + response = await send_message_to_model_wrapper(image_prompt, uploaded_image_url=uploaded_image_url, user=user) response = response.strip() if response.startswith(('"', "'")) and response.endswith(('"', "'")): response = response[1:-1] @@ -735,8 +732,9 @@ def send_message_to_model_wrapper_sync( message: str, system_message: str = "", response_type: str = "text", + user: KhojUser = None, ): - conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config() + conversation_config: ChatModelOptions = ConversationAdapters.get_default_conversation_config(user) if conversation_config is None: raise HTTPException(status_code=500, detail="Contact the server administrator to set a default chat model.") @@ -1124,7 +1122,7 @@ class CommonQueryParamsClass: CommonQueryParams = Annotated[CommonQueryParamsClass, Depends()] -def should_notify(original_query: str, executed_query: str, ai_response: str) -> bool: +def should_notify(original_query: str, executed_query: str, ai_response: str, user: KhojUser) -> bool: """ Decide whether to notify the user of the AI response. Default to notifying the user for now. @@ -1141,7 +1139,7 @@ def should_notify(original_query: str, executed_query: str, ai_response: str) -> with timer("Chat actor: Decide to notify user of automation response", logger): try: # TODO Replace with async call so we don't have to maintain a sync version - response = send_message_to_model_wrapper_sync(to_notify_or_not) + response = send_message_to_model_wrapper_sync(to_notify_or_not, user) should_notify_result = "no" not in response.lower() logger.info(f'Decided to {"not " if not should_notify_result else ""}notify user of automation response.') return should_notify_result @@ -1233,7 +1231,9 @@ def scheduled_chat( ai_response = raw_response.text # Notify user if the AI response is satisfactory - if should_notify(original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response): + if should_notify( + original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response, user=user + ): if is_resend_enabled(): send_task_email(user.get_short_name(), user.email, cleaned_query, ai_response, subject, is_image) else: @@ -1243,7 +1243,7 @@ def scheduled_chat( async def create_automation( q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}, conversation_id: str = None ): - crontime, query_to_run, subject = await schedule_query(q, meta_log) + crontime, query_to_run, subject = await schedule_query(q, meta_log, user) job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id) return job, crontime, query_to_run, subject @@ -1429,9 +1429,9 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False) current_notion_config = get_user_notion_config(user) notion_token = current_notion_config.token if current_notion_config else "" - selected_chat_model_config = ( - ConversationAdapters.get_conversation_config(user) or ConversationAdapters.get_default_conversation_config() - ) + selected_chat_model_config = ConversationAdapters.get_conversation_config( + user + ) or ConversationAdapters.get_default_conversation_config(user) chat_models = ConversationAdapters.get_conversation_processor_options().all() chat_model_options = list() for chat_model in chat_models: diff --git a/src/khoj/routers/subscription.py b/src/khoj/routers/subscription.py index 2730b775..abaac675 100644 --- a/src/khoj/routers/subscription.py +++ b/src/khoj/routers/subscription.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, Request from starlette.authentication import requires from khoj.database import adapters +from khoj.routers.helpers import update_telemetry_state from khoj.utils import state # Stripe integration for Khoj Cloud Subscription @@ -48,6 +49,8 @@ async def subscribe(request: Request): customer_id = subscription["customer"] customer = stripe.Customer.retrieve(customer_id) customer_email = customer["email"] + user = None + is_new = False # Handle valid stripe webhook events success = True @@ -55,7 +58,9 @@ async def subscribe(request: Request): # Mark the user as subscribed and update the next renewal date on payment subscription = stripe.Subscription.list(customer=customer_id).data[0] renewal_date = datetime.fromtimestamp(subscription["current_period_end"], tz=timezone.utc) - user = await adapters.set_user_subscription(customer_email, is_recurring=True, renewal_date=renewal_date) + user, is_new = await adapters.set_user_subscription( + customer_email, is_recurring=True, renewal_date=renewal_date + ) success = user is not None elif event_type in {"customer.subscription.updated"}: user_subscription = await sync_to_async(adapters.get_user_subscription)(customer_email) @@ -63,15 +68,24 @@ async def subscribe(request: Request): if user_subscription and user_subscription.renewal_date: # Mark user as unsubscribed or resubscribed is_recurring = not subscription["cancel_at_period_end"] - updated_user = await adapters.set_user_subscription(customer_email, is_recurring=is_recurring) - success = updated_user is not None + user, is_new = await adapters.set_user_subscription(customer_email, is_recurring=is_recurring) + success = user is not None elif event_type in {"customer.subscription.deleted"}: # Reset the user to trial state - user = await adapters.set_user_subscription( + user, is_new = await adapters.set_user_subscription( customer_email, is_recurring=False, renewal_date=False, type="trial" ) success = user is not None + if user and is_new: + update_telemetry_state( + request=request, + telemetry_type="api", + api="create_user", + metadata={"user_id": str(user.user.uuid)}, + ) + logger.log(logging.INFO, f"🥳 New User Created: {user.user.uuid}") + logger.info(f'Stripe subscription {event["type"]} for {customer_email}') return {"success": success} diff --git a/src/khoj/utils/initialization.py b/src/khoj/utils/initialization.py index 90bb9921..6a39c41a 100644 --- a/src/khoj/utils/initialization.py +++ b/src/khoj/utils/initialization.py @@ -129,9 +129,6 @@ def initialization(interactive: bool = True): if user_chat_model_name and ChatModelOptions.objects.filter(chat_model=user_chat_model_name).exists(): default_chat_model_name = user_chat_model_name - # Create a server chat settings object with the default chat model - default_chat_model = ChatModelOptions.objects.filter(chat_model=default_chat_model_name).first() - ServerChatSettings.objects.create(chat_default=default_chat_model) logger.info("🗣️ Chat model configuration complete") # Set up offline speech to text model diff --git a/versions.json b/versions.json index 59c69190..24ed5a84 100644 --- a/versions.json +++ b/versions.json @@ -76,5 +76,6 @@ "1.23.2": "0.15.0", "1.23.3": "0.15.0", "1.24.0": "0.15.0", - "1.24.1": "0.15.0" + "1.24.1": "0.15.0", + "1.25.0": "0.15.0" }