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.
Files changed (30) hide show
  1. dnotifier-1.0.0/.gitignore +12 -0
  2. dnotifier-1.0.0/CHANGELOG.md +8 -0
  3. dnotifier-1.0.0/LICENSE +6 -0
  4. dnotifier-1.0.0/PKG-INFO +185 -0
  5. dnotifier-1.0.0/README.md +155 -0
  6. dnotifier-1.0.0/pyproject.toml +55 -0
  7. dnotifier-1.0.0/src/dnotifier/__init__.py +6 -0
  8. dnotifier-1.0.0/src/dnotifier/config/__init__.py +3 -0
  9. dnotifier-1.0.0/src/dnotifier/config/endpoints.py +13 -0
  10. dnotifier-1.0.0/src/dnotifier/dnotifier.py +234 -0
  11. dnotifier-1.0.0/src/dnotifier/transport/__init__.py +4 -0
  12. dnotifier-1.0.0/src/dnotifier/transport/http_transport.py +13 -0
  13. dnotifier-1.0.0/src/dnotifier/transport/websocket_transport.py +30 -0
  14. dnotifier-1.0.0/src/dnotifier/utils/__init__.py +7 -0
  15. dnotifier-1.0.0/src/dnotifier/utils/buffer.py +7 -0
  16. dnotifier-1.0.0/src/dnotifier/utils/frame.py +26 -0
  17. dnotifier-1.0.0/src/dnotifier/utils/id.py +5 -0
  18. dnotifier-1.0.0/src/dnotifier/utils/logs.py +42 -0
  19. dnotifier-1.0.0/src/dnotifier/utils/payload.py +22 -0
  20. dnotifier-1.0.0/src/dnotifier/workflow/__init__.py +7 -0
  21. dnotifier-1.0.0/src/dnotifier/workflow/agent.py +12 -0
  22. dnotifier-1.0.0/src/dnotifier/workflow/context.py +68 -0
  23. dnotifier-1.0.0/src/dnotifier/workflow/errors.py +3 -0
  24. dnotifier-1.0.0/src/dnotifier/workflow/ids.py +4 -0
  25. dnotifier-1.0.0/src/dnotifier/workflow/json_payload.py +19 -0
  26. dnotifier-1.0.0/src/dnotifier/workflow/observability.py +36 -0
  27. dnotifier-1.0.0/src/dnotifier/workflow/runner.py +27 -0
  28. dnotifier-1.0.0/src/dnotifier/workflow/step_types.py +40 -0
  29. dnotifier-1.0.0/src/dnotifier/workflow/workflow.py +28 -0
  30. dnotifier-1.0.0/src/dnotifier/workflow/workflow_observability_client.py +40 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ publish_dist/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
@@ -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.
@@ -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.
@@ -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,3 @@
1
+ from __future__ import annotations
2
+ from.endpoints import DNotifierEndpoints
3
+ __all__=['DNotifierEndpoints']
@@ -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,4 @@
1
+ from __future__ import annotations
2
+ from.http_transport import HttpTransport
3
+ from.websocket_transport import WebsocketTransport
4
+ __all__=['HttpTransport','WebsocketTransport']
@@ -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,7 @@
1
+ from __future__ import annotations
2
+ def to_bytes(data):
3
+ B='utf-8';A=data
4
+ if isinstance(A,bytes):return A
5
+ if isinstance(A,str):return A.encode(B)
6
+ if isinstance(A,(dict,list)):import json;return json.dumps(A).encode(B)
7
+ return bytes(A)
@@ -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,5 @@
1
+ from __future__ import annotations
2
+ def prefix_id(app_id,user_id):
3
+ A=user_id
4
+ if':'in A:return A
5
+ return f"{app_id}:{A}"
@@ -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,3 @@
1
+ from __future__ import annotations
2
+ class WorkflowError(Exception):
3
+ def __init__(A,message,agent_name=None,cause=None):super().__init__(message);A.name='WorkflowError';A.agent_name=agent_name;A.cause=cause
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ def random_uuid():return str(uuid.uuid4())
4
+ def short_id(prefix):return f"{prefix}_{random_uuid().replace('-','')[:12]}"
@@ -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}