lollms-client 0.26.0__py3-none-any.whl → 0.27.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.

lollms_client/__init__.py CHANGED
@@ -6,9 +6,9 @@ from lollms_client.lollms_personality import LollmsPersonality
6
6
  from lollms_client.lollms_utilities import PromptReshaper # Keep general utilities
7
7
  # Import new MCP binding classes
8
8
  from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
9
+ from lollms_client.lollms_llm_binding import LollmsLLMBindingManager
9
10
 
10
-
11
- __version__ = "0.26.0" # Updated version
11
+ __version__ = "0.27.1" # Updated version
12
12
 
13
13
  # Optionally, you could define __all__ if you want to be explicit about exports
14
14
  __all__ = [
@@ -21,5 +21,6 @@ __all__ = [
21
21
  "LollmsDataManager",
22
22
  "PromptReshaper",
23
23
  "LollmsMCPBinding", # Export LollmsMCPBinding ABC
24
+ "LollmsLLMBindingManager",
24
25
  "LollmsMCPBindingManager", # Export LollmsMCPBindingManager
25
26
  ]
@@ -329,7 +329,7 @@ class ClaudeBinding(LollmsLLMBinding):
329
329
  # Note: count_tokens doesn't use a system prompt, so it's safe.
330
330
  # However, for consistency, we could add one if needed by the logic.
331
331
  # For now, this is fine as it only counts user content tokens.
332
- response = self.client.count_tokens( # Changed from messages.count_tokens to top-level client method
332
+ response = self.client.messages.count_tokens( # Changed from messages.count_tokens to top-level client method
333
333
  model=self.model_name,
334
334
  messages=[{"role": "user", "content": text}]
335
335
  )
@@ -0,0 +1,536 @@
1
+ import base64
2
+ import os
3
+ import json
4
+ import requests
5
+ from io import BytesIO
6
+ from pathlib import Path
7
+ from typing import Optional, Callable, List, Union, Dict
8
+
9
+ from lollms_client.lollms_discussion import LollmsDiscussion, LollmsMessage
10
+ from lollms_client.lollms_llm_binding import LollmsLLMBinding
11
+ from lollms_client.lollms_types import MSG_TYPE
12
+ from ascii_colors import ASCIIColors, trace_exception
13
+
14
+ import pipmaster as pm
15
+
16
+ # Ensure the required packages are installed
17
+ pm.ensure_packages(["requests", "pillow", "tiktoken"])
18
+
19
+ from PIL import Image, ImageDraw
20
+ import tiktoken
21
+
22
+ BindingName = "GrokBinding"
23
+
24
+ # API Endpoint
25
+ GROK_API_BASE_URL = "https://api.x.ai/v1"
26
+
27
+ # A hardcoded list to be used as a fallback if the API call fails
28
+ _FALLBACK_MODELS = [
29
+ {'model_name': 'grok-1', 'display_name': 'Grok 1', 'description': 'The flagship conversational model from xAI.', 'owned_by': 'xAI'},
30
+ {'model_name': 'grok-1.5', 'display_name': 'Grok 1.5', 'description': 'The latest multimodal model from xAI.', 'owned_by': 'xAI'},
31
+ {'model_name': 'grok-1.5-vision-preview', 'display_name': 'Grok 1.5 Vision (Preview)', 'description': 'Multimodal model with vision capabilities (preview).', 'owned_by': 'xAI'},
32
+ ]
33
+
34
+ # Helper to check if a string is a valid path to an image
35
+ def is_image_path(path_str: str) -> bool:
36
+ try:
37
+ p = Path(path_str)
38
+ return p.is_file() and p.suffix.lower() in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']
39
+ except Exception:
40
+ return False
41
+
42
+ # Helper to get image media type for base64 URI
43
+ def get_media_type_for_uri(image_path: Union[str, Path]) -> str:
44
+ path = Path(image_path)
45
+ ext = path.suffix.lower()
46
+ if ext == ".jpg" or ext == ".jpeg":
47
+ return "image/jpeg"
48
+ elif ext == ".png":
49
+ return "image/png"
50
+ elif ext == ".gif":
51
+ return "image/gif"
52
+ elif ext == ".webp":
53
+ return "image/webp"
54
+ else:
55
+ # Default to PNG as it's lossless and widely supported
56
+ return "image/png"
57
+
58
+
59
+ class GrokBinding(LollmsLLMBinding):
60
+ """xAI Grok-specific binding implementation."""
61
+
62
+ def __init__(self,
63
+ host_address: str = None, # Ignored, for compatibility
64
+ model_name: str = "grok-1.5-vision-preview",
65
+ service_key: str = None,
66
+ verify_ssl_certificate: bool = True, # Ignored, for compatibility
67
+ **kwargs
68
+ ):
69
+ """
70
+ Initialize the Grok binding.
71
+
72
+ Args:
73
+ model_name (str): Name of the Grok model to use.
74
+ service_key (str): xAI API key.
75
+ """
76
+ super().__init__(binding_name=BindingName)
77
+ self.model_name = model_name
78
+ self.service_key = service_key
79
+ self.base_url = kwargs.get("base_url", GROK_API_BASE_URL)
80
+ self._cached_models: Optional[List[Dict[str, str]]] = None
81
+
82
+ if not self.service_key:
83
+ self.service_key = os.getenv("XAI_API_KEY")
84
+
85
+ if not self.service_key:
86
+ raise ValueError("xAI API key is required. Please set it via the 'service_key' parameter or the XAI_API_KEY environment variable.")
87
+
88
+ self.headers = {
89
+ "Authorization": f"Bearer {self.service_key}",
90
+ "Content-Type": "application/json"
91
+ }
92
+
93
+ def _construct_parameters(self,
94
+ temperature: float,
95
+ top_p: float,
96
+ n_predict: int) -> Dict[str, any]:
97
+ """Builds a parameters dictionary for the Grok API."""
98
+ params = {"stream": True} # Always stream from the API
99
+ if temperature is not None: params['temperature'] = float(temperature)
100
+ if top_p is not None: params['top_p'] = top_p
101
+ # Grok has a model-specific max_tokens, but we can request less.
102
+ if n_predict is not None: params['max_tokens'] = n_predict
103
+ return params
104
+
105
+ def _process_and_handle_stream(self,
106
+ response: requests.Response,
107
+ stream: bool,
108
+ streaming_callback: Optional[Callable[[str, MSG_TYPE], None]]
109
+ ) -> Union[str, dict]:
110
+ """Helper to process streaming responses from the API."""
111
+ full_response_text = ""
112
+
113
+ try:
114
+ for line in response.iter_lines():
115
+ if line:
116
+ decoded_line = line.decode('utf-8')
117
+ if decoded_line.startswith('data: '):
118
+ json_str = decoded_line[len('data: '):]
119
+ if json_str.strip() == "[DONE]":
120
+ break
121
+ try:
122
+ chunk = json.loads(json_str)
123
+ if chunk['choices']:
124
+ delta = chunk['choices'][0].get('delta', {})
125
+ content = delta.get('content', '')
126
+ if content:
127
+ full_response_text += content
128
+ if stream and streaming_callback:
129
+ if not streaming_callback(content, MSG_TYPE.MSG_TYPE_CHUNK):
130
+ # Stop streaming if the callback returns False
131
+ return full_response_text
132
+ except json.JSONDecodeError:
133
+ ASCIIColors.warning(f"Could not decode JSON chunk: {json_str}")
134
+ continue
135
+
136
+ # This handles both cases:
137
+ # - If stream=True, we have already sent chunks. We return the full string.
138
+ # - If stream=False, we have buffered the whole response and now return it.
139
+ return full_response_text
140
+
141
+ except Exception as ex:
142
+ error_message = f"An unexpected error occurred while processing the Grok stream: {str(ex)}"
143
+ trace_exception(ex)
144
+ return {"status": False, "error": error_message}
145
+
146
+
147
+ def generate_text(self,
148
+ prompt: str,
149
+ images: Optional[List[str]] = None,
150
+ system_prompt: str = "",
151
+ n_predict: Optional[int] = 2048,
152
+ stream: Optional[bool] = False,
153
+ temperature: float = 0.7,
154
+ top_p: float = 0.9,
155
+ repeat_penalty: float = 1.1, # Not supported
156
+ repeat_last_n: int = 64, # Not supported
157
+ seed: Optional[int] = None, # Not supported
158
+ n_threads: Optional[int] = None, # Not applicable
159
+ ctx_size: int | None = None, # Determined by model
160
+ streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None,
161
+ **kwargs
162
+ ) -> Union[str, dict]:
163
+ """
164
+ Generate text using the Grok model.
165
+ """
166
+ if not self.service_key:
167
+ return {"status": False, "error": "xAI API key not configured."}
168
+
169
+ api_params = self._construct_parameters(temperature, top_p, n_predict)
170
+
171
+ messages = []
172
+ if system_prompt and system_prompt.strip():
173
+ messages.append({"role": "system", "content": system_prompt})
174
+
175
+ user_content = []
176
+ if prompt and prompt.strip():
177
+ user_content.append({"type": "text", "text": prompt})
178
+
179
+ if images:
180
+ for image_data in images:
181
+ try:
182
+ if is_image_path(image_data):
183
+ media_type = get_media_type_for_uri(image_data)
184
+ with open(image_data, "rb") as image_file:
185
+ b64_data = base64.b64encode(image_file.read()).decode('utf-8')
186
+ else: # Assume it's a base64 string
187
+ b64_data = image_data
188
+ media_type = "image/png" # Assume PNG if raw base64
189
+
190
+ user_content.append({
191
+ "type": "image_url",
192
+ "image_url": {"url": f"data:{media_type};base64,{b64_data}"}
193
+ })
194
+ except Exception as e:
195
+ error_msg = f"Failed to process image: {e}"
196
+ ASCIIColors.error(error_msg)
197
+ return {"status": False, "error": error_msg}
198
+
199
+ if not user_content:
200
+ if stream and streaming_callback:
201
+ streaming_callback("", MSG_TYPE.MSG_TYPE_FINISHED_MESSAGE)
202
+ return ""
203
+
204
+ messages.append({"role": "user", "content": user_content})
205
+
206
+ payload = {
207
+ "model": self.model_name,
208
+ "messages": messages,
209
+ **api_params
210
+ }
211
+
212
+ try:
213
+ response = requests.post(
214
+ f"{self.base_url}/chat/completions",
215
+ headers=self.headers,
216
+ json=payload,
217
+ stream=True # We always use the streaming endpoint
218
+ )
219
+ response.raise_for_status()
220
+
221
+ return self._process_and_handle_stream(response, stream, streaming_callback)
222
+
223
+ except requests.exceptions.RequestException as ex:
224
+ error_message = f"Grok API request failed: {str(ex)}"
225
+ try: # Try to get more info from the response body
226
+ error_message += f"\nResponse: {ex.response.text}"
227
+ except:
228
+ pass
229
+ trace_exception(ex)
230
+ return {"status": False, "error": error_message}
231
+ except Exception as ex:
232
+ error_message = f"An unexpected error occurred with Grok API: {str(ex)}"
233
+ trace_exception(ex)
234
+ return {"status": False, "error": error_message}
235
+
236
+
237
+ def chat(self,
238
+ discussion: LollmsDiscussion,
239
+ branch_tip_id: Optional[str] = None,
240
+ n_predict: Optional[int] = 2048,
241
+ stream: Optional[bool] = False,
242
+ temperature: float = 0.7,
243
+ top_p: float = 0.9,
244
+ streaming_callback: Optional[Callable[[str, MSG_TYPE], None]] = None,
245
+ **kwargs
246
+ ) -> Union[str, dict]:
247
+ """
248
+ Conduct a chat session with the Grok model using a LollmsDiscussion object.
249
+ """
250
+ if not self.service_key:
251
+ return {"status": "error", "message": "xAI API key not configured."}
252
+
253
+ system_prompt = discussion.system_prompt
254
+ discussion_messages = discussion.get_messages(branch_tip_id)
255
+
256
+ messages = []
257
+ if system_prompt and system_prompt.strip():
258
+ messages.append({"role": "system", "content": system_prompt})
259
+
260
+ for msg in discussion_messages:
261
+ role = 'assistant' if msg.sender_type == "assistant" else 'user'
262
+
263
+ content_parts = []
264
+ if msg.content and msg.content.strip():
265
+ content_parts.append({"type": "text", "text": msg.content})
266
+
267
+ if msg.images:
268
+ for file_path in msg.images:
269
+ if is_image_path(file_path):
270
+ try:
271
+ media_type = get_media_type_for_uri(file_path)
272
+ with open(file_path, "rb") as image_file:
273
+ b64_data = base64.b64encode(image_file.read()).decode('utf-8')
274
+ content_parts.append({
275
+ "type": "image_url",
276
+ "image_url": {"url": f"data:{media_type};base64,{b64_data}"}
277
+ })
278
+ except Exception as e:
279
+ ASCIIColors.warning(f"Could not load image {file_path}: {e}")
280
+
281
+ # Grok API expects content to be a string for assistant, or list for user.
282
+ if role == 'user':
283
+ messages.append({'role': role, 'content': content_parts})
284
+ else: # assistant
285
+ # Assistants can't send images, so we just extract the text.
286
+ text_content = next((part['text'] for part in content_parts if part['type'] == 'text'), "")
287
+ if text_content:
288
+ messages.append({'role': role, 'content': text_content})
289
+
290
+ if not messages or messages[-1]['role'] != 'user':
291
+ return {"status": "error", "message": "Cannot start chat without a user message."}
292
+
293
+ api_params = self._construct_parameters(temperature, top_p, n_predict)
294
+
295
+ payload = {
296
+ "model": self.model_name,
297
+ "messages": messages,
298
+ **api_params
299
+ }
300
+
301
+ try:
302
+ response = requests.post(
303
+ f"{self.base_url}/chat/completions",
304
+ headers=self.headers,
305
+ json=payload,
306
+ stream=True
307
+ )
308
+ response.raise_for_status()
309
+
310
+ return self._process_and_handle_stream(response, stream, streaming_callback)
311
+
312
+ except requests.exceptions.RequestException as ex:
313
+ error_message = f"Grok API request failed: {str(ex)}"
314
+ try:
315
+ error_message += f"\nResponse: {ex.response.text}"
316
+ except:
317
+ pass
318
+ trace_exception(ex)
319
+ return {"status": "error", "message": error_message}
320
+ except Exception as ex:
321
+ error_message = f"An unexpected error occurred with Grok API: {str(ex)}"
322
+ trace_exception(ex)
323
+ return {"status": "error", "message": error_message}
324
+
325
+ def tokenize(self, text: str) -> list:
326
+ """
327
+ Tokenize the input text.
328
+ Note: Grok doesn't expose a public tokenizer API.
329
+ Using tiktoken's cl100k_base for a reasonable estimate.
330
+ """
331
+ try:
332
+ encoding = tiktoken.get_encoding("cl100k_base")
333
+ return encoding.encode(text)
334
+ except:
335
+ return list(text.encode('utf-8'))
336
+
337
+ def detokenize(self, tokens: list) -> str:
338
+ """
339
+ Detokenize a list of tokens.
340
+ Note: Based on the placeholder tokenizer.
341
+ """
342
+ try:
343
+ encoding = tiktoken.get_encoding("cl100k_base")
344
+ return encoding.decode(tokens)
345
+ except:
346
+ return bytes(tokens).decode('utf-8', errors='ignore')
347
+
348
+ def count_tokens(self, text: str) -> int:
349
+ """
350
+ Count tokens from a text using the fallback tokenizer.
351
+ """
352
+ return len(self.tokenize(text))
353
+
354
+ def embed(self, text: str, **kwargs) -> List[float]:
355
+ """
356
+ Get embeddings for the input text.
357
+ Note: xAI does not provide a dedicated embedding model API.
358
+ """
359
+ ASCIIColors.warning("xAI does not offer a public embedding API. This method is not implemented.")
360
+ raise NotImplementedError("Grok binding does not support embeddings.")
361
+
362
+ def get_model_info(self) -> dict:
363
+ """Return information about the current Grok model setup."""
364
+ return {
365
+ "name": self.binding_name,
366
+ "host_address": self.base_url,
367
+ "model_name": self.model_name,
368
+ "supports_structured_output": False,
369
+ "supports_vision": "vision" in self.model_name or "grok-1.5" == self.model_name,
370
+ }
371
+
372
+ def listModels(self) -> List[Dict[str, str]]:
373
+ """
374
+ Lists available models from the xAI API.
375
+ Caches the result to avoid repeated API calls.
376
+ Falls back to a static list if the API call fails.
377
+ """
378
+ if self._cached_models is not None:
379
+ return self._cached_models
380
+
381
+ if not self.service_key:
382
+ ASCIIColors.warning("Cannot fetch models without an API key. Using fallback list.")
383
+ self._cached_models = _FALLBACK_MODELS
384
+ return self._cached_models
385
+
386
+ try:
387
+ ASCIIColors.info("Fetching available models from xAI API...")
388
+ response = requests.get(f"{self.base_url}/models", headers=self.headers, timeout=15)
389
+ response.raise_for_status()
390
+
391
+ data = response.json()
392
+
393
+ if "data" in data and isinstance(data["data"], list):
394
+ models_data = data["data"]
395
+ formatted_models = []
396
+ for model in models_data:
397
+ model_id = model.get("id")
398
+ if not model_id: continue
399
+
400
+ display_name = model_id.replace("-", " ").title()
401
+ description = f"Context: {model.get('context_window', 'N/A')} tokens."
402
+
403
+ formatted_models.append({
404
+ 'model_name': model_id,
405
+ 'display_name': display_name,
406
+ 'description': description,
407
+ 'owned_by': model.get('owned_by', 'xAI')
408
+ })
409
+
410
+ self._cached_models = formatted_models
411
+ ASCIIColors.green(f"Successfully fetched {len(self._cached_models)} models.")
412
+ return self._cached_models
413
+ else:
414
+ raise ValueError("API response is malformed.")
415
+
416
+ except Exception as e:
417
+ ASCIIColors.error(f"Failed to fetch models from xAI API: {e}")
418
+ ASCIIColors.warning("Using hardcoded fallback list of models.")
419
+ trace_exception(e)
420
+ self._cached_models = _FALLBACK_MODELS
421
+ return self._cached_models
422
+
423
+ def load_model(self, model_name: str) -> bool:
424
+ """Set the model name for subsequent operations."""
425
+ self.model_name = model_name
426
+ ASCIIColors.info(f"Grok model set to: {model_name}. It will be used on the next API call.")
427
+ return True
428
+
429
+
430
+ if __name__ == '__main__':
431
+ # Example Usage (requires XAI_API_KEY environment variable)
432
+ if 'XAI_API_KEY' not in os.environ:
433
+ ASCIIColors.red("Error: XAI_API_KEY environment variable not set.")
434
+ print("Please get your key from xAI and set it as an environment variable.")
435
+ exit(1)
436
+
437
+ ASCIIColors.yellow("--- Testing GrokBinding ---")
438
+
439
+ # --- Configuration ---
440
+ test_model_name = "grok-1"
441
+ test_vision_model_name = "grok-1.5-vision-preview"
442
+
443
+ try:
444
+ # --- Initialization ---
445
+ ASCIIColors.cyan("\n--- Initializing Binding ---")
446
+ binding = GrokBinding(model_name=test_model_name)
447
+ ASCIIColors.green("Binding initialized successfully.")
448
+
449
+ # --- List Models ---
450
+ ASCIIColors.cyan("\n--- Listing Models (dynamic) ---")
451
+ models = binding.listModels()
452
+ if models:
453
+ ASCIIColors.green(f"Found {len(models)} models.")
454
+ for m in models:
455
+ print(f"- {m['model_name']} ({m['display_name']})")
456
+ else:
457
+ ASCIIColors.error("Failed to list models.")
458
+
459
+ # --- Count Tokens ---
460
+ ASCIIColors.cyan("\n--- Counting Tokens ---")
461
+ sample_text = "Hello, world! This is a test from the Grok binding."
462
+ token_count = binding.count_tokens(sample_text)
463
+ ASCIIColors.green(f"Token count for '{sample_text}': {token_count} (using tiktoken)")
464
+
465
+ # --- Text Generation (Non-Streaming) ---
466
+ ASCIIColors.cyan("\n--- Text Generation (Non-Streaming) ---")
467
+ prompt_text = "Explain who Elon Musk is in one sentence."
468
+ ASCIIColors.info(f"Prompt: {prompt_text}")
469
+ generated_text = binding.generate_text(prompt_text, n_predict=100, stream=False, system_prompt="Be very concise.")
470
+ if isinstance(generated_text, str):
471
+ ASCIIColors.green(f"Generated text:\n{generated_text}")
472
+ else:
473
+ ASCIIColors.error(f"Generation failed: {generated_text}")
474
+
475
+ # --- Text Generation (Streaming) ---
476
+ ASCIIColors.cyan("\n--- Text Generation (Streaming) ---")
477
+
478
+ full_streamed_text = ""
479
+ def stream_callback(chunk: str, msg_type: int):
480
+ ASCIIColors.green(chunk, end="", flush=True)
481
+ full_streamed_text += chunk
482
+ return True
483
+
484
+ ASCIIColors.info(f"Prompt: {prompt_text}")
485
+ result = binding.generate_text(prompt_text, n_predict=150, stream=True, streaming_callback=stream_callback)
486
+ print("\n--- End of Stream ---")
487
+ ASCIIColors.green(f"Full streamed text (for verification): {result}")
488
+ assert result == full_streamed_text
489
+
490
+ # --- Embeddings ---
491
+ ASCIIColors.cyan("\n--- Embeddings ---")
492
+ try:
493
+ binding.embed("This should fail.")
494
+ except NotImplementedError as e:
495
+ ASCIIColors.green(f"Successfully caught expected error for embeddings: {e}")
496
+
497
+ # --- Vision Model Test ---
498
+ dummy_image_path = "grok_dummy_test_image.png"
499
+ try:
500
+ available_model_names = [m['model_name'] for m in models]
501
+ if test_vision_model_name not in available_model_names:
502
+ ASCIIColors.warning(f"Vision test model '{test_vision_model_name}' not available. Skipping vision test.")
503
+ else:
504
+ img = Image.new('RGB', (250, 60), color=('red'))
505
+ d = ImageDraw.Draw(img)
506
+ d.text((10, 10), "This is a test image for Grok", fill=('white'))
507
+ img.save(dummy_image_path)
508
+ ASCIIColors.info(f"Created dummy image: {dummy_image_path}")
509
+
510
+ ASCIIColors.cyan(f"\n--- Vision Generation (using {test_vision_model_name}) ---")
511
+ binding.load_model(test_vision_model_name)
512
+ vision_prompt = "Describe this image. What does the text say?"
513
+ ASCIIColors.info(f"Vision Prompt: {vision_prompt} with image {dummy_image_path}")
514
+
515
+ vision_response = binding.generate_text(
516
+ prompt=vision_prompt,
517
+ images=[dummy_image_path],
518
+ n_predict=100,
519
+ stream=False
520
+ )
521
+ if isinstance(vision_response, str):
522
+ ASCIIColors.green(f"Vision model response: {vision_response}")
523
+ else:
524
+ ASCIIColors.error(f"Vision generation failed: {vision_response}")
525
+ except Exception as e:
526
+ ASCIIColors.error(f"Error during vision test: {e}")
527
+ trace_exception(e)
528
+ finally:
529
+ if os.path.exists(dummy_image_path):
530
+ os.remove(dummy_image_path)
531
+
532
+ except Exception as e:
533
+ ASCIIColors.error(f"An error occurred during testing: {e}")
534
+ trace_exception(e)
535
+
536
+ ASCIIColors.yellow("\nGrokBinding test finished.")
@@ -373,6 +373,10 @@ class LollmsClient():
373
373
  pass
374
374
 
375
375
  # --- Core LLM Binding Methods ---
376
+ def get_ctx_size(self, model_name):
377
+ return self.binding.get_ctx_size(model_name)
378
+
379
+
376
380
  def tokenize(self, text: str) -> list:
377
381
  """
378
382
  Tokenize text using the active LLM binding.
@@ -879,7 +883,7 @@ Don't forget encapsulate the code inside a html code tag. This is mandatory.
879
883
 
880
884
  formatted_agent_history = "No actions taken yet in this turn."
881
885
  if agent_work_history:
882
- history_parts = [ f"### Step {i+1}:\n**Thought:** {entry['thought']}\n**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n**Observation (Tool Output):**\n```json\n{json.dumps(entry['tool_result'], indent=2)}\n```" for i, entry in enumerate(agent_work_history)]
886
+ history_parts = [ f"### Step {i+1}:\n**Thought:**\n{entry['thought']}\n**Action:** Called tool `{entry['tool_name']}` with parameters `{json.dumps(entry['tool_params'])}`\n**Observation (Tool Output):**\n```json\n{json.dumps(entry['tool_result'], indent=2)}\n```" for i, entry in enumerate(agent_work_history)]
883
887
  formatted_agent_history = "\n\n".join(history_parts)
884
888
 
885
889
  llm_decision = None
@@ -1580,8 +1584,8 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1580
1584
 
1581
1585
  # Add the new put_code_in_buffer tool definition
1582
1586
  available_tools.append({
1583
- "name": "generate_code",
1584
- "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 generate_code. generate_code do not execute the code nor does it audit it.""",
1587
+ "name": "put_code_in_buffer",
1588
+ "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.""",
1585
1589
  "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"]}
