lollms-client 0.28.0__py3-none-any.whl → 0.29.1__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.
Potentially problematic release.
This version of lollms-client might be problematic. Click here for more details.
- examples/text_gen.py +1 -1
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/llamacpp/__init__.py +1 -0
- lollms_client/llm_bindings/lollms/__init__.py +411 -267
- lollms_client/llm_bindings/lollms_webui/__init__.py +428 -0
- lollms_client/lollms_core.py +157 -130
- lollms_client/lollms_discussion.py +343 -61
- lollms_client/lollms_personality.py +8 -0
- lollms_client/lollms_utilities.py +10 -2
- lollms_client-0.29.1.dist-info/METADATA +963 -0
- {lollms_client-0.28.0.dist-info → lollms_client-0.29.1.dist-info}/RECORD +14 -14
- lollms_client/llm_bindings/lollms_chat/__init__.py +0 -571
- lollms_client-0.28.0.dist-info/METADATA +0 -604
- {lollms_client-0.28.0.dist-info → lollms_client-0.29.1.dist-info}/WHEEL +0 -0
- {lollms_client-0.28.0.dist-info → lollms_client-0.29.1.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-0.28.0.dist-info → lollms_client-0.29.1.dist-info}/top_level.txt +0 -0
|
@@ -31,6 +31,7 @@ if False:
|
|
|
31
31
|
|
|
32
32
|
from lollms_client.lollms_utilities import build_image_dicts, robust_json_parser
|
|
33
33
|
from ascii_colors import ASCIIColors, trace_exception
|
|
34
|
+
from .lollms_types import MSG_TYPE
|
|
34
35
|
|
|
35
36
|
class EncryptedString(TypeDecorator):
|
|
36
37
|
"""A SQLAlchemy TypeDecorator for field-level database encryption.
|
|
@@ -127,6 +128,11 @@ def create_dynamic_models(
|
|
|
127
128
|
__abstract__ = True
|
|
128
129
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
129
130
|
system_prompt = Column(EncryptedText, nullable=True)
|
|
131
|
+
user_data_zone = Column(EncryptedText, nullable=True) # Field for persistent user-specific data
|
|
132
|
+
discussion_data_zone = Column(EncryptedText, nullable=True) # Field for persistent discussion-specific data
|
|
133
|
+
personality_data_zone = Column(EncryptedText, nullable=True) # Field for persistent personality-specific data
|
|
134
|
+
memory = Column(EncryptedText, nullable=True) # New field for long-term memory across discussions
|
|
135
|
+
|
|
130
136
|
participants = Column(JSON, nullable=True, default=dict)
|
|
131
137
|
active_branch_id = Column(String, nullable=True)
|
|
132
138
|
discussion_metadata = Column(JSON, nullable=True, default=dict)
|
|
@@ -211,8 +217,6 @@ class LollmsDataManager:
|
|
|
211
217
|
with self.engine.connect() as connection:
|
|
212
218
|
print("Checking for database schema upgrades...")
|
|
213
219
|
|
|
214
|
-
# --- THIS IS THE FIX ---
|
|
215
|
-
# We must wrap raw SQL strings in the `text()` function for direct execution.
|
|
216
220
|
cursor = connection.execute(text("PRAGMA table_info(discussions)"))
|
|
217
221
|
columns = [row[1] for row in cursor.fetchall()]
|
|
218
222
|
|
|
@@ -223,9 +227,28 @@ class LollmsDataManager:
|
|
|
223
227
|
if 'pruning_point_id' not in columns:
|
|
224
228
|
print(" -> Upgrading 'discussions' table: Adding 'pruning_point_id' column.")
|
|
225
229
|
connection.execute(text("ALTER TABLE discussions ADD COLUMN pruning_point_id VARCHAR"))
|
|
230
|
+
|
|
231
|
+
if 'data_zone' in columns:
|
|
232
|
+
print(" -> Upgrading 'discussions' table: Removing 'data_zone' column.")
|
|
233
|
+
connection.execute(text("ALTER TABLE discussions DROP COLUMN data_zone"))
|
|
234
|
+
|
|
235
|
+
if 'user_data_zone' not in columns:
|
|
236
|
+
print(" -> Upgrading 'discussions' table: Adding 'user_data_zone' column.")
|
|
237
|
+
connection.execute(text("ALTER TABLE discussions ADD COLUMN user_data_zone TEXT"))
|
|
238
|
+
|
|
239
|
+
if 'discussion_data_zone' not in columns:
|
|
240
|
+
print(" -> Upgrading 'discussions' table: Adding 'discussion_data_zone' column.")
|
|
241
|
+
connection.execute(text("ALTER TABLE discussions ADD COLUMN discussion_data_zone TEXT"))
|
|
242
|
+
|
|
243
|
+
if 'personality_data_zone' not in columns:
|
|
244
|
+
print(" -> Upgrading 'discussions' table: Adding 'personality_data_zone' column.")
|
|
245
|
+
connection.execute(text("ALTER TABLE discussions ADD COLUMN personality_data_zone TEXT"))
|
|
246
|
+
|
|
247
|
+
if 'memory' not in columns:
|
|
248
|
+
print(" -> Upgrading 'discussions' table: Adding 'memory' column.")
|
|
249
|
+
connection.execute(text("ALTER TABLE discussions ADD COLUMN memory TEXT"))
|
|
226
250
|
|
|
227
251
|
print("Database schema is up to date.")
|
|
228
|
-
# This is important to apply the ALTER TABLE statements
|
|
229
252
|
connection.commit()
|
|
230
253
|
|
|
231
254
|
except Exception as e:
|
|
@@ -382,7 +405,6 @@ class LollmsDiscussion:
|
|
|
382
405
|
object.__setattr__(self, '_is_db_backed', db_manager is not None)
|
|
383
406
|
|
|
384
407
|
object.__setattr__(self, '_system_prompt', None)
|
|
385
|
-
|
|
386
408
|
if self._is_db_backed:
|
|
387
409
|
if not db_discussion_obj and not discussion_id:
|
|
388
410
|
raise ValueError("Either discussion_id or db_discussion_obj must be provided for DB-backed discussions.")
|
|
@@ -484,6 +506,10 @@ class LollmsDiscussion:
|
|
|
484
506
|
proxy = SimpleNamespace()
|
|
485
507
|
proxy.id = id or str(uuid.uuid4())
|
|
486
508
|
proxy.system_prompt = None
|
|
509
|
+
proxy.user_data_zone = None
|
|
510
|
+
proxy.discussion_data_zone = None
|
|
511
|
+
proxy.personality_data_zone = None
|
|
512
|
+
proxy.memory = None
|
|
487
513
|
proxy.participants = {}
|
|
488
514
|
proxy.active_branch_id = None
|
|
489
515
|
proxy.discussion_metadata = {}
|
|
@@ -594,7 +620,23 @@ class LollmsDiscussion:
|
|
|
594
620
|
|
|
595
621
|
return [LollmsMessage(self, orm) for orm in reversed(branch_orms)]
|
|
596
622
|
|
|
597
|
-
|
|
623
|
+
def get_full_data_zone(self):
|
|
624
|
+
"""Assembles all data zones into a single, formatted string for the prompt."""
|
|
625
|
+
parts = []
|
|
626
|
+
# If memory is not empty, add it to the list of zones.
|
|
627
|
+
if self.memory and self.memory.strip():
|
|
628
|
+
parts.append(f"-- Memory --\n{self.memory.strip()}")
|
|
629
|
+
if self.user_data_zone and self.user_data_zone.strip():
|
|
630
|
+
parts.append(f"-- User Data Zone --\n{self.user_data_zone.strip()}")
|
|
631
|
+
if self.discussion_data_zone and self.discussion_data_zone.strip():
|
|
632
|
+
parts.append(f"-- Discussion Data Zone --\n{self.discussion_data_zone.strip()}")
|
|
633
|
+
if self.personality_data_zone and self.personality_data_zone.strip():
|
|
634
|
+
parts.append(f"-- Personality Data Zone --\n{self.personality_data_zone.strip()}")
|
|
635
|
+
|
|
636
|
+
# Join the zones with double newlines for clear separation in the prompt.
|
|
637
|
+
return "\n\n".join(parts)
|
|
638
|
+
|
|
639
|
+
|
|
598
640
|
def chat(
|
|
599
641
|
self,
|
|
600
642
|
user_message: str,
|
|
@@ -642,21 +684,92 @@ class LollmsDiscussion:
|
|
|
642
684
|
A dictionary with 'user_message' and 'ai_message' LollmsMessage objects,
|
|
643
685
|
where the 'ai_message' will contain rich metadata if an agentic turn was used.
|
|
644
686
|
"""
|
|
687
|
+
callback = kwargs.get("streaming_callback")
|
|
688
|
+
# extract personality data
|
|
645
689
|
if personality is not None:
|
|
646
690
|
object.__setattr__(self, '_system_prompt', personality.system_prompt)
|
|
691
|
+
|
|
692
|
+
# --- New Data Source Handling Logic ---
|
|
693
|
+
if hasattr(personality, 'data_source') and personality.data_source is not None:
|
|
694
|
+
if isinstance(personality.data_source, str):
|
|
695
|
+
# --- Static Data Source ---
|
|
696
|
+
if callback:
|
|
697
|
+
callback("Loading static personality data...", MSG_TYPE.MSG_TYPE_STEP, {"id": "static_data_loading"})
|
|
698
|
+
if personality.data_source:
|
|
699
|
+
self.personality_data_zone = personality.data_source.strip()
|
|
700
|
+
|
|
701
|
+
elif callable(personality.data_source):
|
|
702
|
+
# --- Dynamic Data Source ---
|
|
703
|
+
qg_id = None
|
|
704
|
+
if callback:
|
|
705
|
+
qg_id = callback("Generating query for dynamic personality data...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "dynamic_data_query_gen"})
|
|
706
|
+
|
|
707
|
+
context_for_query = self.export('markdown')
|
|
708
|
+
query_prompt = (
|
|
709
|
+
"You are an expert query generator. Based on the current conversation, formulate a concise and specific query to retrieve relevant information from a knowledge base. "
|
|
710
|
+
"The query will be used to fetch data that will help you answer the user's latest request.\n\n"
|
|
711
|
+
f"--- Conversation History ---\n{context_for_query}\n\n"
|
|
712
|
+
"--- Instructions ---\n"
|
|
713
|
+
"Generate a single query string."
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
try:
|
|
717
|
+
query_json = self.lollmsClient.generate_structured_content(
|
|
718
|
+
prompt=query_prompt,
|
|
719
|
+
output_format={"query": "Your generated search query here."},
|
|
720
|
+
system_prompt="You are an AI assistant that generates search queries in JSON format.",
|
|
721
|
+
temperature=0.0
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
if not query_json or "query" not in query_json:
|
|
725
|
+
if callback:
|
|
726
|
+
callback("Failed to generate data query.", MSG_TYPE.MSG_TYPE_EXCEPTION, {"id": qg_id})
|
|
727
|
+
else:
|
|
728
|
+
generated_query = query_json["query"]
|
|
729
|
+
if callback:
|
|
730
|
+
callback(f"Generated query: '{generated_query}'", MSG_TYPE.MSG_TYPE_STEP_END, {"id": qg_id, "query": generated_query})
|
|
731
|
+
|
|
732
|
+
dr_id = None
|
|
733
|
+
if callback:
|
|
734
|
+
dr_id = callback("Retrieving dynamic data from personality source...", MSG_TYPE.MSG_TYPE_STEP_START, {"id": "dynamic_data_retrieval"})
|
|
735
|
+
|
|
736
|
+
try:
|
|
737
|
+
retrieved_data = personality.data_source(generated_query)
|
|
738
|
+
if callback:
|
|
739
|
+
callback(f"Retrieved data successfully.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": dr_id, "data_snippet": retrieved_data[:200]})
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
if retrieved_data:
|
|
743
|
+
self.personality_data_zone = retrieved_data.strip()
|
|
744
|
+
|
|
745
|
+
except Exception as e:
|
|
746
|
+
trace_exception(e)
|
|
747
|
+
if callback:
|
|
748
|
+
callback(f"Error retrieving dynamic data: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION, {"id": dr_id})
|
|
749
|
+
except Exception as e:
|
|
750
|
+
trace_exception(e)
|
|
751
|
+
if callback:
|
|
752
|
+
callback(f"An error occurred during query generation: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION, {"id": qg_id})
|
|
753
|
+
|
|
754
|
+
# Determine effective MCPs by combining personality defaults and turn-specific overrides
|
|
755
|
+
effective_use_mcps = use_mcps
|
|
756
|
+
if personality and hasattr(personality, 'active_mcps') and personality.active_mcps:
|
|
757
|
+
if effective_use_mcps in [None, False]:
|
|
758
|
+
effective_use_mcps = personality.active_mcps
|
|
759
|
+
elif isinstance(effective_use_mcps, list):
|
|
760
|
+
effective_use_mcps = list(set(effective_use_mcps + personality.active_mcps))
|
|
647
761
|
|
|
648
762
|
if self.max_context_size is not None:
|
|
649
763
|
self.summarize_and_prune(self.max_context_size)
|
|
650
764
|
|
|
651
765
|
# Step 1: Add user message, now including any images.
|
|
652
766
|
if add_user_message:
|
|
653
|
-
# Pass kwargs through to capture images and other potential message attributes
|
|
654
767
|
user_msg = self.add_message(
|
|
655
768
|
sender="user",
|
|
656
769
|
sender_type="user",
|
|
657
770
|
content=user_message,
|
|
658
771
|
images=images,
|
|
659
|
-
**kwargs
|
|
772
|
+
**kwargs
|
|
660
773
|
)
|
|
661
774
|
else: # Regeneration logic
|
|
662
775
|
if self.active_branch_id not in self._message_index:
|
|
@@ -665,11 +778,9 @@ class LollmsDiscussion:
|
|
|
665
778
|
if user_msg_orm.sender_type != 'user':
|
|
666
779
|
raise ValueError(f"Regeneration failed: active branch tip is a '{user_msg_orm.sender_type}' message, not 'user'.")
|
|
667
780
|
user_msg = LollmsMessage(self, user_msg_orm)
|
|
668
|
-
# For regeneration, we use the images from the original user message
|
|
669
781
|
images = user_msg.images
|
|
670
782
|
|
|
671
|
-
|
|
672
|
-
is_agentic_turn = (use_mcps is not None and use_mcps) or (use_data_store is not None and use_data_store)
|
|
783
|
+
is_agentic_turn = (effective_use_mcps is not None and effective_use_mcps) or (use_data_store is not None and use_data_store)
|
|
673
784
|
|
|
674
785
|
start_time = datetime.now()
|
|
675
786
|
|
|
@@ -678,79 +789,56 @@ class LollmsDiscussion:
|
|
|
678
789
|
final_raw_response = ""
|
|
679
790
|
final_content = ""
|
|
680
791
|
|
|
681
|
-
# Step 3: Execute the appropriate generation logic.
|
|
682
792
|
if is_agentic_turn:
|
|
683
|
-
# --- AGENTIC TURN ---
|
|
684
793
|
prompt_for_agent = self.export("markdown", branch_tip_id if branch_tip_id else self.active_branch_id)
|
|
685
794
|
if debug:
|
|
686
|
-
ASCIIColors.cyan("\n" + "="*50)
|
|
687
|
-
ASCIIColors.cyan("--- DEBUG: AGENTIC TURN TRIGGERED ---")
|
|
688
|
-
ASCIIColors.cyan(f"--- PROMPT FOR AGENT (from discussion history) ---")
|
|
689
|
-
ASCIIColors.magenta(prompt_for_agent)
|
|
690
|
-
ASCIIColors.cyan("="*50 + "\n")
|
|
795
|
+
ASCIIColors.cyan("\n" + "="*50 + "\n--- DEBUG: AGENTIC TURN TRIGGERED ---\n" + f"--- PROMPT FOR AGENT (from discussion history) ---\n{prompt_for_agent}\n" + "="*50 + "\n")
|
|
691
796
|
|
|
692
797
|
agent_result = self.lollmsClient.generate_with_mcp_rag(
|
|
693
798
|
prompt=prompt_for_agent,
|
|
694
|
-
use_mcps=
|
|
799
|
+
use_mcps=effective_use_mcps,
|
|
695
800
|
use_data_store=use_data_store,
|
|
696
801
|
max_reasoning_steps=max_reasoning_steps,
|
|
697
802
|
images=images,
|
|
698
803
|
system_prompt = self._system_prompt,
|
|
699
|
-
debug=debug,
|
|
804
|
+
debug=debug,
|
|
700
805
|
**kwargs
|
|
701
806
|
)
|
|
702
807
|
final_content = agent_result.get("final_answer", "The agent did not produce a final answer.")
|
|
703
808
|
final_scratchpad = agent_result.get("final_scratchpad", "")
|
|
704
809
|
final_raw_response = json.dumps(agent_result, indent=2)
|
|
705
|
-
|
|
706
810
|
else:
|
|
707
|
-
# --- SIMPLE CHAT TURN ---
|
|
708
811
|
if debug:
|
|
709
812
|
prompt_for_chat = self.export("markdown", branch_tip_id if branch_tip_id else self.active_branch_id)
|
|
710
|
-
ASCIIColors.cyan("\n" + "="*50)
|
|
711
|
-
ASCIIColors.cyan("--- DEBUG: SIMPLE CHAT PROMPT ---")
|
|
712
|
-
ASCIIColors.magenta(prompt_for_chat)
|
|
713
|
-
ASCIIColors.cyan("="*50 + "\n")
|
|
813
|
+
ASCIIColors.cyan("\n" + "="*50 + f"\n--- DEBUG: SIMPLE CHAT PROMPT ---\n{prompt_for_chat}\n" + "="*50 + "\n")
|
|
714
814
|
|
|
715
|
-
# For simple chat, we also need to consider images if the model is multi-modal
|
|
716
815
|
final_raw_response = self.lollmsClient.chat(self, images=images, **kwargs) or ""
|
|
717
816
|
|
|
718
817
|
if debug:
|
|
719
|
-
ASCIIColors.cyan("\n" + "="*50)
|
|
720
|
-
ASCIIColors.cyan("--- DEBUG: RAW SIMPLE CHAT RESPONSE ---")
|
|
721
|
-
ASCIIColors.magenta(final_raw_response)
|
|
722
|
-
ASCIIColors.cyan("="*50 + "\n")
|
|
818
|
+
ASCIIColors.cyan("\n" + "="*50 + f"\n--- DEBUG: RAW SIMPLE CHAT RESPONSE ---\n{final_raw_response}\n" + "="*50 + "\n")
|
|
723
819
|
|
|
724
820
|
if isinstance(final_raw_response, dict) and final_raw_response.get("status") == "error":
|
|
725
821
|
raise Exception(final_raw_response.get("message", "Unknown error from lollmsClient.chat"))
|
|
726
822
|
else:
|
|
727
823
|
final_content = self.lollmsClient.remove_thinking_blocks(final_raw_response)
|
|
824
|
+
final_scratchpad = None
|
|
728
825
|
|
|
729
|
-
final_scratchpad = None # No agentic scratchpad in a simple turn
|
|
730
|
-
|
|
731
|
-
# Step 4: Post-generation processing and statistics.
|
|
732
826
|
end_time = datetime.now()
|
|
733
827
|
duration = (end_time - start_time).total_seconds()
|
|
734
828
|
token_count = self.lollmsClient.count_tokens(final_content)
|
|
735
829
|
tok_per_sec = (token_count / duration) if duration > 0 else 0
|
|
736
830
|
|
|
737
|
-
# Step 5: Collect metadata from the agentic turn for storage.
|
|
738
831
|
message_meta = {}
|
|
739
832
|
if is_agentic_turn and isinstance(agent_result, dict):
|
|
740
|
-
if "tool_calls" in agent_result:
|
|
741
|
-
|
|
742
|
-
if "
|
|
743
|
-
|
|
744
|
-
if agent_result.get("clarification_required", False):
|
|
745
|
-
message_meta["clarification_required"] = True
|
|
746
|
-
|
|
747
|
-
# Step 6: Add the final AI message to the discussion.
|
|
833
|
+
if "tool_calls" in agent_result: message_meta["tool_calls"] = agent_result["tool_calls"]
|
|
834
|
+
if "sources" in agent_result: message_meta["sources"] = agent_result["sources"]
|
|
835
|
+
if agent_result.get("clarification_required", False): message_meta["clarification_required"] = True
|
|
836
|
+
|
|
748
837
|
ai_message_obj = self.add_message(
|
|
749
838
|
sender=personality.name if personality else "assistant",
|
|
750
839
|
sender_type="assistant",
|
|
751
840
|
content=final_content,
|
|
752
841
|
raw_content=final_raw_response,
|
|
753
|
-
# Store the agent's full reasoning log in the message's dedicated scratchpad field
|
|
754
842
|
scratchpad=final_scratchpad,
|
|
755
843
|
tokens=token_count,
|
|
756
844
|
generation_speed=tok_per_sec,
|
|
@@ -892,7 +980,21 @@ class LollmsDiscussion:
|
|
|
892
980
|
return "" if format_type == "lollms_text" else []
|
|
893
981
|
|
|
894
982
|
branch = self.get_branch(branch_tip_id)
|
|
895
|
-
|
|
983
|
+
|
|
984
|
+
# Combine system prompt and data zones
|
|
985
|
+
system_prompt_part = (self._system_prompt or "").strip()
|
|
986
|
+
data_zone_part = self.get_full_data_zone() # This now returns a clean, multi-part block or an empty string
|
|
987
|
+
full_system_prompt = ""
|
|
988
|
+
|
|
989
|
+
# Combine them intelligently
|
|
990
|
+
if system_prompt_part and data_zone_part:
|
|
991
|
+
full_system_prompt = f"{system_prompt_part}\n\n{data_zone_part}"
|
|
992
|
+
elif system_prompt_part:
|
|
993
|
+
full_system_prompt = system_prompt_part
|
|
994
|
+
else:
|
|
995
|
+
full_system_prompt = data_zone_part
|
|
996
|
+
|
|
997
|
+
|
|
896
998
|
participants = self.participants or {}
|
|
897
999
|
|
|
898
1000
|
def get_full_content(msg: 'LollmsMessage') -> str:
|
|
@@ -1049,6 +1151,73 @@ class LollmsDiscussion:
|
|
|
1049
1151
|
self.touch()
|
|
1050
1152
|
print(f"[INFO] Discussion auto-pruned. {len(messages_to_prune)} messages summarized. History preserved.")
|
|
1051
1153
|
|
|
1154
|
+
def memorize(self, branch_tip_id: Optional[str] = None):
|
|
1155
|
+
"""
|
|
1156
|
+
Analyzes the current discussion, extracts key information suitable for long-term
|
|
1157
|
+
memory, and appends it to the discussion's 'memory' field.
|
|
1158
|
+
|
|
1159
|
+
This is intended to build a persistent knowledge base about user preferences,
|
|
1160
|
+
facts, and context that can be useful across different future discussions.
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
branch_tip_id: The ID of the message to use as the end of the context
|
|
1164
|
+
for memory extraction. Defaults to the active branch.
|
|
1165
|
+
"""
|
|
1166
|
+
try:
|
|
1167
|
+
# 1. Get the current conversation context
|
|
1168
|
+
discussion_context = self.export("markdown", branch_tip_id=branch_tip_id)
|
|
1169
|
+
if not discussion_context.strip():
|
|
1170
|
+
print("[INFO] Memorize: Discussion is empty, nothing to memorize.")
|
|
1171
|
+
return
|
|
1172
|
+
|
|
1173
|
+
# 2. Formulate the prompt for the LLM
|
|
1174
|
+
system_prompt = (
|
|
1175
|
+
"You are a Memory Extractor AI. Your task is to analyze a conversation "
|
|
1176
|
+
"and extract only the most critical pieces of information that would be "
|
|
1177
|
+
"valuable for a future, unrelated conversation with the same user. "
|
|
1178
|
+
"Focus on: \n"
|
|
1179
|
+
"- Explicit user preferences, goals, or facts about themselves.\n"
|
|
1180
|
+
"- Key decisions or conclusions reached.\n"
|
|
1181
|
+
"- Important entities, projects, or topics mentioned that are likely to recur.\n"
|
|
1182
|
+
"Format the output as a concise list of bullet points. Be brief and factual. "
|
|
1183
|
+
"If no new, significant long-term information is present, output the single word: 'NOTHING'."
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
prompt = (
|
|
1187
|
+
"Analyze the following discussion and extract key information for long-term memory:\n\n"
|
|
1188
|
+
f"--- Conversation ---\n{discussion_context}\n\n"
|
|
1189
|
+
"--- Extracted Memory Points (as a bulleted list) ---"
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
# 3. Call the LLM to extract information
|
|
1193
|
+
print("[INFO] Memorize: Extracting key information from discussion...")
|
|
1194
|
+
extracted_info = self.lollmsClient.generate_text(
|
|
1195
|
+
prompt,
|
|
1196
|
+
system_prompt=system_prompt,
|
|
1197
|
+
n_predict=512, # A reasonable length for a summary
|
|
1198
|
+
temperature=0.1, # Low temperature for factual extraction
|
|
1199
|
+
top_k=10,
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
# 4. Process and append the information
|
|
1203
|
+
if extracted_info and "NOTHING" not in extracted_info.upper():
|
|
1204
|
+
new_memory_entry = extracted_info.strip()
|
|
1205
|
+
|
|
1206
|
+
# Format with a timestamp for context
|
|
1207
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
1208
|
+
formatted_entry = f"\n\n--- Memory entry from {timestamp} ---\n{new_memory_entry}"
|
|
1209
|
+
|
|
1210
|
+
current_memory = self.memory or ""
|
|
1211
|
+
self.memory = (current_memory + formatted_entry).strip()
|
|
1212
|
+
self.touch() # Mark as updated and save if autosave is on
|
|
1213
|
+
print(f"[INFO] Memorize: New information added to long-term memory.")
|
|
1214
|
+
else:
|
|
1215
|
+
print("[INFO] Memorize: No new significant information found to add to memory.")
|
|
1216
|
+
|
|
1217
|
+
except Exception as e:
|
|
1218
|
+
trace_exception(e)
|
|
1219
|
+
print(f"[ERROR] Memorize: Failed to extract memory. {e}")
|
|
1220
|
+
|
|
1052
1221
|
def count_discussion_tokens(self, format_type: str, branch_tip_id: Optional[str] = None) -> int:
|
|
1053
1222
|
"""Counts the number of tokens in the exported discussion content.
|
|
1054
1223
|
|
|
@@ -1083,27 +1252,134 @@ class LollmsDiscussion:
|
|
|
1083
1252
|
|
|
1084
1253
|
return self.lollmsClient.count_tokens(text_to_count)
|
|
1085
1254
|
|
|
1086
|
-
def get_context_status(self, branch_tip_id: Optional[str] = None) -> Dict[str,
|
|
1087
|
-
"""
|
|
1255
|
+
def get_context_status(self, branch_tip_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1256
|
+
"""
|
|
1257
|
+
Returns a detailed breakdown of the context size and its components.
|
|
1088
1258
|
|
|
1089
|
-
This provides a snapshot of the context usage,
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
for pruning calculations.
|
|
1259
|
+
This provides a comprehensive snapshot of the context usage, including the
|
|
1260
|
+
content and token count for each part of the prompt (system prompt, data zones,
|
|
1261
|
+
pruning summary, and message history). The token counts are based on the
|
|
1262
|
+
"lollms_text" export format, which is the format used for pruning calculations.
|
|
1093
1263
|
|
|
1094
1264
|
Args:
|
|
1095
1265
|
branch_tip_id: The ID of the message branch to measure. Defaults
|
|
1096
1266
|
to the active branch.
|
|
1097
1267
|
|
|
1098
1268
|
Returns:
|
|
1099
|
-
A dictionary with
|
|
1269
|
+
A dictionary with a detailed breakdown:
|
|
1270
|
+
{
|
|
1271
|
+
"max_tokens": int | None,
|
|
1272
|
+
"current_tokens": int,
|
|
1273
|
+
"zones": {
|
|
1274
|
+
"system_prompt": {"content": str, "tokens": int},
|
|
1275
|
+
"memory": {"content": str, "tokens": int},
|
|
1276
|
+
"user_data_zone": {"content": str, "tokens": int},
|
|
1277
|
+
"discussion_data_zone": {"content": str, "tokens": int},
|
|
1278
|
+
"personality_data_zone": {"content": str, "tokens": int},
|
|
1279
|
+
"pruning_summary": {"content": str, "tokens": int},
|
|
1280
|
+
"message_history": {"content": str, "tokens": int, "message_count": int}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
Zones are only included if they contain content.
|
|
1100
1284
|
"""
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
"current_tokens":
|
|
1104
|
-
"
|
|
1285
|
+
result = {
|
|
1286
|
+
"max_tokens": self.max_context_size,
|
|
1287
|
+
"current_tokens": 0,
|
|
1288
|
+
"zones": {}
|
|
1289
|
+
}
|
|
1290
|
+
total_tokens = 0
|
|
1291
|
+
|
|
1292
|
+
# 1. System Prompt
|
|
1293
|
+
system_prompt_text = (self._system_prompt or "").strip()
|
|
1294
|
+
if system_prompt_text:
|
|
1295
|
+
# We count tokens for the full block as it would appear in the prompt
|
|
1296
|
+
full_block = f"!@>system:\n{system_prompt_text}\n"
|
|
1297
|
+
tokens = self.lollmsClient.count_tokens(full_block)
|
|
1298
|
+
result["zones"]["system_prompt"] = {
|
|
1299
|
+
"content": system_prompt_text,
|
|
1300
|
+
"tokens": tokens
|
|
1301
|
+
}
|
|
1302
|
+
total_tokens += tokens
|
|
1303
|
+
|
|
1304
|
+
# 2. All Data Zones
|
|
1305
|
+
zones_to_process = {
|
|
1306
|
+
"memory": self.memory,
|
|
1307
|
+
"user_data_zone": self.user_data_zone,
|
|
1308
|
+
"discussion_data_zone": self.discussion_data_zone,
|
|
1309
|
+
"personality_data_zone": self.personality_data_zone,
|
|
1105
1310
|
}
|
|
1106
1311
|
|
|
1312
|
+
for name, content in zones_to_process.items():
|
|
1313
|
+
content_text = (content or "").strip()
|
|
1314
|
+
if content_text:
|
|
1315
|
+
# Mimic the formatting from get_full_data_zone for accurate token counting
|
|
1316
|
+
header = f"-- {name.replace('_', ' ').title()} --\n"
|
|
1317
|
+
full_block = f"{header}{content_text}"
|
|
1318
|
+
# In lollms_text format, zones are part of the system message, so we add separators
|
|
1319
|
+
# This counts the standalone block.
|
|
1320
|
+
tokens = self.lollmsClient.count_tokens(full_block)
|
|
1321
|
+
result["zones"][name] = {
|
|
1322
|
+
"content": content_text,
|
|
1323
|
+
"tokens": tokens
|
|
1324
|
+
}
|
|
1325
|
+
# Note: The 'export' method combines these into one system prompt.
|
|
1326
|
+
# For this breakdown, we count them separately. The total will be a close approximation.
|
|
1327
|
+
|
|
1328
|
+
# 3. Pruning Summary
|
|
1329
|
+
pruning_summary_text = (self.pruning_summary or "").strip()
|
|
1330
|
+
if pruning_summary_text and self.pruning_point_id:
|
|
1331
|
+
full_block = f"!@>system:\n--- Conversation Summary ---\n{pruning_summary_text}\n"
|
|
1332
|
+
tokens = self.lollmsClient.count_tokens(full_block)
|
|
1333
|
+
result["zones"]["pruning_summary"] = {
|
|
1334
|
+
"content": pruning_summary_text,
|
|
1335
|
+
"tokens": tokens
|
|
1336
|
+
}
|
|
1337
|
+
total_tokens += tokens
|
|
1338
|
+
|
|
1339
|
+
# 4. Message History
|
|
1340
|
+
branch_tip_id = branch_tip_id or self.active_branch_id
|
|
1341
|
+
messages_text = ""
|
|
1342
|
+
message_count = 0
|
|
1343
|
+
if branch_tip_id:
|
|
1344
|
+
branch = self.get_branch(branch_tip_id)
|
|
1345
|
+
messages_to_render = branch
|
|
1346
|
+
|
|
1347
|
+
# Adjust for pruning to get the active set of messages
|
|
1348
|
+
if self.pruning_summary and self.pruning_point_id:
|
|
1349
|
+
pruning_index = -1
|
|
1350
|
+
for i, msg in enumerate(branch):
|
|
1351
|
+
if msg.id == self.pruning_point_id:
|
|
1352
|
+
pruning_index = i
|
|
1353
|
+
break
|
|
1354
|
+
if pruning_index != -1:
|
|
1355
|
+
messages_to_render = branch[pruning_index:]
|
|
1356
|
+
|
|
1357
|
+
message_parts = []
|
|
1358
|
+
for msg in messages_to_render:
|
|
1359
|
+
sender_str = msg.sender.replace(':', '').replace('!@>', '')
|
|
1360
|
+
content = msg.content.strip()
|
|
1361
|
+
if msg.images:
|
|
1362
|
+
content += f"\n({len(msg.images)} image(s) attached)"
|
|
1363
|
+
msg_text = f"!@>{sender_str}:\n{content}\n"
|
|
1364
|
+
message_parts.append(msg_text)
|
|
1365
|
+
|
|
1366
|
+
messages_text = "".join(message_parts)
|
|
1367
|
+
message_count = len(messages_to_render)
|
|
1368
|
+
|
|
1369
|
+
if messages_text:
|
|
1370
|
+
tokens = self.lollmsClient.count_tokens(messages_text)
|
|
1371
|
+
result["zones"]["message_history"] = {
|
|
1372
|
+
"content": messages_text,
|
|
1373
|
+
"tokens": tokens,
|
|
1374
|
+
"message_count": message_count
|
|
1375
|
+
}
|
|
1376
|
+
total_tokens += tokens
|
|
1377
|
+
|
|
1378
|
+
# Finalize the total count. This re-calculates based on the actual export format
|
|
1379
|
+
# for maximum accuracy, as combining zones can slightly change tokenization.
|
|
1380
|
+
result["current_tokens"] = self.count_discussion_tokens("lollms_text", branch_tip_id)
|
|
1381
|
+
|
|
1382
|
+
return result
|
|
1107
1383
|
def switch_to_branch(self, branch_id):
|
|
1108
1384
|
self.active_branch_id = branch_id
|
|
1109
1385
|
|
|
@@ -1112,15 +1388,21 @@ class LollmsDiscussion:
|
|
|
1112
1388
|
if self.metadata is None:
|
|
1113
1389
|
self.metadata = {}
|
|
1114
1390
|
discussion = self.export("markdown")[0:1000]
|
|
1115
|
-
|
|
1391
|
+
system_prompt="You are a title builder out of a discussion."
|
|
1392
|
+
prompt = f"""Build a title for the following discussion:
|
|
1116
1393
|
{discussion}
|
|
1117
1394
|
...
|
|
1118
1395
|
"""
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1396
|
+
title_generation_schema = {
|
|
1397
|
+
"type": "object",
|
|
1398
|
+
"properties": {
|
|
1399
|
+
"title": {"type": "string", "description": "Short, catchy title for the discussion."},
|
|
1400
|
+
},
|
|
1401
|
+
"required": ["title"],
|
|
1402
|
+
"description": "JSON object as title of the discussion."
|
|
1403
|
+
}
|
|
1404
|
+
infos = self.lollmsClient.generate_structured_content(prompt = prompt, system_prompt=system_prompt, schema = title_generation_schema)
|
|
1405
|
+
discussion_title = infos["title"]
|
|
1124
1406
|
new_metadata = (self.metadata or {}).copy()
|
|
1125
1407
|
new_metadata['title'] = discussion_title
|
|
1126
1408
|
|
|
@@ -22,6 +22,8 @@ class LollmsPersonality:
|
|
|
22
22
|
# Core behavioral instruction
|
|
23
23
|
system_prompt: str,
|
|
24
24
|
icon: Optional[str] = None, # Base64 encoded image string
|
|
25
|
+
active_mcps: Optional[List[str]] = None, # The list of MCPs to activate with this personality
|
|
26
|
+
data_source: Optional[Union[str, Callable[[str], str]]] = None, # Static string data or a callable for dynamic data retrieval
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
# RAG - Data Files and Application-provided Callbacks
|
|
@@ -46,6 +48,8 @@ class LollmsPersonality:
|
|
|
46
48
|
description: A brief description of what the personality does.
|
|
47
49
|
icon: An optional base64 encoded string for a display icon.
|
|
48
50
|
system_prompt: The core system prompt that defines the AI's behavior.
|
|
51
|
+
active_mcps: An optional list of MCP (tool) names to be automatically activated with this personality.
|
|
52
|
+
data_source: A source of knowledge. Can be a static string or a callable function that takes a query and returns a string.
|
|
49
53
|
data_files: A list of file paths to be used as a knowledge base for RAG.
|
|
50
54
|
vectorize_chunk_callback: A function provided by the host app to vectorize and store a text chunk.
|
|
51
55
|
is_vectorized_callback: A function provided by the host app to check if a chunk is already vectorized.
|
|
@@ -59,6 +63,8 @@ class LollmsPersonality:
|
|
|
59
63
|
self.description = description
|
|
60
64
|
self.icon = icon
|
|
61
65
|
self.system_prompt = system_prompt
|
|
66
|
+
self.active_mcps = active_mcps or []
|
|
67
|
+
self.data_source = data_source
|
|
62
68
|
self.data_files = [Path(f) for f in data_files] if data_files else []
|
|
63
69
|
|
|
64
70
|
# RAG Callbacks provided by the host application
|
|
@@ -177,6 +183,8 @@ class LollmsPersonality:
|
|
|
177
183
|
"category": self.category,
|
|
178
184
|
"description": self.description,
|
|
179
185
|
"system_prompt": self.system_prompt,
|
|
186
|
+
"active_mcps": self.active_mcps,
|
|
187
|
+
"has_data_source": self.data_source is not None,
|
|
180
188
|
"data_files": [str(p) for p in self.data_files],
|
|
181
189
|
"has_script": self.script is not None
|
|
182
190
|
}
|
|
@@ -93,10 +93,18 @@ def robust_json_parser(json_string: str) -> dict:
|
|
|
93
93
|
ValueError: If parsing fails after all correction attempts.
|
|
94
94
|
"""
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
|
|
97
|
+
# STEP 0: Attempt to parse directly
|
|
98
|
+
try:
|
|
99
|
+
return json.loads(json_string)
|
|
100
|
+
except json.JSONDecodeError as e:
|
|
101
|
+
trace_exception(e)
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# STEP 1: Remove code block wrappers if present (e.g., ```json ... ```)
|
|
97
105
|
json_string = re.sub(r"^```(?:json)?\s*|\s*```$", '', json_string.strip())
|
|
98
106
|
|
|
99
|
-
# STEP
|
|
107
|
+
# STEP 2: Attempt to parse directly
|
|
100
108
|
try:
|
|
101
109
|
return json.loads(json_string)
|
|
102
110
|
except json.JSONDecodeError:
|