lollms_client is a powerful and flexible Python library designed to simplify interactions with the LoLLMs (Lord of Large Language Models) ecosystem and various other Large Language Model (LLM) backends. It provides a unified API for text generation, multimodal operations (text-to-image, text-to-speech, etc.), and robust function calling through the Model Context Protocol (MCP).
Whether you're connecting to a remote LoLLMs server, an Ollama instance, the OpenAI API, or running models locally using GGUF (via llama-cpp-python or a managed llama.cpp server), Hugging Face Transformers, or vLLM, lollms-client offers a consistent and developer-friendly experience.
lollms_client is not just another API wrapper. It is a highly optimized, production-grade coordination engine built to grant Large Language Models true local and hybrid autonomy.
- Persistent Multi-Level Storage: Features an advanced, cognitive hierarchical storage system consisting of Volatile Scratchpad (single-turn intermediate reasoning), Working Memory (directly injected into prompt space), Deep Memory (stubbed as handles to prevent context bloating), Archived Memory (historical backup), and Episodic Memory (immutable step-by-step trace of interactions).
- Memory Decay & Consolidation: Memories decay logarithmically over time. Frequently referenced concepts are automatically reinforced.
- AI-Assisted Dreaming (
dream()): During idle cycles, an automated "dream consolidation pass" cleans up old data. Important rules and architecture patterns are maintained, while low-importance noise is forgotten.
- Deterministic State Control: Uses a robust Observe-Think-Act-Verify state machine. If the model generates a thought process but fails to act, the parser detects the omission, restricts reasoning, and injects precise structural corrections to guide it back on track.
- Model Context Protocol (MCP): Native integration of local and remote MCP tool registries (e.g., File I/O, Web Search, Sandboxed Code Execution) giving agents direct hands-on power.
- Real-Time Performance Metrics: Tracks exact performance statistics per spinoff agent turn, capturing Time to First Token (TTFT), Average Generation Speed (TPS), and total token usage stored directly in the discussion database.
- Non-Destructive SEARCH/REPLACE Edits: Features the Lollms Text Processor layer (
lc.llm.tp). Instead of full document rewrites, it supports structural, aider-style Search/Replace code patches. - Schema & Pydantic Enforcement: Easily output structured data with built-in schema validation and truncation-recovery algorithms. If the model's output gets cut off, the Text Processor reconstructs the JSON tree and repairs the output.
- Yes/No & Multi-Choice Helpers: Built-in helper primitives to perform discrete evaluations, ranking, and classification.
- Fine-Grained Vision Controls: Multi-image inputs can be selectively toggled active or inactive on each message turn without purging original databases—significantly reducing vision model token costs.
- Qwen Multi-Image Fusion: Diffusers integration supports cutting-edge local image-to-image engines capable of single-image semantic edits and advanced multi-image fusion, character swaps, pose transfers, and background transplants.
- Unified Configuration: Run local GGUFs (via llama.cpp/python), local Ollama instances, or scale to OpenAI, Anthropic, Gemini, Groq, and OpenRouter using a single, unified
llm_binding_configblock. - Automatic Context Compression: Dynamically monitors context token sizes, summarizes old turns using targeted AI-synthesizers, and collapses long historical sequences into a clean, single-turn Project State Synopsis to keep models sharp and conversational context pristine.
- Memory-Efficient Multi-Binding: Define declarative registries of
LollmsBindingProfileconfigurations for any modality (LLM, TTI, TTS, STT, TTV, TTM). Only the binding marked asis_defaultis eagerly loaded at startup. Other bindings are instantiated lazily on-demand when you switch to them, saving massive amounts of RAM and VRAM. - Dynamic Switching: Seamlessly switch between a local coding model, a cloud vision model, and a fast text model at runtime using
client.switch_model("alias")orclient.switch_tti("alias"). Instantiated bindings are cached for zero-latency re-activation. - 100% Backward Compatible: If you use the legacy
llm_binding_nameorextra_llmsparameters, they are automatically registered as the"master"profiles and eagerly instantiated, ensuring older code runs without modification.
You can install lollms_client directly from PyPI:
pip install lollms-clientThis will install the core library. Some bindings may require additional dependencies (e.g., llama-cpp-python, torch, transformers, ollama, vllm, Pillow for image utilities, docling for document parsing). The library attempts to manage these using pipmaster, but for complex dependencies (especially those requiring compilation like llama-cpp-python with GPU support), manual installation might be preferred.
The LollmsClient provides several methods for generating text, catering to different use cases.
This is the most straightforward method for generating a response based on a simple prompt.
from lollms_client import LollmsClient, MSG_TYPE
from ascii_colors import ASCIIColors
import os
# Callback for streaming output
def simple_streaming_callback(chunk: str, msg_type: MSG_TYPE, params=None, metadata=None) -> bool:
if msg_type == MSG_TYPE.MSG_TYPE_CHUNK:
print(chunk, end="", flush=True)
elif msg_type == MSG_TYPE.MSG_TYPE_EXCEPTION:
ASCIIColors.error(f"\nStreaming Error: {chunk}")
return True # True to continue streaming
try:
# Initialize client to connect to a LoLLMs server.
# All binding-specific parameters now go into the 'llm_binding_config' dictionary.
lc = LollmsClient(
llm_binding_name="lollms", # This is the default binding
llm_binding_config={
"host_address": "http://localhost:9642", # Default port for LoLLMs server
# "service_key": "your_lollms_api_key_here" # Get key from LoLLMs UI -> User Settings if security is enabled
# "verify_ssl_certificate": True #if false the ssl certifcate verification will be ignored (only used when using https in lollms service address)
}
)
prompt = "Tell me a fun fact about space."
ASCIIColors.yellow(f"Prompt: {prompt}")
# Generate text with streaming
ASCIIColors.green("Streaming Response:")
response_text = lc.generate_text(
prompt,
n_predict=100,
stream=True,
streaming_callback=simple_streaming_callback
)
print("\n--- End of Stream ---")
# The 'response_text' variable will contain the full concatenated text
# if streaming_callback returns True throughout.
if isinstance(response_text, str):
ASCIIColors.cyan(f"\nFull streamed text collected: {response_text[:100]}...")
elif isinstance(response_text, dict) and "error" in response_text:
ASCIIColors.error(f"Error during generation: {response_text['error']}")
except ValueError as ve:
ASCIIColors.error(f"Initialization Error: {ve}")
ASCIIColors.info("Ensure a LoLLMs server is running or configure another binding.")
except ConnectionRefusedError:
ASCIIColors.error("Connection refused. Is the LoLLMs server running at http://localhost:9642?")
except Exception as e:
ASCIIColors.error(f"An unexpected error occurred: {e}")For more complex conversational interactions, you can provide the LLM with a list of messages, similar to the OpenAI Chat Completion API. This allows you to define roles (system, user, assistant) and build multi-turn conversations programmatically.
from lollms_client import LollmsClient, MSG_TYPE
from ascii_colors import ASCIIColors
import os
def streaming_callback_for_messages(chunk: str, msg_type: MSG_TYPE, params=None, metadata=None) -> bool:
if msg_type == MSG_TYPE.MSG_TYPE_CHUNK:
print(chunk, end="", flush=True)
return True
try:
# Example for an Ollama binding
# Ensure you have Ollama installed and model 'llama3' pulled (e.g., ollama pull llama3)
lc = LollmsClient(
llm_binding_name="ollama",
llm_binding_config={
"model_name": "llama3",
"host_address": "http://localhost:11434" # Default Ollama address
}
)
# Define the conversation history as a list of messages
messages = [
{"role": "system", "content": "You are a helpful assistant that specializes in programming."},
{"role": "user", "content": "Hello, what's your name?"},
{"role": "assistant", "content": "I am an AI assistant created by Google."},
{"role": "user", "content": "Can you explain recursion in Python?"}
]
ASCIIColors.yellow("\nGenerating response from messages:")
response_text = lc.generate_from_messages(
messages=messages,
n_predict=200,
stream=True,
streaming_callback=streaming_callback_for_messages
)
print("\n--- End of Message Stream ---")
ASCIIColors.cyan(f"\nFull collected response: {response_text[:150]}...")
except Exception as e:
ASCIIColors.error(f"Error during message generation: {e}")The generate_with_tools method enables LLMs to act as agents that can discover, call, and chain external tools. This is the foundation for building autonomous AI assistants that can search the web, query databases, execute code, or interact with APIs.
Key Features:
- File-based tools: Load tools from lollms-format Python scripts (
tool_*.pywith docstring-described arguments) - Inline tools: Pass tool dicts directly with
{"name": ..., "callable": ..., "parameters": [...]} - Automatic execution: The agentic loop parses
<tool>tags, executes tools, and feeds results back - Multi-step reasoning: The model can chain multiple tool calls across rounds to solve complex tasks
Tool Format (lollms scripts): A tool script is a Python file containing:
TOOL_LIBRARY_NAME,TOOL_LIBRARY_DESC,TOOL_LIBRARY_ICONmetadata- An optional
init_tools_library()for dependency setup - One or more
tool_*functions with docstring-described arguments
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
from pathlib import Path
# Create a simple calculator tool file
tool_content = '''
TOOL_LIBRARY_NAME = 'Calculator'
TOOL_LIBRARY_DESC = 'Basic arithmetic operations'
TOOL_LIBRARY_ICON = '🧮'
def tool_calculate(args: dict):
"""
Perform arithmetic calculations.
Args:
args: dict with keys:
- expression (str): Mathematical expression to evaluate (e.g., "2 + 2 * 5")
"""
try:
expression = args.get('expression', '')
# Safe evaluation using limited operators
allowed = {"__builtins__": {}}
allowed.update({k: v for k, v in __import__('math').__dict__.items()})
result = eval(expression, allowed, {})
return f"Result: {result}"
except Exception as e:
return f"Error: {str(e)}"
'''
tool_path = Path.home() / ".lollms_hub" / "tools" / "calculator.py"
tool_path.parent.mkdir(parents=True, exist_ok=True)
tool_path.write_text(tool_content, encoding="utf-8")
try:
lc = LollmsClient(
llm_binding_name="llama_cpp_server",
llm_binding_config={
"models_path": "data/models/llama_cpp_models",
"ctx_size": 4096,
"n_gpu_layers": -1,
}
)
# Load a model first...
# lc.llm.load_model("your-model.gguf")
result = lc.generate_with_tools(
prompt="What is the square root of 144 plus 25?",
tools=[str(tool_path)], # Pass file path(s) or inline dicts
system_prompt="You are a helpful math assistant. Use the calculator tool when needed.",
temperature=0.7,
n_predict=1024,
max_tool_rounds=5,
auto_execute=True,
)
ASCIIColors.green(f"\nFinal Answer: {result['response']}")
ASCIIColors.cyan(f"Tool calls made: {len(result['tool_calls'])}")
for tc in result['tool_calls']:
print(f" - {tc['name']}: {tc['parameters']}")
except Exception as e:
ASCIIColors.error(f"Error during tool generation: {e}")Return Value: The method returns a comprehensive result dict:
{
"response": str, # Final text answer from the model
"tool_calls": [ # All tool calls made during the session
{"round": int, "name": str, "parameters": dict, "raw": str}
],
"tool_results": [ # All tool execution results
{"round": int, "name": str, "result": dict}
],
"rounds": int, # Number of agentic rounds executed
"pending_tool": dict, # Present only if auto_execute=False (manual mode)
}The generate_structured_content method is a powerful utility for forcing an LLM's output into a specific JSON format. It's ideal for extracting information, getting consistent tool parameters, or any task requiring reliable, machine-readable output.
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import json
import os
try:
# Using Ollama as an example binding
lc = LollmsClient(llm_binding_name="ollama", llm_binding_config={"model_name": "llama3"})
text_block = "John Doe is a 34-year-old software engineer from New York. He loves hiking and Python programming."
# Define the exact JSON structure you want
output_template = {
"full_name": "string",
"age": "integer",
"profession": "string",
"city": "string",
"hobbies": ["list", "of", "strings"] # Example of a list in schema
}
ASCIIColors.yellow(f"\nExtracting structured data from: '{text_block}'")
ASCIIColors.yellow(f"Using schema: {json.dumps(output_template)}")
# Generate the structured data
extracted_data = lc.generate_structured_content(
prompt=f"Extract the relevant information from the following text:\n\n{text_block}",
schema=output_template, # Note: parameter is 'schema'
temperature=0.0 # Use low temperature for deterministic structured output
)
if extracted_data:
ASCIIColors.green("\nExtracted Data (JSON):")
print(json.dumps(extracted_data, indent=2))
else:
ASCIIColors.error("\nFailed to extract structured data.")
except Exception as e:
ASCIIColors.error(f"An error occurred during structured content generation: {e}")The Lollms Text Processor is a high-level utility designed to turn raw LLM generations into production-ready workflows. It handles long documents, structured outputs, robust code generation, intelligent editing, and reliable parsing.
It is directly accessible via:
lc.llm.tpfrom lollms_client import LollmsClient
lc = LollmsClient(
llm_binding_name="lollms",
llm_binding_config={
"model_name": "llama3",
"host_address": "http://localhost:9642",
"service_key": "the service key"
}
)
llm = lc.llm
tp = lc.llm.tpllmprovides low-level text generation primitivestpis the Text Processor, ready to use out of the box
The Text Processor automatically handles documents that exceed the model’s context window by chunking, synthesizing intermediate results, and producing a final consolidated output.
summary = tp.long_context_processing(
text_to_process=long_document,
contextual_prompt="Summarize the main findings about climate change",
processing_type="text"
)result = tp.long_context_processing(
text_to_process=long_document,
contextual_prompt="Extract all people mentioned with their roles",
processing_type="structured",
schema={
"type": "object",
"properties": {
"people": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"role": {"type": "string"}
}
}
}
}
}
)answer = tp.long_context_processing(
text_to_process=long_document,
contextual_prompt="Does this document mention Marie Curie?",
processing_type="yes_no",
return_explanation=True
)code = tp.generate_code(
prompt="Create a binary search function",
language="python"
)files = tp.generate_codes(
prompt="Create a Flask web app with an HTML frontend"
)updated_code = tp.edit_code(
original_code=existing_code,
edit_instruction="Add error handling and logging",
language="python"
)Unlike naïve prompting, edits are structural, not full rewrites.
data = tp.generate_structured_content(
prompt="Create a presentation about AI",
schema={
"type": "object",
"properties": {
"slides": {
"type": "array",
"items": {"type": "object"}
}
}
}
)from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
person = tp.generate_structured_content_pydantic(
prompt="Create a person named Alice, age 30",
pydantic_model=Person
)✔ Automatic validation ✔ Truncation recovery ✔ Agent-friendly outputs
answer = tp.yes_no(
question="Is Marie Curie a scientist?",
context="Marie Curie was a physicist...",
return_explanation=True
)choice = tp.multichoice_question(
question="What field did Marie Curie work in?",
possible_answers=["Biology", "Physics", "Chemistry"]
)summary = tp.summerize_text(text="Long article...")keywords = tp.extract_keywords(
text="Long article...",
num_keywords=5
)thoughts = tp.extract_thinking_blocks(llm_response)clean_text = tp.remove_thinking_blocks(llm_response)blocks = tp.extract_code_blocks(
text=llm_response,
format="markdown"
)- ✅ Automatic long-context handling
- ✅ XML-based code generation (no fragile backticks)
- ✅ Truncation recovery for JSON and code
- ✅ Non-destructive, structured code editing
- ✅ JSON Schema & Pydantic support
- ✅ Decision helpers (yes/no, multichoice, ranking)
- ✅ Graceful fallback strategies
lollms_client features a powerful, memory-efficient multi-binding architecture via Universal Lazy Profiles. Instead of eagerly instantiating all models/engines at startup (which wastes RAM and VRAM), you define declarative registries of LollmsBindingProfile configurations for any modality (LLM, TTI, TTS, STT, TTV, TTM).
Only the binding marked as is_default is loaded at startup. Other bindings are instantiated lazily on-demand when you switch to them. This system is 100% backward compatible. If you use the legacy llm_binding_name or tti_binding_name parameters, they are automatically registered as the "master" profiles and marked as defaults.
Define multiple LLM profiles to handle different domains or complexity tiers.
from lollms_client import LollmsClient, LollmsBindingProfile
# 1. Define LLM profiles declaratively (None of these are instantiated yet)
llm_profiles = {
"cloud_vision": LollmsBindingProfile(
name="cloud_vision",
binding_name="openai",
binding_config={"model_name": "gpt-4o", "service_key": "your-key"},
vision_enabled=True,
is_default=True # This one will be loaded at startup
),
"local_coder": LollmsBindingProfile(
name="local_coder",
binding_name="ollama",
binding_config={"host_address": "http://localhost:11434", "model_name": "qwen2.5-coder:7b"},
forced_context_size=32768
),
"local_fast": LollmsBindingProfile(
name="local_fast",
binding_name="ollama",
binding_config={"host_address": "http://localhost:11434", "model_name": "llama3.2:3b"}
)
}
# 2. Initialize the client (Only "cloud_vision" is instantiated)
lc = LollmsClient(llm_profiles=llm_profiles)
# 3. Use the default model
response1 = lc.generate_text("Explain quantum physics.")
# 4. Dynamically switch to the local coder (Instantiated on-the-fly and cached)
lc.switch_model("local_coder")
response2 = lc.generate_text("Write a Python script to sort a list.")
# 5. Switch back to the default cloud vision model (Retrieved from cache, no re-instantiation)
lc.switch_model("cloud_vision")The profile system is universal. You can define profiles for image generation engines and switch between a local Stable Diffusion model and a cloud DALL-E API seamlessly.
# Define TTI profiles
tti_profiles = {
"local_sd": LollmsBindingProfile(
name="local_sd",
binding_name="diffusers",
binding_config={"model_name": "stable-diffusion-v1-5"},
is_default=True
),
"cloud_dalle": LollmsBindingProfile(
name="cloud_dalle",
binding_name="openai",
binding_config={"model_name": "dall-e-3", "service_key": "your-key"}
)
}
lc = LollmsClient(llm_binding_name="ollama", tti_profiles=tti_profiles)
# Generate with default local SD
img_bytes = lc.generate_image("A cyberpunk cat")
# Switch to DALL-E 3 for a specific prompt
lc.switch_tti("cloud_dalle")
img_bytes = lc.generate_image("A hyperrealistic oil painting of a dog")For automated routing, the smart_router meta-binding evaluates incoming prompts using TF-IDF (subject matching), complexity heuristics, and weighted constraints (cost/latency) to delegate generation to the optimal child model automatically.
from lollms_client import LollmsClient
router_config = {
"routing_strategy": "cost_optimized",
"model_profiles": {
"cheap_fast": {
"binding_name": "ollama",
"binding_config": {"model_name": "qwen2.5:3b"},
"routing_profile": {"description": "fast simple tasks", "complexity_tier": 1}
},
"smart_coder": {
"binding_name": "ollama",
"binding_config": {"model_name": "qwen2.5-coder:7b"},
"routing_profile": {"description": "python code debugging", "complexity_tier": 2}
}
}
}
lc = LollmsClient(llm_binding_name="smart_router", llm_binding_config=router_config)
# The router automatically selects "cheap_fast" for simple prompts
response = lc.generate_text("What is 2+2?")
# The router automatically selects "smart_coder" for coding prompts
response = lc.generate_text("Write a Python script to sort a list.")When using a text-only LLM (or Smart Router without a VLM child), you can enable VLM collaboration via the vlm_query LCP tool. This allows the text LLM to delegate visual analysis to a secondary VLM on-demand.
# Client with a text-only LLM and a separate VLM profile
lc = LollmsClient(
llm_profiles={
"text_llm": LollmsBindingProfile(name="text_llm", binding_name="ollama", binding_config={"model_name": "llama3"}, is_default=True),
"vlm": LollmsBindingProfile(name="vlm", binding_name="ollama", binding_config={"model_name": "llava"}, vision_enabled=True)
}
)
# The user sends an image and asks a question
discussion.chat(
user_message="Look at the diagram in the image. What are the main components?",
images=["base64_encoded_image_data..."],
enable_vlm_query=True, # CRITICAL: Explicitly enable the fallback tool
max_reasoning_steps=10
)
# The LLM (llama3) will realize it needs vision, call `tool_vlm_query(0, "Identify the main components.")`,
# receive the text description from the VLM (llava), and synthesize the final answer.In lollms_client, the LollmsPersonality is the sovereign unit of execution. It replaces the legacy bifurcation between stateless "prompt wrappers" and stateful "autonomous agents". A single LollmsPersonality scales progressively from a simple 5-line SOUL.md to a fully-armed, stateful, multi-persona Crew Handbag with tools, memory, skills, and multimodal assets.
A simple personality is just a system prompt and an optional RAG data source. It has no memory, no workspace, and no tools. It is 100% backward compatible with all legacy LollmsDiscussion.chat(personality=...) calls.
from lollms_client import LollmsClient, LollmsDiscussion, LollmsDataManager
from lollms_client.lollms_personality import LollmsPersonality
client = LollmsClient(llm_binding_name="ollama", llm_binding_config={"model_name": "llama3"})
db_manager = LollmsDataManager("sqlite:///discussion.db")
discussion = LollmsDiscussion.create_new(lollms_client=client, db_manager=db_manager)
# A simple, stateless personality
simple_pers = LollmsPersonality(
name="MathTutor",
author="user",
category="general",
description="A helpful math tutor.",
system_prompt="You are a helpful math tutor. Explain concepts simply."
)
# Drops directly into a discussion
discussion.chat(user_message="Explain Pythagoras' theorem.", personality=simple_pers)A Handbag Personality is created by pointing a LollmsPersonality to a Handbag folder. This lazily instantiates a SkillsManager, a MemoryManager, and a ToolRegistry. It can be dropped into a LollmsDiscussion just like a simple personality, but it brings state and capabilities.
from lollms_client.lollms_personality import LollmsPersonality
# A complex, stateful personality loaded from a Handbag folder
# Memory, Skills, and Tools are lazily instantiated upon loading.
autonomous_pers = LollmsPersonality.from_handbag("./my_research_handbag")
# It still drops directly into a discussion!
# The LollmsDiscussion loop will automatically detect the stateful components
# and inject the memory, skills, and tools into the active turn.
discussion.chat(user_message="Refactor the utils file.", personality=autonomous_pers)The Handbag is a self-contained, portable folder that carries ALL of a personality's resources. It allows you to package multiple personas, tools, skills, RAG knowledge, memory, and multimodal assets (like 3D models or voice samples) into a single directory.
my_handbag/
├── SOUL.md # Primary personality (YAML frontmatter + Markdown body)
├── handbag.yaml # Optional manifest (name, default_personality, skills_mode)
├── coworkers/ # Multi-persona support (Crew Handbag)
│ ├── researcher/
│ │ └── SOUL.md
│ └── coder/
│ └── SOUL.md
├── tools/ # Shared LCP tools (.py files or subdirs)
│ ├── my_custom_tool.py
│ └── another_tool/
│ └── another_tool.py
├── skills/ # SKILL.md files (personality creates/updates these)
│ ├── python_patterns/
│ │ └── SKILL.md
│ └── ...
├── rag/ # RAG documents (text files for retrieval)
│ ├── doc1.txt
│ └── doc2.md
├── memory/ # Memory database
│ └── memory.db
└── assets/ # Multimodal assets (icons, voice samples, 3D models)
├── logo.png
└── voice.wav
You can automatically scaffold a new, empty Handbag folder using the Handbag.create_structure() helper.
from lollms_client.lollms_agent.handbag import Handbag
# Scaffolds the complete folder structure
hb_path = Handbag.create_structure("./my_research_handbag", name="Research Handbag")- Personalities (
coworkers/): Create a subdirectory for each persona. Inside each, place aSOUL.mdfile. This forms your Crew. - Tools (
tools/): Place LCP tool scripts (.pyfiles) here. All personas in the Handbag share this tool pool. - Skills (
skills/): Create subdirectories for each skill, each containing aSKILL.mdfile. - RAG (
rag/): Place text files here. The Handbag automatically indexes them. - Memory (
memory/): Amemory.dbSQLite file will be automatically created here. - Assets (
assets/): Store custom graphics, audio, or 3D models for NPC-style applications.
You can optionally configure the handbag.yaml manifest:
name: Research Handbag
version: '1.0'
description: A handbag for research tasks.
default_personality: researcher # Name of the folder in personalities/
skills_mode: always_visible # "always_visible", "loadable", or "mixed"A single Handbag can contain multiple SOUL.md files (e.g., coworkers/coder/ and coworkers/researcher/). When loaded, the primary LollmsPersonality object acts as a router.
All crewmates share the same Handbag tools, memory, and assets, but they have different system prompts and RAG data sources.
from lollms_client.lollms_personality import LollmsPersonality
# Load the Crew Handbag
crew_pers = LollmsPersonality.from_handbag("./my_crew_handbag")
# The primary personality is determined by handbag.yaml (e.g., "researcher")
print(f"Active persona: {crew_pers.name}")
# List all available personas in the crew
print("Available crewmates:", crew_pers.list_crewmates())
# Dynamically switch the active persona (tools and memory remain shared)
crew_pers.switch_crewmate("coder")
# Use it in a discussion
discussion.chat(user_message="Write a Python script.", personality=crew_pers)The host application (via LollmsDiscussion or direct instantiation) dictates the working directory. The Handbag provides the resources (tools, memory, skills), but does not force its own workspace path.
If you use a Handbag-backed Personality inside a LollmsDiscussion, the Discussion's workspace_path is used for file operations. If you use the Handbag's standalone workspace, you must explicitly pass it to the Discussion.
# The Discussion dictates the workspace
discussion = LollmsDiscussion.create_new(
lollms_client=client,
db_manager=db_manager,
workspace_path="./my_project_workspace" # Explicit workspace
)
# The personality brings tools and memory, but uses the discussion's workspace
discussion.chat(
user_message="Analyze the data.",
personality=autonomous_pers
)LollmsDiscussion is a stateful, thread-safe conversational engine that bridges transient LLM tokens and permanent, versioned knowledge storage. It implements the Agentic State Machine and the Dual-Stream Artefact System. It is composed of nine orthogonal mixins:
CoreMixin: Lifecycle, ORM proxy, message CRUD, and thread-safe DB commits.ChatMixin: The agentic reasoning loop, tool execution orchestration, and stream parsing.UtilsMixin: Branch management, export normalization, and context token auditing.PromptMixin: System prompt construction and XML tag post-processing.MemoryMixin: Integration withLollmsMemoryManagerfor tiered persistent memory, episodic memory saving, and graph relationship traversal.FileImportMixin: Multi-modal ingestion (PDF, DOCX, Data) and Dual-Stream storage.InternetImportMixin: Web content extraction and quality scoring for internet-based RAG.ExportMixin: Standalone Artefact Archive (.laa) and Linked Artefact Bundle (.lab) export/import protocols.BranchMixin: Directed Acyclic Graph (DAG) branch discovery, navigation, forking, and merging.
To solve the "Context vs. Tool" paradox, artefacts are split into two streams:
- Physical Twin: Raw bytes on disk (CSV, SQLite, PNG). Executable by local tools.
- Logical Twin (
.lam): High-density text schemas. Injected into the LLM context.
Visibility Tiers:
[C]FULL: Verbatim content injected.[U]TREE_UNLOCKABLE: Listed in directory, excluded from context. The LLM can unlock this via<add_files_to_context>.[L]TREE_LOCKED: Excluded from context. Cannot be unlocked by the LLM.
The chat() method is an Agentic State Machine. The LLM is given explicit control via the <done/> tag.
- Round 1 Short-Circuit: If the LLM generates pure text without functional tags, the loop breaks immediately.
- Action Continuation: If the LLM emits
<tool>or<artifact>, the action is executed, and a mandate is injected. - Explicit Termination: The loop only breaks if the LLM emits
<done/>on a new line, or ifmax_reasoning_stepsis reached.
from lollms_client import LollmsClient, LollmsDiscussion, LollmsDataManager
client = LollmsClient(llm_binding_name="ollama", llm_binding_config={"model_name": "llama3"})
db_manager = LollmsDataManager("sqlite:///discussion.db")
discussion = LollmsDiscussion.create_new(lollms_client=client, db_manager=db_manager)
response = discussion.chat(
user_message="Analyze data.csv and build a plot.",
enable_artefacts=True,
enable_code_execution=True,
max_reasoning_steps=20
)The chat() method is the primary entry point for the LollmsDiscussion session. It orchestrates the entire agentic loop, including pre-hydration, multi-step reasoning, tool execution, and self-healing file restoration.
def chat(
self,
user_message: str,
personality=None,
branch_tip_id=None,
tools=None,
add_user_message: bool = True,
images=None,
debug: bool = False,
remove_thinking_blocks: bool = True,
enable_image_generation: bool = True,
enable_image_editing: bool = True,
auto_activate_artefacts: bool = True,
enable_inline_widgets: bool = False,
enable_notes: bool = True,
enable_skills: bool = True,
enable_forms: bool = True,
enable_books: bool = False,
enable_presentations: bool = False,
memory_manager=None,
enable_artefacts: bool = True,
enable_memory: bool = True,
enable_auto_dream: bool = True,
enable_deep_memory_pulling: bool = True,
prehydrate_rag: bool = True,
max_reasoning_steps: int = 20,
enable_in_message_status: bool = False,
enable_sub_agents: bool = False,
forward_artefact_chunks: bool = False,
fast_artefact_replicas: Optional[List[str]] = None,
tolerance_level: Optional[str] = "strict",
allow_dynamic_tools: bool = False,
enable_data_tools: bool = True,
enable_code_execution: bool = False,
suppress_images: bool = False,
debug_export: bool = False,
**kwargs
) -> Dict[str, Any]:Key Parameters:
- Core Conversation:
user_message,personality,branch_tip_id,add_user_message,images,suppress_images(setTruefor non-vision LLMs). - Feature Flags:
enable_artefacts,enable_inline_widgets,enable_notes,enable_skills,enable_forms,enable_books,enable_presentations,enable_image_generation,enable_image_editing. - Security Gates:
allow_dynamic_tools(LLM writes its own tools),enable_code_execution(arbitrary Python string execution),enable_data_tools(auto-mountssemantic_data_engineerif data files exist). - Memory & RAG:
memory_manager,enable_memory,enable_deep_memory_pulling,enable_auto_dream,prehydrate_rag. - Debugging & UI:
debug,debug_export,enable_in_message_status,remove_thinking_blocks,event_mode(see Event Modes below).
Return Value: Returns a dictionary containing the complete result of the conversational turn:
{
"user_message": LollmsMessage, # The user message object
"ai_message": LollmsMessage, # The final AI message object
"sources": List[Dict], # RAG sources retrieved
"artefacts": List[Dict], # Artifacts created/modified this turn
"memory_report": Dict, # Memory operations report
"dream_report": Optional[Dict], # Auto-dream consolidation report
"was_cancelled": bool # Cancellation status
}The chat() method supports multiple event reporting strategies via the event_mode parameter (using the EventMode enum from lollms_client.lollms_types). This allows host applications to choose between parsing raw text tags or consuming structured callback events.
| Mode | Behavior | Use Case |
|---|---|---|
EventMode.PROCESSING_TAG_MODE (Default) |
Injects <processing> tags into the MSG_TYPE_CHUNK stream. |
Simple text-based UIs, CLIs. |
EventMode.FULL_CALLBACK_MODE |
Emits specific MSG_TYPE_* events via the callback with structured metadata. No <processing> tags in text. |
Rich UI applications that render dedicated panels. |
EventMode.MIXED_MODE |
Emits both <processing> tags and structured MSG_TYPE_* events. |
Debugging or transitioning applications. |
EventMode.SILENT_MODE |
Suppresses all event reporting. Only final text is streamed. | Background tasks. |
Structured Events in FULL_CALLBACK_MODE:
MSG_TYPE_TOOL_START: Meta{"tool_name": str, "parameters": dict}MSG_TYPE_TOOL_END: Meta{"tool_name": str, "success": bool, "output": str, "error": str|None}MSG_TYPE_ARTEFACT_BUILD_START: Meta{"title": str, "art_type": str, "language": str|None, "is_patch": bool}MSG_TYPE_ARTEFACT_BUILD_END: Meta{"title": str, "art_type": str, "version": int, "success": bool, "error": str|None}MSG_TYPE_CONTEXT_UPDATE: Meta{"action": str, "files": list[str], "status": str}
The chat() method enforces a Strict Sovereign Opt-In Doctrine. Tools are ONLY activated if explicitly requested:
- Personality Handbag Tools: Auto-mounted if the personality contains a
toolsattribute. - Explicit
toolsparameter: Accepts a dict of callables OR a list of string names matching the LCP registry. - Auto-Mounted Data Tools:
semantic_data_engineeris auto-mounted IFenable_data_tools=TrueAND data files exist.
# Explicitly activate default LCP tools by name
discussion.chat(
user_message="Search the internet and run python code.",
tools=["tool_internet_search", "tool_execute_python_code"],
enable_code_execution=True
)LCP Tool Agnosticism Doctrine:
LCP tools are strictly agnostic by default. They must NEVER accept discussion_instance or lollms_client_instance as input parameters (unless explicitly required for advanced patterns like recursive sub-agent spawning). The LCP AST parser filters these internal parameters out when building the JSON schema for the LLM. Tools must rely on CWD for file resolution and communicate results back solely via their return dictionary.
To prevent context window bloat and preserve the LLM's cognitive state across turns, the ChatMixin implements three core mechanisms:
- Smart Tool Output Offloading: When a tool returns >1500 tokens, the output is intercepted before entering context. Structured data is replaced with compact markers; unstructured text is saved to a
.logfile and registered as aTREE_UNLOCKABLEartifact. - Unfinished Intent Interceptor: If the LLM stops generating before emitting a functional tag but its text matches an intent pattern (e.g., "Let me query..."), the system intercepts and forces a correction round.
- Cognitive Scratchpad Protocol: The LLM maintains a
scratchpad.mdartifact for intermediate hypotheses during multi-step analysis, preventing contradictory context accumulation.
The chat() method implements a Thread-Safe Cancellation Protocol using a boolean flag, ensuring long-running loops can be interrupted without database corruption.
# Start generation in a background thread
import threading
def run_chat():
response = discussion.chat(user_message="Analyze this 1GB CSV...")
print(response["was_cancelled"])
thread = threading.Thread(target=run_chat)
thread.start()
# User clicks "Stop"
discussion.cancel_generation()
# Check status
if discussion.is_generation_cancelled():
print("Stopping...")
# The cancel state is automatically reset after chat() returns.The loop checks the flag at four critical safe points: start of reasoning round, during streaming, post-generation, and tool cleanup. Partial messages are saved with a "[Generation cancelled by user]" marker.
The system ensures the LLM always sees error details when a tool fails, implementing a three-pronged defense against infinite retry loops:
- Raw Dict Failure Detection: Checks for
success: Falseor non-200status_codebefore examining sanitized text. - Error-Aware Sanitization: Includes the actual
errormessage in the text fed back to the LLM. FailureMemoryLoop Guard: A signature-based interceptor (tool_name::params). The first failure is recorded and the LLM is allowed to retry. A second identical call is blocked and the loop breaks, preventing token waste.
The import_file method supports 6 ingestion modes: text, text_images, images_only, ocr, data (Dual-Stream), and data_bundle (Schema Fusion).
When importing, 4 conflict resolution strategies are available via on_conflict:
suffix(default): Renames new file (e.g.,README_1.md).version: Updates existing artifact and bumps version.overwrite: Replaces content without version bump.replace: Purges all history and creates freshv1.
Artefacts are fully decoupled from discussions via two standalone archive formats:
.laa(Standalone Artefact Archive): Exports a single artefact with its entire version history..lab(Linked Artefact Bundle): Exports multiple artefacts preserving relative folder structure.
# Export/Import single artefact
discussion.artefacts.export_artefact_to_archive("main.py", "main.laa")
discussion.artefacts.import_artefact_from_archive("main.laa")
# Export/Import bundle
discussion.artefacts.export_artefact_bundle(["main.py", "index.html"], "app.lab")
discussion.artefacts.import_artefact_bundle("app.lab")A Global Artefact Library (data_workspace/standalone_artefacts/) exists for cross-discussion sharing.
If your host application needs to execute an artifact directly without the LLM loop, use the built-in path getters and sync method:
import subprocess
script_path = discussion.get_active_file_path("my_script.py")
ws_data_path = discussion.get_workspace_data_path()
if script_path:
result = subprocess.run(
["python", script_path],
capture_output=True, text=True,
cwd=ws_data_path # 🛡️ MANDATORY: Set CWD so relative paths resolve!
)
# Sync new files created by the script back to the artefact system
sync_report = discussion.sync_workspace_to_artefacts()Widgets are ephemeral, in-context, interactive educational demonstrations rendered inside the chat bubble. They are strictly constrained to 600x400px and are for teaching/visualizing concepts only (not for building apps). The backend passes raw HTML/CSS/JS to the UI via streaming events (MSG_TYPE_WIDGET_CHUNK, MSG_TYPE_WIDGET_DONE). The host application must render them inside a sandboxed <iframe> using the Blob URL protocol.
Forms allow the LLM to request structured data from the user mid-generation. When the LLM emits a <lollms_form> tag, the system parses it, fires MSG_TYPE_FORM_READY, and pauses the generation loop. The host application renders the form, collects answers, and calls discussion.submit_form_response(form_id, answers) to resume. Supported field types: text, textarea, number, range, select, radio, checkbox, rating, section.
Personalities are packaged using the Bundle Format. A bundle is a directory containing a SOUL.md file and optional resource folders (tools/, skills/, knowledge/).
from lollms_client.lollms_personality import PersonalityBundle
# Import a personality from a folder
personality = PersonalityBundle.import_bundle(
bundle_path="./personalities/my_cinema_agent",
lollms_client=client
)
# Use it in a discussion
discussion.chat(user_message="Pitch a sci-fi movie.", personality=personality)The LollmsDiscussion class is a core component for managing conversational state, including message history, long-term memory, and various context zones.
For general conversational agents that need to maintain context across turns, LollmsDiscussion simplifies the process. It automatically handles message formatting, history management, and context window limitations.
from lollms_client import LollmsClient, LollmsDiscussion, MSG_TYPE, LollmsDataManager
from ascii_colors import ASCIIColors
import os
import tempfile
# Initialize LollmsClient
try:
lc = LollmsClient(
llm_binding_name="ollama",
llm_binding_config={
"model_name": "llama3",
"host_address": "http://localhost:11434"
}
)
except Exception as e:
ASCIIColors.error(f"Failed to initialize LollmsClient for discussion: {e}")
exit()
# Create a new discussion. For persistent discussions, pass a db_manager.
# Using a temporary directory for the database for this example's simplicity
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "discussion_db.sqlite"
db_manager = LollmsDataManager(f"sqlite:///{db_path}")
discussion_id = "basic_chat_example"
discussion = db_manager.get_discussion(lc, discussion_id)
if not discussion:
ASCIIColors.yellow(f"\nCreating new discussion '{discussion_id}'...")
discussion = LollmsDiscussion.create_new(
lollms_client=lc,
db_manager=db_manager,
id=discussion_id,
autosave=True # Important for persistence
)
discussion.system_prompt = "You are a friendly and helpful AI."
discussion.commit()
else:
ASCIIColors.green(f"\nLoaded existing discussion '{discussion_id}'.")
# Define a simple callback for streaming
def chat_callback(chunk: str, msg_type: MSG_TYPE, **kwargs) -> bool:
if msg_type == MSG_TYPE.MSG_TYPE_CHUNK:
print(chunk, end="", flush=True)
return True
try:
ASCIIColors.cyan("> User: Hello, how are you today?")
response = discussion.chat(
user_message="Hello, how are you today?",
streaming_callback=chat_callback
)
print("\n") # Newline after stream finishes
ai_message = response['ai_message']
user_message = response['user_message']
ASCIIColors.green(f"< Assistant (Full): {ai_message.content[:100]}...")
# Now, continue the conversation
ASCIIColors.cyan("\n> User: Can you recommend a good book?")
response = discussion.chat(
user_message="Can you recommend a good book?",
streaming_callback=chat_callback
)
print("\n")
# You can inspect the full message history
ASCIIColors.magenta("\n--- Discussion History (last 3 messages) ---")
for msg in discussion.get_messages()[-3:]:
print(f"[{msg.sender.capitalize()}]: {msg.content[:50]}...")
except Exception as e:
ASCIIColors.error(f"An error occurred during discussion chat: {e}")The LollmsDiscussion class provides a sophisticated system for creating stateful agents that can remember information across conversations. This is achieved through a layered system of "context zones" that are automatically combined into the AI's system prompt.
The AI's context is more than just chat history. It's built from several distinct components, each with a specific purpose:
system_prompt: The foundational layer defining the AI's core identity, persona, and primary instructions.memory: The AI's long-term, persistent memory. It stores key facts about the user or topics, built up over time using thememorize()method.user_data_zone: Holds session-specific information about the user's current state or goals (e.g., "User is currently working on 'file.py'").discussion_data_zone: Contains state or meta-information about the current conversational task (e.g., "Step 1 of the plan is complete").personality_data_zone: A knowledge base or set of rules automatically injected from aLollmsPersonality'sdata_source.pruning_summary: An automatic, AI-generated summary of the oldest messages in a very long chat, used to conserve tokens without losing the gist of the early conversation.
The get_context_status() method is your window into this system, showing you exactly how these zones are combined and how many tokens they consume.
Let's see this in action with a "Personal Assistant" agent that learns about the user over time.
from lollms_client import LollmsClient, LollmsDataManager, LollmsDiscussion, MSG_TYPE
from ascii_colors import ASCIIColors
import json
import tempfile
import os
# --- 1. Setup a persistent database for our discussion ---
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "my_assistant.db"
db_manager = LollmsDataManager(f"sqlite:///{db_path}")
try:
lc = LollmsClient(llm_binding_name="ollama", llm_binding_config={"model_name": "llama3"})
except Exception as e:
ASCIIColors.error(f"Failed to initialize LollmsClient for stateful agent: {e}")
exit()
# Try to load an existing discussion or create a new one
discussion_id = "user_assistant_chat_1"
discussion = db_manager.get_discussion(lc, discussion_id)
if not discussion:
ASCIIColors.yellow("Creating a new discussion for stateful agent...")
discussion = LollmsDiscussion.create_new(
lollms_client=lc,
db_manager=db_manager,
id=discussion_id,
autosave=True # Important for persistence
)
# Let's preset some data in different zones
discussion.system_prompt = "You are a helpful Personal Assistant."
discussion.user_data_zone = "User's Name: Alex\nUser's Goal: Learn about AI development."
discussion.commit()
else:
ASCIIColors.green("Loaded existing discussion for stateful agent.")
def run_chat_turn(prompt: str):
"""Helper function to run a single chat turn and print details."""
ASCIIColors.cyan(f"\n> User: {prompt}")
# --- A. Check context status BEFORE the turn using get_context_status() ---
ASCIIColors.magenta("\n--- Context Status (Before Generation) ---")
status = discussion.get_context_status()
print(f"Max Tokens: {status.get('max_tokens')}, Current Tokens: {status.get('current_tokens')}")
# Print the system context details
if 'system_context' in status['zones']:
sys_ctx = status['zones']['system_context']
print(f" - System Context Tokens: {sys_ctx['tokens']}")
# The 'breakdown' shows the individual zones that were combined
for name, content in sys_ctx.get('breakdown', {}).items():
# For brevity, show only first line of content
print(f" -> Contains '{name}': {content.split(os.linesep)}...")
# Print the message history details
if 'message_history' in status['zones']:
msg_hist = status['zones']['message_history']
print(f" - Message History Tokens: {msg_hist['tokens']} ({msg_hist['message_count']} messages)")
print("------------------------------------------")
# --- B. Run the chat ---
ASCIIColors.green("\n< Assistant:")
response = discussion.chat(
user_message=prompt,
streaming_callback=lambda chunk, type, **k: print(chunk, end="", flush=True) if type==MSG_TYPE.MSG_TYPE_CHUNK else None
)
print() # Newline after stream
# --- C. Trigger memorization to update the 'memory' zone ---
ASCIIColors.yellow("\nTriggering memorization process...")
discussion.memorize()
discussion.commit() # Save the new memory to the DB
ASCIIColors.yellow("Memorization complete.")
# --- Run a few turns ---
run_chat_turn("Hi there! Can you recommend a good Python library for building web APIs?")
run_chat_turn("That sounds great. By the way, my favorite programming language is Rust, I find its safety features amazing.")
run_chat_turn("What was my favorite programming language again?")
# --- Final Inspection of Memory ---
ASCIIColors.magenta("\n--- Final Context Status ---")
status = discussion.get_context_status()
print(f"Max Tokens: {status.get('max_tokens')}, Current Tokens: {status.get('current_tokens')}")
if 'system_context' in status['zones']:
sys_ctx = status['zones']['system_context']
print(f" - System Context Tokens: {sys_ctx['tokens']}")
for name, content in sys_ctx.get('breakdown', {}).items():
# Print the full content of the memory zone to verify it was updated
if name == 'memory':
ASCIIColors.yellow(f" -> Full '{name}' content:\n{content}")
else:
print(f" -> Contains '{name}': {content.split(os.linesep)}...")
print("------------------------------------------")- Persistence & Initialization: The
LollmsDataManagersaves and loads the discussion. We initialize thesystem_promptanduser_data_zoneto provide initial context. get_context_status(): Before each generation, we call this method. The output shows asystem_contextblock with a token count for all combined zones and abreakdownfield that lets us see the content of each individual zone that contributed to it.memorize(): After the user mentions their favorite language,memorize()is called. The LLM analyzes the last turn, identifies this new, important fact, and appends it to thediscussion.memoryzone.- Recall: In the final turn, when asked to recall the favorite language, the AI has access to the updated
memorycontent within its system context and can correctly answer "Rust". This demonstrates true long-term, stateful memory.
When working with models, accurately determining the context window size is critical for token budgeting and preventing overflow. lollms_client employs a sophisticated 4-layer resolution cascade when get_ctx_size() is called on an LLM binding:
- Forced Context Size (
forced_ctx_size): If explicitly set (either viakwargsduring initialization or dynamically viaset_forced_ctx_size()), this value is returned immediately. It acts as the absolute source of truth, overriding all automatic detection. - Binding-Specific Detection (
_get_ctx_size): If the binding instance implements a_get_ctx_size()method (e.g., querying an API for the model's specific limit), it is called. If it returns a valid integer, that is used. - Hardcoded List (
assets/models_ctx_sizes.json): The library maintains a local JSON file mapping known model names/aliases to their context sizes. If themodel_namematches an entry, the hardcoded value is used. - Default Fallback (
default_ctx_size): If the model is completely unknown and no binding-specific method exists, it falls back to thedefault_ctx_sizeprovided during initialization.
This protocol ensures that you can always force a specific context window for testing or constrained environments, while still benefiting from automatic detection for known models.
from lollms_client import LollmsClient
# Initialize with a forced context size (Layer 1)
lc = LollmsClient(
llm_binding_name="ollama",
llm_binding_config={
"model_name": "llama3",
"forced_ctx_size": 4096 # Forces get_ctx_size() to always return 4096
}
)
print(f"Forced Context Size: {lc.get_ctx_size()}") # Output: 4096
# You can also dynamically update it at runtime
lc.llm.set_forced_ctx_size(8192)
print(f"Updated Context Size: {lc.get_ctx_size()}") # Output: 8192
# Clear the forced override to fall back to automatic detection (Layers 2-4)
lc.llm.set_forced_ctx_size(None)
print(f"Auto-detected Context Size: {lc.get_ctx_size()}") # Output: 8192 (from hardcoded list for llama3)LollmsDiscussion incorporates a biological-inspired persistent memory system (LollmsMemoryManager) consisting of five hierarchical layers (Levels 0-4):
- Level 0 — Volatile Scratchpad: Appended before the last user prompt for single-turn intermediate reasoning. Cleared after the turn.
- Level 1 — Working Memory: Active, high-importance facts currently in focus. Injected directly into the conversation context. Capped by a token budget; excess memories are automatically demoted to Deep Memory.
- Level 2 — Deep Memory: Long-term memories that have faded due to lack of use. Not injected in full. Instead, compact handles (stubs) are displayed in the context so the LLM knows they exist and can call
<mem_load id="XXXXXXXX" />to load them back to active Working Memory. - Level 3 — Archived Memory: Extremely old or low-importance memories. Never loaded automatically. Subject to automatic pruning or re-activation during the periodic dream consolidation pass.
- Level 4 — Episodic Memory: Chronological, highly preserved event and conversation interaction logs of past turns. These provide permanent historical context of past conversations and are stable against decay.
The memory manager intercepts several XML tags emitted by the model during a chat turn to manipulate its database state:
<mem_new importance="0.9">Your fact here</mem_new>: Store a new long-term memory.<mem_tag id="UUID" />: Retrieve or reference a memory (boosts importance).<mem_update id="UUID">New updated content</mem_update>: Actively update a memory's content.<mem_delete id="UUID" />: Permanently delete a memory.<mem_load id="UUID" />: Load a deep memory handle back to active Working Memory.
To manage and organize its memories over time, the AI periodically executes a "dream cycle". You can trigger this at the end of chat turns or in a background worker:
- Reinforcement: Memories that are frequently tagged/retrieved have their importance boosted logarithmically.
- Decay: Unused memories decay over time and are automatically demoted (Working → Deep → Archived).
- LLM-Assisted Selective Forgetting: Archive memories falling below the
forget_threshold(e.g., 0.02) are evaluated by the subconscious "Dreamer" LLM. If the dreamer decides they contain critical architectural/preference rules, they are restored to active status; otherwise, they are permanently pruned.
The system supports both shared and isolated scopes across multiple conversations using a single parameters layout:
- Logical User Scoping (Cross-Discussion Propagation): Point your
LollmsMemoryManagerto a shared database path and assignowner_idto the unique User ID (e.g.,owner_id="user_ParisNeo"). Memories learned in Chat A will propagate automatically to Chat B. - Discussion Isolation: Set
owner_idto the specific Discussion Thread ID (e.g.owner_id=discussion.id) to confine learned facts to that specific context. - Physical Partitioning: Provide user-specific file paths (e.g.
sqlite:///app_data/users/{user_id}/long_term_memory.db) for full physical filesystem data isolation.
When working with multimodal models, you can now control which images in a message are active and sent to the model. This is useful for focusing the AI's attention, saving tokens on expensive vision models, or allowing a user to correct which images are relevant.
This is managed at the LollmsMessage level using the toggle_image_activation() method.
from lollms_client import LollmsClient, LollmsDiscussion, LollmsDataManager, MSG_TYPE
from ascii_colors import ASCIIColors
import base64
from pathlib import Path
import os
import tempfile
# Helper to create a dummy image b64 string
def create_dummy_image(text, output_dir):
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
ASCIIColors.warning("Pillow not installed. Skipping image example.")
return None
# Try to find a common font, otherwise use default
font_path = Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") # Common Linux path
if not font_path.exists():
font_path = Path("/Library/Fonts/Arial.ttf") # Common macOS path
if not font_path.exists():
font_path = Path("C:/Windows/Fonts/arial.ttf") # Common Windows path
try:
font = ImageFont.truetype(str(font_path), 15)
except (IOError, OSError):
font = ImageFont.load_default() # Fallback to default if font not found
img = Image.new('RGB', (200, 50), color = (73, 109, 137))
d = ImageDraw.Draw(img)
d.text((10,10), text, fill=(255,255,0), font=font)
temp_file = Path(output_dir) / f"temp_img_{text.replace(' ', '_')}.png"
img.save(temp_file, "PNG")
b64 = base64.b64encode(temp_file.read_bytes()).decode('utf-8')
temp_file.unlink() # Clean up temporary file
return b64
# --- 1. Setup ---
try:
# Llava is a good multi-modal model for Ollama
# Ensure Ollama is running and 'llava' model is pulled (e.g., ollama pull llava)
lc = LollmsClient(llm_binding_name="ollama", llm_binding_config={"model_name": "llava"})
except Exception as e:
ASCIIColors.warning(f"Failed to initialize LollmsClient for image example: {e}")
ASCIIColors.warning("Skipping image activation example. Ensure Ollama is running and 'llava' model is pulled.")
exit()
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "image_discussion_db.sqlite"
db_manager = LollmsDataManager(f"sqlite:///{db_path}")
discussion = LollmsDiscussion.create_new(lollms_client=lc, db_manager=db_manager)
# --- 2. Add a message with multiple images ---
# Ensure Pillow is installed: pip install Pillow
img1_b64 = create_dummy_image("Image 1: Apple", tmpdir)
img2_b64 = create_dummy_image("Image 2: Cat", tmpdir)
img3_b64 = create_dummy_image("Image 3: Dog", tmpdir)
if not img1_b64 or not img2_b64 or not img3_b64:
ASCIIColors.warning("Skipping image activation example due to image creation failure (likely missing Pillow or font).")
exit()
discussion.add_message(
sender="user",
content="What is in the second image?",
images=[img1_b64, img2_b64, img3_b64]
)
user_message = discussion.get_messages()[-1]
# --- 3. Check the initial state ---
ASCIIColors.magenta("--- Initial State (All 3 Images Active) ---")
status_before = discussion.get_context_status()
# The 'content' field for message history will indicate the number of images if present
print(f"Message History Text (showing active images):\n{status_before['zones']['message_history']['content']}")
# --- 4. Deactivate irrelevant images ---
ASCIIColors.magenta("\n--- Deactivating images 1 and 3 ---")
user_message.toggle_image_activation(index=0, active=False) # Deactivate first image (Apple)
user_message.toggle_image_activation(index=2, active=False) # Deactivate third image (Dog)
discussion.commit() # Save changes to the message
# --- 5. Check the new state ---
ASCIIColors.magenta("\n--- New State (Only Image 2 is Active) ---")
status_after = discussion.get_context_status()
print(f"Message History Text (showing active images):\n{status_after['zones']['message_history']['content']}")
ASCIIColors.green("\nNotice the message now says '(1 image(s) attached)' instead of 3, and only the active image will be sent to the multimodal LLM.")
ASCIIColors.green("To confirm, let's ask the model what it sees:")
# This will send only the activated image
response = discussion.chat(
user_message="What do you see in the image(s) attached to my last message?",
# Use a streaming callback to see the response
streaming_callback=lambda chunk, type, **k: print(chunk, end="", flush=True) if type==MSG_TYPE.MSG_TYPE_CHUNK else None
)
print("\n")
ASCIIColors.green(f"Assistant's response after toggling images: {response['ai_message'].content}")Note: The image generation helper in the example requires Pillow (pip install Pillow). It also attempts to find common system fonts; if issues persist, you might need to install matplotlib for better font handling or provide a specific font path.
Let's create a Python Coder Agent. This agent will use a set of coding rules from a local file as its knowledge base and will be equipped with a tool to execute the code it writes. This demonstrates the synergy between LollmsPersonality (with data_source and active_mcps), LollmsDiscussion, and the MCP system.
Create a simple text file with the rules our agent must follow.
# File: coding_rules.txt
1. All Python functions must include a Google-style docstring.
2. Use type hints for all function parameters and return values.
3. The main execution block should be protected by `if __name__ == "__main__":`.
4. After defining a function, add a simple example of its usage inside the main block.
5. Print the output of the example usage to the console.
This script will define the personality, initialize the client, and run the agent.
from pathlib import Path
from lollms_client import LollmsClient, LollmsPersonality, LollmsDiscussion, MSG_TYPE
from ascii_colors import ASCIIColors, trace_exception
import json
import tempfile
import os
# A detailed callback to visualize the agent's process
def agent_callback(chunk: str, msg_type: MSG_TYPE, params: dict = None, **kwargs) -> bool:
if not params: params = {}
if msg_type == MSG_TYPE.MSG_TYPE_STEP:
ASCIIColors.yellow(f"\n>> Agent Step: {chunk}")
elif msg_type == MSG_TYPE.MSG_TYPE_STEP_START:
ASCIIColors.yellow(f"\n>> Agent Step Start: {chunk}")
elif msg_type == MSG_TYPE.MSG_TYPE_STEP_END:
result = params.get('result', '')
# Only print a snippet of result to avoid overwhelming console for large outputs
if isinstance(result, dict):
result_str = json.dumps(result)[:150] + ("..." if len(json.dumps(result)) > 150 else "")
else:
result_str = str(result)[:150] + ("..." if len(str(result)) > 150 else "")
ASCIIColors.green(f"<< Agent Step End: {chunk} -> Result: {result_str}")
elif msg_type == MSG_TYPE.MSG_TYPE_THOUGHT_CONTENT:
ASCIIColors.magenta(f"🤔 Agent Thought: {chunk}")
elif msg_type == MSG_TYPE.MSG_TYPE_TOOL_CALL:
tool_name = params.get('name', 'unknown_tool')
tool_params = params.get('parameters', {})
ASCIIColors.blue(f"🛠️ Agent Action: Called '{tool_name}' with {tool_params}")
elif msg_type == MSG_TYPE.MSG_TYPE_TOOL_OUTPUT:
ASCIIColors.cyan(f"👀 Agent Observation (Tool Output): {params.get('result', 'No result')}")
elif msg_type == MSG_TYPE.MSG_TYPE_CHUNK:
print(chunk, end="", flush=True) # Final answer stream
return True
# Create a temporary directory for the discussion DB and coding rules file
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "agent_discussion.db"
# Create the coding rules file
rules_path = Path(tmpdir) / "coding_rules.txt"
rules_content = """
1. All Python functions must include a Google-style docstring.
2. Use type hints for all function parameters and return values.
3. The main execution block should be protected by `if __name__ == "__main__":`.
4. After defining a function, add a simple example of its usage inside the main block.
5. Print the output of the example usage to the console.
"""
rules_path.write_text(rules_content.strip())
ASCIIColors.yellow(f"Created temporary coding rules file at: {rules_path}")
try:
# --- 1. Load the knowledge base from the file ---
coding_rules = rules_path.read_text()
# --- 2. Define the Coder Agent Personality ---
coder_personality = LollmsPersonality(
name="Python Coder Agent",
author="lollms-client",
category="Coding",
description="An agent that writes and executes Python code according to specific rules.",
system_prompt=(
"You are an expert Python programmer. Your task is to write clean, executable Python code based on the user's request. "
"You MUST strictly follow all rules provided in the 'Personality Static Data' section. "
"First, think about the plan. Then, use the `python_code_interpreter` tool to write and execute the code. "
"Finally, present the code and its output to the user."
),
# A) Attach the static knowledge base
data_source=coding_rules,
# B) Equip the agent with a code execution tool
active_mcps=["python_code_interpreter"]
)
# --- 3. Initialize the Client and Discussion ---
# A code-specialized model is recommended (e.g., codellama, deepseek-coder)
# Ensure Ollama is running and 'codellama' model is pulled (e.g., ollama pull codellama)
lc = LollmsClient(
llm_binding_name="ollama",
llm_binding_config={
"model_name": "codellama",
"host_address": "http://localhost:11434"
},
tools_binding_name="local_mcp" # Enable the local tool execution engine
)
# For agentic workflows, it's often good to have a persistent discussion
db_manager = LollmsDataManager(f"sqlite:///{db_path}")
discussion = LollmsDiscussion.create_new(lollms_client=lc, db_manager=db_manager)
# --- 4. The User's Request ---
user_prompt = "Write a Python function that takes two numbers and returns their sum."
ASCIIColors.yellow(f"User Prompt: {user_prompt}")
print("\n" + "="*50 + "\nAgent is now running...\n" + "="*50)
# --- 5. Run the Agentic Chat Turn ---
response = discussion.chat(
user_message=user_prompt,
personality=coder_personality,
streaming_callback=agent_callback,
max_llm_iterations=5, # Limit iterations for faster demo
tool_call_decision_temperature=0.0 # Make decision more deterministic
)
print("\n\n" + "="*50 + "\nAgent finished.\n" + "="*50)
# --- 6. Inspect the results ---
ai_message = response['ai_message']
ASCIIColors.green("\n--- Final Answer from Agent ---")
print(ai_message.content)
ASCIIColors.magenta("\n--- Tool Calls Made (from metadata) ---")
if "tool_calls" in ai_message.metadata:
print(json.dumps(ai_message.metadata["tool_calls"], indent=2))
else:
print("No tool calls recorded in message metadata.")
except Exception as e:
ASCIIColors.error(f"An error occurred during agent execution: {e}")
ASCIIColors.warning("Please ensure Ollama is running, 'codellama' model is pulled, and 'local_mcp' binding is available.")
trace_exception(e) # Provide detailed tracebackWhen you run agent_example.py, a sophisticated process unfolds:
- Initialization: The
LollmsDiscussion.chat()method is called with thecoder_personality. - Knowledge Injection: The
chatmethod sees thatpersonality.data_sourceis a string. It automatically takes the content ofcoding_rules.txtand injects it into the discussion's data zones. - Tool Activation: The method also sees
personality.active_mcps. It enables thepython_code_interpretertool for this turn. - Context Assembly: The
LollmsClientassembles a rich prompt for the LLM that includes:- The personality's
system_prompt. - The content of
coding_rules.txt(from the data zones). - The list of available tools (including
python_code_interpreter). - The user's request ("Write a function...").
- The personality's
- Reason and Act: The LLM, now fully briefed, reasons that it needs to use the
python_code_interpretertool. It formulate the Python code according to the rules it was given. - Tool Execution: The
local_mcpbinding receives the code and executes it in a secure local environment. It captures any output (stdout,stderr) and results. - Observation: The execution results are sent back to the LLM as an "observation."
- Final Synthesis: The LLM now has the user's request, the rules, the code it wrote, and the code's output. It synthesizes all of this into a final, comprehensive answer for the user.
This example showcases how lollms-client allows you to build powerful, knowledgeable, and capable agents by simply composing personalities with data and tools.
The Agent class combines LollmsClient, LollmsPersonality, and tool execution into a single, powerful unit for building autonomous agents. This enables multi-step reasoning where the agent can chain tool calls, reflect on results, and synthesize comprehensive answers.
This example demonstrates a Research Agent that:
- Uses a custom personality with research-focused system prompts
- Loads multiple tools (arXiv search + Wikipedia search)
- Performs multi-step reasoning: searches arXiv → searches Wikipedia → synthesizes findings
- Uses
Agent.generate_with_tools()for the full agentic loop
#!/usr/bin/env python3
"""
research_agent_example.py
=========================
A full agentic workflow demonstrating:
- Custom personality definition
- Multiple file-based tools
- Multi-step reasoning with tool chaining
- Agent.generate_with_tools() with rich metadata
"""
import sys
import json
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from lollms_client import LollmsClient
from lollms_client.lollms_agent import Agent, ToolsManager, AgentRole
from lollms_client.lollms_personality import LollmsPersonality
from lollms_client.lollms_types import MSG_TYPE
# ── Tool Definitions ─────────────────────────────────────────────────────────
ARXIV_TOOL = '''TOOL_LIBRARY_NAME = 'ArXiv Explorer'
TOOL_LIBRARY_DESC = 'Search scientific papers on ArXiv.'
TOOL_LIBRARY_ICON = '🔬'
def init_tools_library() -> None:
import pipmaster as pm
pm.ensure_packages({'arxiv': '>=2.1.0'})
def tool_search_papers(args: dict):
"""
Search for scientific papers on ArXiv.
Args:
args: dict with keys:
- query (str): Scientific keywords
- count (int, optional): Number of papers (default: 3)
- year_start (int, optional): Start year filter
- year_end (int, optional): End year filter
"""
import arxiv
try:
query = args.get('query', '')
count = args.get('count', 3)
search = arxiv.Search(query=query, max_results=100)
client = arxiv.Client()
results = []
for res in client.results(search):
authors = ', '.join(a.name for a in res.authors)
date = res.published.strftime('%Y-%m-%d') if res.published else "Unknown"
results.append(
f"[{res.entry_id}] {res.title}\\n"
f"Authors: {authors} | Published: {date}\\n"
f"Abstract: {res.summary[:400]}..."
)
if len(results) >= count:
break
return "\\n\\n".join(results) if results else "No papers found."
except Exception as e:
return f"Error: {str(e)}"
'''
WIKI_TOOL = '''TOOL_LIBRARY_NAME = 'Wikipedia Search'
TOOL_LIBRARY_DESC = 'Search and retrieve article summaries from Wikipedia.'
TOOL_LIBRARY_ICON = '📖'
def init_tools_library() -> None:
import pipmaster as pm
pm.ensure_packages({'wikipedia': '>=1.4.0'})
def tool_search_wikipedia(args: dict):
"""
Search Wikipedia for articles.
Args:
args: dict with keys:
- query (str): Search term
- max_results (int, optional): Max results (default: 3)
"""
import wikipedia
try:
query = args.get('query', '')
limit = args.get('max_results', 3)
search_results = wikipedia.search(query)
output = []
for title in search_results[:limit]:
try:
page = wikipedia.summary(title, sentences=5)
output.append(f"--- {title} ---\\n{page}")
except:
continue
return "\\n\\n".join(output) if output else "No results found."
except Exception as e:
return f"Error: {str(e)}"
'''
def setup_tools():
"""Create tool files in the lollms hub directory."""
tools_dir = Path.home() / ".lollms_hub" / "tools"
tools_dir.mkdir(parents=True, exist_ok=True)
arxiv_path = tools_dir / "arxiv_search.py"
wiki_path = tools_dir / "wikipedia_search.py"
if not arxiv_path.exists():
arxiv_path.write_text(ARXIV_TOOL, encoding="utf-8")
if not wiki_path.exists():
wiki_path.write_text(WIKI_TOOL, encoding="utf-8")
return str(arxiv_path), str(wiki_path)
def main():
print("=" * 70)
print("🔬 Research Agent — Multi-Step Reasoning Demo")
print("=" * 70)
# ── 1. Setup tools ──────────────────────────────────────────────────
arxiv_path, wiki_path = setup_tools()
print(f"📁 Tools ready: arxiv_search.py, wikipedia_search.py")
# ── 2. Create LollmsClient ──────────────────────────────────────────
client = LollmsClient(
llm_binding_name="llama_cpp_server",
llm_binding_config={
"models_path": "data/models/llama_cpp_models",
"binaries_path": "data/bin/llm/llama_cpp_server",
"ctx_size": 8192,
"n_gpu_layers": -1,
"n_threads": 4,
"idle_timeout": 300,
},
)
# Download/load model (Ministral 3B for this demo)
zoo = client.llm.get_zoo()
model_idx = 1 # Ministral-3-3B-Instruct-2512
chosen = zoo[model_idx]
model_file = chosen["filename"]
model_path = Path("data/models/llama_cpp_models") / model_file
if not model_path.exists():
print(f"\n⬇️ Downloading {chosen['name']}...")
client.llm.download_from_zoo(model_idx)
print(f"\n🔌 Loading {model_file}...")
client.llm.load_model(model_file)
# ── 3. Create Personality ───────────────────────────────────────────
personality = LollmsPersonality(
name="ResearchAgent",
system_prompt=(
"You are an expert research assistant with deep knowledge of "
"computer science and artificial intelligence. Your workflow:\n"
"1. Search arXiv for the latest academic papers on the topic\n"
"2. Search Wikipedia for foundational concepts and background\n"
"3. Synthesize findings into a comprehensive, well-structured report\n"
"4. Cite sources clearly and highlight key insights\n\n"
"Always use tools when available — never rely solely on training data."
),
)
# ── 4. Create Agent ────────────────────────────────────────────────
agent = Agent(
lc=client,
personality=personality,
name="ResearchAgent",
role=AgentRole.DOMAIN_EXPERT,
model_params={"temperature": 0.7, "n_predict": 2048},
max_tokens_per_turn=4096,
)
print(f"\n🤖 Agent created: {agent}")
# ── 5. Multi-step research query ──────────────────────────────────
query = (
"I want to understand the current state of reasoning in large language models. "
"Find recent papers from 2024-2025, then look up background on chain-of-thought "
"reasoning, and finally synthesize a comprehensive overview with citations."
)
print("\n" + "-" * 70)
print("📝 RESEARCH QUERY:")
print("-" * 70)
print(query)
print("-" * 70)
# ── 6. Execute agentic generation ─────────────────────────────────
print("\n🔍 Starting multi-step research (this may take several rounds)...\n")
result = agent.generate_with_tools(
prompt=query,
tools=[arxiv_path, wiki_path], # Both tools available
system_prompt=personality.system_prompt,
temperature=0.7,
n_predict=4096,
max_tool_rounds=10, # Allow multiple tool chains
auto_execute=True,
)
# ── 7. Display results ────────────────────────────────────────────
print("\n" + "=" * 70)
print("📊 EXECUTION METADATA")
print("=" * 70)
print(f"Total rounds: {result['rounds']}")
print(f"Tool calls made: {len(result['tool_calls'])}")
for i, tc in enumerate(result['tool_calls'], 1):
print(f"\n Round {tc['round']} — {tc['name']}")
print(f" Parameters: {json.dumps(tc['parameters'], indent=2)}")
# Show result summary
tr = next((r for r in result['tool_results'] if r['round'] == tc['round']), None)
if tr:
res = tr['result']
status = "✅" if res.get('success') else "❌"
output = str(res.get('output', res))[:200]
print(f" Result: {status} {output}...")
print("\n" + "=" * 70)
print("📝 FINAL SYNTHESIZED REPORT")
print("=" * 70)
print(result['response'])
# ── 8. Cleanup ─────────────────────────────────────────────────────
print("\n" + "=" * 70)
print("🧹 Cleanup")
print("=" * 70)
client.llm.unload_model()
print("✅ Done!")
if __name__ == "__main__":
main()How Multi-Step Reasoning Works:
-
Round 1: The agent receives the query and decides to search arXiv for recent papers on LLM reasoning. It emits a
<tool>fortool_search_papers. -
Tool Execution: The arXiv tool executes and returns 3 recent papers with abstracts.
-
Round 2: The agent sees the arXiv results and decides it needs background on chain-of-thought reasoning. It emits a
<tool>fortool_search_wikipedia. -
Tool Execution: The Wikipedia tool returns foundational concepts and explanations.
-
Round 3: With both academic and encyclopedic sources in context, the agent synthesizes a comprehensive report with proper citations and key insights.
-
Final Answer: The agent produces a structured response combining all gathered information.
Key Agent Configuration Options:
| Parameter | Description |
|---|---|
tools |
List of file paths (.py) or inline tool dicts |
max_tool_rounds |
Maximum agentic loops (default: 10) |
auto_execute |
If False, returns pending tool for manual execution |
system_prompt |
Override personality's system prompt for this call |
temperature |
Sampling temperature for generation |
n_predict |
Max tokens per generation step |
Using generate_with_tools_sync():
For simple fire-and-forget usage, the sync wrapper returns only the final text:
answer = agent.generate_with_tools_sync(
prompt="What are the latest papers on quantum computing?",
tools=[arxiv_path],
)
print(answer) # Just the final response stringlollms_client features a powerful, memory-efficient multi-binding architecture via Universal Lazy Profiles. Instead of eagerly instantiating all models/engines at startup (which wastes RAM and VRAM), you define declarative registries of LollmsBindingProfile configurations for any modality (LLM, TTI, TTS, STT, TTV, TTM).
Only the binding marked as is_default is loaded at startup. Other bindings are instantiated lazily on-demand when you switch to them. This system is 100% backward compatible. If you use the legacy llm_binding_name or tti_binding_name parameters, they are automatically registered as the "master" profiles and marked as defaults.
Define multiple LLM profiles to handle different domains or complexity tiers.
from lollms_client import LollmsClient, LollmsBindingProfile
# 1. Define LLM profiles declaratively (None of these are instantiated yet)
llm_profiles = {
"cloud_vision": LollmsBindingProfile(
name="cloud_vision",
binding_name="openai",
binding_config={"model_name": "gpt-4o", "service_key": "your-key"},
vision_enabled=True,
is_default=True # This one will be loaded at startup
),
"local_coder": LollmsBindingProfile(
name="local_coder",
binding_name="ollama",
binding_config={"host_address": "http://localhost:11434", "model_name": "qwen2.5-coder:7b"},
forced_context_size=32768
),
"local_fast": LollmsBindingProfile(
name="local_fast",
binding_name="ollama",
binding_config={"host_address": "http://localhost:11434", "model_name": "llama3.2:3b"}
)
}
# 2. Initialize the client (Only "cloud_vision" is instantiated)
lc = LollmsClient(llm_profiles=llm_profiles)
# 3. Use the default model
response1 = lc.generate_text("Explain quantum physics.")
# 4. Dynamically switch to the local coder (Instantiated on-the-fly and cached)
lc.switch_model("local_coder")
response2 = lc.generate_text("Write a Python script to sort a list.")
# 5. Switch back to the default cloud vision model (Retrieved from cache, no re-instantiation)
lc.switch_model("cloud_vision")The profile system is universal. You can define profiles for image generation engines and switch between a local Stable Diffusion model and a cloud DALL-E API seamlessly.
# Define TTI profiles
tti_profiles = {
"local_sd": LollmsBindingProfile(
name="local_sd",
binding_name="diffusers",
binding_config={"model_name": "stable-diffusion-v1-5"},
is_default=True
),
"cloud_dalle": LollmsBindingProfile(
name="cloud_dalle",
binding_name="openai",
binding_config={"model_name": "dall-e-3", "service_key": "your-key"}
)
}
lc = LollmsClient(llm_binding_name="ollama", tti_profiles=tti_profiles)
# Generate with default local SD
img_bytes = lc.generate_image("A cyberpunk cat")
# Switch to DALL-E 3 for a specific prompt
lc.switch_tti("cloud_dalle")
img_bytes = lc.generate_image("A hyperrealistic oil painting of a dog")For automated routing, the smart_router meta-binding evaluates incoming prompts using TF-IDF (subject matching), complexity heuristics, and weighted constraints (cost/latency) to delegate generation to the optimal child model automatically.
from lollms_client import LollmsClient
router_config = {
"routing_strategy": "cost_optimized",
"model_profiles": {
"cheap_fast": {
"binding_name": "ollama",
"binding_config": {"model_name": "qwen2.5:3b"},
"routing_profile": {"description": "fast simple tasks", "complexity_tier": 1}
},
"smart_coder": {
"binding_name": "ollama",
"binding_config": {"model_name": "qwen2.5-coder:7b"},
"routing_profile": {"description": "python code debugging", "complexity_tier": 2}
}
}
}
lc = LollmsClient(llm_binding_name="smart_router", llm_binding_config=router_config)
# The router automatically selects "cheap_fast" for simple prompts
response = lc.generate_text("What is 2+2?")
# The router automatically selects "smart_coder" for coding prompts
response = lc.generate_text("Write a Python script to sort a list.")When using a text-only LLM (or Smart Router without a VLM child), you can enable VLM collaboration via the vlm_query LCP tool. This allows the text LLM to delegate visual analysis to a secondary VLM on-demand.
# Client with a text-only LLM and a separate VLM profile
lc = LollmsClient(
llm_profiles={
"text_llm": LollmsBindingProfile(name="text_llm", binding_name="ollama", binding_config={"model_name": "llama3"}, is_default=True),
"vlm": LollmsBindingProfile(name="vlm", binding_name="ollama", binding_config={"model_name": "llava"}, vision_enabled=True)
}
)
# The user sends an image and asks a question
discussion.chat(
user_message="Look at the diagram in the image. What are the main components?",
images=["base64_encoded_image_data..."],
enable_vlm_query=True, # CRITICAL: Explicitly enable the fallback tool
max_reasoning_steps=10
)
# The LLM (llama3) will realize it needs vision, call `tool_vlm_query(0, "Identify the main components.")`,
# receive the text description from the VLM (llava), and synthesize the final answer.lollms_client introduces a powerful Smart Router binding and a dynamic VLM Query Tool to enable cost-effective, multi-model ecosystems.
The smart_router is a meta-binding that masquerades as a standard LLM binding. When selected, it accepts a dictionary of model_profiles. It evaluates incoming prompts using TF-IDF (subject matching), complexity heuristics, and weighted constraints (cost/latency) to delegate generation to the optimal child model.
from lollms_client import LollmsClient
# 1. Define the routing configuration
router_config = {
"routing_strategy": "cost_optimized", # or "balanced", "quality_optimized"
"model_profiles": {
"cheap_fast": {
"binding_name": "ollama",
"binding_config": {"model_name": "qwen2.5:3b"},
"routing_profile": {
"description": "fast simple tasks math formatting",
"cost_per_1k_tokens": 0.0,
"avg_latency_ms": 20,
"complexity_tier": 1
}
},
"smart_coder": {
"binding_name": "ollama",
"binding_config": {"model_name": "qwen2.5-coder:7b"},
"routing_profile": {
"description": "python javascript rust code debugging algorithms",
"cost_per_1k_tokens": 0.0,
"avg_latency_ms": 40,
"complexity_tier": 2
}
}
}
}
# 2. Initialize the client with the Smart Router as the primary binding
lc = LollmsClient(
llm_binding_name="smart_router",
llm_binding_config=router_config
)
# The router automatically selects "cheap_fast" for simple prompts
response = lc.generate_text("What is 2+2?")
# The router automatically selects "smart_coder" for coding prompts
response = lc.generate_text("Write a Python script to sort a list.")For VLM+LLM collaboration, the routing binding is stateless by design. Instead of forcing a VLM to process every image, lollms_client uses a dynamic LCP tool called vlm_query.
The LollmsDiscussion chat loop conditionally mounts this tool as a fallback mechanism. It is only mounted if the active binding lacks vision capabilities, another instantiated binding supports vision, and the user explicitly passes enable_vlm_query=True to discussion.chat(). The LLM can then call tool_vlm_query(image_index, query) on-demand to ask a VLM specific questions about images, preserving context and saving compute.
from lollms_client import LollmsClient, LollmsDiscussion, LollmsDataManager
# Client with a Smart Router that includes a VLM profile
lc = LollmsClient(
llm_binding_name="smart_router",
llm_binding_config={
"model_profiles": {
"text_llm": {
"binding_name": "ollama",
"binding_config": {"model_name": "llama3"},
"vision_enabled": False
},
"vlm": {
"binding_name": "ollama",
"binding_config": {"model_name": "llava"},
"vision_enabled": True
}
}
}
)
db_manager = LollmsDataManager("sqlite:///discussion.db")
discussion = LollmsDiscussion.create_new(lollms_client=lc, db_manager=db_manager)
# The user sends an image and asks a question
discussion.chat(
user_message="Look at the diagram in the image. What are the main components?",
images=["base64_encoded_image_data..."],
max_reasoning_steps=10
)
# The LLM (llama3) will realize it needs vision, call `tool_vlm_query(0, "Identify the main components in this diagram.")`,
# receive the text description from the VLM (llava), and synthesize the final answer.
# CRITICAL: The tool is ONLY mounted if enable_vlm_query=True is passed.
# Since the active model (text_llm) lacks vision, we MUST enable it explicitly.lollms-client supports a wide range of LLM backends through its binding system. This section provides practical examples of how to initialize LollmsClient for each of the major supported bindings.
Configuration for all bindings has been unified. Instead of passing parameters like host_address or model_name directly to the LollmsClient constructor, you now pass them inside a single dictionary: llm_binding_config.
This approach provides a clean, consistent, and extensible way to manage settings for any backend. Each binding defines its own set of required and optional parameters (e.g., host_address, model_name, service_key, n_gpu_layers).
# General configuration pattern
from lollms_client import LollmsClient
# ... other imports as needed
# lc = LollmsClient(
# llm_binding_name="your_binding_name",
# llm_binding_config={
# "parameter_1_for_this_binding": "value_1",
# "parameter_2_for_this_binding": "value_2",
# # ... and so on
# }
# )These bindings connect to servers running on your local network, including the core LoLLMs server itself.
This connects to a running LoLLMs service, which acts as a powerful backend providing access to models, personalities, and tools. This is the default and most feature-rich way to use lollms-client.
Prerequisites:
- A LoLLMs server instance installed and running (e.g.,
lollms-webui). - An API key can be generated from the LoLLMs web UI (under User Settings -> Security) if security is enabled.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
try:
# The default port for a LoLLMs server is 9642 (a nod to The Hitchhiker's Guide to the Galaxy).
# The API key can also be set via the LOLLMS_API_KEY environment variable.
config = {
"host_address": "http://localhost:9642",
# "service_key": "your_lollms_api_key_here" # Uncomment and replace if security is enabled
# "verify_ssl_certificate": True #if false the ssl certifcate verification will be ignored (only used when using https in lollms service address)
}
lc = LollmsClient(
llm_binding_name="lollms", # This is the default, so specifying it is optional
llm_binding_config=config
)
response = lc.generate_text("What is the answer to life, the universe, and everything?")
ASCIIColors.green(f"\nResponse from LoLLMs: {response}")
except ConnectionRefusedError:
ASCIIColors.error("Connection refused. Is the LoLLMs server running at http://localhost:9642?")
except ValueError as ve:
ASCIIColors.error(f"Initialization Error: {ve}")
except Exception as e:
ASCIIColors.error(f"An unexpected error occurred: {e}")The ollama binding connects to a running Ollama server instance on your machine or network.
Prerequisites:
- Ollama installed and running.
- Models pulled, e.g.,
ollama pull llama3.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
try:
# Configuration for a local Ollama server
lc = LollmsClient(
llm_binding_name="ollama",
llm_binding_config={
"model_name": "llama3", # Or any other model you have pulled
"host_address": "http://localhost:11434" # Default Ollama address
}
)
# Now you can use lc.generate_text(), lc.chat(), etc.
response = lc.generate_text("Why is the sky blue?")
ASCIIColors.green(f"\nResponse from Ollama: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing Ollama binding: {e}")
ASCIIColors.info("Please ensure Ollama is installed, running, and the specified model is pulled.")The pythonllamacpp binding loads and runs GGUF model files directly using the powerful llama-cpp-python library. This is ideal for high-performance, local inference on CPU or GPU.
Prerequisites:
- A GGUF model file downloaded to your machine.
llama-cpp-pythoninstalled. For GPU support, it must be compiled with the correct flags (e.g.,CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python).
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
from pathlib import Path
# Path to your GGUF model file
# IMPORTANT: Replace this with the actual path to your model file
# Example: MODEL_PATH = Path.home() / "models" / "your_model_name.gguf"
MODEL_PATH = Path("./path/to/your/model.gguf")
# Binding-specific configuration
config = {
"model_path": str(MODEL_PATH), # The path to the GGUF file
"n_gpu_layers": -1, # -1 for all layers to GPU, 0 for CPU
"n_ctx": 4096, # Context size
"seed": -1, # -1 for random seed
"chat_format": "chatml" # Or another format like 'llama-2' or 'mistral'
}
if not MODEL_PATH.exists():
ASCIIColors.warning(f"Model file not found at: {MODEL_PATH}")
ASCIIColors.warning("Skipping PythonLlamaCpp example. Please download a GGUF model and update MODEL_PATH.")
else:
try:
lc = LollmsClient(
llm_binding_name="pythonllamacpp",
llm_binding_config=config
)
response = lc.generate_text("Write a recipe for a great day.")
ASCIIColors.green(f"\nResponse from PythonLlamaCpp: {response}")
except ImportError:
ASCIIColors.error("`llama-cpp-python` not installed. Please install it (`pip install llama-cpp-python`) to run this example.")
except Exception as e:
ASCIIColors.error(f"Error initializing PythonLlamaCpp binding: {e}")
ASCIIColors.info("Please ensure the model path is correct and `llama-cpp-python` is correctly installed (with GPU support if desired).")These bindings connect to hosted LLM APIs from major providers.
Connects to the official OpenAI API to use models like GPT-4o, GPT-4, and GPT-3.5.
Prerequisites:
- An OpenAI API key (starts with
sk-...). It's recommended to set this as an environment variableOPENAI_API_KEY.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
# Set your API key as an environment variable or directly in the config
# os.environ["OPENAI_API_KEY"] = "your_openai_api_key_here"
try:
if "OPENAI_API_KEY" not in os.environ and "your_openai_api_key_here" in "your_openai_api_key_here":
ASCIIColors.warning("OPENAI_API_KEY not set in environment or hardcoded. Skipping OpenAI example.")
else:
lc = LollmsClient(
llm_binding_name="openai",
llm_binding_config={
"model_name": "gpt-4o", # Or "gpt-3.5-turbo"
"service_key": os.environ.get("OPENAI_API_KEY", "your_openai_api_key_here")
# ^ service_key is optional if OPENAI_API_KEY env var is set
}
)
response = lc.generate_text("What is the difference between AI and machine learning?")
ASCIIColors.green(f"\nResponse from OpenAI: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing OpenAI binding: {e}")
ASCIIColors.info("Please ensure your OpenAI API key is correctly set and you have access to the specified model.")Connects to Google's Gemini family of models via the Google AI Studio API.
Prerequisites:
- A Google AI Studio API key. It's recommended to set this as an environment variable
GEMINI_API_KEY.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
# Set your API key as an environment variable or directly in the config
# os.environ["GEMINI_API_KEY"] = "your_google_api_key_here"
try:
if "GEMINI_API_KEY" not in os.environ and "your_google_api_key_here" in "your_google_api_key_here":
ASCIIColors.warning("GEMINI_API_KEY not set in environment or hardcoded. Skipping Gemini example.")
else:
lc = LollmsClient(
llm_binding_name="gemini",
llm_binding_config={
"model_name": "gemini-1.5-pro-latest",
"service_key": os.environ.get("GEMINI_API_KEY", "your_google_api_key_here")
}
)
response = lc.generate_text("Summarize the plot of 'Dune' in three sentences.")
ASCIIColors.green(f"\nResponse from Gemini: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing Gemini binding: {e}")
ASCIIColors.info("Please ensure your Google AI Studio API key is correctly set and you have access to the specified model.")Connects to Anthropic's API to use the Claude family of models, including Claude 3.5 Sonnet, Opus, and Haiku.
Prerequisites:
- An Anthropic API key. It's recommended to set this as an environment variable
ANTHROPIC_API_KEY.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
# Set your API key as an environment variable or directly in the config
# os.environ["ANTHROPIC_API_KEY"] = "your_anthropic_api_key_here"
try:
if "ANTHROPIC_API_KEY" not in os.environ and "your_anthropic_api_key_here" in "your_anthropic_api_key_here":
ASCIIColors.warning("ANTHROPIC_API_KEY not set in environment or hardcoded. Skipping Claude example.")
else:
lc = LollmsClient(
llm_binding_name="claude",
llm_binding_config={
"model_name": "claude-3-5-sonnet-20240620",
"service_key": os.environ.get("ANTHROPIC_API_KEY", "your_anthropic_api_key_here")
}
)
response = lc.generate_text("What are the core principles of constitutional AI?")
ASCIIColors.green(f"\nResponse from Claude: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing Claude binding: {e}")
ASCIIColors.info("Please ensure your Anthropic API key is correctly set and you have access to the specified model.")These bindings connect to services that provide access to many different models through a single API.
OpenRouter provides a unified, OpenAI-compatible interface to access models from dozens of providers (Google, Anthropic, Mistral, Groq, etc.) with one API key.
Prerequisites:
- An OpenRouter API key (starts with
sk-or-...). It's recommended to set this as an environment variableOPENROUTER_API_KEY.
Usage:
Model names must be specified in the format provider/model-name.
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
# Set your API key as an environment variable or directly in the config
# os.environ["OPENROUTER_API_KEY"] = "your_openrouter_api_key_here"
try:
if "OPENROUTER_API_KEY" not in os.environ and "your_openrouter_api_key_here" in "your_openrouter_api_key_here":
ASCIIColors.warning("OPENROUTER_API_KEY not set in environment or hardcoded. Skipping OpenRouter example.")
else:
lc = LollmsClient(
llm_binding_name="open_router",
llm_binding_config={
"model_name": "anthropic/claude-3-haiku-20240307",
# "open_router_api_key": os.environ.get("OPENROUTER_API_KEY", "your_openrouter_api_key_here")
}
)
response = lc.generate_text("Explain what an API aggregator is, as if to a beginner.")
ASCIIColors.green(f"\nResponse from OpenRouter: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing OpenRouter binding: {e}")
ASCIIColors.info("Please ensure your OpenRouter API key is correctly set and you have access to the specified model.")While Groq is a direct provider, it's famous as an aggregator of speed. It runs open-source models on custom LPU hardware for exceptionally fast inference.
Prerequisites:
- A Groq API key. It's recommended to set this as an environment variable
GROQ_API_KEY.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
# Set your API key as an environment variable or directly in the config
# os.environ["GROQ_API_KEY"] = "your_groq_api_key_here"
try:
if "GROQ_API_KEY" not in os.environ and "your_groq_api_key_here" in "your_groq_api_key_here":
ASCIIColors.warning("GROQ_API_KEY not set in environment or hardcoded. Skipping Groq example.")
else:
lc = LollmsClient(
llm_binding_name="groq",
llm_binding_config={
"model_name": "llama3-8b-8192", # Or "mixtral-8x7b-32768"
# "groq_api_key": os.environ.get("GROQ_API_KEY", "your_groq_api_key_here")
}
)
response = lc.generate_text("Write a 3-line poem about incredible speed.")
ASCIIColors.green(f"\nResponse from Groq: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing Groq binding: {e}")
ASCIIColors.info("Please ensure your Groq API key is correctly set and you have access to the specified model.")This connects to the serverless Hugging Face Inference API, allowing experimentation with thousands of open-source models without local hardware.
Note: This API can have "cold starts," so the first request might be slow.
Prerequisites:
- A Hugging Face User Access Token (starts with
hf_...). It's recommended to set this as an environment variableHF_API_KEY.
Usage:
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
# Set your API key as an environment variable or directly in the config
# os.environ["HF_API_KEY"] = "your_hugging_face_token_here"
try:
if "HF_API_KEY" not in os.environ and "your_hugging_face_token_here" in "your_hugging_face_token_here":
ASCIIColors.warning("HF_API_KEY not set in environment or hardcoded. Skipping Hugging Face Inference API example.")
else:
lc = LollmsClient(
llm_binding_name="hugging_face_inference_api",
llm_binding_config={
"model_name": "google/gemma-1.1-7b-it", # Or other suitable models from HF
# "hf_api_key": os.environ.get("HF_API_KEY", "your_hugging_face_token_here")
}
)
response = lc.generate_text("Write a short story about a robot who discovers music.")
ASCIIColors.green(f"\nResponse from Hugging Face: {response}")
except Exception as e:
ASCIIColors.error(f"Error initializing Hugging Face Inference API binding: {e}")
ASCIIColors.info("Please ensure your Hugging Face API token is correctly set and you have access to the specified model.")```The diffusers binding leverages the Hugging Face diffusers library to run a vast array of text-to-image models locally on your own hardware (CPU or GPU). It supports models from Hugging Face and Civitai, providing everything from basic image generation to advanced, state-of-the-art image editing.
Prerequisites:
torchandtorchvisionmust be installed. For GPU acceleration, it's critical to install the version that matches your CUDA toolkit.- The binding will attempt to auto-install other requirements like
diffusers,transformers, andsafetensors.
Usage:
Example 1: Basic Text-to-Image Generation This example shows how to generate an image from a simple text prompt using a classic Stable Diffusion model.
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
from pathlib import Path
try:
# Initialize the client with the diffusers TTI binding
# Let's use a classic Stable Diffusion model for this example
lc = LollmsClient(
tti_binding_name="diffusers",
tti_binding_config={
"model_name": "runwayml/stable-diffusion-v1-5",
# Other options: "device", "torch_dtype_str", "enable_xformers"
}
)
prompt = "A high-quality photograph of an astronaut riding a horse on Mars."
ASCIIColors.yellow(f"Generating image for prompt: '{prompt}'")
# Generate the image. The result is returned as bytes.
image_bytes = lc.generate_image(prompt, width=512, height=512)
if image_bytes:
output_path = Path("./astronaut_on_mars.png")
with open(output_path, "wb") as f:
f.write(image_bytes)
ASCIIColors.green(f"Image saved successfully to: {output_path.resolve()}")
else:
ASCIIColors.error("Image generation failed.")
except Exception as e:
ASCIIColors.error(f"An error occurred with the Diffusers binding: {e}")
ASCIIColors.info("Please ensure torch is installed correctly for your hardware (CPU/GPU).")Example 2: Advanced Multi-Image Fusion with Qwen-Image-Edit-2509 This example demonstrates a cutting-edge capability: using a specialized model to fuse elements from multiple input images based on a text prompt. Here, we'll ask the model to take a person from one image and place them in the background of another.
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
from pathlib import Path
# --- IMPORTANT ---
# Replace these with actual paths to your local images
path_to_person_image = "./path/to/your/person.jpg"
path_to_background_image = "./path/to/your/background.jpg"
if not Path(path_to_person_image).exists() or not Path(path_to_background_image).exists():
ASCIIColors.warning("Input images not found. Skipping multi-image fusion example.")
ASCIIColors.warning(f"Please update 'path_to_person_image' and 'path_to_background_image'.")
else:
try:
# Initialize with the advanced Qwen multi-image editing model
lc = LollmsClient(
tti_binding_name="diffusers",
tti_binding_config={
"model_name": "Qwen/Qwen-Image-Edit-2509",
"torch_dtype_str": "bfloat16" # Recommended for this model
}
)
# The prompt guides how the images are combined
prompt = "Place the person from the first image into the scenic background of the second image."
ASCIIColors.yellow(f"Fusing images with prompt: '{prompt}'")
# The edit_image method can accept a list of image paths for fusion
fused_image_bytes = lc.edit_image(
images=[path_to_person_image, path_to_background_image],
prompt=prompt,
num_inference_steps=50
)
if fused_image_bytes:
output_path = Path("./fused_image_result.png")
with open(output_path, "wb") as f:
f.write(fused_image_bytes)
ASCIIColors.green(f"Fused image saved successfully to: {output_path.resolve()}")
else:
ASCIIColors.error("Multi-image editing failed.")
except Exception as e:
ASCIIColors.error(f"An error occurred during multi-image fusion: {e}")This powerful feature allows for complex creative tasks like character swapping, background replacement, and style transfer directly through the lollms_client library.
The library supports two primary tool execution frameworks:
A lightweight, zero-dependency local tool execution framework. LCP uses AST parsing to automatically extract tool schemas from Python docstrings.
Writing an LCP Tool:
# my_tool.py
def tool_analyze_data(file_name: str) -> dict:
"""
Analyzes a file and returns statistics.
Args:
file_name (str): Name of the file to inspect in the workspace.
"""
from pathlib import Path
path = Path(file_name)
if not path.exists():
return {"success": False, "error": f"File '{file_name}' not found."}
return {
"success": True,
"output": f"File contains {len(path.read_text())} characters.",
"prompt_injection": "Tell the user the analysis is complete."
}Security Gates:
enable_code_execution=True: Registerstool_execute_python_code(arbitrary Python string execution).allow_dynamic_tools=True: Allows the LLM to write and execute its own.pytools on the fly viatype="tool"artifacts.
Connects to external MCP-compliant servers (local stdio or remote http). Tools are namespaced using alias::tool_name.
from lollms_client.tools_bindings.mcp import MCPBinding
mcp_binding = MCPBinding(servers=[
{
"alias": "local_fs",
"type": "stdio",
"command": ["python", "-m", "mcp_server_filesystem", "/path/to/allowed/dir"]
}
])
result = mcp_binding.execute_tool(
tool_name="local_fs::read_file",
params={"path": "/path/to/allowed/dir/test.txt"}
)The memory system is a stateful, human-brain-inspired cognitive graph that persists across sessions. It combines mathematical decay with semantic graph traversal to manage context efficiently.
| Tier | Type | Scope | Context Behavior |
|---|---|---|---|
| Level 0 | Volatile Scratchpad | Single Turn | Appended before the last user prompt. Cleared after turn. |
| Level 1 | Working Memory | Active Session | Rendered verbatim in the system prompt context. |
| Level 2 | Deep Memory | Inactive / Latent | Injected as lightweight ID handles only. The LLM must use <mem_load> to promote them. |
| Level 3 | Archived Memory | Deep Storage | Completely excluded from context. Evaluated during the Dream Cycle for permanent deletion or restoration. |
| Level 4 | Episodic Memory | Permanent History | Chronological, immutable logs of past interactions. Used for retrospective queries. |
Memories do not expire linearly. Their activation energy (demotion_threshold, the memory is demoted from Level 1 to Level 2.
When a memory is retrieved, energy is spread multiplicatively to its semantically linked neighbors (defined via RELATED_TO, SUPPORTS, CONTRADICTS graph edges). This pre-warms related concepts in Deep Memory without injecting them into the context.
An asynchronous consolidation pass that runs periodically or on-demand:
- Soft-Delete Purge: Permanently deletes nodes with
0.0importance. - Centrality Auditing: Computes PageRank-like centrality to identify keystone memories.
- Synaptic Fusion: Merges redundant memories sharing identical tags.
- LLM-Assisted Forgetting: Evaluates faded memories and decides whether to restore or permanently purge them.
The LLM interacts with the memory graph using stream tags:
<mem_new tags="..." subject="..." predicate="..." object="..." importance="0.9">Fact</mem_new><mem_update id="UUID">Updated Fact</mem_update><mem_tag id="UUID" />(Boosts importance)<mem_load id="UUID" />(Promotes Deep Memory to Working Memory)<mem_rel source="UUID" target="UUID" type="SUPPORTS" weight="1.0" />
The system utilizes a Dual-Database Attachment paradigm over SQLite:
- Private Local Database (
main.memories): Bound strictly to the current discussion session. - Shared Semantic Database (
shared_mem_db.memories): Shared across all discussions inside a given project workspace.
When a shared_db_path is provided, the manager executes ATTACH DATABASE and constructs a cross-schema UNION ALL query layer, allowing the application to query both local and shared schemas transparently as a single unified graph.
Graph Traversal API (MemoryMixin):
add_memory_relationship(source_id, target_id, relationship_type, weight): Create an explicit graph edge.traverse_memory_graph(start_id, max_depth, relationship_types): Perform a Breadth-First Search (BFS) traversal to discover distant connections.get_high_centrality_memories(top_k, level): Retrieve the most connected/important memories based on graph centrality.
To prevent context window exhaustion, the Artefact System enforces a strict visibility state machine. All newly registered or tool-generated files default to TREE_UNLOCKABLE ([U]) to prevent automatic context pollution.
| Tier | Symbol | Context Behavior |
|---|---|---|
| FULL | [C] |
Content (or .lam schema) fully injected into the prompt. |
| METADATA | [M] |
Only basic metadata (filename, size, type) is injected. |
| TREE_UNLOCKABLE | [U] |
Listed in the directory index, but excluded from context. The default state. |
| TREE_LOCKED | [L] |
Excluded from context. The LLM cannot unlock this. |
| HIDDEN | — | Completely excluded from both context and the directory tree. |
The system enforces a strict state machine for artifact visibility. The LLM can trigger transitions using XML tags, while the host application or system orchestrator can trigger transitions via API calls or background processes.
| Current State | Target State | Trigger / Mechanism | Description |
|---|---|---|---|
[U] TREE_UNLOCKABLE |
[C] FULL |
<unlock_file> tag |
LLM requests to load file content into context. |
[U] TREE_UNLOCKABLE |
[L] TREE_LOCKED |
<lock_file> tag |
LLM requests to lock the file. |
[U] TREE_UNLOCKABLE |
HIDDEN |
<hide_file> tag |
LLM removes file from awareness. |
[C] FULL |
[L] TREE_LOCKED |
<lock_file> tag |
LLM requests to lock the file. |
[C] FULL |
HIDDEN |
<hide_file> tag |
LLM removes file from awareness. |
[L] TREE_LOCKED |
HIDDEN |
<hide_file> tag |
LLM removes file from awareness. |
[M] METADATA |
[C] FULL |
<unlock_file> tag |
LLM promotes from metadata to full content. |
[M] METADATA |
[L] TREE_LOCKED |
<lock_file> tag |
LLM requests to lock the file. |
[M] METADATA |
HIDDEN |
<hide_file> tag |
LLM removes file from awareness. |
HIDDEN |
[C] FULL |
Host Application API | User or host app explicitly activates the artifact. |
HIDDEN |
[U] TREE_UNLOCKABLE |
System Auto-Sync | New file detected on disk or external tool modifies hidden file. |
[L] TREE_LOCKED |
[C] FULL |
Host Application API | User or host app explicitly unlocks the file. |
[C] FULL |
[U] TREE_UNLOCKABLE |
System Auto-Prune | Context management demotes file to save tokens. |
[C] FULL |
[M] METADATA |
System Auto-Prune | Context management demotes to metadata-only. |
Note: The LLM cannot unlock a
[L](Locked) file. Once locked, it remains locked unless the host application intervenes.
The LLM can dynamically manage its context budget by emitting these tags:
<unlock_file>
filename.ext
</unlock_file>
<lock_file>
filename.ext
</lock_file>
<hide_file>
filename.ext
</hide_file>The ChatMixin intercepts these tags, applies the visibility changes via ArtefactManager.set_visibility(), and forces a continuation round so the LLM immediately utilizes the newly available (or freed) context.
The system actively blocks the LLM from unlocking files >50,000 tokens, instructing it to use tools (SQL, grep) instead. Tool-generated files >100KB are automatically registered as [U] to protect the context window.
The LollmsDiscussion.chat() method is an Agentic State Machine that goes far beyond simple tool execution.
The loop does not rely on heuristics to decide when to stop.
- Round 1 Short-Circuit: If the LLM generates pure text without
<tool>or<artifact>tags on the first round, the loop breaks immediately. - Action Continuation: If the LLM emits a functional tag, the action is executed. A mandate is injected: "When you think you finished your task, issue a final conversational text and end it with a
<done/>tag." - Explicit Termination: The loop only breaks if the LLM emits
<done/>on a new line, or ifmax_reasoning_stepsis reached. The tag is stripped from the final UI output.
To prevent context bloat during long autonomous sessions, historical messages are sanitized using a three-view protocol:
- Recent View (Last 2 Functional Actions): Raw content is preserved verbatim, including
<tool>tags and<processing>logs. This maintains perfect KV-cache alignment for multi-turn tool chaining. - Reduced View (Older Turns): Functional XML is replaced with opaque placeholders (e.g.,
[🔒SYSTEM_ARTIFACT_ANCHOR:main.py]). Execution logs are scrubbed completely. - User View (Always Verbatim): User messages are never sanitized.
To solve Multi-Turn Context Amnesia, the system stores two copies of assistant messages involving multi-step tool calls:
- UI Content: The sanitized, user-facing text.
- Virtual History: The raw, unsanitized alternation of
<tool>tags and<tool_result>payloads.
When building the context for the next turn, the sanitized UI content is discarded and replaced with the raw Virtual History. This ensures the LLM sees its exact previous execution path, preventing it from repeating tools or halting prematurely.
To build a robust autonomous application using lollms_client, follow this architectural pattern:
- Initialize the Client: Configure your LLM and Tool bindings using the unified
llm_binding_configdictionary. - Define the Personality: Use
LollmsPersonalityor import a bundle viaPersonalityBundle.import_bundle(). Include explicit autonomous instructions in thesystem_prompt(e.g., "NEVER ask the user for help", "Emit<done/>when finished"). - Set Capability Flags: Use
CapabilityFlagsto strictly gate dangerous capabilities (enable_code_execution=True,enable_sub_agents=True). - Configure the Workspace: Point the
AgentorLollmsDiscussionto an isolatedworkspace_path. - Execute the Loop: Call
agent.chat()with a highmax_reasoning_steps(80-150 for build→test→fix loops) and lowtemperature(0.1-0.3 for code).
You can query the active LLM binding to get a list of models it supports or has available. The exact information returned depends on the binding (e.g., Ollama lists local models, OpenAI lists all its API models).
from lollms_client import LollmsClient
from ascii_colors import ASCIIColors
import os
try:
# Initialize client for Ollama (or any other binding)
lc = LollmsClient(
llm_binding_name="ollama",
llm_binding_config={
"host_address": "http://localhost:11434"
# model_name is not needed just to list models
}
)
ASCIIColors.yellow("\nListing available models for the current binding:")
available_models = lc.list_models()
if isinstance(available_models, list):
for model in available_models:
# Model structure varies by binding, common fields are 'name'
model_name = model.get('name', 'N/A')
model_size = model.get('size', 'N/A') # Common for Ollama
print(f"- {model_name} (Size: {model_size})")
elif isinstance(available_models, dict) and "error" in available_models:
ASCIIColors.error(f"Error listing models: {available_models['error']}")
else:
print("Could not retrieve model list or unexpected format.")
except Exception as e:
ASCIIColors.error(f"An error occurred: {e}")