From 4a7e0ade0a8a2083154030c586e8ce3a05e75474 Mon Sep 17 00:00:00 2001
From: Mohsin Ali
Date: Wed, 29 Apr 2026 13:09:51 +0500
Subject: [PATCH 1/2] lazy loading add
---
CHANGELOG.md | 6 +
longtrainer/__init__.py | 2 +-
longtrainer/trainer.py | 157 ++++++++++++-----
longtrainer/vectorstores.py | 8 +-
pyproject.toml | 2 +-
tests/test_10_lazy_loading.py | 323 ++++++++++++++++++++++++++++++++++
6 files changed, 451 insertions(+), 47 deletions(-)
create mode 100644 tests/test_10_lazy_loading.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07b5307..710683d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
+## [1.2.3] β 2026-04-29
+
+### π Performance & Scalability
+- **Lazy Loading Chat Histories:** Significantly optimized startup time and RAM consumption. `load_bot()` no longer eagerly loads all chat histories into RAM. Instead, chat histories are lazy-loaded on-demand from MongoDB via new `_ensure_chat_loaded()` and `_ensure_vision_chat_loaded()` methods.
+- **Qdrant Initialization Fix:** Fixed a bug in `vectorstores.py` where passing `:memory:` to Qdrant would fail due to improper URL parsing. In-memory fallback is now properly handled via the `location` parameter.
+
## [1.2.2] β 2026-04-11
### π Bug Fixes & Perf Improvements
diff --git a/longtrainer/__init__.py b/longtrainer/__init__.py
index 022ffaa..cf60c6b 100644
--- a/longtrainer/__init__.py
+++ b/longtrainer/__init__.py
@@ -14,4 +14,4 @@
"get_builtin_tools",
]
-__version__ = "1.2.2"
+__version__ = "1.2.3"
diff --git a/longtrainer/trainer.py b/longtrainer/trainer.py
index d456793..110a404 100644
--- a/longtrainer/trainer.py
+++ b/longtrainer/trainer.py
@@ -244,9 +244,11 @@ def create_bot(
print(f"[ERROR] Error creating bot: {e}")
def load_bot(self, bot_id: str) -> None:
- """Load an existing bot from MongoDB and FAISS.
+ """Load an existing bot from MongoDB and its vector store.
- Restores bot configuration, FAISS index, and all previous chat histories.
+ Restores bot configuration and vector store index only.
+ Chat histories are lazy-loaded on demand via _ensure_chat_loaded()
+ when a user first sends a message to a specific chat.
Args:
bot_id: The bot's unique identifier.
@@ -316,48 +318,12 @@ def load_bot(self, bot_id: str) -> None:
bot["retriever"] = bot["ensemble_retriever"]
- load_chat_history = self._storage.list_chats(bot_id)
-
- print("[INFO] Loading previous chats...")
- for chat_id in load_chat_history["chat_ids"]:
- data = self._storage.get_chat_by_id(chat_id, "oldest")
- if not data:
- continue
-
- rag_bot = RAGBot(
- retriever=bot["ensemble_retriever"],
- llm=self.llm,
- prompt=bot["prompt"],
- token_limit=self.max_token_limit,
- )
- for item in data:
- rag_bot.save_context(
- str(item["question"]),
- str(item["answer"]),
- )
- bot["chains"][chat_id] = rag_bot
-
- for vision_chat_id in load_chat_history["vision_chat_ids"]:
- data = self._storage.get_vision_chat_by_id(vision_chat_id, "oldest")
- if not data:
- continue
-
- vision_mem = VisionMemory(
- token_limit=self.max_token_limit,
- llm=self.llm,
- ensemble_retriever=bot["ensemble_retriever"],
- prompt_template=bot["prompt_template"],
- )
- for item in data:
- vision_mem.save_context(
- str(item["question"]),
- str(item["response"]),
- )
- bot["assistants"][vision_chat_id] = vision_mem
-
- print("[INFO] Previous chats loaded successfully.")
+ # Chat histories are lazy-loaded on demand β no eager loading.
+ # When a user sends a message, _ensure_chat_loaded() or
+ # _ensure_vision_chat_loaded() pulls just that one chat from
+ # MongoDB and replays its history into a fresh RAGBot/VisionMemory.
gc.collect()
- print(f"[INFO] Bot {bot_id} loaded successfully.")
+ print(f"[INFO] Bot {bot_id} loaded successfully (chats will be lazy-loaded on demand).")
except Exception as e:
print(f"[ERROR] Error loading bot: {e}")
@@ -547,6 +513,105 @@ def _get_bot_tools(self, bot_id: str) -> list[BaseTool]:
tools.extend(self.bot_data[bot_id]["tools"].get_tools())
return tools
+ # βββ Lazy Chat Loading βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ def _ensure_chat_loaded(self, bot_id: str, chat_id: str) -> None:
+ """Lazy-load a single chat session from MongoDB on demand.
+
+ If the chat_id already exists in ``bot["chains"]``, this is a no-op.
+ Otherwise, pulls the chat history from MongoDB, creates a fresh
+ RAGBot (or AgentBot in agent mode), replays the stored messages,
+ and caches it in ``bot["chains"]``.
+
+ If the chat_id has no history in MongoDB (e.g. a brand-new chat
+ that was never persisted), a fresh bot instance is created with
+ an empty history.
+
+ Args:
+ bot_id: The bot's unique identifier.
+ chat_id: The chat session's unique identifier.
+ """
+ if bot_id not in self.bot_data:
+ raise ValueError(f"Bot ID {bot_id} not found.")
+
+ bot = self.bot_data[bot_id]
+
+ # Already in memory β nothing to do
+ if chat_id in bot["chains"]:
+ return
+
+ # Create the appropriate bot instance
+ if bot.get("agent_mode"):
+ tools = self._global_tools.get_tools()
+ tools.extend(bot["tools"].get_tools())
+ bot_instance = AgentBot(
+ llm=self.llm,
+ tools=tools,
+ system_prompt=bot.get("prompt_template", self.prompt_template),
+ token_limit=self.max_token_limit,
+ )
+ else:
+ bot_instance = RAGBot(
+ retriever=bot["ensemble_retriever"],
+ llm=self.llm,
+ prompt=bot["prompt"],
+ token_limit=self.max_token_limit,
+ )
+
+ # Replay history from MongoDB (if any exists)
+ data = self._storage.get_chat_by_id(chat_id, "oldest")
+ if data:
+ for item in data:
+ bot_instance.save_context(
+ str(item["question"]),
+ str(item["answer"]),
+ )
+ print(f"[INFO] Lazy-loaded chat {chat_id} ({len(data)} messages).")
+ else:
+ print(f"[INFO] No history found for chat {chat_id}, starting fresh.")
+
+ bot["chains"][chat_id] = bot_instance
+
+ def _ensure_vision_chat_loaded(self, bot_id: str, vision_chat_id: str) -> None:
+ """Lazy-load a single vision chat session from MongoDB on demand.
+
+ Same semantics as ``_ensure_chat_loaded`` but for vision chats
+ stored in ``bot["assistants"]``.
+
+ Args:
+ bot_id: The bot's unique identifier.
+ vision_chat_id: The vision chat session's unique identifier.
+ """
+ if bot_id not in self.bot_data:
+ raise ValueError(f"Bot ID {bot_id} not found.")
+
+ bot = self.bot_data[bot_id]
+
+ # Already in memory β nothing to do
+ if vision_chat_id in bot["assistants"]:
+ return
+
+ vision_mem = VisionMemory(
+ token_limit=self.max_token_limit,
+ llm=self.llm,
+ ensemble_retriever=bot["ensemble_retriever"],
+ prompt_template=bot.get("prompt_template", self.prompt_template),
+ )
+
+ # Replay history from MongoDB (if any exists)
+ data = self._storage.get_vision_chat_by_id(vision_chat_id, "oldest")
+ if data:
+ for item in data:
+ vision_mem.save_context(
+ str(item["question"]),
+ str(item["response"]),
+ )
+ print(f"[INFO] Lazy-loaded vision chat {vision_chat_id} ({len(data)} messages).")
+ else:
+ print(f"[INFO] No history found for vision chat {vision_chat_id}, starting fresh.")
+
+ bot["assistants"][vision_chat_id] = vision_mem
+
# βββ Chat Sessions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def new_chat(self, bot_id: str) -> str:
@@ -580,6 +645,8 @@ def get_response(
"""Get a response from the chatbot."""
if bot_id not in self.bot_data:
raise ValueError(f"Bot ID {bot_id} not found.")
+ # Lazy-load chat history from MongoDB if not already in memory
+ self._ensure_chat_loaded(bot_id, chat_id)
return self._chat_manager.get_response(
query, bot_id, chat_id, self.bot_data[bot_id],
stream, uploaded_files, web_search, schema=schema,
@@ -596,6 +663,8 @@ async def aget_response(
"""Async streaming response."""
if bot_id not in self.bot_data:
raise ValueError(f"Bot ID {bot_id} not found.")
+ # Lazy-load chat history from MongoDB if not already in memory
+ self._ensure_chat_loaded(bot_id, chat_id)
async for chunk in self._chat_manager.aget_response(
query, bot_id, chat_id, self.bot_data[bot_id],
uploaded_files, web_search,
@@ -614,6 +683,8 @@ def get_vision_response(
"""Get a response from the vision AI assistant."""
if bot_id not in self.bot_data:
raise ValueError(f"Bot ID {bot_id} not found.")
+ # Lazy-load vision chat history from MongoDB if not already in memory
+ self._ensure_vision_chat_loaded(bot_id, vision_chat_id)
return self._chat_manager.get_vision_response(
query, image_paths, bot_id, vision_chat_id,
self.bot_data[bot_id], uploaded_files, web_search,
diff --git a/longtrainer/vectorstores.py b/longtrainer/vectorstores.py
index c903af2..9951567 100644
--- a/longtrainer/vectorstores.py
+++ b/longtrainer/vectorstores.py
@@ -92,10 +92,14 @@ def get_vectorstore(
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
- url = kwargs.get("url", ":memory:")
+ url = kwargs.get("url")
+ location = kwargs.get("location")
api_key = kwargs.get("api_key", None)
- client = QdrantClient(url=url, api_key=api_key)
+ if url:
+ client = QdrantClient(url=url, api_key=api_key)
+ else:
+ client = QdrantClient(location=location or ":memory:", api_key=api_key)
if not client.collection_exists(collection_name):
try:
diff --git a/pyproject.toml b/pyproject.toml
index 52b8cc4..dc95fe0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "longtrainer"
-version = "1.2.2"
+version = "1.2.3"
description = "Production-ready RAG framework built on LangChain β multi-tenant chatbots with streaming, tool calling, agent mode, FAISS vector search, and persistent MongoDB memory"
readme = "README.md"
license = "MIT"
diff --git a/tests/test_10_lazy_loading.py b/tests/test_10_lazy_loading.py
new file mode 100644
index 0000000..07522fb
--- /dev/null
+++ b/tests/test_10_lazy_loading.py
@@ -0,0 +1,323 @@
+"""Test 10: Lazy Loading β Real Integration Test
+
+Validates that:
+1. load_bot() no longer eagerly loads chat histories into RAM
+2. _ensure_chat_loaded() correctly lazy-loads a single chat from MongoDB
+3. _ensure_vision_chat_loaded() correctly lazy-loads a single vision chat
+4. get_response() auto-lazy-loads before processing
+5. get_vision_response() auto-lazy-loads before processing
+6. new_chat() / new_vision_chat() still work correctly
+7. Subsequent calls (cache hit) skip MongoDB entirely
+8. All other functionalities (list_chats, delete, etc.) are unbroken
+
+Requires a running MongoDB instance and real dependencies (no mocks).
+"""
+
+import sys
+import time
+import gc
+
+# Allow running standalone
+if __name__ == "__main__":
+ sys.path.insert(0, ".")
+
+
+def test_lazy_loading():
+ """Real integration test for lazy chat loading."""
+ print("=" * 70)
+ print("TEST 10: Lazy Loading β Real Integration Test")
+ print("=" * 70)
+
+ results = []
+
+ # βββ Setup: Create a real LongTrainer with local MongoDB βββββββββββββββββ
+ try:
+ import os
+ os.environ["OPENAI_API_KEY"] = "sk-mock-key-for-testing"
+ from langchain_core.embeddings import FakeEmbeddings
+ from longtrainer.trainer import LongTrainer
+ from longtrainer.bot import RAGBot, AgentBot
+ from longtrainer.vision_bot import VisionMemory
+
+ trainer = LongTrainer(
+ mongo_endpoint="mongodb://localhost:27017/",
+ vector_store_provider="qdrant",
+ vector_store_kwargs={"location": ":memory:"},
+ embedding_model=FakeEmbeddings(size=1536),
+ )
+ results.append(("LongTrainer init", True, "Connected to MongoDB"))
+ except Exception as e:
+ results.append(("LongTrainer init", False, str(e)))
+ _print_results(results)
+ return False
+
+ bot_id = None
+ chat_id = None
+ vision_chat_id = None
+
+ # βββ Test 1: Initialize + Create Bot βββββββββββββββββββββββββββββββββββββ
+ try:
+ bot_id = trainer.initialize_bot_id()
+ assert bot_id, "Bot ID should not be empty"
+ assert bot_id in trainer.bot_data, "Bot should be in bot_data"
+ results.append(("initialize_bot_id", True, f"Created {bot_id}"))
+ except Exception as e:
+ results.append(("initialize_bot_id", False, str(e)))
+ _print_results(results)
+ return False
+
+ # Add a minimal document so create_bot doesn't fail on empty vectorstore
+ try:
+ from langchain_core.documents import Document
+ trainer.pass_documents([Document(page_content="Test document for lazy loading verification.")], bot_id)
+ trainer.create_bot(bot_id=bot_id)
+ results.append(("create_bot", True, "Bot created with test document"))
+ except Exception as e:
+ results.append(("create_bot", False, str(e)))
+ _print_results(results)
+ return False
+
+ # βββ Test 2: new_chat() works β creates an in-memory bot instance ββββββββ
+ try:
+ chat_id = trainer.new_chat(bot_id)
+ assert chat_id, "Chat ID should not be empty"
+ assert chat_id in trainer.bot_data[bot_id]["chains"], "New chat should be in chains"
+ results.append(("new_chat", True, f"Created {chat_id}"))
+ except Exception as e:
+ results.append(("new_chat", False, str(e)))
+
+ # βββ Test 3: new_vision_chat() works βββββββββββββββββββββββββββββββββββββ
+ try:
+ vision_chat_id = trainer.new_vision_chat(bot_id)
+ assert vision_chat_id, "Vision chat ID should not be empty"
+ assert vision_chat_id in trainer.bot_data[bot_id]["assistants"], "Vision chat should be in assistants"
+ results.append(("new_vision_chat", True, f"Created {vision_chat_id}"))
+ except Exception as e:
+ results.append(("new_vision_chat", False, str(e)))
+
+ # βββ Test 4: Manually store some chat history in MongoDB βββββββββββββββββ
+ # Simulate what happens when a real user sends messages
+ test_chat_id = "chat-lazy-test-001"
+ test_vision_chat_id = "vision-lazy-test-001"
+ try:
+ # Store 3 chat messages directly into MongoDB
+ for i in range(3):
+ trainer._storage.store_chat(
+ bot_id=bot_id,
+ chat_id=test_chat_id,
+ query=f"Test question {i+1}",
+ answer=f"Test answer {i+1}",
+ )
+
+ # Store 2 vision chat messages
+ for i in range(2):
+ trainer._storage.store_vision_chat(
+ bot_id=bot_id,
+ vision_chat_id=test_vision_chat_id,
+ image_paths=["test.jpg"],
+ query=f"Vision question {i+1}",
+ response=f"Vision response {i+1}",
+ )
+
+ # Verify MongoDB has the records
+ chat_data = trainer._storage.get_chat_by_id(test_chat_id, "oldest")
+ vision_data = trainer._storage.get_vision_chat_by_id(test_vision_chat_id, "oldest")
+ assert chat_data and len(chat_data) == 3, f"Expected 3 chat records, got {len(chat_data) if chat_data else 0}"
+ assert vision_data and len(vision_data) == 2, f"Expected 2 vision records, got {len(vision_data) if vision_data else 0}"
+
+ results.append(("Store test history in MongoDB", True, f"3 chats + 2 vision chats stored"))
+ except Exception as e:
+ results.append(("Store test history in MongoDB", False, str(e)))
+
+ # βββ Test 5: load_bot() does NOT eagerly load chat history βββββββββββββββ
+ try:
+ # Clear in-memory data to simulate server restart
+ del trainer.bot_data[bot_id]
+ gc.collect()
+
+ # Measure load time
+ start = time.perf_counter()
+ trainer.load_bot(bot_id)
+ elapsed = time.perf_counter() - start
+
+ assert bot_id in trainer.bot_data, "Bot should be in bot_data after load"
+ assert trainer.bot_data[bot_id]["chains"] == {}, f"chains should be empty after load, got {len(trainer.bot_data[bot_id]['chains'])} entries"
+ assert trainer.bot_data[bot_id]["assistants"] == {}, f"assistants should be empty after load, got {len(trainer.bot_data[bot_id]['assistants'])} entries"
+ assert trainer.bot_data[bot_id]["vectorstore"] is not None, "Vectorstore should be loaded"
+ assert trainer.bot_data[bot_id]["ensemble_retriever"] is not None, "Retriever should be loaded"
+
+ results.append(("load_bot β no eager loading", True, f"Loaded in {elapsed*1000:.1f}ms, 0 chats in RAM"))
+ except Exception as e:
+ results.append(("load_bot β no eager loading", False, str(e)))
+
+ # βββ Test 6: _ensure_chat_loaded() β lazy loads from MongoDB βββββββββββββ
+ try:
+ # Chat should NOT be in memory
+ assert test_chat_id not in trainer.bot_data[bot_id]["chains"], "Chat should not be in memory before lazy load"
+
+ # Lazy load it
+ start = time.perf_counter()
+ trainer._ensure_chat_loaded(bot_id, test_chat_id)
+ elapsed = time.perf_counter() - start
+
+ # Now it should be in memory
+ assert test_chat_id in trainer.bot_data[bot_id]["chains"], "Chat should be in memory after lazy load"
+
+ # Verify the bot instance has the correct history
+ bot_instance = trainer.bot_data[bot_id]["chains"][test_chat_id]
+ assert isinstance(bot_instance, RAGBot), f"Expected RAGBot, got {type(bot_instance)}"
+ assert len(bot_instance.chat_history.messages) == 6, f"Expected 6 messages (3 Q+A pairs), got {len(bot_instance.chat_history.messages)}"
+
+ # Verify message content
+ messages = bot_instance.chat_history.messages
+ assert "Test question 1" in messages[0].content, f"First message should contain 'Test question 1', got '{messages[0].content}'"
+ assert "Test answer 1" in messages[1].content, f"Second message should contain 'Test answer 1', got '{messages[1].content}'"
+
+ results.append(("_ensure_chat_loaded β MongoDB replay", True, f"6 messages loaded in {elapsed*1000:.1f}ms"))
+ except Exception as e:
+ results.append(("_ensure_chat_loaded β MongoDB replay", False, str(e)))
+
+ # βββ Test 7: _ensure_chat_loaded() β cache hit (no-op) βββββββββββββββββββ
+ try:
+ # Store reference to current bot instance
+ cached_instance = trainer.bot_data[bot_id]["chains"].get(test_chat_id)
+
+ # Call again β should be a no-op
+ start = time.perf_counter()
+ trainer._ensure_chat_loaded(bot_id, test_chat_id)
+ elapsed = time.perf_counter() - start
+
+ # Same object should still be there (identity check)
+ assert trainer.bot_data[bot_id]["chains"][test_chat_id] is cached_instance, "Cache hit should return same object"
+
+ results.append(("_ensure_chat_loaded β cache hit", True, f"No-op in {elapsed*1000:.3f}ms"))
+ except Exception as e:
+ results.append(("_ensure_chat_loaded β cache hit", False, str(e)))
+
+ # βββ Test 8: _ensure_vision_chat_loaded() β lazy loads from MongoDB ββββββ
+ try:
+ assert test_vision_chat_id not in trainer.bot_data[bot_id]["assistants"], "Vision chat should not be in memory"
+
+ start = time.perf_counter()
+ trainer._ensure_vision_chat_loaded(bot_id, test_vision_chat_id)
+ elapsed = time.perf_counter() - start
+
+ assert test_vision_chat_id in trainer.bot_data[bot_id]["assistants"], "Vision chat should be in memory"
+
+ vision_mem = trainer.bot_data[bot_id]["assistants"][test_vision_chat_id]
+ assert isinstance(vision_mem, VisionMemory), f"Expected VisionMemory, got {type(vision_mem)}"
+ assert len(vision_mem.chat_history) == 2, f"Expected 2 history entries, got {len(vision_mem.chat_history)}"
+ assert len(vision_mem.chat_history_store.messages) == 4, f"Expected 4 messages (2 Q+A pairs), got {len(vision_mem.chat_history_store.messages)}"
+
+ results.append(("_ensure_vision_chat_loaded β MongoDB replay", True, f"4 messages loaded in {elapsed*1000:.1f}ms"))
+ except Exception as e:
+ results.append(("_ensure_vision_chat_loaded β MongoDB replay", False, str(e)))
+
+ # βββ Test 9: _ensure_chat_loaded β unknown chat (no history) βββββββββββββ
+ try:
+ fresh_chat_id = "chat-never-existed-xyz"
+ trainer._ensure_chat_loaded(bot_id, fresh_chat_id)
+
+ assert fresh_chat_id in trainer.bot_data[bot_id]["chains"], "Fresh chat should be created"
+ bot_inst = trainer.bot_data[bot_id]["chains"][fresh_chat_id]
+ assert isinstance(bot_inst, RAGBot), f"Expected RAGBot, got {type(bot_inst)}"
+ assert len(bot_inst.chat_history.messages) == 0, "Fresh chat should have 0 messages"
+
+ results.append(("_ensure_chat_loaded β no history (fresh)", True, "Created empty RAGBot"))
+ except Exception as e:
+ results.append(("_ensure_chat_loaded β no history (fresh)", False, str(e)))
+
+ # βββ Test 10: list_chats() still works (queries MongoDB directly) ββββββββ
+ try:
+ chats = trainer.list_chats(bot_id)
+ assert "chat_ids" in chats, "list_chats should return chat_ids"
+ assert "vision_chat_ids" in chats, "list_chats should return vision_chat_ids"
+ assert test_chat_id in chats["chat_ids"], f"test_chat_id should be in chat_ids"
+ assert test_vision_chat_id in chats["vision_chat_ids"], f"test_vision_chat_id should be in vision_chat_ids"
+
+ results.append(("list_chats β MongoDB query", True, f"{len(chats['chat_ids'])} chats, {len(chats['vision_chat_ids'])} vision"))
+ except Exception as e:
+ results.append(("list_chats β MongoDB query", False, str(e)))
+
+ # βββ Test 11: get_chat_by_id() still works βββββββββββββββββββββββββββββββ
+ try:
+ history = trainer.get_chat_by_id(test_chat_id, "oldest")
+ assert history is not None, "Chat history should not be None"
+ assert len(history) == 3, f"Expected 3 messages, got {len(history)}"
+ results.append(("get_chat_by_id", True, f"Retrieved {len(history)} messages"))
+ except Exception as e:
+ results.append(("get_chat_by_id", False, str(e)))
+
+ # βββ Test 12: Verify the load_bot β get_response flow works end-to-end βββ
+ # (This simulates what the production Rag_on_Longtrainer/api.py does)
+ try:
+ # Simulate server restart: clear all in-memory data
+ del trainer.bot_data[bot_id]
+ gc.collect()
+
+ # Reload bot (should be instant, no chat loading)
+ start = time.perf_counter()
+ trainer.load_bot(bot_id)
+ load_time = time.perf_counter() - start
+
+ # Verify chains are empty
+ assert trainer.bot_data[bot_id]["chains"] == {}, "Chains should be empty after fresh load"
+
+ # Now create a new chat and verify it works
+ new_cid = trainer.new_chat(bot_id)
+ assert new_cid in trainer.bot_data[bot_id]["chains"], "New chat should be in chains"
+
+ results.append(("Full reloadβnew_chat flow", True, f"Boot: {load_time*1000:.1f}ms, new chat: {new_cid[:20]}..."))
+ except Exception as e:
+ results.append(("Full reloadβnew_chat flow", False, str(e)))
+
+ # βββ Test 13: set_custom_prompt_template works after lazy load βββββββββββ
+ try:
+ custom_prompt = "You are a medical assistant. {context}\n{chat_history}\n{question}"
+ trainer.set_custom_prompt_template(bot_id, custom_prompt)
+ assert trainer.bot_data[bot_id]["prompt_template"] == custom_prompt, "Prompt should be updated"
+ results.append(("set_custom_prompt_template", True, "Prompt updated successfully"))
+ except Exception as e:
+ results.append(("set_custom_prompt_template", False, str(e)))
+
+ # βββ Cleanup: Delete test bot and all data βββββββββββββββββββββββββββββββ
+ try:
+ trainer.delete_chatbot(bot_id)
+ assert bot_id not in trainer.bot_data, "Bot should be removed from bot_data"
+
+ # Verify MongoDB is clean
+ assert trainer._storage.find_bot(bot_id) is None, "Bot should be deleted from MongoDB"
+ chats_after = trainer._storage.list_chats(bot_id)
+ assert len(chats_after["chat_ids"]) == 0, "All chats should be deleted"
+ assert len(chats_after["vision_chat_ids"]) == 0, "All vision chats should be deleted"
+
+ results.append(("Cleanup β delete_chatbot", True, "All data cleaned"))
+ except Exception as e:
+ results.append(("Cleanup β delete_chatbot", False, str(e)))
+
+ _print_results(results)
+ return all(ok for _, ok, _ in results)
+
+
+def _print_results(results):
+ """Print formatted test results."""
+ print()
+ passed = 0
+ failed = 0
+ for name, ok, detail in results:
+ status = "β
PASS" if ok else "β FAIL"
+ detail_str = f" β {detail}" if detail else ""
+ print(f" {status}: {name}{detail_str}")
+ if ok:
+ passed += 1
+ else:
+ failed += 1
+
+ print()
+ print(f"Results: {passed} passed, {failed} failed out of {passed + failed} tests")
+ print("=" * 70)
+
+
+if __name__ == "__main__":
+ success = test_lazy_loading()
+ sys.exit(0 if success else 1)
From bc57eea6bb0e10b8bb8adc4b070308a6f38561a1 Mon Sep 17 00:00:00 2001
From: Mohsin Ali
Date: Wed, 29 Apr 2026 13:21:19 +0500
Subject: [PATCH 2/2] version update doc
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 488d06a..6f2c45b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-LongTrainer 1.2.2 β Production-Ready RAG Framework
+LongTrainer 1.2.3 β Production-Ready RAG Framework
Multi-tenant bots, streaming, tools, and persistent memory β all batteries included.
@@ -99,7 +99,7 @@ brew install libmagic poppler tesseract qpdf libreoffice pandoc
## Quick Start π
-### 1. Zero-Code CLI & API Server (New in 1.2.2!)
+### 1. Zero-Code CLI & API Server (New in 1.2.3!)
Manage bots, chat, and run a production API directly from your terminalβno Python required.