1586
1590
  })
1587
1591
  # Add the new refactor_scratchpad tool definition
@@ -1658,7 +1662,7 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1658
1662
 
1659
1663
 
1660
1664
  current_scratchpad += f"\n\n### Step {i+1}: Thought\n{thought}"
1661
- log_event(f"**Thought**: {thought}", MSG_TYPE.MSG_TYPE_THOUGHT_CONTENT)
1665
+ log_event(f"{thought}", MSG_TYPE.MSG_TYPE_THOUGHT_CONTENT)
1662
1666
 
1663
1667
  if not tool_name:
1664
1668
  # Handle error...
@@ -1691,7 +1695,7 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1691
1695
  tool_calls_this_turn.append({"name": "put_code_in_buffer", "params": tool_params, "result": tool_result})
1692
1696
  observation_text = f"```json\n{json.dumps(tool_result, indent=2)}\n```"
1693
1697
  current_scratchpad += f"\n\n### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n- **Result:**\n{observation_text}"
1694
- log_event(f"**Observation**:Code generated with ID: {code_uuid}", MSG_TYPE.MSG_TYPE_OBSERVATION)
1698
+ log_event(f"Code generated with ID: {code_uuid}", MSG_TYPE.MSG_TYPE_OBSERVATION)
1695
1699
  if code_gen_id: log_event(f"Generating code...", MSG_TYPE.MSG_TYPE_TOOL_CALL, metadata={"id": code_gen_id, "result": tool_result})
