terno-agent 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- terno_agent-0.1.0/.env.example +22 -0
- terno_agent-0.1.0/.gitignore +30 -0
- terno_agent-0.1.0/.python-version +1 -0
- terno_agent-0.1.0/Chinook_Sqlite.sqlite +0 -0
- terno_agent-0.1.0/PKG-INFO +317 -0
- terno_agent-0.1.0/README.md +272 -0
- terno_agent-0.1.0/pyproject.toml +76 -0
- terno_agent-0.1.0/src/terno_agent/__init__.py +7 -0
- terno_agent-0.1.0/src/terno_agent/__main__.py +4 -0
- terno_agent-0.1.0/src/terno_agent/agents/__init__.py +6 -0
- terno_agent-0.1.0/src/terno_agent/agents/base.py +123 -0
- terno_agent-0.1.0/src/terno_agent/agents/coder.py +22 -0
- terno_agent-0.1.0/src/terno_agent/agents/database.py +28 -0
- terno_agent-0.1.0/src/terno_agent/agents/orchestrator.py +149 -0
- terno_agent-0.1.0/src/terno_agent/cli.py +323 -0
- terno_agent-0.1.0/src/terno_agent/config.py +110 -0
- terno_agent-0.1.0/src/terno_agent/core/__init__.py +52 -0
- terno_agent-0.1.0/src/terno_agent/core/events.py +69 -0
- terno_agent-0.1.0/src/terno_agent/core/exceptions.py +22 -0
- terno_agent-0.1.0/src/terno_agent/core/messages.py +60 -0
- terno_agent-0.1.0/src/terno_agent/core/tool.py +34 -0
- terno_agent-0.1.0/src/terno_agent/db/__init__.py +3 -0
- terno_agent-0.1.0/src/terno_agent/db/connection.py +117 -0
- terno_agent-0.1.0/src/terno_agent/llm/__init__.py +4 -0
- terno_agent-0.1.0/src/terno_agent/llm/anthropic_client.py +138 -0
- terno_agent-0.1.0/src/terno_agent/llm/base.py +46 -0
- terno_agent-0.1.0/src/terno_agent/llm/factory.py +23 -0
- terno_agent-0.1.0/src/terno_agent/llm/openai_client.py +169 -0
- terno_agent-0.1.0/src/terno_agent/prompts/__init__.py +5 -0
- terno_agent-0.1.0/src/terno_agent/prompts/coder.py +20 -0
- terno_agent-0.1.0/src/terno_agent/prompts/database.py +21 -0
- terno_agent-0.1.0/src/terno_agent/prompts/orchestrator.py +29 -0
- terno_agent-0.1.0/src/terno_agent/sandbox/__init__.py +4 -0
- terno_agent-0.1.0/src/terno_agent/sandbox/base.py +52 -0
- terno_agent-0.1.0/src/terno_agent/sandbox/docker.py +111 -0
- terno_agent-0.1.0/src/terno_agent/sandbox/factory.py +22 -0
- terno_agent-0.1.0/src/terno_agent/sandbox/local.py +54 -0
- terno_agent-0.1.0/src/terno_agent/tools/__init__.py +9 -0
- terno_agent-0.1.0/src/terno_agent/tools/code_exec.py +48 -0
- terno_agent-0.1.0/src/terno_agent/tools/sql.py +123 -0
- terno_agent-0.1.0/tests/__init__.py +0 -0
- terno_agent-0.1.0/tests/test_agent_events.py +111 -0
- terno_agent-0.1.0/tests/test_chat_gated.py +45 -0
- terno_agent-0.1.0/tests/test_config_dotenv.py +80 -0
- terno_agent-0.1.0/tests/test_imports.py +14 -0
- terno_agent-0.1.0/tests/test_local_sandbox.py +22 -0
- terno_agent-0.1.0/tests/test_sql_tool.py +47 -0
- terno_agent-0.1.0/uv.lock +998 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copy to .env and fill in values. The agent loads this automatically from the
|
|
2
|
+
# current working directory (or any parent dir) via python-dotenv. Variables
|
|
3
|
+
# already set in the shell win over values in this file.
|
|
4
|
+
|
|
5
|
+
# --- LLM ---
|
|
6
|
+
TERNO_LLM_PROVIDER=anthropic # anthropic | openai
|
|
7
|
+
TERNO_LLM_MODEL=claude-opus-4-7 # optional; sensible default per provider
|
|
8
|
+
ANTHROPIC_API_KEY=
|
|
9
|
+
# OPENAI_API_KEY=
|
|
10
|
+
|
|
11
|
+
# --- Database (any SQLAlchemy URL) ---
|
|
12
|
+
TERNO_DATABASE_URL=sqlite:///./demo.db
|
|
13
|
+
# TERNO_DATABASE_URL=postgresql+psycopg://user:pass@host:5432/dbname
|
|
14
|
+
# TERNO_DATABASE_URL=mysql+pymysql://user:pass@host:3306/dbname
|
|
15
|
+
|
|
16
|
+
# --- Sandbox ---
|
|
17
|
+
TERNO_SANDBOX=local # docker | local | none
|
|
18
|
+
TERNO_SANDBOX_IMAGE=python:3.12-slim # only used for the docker sandbox
|
|
19
|
+
|
|
20
|
+
# --- Behavior ---
|
|
21
|
+
TERNO_MAX_ROWS=200
|
|
22
|
+
TERNO_READ_ONLY_SQL=true # set to false to allow writes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Local config / secrets — never commit real .env files
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
23
|
+
!.env.example
|
|
24
|
+
config.local.toml
|
|
25
|
+
|
|
26
|
+
# Editors
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
.DS_Store
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
Binary file
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: terno-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-agent CLI that answers questions about your database and runs code in a sandbox.
|
|
5
|
+
Project-URL: Homepage, https://github.com/terno-ai/terno-agent
|
|
6
|
+
Project-URL: Issues, https://github.com/terno-ai/terno-agent/issues
|
|
7
|
+
Author: Sandeep Akode
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agent,ai,cli,database,llm,sandbox,sql
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: pydantic>=2.6
|
|
20
|
+
Requires-Dist: python-dotenv>=1.0
|
|
21
|
+
Requires-Dist: rich>=13.7
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: anthropic>=0.39; extra == 'all'
|
|
25
|
+
Requires-Dist: docker>=7.0; extra == 'all'
|
|
26
|
+
Requires-Dist: openai>=1.40; extra == 'all'
|
|
27
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
|
|
28
|
+
Requires-Dist: pymysql>=1.1; extra == 'all'
|
|
29
|
+
Provides-Extra: anthropic
|
|
30
|
+
Requires-Dist: anthropic>=0.39; extra == 'anthropic'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
36
|
+
Provides-Extra: docker
|
|
37
|
+
Requires-Dist: docker>=7.0; extra == 'docker'
|
|
38
|
+
Provides-Extra: mysql
|
|
39
|
+
Requires-Dist: pymysql>=1.1; extra == 'mysql'
|
|
40
|
+
Provides-Extra: openai
|
|
41
|
+
Requires-Dist: openai>=1.40; extra == 'openai'
|
|
42
|
+
Provides-Extra: postgres
|
|
43
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# Terno Agent
|
|
47
|
+
|
|
48
|
+
A multi-agent CLI that answers questions about your database. It plans,
|
|
49
|
+
generates and executes SQL, and can write Python and run it in a sandbox
|
|
50
|
+
(Docker by default) to analyze results.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Multi-agent**: an Orchestrator plans and delegates to a Database
|
|
55
|
+
specialist (SQL) and a Coder specialist (sandboxed Python).
|
|
56
|
+
- **Provider-agnostic LLM**: Anthropic Claude and OpenAI — pick at runtime.
|
|
57
|
+
- **Any database**: anything SQLAlchemy can talk to (Postgres, MySQL, SQLite,
|
|
58
|
+
…) via a single URL.
|
|
59
|
+
- **Sandboxed code execution**: Docker by default (`--network none`,
|
|
60
|
+
read-only rootfs, mem/CPU caps); local subprocess fallback for dev.
|
|
61
|
+
- **Read-only by default**: only `SELECT` / `WITH` / `EXPLAIN` allowed unless
|
|
62
|
+
you opt in.
|
|
63
|
+
- **Streaming + typed events**: assistant text streams live; tool calls and
|
|
64
|
+
results render with syntax-highlighted panels and result tables.
|
|
65
|
+
- **CLI + library**: `terno ask "..."` from the shell, or
|
|
66
|
+
`from terno_agent import Agent` in Python.
|
|
67
|
+
|
|
68
|
+
## Architecture
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
┌────────────────────────┐
|
|
72
|
+
user query → │ Orchestrator │ ← planner: decomposes into steps
|
|
73
|
+
└────────────┬───────────┘ and routes to a specialist
|
|
74
|
+
│
|
|
75
|
+
┌────────────┼────────────┐
|
|
76
|
+
▼ ▼
|
|
77
|
+
┌────────────────┐ ┌────────────────┐
|
|
78
|
+
│ DatabaseAgent │ │ CoderAgent │
|
|
79
|
+
│ • sql_query │ │ • run_python │
|
|
80
|
+
│ • list_tables │ │ (sandboxed) │
|
|
81
|
+
│ • describe │ │ │
|
|
82
|
+
└────────┬───────┘ └────────┬───────┘
|
|
83
|
+
│ │
|
|
84
|
+
▼ ▼
|
|
85
|
+
SQLAlchemy engine Docker / local
|
|
86
|
+
(any DB URL) sandbox runner
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
All cross-cutting boundaries are protocols, so each layer is swappable:
|
|
90
|
+
|
|
91
|
+
| Boundary | Protocol | Implementations |
|
|
92
|
+
| -------- | -------------- | ---------------------------- |
|
|
93
|
+
| LLM | `LLMClient` | Anthropic, OpenAI |
|
|
94
|
+
| Sandbox | `Sandbox` | Docker, local subprocess |
|
|
95
|
+
| Tool | `Tool` | sql_query, run_python, ... |
|
|
96
|
+
| Database | SQLAlchemy URL | Postgres, MySQL, SQLite, ... |
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
You can install with either `uv` or plain `pip`. Both produce the same `terno`
|
|
101
|
+
CLI on your `PATH` and the same importable `terno_agent` package.
|
|
102
|
+
|
|
103
|
+
### Optional extras
|
|
104
|
+
|
|
105
|
+
Pick only what you need (or use `all` to get everything):
|
|
106
|
+
|
|
107
|
+
| Extra | What it pulls in |
|
|
108
|
+
| ----------- | ------------------------------- |
|
|
109
|
+
| `anthropic` | the `anthropic` SDK |
|
|
110
|
+
| `openai` | the `openai` SDK |
|
|
111
|
+
| `docker` | the `docker` SDK for sandboxing |
|
|
112
|
+
| `postgres` | `psycopg[binary]` |
|
|
113
|
+
| `mysql` | `pymysql` |
|
|
114
|
+
| `all` | all of the above |
|
|
115
|
+
| `dev` | pytest, ruff, mypy |
|
|
116
|
+
|
|
117
|
+
### With `uv` (recommended)
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# install globally as a uv tool — `terno` works from anywhere
|
|
121
|
+
uv tool install terno-agent
|
|
122
|
+
uv tool install "terno-agent[anthropic,docker,postgres]"
|
|
123
|
+
|
|
124
|
+
# editable install from a local checkout
|
|
125
|
+
git clone https://github.com/terno-ai/terno-agent.git
|
|
126
|
+
cd terno-agent
|
|
127
|
+
uv tool install --editable ".[all]"
|
|
128
|
+
|
|
129
|
+
# add it as a dependency of another uv project
|
|
130
|
+
uv add terno-agent
|
|
131
|
+
uv add "terno-agent[anthropic,docker]"
|
|
132
|
+
|
|
133
|
+
# from a local path or git
|
|
134
|
+
uv add /path/to/terno_agent
|
|
135
|
+
uv add "git+https://github.com/terno-ai/terno-agent.git"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Refresh after changing `pyproject.toml`:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uv tool install --editable ".[all]" --force
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### With `pip`
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# from PyPI
|
|
148
|
+
pip install terno-agent
|
|
149
|
+
pip install "terno-agent[anthropic,docker,postgres]"
|
|
150
|
+
|
|
151
|
+
# from a local checkout (editable)
|
|
152
|
+
git clone https://github.com/terno-ai/terno-agent.git
|
|
153
|
+
cd terno-agent
|
|
154
|
+
python -m venv .venv && source .venv/bin/activate
|
|
155
|
+
pip install -e ".[all]"
|
|
156
|
+
|
|
157
|
+
# from git
|
|
158
|
+
pip install "git+https://github.com/terno-ai/terno-agent.git"
|
|
159
|
+
|
|
160
|
+
# from a built wheel
|
|
161
|
+
pip install ./dist/terno_agent-0.1.0-py3-none-any.whl
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> **Tip:** if you install into a project venv with plain `pip`, you have to
|
|
165
|
+
> activate the venv (or use its `bin/terno`) to run the CLI. `uv tool install`
|
|
166
|
+
> avoids this by giving the CLI its own isolated environment on `PATH`.
|
|
167
|
+
|
|
168
|
+
## Configure
|
|
169
|
+
|
|
170
|
+
Configuration is read from environment variables, with `.env` auto-loaded from
|
|
171
|
+
your current working directory (or any parent). Process env wins over `.env`.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
cp .env.example .env
|
|
175
|
+
# then edit:
|
|
176
|
+
ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY=
|
|
177
|
+
TERNO_LLM_PROVIDER=anthropic # anthropic | openai
|
|
178
|
+
TERNO_LLM_MODEL=claude-opus-4-7
|
|
179
|
+
TERNO_DATABASE_URL=sqlite:///./demo.db
|
|
180
|
+
TERNO_SANDBOX=docker # docker | local | none
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Run `terno config` to print the effective settings (API keys masked).
|
|
184
|
+
|
|
185
|
+
### SQLAlchemy URL examples
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
sqlite:///./relative.db # relative to CWD
|
|
189
|
+
sqlite:////absolute/path/to.db # absolute (4 slashes)
|
|
190
|
+
postgresql+psycopg://user:pass@host:5432/db
|
|
191
|
+
mysql+pymysql://user:pass@host:3306/db
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Use the CLI
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# one-shot question
|
|
198
|
+
terno ask "what were the top 10 customers by revenue last quarter?"
|
|
199
|
+
|
|
200
|
+
# interactive REPL
|
|
201
|
+
terno chat
|
|
202
|
+
|
|
203
|
+
# suppress streaming/activity, print only the final answer
|
|
204
|
+
terno -q ask "how many tracks are in the database?"
|
|
205
|
+
|
|
206
|
+
# show effective config
|
|
207
|
+
terno config
|
|
208
|
+
|
|
209
|
+
# show version
|
|
210
|
+
terno --version
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If you installed with plain `pip` into a project venv and didn't activate it:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
.venv/bin/terno ask "..."
|
|
217
|
+
# or
|
|
218
|
+
python -m terno_agent ask "..."
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
If you installed with `uv` into the current project rather than as a tool:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uv run terno ask "..."
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Use as a library
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from terno_agent import Agent
|
|
231
|
+
|
|
232
|
+
# Reads .env + env vars
|
|
233
|
+
agent = Agent.from_env()
|
|
234
|
+
result = agent.ask("how many active users signed up this week?")
|
|
235
|
+
print(result.answer)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Programmatic config (no env vars required):
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from terno_agent import Agent
|
|
242
|
+
from terno_agent.config import Config
|
|
243
|
+
|
|
244
|
+
cfg = Config(
|
|
245
|
+
llm_provider="anthropic",
|
|
246
|
+
llm_model="claude-opus-4-7",
|
|
247
|
+
llm_api_key="sk-ant-...",
|
|
248
|
+
database_url="postgresql+psycopg://u:p@host/db",
|
|
249
|
+
sandbox="local", # "docker" | "local" | "none"
|
|
250
|
+
)
|
|
251
|
+
agent = Agent.from_config(cfg)
|
|
252
|
+
print(agent.ask("top 5 tables by row count").answer)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Stream events into your own UI:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from terno_agent import Agent
|
|
259
|
+
from terno_agent.core.events import TextDelta, ToolCallEvent, ToolResultEvent
|
|
260
|
+
|
|
261
|
+
def on_event(e):
|
|
262
|
+
if isinstance(e, TextDelta):
|
|
263
|
+
print(e.text, end="", flush=True)
|
|
264
|
+
elif isinstance(e, ToolCallEvent):
|
|
265
|
+
print(f"\n[tool] {e.call.name}({e.call.arguments})")
|
|
266
|
+
elif isinstance(e, ToolResultEvent):
|
|
267
|
+
print(f"[result] {e.result.content[:200]}")
|
|
268
|
+
|
|
269
|
+
agent = Agent.from_env(on_event=on_event)
|
|
270
|
+
agent.ask("describe the users table and count rows")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Project layout
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
src/terno_agent/
|
|
277
|
+
cli.py # argparse entry point + rich renderer
|
|
278
|
+
config.py # env + .env-driven Config
|
|
279
|
+
core/ # message/tool/event/exception types
|
|
280
|
+
llm/ # LLMClient protocol + Anthropic + OpenAI (streaming)
|
|
281
|
+
agents/ # orchestrator + specialist agents
|
|
282
|
+
tools/ # sql_query, run_python, list_tables, describe_table
|
|
283
|
+
sandbox/ # Docker + local runners
|
|
284
|
+
db/ # SQLAlchemy engine & inspector
|
|
285
|
+
prompts/ # system prompts per agent
|
|
286
|
+
tests/ # pytest suite
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Develop
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# clone + editable install with dev extras
|
|
293
|
+
git clone https://github.com/terno-ai/terno-agent.git
|
|
294
|
+
cd terno-agent
|
|
295
|
+
uv venv --python 3.12
|
|
296
|
+
uv pip install -e ".[dev,all]"
|
|
297
|
+
|
|
298
|
+
# tests
|
|
299
|
+
uv run pytest -q
|
|
300
|
+
|
|
301
|
+
# lint / format / typecheck
|
|
302
|
+
uv run ruff check .
|
|
303
|
+
uv run ruff format .
|
|
304
|
+
uv run mypy src
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Or with plain `pip`:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
python -m venv .venv && source .venv/bin/activate
|
|
311
|
+
pip install -e ".[dev,all]"
|
|
312
|
+
pytest -q
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
MIT
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Terno Agent
|
|
2
|
+
|
|
3
|
+
A multi-agent CLI that answers questions about your database. It plans,
|
|
4
|
+
generates and executes SQL, and can write Python and run it in a sandbox
|
|
5
|
+
(Docker by default) to analyze results.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-agent**: an Orchestrator plans and delegates to a Database
|
|
10
|
+
specialist (SQL) and a Coder specialist (sandboxed Python).
|
|
11
|
+
- **Provider-agnostic LLM**: Anthropic Claude and OpenAI — pick at runtime.
|
|
12
|
+
- **Any database**: anything SQLAlchemy can talk to (Postgres, MySQL, SQLite,
|
|
13
|
+
…) via a single URL.
|
|
14
|
+
- **Sandboxed code execution**: Docker by default (`--network none`,
|
|
15
|
+
read-only rootfs, mem/CPU caps); local subprocess fallback for dev.
|
|
16
|
+
- **Read-only by default**: only `SELECT` / `WITH` / `EXPLAIN` allowed unless
|
|
17
|
+
you opt in.
|
|
18
|
+
- **Streaming + typed events**: assistant text streams live; tool calls and
|
|
19
|
+
results render with syntax-highlighted panels and result tables.
|
|
20
|
+
- **CLI + library**: `terno ask "..."` from the shell, or
|
|
21
|
+
`from terno_agent import Agent` in Python.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌────────────────────────┐
|
|
27
|
+
user query → │ Orchestrator │ ← planner: decomposes into steps
|
|
28
|
+
└────────────┬───────────┘ and routes to a specialist
|
|
29
|
+
│
|
|
30
|
+
┌────────────┼────────────┐
|
|
31
|
+
▼ ▼
|
|
32
|
+
┌────────────────┐ ┌────────────────┐
|
|
33
|
+
│ DatabaseAgent │ │ CoderAgent │
|
|
34
|
+
│ • sql_query │ │ • run_python │
|
|
35
|
+
│ • list_tables │ │ (sandboxed) │
|
|
36
|
+
│ • describe │ │ │
|
|
37
|
+
└────────┬───────┘ └────────┬───────┘
|
|
38
|
+
│ │
|
|
39
|
+
▼ ▼
|
|
40
|
+
SQLAlchemy engine Docker / local
|
|
41
|
+
(any DB URL) sandbox runner
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
All cross-cutting boundaries are protocols, so each layer is swappable:
|
|
45
|
+
|
|
46
|
+
| Boundary | Protocol | Implementations |
|
|
47
|
+
| -------- | -------------- | ---------------------------- |
|
|
48
|
+
| LLM | `LLMClient` | Anthropic, OpenAI |
|
|
49
|
+
| Sandbox | `Sandbox` | Docker, local subprocess |
|
|
50
|
+
| Tool | `Tool` | sql_query, run_python, ... |
|
|
51
|
+
| Database | SQLAlchemy URL | Postgres, MySQL, SQLite, ... |
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
You can install with either `uv` or plain `pip`. Both produce the same `terno`
|
|
56
|
+
CLI on your `PATH` and the same importable `terno_agent` package.
|
|
57
|
+
|
|
58
|
+
### Optional extras
|
|
59
|
+
|
|
60
|
+
Pick only what you need (or use `all` to get everything):
|
|
61
|
+
|
|
62
|
+
| Extra | What it pulls in |
|
|
63
|
+
| ----------- | ------------------------------- |
|
|
64
|
+
| `anthropic` | the `anthropic` SDK |
|
|
65
|
+
| `openai` | the `openai` SDK |
|
|
66
|
+
| `docker` | the `docker` SDK for sandboxing |
|
|
67
|
+
| `postgres` | `psycopg[binary]` |
|
|
68
|
+
| `mysql` | `pymysql` |
|
|
69
|
+
| `all` | all of the above |
|
|
70
|
+
| `dev` | pytest, ruff, mypy |
|
|
71
|
+
|
|
72
|
+
### With `uv` (recommended)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# install globally as a uv tool — `terno` works from anywhere
|
|
76
|
+
uv tool install terno-agent
|
|
77
|
+
uv tool install "terno-agent[anthropic,docker,postgres]"
|
|
78
|
+
|
|
79
|
+
# editable install from a local checkout
|
|
80
|
+
git clone https://github.com/terno-ai/terno-agent.git
|
|
81
|
+
cd terno-agent
|
|
82
|
+
uv tool install --editable ".[all]"
|
|
83
|
+
|
|
84
|
+
# add it as a dependency of another uv project
|
|
85
|
+
uv add terno-agent
|
|
86
|
+
uv add "terno-agent[anthropic,docker]"
|
|
87
|
+
|
|
88
|
+
# from a local path or git
|
|
89
|
+
uv add /path/to/terno_agent
|
|
90
|
+
uv add "git+https://github.com/terno-ai/terno-agent.git"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Refresh after changing `pyproject.toml`:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv tool install --editable ".[all]" --force
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### With `pip`
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# from PyPI
|
|
103
|
+
pip install terno-agent
|
|
104
|
+
pip install "terno-agent[anthropic,docker,postgres]"
|
|
105
|
+
|
|
106
|
+
# from a local checkout (editable)
|
|
107
|
+
git clone https://github.com/terno-ai/terno-agent.git
|
|
108
|
+
cd terno-agent
|
|
109
|
+
python -m venv .venv && source .venv/bin/activate
|
|
110
|
+
pip install -e ".[all]"
|
|
111
|
+
|
|
112
|
+
# from git
|
|
113
|
+
pip install "git+https://github.com/terno-ai/terno-agent.git"
|
|
114
|
+
|
|
115
|
+
# from a built wheel
|
|
116
|
+
pip install ./dist/terno_agent-0.1.0-py3-none-any.whl
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **Tip:** if you install into a project venv with plain `pip`, you have to
|
|
120
|
+
> activate the venv (or use its `bin/terno`) to run the CLI. `uv tool install`
|
|
121
|
+
> avoids this by giving the CLI its own isolated environment on `PATH`.
|
|
122
|
+
|
|
123
|
+
## Configure
|
|
124
|
+
|
|
125
|
+
Configuration is read from environment variables, with `.env` auto-loaded from
|
|
126
|
+
your current working directory (or any parent). Process env wins over `.env`.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cp .env.example .env
|
|
130
|
+
# then edit:
|
|
131
|
+
ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY=
|
|
132
|
+
TERNO_LLM_PROVIDER=anthropic # anthropic | openai
|
|
133
|
+
TERNO_LLM_MODEL=claude-opus-4-7
|
|
134
|
+
TERNO_DATABASE_URL=sqlite:///./demo.db
|
|
135
|
+
TERNO_SANDBOX=docker # docker | local | none
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Run `terno config` to print the effective settings (API keys masked).
|
|
139
|
+
|
|
140
|
+
### SQLAlchemy URL examples
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
sqlite:///./relative.db # relative to CWD
|
|
144
|
+
sqlite:////absolute/path/to.db # absolute (4 slashes)
|
|
145
|
+
postgresql+psycopg://user:pass@host:5432/db
|
|
146
|
+
mysql+pymysql://user:pass@host:3306/db
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Use the CLI
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# one-shot question
|
|
153
|
+
terno ask "what were the top 10 customers by revenue last quarter?"
|
|
154
|
+
|
|
155
|
+
# interactive REPL
|
|
156
|
+
terno chat
|
|
157
|
+
|
|
158
|
+
# suppress streaming/activity, print only the final answer
|
|
159
|
+
terno -q ask "how many tracks are in the database?"
|
|
160
|
+
|
|
161
|
+
# show effective config
|
|
162
|
+
terno config
|
|
163
|
+
|
|
164
|
+
# show version
|
|
165
|
+
terno --version
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If you installed with plain `pip` into a project venv and didn't activate it:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
.venv/bin/terno ask "..."
|
|
172
|
+
# or
|
|
173
|
+
python -m terno_agent ask "..."
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
If you installed with `uv` into the current project rather than as a tool:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
uv run terno ask "..."
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Use as a library
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from terno_agent import Agent
|
|
186
|
+
|
|
187
|
+
# Reads .env + env vars
|
|
188
|
+
agent = Agent.from_env()
|
|
189
|
+
result = agent.ask("how many active users signed up this week?")
|
|
190
|
+
print(result.answer)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Programmatic config (no env vars required):
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from terno_agent import Agent
|
|
197
|
+
from terno_agent.config import Config
|
|
198
|
+
|
|
199
|
+
cfg = Config(
|
|
200
|
+
llm_provider="anthropic",
|
|
201
|
+
llm_model="claude-opus-4-7",
|
|
202
|
+
llm_api_key="sk-ant-...",
|
|
203
|
+
database_url="postgresql+psycopg://u:p@host/db",
|
|
204
|
+
sandbox="local", # "docker" | "local" | "none"
|
|
205
|
+
)
|
|
206
|
+
agent = Agent.from_config(cfg)
|
|
207
|
+
print(agent.ask("top 5 tables by row count").answer)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Stream events into your own UI:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from terno_agent import Agent
|
|
214
|
+
from terno_agent.core.events import TextDelta, ToolCallEvent, ToolResultEvent
|
|
215
|
+
|
|
216
|
+
def on_event(e):
|
|
217
|
+
if isinstance(e, TextDelta):
|
|
218
|
+
print(e.text, end="", flush=True)
|
|
219
|
+
elif isinstance(e, ToolCallEvent):
|
|
220
|
+
print(f"\n[tool] {e.call.name}({e.call.arguments})")
|
|
221
|
+
elif isinstance(e, ToolResultEvent):
|
|
222
|
+
print(f"[result] {e.result.content[:200]}")
|
|
223
|
+
|
|
224
|
+
agent = Agent.from_env(on_event=on_event)
|
|
225
|
+
agent.ask("describe the users table and count rows")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Project layout
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
src/terno_agent/
|
|
232
|
+
cli.py # argparse entry point + rich renderer
|
|
233
|
+
config.py # env + .env-driven Config
|
|
234
|
+
core/ # message/tool/event/exception types
|
|
235
|
+
llm/ # LLMClient protocol + Anthropic + OpenAI (streaming)
|
|
236
|
+
agents/ # orchestrator + specialist agents
|
|
237
|
+
tools/ # sql_query, run_python, list_tables, describe_table
|
|
238
|
+
sandbox/ # Docker + local runners
|
|
239
|
+
db/ # SQLAlchemy engine & inspector
|
|
240
|
+
prompts/ # system prompts per agent
|
|
241
|
+
tests/ # pytest suite
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Develop
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# clone + editable install with dev extras
|
|
248
|
+
git clone https://github.com/terno-ai/terno-agent.git
|
|
249
|
+
cd terno-agent
|
|
250
|
+
uv venv --python 3.12
|
|
251
|
+
uv pip install -e ".[dev,all]"
|
|
252
|
+
|
|
253
|
+
# tests
|
|
254
|
+
uv run pytest -q
|
|
255
|
+
|
|
256
|
+
# lint / format / typecheck
|
|
257
|
+
uv run ruff check .
|
|
258
|
+
uv run ruff format .
|
|
259
|
+
uv run mypy src
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Or with plain `pip`:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
python -m venv .venv && source .venv/bin/activate
|
|
266
|
+
pip install -e ".[dev,all]"
|
|
267
|
+
pytest -q
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|