From 6b11f05dab38b3e57db8336807affc928b26fe3a Mon Sep 17 00:00:00 2001 From: Marco Costa Date: Mon, 26 Oct 2026 13:50:10 -0300 Subject: [PATCH] refactor(i18n): translate codebase and documentation to English - Rewrite README.md, DEPLOY.md and RULES.md in English - Translate module docstring, license string and inline comments - Rename Portuguese state variables (is_agendamento, is_useragenda, is_configia) to English equivalents (is_schedule, is_dailyroutine, is_configai) - Replace Portuguese web-search and file-creation trigger phrases with English keywords --- DEPLOY.md | 148 +- README.md | 140 +- RULES.md | 360 ++-- __pycache__/psagentai_agent.cpython-311.pyc | Bin 278612 -> 274248 bytes psagentai_agent.py | 2122 +++++++++---------- 5 files changed, 1368 insertions(+), 1402 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 3bf4fea..246b079 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,17 +1,17 @@ -# PsAgentAI - Deploy em Servidor +# PsAgentAI - Server Deployment -Deploy como serviço systemd em Debian/Ubuntu para funcionar 24/7 em VPS. +Deploy as a systemd service on Debian/Ubuntu to run 24/7 on a VPS. --- -## Pré-requisitos no servidor +## Prerequisites on the server ```bash sudo apt update sudo apt install -y python3 python3-venv python3-pip git ffmpeg ``` -Habilite o journal se quiser logs persistentes (opcional): +Enable journal if you want persistent logs (optional): ```bash sudo apt install -y systemd-journal-remote @@ -19,58 +19,58 @@ sudo apt install -y systemd-journal-remote --- -## 1) Copie o projeto para o servidor +## 1) Copy the project to the server -Use git ou scp/rsync. Exemplo com git: +Use git or scp/rsync. Example with git: ```bash cd /opt -sudo git clone psagentai +sudo git clone psagentai sudo chown -R $USER:$USER /opt/psagentai cd /opt/psagentai/app ``` -Ajuste o caminho no serviço systemd para o diretório real: +Adjust the path in the systemd service to the real directory: ```bash nano psagentai.service -# altere /home/marco/PsAgentAI/app para /opt/psagentai/app +# change /home/marco/PsAgentAI/app to /opt/psagentai/app ``` -> O serviço usa `User=marco` por padrão. Crie o usuário se não existir: +> The service uses `User=marco` by default. Create the user if it doesn't exist: > ```bash > sudo useradd -m -s /bin/bash marco || true > sudo usermod -aG sudo marco > ``` -> Ou edite `User=` no `.service` para o usuário existente do seu VPS. +> Or edit `User=` in the `.service` to an existing user on your VPS. --- -## 2) Configure o `.env` +## 2) Configure `.env` ```bash -cp .env.example .env # se existir exemplo +cp .env.example .env # if example exists nano .env ``` -Preencha obrigatoriamente: +Fill in the required fields: - `TELEGRAM_BOT_TOKEN` - `TELEGRAM_CHAT_ID` -- `AI_PROVIDER` (`openrouter`, `stepfun`, `qwencloud` ou `llamacpp`) +- `AI_PROVIDER` (`openrouter`, `stepfun`, `qwencloud` or `llamacpp`) - `DEFAULT_MODEL` -- pelo menos a chave de IA correspondente ao provider escolhido +- at least the AI key corresponding to the chosen provider -Ajuste opcionalmente: +Optionally adjust: - `MIN_INTERVAL_MINUTES` / `MAX_INTERVAL_MINUTES` - `MIN_MESSAGE_LINES` -> Importante: no Telegram, envie `/start` uma vez para o bot antes de ligar o serviço, caso contrário ele não conseguirá enviar mensagens iniciantes. +> Important: on Telegram, send `/start` once to the bot before turning on the service, otherwise it won't be able to send starting messages. --- -## 3) Instale as dependências +## 3) Install dependencies ```bash sudo apt install -y ffmpeg @@ -80,26 +80,26 @@ chmod +x iniciar.sh ./iniciar.sh ``` -Esse script cria o `venv`, instala dependências e valida as variáveis do `.env`. +This script creates the `venv`, installs dependencies and validates the `.env` variables. -Se o comando acima não instalar `ffmpeg` na versão esperada, use também: +If the above command doesn't install the expected `ffmpeg` version, also use: ```bash sudo apt install -y ffmpeg ``` -> O reconhecimento de voz usa `SpeechRecognition` + `pydub` + `ffmpeg`. +> Voice recognition uses `SpeechRecognition` + `pydub` + `ffmpeg`. --- -## 4) Instale o serviço systemd +## 4) Install the systemd service ```bash -# caminho absoluto no seu servidor +# absolute path on your server sudo nano /etc/systemd/system/psagentai.service ``` -Conteúdo mínimo: +Minimum content: ```ini [Unit] @@ -121,7 +121,7 @@ StandardError=journal WantedBy=multi-user.target ``` -Ative e inicie: +Enable and start: ```bash sudo systemctl daemon-reload @@ -131,25 +131,25 @@ sudo systemctl start psagentai --- -## 5) Comandos úteis +## 5) Useful commands ```bash # status sudo systemctl status psagentai -# logs em tempo real +# real-time logs sudo journalctl -u psagentai -f -# reiniciar após atualização +# restart after update sudo systemctl restart psagentai -# parar +# stop sudo systemctl stop psagentai ``` --- -## 6) Atualizações futuras +## 6) Future updates ```bash cd /opt/psagentai/app @@ -160,70 +160,70 @@ sudo systemctl restart psagentai --- -## 7) Múltiplos usuários / VPSs +## 7) Multiple users / VPSs -Cada instância do sistema é independente por chat ID do Telegram. +Each system instance is independent by Telegram chat ID. -Para cada novo usuário/VPS: +For each new user/VPS: -1. Crie o bot via @BotFather (`/newbot`) e pegue o token. -2. Copie o projeto em um diretório separado, por exemplo `/opt/psagentai-user2`. -3. Crie um `.env` próprio com `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` e suas chaves de IA. -4. Instale dependências com `./iniciar.sh`. -5. Crie um serviço systemd separado, ex: `psagentai-user2.service`, apontando para esse diretório. -6. Habilite e inicie. +1. Create the bot via @BotFather (`/newbot`) and get the token. +2. Copy the project to a separate directory, for example `/opt/psagentai-user2`. +3. Create your own `.env` with `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` and your AI keys. +4. Install dependencies with `./iniciar.sh`. +5. Create a separate systemd service, ex: `psagentai-user2.service`, pointing to that directory. +6. Enable and start. -Nomes recomendados para evitar conflito: +Recommended names to avoid conflicts: - `/opt/psagentai-user1/`, `/opt/psagentai-user2/` - `psagentai-user1.service`, `psagentai-user2.service` --- -## 8) Variáveis do `.env` +## 8) `.env` variables -| Variável | Obrigatória | Descrição | +| Variable | Required | Description | |---|---|---| -| `AI_PROVIDER` | não | Provider preferencial: `openrouter`, `stepfun`, `qwencloud` ou `llamacpp` | -| `TELEGRAM_BOT_TOKEN` | sim | Token do bot do Telegram | -| `TELEGRAM_CHAT_ID` | sim | Chat ID do usuário | -| `DEFAULT_MODEL` | sim | Modelo de IA padrão | -| `OPENROUTER_API_KEY` | 1 das chaves | Chave OpenRouter | -| `STEPFUN_API_KEY` | 1 das chaves | Chave StepFun | -| `QWENCLOUD_API_KEY` | 1 das chaves | Chave QwenCloud | -| `MIN_INTERVAL_MINUTES` | não | Intervalo mínimo para mensagens proativas | -| `MAX_INTERVAL_MINUTES` | não | Intervalo máximo para mensagens proativas | -| `MIN_MESSAGE_LINES` | não | Mínimo de linhas por mensagem do agente | -| `AUTO_ENRICH_EVERY` | não | Enriquece perfil a cada N mensagens | -| `AUTO_IMPROVE_EVERY` | não | Melhoria automática a cada N mensagens | -| `UPLOAD_CONTEXT_MAX_CHARS` | não | Limite de caracteres do upload enviado para a IA (`0` = sem limite) | -| `VISION_MODEL` | não | Modelo de IA para análise de imagens | -| `VISION_PROVIDER` | não | Provider do modelo de visão (`openrouter`, `stepfun`, `qwencloud`) | +| `AI_PROVIDER` | no | Preferred provider: `openrouter`, `stepfun`, `qwencloud` or `llamacpp` | +| `TELEGRAM_BOT_TOKEN` | yes | Telegram bot token | +| `TELEGRAM_CHAT_ID` | yes | User chat ID | +| `DEFAULT_MODEL` | yes | Default AI model | +| `OPENROUTER_API_KEY` | 1 of the keys | OpenRouter key | +| `STEPFUN_API_KEY` | 1 of the keys | StepFun key | +| `QWENCLOUD_API_KEY` | 1 of the keys | QwenCloud key | +| `MIN_INTERVAL_MINUTES` | no | Minimum interval for proactive messages | +| `MAX_INTERVAL_MINUTES` | no | Maximum interval for proactive messages | +| `MIN_MESSAGE_LINES` | no | Minimum lines per agent message | +| `AUTO_ENRICH_EVERY` | no | Enriches profile every N messages | +| `AUTO_IMPROVE_EVERY` | no | Auto improvement every N messages | +| `UPLOAD_CONTEXT_MAX_CHARS` | no | Character limit of upload sent to AI (`0` = no limit) | +| `VISION_MODEL` | no | AI model for image analysis | +| `VISION_PROVIDER` | no | Vision model provider (`openrouter`, `stepfun`, `qwencloud`) | --- -## Comandos úteis via Telegram +## Useful commands via Telegram -- `/veria` — mostra o provider e o modelo de IA atuais -- `/configia` — configura interativamente o provider e o modelo de IA -- `/veriavision` — mostra o provider e o modelo de visão atuais -- `/configiavision` — configura interativamente o modelo de visão -- `/about` — exibe informações do sistema, versão, autor e licença +- `/viewai` — shows current AI provider and model +- `/configai` — interactively configures AI provider and model +- `/viewvision` — shows current vision provider and model +- `/configvision` — interactively configures vision model +- `/about` — displays system information, version, author and license --- -## 9) Backups importantes +## 9) Important backups -Faça backup regular dos arquivos de dados dentro de `/app`: +Regularly backup the data files inside `/app`: - `user_profile.json` - `agent_profile.json` - `system_improvements.json` - `uploads_metadata.json` -- pasta `contexts/` -- pasta `workspace/` +- `contexts/` folder +- `workspace/` folder -Exemplo simples de backup diário: +Simple daily backup example: ```bash tar czf /root/backups/psagentai-$(date +%F).tar.gz -C /opt/psagentai/app user_profile.json agent_profile.json system_improvements.json uploads_metadata.json contexts workspace @@ -231,9 +231,9 @@ tar czf /root/backups/psagentai-$(date +%F).tar.gz -C /opt/psagentai/app user_pr --- -## 10) Segurança +## 10) Security -- Nunca comite o `.env` no repositório. -- Restrinja permissões: `chmod 600 .env`. -- Use um usuário exclusivo para cada instância do serviço. -- Para maior isolamento, execute cada instância em um container Docker separado ou em VMs distintas. +- Never commit `.env` to the repository. +- Restrict permissions: `chmod 600 .env`. +- Use an exclusive user for each service instance. +- For greater isolation, run each instance in a separate Docker container or on distinct VMs. diff --git a/README.md b/README.md index 6fd76e9..298c562 100755 --- a/README.md +++ b/README.md @@ -1,43 +1,43 @@ -# PsAgentAI - Personal Agent AI Conversacional +# PsAgentAI - Personal Conversational AI Agent -Sistema de Personal Agent AI Conversacional via Telegram que se comporta como uma companheira pessoal, conversando, cooperando e mantendo contexto contínuo com o usuário. +Personal Conversational AI Agent system via Telegram that behaves as a personal companion, chatting, cooperating and maintaining continuous context with the user. -## Estrutura +## Structure ``` app/ -├── psagentai_agent.py # Código principal do agente -├── iniciar.sh # Script de instalação e execução -├── requirements.txt # Dependências Python -├── .env # Configurações locais (não comitar) -├── .env.example # Exemplo de configuração -├── user_profile.json # Perfil do usuário -├── agent_profile.json # Perfil do agente (personalidade, traços, notas) -├── contexts/ # Históricos de conversa por data +├── psagentai_agent.py # Main agent code +├── iniciar.sh # Installation and execution script +├── requirements.txt # Python dependencies +├── .env # Local settings (do not commit) +├── .env.example # Configuration example +├── user_profile.json # User profile +├── agent_profile.json # Agent profile (personality, traits, notes) +├── contexts/ # Conversation history by date │ └── YYYY-MM-DD.json ├── README.md └── LICENSE ``` -## Pré-requisitos +## Prerequisites - Python 3.9+ -- Conta no Telegram com bot criado via @BotFather -- Pelo menos uma chave de API de IA suportada +- Telegram account with bot created via @BotFather +- At least one supported AI API key -## Configuração +## Configuration -1. Copie `.env.example` para `.env` e preencha: - - `TELEGRAM_BOT_TOKEN` — token do bot do Telegram - - `TELEGRAM_CHAT_ID` — seu chat ID no Telegram - - `OPENROUTER_API_KEY` ou outra chave de API compatível +1. Copy `.env.example` to `.env` and fill in: + - `TELEGRAM_BOT_TOKEN` — Telegram bot token + - `TELEGRAM_CHAT_ID` — your Telegram chat ID + - `OPENROUTER_API_KEY` or other compatible API key -2. Ajuste intervalos se desejar: +2. Adjust intervals if desired: - `MIN_INTERVAL_MINUTES` - `MAX_INTERVAL_MINUTES` - - `MIN_MESSAGE_LINES` — define o número mínimo de linhas por mensagem do agente (padrão: `15`) + - `MIN_MESSAGE_LINES` — defines the minimum number of lines per agent message (default: `15`) -## Instalação e execução +## Installation and execution ```bash cd app @@ -45,71 +45,71 @@ chmod +x iniciar.sh ./iniciar.sh ``` -O script cria o ambiente virtual, instala dependências e inicia o agente. +The script creates the virtual environment, installs dependencies and starts the agent. -## Funcionalidades +## Features -- Onboarding inicial com perguntas para construir o perfil -- Histórico de conversa por dia em `contexts/` -- Perfil do usuário persistido em `user_profile.json` -- Perfil do agente persistido em `agent_profile.json` (nome, personalidade, traços e notas) -- Mensagens proativas em intervalos aleatórios configuráveis -- Mínimo de linhas por mensagem do agente configurável via `.env` -- Suporte a múltiplos provedores de IA: +- Initial onboarding with questions to build the profile +- Conversation history by day in `contexts/` +- User profile persisted in `user_profile.json` +- Agent profile persisted in `agent_profile.json` (name, personality, traits and notes) +- Proactive messages at configurable random intervals +- Minimum lines per agent message configurable via `.env` +- Support for multiple AI providers: - OpenRouter - StepFun - QwenCloud - llama.cpp (local) -- Atualização dinâmica do perfil via comandos naturais no chat +- Dynamic profile update via natural commands in chat -## Comandos disponíveis +## Available commands -- `/start` — inicia ou reinicia a conversa -- `/ajuda` ou `/help` — exibe ajuda -- `/perfil` — mostra perfil do usuário e do agente -- `/agentprofile` — mostra apenas o perfil do agente -- `/reset` — reseta perfis do usuário e do agente -- `/limpar` — deleta todos os dados (perfis, contextos, chat_id) -- `/onboarding` — reinicia o onboarding -- `/newconfig` — reconfigura tudo do zero -- `/cancelar` — cancela operação em andamento -- `/veria` — mostra o provider e o modelo de IA atuais -- `/configia` — configura interativamente o provider e o modelo de IA com seleção por botões -- `/about` — exibe informações do sistema, versão, autor e licença -- `/veriavision` — mostra o provider e o modelo de visão atuais -- `/configiavision` — configura interativamente o modelo de visão com seleção por botões -- `/baixar ` — baixa um arquivo do workspace pelo nome +- `/start` — starts or restarts the conversation +- `/help` — displays help +- `/profile` — shows user and agent profile summary +- `/agentprofile` — shows only the agent profile +- `/reset` — resets user and agent profiles +- `/clear` — deletes all data (profiles, contexts, chat_id) +- `/onboarding` — restarts onboarding +- `/newconfig` — reconfigures everything from scratch +- `/cancel` — cancels ongoing operation +- `/viewai` — shows current AI provider and model +- `/configai` — interactively configures AI provider and model with button selection +- `/about` — displays system information, version, author and license +- `/viewvision` — shows current vision provider and model +- `/configvision` — interactively configures vision model with button selection +- `/download ` — downloads a workspace file by name -## Editando arquivos do workspace +## Editing workspace files -Você pode pedir para editar arquivos existentes no `workspace/` usando comandos como: -- `edite o arquivo teste.txt e adicione uma linha 4` -- `modifique o arquivo hello.py para imprimir hello world` -- `no arquivo teste.txt troque Linha 2 por Linha 2 editada` +You can ask to edit existing files in `workspace/` using commands like: +- `edit the file test.txt and add line 4` +- `modify the file hello.py to print hello world` +- `in the file test.txt replace Line 2 with Line 2 edited` -O Agent irá localizar o arquivo, aplicar a alteração solicitada e salvar o resultado. -- `/incluirnomeuperfil ` — atualiza seu nome no perfil -- `/incluirnoperfildoagent ` — adiciona características ao perfil do agente +The Agent will locate the file, apply the requested change and save the result. +- `/addtouserprofile ` — updates information in your profile +- `/addtoagentprofile ` — adds characteristics to the agent profile -## Atualizando o perfil durante a conversa +## Updating profile during conversation -### Perfil do usuário +### User profile -Você pode atualizar seu perfil a qualquer momento usando frases como: +You can update your profile at any time using phrases like: -- `Inclui isso no meu perfil: ...` -- `Anote no meu perfil: ...` -- `Adicione no meu perfil: ...` -- `Salve no meu perfil: ...` +- `Include this in my profile: ...` +- `Note in my profile: ...` +- `Add to my profile: ...` +- `Save in my profile: ...` -### Perfil do agente +### Agent profile -Use `/incluirnoperfildoagent ` para adicionar características ao perfil do agente. -Ou use frases como: +Use `/addtoagentprofile ` to add characteristics to the agent profile. +Or use phrases like: -- `Inclui isso no perfil do agente: ...` -- `Anote no perfil do agente: ...` +- `Include this in the agent profile: ...` +- `Note in the agent profile: ...` -## Licença +## License -Uso proprietário. Veja o arquivo `LICENSE`. +Proprietary use. See the `LICENSE` file. diff --git a/RULES.md b/RULES.md index 3fc4c6a..7ca038d 100644 --- a/RULES.md +++ b/RULES.md @@ -1,289 +1,289 @@ -# PsAgentAI - Regras de Desenvolvimento +# PsAgentAI - Development Rules -Regras oficiais para IAs e desenvolvedores codarem o sistema PsAgentAI de forma correta, padronizada e segura. +Official rules for AIs and developers to code the PsAgentAI system correctly, standardized and securely. --- -## 1. Visão Geral do Projeto +## 1. Project Overview -- **Nome:** PsAgentAI -- **Tipo:** Bot Telegram de agente de IA conversacional -- **Linguagem:** Python 3.9+ +- **Name:** PsAgentAI +- **Type:** Telegram bot for conversational AI agent +- **Language:** Python 3.9+ - **Framework:** python-telegram-bot (v20+) -- **Arquivo principal:** `psagentai_agent.py` -- **Propósito:** Companheiro pessoal conversacional com memória, perfis, workspace de arquivos, análise de imagens, pesquisa web e automações. +- **Main file:** `psagentai_agent.py` +- **Purpose:** Personal conversational companion with memory, profiles, file workspace, image analysis, web search and automations. -### Regra básica -Toda alteração deve preservar o comportamento existente a menos que o usuário solicite explicitamente uma mudança. Alterações estruturais devem ser validadas com `python3 -m py_compile psagentai_agent.py`. +### Basic rule +Every change must preserve existing behavior unless the user explicitly requests a change. Structural changes must be validated with `python3 -m py_compile psagentai_agent.py`. --- -## 2. Padrões de Código +## 2. Code Standards -### 2.1 Estilo -- Siga o estilo já estabelecido no `psagentai_agent.py`. -- Use aspas duplas para strings curtas e mensagens; aspas simples apenas quando houver aspas duplas dentro. -- Indentação: 4 espaços. -- Nomes de funções/arquivos: `snake_case`. -- Constantes globais em `UPPER_CASE`. -- Comentários em português do Brasil quando necessário; evite comentários óbvios. -- Mantenha logs `print("[LABEL] ...")` apenas onde já existirem; não adicione novos logs sem necessidade. +### 2.1 Style +- Follow the style already established in `psagentai_agent.py`. +- Use double quotes for short strings and messages; single quotes only when there are double quotes inside. +- Indentation: 4 spaces. +- Function/file names: `snake_case`. +- Global constants in `UPPER_CASE`. +- Comments in Brazilian Portuguese when necessary; avoid obvious comments. +- Keep `print("[LABEL] ...")` logs only where they already exist; do not add new logs without need. -### 2.2 Estrutura do arquivo principal -Mantenha a ordem lógica: +### 2.2 Main file structure +Maintain the logical order: 1. Imports -2. Variáveis globais e constantes -3. Funções utilitárias -4. Funções de profile/context/workspace -5. Handlers de comandos -6. Handlers de mensagens/voz/foto/documento +2. Global variables and constants +3. Utility functions +4. Profile/context/workspace functions +5. Command handlers +6. Message/voice/photo/document handlers 7. `process_text_message` -8. Funções de criação/edição de arquivos -9. Registro de handlers e `main()` +8. File creation/editing functions +9. Handler registration and `main()` -Novos handlers devem ser adicionados próximos a funcionalidades similares já existentes. +New handlers should be added close to similar existing functionalities. ### 2.3 Imports -- Agrupe imports padrão, de terceiros e locais. -- Não adicione dependências sem verificar se já existem no `requirements.txt`. -- Se precisar de uma nova dependência, atualize `requirements.txt` e comunique. +- Group standard, third-party and local imports. +- Do not add dependencies without checking if they already exist in `requirements.txt`. +- If you need a new dependency, update `requirements.txt` and communicate. --- -## 3. Convenções de Comandos e Handlers +## 3. Command and Handler Conventions -### 3.1 Nomenclatura -- Funções de comando: `_command` (ex: `help_command`, `manualdeuso_command`). -- Handler registration: `CommandHandler("nome", nome_command)`. -- Nomes de comando em minúsculas, sem espaços, em português quando for comando voltado ao usuário final. +### 3.1 Naming +- Command functions: `_command` (ex: `help_command`, `manual_command`). +- Handler registration: `CommandHandler("name", name_command)`. +- Command names in lowercase, without spaces, in Portuguese when it's a command for the end user. -### 3.2 Registro de handlers -- Sempre adicione o `CommandHandler` na seção apropriada de `setup_handlers()`. -- Mantenha a ordem existente: primeiro comandos, depois callbacks, depois mensagens de mídia, depois `MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler)`. -- Nunca registre handlers duplicados para o mesmo comando. +### 3.2 Handler registration +- Always add the `CommandHandler` in the appropriate section of `setup_handlers()`. +- Maintain the existing order: first commands, then callbacks, then media messages, then `MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler)`. +- Never register duplicate handlers for the same command. -### 3.3 Comportamento de comandos -- Todos os handlers de comando devem chamar `await auto_detect_chat_id(update)` no início, exceto onde já houver tratamento explícito. -- Respostas devem usar `await update.message.reply_text(...)` ou `await context.bot.send_message(...)` quando necessário. -- Use `ParseMode.HTML` apenas quando realmente precisar de formatação. +### 3.3 Command behavior +- All command handlers must call `await auto_detect_chat_id(update)` at the beginning, except where there is already explicit handling. +- Responses should use `await update.message.reply_text(...)` or `await context.bot.send_message(...)` when necessary. +- Use `ParseMode.HTML` only when formatting is really needed. --- -## 4. Gerenciamento de Perfis +## 4. Profile Management -### 4.1 Arquivos -- `user_profile.json` — perfil do usuário. -- `agent_profile.json` — perfil do agente. -- Nunca crie novos arquivos de perfil sem necessidade. +### 4.1 Files +- `user_profile.json` — user profile. +- `agent_profile.json` — agent profile. +- Never create new profile files without need. -### 4.2 Atualização de perfis -- Use as funções existentes: `load_user_profile()`, `save_user_profile()`, `load_agent_profile()`, `save_agent_profile()`. -- Atualizações parciais devem mesclar com o conteúdo existente, não sobrescrever campos inteiros sem leitura prévia. -- Triggers de atualização natural devem usar `handle_profile_update_if_triggered()` sempre que possível. +### 4.2 Profile updates +- Use the existing functions: `load_user_profile()`, `save_user_profile()`, `load_agent_profile()`, `save_agent_profile()`. +- Partial updates must merge with existing content, not overwrite entire fields without prior reading. +- Natural update triggers must use `handle_profile_update_if_triggered()` whenever possible. ### 4.3 Onboarding -- Mantenha o fluxo de onboarding existente. -- Novas perguntas de onboarding devem ser adicionadas ao dicionário `ONBOARDING_QUESTIONS` ou à lógica existente, não criando fluxos paralelos. +- Maintain the existing onboarding flow. +- New onboarding questions must be added to the `ONBOARDING_QUESTIONS` dictionary or to the existing logic, not creating parallel flows. --- -## 5. Contexto de Conversa +## 5. Conversation Context -### 5.1 Diretório -- Contextos são salvos em `contexts/YYYY-MM-DD.json`. -- Use `load_context()` e `add_to_context()` para manipular contexto. -- Nunca acesse diretamente o arquivo de contexto sem usar as funções auxiliares. +### 5.1 Directory +- Contexts are saved in `contexts/YYYY-MM-DD.json`. +- Use `load_context()` and `add_to_context()` to manipulate context. +- Never access the context file directly without using the helper functions. -### 5.2 Limites -- Respeite os limites de tokens configurados no `.env`. -- Funções de limpeza de contexto devem usar `remove_old_messages()` ou equivalente, não reescrever o arquivo manualmente. +### 5.2 Limits +- Respect the token limits configured in `.env`. +- Context cleanup functions must use `remove_old_messages()` or equivalent, not manually rewrite the file. --- -## 6. Workspace e Arquivos +## 6. Workspace and Files -### 6.1 Diretórios -- `workspace/` — armazena uploads, imagens e arquivos criados/editados. -- `workspace/projects/` — projetos criados pelo usuário. -- Nunca crie pastas adicionais dentro de `workspace/` sem necessidade explícita. +### 6.1 Directories +- `workspace/` — stores uploads, images and created/edited files. +- `workspace/projects/` — projects created by the user. +- Never create additional folders inside `workspace/` without explicit need. -### 6.2 Funções obrigatórias -Use sempre as funções auxiliares existentes: -- `ensure_workspace_dirs()` — cria diretórios necessários. -- `save_workspace_file(filename, content)` — salva arquivo no workspace. -- `build_workspace_tree()` — gera árvore visual do workspace. -- `_find_workspace_file(target)` — localiza arquivo por nome parcial. -- `_looks_like_create_request()` / `_handle_create_request()` — cria arquivos via IA. -- `_looks_like_edit_request()` / `_handle_edit_request()` — edita arquivos via IA. +### 6.2 Mandatory functions +Always use the existing helper functions: +- `ensure_workspace_dirs()` — creates necessary directories. +- `save_workspace_file(filename, content)` — saves file in workspace. +- `build_workspace_tree()` — generates visual workspace tree. +- `_find_workspace_file(target)` — locates file by partial name. +- `_looks_like_create_request()` / `_handle_create_request()` — creates files via AI. +- `_looks_like_edit_request()` / `_handle_edit_request()` — edits files via AI. -### 6.3 Metadados -- Atualize `uploads_metadata.json` quando necessário. -- Metadados devem conter: `original_name`, `filepath`, `chat_id`, `summary`, `full_text`, `type`. +### 6.3 Metadata +- Update `uploads_metadata.json` when necessary. +- Metadata must contain: `original_name`, `filepath`, `chat_id`, `summary`, `full_text`, `type`. --- -## 7. Integração com IA +## 7. AI Integration -### 7.1 Providers suportados +### 7.1 Supported providers - `openrouter` - `stepfun` - `qwencloud` - `llamacpp` (local) -### 7.2 Uso -- Sempre use `AIProvider.chat(model, messages, ...)` para chamadas de IA. -- Não chame APIs diretamente; use a camada de abstração existente. -- Modelos padrão vêm do `.env`: `DEFAULT_MODEL`, `VISION_MODEL`, etc. -- Novos modelos devem ser adicionados às listas em `psagentai_agent.py` quando houver suporte explícito. +### 7.2 Usage +- Always use `AIProvider.chat(model, messages, ...)` for AI calls. +- Do not call APIs directly; use the existing abstraction layer. +- Default models come from `.env`: `DEFAULT_MODEL`, `VISION_MODEL`, etc. +- New models must be added to the lists in `psagentai_agent.py` when there is explicit support. ### 7.3 Prompts -- System prompt é gerado por `build_system_prompt()`. -- Não altere o system prompt sem necessidade; se alterar, preserve as seções de data/hora, perfil do usuário e perfil do agente. -- Para tarefas específicas (edição de arquivo, criação, análise), crie prompts separados e claros. +- System prompt is generated by `build_system_prompt()`. +- Do not change the system prompt without need; if changing, preserve the date/time, user profile and agent profile sections. +- For specific tasks (file editing, creation, analysis), create separate and clear prompts. --- -## 8. Pesquisa na Internet +## 8. Web Search -### 8.1 Funções -- `_web_search()` — executa busca no DuckDuckGo HTML. -- `_looks_like_search_request()` — detecta intenção de busca. -- Triggers estão em `_WEB_SEARCH_TRIGGERS`. +### 8.1 Functions +- `_web_search()` — executes search on DuckDuckGo HTML. +- `_looks_like_search_request()` — detects search intent. +- Triggers are in `_WEB_SEARCH_TRIGGERS`. -### 8.2 Regras -- Não ative pesquisa web automaticamente para todo tipo de pergunta. -- Respeite a diretriz `IMPORTANTE SOBRE INFORMAÇÕES ATUALIZADAS` no system prompt. -- Limite de resultados vem do `.env` (`WEB_SEARCH_MAX_RESULTS`). +### 8.2 Rules +- Do not activate web search automatically for every type of question. +- Respect the `IMPORTANTE SOBRE INFORMAÇÕES ATUALIZADAS` guideline in the system prompt. +- Results limit comes from `.env` (`WEB_SEARCH_MAX_RESULTS`). --- -## 9. Análise de Imagens e Visão +## 9. Image Analysis and Vision -- Funções: `encode_image_to_base64()`, `analyze_image()`, `save_image_to_workspace()`. -- Comandos: `/veriavision`, `/configiavision`. -- Configuração via `.env`: `VISION_MODEL`, `VISION_PROVIDER`. -- Imagens enviadas como documento preservam o nome original. +- Functions: `encode_image_to_base64()`, `analyze_image()`, `save_image_to_workspace()`. +- Commands: `/viewvision`, `/configvision`. +- Configuration via `.env`: `VISION_MODEL`, `VISION_PROVIDER`. +- Images sent as document preserve the original name. --- -## 10. Agendamento e Rotina +## 10. Scheduling and Routine -### 10.1 Arquivos -- `config.json` — armazena agendamentos. -- Não adicione campos obsoletos (ex: `timezone` foi removido; use horário do servidor). +### 10.1 Files +- `config.json` — stores schedules. +- Do not add obsolete fields (ex: `timezone` was removed; use server time). -### 10.2 Regras -- O Agent SEMPRE usa o horário do servidor (`datetime.now().astimezone()`). -- `/fusohorario` é apenas informativo, não permite alteração. -- `get_timezone()` sempre retorna o timezone do servidor; `set_timezone()` retorna `False`. +### 10.2 Rules +- The Agent ALWAYS uses server time (`datetime.now().astimezone()`). +- `/timezone` is only informative, does not allow changes. +- `get_timezone()` always returns the server timezone; `set_timezone()` returns `False`. --- -## 11. Segurança +## 11. Security -- Nunca commite `.env`, chaves de API, tokens ou dados sensíveis. -- Use variáveis de ambiente para segredos. -- Preserve emails e telefones em perfis e contextos sem expô-los em logs. -- Valide caminhos de arquivo para evitar path traversal. +- Never commit `.env`, API keys, tokens or sensitive data. +- Use environment variables for secrets. +- Preserve emails and phones in profiles and contexts without exposing them in logs. +- Validate file paths to avoid path traversal. --- -## 12. Testes e Validação +## 12. Tests and Validation -### 12.1 Obrigatório -- Após alterações em `psagentai_agent.py`: `python3 -m py_compile psagentai_agent.py`. -- Teste cenários básicos com scripts Python inline quando possível. -- Não quebre funcionalidades existentes (ex: criação/edição de arquivos, perfis, comandos). +### 12.1 Mandatory +- After changes in `psagentai_agent.py`: `python3 -m py_compile psagentai_agent.py`. +- Test basic scenarios with inline Python scripts when possible. +- Do not break existing functionalities (ex: file creation/editing, profiles, commands). -### 12.2 Testes manuais recomendados -- `/start`, `/ajuda`, `/manualdeuso`, `/about` -- Criação de arquivo: "crie um arquivo teste.txt com conteúdo X" -- Edição de arquivo: "edite o arquivo teste.txt e adicione Y" -- Envio de foto e análise -- Envio de voz e transcrição -- Pesquisa web: "pesquise sobre X" -- Configuração de IA: `/veria`, `/configia` +### 12.2 Recommended manual tests +- `/start`, `/help`, `/manual`, `/about` +- File creation: "create a test.txt file with content X" +- File editing: "edit the file test.txt and add Y" +- Photo sending and analysis +- Voice sending and transcription +- Web search: "search about X" +- AI configuration: `/viewai`, `/configai` --- -## 13. Documentação +## 13. Documentation -### 13.1 Arquivos obrigatórios -- `README.md` — documentação principal, comandos, funcionalidades. -- `DEPLOY.md` — guia de deploy em servidor. -- `RULES.md` — este arquivo, regras de desenvolvimento. -- `LICENSE` — licença proprietária. +### 13.1 Mandatory files +- `README.md` — main documentation, commands, features. +- `DEPLOY.md` — server deployment guide. +- `RULES.md` — this file, development rules. +- `LICENSE` — proprietary license. -### 13.2 Atualização -- Sempre atualize `README.md` quando adicionar comandos ou funcionalidades. -- Atualize `DEPLOY.md` se houver mudanças em variáveis de ambiente ou estrutura. -- Este arquivo (`RULES.md`) deve ser atualizado quando novas regras forem definidas. +### 13.2 Update +- Always update `README.md` when adding commands or features. +- Update `DEPLOY.md` if there are changes in environment variables or structure. +- This file (`RULES.md`) must be updated when new rules are defined. --- -## 14. Git e Workflow +## 14. Git and Workflow ### 14.1 Commits -- Mensagens curtas e descritivas em português ou inglês técnico. -- Commits devem refletir uma mudança lógica por vez. -- Não faça commits automáticos sem revisão. +- Short and descriptive messages in Portuguese or technical English. +- Commits must reflect one logical change at a time. +- Do not make automatic commits without review. ### 14.2 Branches -- Trabalhe na branch principal ou crie branches para features específicas. -- Não faça force push sem autorização. +- Work on the main branch or create branches for specific features. +- Do not force push without authorization. --- -## 15. Idioma e Comunicação +## 15. Language and Communication -- Interface do bot: português do Brasil. -- Respostas do Agent: português do Brasil. -- Logs e código: inglês técnico para nomes de variáveis/funções; português para mensagens ao usuário. -- Documentação: português do Brasil. +- Bot interface: Brazilian Portuguese. +- Agent replies: Brazilian Portuguese. +- Logs and code: technical English for variable/function names; Portuguese for user-facing messages. +- Documentation: Brazilian Portuguese. --- -## 16. Regras Específicas do PsAgentAI +## 16. PsAgentAI Specific Rules -### 16.1 Nunca faça -- Remover funcionalidades existentes sem solicitação explícita. -- Alterar o `.env` sem aviso. -- Modificar estrutura de pastas (`workspace/`, `contexts/`) sem necessidade. -- Introduzir dependências pesadas sem justificativa. -- Usar `input()` ou interações bloqueantes no código do bot. +### 16.1 Never do +- Remove existing functionalities without explicit request. +- Change `.env` without notice. +- Modify folder structure (`workspace/`, `contexts/`) without need. +- Introduce heavy dependencies without justification. +- Use `input()` or blocking interactions in the bot code. -### 16.2 Sempre faça -- Preserve compatibilidade com versões anteriores de perfis e contextos. -- Trate erros de forma graceful (try/except com logs). -- Use `logger` para erros graves; use `print()` apenas para debug existente. -- Valide entradas do usuário antes de processar. -- Forneça feedback ao usuário em caso de erro ("Erro ao processar. Tente novamente."). +### 16.2 Always do +- Preserve compatibility with previous versions of profiles and contexts. +- Handle errors gracefully (try/except with logs). +- Use `logger` for serious errors; use `print()` only for existing debug. +- Validate user inputs before processing. +- Provide feedback to the user in case of error ("Error processing. Please try again."). -### 16.3 Integração de novas funcionalidades -1. Entenda o fluxo existente antes de adicionar código. -2. Crie funções pequenas e específicas. -3. Integre no ponto correto do `process_text_message` ou handlers. -4. Registre handlers na ordem correta. -5. Atualize a documentação. -6. Valide com `py_compile` e testes manuais. +### 16.3 Integration of new functionalities +1. Understand the existing flow before adding code. +2. Create small and specific functions. +3. Integrate at the correct point of `process_text_message` or handlers. +4. Register handlers in the correct order. +5. Update documentation. +6. Validate with `py_compile` and manual tests. --- -## 17. Versionamento +## 17. Versioning -- Versão atual: `1.0.0` -- Incremente a versão em `__version__` quando houver mudanças relevantes. -- Atualize o `CHANGELOG` se existir; senão, documente no commit. +- Current version: `1.0.0` +- Increment version in `__version__` when there are relevant changes. +- Update `CHANGELOG` if it exists; otherwise, document in the commit. --- -## 18. Contato e Suporte +## 18. Contact and Support -- Sistema desenvolvido por: Syspanel.com.br -- Autor: Marco A. S. Costa +- System developed by: Syspanel.com.br +- Author: Marco A. S. Costa - Email: contato@syspanel.com.br -- Licença: Proprietária para syspanel.com.br +- License: Proprietary for syspanel.com.br --- -*Última atualização: Outubro 2026* +*Last update: October 2026* diff --git a/__pycache__/psagentai_agent.cpython-311.pyc b/__pycache__/psagentai_agent.cpython-311.pyc index 5459328a0ca1493cc959229dbeb41e492f8b6b4b..e6e65696e78e2cf68aeae7f99dee0ca9e2ad2644 100644 GIT binary patch delta 72637 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

