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.

@@ -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 # Use kwargs to allow other fields to be set from the caller
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
- # Step 2: Determine if this is a simple chat or a complex agentic turn.
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=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, # Pass the debug flag down
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
- message_meta["tool_calls"] = agent_result["tool_calls"]
742
- if "sources" in agent_result:
743
- message_meta["sources"] = agent_result["sources"]
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
- full_system_prompt = self._system_prompt # Simplified for clarity
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, Optional[int]]:
1087
- """Returns the current token count and the maximum context size.
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, taking into account
1090
- any non-destructive pruning that has occurred. The token count is
1091
- based on the "lollms_text" export format, which is the format used
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 'current_tokens' and 'max_tokens'.
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
- current_tokens = self.count_discussion_tokens("lollms_text", branch_tip_id)
1102
- return {
1103
- "current_tokens": current_tokens,
1104
- "max_tokens": self.max_context_size
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
- prompt = f"""You are a title builder. Your oibjective is to build a title for the following discussion:
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
- template = """{
1120
- "title": "An short but comprehensive discussion title"
1121
- }"""
1122
- infos = self.lollmsClient.generate_code(prompt = prompt, template = template)
1123
- discussion_title = robust_json_parser(infos)["title"]
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
- # STEP 0: Remove code block wrappers if present (e.g., ```json ... ```)
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 1: Attempt to parse directly
107
+ # STEP 2: Attempt to parse directly
100
108
  try:
101
109
  return json.loads(json_string)
102
110
  except json.JSONDecodeError: