Metadata-Version: 2.5
Name: signalcore
Version: 2.0.0
Summary: SignalCore's server SDK: agents, conversations and everything of the public API, from your backend.
Project-URL: Homepage, https://signalcore.ai
Project-URL: Documentation, https://signalcore.ai/docs
Project-URL: Repository, https://github.com/OmnistGroup/signalcore
Author-email: SignalCore <ismael@signalcore.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: agents,ai,sdk,signalcore
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: typing-extensions>=4.7
Requires-Dist: websockets>=13
Description-Content-Type: text/markdown

# signalcore

SignalCore's server SDK for Python: your backend makes organizations for your customers, sets up
their agents, talks to them and hears what happens, through SignalCore's public API
(`https://signalcore.ai/v1`). Every operation of the API is a typed method; the SDK holds no logic
of its own.

```bash
pip install "signalcore>=2"     # uv add, poetry add alike
```

Python 3.10 or later. Version 2 is for SignalCore 2.0's API; 0.x was for the API it replaced.

## Quickstart

Make a secret key (`sk_…`) in SignalCore, keep it on your server, and talk to an agent:

```python
from signalcore import SignalCore

client = SignalCore("sk_…")  # or set SIGNALCORE_API_KEY

squad = client.squads.list()[0]
agent = client.agents.create(squad["id"], name="Recepción", instructions="Answer briefly.")
conversation = client.conversations.start(agent["id"])

reply = client.conversations.say(conversation["id"], text="¿Abrís el sábado?", wait=True)
print(reply["status"], reply["replies"][0]["text"])
```

With `wait=True` the answer comes when the agent's turn ends (up to a minute): `status` is `done`,
`waiting` (on a question or an approval, in `waiting`) or `failed` (`error`). Without it the call
answers at once with `running`, and the reply comes on the live socket or a webhook.

Answers are plain dicts typed with `TypedDict`s (`signalcore.types`), so your editor knows every
field.

## Your customers' organizations

A key acts on its own organization, or on one it manages with `org`:

```python
customer = client.orgs.create(name="Clínica Norte")
theirs = SignalCore("sk_…", org=customer["id"])  # SignalCore-Org
theirs.squads.list()  # their own agents, data and channels, never shared
```

## A person in your page

Your backend mints a token for the person (an hour, the agents it names), and your page talks with
it (`@signalcore/ui`):

```python
token = client.tokens.create(agents=[agent["id"]], external_id=user.id, name=user.name)
return {"token": token["token"]}
```

## Live

Every frame of a conversation as it happens (`message`, `text`, `step`, `done`, …), as an async
iterator:

```python
from signalcore import AsyncSignalCore

async with AsyncSignalCore("sk_…") as client:
    async with client.conversations.live(conversation_id) as live:
        await live.say("Hola")
        async for frame in live:
            if frame["type"] == "done":
                break
```

`SignalCore` has the same `live()`, iterated with a plain `for`. `AsyncSignalCore` has every
method of `SignalCore`, awaited.

## Webhooks

Register an endpoint (`client.webhooks.create(url=…, events=[…])` answers its `secret` once), and
check each delivery before trusting it, with the body exactly as it came:

```python
from signalcore import SignatureError, verify

@app.post("/signalcore")
async def hook(request: Request):
    try:
        event = verify(await request.body(), request.headers, WEBHOOK_SECRET)
    except SignatureError:
        raise HTTPException(400)
    if event["type"] == "message.created":
        ...
```

`client.webhooks.verify` is the same function.

## Errors

Whatever the API refuses or fails raises one `ApiError`: `status` (HTTP), `code` to decide by
(`not_found`, `forbidden`, `busy`, `rate_limited`, …) and `detail`, a sentence you can show.

```python
from signalcore import ApiError

try:
    client.agents.get("agt_…")
except ApiError as error:
    if error.code == "not_found":
        ...
```

## Every operation

Each operation of the API reference is a method named from its id, `<group>_<verb>`: `agents_get`
is `client.agents.get(agent_id)`, `agents_memories_create` is `client.agents.memories_create(…)`.
A few read better renamed: `conversations.say` (`conversations_messages_create`), `runs.list` and
`runs.get`, `agents.create(squad_id, …)`; `signalcore.OPERATIONS` maps every id to its method.

- Path parameters are positional; the body's fields and the query are keyword arguments.
- A field left out isn't sent; `None` sends `null`.
- Writes take `idempotency_key=`: the same key answers the first answer again for a day.
- Lists read page after page with `iter`: `for run in client.runs.iter(agent=agent_id): …`
  (`activity.iter` too).
- Files go up as bytes (`client.files.put("catalog.pdf", data, content_type="application/pdf")`)
  and come down as bytes (`client.files.get("catalog.pdf")`).

## Developing it

Half of the package is generated from the API's contract (`v3/api/openapi.json`):
`_resources.py` and `_types.py`, by `python v3/clients/generate.py`. The rest is by hand:
`_base.py` (requests, errors, pages), `_extras.py` (the live socket, webhooks). When the contract
changes, generate again; CI fails while they differ.

```bash
uv sync
uv run pytest      # against the local API (v3-api) at http://localhost:8300, or SIGNALCORE_TEST_API
uv run mypy && uv run ruff check
```

The tests that need an API sign up an owner of their own there and skip themselves when none
answers.
