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

Configurable presets to support easy extension of MemGPT's function set #420

Merged
merged 17 commits into from
Nov 13, 2023
Merged
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
256 changes: 62 additions & 194 deletions memgpt/agent.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion memgpt/autogen/memgpt_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from memgpt.persistence_manager import LocalStateManager
import memgpt.system as system
import memgpt.constants as constants
import memgpt.presets as presets
import memgpt.presets.presets as presets
from memgpt.personas import personas
from memgpt.humans import humans
from memgpt.config import AgentConfig
Expand Down
2 changes: 1 addition & 1 deletion memgpt/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from memgpt.cli.cli_config import configure
import memgpt.agent as agent
import memgpt.system as system
import memgpt.presets as presets
import memgpt.presets.presets as presets
import memgpt.constants as constants
import memgpt.personas.personas as personas
import memgpt.humans.humans as humans
Expand Down
2 changes: 1 addition & 1 deletion memgpt/cli/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
def configure():
"""Updates default MemGPT configurations"""

from memgpt.presets import DEFAULT_PRESET, preset_options
from memgpt.presets.presets import DEFAULT_PRESET, preset_options

MemGPTConfig.create_config_dir()

Expand Down
14 changes: 12 additions & 2 deletions memgpt/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import glob
import inspect
import random
import string
import json
Expand All @@ -23,7 +24,7 @@
import memgpt.constants as constants
import memgpt.personas.personas as personas
import memgpt.humans.humans as humans
from memgpt.presets import DEFAULT_PRESET, preset_options
from memgpt.presets.presets import DEFAULT_PRESET, preset_options


