Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,15 @@ src/smallestai/waves/helpers/**

# Hand-written README (do not regenerate)
README.md


# Prebuilt tools framework (hand-written)
src/smallestai/tools/**

# Anonymous opt-out telemetry (hand-written)
src/smallestai/telemetry.py

# Dev quality/security tooling (hand-written)
.pre-commit-config.yaml
.gitleaks.toml
.deepsource.toml
45 changes: 44 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,49 @@ jobs:
- name: Test
run: poetry run pytest -rP -n auto .

lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install ruff
run: pip install ruff==0.16.1
- name: Ruff lint
run: ruff check .
- name: Ruff format check
run: ruff format --check .

security:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Secret scan (gitleaks)
run: |
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz | tar -xz gitleaks
./gitleaks dir . --config .gitleaks.toml --redact --no-banner
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
- name: Install dependencies
run: poetry install
- name: Dependency audit (pip-audit)
# Report-only for now: the pinned transitive deps (pytest, requests, urllib3,
# starlette, setuptools, python-dotenv) carry a pre-existing advisory backlog to
# clear in a dependency-bump PR. Flip to blocking (remove continue-on-error) after.
continue-on-error: true
run: |
poetry run pip install pip-audit
poetry run pip-audit

# Auto-publish to PyPI when the version in pyproject.toml has been bumped
# beyond the latest git tag. Only fires on pushes to main, only after
# compile+test pass. No-ops on pushes that didn't change the version.
Expand All @@ -57,7 +100,7 @@ jobs:
# Required repo secret: PYPI_API_TOKEN (a PyPI API token with upload scope
# for the `smallestai` project).
publish:
needs: [compile, test]
needs: [compile, test, lint, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
Expand Down
17 changes: 17 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# gitleaks config (used by the pre-commit hook and the CI security job).
# Extends the built-in ruleset and allowlists the PostHog project key, which is a
# write-only client key that is meant to ship in client code.
[extend]
useDefault = true

[allowlist]
description = "PostHog public write-only project key, and generated wire-test fixtures"
regexes = [
'''phc_[A-Za-z0-9]{40,}''',
]
# wiremock/wiremock-mappings.json is a generated wire-test fixture. Its stub
# responses contain example (non-real) LiveKit JWT tokens for the web-call
# endpoints, which gitleaks flags as JWTs. It is test data, not source secrets.
paths = [
'''wiremock/wiremock-mappings\.json''',
]
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Local quality + security gates. Install once with `pre-commit install`,
# then hooks run on every commit. Check everything with `pre-commit run --all-files`.
# Keep the ruff rev in sync with the `lint` job in .github/workflows/ci.yml.
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.1
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ endpoint (a hosted API, or a local model via Ollama):
from smallestai.atoms.crew.nodes import OutputCrewNode
from smallestai.atoms.crew.clients.openai import OpenAIClient


class Assistant(OutputCrewNode):
def __init__(self):
super().__init__(name="assistant")
self.llm = OpenAIClient(
model="claude-haiku-4-5",
api_key="<your-llm-key>",
base_url="https://api.anthropic.com/v1/", # or http://localhost:11434/v1 for Ollama
base_url="https://api.anthropic.com/v1/", # or http://localhost:11434/v1 for Ollama
)

async def generate_response(self):
Expand Down Expand Up @@ -129,11 +130,13 @@ The SDK exports an `async` client with the same surface:
import asyncio
from smallestai import AsyncSmallestAI


async def main():
client = AsyncSmallestAI(api_key="<your-api-key>")
agents = await client.atoms.agents.list_agents()
print(agents.data)


asyncio.run(main())
```

Expand Down Expand Up @@ -182,9 +185,9 @@ Use `.with_raw_response` to get the response headers and status alongside the pa

```python
response = client.atoms.agents.with_raw_response.get_agent(id="<agent-id>")
print(response.headers) # response headers
print(response.status_code) # status code
print(response.data) # parsed object
print(response.headers) # response headers
print(response.status_code) # status code
print(response.data) # parsed object
```

### Retries
Expand Down Expand Up @@ -231,6 +234,20 @@ client = SmallestAI(
- Full API reference: [reference.md](./reference.md)
- Product docs: https://smallest.ai/docs

## Telemetry

The SDK sends anonymous, aggregated usage telemetry (which CLI commands run, deploy
outcomes) so we can see what to improve. It never includes personal data or secrets:
no API keys, agent ids, prompts, transcripts, phone numbers, file paths, or error
messages. Only the event name, SDK / Python / OS version, and a random anonymous
install id. It is fire-and-forget and never blocks your program.

Opt out any time:

```bash
export SMALLESTAI_TELEMETRY=0 # or DO_NOT_TRACK=1
```

## Contributing

Most of `src/` is generated from an API spec and gets overwritten on regeneration,
Expand Down
40 changes: 40 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
## 5.10.0 - 2026-08-07

* **tools**: new `smallestai.tools` framework for prebuilt, pluggable crew tools. Each tool
plugs into a crew's `ToolRegistry` and is also callable directly; third-party libraries
install as optional extras, lazy-imported. First tool: `ExaSearchTool` (web search;
`pip install "smallestai[exa]"`, reads `EXA_API_KEY`).
* **cli**: new `smallestai agent-crew doctor [--agent-id]` inspects a crew agent and flags
common gotchas (no live build, wrong workflow type, PII redaction on, `transfer_call`
toggled in the dashboard). `agent-crew init` now prints the crew-vs-platform config
ownership boundary.
* **cli**: new `smallestai mcp` command to set up or run the Smallest AI MCP server
(`@developer-smallestai/smallest-mcp-server`) for Cursor / Claude. `mcp` prints the config
+ `claude mcp add` line, `mcp run` launches it via npx, `mcp config` prints the mcp.json.
* **cli**: running bare `smallestai` now shows a banner + command list instead of a
"Missing command" error.
* **telemetry**: anonymous, opt-out usage telemetry (PostHog). No personal data or secrets:
only the event name, SDK/Python/OS version, and a random anonymous install id.
Fire-and-forget, never blocks. Opt out with `SMALLESTAI_TELEMETRY=0` (or `DO_NOT_TRACK=1`).
* **crew**: `SDKSystemUpdateOutputAgentSettingsEvent` is deprecated - the platform does not
apply crew-sent output-agent settings. It will be removed in a future release.
* **dev**: pre-commit hooks (ruff lint + format, gitleaks) and CI quality/security jobs -
`ruff check` / `ruff format --check` and a gitleaks secret scan gate publishing, plus
`pip-audit` dependency-CVE reporting (report-only for now).
* **api (Voice Agents)**: new endpoints on the client:
* `client.atoms.user.get_subscription()` - plan id, credit balance, per-plan limits, feature flags.
* `client.atoms.account.get_account_details()` - profile plus the orgs the user belongs to.
* `client.atoms.account.update_organization_name(...)` - rename the active org (owner role).
* `client.atoms.web_call.start_web_chat_conversation(...)` / `start_web_call_conversation(...)` - mint a LiveKit token + room for a browser text/voice session.
* `client.atoms.campaigns.export_campaign_logs(...)` - campaign call logs.
* `client.atoms.campaigns.export_campaign_results_by_audience_member(...)` - results grouped by contact (`format=json|csv`).
* **api (Speech)**: new endpoints on the client:
* `client.waves.post_call_analysis.analyze(...)` - disposition metrics from a transcript.
* `client.waves.post_call_analysis.generate(...)` - single-prompt text generation.
* `client.waves.voices.get_all_voice_models()` - the full voice catalog.
* `client.waves.analytics.*` - ASR/TTS logs and usage/credits/concurrency timeseries, webhook logs.
* `client.waves.ops.get_waves_health()` - service health.
* **api (removed)**: `client.atoms.organization` is removed. Its endpoint (`GET /organization`)
returned 404 on the public API and was never functional; use
`client.atoms.account.get_account_details()` / `client.atoms.user.get_user_details()` instead.

## 5.5.0 - 2026-08-05

DevX pass (backward-compatible).
Expand Down
6 changes: 3 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ The CLI (`smallestai`) reads `SMALLEST_API_KEY`, or the key stored by
```python
agent_id = client.atoms.agents.create_agent(
name="Front desk",
workflow_type="single_prompt", # the default; the other type is workflow_graph
workflow_type="single_prompt", # the default; the other type is workflow_graph
first_message="Hi, thanks for calling. How can I help?",
).data
print(agent_id) # NOTE: .data here is the agent id string, not an object
print(agent_id) # NOTE: .data here is the agent id string, not an object
```

Two things worth knowing up front:
Expand All @@ -43,7 +43,7 @@ Two things worth knowing up front:
client.atoms.calls.start_outbound_call(
agent_id=agent_id,
phone_number="+15559990000",
from_product_id="<telephony_product_id>", # a number you own; see client.atoms.phone_numbers.list()
from_product_id="<telephony_product_id>", # a number you own; see client.atoms.phone_numbers.list()
)
```

Expand Down
1 change: 1 addition & 0 deletions docs/guides/crew.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ endpoint, e.g. Claude via Anthropic) and full control over the turn. You subclas
```python
from smallestai.atoms.crew.nodes import OutputCrewNode


class Assistant(OutputCrewNode):
def __init__(self):
super().__init__(name="assistant")
Expand Down
9 changes: 5 additions & 4 deletions docs/guides/transfer-call.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ the next call.
```python
from smallestai.atoms.helpers import AgentTools

tools = AgentTools(api_key="sk_...") # or SMALLEST_API_KEY env var
tools = AgentTools(api_key="sk_...") # or SMALLEST_API_KEY env var

tools.add_transfer_call(
"AGENT_ID",
number="+15551234567",
transfer_type="cold_transfer", # or "warm_transfer"
on_hold_music="relaxing_sound", # audio while bridging
transfer_type="cold_transfer", # or "warm_transfer"
on_hold_music="relaxing_sound", # audio while bridging
)
```

Expand Down Expand Up @@ -97,13 +97,14 @@ from smallestai.atoms.crew.events import (
TransferOptionType,
)


@function_tool(name="transfer_call")
async def transfer_call(self) -> None:
await self.send_event(
SDKAgentTransferConversationEvent(
transfer_call_number="+15551234567",
transfer_options=TransferOption(type=TransferOptionType.COLD_TRANSFER),
on_hold_music="relaxing_sound", # optional; omit for platform default
on_hold_music="relaxing_sound", # optional; omit for platform default
)
)
```
Expand Down
13 changes: 8 additions & 5 deletions examples/agent_versioning_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
export SMALLEST_API_KEY=sk_...
python examples/agent_versioning_lifecycle.py
"""

import os

from smallestai import SmallestAI
from smallestai.atoms.helpers.versioning import Versioning, DraftConflictError
from smallestai.atoms.helpers.versioning import DraftConflictError, Versioning

client = SmallestAI(api_key=os.environ["SMALLEST_API_KEY"])

Expand Down Expand Up @@ -44,7 +45,8 @@
# Quick edit: change the config and publish in one call (draft -> publish -> live,
# security scan handled). This is the simple "update my agent" path.
revision = v.edit_and_publish(
agent_id, main_id,
agent_id,
main_id,
global_prompt="You are a warm, concise receptionist. Confirm details before booking.",
label="tone tweak",
)
Expand All @@ -60,7 +62,7 @@
# Fork Main so real traffic keeps hitting the live config while you experiment.
staging_id = v.branches.create_branch(id=agent_id, source_branch_id=main_id, name="staging").data.id
v.edit_and_publish(agent_id, staging_id, global_prompt="New experimental prompt.", label="experiment")
v.branches.make_live(id=agent_id, branch_id=staging_id) # staging live, Main flips to not-live
v.branches.make_live(id=agent_id, branch_id=staging_id) # staging live, Main flips to not-live

# Roll back: restore an older revision as a new head revision on the live branch.
older = v.revisions.list(id=agent_id, branch_id=staging_id).data.revisions[-1].id
Expand All @@ -72,8 +74,9 @@
draft = v.branches.get_draft(id=agent_id, branch_id=main_id).data
try:
v.edit_and_publish(
agent_id, main_id,
expected_revision=draft.latest.draft_revision, # only apply if nobody edited since
agent_id,
main_id,
expected_revision=draft.latest.draft_revision, # only apply if nobody edited since
global_prompt="Applied only if no conflicting edit landed first.",
)
except DraftConflictError as e:
Expand Down
7 changes: 3 additions & 4 deletions examples/build_voice_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
see the cookbook / `smallestai agent-crew deploy`. This script proves the
create-configure-publish-activate path that every voice agent starts from.
"""

import os
import time

from smallestai import SmallestAI
from smallestai.environment import SmallestAIEnvironment
from smallestai.atoms.helpers import as_page
from smallestai.environment import SmallestAIEnvironment


def _id_of(obj):
Expand Down Expand Up @@ -70,9 +71,7 @@ def main() -> None:
print("4. (optional) knowledge base for the menu")
created_kb = None
try:
kb = c.atoms.knowledge_base.create(
name="mario-menu", description="Pizza menu + prices"
)
kb = c.atoms.knowledge_base.create(name="mario-menu", description="Pizza menu + prices")
created_kb = getattr(kb, "data", None)
print(" KB created:", created_kb)
except Exception as e:
Expand Down
8 changes: 4 additions & 4 deletions examples/crew_transfer_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
requirements.txt pinning smallestai>=5.4.0 plus your LLM client):
smallestai agent-crew deploy --entry-point server.py
"""

import os

from smallestai.atoms.crew.nodes import OutputCrewNode
from smallestai.atoms.crew.events import (
SDKAgentTransferConversationEvent,
TransferOption,
TransferOptionType,
)
from smallestai.atoms.crew.nodes import OutputCrewNode

# Bring your own OpenAI-compatible client + tool registry. This example assumes a
# client exposing `.chat(messages=..., stream=True, tools=...)` and a registry that
Expand All @@ -41,6 +42,7 @@ def _decorator(fn):

return _decorator


TRANSFER_NUMBER = os.getenv("TRANSFER_CALL_NUMBER", "+15551234567")


Expand Down Expand Up @@ -82,9 +84,7 @@ async def transfer_call(self) -> None:
async def generate_response(self):
# self.context.messages is seeded from the platform's authoritative
# messages before this runs (5.4.0), so it always has the latest user turn.
response = await self.llm.chat(
messages=self.context.messages, stream=True, tools=self.tool_schemas
)
response = await self.llm.chat(messages=self.context.messages, stream=True, tools=self.tool_schemas)
full = ""
tool_calls = []
async for chunk in response:
Expand Down
1 change: 1 addition & 0 deletions examples/inspect_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
python examples/inspect_calls.py # lists recent calls
python examples/inspect_calls.py CALL-... # details + transcript for one call
"""

import os
import sys

Expand Down
Loading
Loading