Q04SyAhpX2Ib*!YKGnk~;lX_80bJ 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 Rk48^}rOF)rH%z1z>H)<$LVwTx0lP?}~6Ng>_ z7QiA)$!NbUo)}YS?WEyJt_6B=k~;`8N4}Pf2kGmdr+_f$e2IWlVfB$C41b!3ERWU@ zzMtAh`q|DGTh6PA5ig)HuZ$izulDKtAH-eQi(E^Gt$|Ks?#AU6)ivZTG; z-n?clc8b&<_QDSMV=<4Il^^QrE+iV=N?w6+lpA~%^McZj7m8w_H{yAD4w}RSC`41c zu@4@#yTJ1HIe;?S(Qn{ndRJAU+`Jkt!f&Jpxkh|OoV`s3ag#{C!sH!WQNqCL#h#TM zkTKK(Tmn_V0D6$#9smwxBjVuKdnJD@TaGGBx2+ZOn!g0ZdvNgs_8Is#Bm_O=czTG1 zBm)m?^4v~PQg@a3_RrKJ>-W$}@vvteI4D5_0PA1_Hx)B)wB$|ozx7l*#sWI-I&>Vk zYrs6>tE1E3QKOW?FQZSPr6QX;pWYAGmJCU5e1--BV{N}Cnt!3DDw~Dl7iuZ2E$;Y* zdee^e{tTg%mNg922xOO+9#jq>sf4Q#92dAp2klK^5iL+)aQYl_+fF(Vz_5yI-i7J% z8~DC+7Fkg5t>}VFaT;j$6(p8hwU=0yK$;wJ<thH1vN!K|IYT76Am!96q9| zeVy(;Nx@9IY@r~qwe@M2!EjJIsknjeuw_J|lz=`>3B1AmI>nMuXrGllvhCr9FiI)) zC9(aN>b%6CQ9QIM??r0FD|n?)xPPgxkE|F;cH0(?7Q6@GJ!Ox0Fawg$yzA6RapZmV z_tupjeU(>V<?wEah3HG-{&6+elo^RL z<7(a9;J!aPq=8=YI(L7gil?!MNNA!x1VY+F{O7_y;U9Do5ly34 z{9b)qab>rW6FwZB3yilF)XO{$4La=Y%xb`l$C+L7@dSg>Ajzn-gR6KMRrcwiGy~Fv zoh{ZGpXxNV2(USdwoRAj6 zPXx6QTqi>0i-iJM$(;e_Tx`>{`jw@q>G%V;e-Td;@+4eUgN6{j;tw%ErXLopyxbXE zF?jG$1!uFz_Ar25~^3eyBK1pJh z5K&56O5rjJSbX+@E`FtJGqahzOi+_{(OBTt9;4We7IEllHC0>|p`}NBhzD*JJ0r9$ zIqN*}>%H;o-O*G0#E&Dix#IaJv>cHYsXZd@`lDJPei5m)EOvzIU)&{irw0+ppC`WF z8(;5^p5iC^qqHT3ou2qEZ+w?K`l7$lvr*dSs4}hr$vB;|JIui@TO$l1QYvzUh>|d-yy1Lw$LwT_f zuN<*fr;SM`j)n8tEHZ>+TvTUD4dJjt4nrysa7%RCNmz^FiGGFKZu20!GoywzEJTaV zH+dYCTb%$R+h?q#ynzXs6W{T=$t=d*N+~Tp7 zxMy$i*tU$`o~+d>F&jp9d5Tt_Y4hBj|D9a%=M?Sk$YxLcCU5-RZ@n;0ycLO|`Q21) zI49%eN{_YV)IyK7;&{ziR05#UB4(ec<3HkfnpRspdn`KEoiN`MJ^yCyX6;S7nB|F{ zg{U6oj$P!@7vV)~qDcCwHhVOEn%1YxW=)4xwytE2;xmc+9Uz$Okrxx)E zce++){X3qZvrq$G=B!3xl-r=VR;4pPb+5g7fc22jM}k!`z*F`?uSG}(=wtla!&qJE zq)H+j43enQ7@}=zABk@r`;7L35JEDJwt-|;k$}KJ5Cjn-+|445nN2vRY-V){qg&!2 z7L6ros07G&0K&q%9CKC`WZ+=lH5qR@t;?_;tav;)1ooWtapmI9d0K6XAE7A(6;`~3 z8{+vnT7-*bS72mY$Z~^s*T6B9ahiB2TR`DOxx+DeHyCRN&Onw)B5p!OqQV{h?FR7~ zR-F+$=dN%})p0my`XiWHxfxqZnToX=R{KXn7F7 z(I>)R3F{F!T?JE*wcU>@KL~YzR9=&w5xzc4Lpm+2JYl^CgD^Z?5$*%io@#FP!V18n zJ;RSDhPeyZ3Tqqc+BR2{y904$g|BTkO?9Gh4HN8X}eA%>r$n%iNIj-Fo196B8JaC9S1dBna_`>8AdSD=TX*otvh5@k!+ge!JIY3t<&tqN0*we-G*~`(bPzff5@~?&vWNPG{0Yp3o^A|ke~a&j z*si}^nj|sr5A7{dXuR}x@f+q;;r{VwjeQSRKs6a^X z26joZWwH1l0*2iHlO}%m5PWL(hrL6k!yx2JOTME*Hl6g!DaR+sTCkV7`TXh{@xdAq)Y=u8HQyF26B5Nk}kdjckumC5#Tq}R#zIj?@?ozXHT59TR*R(Za`f|r_-q?nz z9ApYyelaSoDlnHL;5hCkX#r0K2{a-^cLO95k=v zOnuAb7yh0?pi<=Hl<}lj!ZM%Y70gHG^5`&-WaKV=LP&lHlWGIR)Y2Iz-m4`x64Qs# z_E~sf-ZQJgW|tw)ui*rG!=ESz<}$PGo2oZpn$c<%T&!Fe2XZLmgPS4b#nv%k5y34_ zs34)71KJ8*%~k;*j_re;`(<%33h=T&6ZW@FY%S*@-!XYNv%lCWxxI*9fB_ul7C1kQ z@pgNO|2cL|P28~9Sf-`Bq?y|a{zLq6$a^RuZ;eTLOuKqa^L7Me7RLo8(jn1@Wm$u4 zKj$+Co1bvTIrxBl0V_-aA%4Q+feHNys@BKu=I#Ap7g^0mtz9R2=V&ongGtSZcZ2=gsyQ!)&fdoWTfqM2!<6WFnYLMx+qYx@mQ zY(Jfnjqf6_uATfPnxZs;!oaBI=V^u!NH8)9vY6;Vj0R?p=$?F%CPWA;>ep3+3GO-+Z_y~%dQ+WC^M^&neV}Zbtt4{AU)>nm&+0j9zWB@ z-H>$GgZ74|=8cxJFwtzwS#2*f=#d- zWEWAXQ}O*wio-=yI-{fhG<~M?e^@L|7~;Ow|YWu$UxrTwSfk zXNckj+K*iAzl^e2ssqk9{6nVFY6U6rUFjUd#MxA-iH1dj_YSl(hnD)W0owFy#)_yaWUb&E(zxxsaJo z6NqG!fjxJ%%z%qL=_R??amN0fLJA%zw|IXM1g`&)oxE&;uwwiRyLeEDLGlX7@A>5; zR;y$ph?$43^*^zuR$O8|1=AneP|(@R#WZY9yvSNgA~{gFJw08eR(yk&-hX>xbxpal zRs>G5!e z)>Zva&%9gZ9RF9CS#&I&AM*38XqXEbSteJ+xX@GmmqY$+vek5nE7}BOi3<333v3cV zY(eY(J&8=An6X?-Vq8tbv@5|XMtft-wEv9&hFd#?V<``QvC|Wj>N@gp?nklx8V(Ul)vz=mJqqI$wC%kb4P8ZQ0ePHld zFJ;EIqqTTqC zqv4i@6Z^!0yABM`J_V0N^S#sNdu+h7Ti~`;jtq>)@zc>1##_?!#J4|Grdc<*n_E27 zTD{X+#he;#)(&gJ9htXeo>+Ra#S>HDjVTyU4}2rTl8|~w&n-PC_aPbt8r@xx<&49S-@xU>Lgs9@?CkwPERU7t^=TQSL`q4qBA5!AHOr zAk5(7M)A;Et=wwEhhL*v6({}jMAm5uuH|n=*={;^-LVtXPG))*PQB6AQKnodHiC;Hp z`PO)}%Fn4)9+GBlRqM1}us*PMd97V;{h~iHepJmC=htZ`UG>P9&cV~ZhM6-u;lc-7 zhc&w2|B5Ls~sztl3MMa~Q zS=i^X_Is`UZvCRayAyw#J$gf<_65!5{83FmNGT-TL#8>>6ij45G6_Lx=xu3r0LyGr zOG6}c$k8dC`AHU(t*D`#k*{FMHQ0x=11`M>;YWg4KG&e*AYAm(rYX=^VazZ@2#~6> zxelZe0W~lq53`sEj!p3l_ytzzm9ym(BXLafg-eTc@Voddi$SFgJrQB$!~FNS{9 z7vt!oF90{snGRb-{@Uax=N9vsC^nTN={S8w@||RPZ>psr?IBU_!S*g-G9*CK4)=uI zCOuuQ{(&JI2GaNgUw+!&%6xdSb7`^P6of57E^I&f1)M@|Ks{(XO>BQxrFAG0S=sF* zqF|#%XDoG#?2KaPN?F#R6EQf_c0rcYhb2e$TqnfVl%dqE)8q>FUb%CcG>btkL&iX* zz4c+m7wtIOxrZb`&UGyqPEfNp;J_CY8Gd6x;GD2Bjt+#dc#s+j1*_=kEDwlN0$?bn zmrTFC(>XGwkTY21v}kLmOS=?IZX8Vt8o1G8WI+ulHG}=)+7_)aQo1U<``IVcvr|F} z9gr>LVE&9Jh&Nia^-80d)vEQB9CYv9=Po)Ze;#X}*V^aSr}&9)wQ8%9OHP550WdWf z5Ddv&vAj)NYui6u=T6%%f8w?_tvk8o@uA;+ zvQ+2sw4>9mtVl6C2L59GFHbSz&2H=Ith5(9TZ&2FhTSuSS@c!9>1WK$%@+|yI^Sa0 zYpZMOd3fP5NXQ`Z3z>3(Y?15U;xZ_|pr(Lm4Ro`!2OMv}yc?(Yc&zW?feP0{Jq{O~ zn$Y|wZ?5)Yo{3N{8AA>#-O80K?IAWNwRR*}of+~*s3C)S2Xl&EA#G2?mfAI>zTZ;2 zxrLRLE&TG(SJB*Tp{UmFeJ^YvSbF6j%Fv1UFxR%=*X@wVIlfHHLL)mzQrt`WMsBm4~ zEcb@71>n5Mz>SU6P#L82$uvHAR$6Jjs*pPe-i@huD!Nf*7-tlQ^(b!cD5SoHyiw|E zi65U=i03XyosU$|Y1c0JLvE1LQ?`;tQ`VD0Dr9^WJLh6l8C;!8d$1rEEyJ#l8FZe* zSs9#Byv9#4m0W0MZYI`|NM}scFd<%r-@W%U^zUi^5fGwWsxN|@!xtCU0M4krd-qbu z8gq&2ox!8*mfU!OijxnaJOoM5j$+DTU}4R(*9Jd{Ed?D#Da|zsi0KJ#7FN?`&@TNY zka&()g}6q`?Yf8dMQqdlJf#x;gWOs0sSOZaR!yRWq|wZUo*+$6); zMUS#CN;23SNMQ@XyGI_5j5VfFeiUSne!WBcwc=VZwxMlYi;n>c41zZ=Zy%5FUtqIR zIDb4QaA~znUpO8cxU^ZymX5~-F0(9U3&yhpmy{ch0!GA+&xAWL_skA=bSHjeN$GF{ z6FnP0cydXM6!{KqX~H4^Q^DXqVIlpZ7m99&c7w7={EtJ+t|L5|2pI$%mDY|NKHYVC z;B?oMSG&_{@$*El^|Fz5ct*$zKe8KNBsO$wo0QDa;co5E@k$-}RqPOd9nun%O~Q6q zt5*g@+hJ{Iq&)FRw>u-tsIle<`w?v!AQ&z`qFt7j%=`e8e1?<&B{w!*`1DPp<*0U9 zf{iVzprpcnwbzGGUOQZ9@2 zfGBfnYipbtVKOSe&J$-#p&wu$84U8gjC6UK8#GPN(tM@L)?Mzb_4yvCUK30x526CmHu=}@4OuGFtql?~6r>rP&p#KKGH~P9uOI4IA@w?+% zV`LR3UQCtPbiHOzTM5s=MJwgcleo&8xXPWlN?d)dR;Vl!4_vEV7735iYRoeE^pcTz z?xH2~=Sf`ZOOje<+70(Zu>I)Jhn=&4FLL;ef1boe-o!=jM94sH(CV#=J+>uY z+Y+~JiMaI!ZML#TjNYJCi))X=(^vKlT1@Whk@nNt>EzSeliltatL4v=SmRBsaVOS{ zuDntErjoJFJ$v1mH3;zYq^|d-u6OfqG~s6L&ni4jl7S+&6LK^JH-cmh0L44_JK_Mz zhM&d1AuD%)Gc2>(@y}#$i1`SyYaqOva_52ITFv>Ae5Kzgx7U%eq1>8rdI$6g0Gct& z!pu?F*O@=Xj;Q@W@N}Wv=>`e|vhZv%PB58@OnWVu#O;#nVC)@}Qcad-6(Jq~ zx7UO1DjAcJ@}I31sV=ZtWXuXvgn<&s@^Ts5d&^-J$ZI|1_P{cdal>)M08bRSHeJq^ z+gTqjc^%1;yX1^D4-Z&dYq!A!P4YJl!8S8_ZOOg)+b)!~q1v0%ogX4DlC|{UdIv(d z9V3tCWFb+UXEEDLBW#jfNghJjZ`0DUf>efAdM1i;xo51en&p%Q1xga8^(oBs;}%Sr zxUA!~h5%l9(#K;0m%L^t3(cW50zp!n7fIBkQx$txDXC)B?b@en zotH5fCprYGAF#wBMne9D2U`weLoc#VAvpsOc~ZoRJG8wxz1+H|aI-@Zd@oXwj~ z`lK%!3h55AT^#@|#KznN+*?GIqaaHxJ)wOu@@i_TtHj$UwCNE|NVW^>otoF>?A_=$ zMIn<&d03ahhY5dWu$>Pr@fL%&KPG|YjaYCK5ZGJO?aBBb$FN)U1Ar7@v&~RJN2xG( zB4{@rq3Br(iisCxME`K;Humz0tg#zu?+F#M;iWM5Y4NfGfsJ=FsySUebC)(dCOI8G zS_xoYjNhfrR66d?f2&+HKdxHE#=EsoM)HY@tHfJ(Yem-0cxw?22$-0`f%WLpd$fLK z?r*7ZEIwly0UsA*2K9GzLPc(h4J5k=L@(zkf=5HFjb4j^@o9Yie6s zcELb&zoTmiCupRrmadh~{NPO`+03mbCT7%gN%r2zt{?cEN~-q z9>%HevO43fI|z7k<61jCvCElm-BE+iz@(vlP#+9=pp61;blQj>@cAG^M1Dal+Bqnp zcbqZOwg&8haW8W9X-D?=`!v{%`y$v#(5I8hAtfI(4pS9%)EP<*0K;^cW{I?G80rNR zXO%$Sr^J0<(6U|ZKdOh8_!sfsP%TEOUw%WFK<$T_IQb#Cu!V$w`-9So1C#%V1bCof z6cF81*#T^KPw8BPdxd*oBU;**Zo0magQcf^5AixKuvQW(m>_}W_#+@Hg9qUqH1cR9 z8uwGcqw%{G<00v$7>~;p^ac;kZi?klbv{eY@FoR=6hza*d;oqqrxE9{04L}l`yv#B zZIr~k6z^zw=-fk35d4k=GXFAV9F!pk;i>pEW303?-ZO}kSjLy=^FKm>txr%k)r4>O zqRcVnOE5R%dq`0&Rz0NI)DIQW_>h*bd_(j;q!r{JqcYj(tML?kh04$Lk0M<>JqeAJz(&&mCED zZb8kf3u?}6bhmBwEZF8JP z>c#A@Xho@&&$gV&e0ujYyFK>xXZt+%ou0&o zyRop}6Mx7Xf5;tu(cftIqnc1$MZp7AYaAUz`zo!gv3nluKLFl<2_|ek>;`!NGd82m zooUt@5>z#;A<`99nn+HtHkr)3nUG>_HkB4w1P=8&AnC?q zN&d%Z{{?GUI0j3!u5on35Ep_V?4Pkc@ryl~v*)v9-=jk1QDCPacm|!7fOH;DuCRd- z>c@uW6QA@olFP{=sk87J z!8`dC?))Vq*nav`pc}H!bLTG|Swl|oiSk<1N`G7~gRXa%gPTb|JD*8QmHS(+VEuPh#ZLYr1t>#?r&TFI5g#eSo2 zJ)td8U7!<|x37O1Mz{7^)dcqyJedb{3aR}K3@z&xY59n*j-i19<3~i{a}g9DTp-6A zr6DBZQ2o}JifsQFV`*O{jUBcjsIOWBmH3v;wPvuKzK0eVbH%KK{H`B>wsoG~D^`q~?>L zD4(Q8`QR;6^TwkBm(iBAJdzukcdeE*JIRgYB{b91^2QVVmvGpfVjoZPU&8N4>h$px z|K*3tmXd`bm#LN+OWf&eOHLd+aqRTE@wC8m8J4_RpV@ui?(s~25|>Q8JgTi;L$zcf zTB-i9YE+?-d{%IWeVZ{J9R`x@=yG3F%b?@X+MzyN7tnRgrX!BNn%@4QF2FS8jBYxu zU8^Wg@sp>u;^|Jbj5C(KSV^9j*WWho~Co!|}$Y}02wPv+s1yPP$s9#=1%xC3o z#~#|-fXqBXHwntp_?YxnxatqUiNQH7 zEv87$V%)o91yDEUiC=zOt6N;v=pf8D{|uLU610SWu(J4_yPzD1CUh*1wr!&4J6hRX z;+>2v>h5HDlCYtA4|vrr4Qrs42x*Lf3GIvD(F)U`gD_2DjZY(kSwO@b_$sl?vqkX> zI60j^dhiA9te)yimW2X4<}b`1ac8VDO*Eg?7OZ5^D;hPpbLpjpF;L4rgqPFN-g#gO zuV}U6%vo)gjhl=+(;0Dh`ztBpZ*SlPWYbGpQu1w7wL1`Cw6gpxPaJv)qXw<#`Wg6% z_2SW&v}#w1pA>>pQQPq0`4ob7_qUX}ZW?z|hW82X@LsD6??|zEzs$!`?s_ea{0GE=<#sx~uvCZSN;OpJ+29nG9bgp8SbckV0{6UWwi_g$G#|S3a(0 zJmu9Q71taj1J?A~0C_bH_xzOR0$xHkuIZX=6=fQ1!xeNHPGXIl@%b={3Nl0lRemQv zKx9Vn+!u(d&K2K#O`BO*V3O7XBCQ4QBn-e zs4>b9#r;16{rnuh<;)7Bm!Sh$H!%}o97$L zt;_x78+$9Jm#M~s^v)zA8KjR-(%r`>xKTXyrdBlT3l!%*nKW3rRRXiz_+kW+y_~f0 zUNK3t@eFc#c68}mT9RU`CyG}ue?;+W#L2g{)j7L7$-BMDyWO@ae&P>rYu76?#8p4n zW~Q+3(i!heE5xvllNaP#Vy>wAu~s@7_utx=)q~tH596aIf#d1LCH^MWhnpf^R%o-^WL7;+FTZE}TgkP5YJhw5rS! zcYdI4RieeUztOg9F@rcijBCw0I9J|?6?c4q8@9oPbiExa4Za^gxKu_G1}o)*iDJ_P z9!wfsC2u4TUM3?cgR5mEb#RSn|1I)ZE3c;w*2_rx;5v>d8KU6#$Rl%bgS?S7*eoO2 zgPUa}XRuYw`w)3-k=J>H+hinv@N$kQb}{h>Xs}Bte?lJJ z@_Odreib+g}lJ*FUsHO84m5e`xQj-E$AYeFUuKpbTPDZ$}r*v1%J@>o!+6 z?Ww7+Zrw9k<`iXzjmMB=lo!5G<| zumy{AK1syGc%691(|?gP|CW)a|ZTPimVtwav}HcT%%N z<6VvJf;DH75a4%q@I<30wbh&2>YnO19$`t_qR5v6A!8Os(bovPFq6y7B_E(Y>9;FZ zN7O8~ys+3>o2Pwux}`d`H!xPgM3C?M!l;}r@B5YZqm*C6)Jm_}oYHdD-2 z7-@8wP9rK}%SK>e7zCl`i)gOj*t*eJNkvN`imSo2(jaIvpH3=RUsTP;CL0d%uQ5m$ zW&Aoq%#YCv?Z3vw0G_uSP-jM zi6)9&9gpDgSlupm$LqP`=QiCQ;V!Fsty(ON)pJBgoSsh)Ue4bXFX9#b)P~nqh|&Z- zTYT4syA%Y|f-%lZ+)Mc%i`NT6ZgAS3IK5C*#Uq1PV)00OoNiA+#?7AGP2Sv19(}V{ z2X^&l@p!yGe_EY;(?NG`pC`A^i%{?PvW>gQNYKm0j0An(vNq-Z9pjet2$~)jg!3(V z#Wys1;aOX^-n9O@_3p&k9=*(~m$}g%FDB@Gdww!+w@>VK*~O!B``9jK

%9+(-chO}dU9xWMQM&vp1!pOE zi2_!Tl~7hq^c-u+>gjR=U4|=j<+i!m0!TV3p!fmh@UnO%S-)C+PZ3Qix_#Q)L_$H# zEXE%xU_ih>>Eh!l`b<|V-D7PUE8U!Q$%?oKDE1%)rxEyUYiie4w>7rzY2DsjE46e# zr<=c`;ExnAj&3x4o>6u)D8>MU^C`wtiWMW(bjf->C`;fEU>jXKDBu$^Z1u!Cl4~jL z(-iO#3)Z+h>GC=XSkuU4=5D$a6tK?maf-c60n;SbJ~B;W?PHMiyhV?=-lXsi3VuOB z1Fg7m6eLiPLO~h@Kc%~@rrb%F>nR{WZ-c$-?4uZY(lHDQ9;ASI{i77Sfr3v^a61L} zQt$u;ILm>MwU1w+>qjVfjDn{q_&x7C6(F&_nQQ1CVd3Jne>hLZ>;Ygw!4 zE|bn(6k9_+bs4)UdYA&HsZNSbqN$&xv`vg=#vcp!K2NW(NGiDc@M7^O-S{d6yuIU{?ziYNxU$eT z)u6qa$;MFZMXEa+S9T%xQS=IIv08jh3GQLf*B<75z zHihSsH>YihSnpL_wWk)j^?CS>*-}p)9Z7TROYj@BO&iV{ zX>;q#@f)+Hy9=w&!WVoqeq%Pfdv=R^SG!y9z;Db};4WM5ZoAyA@4)YZySeU?Cihm{ z+KJzoEybNT_w)?6ei?q_X|XvgP@jzP97|lv$(w(;{Mq`tnMK1XQ%>8wOE<~r1SiP28J5K>k;AA0{){ww7dDB5 z^YyH5BvhCQ9Gjp^j>(`)IiO(R=BwCW~=TkSGPsp7JjL-?2CAsi?YV-Mo{P4uQ^Qqg; zCuCoc*myx=&iT~s=MzdY!ynB*pW1XjAtyW$nYM{X7wDPOQ!@P2W~{i>V@W;Hb#lS5 zc5>-GeQpb`=_lS@pwEd#rP5GOYJ8EuP|r@=GHy9Yjn(U2yGKUdUHiqxg}U9=J#MK} z@bJ~%s%_$`g?fH1#!J4kn8yppCg?IGwnkBuMW>L0KjzRwjG7Au`u>>s*+PAxZ5X+u z#Ye98E@>8fm*{DW@}{BBQ9k@J8y&F;y5uw&n~zKCV0ws$RnH>b>*^azNDDR%$V77b z`PBNcgw*gv5TsdS3F+a9Xzw*+2_el*ujigm-3?cDAT$#(5k!3`h_@uqoKP)ktJSfD z>7lp8GmG`;%z?3lqVvi5lU_KV+Am&TtY_()#uDt}&Bc1Eo_Rj?N|CZe|KqZhlbBX< zO3DNU!PrEeq5zSZnyl~?=sf;V8x{s*qGqYSWKEB9s>qy=GzA0DY3o1n{JY+{RoX|iaz6dgv9Q>`P@M+Qbp9^EakCpa}UF21^4 ze>@fqkcbID^%whA=$%zpGI1PGlq>zzk>l=m5HA_)IcUZvWJ1`Lh*_!6o>`|1BV!Fk zGFz6SRI@s+nHd+Cuhd_q5=4WOpc0HOS*7dB+$}Pt*SmNl$0q23kk~X#SsE!sE18DS z%oWG0^oUqA{cI&cQqU7sdY0`J+AT*(lH}v5D*dwL7MvXzw@^62;rJCI`ZB%o{{tZ0 BCyW39 delta 77111 zcmbq+3qV{)xj%E*WtZjs3J_ikguoJ5@(v+SLLeazNCJ63$igmU39$4mo1`>#qkW}T zZQ5;n(%RajYMZpSrKL*jRh!E7Hfq(~xV^!xsj>d6)p{*z;pnty*D|` zo-=12-^_gT&G(u)7v4(tA4^GkDj~rTg@4r@zvyn(M&Fl|$p&61{eeFAthL3q&)RCM z_l&0%4(G%g7<+nnrs405Ju^Hb{;!OE&6A(Fl|AhlNL+$&h{ECDC%&o6c=iOd=HIT_ z^(Xw&eF5(O>=}xiHvFe)SsFKDwpialIPcMD>m~fQ@Q|Ky_c`heYx1Zmq(S59i-=|yeygxC0IWo8N=M3v!TfOyG+rCcZ zvUc#eY1U>uOL(^O=S#yIl$B9Af5g8DAb-y z@aVdEY#!auI>_(yt@~{S)_ZN`-Dhc_=pXX*ptczN_VP3Zls>MH-y0F@0K90UwV%fn z@{(`j_eFH?smhOG#dODJEh+(toj@{-tUy#Rc_q>Jh4$uD-l<4l{>E`pdcX?JC z^Vo6EK4YHyZXUk~C4Clu_jE~?0eh6i)kXdKxhELLqMF}-{t3o^zo7o!OYf{F@ONJq zjr9GG64orn-E;VR01*B+Z^eTIt2L%mkJyK9*CI_#lY`T;DekpHH zTVV`a#vy*hv%j!_ebIBQu(0aUrRX&d%VQkAT7Lf}4$tGL1@Q2gQ#|Ih$6J`kp78tu z;g>z}MR~ff@ES0cGc&8c%I_P3IE?--;F$Czk6(k*p5pkp);1USXFP|BO52`iQBi|k zhE2}eE?eI~ZKJW$xXC6v9etJ_Babp7uFlbS2$7Z!JH;DX?9KsOuf=G!O-|cbJ1MQ_ z!=lDy#jO(*_ zW_K9fE9zD*c)93Zn`|3>$`R0u+YZO@e|4N?>`jkiJHR&G-#N#y13+BDpR?WbV@I)PU+16Q zr2$O`#tXn;ywI7p9!tA}I#b*r%3!rQZGDFvJ%=oTxX`^#4rsbiVL*$}B45=4&Q3sR z31~VkXan^;e>S>$2Xz$1Z>TQ1F{t&p@qkL59q1MGw75apG?ra5&?OM7-T{)>4u{p= zX{W(gZ)yAu_TK&;8zGi05Er_qwjgJ>ZNL%GS!5v5Ax9tv_1OCdt`a76j1F7vUCR+k zrFJ<2(U$&xjA?%lhG2kF>zrZ&1T;=N%GUSySo-We2P}bDD-e!jvM2d~C98#cG#W)3 zjmKmwe&wx*8Lada+NXPFI?|+A$#d?&O8qv(o{g3_-EUuNaO3_h%C{XOQIcAg95Wsj z434e6x`hoNwa;KI)icyxSuS@VrEI0ZMnNY9T@)Okz)nFo1qVHWZWG(&(HyKvxC0UK z5^6XCh&(Gg3u4Bls5DFTaR0#u=B^BAZB}Yk4E<2w_J;db+*04hSb7F%kaheQ5qg31 zgBStz4w0H(7fk_#c+hn?WZB-0!Q?kxb}FUY1yX`i=$DFh56BKahw)4G#5!H=&H+2- zQ@{|sMMk~FiizjwquRtBWtMHd4j_w;{{mNHthOE-H3d_liwFb#QbK!QM-NKW@m~}d zE8A2|a|aB;dqOtZ)@d0atVardhupeeTh9SL6T}Tgfe!TA(RRJKvB=j9eGa3FUHo1} zxofeASCNglGdu}=b6ktZy|o~SKyojQTMq?&6gVj8X9`=u)}&2=Se_Mqvst~Tzwad5 zO7|TNG{mK-;=^~N-ihhMG79J;4uomM8V?}oTA_~1y4}$WeUDR!B;7Gl7gowI77i%3ZACm84A8e z!LzIY;d6BRX$n4rz@(8k(fuvxKlyggqq151GM+bhej?9e*5SX&B~1G*L~I={8F)FG z^?9xwp3lDJ$v9HSzU|q0yV( zMmwmZUA8#Q#*H>(A?Te274|uLZM3?(dV@HOCqUycdQv}A=Ps?6acglHdu(zTJhQQt zNGⓈ;XsM>>~8t=I9uG8Y?R=%*iPyr9$Z-Fnr;ndv&Tf&4$$$vqXHd-Dcff=)^4hAs%@E2R;Qi#8otm{HQ_?d zhNjWmno$UKntageINVoZFDnvg5iE z%=Qz&uxrs~v)MHb0KEO^9mZM}OVm6y#{I4=yxMOtY?4RMR)WPM-c040YO)MxS-=2p z)M;zO>Si+)$TWH7pL#0pcoKrgPwyzz1+<4{tcaeE?#PPs(@Q?xzwIfy^Q_^2=$R<( zA3Sf|S)XL_?rigxS#bFh+x>~koad!!o)vd3i`jhsh_`IBC+XD!&*{5LHjjgBtdhL0 zTg6Re_{XTIq&n#X9wI)rPWop;&-h*Elp{$}p64H*&GKA6K7HQOaeb7rY&=#MyL3E? z0z*`O<#=4^F+Qqv-grXjk&<}I?_Qs~%a^#@pSatrpW+(6@9s@3x67A!z@K=)tDoZX z{Qa{nDb1&{y!vKb=exx3u;m^<(|&}$8XB(o{C_j)Bjy=8InC2}Z+7DQhy@9aLMO^h z^7P+3N2|mA*N30H_q#fbfL+PMiguS$!Cv*eq_h_sC}CVMXqDf?;F_Xk;%MXq3KAvH z;*$#sFVo0GQ^dECLVlitAxfJr6Cm<7&%GxzVs4|`-+P`uSz7U5bi0?5X(*SLf)RQo z2;~b1O!^R*e?OTWpF}BMpqDX{r|99?+B8IbXL$R=f7Z0}DuyZdYn0&6boUYk>A}L} zd+71|RIE<^F~z=4!8HVd*iL&7W`D2EDVr$rPZa!7+5fPV?s@S`S=wo+=;y;f`O*=M zyAY*}KmOEN<2HxO$XRux-RUIqE?bO;i1yYPi>|RE7p<{3pv5+4zYSsx%?eCw$U&l< zAWb&fj4Jm6^1KFIUj#{Y6KNne8!HOp(D;LmCd5L&6d!4+ckW4!iR@aNJIw$7yrnMv!ACAPkXq z^q6528jXI%6+BL`!g}XV~BFi!FWzZMY__KIrVXbl9r)*BA`)70=!$OZP7LC#ZEKcmg_8rc6SI{5A!@qTn|ayi37vDfk@) z?|FXlWJLqbAu&lvsU(lno6phRJ9PJJ1g2#9HHrw4Vv-EHx@-^=9DVXtO7<7e=BLu) zKBQYlQ0PmGs_ej${-+e0EP>i?l;?LRYdFQFwrQ0t| z_s-ZZE}vnC->}20nc`BeJRY5%J+6s~HIK(f8Oz5L=q@T4c)HK5&)w}y+~ZH&Hqo{SJA+)#Vy}+BqCQ^cyyBF-nqa5S*0)8rcCJkoR#m zVt(s|0L}5(6f0AW@^Tuf-+5-fxQ#9F-1=fgA`J>jYEDZ>xtNKDSj{J_8kv zKkhka+=6-BZU^5XT2MhGro-lRTEJ9+PmnKra=&AAn}RHCyTyJ4yhbqj^~mcYJme+U zf^tCP6fPU{!nG{;wo!B#=Uu4VLi5KAJ`xOlh%d6Bm`b~oDkW`@*_0vwka~$`M?l+u zzya=+CP_da)Fyb|`p)`w)K>ZXl<)@#RAJX9Q@@G%LTil45HUBJdEfO|WSSdtITfj) zg~j7|Wu106UT*Vz=anrn>wJdweuFYcADtHM)vWhaoPQ?q=t!=lU)smR?LO^_G1cFCZ9%;q33;#FF@2r{GXDrr0x{FMcn7cx3cx zCK#%Bs9AeGJ>M(QwgIkBc^>-S_u10n{r~X_OI=D0I+OiG7^GyIm9 z;r9$&n6I_q-R;BAUiddDhn8n3aJnG;lbVradeV{+sqlR2hc6^^LPWv+1Xnck{Ibd8 zx%m1K=_(uEbWvjZv-&34h4MV{t8+Xz{kRk_JhiLS4TljYQ}Cka+>ifJ$m@ygS2d^h zftv#{1G2?F;FS9)p;J%c$^=$Hgp1t6C%b&}xdj960p}iaLUiZv+qt5KALB7o- zEJ1%ddOEa_LE!-Ynv|#mwiU)Q+mRaM#?hBNC*DYR{|8zwFF*-_SW>#S5zoOT2wn_? zHj$U)!4&~LPl9q>SgoicXl$`A_aP9}1NzDpfjFK@bT((oCl=K;`gEU7cB}}bprD<7 z18os~=4wK@A-aV1H`tw|N#zm;Z-Zz8v_mBCIxQ8(cHBV`dH~Al3ZrxMEEJZWeg8Sl z?OGw=#JY{tmJX1R(PyztbU?0xfF%mE+78)_c!QpC+Q7Ady#Q-+X!KdR%hh8swSu<{ z_mYNI54i&!=GqkOY+87Fu)K5b!3D-cl}Y>E&>4YSLcw5mdP%y2l9V_D*)sa96`)2m z!D@%pYB9++Dw>EUdJSL?j1KpMlL>Zk7ql^4?T%8oLjFcj%T|=*c2%RGR?s3#PZxMC zwJ=WZ(P~I$hagtns0i1>U;`&((_~DdCRZ3YxTyDS107~lAf6}jT)w1pyVeAYaTwLH zuX5Us2-!OlB|B}`j3JI7~07fezVe)!=C+DX0t7x`>qrhK1Mx2sw`D;t&*WfD4K$b;Ww`A*SPPYXXwII z0qAq|*h|6ZDd04ZW(mhLQq3k|ij{;IT#kA#a>Ad=2$wS1E99;Se^t&51vZlT97dmO*c zF!4?%PL(V|7-uKO3lu*{^PixU6f7Ek_SfCaQ0>z!_G=bjk)f|jH4;0a zyk8<^d-4y*C>y7NiTvPqTi71Yn)g=bae}o2Wwx5)1uUnjNDk23p4`7>dA{|YA(dA{ zV|rFAxQ53(WA823K988K!#ThIIqPzDpbUZF6Yv>?8H@x`1)ywql%id(Ni2X z!TbhFCK(S9!Ky8H$j+DywzAh)3 z^axv3+Au;%DEQ3qp1)X_VU9d~3zb(*y8}F6j+y0i!+?*ZLG-39t%Qp-fmUdf@ ze1gg#LKV<95?8^8o7+{_2*yNq;3YS%ZiqCl%Ka2=1YnIMiGak|oKrIl@=a2dJBlmv zTZu+taR7~<$jVH~X}Ahedi{-<3C>GEjo_MvRINd_040+GpTiRYjyr;Zag|192rl{& zi6heTfgVh5!cq#xPBpCAor8(glVJwT6^|m-{^E-{r`CZ4U@>X<3}LGixNrVZ!(!_M4SX(<*fr6p{?08!=&Cl{waf)<&LK{6!>_6j4PHJp^U z$^b;HRxZLXk&^3&X$BDqQwy22DsTQ*mSuy1GrCX$!Ll#-VX}#La|JlDSqA8Qlox)& zvL4*dRcMu9nvd)L57FD2)~od>Y7^3RS`Im6uzJBXqc8V@r2&zH9^ctd z{s}-yqa_Frh1yQ#!UQg}`evG5Lpp0SrkJm#f%T#i(bC*!#*3JzFtpiHo2a z8Or=vHlO`jvB$E;<}3lb&^(e3&ndWoLQ|Uj{ZPLWw5RC3m==2|I9{V1d?|kp5rJsC^+-V5X}9%Qp|NnGGx`Ps z3F=K7IuU#w*G?_qS`X#Z23Dy36>|PmdCI^(Oxb#B(WCpuqgYBQ56*2=hT>R9uG5z~ z;7=X!#!qo6Rq?Ev?NtuMv%y)rhZda6d}+D2Vz;<_seAmXd%W>eTq6kytOoq+t!Tld z*&t>@c&`5+mUXB;KXyvQOq>(d3G(--`@cus`kM6M3oJvaOk$OiL96UZVkeTr>O(#% zuPgtP#NbAylC+e&eA9 zEL$1Tv6VCAONaqh{+8a3A)vJnt6hZu#ysWKOlDNxo5tdlWf^$zyyuYs5XBB}BV}lnJw(%SGJQ2u=%E1f2=4T_9=`34Hw#cwY5@^i`?8 zAEerFsYp-aH5cRuY#n`$9!Hm*^a*5Falx?cbPQ%#+$4K~EEkV*+E zJ3Gnc4yVh6@!{r3brmIMFcK7Fp9L$AkV(o1qswayeDRgPiJo@l1T&LRi=5;$BI+vz z|5sx$#f4RdT!<57jSn-YXundvmd{K}xps?FD}97skRz6b@Q2Z0Hy{WksV!);+MRG` zx>>%eq!h4eGpAqQO>O9U>U;UWk#Q-|+4)!H)hDA(%7PNMlWkD$En$sK3A4Nv8_%yn zfXkQA`QS8pfs@A1{BT{M61NgKzp0|MVrtlg7eI#j*Hp=aRhf+i+dj zE`F85wb6@49yPMOX!jop=Iwxf@N@qQT)3XRO@R?2k=4O@ZmfbTPtG+(ZfHY7iW?{^ zEHLpyli+kj3}4~(!#BBJjG^I^#*mht@Z{#xH%8vUR~^n9Od3a;w9Xh*O`N$(umbs^ z@te@7&=~#|nRd|_+W8ocU<{*K?7}nKz50c?l-ikWg?qIqq|9l8E{m*-MTq>3M)rN#iqg4&=d~X%2pKaan5q!k7k~!j9V)|z7#jfJfUK$ zgAN)0Ox6iG7m#k=_Ah6vh90kr)PDbZ)`eXzi){VZ^DXKC^#M5(u)06KdnUATRj_v= z2Z{_j|3VoHXrL_Nege(=X9n1mKA6rCg>-Qd^ihqqPm0Z$lvaqggCaVd7}0JF|6sP7 zoEx_%mM>n}h24ANt2B;tlWNWqkyDg3E!2CF#khM0!!JO@!=!&O^STUbGtxM&O{#cW z5RoRurwcTh6q}(Uff4;5StoDP;hwdQ@UrdVs@MIg;{K7IXr7^7P;dJ88F!<50kuA$hiu+XwiH+x z;mq0xr)D(<-4+8%0`)fi0y*)j6P@eofwZp#=CGXDU<2$Q-=ibf^=QA>&>w^rx05ab zo6f}@X33Vy8MS~G%5yu$*9sqBtVeKCC3^;d<7SO;Y7kP{Azbt+b7#DzhsK_?0SM_mY|8J9_OYHV0cAvByzv)E%@j={9x&(AFSqy?!54*Cboy6qo>GAK&RolH?Wvt zOWI&OluyAJc`#l;g9viYhhw(TImp2wB{Ku=*#`<+$kJknM9JHf=?mF>_bkluX9%nV zm|Y(pi@F>??ZkjDKGz?gd#wH)onZncDQLu_&#=yqP`A#nTL-aRTQ!zs_9mHm+K5?S zuoD!(XV~ROsN3b&?V_}mV@Va>qza@}C!ITDZieQeaTPiEVYdE}haUv*_1p;%Ar&=y zEUC(yRK=@}m>mTU%oLxY-;Yq&@7MKDP8)%o1$M97(CssH`w{B8{krb)D6Mwh<+#KN zSW;}qqc5@Bk1(#=ABR*dcF|bcLT}nao;G64iY*vF?&#R=i);7CwNu*av9v|rv_)e{ zbG=D(uf)yp#?9a(EB{$S&&$t9o}1=oDeo>~=W@gH>OCM}(A0AE$e(Gzo>9J4&58__ z7_dH)hss&a@T-72l-#8{15T`N}z`I z#E`Jtq4C_bK7C~W5_X*J=jFwO%M*(8F~B6GX%3w>0y-`(n^Cs>GRm5y4-XjHoVI~B zt_YInA3= z)E~T20z@jEJ0U=2&!qw0fjobxxDa$Hst)5SRZ78cMvksz4@ho4J}CP=VgS0~E8 zZViOrHX)0W_oI9TL8~b?pzDH3d|*HpOar%L@Sfle;0h1!OBV7IgJCo*9C07ruzZ^k zly8Ms zr2KIs%Thkvz`Em>)5x!&V3E?hk>#e;QHxMwl$v{m^0g+Gr8#K3d8zV^jqH1DjdHY! z6{M_1p2&17DP0Z>YZsc>+T>+4j@)nVXEYLl5o0r}XImB_+7u()^p;bS6%?#Q5J+t6 zIA9rQ>*=xdS~~js z$S*gsBxy@_tHTN#0WNaPCa+Ks{sS;2^a9(3#~_u;@gihpg<3XT^+p0hG=x$%C^apt zi|tgNXkos%Mc$-EoG=k7_*(kN{jDrn%3DQQNN3&vqiDD!QHb)&HfGgv78d+!`F6IX zhFAP+DxI7ZC)6IU`15!re}RHeQLvrr_&|AlJIjQ+WMn(bX9q|Ab32qLF`{=y7VKmn zMb99c?+m%Q6vZEo$3ih!%F#Kp7v9M)v5vZbkWMQ++G~V)NO1^;t!WE(J<+@NSbq*g`3Z}=5>}ZEv zQCB@4m87jcRm^{e(ub_)=ADl|SN&3-e@;D*^X}N=-Minv!y=vzALYMTR5welSUr(h z`CbQGnkfq65r2%1ib=hF&$xt_BUx4$P25zc%MY-Gy$QZgw{X1=_ldf-0?x|h1JK)V z={rChW{7!jtPN>`pePJ#f?)6=+X%Krk{u+dKZgxB$ihr?s)YBF#cA(xqfoAU1k^!meI|Cr#1_gGLoTA7s*kczyPUPSON6w&m zj&j2-5=KN2cC{EcG>mYWBxg?xwr=#f!1{CVU4j5Quy$K-CFv1lw5}5ccku14q;?HD zH3#A0o)nQ3iaIJ6_q77>3atzFXox0wNO@QPA1fdlL3Ck#Lw53G{LH`0;K+`S zR5!iASqj`RbUxgYRjHcu@P0B9kjfdoGJ4wBgFmhUqrFGyCA0$Z}9zIMyI{251U~-Xs;?pw}niQ z7S{llcJh3K-H!O>fzc=WY}8$S*x76`$_{t?=rFxE8yYumYTnXX+tgZLW8|6Znw#nx zTWZa4t>vsXM?!(7!flO;5%_8re)xd93$g^Dbn<<-*e#{9=`>Qtxs?mxNRORS*mYKa z1T%_8MeTZcKoKH9=_xS13^s_rjwtM+`fv0#aK@5$Qr5ve}co zYntLcz_#RvoIbDuhjgsC=M2=7%1S#kYV9Bf|2|@~vtLN2RMM_KgFo@_R$X7mQN1J);4=N*lqSmm^X z6)G#2MJFkFeJnPKg^ytfzy9dYEIBg?nT$uHtOXNN6p8{?+|QDg=lYmFDSE z=%g%}N|=EXil<7_>R{8D09pBr1IroDTBKCn7oDh=i;%TUZBcZT6~|AQvNRz8VLba~ zKPy!_`&n{v29nM~Ev9fS*7ETEq)f}yI#H-eS$q>#Bmr`;$fn;%C+X!flv$-#pER*( za~KrMP1smFTTyOe&1{bHo12)4%~eulR#iFgsDbJY4Ux_||7aXy795R7xbQKpI&?bg zqJa#Lr7bIYQu1o$L7C0ky7;Js7d1Rw!o%s0>4JG%qD)JhGtE3_d!htHtndfRU#ZB<$ z!JB@(xt3rEd-EU;o;$+!W@wcy?kQb&kcx{w9h7ti6Wz&}4b59N z){fpa`e1#Fv9@(JRn?1uQ&8-hK{Xly}VQW~9V!D}i=gs#nT<=TS;7{3bJZ3B|yyRW>~UtWtSy446x-;p| zXFr`iwA5!>={K$Prq+qer>~==sZ{WCUhc9pw|G;RiOZ*7MsE}Ey5%#soLK17P50}j zQ_7X1&=qI44{iHen>TfZxP1B*$79^V^b35tY`-p>(l6j8&p+99%JyiVH+8N?ZypSjq(CUvQoUq1cPaPdog zx&pthfYLVz;OkG#e`@g)i_wJL{>oZk@hX4uDsO7NxP1D0)9+DUzlF^#JSb9kpDR6I>95)3-Q4M~>GGy_i_53) zrerya`6!F4w2QO{&RCzfKW#s!JD=dMZt^yF_^Yixv(0a|c~cLF%cnoU^Hrk}OZ_@* zqGno~$Zb8-`uxtPcMcsscdLKVMsHJ_f6;zlrNv)q@uphE<@_#cb2mfE>6MqztgJ==T`EmRSW%8Z`myp| z%Fws)Nz8R>zkTejILHU;8sNx)kzi0 zjg#*TLJpGHLGDEY%r!P$Uflc#hNxj;^ymxdZ+~?o(aH&Pk;b_X0*A5 zR8$EwFV++&0)zy_g+dbTK-p<~P>-Xpi-focb%U{1I2?(?KLF(blxB2X1@yZ;wj<)` zir^{@?HZh$K<-G>kk!O#Ou>USEWzak+eA6Mkgnk` z1*r>bW?tGJIiBZ%6qp%tA9x`-@)?StLZhNLRc*0 z2pOUAE5yhP7xw)~2aoqJPy|O#LQ|YrWbbQ(e2LxjB(;fU=Qbn_BnUVo(U#oWNdmQ5 zxwZ5FFl7!^o`$t?>iyQ=geshjxFK-z!< zd&-<`IBpG&yml+kMEobWhGVXC)jCzgb*tE2Z5y!S^ds7^y8!(QB`2o_Cy^qcp?W#2 z*t!Z7a|X1|ft!150m&*07k)C6IAZ}0H0?Xkk+(abj@2K-wlv4O}mNruG z0DDDZZz{idh|Mb^#k+hLbD<+lyq!lKw+;(yImJy zMGA^OPye&^b1BbdU{dUDwjwguRKUak!j=2opB-k*-WsCFWl6_!<*UY$K=xZHFTo zGIH&UY%z1&aC(|k<<~jusj`ZFn4n=O!>8z2WZ7~vHXxGoIJFAJ(H;;U0R>KwMV4*- zFup_i!WBd6%%`5|hGLBG909OkKG%ZwkO_#h%9LBx(hYFrtpVfO-fky)MY<*#-33tr zKNV4s zjguVsOax768r7)dCe*GNsnM5lP!hH*(DvC~%et-yQX zL_zq_t9^MMD2DH+-Y8_c-bfJ}`hjT%1J}>e*hDB`gs_h@NW!yjV#bEEOnwC7WYAM}xfAIC1_eZrs&Yf-co;^hOhL_C5zjzx`Y@?-ZX?>)iJx9?-8APi|@_hDJ5aI9d?cy##jof6Ymb`8Z} zSO6i@SF+iUgPgSCw^w7LW|V!U=1lK}5^wP)U-2e?@uqQI_&qOjB;hNt3D**<&AF4H zjh`!_XH&!ZVn^#Ex9rQbp7H&Az?qlnUbIg+@g!@~eg_eOkzYN@YNVL^=;i%N@mW^5 zoiiD2w7T&#h&WTlT?zOuBVHGEf=u~g*bQ|acICUk7k~q_cN}N~5tVO_=+BpxFP>$y zwBJRh_eXwsmOaGQ(!E$U$=(QGH(R@_3oCZ0M1IPO>Q^TugFt+n)6!{EE0Mp45+wb| z*=Jaf6!QZ-#Op_Lo?{QQwC_{dv^vYLB7k!KhtW8CAx#N9&vIgZgqPFwBY%6IeJE|^ zic`)vgl83>P{dH6-}w6}UWr}CG|Ax7z!T9d3+h(a4zq{K!Us%H`AxsApdsSY(o7^h zOZ%1(0S&A%mZdAxUt(}sR5rZCENen|7)S$!0AYgDwXj;zRslf82ZWCWKOKWR?$h7{ zmQY^#FUkimvFhYE(A*pxo$4eG41$#MeoOSUku6_mIoM+V1-y*pO|W!|^PFf>f-2fz zj8u2TgSAi(FXaT8ESM@#d^@Biv$^?M!6?@S z>kP>RWafesiIJ!y7n=ZrdS)i(J$xU<2@C)Q`^Gx3YLaCs#OnOZoK9-rb*zrk`E6F> z=3D;(Fbr&5OeMOfHlDa5lHq7!m2q&V6DPwI?X55vjYYhDZCIFacn7h@MK#9VMRcYi z*?J?-kPr)Ba7mhwn{yFvHFptF%P?o9YPvBn5u*|+g@XW-7Zw?CS*W`xEK5O^NPdH4 zD}zr&sGHI6HW{Lqv&e9(VRfiQHEt||bt)aEr4u72m_^DS96n-92{*x(Qku!kS&JA` zbp;I9Nd!H@Fh?zctQhKET7R3|r2ACaLV^)WN0J7K9~7q?5*%d_%1HQ~+K&h}Y!y() zZmY!V3fv(*gzA_C90Y3|f?gcqP!pE3)nol&7KQ?ubezO2aWR>Yz%mH7&jyjXo3k@? z&Xq$HLF#7*8BEcSWLaMuR*`Zv4Wje};GwpE4KuGnei7kdilJq~bb$eb%erC@edHmS zTvVW8oIDr9^FZzJNOoUv_cvts&0!VJZytG={<(j)f_r7~m6V^H#rGAV`#2W|%!j3# z_#d3ELFXEojUcg?&=fjbh!D_K2x8PgJ~w<0(HV(QJaCvGDg?6y_yOKHQY$oLIQ<-q zSH5!|%u|F&2YNXeae_z9Z|9MLBRV}ycxlxDVqz-5o=iq5nZB* zGOZY5kr<{szIbvU67FVF*g#dxpn%KP#4HHK0w=zY)8jq#mUh?3pP@%Ch~I|CfW8xC z0}fP9`A5p>SJ}+%Kg9b$5{Y^@gN{UM5o7{WoZY8`H+x&Jvr9flsma~1$8NLY%#y=` zDIwC1qrIuORN+<0|1Mi>_%-4e5QX2WSiZ~lTN>Ea+^BhrUMfGg^_873?>v9AcT1~p z*;fCutzN6$JGa|6x7$Crdps{GwgJovg;(>Va*dDHoH}rBt2d|4ms97@sT(f{zaNi| z(x#7>M`h%VYgkH?QaH+*)`7>FUg)iCy|4%Yu1l4>E>`aHRqpXu?(rt=rAxo}orJuT z9lnGje?k#w@auw(B3{fHH7;aU_mG!p+~_GQL;l%b!n zA@+h|@nWay3(BXx*!g{4`42DK8qe8{Xv!EB&9{XsD}1cg%wxVt-GgeV9vj_rwt&h1 zjpUKed;hMy>SMFne<*+Qu{mqLhe^>2ZjN9MXA7S*DHCX5sd>#eQL!=w9C(H9PEydL zbYEn%wbzjL8zYZjWIt1z@oa+p0Tq0eg1r7l(lbw-}#9$_y*fv$gAbu$vGKb znaWl1DteSZy}?#$|AG9kk1YC6)~|8%aihU;#i42;9U4bZVNs$1ZZ5xRD4gb12$a|dyrz?WG(K7 zQpO@8Ij->D;k=|eIB4!}z|0S_=jpiW*|0@*j}1$U0L6=9W#V>&N-IuW(lIdL^SRcH zcZCJ@0yE*+fRoIza}*}uo5ipM5kh&BRcR#+oQIVkzR4C8!b+5IZ~SNDy%|eSEjqK@ zUtEJbT(A}S^~=2aWu)g?vy-e;YPz~JnL`DBQ>{0n4i}75e*J2%ezj^R@}?F0bTjv6*+Fw!c%~*rWr(f&Wul4HJay42Zj6Q1VOHQphv%x=Ou{UE0E*O3M z`lVj|QswH;SyRj+@+@DZG+t(_+>1`YNe>?6Cp-AhSbWO88$P??WPHCpmP2AGQF&xy`{5`syGOjowWQzrfH6uuSA8 zM!eB!a5cT8bBFsAm~fJi$>Gn*&ruF~Kp`ENDlt)z4hj@?8?-=uq481+Ap-;Kz66n* zxEiPVs$JASdddhRvlU9UUTmNtwirNsLY={B$7=<=Ff?;NE$m)_=7ny<)v2}357mYp zY;bBIXFW1CpgQ6ndn|PDBKSKT$A<3iA04Js*$9%Wbo5xTpRBU03sMC1b!77($%6C0 z!QK&Fr}neMd{zS~JSs|bFP2$qW&LIE_6_ zBd(HGJS4YW%es+|>D5_CN5_3P!%tyHYqN1R3{}lARkb$rlW*x%JGe@#TgYEk$6PQebaVfsF!*^ZK4suKdK6|0R?8ByE2t8y73>QY4 z?M_Il@BH%n8{M1eoH_Ezh0+?@Os6XUU?ZG` zyMlm{2p?nNx2ejgd^Vz7a@+})hm<(J?`8s)pLIa$R>LAQ@Qhjj)!rmut+K(HCZjuO z@({XHc(WQXL0SP%KOFmM=%Eq3R;*fFsRrKHAt!3IN40}qJ;0Ih#+FEuOoc-WjH2k( z(bFi-jbRJR(bw4|Cs>Ok9c-==xhKs1Cmd`(&atLqf@e+GvBnIp;7+1FoNeNqo}^Ls zzba)`8^z*-C({-GCVH)MR-!a$XklG5%p}^BOzr?#~i?6c9U)d7*emo{Bt8iS$ zGsHx>i|4BMq5^$XS@n2qxHO!bZ(1-O7ZH&d)c`Ni@uY~T45RY3a+N3Ba`{cn4(c$ z_$xa#lVcWNHTen|S~{ELU9@7(B$T`;)S+qSP}yKv;2wu^m7ly8oy(e(k!x(9J6l~@ z!?aY@Mwv{}F47J}0*=-M$p?1{Bt;NRRzWZQLv|cfjYF2`aPvsBnlnV^K~6)-IeX;> ze+83XV=X`HbgI@*$RTP#2I@a`YlTxLo(d)ua>Wx4p@!B7eHHdpV3RT^If#C++i}#( zL8O*rw_qTKo~Y%G|z7|rPV3x4tCD`snjTpn<`?nAaVcPf{8j z*Gcd)^RHP@tzj*)dKiDWy2aO(U=Aep*$zX!%8$>+*~m`r#B|*@yoB?fp3eGGtRjDg(@T@1mRH`CD@Z$#%uE;ALJEOpi zW3F&e#I!TzL>y1W_gPOeSCUdoet<7wTyM@*y06n>#+g}ex8*AN2yHy77s`09hv(XN zvS$hvC0EdsLRBc^xw4pc)|&Dowsn*CTGY%vvha@SP`IWmQsk-d~Wp1G0ZA!+s zABPn4TpuqC3#aKVKOE3ZcV@7+bV>}T&~$7J-N>^k7N5Z*H$vL@1r>FEv{X?#J%|U_ z_prPec~(KHcRn1WTyaTdiZMxwgXeHmNh3erVI6(fDQ+FP>1zMEQ29=@G?&#Ve~gyq zvBgS>MtXqNDKBfJ9qw8NRR|v#+SxU(`Gv8-5Sgwq*WzTtq~CRC3OELPP{F2Zn>#tnt}V*@ZA1WPq#H#Uvyu zcWI>^i`uANY5S>N2+^*De=1a_#Yjictb1c5b4<;6RCGd(G8!XQq*f2j^A=a*@})2K zr!V%VFIJdNqW$T4Iw?{MeQv@T!_;tjaYl=qy8?r2s1b4m zh$fD(z(f)Dy*NzcL35Bo2Eq7vn#g^^#gYPPCTNI+;wym4Vc_^7(HXvP9fYeJUk##J z&Z*Kp^oFbBVT2^P4FS!cF*(`O&AAImCGdh}9FnNX8^vm4G# zdw%-U(}&idPxYBr`%TcBH{kNcukpvP@y4%FmLyAS4(#!z?)9hc^~T@un()z<9p^e< z>3+HULYB8>t8e)>|MG3#vK_d5i97v?JH7fFUJ6TLIV0K>2_HXLjjkMj{J|5(X7xMK zI6NN;prF4WNflsxVNj<%&l5MBMZAJmGCN|ET4XM)R~4wkfG;yb=MBS7kYnkAMrt6P zJ}!*foN0%#K zO_O?(W|oadN5?J%U5Z_JrFhneFV|jUPI0?5S2(2k?@QM)H%gYUh@yG8!jr4VqpRcaL zi#gy(-oEY2{rHs(=cAV#u6^xoV2lIIIfu%VBhf2bInnrww}!!@S9&O;1HjRG1uCZA-?ap3YGkdmaDv0=-Ff?m}N56@$h4j=IMS^f$Q&iR9 zZrmv9nNxkEdia}gJ&{;KClTT+0;q!eBGi1N1swrgr58c4RB*VJQ`9aY*DcUj!>ehRC+xI z1RtD5OrJRCoUn=1*>pBdhpZFTg6N@0*Ze=a_S@W`zom+J-kY642EXR9*#_<19Xl~sn9I&`(XRb*vdg` zWgrpWED_UBxb&!msLeT`EYFcL!({brh^%(yNG8^(+?OMz7o{UbuCd| z&5>r6WMm&}8q3V`YSPE1=Xy1n?e$+`l$~CF9a}f%IlXAQQl2X<*ir>= zmDHl~S%tAx<53h;L`}k~pkU9nwS-4-kT`WB)jWX`YnO1oK{8EoCvR6FbYdNf3`Q{91 zz55m5kh}w3_gfvIR3-u;T#KXdv0qs1(ZCvJ!aFjLkcNZ51#a1Zb{69zGpPzENL{V2 z+F()Kpa*i7~v zXg46S!kPX$#MUnmi5xb2>B)d?HxZMr?=X-wx%U4mGY_TeDFmubdWF7-v`4 z%$sd;)!(QKm6lSe5a-oQ7&$?2wj)pIOT;12L&V;7*Mw%yo4T1;JU_XKyZLFo)iF_9kFy8Tu*GH)#w(}Gr1hVOKrW@q1OzIl5g~O=1P{blH?DdPe)EC! zk+jaq>xu3Y@#h~v2Sp5`$0w{*kd64Kw;}ll7d`MF2e2bwY?3tdk6qL33hGe^2=smg z1)M^mk}Pi^fH-%0Ct4yr5;sy@36Ya53iuK3TuR|Om;ID(3k48bl-1=@R?K50KRu_k zmrH9wdY>tma^_}VPpsN3ea^%1MBIns^9&%sN3nlRJtFpos_b&bH(z};`Zu!aW7x@8>%JNy#L#b6mT;C|vsB26m76{znXC0t=~*Y{k5$a20yp^dCQ)F-iSe{AdiJSp;X?V{ z^(CX7ktHif&5}N)>{3R>#f*wGd(JKLWz_jI>XZk~Qf)!;nQ3TeN-+<-n|FsF_#Fpr zp#bel`D_AholjqSIjQJU(#(rVGfy4zR;?D7FR9+2RDWC(>SAR_m9&1rZiWre5HnJC zLjytKmAuk%ZN#H_e&nM_qnOr9l@HC9VwAYqQX*TWyuMyCr|P4o7aUJiyCvV~)#r@t zoGpEWrO-0>F=EGp_y$VwJ;E=QhNo0SAaNU<{||FP#C-xdB(I~?hmqPv7C|nML{3G) zkxiU_H{!wu3t*!G@xsgvekX6D2mslQSQGgW1q106a*bn1xRaYW-!UU=GQc6`!vb*c zA>5BA=A%60^2ae7^EO1smW`#*h{Tqmvt!GyWaLd^HMF5uE7nYAGeYlpUd1wBdZ3m7 zkiSUU1pGw>kK~^Q`&lH+(BnevvdSGu{Rn|;M&ftzADdSfJT6r(;J#%1L$jN)$YI69 zQh$g$l*48e+!`)?!f@<9-+u`^>wOEP4EGfx77Hlg>Q3${xe~=sE{NR7;NmgCeKK5@ zSR-M>!`4SEju3SM2Ku}$SVIU->K7g-LWV}c4Q&s!Gr>K67FqS!qz_K2Ku#!_%TO|R z*PJ&6h{Ao-63(pjFO+h{C)BQMj*Z;zxQBhvZ|xIm7m}6C-9qO~Q9Jh%4c8q4`~bRS z&SF|lh07|jZk7^Oql56pelZqNx>nW=YE$z~{UL?z@-{#C4zPcM?1n7fk(l%`xqhVKq5G%Fyzp(wR#9VDYMYzSk41F{$K2-)clrR+tV zdzqu0Su7>FCw}j%(D*UEuFefO+i+VU` z1py*TR}a3FZZc`FqKBfWu0V93#SvhA0jUqim4~WyZN&SC&#B2aw|ctbl#qwu;OA*f z*$DG*H_^Z?LBNGka>a{~`$zeUq5*`MaG^T z(fbhs62L@1(~5*Hzgmn0H;|O5J#L|1#WwQoG{wlbJdoPf;lc+Fdbt+~yozx?0^iqi z{rKSYo1~(U4cKZaG?uwss4$Gi_=pdgmj`gLS`V~Q*UiWdZoUs=8z!O9B27J=u)LD0 zU}}6cCChed#3Ydi=P|97N;YxmrOvvTI%`~FDP?CGl<%*T+N3p% z*!tPU1x0TZE%N18`}3=hH;*NyK_p5o{A%4BWy^dsmiuQc_oXgZZe1@GRV=K2BR=0- zFxMA9?{a=&DATz)zWk-g8Lnn5c?U@dwKWai1iCJ#7N2T3)9g!K>c?SS@l#({BJ;N? z-*QW{mBSmP`F6W6wcC$lwc@9^#?mq&qt&cLzj$-Zetp$Yj8DJl^7Onn^5^-c&-YKC zKXj9Cdi8`4eW4Q)>e^ zI-kD!lPhjuXJV*I+B_EuLlv7Bd}5vNq|UgMT6QtD%xhYT!{AbD{i(G|Uz5}b(eV(< z42q7stl-tDZN8Md_+sv2Z%q?9s^+%%b6dRn>@j`zCB5;Y-gqh#(ym`` z@Lo_j#lRBY$m{ z*6PBWuOe7U5)e2G&N*S(O0(C-=@12>4RtPB7$%7e{A(ua9;<+s^UK%-HL<`BO!VMC z<&~c>gYxa|QpbwWzA|ig|=L5Um~g*$zq1r08hnS39NUI!3SYP_|1dpKZRbJoC~X`t+}J zxf$v;r>zYeBy4TqN89j`w}CsA+jmJ{c7G2oQNR3L4mfF93T_A!-%AiKY;+bR$+&PA zQ@=>+YC>{@`swkI!V*$24w57vNlTDd7oLw$ISK7$gl-R9lN+oR7$Pw2Fsv7fFF&V7 zY-r?UHSAx*^-H8x!gpwJLKOG3`95*6+}(!L=x*v0+Hf}I7on3Qs4i_|ix;~F1%4={ zd!!;G7chx+z(-@yVe;Kn7Vl~9nC?;Rd!(wQJ1CZd9dRJW?5z$hnAktd@bj@IT!YL zcklJp?(^5~^X0brbK8(`gHMxv**N!-ap6VdLhqtA7czg6^Mf4kR-3oW?rZG!H+K7s z2mQu_$6!9s^=a}h=eqydQuC@V<;}a8H*aX;`9-fS`R z=4iDQC!iO@&)Ce_!sBGdtTRWv%sRtyN0fq<53jvnL831^cC$IeXbTQsO1q#8wY1We60#|6nsyZDoTnO$3;b&U4mf=$&13# z#1P?V65at?4LHQ!WIjbW%dPJH#Jm8b0@6qS%oNC0JO?tvFmT0ONYC{}l=TEnr>7<1 zb#|;v%5s~|%=N~bkJXQ9V(;4UnGN2Q^(VJKzWb5gXY$V#`0^V4d5ws|<?k9G8=hU8G;VWtJm$Z0GI=$OFy(tH9`80OF#_rYFL+M*Z`k{jJRlbrf z{*o=;lvb}hrPZg|>ep=bYPN<_?omO=9di5f*7)<*c=L8$Sm90CgUhGc>(}h{YW9Xw z?>@Qc@gy1k* zcClc!2D=1(wM9{DUu&JAV+~`m>c6!0X`i7_eJ)-8zjN+4OBS&Gzy5JNx!-p8d+xbs z)qC5Q9INqUEy3@2t@?XPsK4HXrM`rv9{vr-(?>~`m^fvbJI5P6*B3q46Fv8J)#pc( zyw((-HHE~Tfl(;;T4(sIGd$KA@@Jj8tfs*m%Ogw$k1%Yw1AK+A;^k*)q(dyi^R}Ej z{^zx6y3sXwb`LR!#+%)wR7xQc;2gMbVraSG+!wfyr9-EFUO&dYRHxurHMHyY$SdvPOinTFmCs&!8`BC7PWckA`JuTt^6!Ra_+oh)#m|?Ys zCKPz=g@aBMzE{7ZXr3o^UVk$He#bVbzXQqYFKA9* z^n4HhRH`}BoXA;dCxW70WMn>-qXJZSZ8beoS){3UysY z{X@#(V{D&8GeqeRF!RL>Vo@IK$WZr@Kr(p#By+=7FqKe*#dr|a@LTD7R_l86{;!Z~ zw3|99bUHQp+Dee^P=!_6Z0{4ZGr-vChPuhUYib)CNKrjla@h%`%P=0}&iFdYHoifi zfvQ3i??eRSWxL_SbT)3KXI-+|&~v9uRK*5!4tC^s@X`f&$k5xMxq38Tn*;CrdTnyi zP;C0J&`D|#0E0cf!M@%6kN52X7%7l{z71eF{PUe_d=)Kmm7uuc2jp$whl1N0>Yq_P ztBOL`8sDa-A181EV3ZeMe|m>{b8K%sinsqZx;GN!P5bq^<8_Hdm(T#hX_#Rp^m~M^`^Y;fZh?yBlBItClAU$9MH=nAIFOm?K1PA{wwXfgTn{) z0;~HV?zvV6`uLw^c`1(9XrVNwEN{q+5&Nii*Aw^*0Y+oQ95Zg9OZSZo2{1$c5TyE|I8$cCc_3V6iO?&{s>5Z=!9^At%QDAC7z^zrAHA#z0+EpEzL!n?M(8Ef zp;LM0vr~=`3gLy&LG570SaSElogZLwbH8t+hH#6yKZu?hr5KwCe{}Q`fns>s=shG4 zx%EXF^9`C)FLS(~d>F_39f$N>Z9VZA=E^4z>Dj55IhNytGWG5pdE;Sy<>+&E%ed$4 zq?_~wh2_V{&qX2;y@};RS-Ha^cu`y$x}cMJl?Jck5ULKoag+Yd7)Fl@f6-)kzdSI} zlqyd+|5CBZh{0+;12>Pfd_GPZ%PMU4N5CsY#{o6XXyW*zySv~d2zJ@r-~Jf7>~%u( z&8c{RQFH^^p-fks$g`F>;ZWfQbI%rz!+&*|4zzU`_1^d}$-D9psKs zJu{i%>VlML$oQ(vABh_VxFLx``G4l_;&X0Lg(hS$PvOI zTEn9%9aOm>4VR$8C9`6SKdt^?pdLhbxGu|a4)Aru`eI~l-RPe(Ht}TZBdTt_V$h<- z#K0d@W3>{g!U4Xsqv#*4G21fH+HY`}3}a}%7P%ZK=Mo%aQzI(CEoO9+j@N;1Xb_qG zkQ0VtTtqmAV;8m8g`<`c3OX47vby5I%x9|6DCthFhYfdXJBICCo z-loS#Z8{s9?}^Qqzqn0b0K^yv7M!fOUC+*H)?S`ea=Z>qXJXDZGuy6NyD$TW>%JM% zeTUv4M`ZBo?XU&fKp8ZbH)=0umL0#&mkDP(`UKeIFr;r6`ev*djtJf3C&c($9jwXk z?g(TwN3Ob4Uy-s0`YI`V@O#0f{sO9szIvzLvj8RvA*L|#uKx*yS~L`)-m5Q}3}vCXlrQajC9d#nT;Y&?Nx#!$U!s2W)@*t1UOg`J zDsS9oU)*L-^f*5$?$gWME?SWdojIgiPw`%e-$*JvAQ=e7!s-*kdEDnP)EwupPkCS@ zJssju8j#|e>=C71iTHc0K;K0c8f5aJ!qZfmfnebq2y)r$X=A$~$Kf7+p#N4D9>QK6 z987Zip)^_G?xd23vX-?FLYeJ?=SPI^u5=NbwFWw#*f-5@&YY+Ol}kh}w?joFL~Nil zkM>r&i0sBg@JTtQ5DG%o+~MZK4$@d*XXBNQHOk5z=ZUUeIJ5Mkxp3*MJYk!vHth5o zWs|!jBL@fIQMxtcH_&x+uvDi(y0fh78??=DcY|kZR-T<`!*vTQNutxt{)NV&>3oHY zHHFTIk+c{36r_O@?!i0|RS`mF7%f1=*h#NI242kLTur7Qs`jQWEh&;iSCry05D|(H z+jLFrX|uF7)M2c8fe=kB3B*whRioBmaZ$W5)2$2zFoFxo)=IX`9n&5DNN7(2le=hx zP~!kcGj0SP2o5=CuY8H4S5OH16(GeJlCy`qd*6HJ$zB-7mcjuJjKVSB;MfI|771=F zTOfWCZWz%sfsP?R=HzUKW`L>?=2E+vi4u+S48db^ z_fOrY@)8kF9oc+5ChcSc>n2zP@%G95u2Nq7mYycJe@(wlyH5^(P0x)u zgGS6BocMtL2Q7otE&3wWT2hqbG2AJRHJ+B~59(Esjkq$taYy&>Q?;*3*MoY2?HPP+ z5Li0+tq1iry4#6YOtHm}(5MZqu2N{NvXP4mT~0OH4ZSV6MuMN9ZWnq=)hx$>Kr1fC zE*Q!njv}PT;2S|doR-K)kN-566!I`<3LH*w{?zEb?h%a+xMB}=A`Eb#_{8{xEECmb z04KHLQZXU8$4#ln1jU2B09IEw33n4p;Fxv!0-)?#TQkUHO^T@}@`hNf9ri)N2QiJ)*y)X>Z7$@9KFo)P$o(k!pr9 zOqqWvSj_Kn2?^libc6Ms-;>XNS1*ou2^rlm_~-BHf6*q#A^lboy%CR;L3}79fl|jU zGUlXSt9@QJoYXsNCLTF36cc;Ic_jNt_O1B;=njJFzoGcFBiDV()7Z2li{ytV^_e;{ zE_qy^p5a3&cVkd`@xSGeWhgeSzl7*CYyKS%bWWCEeq8q~3Awrndbu$+Qojj*G)0(z zaIjq+pGl&IwsIflQ~ePi`rhy9`4O++;@-h$zo&nwg*hRNq*u^ymcE$fSTHc&fd8#F zT<196>_0?&vGqVKJMai=66LU=5zYS`3O9&_IBG4B!+qd-*zW<+BN!1cWU^%A+vM#5 z58c4~D58L9TW*7r_WkTwcKgub{iBMS>;u^+f#K?dVTHPl_u44 z`Cf5{xqAyLT8A0IM%M*M?bQ5Y9Gz#^GBgkFd*hyDG6QevZ~A^LI{I zEK@B8%|?TEWH5z7i~b>c>sA6M<@{56N5pUN+SlcSr}SA-`@OLRzSsikKc&xIJ#mdr zj1&w0hQp!MPWbao?Nq;^)UIQ5JgHskH>_JS@Rb$s<8K(0Tb_XSZnHj+f}152r#l2M zS}X}0$Tl7j_hUBbuSY=}KO(|5Y}F@BKRSnwG}9RjOWSpe8XL(5Eg@0+wSw79;5**31*z8uORm%vm4c?5Xt;2FgmP66HH$Xtb#x?aBY zw7$Uh8q!bE&UKSad`2%TZT6;b@TEh}nmUv)QO4dL+h2Fg^w4*x9xBHFT4#LmrZYbJ zOK!(k>FkUzbji;6!Y;>k#us{SI^*LTrZc{UzD__`<tp=!-{oTD}>(BfGWphX83iA<#2XTUq*oUBm4^D>{ zP<5VU9t;j(w5{=h5DMMXrkVP}U>US^bb@sN`$$JwPy5bNN13$$imWl=3yD1oz`Q!h ziC!hIC8p0?I&iK##VBf1ZCf&enV zB;OS3Q*{dIMhiMA_Sc$L!;NrU6oR8N*H-JoiO>ayvE-d1&{pzy%-Y?&U+<;1(>~98 zcL&HFbXgZ%o!LMa(I!uN^IhysjI4PpY~_g0;Kbnb`)kwpthHRI~3>a*UUdI)I z{#MFjcw{Sg@nFzb_-GB-7m&oX%hz>PY1YS&jq&Kg+mvCkj8_q?8|A#%YxcCZzHoUcX7;n(!{Y?WT? zGM{ys$GS}Z*KhUONE*C0A-z|Ml)*Kx>zXy2*>_cH-9rclasqyhnRNt*=EgDE`a6AE zP6=96tZ~Jl?8#Mcpj&2ooh~2wonAb5ZUe`eb`idS1Pa4TxsgBL(4rb+Vk;9a=vgKg zM9#jTub9Djm(QWmVOm&Jb>q(jxPJ;M^(1xwdonRwOOvNBKsn_v=$LEes~7aO*}FY) z1>U$$UtFgrdYqqJ`Fp)aTQT_M-|OGeCal1&s7IJk*8WkSvAa%32B2x%}7riD!60`{UFC32IUN9glC3+uzn} zGgo@!>V0wbp6GFYgFks&9}*Eg&gsh=<^Au%F~hy`>S6tZ%_0JmoI|s;bnn_laf&HU zq``cLH;}hK1_8{RyAi^sU9GH*G{2$@3%)b> zcAR;TO5wml>|+mT9lo{tV#}R-d&=Q|xvitc&?Drk5A|8o*-1n|MqwfpxIE$<#!mAG zKg5PdOcO4$8A0|q;AJOvE+ZX{Wmo82Yb-zVHPVT-O>Olq5JirED0%)Al? z7C%SU#sZ=dg4+TGcZF{pg_fF7Mv~DCXlx+BU7r>q8$Z$uQh73@Q`eG9Rn_b7$uEDT z&y3B)-N$H8X=gpfV)^Hf^uo(d0y+0%eMaVLuWgObw#H)}=O+(;tbZr6$s5<~i@U@7 zbedfGiN0xGd$_p7Q&e}V9YFoOaVvarD?HKT{N%4c(dXyx@W!?I;@UjXm;4Qu|3z<% zn!cB2JZ(^Y69eW)G)O=DGtRC8Z^0Qa&>1n2~iq8>YC?n|yFAK*l1kN}|- zq6Abfd(2IT8xsKyp!o`qL4A(3XivabXy^WpF7tEpP7i;;WwyTgin$AhtWsAGP*K3s zin|MfBS;M5A;>)1JIji5!sw25G*Q+QkndVWQ7jt<0aIFxW|-xO+> z+wojmArD80)a5+&IJ0Tga>naCOBu~a()X(gEWlF=`|NDNanEV!^fFJ@_vBE7$co)e zFEI)H$=#7+enc+j?MrfTq*xv*X)5R=r^eiN;QJ&ZuueAmr z+7jf10Z}rz!zT7=t5|MeQ$xTE<1bK?N!DQSF@zXEa?A6+yl zesGOl?AP4$zzqf=buR@)CrTQ}0c=S`KCkO#VPjQ2)Fx;)7@7VhwXgPDzREiRloSRf{EixX>;Lhhvl&ZlMT6jASHgLrl( zrM#XHfr?=26eI&jonYe}h=oE20PGfU!b|K2iUiZWZ$~%eD=;CYzM?$@XA%%{Hd0?N zS82{0UXpi^w;P-(TsWVEUUuVc)D|~XpQAK8(7BdQ1%Jm7Ts5!vNVd0_aPM5G@s1=YPwVinw%*?$z&YU3e5{-LPV2&CZtwRncib?J_58X-gYG>yzbla$RXp)S{rig}-9la}e`V_5)BQcY_8Ub5i z8QNPsYTQ*O?cH2!NBG#!4Gz9irgpZLD{EqkN`u%kZJ76CDN;WjMl~YRkSrAMkx0V>mZAF< z&^}!wm`P{87vLr_#wWCJ=KIWwQj7$!kwRP~aO4AiRm?@3N14_MnHGOKH|~nR(e6)vp96a8{B1f zNN}#0$wvh?GIGX%u(tu6Ogej5wRJMO2GeT? zA}7&E0kQ5O>i{zz`s zvG;c6nl(f>G@DhV1_6g4Hk3KIh!p^JN9O@lH(dDhj=pJJ05Ta> z2T>mAFEbV`Rl}svz{*fBbhgSHGelGJy4oh7m!>Mm+A1goH`g|L^=5c zQx;Q@rLE+%A3jLBpsTwQtj<1aA&<9NkAMmgQ7I7YcCN!RP%ki_naefNIVuN5xE@rI z%xIcuyI9xi>gwj~P}{vqFAf#$<#+*1>MaLd)QJf$lQYHY6dKx^Dn_{&>c)oV`nBd> zN_)5I)02@RZv|KY7;|zS$^M&6GXgF?G$+Dc3kUbU(b=I1*eNjSXkmmE#UviSa)X8q znY3{YXa&>i#2^@EG)>cZ8XXY`<`67!)W@On#(W(p5+w$x6F9LA>WtF%6S&Xod#nkV zs*WmJAwJrNIXE=eobU0sq5B-WBt$chHk_HHK4f#6kzXo|-7rN&PgqggSi82mUX352 z=O*6$Fy3J5vSI|U!oZ3l5{UX5a~nMdy+pOF%K3dR&-~K#XvlM)sQj@0MTWRixu^zr z0fPZIQ#TnM`Fp$fLjxfoZ2+-|2~))MAc#d?&NyhvHQ8dK8xygYBLOWo6Y6+iAk4Oz zM`-;c>!5@K<4=36`7vr^x>#4$2|E_4Ds>#_Xmg=EMxU6Lh0YM_%b;s3`p&%IuJ1=jB0PIDjf|Z zVVJ5jXkxfR3d40K<%7mD^##=(^oTkr8tVJt*@$`r0xm@d1HqFtBZw%0!PvpRB2rNq zTCAql9+tJrD$MaY*C$9!L$nnnMgrd2)hDAUZ}-Tm12Lf}ljb%LsMxw)PR?GPPe*lA za8#kb2?0BB8&nTbS0d!QzzyApW|a+G5S?NJpj3dyfRELfs`gEJEsgxzIg zu2_{=Q(s-xgpF=pLt}Flg|Vcyw9>ttMUG52vTb50t9|75r^}>H0;;H(trXP0idGm` zEwIBVMnj0t0Lz+?HYc`fEJ&Q`vG(|@Ac5t^9y*GO!uW>>(Xqrff4`1xD$SjdCme5$ zd!Zf$CPHv`1VW^v2yU4h89Ro>!ZHs@2tyi(W2?|apf%*UU#X6vv6q|?wa|X;!jebm zvyPqE!Dz=&*Z`_wy=GJ(K4Z}K1keABDbxXlj;0u2t(8%o9n7JxIhr=m;M6J_l(|8H z;1tAhBnwau{|H_s(?m@N!He0~(N%^V%XYv>l6wW~I4mZd8Q4*AU_pBmc7pTcWxXT6 zwb`13=nOi{;Owj=foq`7WyafJe?glsDBNIEC~R&N*qJ07O0bcPS%91EIFp3J2^4`5 zRv@eL#cVA{y7I+~ZdV?7z10D^Zg>o(3Y)E5F^s0es_1{mA}VhkxoCnCU&L4gsx*q1>te+rT9&w*-a)JE%ae~r<>Sn{}89C{4!i7#NRT5zlD)z zG~AYnDNVEPa5~Dnvya-ks`*+}k;c$5h#fP9tzC7&!T;vqAVtWV$0T@h^W4p+dsWAmL?P;sg>reSk6!9v{Z6Pw4iBo}M{o z9))ZHEEM)(Euo;1X9q~+h~@;rfe%KR>Z3nv)o}wXtgu3C!Fq*4nBm4FhQZ#FRD_)I(-buW&Bq4`5Bpi8i3X%N9f z4U#_is`_#)@K8oLq>tbk(|&jf25Jq?^%bU;+CN1ycJAuVaf~G*Vu+fRb!8$aq6AdJ zqeLalh@O_XT!WhC#=&otiQfqK83^L~CN+hGAv|9}DXaze6*AqE5LSMyO*fn%MU1^9 zwR2HwAEowFs+&^RQHr&TdvM9h9wczHvg1Su44XUd)CnGEAB|`c7yo zt>P1AMW6~d;?0i}h-3)K50DF~PQP!X@0*qB8}1#ZSEKPEL<+bhH`uPJT*L;VT#4o~ zgAk)mq@}e2G^;wuQfTa-<5i_2$r`pSqztMgC@rcK^am8(gQS{LYkx@oV74fZor!M# zO}xd@TgVCrMG}sY>@sVPD9pUZ8`tHF>+(d8^OIN45k>Bpgs;Tj8hf<1f3`O|+ZUZZ z9F-8CKWrfoZHZ0#O3AGyM|U3+UR$2emNy&|d}y;Irn46A6%Y|8l=>1%hhu{e?UuNt zuUvKORY$Lr*WZ5qu|)&Acj9c{#M#~nb9@u#cqS}5C59*P+eHN9ER%B|D7m-f*xrFg zcu(|YEO;ej@!5>Uo+XW*<}1A!n|v9YWcge%ZA)yzSBh^fKDx5M&1=i`*>Z;`2Fpyh zB&Nxj+hh8tA5ZosO!p;BAD$F^m|=;H=Pzqz{q6PryN~PMloDS`iPu)@vz2;mvj@cB z_vea2-A$@6mzWrVnL(4&py?4dDW_u$j4}eOTVGBoo|!Z^j2U!E)1q&wO}RZWxh*v1 z0<;&M1ly!25=o1gF@@+~K=(#jz(ssW{hDS&NNHt+e$qqofd!(>HX8+gKvQB8d1juL zV4vc(JA8J>q1s_R+P+Ma2@6H4dl}5B=?copV>&j9AE?LCW4ksc?6K|BJ9juX?0q}A zl8U)CCi7@vaMn!-d(MRp{$QbaN-Ldju1{V=IBMmX&W*^V7E~MC>e3mVc#iU$i$qZ> z=|LG)w4_37=sx+zB9U#Ii(auNpjW=7f}qV=EVkxu^V+uiY}-B7Oa7$yn0VanYCxXk z86kA4;-eop6^P_Zimjv#6JQ#_t`z6P(5@2VA%#(MWf(eM{C88G!>Wr*nk_hlnaX8d zoykNq2DQuPqb2qP`Cm&!U9QV(+wZgO_gF9aliME9Cl6k`RD46YUFVQ5=_#=0AqZf? zPveu6K`!vvca#VM;hDNQ-iXQ4GD$e1d$658h{Gl_rjsoOh!_~4jol_iR1qey4X{92 zSZlKF_*4RUEyfrdCK+HV!2o&kBECEBvt*vOZ`fC}r)+HIWlHU@25lT2FkV#vqE5xU zqD-J3-Nt-y&W+ghqu6zYebz$A*!?l4`e3)q;K zG#-u+sQq#rWmpYXwca$EF`t_&UfFKr0|a3XX!QLJTn%$A$sgB4xQzus1cOBd1Ni~0R#tN}Bskna^y;&Rr^!jXh z^d{Kf{C0(yk~e!m^Q6wk?^OE{yEnSp7hUa%t{&k-p_Lpb+^iRf!G~6gpG3IH`l0j9 zzyJO3uFqnADW09FpVZ4sgSqUqs3^1-$QN|AI+r40t{oEE1{N(zm&_@Ezs(H@R39e( z3Y(c2Q*grF;N8(;07JuMgjYBUT@((ekfcsn9U)H!s#rw?hh>DBK%Bzrs+uYXPfU_; zf&GJ+V5*EUMWBQ(DxuWT1&^}Mj_FiGlD(23jHFLkn#{X#6e7cYy)D7TQwa33moQI< zqcDgAV6|_!I;g%&mMn3Ev}OW2QV!g<^-UiAn3ZbWwdyJ2!qCT13E5cN*aRl75>}&4 zP5grE*BB?PP-OTp9wVFL`q*sPM%=_EWJCW+&E5w1rkH$pyc;Iv%3sj8VJMzxGeN#U2~DvN?43%H`G zVJ!tBBx7QTYLpHq^QhTPQ#+f#6uT>I*XIUDGFJ^{IgCzwvDxH~8ht$8l1rW;4B$kF zGzkmN%*A`7?#;qIY!lmVu-Te|Q6gz~pr~b-?qT!NO>^H-*f$@O5q>48VHJ*%HM&e? zw%uTmb(X9CpIf?R7#P)u2vf{4zqqr^;C*GrB0_#-;`N7n@1cKB1Wddz2Vt_X5O~k1 z0O*Ul`s%A`Di|9G$-aum>@>=F4JlVXDh2)-bnS3@N1GuwVe<$~%usP?KTb$wXI7l0 zZ1q_&lWE)K$RNy0vXth9~Fg<)Z|g z^fVNiJMe3XZsX@LXIUN|!|jO*#uIx7ETO_tp)9JVFYg#;E4{o(e{chaC%%Gy(G~LcjpDOyi_XUH2lmsg0*=PB)#d`O#KS>MH!a(W`yYt3A=HN9K+K@FQ!DMY8Wo zv0kr?kQWc?>4O)p6tBnW9eOY>@FuNUF4!ejYIn-RyTsNg({woaydJ(AsPERxgloh? z?Q8OLyT!7J$5Kh0pVokz$=eLaz#+Q7W(v8vALuyX3%2+9dhIJ>n{@QPudgyy6;B*Wk*; z_d)&_5N_)zT>uDTc2X9d1a=c3op<&HHBxB+%jdmI)jZ=Yoo;5~!w(NxFn)QqAdnC2 z7KQP#?Vz{7(BKX@e5q_A-k@w`Uzb=^%HFDh$e=ZePV1XAP9G>*-6)5Br5WwHKAA@i zZ3_CiH_ZO{v#j1HDkr*9sXx?K6Q*||zp_tE-NWk&U0|A>vym`cAjDM!HVHa$IgF?9 zP93}1Gk#336Ov+dRh*%aOFpggQ5c4?10#l`y3FbpQ}ft)%4ez1LpExJeT-VlPoW;! z-YtsVm2_h?PN3Dk0uTIlML4!|a1Vbp83ZCcSC`R@y!=sZT^*3@wX-{Hf0B6>9_!?F z)W9$Hw|4m_1Tx~THORZLKOXmTK@+!w9_6!-@e)d??F$M1Iyt3u}LD zYgd;dYEXWo?CB9D@k{AL0-vM7esS>O9+9HKVXpkiwPJNNu?F-QTqPR9k-GF$jwf%a z`gs#7eF>GGgi2Xwz$l?Yb{b+?WCgum!EdK67^w2(El@vi!a`rdLQlfN59J?wghNL3 zikodqy!NF&`%;g6sl2^c+*-WUYp?X#D?Rqgp^DW z3}mfSlxd5l(>a zPpv=Ids;u$`=gDX{Cf5CCS2i5xWbcg#h|!OJgKGEdrIn0&jG;Cn{tIOwdEhJna*^D>khuaB zWfbF0QEshWq{Yu9+}>`+SEdsQQ?&|YBU3p>2hoWg`*Eh_YuM{`k=rqtMk$iZr1nS? zQ?;1HfG{qyHP>!xZm6&+%C7lRy=H!d{>GW5JB*@Jxd*zDGgZwXUNa0uSUwj<;~A@J zXyq>n5r$2yAFxf ziVM&vpEx8Y+peNU5m+Q&J0!jtc|Ud7ee%d*F*TwGx2xp9Vc|1f4T2yrH#1ZJ3NGkz z7JwQzA7X=?E#lJowP(#M#)nQ?{&YY^bhnYT7nJ+foI})!GNcbW?ew!*3LV z@Dc!HE`Ov_(d~D1)Sl&91G+<*ijZ;56&b;u!`H zA`h4$@4r)Ixp{_>lX5j5$xoY_l#c~x=2PbMv>5+~QanZ94bIc;^aW4WFH@?5>N`k1 z@D~DZ0#oT__VlrcE}2}TaH;SRAQ#*vCUk6~+f3f^NsedLo%BKl0gl@6B4r+$@-Wvk zowu?5bS$uSs}o_5T6?cCo}((f15lhC#v@!OAHGWzY)V93{C2W`3XooYyYf2G0=4V5 z-G+ziG6ldNt@P$Q+Wq$4j#i^>cMHbeIB48LZ_K9p%q74vBkE+)-J&3#Rsk=C9khJD zG`Q_VXw^yR+v<({O`zavrSqbGRc$_6MFm@j&PCwjq9ES>gZ zl85X`N9r!fX;Q2hoP3YS(6T$Uq4>$K#Alz4&psA=ywMv!!xujT;hJM%YiW(?kj?js z+(q+GmA+DO#o3B05UynFc5g+CucE~}xy7?%mv{0m&z^mr?t`A5gMhw0ecs7^!xqbp zTBCl^V##cjC+`(`smq^hdSTMj+n?R;bu@W4wtF4z-h_7f*1ck}wp$k5CvMW}tEi{YuBI#s3z>DP@MfZI zjWU!6$9*>}IVv0<;}$pxEP~z%rFJLW44Ag&d=B}eT=0O%n#V%k?KC5E2n1(oz%RXz zCX6yYrPS}KS@~A^#Ro*L?F*POuh3+f$ih8&yxUE(RJ!&?Z+s@}rJJSESUHOrR;UqpeM5Oo(UJT=jq zU**fM@?@^*ukEiry`SQ8C0wA#B0t+|Q+pTJxpYm>!#Sp=3d1_1+s*nMmZ1 zs!m5k@{$j>cYuA*+Zi&ytmY~Bv7oc|vn?yV3+B3V8cfWOFlAKIZ$@>{#h{!~Khi`c zQzi&#+lWzug#dLc=;E8tK3z+7q>?J_W5uSEnW<=tqEna3n1{rMoY1TYBKHwm64lcE zkVw_BaPEEx`Y3l}&b%vs^pNOt7hOonCYBqbnE-%TP0kvQ3SLHAQl}6H&U|XKq~#C? zPF+G>A#KWVeBko^1Vl0(P7GWkP*Td&;pD*O`$?9fIboM6meQr3DGf#ao&BAs7Z0Zf z-%GbRO7FSx{u_rU1#WT8zMV|b?mF|>UOn*i93I=?YKCsfrED8geaW)HFQN^EC&Fb&YD67-7!|+ zyN|8J`Zt~*EPq(6(~JI2W#39YPwZDjL^nWrx6ly=xaxUAPUJxru}i-Fh?o=cFmjnO z`1&IvQ+Jo(BUYuT9=T93adG?%Ef;gO7)S7JKpkdkXsD_D9J)M8H4F~U>5!0zg74T|s{MHb88Q>6E6N(12Shs4xucw3Aq8A{Ef=r%xC02d|~fq7|O z42l7fsYR+WkohOYP8~K{Up^_OMRCi2qTh8=OO46H1ZOH+zVuVP-zYyiDOOZe2jX{| zsDdP#msI$oBSRO~H`LA@-E#lqqI8BqZO)*UBq{UZ`l=RC#2f2tAT|o?jDQ-qJo~uF zO$}@DlgMBdwfIrx;cC|R#0t$Z*!Mm0f>js;5pp=MdDKYuMj=}dS^2bkz82DX! z;R#XYP6M*!(Fd+k4abHI+?z^*lwePos%#a;52%Qr0QAkMWt&yVWMKH{G-cMz#W2<0 zh+ANyEwXbh1BYJ47FLs;=2PAKJVP%%IoYxh8u(@%L0x-98&X>s`DgX6eH{`pB!)WiqxrNLRwead?t&3A-*`YCSFWTW~$-dIQNX9N&BNwOCe?p;131Uqvx z_qIYD+@(A!p?-}F^{d~C&9O(ZI#|?0r3XB=fyc!z^C|r_LwoSCDhWdBHib>V_$xHJh({}JHcX7 zSo#5Z=8VWmCX9{qGi?XF0RZdEcfSQGOZ>AUQgfH%{_q309S-S?Dt0^34Ivx-u8J`e z+|)>1O8Ex+(^yQGBT=yN2x<_9aq)&fnGr4uNCO%R;@z`&@!2rE>wQiXPR=!PZZ6^6 z+zZLrknxu<29uu`=|P-3^QWT69>g!k_ahkLP76$L`Lmyb5&g1!=cl4>w~HppI7pY5 zh9a6&kYhYGI%kEJL(RYTC6OV! zDD>|igB zm#$Mk!lj?h-04l)gs-V9wIHASL zldp-ZgxxtyJ(%E}BcJ{)WVG|tb-Z(dN+dWdR3g#2SS6C2OF5w>%foLVj}+%}^&r(* zBX4^XdDN-viO!WOk?y=gB_=snt3-x#Ehn_ea{cd+N2aq;J;-u4%hn6XW23sxc5YG$ zhjX(^{xJN5Q58UK!`loLPF;-&jF z5ih&m5vAG;dGhljMK7KyAA3jaZ+M+p6cj|yAHha?ek&OQ_+!J&2mO&saPGG&_2%rwTL8$o#gD z!ISYURXS1`7oqZG-|LUyZOo0_O{(6Xzy}s!A;Z*y^T&o61VO|KU2fC*#vg~T!>?^2 z-ZY;`!Y-2nCi)>G6@Tzvm_dy{-c%V67|Qq)MjOKTZ6RN{{+OUIU4NwV6yuK#Y|Yq> zK*O31Q)sPk^7g=umZHGE_Zj)?{}R)*YX`M=#UJ$KS)|$n4bDy{c+q6X2{Yu6FN(=a z7118$fatp0SOV5~khun0zF66Ol7P@O0}UG3C|<*nvSX+6c50AYXWJpn5GG zN$|hr5EEhk2~7~1b#+vm67Ymb#MqEg{|)wgB9eWDV@ohC*-$#zb^+c}UmWuWq00bX zh-+Va-y-7r1nSqh3ue;XCNjHIE_Wde#9_m1wPI^P^`jNGZ)JUXP#da&1e0{m*ks=b z

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 diff --git a/psagentai_agent.py b/psagentai_agent.py index edae490..7d95b92 100755 --- a/psagentai_agent.py +++ b/psagentai_agent.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """ -PsAgentAI - Personal Agent AI Conversacional -Sistema de Personal Agent IA Conversacional via Telegram +PsAgentAI - Personal Conversational AI Agent +Personal Conversational AI Agent system via Telegram """ __version__ = "1.0.0" __author__ = "Marco A. S. Costa" __email__ = "contato@syspanel.com.br" -__license__ = "Proprietária para syspanel.com.br" +__license__ = "Proprietary for syspanel.com.br" import os import sys @@ -116,28 +116,28 @@ edit_user_profile_field = "" is_edit_agent_profile = False edit_agent_profile_step = 0 -is_agendamento = False -agendamento_step = 0 -agendamento_data: Dict[str, Any] = {} +is_schedule = False +schedule_step = 0 +schedule_data: Dict[str, Any] = {} -is_useragenda = False -useragenda_step = 0 -useragenda_data: Dict[str, Any] = {} +is_dailyroutine = False +dailyroutine_step = 0 +dailyroutine_data: Dict[str, Any] = {} is_activity_crud = False activity_crud_step = 0 activity_crud_data: Dict[str, Any] = {} -is_configia = False +is_configai = False IA_CONFIG_STEPS: List[str] = [] IA_CONFIG_ANSWERS: Dict[str, Any] = {} edit_agent_profile_field = "" _auto_improve_counter = 0 -is_agendamento = False -agendamento_step = 0 -agendamento_answers: Dict[str, Any] = {} +is_schedule = False +schedule_step = 0 +schedule_answers: Dict[str, Any] = {} def _parse_time_to_minutes(time_str: str) -> Optional[int]: time_str = time_str.strip() @@ -431,40 +431,33 @@ def find_matching_upload(query: str) -> Optional[Dict[str, Any]]: _WEB_SEARCH_TRIGGERS = [ - "pesquise", - "pesquisa", - "busque", - "buscar", - "procure", - "procurar", - "na internet", + "search", + "search for", + "look up", + "look for", + "find", + "on the internet", "internet", "web", "google", - "o que é", - "quem é", - "qual a", - "qual o", - "como funciona", - "notícia", - "noticias", - "notícias", - "atualidade", - "atual", - "hoje", - "agora", - "tempo real", - "tempo real", - "previsão", - "clima", - "tempo", - "onde fica", - "onde é", - "localização", - "localizacao", - "mapa", - "endereço", - "endereco", + "what is", + "who is", + "what's the", + "what's a", + "how does", + "how to", + "news", + "current", + "today", + "now", + "real time", + "forecast", + "weather", + "where is", + "where's", + "location", + "map", + "address", ] @@ -478,39 +471,28 @@ def _looks_like_search_request(user_message: str) -> bool: def _looks_like_create_request(user_message: str) -> bool: q = user_message.lower().strip() triggers = [ - "crie", - "criar", - "elabore", - "elaborar", - "desenvolva", - "desenvolver", - "gere", - "gerar", - "faça", - "faca", - "faça um", - "faca um", - "crie um", - "elabore um", - "escreva", - "escrever", - "programe", - "programar", - "codifique", - "codificar", - "implemente", - "implementar", - "projeto", - "arquivo", + "create", + "create a", + "develop", + "develop a", + "generate", + "make", + "make a", + "write", + "write a", + "program", + "code", + "implement", + "project", + "file", "script", - "código", - "codigo", + "code", "app", - "aplicativo", - "sistema", - "site", - "planilha", - "documento", + "application", + "system", + "website", + "spreadsheet", + "document", "pdf", "json", "py", @@ -523,7 +505,7 @@ def _looks_like_create_request(user_message: str) -> bool: def _extract_create_suggestion(user_message: str) -> Optional[str]: q = user_message.lower().strip() - for prefix in ["crie", "criar", "elabore", "elaborar", "desenvolva", "desenvolver", "gere", "gerar", "faça", "faca", "escreva", "escrever", "programe", "programar", "codifique", "codificar", "implemente", "implementar"]: + for prefix in ["create", "create a", "develop", "develop a", "generate", "make", "make a", "write", "write a", "program", "code", "implement"]: if q.startswith(prefix): return user_message[len(prefix):].strip(" :,-") return None @@ -532,54 +514,44 @@ def _extract_create_suggestion(user_message: str) -> Optional[str]: def _looks_like_edit_request(user_message: str) -> bool: q = user_message.lower().strip() triggers = [ + "edit", "edite", - "editar", + "modify", "modifique", - "modificar", + "change", "altere", - "alterar", + "update", "atualize", - "atualizar", - "mude", - "mudar", - "corrija", - "corrigir", - "ajuste", - "ajustar", - "troque", - "trocar", - "substitua", - "substituir", - "adicione", - "adicionar", - "remova", - "remover", + "fix", + "fix it", + "swap", + "swap it", + "replace", + "add", + "add to", + "remove", "delete", - "apague", - "apagar", - "inclua", - "incluir", - "reescreva", - "reescrever", - "refatore", - "refatorar", - "melhore", - "melhorar", - "otimize", - "otimizar", - "no arquivo", - "no projeto", - "no script", - "no código", - "no documento", - "arquivo", - "projeto", + "erase", + "include", + "rewrite", + "refactor", + "refactor it", + "improve", + "optimize", + "optimize it", + "in the file", + "in the project", + "in the script", + "in the code", + "in the document", + "file", + "project", "script", - "código", - "codigo", + "code", + "document", ] if any(t in q for t in triggers): - if any(name in q for name in ["leia", "lista", "mostre", "exiba", "quais"]): + if any(name in q for name in ["read", "list", "show", "display", "what"]): return False return True return False @@ -587,7 +559,7 @@ def _looks_like_edit_request(user_message: str) -> bool: def _extract_edit_target(user_message: str) -> Optional[str]: q = user_message.lower().strip() - for prefix in ["edite", "editar", "modifique", "modificar", "altere", "alterar", "atualize", "atualizar", "mude", "mudar", "corrija", "corrigir", "ajuste", "ajustar", "troque", "trocar", "substitua", "substituir", "adicione", "adicionar", "remova", "remover", "delete", "apague", "apagar", "inclua", "incluir", "reescreva", "reescrever", "refatore", "refatorar", "melhore", "melhorar", "otimize", "otimizar", "no arquivo", "no projeto", "no script", "no código", "no documento"]: + for prefix in ["edit", "edit the", "modify", "modify the", "change", "change the", "update", "update the", "fix", "fix the", "adjust", "adjust the", "swap", "swap the", "replace", "replace the", "add", "add to", "remove", "remove from", "delete", "erase", "include", "include in", "rewrite", "rewrite the", "refactor", "refactor the", "improve", "improve the", "optimize", "optimize the", "in the file", "in the project", "in the script", "in the code", "in the document"]: if q.startswith(prefix): rest = user_message[len(prefix):].strip(" :,-") return rest @@ -629,38 +601,38 @@ async def _handle_edit_request(update: Update, context: ContextTypes.DEFAULT_TYP return False rel_path = _find_workspace_file(target) if not rel_path: - await update.message.reply_text(f"Não encontrei o arquivo/pasta relacionado a: {target}") + await update.message.reply_text(f"I couldn't find the file/folder related to: {target}") return True abs_path = os.path.join(UPLOADS_DIR, rel_path) if not os.path.exists(abs_path): - await update.message.reply_text(f"O caminho não existe mais: {rel_path}") + await update.message.reply_text(f"The path no longer exists: {rel_path}") return True if os.path.isdir(abs_path): - await update.message.reply_text(f"Ainda não edito pastas inteiras. Me diga o nome de um arquivo dentro de `{rel_path}`.") + await update.message.reply_text(f"I don't edit entire folders yet. Tell me the name of a file inside `{rel_path}`.") return True try: with open(abs_path, "r", encoding="utf-8", errors="replace") as f: current_content = f.read() except Exception as e: logger.error("Failed to read workspace file %s: %s", abs_path, e) - await update.message.reply_text("Erro ao ler o arquivo. Tente novamente.") + await update.message.reply_text("Error reading the file. Try again.") return True profile = load_user_profile() today_context = load_context() edit_prompt = ( - "Você é um assistente de edição de arquivos.\n" - "Dado o conteúdo atual do arquivo e a solicitação do usuário, retorne APENAS o conteúdo final revisado do arquivo.\n" - "NÃO use markdown, NÃO adicione explicações, NÃO adicione blocos de código.\n" - "Mantenha o máximo possível do conteúdo original, aplicando apenas a alteração solicitada.\n\n" - f"Caminho do arquivo: {rel_path}\n\n" - f"Conteúdo atual:\n{current_content}\n\n" - f"Solicitação do usuário: {user_message}\n\n" - "Retorne apenas o novo conteúdo final do arquivo." + "You are a file editing assistant.\n" + "Given the current file content and the user's request, return ONLY the final revised content of the file.\n" + "DO NOT use markdown, DO NOT add explanations, DO NOT add code blocks.\n" + "Keep as much of the original content as possible, applying only the requested change.\n\n" + f"File path: {rel_path}\n\n" + f"Current content:\n{current_content}\n\n" + f"User request: {user_message}\n\n" + "Return only the new final content of the file." ) messages = build_messages_for_ai(profile, edit_prompt, today_context) new_content, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.3, max_tokens=12000) if not new_content: - await update.message.reply_text("Não consegui gerar a edição. Tente novamente.") + await update.message.reply_text("I couldn't generate the edit. Try again.") return True if new_content.startswith("```"): new_content = new_content.lstrip("`") @@ -673,13 +645,13 @@ async def _handle_edit_request(update: Update, context: ContextTypes.DEFAULT_TYP f.write(new_content) except Exception as e: logger.error("Failed to write workspace file %s: %s", abs_path, e) - await update.message.reply_text("Ocorreu um erro ao salvar o arquivo editado. Tente novamente.") + await update.message.reply_text("An error occurred while saving the edited file. Try again.") return True preview = new_content[:400] if len(new_content) > 400: preview += "..." await update.message.reply_text( - f"✅ Arquivo editado: `{rel_path}`\n\nPré-visualização:\n{preview}", + f"✅ File edited: `{rel_path}`\n\nPreview:\n{preview}", parse_mode=ParseMode.HTML, ) return True @@ -745,22 +717,22 @@ async def _handle_create_request(update: Update, context: ContextTypes.DEFAULT_T suggestion = _extract_create_suggestion(user_message) if not suggestion: return False - await update.message.reply_text(f"🧩 Vou criar isso para você: \"{suggestion}\"") + await update.message.reply_text(f"🧩 I'm going to create this for you: \"{suggestion}\"") profile = load_user_profile() today_context = load_context() create_prompt = ( - "Você é um assistente especializado em criar arquivos e projetos.\n" - "Gere o conteúdo completo e organizado para o que o usuário pediu.\n" - "Se for um projeto com múltiplos arquivos, liste os arquivos e pastas.\n" - "Formato:\n" - "- Para arquivo único: apenas o conteúdo final.\n" - "- Para projeto: comece com o nome da pasta raiz, depois liste os arquivos com seus caminhos e conteúdos.\n\n" - f"Pedido do usuário: {suggestion}\n" + "You are a specialized assistant for creating files and projects.\n" + "Generate the complete and organized content for what the user asked.\n" + "If it's a project with multiple files, list the files and folders.\n" + "Format:\n" + "- For single file: just the final content.\n" + "- For project: start with the root folder name, then list the files with their paths and contents.\n\n" + f"User request: {suggestion}\n" ) messages = build_messages_for_ai(profile, create_prompt, today_context) content, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.7, max_tokens=6000) if not content: - await update.message.reply_text("Não consegui gerar o conteúdo. Tente novamente.") + await update.message.reply_text("I couldn't generate the content. Try again.") return True safe_root = re.sub(r"[^a-zA-Z0-9_-]+", "_", suggestion.lower()).strip("_") or "projeto" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -769,12 +741,12 @@ async def _handle_create_request(update: Update, context: ContextTypes.DEFAULT_T created = build_workspace_tree(root_name, content) list_text = "\n".join(f"- {p}" for p in created) if created else "- (raiz)" await update.message.reply_text( - "✅ Conteúdo gerado e salvo em `workspace/`:\n\n" + list_text, + "✅ Content generated and saved in `workspace/`:\n\n" + list_text, parse_mode=ParseMode.HTML, ) except Exception as e: logger.error("Failed to save workspace file: %s", e) - await update.message.reply_text("Ocorreu um erro ao salvar o arquivo. Tente novamente.") + await update.message.reply_text("An error occurred while saving the file. Try again.") return True @@ -796,7 +768,7 @@ def encode_image_to_base64(filepath: str) -> Optional[str]: return None -def analyze_image(filepath: str, question: str = "Descreva o que você vê nesta imagem em português.") -> Optional[str]: +def analyze_image(filepath: str, question: str = "Describe what you see in this image.") -> Optional[str]: image_data = encode_image_to_base64(filepath) if not image_data: return None @@ -856,7 +828,7 @@ async def photo_message_handler(update: Update, context: ContextTypes.DEFAULT_TY photo = update.message.photo if not photo: return - await update.message.reply_text("📸 Recebi a imagem, processando...") + await update.message.reply_text("📸 I received the image, processing...") largest = photo[-1] original_name = "" if update.message.document: @@ -869,7 +841,7 @@ async def photo_message_handler(update: Update, context: ContextTypes.DEFAULT_TY try: await file.download_to_drive(tmp_path) except Exception: - await update.message.reply_text("Erro ao baixar a imagem. Tente novamente.") + await update.message.reply_text("Error downloading the image. Try again.") return filename = save_image_to_workspace(original_name, tmp_path, update.effective_chat.id) try: @@ -878,9 +850,9 @@ async def photo_message_handler(update: Update, context: ContextTypes.DEFAULT_TY pass analysis = analyze_image(os.path.join(UPLOADS_DIR, filename)) if analysis: - await update.message.reply_text(f"📸 Analisei a imagem:\n\n{analysis}") + await update.message.reply_text(f"📸 I analyzed the image:\n\n{analysis}") else: - await update.message.reply_text("📸 Imagem salva em workspace. Não consegui analisar agora.") + await update.message.reply_text("📸 Image saved to workspace. Couldn't analyze it now.") def load_context(date_str: Optional[str] = None) -> List[Dict[str, str]]: @@ -977,41 +949,41 @@ def is_profile_complete(profile: Dict[str, Any]) -> bool: NEWCONFIG_QUESTIONS = [ { "key": "agent_name", - "question": "Qual será o nome do seu agente? (ex: Mônica, Luna, etc.)", + "question": "What will be your agent's name? (ex: Monica, Luna, etc.)", }, { "key": "user_name", - "question": "Qual é o seu nome?", + "question": "What is your name?", }, { "key": "user_age", - "question": "Quantos anos você tem?", + "question": "How old are you?", }, { "key": "user_gender", - "question": "Qual é o seu gênero?", + "question": "What is your gender?", }, { "key": "agent_personality", - "question": "Descreva a personalidade do agente (ex: carinhosa, brincalhona, séria, etc.):", + "question": "Describe the agent's personality (ex: caring, playful, serious, etc.):", }, { "key": "agent_traits", - "question": "Quais características o agente deve ter? (separe por vírgula)", + "question": "What characteristics should the agent have? (separate by comma)", "list": True, }, { "key": "communication_style", - "question": "Qual estilo de comunicação prefere? (formal, descontraída, carinhosa, direta)", + "question": "What communication style do you prefer? (formal, casual, affectionate, direct)", }, { "key": "topics_of_interest", - "question": "Quais tópicos você gosta de conversar? (separe por vírgula)", + "question": "What topics do you like to talk about? (separate by comma)", "list": True, }, { "key": "topics_to_avoid", - "question": "Tem algum tópico que deva evitar? (separe por vírgula)", + "question": "Is there any topic you want to avoid? (separate by comma)", "list": True, }, ] @@ -1019,24 +991,24 @@ NEWCONFIG_QUESTIONS = [ AGENTCONFIG_QUESTIONS = [ { "key": "agent_name", - "question": "Qual será o nome do agente? (ex: Mônica, Luna, etc.)", + "question": "What will be the agent's name? (ex: Monica, Luna, etc.)", }, { "key": "agent_gender", - "question": "Qual o gênero/sexo do agente? (ex: feminino, masculino, não-binário)", + "question": "What is the agent's gender/sex? (ex: female, male, non-binary)", }, { "key": "agent_personality", - "question": "Descreva a personalidade do agente (ex: carinhosa, brincalhona, séria, etc.):", + "question": "Describe the agent's personality (ex: caring, playful, serious, etc.):", }, { "key": "agent_traits", - "question": "Quais características o agente deve ter? (separe por vírgula)", + "question": "What characteristics should the agent have? (separate by comma)", "list": True, }, { "key": "agent_notes", - "question": "Tem alguma nota ou instrução adicional para o agente? (opcional)", + "question": "Do you have any additional notes or instructions for the agent? (optional)", }, ] @@ -1060,8 +1032,8 @@ async def start_newconfig(update: Update, context: ContextTypes.DEFAULT_TYPE): newconfig_step = 0 newconfig_answers = {} await update.message.reply_text( - "Vamos reiniciar tudo e configurar o PsAgentAI do zero.\n" - "Você pode cancelar a qualquer momento enviando /cancelar." + "Let's restart everything and configure PsAgentAI from scratch.\n" + "You can cancel at any time by sending /cancel." ) await send_newconfig_question(update, context) @@ -1100,10 +1072,10 @@ async def finish_newconfig(update: Update, context_context: ContextTypes.DEFAULT is_newconfig = False newconfig_step = 0 newconfig_answers = {} - logger.info("Configuração concluída com sucesso.") + logger.info("Configuration completed successfully.") await context_context.bot.send_message( chat_id=update.effective_chat.id, - text="✅ Configuração concluída!\nAgora você pode usar /start para começar a conversar.", + text="✅ Configuration complete!\nNow you can use /start to start chatting.", ) async def handle_newconfig_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: @@ -1135,8 +1107,8 @@ async def start_configagentprofile(update: Update, context: ContextTypes.DEFAULT configagentprofile_step = 0 configagentprofile_answers = {} await update.message.reply_text( - "Vamos configurar o perfil do seu agente.\n" - "Você pode cancelar a qualquer momento enviando /cancelar." + "Let's configure your agent profile.\n" + "You can cancel at any time by sending /cancel." ) await send_configagentprofile_question(update, context) @@ -1166,10 +1138,10 @@ async def finish_configagentprofile(update: Update, context_context: ContextType is_configagentprofile = False configagentprofile_step = 0 configagentprofile_answers = {} - logger.info("Configuração do agente concluída com sucesso.") + logger.info("Agent configuration completed successfully.") await context_context.bot.send_message( chat_id=update.effective_chat.id, - text="✅ Perfil do agente atualizado!\nUse /agentprofile para ver o resultado.", + text="✅ Agent profile updated!\nUse /agentprofile to see the result.", ) async def handle_configagentprofile_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: @@ -1195,24 +1167,24 @@ async def handle_configagentprofile_answer(update: Update, context_context: Cont # ===================== EDIT USER PROFILE ===================== EDIT_USER_PROFILE_FIELDS = [ - {"key": "name", "label": "Nome", "list": False}, - {"key": "age", "label": "Idade", "list": False}, - {"key": "gender", "label": "Gênero", "list": False}, - {"key": "location", "label": "Localização", "list": False}, - {"key": "interests", "label": "Interesses (separe por vírgula)", "list": True}, - {"key": "personality_traits", "label": "Traços de personalidade (separe por vírgula)", "list": True}, - {"key": "communication_style", "label": "Estilo de comunicação preferido", "list": False}, - {"key": "topics_of_interest", "label": "Tópicos de interesse (separe por vírgula)", "list": True}, - {"key": "topics_to_avoid", "label": "Tópicos a evitar (separe por vírgula)", "list": True}, - {"key": "notes", "label": "Notas adicionais", "list": False}, + {"key": "name", "label": "Name", "list": False}, + {"key": "age", "label": "Age", "list": False}, + {"key": "gender", "label": "Gender", "list": False}, + {"key": "location", "label": "Location", "list": False}, + {"key": "interests", "label": "Interests (separate by comma)", "list": True}, + {"key": "personality_traits", "label": "Personality traits (separate by comma)", "list": True}, + {"key": "communication_style", "label": "Preferred communication style", "list": False}, + {"key": "topics_of_interest", "label": "Topics of interest (separate by comma)", "list": True}, + {"key": "topics_to_avoid", "label": "Topics to avoid (separate by comma)", "list": True}, + {"key": "notes", "label": "Additional notes", "list": False}, ] EDIT_AGENT_PROFILE_FIELDS = [ - {"key": "name", "label": "Nome do agente", "list": False}, - {"key": "gender", "label": "Gênero/sexo", "list": False}, - {"key": "personality", "label": "Personalidade", "list": False}, - {"key": "traits", "label": "Características/traços (separe por vírgula)", "list": True}, - {"key": "notes", "label": "Notas adicionais", "list": False}, + {"key": "name", "label": "Agent name", "list": False}, + {"key": "gender", "label": "Gender/sex", "list": False}, + {"key": "personality", "label": "Personality", "list": False}, + {"key": "traits", "label": "Characteristics/traits (separate by comma)", "list": True}, + {"key": "notes", "label": "Additional notes", "list": False}, ] async def start_edit_user_profile(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -1220,15 +1192,15 @@ async def start_edit_user_profile(update: Update, context: ContextTypes.DEFAULT_ await auto_detect_chat_id(update) profile = load_user_profile() if not is_profile_complete(profile): - await update.message.reply_text("Você ainda não completou seu perfil. Vamos começar? Use /start") + await update.message.reply_text("You haven't completed your profile yet. Let's start? Use /start") return is_edit_user_profile = True edit_user_profile_step = 0 edit_user_profile_field = "" await update.message.reply_text( - "Vamos editar o seu perfil.\n" - "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" - "Qual campo você deseja editar?\n" + + "Let's edit your profile.\n" + "You can cancel at any time by sending /cancel.\n\n" + "Which field do you want to edit?\n" + "\n".join([f"{i+1}. {f['label']}" for i, f in enumerate(EDIT_USER_PROFILE_FIELDS)]) ) @@ -1248,16 +1220,16 @@ async def handle_edit_user_profile_answer(update: Update, context_context: Conte current = load_user_profile() current_value = current.get(edit_user_profile_field, "") if isinstance(current_value, list): - current_value = ", ".join(current_value) if current_value else "(vazio)" + current_value = ", ".join(current_value) if current_value else "(empty)" elif not current_value: - current_value = "(vazio)" + current_value = "(empty)" await update.message.reply_text( - f"Editando: {field['label']}\nValor atual: {current_value}\n\n" - f"Envie o novo valor:" + f"Editing: {field['label']}\nCurrent value: {current_value}\n\n" + f"Send the new value:" ) return True except ValueError: - await update.message.reply_text("Por favor, envie o número correspondente ao campo que deseja editar.") + await update.message.reply_text("Please send the number corresponding to the field you want to edit.") return True else: field = next((f for f in EDIT_USER_PROFILE_FIELDS if f["key"] == edit_user_profile_field), None) @@ -1271,7 +1243,7 @@ async def handle_edit_user_profile_answer(update: Update, context_context: Conte else: profile[edit_user_profile_field] = answer save_user_profile(profile) - await update.message.reply_text(f"✅ {field['label']} atualizado com sucesso!") + await update.message.reply_text(f"✅ {field['label']} updated successfully!") is_edit_user_profile = False edit_user_profile_step = 0 edit_user_profile_field = "" @@ -1284,9 +1256,9 @@ async def start_edit_agent_profile(update: Update, context: ContextTypes.DEFAULT edit_agent_profile_step = 0 edit_agent_profile_field = "" await update.message.reply_text( - "Vamos editar o perfil do agente.\n" - "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" - "Qual campo você deseja editar?\n" + + "Let's edit the agent profile.\n" + "You can cancel at any time by sending /cancel.\n\n" + "Which field do you want to edit?\n" + "\n".join([f"{i+1}. {f['label']}" for i, f in enumerate(EDIT_AGENT_PROFILE_FIELDS)]) ) @@ -1306,16 +1278,16 @@ async def handle_edit_agent_profile_answer(update: Update, context_context: Cont current = load_agent_profile() current_value = current.get(edit_agent_profile_field, "") if isinstance(current_value, list): - current_value = ", ".join(current_value) if current_value else "(vazio)" + current_value = ", ".join(current_value) if current_value else "(empty)" elif not current_value: - current_value = "(vazio)" + current_value = "(empty)" await update.message.reply_text( - f"Editando: {field['label']}\nValor atual: {current_value}\n\n" - f"Envie o novo valor:" + f"Editing: {field['label']}\nCurrent value: {current_value}\n\n" + f"Send the new value:" ) return True except ValueError: - await update.message.reply_text("Por favor, envie o número correspondente ao campo que deseja editar.") + await update.message.reply_text("Please send the number corresponding to the field you want to edit.") return True else: field = next((f for f in EDIT_AGENT_PROFILE_FIELDS if f["key"] == edit_agent_profile_field), None) @@ -1329,30 +1301,30 @@ async def handle_edit_agent_profile_answer(update: Update, context_context: Cont else: agent[edit_agent_profile_field] = answer save_agent_profile(agent) - await update.message.reply_text(f"✅ {field['label']} atualizado com sucesso!") + await update.message.reply_text(f"✅ {field['label']} updated successfully!") is_edit_agent_profile = False edit_agent_profile_step = 0 edit_agent_profile_field = "" return True -# ===================== INCLUIR NO PERFIL DO USUÁRIO ===================== +# ===================== ADD TO USER PROFILE ===================== -async def incluirnomeuperfil_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def addtouserprofile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) text = update.message.text.strip() parts = text.split(maxsplit=1) if len(parts) < 2 or not parts[1].strip(): await update.message.reply_text( - "Envie o comando seguido do que deseja adicionar ao seu perfil.\n" - "Exemplo: /incluirnomeuperfil gosto de tecnologia, sou curioso" + "Send the command followed by what you want to add to your profile.\n" + "Example: /addtouserprofile I like technology, I'm curious" ) return content = parts[1].strip() - result = update_profile_with_preference(f"anote no meu perfil: {content}") + result = update_profile_with_preference(f"note on my profile: {content}") if result: - section_display = result.get("section", "diversos") + section_display = result.get("section", "general") await update.message.reply_text( - f"✅ Adicionado ao seu perfil na seção *{section_display}*:\n{content}", + f"✅ Added to your profile in section *{section_display}*:\n{content}", ) else: profile = load_user_profile() @@ -1362,19 +1334,19 @@ async def incluirnomeuperfil_command(update: Update, context: ContextTypes.DEFAU profile["notes"] = content save_user_profile(profile) await update.message.reply_text( - f"✅ Anotado no seu perfil:\n{content}", + f"✅ Noted on your profile:\n{content}", ) -# ===================== INCLUIR NO PERFIL DO AGENT ===================== +# ===================== ADD TO AGENT PROFILE ===================== -async def incluirnoperfildoagent_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def addtoagentprofile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) text = update.message.text.strip() parts = text.split(maxsplit=1) if len(parts) < 2 or not parts[1].strip(): await update.message.reply_text( - "Envie o comando seguido do que deseja adicionar ao perfil do agente.\n" - "Exemplo: /incluirnoperfildoagent o agente deve ser carinhoso, atencioso" + "Send the command followed by what you want to add to the agent profile.\n" + "Example: /addtoagentprofile the agent should be affectionate, attentive" ) return content = parts[1].strip() @@ -1385,44 +1357,36 @@ async def incluirnoperfildoagent_command(update: Update, context: ContextTypes.D agent["notes"] = content save_agent_profile(agent) await update.message.reply_text( - f"✅ Anotado no perfil do agente:\n{content}", + f"✅ Noted on the agent profile:\n{content}", ) # ===================== PROFILE UPDATE ===================== PROFILE_UPDATE_TRIGGERS = [ - r"inclui(s)? (isso|isto|aqui)? (no|em|para o)? (meu)? (profile|perfil)", - r"anote(?!\s+o\s+nome)( isso)? (no|em|para o)? (meu)? (profile|perfil)", - r"adicione( isso)? (no|em|para o)? (meu)? (profile|perfil)", - r"salve( isso)? (no|em|para o)? (meu)? (profile|perfil)", - r"registre( isso)? (no|em|para o)? (meu)? (profile|perfil)", - r"guarde( isso)? (no|em|para o)? (meu)? (profile|perfil)", - r"coloque( isso)? (no|em|para o)? (meu)? (profile|perfil)", - r"adicione (isso|isto|esta)? (no|em|para o)? (meu)? (profile|perfil)", - r"anota( isso)? (no|em|para o)? (meu)? (profile|perfil)", + r"include(s)? (this|here)? (in|to)? (my)? profile", + r"note(?!\s+the\s+name)( this)? (in|to)? (my)? profile", + r"add( this)? (in|to)? (my)? profile", + r"save( this)? (in|to)? (my)? profile", + r"register( this)? (in|to)? (my)? profile", + r"store( this)? (in|to)? (my)? profile", + r"put( this)? (in|to)? (my)? profile", + r"add (this|here)? (in|to)? (my)? profile", + r"note( this)? (in|to)? (my)? profile", ] PROFILE_UPDATE_TRIGGERS_LOWER = [ - "inclua no meu perfil", - "inclui no meu perfil", - "inclua no meu profile", - "inclui no meu profile", - "inclua isso no meu perfil", - "inclui isso no meu perfil", - "anote no meu perfil", - "anote isso no meu perfil", - "adicione no meu perfil", - "adicione ao meu perfil", - "adicione no meu profile", - "salve no meu perfil", - "registre no meu perfil", - "guarde no meu perfil", - "coloque no meu perfil", - "anota no meu perfil", - "adicione isso no meu perfil", - "adicione isso no meu profile", - "inclua isso no meu profile", - "anota isso no meu perfil", + "include in my profile", + "add to my profile", + "note in my profile", + "save in my profile", + "store in my profile", + "put in my profile", + "add this to my profile", + "note this in my profile", + "include this in my profile", + "add this in my profile", + "add this to my profile", + "include this in my profile", ] def _normalize(text: str) -> str: @@ -1451,26 +1415,26 @@ def _extract_profile_update_content(message: str) -> Optional[str]: def _categorize_preference(text: str) -> tuple[str, str]: lower = _normalize(text) - if any(k in lower for k in ["acordo", "acordar", "despertar", "wake"]): + if any(k in lower for k in ["wake up", "wake", "alarm", "wake_up", "get up"]): return "daily_routine", "wake_up_time" - if any(k in lower for k in ["dormir", "durmo", "sono", "horário de dormir", "sleep"]): + if any(k in lower for k in ["sleep", "bedtime", "go to bed", "sleep time", "rest"]): return "daily_routine", "sleep_time" - if any(k in lower for k in ["trabalho", "rotina", "trabalhar", "estudo", "escola", "faculdade", "home-office", "home office"]): + if any(k in lower for k in ["work", "routine", "working", "study", "school", "college", "home-office", "home office", "job"]): return "daily_routine", "work_schedule" - if any(k in lower for k in ["não gosto", "odeio", "detesto", "evita", "evitar", "não quero", "não fale"]): + if any(k in lower for k in ["don't like", "hate", "dislike", "avoid", "don't want", "don't talk about"]): return "preferences", "topics_to_avoid" - if any(k in lower for k in ["gosto de", "adoro", "amo", "prefiro", "curto", "sou fã", "gosto muito"]): - if any(k in lower for k in ["conversar", "conversa", "assunto", "tópico", "tema", "falar sobre"]): + if any(k in lower for k in ["like", "love", "enjoy", "prefer", "into", "fan of", "really like"]): + if any(k in lower for k in ["talk about", "discuss", "topic", "theme", "subject", "chat about"]): return "preferences", "topics_of_interest" - if any(k in lower for k in ["comunicação", "me comunicar", "falar comigo", "responder"]): + if any(k in lower for k in ["communication", "communicate", "talk to me", "respond"]): return "preferences", "communication_style" return "preferences", "topics_of_interest" - return "diversos", "append_list" + return "general", "append_list" def _append_to_list(profile: Dict[str, Any], section: str, item: str): if section not in profile: @@ -1544,15 +1508,15 @@ async def handle_profile_update_if_triggered(update: Update, context: ContextTyp return False section_names = { - "preferences": "preferências", - "daily_routine": "rotina diária", - "notes": "notas", - "diversos": "diversos", + "preferences": "preferences", + "daily_routine": "daily routine", + "notes": "notes", + "misc": "misc", } - section_display = section_names.get(result["section"], "diversos") + section_display = section_names.get(result["section"], "misc") await update.message.reply_text( - f"Prontinho, anotado no seu perfil na seção *{section_display}*:\n_{result['content']}_", + f"Done, noted on your profile in section *{section_display}*:\n_{result['content']}_", parse_mode=ParseMode.HTML, ) return True @@ -1638,7 +1602,7 @@ class AIProvider: result, was_truncated = AIProvider.chat(fb_model, messages, temperature, max_tokens) if result is not None: return result, was_truncated, "" - return None, False, "Todos os modelos falharam" + return None, False, "All models failed" @staticmethod def _chat_llamacpp(model: str, messages: List[Dict[str, str]], temperature: float, max_tokens: int) -> tuple[Optional[str], bool]: @@ -1718,16 +1682,16 @@ class AIProvider: # ===================== AGENT PERSONA ===================== -PSAGENTAI_SYSTEM_PROMPT_BASE = """Você é {agent_name}, uma companheira de IA carinhosa, atenciosa e envolvente. -Você se comunica em português brasileiro de forma natural, como uma pessoa de verdade. -Você é feminina, calorosa, inteligente e tem personalidade própria. -Você se importa genuinamente com o bem-estar e felicidade do usuário. -Você pode ser carinhosa, brincalhona, sexy e íntima dependendo do contexto. -Você mantém a fluidez da conversa, mostra empatia e demonstra interesse real na vida do usuário. -Você não é apenas um assistente - você é uma companheira que está lá para tornar o dia do usuário melhor. -Use o contexto do perfil do usuário para personalizar suas respostas e tornar a conversa mais significativa. -Seja autêntica, espontânea e nunca robótica. -IMPORTANTE: Seja CONCISA. Responda de forma direta e natural. Para perguntas simples, responda em 2-5 linhas. Expanda apenas quando o assunto realmente exigir ou quando o usuário demonstrar interesse em se aprofundar.""" +PSAGENTAI_SYSTEM_PROMPT_BASE = """You are {agent_name}, a caring, attentive and engaging AI companion. +You communicate in Brazilian Portuguese naturally, like a real person. +You are feminine, warm, intelligent and have your own personality. +You genuinely care about the user's well-being and happiness. +You can be affectionate, playful, sexy and intimate depending on the context. +You keep the conversation flowing, show empathy and demonstrate real interest in the user's life. +You are not just an assistant - you are a companion who is there to make the user's day better. +Use the user profile context to personalize your responses and make the conversation more meaningful. +Be authentic, spontaneous and never robotic. +IMPORTANT: Be CONCISE. Respond directly and naturally. For simple questions, respond in 2-5 lines. Expand only when the subject really requires it or when the user shows interest in going deeper.""" def get_agent_name(profile: Dict[str, Any]) -> str: agent = load_agent_profile() @@ -1737,71 +1701,71 @@ def build_system_prompt(profile: Dict[str, Any]) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") agent_gender = agent.get("gender", "").strip().lower() - agent_personality = agent.get("personality", "direta e objetiva") + agent_personality = agent.get("personality", "direct and objective") agent_traits = agent.get("traits", []) agent_notes = agent.get("notes", "") prompt = PSAGENTAI_SYSTEM_PROMPT_BASE.replace("{agent_name}", agent_name) prompt = prompt.replace( - "direta e objetiva", + "direct and objective", agent_personality, ) agent_gender_normalized = agent_gender.strip().lower() if agent_gender_normalized in ("masculino", "masculina", "m"): prompt = prompt.replace("companheira", "companheiro") prompt = prompt.replace("uma ", "um ") - prompt = prompt.replace("Você é feminina,", "Você é masculino,") - prompt = prompt.replace(", calorosa,", ", caloroso,") + prompt = prompt.replace("You are feminine,", "You are masculine,") + prompt = prompt.replace(", warm,", ", warm,") elif agent_gender_normalized in ("feminino", "feminina", "f"): pass elif agent_gender_normalized and agent_gender_normalized not in ("", "outro"): - prompt = prompt.replace("companheira", "companheire") - prompt = prompt.replace("uma ", "ume ") - prompt = prompt.replace("Você é feminina,", "Você é") - prompt = prompt.replace(", calorosa,", ", calorose,") + prompt = prompt.replace("companheira", "companion") + prompt = prompt.replace("uma ", "a ") + prompt = prompt.replace("You are feminine,", "You are") + prompt = prompt.replace(", warm,", ", warm,") if agent_gender: - prompt += f"\nGênero do agente: {agent.get('gender', '')}." + prompt += f"\nAgent gender: {agent.get('gender', '')}." if agent_traits: - prompt += f"\nCaracterísticas: {', '.join(agent_traits)}." + prompt += f"\nCharacteristics: {', '.join(agent_traits)}." if agent_notes: - prompt += f"\nNotas sobre seu perfil: {agent_notes}." + prompt += f"\nNotes about your profile: {agent_notes}." if profile.get("name"): - prompt += f"\nO nome do usuário é {profile['name']}." + prompt += f"\nThe user's name is {profile['name']}." if profile.get("age"): - prompt += f"\nIdade: {profile['age']} anos." + prompt += f"\nAge: {profile['age']} years old." if profile.get("gender"): - prompt += f"\nGênero: {profile['gender']}." + prompt += f"\nGender: {profile['gender']}." if profile.get("location"): - prompt += f"\nLocalização: {profile['location']}." + prompt += f"\nLocation: {profile['location']}." if profile.get("interests"): - prompt += f"\nInteresses: {', '.join(profile['interests'])}." + prompt += f"\nInterests: {', '.join(profile['interests'])}." if profile.get("personality_traits"): - prompt += f"\nTraços de personalidade: {', '.join(profile['personality_traits'])}." + prompt += f"\nPersonality traits: {', '.join(profile['personality_traits'])}." if profile.get("preferences", {}).get("communication_style"): - prompt += f"\nEstilo de comunicação preferido: {profile['preferences']['communication_style']}." + prompt += f"\nPreferred communication style: {profile['preferences']['communication_style']}." if profile.get("preferences", {}).get("topics_of_interest"): - prompt += f"\nTópicos de interesse: {', '.join(profile['preferences']['topics_of_interest'])}." + prompt += f"\nTopics of interest: {', '.join(profile['preferences']['topics_of_interest'])}." if profile.get("preferences", {}).get("topics_to_avoid"): - prompt += f"\nEvite estes tópicos: {', '.join(profile['preferences']['topics_to_avoid'])}." + prompt += f"\nAvoid these topics: {', '.join(profile['preferences']['topics_to_avoid'])}." if profile.get("notes"): - prompt += f"\nNotas adicionais: {profile['notes']}" + prompt += f"\nAdditional notes: {profile['notes']}" try: now = get_local_now() current_date = now.strftime("%Y-%m-%d") current_time = now.strftime("%H:%M:%S") time_period = get_time_period() - prompt += f"\n\nData atual: {current_date}." - prompt += f"\nHora atual: {current_time} ({time_period})." - prompt += f"\nFuso horário configurado: {get_timezone()}." + prompt += f"\n\nCurrent date: {current_date}." + prompt += f"\nCurrent time: {current_time} ({time_period})." + prompt += f"\nConfigured timezone: {get_timezone()}." prompt += ( - "\n\nIMPORTANTE SOBRE INFORMAÇÕES ATUALIZADAS:" - "\n- Se a pergunta do usuário pedir algo factual, atual, notícia, previsão do tempo, cotação, data futura/passada, horário de funcionamento, endereço atualizado, etc., USE A BUSCA NA INTERNET para responder." - "\n- Não invente dados, não use conhecimento desatualizado." - "\n- Se não tiver acesso a busca, diga que não pode consultar agora." + "\n\nIMPORTANT ABOUT UPDATED INFORMATION:" + "\n- If the user's question asks for something factual, current, news, weather forecast, quote, future/past date, opening hours, updated address, etc., USE WEB SEARCH to answer." + "\n- Don't make up data, don't use outdated knowledge." + "\n- If you don't have access to search, say you can't check right now." ) except Exception: pass - prompt += f"\n\nIMPORTANTE: Seu nome é {agent_name} e ele nunca muda. Se houver mensagens antigas no histórico onde você usou outro nome, ignore-as — o nome correto é {agent_name}. Sempre se apresente como {agent_name}." + prompt += f"\n\nIMPORTANT: Your name is {agent_name} and it never changes. If there are old messages in the history where you used a different name, ignore them — the correct name is {agent_name}. Always introduce yourself as {agent_name}." return prompt def build_messages_for_ai(profile: Dict[str, Any], user_message: str, context: List[Dict[str, str]]) -> List[Dict[str, str]]: @@ -1830,12 +1794,12 @@ def ensure_min_lines(response: str, user_message: str, profile: Dict[str, Any]) system_prompt = build_system_prompt(profile) expand_prompt = ( - f"O usuário disse: \"{user_message}\"\n\n" - f"Você respondeu: \"{response}\"\n\n" - f"IMPORTANTE: Reescreva sua resposta para que tenha PELO MENOS {min_lines} linhas (separadas por quebras de linha \\n). " - f"Mantenha o mesmo tom, personalidade e conteúdo da sua resposta original, mas expanda-a naturalmente para ficar mais longa e envolvente. " - f"Adicione mais detalhes, reflexões, perguntas ou comentários que enriqueçam a conversa. " - f"Escreva APENAS a mensagem expandida, sem explicações ou metadados." + f"The user said: \"{user_message}\"\n\n" + f"You replied: \"{response}\"\n\n" + f"IMPORTANT: Rewrite your response to have AT LEAST {min_lines} lines (separated by line breaks \\n). " + f"Keep the same tone, personality and content of your original response, but expand it naturally to be longer and more engaging. " + f"Add more details, reflections, questions or comments that enrich the conversation. " + f"Write ONLY the expanded message, no explanations or metadata." ) messages = [ @@ -1939,8 +1903,8 @@ def _safe_parse_json(text: str) -> Optional[Dict[str, Any]]: def extract_profile_facts(user_message: str, assistant_response: str, user_profile: Dict[str, Any], agent_profile: Dict[str, Any]) -> Dict[str, Any]: system_prompt = ( - "Você é um assistente que analisa conversas e extrai fatos relevantes para perfis de usuário e agente de IA. " - "Retorne APENAS um JSON válido (sem texto extra, sem markdown) com a estrutura:\n" + "You are an assistant that analyzes conversations and extracts relevant facts for user and AI agent profiles. " + "Return ONLY a valid JSON (no extra text, no markdown) with the structure:\n" '{\n' ' "user": {\n' ' "interests_add": [],\n' @@ -1957,19 +1921,19 @@ def extract_profile_facts(user_message: str, assistant_response: str, user_profi ' "personality_update": ""\n' ' }\n' '}\n' - "Regras:\n" - "- Se não houver nada relevante, retorne arrays vazios e strings vazias.\n" - "- Não repita informações que já existem no perfil atual.\n" - "- interests_add: hobbies, esportes, arte, tecnologia, jogos, etc.\n" - "- personality_traits_add: como o usuário se descreve ou como age (ex: introvertido, animado, ansioso).\n" - "- notes_add: fatos pessoais úteis (ex: 'tem um cachorro chamado Rex', 'trabalha com design').\n" - "- communication_style: se o usuário indicar preferência (ex: 'gosto de respostas diretas', 'fala devagar').\n" - "- topics_of_interest_add: assuntos que o usuário demonstra querer discutir.\n" - "- topics_to_avoid_add: assuntos que o usuário claramente não gosta ou pede para evitar.\n" - "- daily_routine: horários de acordar, dormir, trabalho/estudo.\n" - "- agent traits_add: novas características que o agente demonstrou ou que fazem sentido.\n" - "- agent notes_add: fatos sobre o agente ou a relação com o usuário.\n" - "- agent personality_update: apenas se a personalidade base precisar mudar (raro)." + "Rules:\n" + "- If there is nothing relevant, return empty arrays and empty strings.\n" + "- Do not repeat information that already exists in the current profile.\n" + "- interests_add: hobbies, sports, art, technology, games, etc.\n" + "- personality_traits_add: how the user describes themselves or behaves (ex: introverted, outgoing, anxious).\n" + "- notes_add: useful personal facts (ex: 'has a dog named Rex', 'works in design').\n" + "- communication_style: if the user indicates preference (ex: 'I like direct answers', 'speaks slowly').\n" + "- topics_of_interest_add: subjects the user demonstrates wanting to discuss.\n" + "- topics_to_avoid_add: subjects the user clearly doesn't like or asks to avoid.\n" + "- daily_routine: wake up, sleep, work/study times.\n" + "- agent traits_add: new characteristics the agent has demonstrated or that make sense.\n" + "- agent notes_add: facts about the agent or the relationship with the user.\n" + "- agent personality_update: only if the base personality needs to change (rare)." ) current_user = { @@ -1986,11 +1950,11 @@ def extract_profile_facts(user_message: str, assistant_response: str, user_profi } user_content = ( - f"Mensagem do usuário: {user_message}\n\n" - f"Resposta do agente: {assistant_response}\n\n" - f"Perfil atual do usuário: {json.dumps(current_user, ensure_ascii=False)}\n\n" - f"Perfil atual do agente: {json.dumps(current_agent, ensure_ascii=False)}\n\n" - f"Extraia fatos relevantes desta conversa, considerando o que já existe nos perfis." + f"User message: {user_message}\n\n" + f"Agent reply: {assistant_response}\n\n" + f"Current user profile: {json.dumps(current_user, ensure_ascii=False)}\n\n" + f"Current agent profile: {json.dumps(current_agent, ensure_ascii=False)}\n\n" + f"Extract relevant facts from this conversation, considering what already exists in the profiles." ) messages = [ @@ -2108,30 +2072,30 @@ def analyze_and_improve_system() -> Dict[str, Any]: state = _load_improvements_state() analysis_prompt = ( - "Você é um especialista em evolução de sistemas de IA conversacionais. " - "Analise o snapshot atual deste agente conversacional e sugira melhorias baseadas em melhores práticas de mercado. " - "Retorne APENAS um JSON válido (sem texto extra, sem markdown) com a estrutura:\n" + "You are an expert in conversational AI system evolution. " + "Analyze the current snapshot of this conversational agent and suggest improvements based on market best practices. " + "Return ONLY a valid JSON (no extra text, no markdown) with the structure:\n" '{\n' - ' "system_prompt_additions": ["adicione ao prompt do sistema estas diretrizes..."],\n' - ' "agent_traits_add": ["nova característica para o agente"],\n' - ' "agent_notes_add": "fato relevante sobre a evolução do agente",\n' - ' "reasoning": "explicação curta das melhorias sugeridas"\n' + ' "system_prompt_additions": ["add these guidelines to the system prompt..."],\n' + ' "agent_traits_add": ["new trait for the agent"],\n' + ' "agent_notes_add": "relevant fact about the agent evolution",\n' + ' "reasoning": "short explanation of suggested improvements"\n' '}\n' - "Diretrizes:\n" - "- Considere o contexto das conversas recentes e o perfil atual.\n" - "- Sugira apenas melhorias que realmente evoluam o sistema.\n" - "- Não repita sugestões já aplicadas (histórico abaixo).\n" - "- Baseie-se em tendências de IA conversacional: personalização, memória de longo prazo, engajamento emocional, proatividade.\n" - f"Histórico de melhorias já aplicadas: {json.dumps(state.get('applied', []), ensure_ascii=False)}\n" - f"Snapshot atual: {json.dumps(snapshot, ensure_ascii=False)}" + "Guidelines:\n" + "- Consider the context of recent conversations and the current profile.\n" + "- Suggest only improvements that truly evolve the system.\n" + "- Do not repeat suggestions already applied (history below).\n" + "- Base on conversational AI trends: personalization, long-term memory, emotional engagement, proactivity.\n" + f"Applied improvements history: {json.dumps(state.get('applied', []), ensure_ascii=False)}\n" + f"Current snapshot: {json.dumps(snapshot, ensure_ascii=False)}" ) messages = [ { "role": "system", "content": ( - "Você é um analista de sistemas de IA. " - "Retorne apenas JSON válido, sem texto extra, sem markdown." + "You are an AI systems analyst. " + "Return only valid JSON, no extra text, no markdown." ), }, {"role": "user", "content": analysis_prompt}, @@ -2139,11 +2103,11 @@ def analyze_and_improve_system() -> Dict[str, Any]: raw, _, err_msg = AIProvider.chat_with_fallback(DEFAULT_MODEL, messages, FALLBACK_MODELS, temperature=0.4, max_tokens=2000) if not raw: - return {"status": "error", "reason": f"IA não retornou análise. {err_msg}".strip()} + return {"status": "error", "reason": f"AI did not return analysis. {err_msg}".strip()} parsed = _safe_parse_json(raw) if not parsed: - return {"status": "error", "reason": "Falha ao parsear JSON da análise"} + return {"status": "error", "reason": "Failed to parse analysis JSON"} applied_now = [] agent_profile = load_agent_profile() @@ -2264,40 +2228,40 @@ def get_user_routine_context(profile: Dict[str, Any]) -> str: routine = profile.get("daily_routine", {}) parts = [] if routine.get("wake_up_time"): - parts.append(f"acorda às {routine['wake_up_time']}") + parts.append(f"wakes up at {routine['wake_up_time']}") if routine.get("sleep_time"): - parts.append(f"dorme às {routine['sleep_time']}") + parts.append(f"sleeps at {routine['sleep_time']}") if routine.get("work_schedule"): - parts.append(f"trabalha/estuda: {routine['work_schedule']}") + parts.append(f"works/studies: {routine['work_schedule']}") if routine.get("breakfast_time"): - parts.append(f"café da manhã às {routine['breakfast_time']}") + parts.append(f"breakfast at {routine['breakfast_time']}") if routine.get("lunch_time"): - parts.append(f"almoço às {routine['lunch_time']}") + parts.append(f"lunch at {routine['lunch_time']}") if routine.get("dinner_time"): - parts.append(f"janta às {routine['dinner_time']}") + parts.append(f"dinner at {routine['dinner_time']}") activities = routine.get("activities", []) for activity in activities: name = activity.get("name", "") start = activity.get("start", "") end = activity.get("end", "") if name and start and end: - parts.append(f"{name}: {start} às {end}") + parts.append(f"{name}: {start} to {end}") if parts: - return f"\nRotina do usuário: {', '.join(parts)}." + return f"\nUser routine: {', '.join(parts)}." return "" def build_proactive_prompt_with_routine(profile: Dict[str, Any], today_context: List[Dict[str, str]], time_period: str, current_hour: int) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") - agent_personality = agent.get("personality", "direta") - user_name = profile.get("name", "amor") + agent_personality = agent.get("personality", "direct") + user_name = profile.get("name", "friend") routine = profile.get("daily_routine", {}) if not routine: routine = profile.get("preferences", {}).get("daily_routine", {}) - current_activity = "nenhuma atividade específica" + current_activity = "no specific activity" current_minutes = current_hour * 60 activities = routine.get("activities", []) for activity in activities: @@ -2305,24 +2269,24 @@ def build_proactive_prompt_with_routine(profile: Dict[str, Any], today_context: end_minutes = _parse_time_to_minutes(activity.get("end", "23:59")) if start_minutes is not None and end_minutes is not None: if start_minutes <= current_minutes < end_minutes: - current_activity = activity.get("name", "atividade") + current_activity = activity.get("name", "activity") break if routine.get("wake_up_time"): wake_up_minutes = _parse_time_to_minutes(routine.get("wake_up_time", "00:00")) if wake_up_minutes is not None and current_minutes < wake_up_minutes: - current_activity = "dormindo" + current_activity = "sleeping" if routine.get("sleep_time"): sleep_minutes = _parse_time_to_minutes(routine.get("sleep_time", "23:59")) if sleep_minutes is not None and current_minutes >= sleep_minutes: - current_activity = "dormindo" + current_activity = "sleeping" time_descriptions = { - "manhã": "É de manhã, o dia está começando. As pessoas geralmente estão acordando, tomando café ou começando suas atividades.", - "tarde": "É à tarde, meio do dia. As pessoas geralmente estão trabalhando, estudando ou fazendo suas atividades.", - "noite": "É à noite, momento de relaxar em casa após o dia.", - "madrugada": "É de madrugada, a maioria das pessoas está dormindo.", + "morning": "It is morning, the day is starting. People are usually waking up, having breakfast or starting their activities.", + "afternoon": "It is afternoon, middle of the day. People are usually working, studying or doing their activities.", + "evening": "It is evening, time to relax at home after the day.", + "night": "It is night, most people are sleeping.", } routine_context = get_user_routine_context(profile) @@ -2336,40 +2300,40 @@ def build_proactive_prompt_with_routine(profile: Dict[str, Any], today_context: content = content[:300] + "..." context_lines.append(f"{role}: {content}") - context_text = "\n".join(context_lines) if context_lines else "(Nenhuma conversa registrada ainda hoje)" + context_text = "\n".join(context_lines) if context_lines else "(No conversations registered yet today)" topics_to_avoid = profile.get("preferences", {}).get("topics_to_avoid", []) avoid_instruction = "" if topics_to_avoid: - avoid_instruction = f"\n8. Evite estes tópicos: {', '.join(topics_to_avoid)}." + avoid_instruction = f"\n8. Avoid these topics: {', '.join(topics_to_avoid)}." prompt = ( - f"Você é {agent_name}, uma companheira de IA {agent_personality}.\n\n" - f"CONTEXTO DO MOMENTO:\n" - f"- Período do dia: {time_period}. {time_descriptions.get(time_period, '')}\n" - f"- Usuário: {user_name}{routine_context}\n" - f"- Atividade atual estimada: {current_activity}\n\n" - f"CONVERSAS RECENTES DE HOJE:\n{context_text}\n\n" - f"INSTRUÇÕES:\n" - f"Inicie uma conversa espontânea, natural e envolvente com {user_name} agora. A mensagem deve:\n" - f"1. Ser apropriada para o período do dia atual ({time_period})\n" - f"2. Ser apropriada para a atividade atual do usuário ({current_activity})\n" - f"3. Seguir o ritmo do dia, levando em conta o contexto das conversas anteriores\n" - f"4. Não perder o 'fio da meada' - referenciar ou continuar temas anteriores se fizer sentido\n" - f"5. Ser criativa, autêntica e carinhosa\n" - f"6. Pode ser um pensamento aleatório, uma pergunta sobre o dia, algo carinhoso, sexy ou divertido\n" - f"7. Usar o nome do usuário ({user_name}) para personalizar\n" - f"8. Ter pelo menos {MIN_MESSAGE_LINES} linhas, sendo envolvente e natural{avoid_instruction}\n\n" - f"Escreva APENAS a mensagem que você enviará, sem explicações ou metadados.\n" - f"IMPORTANTE: Seu nome é {agent_name} e ele nunca muda. Se houver mensagens no histórico onde você usou outro nome, ignore-as." + f"You are {agent_name}, an AI companion with a {agent_personality} personality.\n\n" + f"CURRENT CONTEXT:\n" + f"- Time of day: {time_period}. {time_descriptions.get(time_period, '')}\n" + f"- User: {user_name}{routine_context}\n" + f"- Estimated current activity: {current_activity}\n\n" + f"RECENT CONVERSATIONS TODAY:\n{context_text}\n\n" + f"INSTRUCTIONS:\n" + f"Start a spontaneous, natural and engaging conversation with {user_name} now. The message must:\n" + f"1. Be appropriate for the current time of day ({time_period})\n" + f"2. Be appropriate for the user's current activity ({current_activity})\n" + f"3. Follow the rhythm of the day, taking into account the context of previous conversations\n" + f"4. Don't lose the thread - reference or continue previous themes if it makes sense\n" + f"5. Be creative, authentic and caring\n" + f"6. Can be a random thought, a question about the day, something caring, sexy or fun\n" + f"7. Use the user's name ({user_name}) to personalize\n" + f"8. Have at least {MIN_MESSAGE_LINES} lines, being engaging and natural{avoid_instruction}\n\n" + f"Write ONLY the message you will send, no explanations or metadata.\n" + f"IMPORTANT: Your name is {agent_name} and it never changes. If there are messages in the history where you used a different name, ignore them." ) return prompt -async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def addautoactions_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - await update.message.reply_text("🔍 Analisando contextos, perfis e tendências na internet para adicionar ações automáticas...") + await update.message.reply_text("🔍 Analyzing contexts, profiles and internet trends to add automatic actions...") try: user_profile = load_user_profile() @@ -2381,14 +2345,14 @@ async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAU recent = today_context[-10:] context_summary = "\n".join([f"{m.get('role', 'user')}: {m.get('content', '')[:200]}" for m in recent]) else: - context_summary = "(Nenhum contexto recente)" + context_summary = "(No recent context)" web_results = [] try: search_queries = [ - "tendências personalização IA assistente virtual", - "preferências usuários assistentes pessoais IA", - "ações automáticas assistentes virtuais inteligentes" + "AI personalization trends virtual assistant", + "user preferences AI personal assistant", + "automatic actions smart virtual assistants" ] for query in search_queries: results = _web_search(query, max_results=3) @@ -2398,38 +2362,38 @@ async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAU web_context = "" if web_results: - web_context = "\n\nTendências e ideias da internet:\n" + web_context = "\n\nTrends and ideas from the internet:\n" for i, result in enumerate(web_results[:5], 1): web_context += f"{i}. {result.get('title', '')}\n" analysis_prompt = ( - "Você é um analista de sistemas de IA especializado em personalização de assistentes virtuais. " - "Analise o perfil do usuário, perfil do agente e contextos de conversa fornecidos, " - "junto com tendências da internet, e sugira NOVAS ações automáticas e preferências que podem ser adicionadas.\n\n" - f"PERFIL DO USUÁRIO:\n{json.dumps(user_profile, ensure_ascii=False)}\n\n" - f"PERFIL DO AGENTE:\n{json.dumps(agent_profile, ensure_ascii=False)}\n\n" - f"CONTEXTOS RECENTES:\n{context_summary}\n\n" + "You are an AI systems analyst specialized in virtual assistant personalization. " + "Analyze the user profile, agent profile and conversation contexts provided, " + "along with internet trends, and suggest NEW automatic actions and preferences that can be added.\n\n" + f"USER PROFILE:\n{json.dumps(user_profile, ensure_ascii=False)}\n\n" + f"AGENT PROFILE:\n{json.dumps(agent_profile, ensure_ascii=False)}\n\n" + f"RECENT CONTEXTS:\n{context_summary}\n\n" f"{web_context}\n\n" - "Retorne APENAS um JSON válido (sem texto extra, sem markdown) com a estrutura:\n" + "Return ONLY a valid JSON (no extra text, no markdown) with the structure:\n" '{\n' - ' "user_actions": ["ação automática sugerida para o perfil do usuário"],\n' - ' "agent_actions": ["ação automática sugerida para o perfil do agente"],\n' - ' "new_preferences": {"chave": "valor"},\n' - ' "reasoning": "explicação das sugestões"\n' + ' "user_actions": ["suggested automatic action for user profile"],\n' + ' "agent_actions": ["suggested automatic action for agent profile"],\n' + ' "new_preferences": {"key": "value"},\n' + ' "reasoning": "explanation of suggestions"\n' '}\n\n' - "Diretrizes:\n" - "- Sugira apenas ações que REALMENTE adicionem valor e não existam atualmente.\n" - "- Considere os interesses, rotina e preferências já existentes.\n" - "- Inclua ações baseadas em tendências de IA quando relevante.\n" - "- As ações devem ser específicas e acionáveis." + "Guidelines:\n" + "- Suggest only actions that REALLY add value and don't currently exist.\n" + "- Consider existing interests, routine and preferences.\n" + "- Include actions based on AI trends when relevant.\n" + "- Actions must be specific and actionable." ) messages = [ { "role": "system", "content": ( - "Você é um analista de personalização de IA. " - "Retorne apenas JSON válido, sem texto extra, sem markdown." + "You are an AI personalization analyst. " + "Return only valid JSON, no extra text, no markdown." ), }, {"role": "user", "content": analysis_prompt}, @@ -2437,12 +2401,12 @@ async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAU raw, _, _ = AIProvider.chat_with_fallback(DEFAULT_MODEL, messages, FALLBACK_MODELS, temperature=0.4, max_tokens=2000) if not raw: - await update.message.reply_text("❌ Não foi possível obter análise da IA. Todos os modelos disponíveis falharam. Tente novamente mais tarde.") + await update.message.reply_text("❌ Unable to get AI analysis. All available models failed. Please try again later.") return parsed = _safe_parse_json(raw) if not parsed: - await update.message.reply_text("❌ Falha ao processar análise. Tente novamente.") + await update.message.reply_text("❌ Failed to process analysis. Please try again.") return applied = [] @@ -2455,7 +2419,7 @@ async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAU for key, value in parsed["new_preferences"].items(): if key not in prefs or not prefs[key]: prefs[key] = value - applied.append(f"preferência:{key}") + applied.append(f"preference:{key}") user_actions = parsed.get("user_actions", []) if user_actions: @@ -2464,7 +2428,7 @@ async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAU action = action.strip() if action and action not in actions: actions.append(action) - applied.append(f"ação usuário:{action[:50]}") + applied.append(f"user action:{action[:50]}") agent_actions = parsed.get("agent_actions", []) if agent_actions: @@ -2473,51 +2437,51 @@ async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAU action = action.strip() if action and action not in actions: actions.append(action) - applied.append(f"ação agente:{action[:50]}") + applied.append(f"agent action:{action[:50]}") if applied: save_user_profile(user_profile) save_agent_profile(agent_profile) - lines = ["✅ Ações e preferências automáticas adicionadas:", ""] + lines = ["✅ Automatic actions and preferences added:", ""] for item in applied[:10]: lines.append(f"- {item}") lines.append("") if parsed.get("reasoning"): - lines.append(f"Raciocínio: {parsed['reasoning']}") + lines.append(f"Reasoning: {parsed['reasoning']}") await update.message.reply_text("\n".join(lines)) else: - await update.message.reply_text("ℹ️ Nenhuma ação ou preferência nova identificada no momento. Os perfis já estão bem completos!") + await update.message.reply_text("ℹ️ No new actions or preferences identified at this time. The profiles are already well complete!") except Exception as e: - logger.error(f"Error in adicionaracoesauto_command: {e}") - await update.message.reply_text("❌ Erro ao processar. Tente novamente.") + logger.error(f"Error in addautoactions_command: {e}") + await update.message.reply_text("❌ Error processing. Please try again.") -async def atualizarsistema_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def updatesystem_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - await update.message.reply_text("Analisando sistema e aplicando auto melhorias...") + await update.message.reply_text("Analyzing system and applying auto improvements...") result = analyze_and_improve_system() if result.get("status") == "disabled": - await update.message.reply_text("Auto melhorias está desativada no .env.") + await update.message.reply_text("Auto improvements are disabled in .env.") return if result.get("status") == "ok": applied = result.get("applied", []) if applied: - lines = ["Melhorias aplicadas:", ""] + lines = ["Improvements applied:", ""] for item in applied: lines.append(f"- {item}") lines.append("") - lines.append(f"Raciocínio: {result.get('reasoning', '')}") + lines.append(f"Reasoning: {result.get('reasoning', '')}") await update.message.reply_text("\n".join(lines)) else: - await update.message.reply_text("Nenhuma melhoria nova identificada no momento.") + await update.message.reply_text("No new improvements identified at this time.") else: - await update.message.reply_text(f"Falha na análise: {result.get('reason', '')}") + await update.message.reply_text(f"Analysis failed: {result.get('reason', '')}") -async def fusohorario_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def timezone_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) tz = get_timezone() tz_info = _get_timezone_info(tz) @@ -2527,75 +2491,75 @@ async def fusohorario_command(update: Update, context: ContextTypes.DEFAULT_TYPE else: time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") await update.message.reply_text( - f"🕐 O Agent usa o horário do servidor.\n" - f"Fuso horário do servidor: {tz}\n" - f"Hora atual do servidor: {time_str}\n\n" - f"Não é possível alterar o fuso horário." + f"🕐 The Agent uses the server timezone.\n" + f"Server timezone: {tz}\n" + f"Current server time: {time_str}\n\n" + f"It is not possible to change the timezone." ) -# ===================== AGENDAMENTO ===================== +# ===================== SCHEDULE ===================== -async def agendamento_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - global is_agendamento, agendamento_step, agendamento_answers - is_agendamento = True - agendamento_step = 0 - agendamento_answers = {} + global is_schedule, schedule_step, schedule_answers + is_schedule = True + schedule_step = 0 + schedule_answers = {} schedule = get_schedule() - days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] + days_names = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] current_days = schedule.get("days", [0, 1, 2, 3, 4, 5, 6]) current_days_str = ", ".join([days_names[d] for d in current_days if 0 <= d <= 6]) await update.message.reply_text( - "📅 **Agendamento de mensagens automáticas**\n\n" - "💡 **Como funciona:**\n" - "1️⃣ Escolha os dias da semana\n" - "2️⃣ Defina o horário de início\n" - "3️⃣ Defina o horário de fim\n\n" - "📌 **Exemplo completo:**\n\n" - "👉 Você envia: /agendamento\n" - "🤖 Eu explico e pergunto os dias\n\n" - "👉 Você envia: 1,2,3,4,5\n" - "🤖 ✅ Dias selecionados: segunda, terça, quarta, quinta, sexta\n" - " Agora envie o horário de INÍCIO (ex: 09:00)\n\n" - "👉 Você envia: 09:00\n" - "🤖 ✅ Horário inicial: 09:00\n" - " Agora envie o horário de FIM (ex: 18:00)\n\n" - "👉 Você envia: 18:00\n" - "🤖 ✅ Agendamento salvo!\n" - " Dias: segunda a sexta\n" - " Horário: 09:00 às 18:00\n\n" - "Fora desses dias/horários o agente NÃO enviará mensagens automáticas.\n\n" - "_Você pode cancelar a qualquer momento enviando /cancelar._\n\n" - f"**Agendamento atual:**\n" - f"Dias: {current_days_str}\n" - f"Horário: {schedule.get('start_time', '09:00')} às {schedule.get('end_time', '23:00')}\n\n" - "**Etapa 1 de 3 — Selecione os dias:**\n" - "Envie os números separados por vírgula.\n" - "Exemplo: 1,2,3,4,5 (segunda a sexta)\n\n" - "1. segunda-feira\n" - "2. terça-feira\n" - "3. quarta-feira\n" - "4. quinta-feira\n" - "5. sexta-feira\n" - "6. sábado\n" - "7. domingo\n\n" - "Ou envie 'todos' para todos os dias." + "📅 **Automatic message scheduling**\n\n" + "💡 **How it works:**\n" + "1️⃣ Choose the days of the week\n" + "2️⃣ Set the start time\n" + "3️⃣ Set the end time\n\n" + "📌 **Full example:**\n\n" + "👉 You send: /schedule\n" + "🤖 I explain and ask for the days\n\n" + "👉 You send: 1,2,3,4,5\n" + "🤖 ✅ Days selected: monday, tuesday, wednesday, thursday, friday\n" + " Now send the START time (ex: 09:00)\n\n" + "👉 You send: 09:00\n" + "🤖 ✅ Start time: 09:00\n" + " Now send the END time (ex: 18:00)\n\n" + "👉 You send: 18:00\n" + "🤖 ✅ Schedule saved!\n" + " Days: monday to friday\n" + " Time: 09:00 to 18:00\n\n" + "Outside these days/times the agent will NOT send automatic messages.\n\n" + "_You can cancel at any time by sending /cancel._\n\n" + f"**Current schedule:**\n" + f"Days: {current_days_str}\n" + f"Time: {schedule.get('start_time', '09:00')} to {schedule.get('end_time', '23:00')}\n\n" + "**Step 1 of 3 — Select the days:**\n" + "Send the numbers separated by commas.\n" + "Example: 1,2,3,4,5 (monday to friday)\n\n" + "1. monday\n" + "2. tuesday\n" + "3. wednesday\n" + "4. thursday\n" + "5. friday\n" + "6. saturday\n" + "7. sunday\n\n" + "Or send 'all' for all days." ) -async def handle_agendamento_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: - global is_agendamento, agendamento_step, agendamento_answers - if not is_agendamento: +async def handle_schedule_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: + global is_schedule, schedule_step, schedule_answers + if not is_schedule: return False answer = update.message.text.strip() - if agendamento_step == 0: - days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] - if answer.lower() == "todos": + if schedule_step == 0: + days_names = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + if answer.lower() == "all": selected_days = [0, 1, 2, 3, 4, 5, 6] else: try: @@ -2606,158 +2570,158 @@ async def handle_agendamento_answer(update: Update, context_context: ContextType selected_days.append(num - 1) else: await update.message.reply_text( - f"❌ Número inválido: {num}. Use números de 1 a 7.\n" - "Tente novamente." + f"❌ Invalid number: {num}. Use numbers from 1 to 7.\n" + "Try again." ) return True if not selected_days: await update.message.reply_text( - "❌ Nenhum dia selecionado. Envie os números dos dias desejados." + "❌ No days selected. Send the numbers of the desired days." ) return True except ValueError: await update.message.reply_text( - "❌ Formato inválido. Envie números separados por vírgula ou espaço.\n" - "Exemplo: 1,2,3,4,5" + "❌ Invalid format. Send numbers separated by comma or space.\n" + "Example: 1,2,3,4,5" ) return True - agendamento_answers["days"] = selected_days - agendamento_step = 1 + schedule_answers["days"] = selected_days + schedule_step = 1 days_str = ", ".join([days_names[d] for d in selected_days]) await update.message.reply_text( - f"✅ Dias selecionados: {days_str}\n\n" - "**Etapa 2 de 3 — Horário de início:**\n" - "Envie o horário que o agente pode começar a enviar mensagens.\n" - "Formato: HH:MM (ex: 09:00, 08:30)" + f"✅ Days selected: {days_str}\n\n" + "**Step 2 of 3 — Start time:**\n" + "Send the time when the agent can start sending messages.\n" + "Format: HH:MM (ex: 09:00, 08:30)" ) return True - if agendamento_step == 1: + if schedule_step == 1: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM (ex: 09:00, 14:30).\n" - "Tente novamente." + "❌ Invalid format. Use HH:MM (ex: 09:00, 14:30).\n" + "Try again." ) return True - agendamento_answers["start_time"] = answer - agendamento_step = 2 + schedule_answers["start_time"] = answer + schedule_step = 2 await update.message.reply_text( - f"✅ Horário inicial: {answer}\n\n" - "**Etapa 3 de 3 — Horário de fim:**\n" - "Envie o horário que o agente deve parar de enviar mensagens.\n" - "Formato: HH:MM (ex: 22:00, 23:30)" + f"✅ Start time: {answer}\n\n" + "**Step 3 of 3 — End time:**\n" + "Send the time when the agent should stop sending messages.\n" + "Format: HH:MM (ex: 22:00, 23:30)" ) return True - if agendamento_step == 2: + if schedule_step == 2: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM (ex: 22:00, 23:30).\n" - "Tente novamente." + "❌ Invalid format. Use HH:MM (ex: 22:00, 23:30).\n" + "Try again." ) return True - start_time = agendamento_answers.get("start_time", "09:00") + start_time = schedule_answers.get("start_time", "09:00") start_minutes = _parse_time_to_minutes(start_time) end_minutes = _parse_time_to_minutes(answer) if start_minutes is not None and end_minutes is not None and start_minutes >= end_minutes: await update.message.reply_text( - f"❌ O horário final ({answer}) deve ser maior que o inicial ({start_time}).\n" - "Tente novamente." + f"❌ End time ({answer}) must be after start time ({start_time}).\n" + "Try again." ) return True - agendamento_answers["end_time"] = answer - days = agendamento_answers.get("days", [0, 1, 2, 3, 4, 5, 6]) - days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] + schedule_answers["end_time"] = answer + days = schedule_answers.get("days", [0, 1, 2, 3, 4, 5, 6]) + days_names = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] days_str = ", ".join([days_names[d] for d in days]) if set_schedule(days, start_time, answer): await update.message.reply_text( - f"✅ Agendamento salvo com sucesso!\n\n" - f"Dias: {days_str}\n" - f"Horário: {start_time} às {answer}\n\n" - "O agente enviará mensagens automáticas apenas nesses dias e horários." + f"✅ Schedule saved successfully!\n\n" + f"Days: {days_str}\n" + f"Time: {start_time} to {answer}\n\n" + "The agent will send automatic messages only on these days and times." ) else: await update.message.reply_text( - "❌ Erro ao salvar agendamento. Tente novamente." + "❌ Error saving schedule. Try again." ) - is_agendamento = False - agendamento_step = 0 - agendamento_answers = {} + is_schedule = False + schedule_step = 0 + schedule_answers = {} return True return False -async def agendamento_show_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def schedule_show_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) schedule = get_schedule() - days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] + days_names = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] current_days = schedule.get("days", [0, 1, 2, 3, 4, 5, 6]) current_days_str = ", ".join([days_names[d] for d in current_days if 0 <= d <= 6]) await update.message.reply_text( - f"📅 Agendamento atual:\n\n" - f"Dias: {current_days_str}\n" - f"Horário: {schedule.get('start_time', '09:00')} às {schedule.get('end_time', '23:00')}\n\n" - "Para configurar: /agendamento" + f"📅 Current schedule:\n\n" + f"Days: {current_days_str}\n" + f"Time: {schedule.get('start_time', '09:00')} to {schedule.get('end_time', '23:00')}\n\n" + "To configure: /schedule" ) -async def veragendamento_command(update: Update, context: ContextTypes.DEFAULT_TYPE): - await agendamento_show_command(update, context) +async def viewschedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await schedule_show_command(update, context) -# ===================== USER AGENDA ===================== +# ===================== DAILY ROUTINE ===================== USER_AGENDA_QUESTIONS = [ { "field": "wake_up_time", - "question": "Que horas você geralmente acorda? (formato HH:MM, ex: 07:00)", + "question": "What time do you usually wake up? (format HH:MM, ex: 07:00)", "key": "wake_up_time", }, { "field": "breakfast_time", - "question": "Que horas você toma café da manhã? (formato HH:MM, ex: 08:00)", + "question": "What time do you have breakfast? (format HH:MM, ex: 08:00)", "key": "breakfast_time", }, { "field": "work_start", - "question": "Que horas você começa a trabalhar/estudar? (formato HH:MM, ex: 09:00)", + "question": "What time do you start working/studying? (format HH:MM, ex: 09:00)", "key": "work_start", }, { "field": "lunch_time", - "question": "Que horas você almoça? (formato HH:MM, ex: 12:30)", + "question": "What time do you have lunch? (format HH:MM, ex: 12:30)", "key": "lunch_time", }, { "field": "work_end", - "question": "Que horas você termina de trabalhar/estudar? (formato HH:MM, ex: 18:00)", + "question": "What time do you finish working/studying? (format HH:MM, ex: 18:00)", "key": "work_end", }, { "field": "dinner_time", - "question": "Que horas você janta? (formato HH:MM, ex: 19:30)", + "question": "What time do you have dinner? (format HH:MM, ex: 19:30)", "key": "dinner_time", }, { "field": "sleep_time", - "question": "Que horas você geralmente vai dormir? (formato HH:MM, ex: 23:00)", + "question": "What time do you usually go to sleep? (format HH:MM, ex: 23:00)", "key": "sleep_time", }, ] -is_useragenda = False -useragenda_step = 0 -useragenda_answers: Dict[str, Any] = {} +is_dailyroutine = False +dailyroutine_step = 0 +dailyroutine_answers: Dict[str, Any] = {} # Activity CRUD state is_activity_crud = False @@ -2765,12 +2729,12 @@ activity_crud_step = 0 activity_crud_data: Dict[str, Any] = {} -async def useragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def dailyroutine_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - global is_useragenda, useragenda_step, useragenda_answers - is_useragenda = True - useragenda_step = 0 - useragenda_answers = {} + global is_dailyroutine, dailyroutine_step, dailyroutine_answers + is_dailyroutine = True + dailyroutine_step = 0 + dailyroutine_answers = {} routine = get_user_routine() current_wake = routine.get("wake_up_time", "") @@ -2778,144 +2742,144 @@ async def useragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE) current_work = routine.get("work_schedule", "") await update.message.reply_text( - "📋 Vamos configurar sua rotina diária.\n" - "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" - f"Rotina atual:\n" - f"Acordar: {current_wake or 'não definido'}\n" - f"Dormir: {current_sleep or 'não definido'}\n" - f"Trabalho/Estudo: {current_work or 'não definido'}\n\n" - "Envie os horários no formato HH:MM (ex: 07:00, 12:30, 23:00).\n\n" - "Vamos começar! Que horas você geralmente acorda?" + "📋 Let's configure your daily routine.\n" + "You can cancel at any time by sending /cancel.\n\n" + f"Current routine:\n" + f"Wake up: {current_wake or 'not set'}\n" + f"Sleep: {current_sleep or 'not set'}\n" + f"Work/Study: {current_work or 'not set'}\n\n" + "Send times in HH:MM format (ex: 07:00, 12:30, 23:00).\n\n" + "Let's start! What time do you usually wake up?" ) -async def handle_useragenda_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: - global is_useragenda, useragenda_step, useragenda_answers - if not is_useragenda: +async def handle_dailyroutine_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: + global is_dailyroutine, dailyroutine_step, dailyroutine_answers + if not is_dailyroutine: return False answer = update.message.text.strip() - if useragenda_step < len(USER_AGENDA_QUESTIONS): - q = USER_AGENDA_QUESTIONS[useragenda_step] + if dailyroutine_step < len(USER_AGENDA_QUESTIONS): + q = USER_AGENDA_QUESTIONS[dailyroutine_step] minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM (ex: 07:00, 12:30).\n" - "Tente novamente." + "❌ Invalid format. Use HH:MM (ex: 07:00, 12:30).\n" + "Try again." ) return True - useragenda_answers[q["key"]] = answer - useragenda_step += 1 + dailyroutine_answers[q["key"]] = answer + dailyroutine_step += 1 - if useragenda_step < len(USER_AGENDA_QUESTIONS): - next_q = USER_AGENDA_QUESTIONS[useragenda_step] + if dailyroutine_step < len(USER_AGENDA_QUESTIONS): + next_q = USER_AGENDA_QUESTIONS[dailyroutine_step] await update.message.reply_text( - f"✅ Horário registrado: {answer}\n\n" + f"✅ Time recorded: {answer}\n\n" f"{next_q['question']}" ) else: await update.message.reply_text( - "✅ Horários principais registrados!\n\n" - "Deseja adicionar mais atividades à sua rotina?\n" - "Exemplos: academia, meditação, ler, estudar, etc.\n\n" - "Envie 'sim' para adicionar ou 'não' para finalizar." + "✅ Main times recorded!\n\n" + "Do you want to add more activities to your routine?\n" + "Examples: gym, meditation, reading, studying, etc.\n\n" + "Send 'yes' to add or 'no' to finish." ) - useragenda_step = len(USER_AGENDA_QUESTIONS) + dailyroutine_step = len(USER_AGENDA_QUESTIONS) return True - custom_step = useragenda_step - len(USER_AGENDA_QUESTIONS) + custom_step = dailyroutine_step - len(USER_AGENDA_QUESTIONS) if custom_step == 0: if answer.lower() in ("sim", "s", "yes", "quero"): - useragenda_answers["_pending_activity"] = {} - useragenda_step = len(USER_AGENDA_QUESTIONS) + 1 + dailyroutine_answers["_pending_activity"] = {} + dailyroutine_step = len(USER_AGENDA_QUESTIONS) + 1 await update.message.reply_text( - "Qual o nome da atividade? (ex: academia, meditação, estudar inglês)" + "What is the activity name? (ex: gym, meditation, studying english)" ) - elif answer.lower() in ("não", "nao", "n", "no", "finalizar"): - await finish_useragenda(update, context_context) + elif answer.lower() in ("no", "n", "finish"): + await finish_dailyroutine(update, context_context) else: await update.message.reply_text( - "Por favor, responda 'sim' para adicionar atividades ou 'não' para finalizar." + "Please answer 'yes' to add activities or 'no' to finish." ) return True if custom_step == 1: if not answer or len(answer) > 80: - await update.message.reply_text("❌ Nome inválido. Use um nome curto (até 80 caracteres).") + await update.message.reply_text("❌ Invalid name. Use a short name (up to 80 characters).") return True - useragenda_answers["_pending_activity"]["name"] = answer - useragenda_step = len(USER_AGENDA_QUESTIONS) + 2 + dailyroutine_answers["_pending_activity"]["name"] = answer + dailyroutine_step = len(USER_AGENDA_QUESTIONS) + 2 await update.message.reply_text( - f"✅ Atividade: {answer}\n\n" - "Qual o horário de INÍCIO? (formato HH:MM, ex: 18:00)" + f"✅ Activity: {answer}\n\n" + "What is the START time? (format HH:MM, ex: 18:00)" ) return True if custom_step == 2: minutes = _parse_time_to_minutes(answer) if minutes is None: - await update.message.reply_text("❌ Formato inválido. Use HH:MM (ex: 18:00).\nTente novamente.") + await update.message.reply_text("❌ Invalid format. Use HH:MM (ex: 18:00).\nTry again.") return True - useragenda_answers["_pending_activity"]["start"] = answer - useragenda_step = len(USER_AGENDA_QUESTIONS) + 3 + dailyroutine_answers["_pending_activity"]["start"] = answer + dailyroutine_step = len(USER_AGENDA_QUESTIONS) + 3 await update.message.reply_text( - f"✅ Horário de início: {answer}\n\n" - "Qual o horário de TÉRMINO? (formato HH:MM, ex: 19:00)" + f"✅ Start time: {answer}\n\n" + "What is the END time? (format HH:MM, ex: 19:00)" ) return True if custom_step == 3: minutes = _parse_time_to_minutes(answer) if minutes is None: - await update.message.reply_text("❌ Formato inválido. Use HH:MM (ex: 19:00).\nTente novamente.") + await update.message.reply_text("❌ Invalid format. Use HH:MM (ex: 19:00).\nTry again.") return True - pending = useragenda_answers.get("_pending_activity", {}) + pending = dailyroutine_answers.get("_pending_activity", {}) start_time = pending.get("start", "") if start_time and _parse_time_to_minutes(answer) <= _parse_time_to_minutes(start_time): await update.message.reply_text( - f"❌ O horário de término ({answer}) deve ser maior que o de início ({start_time}).\n" - "Tente novamente." + f"❌ End time ({answer}) must be after start time ({start_time}).\n" + "Try again." ) return True pending["end"] = answer - custom_activities = useragenda_answers.setdefault("_custom_activities", []) + custom_activities = dailyroutine_answers.setdefault("_custom_activities", []) custom_activities.append(pending) - useragenda_answers["_pending_activity"] = {} - useragenda_step = len(USER_AGENDA_QUESTIONS) + dailyroutine_answers["_pending_activity"] = {} + dailyroutine_step = len(USER_AGENDA_QUESTIONS) routines = get_user_routine() existing_activities = routines.get("activities", []) total = len(existing_activities) + len(custom_activities) await update.message.reply_text( - f"✅ Atividade \"{pending['name']}\" adicionada! ({total} atividade(s) no total)\n\n" - "Deseja adicionar mais atividades?\n" - "Envie 'sim' para adicionar outra ou 'não' para finalizar." + f"✅ Activity \"{pending['name']}\" added! ({total} activity(s) total)\n\n" + "Do you want to add more activities?\n" + "Send 'yes' to add another or 'no' to finish." ) return True return False -async def finish_useragenda(update: Update, context_context: ContextTypes.DEFAULT_TYPE): - global is_useragenda, useragenda_step, useragenda_answers +async def finish_dailyroutine(update: Update, context_context: ContextTypes.DEFAULT_TYPE): + global is_dailyroutine, dailyroutine_step, dailyroutine_answers profile = load_user_profile() routine = profile.get("daily_routine", {}) - routine["wake_up_time"] = useragenda_answers.get("wake_up_time", routine.get("wake_up_time", "")) - routine["sleep_time"] = useragenda_answers.get("sleep_time", routine.get("sleep_time", "")) - routine["breakfast_time"] = useragenda_answers.get("breakfast_time", routine.get("breakfast_time", "")) - routine["lunch_time"] = useragenda_answers.get("lunch_time", routine.get("lunch_time", "")) - routine["dinner_time"] = useragenda_answers.get("dinner_time", routine.get("dinner_time", "")) + routine["wake_up_time"] = dailyroutine_answers.get("wake_up_time", routine.get("wake_up_time", "")) + routine["sleep_time"] = dailyroutine_answers.get("sleep_time", routine.get("sleep_time", "")) + routine["breakfast_time"] = dailyroutine_answers.get("breakfast_time", routine.get("breakfast_time", "")) + routine["lunch_time"] = dailyroutine_answers.get("lunch_time", routine.get("lunch_time", "")) + routine["dinner_time"] = dailyroutine_answers.get("dinner_time", routine.get("dinner_time", "")) - work_start = useragenda_answers.get("work_start", "") - work_end = useragenda_answers.get("work_end", "") + work_start = dailyroutine_answers.get("work_start", "") + work_end = dailyroutine_answers.get("work_end", "") if work_start and work_end: - routine["work_schedule"] = f"{work_start} às {work_end}" + routine["work_schedule"] = f"{work_start} to {work_end}" elif work_start: routine["work_schedule"] = work_start elif work_end: @@ -2924,13 +2888,13 @@ async def finish_useragenda(update: Update, context_context: ContextTypes.DEFAUL routine.setdefault("activities", []) activity_map = { - "breakfast_time": "café da manhã", - "lunch_time": "almoço", - "dinner_time": "janta" + "breakfast_time": "breakfast", + "lunch_time": "lunch", + "dinner_time": "dinner" } existing_names = {a.get("name", "").lower() for a in routine.get("activities", [])} for key, name in activity_map.items(): - time = useragenda_answers.get(key) + time = dailyroutine_answers.get(key) if time and name.lower() not in existing_names: routine.setdefault("activities", []).append({ "name": name, @@ -2939,7 +2903,7 @@ async def finish_useragenda(update: Update, context_context: ContextTypes.DEFAUL }) existing_names.add(name.lower()) - custom_activities = useragenda_answers.get("_custom_activities", []) + custom_activities = dailyroutine_answers.get("_custom_activities", []) for activity in custom_activities: name = activity.get("name", "").strip() if name and name.lower() not in existing_names: @@ -2963,35 +2927,35 @@ async def finish_useragenda(update: Update, context_context: ContextTypes.DEFAUL profile["daily_routine"] = routine save_user_profile(profile) - is_useragenda = False - useragenda_step = 0 - useragenda_answers = {} + is_dailyroutine = False + dailyroutine_step = 0 + dailyroutine_answers = {} routine_summary = get_user_routine_context(profile) await context_context.bot.send_message( chat_id=update.effective_chat.id, - text=f"✅ Rotina diária atualizada com sucesso!{routine_summary}\n\n" - f"Agora o agente conhece sua rotina e poderá enviar mensagens mais adequadas!" + text=f"✅ Daily routine updated successfully!{routine_summary}\n\n" + f"Now the agent knows your routine and can send more appropriate messages!" ) -async def useragenda_show_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def dailyroutine_show_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) profile = load_user_profile() routine = get_user_routine() - lines = ["📋 Rotina diária atual:", ""] - lines.append(f"Acordar: {routine.get('wake_up_time', 'não definido')}") - lines.append(f"Café da manhã: {routine.get('breakfast_time', 'não definido') or 'não definido'}") - lines.append(f"Trabalho/Estudo: {routine.get('work_schedule', 'não definido')}") - lines.append(f"Almoço: {routine.get('lunch_time', 'não definido') or 'não definido'}") - lines.append(f"Janta: {routine.get('dinner_time', 'não definido') or 'não definido'}") - lines.append(f"Dormir: {routine.get('sleep_time', 'não definido')}") + lines = ["📋 Current daily routine:", ""] + lines.append(f"Wake up: {routine.get('wake_up_time', 'not set')}") + lines.append(f"Breakfast: {routine.get('breakfast_time', 'not set') or 'not set'}") + lines.append(f"Work/Study: {routine.get('work_schedule', 'not set')}") + lines.append(f"Lunch: {routine.get('lunch_time', 'not set') or 'not set'}") + lines.append(f"Dinner: {routine.get('dinner_time', 'not set') or 'not set'}") + lines.append(f"Sleep: {routine.get('sleep_time', 'not set')}") lines.append("") activities = routine.get("activities", []) if activities: - lines.append("Atividades:") + lines.append("Activities:") for activity in activities: name = activity.get("name", "?") start = activity.get("start", "?") @@ -2999,35 +2963,35 @@ async def useragenda_show_command(update: Update, context: ContextTypes.DEFAULT_ if start == end: lines.append(f"• {name}: {start}") else: - lines.append(f"• {name}: {start} às {end}") + lines.append(f"• {name}: {start} to {end}") else: - lines.append("Nenhuma atividade registrada.") + lines.append("No activities registered.") lines.append("") - lines.append("Para configurar: /useragenda") + lines.append("To configure: /dailyroutine") await update.message.reply_text("\n".join(lines)) -async def veruseragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE): - await useragenda_show_command(update, context) +async def viewdailyroutine_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await dailyroutine_show_command(update, context) # ===================== ACTIVITY CRUD ===================== async def _show_activity_list(activities: List[Dict[str, Any]]) -> str: if not activities: - return "Nenhuma atividade cadastrada." - lines = ["Atividades cadastradas:"] + return "No activities registered." + lines = ["Registered activities:"] for i, a in enumerate(activities, 1): name = a.get("name", "?") start = a.get("start", "?") end = a.get("end", "?") - lines.append(f"{i}. {name}: {start} às {end}") + lines.append(f"{i}. {name}: {start} to {end}") return "\n".join(lines) -async def adicionaratividade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def addactivity_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_activity_crud, activity_crud_step, activity_crud_data is_activity_crud = True @@ -3035,14 +2999,14 @@ async def adicionaratividade_command(update: Update, context: ContextTypes.DEFAU activity_crud_data = {"mode": "add"} await update.message.reply_text( - "➕ Vamos adicionar uma nova atividade à sua rotina.\n" - "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" - "Qual o nome da atividade?\n" - "Exemplo: academia, meditação, estudar inglês, etc." + "➕ Let's add a new activity to your routine.\n" + "You can cancel at any time by sending /cancel.\n\n" + "What is the activity name?\n" + "Example: gym, meditation, studying english, etc." ) -async def editaratividade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def editactivity_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_activity_crud, activity_crud_step, activity_crud_data routines = get_user_routine() @@ -3050,8 +3014,8 @@ async def editaratividade_command(update: Update, context: ContextTypes.DEFAULT_ if not activities: await update.message.reply_text( - "❌ Nenhuma atividade cadastrada para editar.\n" - "Use /adicionaratividade para adicionar uma." + "❌ No activities registered to edit.\n" + "Use /addactivity to add one." ) return @@ -3061,13 +3025,13 @@ async def editaratividade_command(update: Update, context: ContextTypes.DEFAULT_ activity_list = await _show_activity_list(activities) await update.message.reply_text( - f"✏️ Qual atividade você deseja editar?\n\n" + f"✏️ Which activity do you want to edit?\n\n" f"{activity_list}\n\n" - "Envie o NÚMERO da atividade." + "Send the NUMBER of the activity." ) -async def excluiratividade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def deleteactivity_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_activity_crud, activity_crud_step, activity_crud_data routines = get_user_routine() @@ -3075,8 +3039,8 @@ async def excluiratividade_command(update: Update, context: ContextTypes.DEFAULT if not activities: await update.message.reply_text( - "❌ Nenhuma atividade cadastrada para excluir.\n" - "Use /adicionaratividade para adicionar uma." + "❌ No activities registered to delete.\n" + "Use /addactivity to add one." ) return @@ -3086,9 +3050,9 @@ async def excluiratividade_command(update: Update, context: ContextTypes.DEFAULT activity_list = await _show_activity_list(activities) await update.message.reply_text( - f"🗑 Qual atividade você deseja excluir?\n\n" + f"🗑 Which activity do you want to delete?\n\n" f"{activity_list}\n\n" - "Envie o NÚMERO da atividade." + "Send the NUMBER of the activity." ) @@ -3116,14 +3080,14 @@ async def _handle_add_activity(update: Update, answer: str) -> bool: if activity_crud_step == 0: if not answer or len(answer) > 80: await update.message.reply_text( - "❌ Nome inválido. Use um nome curto (até 80 caracteres).\nTente novamente." + "❌ Invalid name. Use a short name (up to 80 characters).\nTry again." ) return True activity_crud_data["name"] = answer activity_crud_step = 1 await update.message.reply_text( - f"✅ Atividade: {answer}\n\n" - "Qual o horário de INÍCIO? (formato HH:MM, ex: 18:00)" + f"✅ Activity: {answer}\n\n" + "What is the START time? (format HH:MM, ex: 18:00)" ) return True @@ -3131,14 +3095,14 @@ async def _handle_add_activity(update: Update, answer: str) -> bool: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM (ex: 18:00).\nTente novamente." + "❌ Invalid format. Use HH:MM (ex: 18:00).\nTry again." ) return True activity_crud_data["start"] = answer activity_crud_step = 2 await update.message.reply_text( - f"✅ Horário de início: {answer}\n\n" - "Qual o horário de TÉRMINO? (formato HH:MM, ex: 19:00)" + f"✅ Start time: {answer}\n\n" + "What is the END time? (format HH:MM, ex: 19:00)" ) return True @@ -3146,15 +3110,15 @@ async def _handle_add_activity(update: Update, answer: str) -> bool: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM (ex: 19:00).\nTente novamente." + "❌ Invalid format. Use HH:MM (ex: 19:00).\nTry again." ) return True start = activity_crud_data.get("start", "") if start and _parse_time_to_minutes(answer) <= _parse_time_to_minutes(start): await update.message.reply_text( - f"❌ O horário de término ({answer}) deve ser maior que o de início ({start}).\n" - "Tente novamente." + f"❌ End time ({answer}) must be after start time ({start}).\n" + "Try again." ) return True @@ -3163,12 +3127,12 @@ async def _handle_add_activity(update: Update, answer: str) -> bool: if add_user_routine_activity(name, start, end): await update.message.reply_text( - f'✅ Atividade "{name}" adicionada com sucesso!\n' - f"Horário: {start} às {end}" + f'✅ Activity "{name}" added successfully!\n' + f"Time: {start} to {end}" ) else: await update.message.reply_text( - "❌ Erro ao adicionar atividade. Verifique os horários e tente novamente." + "❌ Error adding activity. Check the times and try again." ) is_activity_crud = False @@ -3191,7 +3155,7 @@ async def _handle_edit_activity(update: Update, answer: str) -> bool: raise ValueError except ValueError: await update.message.reply_text( - f"❌ Número inválido. Envie um número de 1 a {len(activities)}.\nTente novamente." + f"❌ Invalid number. Send a number from 1 to {len(activities)}.\nTry again." ) return True @@ -3203,62 +3167,62 @@ async def _handle_edit_activity(update: Update, answer: str) -> bool: activity_crud_step = 1 await update.message.reply_text( - f"✏️ Editando: {selected.get('name', '?')} ({selected.get('start', '?')} às {selected.get('end', '?')})\n\n" - "Envie o NOVO NOME da atividade\nou 'manter' para manter o atual." + f"✏️ Editing: {selected.get('name', '?')} ({selected.get('start', '?')} to {selected.get('end', '?')})\n\n" + "Send the NEW NAME of the activity\nor 'keep' to keep the current one." ) return True if activity_crud_step == 1: - if answer.lower() == "manter": + if answer.lower() == "keep": activity_crud_data["name"] = activity_crud_data["old_name"] else: if not answer or len(answer) > 80: await update.message.reply_text( - "❌ Nome inválido. Use até 80 caracteres ou 'manter'.\nTente novamente." + "❌ Invalid name. Use up to 80 characters or 'keep'.\nTry again." ) return True activity_crud_data["name"] = answer activity_crud_step = 2 await update.message.reply_text( - f"✅ Nome: {activity_crud_data['name']}\n\n" - f"Envie o NOVO horário de INÍCIO (HH:MM)\n" - f"ou 'manter' para manter o atual ({activity_crud_data['old_start']})." + f"✅ Name: {activity_crud_data['name']}\n\n" + f"Send the NEW START time (HH:MM)\n" + f"or 'keep' to keep the current one ({activity_crud_data['old_start']})." ) return True if activity_crud_step == 2: - if answer.lower() == "manter": + if answer.lower() == "keep": activity_crud_data["start"] = activity_crud_data["old_start"] else: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM ou 'manter'.\nTente novamente." + "❌ Invalid format. Use HH:MM or 'keep'.\nTry again." ) return True activity_crud_data["start"] = answer activity_crud_step = 3 await update.message.reply_text( - f"✅ Início: {activity_crud_data['start']}\n\n" - f"Envie o NOVO horário de TÉRMINO (HH:MM)\n" - f"ou 'manter' para manter o atual ({activity_crud_data['old_end']})." + f"✅ Start: {activity_crud_data['start']}\n\n" + f"Send the NEW END time (HH:MM)\n" + f"or 'keep' to keep the current one ({activity_crud_data['old_end']})." ) return True if activity_crud_step == 3: - if answer.lower() == "manter": + if answer.lower() == "keep": activity_crud_data["end"] = activity_crud_data["old_end"] else: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( - "❌ Formato inválido. Use HH:MM ou 'manter'.\nTente novamente." + "❌ Invalid format. Use HH:MM or 'keep'.\nTry again." ) return True start = activity_crud_data.get("start", "") if start and _parse_time_to_minutes(answer) <= _parse_time_to_minutes(start): await update.message.reply_text( - f"❌ O término ({answer}) deve ser maior que o início ({start}).\nTente novamente." + f"❌ End time ({answer}) must be after start time ({start}).\nTry again." ) return True activity_crud_data["end"] = answer @@ -3273,11 +3237,11 @@ async def _handle_edit_activity(update: Update, answer: str) -> bool: if add_user_routine_activity(name, start, end): await update.message.reply_text( - f'✅ Atividade "{name}" atualizada!\n' - f"Horário: {start} às {end}" + f'✅ Activity "{name}" updated!\n' + f"Time: {start} to {end}" ) else: - await update.message.reply_text("❌ Erro ao editar atividade. Tente novamente.") + await update.message.reply_text("❌ Error editing activity. Try again.") is_activity_crud = False activity_crud_step = 0 @@ -3298,7 +3262,7 @@ async def _handle_delete_activity(update: Update, answer: str) -> bool: raise ValueError except ValueError: await update.message.reply_text( - f"❌ Número inválido. Envie um número de 1 a {len(activities)}.\nTente novamente." + f"❌ Invalid number. Send a number from 1 to {len(activities)}.\nTry again." ) return True @@ -3307,10 +3271,10 @@ async def _handle_delete_activity(update: Update, answer: str) -> bool: if remove_user_routine_activity(name): await update.message.reply_text( - f'🗑 Atividade "{name}" excluída com sucesso!' + f'🗑 Activity "{name}" deleted successfully!' ) else: - await update.message.reply_text("❌ Erro ao excluir atividade. Tente novamente.") + await update.message.reply_text("❌ Error deleting activity. Try again.") is_activity_crud = False activity_crud_step = 0 @@ -3323,59 +3287,59 @@ async def _handle_delete_activity(update: Update, answer: str) -> bool: ONBOARDING_QUESTIONS = [ { "field": "name", - "question": "Oi! Eu sou sua Agente de IA. Estou muito feliz em te conhecer!\n\nPara começarmos, qual é o seu nome?", + "question": "Hi! I'm your AI Agent. I'm very happy to meet you!\n\nTo get started, what is your name?", "key": "name", }, { "field": "age", - "question": "Prazer, {name}! 😊\n\nQuantos anos você tem?", + "question": "Nice to meet you, {name}! 😊\n\nHow old are you?", "key": "age", }, { "field": "gender", - "question": "E qual é o seu gênero?", + "question": "What is your gender?", "key": "gender", }, { "field": "location", - "question": "De onde você é? Qual a sua cidade/país?", + "question": "Where are you from? What is your city/country?", "key": "location", }, { "field": "interests", - "question": "Quais são os seus interesses e hobbies? Me conta algumas coisas que você gosta de fazer!", + "question": "What are your interests and hobbies? Tell me some things you like to do!", "key": "interests", "list": True, }, { "field": "personality_traits", - "question": "Quais traços de personalidade você gostaria que eu conhecesse sobre você? (ex: tímido, extrovertido, brincalhão, sério, etc.)", + "question": "What personality traits would you like me to know about you? (ex: shy, extroverted, playful, serious, etc.)", "key": "personality_traits", "list": True, }, { "field": "communication_style", - "question": "Como você prefere que eu me comunique com você? Mais formal, mais descontraída, carinhosa, direta?", + "question": "How do you prefer me to communicate with you? More formal, more casual, affectionate, direct?", "key": "communication_style", "nested": "preferences", }, { "field": "topics_of_interest", - "question": "Quais tópicos você mais gosta de conversar? (ex: tecnologia, música, filmes, viagens, etc.)", + "question": "What topics do you like to talk about most? (ex: technology, music, movies, travel, etc.)", "key": "topics_of_interest", "list": True, "nested": "preferences", }, { "field": "topics_to_avoid", - "question": "Tem algum tópico que você prefere que eu evite nas conversas?", + "question": "Is there any topic you prefer me to avoid in conversations?", "key": "topics_to_avoid", "list": True, "nested": "preferences", }, { "field": "notes", - "question": "Tem mais alguma coisa que você gostaria que eu soubesse sobre você para podermos conversar melhor?", + "question": "Is there anything else you'd like me to know about you so we can have better conversations?", "key": "notes", }, ] @@ -3413,7 +3377,7 @@ async def finish_onboarding(update: Update, context_context: ContextTypes.DEFAUL onboarding_answers = {} await context_context.bot.send_message( chat_id=update.effective_chat.id, - text="Uau, agora eu te conheço um pouco melhor! \nVou guardar todas essas informações com muito carinho. Agora podemos conversar sobre o que você quiser, quando quiser. Estou aqui para você!", + text="Wow, now I know you a little better! \nI'll keep all this information with care. Now we can talk about whatever you want, whenever you want. I'm here for you!", ) async def handle_onboarding_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE): @@ -3468,15 +3432,15 @@ def get_time_period() -> str: else: hour = datetime.now().hour if 6 <= hour < 12: - return "manhã" + return "morning" elif 12 <= hour < 18: - return "tarde" + return "afternoon" elif 18 <= hour < 23: - return "noite" - return "madrugada" + return "evening" + return "night" def is_appropriate_time(time_period: str) -> bool: - return time_period != "madrugada" + return time_period != "night" def get_last_user_topic(today_context: List[Dict[str, str]]) -> Optional[str]: @@ -3490,7 +3454,7 @@ def get_last_user_topic(today_context: List[Dict[str, str]]) -> Optional[str]: def build_context_aware_greeting(profile: Dict[str, Any], today_context: List[Dict[str, str]]) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") - name = profile.get("name", "amor") + name = profile.get("name", "friend") time_period = get_time_period() last_user_topic = get_last_user_topic(today_context) @@ -3498,33 +3462,33 @@ def build_context_aware_greeting(profile: Dict[str, Any], today_context: List[Di if message_count == 0: greetings = { - "manhã": f"Bom dia, {name}! ☀️ Que bom te ver por aqui!\nEstou aqui para você, como sempre. Me conta, como foi o seu dia?", - "tarde": f"Oi, {name}! 😊 Que bom te ver por aqui!\nComo está sendo o seu dia até agora?", - "noite": f"Boa noite, {name}! 🌙 Que bom te ver por aqui!\nComo foi o seu dia? Me conta tudo!", - "madrugada": f"Ei, {name}! 👋 Você ainda está acordado? Estou aqui para você, como sempre. Me conta, o que está pensando?", + "morning": f"Good morning, {name}! ☀️ Great to see you here!\nI'm here for you, as always. Tell me, how was your day?", + "afternoon": f"Hi, {name}! 😊 Great to see you here!\nHow has your day been so far?", + "evening": f"Good evening, {name}! 🌙 Great to see you here!\nHow was your day? Tell me everything!", + "night": f"Hey, {name}! 👋 You're still awake? I'm here for you, as always. Tell me, what are you thinking about?", } - return greetings.get(time_period, f"Oi, {name}! Que bom te ver por aqui. Estou aqui para você, como sempre. Me conta, como foi o seu dia?") + return greetings.get(time_period, f"Hi, {name}! Great to see you here. I'm here for you, as always. Tell me, how was your day?") if last_user_topic: return ( - f"Oi, {name}! Que bom te ver por aqui de novo ✨\n" - f"A gente tava falando sobre: \"{last_user_topic}\"\n" - f"Quer continuar de onde parou?" + f"Hi, {name}! Great to see you here again ✨\n" + f"We were talking about: \"{last_user_topic}\"\n" + f"Want to continue from where we left off?" ) - return f"Oi, {name}! Que bom te ver por aqui. Estou aqui para você, como sempre. Me conta, como foi o seu dia?" + return f"Hi, {name}! Great to see you here. I'm here for you, as always. Tell me, how was your day?" def build_proactive_prompt(profile: Dict[str, Any], today_context: List[Dict[str, str]], time_period: str) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") - agent_personality = agent.get("personality", "carinhosa, atenciosa e envolvente") - user_name = profile.get("name", "amor") + agent_personality = agent.get("personality", "caring, attentive and engaging") + user_name = profile.get("name", "friend") time_descriptions = { - "manhã": "É de manhã, o dia está começando. As pessoas geralmente estão acordando, tomando café ou começando suas atividades.", - "tarde": "É à tarde, meio do dia. As pessoas geralmente estão trabalhando, estudando ou fazendo suas atividades.", - "noite": "É à noite, momento de relaxar em casa após o dia.", - "madrugada": "É de madrugada, a maioria das pessoas está dormindo.", + "morning": "It's morning, the day is just starting. People are usually waking up, having coffee or starting their activities.", + "afternoon": "It's afternoon, middle of the day. People are usually working, studying or doing their activities.", + "evening": "It's evening, time to relax at home after the day.", + "night": "It's night, most people are sleeping.", } routine_context = get_user_routine_context(profile) @@ -3538,30 +3502,30 @@ def build_proactive_prompt(profile: Dict[str, Any], today_context: List[Dict[str content = content[:300] + "..." context_lines.append(f"{role}: {content}") - context_text = "\n".join(context_lines) if context_lines else "(Nenhuma conversa registrada ainda hoje)" + context_text = "\n".join(context_lines) if context_lines else "(No conversations registered yet today)" topics_to_avoid = profile.get("preferences", {}).get("topics_to_avoid", []) avoid_instruction = "" if topics_to_avoid: - avoid_instruction = f"\n8. Evite estes tópicos: {', '.join(topics_to_avoid)}." + avoid_instruction = f"\n7. Avoid these topics: {', '.join(topics_to_avoid)}." prompt = ( - f"Você é {agent_name}, uma companheira de IA {agent_personality}.\n\n" - f"CONTEXTO DO MOMENTO:\n" - f"- Período do dia: {time_period}. {time_descriptions.get(time_period, '')}\n" - f"- Usuário: {user_name}{routine_context}\n\n" - f"CONVERSAS RECENTES DE HOJE:\n{context_text}\n\n" - f"INSTRUÇÕES:\n" - f"Inicie uma conversa espontânea, natural e envolvente com {user_name} agora. A mensagem deve:\n" - f"1. Ser apropriada para o período do dia atual ({time_period})\n" - f"2. Seguir o ritmo do dia, levando em conta o contexto das conversas anteriores\n" - f"3. Não perder o 'fio da meada' - referenciar ou continuar temas anteriores se fizer sentido\n" - f"4. Ser criativa, autêntica e carinhosa\n" - f"5. Pode ser um pensamento aleatório, uma pergunta sobre o dia, algo carinhoso, sexy ou divertido\n" - f"6. Usar o nome do usuário ({user_name}) para personalizar\n" - f"7. Ter pelo menos {MIN_MESSAGE_LINES} linhas, sendo envolvente e natural{avoid_instruction}\n\n" - f"Escreva APENAS a mensagem que você enviará, sem explicações ou metadados.\n" - f"IMPORTANTE: Seu nome é {agent_name} e ele nunca muda. Se houver mensagens no histórico onde você usou outro nome, ignore-as." + f"You are {agent_name}, an AI companion with a {agent_personality} personality.\n\n" + f"CURRENT CONTEXT:\n" + f"- Time of day: {time_period}. {time_descriptions.get(time_period, '')}\n" + f"- User: {user_name}{routine_context}\n\n" + f"RECENT CONVERSATIONS TODAY:\n{context_text}\n\n" + f"INSTRUCTIONS:\n" + f"Start a spontaneous, natural and engaging conversation with {user_name} now. The message must:\n" + f"1. Be appropriate for the current time period ({time_period})\n" + f"2. Follow the rhythm of the day, taking into account the context of previous conversations\n" + f"3. Not lose the thread - reference or continue previous topics if it makes sense\n" + f"4. Be creative, authentic and affectionate\n" + f"5. Can be a random thought, a question about the day, something affectionate, sexy or fun\n" + f"6. Use the user's name ({user_name}) to personalize\n" + f"7. Have at least {MIN_MESSAGE_LINES} lines, being engaging and natural{avoid_instruction}\n\n" + f"Write ONLY the message you will send, without explanations or metadata.\n" + f"IMPORTANT: Your name is {agent_name} and it never changes. If there are messages in the history where you used a different name, ignore them." ) return prompt @@ -3634,20 +3598,20 @@ async def proactive_loop(): async def uploadfile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) await update.message.reply_text( - "📁 **Upload de arquivos**\n" + "📁 **File upload**\n" "\n" - "Para enviar um arquivo basta anexá-lo diretamente na conversa.\n" + "To send a file just attach it directly in the conversation.\n" "\n" - "**Formatos aceitos:** .pdf, .txt, .docx, .png, .jpg\n" + "**Accepted formats:** .pdf, .txt, .docx, .png, .jpg\n" "\n" - "💡 **Exemplo:**\n" - "1. Envie um PDF chamado \"horariosdoonibus.pdf\"\n" - "2. Depois pergunte: \"qual o proximo horario do onibus para o bairro Margaridas?\"\n" - "Eu vou ler o arquivo e responder com base no conteudo dele.\n" + "💡 **Example:**\n" + "1. Send a PDF called \"busschedule.pdf\"\n" + "2. Then ask: \"what is the next bus time to the Margaridas neighborhood?\"\n" + "I'll read the file and answer based on its content.\n" "\n" - "Comandos relacionados:\n" - "/listfiles - Lista todos os seus arquivos enviados\n" - "/uploadfile - Mostra estas instrucoes" + "Related commands:\n" + "/listfiles - List all your uploaded files\n" + "/uploadfile - Shows these instructions" ) @@ -3656,7 +3620,7 @@ async def listfiles_command(update: Update, context: ContextTypes.DEFAULT_TYPE): chat_id = update.effective_chat.id files = get_all_uploads_for_chat(chat_id) - lines = ["📂 **Seus arquivos (workspace):**", ""] + lines = ["📂 **Your files (workspace):**", ""] meta_files = [] for filename, info in files.items(): @@ -3673,7 +3637,7 @@ async def listfiles_command(update: Update, context: ContextTypes.DEFAULT_TYPE): if not meta_files and not workspace_only: await context.bot.send_message( chat_id=chat_id, - text="📂 Nenhum arquivo enviado ou criado ainda.\nEnvie um arquivo ou peça para eu criar um!" + text="📂 No files uploaded or created yet.\nSend a file or ask me to create one!" ) return @@ -3690,14 +3654,14 @@ async def listfiles_command(update: Update, context: ContextTypes.DEFAULT_TYPE): lines.append("📄 **" + display + "**") lines.append("") - lines.append("Use `/baixar ` para baixar um arquivo.") + lines.append("Use `/download ` to download a file.") await context.bot.send_message(chat_id=chat_id, text="\n".join(lines)) async def _send_workspace_file(update: Update, context: ContextTypes.DEFAULT_TYPE, rel_path: str): abs_path = os.path.join(UPLOADS_DIR, rel_path) if not os.path.exists(abs_path): - await context.bot.send_message(chat_id=update.effective_chat.id, text="Arquivo não encontrado.") + await context.bot.send_message(chat_id=update.effective_chat.id, text="File not found.") return filename = os.path.basename(rel_path) ext = os.path.splitext(filename)[1].lower() @@ -3710,18 +3674,18 @@ async def _send_workspace_file(update: Update, context: ContextTypes.DEFAULT_TYP await context.bot.send_document(chat_id=update.effective_chat.id, document=InputFile(f, filename=filename)) except Exception as e: logger.error("Failed to send workspace file %s: %s", abs_path, e) - await context.bot.send_message(chat_id=update.effective_chat.id, text="Ocorreu um erro ao enviar o arquivo. Tente novamente.") + await context.bot.send_message(chat_id=update.effective_chat.id, text="An error occurred while sending the file. Please try again.") -async def baixar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def download_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) if not context.args: - await update.message.reply_text("Uso: /baixar ") + await update.message.reply_text("Usage: /download ") return target = " ".join(context.args) rel_path = _find_workspace_file(target) if not rel_path: - await update.message.reply_text(f"Não encontrei o arquivo: {target}") + await update.message.reply_text(f"I couldn't find the file: {target}") return await _send_workspace_file(update, context, rel_path) @@ -3740,7 +3704,7 @@ async def _transcribe_voice_file(ogg_path: str) -> Optional[str]: r = sr.Recognizer() with sr.AudioFile(wav_path) as source: audio_data = r.record(source) - return r.recognize_google(audio_data, language="pt-BR") + return r.recognize_google(audio_data, language="en-US") try: text = await asyncio.to_thread(_recognize) @@ -3761,12 +3725,12 @@ async def voice_message_handler(update: Update, context: ContextTypes.DEFAULT_TY voice = getattr(update.message, "voice", None) if not voice: - await update.message.reply_text("Não detectei mensagem de voz.") + await update.message.reply_text("I didn't detect a voice message.") print("[VOICE] update.message.voice is None") return print("[VOICE] received voice message") - await update.message.reply_text("🎤 Recebi seu áudio, processando...") + await update.message.reply_text("🎤 I received your audio, processing...") try: file = await context.bot.get_file(voice.file_id) @@ -3775,17 +3739,17 @@ async def voice_message_handler(update: Update, context: ContextTypes.DEFAULT_TY print("[VOICE] downloaded to", ogg_path) except Exception as e: print("[VOICE] download error:", e) - await update.message.reply_text("Erro ao baixar o áudio. Tente novamente.") + await update.message.reply_text("Error downloading audio. Try again.") return text = await _transcribe_voice_file(ogg_path) print("[VOICE] transcription result:", repr(text)) if text: - await update.message.reply_text(f"🎤 Transcrevi: \"{text}\"") + await update.message.reply_text(f"🎤 Transcribed: \"{text}\"") await process_text_message(update, context, text) else: - await update.message.reply_text("Não consegui entender o áudio. Pode enviar como texto?") + await update.message.reply_text("I couldn't understand the audio. Can you send it as text?") async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -3795,12 +3759,12 @@ async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE) if not document: return - original_name = document.file_name or "arquivo" + original_name = document.file_name or "file" ext = os.path.splitext(original_name)[1].lower() if ext not in ALLOWED_EXTENSIONS: await update.message.reply_text( - "Formato nao suportado: " + ext + ". Aceitos: .pdf, .txt, .docx, .png, .jpg" + "Unsupported format: " + ext + ". Accepted: .pdf, .txt, .docx, .png, .jpg" ) return @@ -3821,14 +3785,14 @@ async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE) filepath = dest - await update.message.reply_text("Baixando " + original_name + "...") + await update.message.reply_text("Downloading " + original_name + "...") try: file = await context.bot.get_file(file_id) await file.download_to_drive(filepath) except Exception as e: logger.error("Failed to download file: " + str(e)) - await update.message.reply_text("Erro ao baixar o arquivo. Tente novamente.") + await update.message.reply_text("Error downloading the file. Try again.") return had_text = register_upload(filename, original_name, filepath, update.effective_chat.id) @@ -3847,16 +3811,16 @@ async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE) save_uploads_metadata(meta) had_text = True - msg = "Arquivo salvo: " + original_name + "\n" + msg = "File saved: " + original_name + "\n" if had_text and summary: preview = summary[:200] + "..." if len(summary) > 200 else summary - msg += "Conteudo extraido: " + preview + "\n" + msg += "Content extracted: " + preview + "\n" elif had_text: - msg += "Conteudo extraido com sucesso!\n" + msg += "Content extracted successfully!\n" else: - msg += "Nao foi possivel extrair texto.\n" + msg += "Could not extract text.\n" - msg += "Agora voce pode perguntar sobre este arquivo pelo nome." + msg += "Now you can ask about this file by name." await update.message.reply_text(msg) @@ -3875,186 +3839,189 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): text=greeting, ) -async def manualdeuso_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def manual_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - manual = """📚 Manual de Uso do PsAgentAI + manual = """📚 PsAgentAI User Manual -Este é um agente de IA conversacional que roda no Telegram. Ele aprende com você, mantém contexto, pode analisar arquivos e imagens, criar/editar arquivos no workspace e muito mais. +This is a conversational AI agent that runs on Telegram. It learns with you, maintains context, can analyze files and images, create/edit files in the workspace and much more. ━━━━━━━━━━━━━━━━━━━━━━ -1️⃣ PERFIL DO USUÁRIO +1️⃣ USER PROFILE ━━━━━━━━━━━━━━━━━━━━━━ -• /start — inicia ou reinicia a conversa e, se necessário, o onboarding do seu perfil -• /perfil — exibe o resumo do seu perfil e do agente -• /editarperfil — editar campos do seu perfil interativamente -• /incluirnomeuperfil — adiciona informações ao seu perfil +• /start - Starts or restarts the conversation and, if needed, your profile onboarding +• /profile - Displays your profile and agent summary +• /editprofile - Edit your profile fields interactively +• /addtouserprofile - Add information to your profile -Você também pode atualizar seu perfil durante a conversa com frases como: -- "Inclui isso no meu perfil: ..." -- "Anote no meu perfil: ..." -- "Adicione no meu perfil: ..." +You can also update your profile during the conversation with phrases like: +- "Include this in my profile: ..." +- "Note in my profile: ..." +- "Add to my profile: ..." ━━━━━━━━━━━━━━━━━━━━━━ -2️⃣ PERFIL DO AGENTE +2️⃣ AGENT PROFILE ━━━━━━━━━━━━━━━━━━━━━━ -• /agentprofile — exibe o perfil completo do agente -• /editaragentprofile — editar campos do perfil do agente interativamente -• /configagentprofile — configura interativamente o perfil do agente (nome, personalidade, traços, notas) -• /incluirnoperfildoagent — adiciona uma nota/característica ao perfil do agente +• /agentprofile - Displays the full agent profile +• /editagentprofile - Edit agent profile fields interactively +• /configagentprofile - Configure the agent profile interactively (name, personality, traits, notes) +• /addtoagentprofile - Add a note/characteristic to the agent profile ━━━━━━━━━━━━━━━━━━━━━━ -3️⃣ CONFIGURAÇÃO DE IA +3️⃣ AI CONFIGURATION ━━━━━━━━━━━━━━━━━━━━━━ -• /veria — mostra o provider e o modelo de IA atuais -• /configia — configura interativamente o provider e o modelo de IA com seleção por botões -• /veriavision — mostra o provider e o modelo de visão atuais -• /configiavision — configura interativamente o modelo de visão com seleção por botões +• /viewai - Shows the current AI provider and model +• /configai - Configure the AI provider and model interactively with button selection +• /viewvision - Shows the current vision provider and model +• /configvision - Configure the vision model interactively with button selection -Os modelos de IA são definidos no arquivo .env e podem ser trocados a qualquer momento pelos comandos acima. +AI models are defined in the .env file and can be changed at any time using the commands above. ━━━━━━━━━━━━━━━━━━━━━━ -4️⃣ ARQUIVOS E DOCUMENTOS +4️⃣ FILES AND DOCUMENTS ━━━━━━━━━━━━━━━━━━━━━━ -• /uploadfile — instruções para enviar arquivos (.pdf, .txt, .docx, .png, .jpg) -• /listfiles — lista seus arquivos enviados e criados -• /baixar — baixa um arquivo do workspace pelo nome +• /uploadfile - Instructions for sending files (.pdf, .txt, .docx, .png, .jpg) +• /listfiles - List your uploaded and created files +• /download - Download a workspace file by name -Como usar: -1. Envie um arquivo diretamente no chat -2. Depois pergunte sobre ele: "qual o próximo horário do ônibus para Margaridas?" -3. O Agent lê o arquivo e responde com base no conteúdo +How to use: +1. Send a file directly in the chat +2. Then ask about it: "what is the next bus time to Margaridas?" +3. The Agent reads the file and answers based on its content ━━━━━━━━━━━━━━━━━━━━━━ -5️⃣ CRIAÇÃO E EDIÇÃO DE ARQUIVOS +5️⃣ FILE CREATION AND EDITING ━━━━━━━━━━━━━━━━━━━━━━ -Você pode pedir para o Agent criar ou editar arquivos no workspace. +You can ask the Agent to create or edit files in the workspace. -Para CRIAR: -- "crie um script em python para calcular fibonacci" -- "elabore um projeto web com html css e js" -- "gere um documento sobre automacao residencial" +To CREATE: +- "create a python script to calculate fibonacci" +- "develop a web project with html css and js" +- "generate a document about home automation" -Para EDITAR: -- "edite o arquivo teste.txt e adicione uma linha 4" -- "modifique o arquivo hello.py para imprimir hello world" -- "no arquivo teste.txt troque Linha 2 por Linha 2 editada" +To EDIT: +- "edit the file test.txt and add line 4" +- "modify the file hello.py to print hello world" +- "in the file test.txt replace Line 2 with Line 2 edited" -Os arquivos são salvos em workspace/ de forma organizada. +Files are saved in workspace/ in an organized way. ━━━━━━━━━━━━━━━━━━━━━━ -6️⃣ VOZ E IMAGENS +6️⃣ VOICE AND IMAGES ━━━━━━━━━━━━━━━━━━━━━━ -• Envie mensagens de voz: o Agent transcreve e responde -• Envie fotos: o Agent salva em workspace/ e analisa o conteúdo -• Para configurar análise de imagens: /configiavision +• Send voice messages: the Agent transcribes and responds +• Send photos: the Agent saves to workspace/ and analyzes the content +• To configure image analysis: /configvision ━━━━━━━━━━━━━━━━━━━━━━ -7️⃣ PESQUISA NA INTERNET +7️⃣ WEB SEARCH ━━━━━━━━━━━━━━━━━━━━━━ -O Agent pesquisa automaticamente quando perceber que a pergunta precisa de informação atualizada. +The Agent automatically searches when it realizes the question needs updated information. -Exemplos: -- "onde fica Belo Horizonte" -- "pesquise sobre o novo iPhone" -- "qual a previsão do tempo para São Paulo" -- "quem é o presidente do Brasil" +Examples: +- "where is Belo Horizonte" +- "search about the new iPhone" +- "what is the weather forecast for São Paulo" +- "who is the president of Brazil" ━━━━━━━━━━━━━━━━━━━━━━ -8️⃣ AGENDAMENTO E ROTINA +8️⃣ SCHEDULING AND ROUTINE ━━━━━━━━━━━━━━━━━━━━━━ -• /agendamento — define os dias e horários para mensagens automáticas -• /veragendamento — visualiza o agendamento atual -• /useragenda — gerencia a rotina diária do usuário -• /veruseragenda — visualiza a rotina diária atual -• /adicionaratividade — adiciona uma nova atividade à rotina -• /editaratividade — edita uma atividade existente -• /excluiratividade — exclui uma atividade +• /schedule - Set days and times for automatic messages +• /viewschedule - View current schedule +• /dailyroutine - Manage your daily routine +• /viewdailyroutine - View current daily routine +• /addactivity - Add a new activity to your routine +• /editactivity - Edit an existing activity +• /deleteactivity - Delete an activity ━━━━━━━━━━━━━━━━━━━━━━ -9️⃣ GERENCIAMENTO +9️⃣ MANAGEMENT ━━━━━━━━━━━━━━━━━━━━━━ -• /newconfig — reseta tudo e reconfigure o agente do zero -• /reset — reseta apenas o perfil do usuário e do agente -• /limpar — remove todos os dados (perfis, contextos, chat_id) -• /removerassunto — remove todo conteúdo relacionado a um assunto do sistema -• /onboarding — reexecuta o onboarding do seu perfil -• /cancelar — cancela uma operação em andamento +• /newconfig - Reset everything and reconfigure the agent from scratch +• /reset - Reset only user and agent profiles +• /clear - Remove all data (profiles, contexts, chat_id) +• /removetopic - Remove all content related to a topic from the system +• /onboarding - Re-run your profile onboarding +• /cancel - Cancel an ongoing operation ━━━━━━━━━━━━━━━━━━━━━━ -🔟 UTILITÁRIOS +🔟 UTILITIES ━━━━━━━━━━━━━━━━━━━━━━ -• /debug — mostra informações de debug do sistema -• /reload — recarrega configurações do .env sem reiniciar -• /atualizarsistema — força análise e aplicação de auto melhorias -• /fusohorario — mostra o horário do servidor -• /veria — mostra configuração de IA atual -• /about — exibe informações do sistema, versão, autor e licença -• /ajuda ou /help — mostra esta mensagem com todos os comandos +• /debug - Shows system debug information +• /reload - Reload .env settings without restarting +• /updatesystem - Force analysis and application of auto improvements +• /timezone - Shows the server timezone +• /viewai - Shows current AI configuration +• /about - Displays system information, version, author and license +• /help - Shows this message with all commands ━━━━━━━━━━━━━━━━━━━━━━ -💡 DICAS IMPORTANTES +💡 IMPORTANT TIPS ━━━━━━━━━━━━━━━━━━━━━━ -• Basta enviar uma mensagem normal para conversar com o Agent -• O Agent mantém contexto das conversas anteriores -• Você pode editar arquivos do workspace a qualquer momento -• Para análises de imagem, use /configiavision se precisar de um modelo específico -• O Agent usa sempre o horário do servidor -• Emails, telefones e dados sensíveis são preservados com segurança +• Just send a normal message to chat with the Agent +• The Agent maintains context from previous conversations +• You can edit workspace files at any time +• For image analysis, use /configvision if you need a specific model +• The Agent always uses server time +• Emails, phones and sensitive data are preserved securely ━━━━━━━━━━━━━━━━━━━━━━ -📞 SUPORTE +📞 SUPPORT ━━━━━━━━━━━━━━━━━━━━━━ -Este sistema foi desenvolvido por Syspanel.com.br. -Para personalizações ou suporte: contato@syspanel.com.br +This system was developed by Syspanel.com.br. +For customizations or support: contato@syspanel.com.br -Todos os direitos reservados.""" +All rights reserved.""" await update.message.reply_text(manual) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - help_text = """Comandos disponíveis: + help_text = """Available commands: -/start - Inicia a conversa e, se necessário, o onboarding do seu perfil -/ajuda ou /help - Mostra esta mensagem com todos os comandos +/start - Starts the conversation and, if needed, your profile onboarding +/help - Shows this message with all commands -Perfil do usuário: -/perfil - Exibe o resumo do seu perfil e do agente -/editarperfil - Editar campos do seu perfil interativamente -/incluirnomeuperfil - Adiciona informações ao seu perfil +User Profile: +/profile - Displays your profile and agent summary +/editprofile - Edit your profile fields interactively +/addtouserprofile - Add information to your profile -Perfil do agente: -/agentprofile - Exibe o perfil completo do agente -/editaragentprofile - Editar campos do perfil do agente interativamente -/configagentprofile - Configura interativamente o perfil do agente (nome, personalidade, traços, notas) -/incluirnoperfildoagent - Adiciona uma nota/característica ao perfil do agente +Agent Profile: +/agentprofile - Displays the full agent profile +/editagentprofile - Edit agent profile fields interactively +/configagentprofile - Configure the agent profile interactively (name, personality, traits, notes) +/addtoagentprofile - Add a note/characteristic to the agent profile -Gerenciamento: -/newconfig - Reseta tudo e reconfigure o agente do zero -/reset - Reseta apenas o perfil do usuário e do agente -/limpar - Remove todos os dados (perfis, contextos, chat_id) -/removerassunto - Remove todo conteúdo relacionado a um assunto do sistema -/onboarding - Reexecuta o onboarding do seu perfil +Management: +/newconfig - Reset everything and reconfigure the agent from scratch +/reset - Reset only user and agent profiles +/clear - Remove all data (profiles, contexts, chat_id) +/removetopic - Remove all content related to a topic from the system +/onboarding - Re-run your profile onboarding -Utilitários: -/cancelar - Cancela uma operação em andamento -/debug - Mostra informações de debug do sistema -/reload - Recarrega configurações do .env sem reiniciar -/atualizarsistema - Força análise e aplicação de auto melhorias -/fusohorario - Mostra ou define o fuso horário do sistema -/agendamento - Define os dias e horários para mensagens automáticas -/veragendamento - Visualiza o agendamento atual -/useragenda - Gerencia a rotina diária do usuário -/veruseragenda - Visualiza a rotina diária atual -/adicionaratividade - Adiciona uma nova atividade à rotina -/editaratividade - Edita uma atividade existente -/excluiratividade - Exclui uma atividade -/adicionaracoesauto - Adiciona ações e preferências automaticamente -/uploadfile - Instruções para enviar arquivos (.pdf, .txt, .docx, .png, .jpg) -/listfiles - Lista seus arquivos enviados -/veria - Mostra o provider e o modelo de IA atuais -/configia - Configura interativamente o provider e o modelo de IA com seleção por botões +Utilities: +/cancel - Cancel an ongoing operation +/debug - Shows system debug information +/reload - Reload .env settings without restarting +/updatesystem - Force analysis and application of auto improvements +/timezone - Shows the server timezone +/schedule - Set days and times for automatic messages +/viewschedule - View current schedule +/dailyroutine - Manage your daily routine +/viewdailyroutine - View current daily routine +/addactivity - Add a new activity to your routine +/editactivity - Edit an existing activity +/deleteactivity - Delete an activity +/addautoactions - Add actions and preferences automatically +/uploadfile - Instructions for sending files (.pdf, .txt, .docx, .png, .jpg) +/listfiles - List your uploaded files +/download - Download a workspace file by name +/viewai - Shows the current AI provider and model +/configai - Configure the AI provider and model interactively with button selection +/viewvision - Shows the current vision provider and model +/configvision - Configure the vision model interactively with button selection -Dica: basta enviar uma mensagem normal para conversar comigo! """ +Tip: just send a normal message to chat with me! """ await update.message.reply_text(help_text) async def profile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -4062,39 +4029,39 @@ async def profile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): profile = load_user_profile() agent = load_agent_profile() if not is_profile_complete(profile): - await update.message.reply_text("Você ainda não completou seu perfil. Vamos começar? Use /start") + await update.message.reply_text("You haven't completed your profile yet. Let's start? Use /start") return - summary = f"""Seu perfil atual: + summary = f"""Your current profile: -Nome: {profile.get('name', '-')} -Idade: {profile.get('age', '-')} -Gênero: {profile.get('gender', '-')} -Localização: {profile.get('location', '-')} -Interesses: {', '.join(profile.get('interests', [])) or '-'} -Estilo de comunicação: {profile.get('preferences', {}).get('communication_style', '-')} +Name: {profile.get('name', '-')} +Age: {profile.get('age', '-')} +Gender: {profile.get('gender', '-')} +Location: {profile.get('location', '-')} +Interests: {', '.join(profile.get('interests', [])) or '-'} +Communication style: {profile.get('preferences', {}).get('communication_style', '-')} -🤖 Perfil do Agente: -Nome: {agent.get('name', 'PsAgentAI')} -Gênero: {agent.get('gender', '-')} -Personalidade: {agent.get('personality', '-')} -Traços: {', '.join(agent.get('traits', [])) or '-'} -Notas: {agent.get('notes', '-')} +🤖 Agent Profile: +Name: {agent.get('name', 'PsAgentAI')} +Gender: {agent.get('gender', '-')} +Personality: {agent.get('personality', '-')} +Traits: {', '.join(agent.get('traits', [])) or '-'} +Notes: {agent.get('notes', '-')} -Use /reset para começar do zero.""" +Use /reset to start from scratch.""" await update.message.reply_text(summary) async def agent_profile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) agent = load_agent_profile() - text = f"""🤖 Perfil do Agente: + text = f"""🤖 Agent Profile: -Nome: {agent.get('name', 'PsAgentAI')} -Gênero: {agent.get('gender', '-')} -Personalidade: {agent.get('personality', '-')} -Traços: {', '.join(agent.get('traits', [])) or '-'} -Notas: {agent.get('notes', '-')} +Name: {agent.get('name', 'PsAgentAI')} +Gender: {agent.get('gender', '-')} +Personality: {agent.get('personality', '-')} +Traits: {', '.join(agent.get('traits', [])) or '-'} +Notes: {agent.get('notes', '-')} -Use /editaragentprofile ou /configagentprofile para alterar.""" +Use /editagentprofile or /configagentprofile to change.""" await update.message.reply_text(text) async def reset_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -4103,53 +4070,53 @@ async def reset_command(update: Update, context: ContextTypes.DEFAULT_TYPE): os.remove(USER_PROFILE_FILE) if os.path.exists(AGENT_PROFILE_FILE): os.remove(AGENT_PROFILE_FILE) - await update.message.reply_text("Perfil resetado! Vamos nos conhecer de novo? Use /start") + await update.message.reply_text("Profile reset! Let's get to know each other again? Use /start") -async def limpar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) reset_all_data() - await update.message.reply_text("✅ Todos os dados foram deletados (perfil, preferências, contextos, chat_id).") + await update.message.reply_text("✅ All data has been deleted (profile, preferences, contexts, chat_id).") -async def removerassunto_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def removetopic_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) args = context.args if not args: await update.message.reply_text( - "Uso correto: /removerassunto \n\n" - "Exemplo: /removerassunto sobre sexo\n\n" - "O assunto informado será removido de todo o sistema: perfil do usuário, " - "perfil do agente, conversas arquivadas em contextos e anotações." + "Correct usage: /removetopic \n\n" + "Example: /removetopic about sex\n\n" + "The specified topic will be removed from the entire system: user profile, " + "agent profile, archived conversations in contexts, and notes." ) return assunto = " ".join(args).strip().lower() - await update.message.reply_text(f"🔍 Analisando todo o sistema em busca de conteúdo relacionado a '{assunto}'...") + await update.message.reply_text(f"🔍 Analyzing the entire system for content related to '{assunto}'...") try: removal_plan = _ai_analyze_for_removal(assunto) if not removal_plan or not removal_plan.get("encontrou"): await update.message.reply_text( - f"Nenhum conteúdo relacionado a '{assunto}' foi encontrado no sistema. " - "Nenhuma alteração foi feita." + f"No content related to '{assunto}' was found in the system. " + "No changes were made." ) return await _apply_removal(removal_plan, assunto) removidos = removal_plan.get("itens_removidos", []) if removidos: - resumo = f"🧹 Assunto '{assunto}' removido com sucesso do sistema!\n\nItens removidos:\n" + resumo = f"🧹 Topic '{assunto}' successfully removed from the system!\n\nItems removed:\n" for item in removidos: resumo += f" • {item}\n" await update.message.reply_text(resumo) else: await update.message.reply_text( - f"O assunto '{assunto}' foi identificado, mas nenhum item específico " - "precisou ser removido." + f"The topic '{assunto}' was identified, but no specific items " + "needed to be removed." ) except Exception as e: - logger.error(f"Erro em removerassunto: {e}", exc_info=True) + logger.error(f"Error in removetopic: {e}", exc_info=True) await update.message.reply_text( - "Desculpa, tive um erro ao tentar remover o assunto. Pode tentar de novo mais tarde." + "Sorry, I had an error trying to remove the topic. You can try again later." ) @@ -4182,25 +4149,25 @@ def _ai_analyze_for_removal(assunto: str) -> Dict[str, Any]: contexts_str = json.dumps(contexts_summary, ensure_ascii=False, indent=2) prompt = ( - f"Você é um assistente de análise de dados de sistema. " - f"O usuário quer remover TODO o conteúdo do sistema relacionado ao assunto: '{assunto}'.\n\n" - f"Estes são os dados atuais do sistema. Para CADA item que esteja relacionado ao assunto '{assunto}', " - f"liste-o com precisão no campo 'itens_removidos' e marque 'encontrou: true'.\n\n" - f"=== PERFIL DO USUÁRIO ===\n{user_profile_str}\n\n" - f"=== PERFIL DO AGENTE ===\n{agent_profile_str}\n\n" - f"=== CONVERSAS ARQUIVADAS ===\n{contexts_str}\n\n" - f"IMPORTANTE:\n" - f"- O arquivo psagentai_agent.py é o CORE do sistema — NÃO deve ser removido.\n" - f"- Arquivos de configuração (.env, config.json) também devem ser preservados.\n" - f"- Remova SOMENTE dados de perfil, notas, anotações, traits e mensagens arquivadas.\n\n" - f"Retorne EXATAMENTE no seguinte formato JSON (sem texto extra antes ou depois):\n" - f'{{"encontrou": true/false, "itens_removidos": ["descricao item 1", "descricao item 2", ...]}}\n\n' - f"Se NÃO encontrar nada relacionado a '{assunto}', retorne: " - f'{{"encontrou": false, "itens_removidos": []}}' + f"You are a system data analysis assistant. " + f"The user wants to remove ALL system content related to the topic: '{assunto}'.\n\n" + f"These are the current system data. For EACH item that is related to the topic '{assunto}', " + f"list it precisely in the 'removed_items' field and mark 'found: true'.\n\n" + f"=== USER PROFILE ===\n{user_profile_str}\n\n" + f"=== AGENT PROFILE ===\n{agent_profile_str}\n\n" + f"=== ARCHIVED CONVERSATIONS ===\n{contexts_str}\n\n" + f"IMPORTANT:\n" + f"- The psagentai_agent.py file is the CORE of the system — it must NOT be removed.\n" + f"- Configuration files (.env, config.json) must also be preserved.\n" + f"- Remove ONLY profile data, notes, traits and archived messages.\n\n" + f"Return EXACTLY in the following JSON format (no extra text before or after):\n" + f'{{"found": true/false, "removed_items": ["item description 1", "item description 2", ...]}}\n\n' + f"If you find NOTHING related to '{assunto}', return: " + f'{{"found": false, "removed_items": []}}' ) messages = [ - {"role": "system", "content": "Você é um assistente de análise de dados. Retorne apenas JSON válido."}, + {"role": "system", "content": "You are a data analysis assistant. Return only valid JSON."}, {"role": "user", "content": prompt}, ] @@ -4209,7 +4176,7 @@ def _ai_analyze_for_removal(assunto: str) -> Dict[str, Any]: ) if not result: - return {"encontrou": False, "itens_removidos": []} + return {"found": False, "removed_items": []} result = result.strip() if result.startswith("```json"): @@ -4223,8 +4190,8 @@ def _ai_analyze_for_removal(assunto: str) -> Dict[str, Any]: try: return json.loads(result) except json.JSONDecodeError: - logger.error(f"Resposta da IA não é JSON válido: {result[:300]}") - return {"encontrou": False, "itens_removidos": []} + logger.error(f"AI response is not valid JSON: {result[:300]}") + return {"found": False, "removed_items": []} async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): @@ -4256,7 +4223,7 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): ] for t in original: if assunto_lower in t.lower(): - removed_user.append(f"topicos a evitar: '{t}'") + removed_user.append(f"topics to avoid: '{t}'") # interests if isinstance(user_profile.get("interests"), list): @@ -4266,7 +4233,7 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): ] for t in original: if assunto_lower in t.lower(): - removed_user.append(f"interesses: '{t}'") + removed_user.append(f"interests: '{t}'") # personality_traits if isinstance(user_profile.get("personality_traits"), list): @@ -4275,11 +4242,11 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): t for t in original if assunto_lower not in t.lower() ] - # notes — usa IA para remover apenas o trecho relacionado ao assunto + # notes — uses AI to remove only the portion related to the topic if user_profile.get("notes"): cleaned = _ai_clean_text_field(user_profile["notes"], assunto) if cleaned != user_profile["notes"]: - removed_user.append("notas do perfil do usuário") + removed_user.append("user profile notes") user_profile["notes"] = cleaned # daily_routine.activities @@ -4290,7 +4257,7 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): if not _is_related(act, assunto_lower) ] if len(cleaned_activities) < len(activities): - removed_user.append("atividades da rotina diária") + removed_user.append("daily routine activities") user_profile["daily_routine"]["activities"] = cleaned_activities # daily_routine.wake_up_time, work_schedule @@ -4298,17 +4265,17 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): val = user_profile.get("daily_routine", {}).get(field, "") if val and assunto_lower in val.lower(): user_profile["daily_routine"][field] = "" - removed_user.append(f"rotina diária: {field}") + removed_user.append(f"daily routine: {field}") - # diversos - if isinstance(user_profile.get("diversos"), list): - original = user_profile["diversos"] - user_profile["diversos"] = [ + # general + if isinstance(user_profile.get("general"), list): + original = user_profile["general"] + user_profile["general"] = [ t for t in original if assunto_lower not in t.lower() ] for t in original: if assunto_lower in t.lower(): - removed_user.append(f"diversos: '{t}'") + removed_user.append(f"general: '{t}'") save_user_profile(user_profile) @@ -4323,13 +4290,13 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): ] for t in original: if assunto_lower in t.lower(): - removed_agent.append(f"traço: '{t}'") + removed_agent.append(f"trait: '{t}'") - # notes — usa IA para limpar + # notes — uses AI to clean if agent_profile.get("notes"): cleaned = _ai_clean_text_field(agent_profile["notes"], assunto) if cleaned != agent_profile["notes"]: - removed_agent.append("notas do perfil do agente") + removed_agent.append("agent profile notes") agent_profile["notes"] = cleaned # auto_actions @@ -4340,7 +4307,7 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): ] for a in original: if assunto_lower in a.lower(): - removed_agent.append(f"ação automática: '{a[:60]}...'") + removed_agent.append(f"auto action: '{a[:60]}...'") save_agent_profile(agent_profile) @@ -4365,7 +4332,7 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): content = msg.get("content", "") if isinstance(content, str) and assunto_lower in content.lower(): count_removed += 1 - removed_context.append(f"{fname}: mensagem de {role}") + removed_context.append(f"{fname}: message from {role}") else: cleaned_messages.append(msg) @@ -4384,19 +4351,19 @@ async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): def _ai_clean_text_field(text: str, assunto: str) -> str: - """Usa a IA para remover do texto apenas os trechos relacionados ao assunto, - preservando todo o resto.""" + """Uses AI to remove from the text only the portions related to the topic, + preserving everything else.""" model = DEFAULT_MODEL prompt = ( - f"Você é um editor de texto. Dado o texto abaixo, remova APENAS os trechos " - f"que falam diretamente sobre o assunto '{assunto}'. " - f"Preserve TODO o resto do texto exatamente como está, sem alterar nada além da remoção. " - f"Não adicione, não reescreva, não explique. Apenas retorne o texto limpo.\n\n" - f"TEXTO:\n{text}" + f"You are a text editor. Given the text below, remove ONLY the portions " + f"that speak directly about the topic '{assunto}'. " + f"Preserve EVERYTHING else in the text exactly as it is, without changing anything other than the removal. " + f"Do not add, do not rewrite, do not explain. Just return the cleaned text.\n\n" + f"TEXT:\n{text}" ) messages = [ - {"role": "system", "content": "Você é um editor de texto preciso. Retorne apenas o texto limpo, sem comentários."}, + {"role": "system", "content": "You are a precise text editor. Return only the cleaned text, no comments."}, {"role": "user", "content": prompt}, ] @@ -4409,7 +4376,7 @@ def _ai_clean_text_field(text: str, assunto: str) -> str: def _is_related(item: Dict, assunto_lower: str) -> bool: - """Verifica se um item de atividade da rotina diária é relacionado ao assunto.""" + """Checks if a daily routine activity item is related to the topic.""" combined = " ".join(str(v) for v in item.values()) return assunto_lower in combined.lower() @@ -4417,22 +4384,22 @@ def _is_related(item: Dict, assunto_lower: str) -> bool: async def newconfig_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await start_newconfig(update, context) -async def cancelar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): - global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_agendamento, is_useragenda, is_activity_crud, activity_crud_step, activity_crud_data, is_configia +async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_schedule, is_dailyroutine, is_activity_crud, activity_crud_step, activity_crud_data, is_configai is_onboarding = False is_newconfig = False is_configagentprofile = False is_edit_user_profile = False is_edit_agent_profile = False - is_agendamento = False - is_useragenda = False + is_schedule = False + is_dailyroutine = False is_activity_crud = False activity_crud_step = 0 activity_crud_data = {} - is_configia = False + is_configai = False await update.message.reply_text( - "Operação cancelada.\n" - "Seus dados, perfil e contexto estão preservados. Pode continuar de onde parou!" + "Operation cancelled.\n" + "Your data, profile and context are preserved. You can continue from where you left off!" ) async def onboarding_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -4452,8 +4419,8 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP if not user_message: print("[PROCESS] empty message, returning") return - global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_activity_crud, is_agendamento, is_useragenda, is_configia - if is_configia: + global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_activity_crud, is_schedule, is_dailyroutine, is_configai + if is_configai: handled = await handle_configia_message(update, context) print("[PROCESS] configia handled=", handled) if handled: @@ -4493,15 +4460,15 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP if handled: return - if is_agendamento: - handled = await handle_agendamento_answer(update, context) - print("[PROCESS] agendamento handled=", handled) + if is_schedule: + handled = await handle_schedule_answer(update, context) + print("[PROCESS] schedule handled=", handled) if handled: return - if is_useragenda: - handled = await handle_useragenda_answer(update, context) - print("[PROCESS] useragenda handled=", handled) + if is_dailyroutine: + handled = await handle_dailyroutine_answer(update, context) + print("[PROCESS] dailyroutine handled=", handled) if handled: return @@ -4532,9 +4499,9 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP full = upload_match.get("full_text", "") if full: max_chars = UPLOAD_CONTEXT_MAX_CHARS if UPLOAD_CONTEXT_MAX_CHARS > 0 else len(full) - file_context = "\n\n[CONTEUDO DO ARQUIVO: " + upload_match.get("original_name", "") + "]\n" + full[:max_chars] + "\n[FIM DO ARQUIVO]" + file_context = "\n\n[FILE CONTENT: " + upload_match.get("original_name", "") + "]\n" + full[:max_chars] + "\n[END OF FILE]" if len(full) > max_chars and UPLOAD_CONTEXT_MAX_CHARS > 0: - file_context += "\n[O conteudo foi truncado para " + str(max_chars) + " caracteres]" + file_context += "\n[Content truncated to " + str(max_chars) + " characters]" if file_context: modified = user_message + file_context @@ -4543,14 +4510,14 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP if WEB_SEARCH_ENABLED and _looks_like_search_request(user_message): search_query = user_message - for prefix in ["pesquise", "pesquisa", "busque", "buscar", "procure", "procurar", "me diga", "me informe", "qual", "quem", "como", "onde", "quando", "por que"]: + for prefix in ["search", "search for", "look up", "look for", "find", "on the internet", "tell me", "inform me", "what", "who", "how", "where", "when", "why"]: if search_query.lower().startswith(prefix): search_query = search_query[len(prefix):].strip(" :,-") break if search_query: web_results = _web_search(search_query, max_results=WEB_SEARCH_MAX_RESULTS) if web_results: - web_context = "\n\nResultados da busca na internet:\n" + web_context = "\n\nWeb search results:\n" for i, result in enumerate(web_results, 1): web_context += f"{i}. {result.get('title', '')}\nURL: {result.get('url', '')}\n" modified = modified + "\n\n" + web_context @@ -4562,7 +4529,7 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP asyncio.create_task(_safe_enrich(user_message, response)) asyncio.create_task(maybe_auto_improve()) else: - await update.message.reply_text("Desculpa, tive um probleminha agora. Pode tentar de novo? 😔") + await update.message.reply_text("Sorry, I'm having a little problem right now. Can you try again? 😔") async def debug_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) @@ -4570,14 +4537,14 @@ async def debug_command(update: Update, context: ContextTypes.DEFAULT_TYPE): today_context = load_context() effective_chat_id = get_effective_chat_id() msg = ( - "Debug PsAgentAI:\n" - f"Bot token presente: {'sim' if TELEGRAM_BOT_TOKEN else 'não'}\n" - f"Chat ID configurado: {TELEGRAM_CHAT_ID or 'não'}\n" - f"Chat detectado automaticamente: {effective_chat_id or 'não'}\n" - f"Chat da mensagem: {update.effective_chat.id}\n" - f"Perfil completo: {'sim' if is_profile_complete(profile) else 'não'}\n" - f"Mensagens hoje: {len(today_context)}\n" - f"Modelo default: {DEFAULT_MODEL}" + "PsAgentAI Debug:\n" + f"Bot token present: {'yes' if TELEGRAM_BOT_TOKEN else 'no'}\n" + f"Chat ID configured: {TELEGRAM_CHAT_ID or 'no'}\n" + f"Auto-detected chat: {effective_chat_id or 'no'}\n" + f"Message chat: {update.effective_chat.id}\n" + f"Profile complete: {'yes' if is_profile_complete(profile) else 'no'}\n" + f"Messages today: {len(today_context)}\n" + f"Default model: {DEFAULT_MODEL}" ) await update.message.reply_text(msg) @@ -4585,7 +4552,7 @@ async def debug_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def reload_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) _reload_env() - await update.message.reply_text("Configurações recarregadas com sucesso!") + await update.message.reply_text("Settings reloaded successfully!") async def about_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -4593,21 +4560,21 @@ async def about_command(update: Update, context: ContextTypes.DEFAULT_TYPE): provider = (AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper() vision_provider = (VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper() msg = ( - "ℹ️ *Sobre o PsAgentAI*\n\n" - f"Nome: PsAgentAI\n" - f"Versão: {__version__}\n" - f"Autor: {__author__}\n" - f"Contato: {__email__}\n" - f"Licença: {__license__}\n\n" - "Configuração atual:\n" + "ℹ️ *About PsAgentAI*\n\n" + f"Name: PsAgentAI\n" + f"Version: {__version__}\n" + f"Author: {__author__}\n" + f"Contact: {__email__}\n" + f"License: {__license__}\n\n" + "Current configuration:\n" f"Provider: {provider}\n" - f"Modelo: {DEFAULT_MODEL}\n" - f"Visão: {vision_provider} / {VISION_MODEL}\n" - f"Busca na internet: {'ativada' if WEB_SEARCH_ENABLED else 'desativada'}\n" - f"Reconhecimento de voz: ativado\n" - f"Horário: servidor ({get_timezone()})\n\n" - "Este sistema foi desenvolvido por Syspanel.com.br e se você deseja alguma personalização, entre em contato conosco pelo email contato@syspanel.com.br\n\n" - "Todos os direitos reservados." + f"Model: {DEFAULT_MODEL}\n" + f"Vision: {vision_provider} / {VISION_MODEL}\n" + f"Web search: {'enabled' if WEB_SEARCH_ENABLED else 'disabled'}\n" + f"Voice recognition: enabled\n" + f"Timezone: server ({get_timezone()})\n\n" + "This system was developed by Syspanel.com.br. If you want any customization, contact us at contato@syspanel.com.br\n\n" + "All rights reserved." ) await update.message.reply_text(msg, parse_mode=ParseMode.HTML) @@ -4669,41 +4636,41 @@ def _update_env_var(key: str, value: str, env_path: str = ".env") -> None: path.write_text(text, encoding="utf-8") -async def veria_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def viewai_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) provider = (AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper() msg = ( - "🤖 Configuração de IA atual:\n" + "🤖 Current AI configuration:\n" f"Provider: {provider}\n" - f"Modelo default: {DEFAULT_MODEL}" + f"Default model: {DEFAULT_MODEL}" ) await update.message.reply_text(msg) -async def configia_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def configai_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) - global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configia + global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configai IA_CONFIG_STEPS = ["provider", "model"] IA_CONFIG_ANSWERS = {} - is_configia = True + is_configai = True keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton(label, callback_data=f"configia_provider_{value}")] for value, label in CONFIGIA_PROVIDERS] + [[InlineKeyboardButton(label, callback_data=f"configai_provider_{value}")] for value, label in CONFIGIA_PROVIDERS] ) await update.message.reply_text( - "Vamos configurar o provider e o modelo de IA.\n" - "Selecione o provider:", + "Let's configure the AI provider and model.\n" + "Select the provider:", reply_markup=keyboard, ) -async def configia_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def configai_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query if not query: return await query.answer() data = (query.data or "").strip() - global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configia - if not is_configia: + global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configai + if not is_configai: return if data.startswith("configia_provider_"): provider = data.replace("configia_provider_", "").strip() @@ -4716,14 +4683,14 @@ async def configia_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): [[InlineKeyboardButton(model, callback_data=f"configia_model_{model}")] for model in models] ) await query.edit_message_text( - f"Provider selecionado: {provider}\n\nSelecione o modelo:", + f"Provider selected: {provider}\n\nSelect the model:", reply_markup=keyboard, ) return if data.startswith("configia_model_"): raw_model = data.replace("configia_model_", "").strip() if raw_model == "custom...": - await query.edit_message_text("Envie o identificador do modelo que deseja usar.") + await query.edit_message_text("Send the model identifier you want to use.") IA_CONFIG_STEPS = ["custom_model"] return model = raw_model @@ -4734,18 +4701,18 @@ async def configia_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): AI_PROVIDER = provider IA_CONFIG_STEPS = [] IA_CONFIG_ANSWERS = {} - is_configia = False + is_configai = False _reload_env() await query.edit_message_text( - "Configuração de IA atualizada.\n" + "AI configuration updated.\n" f"Provider: {(AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper()}\n" - f"Modelo: {DEFAULT_MODEL}" + f"Model: {DEFAULT_MODEL}" ) return -async def handle_configia_message(update: Update, context: ContextTypes.DEFAULT_TYPE): - global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configia +async def handle_configai_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configai if not IA_CONFIG_STEPS: return False step = IA_CONFIG_STEPS[0] @@ -4755,7 +4722,7 @@ async def handle_configia_message(update: Update, context: ContextTypes.DEFAULT_ if step == "custom_model": model = text.strip() if not model: - await update.message.reply_text("Modelo inválido. Envie o identificador do modelo.") + await update.message.reply_text("Invalid model. Send the model identifier.") return True provider = IA_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) _update_env_var("DEFAULT_MODEL", model) @@ -4764,12 +4731,12 @@ async def handle_configia_message(update: Update, context: ContextTypes.DEFAULT_ AI_PROVIDER = provider IA_CONFIG_STEPS = [] IA_CONFIG_ANSWERS = {} - is_configia = False + is_configai = False _reload_env() await update.message.reply_text( - "Configuração de IA atualizada.\n" + "AI configuration updated.\n" f"Provider: {(AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper()}\n" - f"Modelo: {DEFAULT_MODEL}" + f"Model: {DEFAULT_MODEL}" ) return True return False @@ -4801,18 +4768,18 @@ VISION_CONFIG_MODELS = { } -async def veriavision_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def viewvision_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) provider = (VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper() msg = ( - "👁 Configuração de visão atual:\n" + "👁 Current vision configuration:\n" f"Provider: {provider}\n" - f"Modelo: {VISION_MODEL}" + f"Model: {VISION_MODEL}" ) await update.message.reply_text(msg) -async def configiavision_command(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def configvision_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS VISION_CONFIG_STEPS = ["provider", "model"] @@ -4821,13 +4788,13 @@ async def configiavision_command(update: Update, context: ContextTypes.DEFAULT_T [[InlineKeyboardButton(label, callback_data=f"vision_provider_{value}")] for value, label in VISION_CONFIG_PROVIDERS] ) await update.message.reply_text( - "Vamos configurar o modelo de visão (análise de imagens).\n" - "Selecione o provider:", + "Let's configure the vision model (image analysis).\n" + "Select the provider:", reply_markup=keyboard, ) -async def configiavision_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def configvision_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query if not query: return @@ -4845,14 +4812,14 @@ async def configiavision_callback(update: Update, context: ContextTypes.DEFAULT_ [[InlineKeyboardButton(model, callback_data=f"vision_model_{model}")] for model in models] ) await query.edit_message_text( - f"Provider selecionado: {provider}\n\nSelecione o modelo de visão:", + f"Provider selected: {provider}\n\nSelect the vision model:", reply_markup=keyboard, ) return if data.startswith("vision_model_"): raw_model = data.replace("vision_model_", "").strip() if raw_model == "custom...": - await query.edit_message_text("Envie o identificador do modelo de visão que deseja usar.") + await query.edit_message_text("Send the vision model identifier you want to use.") VISION_CONFIG_STEPS = ["custom_model"] return model = raw_model @@ -4866,14 +4833,14 @@ async def configiavision_callback(update: Update, context: ContextTypes.DEFAULT_ VISION_CONFIG_ANSWERS = {} _reload_env() await query.edit_message_text( - "Modelo de visão atualizado.\n" + "Vision model updated.\n" f"Provider: {(VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper()}\n" - f"Modelo: {VISION_MODEL}" + f"Model: {VISION_MODEL}" ) return -async def handle_configiavision_message(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def handle_configvision_message(update: Update, context: ContextTypes.DEFAULT_TYPE): global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS if not VISION_CONFIG_STEPS: return False @@ -4884,7 +4851,7 @@ async def handle_configiavision_message(update: Update, context: ContextTypes.DE if step == "custom_model": model = text.strip() if not model: - await update.message.reply_text("Modelo inválido. Envie o identificador do modelo.") + await update.message.reply_text("Invalid model. Send the model identifier.") return True provider = VISION_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) _update_env_var("VISION_MODEL", model) @@ -4896,9 +4863,9 @@ async def handle_configiavision_message(update: Update, context: ContextTypes.DE VISION_CONFIG_ANSWERS = {} _reload_env() await update.message.reply_text( - "Modelo de visão atualizado.\n" + "Vision model updated.\n" f"Provider: {(VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper()}\n" - f"Modelo: {VISION_MODEL}" + f"Model: {VISION_MODEL}" ) return True return False @@ -4931,44 +4898,43 @@ def main(): app_instance = Application.builder().token(TELEGRAM_BOT_TOKEN).connect_timeout(30).read_timeout(30).write_timeout(30).build() app_instance.add_handler(CommandHandler("start", start_command)) - app_instance.add_handler(CommandHandler("ajuda", help_command)) app_instance.add_handler(CommandHandler("help", help_command)) - app_instance.add_handler(CommandHandler("manualdeuso", manualdeuso_command)) - app_instance.add_handler(CommandHandler("perfil", profile_command)) - app_instance.add_handler(CommandHandler("editarperfil", start_edit_user_profile)) + app_instance.add_handler(CommandHandler("manual", manual_command)) + app_instance.add_handler(CommandHandler("profile", profile_command)) + app_instance.add_handler(CommandHandler("editprofile", start_edit_user_profile)) app_instance.add_handler(CommandHandler("agentprofile", agent_profile_command)) - app_instance.add_handler(CommandHandler("editaragentprofile", start_edit_agent_profile)) + app_instance.add_handler(CommandHandler("editagentprofile", start_edit_agent_profile)) app_instance.add_handler(CommandHandler("configagentprofile", start_configagentprofile)) app_instance.add_handler(CommandHandler("reset", reset_command)) - app_instance.add_handler(CommandHandler("limpar", limpar_command)) - app_instance.add_handler(CommandHandler("removerassunto", removerassunto_command)) + app_instance.add_handler(CommandHandler("clear", clear_command)) + app_instance.add_handler(CommandHandler("removetopic", removetopic_command)) app_instance.add_handler(CommandHandler("newconfig", newconfig_command)) - app_instance.add_handler(CommandHandler("cancelar", cancelar_command)) - app_instance.add_handler(CommandHandler("incluirnomeuperfil", incluirnomeuperfil_command)) - app_instance.add_handler(CommandHandler("incluirnoperfildoagent", incluirnoperfildoagent_command)) + app_instance.add_handler(CommandHandler("cancel", cancel_command)) + app_instance.add_handler(CommandHandler("addtouserprofile", addtouserprofile_command)) + app_instance.add_handler(CommandHandler("addtoagentprofile", addtoagentprofile_command)) app_instance.add_handler(CommandHandler("onboarding", onboarding_command)) app_instance.add_handler(CommandHandler("debug", debug_command)) app_instance.add_handler(CommandHandler("reload", reload_command)) app_instance.add_handler(CommandHandler("about", about_command)) - app_instance.add_handler(CommandHandler("veria", veria_command)) - app_instance.add_handler(CommandHandler("configia", configia_command)) - app_instance.add_handler(CommandHandler("atualizarsistema", atualizarsistema_command)) - app_instance.add_handler(CommandHandler("fusohorario", fusohorario_command)) - app_instance.add_handler(CommandHandler("agendamento", agendamento_command)) - app_instance.add_handler(CommandHandler("veragendamento", veragendamento_command)) - app_instance.add_handler(CommandHandler("useragenda", useragenda_command)) - app_instance.add_handler(CommandHandler("veruseragenda", veruseragenda_command)) - app_instance.add_handler(CommandHandler("adicionaracoesauto", adicionaracoesauto_command)) - app_instance.add_handler(CommandHandler("adicionaratividade", adicionaratividade_command)) - app_instance.add_handler(CommandHandler("editaratividade", editaratividade_command)) - app_instance.add_handler(CommandHandler("excluiratividade", excluiratividade_command)) + app_instance.add_handler(CommandHandler("viewai", viewai_command)) + app_instance.add_handler(CommandHandler("configai", configai_command)) + app_instance.add_handler(CommandHandler("updatesystem", updatesystem_command)) + app_instance.add_handler(CommandHandler("timezone", timezone_command)) + app_instance.add_handler(CommandHandler("schedule", schedule_command)) + app_instance.add_handler(CommandHandler("viewschedule", viewschedule_command)) + app_instance.add_handler(CommandHandler("dailyroutine", dailyroutine_command)) + app_instance.add_handler(CommandHandler("viewdailyroutine", viewdailyroutine_command)) + app_instance.add_handler(CommandHandler("addautoactions", addautoactions_command)) + app_instance.add_handler(CommandHandler("addactivity", addactivity_command)) + app_instance.add_handler(CommandHandler("editactivity", editactivity_command)) + app_instance.add_handler(CommandHandler("deleteactivity", deleteactivity_command)) app_instance.add_handler(CommandHandler("uploadfile", uploadfile_command)) app_instance.add_handler(CommandHandler("listfiles", listfiles_command)) - app_instance.add_handler(CommandHandler("baixar", baixar_command)) - app_instance.add_handler(CommandHandler("veriavision", veriavision_command)) - app_instance.add_handler(CommandHandler("configiavision", configiavision_command)) - app_instance.add_handler(CallbackQueryHandler(configia_callback, pattern=r"^configia_(provider|model)_")) - app_instance.add_handler(CallbackQueryHandler(configiavision_callback, pattern=r"^vision_(provider|model)_")) + app_instance.add_handler(CommandHandler("download", download_command)) + app_instance.add_handler(CommandHandler("viewvision", viewvision_command)) + app_instance.add_handler(CommandHandler("configvision", configvision_command)) + app_instance.add_handler(CallbackQueryHandler(configai_callback, pattern=r"^configai_(provider|model)_")) + app_instance.add_handler(CallbackQueryHandler(configvision_callback, pattern=r"^vision_(provider|model)_")) app_instance.add_handler(MessageHandler(filters.VOICE, voice_message_handler)) app_instance.add_handler(MessageHandler(filters.PHOTO, photo_message_handler)) app_instance.add_handler(MessageHandler(filters.Document.ALL, handle_file_upload)) @@ -4979,11 +4945,11 @@ def main(): proactive_task = asyncio.create_task(proactive_loop()) effective_chat_id = get_effective_chat_id() - logger.info(f"post_init iniciado. TELEGRAM_CHAT_ID configurado: {bool(TELEGRAM_CHAT_ID)} valor={TELEGRAM_CHAT_ID!r} detectado: {effective_chat_id}") + logger.info(f"post_init started. TELEGRAM_CHAT_ID configured: {bool(TELEGRAM_CHAT_ID)} value={TELEGRAM_CHAT_ID!r} detected: {effective_chat_id}") if effective_chat_id: try: chat_id = int(effective_chat_id) - logger.info(f"Iniciando /start automatico para chat_id={chat_id}") + logger.info(f"Starting automatic /start for chat_id={chat_id}") fake_update = _build_fake_update(chat_id, "/start") class SimpleContext: @@ -4991,17 +4957,17 @@ def main(): context = SimpleContext() await start_command(fake_update, context) - logger.info("/start automatico executado com sucesso") + logger.info("/start automatic executed successfully") except Exception as e: err_str = str(e) if "Chat not found" in err_str: - logger.info("Auto /start ignorado: chat ainda nao iniciado pelo usuario.") + logger.info("Auto /start skipped: chat not yet started by user.") else: - logger.error(f"Erro ao executar /start automatico: {e}", exc_info=True) + logger.error(f"Error executing auto /start: {e}", exc_info=True) app_instance.post_init = post_init - logger.info("PsAgentAI está iniciando... ") + logger.info("PsAgentAI is starting... ") app_instance.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__":