1696
1700
  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)
1697
1701
  continue # Go to the next reasoning step immediately
@@ -1755,7 +1759,7 @@ Provide your response as a single JSON object inside a JSON markdown tag. Use th
1755
1759
 
1756
1760
  tool_calls_this_turn.append({"name": tool_name, "params": tool_params, "result": tool_result})
1757
1761
  current_scratchpad += f"\n\n### Step {i+1}: Observation\n- **Action:** Called `{tool_name}`\n- **Result:**\n{observation_text}"
1758
- log_event(f"**Observation**: Result from `{tool_name}`:\n{dict_to_markdown(sanitized_result)}", MSG_TYPE.MSG_TYPE_OBSERVATION)
1762
+ log_event(f"Result from `{tool_name}`:\n{dict_to_markdown(sanitized_result)}", MSG_TYPE.MSG_TYPE_OBSERVATION)
1759
1763
 
1760
1764
  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)
1761
1765
  except Exception as ex:
@@ -1918,6 +1922,97 @@ Do not split the code in multiple tags.
1918
1922
 
1919
1923
  return code_content # Return the (potentially completed) code content or None
1920
1924
 
1925
+ def generate_structured_content(
1926
+ self,
1927
+ prompt,
1928
+ output_format,
1929
+ extra_system_prompt=None,
1930
+ **kwargs
1931
+ ):
1932
+ """
1933
+ Generates structured data (a dict) from a prompt using a JSON template.
1934
+
1935
+ This method is a high-level wrapper around `generate_code`, specializing it
1936
+ for JSON output. It ensures the LLM sticks to a predefined structure,
1937
+ and then parses the output into a Python dictionary.
1938
+
1939
+ Args:
1940
+ prompt (str):
1941
+ The user's request (e.g., "Extract the name, age, and city of the person described").
1942
+ output_format (dict or str):
1943
+ A Python dictionary or a JSON string representing the desired output
1944
+ structure. This will be used as a template for the LLM.
1945
+ Example: {"name": "string", "age": "integer", "city": "string"}
1946
+ extra_system_prompt (str, optional):
1947
+ Additional instructions for the system prompt, to be appended to the
1948
+ main instructions. Defaults to None.
1949
+ **kwargs:
1950
+ Additional keyword arguments to be passed directly to the
1951
+ `generate_code` method (e.g., temperature, max_size, top_k, debug).
1952
+
1953
+ Returns:
1954
+ dict: The parsed JSON data as a Python dictionary, or None if
1955
+ generation or parsing fails.
1956
+ """
1957
+ # 1. Validate and prepare the template string from the output_format
1958
+ if isinstance(output_format, dict):
1959
+ # Convert the dictionary to a nicely formatted JSON string for the template
1960
+ template_str = json.dumps(output_format, indent=2)
1961
+ elif isinstance(output_format, str):
1962
+ # Assume it's already a valid JSON string template
1963
+ template_str = output_format
1964
+ else:
1965
+ # It's good practice to fail early for invalid input types
1966
+ raise TypeError("output_format must be a dict or a JSON string.")
1967
+
1968
+ # 2. Construct a specialized system prompt for structured data generation
1969
+ system_prompt = (
1970
+ "You are a highly skilled AI assistant that processes user requests "
1971
+ "and returns structured data in JSON format. You must strictly adhere "
1972
+ "to the provided JSON template, filling in the values accurately based "
1973
+ "on the user's prompt. Do not add any commentary, explanations, or text "
1974
+ "outside of the final JSON code block. Your entire response must be a single "
1975
+ "valid JSON object within a markdown code block."
1976
+ )
1977
+ if extra_system_prompt:
1978
+ system_prompt += f"\n\nAdditional instructions:\n{extra_system_prompt}"
1979
+
1980
+ # 3. Call the underlying generate_code method with JSON-specific settings
1981
+ if kwargs.get('debug'):
1982
+ ASCIIColors.info("Generating structured content...")
1983
+
1984
+ json_string = self.generate_code(
1985
+ prompt=prompt,
1986
+ system_prompt=system_prompt,
1987
+ template=template_str,
1988
+ language="json",
1989
+ code_tag_format="markdown", # Sticking to markdown is generally more reliable
1990
+ **kwargs # Pass other params like temperature, top_k, etc.
1991
+ )
1992
+
1993
+ # 4. Parse the result and return
1994
+ if not json_string:
1995
+ # generate_code already logs the error, so no need for another message
1996
+ return None
1997
+
1998
+ if kwargs.get('debug'):
1999
+ ASCIIColors.info("Parsing generated JSON string...")
2000
+ print(f"--- Raw JSON String ---\n{json_string}\n-----------------------")
2001
+
2002
+ try:
2003
+ # Use the provided robust parser
2004
+ parsed_json = self.robust_json_parser(json_string)
2005
+
2006
+ if parsed_json is None:
2007
+ ASCIIColors.warning("Failed to robustly parse the generated JSON.")
2008
+ return None
2009
+
2010
+ return parsed_json
2011
+
2012
+ except Exception as e:
2013
+ ASCIIColors.error(f"An unexpected error occurred during JSON parsing: {e}")
2014
+ return None
2015
+
1921
2016
 
