From 4cd968a74e11abc328e85f2cfe22d559ddf8edbd Mon Sep 17 00:00:00 2001 From: Marco Costa Date: Mon, 26 Oct 2026 11:28:59 -0300 Subject: [PATCH] feat(vision): add configurable image analysis model and workspace file editing - Add VISION_MODEL and VISION_PROVIDER env options with openrouter default and provider auto-detection from the model id - Add /veriavision, /configiavision and /about commands - Support natural-language editing of files inside workspace/ and a /baixar command to download them - Replace config-based timezone with system-local time detection - Add module version/author/license metadata - Document development conventions in new RULES.md and update README.md/DEPLOY.md with the new commands and env vars --- .env | 4 + DEPLOY.md | 5 + README.md | 13 + RULES.md | 289 +++++++ __pycache__/psagentai_agent.cpython-311.pyc | Bin 233313 -> 278612 bytes config.json | 3 +- psagentai_agent.py | 911 ++++++++++++++++++-- 7 files changed, 1148 insertions(+), 77 deletions(-) create mode 100644 RULES.md diff --git a/.env b/.env index 9230aa2..0b53a8d 100755 --- a/.env +++ b/.env @@ -39,6 +39,10 @@ MAX_HISTORY_LENGTH=50 WEB_SEARCH_ENABLED=true WEB_SEARCH_MAX_RESULTS=5 +# Vision / image analysis model +VISION_MODEL=meta-llama/llama-4-maverick-17b-128e-instruct +VISION_PROVIDER=openrouter + # Upload file context max characters sent to AI (0 = no truncation) UPLOAD_CONTEXT_MAX_CHARS=0 diff --git a/DEPLOY.md b/DEPLOY.md index d9053e2..3bf4fea 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -197,6 +197,8 @@ Nomes recomendados para evitar conflito: | `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`) | --- @@ -204,6 +206,9 @@ Nomes recomendados para evitar conflito: - `/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 --- diff --git a/README.md b/README.md index ca9b114..6fd76e9 100755 --- a/README.md +++ b/README.md @@ -75,6 +75,19 @@ O script cria o ambiente virtual, instala dependências e inicia o agente. - `/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 + +## Editando arquivos do workspace + +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` + +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 diff --git a/RULES.md b/RULES.md new file mode 100644 index 0000000..3fc4c6a --- /dev/null +++ b/RULES.md @@ -0,0 +1,289 @@ +# PsAgentAI - Regras de Desenvolvimento + +Regras oficiais para IAs e desenvolvedores codarem o sistema PsAgentAI de forma correta, padronizada e segura. + +--- + +## 1. Visão Geral do Projeto + +- **Nome:** PsAgentAI +- **Tipo:** Bot Telegram de agente de IA conversacional +- **Linguagem:** 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. + +### 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`. + +--- + +## 2. Padrões de Código + +### 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.2 Estrutura do arquivo principal +Mantenha a ordem lógica: +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 +7. `process_text_message` +8. Funções de criação/edição de arquivos +9. Registro de handlers e `main()` + +Novos handlers devem ser adicionados próximos a funcionalidades similares já existentes. + +### 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. + +--- + +## 3. Convenções de Comandos e Handlers + +### 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.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.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. + +--- + +## 4. Gerenciamento de Perfis + +### 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.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.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. + +--- + +## 5. Contexto de Conversa + +### 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.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. + +--- + +## 6. Workspace e Arquivos + +### 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.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.3 Metadados +- Atualize `uploads_metadata.json` quando necessário. +- Metadados devem conter: `original_name`, `filepath`, `chat_id`, `summary`, `full_text`, `type`. + +--- + +## 7. Integração com IA + +### 7.1 Providers suportados +- `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.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. + +--- + +## 8. Pesquisa na Internet + +### 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.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`). + +--- + +## 9. Análise de Imagens e Visão + +- 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. + +--- + +## 10. Agendamento e Rotina + +### 10.1 Arquivos +- `config.json` — armazena agendamentos. +- Não adicione campos obsoletos (ex: `timezone` foi removido; use horário do servidor). + +### 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`. + +--- + +## 11. Segurança + +- 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. + +--- + +## 12. Testes e Validação + +### 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.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` + +--- + +## 13. Documentação + +### 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.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. + +--- + +## 14. Git e 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. + +### 14.2 Branches +- Trabalhe na branch principal ou crie branches para features específicas. +- Não faça force push sem autorização. + +--- + +## 15. Idioma e Comunicação + +- 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. + +--- + +## 16. Regras Específicas do PsAgentAI + +### 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.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.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. + +--- + +## 17. Versionamento + +- 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. + +--- + +## 18. Contato e Suporte + +- Sistema desenvolvido por: Syspanel.com.br +- Autor: Marco A. S. Costa +- Email: contato@syspanel.com.br +- Licença: Proprietária para syspanel.com.br + +--- + +*Última atualização: Outubro 2026* diff --git a/__pycache__/psagentai_agent.cpython-311.pyc b/__pycache__/psagentai_agent.cpython-311.pyc index c34347fe9f7d54c86f0d73c6dd4b008b20cc14d3..5459328a0ca1493cc959229dbeb41e492f8b6b4b 100644 GIT binary patch delta 74050 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 z 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 Sb*AC<%;>AfsITWMfd3Z?7!_0i delta 42341 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 diff --git a/config.json b/config.json index e8cca41..2214d66 100755 --- a/config.json +++ b/config.json @@ -11,6 +11,5 @@ ], "start_time": "06:00", "end_time": "23:30" - }, - "timezone": "America/Sao_Paulo" + } } \ No newline at end of file diff --git a/psagentai_agent.py b/psagentai_agent.py index 53d7b11..edae490 100755 --- a/psagentai_agent.py +++ b/psagentai_agent.py @@ -4,13 +4,20 @@ PsAgentAI - Personal Agent AI Conversacional Sistema de Personal Agent IA Conversacional via Telegram """ +__version__ = "1.0.0" +__author__ = "Marco A. S. Costa" +__email__ = "contato@syspanel.com.br" +__license__ = "Proprietária para syspanel.com.br" + import os import sys import json import random import re import asyncio +import base64 import logging +import mimetypes from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Optional, Dict, List, Any @@ -22,7 +29,7 @@ except ImportError: import requests from dotenv import load_dotenv -from telegram import Update, Chat, User, Message +from telegram import Update, Chat, User, Message, InputFile from telegram.ext import ( Application, CommandHandler, @@ -71,6 +78,13 @@ MAX_HISTORY_LENGTH = int(os.getenv("MAX_HISTORY_LENGTH", "50")) UPLOAD_CONTEXT_MAX_CHARS = int(os.getenv("UPLOAD_CONTEXT_MAX_CHARS", "8000")) WEB_SEARCH_ENABLED = os.getenv("WEB_SEARCH_ENABLED", "true").lower() in ("true", "1", "yes") WEB_SEARCH_MAX_RESULTS = int(os.getenv("WEB_SEARCH_MAX_RESULTS", "5")) +VISION_MODEL = os.getenv("VISION_MODEL", "meta-llama/llama-4-maverick-17b-128e-instruct") +VISION_PROVIDER = os.getenv("VISION_PROVIDER", "").strip().lower() +if not VISION_PROVIDER: + try: + VISION_PROVIDER = AIProvider.get_provider(VISION_MODEL) + except Exception: + VISION_PROVIDER = "openrouter" USER_PROFILE_FILE = os.getenv("USER_PROFILE_FILE", "user_profile.json") AGENT_PROFILE_FILE = os.getenv("AGENT_PROFILE_FILE", "agent_profile.json") @@ -268,20 +282,16 @@ def save_config(config: Dict[str, Any]): json.dump(config, f, ensure_ascii=False, indent=2) def get_timezone() -> str: - config = load_config() - tz = config.get("timezone", "") - return tz or "UTC" + try: + tz = datetime.now().astimezone().tzinfo + if tz is not None: + return str(tz) + except Exception: + pass + return "UTC" def set_timezone(tz: str) -> bool: - try: - if ZoneInfo is not None: - ZoneInfo(tz) - config = load_config() - config["timezone"] = tz - save_config(config) - return True - except Exception: - return False + return False def _get_timezone_info(tz_name: str): if ZoneInfo is None: @@ -291,6 +301,9 @@ def _get_timezone_info(tz_name: str): except Exception: return None +def get_local_now() -> datetime: + return datetime.now().astimezone() + # ===================== UTILITY FUNCTIONS ===================== def get_today_date() -> str: @@ -313,6 +326,7 @@ def ensure_contexts_dir(): UPLOADS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspace") UPLOADS_META_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads_metadata.json") +WORKSPACE_PROJECTS_DIR = os.path.join(UPLOADS_DIR, "projects") def ensure_uploads_dir(): @@ -461,10 +475,414 @@ def _looks_like_search_request(user_message: str) -> bool: return any(t in q for t in _WEB_SEARCH_TRIGGERS) +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", + "script", + "código", + "codigo", + "app", + "aplicativo", + "sistema", + "site", + "planilha", + "documento", + "pdf", + "json", + "py", + "html", + "css", + "js", + ] + return any(t in q for t in triggers) + + +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"]: + if q.startswith(prefix): + return user_message[len(prefix):].strip(" :,-") + return None + + +def _looks_like_edit_request(user_message: str) -> bool: + q = user_message.lower().strip() + triggers = [ + "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", + "arquivo", + "projeto", + "script", + "código", + "codigo", + ] + if any(t in q for t in triggers): + if any(name in q for name in ["leia", "lista", "mostre", "exiba", "quais"]): + return False + return True + return False + + +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"]: + if q.startswith(prefix): + rest = user_message[len(prefix):].strip(" :,-") + return rest + return None + + +def _extract_filename_from_text(text: str) -> Optional[str]: + matches = re.findall(r"[A-Za-z0-9_./-]+\.[A-Za-z0-9]+", text) + if matches: + return matches[0] + return None + + +def _find_workspace_file(target: str) -> Optional[str]: + filename = _extract_filename_from_text(target) + if not filename and target and " " not in target: + filename = target + if not filename: + return None + target_lower = filename.lower().strip() + base = os.path.splitext(target_lower)[0] + candidates = [] + for root, dirs, files in os.walk(UPLOADS_DIR): + for fname in files: + full = os.path.join(root, fname) + rel = os.path.relpath(full, UPLOADS_DIR) + name_lower = fname.lower() + if target_lower in name_lower or base in name_lower or target_lower in rel.lower(): + candidates.append(rel) + if not candidates: + return None + candidates.sort(key=lambda x: (x.count(os.sep), len(x))) + return candidates[0] + + +async def _handle_edit_request(update: Update, context: ContextTypes.DEFAULT_TYPE, user_message: str) -> bool: + target = _extract_edit_target(user_message) + if not target: + 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}") + 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}") + 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}`.") + 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.") + 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." + ) + 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.") + return True + if new_content.startswith("```"): + new_content = new_content.lstrip("`") + new_content = new_content.split("\n", 1)[-1] if "\n" in new_content else new_content + if new_content.endswith("```"): + new_content = new_content[:-3].rstrip() + new_content = new_content.strip() + try: + with open(abs_path, "w", encoding="utf-8") as f: + 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.") + 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}", + parse_mode=ParseMode.HTML, + ) + return True + + +def ensure_workspace_dirs(): + Path(UPLOADS_DIR).mkdir(parents=True, exist_ok=True) + Path(WORKSPACE_PROJECTS_DIR).mkdir(parents=True, exist_ok=True) + + +def save_workspace_file(relative_path: str, content: str) -> str: + ensure_workspace_dirs() + dest = os.path.join(UPLOADS_DIR, relative_path) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as f: + f.write(content) + return dest + + +def build_workspace_tree(relative_path: str, content: str) -> List[str]: + ensure_workspace_dirs() + created = [] + if "\n" in content and any(line.startswith(("## ", "# ", "FILE:", "DIR:", "./", "/")) for line in content.splitlines()[:10]): + current_dir = "" + for line in content.splitlines(): + line = line.strip() + if not line: + continue + if line.startswith(("## ", "# ")): + current_dir = line.lstrip("# ").strip() + Path(os.path.join(UPLOADS_DIR, current_dir)).mkdir(parents=True, exist_ok=True) + created.append(current_dir) + elif line.startswith(("FILE:", "./", "/")) or (current_dir and line.endswith((".py", ".html", ".css", ".js", ".json", ".md", ".txt"))): + name = line.split(":", 1)[-1].strip() if ":" in line else line + if name: + dest = os.path.join(UPLOADS_DIR, current_dir, name) if current_dir else os.path.join(UPLOADS_DIR, name) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as f: + f.write("") + created.append(os.path.relpath(dest, UPLOADS_DIR)) + else: + dest = os.path.join(UPLOADS_DIR, relative_path) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as f: + f.write(content) + created.append(relative_path) + return created + + +def _list_workspace_files() -> List[str]: + ensure_workspace_dirs() + files = [] + for root, _, filenames in os.walk(UPLOADS_DIR): + for filename in filenames: + abs_path = os.path.join(root, filename) + rel_path = os.path.relpath(abs_path, UPLOADS_DIR) + files.append(rel_path) + files.sort() + return files + + +async def _handle_create_request(update: Update, context: ContextTypes.DEFAULT_TYPE, user_message: str) -> bool: + suggestion = _extract_create_suggestion(user_message) + if not suggestion: + return False + await update.message.reply_text(f"🧩 Vou criar isso para você: \"{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" + ) + 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.") + 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") + root_name = f"{timestamp}_{safe_root}" + try: + 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, + 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.") + return True + + def get_all_uploads_for_chat(chat_id: int) -> Dict[str, Dict[str, Any]]: meta = load_uploads_metadata() return {fn: info for fn, info in meta.items() if info.get("chat_id") == chat_id} + +def encode_image_to_base64(filepath: str) -> Optional[str]: + try: + with open(filepath, "rb") as f: + data = f.read() + mime, _ = mimetypes.guess_type(filepath) + if not mime or not mime.startswith("image/"): + mime = "image/jpeg" + b64 = base64.b64encode(data).decode("utf-8") + return f"data:{mime};base64,{b64}" + except Exception: + return None + + +def analyze_image(filepath: str, question: str = "Descreva o que você vê nesta imagem em português.") -> Optional[str]: + image_data = encode_image_to_base64(filepath) + if not image_data: + return None + provider = VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL) + api_key = AIProvider.get_api_key(provider) + api_url = AIProvider.get_api_url(provider) + if not api_key or provider == "llamacpp": + return None + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": question}, + {"type": "image_url", "image_url": {"url": image_data}}, + ], + } + ] + try: + result, _ = AIProvider._chat_openai_compatible(provider, api_key, api_url, VISION_MODEL, messages, temperature=0.7, max_tokens=2000) + return result + except Exception: + return None + + +def save_image_to_workspace(original_name: str, filepath: str, chat_id: int) -> str: + ensure_uploads_dir() + meta = load_uploads_metadata() + if original_name: + filename = original_name + dest = os.path.join(UPLOADS_DIR, filename) + counter = 1 + while os.path.exists(dest): + base, ext = os.path.splitext(filename) + dest = os.path.join(UPLOADS_DIR, f"{base}_{counter}{ext}") + counter += 1 + else: + ext = os.path.splitext(filepath)[1] or ".jpg" + filename = datetime.now().strftime("photo_%Y%m%d_%H%M%S") + ext + dest = os.path.join(UPLOADS_DIR, filename) + import shutil + shutil.copy2(filepath, dest) + meta[filename] = { + "original_name": original_name or filename, + "filepath": dest, + "chat_id": chat_id, + "uploaded_at": datetime.now().isoformat(), + "summary": "[Imagem]", + "full_text": "[Imagem]", + "type": "image", + } + save_uploads_metadata(meta) + return filename + + +async def photo_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + await auto_detect_chat_id(update) + photo = update.message.photo + if not photo: + return + await update.message.reply_text("📸 Recebi a imagem, processando...") + largest = photo[-1] + original_name = "" + if update.message.document: + original_name = update.message.document.file_name or "" + file = await context.bot.get_file(largest.file_id) + ext = ".jpg" + if original_name: + ext = os.path.splitext(original_name)[1] or ext + tmp_path = f"/tmp/photo_{largest.file_id}{ext}" + try: + await file.download_to_drive(tmp_path) + except Exception: + await update.message.reply_text("Erro ao baixar a imagem. Tente novamente.") + return + filename = save_image_to_workspace(original_name, tmp_path, update.effective_chat.id) + try: + os.remove(tmp_path) + except Exception: + pass + analysis = analyze_image(os.path.join(UPLOADS_DIR, filename)) + if analysis: + await update.message.reply_text(f"📸 Analisei a imagem:\n\n{analysis}") + else: + await update.message.reply_text("📸 Imagem salva em workspace. Não consegui analisar agora.") + + def load_context(date_str: Optional[str] = None) -> List[Dict[str, str]]: ensure_contexts_dir() file_path = get_context_file_path(date_str) @@ -1368,15 +1786,13 @@ def build_system_prompt(profile: Dict[str, Any]) -> str: if profile.get("notes"): prompt += f"\nNotas adicionais: {profile['notes']}" try: - tz = get_timezone() - tz_info = _get_timezone_info(tz) - now = datetime.now(tz_info) + 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: {tz}." + prompt += f"\nFuso horário configurado: {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." @@ -2103,50 +2519,19 @@ async def atualizarsistema_command(update: Update, context: ContextTypes.DEFAULT async def fusohorario_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(): - tz = get_timezone() - tz_info = _get_timezone_info(tz) - if tz_info: - now = datetime.now(tz_info) - time_str = now.strftime("%Y-%m-%d %H:%M:%S") - else: - time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - await update.message.reply_text( - f"🕐 Fuso horário atual: {tz}\n" - f"Hora atual: {time_str}\n\n" - f"Para alterar: /fusohorario \n" - f"Exemplo: /fusohorario America/Sao_Paulo\n\n" - f"Fusos comuns:\n" - f"• America/Sao_Paulo\n" - f"• America/New_York\n" - f"• Europe/London\n" - f"• Europe/Lisbon\n" - f"• Asia/Tokyo\n" - f"• UTC" - ) - return - - new_tz = parts[1].strip() - if set_timezone(new_tz): - tz_info = _get_timezone_info(new_tz) - if tz_info: - now = datetime.now(tz_info) - time_str = now.strftime("%Y-%m-%d %H:%M:%S") - else: - time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - await update.message.reply_text( - f"✅ Fuso horário alterado para {new_tz}\n" - f"Hora atual: {time_str}" - ) + tz = get_timezone() + tz_info = _get_timezone_info(tz) + if tz_info: + now = datetime.now(tz_info) + time_str = now.strftime("%Y-%m-%d %H:%M:%S") else: - await update.message.reply_text( - f"❌ Fuso horário inválido: {new_tz}\n\n" - f"Use /fusohorario para ver o fuso atual.\n" - f"Exemplos válidos: America/Sao_Paulo, America/New_York, Europe/London, UTC, etc." - ) + 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." + ) # ===================== AGENDAMENTO ===================== @@ -3271,25 +3656,74 @@ async def listfiles_command(update: Update, context: ContextTypes.DEFAULT_TYPE): chat_id = update.effective_chat.id files = get_all_uploads_for_chat(chat_id) - if not files: - await update.message.reply_text( - "📂 Nenhum arquivo enviado ainda.\n" - "Envie um .pdf, .txt, .docx, .png ou .jpg para eu analisar!" + lines = ["📂 **Seus arquivos (workspace):**", ""] + + meta_files = [] + for filename, info in files.items(): + orig = info.get("original_name", filename) + meta_files.append((orig, info.get("summary", ""))) + + workspace_files = _list_workspace_files() + workspace_only = [] + for rel_path in workspace_files: + display = os.path.basename(rel_path) + if not any(rel_path == info.get("filepath", "").replace(UPLOADS_DIR + os.sep, "") or display == orig for orig, _ in meta_files): + workspace_only.append((display, "")) + + 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!" ) return - lines = ["📂 **Seus arquivos enviados:**", ""] - for filename, info in files.items(): - orig = info.get("original_name", filename) - summary = info.get("summary", "") + shown = set() + for orig, summary in meta_files: preview = summary[:80] + "..." if len(summary) > 80 else summary lines.append("📄 **" + orig + "**") if preview: lines.append(" _" + preview + "_") lines.append("") + shown.add(orig) - lines.append("Pergunte sobre qualquer arquivo pelo nome!") - await update.message.reply_text("\n".join(lines)) + for display, summary in workspace_only: + lines.append("📄 **" + display + "**") + lines.append("") + + lines.append("Use `/baixar ` para baixar um arquivo.") + 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.") + return + filename = os.path.basename(rel_path) + ext = os.path.splitext(filename)[1].lower() + try: + if ext in {".png", ".jpg", ".jpeg", ".gif", ".webp"}: + with open(abs_path, "rb") as f: + await context.bot.send_photo(chat_id=update.effective_chat.id, photo=InputFile(f, filename=filename)) + else: + with open(abs_path, "rb") as f: + 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.") + + +async def baixar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await auto_detect_chat_id(update) + if not context.args: + await update.message.reply_text("Uso: /baixar ") + 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}") + return + await _send_workspace_file(update, context, rel_path) async def _transcribe_voice_file(ogg_path: str) -> Optional[str]: @@ -3373,8 +3807,19 @@ async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE) ensure_uploads_dir() file_id = document.file_id - safe_name = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + file_id[:8] + ext - filepath = os.path.join(UPLOADS_DIR, safe_name) + if ext in (".png", ".jpg", ".jpeg"): + filename = original_name + dest = os.path.join(UPLOADS_DIR, filename) + counter = 1 + while os.path.exists(dest): + base, ext_part = os.path.splitext(filename) + dest = os.path.join(UPLOADS_DIR, f"{base}_{counter}{ext_part}") + counter += 1 + else: + filename = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + file_id[:8] + ext + dest = os.path.join(UPLOADS_DIR, filename) + + filepath = dest await update.message.reply_text("Baixando " + original_name + "...") @@ -3386,12 +3831,22 @@ async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE) await update.message.reply_text("Erro ao baixar o arquivo. Tente novamente.") return - had_text = register_upload(safe_name, original_name, filepath, update.effective_chat.id) + had_text = register_upload(filename, original_name, filepath, update.effective_chat.id) meta = load_uploads_metadata() - info = meta.get(safe_name, {}) + info = meta.get(filename, {}) summary = info.get("summary", "") + if ext in (".png", ".jpg", ".jpeg"): + analysis = analyze_image(filepath) + if analysis: + summary = analysis + info["summary"] = summary + info["full_text"] = analysis + meta[filename] = info + save_uploads_metadata(meta) + had_text = True + msg = "Arquivo salvo: " + original_name + "\n" if had_text and summary: preview = summary[:200] + "..." if len(summary) > 200 else summary @@ -3403,7 +3858,6 @@ async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE) msg += "Agora voce pode perguntar sobre este arquivo pelo nome." await update.message.reply_text(msg) - # ===================== TELEGRAM HANDLERS ===================== @@ -3421,6 +3875,141 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): text=greeting, ) +async def manualdeuso_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await auto_detect_chat_id(update) + manual = """📚 Manual de Uso do PsAgentAI + +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. + +━━━━━━━━━━━━━━━━━━━━━━ +1️⃣ PERFIL DO USUÁRIO +━━━━━━━━━━━━━━━━━━━━━━ +• /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 + +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: ..." + +━━━━━━━━━━━━━━━━━━━━━━ +2️⃣ 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 + +━━━━━━━━━━━━━━━━━━━━━━ +3️⃣ CONFIGURAÇÃO DE IA +━━━━━━━━━━━━━━━━━━━━━━ +• /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 + +Os modelos de IA são definidos no arquivo .env e podem ser trocados a qualquer momento pelos comandos acima. + +━━━━━━━━━━━━━━━━━━━━━━ +4️⃣ ARQUIVOS E DOCUMENTOS +━━━━━━━━━━━━━━━━━━━━━━ +• /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 + +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 + +━━━━━━━━━━━━━━━━━━━━━━ +5️⃣ CRIAÇÃO E EDIÇÃO DE ARQUIVOS +━━━━━━━━━━━━━━━━━━━━━━ +Você pode pedir para o Agent criar ou editar arquivos no 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" + +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" + +Os arquivos são salvos em workspace/ de forma organizada. + +━━━━━━━━━━━━━━━━━━━━━━ +6️⃣ VOZ E IMAGENS +━━━━━━━━━━━━━━━━━━━━━━ +• 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 + +━━━━━━━━━━━━━━━━━━━━━━ +7️⃣ PESQUISA NA INTERNET +━━━━━━━━━━━━━━━━━━━━━━ +O Agent pesquisa automaticamente quando perceber que a pergunta precisa de informação atualizada. + +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" + +━━━━━━━━━━━━━━━━━━━━━━ +8️⃣ AGENDAMENTO E ROTINA +━━━━━━━━━━━━━━━━━━━━━━ +• /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 + +━━━━━━━━━━━━━━━━━━━━━━ +9️⃣ 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 +• /cancelar — cancela uma operação em andamento + +━━━━━━━━━━━━━━━━━━━━━━ +🔟 UTILITÁRIOS +━━━━━━━━━━━━━━━━━━━━━━ +• /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 + +━━━━━━━━━━━━━━━━━━━━━━ +💡 DICAS IMPORTANTES +━━━━━━━━━━━━━━━━━━━━━━ +• 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 + +━━━━━━━━━━━━━━━━━━━━━━ +📞 SUPORTE +━━━━━━━━━━━━━━━━━━━━━━ +Este sistema foi desenvolvido por Syspanel.com.br. +Para personalizações ou suporte: contato@syspanel.com.br + +Todos os direitos reservados.""" + 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: @@ -3921,6 +4510,18 @@ async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYP if profile_updated: return + if _looks_like_edit_request(user_message): + edited = await _handle_edit_request(update, context, user_message) + print("[PROCESS] edit handled=", edited) + if edited: + return + + if _looks_like_create_request(user_message): + created = await _handle_create_request(update, context, user_message) + print("[PROCESS] create handled=", created) + if created: + return + add_to_context("user", user_message) print("[PROCESS] added context, getting AI response...") @@ -3987,6 +4588,30 @@ async def reload_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Configurações recarregadas com sucesso!") +async def about_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await auto_detect_chat_id(update) + 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" + 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." + ) + await update.message.reply_text(msg, parse_mode=ParseMode.HTML) + + IA_CONFIG_STEPS = ["provider", "model"] IA_CONFIG_ANSWERS = {} CONFIGIA_PROVIDERS = [ @@ -4150,6 +4775,135 @@ async def handle_configia_message(update: Update, context: ContextTypes.DEFAULT_ return False +VISION_CONFIG_STEPS: List[str] = [] +VISION_CONFIG_ANSWERS: Dict[str, Any] = {} +VISION_CONFIG_PROVIDERS = [ + ("openrouter", "OpenRouter"), + ("stepfun", "StepFun"), + ("qwencloud", "QwenCloud"), +] +VISION_CONFIG_MODELS = { + "openrouter": [ + "meta-llama/llama-4-maverick-17b-128e-instruct", + "meta-llama/llama-4-maverick:free", + "google/gemini-2.0-flash-exp:free", + "custom...", + ], + "stepfun": [ + "step-2-16k", + "custom...", + ], + "qwencloud": [ + "qwen-vl-plus", + "qwen-vl-max", + "custom...", + ], +} + + +async def veriavision_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" + f"Provider: {provider}\n" + f"Modelo: {VISION_MODEL}" + ) + await update.message.reply_text(msg) + + +async def configiavision_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"] + VISION_CONFIG_ANSWERS = {} + keyboard = InlineKeyboardMarkup( + [[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:", + reply_markup=keyboard, + ) + + +async def configiavision_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + if not query: + return + await query.answer() + data = (query.data or "").strip() + global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS + if data.startswith("vision_provider_"): + provider = data.replace("vision_provider_", "").strip() + if provider not in {p for p, _ in VISION_CONFIG_PROVIDERS}: + return + VISION_CONFIG_ANSWERS["provider"] = provider + _update_env_var("VISION_PROVIDER", provider) + models = VISION_CONFIG_MODELS.get(provider, ["custom..."]) + keyboard = InlineKeyboardMarkup( + [[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:", + 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.") + VISION_CONFIG_STEPS = ["custom_model"] + return + model = raw_model + provider = VISION_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) + _update_env_var("VISION_MODEL", model) + _update_env_var("VISION_PROVIDER", provider) + global VISION_MODEL, VISION_PROVIDER + VISION_MODEL = model + VISION_PROVIDER = provider + VISION_CONFIG_STEPS = [] + VISION_CONFIG_ANSWERS = {} + _reload_env() + await query.edit_message_text( + "Modelo de visão atualizado.\n" + f"Provider: {(VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper()}\n" + f"Modelo: {VISION_MODEL}" + ) + return + + +async def handle_configiavision_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS + if not VISION_CONFIG_STEPS: + return False + step = VISION_CONFIG_STEPS[0] + text = (update.message.text or "").strip() + if not text: + return False + if step == "custom_model": + model = text.strip() + if not model: + await update.message.reply_text("Modelo inválido. Envie o identificador do modelo.") + return True + provider = VISION_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) + _update_env_var("VISION_MODEL", model) + _update_env_var("VISION_PROVIDER", provider) + global VISION_MODEL, VISION_PROVIDER + VISION_MODEL = model + VISION_PROVIDER = provider + VISION_CONFIG_STEPS = [] + VISION_CONFIG_ANSWERS = {} + _reload_env() + await update.message.reply_text( + "Modelo de visão atualizado.\n" + f"Provider: {(VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper()}\n" + f"Modelo: {VISION_MODEL}" + ) + return True + return False + + # ===================== MAIN ===================== def _build_fake_update(chat_id: int, text: str) -> Update: @@ -4179,6 +4933,7 @@ def main(): 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("agentprofile", agent_profile_command)) @@ -4194,6 +4949,7 @@ def main(): 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)) @@ -4208,8 +4964,13 @@ def main(): app_instance.add_handler(CommandHandler("excluiratividade", excluiratividade_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(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)) app_instance.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler))