Skip to content

AhrendsW/llm-cli-chat

Repository files navigation

llm-cli-chat

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.

Arquitetura

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
Loading

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.

Decisões de Arquitetura

Por que ADK ao invés de LangChain

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.

Por que Typer

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.

Por que Rich para Output

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.

Por que InMemorySessionService

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.

Padrões de Design

Streaming via Geradores Assíncronos

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.

Gerenciamento de Sessão

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.

Configuração

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

Instalaçã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

Uso

# 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 config

Docker

docker build -t llm-cli-chat .
docker run -it --env-file .env llm-cli-chat chat

Desenvolvimento

uv 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/

Trade-offs e Limitações

  • 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 sqlite existe no config, mas a implementação completa exigiria o DatabaseSessionService do 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_eval ou 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 que Aprendi

  • 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 Event do run_async() pode conter um ou mais objetos Part. 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_file e extra="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.

Licença

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors