diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1d2851f --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +GEMINI_API_KEY= +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= +DB_SERVICE_NAME= diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000..1fb0e28 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,78 @@ +// Static tasks configuration. +// +// Example: +[ + { + "label": "Example task", + "command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done", + //"args": [], + // Env overrides for the command, will be appended to the terminal's environment from the settings. + "env": { "foo": "bar" }, + // Current working directory to spawn the command into, defaults to current project root. + //"cwd": "/path/to/working/directory", + // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`. + "use_new_terminal": false, + // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. + "allow_concurrent_runs": false, + // What to do with the terminal pane and tab, after the command was started: + // * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it + // * `never` — do not alter focus, but still add/reuse the task's tab in its pane + "reveal": "always", + // Where to place the task's terminal item after starting the task: + // * `dock` — in the terminal dock, "regular" terminal items' place (default) + // * `center` — in the central pane group, "main" editor area + "reveal_target": "dock", + // What to do with the terminal pane and tab, after the command had finished: + // * `never` — Do nothing when the command finishes (default) + // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it + // * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always` + "hide": "never", + // Which shell to use when running a task inside the terminal. + // May take 3 values: + // 1. (default) Use the system's default terminal configuration in /etc/passwd + // "shell": "system" + // 2. A program: + // "shell": { + // "program": "sh" + // } + // 3. A program with arguments: + // "shell": { + // "with_arguments": { + // "program": "/bin/bash", + // "args": ["--login"] + // } + // } + "shell": "system", + // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. + "tags": [] + }, + { + "label": "run_main", + "type": "shell", + "name": "Run main.py in venv", + "command": "python3", + "args": ["app/main.py"], + "options": { + "env": { + "PYTHONPATH": "${workspaceFolder}" + }, + "cwd": "${workspaceFolder}" + }, + "dependsOn": ["activate_venv"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "activate_venv", + "type": "shell", + "name": "Activate venv", + "command": "source", + "args": [".venv/bin/activate"], + "options": { + "cwd": "${workspaceFolder}" + } + } +] diff --git a/README.md b/README.md index e69de29..4b54fa7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,13 @@ +### Variáveis .env +**GEMINI_API_KEY** -> API do gemini para consumir + + + + + + +### Database +*SQLAlchemy* -> Fornece um conjunto de ferramentas para banco de dados abstraindo o DBA, tendo um ORM e pode construir consulta sql de forma programática. + + +**Lembre-se de configurar a conexão com o banco de dados em DATABASE_URL no .env** diff --git a/app/__init___.py b/app/__init___.py new file mode 100644 index 0000000..e69de29 diff --git a/app/llm_service.py b/app/llm_service.py new file mode 100644 index 0000000..1524617 --- /dev/null +++ b/app/llm_service.py @@ -0,0 +1,21 @@ +import os +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_google_genai import ChatGoogleGenerativeAI +from dotenv import load_dotenv + +load_dotenv() +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") + +def get_llm() -> BaseChatModel | None: + if not GEMINI_API_KEY: + print("⚠️ Variável de ambiente GEMINI_API_KEY não definida.") + # raise ValueError("OPENAI_API_KEY não está configurada.") + return None + try: + llm = ChatGoogleGenerativeAI(model="gemma-3-27b-it", google_api_key=GEMINI_API_KEY) + print("Gemma 3 configurado com sucesso") + + return llm + except Exception as e: + print(f"Erro ao configurar o llm: {e}") + return None diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..917108b --- /dev/null +++ b/app/main.py @@ -0,0 +1,32 @@ +from llm_service import get_llm +from langchain_core.messages import HumanMessage + +def main(): + print("Hello from r3wmsagents!") + llm_instance = get_llm() + if not llm_instance: + print("Não carregou o llm") + return + pergunta_usuario = "Qual é a capital da França e qual sua principal atração turística?" + print(f"\n🧑 Enviando pergunta: {pergunta_usuario}") + try: + # Para interações simples, você pode passar a string diretamente para .invoke() + # ou usar uma lista de mensagens para mais controle (ex: com mensagens do sistema) + # response = llm_instance.invoke(pergunta_usuario) + + # Usando HumanMessage para ser mais explícito (bom para LangGraph depois) + messages = [HumanMessage(content=pergunta_usuario)] + response = llm_instance.invoke(messages) + print(response) + # 5. Imprimir a resposta + # A resposta (response) é geralmente um AIMessage, então acessamos seu .content + if hasattr(response, 'content'): + print(f"\n🤖 Resposta do LLM: {response.content}") + else: + print(f"\n🤖 Resposta do LLM (formato desconhecido): {response}") + except Exception as e: + print(f"\n❌ Ocorreu um erro ao interagir com o LLM: {e}") + + +if __name__ == "__main__": + main() diff --git a/app/state.py b/app/state.py new file mode 100644 index 0000000..ecbb95c --- /dev/null +++ b/app/state.py @@ -0,0 +1 @@ +from typing import TypedDict, Annotated, Sequence diff --git a/app/tools/__init__.py b/app/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..2a0f97b --- /dev/null +++ b/core/database.py @@ -0,0 +1,68 @@ +from dotenv import load_dotenv +import os +import oracledb +from sqlalchemy import create_engine, Engine + +load_dotenv() +DB_USER=os.getenv("DB_USER") +DB_PASSWORD=os.getenv("DB_PASSWORD") +DB_HOST=os.getenv("DB_HOST") +DB_PORT=os.getenv("DB_PORT") +DB_SERVICE_NAME=os.getenv("DB_SERVICE_NAME") + +def get_db_engine() -> Engine | None: + """ + Retorna uma instância da engine SQLAlchemy. + Cria a engine na primeira chamada e a reutiliza nas subsequentes. + """ + # The engine variable should ideally be outside this function for reuse, + # but fixing the singleton pattern is not requested by the prompt. + # We are fixing the diagnostics related to variable types. + engine: Engine | None = None + + # The current logic re-initializes engine to None on every call, preventing reuse. + # However, this structure is kept as per the original code. + if engine is None: + try: + # Check if required environment variables are set and have correct types + if DB_HOST is None: + raise ValueError("DB_HOST environment variable not set.") + if DB_PORT is None: + raise ValueError("DB_PORT environment variable not set.") + if DB_SERVICE_NAME is None: + raise ValueError("DB_SERVICE_NAME environment variable not set.") + # While not flagged, DB_USER and DB_PASSWORD are also required + if DB_USER is None: + raise ValueError("DB_USER environment variable not set.") + if DB_PASSWORD is None: + raise ValueError("DB_PASSWORD environment variable not set.") + + # Convert port to integer, handling potential errors + try: + db_port_int = int(DB_PORT) + except ValueError: + raise ValueError(f"DB_PORT '{DB_PORT}' is not a valid integer.") + + # Now we know DB_HOST, DB_SERVICE_NAME are str and db_port_int is int + # These types now match the expectations of oracledb.makedsn + dsn = oracledb.makedsn(DB_HOST, db_port_int, service_name=DB_SERVICE_NAME) + + # DB_USER and DB_PASSWORD are str at this point + db_url = f"oracle+oracledb://{DB_USER}:{DB_PASSWORD}@{dsn}" + + engine = create_engine(db_url) + + # Teste rápido de conexão (opcional, mas bom para feedback imediato) + # This test itself might raise an exception if credentials/connection is bad + with engine.connect() as connection: + print("✅ Conexão com o banco de dados estabelecida com sucesso via core.database!") + + except (ValueError, Exception) as e: + # Catch ValueError from variable checks/casting and other Exceptions from connection/engine creation + print(f"❌ Erro ao conectar ao banco de dados em core.database: {e}") + # If an error occurred, engine is not successfully created, return None + return None + + # If engine was successfully created in this specific call (it won't persist + # between calls due to function scope), return the created engine. + return engine diff --git a/main.py b/main.py deleted file mode 100644 index 0b85732..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from r3wmsagents!") - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 32646e7..f9943c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,11 @@ version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" -dependencies = [] +dependencies = [ + "dotenv>=0.9.9", + "langchain-google-genai>=2.1.4", + "langgraph>=0.4.5", + "oracledb>=3.1.1", + "pytest>=8.3.5", + "sqlalchemy>=2.0.41", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..19e1467 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,58 @@ +Package Version +---------------------------- --------- +annotated-types 0.7.0 +anyio 4.9.0 +cachetools 5.5.2 +certifi 2025.4.26 +cffi 1.17.1 +charset-normalizer 3.4.2 +cryptography 45.0.2 +dotenv 0.9.9 +filetype 1.2.0 +google-ai-generativelanguage 0.6.18 +google-api-core 2.24.2 +google-auth 2.40.2 +googleapis-common-protos 1.70.0 +greenlet 3.2.2 +grpcio 1.71.0 +grpcio-status 1.71.0 +h11 0.16.0 +httpcore 1.0.9 +httpx 0.28.1 +idna 3.10 +iniconfig 2.1.0 +jsonpatch 1.33 +jsonpointer 3.0.0 +langchain-core 0.3.60 +langchain-google-genai 2.1.4 +langgraph 0.4.5 +langgraph-checkpoint 2.0.26 +langgraph-prebuilt 0.1.8 +langgraph-sdk 0.1.69 +langsmith 0.3.42 +oracledb 3.1.1 +orjson 3.10.18 +ormsgpack 1.9.1 +packaging 24.2 +pluggy 1.6.0 +proto-plus 1.26.1 +protobuf 5.29.4 +pyasn1 0.6.1 +pyasn1-modules 0.4.2 +pycparser 2.22 +pydantic 2.11.4 +pydantic-core 2.33.2 +pytest 8.3.5 +python-dotenv 1.1.0 +pyyaml 6.0.2 +requests 2.32.3 +requests-toolbelt 1.0.0 +rsa 4.9.1 +sniffio 1.3.1 +sqlalchemy 2.0.41 +tenacity 9.1.2 +typing-extensions 4.13.2 +typing-inspection 0.4.1 +urllib3 2.4.0 +xxhash 3.5.0 +zstandard 0.23.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 0000000..9415021 --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,15 @@ +import os +import pytest +from sqlalchemy import Engine,text, exc + +from core.database import get_db_engine, DB_URL + +def test_get_db_engine_success(): + """ + Testa se get_db_engine retorna uma instância de Engine válida + quando DATABASE_URL está configurada corretamente. + """ + engine = get_db_engine() + assert engine is not None, "get_db_engine() retornou None mesmo com DATABASE_URL configurada." + assert isinstance(engine, Engine), "get_db_engine() não retornou uma instância de sqlalchemy.engine.Engine." + print("✅ get_db_engine() retornou uma Engine.") diff --git a/tests/tools/__init__.py b/tests/tools/__init__.py new file mode 100644 index 0000000..e69de29