A sophisticated Gemini-powered CLI agent that leverages Google's Gemini API to autonomously execute file operations and Python code via a structured tool-calling interface. The agent can list directories, read files, write files, and execute Python scripts—all in response to natural language commands.
- ** Multi-turn Agent Loop**: Maintains conversation history and iteratively calls functions until reaching a final answer (max 20 iterations).
- Structured Tool Calling: Four core tools with schema validation via Google's
genailibrary:get_files_info– list directory contents with metadataget_file_content– read file contents (with truncation at 10k chars)run_python_file– execute Python scripts with optional argumentswrite_file– create/overwrite files safely
- Security: All paths are normalized and validated to stay within a designated working directory (
./calculator). - Integrated Calculator: Ships with a working infix expression evaluator supporting operator precedence (
+,-,*,/). - Comprehensive Tests: Unit tests for the calculator and integration tests for each tool.
- Python 3.8+
uv(orpipfor package management)- A valid
GEMINI_API_KEYenvironment variable
# Clone the repository
git clone <repo-url>
cd meow
# Install dependencies
uv sync
# or: pip install -r requirements.txt# Ask the agent to list the calculator directory
uv run main.py "list the contents of calculator"
# Ask it to read a file
uv run main.py "show me the contents of calculator/main.py"
# Ask it to run the calculator
uv run main.py "calculate 3 + 7 * 2"
# Enable verbose output to see function calls
uv run main.py "calculate 3 + 7 * 2" --verboseThe heartbeat of the system. It:
- Parses user input from CLI arguments
- Initializes conversation history
- Calls the Gemini API with available tools
- Extracts function calls from the model's response
- Dispatches them via
call_function - Feeds results back to the model
- Repeats until a final answer or 20 iterations
A unified module containing all four tool functions plus their schema declarations. Each function:
- Accepts a
working_directoryparameter - Uses
os.path.normpath()andos.path.commonpath()to prevent path traversal attacks - Returns human‑readable success/error messages
Schemas define the tool interface for Gemini:
schema_get_files_info
schema_get_file_content
schema_run_python_file
schema_write_file
available_functions # types.Tool wrapping all schemasMaps function names to implementations and:
- Validates the requested function exists
- Injects
working_directory="./calculator" - Wraps results in
types.Contentfor the agent loop - Returns structured response objects
Provides the model with a clear directive:
"You are a helpful AI coding agent. When a user makes a request that matches one of the available operations, respond with a function call instead of text."
Houses settable constants like MAX_FILE_CHARS = 10000 to limit file read sizes.
.
├── calculator/ # Working directory for the agent
│ ├── main.py # CLI calculator script
│ ├── tests.py # Unit tests for Calculator class
│ ├── lorem.txt # Sample file for testing
│ └── pkg/
│ ├── calculator.py # Core infix evaluator (operator precedence)
│ ├── render.py # JSON output formatter
│ └── morelorem.txt # Another test file
│
├── functions/ # Legacy directory (consolidated into functions.py)
│ ├── get_file_content.py
│ ├── get_files_info.py
│ ├── run_python_file.py
│ └── write_file.py
│
├── call_function.py # Function dispatcher
├── config.py # Configuration constants
├── functions.py # Consolidated tool implementations ⭐
├── main.py # Agent loop entry point ⭐
├── prompts.py # System prompt for Gemini
├── pyproject.toml # Project metadata & dependencies
│
├── test_*.py # Integration tests
│ ├── test_get_files_info.py
│ ├── test_get_file_content.py
│ ├── test_run_python_file.py
│ └── test_write_file.py
│
└── README.md # This file
User Input
↓
main.py parses CLI args
↓
Gemini API call with:
- User message
- available_functions (schemas)
- system_prompt
↓
Model Returns Response
├─ Text → Print & Exit
└─ Function Calls → call_function()
↓
Dispatcher validates function name
↓
Execute tool (get_files_info, read, run, write)
↓
Wrap result as types.Content
↓
Add tool response to conversation
↓
Loop back to Gemini (max 20 iterations)
Lists files in a directory relative to the working directory.
Example:
get_files_info("./calculator", "pkg")
# Output:
# - calculator.py: file_size=1234 bytes, is_dir=False
# - render.py: file_size=567 bytes, is_dir=False
# - __pycache__: file_size=4096 bytes, is_dir=TrueSecurity: Validates path stays within the working directory.
Reads file contents, truncated at MAX_FILE_CHARS (10,000 by default).
Example:
get_file_content("./calculator", "main.py")
# Returns the full contents of main.py
# If exceeded char limit, marks with: [...File "..." truncated at 10000 characters]Security: Prevents reading files outside the working directory; validates the target is a regular file.
Executes a Python script with optional arguments. Captures stdout, stderr, and exit code.
Example:
run_python_file("./calculator", "main.py", ["3 + 5"])
# Output:
# STDOUT:
# {"expression": "3 + 5", "result": 8}Security: Validates file path, enforces .py extension, sandboxes execution to the working directory.
Creates or overwrites a file relative to the working directory.
Example:
write_file("./calculator", "new_file.txt", "Hello, world!")
# Output:
# Successfully wrote to "new_file.txt" (13 characters written)Security: Prevents writing outside the working directory; blocks overwriting directories.
The calculator.pkg.Calculator class implements the Shunting Yard algorithm for infix expression evaluation:
- Tokenization: Splits input on whitespace
- Precedence:
*,/(level 2) before+,-(level 1) - Left Associativity:
5 - 3 - 1evaluates as(5 - 3) - 1 = 1 - Error Handling: Validates syntax and operand counts
Example:
calc = Calculator()
calc.evaluate("3 + 7 * 2") # Returns 17 (not 20!)
calc.evaluate("(2 + 3) * 4") # Would work with parentheses (not yet supported)cd calculator
python -m unittest tests.py -vCovers:
- Basic arithmetic (+, −, ×, ÷)
- Operator precedence
- Complex expressions
- Edge cases (empty input, invalid tokens, insufficient operands)
uv run test_get_files_info.py
uv run test_get_file_content.py
uv run test_run_python_file.py
uv run test_write_file.pyEach script validates:
- Success cases (valid paths/operations)
- Security (path traversal attempts blocked)
- Edge cases (non-existent files, truncation)
All four tools enforce path sandboxing:
- Absolute Path Normalization: Convert relative paths to absolute
- Common Path Validation: Ensure the target shares a common prefix with the working directory
- Type Checks: Confirm files are regular files (not directories) and scripts end in
.py
Example Block:
working_dir_abs = os.path.abspath(working_directory)
target_path = os.path.normpath(os.path.join(working_dir_abs, file_path))
valid_target = os.path.commonpath([working_dir_abs, target_path]) == working_dir_abs
if not valid_target:
return f'Error: Cannot access "{file_path}" as it is outside the permitted working directory'This prevents:
../../../etc/passwdattacks- Absolute path escapes like
/tmp/malicious.txt - Symlink traversal within the sandbox
$ uv run main.py "Fix the bug: 3 + 7 * 2 shouldn't be 20." --verbose
- Calling function: get_file_content
- path: calculator/pkg/calculator.py
-> {"result": "# calculator/pkg/calculator.py\n\nclass Calculator:\n def __init__(self):\n self.operators = {..."}
- Calling function: run_python_file
- path: calculator/main.py
- args: ["3 + 7 * 2"]
-> {"result": "STDOUT:\n{\"expression\": \"3 + 7 * 2\", \"result\": 17}"}
Final response:
The expression "3 + 7 * 2" correctly evaluates to 17. The calculator properly applies operator precedence ...- Parentheses Support: Extend the evaluator to handle
(3 + 4) * 2 - More Operations: Add exponentiation (
**), modulo (%), bitwise ops - Better Error Messages: Include suggestions for common typos
- File Editor Mode: Interactive file editing rather than whole-file operations
- Multi-workspace Support: Allow switching between different sandboxed directories
- Conversation Persistence: Save and load chat history
- Tool Expansions: Add web scraping, API calls, database queries
# Set your API key
export GEMINI_API_KEY="sk-..."
# Run the agent
uv run main.py "your prompt here" --verbose
# Run tests
uv run test_get_files_info.py
python -m unittest calculator/tests.py-
Define the schema in
functions.py:schema_my_tool = types.FunctionDeclaration( name="my_tool", description="...", parameters=types.Schema(...) )
-
Implement the function:
def my_tool(working_directory, **kwargs): # Implementation here return result
-
Register in
available_functions:available_functions = types.Tool( function_declarations=[ schema_my_tool, # Add here ... ] )
-
Add to
function_mapincall_function.py:function_map = { "my_tool": my_tool, ... }
- google-genai: Google's Generative AI SDK for Gemini API calls
- python-dotenv: Load environment variables from
.env - Standard Library:
os,subprocess,json,argparse,unittest
This project is provided as-is for educational and demonstration purposes.
Contributions, bug reports, and feature requests welcome! This is an active learning project.
This codebase demonstrates:
- LLM Integration: How to structure tool schemas and function calling
- Agentic Loops: Multi-turn conversation with autonomous tool dispatch
- Security: Safe file system operations with path validation
- Modular Design: Clear separation between agent logic, tools, and domain logic
- Testing: Comprehensive coverage of normal and edge cases
Perfect for learning how to build AI agents that interact with the real world safely and predictably!
Questions? Check the individual test files for usage examples, or inspect main.py to see the agent loop in action.