This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ERPClaw is a mini-ERP managed by an AI agent via Telegram. Users interact through natural language (text or voice). The AI agent (configurable: LM Studio local model or DeepSeek cloud, via the agno framework) translates requests into ERP operations on a SQLite database.
This project uses uv as the package manager. Python version is pinned to 3.13 (.python-version file).
# Install/sync dependencies
uv sync
# Run the Telegram bot (main entry point)
uv run erpclaw
# Run the web admin panel + shop portal (same process)
uv run uvicorn erpclaw.web:app --reload
# Admin: http://localhost:8000/admin
# Shop: http://localhost:8000/shop/registerstart.bat launches both processes (web+shop in a separate window, then the bot).
reset_db.bat deletes and regenerates erp.db and agent.db (asks for confirmation).
uv sync gotcha: If the bot is running, erpclaw.exe is locked and uv sync fails. Install packages directly with uv pip install <pkg> instead. Run tests with uv run --no-sync python -m pytest.
A .env file is required:
TELEGRAM_BOT_TOKEN— Telegram bot tokenALLOWED_CHAT_ID— Telegram numeric user ID allowed to use the botOPENAI_API_KEY— OpenAI API key (Whisper voice transcription only)SHOP_SECRET_KEY— Secret for shop session cookies (optional; defaults to dev value)LLM_PROVIDER—lmstudio(default) ordeepseekLLM_MODEL_ID— Model ID (default:qwen/qwen3.5-9bfor LM Studio,deepseek-reasonerfor DeepSeek)LMSTUDIO_BASE_URL— LM Studio server URL (default:http://localhost:1234/v1)DEEPSEEK_API_KEY— DeepSeek API key (only required ifLLM_PROVIDER=deepseek)
Two independent databases, two entry points:
erp.db— Business data (SQLAlchemy, synchronous): all ERP and logistics models (see table list below)agent.db— agno agent memory (AsyncSqliteDb): conversation history and user preferences per Telegram user ID./cataloghi/— Local directory for downloaded PDF catalogs (created automatically)
| Group | Tables |
|---|---|
| Catalog | articoli, categorie, stock_ubicazioni |
| Clients | clienti, indirizzi, clienti_auth |
| Customer Orders | ordini, righe_ordine |
| Supplier Orders | ordini_fornitori, righe_ordini_fornitori |
| Suppliers | fornitori, cataloghi_fornitori |
| Warehouse | magazzini, zone, scaffali, ripiani |
| Movements | movimenti_magazzino |
Articolo.giacenza is a column_property (SQLAlchemy correlated subquery) summing StockUbicazione.quantita. It is read-only — never write to it directly. Stock is managed via StockUbicazione rows.
Articolo.scorta_minima is a physical column (Integer, nullable, default 0). Used by the articoli_sotto_scorta_minima tool to identify items needing reorder (giacenza < scorta_minima).
Articolo.prezzo_vendita (Float, not null) — selling price to customers. Articolo.prezzo_acquisto (Float, nullable) — purchase price from suppliers. Old prezzo column is kept in SQLite for legacy DBs; _migrate() copies it to prezzo_vendita if both coexist.
- Telegram Bot (
erpclaw/bot.py): Handles text and voice. Voice → Whisper transcription → agent. Theuser_idpassed to the agent is the Telegram numeric user ID (as string). - Web Admin + Shop (
erpclaw/web.py): FastAPI app with:- SQLAdmin CRUD at
/adminfor all ERP/logistics models. - Customer shop portal at
/shop(register, login, search articles, cart, checkout, order history). - JSON API endpoints for the React SPA (
/agents/api/*,/config/api,/chat/api/*). - SPA catch-all: serves
frontend/dist/index.htmlfor all non-API routes (production).
- SQLAdmin CRUD at
- React SPA (
frontend/): Vite 6 + React 19 + TypeScript 5 + Tailwind CSS v4 + shadcn/ui + @xyflow/react. Four pages: Home, AgentDashboard, ConfigPanel, Chat. In dev, Vite proxies API calls to FastAPI :8000. In production,npm run buildwrites tofrontend/dist/which FastAPI serves.
Stack: Vite 6, React 19, TypeScript 5, Tailwind CSS v4 (@tailwindcss/vite plugin, CSS-first config — no tailwind.config.js), shadcn/ui (Default style, Slate, CSS variables), React Router v7, @xyflow/react, lucide-react, sonner.
Dev workflow:
cd frontend && npm run dev # → http://localhost:5173Vite proxies /agents, /config, /chat, /admin, /shop → http://localhost:8000.
Production:
cd frontend && npm run build # → frontend/dist/FastAPI serves frontend/dist/assets/ at /assets and catches all other routes with spa_fallback returning frontend/dist/index.html.
Key frontend files:
frontend/src/lib/types.ts—AgentConfig,EnvConfig,ChatMessageinterfacesfrontend/src/lib/api.ts— typed fetch wrappers:agentApi,configApi,chatApifrontend/src/pages/AgentDashboard.tsx— xyflow canvas withNODE_TYPESat module level, save/reload toolbar,NodeEditSheetfrontend/src/components/agents/flowUtils.ts—configToFlow(config)andextractPositions(nodes)frontend/src/components/agents/NodeEditSheet.tsx— shadcn Sheet editor for team/agent/tool/memory nodes usingstructuredClone+ path-based setterfrontend/vite.config.ts— proxy config +@alias to./src
JSON API endpoints added:
GET /agents/api/config/PUT /agents/api/config/POST /agents/api/reload— inerpclaw/agents_dashboard.pyGET /config/api/PUT /config/api— inerpclaw/config_panel.pyGET /chat/api/history/POST /chat/api/send— inerpclaw/chat.py
Tailwind CSS v4 note: Uses @import "tailwindcss" in CSS, no JS config file. The @tailwindcss/vite plugin handles everything. Adding plugins uses @plugin directive in CSS.
Telegram message → bot.py → agent.py (agno Team + LM Studio or DeepSeek)
→ ERPTools (erp_tools.py) → SQLAlchemy session → erp.db
→ LogisticaTools (logistica_tools.py) → SQLAlchemy session → erp.db
→ delegates to fornitore_research_agent → FornitoreResearchTools / DuckDuckGoTools
The team object (agno Team) is the main entry point. It holds ERPTools + LogisticaTools and delegates to fornitore_research_agent for supplier web research.
_make_model(thinking=True/False) in agent.py builds the model based on LLM_PROVIDER:
lmstudio→LMStudio(id, base_url, extra_body={"enable_thinking": ...})deepseek→DeepSeek(id, api_key)
Thinking mode is enabled for all three components (team, fornitore_research_agent, memory_manager) by default. _strip_reasoning() strips <reasoning>...</reasoning> tags from responses (Qwen3.5 thinking output) before sending to Telegram.
Always use the latest version of agno. The agno API changes frequently — when upgrading, verify that Agent and Team constructor parameters are still valid. The current architecture uses Team (not Agent) as the top-level entry point because Agent no longer accepts a team parameter (removed in agno ≥2.5.x in favor of the dedicated Team class).
erpclaw/config.py— Loads.env; fails fast if required variables are missing. ExportsLLM_PROVIDER,LLM_MODEL_ID,LMSTUDIO_BASE_URL,SHOP_SECRET_KEY(with dev default),DEEPSEEK_API_KEY(optional).erpclaw/erp_db.py— SQLAlchemy models andget_session()/init_db().init_db()is called at import time in botherp_tools.py,logistica_tools.py, andweb.py. Usescreate_all(checkfirst=True)to avoid errors on existing DBs. Also runs_migrate()to add missing columns to existing DBs (idempotent).erpclaw/erp_tools.py—ERPTools(Toolkit): tools for articles (dual pricing:prezzo_vendita/prezzo_acquisto), clients, customer orders, supplier orders (crea_ordine_fornitore,aggiungi_riga_ordine_fornitore,lista_ordini_fornitori,visualizza_ordine_fornitore,avanza_stato_ordine_fornitore), categories, and addresses.capparameter acceptsUnion[str, int](local models tend to pass CAP as integer).erpclaw/logistica_tools.py—LogisticaTools(Toolkit): tools for warehouse locations (Magazzino→Zona→Scaffale→Ripiano), stock assignment/transfer, order discharge, and movement history.erpclaw/fornitore_research_tools.py—FornitoreResearchTools(Toolkit): PDF catalog download (httpx), parsing (pdfplumber), DB management.erpclaw/agent.py—team(agnoTeam) +fornitore_research_agentsub-agent +memory_manager. TheTeamis the entry point; it holdsERPTools+LogisticaTools,db, memory, and delegates tofornitore_research_agent. All components use_make_model(thinking=True)._strip_reasoning()cleans Qwen3.5 thinking tags from responses.erpclaw/agents_dashboard.py—APIRouter(prefix="/agents"):GET/PUT /api/configandPOST /api/reloadfor the React Agent Dashboard.erpclaw/config_panel.py—APIRouter(prefix="/config"):GET/PUT /api(JSON) for the React Config Panel.parse_env/write_envfor.envfile I/O. Uses Pydantic_ConfigUpdatemodel.erpclaw/chat.py—APIRouter(prefix="/chat"):GET /api/historyandPOST /api/sendfor the React Chat page. Session-based chat history stored in memory (keyed by cookie).erpclaw/shop.py— FastAPIAPIRouter(prefix="/shop"): register/login/logout, article search (HTMX), cart (cookie JSON), checkout, order history. Auth viaitsdangerous+passlib[bcrypt].erpclaw/templates/shop/— Jinja2 templates:base.html,register.html,login.html,search.html,orders.html,_risultati.html(HTMX partial),_carrello.html(HTMX partial).docs/lmstudio-settings.md— LM Studio recommended settings for ERPClaw (GGUF variant, GPU offload, inference params).
enable_agentic_memory=True+add_history_to_context=True(last 5 runs) per user.- Memory capture instructions are in Italian and collect user name/preferences.
Customer orders: bozza → confermato → spedito → chiuso
Supplier orders (OrdineFornitore): bozza → inviato → ricevuto
Use avanza_stato_ordine_fornitore to progress. When ricevuto, use logistics tools to load goods into warehouse locations.
When an order is marked spedito, the agent should propose running scarica_ordine_da_ubicazione to discharge quantities from warehouse locations (LIFO strategy).
- Add a method to
ERPToolsinerpclaw/erp_tools.pywith a clear Italian docstring (the agent uses it to decide when to call the tool). - Register it with
self.register(self.method_name)inERPTools.__init__. - Methods must return
str(markdown-formatted for Telegram display). - Use
get_session()as a context manager (with get_session() as s:). - For parameters that could be passed as int by local models (e.g. CAP codes), use
Union[str, int].
- Add a method to
LogisticaToolsinerpclaw/logistica_tools.pywith a clear Italian docstring. - Register it with
self.register(self.method_name)inLogisticaTools.__init__. - Methods must return
str(markdown-formatted for Telegram display). - Add a mention in the team's
instructionsinagent.pyso the LLM knows when to use it.
uv run --no-sync python -m pytest tests/ -vpytest is in [optional-dependencies] dev and not installed by default. Install once: uv pip install pytest.
Tests use SQLite in-memory databases (via tests/conftest.py) and unittest.mock.patch to redirect get_session calls. When patching get_session for tools tests, use side_effect=make_session where make_session returns a new Session(engine) per call.
get_session() returns a plain Session. Always use it as a context manager. After writing, call s.commit() before accessing generated IDs or relationships outside the with block.
DetachedInstanceError: After s.commit(), SQLAlchemy expires all ORM attributes (expire_on_commit=True default). Accessing them after the with block closes raises DetachedInstanceError. Rule: never use ORM objects outside with get_session(). Either return inside the with, or capture primitive values (str/float/int) as local variables before the block ends.
- Routes:
erpclaw/shop.py,APIRouter(prefix="/shop"), mounted inweb.py. - Auth:
itsdangerous.URLSafeSerializerfor signed session cookies +passlib[bcrypt]for password hashing. - Cart: stored in a plain JSON cookie (
shop_cart). - bcrypt constraint:
passlib[bcrypt]>=1.7.4requiresbcrypt<5.0.0— bcrypt 5+ broke the passlib API. Pinned inpyproject.toml. - Order numbering:
WEB-YYYYMMDD-NNNN(distinct from agent ordersORD-NNNN).
tool.uv.package = trueis required for theerpclawentry point script to be installed byuv sync.requires-python = ">=3.13"and.python-version = 3.13pin away from a broken uv-managed CPython 3.12 distribution.