1922
2017
  def extract_code_blocks(self, text: str, format: str = "markdown") -> List[dict]:
1923
2018
  """
@@ -333,7 +333,12 @@ class LollmsMessage:
333
333
  def __repr__(self) -> str:
334
334
  """Provides a developer-friendly representation of the message."""
335
335
  return f"<LollmsMessage id={self.id} sender='{self.sender}'>"
336
-
336
+
337
+ def set_metadata_item(self, itemname:str, item_value, discussion):
338
+ new_metadata = (self.metadata or {}).copy()
339
+ new_metadata[itemname] = item_value
340
+ self.metadata = new_metadata
341
+ discussion.commit()
337
342
 
338
343
  class LollmsDiscussion:
339
344
  """Represents and manages a single discussion.
@@ -374,7 +379,9 @@ class LollmsDiscussion:
374
379
  object.__setattr__(self, '_message_index', None)
375
380
  object.__setattr__(self, '_messages_to_delete_from_db', set())
376
381
  object.__setattr__(self, '_is_db_backed', db_manager is not None)
377
-
382
+
383
+ object.__setattr__(self, '_system_prompt', None)
384
+
378
385
  if self._is_db_backed:
379
386
  if not db_discussion_obj and not discussion_id:
380
387
  raise ValueError("Either discussion_id or db_discussion_obj must be provided for DB-backed discussions.")
@@ -634,6 +641,9 @@ class LollmsDiscussion:
634
641
  A dictionary with 'user_message' and 'ai_message' LollmsMessage objects,
635
642
  where the 'ai_message' will contain rich metadata if an agentic turn was used.
636
643
  """
644
+ if personality is not None:
645
+ object.__setattr__(self, '_system_prompt', personality.system_prompt)
646
+
637
647
  if self.max_context_size is not None:
638
648
  self.summarize_and_prune(self.max_context_size)
639
649
 
@@ -684,6 +694,7 @@ class LollmsDiscussion:
684
694
  use_data_store=use_data_store,
685
695
  max_reasoning_steps=max_reasoning_steps,
686
696
  images=images,
697
+ system_prompt = self._system_prompt,
687
698
  debug=debug, # Pass the debug flag down
688
699
  **kwargs
689
700
  )
@@ -880,7 +891,7 @@ class LollmsDiscussion:
880
891
  return "" if format_type == "lollms_text" else []
881
892
 
882
893
  branch = self.get_branch(branch_tip_id)
883
- full_system_prompt = self.system_prompt # Simplified for clarity
894
+ full_system_prompt = self._system_prompt # Simplified for clarity
884
895
  participants = self.participants or {}
885
896
 
886
897
  def get_full_content(msg: 'LollmsMessage') -> str:
@@ -941,7 +952,10 @@ class LollmsDiscussion:
941
952
  # --- OPENAI & OLLAMA CHAT FORMATS ---
942
953
  messages = []
943
954
  if full_system_prompt:
944
- messages.append({"role": "system", "content": full_system_prompt})
955
+ if format_type == "markdown":
956
+ messages.append(f"system: {full_system_prompt}")
957
+ else:
958
+ messages.append({"role": "system", "content": full_system_prompt})
945
959
 
946
960
  for msg in branch:
947
961
  if msg.sender_type == 'user':
@@ -1051,8 +1065,17 @@ class LollmsDiscussion:
1051
1065
  }"""
