dnotifier 1.0.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.
- dnotifier-1.0.0/.gitignore +12 -0
- dnotifier-1.0.0/CHANGELOG.md +8 -0
- dnotifier-1.0.0/LICENSE +6 -0
- dnotifier-1.0.0/PKG-INFO +185 -0
- dnotifier-1.0.0/README.md +155 -0
- dnotifier-1.0.0/pyproject.toml +55 -0
- dnotifier-1.0.0/src/dnotifier/__init__.py +6 -0
- dnotifier-1.0.0/src/dnotifier/config/__init__.py +3 -0
- dnotifier-1.0.0/src/dnotifier/config/endpoints.py +13 -0
- dnotifier-1.0.0/src/dnotifier/dnotifier.py +234 -0
- dnotifier-1.0.0/src/dnotifier/transport/__init__.py +4 -0
- dnotifier-1.0.0/src/dnotifier/transport/http_transport.py +13 -0
- dnotifier-1.0.0/src/dnotifier/transport/websocket_transport.py +30 -0
- dnotifier-1.0.0/src/dnotifier/utils/__init__.py +7 -0
- dnotifier-1.0.0/src/dnotifier/utils/buffer.py +7 -0
- dnotifier-1.0.0/src/dnotifier/utils/frame.py +26 -0
- dnotifier-1.0.0/src/dnotifier/utils/id.py +5 -0
- dnotifier-1.0.0/src/dnotifier/utils/logs.py +42 -0
- dnotifier-1.0.0/src/dnotifier/utils/payload.py +22 -0
- dnotifier-1.0.0/src/dnotifier/workflow/__init__.py +7 -0
- dnotifier-1.0.0/src/dnotifier/workflow/agent.py +12 -0
- dnotifier-1.0.0/src/dnotifier/workflow/context.py +68 -0
- dnotifier-1.0.0/src/dnotifier/workflow/errors.py +3 -0
- dnotifier-1.0.0/src/dnotifier/workflow/ids.py +4 -0
- dnotifier-1.0.0/src/dnotifier/workflow/json_payload.py +19 -0
- dnotifier-1.0.0/src/dnotifier/workflow/observability.py +36 -0
- dnotifier-1.0.0/src/dnotifier/workflow/runner.py +27 -0
- dnotifier-1.0.0/src/dnotifier/workflow/step_types.py +40 -0
- dnotifier-1.0.0/src/dnotifier/workflow/workflow.py +28 -0
- dnotifier-1.0.0/src/dnotifier/workflow/workflow_observability_client.py +40 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
* Initial Python SDK release with feature parity to JS and Dart SDKs.
|
|
6
|
+
* WebSocket and HTTP transport, binary framing, chunking, AI pipeline, RAG documents, chat history, session logging, and workflows.
|
|
7
|
+
* Production endpoints at `api.dnotifier.com`.
|
|
8
|
+
* Publish pipeline: minify + PyArmor obfuscation for PyPI distributions.
|
dnotifier-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Copyright (c) DNotifier. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and associated documentation files (the "Software") are the
|
|
4
|
+
property of DNotifier. Unauthorized copying, modification, distribution, or
|
|
5
|
+
use of the Software, in whole or in part, is strictly prohibited without
|
|
6
|
+
express written permission from DNotifier.
|
dnotifier-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dnotifier
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Real-time notification and messaging SDK for Python. WebSocket & HTTP, auth, binary frames, AI, RAG, and workflows.
|
|
5
|
+
Project-URL: Homepage, https://dnotifier.com
|
|
6
|
+
Project-URL: Documentation, https://dnotifier.gitbook.io/product-docs
|
|
7
|
+
Project-URL: Repository, https://github.com/smartguy6666/dnotifier-sdk
|
|
8
|
+
Author: DNotifier
|
|
9
|
+
License: Proprietary
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,dnotifier,messaging,notifications,rag,realtime,sdk,websocket
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: websockets>=12.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: pyarmor>=8.5; extra == 'dev'
|
|
27
|
+
Requires-Dist: python-minifier>=2.9; extra == 'dev'
|
|
28
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# DNotifier Python SDK
|
|
32
|
+
|
|
33
|
+
**DNotifier** is a real-time notification and messaging SDK for **Python 3.9+**. It supports **WebSocket** and **HTTP** transports, **binary streaming**, framed packets, an **AI pipeline**, **knowledge-base (RAG)** APIs, optional **session logging**, and **multi-step workflows** with agents and observability.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- WebSocket and HTTP transport
|
|
40
|
+
- Secure authentication handshake
|
|
41
|
+
- Real-time messaging (text and binary)
|
|
42
|
+
- Large payloads with automatic chunking and reassembly
|
|
43
|
+
- AI: `send_ai`, `fetch_ai_history`, semantic `search`
|
|
44
|
+
- Knowledge base: add, update, get, list, delete, and search documents
|
|
45
|
+
- Chat history fetch and delete
|
|
46
|
+
- Optional SDK session logging (`logs=True`)
|
|
47
|
+
- Workflows: named agents, sequential pipelines, dashboard observability
|
|
48
|
+
- Plan limits from auth (`get_plan_limits`)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install dnotifier
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from dnotifier import DNotifier
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
notifier = DNotifier(
|
|
68
|
+
app_id="your-app-id",
|
|
69
|
+
secret="your-app-secret",
|
|
70
|
+
transport="ws", # or "http"
|
|
71
|
+
user_id="user-123",
|
|
72
|
+
on_connected=lambda: print("Connected"),
|
|
73
|
+
on_message=lambda msg: print(msg.payload.to_json()),
|
|
74
|
+
on_disconnected=lambda **kw: print("Disconnected", kw),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
await notifier.connect()
|
|
78
|
+
|
|
79
|
+
await notifier.send(
|
|
80
|
+
sender_id="user-123",
|
|
81
|
+
receiver_id="user-456",
|
|
82
|
+
data={"type": "text", "text": "Hello from DNotifier"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
await notifier.disconnect()
|
|
86
|
+
|
|
87
|
+
asyncio.run(main())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Connection options
|
|
93
|
+
|
|
94
|
+
| Parameter | Type | Description |
|
|
95
|
+
|-----------|------|-------------|
|
|
96
|
+
| `app_id` | `str` | Your DNotifier application id |
|
|
97
|
+
| `secret` | `str` | Application secret |
|
|
98
|
+
| `transport` | `"ws"` \| `"http"` | WebSocket for realtime; HTTP for RPC-style AI/RAG |
|
|
99
|
+
| `user_id` | `str` | End-user or agent id for auth and routing |
|
|
100
|
+
| `logs` | `bool` | Enable SDK session logging (default `False`) |
|
|
101
|
+
| `url` | `str` | WebSocket URL override (default production `wss://api.dnotifier.com/ecs/ws`) |
|
|
102
|
+
| `on_connected` | `callable` | Called after successful connect |
|
|
103
|
+
| `on_message` | `callable` | Called for each incoming message |
|
|
104
|
+
| `on_disconnected` | `callable` | Called on disconnect (`code`, `reason`) |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## API overview
|
|
109
|
+
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `connect()` | Authenticate and open transport |
|
|
113
|
+
| `disconnect()` | Close WebSocket connection |
|
|
114
|
+
| `send(...)` | Send realtime message |
|
|
115
|
+
| `send_binary(...)` | Send binary payload (WebSocket) |
|
|
116
|
+
| `send_ai(...)` | Send to AI pipeline |
|
|
117
|
+
| `fetch_ai_history(...)` | Fetch AI conversation history |
|
|
118
|
+
| `delete_ai_history_message(...)` | Delete one AI history entry |
|
|
119
|
+
| `fetch_chat_history(...)` | Request chat history |
|
|
120
|
+
| `delete_chat_history_message(...)` | Delete one chat message |
|
|
121
|
+
| `search(...)` | Semantic / vector search |
|
|
122
|
+
| `add_document(...)` | Add RAG document |
|
|
123
|
+
| `update_document(...)` | Update RAG document |
|
|
124
|
+
| `delete_document(...)` | Delete RAG document |
|
|
125
|
+
| `list_documents(...)` | List RAG documents |
|
|
126
|
+
| `get_document(...)` | Get RAG document + chunks |
|
|
127
|
+
| `get_plan_limits()` | Plan limits from last auth |
|
|
128
|
+
| `run_workflow(...)` | Run a registered workflow |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Workflows
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from dnotifier import DNotifier, Workflow, define_agent
|
|
136
|
+
|
|
137
|
+
researcher = define_agent(
|
|
138
|
+
name="researcher",
|
|
139
|
+
run=lambda ctx: ctx.send_ai(message=ctx.input, label="Research"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
workflow = Workflow(
|
|
143
|
+
name="qa-pipeline",
|
|
144
|
+
observability=True,
|
|
145
|
+
entry=lambda ctx: ctx.run_agent("researcher"),
|
|
146
|
+
)
|
|
147
|
+
workflow.register_agent("researcher", researcher)
|
|
148
|
+
|
|
149
|
+
async def main():
|
|
150
|
+
notifier = DNotifier(...)
|
|
151
|
+
await notifier.connect()
|
|
152
|
+
result = await notifier.run_workflow(workflow=workflow, input_data="What is RAG?")
|
|
153
|
+
print(result)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
From `dnotifier-python-sdk/`:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pip install -e ".[dev]"
|
|
164
|
+
python -c "from dnotifier import DNotifier; print(DNotifier)"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Publishing (maintainers)
|
|
170
|
+
|
|
171
|
+
The published PyPI wheel is **minified and encrypted** so source logic is not shipped in readable form (same approach as the JS and Dart SDKs).
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Prepare minified + PyArmor-encrypted package in publish_dist/
|
|
175
|
+
python scripts/minify_for_publish.py
|
|
176
|
+
|
|
177
|
+
# Bump version, build wheel, upload to PyPI
|
|
178
|
+
python scripts/publish_prod.py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
Proprietary — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# DNotifier Python SDK
|
|
2
|
+
|
|
3
|
+
**DNotifier** is a real-time notification and messaging SDK for **Python 3.9+**. It supports **WebSocket** and **HTTP** transports, **binary streaming**, framed packets, an **AI pipeline**, **knowledge-base (RAG)** APIs, optional **session logging**, and **multi-step workflows** with agents and observability.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- WebSocket and HTTP transport
|
|
10
|
+
- Secure authentication handshake
|
|
11
|
+
- Real-time messaging (text and binary)
|
|
12
|
+
- Large payloads with automatic chunking and reassembly
|
|
13
|
+
- AI: `send_ai`, `fetch_ai_history`, semantic `search`
|
|
14
|
+
- Knowledge base: add, update, get, list, delete, and search documents
|
|
15
|
+
- Chat history fetch and delete
|
|
16
|
+
- Optional SDK session logging (`logs=True`)
|
|
17
|
+
- Workflows: named agents, sequential pipelines, dashboard observability
|
|
18
|
+
- Plan limits from auth (`get_plan_limits`)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install dnotifier
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from dnotifier import DNotifier
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
notifier = DNotifier(
|
|
38
|
+
app_id="your-app-id",
|
|
39
|
+
secret="your-app-secret",
|
|
40
|
+
transport="ws", # or "http"
|
|
41
|
+
user_id="user-123",
|
|
42
|
+
on_connected=lambda: print("Connected"),
|
|
43
|
+
on_message=lambda msg: print(msg.payload.to_json()),
|
|
44
|
+
on_disconnected=lambda **kw: print("Disconnected", kw),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
await notifier.connect()
|
|
48
|
+
|
|
49
|
+
await notifier.send(
|
|
50
|
+
sender_id="user-123",
|
|
51
|
+
receiver_id="user-456",
|
|
52
|
+
data={"type": "text", "text": "Hello from DNotifier"},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
await notifier.disconnect()
|
|
56
|
+
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Connection options
|
|
63
|
+
|
|
64
|
+
| Parameter | Type | Description |
|
|
65
|
+
|-----------|------|-------------|
|
|
66
|
+
| `app_id` | `str` | Your DNotifier application id |
|
|
67
|
+
| `secret` | `str` | Application secret |
|
|
68
|
+
| `transport` | `"ws"` \| `"http"` | WebSocket for realtime; HTTP for RPC-style AI/RAG |
|
|
69
|
+
| `user_id` | `str` | End-user or agent id for auth and routing |
|
|
70
|
+
| `logs` | `bool` | Enable SDK session logging (default `False`) |
|
|
71
|
+
| `url` | `str` | WebSocket URL override (default production `wss://api.dnotifier.com/ecs/ws`) |
|
|
72
|
+
| `on_connected` | `callable` | Called after successful connect |
|
|
73
|
+
| `on_message` | `callable` | Called for each incoming message |
|
|
74
|
+
| `on_disconnected` | `callable` | Called on disconnect (`code`, `reason`) |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## API overview
|
|
79
|
+
|
|
80
|
+
| Method | Description |
|
|
81
|
+
|--------|-------------|
|
|
82
|
+
| `connect()` | Authenticate and open transport |
|
|
83
|
+
| `disconnect()` | Close WebSocket connection |
|
|
84
|
+
| `send(...)` | Send realtime message |
|
|
85
|
+
| `send_binary(...)` | Send binary payload (WebSocket) |
|
|
86
|
+
| `send_ai(...)` | Send to AI pipeline |
|
|
87
|
+
| `fetch_ai_history(...)` | Fetch AI conversation history |
|
|
88
|
+
| `delete_ai_history_message(...)` | Delete one AI history entry |
|
|
89
|
+
| `fetch_chat_history(...)` | Request chat history |
|
|
90
|
+
| `delete_chat_history_message(...)` | Delete one chat message |
|
|
91
|
+
| `search(...)` | Semantic / vector search |
|
|
92
|
+
| `add_document(...)` | Add RAG document |
|
|
93
|
+
| `update_document(...)` | Update RAG document |
|
|
94
|
+
| `delete_document(...)` | Delete RAG document |
|
|
95
|
+
| `list_documents(...)` | List RAG documents |
|
|
96
|
+
| `get_document(...)` | Get RAG document + chunks |
|
|
97
|
+
| `get_plan_limits()` | Plan limits from last auth |
|
|
98
|
+
| `run_workflow(...)` | Run a registered workflow |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Workflows
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from dnotifier import DNotifier, Workflow, define_agent
|
|
106
|
+
|
|
107
|
+
researcher = define_agent(
|
|
108
|
+
name="researcher",
|
|
109
|
+
run=lambda ctx: ctx.send_ai(message=ctx.input, label="Research"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
workflow = Workflow(
|
|
113
|
+
name="qa-pipeline",
|
|
114
|
+
observability=True,
|
|
115
|
+
entry=lambda ctx: ctx.run_agent("researcher"),
|
|
116
|
+
)
|
|
117
|
+
workflow.register_agent("researcher", researcher)
|
|
118
|
+
|
|
119
|
+
async def main():
|
|
120
|
+
notifier = DNotifier(...)
|
|
121
|
+
await notifier.connect()
|
|
122
|
+
result = await notifier.run_workflow(workflow=workflow, input_data="What is RAG?")
|
|
123
|
+
print(result)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
From `dnotifier-python-sdk/`:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
python -c "from dnotifier import DNotifier; print(DNotifier)"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Publishing (maintainers)
|
|
140
|
+
|
|
141
|
+
The published PyPI wheel is **minified and encrypted** so source logic is not shipped in readable form (same approach as the JS and Dart SDKs).
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Prepare minified + PyArmor-encrypted package in publish_dist/
|
|
145
|
+
python scripts/minify_for_publish.py
|
|
146
|
+
|
|
147
|
+
# Bump version, build wheel, upload to PyPI
|
|
148
|
+
python scripts/publish_prod.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
Proprietary — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dnotifier"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Real-time notification and messaging SDK for Python. WebSocket & HTTP, auth, binary frames, AI, RAG, and workflows."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Proprietary" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "DNotifier" }]
|
|
13
|
+
keywords = ["dnotifier", "websocket", "realtime", "notifications", "sdk", "messaging", "ai", "rag"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Communications",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.27.0",
|
|
27
|
+
"websockets>=12.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://dnotifier.com"
|
|
32
|
+
Documentation = "https://dnotifier.gitbook.io/product-docs"
|
|
33
|
+
Repository = "https://github.com/smartguy6666/dnotifier-sdk"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"build>=1.2",
|
|
38
|
+
"twine>=5.0",
|
|
39
|
+
"python-minifier>=2.9",
|
|
40
|
+
"pyarmor>=8.5",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/dnotifier"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
47
|
+
"src" = ""
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = [
|
|
51
|
+
"/src/dnotifier",
|
|
52
|
+
"/README.md",
|
|
53
|
+
"/LICENSE",
|
|
54
|
+
"/CHANGELOG.md",
|
|
55
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from.dnotifier import DNotifier,DNotifierMessage,DNotifierMessageMetadata,DNotifierPlanLimits
|
|
3
|
+
from.utils.payload import Payload
|
|
4
|
+
from.workflow import Agent,Workflow,WorkflowContext,WorkflowError,WorkflowRunner,define_agent
|
|
5
|
+
__version__='1.0.0'
|
|
6
|
+
__all__=['DNotifier','DNotifierMessage','DNotifierMessageMetadata','DNotifierPlanLimits','Payload','Agent','define_agent','Workflow','WorkflowContext','WorkflowRunner','WorkflowError','__version__']
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
BASE_HOST='https://api.dnotifier.com'
|
|
3
|
+
_WS_HOST=BASE_HOST.replace('http','ws',1)
|
|
4
|
+
LOG_HOST=f"{BASE_HOST}/ecs"
|
|
5
|
+
WORKFLOW_SDK_BASE=f"{LOG_HOST}/api/v1/log/sdk/workflow"
|
|
6
|
+
class DNotifierEndpoints:
|
|
7
|
+
base_host=BASE_HOST;ws_url=f"{_WS_HOST}/ecs/ws";rpc_send_url=f"{BASE_HOST}/rpc/v1/message/send";auth_url=f"{BASE_HOST}/ecs/api/v1/sdk/auth";log_host=LOG_HOST;add_log_url=f"{LOG_HOST}/api/v1/log/sdk/add-log";update_log_status_url=f"{LOG_HOST}/api/v1/log/sdk/update-log-status";add_session_step_url=f"{LOG_HOST}/api/v1/log/sdk/add-session-step";workflow_register_url=f"{WORKFLOW_SDK_BASE}/workflows";workflow_executions_url=f"{WORKFLOW_SDK_BASE}/executions";workflow_steps_url=f"{WORKFLOW_SDK_BASE}/steps"
|
|
8
|
+
@staticmethod
|
|
9
|
+
def workflow_execution_url(execution_id):return f"{WORKFLOW_SDK_BASE}/executions/{execution_id}"
|
|
10
|
+
@staticmethod
|
|
11
|
+
def workflow_execution_complete_url(execution_id):return f"{WORKFLOW_SDK_BASE}/executions/{execution_id}/complete"
|
|
12
|
+
@staticmethod
|
|
13
|
+
def workflow_execution_steps_url(execution_id):return f"{WORKFLOW_SDK_BASE}/executions/{execution_id}/steps"
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_c='content'
|
|
3
|
+
_b='content or text is required'
|
|
4
|
+
_a='saveHistory'
|
|
5
|
+
_Z='aiEnabled'
|
|
6
|
+
_Y='limits'
|
|
7
|
+
_X='currentPacket'
|
|
8
|
+
_W='totalPackets'
|
|
9
|
+
_V='_responseData'
|
|
10
|
+
_U='recordId'
|
|
11
|
+
_T='record_id is required'
|
|
12
|
+
_S='record_id'
|
|
13
|
+
_R='messageId'
|
|
14
|
+
_Q='receivers'
|
|
15
|
+
_P='text'
|
|
16
|
+
_O='ai'
|
|
17
|
+
_N='sender_id required'
|
|
18
|
+
_M=True
|
|
19
|
+
_L=False
|
|
20
|
+
_K='Not connected'
|
|
21
|
+
_J='id'
|
|
22
|
+
_I='code'
|
|
23
|
+
_H='appId'
|
|
24
|
+
_G='data'
|
|
25
|
+
_F='timestamp'
|
|
26
|
+
_E='token'
|
|
27
|
+
_D='metadata'
|
|
28
|
+
_C='type'
|
|
29
|
+
_B='sender'
|
|
30
|
+
_A=None
|
|
31
|
+
import asyncio,json,time,uuid
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Any,Callable,Optional
|
|
34
|
+
from.config.endpoints import DNotifierEndpoints
|
|
35
|
+
from.transport.http_transport import HttpTransport
|
|
36
|
+
from.transport.websocket_transport import WebsocketTransport
|
|
37
|
+
from.utils.buffer import to_bytes
|
|
38
|
+
from.utils.frame import frame_packet
|
|
39
|
+
from.utils.id import prefix_id
|
|
40
|
+
from.utils.logs import LogsClient
|
|
41
|
+
from.utils.payload import Payload
|
|
42
|
+
from.workflow import Agent,Workflow,WorkflowContext,WorkflowError,WorkflowRunner,define_agent
|
|
43
|
+
_DEFAULT_WS_URL=DNotifierEndpoints.ws_url
|
|
44
|
+
_DEFAULT_AUTH_URL=DNotifierEndpoints.auth_url
|
|
45
|
+
_DEFAULT_HTTP_URL=DNotifierEndpoints.rpc_send_url
|
|
46
|
+
@dataclass
|
|
47
|
+
class DNotifierMessageMetadata:id:Optional[str];sender:str;timestamp:int;type:Optional[str]=_A
|
|
48
|
+
@dataclass
|
|
49
|
+
class DNotifierMessage:metadata:DNotifierMessageMetadata;payload:Payload
|
|
50
|
+
@dataclass
|
|
51
|
+
class DNotifierPlanLimits:messages_hard_limit:Optional[int]=_A;max_ai_requests_per_month:int=0;max_ai_words_per_month:int=0;knowledge_base_max_words:int=0;ai_enabled:bool=_L;max_users:Optional[int]=_A;max_rows_per_user:Optional[int]=_A
|
|
52
|
+
class _ChunkEntry:
|
|
53
|
+
def __init__(A,total_packets):B=total_packets;A.total_packets=B;A.chunks=[_A]*B
|
|
54
|
+
def _merge_chunks(chunks):return b''.join(chunks)
|
|
55
|
+
def _random_uuid():return str(uuid.uuid4())
|
|
56
|
+
OnConnected=Callable[[],_A]
|
|
57
|
+
OnMessage=Callable[[DNotifierMessage],_A]
|
|
58
|
+
OnDisconnected=Callable[...,_A]
|
|
59
|
+
class DNotifier:
|
|
60
|
+
Agent=Agent;define_agent=staticmethod(define_agent);Workflow=Workflow;WorkflowContext=WorkflowContext;WorkflowRunner=WorkflowRunner;WorkflowError=WorkflowError
|
|
61
|
+
def __init__(A,app_id,secret,transport,user_id,logs=_L,url=_DEFAULT_WS_URL,on_connected=_A,on_message=_A,on_disconnected=_A):
|
|
62
|
+
E=user_id;D=secret;C=app_id;B=transport
|
|
63
|
+
if not C or not D or not B or not E:raise ValueError('app_id, secret, transport, and user_id are required')
|
|
64
|
+
A.app_id=C;A.secret=D;A.user_id=E;A.url=url;A.on_connected=on_connected or(lambda:_A);A.on_message=on_message or(lambda _msg:_A);A.on_disconnected=on_disconnected or(lambda**A:_A);A.is_connected=_L;A.ai_enabled=_L;A.message_size_limit=32768;A._logs_enabled=bool(logs);A._logs_client=LogsClient(enabled=A._logs_enabled,app_id=A.app_id);A._auth_response=_A;A.auth_token=_A;A.current_model=_A;A._ws=_A;A._http=_A
|
|
65
|
+
if B=='ws':A._ws=WebsocketTransport()
|
|
66
|
+
elif B=='http':A._http=HttpTransport(_DEFAULT_HTTP_URL)
|
|
67
|
+
else:raise ValueError('transport must be "ws" or "http"')
|
|
68
|
+
def _normalize_rpc_response(C,response):
|
|
69
|
+
A=response
|
|
70
|
+
if not isinstance(A,dict):return A
|
|
71
|
+
if _D in A and _G in A:return A
|
|
72
|
+
B=A.get(_V)
|
|
73
|
+
if isinstance(B,dict)and _D in B and _G in B:return B
|
|
74
|
+
return A
|
|
75
|
+
async def connect(A):
|
|
76
|
+
I=HttpTransport(_DEFAULT_AUTH_URL);D=await I.send({_H:A.app_id,'appSecret':A.secret,'userID':A.user_id});E=D.get('_responseCode')
|
|
77
|
+
if E is not _A and E!=200:
|
|
78
|
+
if E==400:raise RuntimeError(str(D.get(_V,'Authentication failed')))
|
|
79
|
+
raise RuntimeError('Authentication failed.')
|
|
80
|
+
C=D.get(_V)
|
|
81
|
+
if not isinstance(C,dict):raise RuntimeError('Authentication failed: invalid response')
|
|
82
|
+
A._auth_response=C;A.auth_token=C.get(_E)
|
|
83
|
+
if not A.auth_token:raise RuntimeError('Authentication failed: no token')
|
|
84
|
+
A.current_model=C.get('current_model');F=C.get(_Y)or{};A.ai_enabled=bool(F.get(_Z));A.message_size_limit=int(F.get('messageSizeKB',32))*1024;A._logs_client.configure(token=A.auth_token,model=A.current_model)
|
|
85
|
+
if A._http is not _A:A.is_connected=_M;A.on_connected();return _M
|
|
86
|
+
B=A._ws
|
|
87
|
+
if B is _A:raise RuntimeError('WebSocket transport not initialized')
|
|
88
|
+
G=_L;H={}
|
|
89
|
+
def J(chunk):
|
|
90
|
+
R='300';N=chunk;nonlocal G
|
|
91
|
+
try:
|
|
92
|
+
O=N.get(_D)or{};E=N.get(_G);B=O.get('packet')or O;I=str(B.get(_I,''));S=str(B.get(_B,''))
|
|
93
|
+
if not G:
|
|
94
|
+
if I=='102'and prefix_id(A.app_id,A.user_id)in S:G=_M;A.is_connected=_M;A.on_connected()
|
|
95
|
+
return
|
|
96
|
+
F=int(B.get(_W)or 1);J=int(B.get(_X)or 1);P=B.get(_J);D=str(P)if P else _A;K=D or f"{B.get(_B)}_{B.get(_F)}_{F}"
|
|
97
|
+
if F==1 or not D:
|
|
98
|
+
if I==R and D:
|
|
99
|
+
try:L=Payload(E).to_json();asyncio.create_task(A._logs_client.complete_ai_logging(session_id=D,response=L))
|
|
100
|
+
except Exception as M:print(f"Session step completion failed: {M}")
|
|
101
|
+
A._emit_message(B,E);return
|
|
102
|
+
C=H.get(K)
|
|
103
|
+
if C is _A or C.total_packets!=F:C=_ChunkEntry(F);H[K]=C
|
|
104
|
+
if J<1 or J>F:return
|
|
105
|
+
T=E if isinstance(E,bytes)else to_bytes(E);C.chunks[J-1]=T
|
|
106
|
+
if any(A is _A for A in C.chunks):return
|
|
107
|
+
H.pop(K,_A);Q=_merge_chunks([A for A in C.chunks if A is not _A])
|
|
108
|
+
if I==R and D:L=Payload(Q).to_json();asyncio.create_task(A._logs_client.complete_ai_logging(session_id=D,response=L))
|
|
109
|
+
A._emit_message(B,Q)
|
|
110
|
+
except Exception as M:print(f"Packet error: {M}")
|
|
111
|
+
def K(code=_A,reason=_A):
|
|
112
|
+
B=A.is_connected;A.is_connected=_L
|
|
113
|
+
if B:A.on_disconnected(code=code,reason=reason or'Disconnected')
|
|
114
|
+
def L(err):A.is_connected=_L;A.on_disconnected(code=500,reason=str(err))
|
|
115
|
+
B.on_message=J;B.on_close=K;B.on_error=L;await B.connect(A.url);M={_C:_P,_I:101,_H:A.app_id,_B:prefix_id(A.app_id,A.user_id),_F:int(time.time()*1000),_E:A.auth_token};N=to_bytes(json.dumps({_E:A.auth_token}));await B.send_binary(frame_packet(M,N));return _M
|
|
116
|
+
def _emit_message(B,packet,data):A=packet;B.on_message(DNotifierMessage(metadata=DNotifierMessageMetadata(id=A.get(_J),sender=str(A.get(_B,'')),timestamp=int(A.get(_F)or 0),type=A.get(_C)or A.get(_B)),payload=Payload(data)))
|
|
117
|
+
def get_plan_limits(C):
|
|
118
|
+
if not C.is_connected:raise RuntimeError(_K)
|
|
119
|
+
A=(C._auth_response or{}).get(_Y)
|
|
120
|
+
if not isinstance(A,dict):return
|
|
121
|
+
B=bool(A.get(_Z));D=A.get('ai_limits')or{};F=A.get('knowledge_base_limits')or{};E=A.get('db_storage_limits')or{};return DNotifierPlanLimits(messages_hard_limit=A.get('messagesHardLimit'),max_ai_requests_per_month=int(D.get('maxRequestsPerMonth',0))if B else 0,max_ai_words_per_month=int(D.get('maxAIWordsPerMonth',0))if B else 0,knowledge_base_max_words=int(F.get('maxWords',0))if B else 0,ai_enabled=B,max_users=E.get('maxUsers'),max_rows_per_user=E.get('maxRowsPerUser'))
|
|
122
|
+
async def send(A,sender_id,data,receiver_id=_A,receiver_ids=_A,save_history=_M):
|
|
123
|
+
I=receiver_ids;H=receiver_id;G=sender_id
|
|
124
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
125
|
+
if not G:raise ValueError(_N)
|
|
126
|
+
if not H and not I:raise ValueError('receiver_id or receiver_ids required')
|
|
127
|
+
K=I or[H];B=to_bytes(json.dumps(data));D=A.message_size_limit or 32768;C=_random_uuid();E={_J:C,_C:_P,_I:200,_W:1,_X:1,_H:A.app_id,_B:prefix_id(A.app_id,G),_Q:[prefix_id(A.app_id,B)for B in K if B],_F:int(time.time()*1000),_E:A.auth_token,_a:save_history}
|
|
128
|
+
if A._http is not _A:return await A._http.send({_D:E,_G:data})
|
|
129
|
+
F=A._ws
|
|
130
|
+
if F is _A:raise RuntimeError('WebSocket not available')
|
|
131
|
+
if len(B)<=D:await F.send_binary(frame_packet(E,B));return C
|
|
132
|
+
J=[B[A:A+D]for A in range(0,len(B),D)];L=len(J)
|
|
133
|
+
for(M,N)in enumerate(J):O={**E,_J:C,_W:L,_X:M+1};await F.send_binary(frame_packet(O,N))
|
|
134
|
+
return C
|
|
135
|
+
async def send_ai(A,sender_id,message,save_history=_M,session_id=_A):
|
|
136
|
+
E=session_id;D=message;C=sender_id
|
|
137
|
+
try:
|
|
138
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
139
|
+
if not C:raise ValueError(_N)
|
|
140
|
+
B=E or _random_uuid();F={_J:B,_C:_O,_I:300,_H:A.app_id,_B:prefix_id(A.app_id,C),'receiverId':prefix_id(A.app_id,C),_F:int(time.time()*1000),_E:A.auth_token,_a:save_history};await A._logs_client.start_ai_logging(session_id=B,prompts=D,is_existing_session=bool(E))
|
|
141
|
+
if A._http is not _A:G=await A._http.send({_D:F,_G:D});await A._logs_client.complete_ai_logging(session_id=B,response=G);return A._normalize_rpc_response(G)
|
|
142
|
+
if A._ws is not _A:H=to_bytes(json.dumps(D));await A._ws.send_binary(frame_packet(F,H))
|
|
143
|
+
return B
|
|
144
|
+
except Exception as I:print(f"error in send_ai: {I}");return
|
|
145
|
+
async def fetch_ai_history(A,sender_id):
|
|
146
|
+
B=sender_id
|
|
147
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
148
|
+
if not B:raise ValueError(_N)
|
|
149
|
+
C={_J:_random_uuid(),_C:_O,_I:301,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
|
|
150
|
+
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:{}}))
|
|
151
|
+
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes('{}')))
|
|
152
|
+
async def fetch_chat_history(A,sender_id,receiver_ids):
|
|
153
|
+
C=receiver_ids;B=sender_id
|
|
154
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
155
|
+
if not B:raise ValueError(_N)
|
|
156
|
+
if not C:raise ValueError('receiver_ids required')
|
|
157
|
+
D={_J:_random_uuid(),_C:_P,_I:201,_H:A.app_id,_B:prefix_id(A.app_id,B),_Q:[prefix_id(A.app_id,B)for B in C],_F:int(time.time()*1000),_E:A.auth_token}
|
|
158
|
+
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:D,_G:{}}))
|
|
159
|
+
if A._ws is not _A:await A._ws.send_binary(frame_packet(D,to_bytes('{}')))
|
|
160
|
+
async def delete_ai_history_message(A,sender_id,message_id):
|
|
161
|
+
B=message_id
|
|
162
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
163
|
+
C={_J:_random_uuid(),_C:_O,_I:302,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_F:int(time.time()*1000),_E:A.auth_token,_R:B};D={_R:B}
|
|
164
|
+
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:D}))
|
|
165
|
+
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(D))))
|
|
166
|
+
async def delete_chat_history_message(A,sender_id,message_id):
|
|
167
|
+
B=message_id
|
|
168
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
169
|
+
C={_J:_random_uuid(),_C:_P,_I:202,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_Q:[],_F:int(time.time()*1000),_E:A.auth_token,_R:B};D={_R:B}
|
|
170
|
+
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:D}))
|
|
171
|
+
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(D))))
|
|
172
|
+
async def search(A,sender_id,query,limit=_A,min_similarity=_A,filterby_source=_A):
|
|
173
|
+
F=filterby_source;E=min_similarity;D=limit;C=query
|
|
174
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
175
|
+
if not C:raise ValueError('query required')
|
|
176
|
+
G={_J:_random_uuid(),_C:_O,_I:303,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_F:int(time.time()*1000),_E:A.auth_token};B={'query':C}
|
|
177
|
+
if D is not _A:B['limit']=D
|
|
178
|
+
if E is not _A:B['minSimilarity']=E
|
|
179
|
+
if F is not _A:B['filterbySource']=F
|
|
180
|
+
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:G,_G:B}))
|
|
181
|
+
if A._ws is not _A:await A._ws.send_binary(frame_packet(G,to_bytes(json.dumps(B))))
|
|
182
|
+
async def add_document(G,sender_id,record_id=_A,content=_A,text=_A,doc_type=_A,metadata=_A,**H):
|
|
183
|
+
D=metadata;C=doc_type;B=content;E=(record_id or H.get(_S)or'').strip();F=B if B is not _A else text or''
|
|
184
|
+
if not E:raise ValueError(_T)
|
|
185
|
+
if not str(F).strip():raise ValueError(_b)
|
|
186
|
+
A={_U:E,_c:str(F)}
|
|
187
|
+
if C is not _A:A[_C]=str(C)
|
|
188
|
+
if D:A[_D]=D
|
|
189
|
+
return await G._send_document_rpc(sender_id,306,A)
|
|
190
|
+
async def update_document(G,sender_id,record_id=_A,content=_A,text=_A,doc_type=_A,metadata=_A,**H):
|
|
191
|
+
D=metadata;C=doc_type;B=content;E=(record_id or H.get(_S)or'').strip();F=B if B is not _A else text or''
|
|
192
|
+
if not E:raise ValueError(_T)
|
|
193
|
+
if not str(F).strip():raise ValueError(_b)
|
|
194
|
+
A={_U:E,_c:str(F)}
|
|
195
|
+
if C is not _A:A[_C]=str(C)
|
|
196
|
+
if D:A[_D]=D
|
|
197
|
+
return await G._send_document_rpc(sender_id,307,A)
|
|
198
|
+
async def delete_document(B,sender_id,record_id=_A,**C):
|
|
199
|
+
A=(record_id or C.get(_S)or'').strip()
|
|
200
|
+
if not A:raise ValueError(_T)
|
|
201
|
+
return await B._send_document_rpc(sender_id,308,{_U:A})
|
|
202
|
+
async def list_documents(E,sender_id,limit=_A,offset=_A,doc_type=_A,**F):
|
|
203
|
+
D=doc_type;C=offset;B=limit;A={}
|
|
204
|
+
if B is not _A:A['limit']=B
|
|
205
|
+
if C is not _A:A['offset']=C
|
|
206
|
+
if D is not _A:A[_C]=str(D)
|
|
207
|
+
return await E._send_document_rpc(sender_id,309,A)
|
|
208
|
+
async def get_document(B,sender_id,record_id=_A,**C):
|
|
209
|
+
A=(record_id or C.get(_S)or'').strip()
|
|
210
|
+
if not A:raise ValueError(_T)
|
|
211
|
+
return await B._send_document_rpc(sender_id,310,{_U:A})
|
|
212
|
+
async def _send_document_rpc(A,sender_id,code,data):
|
|
213
|
+
B=sender_id
|
|
214
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
215
|
+
if not B:raise ValueError(_N)
|
|
216
|
+
C={_J:_random_uuid(),_C:_O,_I:code,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
|
|
217
|
+
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:data}))
|
|
218
|
+
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(data))))
|
|
219
|
+
async def send_binary(A,sender_id,receiver_ids,buffer,msg_type='binary'):
|
|
220
|
+
B=buffer
|
|
221
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
222
|
+
if not B:raise ValueError('buffer is required')
|
|
223
|
+
C={_C:msg_type,_I:202,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_Q:[prefix_id(A.app_id,B)for B in receiver_ids],_F:int(time.time()*1000),_E:A.auth_token}
|
|
224
|
+
if A._ws is _A:raise RuntimeError('Binary send requires WebSocket transport')
|
|
225
|
+
await A._ws.send_binary(frame_packet(C,B))
|
|
226
|
+
async def send_with_openai(A,sender_id,message,**B):
|
|
227
|
+
if not A.ai_enabled:raise RuntimeError('AI is not enabled for this subscription.')
|
|
228
|
+
return await A.send_ai(sender_id=sender_id,message=message)
|
|
229
|
+
async def run_workflow(A,workflow,input_data,sender_id=_A):
|
|
230
|
+
if not A.is_connected:raise RuntimeError(_K)
|
|
231
|
+
B=WorkflowRunner(notifier=A,sender_id=sender_id or A.user_id,workflow=workflow);return await B.run(input_data=input_data)
|
|
232
|
+
async def disconnect(A):
|
|
233
|
+
if A._ws is not _A:await A._ws.close()
|
|
234
|
+
A.is_connected=_L
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any,Optional
|
|
4
|
+
import httpx
|
|
5
|
+
from..config.endpoints import DNotifierEndpoints
|
|
6
|
+
class HttpTransport:
|
|
7
|
+
def __init__(A,url,headers=None):A.url=url;A.headers=headers or{}
|
|
8
|
+
async def send(B,payload):
|
|
9
|
+
async with httpx.AsyncClient(timeout=6e1)as C:
|
|
10
|
+
A=await C.post(B.url,headers={'Content-Type':'application/json',**B.headers},content=json.dumps(payload))
|
|
11
|
+
if A.status_code>=400:raise RuntimeError(f"DNotifier HTTP error: {A.text}")
|
|
12
|
+
return A.json()
|
|
13
|
+
def send_binary(A):raise RuntimeError('Binary is not supported over HTTP yet')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any,Callable,Optional
|
|
5
|
+
import websockets
|
|
6
|
+
from..utils.frame import decode_packet
|
|
7
|
+
class WebsocketTransport:
|
|
8
|
+
def __init__(A):A._ws=_A;A._listen_task=_A;A.on_message=_A;A.on_close=_A;A.on_error=_A
|
|
9
|
+
async def connect(A,url):A._ws=await websockets.connect(url);A._listen_task=asyncio.create_task(A._listen());return True
|
|
10
|
+
async def _listen(A):
|
|
11
|
+
try:
|
|
12
|
+
assert A._ws is not _A
|
|
13
|
+
async for C in A._ws:
|
|
14
|
+
E=C if isinstance(C,bytes)else C.encode('utf-8');D=decode_packet(E)
|
|
15
|
+
if D and A.on_message:A.on_message(D)
|
|
16
|
+
except websockets.ConnectionClosed as B:
|
|
17
|
+
if A.on_close:A.on_close(code=B.code,reason=B.reason or'Disconnected')
|
|
18
|
+
except Exception as B:
|
|
19
|
+
if A.on_error:A.on_error(B)
|
|
20
|
+
if A.on_close:A.on_close(code=500,reason=str(B))
|
|
21
|
+
async def send_binary(A,buffer):
|
|
22
|
+
if not A._ws or A._ws.close_code is not _A:raise RuntimeError('WebSocket not connected')
|
|
23
|
+
await A._ws.send(buffer)
|
|
24
|
+
def is_open(A):return A._ws is not _A and A._ws.close_code is _A
|
|
25
|
+
async def close(A):
|
|
26
|
+
if A._listen_task and not A._listen_task.done():
|
|
27
|
+
A._listen_task.cancel()
|
|
28
|
+
try:await A._listen_task
|
|
29
|
+
except asyncio.CancelledError:pass
|
|
30
|
+
if A._ws:await A._ws.close();A._ws=_A
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from.buffer import to_bytes
|
|
3
|
+
from.frame import decode_packet,frame_packet
|
|
4
|
+
from.id import prefix_id
|
|
5
|
+
from.logs import LogsClient
|
|
6
|
+
from.payload import Payload
|
|
7
|
+
__all__=['to_bytes','decode_packet','frame_packet','prefix_id','LogsClient','Payload']
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A='utf-8'
|
|
3
|
+
import json,struct
|
|
4
|
+
from typing import Any,Optional
|
|
5
|
+
_internal_buffer=bytearray()
|
|
6
|
+
def _write_u32_be(buf,offset,val):struct.pack_into('>I',buf,offset,val&4294967295)
|
|
7
|
+
def _read_u32_be(buf,offset):return struct.unpack_from('>I',buf,offset)[0]
|
|
8
|
+
def frame_packet(metadata,data_buffer):E=data_buffer;B=json.dumps(metadata).encode(_A);D=E if isinstance(E,bytes)else bytes(E);C=bytearray(4+len(B)+4+len(D));A=0;_write_u32_be(C,A,len(B));A+=4;C[A:A+len(B)]=B;A+=len(B);_write_u32_be(C,A,len(D));A+=4;C[A:A+len(D)]=D;return bytes(C)
|
|
9
|
+
def decode_packet(chunk):
|
|
10
|
+
B=chunk;global _internal_buffer
|
|
11
|
+
try:
|
|
12
|
+
if isinstance(B,memoryview):B=B.tobytes()
|
|
13
|
+
_internal_buffer.extend(B);A=0
|
|
14
|
+
if len(_internal_buffer)<4:return
|
|
15
|
+
C=_read_u32_be(_internal_buffer,A);A+=4
|
|
16
|
+
if len(_internal_buffer)<A+C:return
|
|
17
|
+
H=bytes(_internal_buffer[A:A+C]);G=json.loads(H.decode(_A));A+=C
|
|
18
|
+
if len(_internal_buffer)<A+4:return
|
|
19
|
+
D=_read_u32_be(_internal_buffer,A);A+=4
|
|
20
|
+
if len(_internal_buffer)<A+D:return
|
|
21
|
+
E=bytes(_internal_buffer[A:A+D]);A+=D;_internal_buffer=_internal_buffer[A:];F=E
|
|
22
|
+
if G.get('type')=='text':
|
|
23
|
+
try:F=json.loads(E.decode(_A))
|
|
24
|
+
except(json.JSONDecodeError,UnicodeDecodeError):F=E
|
|
25
|
+
return{'metadata':G,'data':F}
|
|
26
|
+
except Exception as I:print(f"decode_packet error: {I}");return
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_F='tokens'
|
|
3
|
+
_E='prompts'
|
|
4
|
+
_D='updatedAt'
|
|
5
|
+
_C='sessionId'
|
|
6
|
+
_B='status'
|
|
7
|
+
_A=None
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime,timezone
|
|
10
|
+
from typing import Any,Optional
|
|
11
|
+
from..config.endpoints import DNotifierEndpoints
|
|
12
|
+
from..transport.http_transport import HttpTransport
|
|
13
|
+
_session_step_tracker={}
|
|
14
|
+
class LogsClient:
|
|
15
|
+
def __init__(A,enabled=False,app_id=_A,token=_A,model=_A):A.enabled=bool(enabled);A.app_id=app_id;A.token=token;A.model=model
|
|
16
|
+
def configure(A,enabled=_A,app_id=_A,token=_A,model=_A):
|
|
17
|
+
E=model;D=token;C=app_id;B=enabled
|
|
18
|
+
if B is not _A:A.enabled=bool(B)
|
|
19
|
+
if C is not _A:A.app_id=C
|
|
20
|
+
if D is not _A:A.token=D
|
|
21
|
+
if E is not _A:A.model=E
|
|
22
|
+
def _transport(A,url):B={'authorization':A.token}if A.token else{};return HttpTransport(url,B)
|
|
23
|
+
async def create_log(A,payload):return await A._transport(DNotifierEndpoints.add_log_url).send(payload)
|
|
24
|
+
async def update_log(A,payload):return await A._transport(DNotifierEndpoints.update_log_status_url).send(payload)
|
|
25
|
+
async def add_session_step_log(A,payload):return await A._transport(DNotifierEndpoints.add_session_step_url).send(payload)
|
|
26
|
+
async def start_ai_logging(A,session_id,prompts,is_existing_session=False):
|
|
27
|
+
F='createdDate';E='appId';D='InProgress';B=session_id
|
|
28
|
+
if not A.enabled or not A.token or not A.app_id or not B:return
|
|
29
|
+
C=datetime.now(timezone.utc).isoformat()
|
|
30
|
+
if is_existing_session:await A.update_log({_C:B,_B:D,_D:C})
|
|
31
|
+
else:await A.create_log({E:A.app_id,_C:B,_B:D,'model':A.model,'calls':1,F:C,'step':1})
|
|
32
|
+
_session_step_tracker[B]={E:A.app_id,_C:B,_F:0,_B:D,_E:prompts,F:C}
|
|
33
|
+
async def complete_ai_logging(B,session_id,response=_A):
|
|
34
|
+
K='usage';J='data';I='Completed';C=session_id;A=response
|
|
35
|
+
if not B.enabled or not B.token or not C:return
|
|
36
|
+
D=_session_step_tracker.get(C)
|
|
37
|
+
if not D:return
|
|
38
|
+
E=datetime.now(timezone.utc).isoformat();await B.update_log({_C:C,_B:I,_D:E});_session_step_tracker.pop(C,_A);L=json.dumps(D.get(_E));F=json.dumps(A);G=0
|
|
39
|
+
if isinstance(A,dict):
|
|
40
|
+
H=A.get(J,{}).get(K)if A.get(J)else A.get(K)
|
|
41
|
+
if isinstance(H,dict):G=H.get('total_tokens',0)or 0
|
|
42
|
+
await B.add_session_step_log({**D,_E:[L,F],'response':F,_B:I,_D:E,_F:G})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_B='base64'
|
|
3
|
+
_A='utf-8'
|
|
4
|
+
import base64,json
|
|
5
|
+
from typing import Any,Optional
|
|
6
|
+
class Payload:
|
|
7
|
+
def __init__(B,buffer):
|
|
8
|
+
A=buffer
|
|
9
|
+
if isinstance(A,(list,tuple)):B.buffer=bytes(A)
|
|
10
|
+
elif isinstance(A,memoryview):B.buffer=A.tobytes()
|
|
11
|
+
elif isinstance(A,str):B.buffer=A.encode(_A)
|
|
12
|
+
elif isinstance(A,dict):B.buffer=json.dumps(A).encode(_A)
|
|
13
|
+
else:B.buffer=bytes(A)
|
|
14
|
+
def raw(A):return A.buffer
|
|
15
|
+
def to_string(A,encoding=_A):
|
|
16
|
+
B=encoding
|
|
17
|
+
if B==_B:return base64.b64encode(A.buffer).decode('ascii')
|
|
18
|
+
return A.buffer.decode(B)
|
|
19
|
+
def to_json(A,encoding=_A):
|
|
20
|
+
try:return json.loads(A.to_string(encoding))
|
|
21
|
+
except(json.JSONDecodeError,UnicodeDecodeError):return
|
|
22
|
+
def to_base64(A):return A.to_string(_B)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from.agent import Agent,define_agent
|
|
3
|
+
from.context import WorkflowContext
|
|
4
|
+
from.errors import WorkflowError
|
|
5
|
+
from.runner import WorkflowRunner
|
|
6
|
+
from.workflow import Workflow
|
|
7
|
+
__all__=['Agent','define_agent','Workflow','WorkflowContext','WorkflowRunner','WorkflowError']
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Awaitable,Callable,TYPE_CHECKING
|
|
3
|
+
from.errors import WorkflowError
|
|
4
|
+
if TYPE_CHECKING:from.context import WorkflowContext
|
|
5
|
+
AgentRun=Callable[['WorkflowContext'],Awaitable]
|
|
6
|
+
class Agent:
|
|
7
|
+
def __init__(B,name,run):
|
|
8
|
+
A=name
|
|
9
|
+
if not A or not isinstance(A,str):raise WorkflowError('Agent name is required')
|
|
10
|
+
if not callable(run):raise WorkflowError(f'Agent "{A}": run must be a callable')
|
|
11
|
+
B.name=A;B.run=run
|
|
12
|
+
def define_agent(name,run):return Agent(name=name,run=run)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_P='usesTokens'
|
|
3
|
+
_O='message'
|
|
4
|
+
_N='output'
|
|
5
|
+
_M='latencyMs'
|
|
6
|
+
_L='status'
|
|
7
|
+
_K='apiName'
|
|
8
|
+
_J='type'
|
|
9
|
+
_I='kind'
|
|
10
|
+
_H='agentName'
|
|
11
|
+
_G='error'
|
|
12
|
+
_F='tokensOut'
|
|
13
|
+
_E='tokensIn'
|
|
14
|
+
_D='model'
|
|
15
|
+
_C='input'
|
|
16
|
+
_B=None
|
|
17
|
+
_A='label'
|
|
18
|
+
import json,time
|
|
19
|
+
from typing import Any,TYPE_CHECKING
|
|
20
|
+
from.errors import WorkflowError
|
|
21
|
+
from.step_types import STEP_TYPE_CUSTOM,to_log_step_type
|
|
22
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier;from.observability import WorkflowObservability;from.workflow import Workflow
|
|
23
|
+
def _usage_from_response(response):
|
|
24
|
+
F='usage';E='data';B=response;A=_B
|
|
25
|
+
if isinstance(B,dict):
|
|
26
|
+
C=B.get(E)
|
|
27
|
+
if isinstance(C,dict):A=C.get(F)
|
|
28
|
+
if A is _B:A=B.get(F)
|
|
29
|
+
if not isinstance(A,dict):return{_E:0,_F:0,_D:_B}
|
|
30
|
+
G=A.get('prompt_tokens')or A.get('input_tokens')or A.get(_E)or 0;H=A.get('completion_tokens')or A.get('output_tokens')or A.get('total_tokens')or A.get(_F)or 0;D=_B
|
|
31
|
+
if isinstance(B,dict):
|
|
32
|
+
C=B.get(E)
|
|
33
|
+
if isinstance(C,dict):D=C.get(_D)
|
|
34
|
+
if D is _B:D=A.get(_D)
|
|
35
|
+
return{_E:G,_F:H,_D:D}
|
|
36
|
+
def _snapshot(value):
|
|
37
|
+
A=value
|
|
38
|
+
try:
|
|
39
|
+
if A is _B:return
|
|
40
|
+
return json.loads(json.dumps(A,default=str))
|
|
41
|
+
except(TypeError,ValueError):return str(A)
|
|
42
|
+
def _api_input(opts):
|
|
43
|
+
if not opts:return
|
|
44
|
+
A={A:B for(A,B)in opts.items()if A!=_A};return A if A else _B
|
|
45
|
+
class WorkflowContext:
|
|
46
|
+
def __init__(A,notifier,sender_id,workflow,input_data,state=_B,observability=_B,execution_id=_B,agent_name=_B):E=execution_id;D=observability;C=state;B=workflow;A.notifier=notifier;A.sender_id=sender_id;A.workflow=B;A.input=input_data;A.state=C if C is not _B else{};A._observability=D;A._execution_id=E;A._observability_enabled=bool(B.observability and D and E);A.agent_name=agent_name
|
|
47
|
+
async def _invoke_sdk_api(A,api_name,invoke,meta=_B):
|
|
48
|
+
D=api_name;B=meta;B=B or{};H=int(time.time()*1000);E='ok';C=_B;F=_B;G=to_log_step_type(D)
|
|
49
|
+
try:C=await invoke();return C
|
|
50
|
+
except Exception as I:E=_G;F={_O:str(I)};raise
|
|
51
|
+
finally:
|
|
52
|
+
if A._observability_enabled and A._observability and A._execution_id:J=_usage_from_response(C)if B.get(_P)else{_E:0,_F:0,_D:_B};K=A.agent_name or D;await A._observability.record_step(A._execution_id,{_K:D,_H:A.agent_name,_I:K,_J:G,_A:B.get(_A)or G,_L:E,_M:int(time.time()*1000)-H,**J,_C:_snapshot(B.get(_C)),_N:_snapshot(C),_G:F})
|
|
53
|
+
async def run_agent(A,name,input_data=_B):
|
|
54
|
+
C=input_data;B=name;D=A.workflow.get_agent(B)
|
|
55
|
+
if not D:raise WorkflowError(f"Agent not found: {B}")
|
|
56
|
+
E=C if C is not _B else A.input;F=WorkflowContext(notifier=A.notifier,sender_id=A.sender_id,workflow=A.workflow,input_data=E,state=A.state,observability=A._observability,execution_id=A._execution_id,agent_name=B);return await D.run(F)
|
|
57
|
+
async def send_ai(B,**A):return await B._invoke_sdk_api('sendAI',lambda:B.notifier.send_ai(sender_id=B.sender_id,**A),{_A:A.get(_A),_C:A.get(_O)or _api_input(A),_P:True})
|
|
58
|
+
async def fetch_ai_history(A,**B):return await A._invoke_sdk_api('fetchAIHistory',lambda:A.notifier.fetch_ai_history(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
59
|
+
async def search(A,**B):return await A._invoke_sdk_api('search',lambda:A.notifier.search(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
60
|
+
async def add_document(A,**B):return await A._invoke_sdk_api('addDocument',lambda:A.notifier.add_document(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
61
|
+
async def update_document(A,**B):return await A._invoke_sdk_api('updateDocument',lambda:A.notifier.update_document(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
62
|
+
async def delete_document(A,**B):return await A._invoke_sdk_api('deleteDocument',lambda:A.notifier.delete_document(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
63
|
+
async def list_documents(A,**B):return await A._invoke_sdk_api('listDocuments',lambda:A.notifier.list_documents(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
64
|
+
async def get_document(A,**B):return await A._invoke_sdk_api('getDocument',lambda:A.notifier.get_document(sender_id=A.sender_id,**B),{_A:B.get(_A),_C:_api_input(B)})
|
|
65
|
+
async def record_step(B,step):
|
|
66
|
+
A=step
|
|
67
|
+
if not B._observability_enabled or not B._observability or not B._execution_id:return
|
|
68
|
+
C=to_log_step_type(A.get(_J))if A.get(_J)else STEP_TYPE_CUSTOM;await B._observability.record_step(B._execution_id,{**A,_K:A.get(_K)or A.get(_I),_H:A.get(_H)or B.agent_name,_I:A.get(_H)or B.agent_name or A.get(_I)or'custom',_J:C,_A:A.get(_A)or C,_E:A.get(_E,0),_F:A.get(_F,0),_D:A.get(_D),_M:A.get(_M,0),_L:A.get(_L,'ok'),_G:A.get(_G),_C:_snapshot(A.get(_C)),_N:_snapshot(A.get(_N))})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
def to_json_column(value):
|
|
5
|
+
C='text';A=value
|
|
6
|
+
if A is None:return
|
|
7
|
+
if isinstance(A,str):
|
|
8
|
+
B=A.strip()
|
|
9
|
+
if not B:return{}
|
|
10
|
+
if B.startswith('{')or B.startswith('['):
|
|
11
|
+
try:return json.loads(B)
|
|
12
|
+
except json.JSONDecodeError:return{C:A}
|
|
13
|
+
return{C:A}
|
|
14
|
+
if isinstance(A,(int,float,bool)):return A
|
|
15
|
+
try:return json.loads(json.dumps(A,default=str))
|
|
16
|
+
except(TypeError,ValueError):return{C:str(A)}
|
|
17
|
+
def set_json_column(target,key,value):
|
|
18
|
+
A=to_json_column(value)
|
|
19
|
+
if A is not None:target[key]=A
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_B='workflowId'
|
|
3
|
+
_A='executionId'
|
|
4
|
+
from datetime import datetime,timezone
|
|
5
|
+
from typing import TYPE_CHECKING,Any
|
|
6
|
+
from.ids import short_id
|
|
7
|
+
from.step_types import resolve_step_type
|
|
8
|
+
from.workflow_observability_client import WorkflowObservabilityClient
|
|
9
|
+
if TYPE_CHECKING:from.workflow import Workflow
|
|
10
|
+
class WorkflowObservability:
|
|
11
|
+
def __init__(A,app_id,token=None,client=None):C=token;B=app_id;A.app_id=B;A.token=C;A.client=client or WorkflowObservabilityClient(app_id=B,token=C);A.client.configure(app_id=B,token=C);A._registered_workflow_names=set();A._step_counter=0
|
|
12
|
+
async def register_workflow(B,workflow):
|
|
13
|
+
A=workflow
|
|
14
|
+
if A.name in B._registered_workflow_names:return
|
|
15
|
+
B._registered_workflow_names.add(A.name)
|
|
16
|
+
try:await B.client.register_workflow({'appId':B.app_id,_B:A.name,'name':A.name,'description':A.description})
|
|
17
|
+
except Exception:pass
|
|
18
|
+
async def start_execution(A,params):
|
|
19
|
+
G='startedAt';F='input';E='userId';B=params;C=short_id('exec');D=datetime.now(timezone.utc).isoformat()
|
|
20
|
+
try:await A.client.create_execution({'id':C,'appId':A.app_id,_B:B[_B],E:B[E],F:B.get(F),G:D})
|
|
21
|
+
except Exception:pass
|
|
22
|
+
A._step_counter=0;return{_A:C,G:D}
|
|
23
|
+
async def record_step(B,execution_id,step):
|
|
24
|
+
I='executedAt';H='kind';E='apiName';D='agentName';C=execution_id;A=step
|
|
25
|
+
if not C:return A
|
|
26
|
+
B._step_counter+=1;J=A.get('id')or f"n{B._step_counter}";F=resolve_step_type(A);K=A.get(D)or A.get(H)or A.get(E);G={**A,'id':J,_A:C,I:A.get(I)or datetime.now(timezone.utc).isoformat(),E:A.get(E),D:A.get(D),H:K,'type':F,'step_type':F}
|
|
27
|
+
try:await B.client.add_step(C,G)
|
|
28
|
+
except Exception:pass
|
|
29
|
+
return G
|
|
30
|
+
async def complete_execution(H,params):
|
|
31
|
+
G='result';F='error';E='stepCount';D='durationMs';C='endedAt';B='status';A=params
|
|
32
|
+
if not A.get(_A):return
|
|
33
|
+
try:await H.client.update_execution(A[_A],{B:A[B],C:A[C],D:A[D],E:A[E],F:A.get(F),G:A.get(G)})
|
|
34
|
+
except Exception:pass
|
|
35
|
+
def get_step_count(A):return A._step_counter
|
|
36
|
+
def create_workflow_observability(app_id,token=None):return WorkflowObservability(app_id=app_id,token=token)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime,timezone
|
|
5
|
+
from typing import Any,TYPE_CHECKING
|
|
6
|
+
from.context import WorkflowContext
|
|
7
|
+
from.errors import WorkflowError
|
|
8
|
+
from.observability import create_workflow_observability
|
|
9
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier;from.workflow import Workflow
|
|
10
|
+
class WorkflowRunner:
|
|
11
|
+
def __init__(A,notifier,sender_id,workflow):
|
|
12
|
+
D=workflow;C=sender_id;B=notifier
|
|
13
|
+
if not B:raise WorkflowError('WorkflowRunner requires a DNotifier instance')
|
|
14
|
+
if not C:raise WorkflowError('WorkflowRunner requires sender_id')
|
|
15
|
+
if not D:raise WorkflowError('WorkflowRunner requires a workflow')
|
|
16
|
+
A.notifier=B;A.sender_id=C;A.workflow=D
|
|
17
|
+
async def run(A,input_data=_A):
|
|
18
|
+
O='result';N='error';M='startedAt';G=input_data;F='executionId';H={};B=_A;C=_A;I=_A;P=int(time.time()*1000)
|
|
19
|
+
if A.workflow.observability:B=create_workflow_observability(app_id=A.notifier.app_id,token=A.notifier.auth_token);await B.register_workflow(A.workflow);J=await B.start_execution({'workflowId':A.workflow.name,'userId':A.sender_id,'input':G});C=J[F];I=J[M]
|
|
20
|
+
Q=WorkflowContext(notifier=A.notifier,sender_id=A.sender_id,workflow=A.workflow,input_data=G,state=H,observability=B,execution_id=C);D=_A;E='ok';K=_A
|
|
21
|
+
try:D=await A.workflow.entry(Q)
|
|
22
|
+
except Exception as R:E=N;K={'message':str(R)};raise
|
|
23
|
+
finally:
|
|
24
|
+
if B and C:S=datetime.now(timezone.utc).isoformat();T=B.get_step_count();await B.complete_execution({F:C,'status':E,'endedAt':S,'durationMs':int(time.time()*1000)-P,'stepCount':T,N:K,O:D if E=='ok'else _A,M:I})
|
|
25
|
+
L={O:D,'state':H}
|
|
26
|
+
if C:L[F]=C
|
|
27
|
+
return L
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_J='Vector Search'
|
|
3
|
+
_I='semanticSearch'
|
|
4
|
+
_H='getDocument'
|
|
5
|
+
_G='listDocuments'
|
|
6
|
+
_F='deleteDocument'
|
|
7
|
+
_E='updateDocument'
|
|
8
|
+
_D='addDocument'
|
|
9
|
+
_C='fetchAIHistory'
|
|
10
|
+
_B='search'
|
|
11
|
+
_A='sendAI'
|
|
12
|
+
STEP_TYPE_AGENT='Agent'
|
|
13
|
+
STEP_TYPE_CUSTOM='Custom'
|
|
14
|
+
SDK_API_STEP_TYPES=[_A,_C,_B,_D,_E,_F,_G,_H]
|
|
15
|
+
LOG_TYPE_BY_API={_A:'AI-LLM',_C:'AI History',_B:_J,_I:_J,_D:'Document Add',_E:'Document Update',_F:'Document Delete',_G:'Document List',_H:'Document Get','agent':STEP_TYPE_AGENT}
|
|
16
|
+
LEGACY_KIND_TO_API={'ai_call':_A,'vector_call':_B}
|
|
17
|
+
def to_log_step_type(api_name):
|
|
18
|
+
B=api_name
|
|
19
|
+
if B is None or not str(B).strip():return STEP_TYPE_CUSTOM
|
|
20
|
+
A=str(B)
|
|
21
|
+
if A in LOG_TYPE_BY_API:return LOG_TYPE_BY_API[A]
|
|
22
|
+
C=A.lower()
|
|
23
|
+
for(D,E)in LOG_TYPE_BY_API.items():
|
|
24
|
+
if D.lower()==C:return E
|
|
25
|
+
return A
|
|
26
|
+
def resolve_api_name(step):
|
|
27
|
+
B=step.get('kind')
|
|
28
|
+
if B is None or not str(B).strip():return
|
|
29
|
+
A=str(B)
|
|
30
|
+
if A in LEGACY_KIND_TO_API:return LEGACY_KIND_TO_API[A]
|
|
31
|
+
if A in('agent',_I)or A in SDK_API_STEP_TYPES:return A
|
|
32
|
+
def resolve_step_type(step):
|
|
33
|
+
A=step.get('type')
|
|
34
|
+
if A is not None and str(A).strip():
|
|
35
|
+
B=str(A).strip()
|
|
36
|
+
if B in LOG_TYPE_BY_API.values():return B
|
|
37
|
+
return to_log_step_type(B)
|
|
38
|
+
C=resolve_api_name(step)
|
|
39
|
+
if C:return to_log_step_type(C)
|
|
40
|
+
return STEP_TYPE_CUSTOM
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Awaitable,Callable,TYPE_CHECKING
|
|
3
|
+
from.agent import Agent
|
|
4
|
+
from.errors import WorkflowError
|
|
5
|
+
if TYPE_CHECKING:from.context import WorkflowContext
|
|
6
|
+
WorkflowEntry=Callable[['WorkflowContext'],Awaitable]
|
|
7
|
+
def _normalize_description(description):
|
|
8
|
+
A=description
|
|
9
|
+
if A is None or A=='':return
|
|
10
|
+
if not isinstance(A,str):raise WorkflowError('Workflow description must be a string')
|
|
11
|
+
B=A.strip()
|
|
12
|
+
if len(B)>100:raise WorkflowError('Workflow description must be at most 100 characters')
|
|
13
|
+
return B if B else None
|
|
14
|
+
class Workflow:
|
|
15
|
+
def __init__(A,name,entry,description=None,observability=False):
|
|
16
|
+
C=entry;B=name
|
|
17
|
+
if not B or not isinstance(B,str):raise WorkflowError('Workflow name is required')
|
|
18
|
+
if not callable(C):raise WorkflowError('Workflow entry must be a callable')
|
|
19
|
+
A.name=B.strip();A.description=_normalize_description(description);A.observability=bool(observability);A.entry=C;A._agents={};A._registered_with_observability=False
|
|
20
|
+
def register_agent(C,name,agent):
|
|
21
|
+
B=agent;A=name
|
|
22
|
+
if not A or not isinstance(A,str):raise WorkflowError('Agent registration name is required')
|
|
23
|
+
if not B or not callable(B.run):raise WorkflowError(f'Invalid agent for "{A}"')
|
|
24
|
+
C._agents[A]=B;return C
|
|
25
|
+
def register_agents(A,agents):
|
|
26
|
+
for(B,C)in agents.items():A.register_agent(B,C)
|
|
27
|
+
return A
|
|
28
|
+
def get_agent(A,name):return A._agents.get(name)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_J='record'
|
|
3
|
+
_I='executionId'
|
|
4
|
+
_H='stepCount'
|
|
5
|
+
_G='error'
|
|
6
|
+
_F='input'
|
|
7
|
+
_E='workflowId'
|
|
8
|
+
_D='appId'
|
|
9
|
+
_C='status'
|
|
10
|
+
_B='id'
|
|
11
|
+
_A=None
|
|
12
|
+
from typing import Any,TYPE_CHECKING
|
|
13
|
+
from..config.endpoints import DNotifierEndpoints
|
|
14
|
+
from..transport.http_transport import HttpTransport
|
|
15
|
+
from.ids import short_id
|
|
16
|
+
from.json_payload import set_json_column
|
|
17
|
+
from.step_types import resolve_step_type
|
|
18
|
+
class WorkflowObservabilityClient:
|
|
19
|
+
def __init__(A,app_id=_A,token=_A):A.app_id=app_id;A.token=token
|
|
20
|
+
def configure(A,app_id=_A,token=_A):
|
|
21
|
+
C=token;B=app_id
|
|
22
|
+
if B is not _A:A.app_id=B
|
|
23
|
+
if C is not _A:A.token=C
|
|
24
|
+
def _transport(A,url):B={'authorization':A.token}if A.token else{};return HttpTransport(url,B)
|
|
25
|
+
async def _post(A,url,body):
|
|
26
|
+
if not A.token:return
|
|
27
|
+
try:await A._transport(url).send(body)
|
|
28
|
+
except Exception:pass
|
|
29
|
+
async def register_workflow(E,payload):
|
|
30
|
+
D='name';B='description';A=payload;C={_D:A[_D],_E:A[_E],D:A[D]}
|
|
31
|
+
if A.get(B):C[B]=A[B]
|
|
32
|
+
await E._post(DNotifierEndpoints.workflow_register_url,C);return{'ok':True}
|
|
33
|
+
async def create_execution(F,payload):E='startedAt';D='userId';A=payload;C=A.get(_B)or short_id('exec');B={_B:C,_D:A[_D],_E:A[_E],D:A[D],_C:'in_progress',_H:0,E:A[E]};set_json_column(B,_F,A.get(_F));await F._post(DNotifierEndpoints.workflow_executions_url,B);return{_I:C,_J:B}
|
|
34
|
+
async def update_execution(G,execution_id,patch):F='result';E='durationMs';D='endedAt';C=execution_id;A=patch;B={_B:C,_C:A[_C],D:A[D],E:A[E],_H:A[_H]};set_json_column(B,_G,A.get(_G));set_json_column(B,F,A.get(F));await G._post(DNotifierEndpoints.workflow_execution_complete_url(C),B);return{'ok':True,_J:B}
|
|
35
|
+
async def add_step(O,execution_id,step):
|
|
36
|
+
N='output';M='tokensOut';L='tokensIn';K='latencyMs';J='executedAt';I='label';H='kind';F=execution_id;E='model';D='apiName';C='agentName';A=step;G=resolve_step_type(A);P=A.get(C)or A.get(H)or A.get(D);B={_B:A[_B],_I:F,H:P,I:A.get(I),'type':G,'step_type':G,_C:A.get(_C),J:A.get(J),K:A.get(K,0),L:A.get(L,0),M:A.get(M,0)}
|
|
37
|
+
if A.get(D):B[D]=A[D]
|
|
38
|
+
if A.get(C):B[C]=A[C]
|
|
39
|
+
if A.get(E):B[E]=str(A[E])
|
|
40
|
+
set_json_column(B,_F,A.get(_F));set_json_column(B,N,A.get(N));set_json_column(B,_G,A.get(_G));await O._post(DNotifierEndpoints.workflow_execution_steps_url(F),B);return{'ok':True}
|