A terminal assistant that manages Google Sheets and Excel (.xlsx) files
on Google Drive through a Turkish- or English-speaking chat. It is
built on the Agno framework, powered by OpenAI gpt-4o-mini, and
talks to Google's official Sheets/Drive APIs. It can read and edit
both spreadsheets that live on Drive and .xlsx files on the local
disk.
- Google Drive integration: file search, listing, opening.
- Reads and writes native Google Sheets files directly through the Sheets API.
- Edits Excel (.xlsx) files while preserving the format. .xlsx files on Drive are never converted to Google Sheets; they are downloaded, edited locally with openpyxl, and uploaded back to the same Drive ID. The file remains .xlsx; sharing settings and links are preserved.
- Bulk operations across a Drive folder
(
bulk_find_replace_in_folder, with recursive support). - Opt-in local folder mode: the agent cannot touch any file on the
local disk unless the user explicitly opens a folder. Once
open_local_folderis called, the agent can list, read and edit files inside that folder. The project ships with a suggestedworkbooks/drop folder;open_local_folder()with no argument opens it. Any other path can be passed explicitly, including.for the whole project root. - Bulk operations across the active local folder
(
bulk_find_replace_in_local_folder). - High-level Excel tools: find/replace, conditional row delete, filtering, column statistics, formula application, row append, new sheet creation, row range insert/delete.
- Multilingual UI: a TR / EN picker at startup; all strings are pulled
from a single
i18nmodule so adding a new language is a one-file change. - Persistent chat memory: conversation history and long-term user
memories are stored in a local SQLite database. The agent
remembers previous turns and durable facts (preferred language,
frequently mentioned files/folders, workflow habits) across
terminal restarts. A small local model running on Ollama
(
qwen2.5:3b-instructby default) performs memory extraction in the background — no extra API cost, no Excel/Drive context leaves the machine. The main agent talks to OpenAIgpt-4o-minifor the tool-using work. - Rolling sessions with manual rotation: one active session is kept
by default; type
:newin the terminal to start a fresh session. Old sessions stay in the DB and can be surfaced via session search. - Path-traversal protection: in local folder mode the agent cannot escape the opened folder.
- Secrets stay out of git:
.env,credentials.json,token.json, service account files and the local DB (agent_data/) are excluded via.gitignore.
F:\excel agent\
├── main.py # Terminal chat entry point
├── pyproject.toml # uv project definition and dependencies
├── uv.lock # uv lockfile
├── .env.example # Environment variable template
├── .gitignore
├── README.md
├── credentials.json # (you provide — Drive OAuth client)
├── token.json # (auto-generated after the first auth)
├── workbooks/ # suggested drop folder for local .xlsx files
├── excel_workdir/ # (runtime) downloaded .xlsx files
├── agent_data/ # (runtime) SQLite DB + active session id
└── src/
├── __init__.py
├── agent.py # Agno agent setup + OpenAI gpt-4o-mini
├── excel_tools.py # Drive .xlsx tools + folder tools
├── local_tools.py # Local folder tools (opt-in)
├── memory.py # SQLite DB + session/user memory wiring
└── i18n.py # All TR/EN strings in one place
The agent operates with four toolkits. It picks the correct one for each task by itself.
Finds and lists files on Drive.
list_files— returns recent files.search_files— Drive query-syntax search.read_file— text-based content read.
For native Google Sheets files.
read_sheet— reads the given range.update_sheet— writes data.create_sheet— creates a new Sheets file.create_duplicate_sheet— copies an existing Sheets file.
For .xlsx files on Drive and Drive folders.
Drive transfer:
download_excel(file_id)— downloads the.xlsxintoexcel_workdir/.upload_excel(file_id)— uploads back to the same Drive ID; format is preserved.
Local inspection (on the downloaded file):
list_sheet_namesread_excel_rangefind_cells_excelfilter_rows_excelcolumn_summary_excel
Single-cell / block editing:
update_excel_cellupdate_excel_rangeappend_excel_rowcreate_excel_sheet_tabset_formula_excel— applies a formula with a{row}placeholder.
Single-file bulk operations:
find_and_replace_excel— case_sensitive, whole_cell, use_regex, sheet_name and column filters.delete_rows_exceldelete_rows_where— conditional delete (==, !=, >, <, >=, <=, contains, not_contains, empty, not_empty).insert_rows_excel
Column operations and sort:
delete_excel_columnsinsert_excel_columnssort_excel_by_column
Sheet tab management:
rename_excel_sheet_tabdelete_excel_sheet_tab
Analytical, export and creation:
describe_excel— pandas-style per-column summary.export_drive_excel_to_csv(file_id, sheet, output_path)— saves a sheet to a local CSV (does not push back to Drive).create_drive_xlsx_file(name, sheet_name, target_folder_id)— uploads a brand-new empty .xlsx to Drive and caches it locally for immediate editing.
Drive-wide file management:
move_drive_file_to_trash(file_id)— recoverable from Drive Trash.rename_drive_file(file_id, new_name)copy_drive_file(file_id, new_name, target_folder_id)
Folder level (Drive):
find_folder_by_name(name)list_excels_in_folder(folder_id, include_sheets, recursive, max_results)bulk_find_replace_in_folder(folder_id, find, replace, ..., recursive)— runs the download → find/replace → upload loop on behalf of the agent. Files with zero replacements are not re-uploaded.
For .xlsx files on the local disk. Stateful: no tool runs until a folder is opened.
Workspace lifecycle:
open_local_folder(path)— activates the folder and returns the initial listing.close_local_folder()— deactivates the workspace.show_local_folder()— shows the active folder.
File system:
list_local_folder(subpath, recursive)— thels. Withrecursive=Trueit walks subdirectories and returns entries as relative paths.
Excel inspection and editing — local counterparts of every
ExcelTools operation:
list_local_sheet_namesread_local_excelfind_cells_local_excel,filter_local_excel_rows,column_summary_local_excelupdate_local_excel_cell,update_local_excel_range,append_local_excel_row,create_local_excel_sheet_tab,set_formula_local_excelfind_and_replace_local_excel,delete_local_excel_rows,delete_local_excel_rows_where,insert_local_excel_rows
Column operations and sort:
delete_local_excel_columns,insert_local_excel_columnssort_local_excel_by_column
Sheet tab management:
rename_local_excel_sheet_tab,delete_local_excel_sheet_tab
File management:
move_local_excel_file_to_trash— sends to the OS Recycle Bin.rename_local_excel_file,copy_local_excel_file,move_local_excel_file(subdirs allowed inside the workspace).
Analytical and export:
describe_local_excel— pandas-style per-column summary.search_in_all_local_files— pattern hit-list across all workspace .xlsx files (optionally recursive).export_local_excel_to_csv— write a sheet as CSV inside the workspace.
Folder-wide bulk operation:
bulk_find_replace_in_local_folder(find, replace, ..., recursive)
Local files are saved in place. There is no download/upload step.
State lives under agent_data/ in the project root and is wired up in
src/memory.py:
agent_data/
├── agent.db # SQLite database (sessions + memories)
└── current_session.txt # ID of the active rolling session
-
Conversation history — every user/agent turn is stored in
agent.dbalong with the session ID. When the agent rebuilds it resumes the same session, so closing and reopening the terminal does not wipe context. -
User memories — after each run a
MemoryManagerextracts durable facts about the user from the conversation: language preference, frequently used files and folders (Drive and local), workflow habits, naming conventions. Memory extraction runs on a local Ollama model (qwen2.5:3b-instructby default), so it costs nothing and your data does not leave the machine. The main agent stays ongpt-4o-mini.Prerequisite: install Ollama, then pull the model once:
ollama pull qwen2.5:3b-instruct
The Ollama daemon must be running (it normally starts automatically after install).
-
Past sessions — older sessions are kept; the agent can search the three most recent sessions (
search_session_history=True,num_history_sessions=3) when context is needed.
Memory extraction runs against a small local model served by Ollama. Once it is set up the agent uses it transparently in the background; you do not interact with Ollama directly.
- Windows: download the installer from https://ollama.com/download and run it. After install, an Ollama icon (a small llama) appears in the system tray; the daemon runs as long as that icon is there.
- macOS: download the
.dmgfrom the same page and drag to Applications. Launch it once to start the menu-bar agent. - Linux:
curl -fsSL https://ollama.com/install.sh | shstarts the daemon as a systemd unit.
The daemon listens on localhost:11434. The simplest check:
ollama listIt should print a (possibly empty) model table without errors. If the command isn't found, see the PATH section below.
ollama pull qwen2.5:3b-instructThis downloads about 2 GB the first time. After that the model is
cached locally and ollama list will show it.
On a fresh install you may see:
'ollama' is not recognized as an internal or external command
This means the installer added ollama.exe to the user PATH, but the
shell window you have open was launched before the PATH change.
Fixes, in order of preference:
-
Close every cmd / PowerShell window, then open a new one and retry. The new shell picks up the updated PATH.
-
Verify Ollama is installed:
dir "%LOCALAPPDATA%\Programs\Ollama"
You should see
ollama.exeandollama app.exe. -
Use the full path (works without PATH):
"%LOCALAPPDATA%\Programs\Ollama\ollama.exe" pull qwen2.5:3b-instruct -
Manually add to PATH if it really is missing:
- Win + R →
sysdm.cpl→Advanced→Environment Variables - Under User variables, select
Path→Edit→New - Add:
C:\Users\<your-user>\AppData\Local\Programs\Ollama - OK out, close all shells, open a fresh one, retry.
- Win + R →
In a new shell:
ollama --version
ollama listqwen2.5:3b-instruct should appear in the list. The agent will start
using it on its next run.
If you do not want to install Ollama, edit src/agent.py and set:
enable_user_memories=False,The agent will still keep conversation history within a session, but
the long-term MemoryManager (which is the only thing that talks to
Ollama) will be disabled.
The CLI is single-user. The user ID defaults to default_user and can
be overridden with the EXCEL_AGENT_USER_ID environment variable.
There is one active session at a time. Its ID lives in
current_session.txt and is reloaded automatically on every start.
Rotate to a fresh session by typing one of these in the chat:
:new
/new
yeni oturum
new session
A new session ID is generated and the agent is rebuilt with it. The previous session stays in the database and remains searchable.
If you want a stateless setup, edit src/agent.py and set:
enable_user_memories=False,
search_session_history=False,The conversation history within a single session still works because
the agent reuses its own DB rows for the current session; remove db
entirely to drop all persistence.
To start with a clean slate, delete the agent_data/ directory. It
is in .gitignore, so it will not have been committed.
- Python 3.11 or later
- The
uvpackage manager (https://docs.astral.sh/uv/) - A Google Cloud project with Sheets API and Drive API enabled
- An OpenAI API key (https://platform.openai.com/api-keys)
The project was already set up with uv init and the packages were
added. After a fresh clone, sync the dependencies with:
uv syncManual package add (only if needed):
uv add agno google-genai google-api-python-client google-auth-httplib2 google-auth-oauthlib python-dotenv openpyxl sqlalchemy- Get a key at https://platform.openai.com/api-keys.
- Create a
.envfile in the project root (copy.env.exampleas a template). - Put this line in it:
OPENAI_API_KEY=sk-...your_key
- Open your project at https://console.cloud.google.com.
- From
APIs & Services > Libraryenable both:- Google Sheets API
- Google Drive API
- Open
OAuth consent screen. Choose External. Add your Google account as a test user. - Go to
Credentials > Create credentials > OAuth client ID > Desktop appand create a new OAuth client. - Download the JSON file and save it as
F:\excel agent\credentials.json.
On the first run a browser window opens automatically: pick your
Google account and grant the permissions. After that token.json is
created and you are not asked again.
In the project root:
uv run python main.pyIt asks for a language at startup:
Language / Dil [tr (Türkçe) / en (English)] (default: tr):
Accepted inputs: tr, en, or display names like Türkçe,
English. An empty Enter falls back to Turkish. The selection drives
both the banner and the agent's reply language.
To exit: çık, cik, exit, quit, or :q.
To start a new session (rotate the rolling session id): :new, /new,
yeni oturum, or new session. The previous session stays in the
local DB.
You > open the 2024 sales file
Agent > Searching Drive for "2024 sales"...
3 matches found. Which one should I open?
1) 2024 Sales (January).xlsx
2) 2024 Sales (February).xlsx
3) 2024 Sales Summary (Sheets)
You > 3
Agent > Opened 2024 Sales Summary. Which range should I read?
You > the first 10 rows of Sheet1
Agent > [shows the table]
You > in every excel inside the Customers folder,
replace "old address" with "new address"
Agent > Found the "Customers" folder (ID: 1AbC...).
It contains 12 .xlsx files. I'll run the
download/edit/upload loop for each. Confirm?
You > yes
Agent > Starting...
Result: 47 replacements across 9 files;
3 files had no matches (not re-uploaded).
You > open the folder ./data
Agent > Folder opened: F:\...\data
Contents: report.xlsx, old.xlsx, notes.txt, subdir/
You > list everything including subfolders
Agent > [recursive ls output]
You > in report.xlsx, replace "in progress" with "done"
in the Status column
Agent > Sheet1: 12 replacements, saved in place.
You > close the local folder
Agent > Closed: F:\...\data
You > write =C{row}*D{row} from E2 to E100 in report.xlsx
Agent > Confirm? E2:E100 will receive a formula.
You > yes
Agent > Written, saved in place.
# First session
You > my main reports are under the "2024 Reports" Drive folder
Agent > Got it.
# (close the terminal, reopen, run again)
You > what is my main reports folder again?
Agent > You said your main reports are under the
"2024 Reports" Drive folder.
You > :new
Agent > [OK] Started a new session (a1b2c3d4…).
The previous conversation stays in the DB.
The agent is instructed to honour these rules:
-
Excel is never converted to Sheets: a .xlsx file on Drive always follows the download → edit → upload-back-to-same-ID flow. No format conversion happens.
-
Local folder is opt-in: the agent does not touch the local disk unless the user explicitly asks for
open_local_folder. -
Confirmation before writes: explicit user confirmation is required before any write/delete. Risky ranges trigger a warning.
-
No LLM-side loops for bulk work: for requests like "replace X with Y" it calls the
find_and_replace_*tool directly instead of pulling the whole file into the model. -
Asks on multiple matches: when a file/folder search returns several candidates the agent asks the user which one to use.
Every user-visible string lives in src/i18n.py. To add a language
you only need to add a new entry to the LOCALES dict; no other file
needs to change.
Example (German):
LOCALES["de"] = {
"name": "Deutsch",
"banner": _DE_BANNER,
"instructions": _DE_INSTRUCTIONS,
"ready": "Agent bereit. Stelle deine Frage:\n",
"user": "Du > ",
"bye": "Tschüss!",
"err_init": "[FEHLER] Agent-Start fehlgeschlagen: {e}",
"err_run": "[FEHLER] {e}",
"exit_words": {"exit", "quit", "ende", ":q"},
}The language picker lists the new code automatically.
.env,credentials.json,token.jsonand service-account JSON files are in.gitignore— they never enter version control.- In local folder mode the agent cannot do path traversal.
Attempts to escape via
../or absolute paths are rejected. local_*tools refuse to run until a folder is opened; the default posture is safe-by-default.- When uploading a .xlsx back to Drive the same file ID is reused; links and sharing stay intact.
- Excel lock files (
~$filename.xlsx) are skipped automatically during bulk operations.
All commands run through uv:
uv run python main.py # Start the agent
uv run python -c "from src.agent import build_agent" # Quick smoke testManaged in pyproject.toml:
agno— agent frameworkopenai— OpenAI Python SDK (main agent)google-genai— Gemini SDK (kept for future use, currently unused)google-api-python-client— Sheets + Drive REST APIgoogle-auth-httplib2,google-auth-oauthlib— OAuthpython-dotenv—.envsupportopenpyxl— .xlsx read/writesqlalchemy— required by agno's SQLite backend (sessions + memories)ollama— Python client for the local memory-extraction modelopenai— pulled in transitively by agno's Ollama integration
- To change the main model:
OpenAIChat(id=...)insrc/agent.py. - To change the memory model:
MEMORY_MODEL_IDconstant insrc/memory.py(defaults toqwen2.5:3b-instructon Ollama). To use a different Ollama model, pull it first (ollama pull <name>) and setMEMORY_MODEL_IDto its tag. - History window:
Agent(..., num_history_runs=10). - Past sessions surfaced via search:
num_history_sessions=3. - DB location:
DB_PATHinsrc/memory.py(defaults toagent_data/agent.db). - User identity:
EXCEL_AGENT_USER_IDenvironment variable. - OAuth scopes:
SCOPESlist insrc/excel_tools.py. Currently set tospreadsheets+ fulldrive; the full scope is required for the .xlsx copy/conversion paths.
- Drive .xlsx files are updated in place via
drive.files().update, not by copying. Drive ID, link and sharing all stay the same. - Folder-wide find/replace skips upload for files with zero replacements (bandwidth saving).
find_and_replace_exceldefaults to case-insensitive even in regex mode; passcase_sensitive=Trueto flip it.- Operators accept user-friendly aliases:
==,=,equals;!=,<>,not_equals;>,gt; etc.
"OPENAI_API_KEY is not set in .env"
The .env file must be in the project root. Copy from .env.example
and put the real key inside.
The browser OAuth screen says "access blocked" Add your Gmail address as a test user in Google Cloud Console > OAuth consent screen.
File is not .xlsx (mimeType=...)
download_excel works only on Excel binary files. For native Google
Sheets, use GoogleSheetsTools (read_sheet, update_sheet).
Path escapes the active folder
In local folder mode the agent cannot leave the opened folder. Open a
higher-level folder that contains the path you need.
No folder is open. Call open_local_folder(path) first.
Local tools are opt-in. Call open_local_folder first.
Agent doesn't remember what we talked about last time
Memory persistence requires agent_data/agent.db to be present and
writable. Verify the directory exists after the first run and that
nothing is wiping it between sessions. Use :new only when you
actually want to start a fresh session.
ModuleNotFoundError: No module named 'sqlalchemy'
The SQLite backend needs SQLAlchemy. Run uv add sqlalchemy (or
uv sync after pulling a recent version of pyproject.toml).
Ollama connection error / "model not found"
Memory extraction runs on a local Ollama model. Make sure Ollama is
installed and the daemon is running (ollama list should respond),
and that the model is pulled: ollama pull qwen2.5:3b-instruct.
See the "Ollama setup" subsection under "Memory and Sessions" for the
full walkthrough including Windows PATH fixes.
'ollama' is not recognized as an internal or external command
PATH was updated by the installer but your current shell still has the
old PATH. Close every cmd/PowerShell window, open a fresh one, and try
again. If that does not fix it, see the PATH troubleshooting steps in
the "Ollama setup" subsection.
- Folder-wide bulk operations for native Google Sheets (via the
Sheets API
batchUpdate.findReplace). - More bulk operations on the local folder side
(
bulk_delete_rows_where_in_local_folder, etc.). - Async processing (parallel execution for large folders).
- A web interface (Streamlit or FastAPI).
- A pandas / DataFrame-based advanced analysis tool.
- A
describe_sheettool that summarises columns/types without feeding the data to the LLM.
This project is distributed under the MIT License. See the LICENSE
file in the project root for the full text.