1052
1066
  infos = self.lollmsClient.generate_code(prompt = prompt, template = template)
1053
1067
  discussion_title = robust_json_parser(infos)["title"]
1054
- self.metadata['title'] = discussion_title
1068
+ new_metadata = (self.metadata or {}).copy()
1069
+ new_metadata['title'] = discussion_title
1070
+
1071
+ self.metadata = new_metadata
1055
1072
  self.commit()
1056
1073
  return discussion_title
1057
1074
  except Exception as ex:
1058
- trace_exception(ex)
1075
+ trace_exception(ex)
1076
+
1077
+ def set_metadata_item(self, itemname:str, item_value):
1078
+ new_metadata = (self.metadata or {}).copy()
1079
+ new_metadata[itemname] = item_value
1080
+ self.metadata = new_metadata
1081
+ self.commit()
@@ -302,3 +302,7 @@ class LollmsLLMBindingManager:
302
302
  list[str]: List of binding names.
303
303
  """
304
304
  return [binding_dir.name for binding_dir in self.llm_bindings_dir.iterdir() if binding_dir.is_dir() and (binding_dir / "__init__.py").exists()]
305
+
306
+ def get_available_bindings():
307
+ bindings_dir = Path(__file__).parent/"llm_bindings"
308
+ return [binding_dir.name for binding_dir in bindings_dir.iterdir() if binding_dir.is_dir() and (binding_dir / "__init__.py").exists()]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lollms_client
3
- Version: 0.26.0
3
+ Version: 0.27.1
4
4
  Summary: A client library for LoLLMs generate endpoint
5
5
  Author-email: ParisNeo <parisneoai@gmail.com>
6
6
  License: Apache Software License
@@ -26,12 +26,12 @@ examples/mcp_examples/openai_mcp.py,sha256=7IEnPGPXZgYZyiES_VaUbQ6viQjenpcUxGiHE
26
26
  examples/mcp_examples/run_remote_mcp_example_v2.py,sha256=bbNn93NO_lKcFzfIsdvJJijGx2ePFTYfknofqZxMuRM,14626
27
27
  examples/mcp_examples/run_standard_mcp_example.py,sha256=GSZpaACPf3mDPsjA8esBQVUsIi7owI39ca5avsmvCxA,9419
28
28
  examples/test_local_models/local_chat.py,sha256=slakja2zaHOEAUsn2tn_VmI4kLx6luLBrPqAeaNsix8,456
29
- lollms_client/__init__.py,sha256=1AekKYnWV53viRAkn1ZzdHEk8msiWHoiMZUENt0IAdI,1047
29
+ lollms_client/__init__.py,sha256=sUL95Hk1acM0CHbqz-eQZDE39r9R7dkV4Q6uK18_5IE,1147
30
30
  lollms_client/lollms_config.py,sha256=goEseDwDxYJf3WkYJ4IrLXwg3Tfw73CXV2Avg45M_hE,21876
31
- lollms_client/lollms_core.py,sha256=TujAapwba9gDe6EEY4olVSP-lZrLftY4LOSex-D-IPs,159610
32
- lollms_client/lollms_discussion.py,sha256=P_ecqhWhiFIEnvyZqX_gMCnhi2BzhP66aUxckN9JP40,48660
31
+ lollms_client/lollms_core.py,sha256=x1RF4EZfEqawBtU7iJKMV3JiwwlYK_8AbwZJehHj4kc,163851
32
+ lollms_client/lollms_discussion.py,sha256=aGjjAuRUaPaZvVh8sRWUrSYH8pEGa6j9iM9mTGIqdHQ,49633
33
33
  lollms_client/lollms_js_analyzer.py,sha256=01zUvuO2F_lnUe_0NLxe1MF5aHE1hO8RZi48mNPv-aw,8361
34
- lollms_client/lollms_llm_binding.py,sha256=Kpzhs5Jx8eAlaaUacYnKV7qIq2wbME5lOEtKSfJKbpg,12161
34
+ lollms_client/lollms_llm_binding.py,sha256=vtX158AWHSiUUDh7UU6BmyomhB3IXBTrxvGIXcADZfA,12391
35
35
  lollms_client/lollms_mcp_binding.py,sha256=0rK9HQCBEGryNc8ApBmtOlhKE1Yfn7X7xIQssXxS2Zc,8933
36
36
  lollms_client/lollms_personality.py,sha256=dILUI5DZdzJ3NDDQiIsK2UptVF-jZK3XYXZ2bpXP_ew,8035
37
37
  lollms_client/lollms_python_analyzer.py,sha256=7gf1fdYgXCOkPUkBAPNmr6S-66hMH4_KonOMsADASxc,10246
@@ -44,8 +44,9 @@ lollms_client/lollms_types.py,sha256=0iSH1QHRRD-ddBqoL9EEKJ8wWCuwDUlN_FrfbCdg7Lw
44
44
  lollms_client/lollms_utilities.py,sha256=zx1X4lAXQ2eCUM4jDpu_1QV5oMGdFkpaSEdTASmaiqE,13545
45
45
  lollms_client/llm_bindings/__init__.py,sha256=9sWGpmWSSj6KQ8H4lKGCjpLYwhnVdL_2N7gXCphPqh4,14
46
46
  lollms_client/llm_bindings/azure_openai/__init__.py,sha256=8C-gXoVa-OI9FmFM3PaMgrTfzqCLbs4f7CHJHxKuAR8,16675
47
- lollms_client/llm_bindings/claude/__init__.py,sha256=0kXWJgbClV71PacOwPw3Hnn6ur0Ka8mFVsVzpcfsVwI,24868
47
+ lollms_client/llm_bindings/claude/__init__.py,sha256=CsWILXAFytXtxp1ZAoNwq8KycW0POQ2MCmpT6Bz0Hd0,24877
48
48
  lollms_client/llm_bindings/gemini/__init__.py,sha256=ZflZVwAkAa-GfctuehOWIav977oTCdXUisQy253PFsk,21611
49
+ lollms_client/llm_bindings/grok/__init__.py,sha256=5tIf3348RgAEaSp6FdG-LM9N8R7aR0t7OFspHf3XATs,23141
49
50
  lollms_client/llm_bindings/groq/__init__.py,sha256=zyWKM78qHwSt5g0Bb8Njj7Jy8CYuLMyplx2maOKFFpg,12218
50
51
  lollms_client/llm_bindings/hugging_face_inference_api/__init__.py,sha256=PxgeRqT8dpa9GZoXwtSncy9AUgAN2cDKrvp_nbaWq0E,14027
51
52
  lollms_client/llm_bindings/litellm/__init__.py,sha256=xlTaKosxK1tKz1YJ6witK6wAJHIENTV6O7ZbfpUOdB4,11289
@@ -87,8 +88,8 @@ lollms_client/tts_bindings/piper_tts/__init__.py,sha256=0IEWG4zH3_sOkSb9WbZzkeV5
87
88
  lollms_client/tts_bindings/xtts/__init__.py,sha256=FgcdUH06X6ZR806WQe5ixaYx0QoxtAcOgYo87a2qxYc,18266
88
89
  lollms_client/ttv_bindings/__init__.py,sha256=UZ8o2izQOJLQgtZ1D1cXoNST7rzqW22rL2Vufc7ddRc,3141
89
90
  lollms_client/ttv_bindings/lollms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- lollms_client-0.26.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
91
- lollms_client-0.26.0.dist-info/METADATA,sha256=9sfLeCWj9T_80AmnURpKudbIHYnAx5ENd_ewmZ2_5mM,25778
92
- lollms_client-0.26.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
93
- lollms_client-0.26.0.dist-info/top_level.txt,sha256=NI_W8S4OYZvJjb0QWMZMSIpOrYzpqwPGYaklhyWKH2w,23
94
- lollms_client-0.26.0.dist-info/RECORD,,
91
+ lollms_client-0.27.1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
92
+ lollms_client-0.27.1.dist-info/METADATA,sha256=4QW25XghKUQNEX3y_RFrE9me4SP_efyvet4Z-76h4rc,25778
93
+ lollms_client-0.27.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
94
+ lollms_client-0.27.1.dist-info/top_level.txt,sha256=NI_W8S4OYZvJjb0QWMZMSIpOrYzpqwPGYaklhyWKH2w,23
95
+ lollms_client-0.27.1.dist-info/RECORD,,