277 lines
11 KiB
Python
277 lines
11 KiB
Python
from autoagent.memory.rag_memory import Memory
|
|
import os
|
|
from autoagent.environment.docker_env import DockerEnv
|
|
from autoagent.environment.local_env import LocalEnv
|
|
from typing import Union
|
|
from autoagent.environment.local_env import LocalEnv
|
|
from autoagent.io_utils import compress_folder, get_file_md5
|
|
from autoagent.registry import register_tool, register_plugin_tool
|
|
from litellm import completion
|
|
import zipfile
|
|
import shutil
|
|
from autoagent.environment.markdown_browser.mdconvert import MarkdownConverter
|
|
from autoagent.memory.utils import chunking_by_token_size
|
|
import math
|
|
from autoagent.types import Result
|
|
# @register_tool("load_db")
|
|
# def load_db(db_path: str) -> str:
|
|
|
|
@register_tool("save_raw_docs_to_vector_db")
|
|
@register_plugin_tool("save_raw_docs_to_vector_db")
|
|
def save_raw_docs_to_vector_db(context_variables: dict, doc_name: str, saved_vector_db_name: str, overwrite: bool = False) -> Result:
|
|
"""
|
|
Save the raw documents to the vector database. The documents could be:
|
|
- ANY text document with the extension of pdf, docx, txt, etcs.
|
|
- A zip file containing multiple text documents
|
|
- a directory containing multiple text documents
|
|
All documents will be converted to raw text format and saved to the vector database in the chunks of 4096 tokens.
|
|
|
|
Args:
|
|
doc_name: The name of the raw documents. All documents will be stored in the the directory: /workplace/docs.
|
|
[NOTES] doc_name should be the name of the file or directory, not the path to the file or directory, which means `docs/dir_name/` is not a valid doc_name.
|
|
saved_vector_db_name: the name of collection you want to save the documents to.
|
|
overwrite: Whether to overwrite the existing vector database when the vector database of the documents already exists. (default: False)
|
|
"""
|
|
try:
|
|
memo: Memory = context_variables.get("memo", Memory(project_path=os.path.join(os.getcwd(), "user_db"), db_name = ".user_db"))
|
|
assert memo is not None, "memo is not set"
|
|
code_env: Union[DockerEnv, LocalEnv] = context_variables.get("code_env", LocalEnv())
|
|
assert code_env is not None, "code_env is not set"
|
|
|
|
# check if the saved_vector_db_name is already in the vector database
|
|
if memo.count(saved_vector_db_name) > 0:
|
|
if overwrite:
|
|
prefix_res = f"[WARNING] The collection `{saved_vector_db_name}` of the vector database already exists. Overwriting the existing collection."
|
|
else:
|
|
return f"[WARNING] The collection `{saved_vector_db_name}` of the vector database already exists. Please set the overwrite flag to True if you want to overwrite the existing collection."
|
|
else:
|
|
prefix_res = ""
|
|
|
|
doc_dir = os.path.join(code_env.local_workplace, "docs")
|
|
os.makedirs(doc_dir, exist_ok=True)
|
|
if doc_name.startswith("docs/"):
|
|
doc_name = doc_name.replace("docs/", "")
|
|
elif doc_name.startswith("/workspace/docs/"):
|
|
doc_name = doc_name.replace("/workspace/docs/", "")
|
|
doc_path = os.path.join(doc_dir, doc_name)
|
|
assert os.path.exists(doc_path), f"The document `{doc_name}` does not exist in the directory `/workplace/docs`"
|
|
# the doc_path is a directory
|
|
if os.path.isdir(doc_path):
|
|
file_list = []
|
|
for file in os.listdir(doc_path):
|
|
if file.endswith(('.pdf', '.docx', '.txt')):
|
|
file_list.append(os.path.join(doc_path, file))
|
|
# the doc_path is a zip file
|
|
elif os.path.isfile(doc_path) and (doc_path.endswith('.zip') or doc_path.endswith('.tar') or doc_path.endswith('.tar.gz')):
|
|
file_name = os.path.splitext(doc_name)[0]
|
|
extract_dir = os.path.join(doc_dir, file_name)
|
|
os.makedirs(extract_dir, exist_ok=True)
|
|
|
|
with zipfile.ZipFile(doc_path, 'r') as zip_ref:
|
|
zip_ref.extractall(extract_dir)
|
|
|
|
# 将提取的文件路径添加到file_list中
|
|
file_list = []
|
|
for root, _, files in os.walk(extract_dir):
|
|
for file in files:
|
|
if file.endswith(('.pdf', '.docx', '.txt')):
|
|
file_list.append(os.path.join(root, file))
|
|
# the doc_path is a single file
|
|
elif os.path.isfile(doc_path):
|
|
file_list.append(doc_path)
|
|
else:
|
|
raise ValueError(f"The document `{doc_name}` is not a valid file or directory")
|
|
|
|
mdconvert = MarkdownConverter()
|
|
|
|
ret_val = prefix_res
|
|
batch_size = 200
|
|
for file in file_list:
|
|
queries = []
|
|
doc_content = mdconvert.convert_local(file).text_content
|
|
content_chunks = chunking_by_token_size(doc_content, max_token_size=4096)
|
|
idx_list = ["chunk_" + str(chunk['chunk_order_index']) for chunk in content_chunks]
|
|
for chunk in content_chunks:
|
|
query = {
|
|
'query': chunk['content'],
|
|
'response': f"The {chunk['chunk_order_index']} chunk of the content of the file {file} is: \n{chunk['content']}"
|
|
}
|
|
queries.append(query)
|
|
|
|
num_batches = math.ceil(len(queries) / batch_size)
|
|
|
|
for i in range(num_batches):
|
|
start_idx = i * batch_size
|
|
end_idx = min((i + 1) * batch_size, len(queries))
|
|
batch_queries = queries[start_idx:end_idx]
|
|
batch_idx = idx_list[start_idx:end_idx]
|
|
|
|
# Add the current batch of queries
|
|
memo.add_query(batch_queries, collection=saved_vector_db_name, idx=batch_idx)
|
|
ret_val += f"The {file} has been added to the vector database `{saved_vector_db_name}`."
|
|
context_variables["memo"] = memo
|
|
return Result(
|
|
value=ret_val,
|
|
context_variables=context_variables
|
|
)
|
|
except Exception as e:
|
|
ret_val = f"[ERROR] Failed to save the raw documents to the vector database: {e}"
|
|
return ret_val
|
|
|
|
@register_tool("query_db")
|
|
@register_plugin_tool("query_db")
|
|
def query_db(context_variables: dict, query_text: str, saved_vector_db_name: str, n_results: int = 5) -> str:
|
|
"""
|
|
Retrieve information from the database. Use this function when you need to search for information in the database.
|
|
|
|
Args:
|
|
query_text: The query to search for information in the database.
|
|
saved_vector_db_name: The name of the vector database to search for information.
|
|
n_results: The number of results to return. (default: 5)
|
|
Returns:
|
|
A string representation of the queried results.
|
|
"""
|
|
try:
|
|
memo: Memory = context_variables.get("memo", Memory(project_path=os.path.join(os.getcwd(), "user_db"), db_name = ".user_db"))
|
|
assert memo is not None, "memo is not set"
|
|
if memo.count(saved_vector_db_name) == 0:
|
|
return f"[ERROR] The vector database `{saved_vector_db_name}` does not exist. Please use function `save_raw_docs_to_vector_db` to save the raw documents to the vector database."
|
|
results = memo.query([query_text], collection=saved_vector_db_name, n_results=n_results)
|
|
|
|
metadata_results = results['metadatas'][0]
|
|
results = [item['response'] for item in metadata_results]
|
|
ret_val = "\n".join(results)
|
|
except Exception as e:
|
|
ret_val = f"[ERROR] Failed to query the vector database: {e}"
|
|
finally:
|
|
return ret_val
|
|
|
|
@register_tool("modify_query")
|
|
@register_plugin_tool("modify_query")
|
|
def modify_query(what_you_know: str, query_text: str, **kwargs) -> str:
|
|
"""
|
|
Modify the query based on what you know. Use this function when you need to modify the query to search for more relevant information.
|
|
|
|
Args:
|
|
what_you_know: The knowledge you have about the case.
|
|
query_text: The original query.
|
|
Returns:
|
|
The modified query.
|
|
"""
|
|
system_prompt = \
|
|
f"""
|
|
|
|
Assume you are an assistant searching for information. Now that you already know some knowledge ([What you know]), what sub-questions ([Modified query]) do you need to search for to help you answer the question ([Query]) you want to explore.
|
|
|
|
Modify the query based on what you know, here is some example:
|
|
Example 1:
|
|
[What you know]: Alice and Bob have lunch together at 12:00 PM.
|
|
[Query]: What did Alice and Bob do after the lunch?
|
|
[Modified query]: What did Alice and Bob do after 12:00 PM?
|
|
|
|
Example 2:
|
|
[What you know]: Alice and Bob went to the cinema yesterday.
|
|
[Query]: What did Alice and Bob do after the cinema?
|
|
[Modified query]: What did Alice and Bob do yesterday?
|
|
|
|
Return only 1 modified query.
|
|
"""
|
|
|
|
user_prompt = f"""
|
|
What you know: {what_you_know}
|
|
Query: {query_text}
|
|
Modified query:
|
|
"""
|
|
create_params = {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
"stream": False,
|
|
}
|
|
response = completion(**create_params)
|
|
modified_query = response.choices[0].message.content
|
|
return f"The modified query is: {modified_query}. Now use function `query_db` to search the related information in the DataBase."
|
|
|
|
|
|
|
|
@register_tool("answer_query")
|
|
@register_plugin_tool("answer_query")
|
|
def answer_query(original_user_query: str, supporting_docs: str, **kwargs) -> str:
|
|
"""
|
|
Answer the user query based on the supporting documents.
|
|
|
|
Args:
|
|
original_user_query: The original user query.
|
|
supporting_docs: The supporting documents.
|
|
Returns:
|
|
The answer to the user query.
|
|
"""
|
|
system_prompt = \
|
|
f"""
|
|
You are a helpful assistant. Answer the user query based on the supporting documents.
|
|
If you have not found the answer, say "Insufficient information."
|
|
"""
|
|
|
|
user_prompt = f"""
|
|
|
|
Here is the original user query and the supporting documents:
|
|
Original user query: {original_user_query}
|
|
Supporting documents: {supporting_docs}
|
|
Answer:
|
|
"""
|
|
create_params = {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
"stream": False,
|
|
}
|
|
response = completion(**create_params)
|
|
answer = response.choices[0].message.content
|
|
|
|
|
|
return answer
|
|
|
|
|
|
@register_tool("can_answer")
|
|
@register_plugin_tool("can_answer")
|
|
def can_answer(user_query: str, supporting_docs: str, **kwargs) -> str:
|
|
"""
|
|
Check if you have enough information to answer the user query.
|
|
|
|
Args:
|
|
user_query: The user query.
|
|
supporting_docs: The supporting documents.
|
|
Returns:
|
|
"True" if you have enough information to answer the user query, "False" otherwise.
|
|
"""
|
|
system_prompt = \
|
|
f"""
|
|
You are a helpful assistant. Check if you have enough information to answer the user query.
|
|
The answer should only be "True" or "False".
|
|
"""
|
|
|
|
user_prompt = f"""
|
|
|
|
Here is the original user query and the supporting documents:
|
|
Original user query: {user_query}
|
|
Supporting documents: {supporting_docs}
|
|
Answer:
|
|
"""
|
|
create_params = {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
"stream": False,
|
|
}
|
|
response = completion(**create_params)
|
|
answer = response.choices[0].message.content
|
|
|
|
return answer
|
|
|