lollms-client 0.29.3__py3-none-any.whl → 0.31.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 CHANGED
@@ -8,7 +8,7 @@ from lollms_client.lollms_utilities import PromptReshaper # Keep general utiliti
8
8
  from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
9
9
  from lollms_client.lollms_llm_binding import LollmsLLMBindingManager
10
10
 
11
- __version__ = "0.29.3" # Updated version
11
+ __version__ = "0.31.0" # Updated version
12
12
 
13
13
  # Optionally, you could define __all__ if you want to be explicit about exports
14
14
  __all__ = [
@@ -11,6 +11,7 @@ from typing import Optional, Callable, List, Union, Dict
11
11
 
12
12
  from ascii_colors import ASCIIColors, trace_exception
13
13
  import pipmaster as pm
14
+ from lollms_client.lollms_utilities import ImageTokenizer
14
15
  pm.ensure_packages(["ollama","pillow","tiktoken"])
15
16
 
16
17
 
@@ -468,6 +469,24 @@ class OllamaBinding(LollmsLLMBinding):
468
469
  return -1
469
470
  #return count_tokens_ollama(text, self.model_name, self.ollama_client)
470
471
  return len(self.tokenize(text))
472
+
473
+ def count_image_tokens(self, image: str) -> int:
474
+ """
475
+ Estimate the number of tokens for an image using ImageTokenizer based on self.model_name.
476
+
477
+ Args:
478
+ image (str): Image to count tokens from. Either base64 string, path to image file, or URL.
479
+
480
+ Returns:
481
+ int: Estimated number of tokens for the image. Returns -1 on error.
482
+ """
483
+ try:
484
+ # Delegate token counting to ImageTokenizer
485
+ return ImageTokenizer(self.model_name).count_image_tokens(image)
486
+ except Exception as e:
487
+ ASCIIColors.warning(f"Could not estimate image tokens: {e}")
488
+ return -1
489
+
471
490
  def embed(self, text: str, **kwargs) -> List[float]:
472
491
  """
473
492
  Get embeddings for the input text using Ollama API.
@@ -430,7 +430,21 @@ class LollmsClient():
430
430
  if self.binding:
431
431
  return self.binding.count_tokens(text)
432
432
  raise RuntimeError("LLM binding not initialized.")
433
-
433
+
434
+ def count_image_tokens(self, image: str) -> int:
435
+ """
436
+ Estimate the number of tokens for an image using ImageTokenizer based on self.model_name.
437
+
438
+ Args:
439
+ image (str): Image to count tokens from. Either base64 string, path to image file, or URL.
440
+
441
+ Returns:
442
+ int: Estimated number of tokens for the image. Returns -1 on error.
443
+ """
444
+ if self.binding:
445
+ return self.binding.count_image_tokens(image)
446
+ raise RuntimeError("LLM binding not initialized.")
447
+
434
448
  def get_model_details(self) -> dict:
435
449
  """
436
450
  Get model information from the active LLM binding.
@@ -1574,25 +1588,25 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1574
1588
 
1575
1589
  # Add the new put_code_in_buffer tool definition
1576
1590
  available_tools.append({
1577
- "name": "put_code_in_buffer",
1591
+ "name": "local_tools::put_code_in_buffer",
1578
1592
  "description": """Generates and stores code into a buffer to be used by another tool. You can put the uuid of the generated code into the fields that require long code among the tools. If no tool requires code as input do not use put_code_in_buffer. put_code_in_buffer do not execute the code nor does it audit it.""",
1579
1593
  "input_schema": {"type": "object", "properties": {"prompt": {"type": "string", "description": "A detailed natural language description of the code's purpose and requirements."}, "language": {"type": "string", "description": "The programming language of the generated code. By default it uses python."}}, "required": ["prompt"]}
1580
1594
  })
1581
1595
  available_tools.append({
1582
- "name": "view_generated_code",
1596
+ "name": "local_tools::view_generated_code",
1583
1597
  "description": """Views the code that was generated and stored to the buffer. You need to have a valid uuid of the generated code.""",
1584
1598
  "input_schema": {"type": "object", "properties": {"code_id": {"type": "string", "description": "The case sensitive uuid of the generated code."}}, "required": ["uuid"]}
1585
1599
  })
1586
1600
  # Add the new refactor_scratchpad tool definition
1587
1601
  available_tools.append({
1588
- "name": "refactor_scratchpad",
1602
+ "name": "local_tools::refactor_scratchpad",
1589
1603
  "description": "Rewrites the scratchpad content to clean it and reorganize it. Only use if the scratchpad is messy or contains too much information compared to what you need.",
1590
1604
  "input_schema": {"type": "object", "properties": {}}
1591
1605
  })
1592
1606
 
1593
1607
  formatted_tools_list = "\n".join([f"**{t['name']}**:\n{t['description']}\ninput schema:\n{json.dumps(t['input_schema'])}" for t in available_tools])
1594
- formatted_tools_list += "\n**request_clarification**:\nUse if the user's request is ambiguous and you can not infer a clear idea of his intent. this tool has no parameters."
1595
- formatted_tools_list += "\n**final_answer**:\nUse when you are ready to respond to the user. this tool has no parameters."
1608
+ formatted_tools_list += "\n**local_tools::request_clarification**:\nUse if the user's request is ambiguous and you can not infer a clear idea of his intent. this tool has no parameters."
1609
+ formatted_tools_list += "\n**local_tools::final_answer**:\nUse when you are ready to respond to the user. this tool has no parameters."
1596
1610
 
1597
1611
  if discovery_step_id: log_event(f"**Discovering tools** found {len(available_tools)} tools",MSG_TYPE.MSG_TYPE_STEP_END, event_id=discovery_step_id)
1598
1612
 
@@ -1618,15 +1632,16 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1618
1632
  - Does the latest observation completely fulfill the user's original request?
1619
1633
  - If YES, your next action MUST be to use the `final_answer` tool.
1620
1634
  - If NO, what is the single next logical step needed? This may involve writing code first with `put_code_in_buffer`, then using another tool.
1621
- - If you are stuck or the request is ambiguous, use `request_clarification`.
1635
+ - If you are stuck or the request is ambiguous, use `local_tools::request_clarification`.
1622
1636
  3. **ACT:** Formulate your decision as a JSON object.
1637
+ ** Important ** Always use this format alias::tool_name to call the tool
1623
1638
  """
1624
1639
  action_template = {
1625
1640
  "thought": "My detailed analysis of the last observation and my reasoning for the next action and how it integrates with my global plan.",
1626
1641
  "action": {
1627
- "tool_name": "The single tool to use (e.g., 'put_code_in_buffer', 'time_machine::get_current_time', 'final_answer').",
1642
+ "tool_name": "The single tool to use (e.g., 'local_tools::put_code_in_buffer', 'local_tools::final_answer').",
1628
1643
  "tool_params": {"param1": "value1"},
1629
- "clarification_question": "(string, ONLY if tool_name is 'request_clarification')"
1644
+ "clarification_question": "(string, ONLY if tool_name is 'local_tools::request_clarification')"
1630
1645
  }
1631
1646
  }
1632
1647
  if debug: log_prompt(reasoning_prompt_template, f"REASONING PROMPT (Step {i+1})")
@@ -1664,18 +1679,22 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1664
1679
  break
1665
1680
 
1666
1681
  # --- Handle special, non-executing tools ---
1667
- if tool_name == "request_clarification":
1682
+ if tool_name == "local_tools::request_clarification":
1668
1683
  # Handle clarification...
1669
- return {"final_answer": action.get("clarification_question", "Could you please provide more details?"), "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1670
-
1671
- if tool_name == "final_answer":
1684
+ if isinstance(action, dict):
1685
+ return {"final_answer": action.get("clarification_question", "Could you please provide more details?"), "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1686
+ elif isinstance(action, str):
1687
+ return {"final_answer": action, "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1688
+ else:
1689
+ return {"final_answer": "Could you please provide more details?", "final_scratchpad": current_scratchpad, "tool_calls": tool_calls_this_turn, "sources": sources_this_turn, "clarification_required": True, "error": None}
1690
+ if tool_name == "local_tools::final_answer":
1672
1691
  current_scratchpad += f"\n\n### Step {i+1}: Action\n- **Action:** Decided to formulate the final answer."
1673
1692
  log_event("**Action**: Formulate final answer.", MSG_TYPE.MSG_TYPE_THOUGHT_CHUNK)
1674
1693
  if reasoning_step_id: log_event(f"**Reasoning Step {i+1}/{max_reasoning_steps}**",MSG_TYPE.MSG_TYPE_STEP_END, event_id=reasoning_step_id)
1675
1694
  break
1676
1695
 
1677
1696
  # --- Handle the `put_code_in_buffer` tool specifically ---
1678
- if tool_name == 'put_code_in_buffer':
1697
+ if tool_name == 'local_tools::put_code_in_buffer':
1679
1698
  code_gen_id = log_event(f"Generating code...", MSG_TYPE.MSG_TYPE_STEP_START, metadata={"name": "put_code_in_buffer", "id": "gencode"})
1680
1699
  code_prompt = tool_params.get("prompt", "Generate the requested code.")
1681
1700
 
@@ -1694,7 +1713,7 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1694
1713
  if code_gen_id: log_event(f"Generating code...", MSG_TYPE.MSG_TYPE_TOOL_CALL, metadata={"id": code_gen_id, "result": tool_result})
1695
1714
  if reasoning_step_id: log_event(f"**Reasoning Step {i+1}/{max_reasoning_steps}**", MSG_TYPE.MSG_TYPE_STEP_END, event_id= reasoning_step_id)
1696
1715
  continue # Go to the next reasoning step immediately
1697
- if tool_name == 'view_generated_code':
1716
+ if tool_name == 'local_tools::view_generated_code':
1698
1717
  code_id = tool_params.get("code_id")
1699
1718
  if code_id:
1700
1719
  tool_result = {"status": "success", "code_id": code_id, "generated_code":generated_code_store[code_uuid]}
@@ -1704,7 +1723,7 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1704
1723
  current_scratchpad += f"\n\n### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n- **Result:**\n{observation_text}"
1705
1724
  log_event(f"Result from `{tool_name}`:\n```\n{generated_code_store[code_uuid]}\n```\n", MSG_TYPE.MSG_TYPE_TOOL_CALL, metadata={"id": code_gen_id, "result": tool_result})
1706
1725
  continue
1707
- if tool_name == 'refactor_scratchpad':
1726
+ if tool_name == 'local_tools::refactor_scratchpad':
1708
1727
  scratchpad_cleaning_prompt = f"""Enhance this scratchpad content to be more organized and comprehensive. Keep relevant experience information and remove any useless redundancies. Try to log learned things from the context so that you won't make the same mistakes again. Do not remove the main objective information or any crucial information that may be useful for the next iterations. Answer directly with the new scratchpad content without any comments.
1709
1728
  --- YOUR INTERNAL SCRATCHPAD (Work History & Analysis) ---
1710
1729
  {current_scratchpad}
@@ -2958,12 +2977,12 @@ Provide the final aggregated answer in {output_format} format, directly addressi
2958
2977
  callback("Deep analysis complete.", MSG_TYPE.MSG_TYPE_STEP_END)
2959
2978
  return final_output
2960
2979
 
2961
- def summarize(
2980
+ def long_context_processing(
2962
2981
  self,
2963
- text_to_summarize: str,
2982
+ text_to_process: str,
2964
2983
  contextual_prompt: Optional[str] = None,
2965
- chunk_size_tokens: int = 1500,
2966
- overlap_tokens: int = 250,
2984
+ chunk_size_tokens: int|None = None,
2985
+ overlap_tokens: int = 0,
2967
2986
  streaming_callback: Optional[Callable] = None,
2968
2987
  **kwargs
2969
2988
  ) -> str:
@@ -2975,7 +2994,7 @@ Provide the final aggregated answer in {output_format} format, directly addressi
2975
2994
  2. **Synthesize:** It then takes all the chunk summaries and performs a final summarization pass to create a single, coherent, and comprehensive summary.
2976
2995
 
2977
2996
  Args:
2978
- text_to_summarize (str): The long text content to be summarized.
2997
+ text_to_process (str): The long text content to be summarized.
2979
2998
  contextual_prompt (Optional[str], optional): A specific instruction to guide the summary's focus.
2980
2999
  For example, "Summarize the text focusing on the financial implications."
2981
3000
  Defaults to None.
@@ -2993,25 +3012,40 @@ Provide the final aggregated answer in {output_format} format, directly addressi
2993
3012
  Returns:
2994
3013
  str: The final, comprehensive summary of the text.
2995
3014
  """
2996
- if not text_to_summarize.strip():
3015
+ if not text_to_process and len(kwargs.get("images",[]))==0:
2997
3016
  return ""
2998
-
2999
- # Use the binding's tokenizer for accurate chunking
3000
- tokens = self.binding.tokenize(text_to_summarize)
3017
+ if not text_to_process:
3018
+ text_to_process=""
3019
+ tokens = []
3020
+ else:
3021
+ # Use the binding's tokenizer for accurate chunking
3022
+ tokens = self.binding.tokenize(text_to_process)
3023
+ if chunk_size_tokens is None:
3024
+ chunk_size_tokens = self.default_ctx_size//2
3001
3025
 
3002
3026
  if len(tokens) <= chunk_size_tokens:
3003
3027
  if streaming_callback:
3004
- streaming_callback("Text is short enough for a single summary.", MSG_TYPE.MSG_TYPE_STEP, {"progress": 0})
3028
+ streaming_callback("Text is short enough for a single process.", MSG_TYPE.MSG_TYPE_STEP, {"progress": 0})
3029
+ system_prompt = ("You are a content processor expert.\n"
3030
+ "You perform tasks on the content as requested by the user.\n\n"
3031
+ "--- Content ---\n"
3032
+ f"{text_to_process}\n\n"
3033
+ "** Important **\n"
3034
+ "Strictly adhere to the user prompt.\n"
3035
+ "Do not add comments unless asked to do so.\n"
3036
+ )
3037
+ if "system_prompt" in kwargs:
3038
+ system_prompt += "-- Extra instructions --\n"+ kwargs["system_prompt"] +"\n"
3039
+ del kwargs["system_prompt"]
3040
+ prompt_objective = contextual_prompt or "Provide a comprehensive summary of the content."
3041
+ final_prompt = f"{prompt_objective}"
3005
3042
 
3006
- prompt_objective = contextual_prompt or "Provide a comprehensive summary of the following text."
3007
- final_prompt = f"{prompt_objective}\n\n--- Text to Summarize ---\n{text_to_summarize}"
3008
-
3009
- summary = self.generate_text(final_prompt, **kwargs)
3043
+ processed_output = self.generate_text(final_prompt, system_prompt=system_prompt, **kwargs)
3010
3044
 
3011
3045
  if streaming_callback:
3012
- streaming_callback("Summary generated.", MSG_TYPE.MSG_TYPE_STEP, {"progress": 100})
3046
+ streaming_callback("Content processed.", MSG_TYPE.MSG_TYPE_STEP, {"progress": 100})
3013
3047
 
3014
- return summary
3048
+ return processed_output
3015
3049
 
3016
3050
  # --- Stage 1: Chunking and Independent Summarization ---
3017
3051
  chunks = []
@@ -3028,43 +3062,62 @@ Provide the final aggregated answer in {output_format} format, directly addressi
3028
3062
 
3029
3063
  # Define the prompt for summarizing each chunk
3030
3064
  summarization_objective = contextual_prompt or "Summarize the key points of the following text excerpt."
3031
- chunk_summary_prompt_template = f"{summarization_objective}\n\n--- Text Excerpt ---\n{{chunk_text}}"
3065
+ system_prompt = ("You are a sequential document processing agent.\n"
3066
+ "The process is done in two phases:\n"
3067
+ "** Phase1 : **\n"
3068
+ "Sequencially extracting information from the text chunks and adding them to the scratchpad.\n"
3069
+ "** Phase2: **\n"
3070
+ "Synthesizing a comprehensive Response using the scratchpad content given the objective formatting instructions if applicable.\n"
3071
+ "We are now performing ** Phase 1 **, and we are processing chunk number {{chunk_id}}.\n"
3072
+ "Your job is to extract information from the current chunk given previous chunks extracted information placed in scratchpad as well as the current chunk content.\n"
3073
+ "Add the information to the scratchpad while strictly adhering to the Global objective extraction instructions:\n"
3074
+ "-- Sequencial Scratchpad --\n"
3075
+ "{{scratchpad}}\n"
3076
+ "** Important **\n"
3077
+ "Respond only with the extracted information from the current chunk without repeating things that are already in the scratchpad.\n"
3078
+ "Strictly adhere to the Global objective content for the extraction phase.\n"
3079
+ "Do not add comments.\n"
3080
+ )
3081
+ if "system_prompt" in kwargs:
3082
+ system_prompt += "-- Extra instructions --\n"+ kwargs["system_prompt"] +"\n"
3083
+ del kwargs["system_prompt"]
3084
+ chunk_summary_prompt_template = f"--- Global objective ---\n{summarization_objective}\n\n--- Text Excerpt ---\n{{chunk_text}}"
3032
3085
 
3033
3086
  for i, chunk in enumerate(chunks):
3034
3087
  progress_before = (i / total_steps) * 100
3035
3088
  if streaming_callback:
3036
3089
  streaming_callback(
3037
- f"Summarizing chunk {i + 1} of {len(chunks)}...",
3090
+ f"Processing chunk {i + 1} of {len(chunks)}...",
3038
3091
  MSG_TYPE.MSG_TYPE_STEP_START,
3039
3092
  {"id": f"chunk_{i+1}", "progress": progress_before}
3040
3093
  )
3041
3094
 
3042
3095
  prompt = chunk_summary_prompt_template.format(chunk_text=chunk)
3043
-
3096
+ processed_system_prompt = system_prompt.format(chunk_id=i,scratchpad="\n\n---\n\n".join(chunk_summaries))
3044
3097
  try:
3045
3098
  # Generate summary for the current chunk
3046
- chunk_summary = self.generate_text(prompt, **kwargs)
3099
+ chunk_summary = self.generate_text(prompt, system_prompt=processed_system_prompt, **kwargs)
3047
3100
  chunk_summaries.append(chunk_summary)
3048
3101
 
3049
3102
  progress_after = ((i + 1) / total_steps) * 100
3050
3103
  if streaming_callback:
3051
3104
  streaming_callback(
3052
- f"Chunk {i + 1} summarized. Progress: {progress_after:.0f}%",
3105
+ f"Chunk {i + 1} processed. Progress: {progress_after:.0f}%",
3053
3106
  MSG_TYPE.MSG_TYPE_STEP_END,
3054
- {"id": f"chunk_{i+1}", "summary_snippet": chunk_summary[:100], "progress": progress_after}
3107
+ {"id": f"chunk_{i+1}", "output_snippet": chunk_summary[:100], "progress": progress_after}
3055
3108
  )
3056
3109
  except Exception as e:
3057
3110
  trace_exception(e)
3058
3111
  if streaming_callback:
3059
- streaming_callback(f"Failed to summarize chunk {i+1}: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION)
3112
+ streaming_callback(f"Failed to process chunk {i+1}: {e}", MSG_TYPE.MSG_TYPE_EXCEPTION)
3060
3113
  # Still add a placeholder to not break the chain
3061
- chunk_summaries.append(f"[Error summarizing chunk {i+1}]")
3114
+ chunk_summaries.append(f"[Error processing chunk {i+1}]")
3062
3115
 
3063
3116
  # --- Stage 2: Final Synthesis of All Chunk Summaries ---
3064
3117
  progress_before_synthesis = (len(chunks) / total_steps) * 100
3065
3118
  if streaming_callback:
3066
3119
  streaming_callback(
3067
- "Synthesizing all chunk summaries into a final version...",
3120
+ "Processing the scratchpad content into a final version...",
3068
3121
  MSG_TYPE.MSG_TYPE_STEP_START,
3069
3122
  {"id": "final_synthesis", "progress": progress_before_synthesis}
3070
3123
  )
@@ -3073,16 +3126,29 @@ Provide the final aggregated answer in {output_format} format, directly addressi
3073
3126
 
3074
3127
  # Define the prompt for the final synthesis
3075
3128
  synthesis_objective = contextual_prompt or "Create a single, final, coherent, and comprehensive summary."
3129
+ system_prompt = ("You are a sequential document processing agent.\n"
3130
+ "The process is done in two phases:\n"
3131
+ "** Phase1 : **\n"
3132
+ "Sequencially extracting information from the text chunks and adding them to the scratchpad.\n"
3133
+ "** Phase2: **\n"
3134
+ "Synthesizing a comprehensive Response using the scratchpad content given the objective formatting instructions if applicable.\n"
3135
+ "\n"
3136
+ "We are now performing ** Phase 2 **.\n"
3137
+ "Your job is to use the extracted information to fulfill the user prompt objectives.\n"
3138
+ "Make sure you respect the user formatting if provided and if not, then use markdown output format."
3139
+ "-- Sequencial Scratchpad --\n"
3140
+ f"{combined_summaries}\n"
3141
+ "** Important **\n"
3142
+ "Respond only with the requested task without extra comments unless told to.\n"
3143
+ "Strictly adhere to the Global objective content for the extraction phase.\n"
3144
+ "Do not add comments.\n"
3145
+ )
3076
3146
  final_synthesis_prompt = (
3077
- "You are a master synthesizer. You will be given a series of partial summaries from a long document. "
3078
- f"Your task is to synthesize them into one high-quality summary. {synthesis_objective}\n\n"
3079
- "Please remove any redundancy and ensure a smooth, logical flow.\n\n"
3080
- "--- Collection of Summaries ---\n"
3081
- f"{combined_summaries}\n\n"
3082
- "--- Final Comprehensive Summary ---"
3147
+ f"--- Global objective ---\n{synthesis_objective}\n\n"
3148
+ "--- Final Response ---"
3083
3149
  )
3084
3150
 
3085
- final_summary = self.generate_text(final_synthesis_prompt, **kwargs)
3151
+ final_answer = self.generate_text(final_synthesis_prompt, system_prompt=system_prompt, **kwargs)
3086
3152
 
3087
3153
  if streaming_callback:
3088
3154
  streaming_callback(
@@ -3091,7 +3157,7 @@ Provide the final aggregated answer in {output_format} format, directly addressi
3091
3157
  {"id": "final_synthesis", "progress": 100}
3092
3158
  )
3093
3159
 
3094
- return final_summary.strip()
3160
+ return final_answer.strip()
3095
3161
 
3096
3162
  def chunk_text(text, tokenizer, detokenizer, chunk_size, overlap, use_separators=True):
3097
3163
  """