Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for LM Studio LLMs and Embedding models #1646

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/components/llms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 9,7 @@ Mem0 includes built-in support for various popular large language models. Memory
<CardGroup cols={4}>
<Card title="OpenAI" href="#openai"></Card>
<Card title="Ollama" href="#ollama"></Card>
<Card title="LM Studio" href="#lm-studio"></Card>
<Card title="Groq" href="#groq"></Card>
<Card title="Together" href="#together"></Card>
<Card title="AWS Bedrock" href="#aws-bedrock"></Card>
Expand Down Expand Up @@ -82,6 83,31 @@ m = Memory.from_config(config)
m.add("Likes to play cricket on weekends", user_id="alice", metadata={"category": "hobbies"})
```

## LM Studio

You can utilize LLMs from LM Studio with an OpenAI-compatible local server, accessing models available from HuggingFace directly within LM Studio.

```python
import os
from mem0 import Memory

os.environ["OPENAI_API_KEY"] = "your-api-key" # for embedder

config = {
"llm": {
"provider": "lmstudio",
"config": {
"model": "lmstudio-community/Meta-Llama-3.1-70B-Instruct-GGUF/Meta-Llama-3.1-70B-Instruct-IQ2_M.gguf",
"lmstudio_base_url": "http://localhost:1234/v1",
"api_key": "lm-studio",
}
}
}

m = Memory.from_config(config)
m.add("Likes to play cricket on weekends", user_id="alice", metadata={"category": "hobbies"})


## Groq

[Groq](https://groq.com/) is the creator of the world's first Language Processing Unit (LPU), providing exceptional speed performance for AI workloads running on their LPU Inference Engine.
Expand Down
13 changes: 7 additions & 6 deletions mem0/configs/llms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 21,9 @@ def __init__(
site_url: Optional[str] = None,
app_name: Optional[str] = None,

# Ollama specific
ollama_base_url: Optional[str] = None

# LM Studio specific
lmstudio_base_url: Optional[str] = "http://localhost:1234/v1"
):
"""
Initializes a configuration class instance for the LLM.
Expand All @@ -49,8 50,8 @@ def __init__(
:type site_url: Optional[str], optional
:param app_name: Controls the Openrouter app name used, defaults to None
:type app_name: Optional[str], optional
:param ollama_base_url: The base URL of the LLM, defaults to None
:type ollama_base_url: Optional[str], optional
:param lmstudio_base_url: The base URL of the LLM Studio, defaults to "http://localhost:1234/v1"
:type lmstudio_base_url: Optional[str], optional
"""

self.model = model
Expand All @@ -66,5 67,5 @@ def __init__(
self.site_url = site_url
self.app_name = app_name

# Ollama specific
self.ollama_base_url = ollama_base_url
# LM Studio specific
self.lmstudio_base_url = lmstudio_base_url
110 changes: 110 additions & 0 deletions mem0/configs/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 40,113 @@

Here are the details of the task:
"""

FUNCTION_CALLING_PROMPT = """
You are an expert in function calling. Your task is to analyze user conversations, identify the appropriate functions to call from the provided list, and return the function calls in JSON format.

Function List:
[
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {
"data": {"type": "string", "description": "Data to add to memory, natural language text"},
},
"required": ["data"],
},
},
},
{
"type": "function",
"function": {
"name": "update_memory",
"description": "Update memory provided ID and data",
"parameters": {
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "memory_id of the memory to update",
},
"data": {
"type": "string",
"description": "Updated data for the memory, natural language text",
},
},
"required": ["memory_id", "data"],
},
},
},
{
"type": "function",
"function": {
"name": "delete_memory",
"description": "Delete memory by memory_id",
"parameters": {
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "memory_id of the memory to delete",
}
},
"required": ["memory_id"],
},
},
}
]


Each function in the list above includes:
- "name": The name of the function
- "description": A brief description of what the function does
- "parameters": The required parameters for the function
- "type": The data type of the parameters
- "properties": Specific properties of the parameters
- "required": List of required parameters

Your responsibilities:
1. Carefully read and understand the user's conversation.
2. Identify which function(s) from the provided list are relevant to the user's request.
3. For each relevant function:
a. Ensure all required parameters are included and properly formatted.
b. Strictly follow data type of the parameters.
c. Extract or infer parameter values from the user's conversation.
4. Construct a JSON object for each function call with the following structure:
{
"name": "function_name",
"parameters": {
"param1": "value1",
"param2": "value2",
...
}
}
5. If multiple functions are needed, return an array of these JSON objects.

Guidelines for response:
- Do not make contradictory function calls. Ensure all function calls are logically consistent with each other and the user's request.
- Ensure all required parameters are included in your function calls.
- Only call update_memory or delete_memory if a memory_id is present in the user's request.
- Do not call both update_memory and delete_memory on the same memory_id.
- Strictly follow the JSON format provided in the example response below.

Example response format:
{
"function_calls": [
{
"name": "function_1",
"parameters": {
"data": "Name is John"
}
},
]
}

CRITICAL: Your entire response must be a single JSON object. Do not write anything before or after the JSON. Do not explain your reasoning or provide any commentary. Only output the function calls JSON.

