Skip to content

Commit

Permalink
v2.0.93; search and load old conversations
Browse files Browse the repository at this point in the history
  • Loading branch information
eliranwong committed Jan 20, 2024
1 parent ba7ece8 commit 7b80c43
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 23 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Read more at: https://github.com/eliranwong/letmedoit/wiki#an-interview

# Recent Additions

[Search and Load Old Conversations](https://github.com/eliranwong/letmedoit/wiki/Search-and-Load-Old-Conversations)

![search_chat_records_0](https://github.com/eliranwong/letmedoit/assets/25262722/f2fb9c1c-d239-4723-b9ff-65634863d96f)

[System Tray for Quick Access](https://github.com/eliranwong/letmedoit/wiki/System-Tray-for-Quick-Access)

<img width="1440" alt="system_tray" src="https://github.com/eliranwong/letmedoit/assets/25262722/639d9cd4-a42a-4ca6-85bb-c290f38c023d">
Expand Down Expand Up @@ -114,6 +118,14 @@ LetMeDoIt AI just got smarter with memory retention!

Latest LetMeDoIt Plugins allow you to acheive variety of tasks with natural language:

* [NEW] search old conversations

> search for "joke" in chat records
* [NEW] load old conversations

> Load chat records with this ID: 2024-01-20_19_21_04
* [NEW] connect a sqlite file and fetch data or make changes

> connect /temp/my_database.sqlite and tell me about the tables that it contains
Expand Down
12 changes: 12 additions & 0 deletions package/letmedoit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Read more at: https://github.com/eliranwong/letmedoit/wiki#an-interview

# Recent Additions

[Search and Load Old Conversations](https://github.com/eliranwong/letmedoit/wiki/Search-and-Load-Old-Conversations)

![search_chat_records_0](https://github.com/eliranwong/letmedoit/assets/25262722/f2fb9c1c-d239-4723-b9ff-65634863d96f)

[System Tray for Quick Access](https://github.com/eliranwong/letmedoit/wiki/System-Tray-for-Quick-Access)

<img width="1440" alt="system_tray" src="https://github.com/eliranwong/letmedoit/assets/25262722/639d9cd4-a42a-4ca6-85bb-c290f38c023d">
Expand Down Expand Up @@ -114,6 +118,14 @@ LetMeDoIt AI just got smarter with memory retention!

Latest LetMeDoIt Plugins allow you to acheive variety of tasks with natural language:

* [NEW] search old conversations

> search for "joke" in chat records
* [NEW] load old conversations

> Load chat records with this ID: 2024-01-20_19_21_04
* [NEW] connect a sqlite file and fetch data or make changes

> connect /temp/my_database.sqlite and tell me about the tables that it contains
Expand Down
8 changes: 4 additions & 4 deletions package/letmedoit/plugins/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from pathlib import Path
from chromadb.config import Settings
import uuid, os, chromadb, getpass, geocoder, datetime, json
import numpy as np
from numpy.linalg import norm

memory_store = os.path.join(config.getFiles(), "memory")
Path(memory_store).mkdir(parents=True, exist_ok=True)
chroma_client = chromadb.PersistentClient(memory_store, Settings(anonymized_telemetry=False))

#import numpy as np
#from numpy.linalg import norm
#def cosine_similarity(A, B):
# cosine = np.dot(A, B) / (norm(A) * norm(B))
# return cosine
Expand Down Expand Up @@ -78,7 +78,7 @@ def query_vectors(collection, query, n):
def retrieve_memories(function_args):
query = function_args.get("query") # required
collection = get_or_create_collection("memories")
res = query_vectors(collection, query, config.memoryClosestMatchesNumber)
res = query_vectors(collection, query, config.memoryClosestMatches)
if config.developer:
config.print(config.divider)
print(">>> retrieved memories: ")
Expand Down Expand Up @@ -133,7 +133,7 @@ def retrieve_memories(function_args):
}
}

config.inputSuggestions += ["Remember, "]
config.inputSuggestions += ["Remember, ", "Do you remember?"]
config.pluginsWithFunctionCall += ["save_memory", "retrieve_memories"]
config.chatGPTApiFunctionSignatures += [functionSignature1, functionSignature2]
config.chatGPTApiAvailableFunctions["save_memory"] = save_memory
Expand Down
1 change: 1 addition & 0 deletions package/letmedoit/plugins/pronounce words.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ def pronunce_words(function_args):
config.pluginsWithFunctionCall.append("pronunce_words")
config.chatGPTApiFunctionSignatures.append(functionSignature)
config.chatGPTApiAvailableFunctions["pronunce_words"] = pronunce_words
config.inputSuggestions.append("pronunce ")
146 changes: 146 additions & 0 deletions package/letmedoit/plugins/search chat records.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
LetMeDoIt AI Plugin - search chat records
search and open old chat records
[FUNCTION_CALL]
"""

from letmedoit import config
from letmedoit.health_check import HealthCheck
from pathlib import Path
from chromadb.config import Settings
import uuid, os, chromadb, re
from letmedoit.utils.shared_utils import SharedUtil
from prompt_toolkit import print_formatted_text, HTML

chat_store = os.path.join(config.getFiles(), "chats")
Path(chat_store).mkdir(parents=True, exist_ok=True)
chroma_client = chromadb.PersistentClient(chat_store, Settings(anonymized_telemetry=False))

def get_or_create_collection(collection_name):
collection = chroma_client.get_or_create_collection(
name=collection_name,
metadata={"hnsw:space": "cosine"},
embedding_function=HealthCheck.getEmbeddingFunction(),
)
return collection

def add_vector(collection, text, metadata):
id = str(uuid.uuid4())
collection.add(
documents = [text],
metadatas = [metadata],
ids = [id]
)

def query_vectors(collection, query, n):
return collection.query(
query_texts=[query],
n_results = n,
)

def save_chat_record(timestamp, order, record):
role = record.get("role", "")
content = record.get("content", "")
if role and role in ("user", "assistant") and content:
collection = get_or_create_collection("chats")
metadata = {
"timestamp": timestamp,
"order": order,
"role": role,
}
add_vector(collection, content, metadata)
config.save_chat_record = save_chat_record

def search_chats(function_args):
query = function_args.get("query") # required
config.print3(f"""Query: {query}""")
collection = get_or_create_collection("chats")
res = query_vectors(collection, query, config.chatRecordClosestMatches)
config.stopSpinning()
if res:
exampleID = ""
# display search results
config.print2(config.divider)
print(">>> retrieved chat records: ")
for metadata, document in zip(res["metadatas"][0], res["documents"][0]):
config.print(config.divider)
config.print3(f"""Chat ID: {metadata["timestamp"]}""")
if not exampleID:
exampleID = metadata["timestamp"]
config.print3(f"""Order: {metadata["order"]}""")
config.print3(f"""Role: {metadata["role"]}""")
config.print3(f"""Content: {document}""")
config.print(config.divider)
config.print2("Tips: You can load old chat records by quoting a chat ID or timestamp, e.g.")
config.print(f">>> Load chat records with this ID: {exampleID}")
config.print2(config.divider)
return ""

def load_chats(function_args):
config.stopSpinning()
timestamp = function_args.get("id") # required
config.print3(f"Loading chat records: {timestamp} ...")

try:
folderPath = os.path.join(config.getFiles(), "chats", re.sub("^([0-9]+?\-[0-9]+?)\-.*?$", r"\1", timestamp))
chatFile = os.path.join(folderPath, f"{timestamp}.txt")
with open(chatFile, "r", encoding="utf-8") as fileObj:
messages = fileObj.read()
currentMessages = eval(messages)
if type(currentMessages) == list:
config.currentMessages = currentMessages
# display loaded messages
print("")
for index, i in enumerate(config.currentMessages):
role = i.get("role", "")
content = i.get("content", "")
if role and role in ("user", "assistant") and content:
if role == "user":
print_formatted_text(HTML(f"<{config.terminalPromptIndicatorColor1}>>>> </{config.terminalPromptIndicatorColor1}><{config.terminalCommandEntryColor1}>{content}</{config.terminalCommandEntryColor1}>"))
else:
config.print(content)
if role == 'assistant' and not index == len(config.currentMessages) - 2:
print("")
else:
config.print3(f"Failed to load chat records '{timestamp}' due to invalid format!")
except:
config.print3(f"Failed to load chat records: {timestamp}\n")
SharedUtil.showErrors()

functionSignature1 = {
"name": "search_chats",
"description": """Search chat records""",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query in detail"
},
},
"required": ["query"]
}
}

functionSignature2 = {
"name": "load_chats",
"description": """Load or open old chat records if chat ID or timestamp is given""",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The chat ID or timestamp"
},
},
"required": ["id"]
}
}

config.inputSuggestions += ["Search chat records: ", "Load chat records with this ID: "]
config.pluginsWithFunctionCall += ["search_chats", "load_chats"]
config.chatGPTApiFunctionSignatures += [functionSignature1, functionSignature2]
config.chatGPTApiAvailableFunctions["search_chats"] = search_chats
config.chatGPTApiAvailableFunctions["load_chats"] = load_chats
64 changes: 49 additions & 15 deletions package/letmedoit/utils/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ def setup(self):

self.actions = {
".new": ("start a new chat [ctrl+n]", None),
".save": ("save content [ctrl+s]", lambda: self.saveChat(config.currentMessages, openFile=True)),
".save": ("save content", lambda: self.saveChat(config.currentMessages)),
".export": ("export content [ctrl+s]", lambda: self.exportChat(config.currentMessages)),
#".instruction": ("run a predefined instruction", self.runInstruction),
".context": ("change chat context [ctrl+o]", None),
".contextintegration": ("change chat context integration", self.setContextIntegration),
Expand All @@ -139,7 +140,8 @@ def setup(self):
".mintokens": ("change minimum response tokens", self.setMinTokens),
".dynamictokencount": ("change dynamic token count", self.setDynamicTokenCount),
".maxautoheal": ("change maximum consecutive auto-heal", self.setMaxAutoHeal),
".maxmemorymatches": ("change maximum memory matches", self.setMemoryClosestMatchesNumber),
".maxmemorymatches": ("change maximum memory matches", self.setMemoryClosestMatches),
".maxchatrecordmatches": ("change maximum chat record matches", self.setChatRecordClosestMatches),
".plugins": ("change plugins", self.selectPlugins),
".functioncall": ("change function call", self.setFunctionCall),
".functioncallintegration": ("change function call integration", self.setFunctionResponse),
Expand Down Expand Up @@ -208,9 +210,11 @@ def runCode(text):
else:
with open(script, 'r', encoding='utf8') as f:
runCode(f.read())
return True
except:
self.print("Failed to run '{0}'!".format(os.path.basename(script)))
SharedUtil.showErrors()
return False

def runPlugins(self):
# The following config values can be modified with plugins, to extend functionalities
Expand Down Expand Up @@ -252,7 +256,9 @@ def runPlugins(self):
for plugin in FileUtil.fileNamesWithoutExtension(folder, "py"):
if not plugin in config.pluginExcludeList:
script = os.path.join(folder, "{0}.py".format(plugin))
self.execPythonFile(script)
run = self.execPythonFile(script)
if not run:
config.pluginExcludeList.append(plugin)
if internetSeraches in config.pluginExcludeList:
del config.chatGPTApiFunctionSignatures[0]
self.setupPythonExecution()
Expand Down Expand Up @@ -1180,13 +1186,21 @@ def setCustomTextEditor(self):
config.saveConfig()
self.print3(f"Custom text editor: {config.customTextEditor}")

def setMemoryClosestMatchesNumber(self):
def setChatRecordClosestMatches(self):
self.print("Please specify the number of closest matches in each memory retrieval:")
chatRecordClosestMatches = self.prompts.simplePrompt(style=self.prompts.promptStyle2, numberOnly=True, default=str(config.chatRecordClosestMatches))
if chatRecordClosestMatches and not chatRecordClosestMatches.strip().lower() == config.exit_entry and int(chatRecordClosestMatches) >= 0:
config.chatRecordClosestMatches = int(chatRecordClosestMatches)
config.saveConfig()
self.print3(f"Number of memory closest matches: {config.chatRecordClosestMatches}")

def setMemoryClosestMatches(self):
self.print("Please specify the number of closest matches in each memory retrieval:")
memoryClosestMatchesNumber = self.prompts.simplePrompt(style=self.prompts.promptStyle2, numberOnly=True, default=str(config.memoryClosestMatchesNumber))
if memoryClosestMatchesNumber and not memoryClosestMatchesNumber.strip().lower() == config.exit_entry and int(memoryClosestMatchesNumber) >= 0:
config.memoryClosestMatchesNumber = int(memoryClosestMatchesNumber)
memoryClosestMatches = self.prompts.simplePrompt(style=self.prompts.promptStyle2, numberOnly=True, default=str(config.memoryClosestMatches))
if memoryClosestMatches and not memoryClosestMatches.strip().lower() == config.exit_entry and int(memoryClosestMatches) >= 0:
config.memoryClosestMatches = int(memoryClosestMatches)
config.saveConfig()
self.print3(f"Number of memory closest matches: {config.memoryClosestMatchesNumber}")
self.print3(f"Number of memory closest matches: {config.memoryClosestMatches}")

def setMaxAutoHeal(self):
self.print(f"The auto-heal feature enables {config.letMeDoItName} to automatically fix broken Python code if it was not executed properly.")
Expand Down Expand Up @@ -1324,9 +1338,31 @@ def toggleImprovedWriting(self):
config.saveConfig()
self.print3(f"Improved Writing Display: '{'enabled' if config.displayImprovedWriting else 'disabled'}'!")

def saveChat(self, messages, openFile=False):
def saveChat(self, messages):
if config.conversationStarted:
timestamp = SharedUtil.getCurrentDateTime()

if hasattr(config, "save_chat_record"):
# when plugin "save chat records" is enabled
for order, i in enumerate(messages):
config.save_chat_record(timestamp, order, i)

try:
folderPath = os.path.join(self.getFiles(), "chats", re.sub("^([0-9]+?\-[0-9]+?)\-.*?$", r"\1", timestamp))
Path(folderPath).mkdir(parents=True, exist_ok=True)
if os.path.isdir(folderPath):
chatFile = os.path.join(folderPath, f"{timestamp}.txt")
with open(chatFile, "w", encoding="utf-8") as fileObj:
fileObj.write(pprint.pformat(messages))
except:
self.print2("Failed to save chat!\n")
SharedUtil.showErrors()

def exportChat(self, messages, openFile=True):
if config.conversationStarted:
plainText = ""
timestamp = SharedUtil.getCurrentDateTime()

for i in messages:
if i["role"] == "user":
content = i["content"]
Expand All @@ -1349,12 +1385,10 @@ def saveChat(self, messages, openFile=False):
pydoc.pipepager(plainText, cmd="termux-share -a send")
else:
try:
#filename = re.sub('[\\\/\:\*\?\"\<\>\|]', "", messages[2 if config.customPredefinedContext.strip() else 1]["content"])[:40].strip()
filename = SharedUtil.getCurrentDateTime()
foldername = os.path.join(self.getFiles(), "chats", re.sub("^([0-9]+?\-[0-9]+?)\-.*?$", r"\1", filename))
Path(foldername).mkdir(parents=True, exist_ok=True)
if filename:
chatFile = os.path.join(foldername, f"{filename}.txt")
folderPath = os.path.join(config.letMeDoItAIFolder, "temp")
Path(folderPath).mkdir(parents=True, exist_ok=True)
if os.path.isdir(folderPath):
chatFile = os.path.join(folderPath, f"{timestamp}.txt")
with open(chatFile, "w", encoding="utf-8") as fileObj:
fileObj.write(plainText)
if openFile and os.path.isfile(chatFile):
Expand Down
4 changes: 3 additions & 1 deletion package/letmedoit/utils/configDefault.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def isPackageInstalled(package):
"memory",
"remove image background",
"solve math problems",
"search chat records",
]

defaultSettings = (
Expand Down Expand Up @@ -69,7 +70,8 @@ def isPackageInstalled(package):
('passFunctionCallReturnToChatGPT', True),
('llmTemperature', 0.8),
('max_consecutive_auto_reply', 10), # work with pyautogen
('memoryClosestMatchesNumber', 5),
('memoryClosestMatches', 5),
('chatRecordClosestMatches', 5),
('runPythonScriptGlobally', False),
('openaiApiKey', ''),
('openaiApiOrganization', ''),
Expand Down
2 changes: 1 addition & 1 deletion package/letmedoit/utils/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def _(event):
@this_key_bindings.add("c-s")
def _(event):
buffer = event.app.current_buffer
buffer.text = ".save"
buffer.text = ".export"
buffer.validate_and_handle()
@this_key_bindings.add("escape", "f")
def _(_):
Expand Down
2 changes: 1 addition & 1 deletion package/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/
setup(
name=package,
version="2.0.92",
version="2.0.93",
python_requires=">=3.8, <3.12",
description=f"{appFullName}, an advanced AI assistant, leveraging the capabilities of ChatGPT API, Gemini Pro and AutoGen, capable of engaging in conversations, executing codes with auto-healing, and assisting you with a wide range of tasks on your local devices.",
long_description=long_description,
Expand Down
Loading

0 comments on commit 7b80c43

Please sign in to comment.