lollms-client 0.33.0__py3-none-any.whl → 1.1.0__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.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/azure_openai/__init__.py +6 -10
- lollms_client/llm_bindings/claude/__init__.py +4 -7
- lollms_client/llm_bindings/gemini/__init__.py +3 -7
- lollms_client/llm_bindings/grok/__init__.py +3 -7
- lollms_client/llm_bindings/groq/__init__.py +4 -6
- lollms_client/llm_bindings/hugging_face_inference_api/__init__.py +4 -6
- lollms_client/llm_bindings/litellm/__init__.py +15 -6
- lollms_client/llm_bindings/llamacpp/__init__.py +27 -9
- lollms_client/llm_bindings/lollms/__init__.py +24 -14
- lollms_client/llm_bindings/lollms_webui/__init__.py +6 -12
- lollms_client/llm_bindings/mistral/__init__.py +3 -5
- lollms_client/llm_bindings/ollama/__init__.py +6 -11
- lollms_client/llm_bindings/open_router/__init__.py +4 -6
- lollms_client/llm_bindings/openai/__init__.py +7 -14
- lollms_client/llm_bindings/openllm/__init__.py +12 -12
- lollms_client/llm_bindings/pythonllamacpp/__init__.py +1 -1
- lollms_client/llm_bindings/tensor_rt/__init__.py +8 -13
- lollms_client/llm_bindings/transformers/__init__.py +14 -6
- lollms_client/llm_bindings/vllm/__init__.py +16 -12
- lollms_client/lollms_core.py +303 -490
- lollms_client/lollms_discussion.py +431 -78
- lollms_client/lollms_llm_binding.py +192 -381
- lollms_client/lollms_mcp_binding.py +33 -2
- lollms_client/lollms_tti_binding.py +107 -2
- lollms_client/mcp_bindings/local_mcp/__init__.py +3 -2
- lollms_client/mcp_bindings/remote_mcp/__init__.py +6 -5
- lollms_client/mcp_bindings/standard_mcp/__init__.py +3 -5
- lollms_client/stt_bindings/lollms/__init__.py +6 -8
- lollms_client/stt_bindings/whisper/__init__.py +2 -4
- lollms_client/stt_bindings/whispercpp/__init__.py +15 -16
- lollms_client/tti_bindings/dalle/__init__.py +50 -29
- lollms_client/tti_bindings/diffusers/__init__.py +227 -439
- lollms_client/tti_bindings/gemini/__init__.py +320 -0
- lollms_client/tti_bindings/lollms/__init__.py +8 -9
- lollms_client-1.1.0.dist-info/METADATA +1214 -0
- lollms_client-1.1.0.dist-info/RECORD +69 -0
- {lollms_client-0.33.0.dist-info → lollms_client-1.1.0.dist-info}/top_level.txt +0 -2
- examples/article_summary/article_summary.py +0 -58
- examples/console_discussion/console_app.py +0 -266
- examples/console_discussion.py +0 -448
- examples/deep_analyze/deep_analyse.py +0 -30
- examples/deep_analyze/deep_analyze_multiple_files.py +0 -32
- examples/function_calling_with_local_custom_mcp.py +0 -250
- examples/generate_a_benchmark_for_safe_store.py +0 -89
- examples/generate_and_speak/generate_and_speak.py +0 -251
- examples/generate_game_sfx/generate_game_fx.py +0 -240
- examples/generate_text_with_multihop_rag_example.py +0 -210
- examples/gradio_chat_app.py +0 -228
- examples/gradio_lollms_chat.py +0 -259
- examples/internet_search_with_rag.py +0 -226
- examples/lollms_chat/calculator.py +0 -59
- examples/lollms_chat/derivative.py +0 -48
- examples/lollms_chat/test_openai_compatible_with_lollms_chat.py +0 -12
- examples/lollms_discussions_test.py +0 -155
- examples/mcp_examples/external_mcp.py +0 -267
- examples/mcp_examples/local_mcp.py +0 -171
- examples/mcp_examples/openai_mcp.py +0 -203
- examples/mcp_examples/run_remote_mcp_example_v2.py +0 -290
- examples/mcp_examples/run_standard_mcp_example.py +0 -204
- examples/simple_text_gen_test.py +0 -173
- examples/simple_text_gen_with_image_test.py +0 -178
- examples/test_local_models/local_chat.py +0 -9
- examples/text_2_audio.py +0 -77
- examples/text_2_image.py +0 -144
- examples/text_2_image_diffusers.py +0 -274
- examples/text_and_image_2_audio.py +0 -59
- examples/text_gen.py +0 -30
- examples/text_gen_system_prompt.py +0 -29
- lollms_client-0.33.0.dist-info/METADATA +0 -854
- lollms_client-0.33.0.dist-info/RECORD +0 -101
- test/test_lollms_discussion.py +0 -368
- {lollms_client-0.33.0.dist-info → lollms_client-1.1.0.dist-info}/WHEEL +0 -0
- {lollms_client-0.33.0.dist-info → lollms_client-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -111,6 +111,8 @@ def create_dynamic_models(
|
|
|
111
111
|
optionally including custom mixin classes for extending functionality and
|
|
112
112
|
applying encryption to text fields if a key is provided.
|
|
113
113
|
|
|
114
|
+
Requires the 'cryptography' library to be installed.
|
|
115
|
+
|
|
114
116
|
Args:
|
|
115
117
|
discussion_mixin: An optional class to mix into the Discussion model.
|
|
116
118
|
message_mixin: An optional class to mix into the Message model.
|
|
@@ -312,7 +314,7 @@ class LollmsDataManager:
|
|
|
312
314
|
A list of dictionaries representing the matching discussions.
|
|
313
315
|
"""
|
|
314
316
|
with self.get_session() as session:
|
|
315
|
-
query = session.query(self.DiscussionModel)
|
|
317
|
+
query = query = session.query(self.DiscussionModel)
|
|
316
318
|
for key, value in criteria.items():
|
|
317
319
|
if hasattr(self.DiscussionModel, key):
|
|
318
320
|
query = query.filter(getattr(self.DiscussionModel, key).ilike(f"%{value}%"))
|
|
@@ -521,6 +523,7 @@ class LollmsDiscussion:
|
|
|
521
523
|
# --- END FIX ---
|
|
522
524
|
|
|
523
525
|
self._rebuild_message_index()
|
|
526
|
+
self._validate_and_set_active_branch() # Call for initial load
|
|
524
527
|
|
|
525
528
|
@classmethod
|
|
526
529
|
def create_new(cls, lollms_client: 'LollmsClient', db_manager: Optional[LollmsDataManager] = None, **kwargs) -> 'LollmsDiscussion':
|
|
@@ -639,10 +642,124 @@ class LollmsDiscussion:
|
|
|
639
642
|
|
|
640
643
|
def _rebuild_message_index(self):
|
|
641
644
|
"""Rebuilds the internal dictionary mapping message IDs to message objects."""
|
|
642
|
-
if self._is_db_backed and self._session.is_active and self._db_discussion in self._session:
|
|
645
|
+
if self._is_db_backed and self._session and self._session.is_active and self._db_discussion in self._session:
|
|
646
|
+
# Ensure discussion object's messages collection is loaded/refreshed
|
|
643
647
|
self._session.refresh(self._db_discussion, ['messages'])
|
|
644
648
|
self._message_index = {msg.id: msg for msg in self._db_discussion.messages}
|
|
645
649
|
|
|
650
|
+
def _find_deepest_leaf(self, start_id: Optional[str]) -> Optional[str]:
|
|
651
|
+
"""
|
|
652
|
+
Finds the ID of the most recent leaf message in the branch starting from start_id.
|
|
653
|
+
If start_id is None or not found, it finds the most recent leaf in the entire discussion.
|
|
654
|
+
A leaf message is one that has no children.
|
|
655
|
+
"""
|
|
656
|
+
if not self._message_index:
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
self._rebuild_message_index() # Ensure the internal index is up-to-date
|
|
660
|
+
|
|
661
|
+
# Build an adjacency list (children_of_parent_id -> [child1_id, child2_id])
|
|
662
|
+
children_of = {msg_id: [] for msg_id in self._message_index.keys()}
|
|
663
|
+
for msg_id, msg_obj in self._message_index.items():
|
|
664
|
+
if msg_obj.parent_id in children_of: # Only if parent exists in current index
|
|
665
|
+
children_of[msg_obj.parent_id].append(msg_id)
|
|
666
|
+
|
|
667
|
+
# Helper to find the most recent leaf from a list of messages
|
|
668
|
+
def get_most_recent_leaf_from_list(message_list: List[Any]) -> Optional[str]:
|
|
669
|
+
if not message_list:
|
|
670
|
+
return None
|
|
671
|
+
leaves_in_list = [msg for msg in message_list if not children_of.get(msg.id)]
|
|
672
|
+
if leaves_in_list:
|
|
673
|
+
return max(leaves_in_list, key=lambda msg: msg.created_at).id
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
if start_id and start_id in self._message_index:
|
|
677
|
+
# Perform BFS to get all descendants including the start_id itself
|
|
678
|
+
queue = [self._message_index[start_id]]
|
|
679
|
+
visited_ids = {start_id}
|
|
680
|
+
descendants_and_self = [self._message_index[start_id]]
|
|
681
|
+
|
|
682
|
+
head = 0
|
|
683
|
+
while head < len(queue):
|
|
684
|
+
current_msg_obj = queue[head]
|
|
685
|
+
head += 1
|
|
686
|
+
|
|
687
|
+
for child_id in children_of.get(current_msg_obj.id, []):
|
|
688
|
+
if child_id not in visited_ids:
|
|
689
|
+
visited_ids.add(child_id)
|
|
690
|
+
child_obj = self._message_index[child_id]
|
|
691
|
+
queue.append(child_obj)
|
|
692
|
+
descendants_and_self.append(child_obj)
|
|
693
|
+
|
|
694
|
+
# Now find the most recent leaf among these descendants and the start_id itself
|
|
695
|
+
result_leaf_id = get_most_recent_leaf_from_list(descendants_and_self)
|
|
696
|
+
if result_leaf_id:
|
|
697
|
+
return result_leaf_id
|
|
698
|
+
else:
|
|
699
|
+
# If no actual leaves were found within the branch rooted at start_id,
|
|
700
|
+
# then start_id itself is the 'leaf' of its known subgraph IF it has no children.
|
|
701
|
+
if not children_of.get(start_id):
|
|
702
|
+
return start_id
|
|
703
|
+
return None # The start_id is not a leaf and has no reachable leaves.
|
|
704
|
+
|
|
705
|
+
else: # No specific starting point, find the most recent leaf in the entire discussion
|
|
706
|
+
all_messages_in_discussion = list(self._message_index.values())
|
|
707
|
+
return get_most_recent_leaf_from_list(all_messages_in_discussion)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _validate_and_set_active_branch(self):
|
|
711
|
+
"""
|
|
712
|
+
Ensures that self.active_branch_id points to an existing message and is a leaf message.
|
|
713
|
+
If it's None, points to a non-existent message, or points to a non-leaf message,
|
|
714
|
+
it attempts to set it to the ID of the most recently created leaf message in the entire discussion.
|
|
715
|
+
If a valid active_branch_id exists but is not a leaf, it will try to find the deepest leaf
|
|
716
|
+
from that point onwards.
|
|
717
|
+
This method directly updates the underlying _db_discussion object to avoid recursion.
|
|
718
|
+
"""
|
|
719
|
+
self._rebuild_message_index() # Ensure index is fresh
|
|
720
|
+
|
|
721
|
+
# If the discussion is empty, silently set active_branch_id to None and exit.
|
|
722
|
+
if not self._message_index:
|
|
723
|
+
object.__setattr__(self._db_discussion, 'active_branch_id', None)
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
current_active_id = self._db_discussion.active_branch_id # Access direct attribute
|
|
727
|
+
|
|
728
|
+
# Case 1: Active branch ID is invalid or missing
|
|
729
|
+
if current_active_id is None or current_active_id not in self._message_index:
|
|
730
|
+
ASCIIColors.warning(f"Active branch ID '{current_active_id}' is invalid or missing for discussion {self.id}. Attempting to select a new leaf.")
|
|
731
|
+
new_active_leaf_id = self._find_deepest_leaf(None) # Find most recent leaf in entire discussion
|
|
732
|
+
if new_active_leaf_id:
|
|
733
|
+
object.__setattr__(self._db_discussion, 'active_branch_id', new_active_leaf_id)
|
|
734
|
+
ASCIIColors.success(f"New active branch ID for discussion {self.id} set to: {new_active_leaf_id} (most recent overall leaf).")
|
|
735
|
+
else:
|
|
736
|
+
# This else block should theoretically not be reached if _message_index is not empty,
|
|
737
|
+
# as _find_deepest_leaf(None) would find a leaf. Added for robustness.
|
|
738
|
+
object.__setattr__(self, '_db_discussion.active_branch_id', None) # Use setattr for direct ORM access
|
|
739
|
+
ASCIIColors.yellow(f"Could not find any leaf messages in discussion {self.id}. Active branch ID remains None.")
|
|
740
|
+
|
|
741
|
+
# Case 2: Active branch ID exists, but is it a leaf?
|
|
742
|
+
else:
|
|
743
|
+
# Determine if current_active_id is a leaf
|
|
744
|
+
children_of_current_active = []
|
|
745
|
+
for msg_obj in self._message_index.values():
|
|
746
|
+
if msg_obj.parent_id == current_active_id:
|
|
747
|
+
children_of_current_active.append(msg_obj.id)
|
|
748
|
+
|
|
749
|
+
if children_of_current_active: # If it has children, it's not a leaf
|
|
750
|
+
ASCIIColors.warning(f"Active branch ID '{current_active_id}' is not a leaf message. Finding deepest leaf from this point.")
|
|
751
|
+
new_active_leaf_id = self._find_deepest_leaf(current_active_id)
|
|
752
|
+
if new_active_leaf_id and new_active_leaf_id != current_active_id:
|
|
753
|
+
object.__setattr__(self._db_discussion, 'active_branch_id', new_active_leaf_id)
|
|
754
|
+
ASCIIColors.success(f"Active branch ID for discussion {self.id} updated to: {new_active_leaf_id} (deepest leaf descendant).")
|
|
755
|
+
elif new_active_leaf_id is None: # Should not happen if current_active_id exists
|
|
756
|
+
ASCIIColors.warning(f"Could not find a deeper leaf from '{current_active_id}'. Keeping current ID.")
|
|
757
|
+
else:
|
|
758
|
+
ASCIIColors.info(f"Active branch ID '{current_active_id}' is already the deepest leaf. No change needed.")
|
|
759
|
+
else:
|
|
760
|
+
ASCIIColors.info(f"Active branch ID '{current_active_id}' is already a leaf. No change needed.")
|
|
761
|
+
|
|
762
|
+
|
|
646
763
|
def touch(self):
|
|
647
764
|
"""Marks the discussion as updated, persists images, and saves if autosave is on."""
|
|
648
765
|
# Persist in-memory discussion images to the metadata field before saving.
|
|
@@ -676,7 +793,8 @@ class LollmsDiscussion:
|
|
|
676
793
|
|
|
677
794
|
try:
|
|
678
795
|
self._session.commit()
|
|
679
|
-
self._rebuild_message_index()
|
|
796
|
+
self._rebuild_message_index() # Rebuild index after commit to reflect DB state
|
|
797
|
+
# self._validate_and_set_active_branch() # REMOVED: This validation is now handled by direct operations or on load.
|
|
680
798
|
except Exception as e:
|
|
681
799
|
self._session.rollback()
|
|
682
800
|
raise e
|
|
@@ -735,8 +853,8 @@ class LollmsDiscussion:
|
|
|
735
853
|
new_msg_orm = SimpleNamespace(**message_data)
|
|
736
854
|
self._db_discussion.messages.append(new_msg_orm)
|
|
737
855
|
|
|
856
|
+
self.active_branch_id = msg_id # New message is always a leaf
|
|
738
857
|
self._message_index[msg_id] = new_msg_orm
|
|
739
|
-
self.active_branch_id = msg_id
|
|
740
858
|
self.touch()
|
|
741
859
|
return LollmsMessage(self, new_msg_orm)
|
|
742
860
|
|
|
@@ -754,6 +872,7 @@ class LollmsDiscussion:
|
|
|
754
872
|
|
|
755
873
|
branch_orms = []
|
|
756
874
|
current_id = leaf_id
|
|
875
|
+
# Use _message_index for efficient lookup of parents
|
|
757
876
|
while current_id and current_id in self._message_index:
|
|
758
877
|
msg_orm = self._message_index[current_id]
|
|
759
878
|
branch_orms.append(msg_orm)
|
|
@@ -774,6 +893,18 @@ class LollmsDiscussion:
|
|
|
774
893
|
if db_message:
|
|
775
894
|
return LollmsMessage(self, db_message)
|
|
776
895
|
return None
|
|
896
|
+
|
|
897
|
+
def get_all_messages_flat(self) -> List[LollmsMessage]:
|
|
898
|
+
"""
|
|
899
|
+
Retrieves all messages stored for this discussion as a flat list.
|
|
900
|
+
Useful for building complex UIs or doing comprehensive data analysis.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
A list of LollmsMessage objects, representing all messages in the discussion.
|
|
904
|
+
"""
|
|
905
|
+
self._rebuild_message_index() # Ensure index is fresh
|
|
906
|
+
return [LollmsMessage(self, msg_obj) for msg_obj in self._message_index.values()]
|
|
907
|
+
|
|
777
908
|
|
|
778
909
|
def get_full_data_zone(self):
|
|
779
910
|
"""Assembles all data zones into a single, formatted string for the prompt."""
|
|
@@ -803,6 +934,7 @@ class LollmsDiscussion:
|
|
|
803
934
|
max_reasoning_steps: int = 20,
|
|
804
935
|
images: Optional[List[str]] = None,
|
|
805
936
|
debug: bool = False,
|
|
937
|
+
remove_thinking_blocks:bool = True,
|
|
806
938
|
**kwargs
|
|
807
939
|
) -> Dict[str, 'LollmsMessage']:
|
|
808
940
|
"""Main interaction method that can invoke the dynamic, multi-modal agent.
|
|
@@ -832,6 +964,11 @@ class LollmsDiscussion:
|
|
|
832
964
|
images: A list of base64-encoded images provided by the user, which will
|
|
833
965
|
be passed to the agent or a multi-modal LLM.
|
|
834
966
|
debug: If True, prints full prompts and raw AI responses to the console.
|
|
967
|
+
remove_thinking_blocks: If True, removes any thinking blocks from the final
|
|
968
|
+
response content, cleaning it up for user display.
|
|
969
|
+
branch_tip_id: If provided, this is the ID of the message to use as the
|
|
970
|
+
starting point for the branch. If None, uses the current
|
|
971
|
+
active branch tip.
|
|
835
972
|
**kwargs: Additional keyword arguments passed to the underlying generation
|
|
836
973
|
methods, such as 'streaming_callback'.
|
|
837
974
|
|
|
@@ -851,7 +988,7 @@ class LollmsDiscussion:
|
|
|
851
988
|
if callback:
|
|
852
989
|
callback("Loading static personality data...", MSG_TYPE.MSG_TYPE_STEP, {"id": "static_data_loading"})
|
|
853
990
|
if personality.data_source:
|
|
854
|
-
self.personality_data_zone = personality.
|
|
991
|
+
self.personality_data_zone = personality.data_zone.strip()
|
|
855
992
|
|
|
856
993
|
elif callable(personality.data_source):
|
|
857
994
|
# --- Dynamic Data Source ---
|
|
@@ -927,8 +1064,10 @@ class LollmsDiscussion:
|
|
|
927
1064
|
**kwargs
|
|
928
1065
|
)
|
|
929
1066
|
else: # Regeneration logic
|
|
930
|
-
|
|
931
|
-
|
|
1067
|
+
# _validate_and_set_active_branch ensures active_branch_id is valid and a leaf.
|
|
1068
|
+
# So, if we are regenerating, active_branch_id must be valid.
|
|
1069
|
+
if self.active_branch_id not in self._message_index: # Redundant check, but safe
|
|
1070
|
+
raise ValueError("Regeneration failed: active branch tip not found or is invalid.")
|
|
932
1071
|
user_msg_orm = self._message_index[self.active_branch_id]
|
|
933
1072
|
if user_msg_orm.sender_type != 'user':
|
|
934
1073
|
raise ValueError(f"Regeneration failed: active branch tip is a '{user_msg_orm.sender_type}' message, not 'user'.")
|
|
@@ -975,7 +1114,10 @@ class LollmsDiscussion:
|
|
|
975
1114
|
if isinstance(final_raw_response, dict) and final_raw_response.get("status") == "error":
|
|
976
1115
|
raise Exception(final_raw_response.get("message", "Unknown error from lollmsClient.chat"))
|
|
977
1116
|
else:
|
|
978
|
-
|
|
1117
|
+
if remove_thinking_blocks:
|
|
1118
|
+
final_content = self.lollmsClient.remove_thinking_blocks(final_raw_response)
|
|
1119
|
+
else:
|
|
1120
|
+
final_content = final_raw_response
|
|
979
1121
|
final_scratchpad = None
|
|
980
1122
|
|
|
981
1123
|
end_time = datetime.now()
|
|
@@ -998,6 +1140,8 @@ class LollmsDiscussion:
|
|
|
998
1140
|
tokens=token_count,
|
|
999
1141
|
generation_speed=tok_per_sec,
|
|
1000
1142
|
parent_id=user_msg.id,
|
|
1143
|
+
model_name = self.lollmsClient.llm.model_name,
|
|
1144
|
+
binding_name = self.lollmsClient.llm.binding_name,
|
|
1001
1145
|
metadata=message_meta
|
|
1002
1146
|
)
|
|
1003
1147
|
|
|
@@ -1006,55 +1150,76 @@ class LollmsDiscussion:
|
|
|
1006
1150
|
|
|
1007
1151
|
return {"user_message": user_msg, "ai_message": ai_message_obj}
|
|
1008
1152
|
|
|
1009
|
-
def regenerate_branch(self, branch_tip_id=None, **kwargs) -> Dict[str, 'LollmsMessage']:
|
|
1010
|
-
"""Regenerates the
|
|
1153
|
+
def regenerate_branch(self, branch_tip_id: Optional[str] = None, **kwargs) -> Dict[str, 'LollmsMessage']:
|
|
1154
|
+
"""Regenerates the AI response for a given message or the active branch's AI response.
|
|
1011
1155
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1156
|
+
If the target is an AI message, it's deleted and its children are re-parented to its parent
|
|
1157
|
+
(the user message). A new AI response is then generated from that user message.
|
|
1158
|
+
If the target is a user message, all its existing AI children are deleted, and their
|
|
1159
|
+
descendants are re-parented to the user message. A new AI response is then generated.
|
|
1014
1160
|
|
|
1015
1161
|
Args:
|
|
1162
|
+
branch_tip_id (Optional[str]): The ID of the message to regenerate from.
|
|
1163
|
+
If None, the currently active branch tip is used.
|
|
1016
1164
|
**kwargs: Additional arguments for the chat() method.
|
|
1017
1165
|
|
|
1018
1166
|
Returns:
|
|
1019
1167
|
A dictionary with the user and the newly generated AI message.
|
|
1020
1168
|
"""
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
else:
|
|
1030
|
-
raise ValueError("No active message to regenerate from.")
|
|
1169
|
+
self._rebuild_message_index() # Ensure index is fresh before operations
|
|
1170
|
+
|
|
1171
|
+
target_id = branch_tip_id if branch_tip_id is not None else self.active_branch_id
|
|
1172
|
+
|
|
1173
|
+
if not target_id or target_id not in self._message_index:
|
|
1174
|
+
raise ValueError("Regeneration failed: Target message ID not found or discussion is empty.")
|
|
1175
|
+
|
|
1176
|
+
target_message_orm = self._message_index[target_id]
|
|
1031
1177
|
|
|
1032
|
-
|
|
1178
|
+
# Determine the user message ID that will be the parent for the new AI generation
|
|
1179
|
+
if target_message_orm.sender_type == 'assistant':
|
|
1180
|
+
user_parent_id = target_message_orm.parent_id
|
|
1181
|
+
if user_parent_id is None or user_parent_id not in self._message_index:
|
|
1182
|
+
raise ValueError("Regeneration failed: Assistant message has no valid user parent to regenerate from.")
|
|
1183
|
+
user_msg_to_regenerate_from = self._message_index[user_parent_id]
|
|
1184
|
+
elif target_message_orm.sender_type == 'user':
|
|
1185
|
+
user_msg_to_regenerate_from = target_message_orm
|
|
1186
|
+
user_parent_id = user_msg_to_regenerate_from.id
|
|
1187
|
+
else:
|
|
1188
|
+
raise ValueError(f"Regeneration failed: Target message '{target_id}' is of an unexpected sender type '{target_message_orm.sender_type}'.")
|
|
1189
|
+
|
|
1190
|
+
ai_messages_to_overwrite_ids = set()
|
|
1191
|
+
if target_message_orm.sender_type == 'assistant':
|
|
1192
|
+
# If target is an AI, we only remove this specific AI message.
|
|
1193
|
+
ai_messages_to_overwrite_ids.add(target_message_orm.id)
|
|
1194
|
+
elif target_message_orm.sender_type == 'user':
|
|
1195
|
+
# If target is a user, we remove ALL AI children of this user message.
|
|
1196
|
+
for msg_obj in self._db_discussion.messages:
|
|
1197
|
+
if msg_obj.parent_id == user_msg_to_regenerate_from.id and msg_obj.sender_type == 'assistant':
|
|
1198
|
+
ai_messages_to_overwrite_ids.add(msg_obj.id)
|
|
1033
1199
|
|
|
1034
|
-
if
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1200
|
+
if not ai_messages_to_overwrite_ids:
|
|
1201
|
+
ASCIIColors.warning(f"No AI messages found to regenerate from '{target_id}'. This might be unintended.")
|
|
1202
|
+
# If no AI messages to overwrite, just proceed with generation from user message.
|
|
1203
|
+
# No changes to existing messages needed, so skip the cleanup phase.
|
|
1204
|
+
self.active_branch_id = user_msg_to_regenerate_from.id # Ensure active branch is correct for chat
|
|
1205
|
+
return self.chat(user_message="", add_user_message=False, branch_tip_id=user_msg_to_regenerate_from.id, **kwargs)
|
|
1206
|
+
|
|
1207
|
+
# --- Phase 1: Generate new AI response ---
|
|
1208
|
+
# The user message for the new generation is user_msg_to_regenerate_from
|
|
1209
|
+
self.active_branch_id = user_msg_to_regenerate_from.id
|
|
1210
|
+
|
|
1211
|
+
# Call chat with add_user_message=False as the user message already exists (or was just found)
|
|
1212
|
+
# The chat method's add_message will set the new AI message as the active_branch_id.
|
|
1213
|
+
return self.chat(user_message="", add_user_message=False, branch_tip_id=user_msg_to_regenerate_from.id, **kwargs)
|
|
1048
1214
|
|
|
1049
|
-
# If the last message is a user message, we can just call chat on it
|
|
1050
|
-
return self.chat(user_message="", add_user_message=False, branch_tip_id=branch_tip_id, **kwargs)
|
|
1051
|
-
|
|
1052
1215
|
def delete_branch(self, message_id: str):
|
|
1053
1216
|
"""Deletes a message and its entire descendant branch.
|
|
1054
1217
|
|
|
1055
1218
|
This method removes the specified message and any messages that have it
|
|
1056
1219
|
as a parent or an ancestor. After deletion, the active branch is moved
|
|
1057
|
-
to the parent of the deleted message.
|
|
1220
|
+
to the parent of the deleted message. If the parent doesn't exist or is also
|
|
1221
|
+
deleted, it finds the most recent remaining message. Crucially, it re-parented
|
|
1222
|
+
children of the deleted message to the parent of the deleted message.
|
|
1058
1223
|
|
|
1059
1224
|
This operation is only supported for database-backed discussions.
|
|
1060
1225
|
|
|
@@ -1068,51 +1233,77 @@ class LollmsDiscussion:
|
|
|
1068
1233
|
if not self._is_db_backed:
|
|
1069
1234
|
raise NotImplementedError("Branch deletion is only supported for database-backed discussions.")
|
|
1070
1235
|
|
|
1236
|
+
# Ensure message index is up-to-date with current DB state
|
|
1237
|
+
self._rebuild_message_index()
|
|
1238
|
+
|
|
1071
1239
|
if message_id not in self._message_index:
|
|
1072
1240
|
raise ValueError(f"Message with ID '{message_id}' not found in the discussion.")
|
|
1073
1241
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1242
|
+
original_message_obj = self._message_index[message_id]
|
|
1243
|
+
new_parent_id_for_children = original_message_obj.parent_id
|
|
1244
|
+
|
|
1245
|
+
# Identify direct children of the message being deleted (before removal from _db_discussion.messages)
|
|
1246
|
+
children_of_deleted_message_ids = [
|
|
1247
|
+
msg_obj.id for msg_obj in self._db_discussion.messages
|
|
1248
|
+
if msg_obj.parent_id == message_id and msg_obj.id != message_id
|
|
1249
|
+
]
|
|
1078
1250
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1251
|
+
# Identify all messages to delete (including the one specified and its descendants)
|
|
1252
|
+
messages_to_delete_ids = set()
|
|
1253
|
+
queue = [message_id]
|
|
1254
|
+
processed_queue_idx = 0
|
|
1255
|
+
while processed_queue_idx < len(queue):
|
|
1256
|
+
current_msg_id = queue[processed_queue_idx]
|
|
1257
|
+
processed_queue_idx += 1
|
|
1083
1258
|
|
|
1084
|
-
messages_to_delete_ids
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1259
|
+
if current_msg_id in messages_to_delete_ids:
|
|
1260
|
+
continue
|
|
1261
|
+
|
|
1262
|
+
messages_to_delete_ids.add(current_msg_id)
|
|
1263
|
+
|
|
1264
|
+
# Find children of current_msg_id from the current _message_index
|
|
1265
|
+
for msg_in_index_id, msg_in_index_obj in self._message_index.items():
|
|
1266
|
+
if msg_in_index_obj.parent_id == current_msg_id and msg_in_index_id not in messages_to_delete_ids:
|
|
1267
|
+
queue.append(msg_in_index_id)
|
|
1268
|
+
|
|
1269
|
+
# Re-parent children of the deleted message to its parent BEFORE removing messages from _db_discussion.messages
|
|
1270
|
+
reparented_children_count = 0
|
|
1271
|
+
for msg_obj in self._db_discussion.messages:
|
|
1272
|
+
if msg_obj.id in children_of_deleted_message_ids:
|
|
1273
|
+
msg_obj.parent_id = new_parent_id_for_children
|
|
1274
|
+
reparented_children_count += 1
|
|
1089
1275
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
new_active_branch_id = original_message_orm.parent_id
|
|
1276
|
+
if reparented_children_count > 0:
|
|
1277
|
+
ASCIIColors.info(f"Re-parented {reparented_children_count} children from deleted message '{message_id}' to '{new_parent_id_for_children}'.")
|
|
1093
1278
|
|
|
1094
|
-
#
|
|
1095
|
-
# Remove from the ORM object's list
|
|
1279
|
+
# Update the ORM's in-memory list of messages and mark for actual DB deletion
|
|
1096
1280
|
self._db_discussion.messages = [
|
|
1097
|
-
|
|
1281
|
+
msg_obj for msg_obj in self._db_discussion.messages if msg_obj.id not in messages_to_delete_ids
|
|
1098
1282
|
]
|
|
1099
|
-
|
|
1100
|
-
# Remove from the quick-access index
|
|
1101
|
-
for mid in messages_to_delete_ids:
|
|
1102
|
-
if mid in self._message_index:
|
|
1103
|
-
del self._message_index[mid]
|
|
1104
|
-
|
|
1105
|
-
# Add to the set of messages to be deleted from the DB on next commit
|
|
1106
1283
|
self._messages_to_delete_from_db.update(messages_to_delete_ids)
|
|
1107
1284
|
|
|
1108
|
-
#
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1285
|
+
# Update the internal message index to reflect in-memory changes immediately
|
|
1286
|
+
temp_message_index = {}
|
|
1287
|
+
for mid, mobj in self._message_index.items():
|
|
1288
|
+
if mid not in messages_to_delete_ids:
|
|
1289
|
+
temp_message_index[mid] = mobj
|
|
1290
|
+
object.__setattr__(self, '_message_index', temp_message_index)
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
# Determine the new active_branch_id by finding the most recent leaf
|
|
1294
|
+
# We first try to find a leaf descendant from the `new_parent_id_for_children` path.
|
|
1295
|
+
# If that path does not exist or has no leaves, then we search the entire discussion for the most recent leaf.
|
|
1296
|
+
new_active_id = None
|
|
1297
|
+
if new_parent_id_for_children and new_parent_id_for_children in self._message_index:
|
|
1298
|
+
new_active_id = self._find_deepest_leaf(new_parent_id_for_children)
|
|
1112
1299
|
|
|
1113
|
-
|
|
1300
|
+
if new_active_id is None: # Fallback if direct re-parenting path doesn't yield a leaf
|
|
1301
|
+
new_active_id = self._find_deepest_leaf(None) # Find most recent leaf in the entire remaining discussion
|
|
1114
1302
|
|
|
1115
|
-
|
|
1303
|
+
self.active_branch_id = new_active_id
|
|
1304
|
+
|
|
1305
|
+
self.touch() # Mark for update and auto-save if configured
|
|
1306
|
+
print(f"Branch starting at {message_id} ({len(messages_to_delete_ids)} messages) removed. New active branch: {self.active_branch_id}")
|
|
1116
1307
|
|
|
1117
1308
|
def export(self, format_type: str, branch_tip_id: Optional[str] = None, max_allowed_tokens: Optional[int] = None) -> Union[List[Dict], str]:
|
|
1118
1309
|
"""Exports the discussion history into a specified format.
|
|
@@ -1604,7 +1795,7 @@ class LollmsDiscussion:
|
|
|
1604
1795
|
content += f"\n({len(active_images)} image(s) attached)"
|
|
1605
1796
|
# Count image tokens
|
|
1606
1797
|
for i, image_b64 in enumerate(active_images):
|
|
1607
|
-
tokens =
|
|
1798
|
+
tokens = self.lollmsClient.count_image_tokens(image_b64) # Use self.lollmsClient.count_image_tokens
|
|
1608
1799
|
if tokens > 0:
|
|
1609
1800
|
total_image_tokens += tokens
|
|
1610
1801
|
image_details_list.append({"message_id": msg.id, "index": i, "tokens": tokens})
|
|
@@ -1633,7 +1824,7 @@ class LollmsDiscussion:
|
|
|
1633
1824
|
img for i, img in enumerate(self.images or [])
|
|
1634
1825
|
if i < len(self.active_images or []) and self.active_images[i]
|
|
1635
1826
|
]
|
|
1636
|
-
discussion_image_tokens = sum(
|
|
1827
|
+
discussion_image_tokens = sum(self.lollmsClient.count_image_tokens(img) for img in active_discussion_b64) # Use self.lollmsClient.count_image_tokens
|
|
1637
1828
|
|
|
1638
1829
|
# Add a new zone for discussion images for clarity
|
|
1639
1830
|
if discussion_image_tokens > 0:
|
|
@@ -1716,8 +1907,39 @@ class LollmsDiscussion:
|
|
|
1716
1907
|
|
|
1717
1908
|
return active_discussion_images
|
|
1718
1909
|
|
|
1719
|
-
def switch_to_branch(self, branch_id):
|
|
1720
|
-
|
|
1910
|
+
def switch_to_branch(self, branch_id: str):
|
|
1911
|
+
"""
|
|
1912
|
+
Switches the active discussion branch to the specified message ID.
|
|
1913
|
+
It then finds the deepest leaf descendant of that message and sets it as the active branch.
|
|
1914
|
+
"""
|
|
1915
|
+
if branch_id not in self._message_index:
|
|
1916
|
+
ASCIIColors.warning(f"Attempted to switch to non-existent branch ID: {branch_id}. No action taken.")
|
|
1917
|
+
return
|
|
1918
|
+
|
|
1919
|
+
# Find the deepest leaf in the branch starting from the provided branch_id
|
|
1920
|
+
# This ensures the active_branch_id is always a leaf
|
|
1921
|
+
new_active_leaf_id = self._find_deepest_leaf(branch_id)
|
|
1922
|
+
|
|
1923
|
+
if new_active_leaf_id:
|
|
1924
|
+
self.active_branch_id = new_active_leaf_id
|
|
1925
|
+
self.touch() # Mark for saving if autosave is on
|
|
1926
|
+
ASCIIColors.info(f"Switched active branch to leaf: {self.active_branch_id}.")
|
|
1927
|
+
else:
|
|
1928
|
+
# Fallback: If no deeper leaf is found (e.g., branch_id is already a leaf or has no valid descendants)
|
|
1929
|
+
# then set active_branch_id to the provided branch_id.
|
|
1930
|
+
# _find_deepest_leaf handles returning start_id if it's a leaf.
|
|
1931
|
+
# So, if new_active_leaf_id is None, it means the provided branch_id was either not found (already checked)
|
|
1932
|
+
# or it's a non-leaf with no valid leaf descendants. In that edge case, we'll still try to use it.
|
|
1933
|
+
# Re-confirming it is present in the index:
|
|
1934
|
+
if branch_id in self._message_index:
|
|
1935
|
+
self.active_branch_id = branch_id
|
|
1936
|
+
self.touch()
|
|
1937
|
+
ASCIIColors.warning(f"Could not find a deeper leaf from branch ID {branch_id}. Active branch set to {branch_id}.")
|
|
1938
|
+
else:
|
|
1939
|
+
self.active_branch_id = None
|
|
1940
|
+
self.touch()
|
|
1941
|
+
ASCIIColors.error(f"Failed to set active branch: provided ID {branch_id} is invalid and no leaf could be found.")
|
|
1942
|
+
|
|
1721
1943
|
|
|
1722
1944
|
def auto_title(self):
|
|
1723
1945
|
try:
|
|
@@ -1837,7 +2059,138 @@ class LollmsDiscussion:
|
|
|
1837
2059
|
del self.active_images[index]
|
|
1838
2060
|
self.touch()
|
|
1839
2061
|
|
|
2062
|
+
def fix_orphan_messages(self):
|
|
2063
|
+
"""
|
|
2064
|
+
Detects and re-chains orphan messages or branches in the discussion.
|
|
2065
|
+
|
|
2066
|
+
An "orphan message" is one whose parent_id points to a message that
|
|
2067
|
+
does not exist in the discussion, or whose lineage cannot be traced
|
|
2068
|
+
back to a root message (parent_id is None) within the current discussion.
|
|
2069
|
+
|
|
2070
|
+
This method attempts to reconnect such messages by:
|
|
2071
|
+
1. Identifying all messages and their parent-child relationships.
|
|
2072
|
+
2. Finding the true root message(s) of the discussion.
|
|
2073
|
+
3. Identifying all reachable messages from the true root(s).
|
|
2074
|
+
4. Any message not reachable is considered an orphan.
|
|
2075
|
+
5. For each orphan, traces up to find its "orphan branch top"
|
|
2076
|
+
(the highest message in its disconnected chain).
|
|
2077
|
+
6. Re-parents these orphan branch tops to the main discussion's
|
|
2078
|
+
primary root (the oldest root message). If no primary root exists,
|
|
2079
|
+
the oldest orphan branch top becomes the new primary root.
|
|
2080
|
+
"""
|
|
2081
|
+
ASCIIColors.info(f"Checking discussion {self.id} for orphan messages...")
|
|
2082
|
+
|
|
2083
|
+
self._rebuild_message_index() # Ensure the index is fresh
|
|
2084
|
+
|
|
2085
|
+
all_messages_orms = list(self._message_index.values())
|
|
2086
|
+
if not all_messages_orms:
|
|
2087
|
+
ASCIIColors.yellow("No messages in discussion. Nothing to fix.")
|
|
2088
|
+
return
|
|
2089
|
+
|
|
2090
|
+
message_map = {msg_orm.id: msg_orm for msg_orm in all_messages_orms}
|
|
2091
|
+
|
|
2092
|
+
# 1. Identify all true root messages (parent_id is None)
|
|
2093
|
+
# And also build a children map for efficient traversal
|
|
2094
|
+
root_messages = []
|
|
2095
|
+
children_map = {msg_id: [] for msg_id in message_map.keys()}
|
|
2096
|
+
|
|
2097
|
+
for msg_orm in all_messages_orms:
|
|
2098
|
+
if msg_orm.parent_id is None:
|
|
2099
|
+
root_messages.append(msg_orm)
|
|
2100
|
+
elif msg_orm.parent_id in message_map:
|
|
2101
|
+
# Only add to children map if the parent actually exists in this discussion
|
|
2102
|
+
children_map[msg_orm.parent_id].append(msg_orm.id)
|
|
2103
|
+
|
|
2104
|
+
# Sort roots by creation time to find the 'primary' root
|
|
2105
|
+
root_messages.sort(key=lambda msg: msg.created_at)
|
|
2106
|
+
primary_root = root_messages[0] if root_messages else None
|
|
2107
|
+
|
|
2108
|
+
if primary_root:
|
|
2109
|
+
ASCIIColors.info(f"Primary discussion root identified: {primary_root.id} (created at {primary_root.created_at})")
|
|
2110
|
+
else:
|
|
2111
|
+
ASCIIColors.warning("No root message found in discussion initially.")
|
|
2112
|
+
|
|
2113
|
+
# 2. Find all messages reachable from the primary root (or any identified root)
|
|
2114
|
+
reachable_messages = set()
|
|
2115
|
+
queue = []
|
|
2116
|
+
|
|
2117
|
+
# Add any current roots as starting points for reachability check.
|
|
2118
|
+
for root in root_messages:
|
|
2119
|
+
if root.id not in reachable_messages: # Avoid re-processing if multiple paths lead to same root
|
|
2120
|
+
queue.append(root.id)
|
|
2121
|
+
reachable_messages.add(root.id)
|
|
2122
|
+
|
|
2123
|
+
head = 0
|
|
2124
|
+
while head < len(queue):
|
|
2125
|
+
current_msg_id = queue[head]
|
|
2126
|
+
head += 1
|
|
2127
|
+
|
|
2128
|
+
for child_id in children_map.get(current_msg_id, []):
|
|
2129
|
+
if child_id not in reachable_messages:
|
|
2130
|
+
reachable_messages.add(child_id)
|
|
2131
|
+
queue.append(child_id)
|
|
2132
|
+
|
|
2133
|
+
# 3. Identify orphan messages (those not reachable from any current root)
|
|
2134
|
+
orphan_messages_ids = set(message_map.keys()) - reachable_messages
|
|
2135
|
+
|
|
2136
|
+
if not orphan_messages_ids:
|
|
2137
|
+
ASCIIColors.success("No orphan messages found. Discussion chain is healthy.")
|
|
2138
|
+
return
|
|
2139
|
+
|
|
2140
|
+
ASCIIColors.warning(f"Found {len(orphan_messages_ids)} orphan message(s). Attempting to fix...")
|
|
2141
|
+
|
|
2142
|
+
# 4. Find the "top" message of each orphan branch
|
|
2143
|
+
orphan_branch_tops = set()
|
|
2144
|
+
for orphan_id in orphan_messages_ids:
|
|
2145
|
+
current_id = orphan_id
|
|
2146
|
+
# Trace upwards until parent is None or parent is NOT an orphan
|
|
2147
|
+
# We must use message_map for lookup as parent might be deleted from _message_index
|
|
2148
|
+
while message_map[current_id].parent_id is not None and message_map[current_id].parent_id in orphan_messages_ids:
|
|
2149
|
+
current_id = message_map[current_id].parent_id
|
|
2150
|
+
orphan_branch_tops.add(current_id)
|
|
2151
|
+
|
|
2152
|
+
# Sort orphan branch tops by creation time, oldest first
|
|
2153
|
+
sorted_orphan_tops_orms = sorted(
|
|
2154
|
+
[message_map[top_id] for top_id in orphan_branch_tops],
|
|
2155
|
+
key=lambda msg: msg.created_at
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
reparented_count = 0
|
|
2159
|
+
|
|
2160
|
+
if not primary_root:
|
|
2161
|
+
# If there was no primary root, make the oldest orphan top the new primary root
|
|
2162
|
+
if sorted_orphan_tops_orms:
|
|
2163
|
+
new_primary_root_orm = sorted_orphan_tops_orms[0]
|
|
2164
|
+
new_primary_root_orm.parent_id = None # Make it a root
|
|
2165
|
+
ASCIIColors.success(f"Discussion had no root. Set oldest orphan top '{new_primary_root_orm.id}' as new primary root.")
|
|
2166
|
+
primary_root = new_primary_root_orm
|
|
2167
|
+
reparented_count += 1
|
|
2168
|
+
# Remove this one from the list of tops to be reparented
|
|
2169
|
+
sorted_orphan_tops_orms = sorted_orphan_tops_orms[1:]
|
|
2170
|
+
else:
|
|
2171
|
+
ASCIIColors.warning("No orphan branch tops found to create a new root. Discussion remains empty or unrooted.")
|
|
2172
|
+
return
|
|
2173
|
+
|
|
2174
|
+
if primary_root:
|
|
2175
|
+
# Re-parent all remaining orphan branch tops to the primary root
|
|
2176
|
+
for orphan_top_orm in sorted_orphan_tops_orms:
|
|
2177
|
+
if orphan_top_orm.id != primary_root.id: # Ensure not reparenting the primary root to itself
|
|
2178
|
+
old_parent = orphan_top_orm.parent_id # Store old parent for logging
|
|
2179
|
+
orphan_top_orm.parent_id = primary_root.id
|
|
2180
|
+
ASCIIColors.info(f"Re-parented orphan branch top '{orphan_top_orm.id}' (was child of '{old_parent}') to primary root '{primary_root.id}'.")
|
|
2181
|
+
reparented_count += 1
|
|
2182
|
+
|
|
2183
|
+
if reparented_count > 0:
|
|
2184
|
+
ASCIIColors.success(f"Successfully re-parented {reparented_count} orphan message(s)/branches.")
|
|
2185
|
+
self.touch() # Mark discussion as updated
|
|
2186
|
+
self.commit() # Commit changes to DB
|
|
2187
|
+
self._rebuild_message_index() # Rebuild index after changes
|
|
2188
|
+
self._validate_and_set_active_branch() # Re-validate active branch after fixing orphans
|
|
2189
|
+
else:
|
|
2190
|
+
ASCIIColors.yellow("No new messages were re-parented (they might have already been roots or discussion was already healthy).")
|
|
2191
|
+
|
|
2192
|
+
|
|
1840
2193
|
@property
|
|
1841
2194
|
def system_prompt(self) -> str:
|
|
1842
2195
|
"""Returns the system prompt for this discussion."""
|
|
1843
|
-
return self._system_prompt
|
|
2196
|
+
return self._system_prompt
|