lollms-client 0.29.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.

@@ -128,7 +128,11 @@ def create_dynamic_models(
128
128
  __abstract__ = True
129
129
  id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
130
130
  system_prompt = Column(EncryptedText, nullable=True)
131
- data_zone = Column(EncryptedText, nullable=True) # New field for persistent data
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
+
132
136
  participants = Column(JSON, nullable=True, default=dict)
133
137
  active_branch_id = Column(String, nullable=True)
134
138
  discussion_metadata = Column(JSON, nullable=True, default=dict)
@@ -213,8 +217,6 @@ class LollmsDataManager:
213
217
  with self.engine.connect() as connection:
214
218
  print("Checking for database schema upgrades...")
215
219
 
216
- # --- THIS IS THE FIX ---
217
- # We must wrap raw SQL strings in the `text()` function for direct execution.
218
220
  cursor = connection.execute(text("PRAGMA table_info(discussions)"))
219
221
  columns = [row[1] for row in cursor.fetchall()]
220
222
 
@@ -226,12 +228,27 @@ class LollmsDataManager:
226
228
  print(" -> Upgrading 'discussions' table: Adding 'pruning_point_id' column.")
227
229
  connection.execute(text("ALTER TABLE discussions ADD COLUMN pruning_point_id VARCHAR"))
228
230
 
229
- if 'data_zone' not in columns:
230
- print(" -> Upgrading 'discussions' table: Adding 'data_zone' column.")
231
- connection.execute(text("ALTER TABLE discussions ADD COLUMN data_zone TEXT"))
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"))
232
250
 
233
251
  print("Database schema is up to date.")
234
- # This is important to apply the ALTER TABLE statements
235
252
  connection.commit()
236
253
 
237
254
  except Exception as e:
@@ -388,7 +405,6 @@ class LollmsDiscussion:
388
405
  object.__setattr__(self, '_is_db_backed', db_manager is not None)
389
406
 
390
407
  object.__setattr__(self, '_system_prompt', None)
391
-
392
408
  if self._is_db_backed:
393
409
  if not db_discussion_obj and not discussion_id:
394
410
  raise ValueError("Either discussion_id or db_discussion_obj must be provided for DB-backed discussions.")
@@ -490,7 +506,10 @@ class LollmsDiscussion:
490
506
  proxy = SimpleNamespace()
491
507
  proxy.id = id or str(uuid.uuid4())
492
508
  proxy.system_prompt = None
493
- proxy.data_zone = None
509
+ proxy.user_data_zone = None
510
+ proxy.discussion_data_zone = None
511
+ proxy.personality_data_zone = None
512
+ proxy.memory = None
494
513
  proxy.participants = {}
495
514
  proxy.active_branch_id = None
496
515
  proxy.discussion_metadata = {}
@@ -601,7 +620,23 @@ class LollmsDiscussion:
601
620
 
602
621
  return [LollmsMessage(self, orm) for orm in reversed(branch_orms)]
603
622
 
604
-
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
+
605
640
  def chat(
606
641
  self,
607
642
  user_message: str,
@@ -650,7 +685,7 @@ class LollmsDiscussion:
650
685
  where the 'ai_message' will contain rich metadata if an agentic turn was used.
651
686
  """
652
687
  callback = kwargs.get("streaming_callback")
653
-
688
+ # extract personality data
654
689
  if personality is not None:
655
690
  object.__setattr__(self, '_system_prompt', personality.system_prompt)
656
691
 
@@ -660,8 +695,8 @@ class LollmsDiscussion:
660
695
  # --- Static Data Source ---
661
696
  if callback:
662
697
  callback("Loading static personality data...", MSG_TYPE.MSG_TYPE_STEP, {"id": "static_data_loading"})
663
- current_data_zone = self.data_zone or ""
664
- self.data_zone = (current_data_zone + "\n\n--- Personality Static Data ---\n" + personality.data_source).strip()
698
+ if personality.data_source:
699
+ self.personality_data_zone = personality.data_source.strip()
665
700
 
666
701
  elif callable(personality.data_source):
667
702
  # --- Dynamic Data Source ---
@@ -703,8 +738,9 @@ class LollmsDiscussion:
703
738
  if callback:
704
739
  callback(f"Retrieved data successfully.", MSG_TYPE.MSG_TYPE_STEP_END, {"id": dr_id, "data_snippet": retrieved_data[:200]})
705
740
 
706
- current_data_zone = self.data_zone or ""
707
- self.data_zone = (current_data_zone + "\n\n--- Retrieved Dynamic Data ---\n" + retrieved_data).strip()
741
+
742
+ if retrieved_data:
743
+ self.personality_data_zone = retrieved_data.strip()
708
744
 
709
745
  except Exception as e:
710
746
  trace_exception(e)
@@ -714,7 +750,7 @@ class LollmsDiscussion:
714
750
  trace_exception(e)
715
751
  if callback:
716
752
  callback(f"An error occurred during query generation: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION, {"id": qg_id})
717
-
753
+
718
754
  # Determine effective MCPs by combining personality defaults and turn-specific overrides
719
755
  effective_use_mcps = use_mcps
720
756
  if personality and hasattr(personality, 'active_mcps') and personality.active_mcps:
@@ -945,11 +981,19 @@ class LollmsDiscussion:
945
981
 
946
982
  branch = self.get_branch(branch_tip_id)
947
983
 
948
- # Combine system prompt and the new data_zone if it exists
949
- full_system_prompt = (self._system_prompt or "").strip()
950
- if hasattr(self, 'data_zone') and self.data_zone:
951
- data_zone_text = f"\n\n--- data ---\n{self.data_zone.strip()}"
952
- full_system_prompt = (full_system_prompt + data_zone_text).strip()
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
+
953
997
 
954
998
  participants = self.participants or {}
955
999
 
@@ -1107,6 +1151,73 @@ class LollmsDiscussion:
1107
1151
  self.touch()
1108
1152
  print(f"[INFO] Discussion auto-pruned. {len(messages_to_prune)} messages summarized. History preserved.")
1109
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
+
1110
1221
  def count_discussion_tokens(self, format_type: str, branch_tip_id: Optional[str] = None) -> int:
1111
1222
  """Counts the number of tokens in the exported discussion content.
1112
1223
 
@@ -1141,27 +1252,134 @@ class LollmsDiscussion:
1141
1252
 
1142
1253
  return self.lollmsClient.count_tokens(text_to_count)
1143
1254
 
1144
- def get_context_status(self, branch_tip_id: Optional[str] = None) -> Dict[str, Optional[int]]:
1145
- """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.
1146
1258
 
1147
- This provides a snapshot of the context usage, taking into account
1148
- any non-destructive pruning that has occurred. The token count is
1149
- based on the "lollms_text" export format, which is the format used
1150
- 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.
1151
1263
 
1152
1264
  Args:
1153
1265
  branch_tip_id: The ID of the message branch to measure. Defaults
1154
1266
  to the active branch.
1155
1267
 
1156
1268
  Returns:
1157
- 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.
1158
1284
  """
1159
- current_tokens = self.count_discussion_tokens("lollms_text", branch_tip_id)
1160
- return {
1161
- "current_tokens": current_tokens,
1162
- "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,
1163
1310
  }
1164
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
1165
1383
  def switch_to_branch(self, branch_id):
1166
1384
  self.active_branch_id = branch_id
1167
1385
 
@@ -1170,15 +1388,21 @@ class LollmsDiscussion:
1170
1388
  if self.metadata is None:
1171
1389
  self.metadata = {}
1172
1390
  discussion = self.export("markdown")[0:1000]
1173
- 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:
1174
1393
  {discussion}
1175
1394
  ...
1176
1395
  """
1177
- template = """{
1178
- "title": "An short but comprehensive discussion title"
1179
- }"""
1180
- infos = self.lollmsClient.generate_code(prompt = prompt, template = template)
1181
- 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"]
1182
1406
  new_metadata = (self.metadata or {}).copy()
1183
1407
  new_metadata['title'] = discussion_title
1184
1408
 
@@ -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: