Uma interface conversacional nativa de terminal alimentada pelo Agent Development Kit (ADK) do Google. Em vez de construir mais um wrapper de chatbot, este projeto explora como a abstração Runner/Session do ADK permite conversas stateful e aumentadas com ferramentas, com código de integração mínimo -- tudo pela linha de comando.
A motivação é simples: a maioria das ferramentas CLI para LLMs não possui persistência de sessão ou exige frameworks pesados. Este projeto demonstra que a arquitetura opinativa do ADK (Agent + Runner + SessionService) pode substituir centenas de linhas de lógica de orquestração customizada, oferecendo streaming, invocação de ferramentas e histórico de conversas prontos para uso.
graph TD
User([User Terminal])
CLI[Typer CLI<br/>cli.py]
Config[Settings<br/>config.py]
AgentFactory[Agent Factory<br/>agent.py]
Runner[ADK Runner<br/>runner.py]
SessionMgr[Session Manager<br/>session.py]
SessionSvc[InMemorySessionService]
Agent[ADK Agent]
LLM[Gemini / LiteLLM<br/>Model Provider]
Tools[Tool Functions<br/>tools.py]
User -->|typed input| CLI
CLI --> Config
CLI --> AgentFactory
CLI --> Runner
CLI --> SessionMgr
AgentFactory --> Agent
Agent --> Tools
SessionMgr --> SessionSvc
Runner -->|run_async / streaming| Agent
Runner --> SessionSvc
Agent -->|function calling| Tools
Agent -->|inference| LLM
Runner -->|streamed chunks| CLI
CLI -->|Rich markdown| User
Fluxo de dados: A entrada do usuário chega ao Typer CLI, que cria (ou retoma) uma sessão através do SessionManager, passa a mensagem ao Runner, e o Runner orquestra a interação do Agent com o LLM. As respostas retornam via streaming através de geradores assíncronos para o Rich, que formata a saída no terminal.
O LangChain oferece máxima flexibilidade ao custo de uma grande superfície de configuração. Para um chat CLI com agente único, o Runner.run_async() do ADK gerencia todo o ciclo de vida de execução do agente -- roteamento de mensagens, despacho de ferramentas, atualização de estado da sessão -- em um único iterador assíncrono. Não há composição de chains, nem gerenciador de callbacks, nem output parser para configurar. O trade-off é menos flexibilidade para orquestração multi-agente, o que é aceitável para este caso de uso.
O Typer oferece parsing de argumentos com tipagem segura e texto de ajuda gerado automaticamente a partir das assinaturas de funções. Para uma CLI com três comandos (chat, history, config), ele elimina boilerplate e entrega uma experiência de usuário polida. O utilitário de teste CliRunner também torna os testes de integração triviais.
As respostas de LLMs frequentemente vêm formatadas em markdown. O renderizador Markdown do Rich lida nativamente com blocos de código, negrito/itálico e listas no terminal. Combinado com Panel para metadados de sessão e chamadas de print() com streaming, ele proporciona uma experiência de saída profissional sem a necessidade de um framework TUI.
O ADK fornece InMemorySessionService e DatabaseSessionService (com SQLite). A variante em memória foi escolhida pela simplicidade: as sessões existem durante o tempo de vida do processo, o que corresponde ao padrão típico de uso de CLI com uma única conversa. O módulo session.py é estruturado para permitir troca de implementação via configuração session_backend.
A função run_agent_streaming() encapsula o Runner.run_async() do ADK como um AsyncGenerator[str, None]. Cada evento do runner é filtrado por conteúdo textual e emitido imediatamente, permitindo saída caractere por caractere no terminal:
async for chunk in run_agent_streaming(agent, session_service, message, ...):
console.print(chunk, end="")Este padrão evita o buffering da resposta completa e proporciona responsividade percebida. A variante não-streaming run_agent() coleta todas as partes em uma única string para renderização em lote com Rich Markdown.
As sessões são identificadas por UUID e gerenciadas através da interface SessionService do ADK. A função get_or_create_session() implementa um padrão upsert: se um ID de sessão válido é fornecido e existe, a sessão é retomada; caso contrário, uma nova sessão é criada. Isso habilita a flag --session <id> para continuar conversas anteriores.
Toda a configuração é carregada a partir de variáveis de ambiente (com suporte a arquivo .env) via pydantic-settings:
| Variável | Padrão | Descrição |
|---|---|---|
GOOGLE_API_KEY |
(obrigatório) | Chave de API do Google AI para modelos Gemini |
DEFAULT_MODEL |
gemini-2.0-flash |
Identificador do modelo passado ao ADK Agent |
SESSION_BACKEND |
memory |
Backend de armazenamento de sessão (memory ou sqlite) |
SQLITE_DB_PATH |
./chat_history.db |
Caminho do banco SQLite quando o backend é sqlite |
APP_NAME |
llm-cli-chat |
Nome da aplicação para escopo de sessão |
USER_ID |
default-user |
Identificador do usuário para escopo de sessão |
git clone https://github.com/yourusername/llm-cli-chat.git
cd llm-cli-chat
uv sync
cp .env.example .env
# Add your GOOGLE_API_KEY to .env# Interactive chat (streaming enabled by default)
uv run llm-chat chat
# Specify model
uv run llm-chat chat --model gemini-1.5-pro
# Non-streaming mode (full response with markdown rendering)
uv run llm-chat chat --no-stream
# Resume a previous session
uv run llm-chat chat --session <session-id>
# View conversation history
uv run llm-chat history <session-id>
# Show current config
uv run llm-chat configdocker build -t llm-cli-chat .
docker run -it --env-file .env llm-cli-chat chatuv sync --dev
# Run tests with coverage
uv run pytest --cov=src --cov-report=term-missing -v
# Lint and format
uv run ruff check --fix .
uv run ruff format .
# Type check
uv run mypy src/- Sessões em memória não persistem entre reinicializações. Fechar a CLI perde todo o histórico de conversa. A configuração de backend
sqliteexiste no config, mas a implementação completa exigiria oDatabaseSessionServicedo ADK ou um adaptador customizado. - Sem autenticação ou suporte multi-usuário. O
user_idé um valor estático de configuração. Em um ambiente compartilhado, todas as sessões pertencem ao mesmo usuário. - Arquitetura de agente único. Não há camada de roteamento ou delegação entre agentes. O
chat_agentúnico lida com todas as consultas e chamadas de ferramentas. Para tarefas especializadas, múltiplos agentes com um dispatcher seriam mais apropriados. - Sandboxing de ferramentas é mínimo. A calculadora usa
eval()com builtins restritos, o que é adequado para aritmética, mas precisaria de um parser adequado (ex.:ast.literal_evalou uma biblioteca de expressões matemáticas) para uso em produção. - Sem retry ou tratamento de rate-limit. O Runner do ADK não expõe lógica de retry embutida. Falhas de rede ou limites de taxa da API serão exceções não tratadas.
- O modelo Runner do ADK simplifica significativamente a orquestração de agentes. O método
Runner.run_async()encapsula criação de mensagens, execução de ferramentas e gerenciamento de estado de sessão em uma única iteração assíncrona. Isso eliminou a necessidade de um loop de orquestração customizado que seria necessário com APIs de nível mais baixo. - Streaming no ADK é baseado em eventos, não em tokens. Cada
Eventdorun_async()pode conter um ou mais objetosPart. A granularidade do streaming depende do modelo e da implementação do runner, não de um tamanho de chunk configurável. - O estado da sessão é implícito. O SessionService do ADK automaticamente adiciona mensagens do usuário e respostas do agente à sessão. Não há etapa manual de "adicionar ao histórico" -- o Runner gerencia isso como parte da execução. Isso é conveniente, mas significa que não é possível excluir seletivamente mensagens do histórico.
- Pydantic-settings com
env_fileeextra="ignore"oferece um padrão limpo de configuração para ferramentas CLI que precisam tanto de suporte a variáveis de ambiente quanto de facilidade para testes com valores explícitos no construtor.
MIT