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 c34347f..5459328 100644 Binary files a/__pycache__/psagentai_agent.cpython-311.pyc and b/__pycache__/psagentai_agent.cpython-311.pyc differ 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))