From b6752fe496ae02a2cb6fd3546dab13c6a48fb3e1 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 08:42:15 -0400 Subject: [PATCH 01/17] Set auto_continue as default --- agixt/XT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agixt/XT.py b/agixt/XT.py index 911cc4ec4e69..6304a19c917a 100644 --- a/agixt/XT.py +++ b/agixt/XT.py @@ -1328,7 +1328,7 @@ async def chat_completions(self, prompt: ChatCompletions): analyze_user_input = ( str(self.agent_settings["analyze_user_input"]).lower() == "true" ) - auto_continue = False + auto_continue = True if "auto_continue" in self.agent_settings: auto_continue = str(self.agent_settings["auto_continue"]).lower() == "true" include_sources = False From 89a7759b3b7b0fd546690f28ef29e793809eec9e Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 09:01:06 -0400 Subject: [PATCH 02/17] expose error --- agixt/endpoints/Agent.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/agixt/endpoints/Agent.py b/agixt/endpoints/Agent.py index 9f351b62d61c..282722fdb406 100644 --- a/agixt/endpoints/Agent.py +++ b/agixt/endpoints/Agent.py @@ -344,29 +344,23 @@ async def toggle_command( raise HTTPException(status_code=403, detail="Access Denied") ApiClient = get_api_client(authorization=authorization) agent = Agent(agent_name=agent_name, user=user, ApiClient=ApiClient) - try: - if payload.command_name == "*": - for each_command_name in agent.AGENT_CONFIG["commands"]: - agent.AGENT_CONFIG["commands"][each_command_name] = payload.enable + if payload.command_name == "*": + for each_command_name in agent.AGENT_CONFIG["commands"]: + agent.AGENT_CONFIG["commands"][each_command_name] = payload.enable - agent.update_agent_config( - new_config=agent.AGENT_CONFIG["commands"], config_key="commands" - ) - return ResponseMessage( - message=f"All commands enabled for agent '{agent_name}'." - ) - else: - agent.AGENT_CONFIG["commands"][payload.command_name] = payload.enable - agent.update_agent_config( - new_config=agent.AGENT_CONFIG["commands"], config_key="commands" - ) - return ResponseMessage( - message=f"Command '{payload.command_name}' toggled for agent '{agent_name}'." - ) - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Error enabling all commands for agent '{agent_name}': {str(e)}", + agent.update_agent_config( + new_config=agent.AGENT_CONFIG["commands"], config_key="commands" + ) + return ResponseMessage( + message=f"All commands enabled for agent '{agent_name}'." + ) + else: + agent.AGENT_CONFIG["commands"][payload.command_name] = payload.enable + agent.update_agent_config( + new_config=agent.AGENT_CONFIG["commands"], config_key="commands" + ) + return ResponseMessage( + message=f"Command '{payload.command_name}' toggled for agent '{agent_name}'." ) From 74f6fc835c4d73d790b6278c28942a5578e8e6cb Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 09:08:56 -0400 Subject: [PATCH 03/17] Get list of commands on error --- agixt/Agent.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index 639949535677..ce16f9b11bc5 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -483,10 +483,16 @@ def update_agent_config(self, new_config, config_key): for command_name, enabled in new_config.items(): command = session.query(Command).filter_by(name=command_name).first() if not command: - # If the command doesn't exist, create it (this handles chain commands) - command = Command(name=command_name) - session.add(command) - session.commit() + commands = session.query(Command).all() + command_names = [c.name for c in commands] + logging.error( + f"Command {command_name} not found. Available commands: {command_names}" + ) + session.close() + raise HTTPException( + status_code=401, + detail=f"Command {command_name} not found. Available commands: {command_names}", + ) agent_command = ( session.query(AgentCommand) From b1d3eddc7bf415811ae9e0b0d6d92be36e75e40a Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 09:20:49 -0400 Subject: [PATCH 04/17] handle toggle better --- agixt/Agent.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index ce16f9b11bc5..aaad75f96dfe 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -9,8 +9,10 @@ ChainStep, ChainStepArgument, ChainStepResponse, + Chain as ChainDB, Provider as ProviderModel, User, + Extension, UserPreferences, get_session, ) @@ -483,17 +485,20 @@ def update_agent_config(self, new_config, config_key): for command_name, enabled in new_config.items(): command = session.query(Command).filter_by(name=command_name).first() if not command: - commands = session.query(Command).all() - command_names = [c.name for c in commands] - logging.error( - f"Command {command_name} not found. Available commands: {command_names}" + chain = session.query(ChainDB).filter_by(name=command_name).first() + if not chain: + logging.error(f"Command {command_name} not found.") + return f"Command {command_name} not found." + extension = ( + session.query(Extension).filter_by(name="AGiXT Chains").first() ) - session.close() - raise HTTPException( - status_code=401, - detail=f"Command {command_name} not found. Available commands: {command_names}", - ) - + if not extension: + extension = Extension(name="AGiXT Chains") + session.add(extension) + session.commit() + command = Command(name=command_name, extension_id=extension.id) + session.add(command) + session.commit() agent_command = ( session.query(AgentCommand) .filter_by(agent_id=agent.id, command_id=command.id) From a831e0e0fe21ce9592c3a72df3186440605f4db2 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 09:27:00 -0400 Subject: [PATCH 05/17] add logging --- agixt/Agent.py | 5 +++++ docker-compose-dev.yml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index aaad75f96dfe..85d0b68426b0 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -499,6 +499,11 @@ def update_agent_config(self, new_config, config_key): command = Command(name=command_name, extension_id=extension.id) session.add(command) session.commit() + # Debug, get list of commands to print + commands = session.query(Command).filter(Command).all() + for command in commands: + logging.info(f"Command: {command.name}") + agent_command = ( session.query(AgentCommand) .filter_by(agent_id=agent.id, command_id=command.id) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index ec5da0ba766b..fd5c18f6ae53 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,6 +1,6 @@ services: agixt: - image: joshxt/agixt:main + image: ghcr.io/josh-xt/agixt:dev init: true environment: DATABASE_TYPE: ${DATABASE_TYPE:-sqlite} From f60d58ddcc4680265d3261cb036c1adc20fe50c0 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 10:04:30 -0400 Subject: [PATCH 06/17] fix ref --- agixt/Agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index 85d0b68426b0..ecce757805ed 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -500,7 +500,7 @@ def update_agent_config(self, new_config, config_key): session.add(command) session.commit() # Debug, get list of commands to print - commands = session.query(Command).filter(Command).all() + commands = session.query(Command).all() for command in commands: logging.info(f"Command: {command.name}") From f24d965039b5725b18e5f694f3e46262f98c9171 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 18:41:07 -0400 Subject: [PATCH 07/17] add logging --- agixt/Agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index ecce757805ed..4a3612e5c201 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -501,9 +501,9 @@ def update_agent_config(self, new_config, config_key): session.commit() # Debug, get list of commands to print commands = session.query(Command).all() - for command in commands: - logging.info(f"Command: {command.name}") - + for c in commands: + logging.info(f"Command: {c.name}") + logging.info(command.__dict__) agent_command = ( session.query(AgentCommand) .filter_by(agent_id=agent.id, command_id=command.id) From 079a7da48027ed8e60592928a14eafe30124b78e Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 19:03:20 -0400 Subject: [PATCH 08/17] try again --- agixt/Agent.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index 4a3612e5c201..5980b801ff95 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -499,16 +499,24 @@ def update_agent_config(self, new_config, config_key): command = Command(name=command_name, extension_id=extension.id) session.add(command) session.commit() - # Debug, get list of commands to print - commands = session.query(Command).all() - for c in commands: - logging.info(f"Command: {c.name}") - logging.info(command.__dict__) - agent_command = ( - session.query(AgentCommand) - .filter_by(agent_id=agent.id, command_id=command.id) - .first() - ) + try: + agent_command = ( + session.query(AgentCommand) + .filter_by(agent_id=agent.id, command_id=str(command.id)) + .first() + ) + except: + command = ( + session.query(Command).filter_by(name=command_name).first() + ) + try: + agent_command = ( + session.query(AgentCommand) + .filter_by(agent_id=agent.id, command_id=str(command.id)) + .first() + ) + except: + agent_command = None if agent_command: agent_command.state = enabled else: From 4310715da4484d58adb68aef450d97543f362f11 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 19:12:04 -0400 Subject: [PATCH 09/17] Update logic --- agixt/Agent.py | 58 ++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index 5980b801ff95..b2f129c6823c 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -483,45 +483,49 @@ def update_agent_config(self, new_config, config_key): if config_key == "commands": for command_name, enabled in new_config.items(): + # First try to find an existing command command = session.query(Command).filter_by(name=command_name).first() + if not command: + # Check if this is a chain command chain = session.query(ChainDB).filter_by(name=command_name).first() - if not chain: - logging.error(f"Command {command_name} not found.") - return f"Command {command_name} not found." - extension = ( - session.query(Extension).filter_by(name="AGiXT Chains").first() - ) - if not extension: - extension = Extension(name="AGiXT Chains") - session.add(extension) + if chain: + # Find or create the AGiXT Chains extension + extension = ( + session.query(Extension) + .filter_by(name="AGiXT Chains") + .first() + ) + if not extension: + extension = Extension(name="AGiXT Chains") + session.add(extension) + session.commit() + + # Create a new command entry for the chain + command = Command(name=command_name, extension_id=extension.id) + session.add(command) session.commit() - command = Command(name=command_name, extension_id=extension.id) - session.add(command) - session.commit() + else: + logging.error(f"Command {command_name} not found.") + continue + + # Now handle the agent command association try: agent_command = ( session.query(AgentCommand) - .filter_by(agent_id=agent.id, command_id=str(command.id)) + .filter_by(agent_id=agent.id, command_id=command.id) .first() ) except: - command = ( - session.query(Command).filter_by(name=command_name).first() - ) - try: - agent_command = ( - session.query(AgentCommand) - .filter_by(agent_id=agent.id, command_id=str(command.id)) - .first() - ) - except: - agent_command = None + agent_command = None + if agent_command: agent_command.state = enabled else: agent_command = AgentCommand( - agent_id=agent.id, command_id=command.id, state=enabled + agent_id=agent.id, + command_id=command.id, + state=enabled, ) session.add(agent_command) else: @@ -535,7 +539,9 @@ def update_agent_config(self, new_config, config_key): agent_setting.value = str(setting_value) else: agent_setting = AgentSettingModel( - agent_id=agent.id, name=setting_name, value=str(setting_value) + agent_id=agent.id, + name=setting_name, + value=str(setting_value), ) session.add(agent_setting) From 88dfe50a01e1b8ca70d9c6f807694b59fc4f8a9f Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 19:34:30 -0400 Subject: [PATCH 10/17] fix endpoint --- agixt/endpoints/Agent.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/agixt/endpoints/Agent.py b/agixt/endpoints/Agent.py index 282722fdb406..2e7daa311a77 100644 --- a/agixt/endpoints/Agent.py +++ b/agixt/endpoints/Agent.py @@ -344,24 +344,10 @@ async def toggle_command( raise HTTPException(status_code=403, detail="Access Denied") ApiClient = get_api_client(authorization=authorization) agent = Agent(agent_name=agent_name, user=user, ApiClient=ApiClient) - if payload.command_name == "*": - for each_command_name in agent.AGENT_CONFIG["commands"]: - agent.AGENT_CONFIG["commands"][each_command_name] = payload.enable - - agent.update_agent_config( - new_config=agent.AGENT_CONFIG["commands"], config_key="commands" - ) - return ResponseMessage( - message=f"All commands enabled for agent '{agent_name}'." - ) - else: - agent.AGENT_CONFIG["commands"][payload.command_name] = payload.enable - agent.update_agent_config( - new_config=agent.AGENT_CONFIG["commands"], config_key="commands" - ) - return ResponseMessage( - message=f"Command '{payload.command_name}' toggled for agent '{agent_name}'." - ) + update_config = agent.update_agent_config( + new_config={payload.command_name: payload.enable}, config_key="commands" + ) + return ResponseMessage(message=update_config) # Get agent browsed links From ba108eb43b8f12a12193785279e3f5099e2aa4eb Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 19:43:09 -0400 Subject: [PATCH 11/17] fix id ref --- agixt/Agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agixt/Agent.py b/agixt/Agent.py index b2f129c6823c..e55c61cbbfa1 100644 --- a/agixt/Agent.py +++ b/agixt/Agent.py @@ -523,7 +523,7 @@ def update_agent_config(self, new_config, config_key): agent_command.state = enabled else: agent_command = AgentCommand( - agent_id=agent.id, + agent_id=self.agent_id, command_id=command.id, state=enabled, ) From 3078963c40610f5f00e44aa1514d69a4bde2933f Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 19:57:50 -0400 Subject: [PATCH 12/17] attempt to fix tests --- agixt/XT.py | 113 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/agixt/XT.py b/agixt/XT.py index 6304a19c917a..04eb53a2f7ee 100644 --- a/agixt/XT.py +++ b/agixt/XT.py @@ -647,51 +647,71 @@ async def learn_from_websites( async def learn_spreadsheet(self, user_input, file_path): file_name = os.path.basename(file_path) file_type = str(file_name).split(".")[-1] - if file_type.lower() == "csv": - df = pd.read_csv(file_path) - else: # Excel file - try: - xl = pd.ExcelFile(file_path) - if len(xl.sheet_names) > 1: - sheet_count = len(xl.sheet_names) - for i, sheet_name in enumerate(xl.sheet_names, 1): - df = xl.parse(sheet_name) - csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") - csv_file_name = os.path.basename(csv_file_path) - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY] ({i}/{sheet_count}) Converted sheet `{sheet_name}` in `{file_name}` to CSV file `{csv_file_name}`.", - ) - df.to_csv(csv_file_path, index=False) - message = await self.learn_spreadsheet( - user_input=user_input, - file_path=csv_file_path, - ) - self.conversation.log_interaction( - role=self.agent_name, message=f"[ACTIVITY] {message}" - ) - return f"Processed all sheets in [{file_name}]({file_path})." - else: - df = pd.read_excel(file_path) - except Exception as e: - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", + + def datetime_handler(obj): + if hasattr(obj, 'isoformat'): + return obj.isoformat() + else: + return str(obj) + + try: + if file_type.lower() == "csv": + df = pd.read_csv(file_path) + else: # Excel file + try: + xl = pd.ExcelFile(file_path) + if len(xl.sheet_names) > 1: + sheet_count = len(xl.sheet_names) + for i, sheet_name in enumerate(xl.sheet_names, 1): + df = xl.parse(sheet_name) + csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") + csv_file_name = os.path.basename(csv_file_path) + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY] ({i}/{sheet_count}) Converted sheet `{sheet_name}` in `{file_name}` to CSV file `{csv_file_name}`.", + ) + df.to_csv(csv_file_path, index=False) + message = await self.learn_spreadsheet( + user_input=user_input, + file_path=csv_file_path, + ) + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY] {message}" + ) + return f"Processed all sheets in [{file_name}]({file_path})." + else: + df = pd.read_excel(file_path) + except Exception as e: + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", + ) + return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" + + # Convert DataFrame to dict and handle datetime serialization + df_dict = df.to_dict("records") + df_dict_serializable = json.loads(json.dumps(df_dict, default=datetime_handler)) + + self.input_tokens += get_tokens(json.dumps(df_dict_serializable)) + + for item in df_dict_serializable: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" + await self.file_reader.write_text_to_memory( + user_input=f"{user_input}\n{message}", + text=message, + external_source=f"file {file_path}", ) - return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" - df_dict = df.to_dict("records") - self.input_tokens += get_tokens(json.dumps(df_dict)) - for item in df_dict: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" - await self.file_reader.write_text_to_memory( - user_input=f"{user_input}\n{message}", - text=message, - external_source=f"file {file_path}", + return f"Read [{file_name}]({file_path}) into memory." + + except Exception as e: + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY][ERROR] Failed to process file `{file_name}`: {str(e)}", ) - - return f"Read [{file_name}]({file_path}) into memory." + return f"Failed to process [{file_name}]({file_path}). Error: {str(e)}" async def learn_from_file( self, @@ -756,7 +776,7 @@ async def learn_from_file( message=f"[ACTIVITY] Converting PowerPoint file [{file_name}]({file_url}) to PDF.", ) try: - subprocess.run( + result = subprocess.run( [ "libreoffice", "--headless", @@ -768,10 +788,17 @@ async def learn_from_file( ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + timeout=30, ) + if result.returncode != 0: + raise Exception( + f"Conversion failed: {result.stderr.decode('utf-8', errors='ignore')}" + ) except Exception as e: logging.error(f"Error converting PowerPoint to PDF: {e}") + return f"Failed to convert PowerPoint file [{file_name}]({file_url}) to PDF. Error: {str(e)}" file_path = pdf_file_path + file_type = "pdf" if user_input == "": user_input = "Describe each stage of this image." disallowed_types = ["exe", "bin", "rar"] From ecc76aad7975bd1e2cae797165de86dd0b4d6033 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 20:05:58 -0400 Subject: [PATCH 13/17] lint --- agixt/XT.py | 52 +++++++++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/agixt/XT.py b/agixt/XT.py index 04eb53a2f7ee..4e9cf2aa80ed 100644 --- a/agixt/XT.py +++ b/agixt/XT.py @@ -483,27 +483,18 @@ async def run_chain_step( role=self.agent_name, message=f"[ACTIVITY] Running prompt: `{prompt_name}` with args:\n```json\n{json.dumps(args, indent=2)}```", ) - if "user_input" in args: - user_input = args["user_input"] - del args["user_input"] - if "browse_links" not in args: - args["browse_links"] = False - if "log_output" in args: - del args["log_output"] - if "voice_response" in args: - del args["voice_response"] - if "log_user_input" in args: - del args["log_user_input"] - if "prompt_name" in args: - del args["prompt_name"] if prompt_name != "": - result = await self.inference( + if "browse_links" not in args: + args["browse_links"] = False + args["prompt_name"] = prompt_name + args["log_user_input"] = False + args["voice_response"] = False + args["log_output"] = False + args["user_input"] = user_input + result = self.ApiClient.prompt_agent( + agent_name=agent_name, prompt_name=prompt_name, - user_input=user_input, - log_user_input=False, - log_output=False, - voice_response=False, - **args, + prompt_args=args, ) elif prompt_type == "chain": self.conversation.log_interaction( @@ -647,13 +638,13 @@ async def learn_from_websites( async def learn_spreadsheet(self, user_input, file_path): file_name = os.path.basename(file_path) file_type = str(file_name).split(".")[-1] - + def datetime_handler(obj): - if hasattr(obj, 'isoformat'): + if hasattr(obj, "isoformat"): return obj.isoformat() else: return str(obj) - + try: if file_type.lower() == "csv": df = pd.read_csv(file_path) @@ -664,7 +655,9 @@ def datetime_handler(obj): sheet_count = len(xl.sheet_names) for i, sheet_name in enumerate(xl.sheet_names, 1): df = xl.parse(sheet_name) - csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") + csv_file_path = file_path.replace( + f".{file_type}", f"_{i}.csv" + ) csv_file_name = os.path.basename(csv_file_path) self.conversation.log_interaction( role=self.agent_name, @@ -676,8 +669,7 @@ def datetime_handler(obj): file_path=csv_file_path, ) self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY] {message}" + role=self.agent_name, message=f"[ACTIVITY] {message}" ) return f"Processed all sheets in [{file_name}]({file_path})." else: @@ -691,10 +683,12 @@ def datetime_handler(obj): # Convert DataFrame to dict and handle datetime serialization df_dict = df.to_dict("records") - df_dict_serializable = json.loads(json.dumps(df_dict, default=datetime_handler)) - + df_dict_serializable = json.loads( + json.dumps(df_dict, default=datetime_handler) + ) + self.input_tokens += get_tokens(json.dumps(df_dict_serializable)) - + for item in df_dict_serializable: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" @@ -705,7 +699,7 @@ def datetime_handler(obj): ) return f"Read [{file_name}]({file_path}) into memory." - + except Exception as e: self.conversation.log_interaction( role=self.agent_name, From dc0d73b80584bd7e173eae08966af92461e580fe Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 20:07:18 -0400 Subject: [PATCH 14/17] fix xls --- agixt/XT.py | 110 ++++++++++++++++++++++++---------------------------- 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/agixt/XT.py b/agixt/XT.py index 4e9cf2aa80ed..22e4daaab747 100644 --- a/agixt/XT.py +++ b/agixt/XT.py @@ -638,74 +638,64 @@ async def learn_from_websites( async def learn_spreadsheet(self, user_input, file_path): file_name = os.path.basename(file_path) file_type = str(file_name).split(".")[-1] - + def datetime_handler(obj): - if hasattr(obj, "isoformat"): + if hasattr(obj, 'isoformat'): return obj.isoformat() else: return str(obj) - - try: - if file_type.lower() == "csv": - df = pd.read_csv(file_path) - else: # Excel file - try: - xl = pd.ExcelFile(file_path) - if len(xl.sheet_names) > 1: - sheet_count = len(xl.sheet_names) - for i, sheet_name in enumerate(xl.sheet_names, 1): - df = xl.parse(sheet_name) - csv_file_path = file_path.replace( - f".{file_type}", f"_{i}.csv" - ) - csv_file_name = os.path.basename(csv_file_path) - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY] ({i}/{sheet_count}) Converted sheet `{sheet_name}` in `{file_name}` to CSV file `{csv_file_name}`.", - ) - df.to_csv(csv_file_path, index=False) - message = await self.learn_spreadsheet( - user_input=user_input, - file_path=csv_file_path, - ) - self.conversation.log_interaction( - role=self.agent_name, message=f"[ACTIVITY] {message}" - ) - return f"Processed all sheets in [{file_name}]({file_path})." - else: - df = pd.read_excel(file_path) - except Exception as e: - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", - ) - return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" - - # Convert DataFrame to dict and handle datetime serialization + + if file_type.lower() == "csv": + # Keep original CSV handling as it was working fine + df = pd.read_csv(file_path) df_dict = df.to_dict("records") - df_dict_serializable = json.loads( - json.dumps(df_dict, default=datetime_handler) - ) - - self.input_tokens += get_tokens(json.dumps(df_dict_serializable)) - - for item in df_dict_serializable: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" - await self.file_reader.write_text_to_memory( - user_input=f"{user_input}\n{message}", - text=message, - external_source=f"file {file_path}", + else: # Excel file + try: + xl = pd.ExcelFile(file_path) + if len(xl.sheet_names) > 1: + sheet_count = len(xl.sheet_names) + for i, sheet_name in enumerate(xl.sheet_names, 1): + df = xl.parse(sheet_name) + csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") + csv_file_name = os.path.basename(csv_file_path) + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY] ({i}/{sheet_count}) Converted sheet `{sheet_name}` in `{file_name}` to CSV file `{csv_file_name}`.", + ) + df.to_csv(csv_file_path, index=False) + message = await self.learn_spreadsheet( + user_input=user_input, + file_path=csv_file_path, + ) + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY] {message}" + ) + return f"Processed all sheets in [{file_name}]({file_path})." + else: + df = pd.read_excel(file_path) + # Handle datetime serialization only for Excel files + df_dict = df.to_dict("records") + df_dict = json.loads(json.dumps(df_dict, default=datetime_handler)) + except Exception as e: + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", ) + return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" - return f"Read [{file_name}]({file_path}) into memory." - - except Exception as e: - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY][ERROR] Failed to process file `{file_name}`: {str(e)}", + self.input_tokens += get_tokens(json.dumps(df_dict)) + + for item in df_dict: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" + await self.file_reader.write_text_to_memory( + user_input=f"{user_input}\n{message}", + text=message, + external_source=f"file {file_path}", ) - return f"Failed to process [{file_name}]({file_path}). Error: {str(e)}" + + return f"Read [{file_name}]({file_path}) into memory." async def learn_from_file( self, From c87ad9fad24a8f855a7d1986708e1aab918e365b Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 20:23:17 -0400 Subject: [PATCH 15/17] fix output --- agixt/XT.py | 117 ++++++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/agixt/XT.py b/agixt/XT.py index 22e4daaab747..0609b73015f9 100644 --- a/agixt/XT.py +++ b/agixt/XT.py @@ -638,64 +638,71 @@ async def learn_from_websites( async def learn_spreadsheet(self, user_input, file_path): file_name = os.path.basename(file_path) file_type = str(file_name).split(".")[-1] - - def datetime_handler(obj): - if hasattr(obj, 'isoformat'): - return obj.isoformat() - else: - return str(obj) - - if file_type.lower() == "csv": - # Keep original CSV handling as it was working fine - df = pd.read_csv(file_path) - df_dict = df.to_dict("records") - else: # Excel file + try: + if file_type.lower() == "csv": + df = pd.read_csv(file_path) + else: # Excel file + try: + xl = pd.ExcelFile(file_path) + if len(xl.sheet_names) > 1: + sheet_count = len(xl.sheet_names) + for i, sheet_name in enumerate(xl.sheet_names, 1): + df = xl.parse(sheet_name) + csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") + csv_file_name = os.path.basename(csv_file_path) + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY] ({i}/{sheet_count}) Converted sheet `{sheet_name}` in `{file_name}` to CSV file `{csv_file_name}`.", + ) + df.to_csv(csv_file_path, index=False) + message = await self.learn_spreadsheet( + user_input=user_input, + file_path=csv_file_path, + ) + self.conversation.log_interaction( + role=self.agent_name, message=f"[ACTIVITY] {message}" + ) + return f"Processed all sheets in [{file_name}]({file_path})." + else: + df = pd.read_excel(file_path) + except Exception as e: + self.conversation.log_interaction( + role=self.agent_name, + message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", + ) + return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" + try: - xl = pd.ExcelFile(file_path) - if len(xl.sheet_names) > 1: - sheet_count = len(xl.sheet_names) - for i, sheet_name in enumerate(xl.sheet_names, 1): - df = xl.parse(sheet_name) - csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") - csv_file_name = os.path.basename(csv_file_path) - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY] ({i}/{sheet_count}) Converted sheet `{sheet_name}` in `{file_name}` to CSV file `{csv_file_name}`.", - ) - df.to_csv(csv_file_path, index=False) - message = await self.learn_spreadsheet( - user_input=user_input, - file_path=csv_file_path, - ) - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY] {message}" - ) - return f"Processed all sheets in [{file_name}]({file_path})." - else: - df = pd.read_excel(file_path) - # Handle datetime serialization only for Excel files df_dict = df.to_dict("records") - df_dict = json.loads(json.dumps(df_dict, default=datetime_handler)) + # Test JSON serialization before proceeding + json.dumps(df_dict) except Exception as e: - self.conversation.log_interaction( - role=self.agent_name, - message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", - ) - return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" - - self.input_tokens += get_tokens(json.dumps(df_dict)) - - for item in df_dict: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" - await self.file_reader.write_text_to_memory( - user_input=f"{user_input}\n{message}", - text=message, - external_source=f"file {file_path}", - ) - - return f"Read [{file_name}]({file_path}) into memory." + logging.error(f"Error converting DataFrame to dict: {e}") + return f"Failed to process [{file_name}]({file_path}). Error converting data format: {str(e)}" + + try: + self.input_tokens += get_tokens(json.dumps(df_dict)) + except Exception as e: + logging.error(f"Error calculating tokens: {e}") + # Continue processing even if token calculation fails + + try: + for item in df_dict: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + message = f"Content from file uploaded at {timestamp} named `{file_name}`:\n```json\n{json.dumps(item, indent=2)}```\n" + await self.file_reader.write_text_to_memory( + user_input=f"{user_input}\n{message}", + text=message, + external_source=f"file {file_path}", + ) + return f"Read [{file_name}]({file_path}) into memory." + except Exception as e: + logging.error(f"Error writing to memory: {e}") + return f"Failed to save [{file_name}]({file_path}) to memory. Error: {str(e)}" + + except Exception as e: + logging.error(f"Unexpected error processing spreadsheet: {e}") + return f"Failed to process [{file_name}]({file_path}). Unexpected error: {str(e)}" async def learn_from_file( self, From c5137cc3931c9a00f515fcf659cda0ece12e857c Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 20:29:50 -0400 Subject: [PATCH 16/17] Update tests --- tests/endpoint-tests.ipynb | 48 ++++++++++++++++++-------------------- tests/test.csv | 5 ++++ 2 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 tests/test.csv diff --git a/tests/endpoint-tests.ipynb b/tests/endpoint-tests.ipynb index ee568504d5bc..a4741a4eaa6e 100644 --- a/tests/endpoint-tests.ipynb +++ b/tests/endpoint-tests.ipynb @@ -911,14 +911,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from a File\n" + "## Have the Agent Learn from Files\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from a ZIP File" + "### Zip" ] }, { @@ -928,13 +928,12 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import requests\n", "import base64\n", "\n", - "zip_url = \"https://getsamplefiles.com/download/zip/sample-1.zip\"\n", + "zip_url = \"https://github.com/Josh-XT/Setup/archive/refs/heads/main.zip\"\n", "response = requests.get(zip_url)\n", - "learn_file_path = os.path.join(os.getcwd(), \"sample-1.zip\")\n", + "learn_file_path = os.path.join(os.getcwd(), \"Setup-main.zip\")\n", "with open(learn_file_path, \"wb\") as f:\n", " f.write(response.content)\n", "learn_file_content = base64.b64encode(response.content).decode(\"utf-8\")\n", @@ -944,14 +943,14 @@ " file_name=learn_file_path,\n", " file_content=learn_file_content,\n", " collection_number=\"0\",\n", - ")\n" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from a CSV File" + "### CSV" ] }, { @@ -961,13 +960,9 @@ "metadata": {}, "outputs": [], "source": [ - "\n", - "import requests\n", "import base64\n", "\n", - "csv_url = \"https://getsamplefiles.com/download/csv/sample-1.csv\"\n", - "response = requests.get(csv_url)\n", - "learn_file_path = os.path.join(os.getcwd(), \"sample-1.csv\")\n", + "learn_file_path = \"test.csv\"\n", "with open(learn_file_path, \"wb\") as f:\n", " f.write(response.content)\n", "learn_file_content = base64.b64encode(response.content).decode(\"utf-8\")\n", @@ -977,14 +972,14 @@ " file_name=learn_file_path,\n", " file_content=learn_file_content,\n", " collection_number=\"0\",\n", - ")\n" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from an Excel File" + "### XLS/XLSX" ] }, { @@ -994,7 +989,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import requests\n", "import base64\n", "\n", @@ -1010,14 +1004,14 @@ " file_name=learn_file_path,\n", " file_content=learn_file_content,\n", " collection_number=\"0\",\n", - ")\n" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from a Word File" + "### DOC/DOCX" ] }, { @@ -1027,7 +1021,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import requests\n", "import base64\n", "\n", @@ -1043,14 +1036,14 @@ " file_name=learn_file_path,\n", " file_content=learn_file_content,\n", " collection_number=\"0\",\n", - ")\n" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from a PowerPoint File" + "### PPT/PPTX" ] }, { @@ -1060,7 +1053,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import requests\n", "import base64\n", "\n", @@ -1076,14 +1068,14 @@ " file_name=learn_file_path,\n", " file_content=learn_file_content,\n", " collection_number=\"0\",\n", - ")\n" + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Have the Agent Learn from a PDF File" + "### PDF" ] }, { @@ -1093,7 +1085,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import requests\n", "import base64\n", "\n", @@ -1109,7 +1100,14 @@ " file_name=learn_file_path,\n", " file_content=learn_file_content,\n", " collection_number=\"0\",\n", - ")\n" + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TXT" ] }, { diff --git a/tests/test.csv b/tests/test.csv new file mode 100644 index 000000000000..b49f0216a2b4 --- /dev/null +++ b/tests/test.csv @@ -0,0 +1,5 @@ +"column 1","column 2","column 3" +"row 1","row 1","row 1" +"row 2","row 2","row 2" +"row 3","row 3","row 3" +"row 4","row 4","row 4" \ No newline at end of file From 68bdc714a9fb3dd54a05939e343cb075d3a76e00 Mon Sep 17 00:00:00 2001 From: Josh XT Date: Mon, 21 Oct 2024 20:43:58 -0400 Subject: [PATCH 17/17] use local files for tests --- agixt/XT.py | 12 +++++++----- tests/endpoint-tests.ipynb | 36 +++++++++++------------------------- tests/test.docx | Bin 0 -> 6615 bytes tests/test.xlsx | Bin 0 -> 5476 bytes tests/test.zip | Bin 0 -> 391 bytes 5 files changed, 18 insertions(+), 30 deletions(-) create mode 100644 tests/test.docx create mode 100644 tests/test.xlsx create mode 100644 tests/test.zip diff --git a/agixt/XT.py b/agixt/XT.py index 0609b73015f9..8b3809676f49 100644 --- a/agixt/XT.py +++ b/agixt/XT.py @@ -648,7 +648,9 @@ async def learn_spreadsheet(self, user_input, file_path): sheet_count = len(xl.sheet_names) for i, sheet_name in enumerate(xl.sheet_names, 1): df = xl.parse(sheet_name) - csv_file_path = file_path.replace(f".{file_type}", f"_{i}.csv") + csv_file_path = file_path.replace( + f".{file_type}", f"_{i}.csv" + ) csv_file_name = os.path.basename(csv_file_path) self.conversation.log_interaction( role=self.agent_name, @@ -671,7 +673,7 @@ async def learn_spreadsheet(self, user_input, file_path): message=f"[ACTIVITY][ERROR] Failed to read Excel file `{file_name}`: {str(e)}", ) return f"Failed to read [{file_name}]({file_path}). Error: {str(e)}" - + try: df_dict = df.to_dict("records") # Test JSON serialization before proceeding @@ -679,13 +681,13 @@ async def learn_spreadsheet(self, user_input, file_path): except Exception as e: logging.error(f"Error converting DataFrame to dict: {e}") return f"Failed to process [{file_name}]({file_path}). Error converting data format: {str(e)}" - + try: self.input_tokens += get_tokens(json.dumps(df_dict)) except Exception as e: logging.error(f"Error calculating tokens: {e}") # Continue processing even if token calculation fails - + try: for item in df_dict: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -699,7 +701,7 @@ async def learn_spreadsheet(self, user_input, file_path): except Exception as e: logging.error(f"Error writing to memory: {e}") return f"Failed to save [{file_name}]({file_path}) to memory. Error: {str(e)}" - + except Exception as e: logging.error(f"Unexpected error processing spreadsheet: {e}") return f"Failed to process [{file_name}]({file_path}). Unexpected error: {str(e)}" diff --git a/tests/endpoint-tests.ipynb b/tests/endpoint-tests.ipynb index a4741a4eaa6e..c734c6948b8b 100644 --- a/tests/endpoint-tests.ipynb +++ b/tests/endpoint-tests.ipynb @@ -928,15 +928,11 @@ "metadata": {}, "outputs": [], "source": [ - "import requests\n", "import base64\n", "\n", - "zip_url = \"https://github.com/Josh-XT/Setup/archive/refs/heads/main.zip\"\n", - "response = requests.get(zip_url)\n", - "learn_file_path = os.path.join(os.getcwd(), \"Setup-main.zip\")\n", - "with open(learn_file_path, \"wb\") as f:\n", - " f.write(response.content)\n", - "learn_file_content = base64.b64encode(response.content).decode(\"utf-8\")\n", + "learn_file_path = \"test.zip\"\n", + "with open(learn_file_path, \"rb\") as f:\n", + " learn_file_content = base64.b64encode(f.read()).decode(\"utf-8\")\n", "\n", "file_learning = agixt.learn_file(\n", " agent_name=agent_name,\n", @@ -963,9 +959,8 @@ "import base64\n", "\n", "learn_file_path = \"test.csv\"\n", - "with open(learn_file_path, \"wb\") as f:\n", - " f.write(response.content)\n", - "learn_file_content = base64.b64encode(response.content).decode(\"utf-8\")\n", + "with open(learn_file_path, \"rb\") as f:\n", + " learn_file_content = base64.b64encode(f.read()).decode(\"utf-8\")\n", "\n", "file_learning = agixt.learn_file(\n", " agent_name=agent_name,\n", @@ -989,15 +984,11 @@ "metadata": {}, "outputs": [], "source": [ - "import requests\n", "import base64\n", "\n", - "xlsx_url = \"https://getsamplefiles.com/download/xlsx/sample-1.xlsx\"\n", - "response = requests.get(xlsx_url)\n", - "learn_file_path = os.path.join(os.getcwd(), \"sample-1.xlsx\")\n", - "with open(learn_file_path, \"wb\") as f:\n", - " f.write(response.content)\n", - "learn_file_content = base64.b64encode(response.content).decode(\"utf-8\")\n", + "learn_file_path = \"test.xlsx\"\n", + "with open(learn_file_path, \"rb\") as f:\n", + " learn_file_content = base64.b64encode(f.read()).decode(\"utf-8\")\n", "\n", "file_learning = agixt.learn_file(\n", " agent_name=agent_name,\n", @@ -1021,15 +1012,11 @@ "metadata": {}, "outputs": [], "source": [ - "import requests\n", "import base64\n", "\n", - "doc_url = \"https://getsamplefiles.com/download/word/sample-2.docx\"\n", - "response = requests.get(doc_url)\n", - "learn_file_path = os.path.join(os.getcwd(), \"sample-2.docx\")\n", - "with open(learn_file_path, \"wb\") as f:\n", - " f.write(response.content)\n", - "learn_file_content = base64.b64encode(response.content).decode(\"utf-8\")\n", + "learn_file_path = \"test.docx\"\n", + "with open(learn_file_path, \"rb\") as f:\n", + " learn_file_content = base64.b64encode(f.read()).decode(\"utf-8\")\n", "\n", "file_learning = agixt.learn_file(\n", " agent_name=agent_name,\n", @@ -1126,7 +1113,6 @@ "source": [ "import base64\n", "\n", - "agent_name = \"new_agent\"\n", "learn_file_path = \"test.txt\"\n", "with open(learn_file_path, \"rb\") as f:\n", " learn_file_content = base64.b64encode(f.read()).decode(\"utf-8\")\n", diff --git a/tests/test.docx b/tests/test.docx new file mode 100644 index 0000000000000000000000000000000000000000..e57fa05f91913bdff53bbae9abe43496032c0cb7 GIT binary patch literal 6615 zcma)A1yodRw;o_5T$tD36%u0Q8@kgZIrbB^AW+2jA| z=_6&9^ei@Nlt0=(&6rmG8%vAbUI>3``XFlw3PFdsF(C#lO@223f1kK8eecFxezY4>AW#g-n6R17c8*lpd)cDa>g`q|~i2!o8FHSl>NC#Qx z`yX5+!;l}FzFC7wPLC!{KJ8dJVA%|H*V_(@hb=Sus@I?ez8n-Sj!NHwbHEgZ7yPlC zu$_qcW-wUVzLDD4mk=~gto(LtpmRh`XI|Fg0>pSL^;ROMpZNTWzdVG9SY#GSrP4GKw^wxR)6m03d`204V;)JmLH+Pmab;PWM@I{FSDd0h<{%jIPfH2iTh_7a7t# z#LgZXC40pwqe6hEJ(bv=qERbrd-Pqgt05!0M>-SEd#tWBYxE>Kfb0=cVWhk(JohufiN_LAW8L49$hG z>`_uiV<4Xww=;F~6YYj}88f@xW{-UoZ>^>g2`FgJT95bxFtQhR z4Vse|)ZtuDj;-rW%Cav$EN19fX8Y75`;fA+04{`pg+D$<9Z)K;J>twY4Lg1m#Fq02 z%9nYja%UAgAKKRl2Yy;`D>AK+p+(l^>4~*BKpJl?jNFZ2u%S+wMo>Q6(E8>8rsvdw zdX4WjpaMHtf7917_I@(Hg>{C~3r2aJhNYy9Y)w`H9_CS+ zL9(8)9MDv0OL*~<$KS9>J=k$v>4!F6i%i1y#;Q@ecr2ke!|HitB zt&NlF8;F(huOfo|j`d6w?BGOI6dAs6Ij=`SRK?^#E(eDe);+1mSJ}flVXc*Z$b#muqCCL)`mOC4 zpG!R<$t-RGTbc&7`H!WTJPibE(z?YQX17b0n3Jmu4rv>Wz?M*MqfyL~;V3)-rW?^H zh^7ontyEF!^G2y2gd<7xm8K_#xC|%Wo#UgmDcKvNVkP@jbxk}aV|n`Dr=Qy(ijoeB ze;Hc(yZT93-)??BK`)R zGF2M&upwwplfF(ye)&k*kKk#+Pl~;u;u6EzqV?g%8=w50V?dk`<-vYa4*1a~_N`!4 zsz!3r%08rD1nwbm1%cK8_j~Yb{a5f~{TuvF?pDUXpg$hFXEV))|Dnj>-212r41yO| zGFPkE=E>d8uBh|&GlZ&jRaznU7QP?krAd=Pz$Lo|P+Zk_y}uS193Rk@ld@zYShBJS zVo4}4^BOag>Z9R$|15RYJZ%dF18M! ze&ScV3eP!Kv&w;Fo#D3tkHp3&^4n!oQ>*3@VySU{QhHwc-WF#kK6ZsCU7q;VZ>ai0l2#TY% za9x}6fVh`W^;(q;9aX}0=|3I9Cmq|yE(s1^z15zoK1(?$Z;0!1dB+1xdW)TO)l=*~ z);LL+RXX;fRj&(Y&`=6;f(bbp470CP7$K9vvF|lr6JouoOo=QDr(fL#y50D>-3$eM z|8&|>7wDMisl|7LnS6_CCy%+D{Ej2}_PITIeN#j`zYx^%XVWsxqouM%Ppav9weoqQ z(#?b#`Sa@c*TSEjniHQO?(l1F%Im3hC46+>r!=8|7gHoR=bYi3G`-_?aD-X!8guvU zROA6!&_M|dG6Vo%;eK-dKgOz&t)a8Ev5nJ1N8F2_ahT!3{_r{VziOA|2F-uIhlY*Kd_#%p_x>U7fkwYIBA5{;dcW*ocwKYLb2~Ut* z*wjyz_(T-*fZs5`kt^Sc+qww2d-s762+u7}y;QE|63ou4hdyVxMEZ=4QQ*a&!V{^& zt7q5Bs`pi*=cSH!8MAM_4<2+1Ix0E*S+cc4TS(RWqrJbP&NO+Di10~$k;{ZRX=sDX z)^g$8#qzs)3NOY;dichJLKQ1k(b1?!GzrvonVVI4ZDJ*;%b1A>=}zkc)v0(#LkXV zE(^ccDW7V=?tLkO+eX>l=_jmhrRx`F*FGw7i=v$7PZ;R9^3$J95iN(?#VM7zkns+t zXS74Zi~X82b29wN2ZTPhGl_y<6qOjiIO6&sqp`$4-WBD^WFNf2q~}J zcd;qVyh7^K>CIqk%~w-zO}Uy9G1JY-bR`w^16PWOs2gdIp1_b632soWe#7vbwmw$$ zzK$a|E@)xr(If%8!iJPs0-Ec#D2IE3<~HceS@?~D8vor(0lnvBlV8PPus#QSJ;yNeoZ`*`I~kL18RISOA-7{28w@?b{+v zaceLFV{f^WZ!s=BeEz7pPF6&hDJ0o^$#~mE>kNT`6w8;4=CTPB`Yz(d>S<-;Wgrh% zAH>6h$M&@9^&-K@c521?$qli?_5^DgZW?Ddpk74m zb&Y^txVcws9!22k2T_OQuya;l<>Yu&xyeiz=~|RsJMwd_Ic7^2Dl2d0EJj#f3RNu_t;b)+eUCG}#r}m1zxzmKT2Yx@a-3&lbbQ0iKlT zv{^H%GH8}rZ?$m561j^(+<2C$ThX$qi#>g?ylvZeJqjuzQt_RH!S+Ba+c3#*Pt77r z%O}3{zr|>T(2gNS1nIIy=cpKIcnwK=t-f+eczG3H!5q1A><7oMN(j+la2u|)nCdG1 z&N@Sjqatg@UHks*`6e2?S5eAHkbwrSz|H!0)j@!I0cn7umVMJL9BPIHw7<3N}P;zIJ$oWzYF>D;~y z;=$k#M)Zr@tnnBVg3mr;k}C~}P9#Gd9JEI5Q`}GM5r0f@!I?bM?kh|de%yz4U=Lp-2WG0H%FU@6pj_pnWLJ9T*?Eq_9yym)zWkke8^7$cc9aF4dP1Q!_AuNNaS2sUK=Ab?2~$$A{PZ zsF^7{xKh^#w`KW)kp1ny9S6ba615@9O6LOQn2wWGlQenM0qJk)Rv-1(gwL+Nox_$2 zQZgk86gYcYgZw;rghySH)~!Npl)oLWOSa835ZAeKx)JO*oJ_eDUBVUvTpIUCF2&Jm7teopyO(jN)He^HkGC*sRAOx#sM@$^aGqCn$8_XfGeERu zPsjHQ$qOCp^ z3VqRmgPkC|Q&hDj8z>(3iV-fnW-%s>OtpW0Kn2XzMI_6>v2tCWHO9NsBIN4QLA=Q- z)~ZyK(pfYN)?yyO&C-ya8$dv6- zA1TI1l`#lDKy>0$O;I@-amo(e?znUL}Jn&gL2%!9SnlrXA4X`^3OYk2%^a_bnzl`|n+Bsv8C74VPGxVCAl+15ERMd* z3Nxk4agQA+%H{U7X4LQ$B9JeGvs~Ub3E9~n{bc$+AG{drGsNui*oJ(oVF0p6|7T|4j?`;P{20#h?w7{n_EN-rQ7S%lG}m!b&39Zxj1e zQ&vk6&xGrdOL><(I882t9i-&61wvfAQIziNNGA9r2=7i{ zNtg00lobs$R>^W2i>65+X&sS->dg50Lz(4WLQ_byNtEL|^0iWl*c8a&PW3K+Z<4M_ z|NL0(F20WkVq9+9itcN-ULIO@Wd9k0az%tKWU~!l2UhCZy3iXUITODv~2($HL4SZ!QHH zDr4PRCr39!a)*zT)JgI+W6I6^d6QwGN;p!GQOy_CX?jqotO_~fKP z{{=L6b2jx$lH)AGs$#mI>&*8gP*D~R9v|?R0ROw>`yk5w-Tt>6|EK@&a^Hh6{Fmk2 zulURVcbWK4_uoaQzc0UgPq}`%|4kwM)BpFQ^@B9^mu20{SpQi5-$K=&&c74mzrXt( z%0Hcdr@?=^|4t4c*z;fZ{?R|({{xr)^GUyxuLm;um(^hYjavTc_&baqxY=KpaUYFe q+y9fP{rQOBi{k-re;F>}|A{_DS;Twb0syG@AF+F(*Nf=k?tcK4dV^vB literal 0 HcmV?d00001 diff --git a/tests/test.xlsx b/tests/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d614e1588b7fbf039c11f66387e64f3fa1d2a16c GIT binary patch literal 5476 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 literal 0 HcmV?d00001 diff --git a/tests/test.zip b/tests/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..a58c0115f3bcb81eae66e573c79a71c2d9c4972d GIT binary patch literal 391 zcmWIWW@Zs#0D+3dp^@vlugq-#vO$;wh)Yt7OZ1Y9%aoGyb4qjb6bzMgU`!(f(^!d1 zsVKi3EQ?A*#6e0y;s_cdZVVMi&=7GGs5pWSfV*TNqMfvy8#xGPI4N