swarm-analytics 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.
- swarm_analytics-0.1.0/LICENSE +21 -0
- swarm_analytics-0.1.0/PKG-INFO +122 -0
- swarm_analytics-0.1.0/README.md +101 -0
- swarm_analytics-0.1.0/pyproject.toml +37 -0
- swarm_analytics-0.1.0/setup.cfg +4 -0
- swarm_analytics-0.1.0/src/swarm_analytics/__init__.py +32 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/__init__.py +1 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/endpoints.py +71 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/__init__.py +23 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/_shared.py +31 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/agent.py +68 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/app_lifecycle.py +15 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/dashboard.py +16 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/identify.py +22 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/logs.py +17 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/models/onboarding.py +16 -0
- swarm_analytics-0.1.0/src/swarm_analytics/_generated/routes.py +22 -0
- swarm_analytics-0.1.0/src/swarm_analytics/client.py +83 -0
- swarm_analytics-0.1.0/src/swarm_analytics/errors.py +32 -0
- swarm_analytics-0.1.0/src/swarm_analytics/py.typed +0 -0
- swarm_analytics-0.1.0/src/swarm_analytics/spool.py +92 -0
- swarm_analytics-0.1.0/src/swarm_analytics/transport.py +216 -0
- swarm_analytics-0.1.0/src/swarm_analytics.egg-info/PKG-INFO +122 -0
- swarm_analytics-0.1.0/src/swarm_analytics.egg-info/SOURCES.txt +28 -0
- swarm_analytics-0.1.0/src/swarm_analytics.egg-info/dependency_links.txt +1 -0
- swarm_analytics-0.1.0/src/swarm_analytics.egg-info/requires.txt +5 -0
- swarm_analytics-0.1.0/src/swarm_analytics.egg-info/top_level.txt +1 -0
- swarm_analytics-0.1.0/tests/test_client.py +115 -0
- swarm_analytics-0.1.0/tests/test_drift.py +33 -0
- swarm_analytics-0.1.0/tests/test_integration.py +81 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haik Decie
|
|
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,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swarm-analytics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed, auto-generated client for the OpenSwarm product-analytics ingest API.
|
|
5
|
+
Author: Haik Decie
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/openswarm-ai/product-analytics-v1
|
|
8
|
+
Project-URL: Source, https://github.com/openswarm-ai/product-analytics-v1
|
|
9
|
+
Keywords: analytics,telemetry,openswarm,pydantic
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pydantic>=2.0
|
|
17
|
+
Requires-Dist: httpx>=0.24
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# swarm_analytics
|
|
23
|
+
|
|
24
|
+
A typed, **auto-generated** Python client for the OpenSwarm product-analytics
|
|
25
|
+
ingest API. It is the single network egress the desktop FastAPI backend imports to
|
|
26
|
+
send analytics — identity comes from a bearer token, payloads are validated
|
|
27
|
+
against the *exact* pydantic models the server enforces, and delivery is
|
|
28
|
+
fire-and-forget with background retry.
|
|
29
|
+
|
|
30
|
+
## Why it's hard to call wrong
|
|
31
|
+
|
|
32
|
+
- **Identity is impossible to pass.** No method takes `install_id`/`user_id`; the
|
|
33
|
+
server resolves them from the token.
|
|
34
|
+
- **Per-request meta is auto-filled.** `ts` and `submission_id` never appear in a
|
|
35
|
+
signature — the transport stamps them (and reuses `submission_id` on every
|
|
36
|
+
retry for idempotency).
|
|
37
|
+
- **Enums stay enums.** `status`, `action`, `role`, etc. are `Literal`s. A bad
|
|
38
|
+
value raises `pydantic.ValidationError` synchronously, in your stack, before any
|
|
39
|
+
network I/O.
|
|
40
|
+
- **Models are vendored verbatim** from the service, so the client validates with
|
|
41
|
+
the same schema the server uses.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install ./sdk # from the repo root
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from swarm_analytics import AnalyticsClient, AgentMessage
|
|
53
|
+
|
|
54
|
+
# One-time bootstrap on first launch (unauthenticated, blocking)
|
|
55
|
+
token = AnalyticsClient.register(base_url="https://analytics.openswarm.ai", install_id=install_id)
|
|
56
|
+
# persist `token` in settings; reuse forever
|
|
57
|
+
|
|
58
|
+
client = AnalyticsClient(base_url="https://analytics.openswarm.ai", token=token)
|
|
59
|
+
|
|
60
|
+
client.events.app_lifecycle.opened(os="darwin", os_version="25.3.0", app_version="1.2.0")
|
|
61
|
+
client.events.agent.create(id="sess_123", name="Refactor auth", dashboard_id="dash_1")
|
|
62
|
+
client.events.agent.message(agent_id="sess_123", seq=0,
|
|
63
|
+
message=AgentMessage(id="m1", role="user", content="hello"))
|
|
64
|
+
client.events.onboarding.step(step_id="connect_provider", status="completed")
|
|
65
|
+
client.events.dashboard.event(dashboard_id="dash_1", action="create")
|
|
66
|
+
client.logs.write(tag="agent", subtag="tool", data={"name": "shell"})
|
|
67
|
+
client.identify.link_email(email="user@example.com")
|
|
68
|
+
|
|
69
|
+
# On shutdown
|
|
70
|
+
client.events.app_lifecycle.closed()
|
|
71
|
+
client.flush(timeout=2.0)
|
|
72
|
+
client.close()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Durability (optional)
|
|
76
|
+
|
|
77
|
+
By default events live in an in-memory queue and are lost if the process dies
|
|
78
|
+
with deliveries pending. Pass a spool for crash/offline durability:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from swarm_analytics import SqliteSpool
|
|
82
|
+
client = AnalyticsClient(base_url=..., token=..., spool=SqliteSpool("service_spool.db"))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Opt-out
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
client = AnalyticsClient(base_url=..., token=..., mode="minimal") # mutes product events; diagnostics still flow
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Regenerating (auto-generated — do not hand-edit `_generated/`)
|
|
92
|
+
|
|
93
|
+
The models, route table, and namespaces under `src/swarm_analytics/_generated/`
|
|
94
|
+
are produced from the live service. Regenerate whenever the backend's ingest
|
|
95
|
+
models or routes change:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
PYTHONPATH=<repo_root> python sdk/generate.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`ROUTE_SPECS` in `generate.py` (nice method name + category per endpoint) is the
|
|
102
|
+
only human input; it is cross-checked against the live app, so a new or removed
|
|
103
|
+
endpoint fails generation rather than drifting silently.
|
|
104
|
+
|
|
105
|
+
### Drift check (CI)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
PYTHONPATH=<repo_root> python sdk/generate.py --check
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Exits non-zero if the committed `_generated/` output is stale. The same guard runs
|
|
112
|
+
as `tests/test_drift.py`.
|
|
113
|
+
|
|
114
|
+
## Tests
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd sdk && PYTHONPATH=<repo_root> python -m pytest tests -q
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Covers synchronous validation, meta auto-fill, identity-from-token, idempotent
|
|
121
|
+
`submission_id` reuse across retries, opt-out gating, the drift check, and an
|
|
122
|
+
end-to-end pass through the real FastAPI app.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# swarm_analytics
|
|
2
|
+
|
|
3
|
+
A typed, **auto-generated** Python client for the OpenSwarm product-analytics
|
|
4
|
+
ingest API. It is the single network egress the desktop FastAPI backend imports to
|
|
5
|
+
send analytics — identity comes from a bearer token, payloads are validated
|
|
6
|
+
against the *exact* pydantic models the server enforces, and delivery is
|
|
7
|
+
fire-and-forget with background retry.
|
|
8
|
+
|
|
9
|
+
## Why it's hard to call wrong
|
|
10
|
+
|
|
11
|
+
- **Identity is impossible to pass.** No method takes `install_id`/`user_id`; the
|
|
12
|
+
server resolves them from the token.
|
|
13
|
+
- **Per-request meta is auto-filled.** `ts` and `submission_id` never appear in a
|
|
14
|
+
signature — the transport stamps them (and reuses `submission_id` on every
|
|
15
|
+
retry for idempotency).
|
|
16
|
+
- **Enums stay enums.** `status`, `action`, `role`, etc. are `Literal`s. A bad
|
|
17
|
+
value raises `pydantic.ValidationError` synchronously, in your stack, before any
|
|
18
|
+
network I/O.
|
|
19
|
+
- **Models are vendored verbatim** from the service, so the client validates with
|
|
20
|
+
the same schema the server uses.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install ./sdk # from the repo root
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from swarm_analytics import AnalyticsClient, AgentMessage
|
|
32
|
+
|
|
33
|
+
# One-time bootstrap on first launch (unauthenticated, blocking)
|
|
34
|
+
token = AnalyticsClient.register(base_url="https://analytics.openswarm.ai", install_id=install_id)
|
|
35
|
+
# persist `token` in settings; reuse forever
|
|
36
|
+
|
|
37
|
+
client = AnalyticsClient(base_url="https://analytics.openswarm.ai", token=token)
|
|
38
|
+
|
|
39
|
+
client.events.app_lifecycle.opened(os="darwin", os_version="25.3.0", app_version="1.2.0")
|
|
40
|
+
client.events.agent.create(id="sess_123", name="Refactor auth", dashboard_id="dash_1")
|
|
41
|
+
client.events.agent.message(agent_id="sess_123", seq=0,
|
|
42
|
+
message=AgentMessage(id="m1", role="user", content="hello"))
|
|
43
|
+
client.events.onboarding.step(step_id="connect_provider", status="completed")
|
|
44
|
+
client.events.dashboard.event(dashboard_id="dash_1", action="create")
|
|
45
|
+
client.logs.write(tag="agent", subtag="tool", data={"name": "shell"})
|
|
46
|
+
client.identify.link_email(email="user@example.com")
|
|
47
|
+
|
|
48
|
+
# On shutdown
|
|
49
|
+
client.events.app_lifecycle.closed()
|
|
50
|
+
client.flush(timeout=2.0)
|
|
51
|
+
client.close()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Durability (optional)
|
|
55
|
+
|
|
56
|
+
By default events live in an in-memory queue and are lost if the process dies
|
|
57
|
+
with deliveries pending. Pass a spool for crash/offline durability:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from swarm_analytics import SqliteSpool
|
|
61
|
+
client = AnalyticsClient(base_url=..., token=..., spool=SqliteSpool("service_spool.db"))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Opt-out
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
client = AnalyticsClient(base_url=..., token=..., mode="minimal") # mutes product events; diagnostics still flow
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Regenerating (auto-generated — do not hand-edit `_generated/`)
|
|
71
|
+
|
|
72
|
+
The models, route table, and namespaces under `src/swarm_analytics/_generated/`
|
|
73
|
+
are produced from the live service. Regenerate whenever the backend's ingest
|
|
74
|
+
models or routes change:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
PYTHONPATH=<repo_root> python sdk/generate.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`ROUTE_SPECS` in `generate.py` (nice method name + category per endpoint) is the
|
|
81
|
+
only human input; it is cross-checked against the live app, so a new or removed
|
|
82
|
+
endpoint fails generation rather than drifting silently.
|
|
83
|
+
|
|
84
|
+
### Drift check (CI)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
PYTHONPATH=<repo_root> python sdk/generate.py --check
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Exits non-zero if the committed `_generated/` output is stale. The same guard runs
|
|
91
|
+
as `tests/test_drift.py`.
|
|
92
|
+
|
|
93
|
+
## Tests
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd sdk && PYTHONPATH=<repo_root> python -m pytest tests -q
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Covers synchronous validation, meta auto-fill, identity-from-token, idempotent
|
|
100
|
+
`submission_id` reuse across retries, opt-out gating, the drift check, and an
|
|
101
|
+
end-to-end pass through the real FastAPI app.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "swarm-analytics"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Typed, auto-generated client for the OpenSwarm product-analytics ingest API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Haik Decie" }]
|
|
13
|
+
keywords = ["analytics", "telemetry", "openswarm", "pydantic"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"pydantic>=2.0",
|
|
21
|
+
"httpx>=0.24",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/openswarm-ai/product-analytics-v1"
|
|
31
|
+
Source = "https://github.com/openswarm-ai/product-analytics-v1"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
swarm_analytics = ["py.typed"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""swarm_analytics — typed client for the OpenSwarm product-analytics ingest API.
|
|
2
|
+
|
|
3
|
+
The event payload models under ``swarm_analytics`` are vendored verbatim from the
|
|
4
|
+
analytics service and re-exported here, so callers validate against the exact
|
|
5
|
+
schema the server enforces.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._generated.models import * # noqa: F401,F403
|
|
11
|
+
from ._generated.models import __all__ as _model_all
|
|
12
|
+
from .client import AnalyticsClient
|
|
13
|
+
from .errors import (
|
|
14
|
+
AnalyticsError,
|
|
15
|
+
AuthError,
|
|
16
|
+
RateLimited,
|
|
17
|
+
TransportError,
|
|
18
|
+
ValidationRejected,
|
|
19
|
+
)
|
|
20
|
+
from .spool import SqliteSpool, Spool
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AnalyticsClient",
|
|
24
|
+
"AnalyticsError",
|
|
25
|
+
"AuthError",
|
|
26
|
+
"RateLimited",
|
|
27
|
+
"TransportError",
|
|
28
|
+
"ValidationRejected",
|
|
29
|
+
"SqliteSpool",
|
|
30
|
+
"Spool",
|
|
31
|
+
*_model_all,
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generated SDK surface (do not edit)."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Typed endpoint namespaces (generated — do not edit)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
from ..transport import Transport
|
|
7
|
+
from .models.agent import AgentMessage
|
|
8
|
+
|
|
9
|
+
class AgentNS:
|
|
10
|
+
def __init__(self, t: Transport) -> None:
|
|
11
|
+
self.t = t
|
|
12
|
+
|
|
13
|
+
def create(self, *, id: str, name: str | None = None, dashboard_id: str | None = None) -> None:
|
|
14
|
+
self.t.send("events.agent.create", {"id": id, "name": name, "dashboard_id": dashboard_id})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def message(self, *, agent_id: str, seq: int, message: AgentMessage) -> None:
|
|
18
|
+
self.t.send("events.agent.message", {"agent_id": agent_id, "seq": seq, "message": message})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AppLifecycleNS:
|
|
22
|
+
def __init__(self, t: Transport) -> None:
|
|
23
|
+
self.t = t
|
|
24
|
+
|
|
25
|
+
def closed(self) -> None:
|
|
26
|
+
self.t.send("events.app_lifecycle.closed", {})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def opened(self, *, os: str, os_version: str, timezone: str | None = None, locale: str | None = None, app_version: str) -> None:
|
|
30
|
+
self.t.send("events.app_lifecycle.opened", {"os": os, "os_version": os_version, "timezone": timezone, "locale": locale, "app_version": app_version})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DashboardNS:
|
|
34
|
+
def __init__(self, t: Transport) -> None:
|
|
35
|
+
self.t = t
|
|
36
|
+
|
|
37
|
+
def event(self, *, dashboard_id: str, action: Literal['open', 'close', 'create', 'delete']) -> None:
|
|
38
|
+
self.t.send("events.dashboard.event", {"dashboard_id": dashboard_id, "action": action})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OnboardingNS:
|
|
42
|
+
def __init__(self, t: Transport) -> None:
|
|
43
|
+
self.t = t
|
|
44
|
+
|
|
45
|
+
def step(self, *, step_id: str, status: Literal['started', 'completed', 'abandoned']) -> None:
|
|
46
|
+
self.t.send("events.onboarding.step", {"step_id": step_id, "status": status})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class EventsNS:
|
|
50
|
+
def __init__(self, t: Transport) -> None:
|
|
51
|
+
self.t = t
|
|
52
|
+
self.agent: AgentNS = AgentNS(t)
|
|
53
|
+
self.app_lifecycle: AppLifecycleNS = AppLifecycleNS(t)
|
|
54
|
+
self.dashboard: DashboardNS = DashboardNS(t)
|
|
55
|
+
self.onboarding: OnboardingNS = OnboardingNS(t)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IdentifyNS:
|
|
59
|
+
def __init__(self, t: Transport) -> None:
|
|
60
|
+
self.t = t
|
|
61
|
+
|
|
62
|
+
def link_email(self, *, email: str) -> None:
|
|
63
|
+
self.t.send("identify.link_email", {"email": email})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class LogsNS:
|
|
67
|
+
def __init__(self, t: Transport) -> None:
|
|
68
|
+
self.t = t
|
|
69
|
+
|
|
70
|
+
def write(self, *, tag: str, subtag: str | None = None, data: Any = None) -> None:
|
|
71
|
+
self.t.send("logs.write", {"tag": tag, "subtag": subtag, "data": data})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Vendored payload models (generated — do not edit)."""
|
|
2
|
+
|
|
3
|
+
from ._shared import DeviceContext, IngestMeta
|
|
4
|
+
from .agent import AgentCreated, AgentMessage, AgentMessageEvent
|
|
5
|
+
from .app_lifecycle import AppOpened
|
|
6
|
+
from .dashboard import DashboardEvent
|
|
7
|
+
from .identify import IdentifyRequest, RegisterRequest
|
|
8
|
+
from .logs import LogWrite
|
|
9
|
+
from .onboarding import OnboardingStep
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AgentCreated",
|
|
13
|
+
"AgentMessage",
|
|
14
|
+
"AgentMessageEvent",
|
|
15
|
+
"AppOpened",
|
|
16
|
+
"DashboardEvent",
|
|
17
|
+
"DeviceContext",
|
|
18
|
+
"IdentifyRequest",
|
|
19
|
+
"IngestMeta",
|
|
20
|
+
"LogWrite",
|
|
21
|
+
"OnboardingStep",
|
|
22
|
+
"RegisterRequest",
|
|
23
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Base payload models shared by every event subapp.
|
|
2
|
+
|
|
3
|
+
Identity (install_id / user_id) is NEVER part of a payload — it is resolved
|
|
4
|
+
server-side from the Authorization header by the auth dependency. Every payload
|
|
5
|
+
carries only the per-request fields (ts, submission_id) via IngestMeta.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IngestMeta(BaseModel):
|
|
12
|
+
"""Minimal per-request metadata shared by every event payload.
|
|
13
|
+
|
|
14
|
+
Doubles as the payload type for events that carry no extra args.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
ts: float = Field(..., description="Client-side unix timestamp (seconds)")
|
|
18
|
+
submission_id: str = Field(..., description="UUID for idempotency dedup")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeviceContext(BaseModel):
|
|
22
|
+
"""The four attributes whose combination defines a device_context row.
|
|
23
|
+
|
|
24
|
+
app_version and geo are NOT here — app_version lives on the spine (carried by
|
|
25
|
+
app.opened), geo is derived from edge headers per event.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
os: str
|
|
29
|
+
os_version: str
|
|
30
|
+
timezone: str | None = None
|
|
31
|
+
locale: str | None = None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Typed payloads for the agent event subapp.
|
|
2
|
+
|
|
3
|
+
A agent is captured incrementally over two endpoints, mirroring the agent's
|
|
4
|
+
real lifecycle:
|
|
5
|
+
|
|
6
|
+
- agent.created — emitted once when the agent is created. Carries only the
|
|
7
|
+
scalar agent fields (id/name/dashboard_id); no transcript yet.
|
|
8
|
+
- agent.message — emitted once per transcript message as it happens. Carries
|
|
9
|
+
a single message plus a agent-scoped `seq` for ordering.
|
|
10
|
+
|
|
11
|
+
The one genuinely dynamic leaf — message `content` — is typed as `JsonValue`
|
|
12
|
+
(Pydantic's recursive JSON type), never `Any`. Unmodeled fields from the desktop
|
|
13
|
+
dump are ignored (the client may still ship a richer object); we persist only the
|
|
14
|
+
trimmed columns
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Literal, Optional
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field, JsonValue, ConfigDict
|
|
20
|
+
|
|
21
|
+
from ._shared import IngestMeta
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentMessage(BaseModel):
|
|
25
|
+
"""One transcript message. `content` is polymorphic (text, content blocks, or
|
|
26
|
+
a tool payload) so it's typed as JsonValue. `error` is a terminal role used to
|
|
27
|
+
infer agent outcome at query time."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(validate_assignment=True) # Ensures you can't reassign new types to variables
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
role: Literal[
|
|
33
|
+
"user", "assistant", "tool_call", "tool_result", "system", "thinking", "error"
|
|
34
|
+
]
|
|
35
|
+
content: JsonValue = None # NOTE: JsonValue is a descriminated union so it can be None
|
|
36
|
+
parent_id: Optional[str] = None
|
|
37
|
+
provider: Optional[str] = None
|
|
38
|
+
model: Optional[str] = None
|
|
39
|
+
thinking_level: Optional[Literal["off", "low", "medium", "high", "auto"]] = None
|
|
40
|
+
|
|
41
|
+
model_config = {"populate_by_name": True}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentCreated(IngestMeta):
|
|
45
|
+
"""POST /created — the scalar agent record, sent when the agent is
|
|
46
|
+
created. No transcript: messages arrive separately via POST /message."""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(validate_assignment=True) # Ensures you can't reassign new types to variables
|
|
49
|
+
|
|
50
|
+
agent_id: str = Field(..., alias="id")
|
|
51
|
+
name: Optional[str] = None
|
|
52
|
+
dashboard_id: Optional[str] = None
|
|
53
|
+
|
|
54
|
+
model_config = {"populate_by_name": True}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentMessageEvent(IngestMeta):
|
|
58
|
+
"""POST /message — one transcript message appended to an existing agent.
|
|
59
|
+
|
|
60
|
+
`seq` is a agent-scoped, client-supplied ordinal; it is the durable
|
|
61
|
+
ordering key (do not rely on server receive time, since messages can be
|
|
62
|
+
streamed/retried out of order)."""
|
|
63
|
+
|
|
64
|
+
model_config = ConfigDict(validate_assignment=True) # Ensures you can't reassign new types to variables
|
|
65
|
+
|
|
66
|
+
agent_id: str
|
|
67
|
+
seq: int
|
|
68
|
+
message: AgentMessage
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Typed payloads for the app_lifecycle event subapp.
|
|
2
|
+
|
|
3
|
+
Only open/close survive. app.opened carries the four device-context attributes
|
|
4
|
+
(via DeviceContext) plus app_version, which is stamped on the spine. app.closed
|
|
5
|
+
is a bare meta event.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._shared import DeviceContext, IngestMeta
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AppOpened(IngestMeta, DeviceContext):
|
|
12
|
+
"""POST /opened — sent once per launch. Carries the device fields that define
|
|
13
|
+
the device_context row, plus app_version (stamped on the spine)."""
|
|
14
|
+
|
|
15
|
+
app_version: str
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Typed payload for the dashboard event subapp.
|
|
2
|
+
|
|
3
|
+
One consolidated route carries the lifecycle action in the body; the
|
|
4
|
+
event_dashboard.event_type CHECK constraint already allows these four values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from ._shared import IngestMeta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DashboardEvent(IngestMeta):
|
|
13
|
+
"""POST "" — one dashboard lifecycle action (open/close/create/delete)."""
|
|
14
|
+
|
|
15
|
+
dashboard_id: str
|
|
16
|
+
action: Literal["open", "close", "create", "delete"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Request/response models for the identify SubApp.
|
|
2
|
+
|
|
3
|
+
Kept in a dedicated models.py (like every other domain) so the generated SDK can
|
|
4
|
+
vendor them verbatim. These are NOT events: they carry no IngestMeta (ts /
|
|
5
|
+
submission_id). Identity for authed routes is resolved from the bearer token, so
|
|
6
|
+
only the email travels in the link body.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegisterRequest(BaseModel):
|
|
13
|
+
install_id: str = Field(..., min_length=1, max_length=128)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RegisterResponse(BaseModel):
|
|
17
|
+
token: str
|
|
18
|
+
install_id: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IdentifyRequest(BaseModel):
|
|
22
|
+
email: str = Field(..., min_length=1, max_length=320)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Payload model for the logs SubApp.
|
|
2
|
+
|
|
3
|
+
Reuses the shared IngestMeta (ts + submission_id); identity is resolved
|
|
4
|
+
server-side from the Authorization header, never from the body. `data` is any
|
|
5
|
+
JSON-serializable value — it is serialized with to_json() and stored as opaque
|
|
6
|
+
JSON text (DB-enforced via json_valid()).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ._shared import IngestMeta
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LogWrite(IngestMeta):
|
|
15
|
+
tag: str
|
|
16
|
+
subtag: str | None = None
|
|
17
|
+
data: Any = None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Typed payload for the onboarding event subapp.
|
|
2
|
+
|
|
3
|
+
The whole funnel collapses to one event: a step transition carrying which step
|
|
4
|
+
and its funnel status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from ._shared import IngestMeta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OnboardingStep(IngestMeta):
|
|
13
|
+
"""POST /step — one onboarding step transition."""
|
|
14
|
+
|
|
15
|
+
step_id: str
|
|
16
|
+
status: Literal["started", "completed", "abandoned"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Route table (generated — do not edit)."""
|
|
2
|
+
|
|
3
|
+
from ..transport import Route
|
|
4
|
+
from .models._shared import IngestMeta
|
|
5
|
+
from .models.agent import AgentCreated, AgentMessageEvent
|
|
6
|
+
from .models.app_lifecycle import AppOpened
|
|
7
|
+
from .models.dashboard import DashboardEvent
|
|
8
|
+
from .models.identify import IdentifyRequest, RegisterRequest
|
|
9
|
+
from .models.logs import LogWrite
|
|
10
|
+
from .models.onboarding import OnboardingStep
|
|
11
|
+
|
|
12
|
+
ROUTES: dict[str, Route] = {
|
|
13
|
+
"events.agent.create": Route("POST", "/public/events/agent/create", AgentCreated, True, "product"),
|
|
14
|
+
"events.agent.message": Route("POST", "/public/events/agent/message", AgentMessageEvent, True, "product"),
|
|
15
|
+
"events.app_lifecycle.closed": Route("POST", "/public/events/app_lifecycle/closed", IngestMeta, True, "product"),
|
|
16
|
+
"events.app_lifecycle.opened": Route("POST", "/public/events/app_lifecycle/opened", AppOpened, True, "product"),
|
|
17
|
+
"events.dashboard.event": Route("POST", "/public/events/dashboard/event", DashboardEvent, True, "product"),
|
|
18
|
+
"events.onboarding.step": Route("POST", "/public/events/onboarding/step", OnboardingStep, True, "product"),
|
|
19
|
+
"identify.link_email": Route("POST", "/public/identify/link_email_to_install", IdentifyRequest, True, "identity"),
|
|
20
|
+
"identify.register": Route("POST", "/public/identify/create_install_token", RegisterRequest, False, "bootstrap"),
|
|
21
|
+
"logs.write": Route("POST", "/public/logs", LogWrite, True, "diagnostic"),
|
|
22
|
+
}
|