model_choices = [
Expand Down Expand Up @@ -243,7 +244,7 @@ def create_config_dir():
if not os.path.exists(MEMGPT_DIR):
os.makedirs(MEMGPT_DIR, exist_ok=True)

folders = ["personas", "humans", "archival", "agents"]
folders = ["personas", "humans", "archival", "agents", "functions", "system_prompts", "presets"]
for folder in folders:
if not os.path.exists(os.path.join(MEMGPT_DIR, folder)):
os.makedirs(os.path.join(MEMGPT_DIR, folder))
Expand Down Expand Up @@ -339,6 +340,15 @@ def load(cls, name: str):
assert os.path.exists(agent_config_path), f"Agent config file does not exist at {agent_config_path}"
with open(agent_config_path, "r") as f:
agent_config = json.load(f)

# allow compatibility accross versions
class_args = inspect.getargspec(cls.__init__).args
agent_fields = list(agent_config.keys())
for key in agent_fields:
if key not in class_args:
utils.printd(f"Removing missing argument {key} from agent config")
del agent_config[key]

return cls(**agent_config)


Expand Down
3 changes: 3 additions & 0 deletions memgpt/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@

REQ_HEARTBEAT_MESSAGE = "request_heartbeat == true"
FUNC_FAILED_HEARTBEAT_MESSAGE = "Function call failed"
FUNCTION_PARAM_NAME_REQ_HEARTBEAT = "request_heartbeat"
FUNCTION_PARAM_TYPE_REQ_HEARTBEAT = "boolean"
FUNCTION_PARAM_DESCRIPTION_REQ_HEARTBEAT = "Request an immediate heartbeat after function execution. Set to 'true' if you want to send a follow-up message or run a follow-up function."
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE = 5
Empty file added memgpt/functions/__init__.py
Empty file.
168 changes: 168 additions & 0 deletions memgpt/functions/function_sets/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from typing import Optional
import datetime
import os
import json
import math

from ...constants import MAX_PAUSE_HEARTBEATS, RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE

### Functions / tools the agent can use
# All functions should return a response string (or None)
# If the function fails, throw an exception


def send_message(self, message: str):
"""
Sends a message to the human user.

Args:
message (str): Message contents. All unicode (including emojis) are supported.

Returns:
Optional[str]: None is always returned as this function does not produce a response.
"""
self.interface.assistant_message(message)
return None


# Construct the docstring dynamically (since it should use the external constants)
pause_heartbeats_docstring = f"""
Temporarily ignore timed heartbeats. You may still receive messages from manual heartbeats and other events.

Args:
minutes (int): Number of minutes to ignore heartbeats for. Max value of {MAX_PAUSE_HEARTBEATS} minutes ({MAX_PAUSE_HEARTBEATS // 60} hours).

Returns:
str: Function status response
"""


def pause_heartbeats(self, minutes: int):
minutes = min(MAX_PAUSE_HEARTBEATS, minutes)

# Record the current time
self.pause_heartbeats_start = datetime.datetime.now()
# And record how long the pause should go for
self.pause_heartbeats_minutes = int(minutes)

return f"Pausing timed heartbeats for {minutes} min"


pause_heartbeats.__doc__ = pause_heartbeats_docstring


def core_memory_append(self, name: str, content: str):
"""
Append to the contents of core memory.

Args:
name (str): Section of the memory to be edited (persona or human).
content (str): Content to write to the memory. All unicode (including emojis) are supported.

Returns:
Optional[str]: None is always returned as this function does not produce a response.
"""
new_len = self.memory.edit_append(name, content)
self.rebuild_memory()
return None


def core_memory_replace(self, name: str, old_content: str, new_content: str):
"""
Replace to the contents of core memory. To delete memories, use an empty string for new_content.

Args:
name (str): Section of the memory to be edited (persona or human).
old_content (str): String to replace. Must be an exact match.
new_content (str): Content to write to the memory. All unicode (including emojis) are supported.

Returns:
Optional[str]: None is always returned as this function does not produce a response.
"""
new_len = self.memory.edit_replace(name, old_content, new_content)
self.rebuild_memory()
return None


def conversation_search(self, query: str, page: Optional[int] = 0):
"""
Search prior conversation history using case-insensitive string matching.

Args:
query (str): String to search for.
page (int): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).

Returns:
str: Query result string
"""
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
results, total = self.persistence_manager.recall_memory.text_search(query, count=count, start=page * count)
num_pages = math.ceil(total / count) - 1 # 0 index
if len(results) == 0:
results_str = f"No results found."
else:
results_pref = f"Showing {len(results)} of {total} results (page {page}/{num_pages}):"
results_formatted = [f"timestamp: {d['timestamp']}, {d['message']['role']} - {d['message']['content']}" for d in results]
results_str = f"{results_pref} {json.dumps(results_formatted)}"
return results_str


def conversation_search_date(self, start_date: str, end_date: str, page: Optional[int] = 0):
"""
Search prior conversation history using a date range.

Args:
start_date (str): The start of the date range to search, in the format 'YYYY-MM-DD'.
end_date (str): The end of the date range to search, in the format 'YYYY-MM-DD'.
page (int): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).

Returns:
str: Query result string
"""
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
results, total = self.persistence_manager.recall_memory.date_search(start_date, end_date, count=count, start=page * count)
num_pages = math.ceil(total / count) - 1 # 0 index
if len(results) == 0:
results_str = f"No results found."
else:
results_pref = f"Showing {len(results)} of {total} results (page {page}/{num_pages}):"
results_formatted = [f"timestamp: {d['timestamp']}, {d['message']['role']} - {d['message']['content']}" for d in results]
results_str = f"{results_pref} {json.dumps(results_formatted)}"
return results_str


def archival_memory_insert(self, content: str):
"""
Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.

Args:
content (str): Content to write to the memory. All unicode (including emojis) are supported.

Returns:
Optional[str]: None is always returned as this function does not produce a response.
"""
self.persistence_manager.archival_memory.insert(content)
return None


def archival_memory_search(self, query: str, page: Optional[int] = 0):
"""
Search archival memory using semantic (embedding-based) search.

Args:
query (str): String to search for.
page (Optional[int]): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).

Returns:
str: Query result string
"""
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
results, total = self.persistence_manager.archival_memory.search(query, count=count, start=page * count)
num_pages = math.ceil(total / count) - 1 # 0 index
if len(results) == 0:
results_str = f"No results found."
else:
results_pref = f"Showing {len(results)} of {total} results (page {page}/{num_pages}):"
results_formatted = [f"timestamp: {d['timestamp']}, memory: {d['content']}" for d in results]
results_str = f"{results_pref} {json.dumps(results_formatted)}"
return results_str
126 changes: 126 additions & 0 deletions memgpt/functions/function_sets/extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import Optional
import os
import json
import requests


from ...constants import MESSAGE_CHATGPT_FUNCTION_MODEL, MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE, MAX_PAUSE_HEARTBEATS
from ...openai_tools import completions_with_backoff as create


def message_chatgpt(self, message: str):
"""
Send a message to a more basic AI, ChatGPT. A useful resource for asking questions. ChatGPT does not retain memory of previous interactions.

Args:
message (str): Message to send ChatGPT. Phrase your message as a full English sentence.

Returns:
str: Reply message from ChatGPT
"""
message_sequence = [
{"role": "system", "content": MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE},
{"role": "user", "content": str(message)},
]
response = create(
model=MESSAGE_CHATGPT_FUNCTION_MODEL,
messages=message_sequence,
# functions=functions,
# function_call=function_call,
)

reply = response.choices[0].message.content
return reply


def read_from_text_file(self, filename: str, line_start: int, num_lines: Optional[int] = 1):
"""
Read lines from a text file.

Args:
filename (str): The name of the file to read.
line_start (int): Line to start reading from.
num_lines (Optional[int]): How many lines to read (defaults to 1).

Returns:
str: Text read from the file
"""
max_chars = 500
trunc_message = True
if not os.path.exists(filename):
raise FileNotFoundError(f"The file '{filename}' does not exist.")

if line_start < 1 or num_lines < 1:
raise ValueError("Both line_start and num_lines must be positive integers.")

lines = []
chars_read = 0
with open(filename, "r") as file:
for current_line_number, line in enumerate(file, start=1):
if line_start <= current_line_number < line_start + num_lines:
chars_to_add = len(line)
if max_chars is not None and chars_read + chars_to_add > max_chars:
# If adding this line exceeds MAX_CHARS, truncate the line if needed and stop reading further.
excess_chars = (chars_read + chars_to_add) - max_chars
lines.append(line[:-excess_chars].rstrip("\n"))
if trunc_message:
lines.append(f"[SYSTEM ALERT - max chars ({max_chars}) reached during file read]")
break
else:
lines.append(line.rstrip("\n"))
chars_read += chars_to_add
if current_line_number >= line_start + num_lines - 1:
break

return "\n".join(lines)


def append_to_text_file(self, filename: str, content: str):
"""
Append to a text file.

Args:
filename (str): The name of the file to append to.
content (str): Content to append to the file.

Returns:
Optional[str]: None is always returned as this function does not produce a response.
"""
if not os.path.exists(filename):
raise FileNotFoundError(f"The file '{filename}' does not exist.")

with open(filename, "a") as file:
file.write(content + "\n")


def http_request(self, method: str, url: str, payload_json: Optional[str] = None):
"""
Generates an HTTP request and returns the response.

Args:
method (str): The HTTP method (e.g., 'GET', 'POST').
url (str): The URL for the request.
payload_json (Optional[str]): A JSON string representing the request payload.

Returns:
dict: The response from the HTTP request.
"""
try:
headers = {"Content-Type": "application/json"}

# For GET requests, ignore the payload
if method.upper() == "GET":
print(f"[HTTP] launching GET request to {url}")
response = requests.get(url, headers=headers)
else:
# Validate and convert the payload for other types of requests
if payload_json:
payload = json.loads(payload_json)
else:
payload = {}
print(f"[HTTP] launching {method} request to {url}, payload=\n{json.dumps(payload, indent=2)}")
response = requests.request(method, url, json=payload, headers=headers)

return {"status_code": response.status_code, "headers": dict(response.headers), "body": response.text}
except Exception as e:
return {"error": str(e)}
Loading