signalcore 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.
Files changed (43) hide show
  1. signalcore-0.1.0/.gitignore +40 -0
  2. signalcore-0.1.0/LICENSE +21 -0
  3. signalcore-0.1.0/PKG-INFO +168 -0
  4. signalcore-0.1.0/README.md +142 -0
  5. signalcore-0.1.0/pyproject.toml +47 -0
  6. signalcore-0.1.0/src/signalcore/__init__.py +40 -0
  7. signalcore-0.1.0/src/signalcore/_client.py +177 -0
  8. signalcore-0.1.0/src/signalcore/_http.py +139 -0
  9. signalcore-0.1.0/src/signalcore/_pages.py +40 -0
  10. signalcore-0.1.0/src/signalcore/_sse.py +60 -0
  11. signalcore-0.1.0/src/signalcore/_sync.py +151 -0
  12. signalcore-0.1.0/src/signalcore/_version.py +5 -0
  13. signalcore-0.1.0/src/signalcore/errors.py +105 -0
  14. signalcore-0.1.0/src/signalcore/events.py +74 -0
  15. signalcore-0.1.0/src/signalcore/py.typed +0 -0
  16. signalcore-0.1.0/src/signalcore/resources/__init__.py +1 -0
  17. signalcore-0.1.0/src/signalcore/resources/_base.py +31 -0
  18. signalcore-0.1.0/src/signalcore/resources/agents.py +157 -0
  19. signalcore-0.1.0/src/signalcore/resources/api_keys.py +29 -0
  20. signalcore-0.1.0/src/signalcore/resources/billing.py +10 -0
  21. signalcore-0.1.0/src/signalcore/resources/browser.py +21 -0
  22. signalcore-0.1.0/src/signalcore/resources/build.py +59 -0
  23. signalcore-0.1.0/src/signalcore/resources/channels.py +185 -0
  24. signalcore-0.1.0/src/signalcore/resources/connections.py +72 -0
  25. signalcore-0.1.0/src/signalcore/resources/contacts.py +43 -0
  26. signalcore-0.1.0/src/signalcore/resources/conversations.py +82 -0
  27. signalcore-0.1.0/src/signalcore/resources/documents.py +75 -0
  28. signalcore-0.1.0/src/signalcore/resources/end_users.py +56 -0
  29. signalcore-0.1.0/src/signalcore/resources/files.py +56 -0
  30. signalcore-0.1.0/src/signalcore/resources/live.py +22 -0
  31. signalcore-0.1.0/src/signalcore/resources/memories.py +41 -0
  32. signalcore-0.1.0/src/signalcore/resources/org.py +15 -0
  33. signalcore-0.1.0/src/signalcore/resources/orgs.py +20 -0
  34. signalcore-0.1.0/src/signalcore/resources/playground.py +45 -0
  35. signalcore-0.1.0/src/signalcore/resources/publishable_keys.py +28 -0
  36. signalcore-0.1.0/src/signalcore/resources/records.py +48 -0
  37. signalcore-0.1.0/src/signalcore/resources/requests.py +38 -0
  38. signalcore-0.1.0/src/signalcore/resources/runs.py +64 -0
  39. signalcore-0.1.0/src/signalcore/resources/skills.py +53 -0
  40. signalcore-0.1.0/src/signalcore/resources/ui.py +53 -0
  41. signalcore-0.1.0/src/signalcore/resources/usage.py +11 -0
  42. signalcore-0.1.0/src/signalcore/resources/vault.py +44 -0
  43. signalcore-0.1.0/src/signalcore/types.py +1535 -0
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+
12
+ # Node
13
+ node_modules/
14
+ .next/
15
+ out/
16
+ .turbo/
17
+
18
+ # Env & secrets
19
+ .env
20
+ .env.*
21
+ !.env.example
22
+ *.pem
23
+ *.key
24
+
25
+ # OS / editors
26
+ .DS_Store
27
+ Thumbs.db
28
+ .idea/
29
+ .vscode/*
30
+ !.vscode/extensions.json
31
+
32
+ # Terraform / OpenTofu (plans and state can contain secrets; the lock file IS committed)
33
+ .terraform/
34
+ *.tfplan
35
+ *.tfstate
36
+ *.tfstate.*
37
+ crash.log
38
+
39
+ # Local object storage (SC_FILES_DIR)
40
+ .files/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SignalCore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.5
2
+ Name: signalcore
3
+ Version: 0.1.0
4
+ Summary: The Python client for SignalCore: create agents, talk to them and run everything around them.
5
+ Project-URL: Homepage, https://signalcore.ai
6
+ Project-URL: Documentation, https://signalcore.ai/docs
7
+ Project-URL: Repository, https://github.com/OmnistGroup/signalcore/tree/staging/clients/python
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,ai,api,sdk,signalcore
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: anyio>=4.5
23
+ Requires-Dist: httpx>=0.28
24
+ Requires-Dist: pydantic>=2.7
25
+ Description-Content-Type: text/markdown
26
+
27
+ # signalcore
28
+
29
+ The Python client for [SignalCore](https://signalcore.ai): create agents, talk to them, and run
30
+ everything around them (conversations, approvals, knowledge, records, apps) from your own code.
31
+
32
+ ```bash
33
+ pip install signalcore
34
+ ```
35
+
36
+ Python 3.11 or newer. Create an API key in the web app and export it:
37
+
38
+ ```bash
39
+ export SIGNALCORE_API_KEY=sk_live_...
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ ```python
45
+ import asyncio
46
+
47
+ from signalcore import AsyncSignalcore
48
+
49
+
50
+ async def main() -> None:
51
+ async with AsyncSignalcore() as client: # reads SIGNALCORE_API_KEY
52
+ agent = await client.agents.create("Reception", instructions="You answer for Clínica Sol.")
53
+ await client.agents.publish(agent.id)
54
+
55
+ answer = await client.agents.invoke(
56
+ agent.id, input="Do you open on Saturdays?", thread="ticket-7"
57
+ )
58
+ print(answer.text)
59
+
60
+ async for event in client.runs.stream(answer.run_id): # the run's journal, live
61
+ print(event.event, event.json())
62
+
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ Code that does not use `asyncio` imports `Signalcore` instead: the same client, same names,
68
+ no `await`.
69
+
70
+ ```python
71
+ from signalcore import Signalcore
72
+
73
+ with Signalcore() as client:
74
+ for agent in client.agents.list().items:
75
+ print(agent.name)
76
+ ```
77
+
78
+ ## What you get
79
+
80
+ - **Every endpoint of the API**, one attribute per family: `client.agents`, `client.runs`,
81
+ `client.conversations`, `client.requests`, `client.documents`, `client.records`,
82
+ `client.vault`, `client.connections`, `client.channels`, `client.skills` and the rest. Each
83
+ method names the operation it calls.
84
+ - **Typed answers.** Responses are [pydantic](https://docs.pydantic.dev) models generated from
85
+ the API's contract (`signalcore.types`), so your editor and type checker know every field.
86
+ - **Errors you can catch.** Failures raise `NotFoundError`, `ForbiddenError`, `InvalidError`
87
+ and the like (all `SignalcoreError`), carrying the API's `status`, `code`, `detail` and, for
88
+ a body that did not fit, the `errors` field by field.
89
+ - **Lists that page.** Every list answers `items` and `next_cursor`. To read a whole list:
90
+ `client.iterate(client.conversations.list, status="open")`.
91
+ - **Streams.** `client.runs.stream(run_id)` yields Server-Sent Events until the run is `done`
92
+ or `paused`; pass `last_event_id` to resume after a drop. `client.build.sessions.say(...)`
93
+ streams a builder turn.
94
+ - **Several businesses.** An agency's key acts as each of its clients:
95
+ `client.for_org("org_...").agents.list()`.
96
+ - **Live streams.** What a run shows as it happens is watched from a browser, over a WebSocket.
97
+ `client.live.url(stream_id)` is its address (`wss://<api>/v1/live/<id>?token=...`, with this
98
+ client's credential in the query): give it to `@signalcore/ui`'s `LiveView` or the TypeScript
99
+ client's `live.connect`, only when the client holds an end user's token or a publishable key. It
100
+ carries the credential, so never hand a browser one made with a secret key.
101
+
102
+ ## End users in the browser
103
+
104
+ The people who use your product can talk to your agents without your secret key ever reaching
105
+ their browser. Your backend asks for a token for a person it knows and hands it over:
106
+
107
+ ```python
108
+ minted = await client.end_users.mint_token("user-42", name="Ana", email="ana@example.com")
109
+ # Hand minted.token to their browser: new Signalcore({ endUserToken }) in @signalcore/sdk,
110
+ # or AsyncSignalcore(end_user_token=...) from Python.
111
+ ```
112
+
113
+ For visitors who are not signed in, make a publishable key that works only from your pages
114
+ (`client.publishable_keys.create("Shop", ["https://shop.example.com"])`) and give the client
115
+ `publishable_key=` with a `visitor_id=` the browser keeps. Both credentials can talk to live
116
+ agents and read their own runs, and nothing else; `caller` is who the credential says.
117
+
118
+ ## Your own components
119
+
120
+ What an agent can draw (cards, tables, forms) is a closed catalog, but your product has cards of
121
+ its own. Register one by name, with the props it takes and a text for doors that cannot draw it,
122
+ and agents can show it; your app draws it:
123
+
124
+ ```python
125
+ await client.ui.components.put(
126
+ "PropertyCard",
127
+ "Property card",
128
+ "A listing the customer asked about", # what the agent should use it for
129
+ {
130
+ "type": "object",
131
+ "properties": {"address": {"type": "string"}, "price": {"type": "number"}},
132
+ "required": ["address", "price"],
133
+ },
134
+ "{address}\nPrice: {price} EUR", # the text twin; {prop} is filled from the props
135
+ )
136
+ ```
137
+
138
+ The schema is a small subset of JSON Schema: strings, numbers, whole numbers, booleans, lists of
139
+ those and one level of object; anything else is refused. An agent shows the component as a
140
+ `Host` node, `{"id": "home", "component": "Host", "name": "PropertyCard", "props": {...}}`
141
+ (`sc.ui.host("PropertyCard", {...})` in agent code). Its props are checked against your schema
142
+ before anyone sees them, and the text twin is the surface's text when the agent wrote none.
143
+ `client.ui.catalog()` lists your components after the built-in ones.
144
+
145
+ ## Configuration
146
+
147
+ ```python
148
+ AsyncSignalcore(
149
+ api_key=None, # else SIGNALCORE_API_KEY
150
+ end_user_token=None, # instead of api_key: a person of your product, in their browser
151
+ publishable_key=None, # instead of api_key: a visitor, with visitor_id (8-128 characters)
152
+ visitor_id=None,
153
+ base_url=None, # else SIGNALCORE_BASE_URL, else https://signalcore.ai
154
+ org=None, # act as a business this key manages
155
+ timeout=30, # seconds
156
+ )
157
+ ```
158
+
159
+ Pass exactly one of `api_key`, `end_user_token` and `publishable_key`.
160
+
161
+ Keys carry scopes (`agents:read`, `runs:write`...), so a key made for one job can do only
162
+ that job; a call outside its scopes raises `ForbiddenError`.
163
+
164
+ ## Documentation
165
+
166
+ Guides and the full API reference: <https://signalcore.ai/docs>.
167
+
168
+ MIT licensed.
@@ -0,0 +1,142 @@
1
+ # signalcore
2
+
3
+ The Python client for [SignalCore](https://signalcore.ai): create agents, talk to them, and run
4
+ everything around them (conversations, approvals, knowledge, records, apps) from your own code.
5
+
6
+ ```bash
7
+ pip install signalcore
8
+ ```
9
+
10
+ Python 3.11 or newer. Create an API key in the web app and export it:
11
+
12
+ ```bash
13
+ export SIGNALCORE_API_KEY=sk_live_...
14
+ ```
15
+
16
+ ## Quickstart
17
+
18
+ ```python
19
+ import asyncio
20
+
21
+ from signalcore import AsyncSignalcore
22
+
23
+
24
+ async def main() -> None:
25
+ async with AsyncSignalcore() as client: # reads SIGNALCORE_API_KEY
26
+ agent = await client.agents.create("Reception", instructions="You answer for Clínica Sol.")
27
+ await client.agents.publish(agent.id)
28
+
29
+ answer = await client.agents.invoke(
30
+ agent.id, input="Do you open on Saturdays?", thread="ticket-7"
31
+ )
32
+ print(answer.text)
33
+
34
+ async for event in client.runs.stream(answer.run_id): # the run's journal, live
35
+ print(event.event, event.json())
36
+
37
+
38
+ asyncio.run(main())
39
+ ```
40
+
41
+ Code that does not use `asyncio` imports `Signalcore` instead: the same client, same names,
42
+ no `await`.
43
+
44
+ ```python
45
+ from signalcore import Signalcore
46
+
47
+ with Signalcore() as client:
48
+ for agent in client.agents.list().items:
49
+ print(agent.name)
50
+ ```
51
+
52
+ ## What you get
53
+
54
+ - **Every endpoint of the API**, one attribute per family: `client.agents`, `client.runs`,
55
+ `client.conversations`, `client.requests`, `client.documents`, `client.records`,
56
+ `client.vault`, `client.connections`, `client.channels`, `client.skills` and the rest. Each
57
+ method names the operation it calls.
58
+ - **Typed answers.** Responses are [pydantic](https://docs.pydantic.dev) models generated from
59
+ the API's contract (`signalcore.types`), so your editor and type checker know every field.
60
+ - **Errors you can catch.** Failures raise `NotFoundError`, `ForbiddenError`, `InvalidError`
61
+ and the like (all `SignalcoreError`), carrying the API's `status`, `code`, `detail` and, for
62
+ a body that did not fit, the `errors` field by field.
63
+ - **Lists that page.** Every list answers `items` and `next_cursor`. To read a whole list:
64
+ `client.iterate(client.conversations.list, status="open")`.
65
+ - **Streams.** `client.runs.stream(run_id)` yields Server-Sent Events until the run is `done`
66
+ or `paused`; pass `last_event_id` to resume after a drop. `client.build.sessions.say(...)`
67
+ streams a builder turn.
68
+ - **Several businesses.** An agency's key acts as each of its clients:
69
+ `client.for_org("org_...").agents.list()`.
70
+ - **Live streams.** What a run shows as it happens is watched from a browser, over a WebSocket.
71
+ `client.live.url(stream_id)` is its address (`wss://<api>/v1/live/<id>?token=...`, with this
72
+ client's credential in the query): give it to `@signalcore/ui`'s `LiveView` or the TypeScript
73
+ client's `live.connect`, only when the client holds an end user's token or a publishable key. It
74
+ carries the credential, so never hand a browser one made with a secret key.
75
+
76
+ ## End users in the browser
77
+
78
+ The people who use your product can talk to your agents without your secret key ever reaching
79
+ their browser. Your backend asks for a token for a person it knows and hands it over:
80
+
81
+ ```python
82
+ minted = await client.end_users.mint_token("user-42", name="Ana", email="ana@example.com")
83
+ # Hand minted.token to their browser: new Signalcore({ endUserToken }) in @signalcore/sdk,
84
+ # or AsyncSignalcore(end_user_token=...) from Python.
85
+ ```
86
+
87
+ For visitors who are not signed in, make a publishable key that works only from your pages
88
+ (`client.publishable_keys.create("Shop", ["https://shop.example.com"])`) and give the client
89
+ `publishable_key=` with a `visitor_id=` the browser keeps. Both credentials can talk to live
90
+ agents and read their own runs, and nothing else; `caller` is who the credential says.
91
+
92
+ ## Your own components
93
+
94
+ What an agent can draw (cards, tables, forms) is a closed catalog, but your product has cards of
95
+ its own. Register one by name, with the props it takes and a text for doors that cannot draw it,
96
+ and agents can show it; your app draws it:
97
+
98
+ ```python
99
+ await client.ui.components.put(
100
+ "PropertyCard",
101
+ "Property card",
102
+ "A listing the customer asked about", # what the agent should use it for
103
+ {
104
+ "type": "object",
105
+ "properties": {"address": {"type": "string"}, "price": {"type": "number"}},
106
+ "required": ["address", "price"],
107
+ },
108
+ "{address}\nPrice: {price} EUR", # the text twin; {prop} is filled from the props
109
+ )
110
+ ```
111
+
112
+ The schema is a small subset of JSON Schema: strings, numbers, whole numbers, booleans, lists of
113
+ those and one level of object; anything else is refused. An agent shows the component as a
114
+ `Host` node, `{"id": "home", "component": "Host", "name": "PropertyCard", "props": {...}}`
115
+ (`sc.ui.host("PropertyCard", {...})` in agent code). Its props are checked against your schema
116
+ before anyone sees them, and the text twin is the surface's text when the agent wrote none.
117
+ `client.ui.catalog()` lists your components after the built-in ones.
118
+
119
+ ## Configuration
120
+
121
+ ```python
122
+ AsyncSignalcore(
123
+ api_key=None, # else SIGNALCORE_API_KEY
124
+ end_user_token=None, # instead of api_key: a person of your product, in their browser
125
+ publishable_key=None, # instead of api_key: a visitor, with visitor_id (8-128 characters)
126
+ visitor_id=None,
127
+ base_url=None, # else SIGNALCORE_BASE_URL, else https://signalcore.ai
128
+ org=None, # act as a business this key manages
129
+ timeout=30, # seconds
130
+ )
131
+ ```
132
+
133
+ Pass exactly one of `api_key`, `end_user_token` and `publishable_key`.
134
+
135
+ Keys carry scopes (`agents:read`, `runs:write`...), so a key made for one job can do only
136
+ that job; a call outside its scopes raises `ForbiddenError`.
137
+
138
+ ## Documentation
139
+
140
+ Guides and the full API reference: <https://signalcore.ai/docs>.
141
+
142
+ MIT licensed.
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "signalcore"
3
+ version = "0.1.0"
4
+ description = "The Python client for SignalCore: create agents, talk to them and run everything around them."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = ["signalcore", "agents", "ai", "sdk", "api"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Framework :: AsyncIO",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "anyio>=4.5",
24
+ "httpx>=0.28",
25
+ "pydantic>=2.7",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://signalcore.ai"
30
+ Documentation = "https://signalcore.ai/docs"
31
+ Repository = "https://github.com/OmnistGroup/signalcore/tree/staging/clients/python"
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/signalcore"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["src/signalcore", "README.md", "LICENSE"]
42
+
43
+ # Ruff reads the nearest config: this one keeps the repo's rules and lowers the target to the
44
+ # oldest Python the package supports (the rest of the repo runs on 3.12).
45
+ [tool.ruff]
46
+ extend = "../../pyproject.toml"
47
+ target-version = "py311"
@@ -0,0 +1,40 @@
1
+ """The Python client for SignalCore.
2
+
3
+ from signalcore import AsyncSignalcore, Signalcore
4
+
5
+ ``AsyncSignalcore`` is the client; ``Signalcore`` is the same one for code that blocks.
6
+ Responses are the models in ``signalcore.types``, generated from the API's contract.
7
+ """
8
+
9
+ from signalcore._client import AsyncSignalcore
10
+ from signalcore._sse import ServerSentEvent
11
+ from signalcore._sync import Signalcore
12
+ from signalcore._version import __version__
13
+ from signalcore.errors import (
14
+ ConflictError,
15
+ ForbiddenError,
16
+ InvalidError,
17
+ NotFoundError,
18
+ RateLimitedError,
19
+ ServerError,
20
+ SignalcoreError,
21
+ UnauthenticatedError,
22
+ )
23
+ from signalcore.events import RunEvent, UnknownEvent
24
+
25
+ __all__ = [
26
+ "AsyncSignalcore",
27
+ "ConflictError",
28
+ "ForbiddenError",
29
+ "InvalidError",
30
+ "NotFoundError",
31
+ "RateLimitedError",
32
+ "RunEvent",
33
+ "ServerError",
34
+ "ServerSentEvent",
35
+ "Signalcore",
36
+ "SignalcoreError",
37
+ "UnauthenticatedError",
38
+ "UnknownEvent",
39
+ "__version__",
40
+ ]
@@ -0,0 +1,177 @@
1
+ """The async client: credentials and a connection, with the API's families mounted on it."""
2
+
3
+ import os
4
+ from collections.abc import AsyncIterator, Awaitable, Callable
5
+ from types import TracebackType
6
+ from typing import Any, Self
7
+
8
+ import httpx
9
+
10
+ from signalcore._http import ORG_HEADER, VISITOR_HEADER, Http
11
+ from signalcore._pages import Item, Page, pages
12
+ from signalcore._version import __version__
13
+ from signalcore.resources.agents import Agents
14
+ from signalcore.resources.api_keys import ApiKeys
15
+ from signalcore.resources.billing import Billing
16
+ from signalcore.resources.browser import Browser
17
+ from signalcore.resources.build import Build
18
+ from signalcore.resources.channels import Channels
19
+ from signalcore.resources.connections import Connections
20
+ from signalcore.resources.contacts import Contacts
21
+ from signalcore.resources.conversations import Conversations
22
+ from signalcore.resources.documents import Documents
23
+ from signalcore.resources.end_users import EndUsers
24
+ from signalcore.resources.files import Files
25
+ from signalcore.resources.live import Live
26
+ from signalcore.resources.memories import Memories
27
+ from signalcore.resources.org import Org
28
+ from signalcore.resources.orgs import Orgs
29
+ from signalcore.resources.playground import Playground
30
+ from signalcore.resources.publishable_keys import PublishableKeys
31
+ from signalcore.resources.records import Records
32
+ from signalcore.resources.requests import Requests
33
+ from signalcore.resources.runs import Runs
34
+ from signalcore.resources.skills import Skills
35
+ from signalcore.resources.ui import Ui
36
+ from signalcore.resources.usage import Usage
37
+ from signalcore.resources.vault import Vault
38
+
39
+ DEFAULT_BASE_URL = "https://signalcore.ai"
40
+ API_KEY_VARIABLE = "SIGNALCORE_API_KEY"
41
+ BASE_URL_VARIABLE = "SIGNALCORE_BASE_URL"
42
+
43
+
44
+ def _credential(
45
+ api_key: str | None,
46
+ end_user_token: str | None,
47
+ publishable_key: str | None,
48
+ visitor_id: str | None,
49
+ ) -> dict[str, str]:
50
+ """The headers that say who is calling: exactly one of a secret key, an end user's token,
51
+ or a publishable key together with the visitor's id."""
52
+ passed = {
53
+ name: value
54
+ for name, value in (
55
+ ("api_key", api_key),
56
+ ("end_user_token", end_user_token),
57
+ ("publishable_key", publishable_key),
58
+ )
59
+ if value
60
+ }
61
+ if len(passed) > 1:
62
+ raise ValueError(
63
+ f"Pass one of api_key, end_user_token or publishable_key, not {', '.join(passed)}"
64
+ )
65
+ if visitor_id and not publishable_key:
66
+ raise ValueError("visitor_id goes with publishable_key")
67
+ if publishable_key:
68
+ if not visitor_id:
69
+ raise ValueError("A publishable_key needs the visitor_id the browser keeps")
70
+ return {"Authorization": f"Bearer {publishable_key}", VISITOR_HEADER: visitor_id}
71
+ if end_user_token:
72
+ return {"Authorization": f"Bearer {end_user_token}"}
73
+ key = api_key or os.environ.get(API_KEY_VARIABLE)
74
+ if not key:
75
+ raise ValueError(
76
+ f"Pass api_key, end_user_token or publishable_key, or set {API_KEY_VARIABLE}"
77
+ )
78
+ return {"Authorization": f"Bearer {key}"}
79
+
80
+
81
+ class AsyncSignalcore:
82
+ """A client for the SignalCore API.
83
+
84
+ It acts with one credential. Your servers use a secret key, from ``api_key`` or
85
+ ``SIGNALCORE_API_KEY``. Where a browser holds the client, pass ``end_user_token`` (minted
86
+ for a person of your product by ``end_users.mint_token``) or ``publishable_key`` with the
87
+ ``visitor_id`` the browser keeps; those can talk to agents and read their own runs, nothing
88
+ more. The address comes from ``base_url`` or ``SIGNALCORE_BASE_URL``. ``org`` makes every
89
+ call act as a business the key manages (``for_org`` does it for a few). ``transport``
90
+ replaces the network, to run against an app in-process in tests.
91
+
92
+ async with AsyncSignalcore() as client:
93
+ agent = await client.agents.create("Reception")
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ api_key: str | None = None,
99
+ *,
100
+ end_user_token: str | None = None,
101
+ publishable_key: str | None = None,
102
+ visitor_id: str | None = None,
103
+ base_url: str | None = None,
104
+ org: str | None = None,
105
+ timeout: float = 30,
106
+ transport: httpx.AsyncBaseTransport | None = None,
107
+ ) -> None:
108
+ address = base_url or os.environ.get(BASE_URL_VARIABLE) or DEFAULT_BASE_URL
109
+ headers = {
110
+ **_credential(api_key, end_user_token, publishable_key, visitor_id),
111
+ "Accept": "application/json",
112
+ "User-Agent": f"signalcore-python/{__version__}",
113
+ **({ORG_HEADER: org} if org else {}),
114
+ }
115
+ self._mount(
116
+ Http(
117
+ httpx.AsyncClient(base_url=address, timeout=timeout, transport=transport),
118
+ headers,
119
+ timeout=timeout,
120
+ owns_client=True,
121
+ )
122
+ )
123
+
124
+ def _mount(self, http: Http) -> None:
125
+ self._http = http
126
+ self.orgs = Orgs(http)
127
+ self.api_keys = ApiKeys(http)
128
+ self.org = Org(http)
129
+ self.agents = Agents(http)
130
+ self.build = Build(http)
131
+ self.runs = Runs(http)
132
+ self.playground = Playground(http)
133
+ self.conversations = Conversations(http)
134
+ self.contacts = Contacts(http)
135
+ self.requests = Requests(http)
136
+ self.documents = Documents(http)
137
+ self.end_users = EndUsers(http)
138
+ self.publishable_keys = PublishableKeys(http)
139
+ self.memories = Memories(http)
140
+ self.records = Records(http)
141
+ self.files = Files(http)
142
+ self.vault = Vault(http)
143
+ self.connections = Connections(http)
144
+ self.channels = Channels(http)
145
+ self.skills = Skills(http)
146
+ self.usage = Usage(http)
147
+ self.billing = Billing(http)
148
+ self.ui = Ui(http)
149
+ self.browser = Browser(http)
150
+ self.live = Live(http)
151
+
152
+ def for_org(self, org_id: str) -> Self:
153
+ """A client acting as a business this one manages (``orgs.create``), sharing its
154
+ connection. Closing it leaves the connection to the client it came from."""
155
+ client = object.__new__(type(self))
156
+ client._mount(self._http.acting_as(org_id))
157
+ return client
158
+
159
+ @staticmethod
160
+ def iterate(fetch: Callable[..., Awaitable[Page[Item]]], **params: Any) -> AsyncIterator[Item]:
161
+ """Every item of a paged list, page after page:
162
+ ``client.iterate(client.conversations.list, status="open")``."""
163
+ return pages(fetch, **params)
164
+
165
+ async def aclose(self) -> None:
166
+ await self._http.aclose()
167
+
168
+ async def __aenter__(self) -> Self:
169
+ return self
170
+
171
+ async def __aexit__(
172
+ self,
173
+ exc_type: type[BaseException] | None,
174
+ exc: BaseException | None,
175
+ traceback: TracebackType | None,
176
+ ) -> None:
177
+ await self.aclose()