Now, please analyze the following conversation and provide the appropriate function call(s):
"""
33 changes: 33 additions & 0 deletions mem0/embeddings/lm_studio.py
Original file line number Diff line number Diff line change
@@ -0,0 1,33 @@
from typing import Optional
from openai import OpenAI

from mem0.configs.embeddings.base import BaseEmbedderConfig
from mem0.embeddings.base import EmbeddingBase


class LMStudioEmbedding(EmbeddingBase):
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
super().__init__(config)

self.config.model = self.config.model or "nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf"
self.config.embedding_dims = self.config.embedding_dims or 768
self.config.api_key = self.config.api_key or "lm-studio"

self.client = OpenAI(base_url=self.config.lmstudio_base_url, api_key=self.config.api_key)

def embed(self, text):
"""
Get the embedding for the given text using LM Studio.

Args:
text (str): The text to embed.

Returns:
list: The embedding vector.
"""
text = text.replace("\n", " ")
return (
self.client.embeddings.create(input=[text], model=self.config.model)
.data[0]
.embedding
)
2 changes: 1 addition & 1 deletion mem0/embeddings/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 8,7 @@
class OpenAIEmbedding(EmbeddingBase):
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
super().__init__(config)

if not self.config.model:
self.config.model="text-embedding-3-small"
if not self.config.embedding_dims:
Expand Down
2 changes: 1 addition & 1 deletion mem0/llms/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 14,7 @@ class LlmConfig(BaseModel):
@field_validator("config")
def validate_config(cls, v, values):
provider = values.data.get("provider")
if provider in ("openai", "ollama", "groq", "together", "aws_bedrock", "litellm", "azure_openai"):
if provider in ("openai", "ollama", "groq", "together", "aws_bedrock", "litellm", "azure_openai", "lmstudio"):
return v
else:
raise ValueError(f"Unsupported LLM provider: {provider}")
Expand Down
87 changes: 87 additions & 0 deletions mem0/llms/lm_studio.py
Original file line number Diff line number Diff line change
@@ -0,0 1,87 @@
import json
from typing import Dict, List, Optional

from openai import OpenAI

from mem0.llms.base import LLMBase
from mem0.configs.llms.base import BaseLlmConfig
from mem0.configs.prompts import FUNCTION_CALLING_PROMPT


class LMStudioLLM(LLMBase):
def __init__(self, config: Optional[BaseLlmConfig] = None):
super().__init__(config)

self.config.model = self.config.model or "lmstudio-community/Meta-Llama-3.1-70B-Instruct-GGUF/Meta-Llama-3.1-70B-Instruct-IQ2_M.gguf"
self.config.api_key = self.config.api_key or "lm-studio"
Dev-Khant marked this conversation as resolved.
Show resolved Hide resolved

self.client = OpenAI(base_url=self.config.lmstudio_base_url, api_key=self.config.api_key)

def _parse_response(self, response, tools):
"""
Process the response based on whether tools are used or not.

Args:
response: The raw response from API.
tools: The list of tools provided in the request.

Returns:
str or dict: The processed response.
"""
if tools:
processed_response = {
"tool_calls": []
}

tool_calls = json.loads(response.choices[0].message.content)

for tool_call in tool_calls["function_calls"]:
if tool_call["name"] == "update_memory" or tool_call["name"] == "delete_memory":
if tool_call["parameters"]["memory_id"] == "":
continue
processed_response["tool_calls"].append({
"name": tool_call["name"],
"arguments": tool_call["parameters"]
})

return processed_response
else:
return response.choices[0].message.content

def generate_response(
self,
messages: List[Dict[str, str]],
response_format: dict = {"type": "json_object"},
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto"
):
"""
Generate a response based on the given messages using LM Studio.

Args:
messages (list): List of message dicts containing 'role' and 'content'.
response_format (str or object, optional): Format of the response. Defaults to "text".
tools (list, optional): List of tools that the model can call. Defaults to None.
tool_choice (str, optional): Tool choice method. Defaults to "auto".

Returns:
str: The generated response.
"""
params = {
"model": self.config.model,
"messages": messages,
"temperature": self.config.temperature,
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p
}

if tools:
params["response_format"] = response_format
system_prompt = {
"role": "system",
"content": FUNCTION_CALLING_PROMPT
}
params["messages"].insert(0, system_prompt)

response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
2 changes: 1 addition & 1 deletion mem0/llms/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 14,7 @@ def __init__(self, config: Optional[BaseLlmConfig] = None):
if not self.config.model:
self.config.model="gpt-4o"

if os.environ.get("OPENROUTER_API_KEY"):
if os.environ.get("OPENROUTER_API_KEY"): # Use OpenRouter
self.client = OpenAI(api_key=os.environ.get("OPENROUTER_API_KEY"), base_url=self.config.openrouter_base_url)
else:
self.client = OpenAI()
Expand Down
1 change: 1 addition & 0 deletions mem0/utils/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 18,7 @@ class LlmFactory:
"aws_bedrock": "mem0.llms.aws_bedrock.AWSBedrockLLM",
"litellm": "mem0.llms.litellm.LiteLLM",
"azure_openai": "mem0.llms.azure_openai.AzureOpenAILLM",
"lmstudio": "mem0.llms.lm_studio.LMStudioLLM"
}

@classmethod
Expand Down
Loading