diff --git a/.fernignore b/.fernignore index 9b54b840..09b1d6b4 100644 --- a/.fernignore +++ b/.fernignore @@ -48,3 +48,6 @@ RELEASE.md scripts/** Makefile src/smallestai/waves/helpers/** + +# Hand-written README (do not regenerate) +README.md diff --git a/README.md b/README.md index 26c8b8f1..ece71c61 100644 --- a/README.md +++ b/README.md @@ -1,225 +1,224 @@ -# SmallestAi Python Library +# SmallestAI Python SDK -[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fsmallest-inc%2Fsmallest-python-sdk) [![pypi](https://img.shields.io/pypi/v/smallestai)](https://pypi.python.org/pypi/smallestai) -The SmallestAi Python library provides convenient access to the SmallestAi APIs from Python. +`pip install smallestai` gives you one package with two surfaces, plus a CLI: + +- **Atoms** — build, configure, deploy, and phone-call voice AI agents (`client.atoms`). +- **Waves** — low-latency text-to-speech and speech-to-text, sync/async and streaming (`client.waves`). +- **CLI** — `smallestai` for managing agents and deploying agent-crew code. + +```sh +pip install smallestai +``` ## Table of Contents -- [Installation](#installation) -- [Reference](#reference) -- [Usage](#usage) +- [Quickstart](#quickstart-create-an-agent-and-call-it) +- [Text-to-speech and speech-to-text](#text-to-speech-and-speech-to-text-waves) +- [Agent crew: your own LLM](#agent-crew-your-own-llm-in-the-middle) +- [CLI](#cli) +- [Async client](#async-client) - [Environments](#environments) -- [Async Client](#async-client) -- [Exception Handling](#exception-handling) -- [Streaming](#streaming) -- [Websockets](#websockets) +- [Exception handling](#exception-handling) +- [Streaming and websockets](#streaming-and-websockets) - [Advanced](#advanced) - - [Access Raw Response Data](#access-raw-response-data) + - [Access raw response data](#access-raw-response-data) - [Retries](#retries) - [Timeouts](#timeouts) - - [Custom Client](#custom-client) + - [Custom client](#custom-client) +- [Reference and docs](#reference-and-docs) - [Contributing](#contributing) -## Installation +## Quickstart: create an agent and call it -```sh -pip install smallestai -``` +```python +from smallestai import SmallestAI -## Reference +client = SmallestAI(api_key="") -A full reference for this library is available [here](https://github.com/smallest-inc/smallest-python-sdk/blob/HEAD/./reference.md). +# create an agent (the response .data is the new agent id) +agent_id = client.atoms.agents.create_agent(name="my-first-agent").data -## Usage +# place an outbound call (from_product_id is a rented number's product id) +client.atoms.calls.start_outbound_call( + agent_id=agent_id, + phone_number="+1XXXXXXXXXX", + from_product_id="", +) +``` -Instantiate and use the client with the following: +## Text-to-speech and speech-to-text (Waves) + +`synthesize_tts` streams audio bytes. List available voices with `client.waves.get_voices()`. ```python from smallestai import SmallestAI -client = SmallestAI( - api_key="", -) +client = SmallestAI(api_key="") -client.atoms.agent_templates.create_agent_from_template( - agent_name="agentName", - template_id="templateId", -) +with open("out.wav", "wb") as f: + for chunk in client.waves.synthesize_tts(text="Hello from Smallest.", voice_id=""): + f.write(chunk) ``` -## Environments - -This SDK allows you to configure different environments for API requests. +Streaming speech-to-text helper: ```python -from smallestai import SmallestAI -from smallestai.environment import SmallestAIEnvironment +from smallestai.waves.helpers import stream_speech_to_text -client = SmallestAI( - environment=SmallestAIEnvironment.PRODUCTION, -) +for event in stream_speech_to_text(client, language="en"): + print(event) ``` -## Async Client +## Agent crew: your own LLM in the middle -The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client). +An agent crew runs the LLM turn on a model **you** choose while Smallest handles +STT and TTS. Point the crew node's `OpenAIClient` at any OpenAI-compatible +endpoint (a hosted API, or a local model via Ollama): ```python -import asyncio +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="", + base_url="https://api.anthropic.com/v1/", # or http://localhost:11434/v1 for Ollama + ) + + async def generate_response(self): + async for chunk in await self.llm.chat(self.context.messages, stream=True): + if chunk.content: + yield chunk.content +``` -from smallestai import AsyncSmallestAI +Conversation history is handled for you: every turn is appended to `self.context`, +and you send `self.context.messages` to the model each turn. -client = AsyncSmallestAI( - api_key="", -) +Deploy it with the CLI: +```sh +smallestai auth login +smallestai agent-crew init --agent-id +smallestai agent-crew deploy --entry-point server.py +smallestai agent-crew builds # pick the build -> Make Live +``` -async def main() -> None: - await client.atoms.agent_templates.create_agent_from_template( - agent_name="agentName", - template_id="templateId", - ) +A flat directory (`server.py` + `requirements.txt` at the root) is the simplest +layout; a `src/` layout with a `pyproject.toml` also works (declare all runtime +deps in the pyproject). +## CLI -asyncio.run(main()) +```sh +smallestai auth login # store your API key +smallestai agents list # list, get, call, and manage agents +smallestai agent-crew deploy ... # package and deploy crew code +smallestai agent-crew chat # talk to a running crew locally ``` -## Exception Handling +## Async client -When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error -will be thrown. +The SDK exports an `async` client with the same surface: ```python -from smallestai.core.api_error import ApiError +import asyncio +from smallestai import AsyncSmallestAI -try: - client.atoms.agent_templates.create_agent_from_template(...) -except ApiError as e: - print(e.status_code) - print(e.body) -``` +async def main(): + client = AsyncSmallestAI(api_key="") + agents = await client.atoms.agents.list_agents() + print(agents.data) -## Streaming +asyncio.run(main()) +``` -The SDK supports streaming responses, as well, the response will be a generator that you can loop over. +## Environments ```python from smallestai import SmallestAI +from smallestai.environment import SmallestAIEnvironment -client = SmallestAI( - api_key="", -) - -client.atoms.live_transcripts.subscribe_to_live_events( - call_id="CALL-1758124225863-80752e", -) +client = SmallestAI(environment=SmallestAIEnvironment.PRODUCTION) ``` -## Websockets - -The SDK supports both sync and async websocket connections for real-time, low-latency communication. Sockets can be created using the `connect` method, which returns a context manager. -You can either iterate through the returned `SocketClient` to process messages as they arrive, or attach handlers to respond to specific events. +## Exception handling ```python -from smallestai import SmallestAI +from smallestai.core.api_error import ApiError -client = SmallestAI(...) +try: + client.atoms.agents.get_agent(id="does-not-exist") +except ApiError as e: + print(e.status_code, e.body) +``` -# Connect to the websocket (Sync) -with client.speech_to_text.stream() as socket: - # Iterate over the messages as they arrive - for message in socket: - print(message) +## Streaming and websockets - # Or, attach handlers to specific events - socket.on(EventType.MESSAGE, lambda message: print("received message", message)) +Waves supports real-time, low-latency streaming over websockets. `stream()` returns a context manager; iterate it to process messages as they arrive. -import asyncio -from smallestai import AsyncSmallestAI +```python +from smallestai import SmallestAI -client = AsyncSmallestAI(...) +client = SmallestAI(api_key="") -# Connect to the websocket (Async) -async with client.speech_to_text.stream() as socket: - async for message in socket: +# real-time speech-to-text +with client.waves.speech_to_text.stream() as socket: + for message in socket: print(message) ``` +The async client mirrors this with `async with` / `async for`. Text-to-speech streams too: `synthesize_tts(...)` yields audio bytes as they are generated (see above). + ## Advanced -### Access Raw Response Data +### Access raw response data -The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. -The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. +Use `.with_raw_response` to get the response headers and status alongside the parsed data. ```python -from smallestai import SmallestAI - -client = SmallestAI(...) -response = client.atoms.agent_templates.with_raw_response.create_agent_from_template(...) -print(response.headers) # access the response headers -print(response.status_code) # access the response status code -print(response.data) # access the underlying object +response = client.atoms.agents.with_raw_response.get_agent(id="") +print(response.headers) # response headers +print(response.status_code) # status code +print(response.data) # parsed object ``` ### Retries -The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long -as the request is deemed retryable and the number of retry attempts has not grown larger than the configured -retry limit (default: 2). - -Which status codes are retried depends on the `retryStatusCodes` generator configuration: - -**`legacy`** (current default): retries on -- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) -- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) -- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) - -**`recommended`**: retries on -- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) -- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) -- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) -- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) -- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) - -Use the `max_retries` request option to configure this behavior. +The SDK retries retryable requests with exponential backoff (default 2). Configure it at the client or per request. ```python -client.atoms.agent_templates.create_agent_from_template(..., request_options={ - "max_retries": 1 -}) +client = SmallestAI(api_key="", max_retries=3) + +# or per request +client.atoms.agents.get_agent(id="", request_options={"max_retries": 1}) ``` ### Timeouts -The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. +Defaults to 60 seconds. Configure at the client or per request. ```python -from smallestai import SmallestAI +client = SmallestAI(api_key="", timeout=20.0) -client = SmallestAI(..., timeout=20.0) - -# Override timeout for a specific method -client.atoms.agent_templates.create_agent_from_template(..., request_options={ - "timeout_in_seconds": 1 -}) +# or per request +client.atoms.agents.get_agent(id="", request_options={"timeout_in_seconds": 5}) ``` -### Custom Client +### Custom client -You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies -and transports. +Override the `httpx` client for proxies, custom transports, and similar. ```python import httpx from smallestai import SmallestAI client = SmallestAI( - ..., + api_key="", httpx_client=httpx.Client( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), @@ -227,12 +226,12 @@ client = SmallestAI( ) ``` -## Contributing +## Reference and docs -While we value open-source contributions to this SDK, this library is generated programmatically. -Additions made directly to this library would have to be moved over to our generation code, -otherwise they would be overwritten upon the next generated release. Feel free to open a PR as -a proof of concept, but know that we will not be able to merge it as-is. We suggest opening -an issue first to discuss with us! +- Full API reference: [reference.md](./reference.md) +- Product docs: https://smallest.ai/docs + +## Contributing -On the other hand, contributions to the README are always very welcome! +Most of `src/` is generated from an API spec and gets overwritten on regeneration, +so hand edits there will not stick. If you spot a bug or a gap, open an issue. diff --git a/changelog.md b/changelog.md index 7a722331..99814d30 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,15 @@ +## 5.3.3 - 2026-07-30 + +CLI deploy now reports real build status, and a hand-written README. No API changes. + +* **CLI** (`smallestai agent-crew deploy`): after upload, the command polls the build + to a terminal state (SUCCEEDED / failed) instead of returning at "queued", and on + success points to `agent-crew builds` -> Make Live with a note that the first call + after Make Live can need a few seconds while the pod warms up. +* **README**: replaced the generated boilerplate with a hand-written front door + (agents, Waves TTS/STT, agent crew with a custom LLM, CLI). Added to `.fernignore` + so regeneration no longer overwrites it. + ## 5.3.2 - 2026-07-30 Corrects the agent-crew deploy layout warning. No API or behaviour changes. diff --git a/pyproject.toml b/pyproject.toml index 70fb51b1..74f26e4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ dynamic = ["version"] [tool.poetry] name = "smallestai" -version = "5.3.2" +version = "5.3.3" description = "" readme = "README.md" authors = [] diff --git a/src/smallestai/cli/agent_crew.py b/src/smallestai/cli/agent_crew.py index 23129772..86f1bd18 100644 --- a/src/smallestai/cli/agent_crew.py +++ b/src/smallestai/cli/agent_crew.py @@ -236,9 +236,52 @@ async def async_deploy(directory: str, entry_point: str): api_key=access_token, ) - console.print("[bold green]✓ Deployment successful![/bold green]") + console.print("[bold green]✓ Deployment uploaded![/bold green]") console.print(f"[dim]Build ID: {result.build_id}[/dim]") - console.print(f"[dim]Status: {result.message}[/dim]") + + # Poll the build to a terminal state instead of returning at "queued". + # The build has to reach SUCCEEDED before you can Make Live, and a + # freshly-live crew pod can need a moment to warm up before it serves + # the first call. Surfacing the real status here saves guessing. + console.print("[yellow]Building (this usually takes 1-2 min)...[/yellow]") + terminal = {"SUCCEEDED", "BUILD_FAILED", "DEPLOY_FAILED", "FAILED"} + last = None + status = None + for _ in range(60): # ~5 min cap at 5s intervals + try: + build = await atoms_client.get_agent_build( + agent_id=agent_id, + build_id=result.build_id, + api_key=access_token, + ) + status = str(getattr(build.status, "value", build.status)) + except Exception: + status = None + if status and status != last: + console.print(f"[dim] {status}[/dim]") + last = status + if status in terminal: + break + await asyncio.sleep(5) + + if status == "SUCCEEDED": + console.print("[bold green]✓ Build succeeded.[/bold green]") + console.print( + "[dim]Next: `smallestai agent-crew builds` -> Make Live. " + "The first call right after Make Live can take a few seconds " + "while the pod warms up; if it's silent, give it a moment and " + "try again.[/dim]" + ) + elif status in terminal: + console.print( + f"[red]Build ended with status: {status}. " + "Check `smallestai agent-crew builds` for details.[/red]" + ) + else: + console.print( + "[yellow]Still building. Check `smallestai agent-crew builds` " + "for the final status.[/yellow]" + ) except Exception as e: console.print(f"[red]Error deploying agent {e}[/red]")