softwareai
Advanced tools
| [console_scripts] | ||
| softwareai-cli = softwareai_cli:app |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: softwareai | ||
| Version: 0.5.69 | ||
| Version: 0.5.70 | ||
| Summary: SoftwareAI is a framework with the aim of creating a software/application development company/organization governed by AI, its objective is not just to create the software with updates, documentation, schedules and spreadsheets, SoftwareAI is capable of running a software company completely with all the teams that make up a software company | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/SoftwareAI-Company/SoftwareAI |
+7
-2
@@ -5,3 +5,3 @@ from setuptools import setup, find_packages | ||
| name="softwareai", # Nome do pacote | ||
| version="0.05.69", # Versão inicial | ||
| version="0.05.70", # Versão inicial | ||
| description="SoftwareAI is a framework with the aim of creating a software/application development company/organization governed by AI, its objective is not just to create the software with updates, documentation, schedules and spreadsheets, SoftwareAI is capable of running a software company completely with all the teams that make up a software company", | ||
@@ -26,3 +26,8 @@ long_description=open("READMEPIP.md", encoding="utf-8").read(), # Codificação UTF-8 | ||
| "openai" | ||
| ], | ||
| ], | ||
| entry_points={ | ||
| 'console_scripts': [ | ||
| 'softwareai-cli = softwareai_cli:app', | ||
| ], | ||
| }, | ||
| classifiers=[ | ||
@@ -29,0 +34,0 @@ "Programming Language :: Python :: 3", |
| Metadata-Version: 2.1 | ||
| Name: softwareai | ||
| Version: 0.5.69 | ||
| Version: 0.5.70 | ||
| Summary: SoftwareAI is a framework with the aim of creating a software/application development company/organization governed by AI, its objective is not just to create the software with updates, documentation, schedules and spreadsheets, SoftwareAI is capable of running a software company completely with all the teams that make up a software company | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/SoftwareAI-Company/SoftwareAI |
@@ -12,6 +12,6 @@ LICENSE.txt | ||
| softwareai/__init__.py | ||
| softwareai/softwareai-cli.py | ||
| softwareai.egg-info/PKG-INFO | ||
| softwareai.egg-info/SOURCES.txt | ||
| softwareai.egg-info/dependency_links.txt | ||
| softwareai.egg-info/entry_points.txt | ||
| softwareai.egg-info/requires.txt | ||
@@ -18,0 +18,0 @@ softwareai.egg-info/top_level.txt |
| import typer | ||
| import os | ||
| from typing import Dict | ||
| import ast | ||
| import json | ||
| from transformers import AutoTokenizer, AutoModelForCausalLM | ||
| import torch | ||
| import time | ||
| import re | ||
| import subprocess | ||
| def extract_function_arguments(file_path): | ||
| with open(file_path, "r", encoding="utf-8") as file: | ||
| file_content = file.read() | ||
| tree = ast.parse(file_content) | ||
| function_args = {} | ||
| for node in ast.walk(tree): | ||
| print(node) | ||
| if isinstance(node, ast.FunctionDef): | ||
| arg_names = [arg.arg for arg in node.args.args] | ||
| function_args[node.name] = arg_names | ||
| return function_args | ||
| app = typer.Typer() | ||
| # Configurações de Chaves | ||
| @app.command() | ||
| def configure_db_company( | ||
| namefordb: str = typer.Option(..., help="Nome para banco de dados."), | ||
| databaseurl: str = typer.Option(..., help="URL do banco de dados."), | ||
| storagebucketurl: str = typer.Option(..., help="URL do bucket de armazenamento."), | ||
| pathkey: str = typer.Option(..., help="Caminho + arquivo com a Chave do banco de dados da companhia.") | ||
| ): | ||
| """ | ||
| Configura as credenciais do banco de dados da companhia. | ||
| """ | ||
| file_Pathkey = os.path.join(pathkey) | ||
| contentkey = None | ||
| with open(file_Pathkey, 'r', encoding='utf-8') as file: | ||
| contentkey = file.read() | ||
| file.close() | ||
| PATH_caminho = os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp/KeysFirebase')) | ||
| file_path = os.path.join(PATH_caminho, f"keys.py") | ||
| namefilter = namefordb.replace(" ", "_") | ||
| with open(file_path, 'a', encoding='utf-8') as file: | ||
| file.write(F''' | ||
| def keys_{namefilter}(): | ||
| key = {contentkey} | ||
| credt = credentials.Certificate(key) | ||
| app_{namefilter} = initialize_app(credt, {{ | ||
| 'storageBucket': '{storagebucketurl}', | ||
| 'databaseURL': '{databaseurl}' | ||
| }}, name='{namefilter}') | ||
| return app_{namefilter} | ||
| ''') | ||
| file.close() | ||
| # Lógica para armazenar ou validar a chave do banco de dados da companhia | ||
| typer.echo(f"Chave do banco de dados da companhia Salva em: {file_path}") | ||
| @app.command() | ||
| def configure_db_app( | ||
| namefordb: str = typer.Option(..., help="Nome para banco de dados."), | ||
| databaseurl: str = typer.Option(..., help="URL do banco de dados."), | ||
| storagebucketurl: str = typer.Option(..., help="URL do bucket de armazenamento."), | ||
| pathkey: str = typer.Option(..., help="Caminho + arquivo com a Chave do banco de dados do app a ser governado.") | ||
| ): | ||
| """ | ||
| Configura as credenciais do banco de dados do aplicativo. | ||
| """ | ||
| file_Pathkey = os.path.join(pathkey) | ||
| contentkey = None | ||
| with open(file_Pathkey, 'r', encoding='utf-8') as file: | ||
| contentkey = file.read() | ||
| file.close() | ||
| PATH_caminho = os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp/KeysFirebase')) | ||
| file_path = os.path.join(PATH_caminho, f"keys.py") | ||
| namefilter = namefordb.replace(" ", "_") | ||
| with open(file_path, 'a', encoding='utf-8') as file: | ||
| file.write(F''' | ||
| def keys_{namefilter}(): | ||
| key = {contentkey} | ||
| credt = credentials.Certificate(key) | ||
| app_{namefilter} = initialize_app(credt, {{ | ||
| 'storageBucket': '{storagebucketurl}', | ||
| 'databaseURL': '{databaseurl}' | ||
| }}, name='{namefilter}') | ||
| return app_{namefilter} | ||
| ''') | ||
| file.close() | ||
| typer.echo(f"Chave do banco de dados do aplicativ Salva em: {file_path}") | ||
| @app.command() | ||
| def configure_openai( | ||
| name: str = typer.Option(..., help="Nome para Chave da OpenAI."), | ||
| key: str = typer.Option(..., help="Chave da OpenAI.") | ||
| ): | ||
| """ | ||
| Configura a chave de acesso da OpenAI. | ||
| """ | ||
| PATH_caminho = os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp/KeysOpenAI')) | ||
| file_path = os.path.join(PATH_caminho, f"keys.py") | ||
| with open(file_path, 'a', encoding='utf-8') as file: | ||
| file.write(F''' | ||
| class OpenAI_Keys_{name.replace(" ", "_")}: | ||
| def keys(): | ||
| companyname = "{name.replace(" ", "_")}" | ||
| str_key = "{key.replace(" ", "")}" | ||
| return str_key | ||
| ''') | ||
| file.close() | ||
| typer.echo(f"Chave da OpenAI Salva em: {file_path}") | ||
| @app.command() | ||
| def configure_huggingface( | ||
| name: str = typer.Option(..., help="Nome para Chave da Hugging Face."), | ||
| key: str = typer.Option(..., help="Chave da Hugging Face.") | ||
| ): | ||
| """ | ||
| Configura a chave de acesso da Hugging Face. | ||
| """ | ||
| PATH_caminho = os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp/KeysHuggingFace')) | ||
| file_path = os.path.join(PATH_caminho, f"keys.py") | ||
| with open(file_path, 'a', encoding='utf-8') as file: | ||
| file.write(F''' | ||
| class HugKeys_{name.replace(" ", "_")}: | ||
| def hug_{name.replace(" ", "_")}_keys(): | ||
| token = "{key}" | ||
| return token | ||
| ''') | ||
| file.close() | ||
| typer.echo(f"Chave da Hugging Face Salva em: {file_path}") | ||
| @app.command() | ||
| def configure_github_keys( | ||
| name: str = typer.Option(..., help="Nome para Chave do github."), | ||
| github_username: str = typer.Option(..., help="Usuario do agente no github"), | ||
| github_token: str = typer.Option(..., help="Chave do agente no github") | ||
| ): | ||
| PATH_caminho = os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp/KeysGitHub')) | ||
| file_path = os.path.join(PATH_caminho, f"keys.py") | ||
| with open(file_path, 'a', encoding='utf-8') as file: | ||
| file.write(F''' | ||
| class GithubKeys_{name.replace(" ", "_")}: | ||
| def {name.replace(" ", "_")}_github_keys(): | ||
| github_username = "{github_username}" | ||
| github_token = "{github_token}" | ||
| return github_username, github_token | ||
| ''') | ||
| file.close() | ||
| typer.echo(f"Chave do Github Salva em: {file_path}") | ||
| # Automatização da criacao de tools para agentes softwareai | ||
| @app.command() | ||
| def create_function( | ||
| pathfunction: str = typer.Option(..., help="Caminho + arquivo da função."), | ||
| category: str = typer.Option(..., help="Categoria da função."), | ||
| description_autogen_in_gpu: str = typer.Option(..., help="true"), | ||
| cache_dir: str = typer.Option(..., help="D:/LLMModels"), | ||
| ): | ||
| """ | ||
| Cria uma nova função de maneira automatizada com base na descrição fornecida. | ||
| """ | ||
| name = os.path.basename(pathfunction) | ||
| name_filtrer = name.replace(" ", "_").replace(".py", "") | ||
| category_filtrer = category.replace(" ", "_") | ||
| #### Create tools #### | ||
| args = extract_function_arguments(pathfunction) | ||
| for function_name, arguments in args.items(): | ||
| def create_tool_file(tool_name, function_arguments, description="Descrição da função."): | ||
| """Cria um arquivo de definição de tools baseado nos argumentos da função.""" | ||
| tools_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'CoreApp', 'SoftwareAI', 'Tools', f"{category_filtrer}")) | ||
| os.makedirs(tools_dir, exist_ok=True) | ||
| file_path = os.path.join(tools_dir, f"{tool_name}.py") | ||
| # Criar propriedades baseadas nos argumentos da função | ||
| properties = {arg: {"type": "string", "description": f"Descrição do argumento {arg}."} for arg in function_arguments} | ||
| if description_autogen_in_gpu: | ||
| with open(os.path.abspath(os.path.join(os.path.dirname(__file__), 'CoreApp', 'SoftwareAI', 'Functions', f"{category_filtrer}", f"{name_filtrer}.py" )), "r", encoding="utf-8") as f: | ||
| contentcode = f.read() | ||
| f.close() | ||
| def autocreatedescription(code): | ||
| model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B" | ||
| max_new_tokens = 1900 | ||
| input_text = f""" | ||
| Create a description for the code function arguments | ||
| code: | ||
| {code} | ||
| """ | ||
| # Carregar o modelo e o tokenizer | ||
| model = AutoModelForCausalLM.from_pretrained( | ||
| model_name, | ||
| torch_dtype="auto", | ||
| device_map="auto", | ||
| cache_dir=cache_dir | ||
| ) | ||
| tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir) | ||
| # Preparar a entrada | ||
| messages = [{"role": "user", "content": f"{input_text}"}] | ||
| text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | ||
| model_inputs = tokenizer([text], return_tensors="pt").to(model.device) | ||
| # Inicializar variáveis | ||
| input_ids = model_inputs['input_ids'] | ||
| generated_ids = input_ids | ||
| num_generated_tokens = 0 | ||
| # Variável para armazenar o texto após a segunda ocorrência | ||
| text_after_second_think = None | ||
| found_second_think = False | ||
| # Variável para acumular texto gerado no streaming | ||
| current_text = "" | ||
| # Gerar texto em "streaming" | ||
| while num_generated_tokens < max_new_tokens: | ||
| # Gerar um token de cada vez | ||
| outputs = model.generate( | ||
| generated_ids, | ||
| max_length=generated_ids.shape[1] + 1, # Apenas um token adicional | ||
| do_sample=True, | ||
| top_k=50, | ||
| top_p=0.95, | ||
| temperature=0.7, | ||
| pad_token_id=tokenizer.eos_token_id | ||
| ) | ||
| # Atualizar a sequência gerada | ||
| generated_ids = outputs[:, :generated_ids.shape[1] + 1] | ||
| generated_token = generated_ids[0, -1].item() | ||
| # Mostrar o token gerado | ||
| generated_text = tokenizer.decode(generated_ids[0, -1:], skip_special_tokens=True) | ||
| print(generated_text, end='', flush=True) | ||
| # Acumular texto até a segunda ocorrência de </think> | ||
| current_text += generated_text | ||
| # Verificar se o token gerado é o final (EOS) | ||
| if generated_token == tokenizer.eos_token_id: | ||
| break | ||
| matches = list(re.finditer(r"</think>\n", current_text)) | ||
| #print(len(matches)) | ||
| if len(matches) == 1: | ||
| start_index = matches[0].end() | ||
| text_after_second_think = current_text[start_index:] | ||
| return text_after_second_think | ||
| description_DeepSeek_R1 = autocreatedescription(contentcode) | ||
| description = description_DeepSeek_R1 | ||
| # Criar propriedades baseadas nos argumentos da função | ||
| properties = {arg: {"type": "string", "description": f"Descrição do argumento {arg}."} for arg in function_arguments} | ||
| required_args = function_arguments | ||
| # Criar o conteúdo do arquivo | ||
| tool_content = { | ||
| "type": "function", | ||
| "function": { | ||
| "name": tool_name, | ||
| "description": description, | ||
| "parameters": { | ||
| "type": "object", | ||
| "properties": properties, | ||
| "required": required_args | ||
| } | ||
| } | ||
| } | ||
| # Escrever no arquivo | ||
| with open(file_path, 'w', encoding='utf-8') as file: | ||
| file.write(f"tools_{tool_name} = [\n") | ||
| file.write(json.dumps(tool_content, indent=4)) | ||
| file.write("\n]\n") | ||
| create_tool_file(tool_name=function_name, function_arguments=arguments) | ||
| break | ||
| ################################ | ||
| #### Create init functions #### | ||
| with open(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp')), f"_init_functions_.py"), 'r+', encoding='utf-8') as file: | ||
| content = file.read() | ||
| if f"from softwareai.CoreApp.SoftwareAI.Functions.{category_filtrer}.{name_filtrer}" not in content: | ||
| file.write(F''' | ||
| from softwareai.CoreApp.SoftwareAI.Functions.{category_filtrer}.{name_filtrer} import * | ||
| ''') | ||
| file.close() | ||
| ################################ | ||
| #### Create init tools #### | ||
| with open(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp' )), f"_init_tools_.py"), 'r+', encoding='utf-8') as file: | ||
| content = file.read() | ||
| if f"{category_filtrer}.{name_filtrer}" not in content: | ||
| file.write(F''' | ||
| from softwareai.CoreApp.SoftwareAI.Tools.{category_filtrer}.{name_filtrer} import * | ||
| ''') | ||
| file.close() | ||
| ################################ | ||
| #### Create Submit Output #### | ||
| PATH_caminho_Submit_Output = os.path.abspath(os.path.join(os.path.dirname(__file__), f'CoreApp', 'SoftwareAI', 'Functions_Submit_Outputs', f'{category_filtrer}')) | ||
| os.makedirs(PATH_caminho_Submit_Output, exist_ok=True) | ||
| file_path_Submit_Output = os.path.join(PATH_caminho_Submit_Output, f'{name_filtrer}.py') | ||
| with open(file_path_Submit_Output, 'x', encoding='utf-8') as file: | ||
| file.write(F''' | ||
| ######################################### | ||
| # IMPORT SoftwareAI Libs | ||
| from softwareai.CoreApp._init_libs_ import * | ||
| ######################################### | ||
| # IMPORT SoftwareAI Core | ||
| from softwareai.CoreApp._init_core_ import * | ||
| ######################################### | ||
| # IMPORT SoftwareAI Functions | ||
| from softwareai.CoreApp._init_functions_ import * | ||
| ######################################### | ||
| tool_outputs = [] | ||
| def submit_output_{name_filtrer}(function_name, | ||
| function_arguments, | ||
| tool_call, | ||
| threead_id, | ||
| client, | ||
| run, | ||
| appfb, | ||
| appproduct | ||
| ): | ||
| global tool_outputs | ||
| # Mapear funções pelo nome | ||
| functions_map = {{ | ||
| "{name_filtrer}": {name_filtrer}, | ||
| }} | ||
| # Obter a função correspondente | ||
| target_function = functions_map.get(function_name) | ||
| if not target_function: | ||
| print(f"Função {{function_name}} não encontrada.") | ||
| return False | ||
| # Inspecionar os argumentos da função | ||
| function_signature = inspect.signature(target_function) | ||
| function_parameters = function_signature.parameters | ||
| # Preparar argumentos para chamada | ||
| args = json.loads(function_arguments) | ||
| call_arguments = {{}} | ||
| # Adicionar parâmetros obrigatórios | ||
| if "appcompany" in function_parameters: | ||
| call_arguments["appcompany"] = appfb | ||
| if "appfb" in function_parameters: | ||
| call_arguments["appfb"] = appfb | ||
| if "appproduct" in function_parameters: | ||
| call_arguments["appproduct"] = appproduct | ||
| if "app_product" in function_parameters: | ||
| call_arguments["app_product"] = appproduct | ||
| # Adicionar outros argumentos do JSON somente se estiverem na assinatura da função | ||
| for arg_name, arg_value in args.items(): | ||
| if arg_name in function_parameters: | ||
| call_arguments[arg_name] = arg_value | ||
| try: | ||
| # Chamar a função com os argumentos preparados | ||
| result = target_function(**call_arguments) | ||
| # Submeter o resultado | ||
| tool_call_id = tool_call.id | ||
| client.beta.threads.runs.submit_tool_outputs( | ||
| thread_id=threead_id, | ||
| run_id=run.id, | ||
| tool_outputs=[ | ||
| {{ | ||
| "tool_call_id": tool_call_id, | ||
| "output": json.dumps(result), | ||
| }} | ||
| ] | ||
| ) | ||
| print("Tool outputs submitted successfully.") | ||
| return True | ||
| except Exception as e: | ||
| print(f"Erro ao executar {{function_name}}: {{e}}") | ||
| ''') | ||
| file.close() | ||
| ################################ | ||
| #### Create init submit outputs #### | ||
| new_function_name = f"submit_output_{name_filtrer}" | ||
| with open(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), 'CoreApp')), '_init_submit_outputs_.py'), 'r+', encoding='utf-8') as file: | ||
| content = file.read() | ||
| if new_function_name not in content: | ||
| updated_content = content.replace( | ||
| "functions_to_call = [", | ||
| f"functions_to_call = [\n {new_function_name}," | ||
| ) | ||
| file.seek(0) | ||
| file.write(updated_content) | ||
| file.truncate() | ||
| else: | ||
| print("Função já está na lista.") | ||
| with open(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), 'CoreApp')), '_init_submit_outputs_.py'), 'r+', encoding='utf-8') as file: | ||
| content = file.read() | ||
| new_import = f"from softwareai.CoreApp.SoftwareAI.Functions_Submit_Outputs.{category_filtrer}.{name_filtrer} import submit_output_{name_filtrer}\n" | ||
| if new_import not in content: | ||
| file.seek(0) | ||
| file.write(new_import + content) | ||
| file.truncate() | ||
| ################################ | ||
| file_path_tool = os.path.abspath(os.path.join(os.path.dirname(__file__), 'CoreApp', 'SoftwareAI', 'Tools', f"{category_filtrer}", f"{name_filtrer}.py")) | ||
| typer.echo(f"\nTool criada e salva em: {file_path_tool}") | ||
| # Agente Selecionado Em Modo API | ||
| @app.command() | ||
| def select_agent_mode_api( | ||
| name_agent: str = typer.Option(..., help="Nome do agente a ser executado"), | ||
| category_agent: str = typer.Option(..., help="Nome da Categoria do agente a ser executado"), | ||
| local_execute_port: str = typer.Option(..., help="A porta onde o agente sera consultado"), | ||
| ): | ||
| name_agent_filter = name_agent.replace(" ", "_").replace(".py", "") | ||
| category_agent_filter = category_agent.replace(" ", "_") | ||
| # Caminho do arquivo | ||
| diretorio = os.path.join( | ||
| os.path.dirname(__file__), | ||
| 'CoreApp', | ||
| 'Agents', | ||
| f"{category_agent_filter}", | ||
| f'{name_agent_filter}_api.py' | ||
| ) | ||
| with open(diretorio, 'r') as arquivo: | ||
| conteudo = arquivo.readlines() | ||
| # Modifica a porta na linha contendo app.run | ||
| for i, linha in enumerate(conteudo): | ||
| if "if __name__ == '__main__':" in linha: | ||
| # Procura a linha com app.run na sequência | ||
| for j in range(i + 1, len(conteudo)): | ||
| if "app.run" in conteudo[j]: | ||
| conteudo[j] = f" app.run(port={local_execute_port})\n" | ||
| break | ||
| break | ||
| # Escreve de volta no arquivo | ||
| with open(diretorio, 'w') as arquivo: | ||
| arquivo.writelines(conteudo) | ||
| print(f"A porta foi alterada para {local_execute_port}") | ||
| comando_terminal = ['start', 'python', diretorio] | ||
| subprocess.Popen(comando_terminal, shell=True) | ||
| print(f"Executando {name_agent} em http://127.0.0.1:100") | ||
| @app.command() | ||
| def create_instruction(agent_name: str = typer.Option(..., help="Nome do agente."), instruction: str = typer.Option(..., help="Instrução para o agente.")): | ||
| """ | ||
| Cria instruções para um novo agente de maneira simples e automatizada. | ||
| """ | ||
| # Lógica para configurar as instruções do agente | ||
| typer.echo(f"Instruções criadas para o agente {agent_name}: {instruction}") | ||
| @app.command() | ||
| def create_prompt(agent_name: str = typer.Option(..., help="Nome do agente."), prompt: str = typer.Option(..., help="Prompt para o agente.")): | ||
| """ | ||
| Cria um prompt otimizado para o agente especificado. | ||
| """ | ||
| # Lógica para gerar e armazenar o prompt | ||
| typer.echo(f"Prompt criado para o agente {agent_name}: {prompt}") | ||
| # Operações e Gerenciamento | ||
| @app.command() | ||
| def run_web(): | ||
| """ | ||
| Executa a interface web localmente. | ||
| """ | ||
| # Lógica para iniciar a interface web | ||
| typer.echo("Interface web iniciada localmente.") | ||
| @app.command() | ||
| def modify_agent(agent_name: str = typer.Option(..., help="Nome do agente."), component: str = typer.Option(..., help="Componente a ser modificado."), new_value: str = typer.Option(..., help="Novo valor para o componente.")): | ||
| """ | ||
| Modifica partes específicas de um agente de forma automatizada. | ||
| """ | ||
| # Lógica para modificar o agente | ||
| typer.echo(f"Agente {agent_name} modificado: {component} atualizado para {new_value}") | ||
| @app.command() | ||
| def execute_agent_task(agent_name: str = typer.Option(..., help="Nome do agente."), task: str = typer.Option(..., help="Tarefa a ser executada.")): | ||
| """ | ||
| Executa uma tarefa específica no agente selecionado. | ||
| """ | ||
| # Lógica para executar a tarefa no agente | ||
| typer.echo(f"Tarefa '{task}' executada no agente {agent_name}.") | ||
| if __name__ == "__main__": | ||
| app() |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
71164381
-0.03%22631
-1.92%