dnotifier 1.0.0__tar.gz → 1.0.2__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 → dnotifier-1.0.2}/CHANGELOG.md +0 -1
- {dnotifier-1.0.0 → dnotifier-1.0.2}/PKG-INFO +1 -26
- {dnotifier-1.0.0 → dnotifier-1.0.2}/README.md +0 -25
- {dnotifier-1.0.0 → dnotifier-1.0.2}/pyproject.toml +6 -1
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/config/endpoints.py +1 -1
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/dnotifier.py +137 -67
- dnotifier-1.0.2/src/dnotifier/workflow/context.py +144 -0
- dnotifier-1.0.2/src/dnotifier/workflow/graph_parents.py +18 -0
- dnotifier-1.0.2/src/dnotifier/workflow/observability.py +42 -0
- dnotifier-1.0.2/src/dnotifier/workflow/remote_agent_client.py +25 -0
- dnotifier-1.0.2/src/dnotifier/workflow/remote_agent_protocol.py +22 -0
- dnotifier-1.0.2/src/dnotifier/workflow/runner.py +36 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/step_types.py +10 -5
- dnotifier-1.0.2/src/dnotifier/workflow/workflow.py +55 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/workflow_observability_client.py +10 -4
- dnotifier-1.0.0/src/dnotifier/workflow/context.py +0 -68
- dnotifier-1.0.0/src/dnotifier/workflow/observability.py +0 -36
- dnotifier-1.0.0/src/dnotifier/workflow/runner.py +0 -27
- dnotifier-1.0.0/src/dnotifier/workflow/workflow.py +0 -28
- {dnotifier-1.0.0 → dnotifier-1.0.2}/.gitignore +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/LICENSE +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/__init__.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/config/__init__.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/transport/__init__.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/transport/http_transport.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/transport/websocket_transport.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/__init__.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/buffer.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/frame.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/id.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/logs.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/payload.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/__init__.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/agent.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/errors.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/ids.py +0 -0
- {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/json_payload.py +0 -0
|
@@ -5,4 +5,3 @@
|
|
|
5
5
|
* Initial Python SDK release with feature parity to JS and Dart SDKs.
|
|
6
6
|
* WebSocket and HTTP transport, binary framing, chunking, AI pipeline, RAG documents, chat history, session logging, and workflows.
|
|
7
7
|
* Production endpoints at `api.dnotifier.com`.
|
|
8
|
-
* Publish pipeline: minify + PyArmor obfuscation for PyPI distributions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dnotifier
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Real-time notification and messaging SDK for Python. WebSocket & HTTP, auth, binary frames, AI, RAG, and workflows.
|
|
5
5
|
Project-URL: Homepage, https://dnotifier.com
|
|
6
6
|
Project-URL: Documentation, https://dnotifier.gitbook.io/product-docs
|
|
@@ -155,31 +155,6 @@ async def main():
|
|
|
155
155
|
|
|
156
156
|
---
|
|
157
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
158
|
## License
|
|
184
159
|
|
|
185
160
|
Proprietary — see [LICENSE](LICENSE).
|
|
@@ -125,31 +125,6 @@ async def main():
|
|
|
125
125
|
|
|
126
126
|
---
|
|
127
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
128
|
## License
|
|
154
129
|
|
|
155
130
|
Proprietary — see [LICENSE](LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dnotifier"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
description = "Real-time notification and messaging SDK for Python. WebSocket & HTTP, auth, binary frames, AI, RAG, and workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Proprietary" }
|
|
@@ -53,3 +53,8 @@ include = [
|
|
|
53
53
|
"/LICENSE",
|
|
54
54
|
"/CHANGELOG.md",
|
|
55
55
|
]
|
|
56
|
+
exclude = [
|
|
57
|
+
"/examples",
|
|
58
|
+
"/scripts",
|
|
59
|
+
"/tests",
|
|
60
|
+
]
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
_g='content'
|
|
3
|
+
_f='content or text is required'
|
|
4
|
+
_e='saveHistory'
|
|
5
|
+
_d='agent_name'
|
|
6
|
+
_c='aiEnabled'
|
|
7
|
+
_b='limits'
|
|
8
|
+
_a='currentPacket'
|
|
9
|
+
_Z='totalPackets'
|
|
10
|
+
_Y='_responseData'
|
|
11
|
+
_X='recordId'
|
|
12
|
+
_W='record_id is required'
|
|
13
|
+
_V='record_id'
|
|
14
|
+
_U='messageId'
|
|
15
|
+
_T='receivers'
|
|
16
|
+
_S='future'
|
|
17
|
+
_R='loop'
|
|
18
|
+
_Q='text'
|
|
19
|
+
_P='ai'
|
|
20
|
+
_O='sender_id required'
|
|
21
|
+
_N='handle'
|
|
22
|
+
_M=False
|
|
23
|
+
_L=True
|
|
20
24
|
_K='Not connected'
|
|
21
25
|
_J='id'
|
|
22
26
|
_I='code'
|
|
@@ -40,6 +44,9 @@ from.utils.id import prefix_id
|
|
|
40
44
|
from.utils.logs import LogsClient
|
|
41
45
|
from.utils.payload import Payload
|
|
42
46
|
from.workflow import Agent,Workflow,WorkflowContext,WorkflowError,WorkflowRunner,define_agent
|
|
47
|
+
from.workflow.remote_agent_client import DEFAULT_TIMEOUT_MS,bare_user_id,is_remote_agent_reply,send_remote_agent_error
|
|
48
|
+
from.workflow.remote_agent_protocol import REMOTE_AGENT_OP_ERROR,REMOTE_AGENT_OP_INVOKE,is_remote_agent_envelope
|
|
49
|
+
from.workflow.observability import create_workflow_observability
|
|
43
50
|
_DEFAULT_WS_URL=DNotifierEndpoints.ws_url
|
|
44
51
|
_DEFAULT_AUTH_URL=DNotifierEndpoints.auth_url
|
|
45
52
|
_DEFAULT_HTTP_URL=DNotifierEndpoints.rpc_send_url
|
|
@@ -48,7 +55,7 @@ class DNotifierMessageMetadata:id:Optional[str];sender:str;timestamp:int;type:Op
|
|
|
48
55
|
@dataclass
|
|
49
56
|
class DNotifierMessage:metadata:DNotifierMessageMetadata;payload:Payload
|
|
50
57
|
@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=
|
|
58
|
+
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=_M;max_users:Optional[int]=_A;max_rows_per_user:Optional[int]=_A
|
|
52
59
|
class _ChunkEntry:
|
|
53
60
|
def __init__(A,total_packets):B=total_packets;A.total_packets=B;A.chunks=[_A]*B
|
|
54
61
|
def _merge_chunks(chunks):return b''.join(chunks)
|
|
@@ -58,10 +65,10 @@ OnMessage=Callable[[DNotifierMessage],_A]
|
|
|
58
65
|
OnDisconnected=Callable[...,_A]
|
|
59
66
|
class DNotifier:
|
|
60
67
|
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=
|
|
68
|
+
def __init__(A,app_id,secret,transport,user_id,logs=_M,url=_DEFAULT_WS_URL,on_connected=_A,on_message=_A,on_disconnected=_A):
|
|
62
69
|
E=user_id;D=secret;C=app_id;B=transport
|
|
63
70
|
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=
|
|
71
|
+
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=_M;A.ai_enabled=_M;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;A._remote_agent_pending={};A._wait_for_message_waiters=[];A._workflow_listeners=[]
|
|
65
72
|
if B=='ws':A._ws=WebsocketTransport()
|
|
66
73
|
elif B=='http':A._http=HttpTransport(_DEFAULT_HTTP_URL)
|
|
67
74
|
else:raise ValueError('transport must be "ws" or "http"')
|
|
@@ -69,31 +76,31 @@ class DNotifier:
|
|
|
69
76
|
A=response
|
|
70
77
|
if not isinstance(A,dict):return A
|
|
71
78
|
if _D in A and _G in A:return A
|
|
72
|
-
B=A.get(
|
|
79
|
+
B=A.get(_Y)
|
|
73
80
|
if isinstance(B,dict)and _D in B and _G in B:return B
|
|
74
81
|
return A
|
|
75
82
|
async def connect(A):
|
|
76
83
|
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
84
|
if E is not _A and E!=200:
|
|
78
|
-
if E==400:raise RuntimeError(str(D.get(
|
|
85
|
+
if E==400:raise RuntimeError(str(D.get(_Y,'Authentication failed')))
|
|
79
86
|
raise RuntimeError('Authentication failed.')
|
|
80
|
-
C=D.get(
|
|
87
|
+
C=D.get(_Y)
|
|
81
88
|
if not isinstance(C,dict):raise RuntimeError('Authentication failed: invalid response')
|
|
82
89
|
A._auth_response=C;A.auth_token=C.get(_E)
|
|
83
90
|
if not A.auth_token:raise RuntimeError('Authentication failed: no token')
|
|
84
|
-
A.current_model=C.get('current_model');F=C.get(
|
|
85
|
-
if A._http is not _A:A.is_connected=
|
|
91
|
+
A.current_model=C.get('current_model');F=C.get(_b)or{};A.ai_enabled=bool(F.get(_c));A.message_size_limit=int(F.get('messageSizeKB',32))*1024;A._logs_client.configure(token=A.auth_token,model=A.current_model)
|
|
92
|
+
if A._http is not _A:A.is_connected=_L;A.on_connected();return _L
|
|
86
93
|
B=A._ws
|
|
87
94
|
if B is _A:raise RuntimeError('WebSocket transport not initialized')
|
|
88
|
-
G=
|
|
95
|
+
G=_M;H={}
|
|
89
96
|
def J(chunk):
|
|
90
97
|
R='300';N=chunk;nonlocal G
|
|
91
98
|
try:
|
|
92
99
|
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
100
|
if not G:
|
|
94
|
-
if I=='102'and prefix_id(A.app_id,A.user_id)in S:G=
|
|
101
|
+
if I=='102'and prefix_id(A.app_id,A.user_id)in S:G=_L;A.is_connected=_L;A.on_connected()
|
|
95
102
|
return
|
|
96
|
-
F=int(B.get(
|
|
103
|
+
F=int(B.get(_Z)or 1);J=int(B.get(_a)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
104
|
if F==1 or not D:
|
|
98
105
|
if I==R and D:
|
|
99
106
|
try:L=Payload(E).to_json();asyncio.create_task(A._logs_client.complete_ai_logging(session_id=D,response=L))
|
|
@@ -109,35 +116,90 @@ class DNotifier:
|
|
|
109
116
|
A._emit_message(B,Q)
|
|
110
117
|
except Exception as M:print(f"Packet error: {M}")
|
|
111
118
|
def K(code=_A,reason=_A):
|
|
112
|
-
B=A.is_connected;A.is_connected=
|
|
119
|
+
B=A.is_connected;A.is_connected=_M
|
|
113
120
|
if B:A.on_disconnected(code=code,reason=reason or'Disconnected')
|
|
114
|
-
def L(err):A.is_connected=
|
|
115
|
-
B.on_message=J;B.on_close=K;B.on_error=L;await B.connect(A.url);M={_C:
|
|
116
|
-
def _emit_message(B,packet,data):A=packet;
|
|
121
|
+
def L(err):A.is_connected=_M;A.on_disconnected(code=500,reason=str(err))
|
|
122
|
+
B.on_message=J;B.on_close=K;B.on_error=L;await B.connect(A.url);M={_C:_Q,_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 _L
|
|
123
|
+
def _emit_message(B,packet,data):A=packet;C=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));B._dispatch_inbound_message(C)
|
|
124
|
+
def _dispatch_inbound_message(B,msg):
|
|
125
|
+
O='agentName';N='output';M='state';I='correlationId';F=msg
|
|
126
|
+
try:A=F.payload.to_json()
|
|
127
|
+
except Exception:A=_A
|
|
128
|
+
if is_remote_agent_envelope(A)and is_remote_agent_reply(A):
|
|
129
|
+
E=B._remote_agent_pending.get(A.get(I))
|
|
130
|
+
if E:
|
|
131
|
+
G=E[_R];C=E[_S]
|
|
132
|
+
if E.get(_N)is not _A:E[_N].cancel()
|
|
133
|
+
B._remote_agent_pending.pop(A.get(I),_A)
|
|
134
|
+
if A.get('op')==REMOTE_AGENT_OP_ERROR:
|
|
135
|
+
def P():
|
|
136
|
+
if not C.done():C.set_exception(WorkflowError((A.get('error')or{}).get('message')or'Remote agent failed',agent_name=E.get(_d)))
|
|
137
|
+
G.call_soon_threadsafe(P)
|
|
138
|
+
else:
|
|
139
|
+
J=A.get(M);Q={N:A.get(N),M:J if isinstance(J,dict)else _A}
|
|
140
|
+
def R():
|
|
141
|
+
if not C.done():C.set_result(Q)
|
|
142
|
+
G.call_soon_threadsafe(R)
|
|
143
|
+
return
|
|
144
|
+
if is_remote_agent_envelope(A)and A.get('op')==REMOTE_AGENT_OP_INVOKE:
|
|
145
|
+
K=next((B for B in B._workflow_listeners if B.has_local_agent(A.get(O))),_A)
|
|
146
|
+
if K is not _A:asyncio.create_task(K._handle_remote_invoke(B,F,A));return
|
|
147
|
+
D=A.get('replyTo')
|
|
148
|
+
if not isinstance(D,str)or not D.strip():D=bare_user_id(B.app_id,F.metadata.sender)
|
|
149
|
+
else:D=D.strip()
|
|
150
|
+
L=A.get(I)
|
|
151
|
+
if L and D:asyncio.create_task(send_remote_agent_error(B,sender_id=B.user_id,receiver_id=D,correlation_id=str(L),message=f"Agent not hosted: {A.get(O)}"))
|
|
152
|
+
return
|
|
153
|
+
if is_remote_agent_envelope(A):return
|
|
154
|
+
if B._wait_for_message_waiters:
|
|
155
|
+
H=B._wait_for_message_waiters.pop(0)
|
|
156
|
+
if H.get(_N)is not _A:H[_N].cancel()
|
|
157
|
+
G=H[_R];C=H[_S]
|
|
158
|
+
def S():
|
|
159
|
+
if not C.done():C.set_result(F)
|
|
160
|
+
G.call_soon_threadsafe(S);return
|
|
161
|
+
B.on_message(F)
|
|
162
|
+
def _await_remote_agent_reply(D,correlation_id,*,timeout_ms=DEFAULT_TIMEOUT_MS,agent_name=_A):
|
|
163
|
+
F=timeout_ms;E=correlation_id;A=agent_name;C=asyncio.get_running_loop();B=C.create_future()
|
|
164
|
+
def G():
|
|
165
|
+
C=D._remote_agent_pending.pop(E,_A)
|
|
166
|
+
if C and not B.done():B.set_exception(WorkflowError(f"Remote agent timed out after {F}ms"+(f": {A}"if A else''),agent_name=A))
|
|
167
|
+
H=C.call_later(F/1e3,G);D._remote_agent_pending[E]={_S:B,_R:C,_N:H,_d:A};return B
|
|
168
|
+
async def _wait_for_inbound_message(C,*,timeout_ms=_A):
|
|
169
|
+
D=timeout_ms;E=DEFAULT_TIMEOUT_MS if D is _A else D;B=asyncio.get_running_loop();A=B.create_future()
|
|
170
|
+
def G():
|
|
171
|
+
try:C._wait_for_message_waiters.remove(F)
|
|
172
|
+
except ValueError:pass
|
|
173
|
+
if not A.done():A.set_exception(WorkflowError(f"wait_for_message timed out after {E}ms"))
|
|
174
|
+
H=B.call_later(E/1e3,G);F={_S:A,_R:B,_N:H};C._wait_for_message_waiters.append(F);return await A
|
|
175
|
+
def _register_workflow_listener(A,workflow):
|
|
176
|
+
B=workflow
|
|
177
|
+
if B not in A._workflow_listeners:A._workflow_listeners.append(B)
|
|
178
|
+
def _unregister_workflow_listener(A,workflow):A._workflow_listeners=[A for A in A._workflow_listeners if A is not workflow]
|
|
117
179
|
def get_plan_limits(C):
|
|
118
180
|
if not C.is_connected:raise RuntimeError(_K)
|
|
119
|
-
A=(C._auth_response or{}).get(
|
|
181
|
+
A=(C._auth_response or{}).get(_b)
|
|
120
182
|
if not isinstance(A,dict):return
|
|
121
|
-
B=bool(A.get(
|
|
122
|
-
async def send(A,sender_id,data,receiver_id=_A,receiver_ids=_A,save_history=
|
|
183
|
+
B=bool(A.get(_c));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'))
|
|
184
|
+
async def send(A,sender_id,data,receiver_id=_A,receiver_ids=_A,save_history=_L):
|
|
123
185
|
I=receiver_ids;H=receiver_id;G=sender_id
|
|
124
186
|
if not A.is_connected:raise RuntimeError(_K)
|
|
125
|
-
if not G:raise ValueError(
|
|
187
|
+
if not G:raise ValueError(_O)
|
|
126
188
|
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:
|
|
189
|
+
K=I or[H];B=to_bytes(json.dumps(data));D=A.message_size_limit or 32768;C=_random_uuid();E={_J:C,_C:_Q,_I:200,_Z:1,_a:1,_H:A.app_id,_B:prefix_id(A.app_id,G),_T:[prefix_id(A.app_id,B)for B in K if B],_F:int(time.time()*1000),_E:A.auth_token,_e:save_history}
|
|
128
190
|
if A._http is not _A:return await A._http.send({_D:E,_G:data})
|
|
129
191
|
F=A._ws
|
|
130
192
|
if F is _A:raise RuntimeError('WebSocket not available')
|
|
131
193
|
if len(B)<=D:await F.send_binary(frame_packet(E,B));return C
|
|
132
194
|
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,
|
|
195
|
+
for(M,N)in enumerate(J):O={**E,_J:C,_Z:L,_a:M+1};await F.send_binary(frame_packet(O,N))
|
|
134
196
|
return C
|
|
135
|
-
async def send_ai(A,sender_id,message,save_history=
|
|
197
|
+
async def send_ai(A,sender_id,message,save_history=_L,session_id=_A):
|
|
136
198
|
E=session_id;D=message;C=sender_id
|
|
137
199
|
try:
|
|
138
200
|
if not A.is_connected:raise RuntimeError(_K)
|
|
139
|
-
if not C:raise ValueError(
|
|
140
|
-
B=E or _random_uuid();F={_J:B,_C:
|
|
201
|
+
if not C:raise ValueError(_O)
|
|
202
|
+
B=E or _random_uuid();F={_J:B,_C:_P,_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,_e:save_history};await A._logs_client.start_ai_logging(session_id=B,prompts=D,is_existing_session=bool(E))
|
|
141
203
|
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
204
|
if A._ws is not _A:H=to_bytes(json.dumps(D));await A._ws.send_binary(frame_packet(F,H))
|
|
143
205
|
return B
|
|
@@ -145,60 +207,60 @@ class DNotifier:
|
|
|
145
207
|
async def fetch_ai_history(A,sender_id):
|
|
146
208
|
B=sender_id
|
|
147
209
|
if not A.is_connected:raise RuntimeError(_K)
|
|
148
|
-
if not B:raise ValueError(
|
|
149
|
-
C={_J:_random_uuid(),_C:
|
|
210
|
+
if not B:raise ValueError(_O)
|
|
211
|
+
C={_J:_random_uuid(),_C:_P,_I:301,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
|
|
150
212
|
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:{}}))
|
|
151
213
|
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes('{}')))
|
|
152
214
|
async def fetch_chat_history(A,sender_id,receiver_ids):
|
|
153
215
|
C=receiver_ids;B=sender_id
|
|
154
216
|
if not A.is_connected:raise RuntimeError(_K)
|
|
155
|
-
if not B:raise ValueError(
|
|
217
|
+
if not B:raise ValueError(_O)
|
|
156
218
|
if not C:raise ValueError('receiver_ids required')
|
|
157
|
-
D={_J:_random_uuid(),_C:
|
|
219
|
+
D={_J:_random_uuid(),_C:_Q,_I:201,_H:A.app_id,_B:prefix_id(A.app_id,B),_T:[prefix_id(A.app_id,B)for B in C],_F:int(time.time()*1000),_E:A.auth_token}
|
|
158
220
|
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:D,_G:{}}))
|
|
159
221
|
if A._ws is not _A:await A._ws.send_binary(frame_packet(D,to_bytes('{}')))
|
|
160
222
|
async def delete_ai_history_message(A,sender_id,message_id):
|
|
161
223
|
B=message_id
|
|
162
224
|
if not A.is_connected:raise RuntimeError(_K)
|
|
163
|
-
C={_J:_random_uuid(),_C:
|
|
225
|
+
C={_J:_random_uuid(),_C:_P,_I:302,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_F:int(time.time()*1000),_E:A.auth_token,_U:B};D={_U:B}
|
|
164
226
|
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:D}))
|
|
165
227
|
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(D))))
|
|
166
228
|
async def delete_chat_history_message(A,sender_id,message_id):
|
|
167
229
|
B=message_id
|
|
168
230
|
if not A.is_connected:raise RuntimeError(_K)
|
|
169
|
-
C={_J:_random_uuid(),_C:
|
|
231
|
+
C={_J:_random_uuid(),_C:_Q,_I:202,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_T:[],_F:int(time.time()*1000),_E:A.auth_token,_U:B};D={_U:B}
|
|
170
232
|
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:D}))
|
|
171
233
|
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(D))))
|
|
172
234
|
async def search(A,sender_id,query,limit=_A,min_similarity=_A,filterby_source=_A):
|
|
173
235
|
F=filterby_source;E=min_similarity;D=limit;C=query
|
|
174
236
|
if not A.is_connected:raise RuntimeError(_K)
|
|
175
237
|
if not C:raise ValueError('query required')
|
|
176
|
-
G={_J:_random_uuid(),_C:
|
|
238
|
+
G={_J:_random_uuid(),_C:_P,_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
239
|
if D is not _A:B['limit']=D
|
|
178
240
|
if E is not _A:B['minSimilarity']=E
|
|
179
241
|
if F is not _A:B['filterbySource']=F
|
|
180
242
|
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:G,_G:B}))
|
|
181
243
|
if A._ws is not _A:await A._ws.send_binary(frame_packet(G,to_bytes(json.dumps(B))))
|
|
182
244
|
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(
|
|
184
|
-
if not E:raise ValueError(
|
|
185
|
-
if not str(F).strip():raise ValueError(
|
|
186
|
-
A={
|
|
245
|
+
D=metadata;C=doc_type;B=content;E=(record_id or H.get(_V)or'').strip();F=B if B is not _A else text or''
|
|
246
|
+
if not E:raise ValueError(_W)
|
|
247
|
+
if not str(F).strip():raise ValueError(_f)
|
|
248
|
+
A={_X:E,_g:str(F)}
|
|
187
249
|
if C is not _A:A[_C]=str(C)
|
|
188
250
|
if D:A[_D]=D
|
|
189
251
|
return await G._send_document_rpc(sender_id,306,A)
|
|
190
252
|
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(
|
|
192
|
-
if not E:raise ValueError(
|
|
193
|
-
if not str(F).strip():raise ValueError(
|
|
194
|
-
A={
|
|
253
|
+
D=metadata;C=doc_type;B=content;E=(record_id or H.get(_V)or'').strip();F=B if B is not _A else text or''
|
|
254
|
+
if not E:raise ValueError(_W)
|
|
255
|
+
if not str(F).strip():raise ValueError(_f)
|
|
256
|
+
A={_X:E,_g:str(F)}
|
|
195
257
|
if C is not _A:A[_C]=str(C)
|
|
196
258
|
if D:A[_D]=D
|
|
197
259
|
return await G._send_document_rpc(sender_id,307,A)
|
|
198
260
|
async def delete_document(B,sender_id,record_id=_A,**C):
|
|
199
|
-
A=(record_id or C.get(
|
|
200
|
-
if not A:raise ValueError(
|
|
201
|
-
return await B._send_document_rpc(sender_id,308,{
|
|
261
|
+
A=(record_id or C.get(_V)or'').strip()
|
|
262
|
+
if not A:raise ValueError(_W)
|
|
263
|
+
return await B._send_document_rpc(sender_id,308,{_X:A})
|
|
202
264
|
async def list_documents(E,sender_id,limit=_A,offset=_A,doc_type=_A,**F):
|
|
203
265
|
D=doc_type;C=offset;B=limit;A={}
|
|
204
266
|
if B is not _A:A['limit']=B
|
|
@@ -206,21 +268,21 @@ class DNotifier:
|
|
|
206
268
|
if D is not _A:A[_C]=str(D)
|
|
207
269
|
return await E._send_document_rpc(sender_id,309,A)
|
|
208
270
|
async def get_document(B,sender_id,record_id=_A,**C):
|
|
209
|
-
A=(record_id or C.get(
|
|
210
|
-
if not A:raise ValueError(
|
|
211
|
-
return await B._send_document_rpc(sender_id,310,{
|
|
271
|
+
A=(record_id or C.get(_V)or'').strip()
|
|
272
|
+
if not A:raise ValueError(_W)
|
|
273
|
+
return await B._send_document_rpc(sender_id,310,{_X:A})
|
|
212
274
|
async def _send_document_rpc(A,sender_id,code,data):
|
|
213
275
|
B=sender_id
|
|
214
276
|
if not A.is_connected:raise RuntimeError(_K)
|
|
215
|
-
if not B:raise ValueError(
|
|
216
|
-
C={_J:_random_uuid(),_C:
|
|
277
|
+
if not B:raise ValueError(_O)
|
|
278
|
+
C={_J:_random_uuid(),_C:_P,_I:code,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
|
|
217
279
|
if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:data}))
|
|
218
280
|
if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(data))))
|
|
219
281
|
async def send_binary(A,sender_id,receiver_ids,buffer,msg_type='binary'):
|
|
220
282
|
B=buffer
|
|
221
283
|
if not A.is_connected:raise RuntimeError(_K)
|
|
222
284
|
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),
|
|
285
|
+
C={_C:msg_type,_I:202,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_T:[prefix_id(A.app_id,B)for B in receiver_ids],_F:int(time.time()*1000),_E:A.auth_token}
|
|
224
286
|
if A._ws is _A:raise RuntimeError('Binary send requires WebSocket transport')
|
|
225
287
|
await A._ws.send_binary(frame_packet(C,B))
|
|
226
288
|
async def send_with_openai(A,sender_id,message,**B):
|
|
@@ -229,6 +291,14 @@ class DNotifier:
|
|
|
229
291
|
async def run_workflow(A,workflow,input_data,sender_id=_A):
|
|
230
292
|
if not A.is_connected:raise RuntimeError(_K)
|
|
231
293
|
B=WorkflowRunner(notifier=A,sender_id=sender_id or A.user_id,workflow=workflow);return await B.run(input_data=input_data)
|
|
294
|
+
async def resume_workflow_context(B,trace_ctx,*,workflow=_A,input_data=_A,state=_A):
|
|
295
|
+
G='workflowId';F='executionId';D=state;A=trace_ctx
|
|
296
|
+
if not A.get(F)or not A.get(G):raise ValueError('trace_ctx requires executionId and workflowId')
|
|
297
|
+
E=create_workflow_observability(app_id=B.app_id,token=B.auth_token,append_only=_L)
|
|
298
|
+
async def H(_ctx):0
|
|
299
|
+
C=workflow or Workflow(name=str(A[G]),observability=_L,entry=H)
|
|
300
|
+
if C.observability:await E.register_workflow(C)
|
|
301
|
+
return WorkflowContext(notifier=B,sender_id=B.user_id,workflow=C,input_data=input_data,state=dict(D)if isinstance(D,dict)else{},observability=E if C.observability else _A,execution_id=A.get(F),trace_parent_step_id=A.get('parentStepId'))
|
|
232
302
|
async def disconnect(A):
|
|
233
303
|
if A._ws is not _A:await A._ws.close()
|
|
234
|
-
A.is_connected=
|
|
304
|
+
A.is_connected=_M
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_Z='usesTokens'
|
|
3
|
+
_Y='workflowId'
|
|
4
|
+
_X='executionId'
|
|
5
|
+
_W='siblingIndex'
|
|
6
|
+
_V='branchKey'
|
|
7
|
+
_U='apiName'
|
|
8
|
+
_T='message'
|
|
9
|
+
_S='stepId'
|
|
10
|
+
_R='latencyMs'
|
|
11
|
+
_Q='status'
|
|
12
|
+
_P='ok'
|
|
13
|
+
_O='type'
|
|
14
|
+
_N='kind'
|
|
15
|
+
_M='agentName'
|
|
16
|
+
_L='key'
|
|
17
|
+
_K='state'
|
|
18
|
+
_J='error'
|
|
19
|
+
_I='tokensOut'
|
|
20
|
+
_H='tokensIn'
|
|
21
|
+
_G='model'
|
|
22
|
+
_F='output'
|
|
23
|
+
_E='name'
|
|
24
|
+
_D='parentStepId'
|
|
25
|
+
_C='input'
|
|
26
|
+
_B='label'
|
|
27
|
+
_A=None
|
|
28
|
+
import asyncio,json,time
|
|
29
|
+
from typing import Any,TYPE_CHECKING
|
|
30
|
+
from.errors import WorkflowError
|
|
31
|
+
from.graph_parents import format_parent_step_id,resolve_recorded_parent_step_id
|
|
32
|
+
from.remote_agent_client import invoke_remote_agent
|
|
33
|
+
from.remote_agent_protocol import is_remote_agent_ref,remote_receiver_id
|
|
34
|
+
from.step_types import STEP_TYPE_AGENT,STEP_TYPE_CUSTOM,to_log_step_type
|
|
35
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier;from.observability import WorkflowObservability;from.workflow import Workflow
|
|
36
|
+
def _usage_from_response(response):
|
|
37
|
+
F='usage';E='data';B=response;A=_A
|
|
38
|
+
if isinstance(B,dict):
|
|
39
|
+
C=B.get(E)
|
|
40
|
+
if isinstance(C,dict):A=C.get(F)
|
|
41
|
+
if A is _A:A=B.get(F)
|
|
42
|
+
if not isinstance(A,dict):return{_H:0,_I:0,_G:_A}
|
|
43
|
+
G=A.get('prompt_tokens')or A.get('input_tokens')or A.get(_H)or 0;H=A.get('completion_tokens')or A.get('output_tokens')or A.get('total_tokens')or A.get(_I)or 0;D=_A
|
|
44
|
+
if isinstance(B,dict):
|
|
45
|
+
C=B.get(E)
|
|
46
|
+
if isinstance(C,dict):D=C.get(_G)
|
|
47
|
+
if D is _A:D=A.get(_G)
|
|
48
|
+
return{_H:G,_I:H,_G:D}
|
|
49
|
+
def _snapshot(value):
|
|
50
|
+
A=value
|
|
51
|
+
try:
|
|
52
|
+
if A is _A:return
|
|
53
|
+
return json.loads(json.dumps(A,default=str))
|
|
54
|
+
except(TypeError,ValueError):return str(A)
|
|
55
|
+
def _api_input(opts):
|
|
56
|
+
if not opts:return
|
|
57
|
+
B={_B,'dependsOn','parentStepIds',_V,_W,_D,_C};A={A:C for(A,C)in opts.items()if A not in B};return A if A else _A
|
|
58
|
+
def _normalize_run_agents_spec(spec):
|
|
59
|
+
D=spec
|
|
60
|
+
if isinstance(D,list):
|
|
61
|
+
A=[]
|
|
62
|
+
for(F,B)in enumerate(D):
|
|
63
|
+
if isinstance(B,str):A.append({_E:B,_C:_A,_L:str(F)})
|
|
64
|
+
elif isinstance(B,dict)and B.get(_E):A.append({_E:B[_E],_C:B.get(_C),_L:str(F)})
|
|
65
|
+
else:raise WorkflowError('run_agents entry requires name')
|
|
66
|
+
return A
|
|
67
|
+
if isinstance(D,dict):
|
|
68
|
+
A=[]
|
|
69
|
+
for(E,C)in D.items():
|
|
70
|
+
if isinstance(C,dict)and _E in C:A.append({_E:C[_E],_C:C.get(_C),_L:E})
|
|
71
|
+
else:A.append({_E:E,_C:C,_L:E})
|
|
72
|
+
return A
|
|
73
|
+
raise WorkflowError('run_agents expects a list or dict spec')
|
|
74
|
+
class WorkflowContext:
|
|
75
|
+
def __init__(A,notifier,sender_id,workflow,input_data,state=_A,observability=_A,execution_id=_A,agent_name=_A,entry_step_id=_A,trace_parent_step_id=_A):F=entry_step_id;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 _A else{};A._observability=D;A._execution_id=E;A._observability_enabled=bool(B.observability and D and E);A.agent_name=agent_name;A._entry_step_id=F;A._chain_head_step_id=F;A._trace_parent_step_id=trace_parent_step_id;A._current_step_id=_A;A.root_step_id=_A;A._terminal_step_ids=[]
|
|
76
|
+
def get_end_parent_step_id(A):
|
|
77
|
+
if A._terminal_step_ids:return format_parent_step_id(A._terminal_step_ids)
|
|
78
|
+
if A._entry_step_id:return A._entry_step_id
|
|
79
|
+
def _mark_terminal_steps(A,step_ids):
|
|
80
|
+
if A._current_step_id:return
|
|
81
|
+
A._terminal_step_ids=[str(A)for A in step_ids if A is not _A and str(A).strip()]
|
|
82
|
+
def _resolve_parent_step_id(A,options=_A):
|
|
83
|
+
B=options;B=B or{}
|
|
84
|
+
if _D in B:return B[_D]
|
|
85
|
+
if A._current_step_id:return A._current_step_id
|
|
86
|
+
if A._trace_parent_step_id:return A._trace_parent_step_id
|
|
87
|
+
if A._chain_head_step_id:return A._chain_head_step_id
|
|
88
|
+
if A._entry_step_id:return A._entry_step_id
|
|
89
|
+
def export_trace_context(A):return{_X:A._execution_id,_Y:A.workflow.name,_D:A._current_step_id or A._chain_head_step_id or A._trace_parent_step_id or A._entry_step_id}
|
|
90
|
+
async def _invoke_sdk_api(A,api_name,invoke,meta=_A):
|
|
91
|
+
D=api_name;B=meta;B=B or{};H=int(time.time()*1000);E=_P;C=_A;F=_A;G=to_log_step_type(D)
|
|
92
|
+
try:C=await invoke();return C
|
|
93
|
+
except Exception as I:E=_J;F={_T:str(I)};raise
|
|
94
|
+
finally:
|
|
95
|
+
if A._observability_enabled and A._observability and A._execution_id:J=_usage_from_response(C)if B.get(_Z)else{_H:0,_I:0,_G:_A};K=A.agent_name or D;await A._observability.record_step(A._execution_id,{_U:D,_M:A.agent_name,_N:K,_O:G,_B:B.get(_B)or G,_Q:E,_R:int(time.time()*1000)-H,_D:format_parent_step_id(A._current_step_id),**J,_C:_snapshot(B.get(_C)),_F:_snapshot(C),_K:_snapshot(A.state),_J:F})
|
|
96
|
+
async def _run_agent_observed(A,name,*,input_data=...,depends_on=_A,parent_step_ids=_A,branch_key=_A,sibling_index=_A,parent_step_id=...,timeout_ms=_A):
|
|
97
|
+
J=input_data;F=parent_step_id;C=name;D=A.workflow.get_agent_registration(C)
|
|
98
|
+
if not D:raise WorkflowError(f"Agent not found: {C}")
|
|
99
|
+
G=A.input if J is...else J;K={}
|
|
100
|
+
if F is not...:K[_D]=F
|
|
101
|
+
P=resolve_recorded_parent_step_id(parent_step_id=F if F is not...else...,parent_step_ids=parent_step_ids,depends_on=depends_on,resolve_parent=lambda:A._resolve_parent_step_id(K));B=A._observability.allocate_step_id()if A._observability_enabled and A._observability else _A;Q=int(time.time()*1000);H=_P;E=_A;L=_A
|
|
102
|
+
try:
|
|
103
|
+
if is_remote_agent_ref(D):
|
|
104
|
+
assert isinstance(D,dict);M=_A
|
|
105
|
+
if A._execution_id:M={_X:A._execution_id,_Y:A.workflow.name,_D:B or A._current_step_id or A._chain_head_step_id or A._trace_parent_step_id or A._entry_step_id}
|
|
106
|
+
N=await invoke_remote_agent(notifier=A.notifier,sender_id=A.sender_id,receiver_id=remote_receiver_id(D),agent_name=C,input_data=G,state=A.state,trace=M,timeout_ms=timeout_ms);O=N.get(_K)
|
|
107
|
+
if isinstance(O,dict):A.state.update(O)
|
|
108
|
+
E=N.get(_F);return{_F:E,_S:B}
|
|
109
|
+
R=D;I=WorkflowContext(notifier=A.notifier,sender_id=A.sender_id,workflow=A.workflow,input_data=G,state=A.state,observability=A._observability,execution_id=A._execution_id,agent_name=C,entry_step_id=A._entry_step_id,trace_parent_step_id=A._trace_parent_step_id);I._current_step_id=B;E=await R.run(I)
|
|
110
|
+
if B:I.root_step_id=B
|
|
111
|
+
return{_F:E,_S:B}
|
|
112
|
+
except Exception as S:H=_J;L={_T:str(S)};raise
|
|
113
|
+
finally:
|
|
114
|
+
if A._observability_enabled and A._observability and A._execution_id and B:
|
|
115
|
+
await A._observability.record_step(A._execution_id,{'id':B,_D:P,_V:branch_key,_W:sibling_index,_M:C,_N:C,_O:STEP_TYPE_AGENT,_B:C,_Q:H,_R:int(time.time()*1000)-Q,_C:_snapshot(G),_F:_snapshot(E),_K:_snapshot(A.state),_J:L});A.root_step_id=B
|
|
116
|
+
if H==_P and not A._current_step_id:A._chain_head_step_id=B
|
|
117
|
+
async def run_agent(A,name,input_data=_A,*,depends_on=_A,parent_step_ids=_A,branch_key=_A,sibling_index=_A,parent_step_id=...,timeout_ms=_A):
|
|
118
|
+
B=input_data;C=await A._run_agent_observed(name,input_data=...if B is _A else B,depends_on=depends_on,parent_step_ids=parent_step_ids,branch_key=branch_key,sibling_index=sibling_index,parent_step_id=parent_step_id,timeout_ms=timeout_ms);D=C.get(_S)
|
|
119
|
+
if D and not A._current_step_id:A._mark_terminal_steps([D])
|
|
120
|
+
return C[_F]
|
|
121
|
+
async def wait_for_message(B,*,timeout_ms=_A):
|
|
122
|
+
A=getattr(B.notifier,'_wait_for_inbound_message',_A)
|
|
123
|
+
if not callable(A):raise WorkflowError('wait_for_message requires a DNotifier instance')
|
|
124
|
+
return await A(timeout_ms=timeout_ms)
|
|
125
|
+
async def run_agents(B,spec,*,parent_step_id=...,label=_A):
|
|
126
|
+
I='stepIds';H='results';A=parent_step_id;M=label;D=_normalize_run_agents_spec(spec);E={}
|
|
127
|
+
if A is not...:E[_D]=A
|
|
128
|
+
J=resolve_recorded_parent_step_id(parent_step_id=A if A is not...else...,resolve_parent=lambda:B._resolve_parent_step_id(E))
|
|
129
|
+
async def K(entry,index):A=entry;return await B._run_agent_observed(A[_E],input_data=A[_C]if A[_C]is not _A else...,parent_step_id=J,sibling_index=index)
|
|
130
|
+
F=await asyncio.gather(*[K(B,A)for(A,B)in enumerate(D)]);G=[A[_F]for A in F];C=[A[_S]for A in F];B._mark_terminal_steps(C)
|
|
131
|
+
if isinstance(spec,list):return{H:G,I:C}
|
|
132
|
+
L={B[_L]:G[A]for(A,B)in enumerate(D)};return{H:L,I:C}
|
|
133
|
+
async def send_ai(B,**A):return await B._invoke_sdk_api('sendAI',lambda:B.notifier.send_ai(sender_id=B.sender_id,**A),{_B:A.get(_B),_C:A.get(_T)or _api_input(A),_Z:True})
|
|
134
|
+
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),{_B:B.get(_B),_C:_api_input(B)})
|
|
135
|
+
async def search(A,**B):return await A._invoke_sdk_api('search',lambda:A.notifier.search(sender_id=A.sender_id,**B),{_B:B.get(_B),_C:_api_input(B)})
|
|
136
|
+
async def add_document(A,**B):return await A._invoke_sdk_api('addDocument',lambda:A.notifier.add_document(sender_id=A.sender_id,**B),{_B:B.get(_B),_C:_api_input(B)})
|
|
137
|
+
async def update_document(A,**B):return await A._invoke_sdk_api('updateDocument',lambda:A.notifier.update_document(sender_id=A.sender_id,**B),{_B:B.get(_B),_C:_api_input(B)})
|
|
138
|
+
async def delete_document(A,**B):return await A._invoke_sdk_api('deleteDocument',lambda:A.notifier.delete_document(sender_id=A.sender_id,**B),{_B:B.get(_B),_C:_api_input(B)})
|
|
139
|
+
async def list_documents(A,**B):return await A._invoke_sdk_api('listDocuments',lambda:A.notifier.list_documents(sender_id=A.sender_id,**B),{_B:B.get(_B),_C:_api_input(B)})
|
|
140
|
+
async def get_document(A,**B):return await A._invoke_sdk_api('getDocument',lambda:A.notifier.get_document(sender_id=A.sender_id,**B),{_B:B.get(_B),_C:_api_input(B)})
|
|
141
|
+
async def record_step(B,step):
|
|
142
|
+
A=step
|
|
143
|
+
if not B._observability_enabled or not B._observability or not B._execution_id:return
|
|
144
|
+
C=to_log_step_type(A.get(_O))if A.get(_O)else STEP_TYPE_CUSTOM;D=A.get(_D);await B._observability.record_step(B._execution_id,{**A,_U:A.get(_U)or A.get(_N),_M:A.get(_M)or B.agent_name,_N:A.get(_M)or B.agent_name or A.get(_N)or'custom',_O:C,_B:A.get(_B)or C,_H:A.get(_H,0),_I:A.get(_I,0),_G:A.get(_G),_R:A.get(_R,0),_Q:A.get(_Q,_P),_J:A.get(_J),_D:format_parent_step_id(D)if D is not _A else format_parent_step_id(B._current_step_id),_C:_snapshot(A.get(_C)),_F:_snapshot(A.get(_F)),_K:_snapshot(A[_K]if _K in A else B.state)})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
from collections.abc import Callable,Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
def format_parent_step_id(value):
|
|
6
|
+
A=value
|
|
7
|
+
if A is _A:return
|
|
8
|
+
if isinstance(A,(list,tuple)):B=[str(A).strip()for A in A if A is not _A and str(A).strip()];return','.join(B)if B else _A
|
|
9
|
+
C=str(A).strip();return C or _A
|
|
10
|
+
def resolve_graph_parent_step_id(step):
|
|
11
|
+
B=step;A=B.get('parentStepIds')or B.get('dependsOn')
|
|
12
|
+
if isinstance(A,Sequence)and not isinstance(A,(str,bytes))and len(A)>0:return format_parent_step_id(A)
|
|
13
|
+
return format_parent_step_id(B.get('parentStepId'))
|
|
14
|
+
def resolve_recorded_parent_step_id(*,parent_step_id=...,parent_step_ids=_A,depends_on=_A,resolve_parent=_A):
|
|
15
|
+
C=resolve_parent;B=parent_step_ids;A=parent_step_id;D=B if B else depends_on
|
|
16
|
+
if D:return format_parent_step_id(D)
|
|
17
|
+
if A is not...:return format_parent_step_id(A)
|
|
18
|
+
if C is not _A:return format_parent_step_id(C())
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_C='workflowId'
|
|
3
|
+
_B='executionId'
|
|
4
|
+
_A=None
|
|
5
|
+
from datetime import datetime,timezone
|
|
6
|
+
from typing import TYPE_CHECKING,Any
|
|
7
|
+
from.graph_parents import resolve_graph_parent_step_id
|
|
8
|
+
from.ids import short_id
|
|
9
|
+
from.step_types import resolve_step_type
|
|
10
|
+
from.workflow_observability_client import WorkflowObservabilityClient
|
|
11
|
+
if TYPE_CHECKING:from.workflow import Workflow
|
|
12
|
+
class WorkflowObservability:
|
|
13
|
+
def __init__(A,app_id,token=_A,client=_A,append_only=False):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;A._append_only=append_only
|
|
14
|
+
def allocate_step_id(A):return short_id('step')
|
|
15
|
+
async def register_workflow(B,workflow):
|
|
16
|
+
A=workflow
|
|
17
|
+
if A.name in B._registered_workflow_names:return
|
|
18
|
+
B._registered_workflow_names.add(A.name)
|
|
19
|
+
try:await B.client.register_workflow({'appId':B.app_id,_C:A.name,'name':A.name,'description':A.description})
|
|
20
|
+
except Exception:pass
|
|
21
|
+
async def start_execution(A,params):
|
|
22
|
+
G='startedAt';F='input';E='userId';B=params
|
|
23
|
+
if A._append_only:raise RuntimeError('start_execution is not allowed in append-only workflow observability mode')
|
|
24
|
+
C=short_id('exec');D=datetime.now(timezone.utc).isoformat()
|
|
25
|
+
try:await A.client.create_execution({'id':C,'appId':A.app_id,_C:B[_C],E:B[E],F:B.get(F),G:D})
|
|
26
|
+
except Exception:pass
|
|
27
|
+
A._step_counter=0;return{_B:C,G:D}
|
|
28
|
+
async def record_step(C,execution_id,step):
|
|
29
|
+
K='branchKey';J='siblingIndex';I='executedAt';H='kind';F='apiName';E='agentName';D=execution_id;A=step
|
|
30
|
+
if not D:return A
|
|
31
|
+
C._step_counter+=1;L=A.get('id')or C.allocate_step_id();G=resolve_step_type(A);M=A.get(E)or A.get(H)or A.get(F);N=resolve_graph_parent_step_id(A);B={**A,'id':L,_B:D,I:A.get(I)or datetime.now(timezone.utc).isoformat(),F:A.get(F),E:A.get(E),H:M,'type':G,'step_type':G,'parentStepId':N,J:A.get(J),K:A.get(K)};B.pop('dependsOn',_A);B.pop('parentStepIds',_A)
|
|
32
|
+
try:await C.client.add_step(D,B)
|
|
33
|
+
except Exception:pass
|
|
34
|
+
return B
|
|
35
|
+
async def complete_execution(B,params):
|
|
36
|
+
H='result';G='error';F='stepCount';E='durationMs';D='endedAt';C='status';A=params
|
|
37
|
+
if B._append_only:return
|
|
38
|
+
if not A.get(_B):return
|
|
39
|
+
try:await B.client.update_execution(A[_B],{C:A[C],D:A[D],E:A[E],F:A[F],G:A.get(G),H:A.get(H)})
|
|
40
|
+
except Exception:pass
|
|
41
|
+
def get_step_count(A):return A._step_counter
|
|
42
|
+
def create_workflow_observability(app_id,token=_A,append_only=False):return WorkflowObservability(app_id=app_id,token=token,append_only=append_only)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_B=False
|
|
3
|
+
_A=None
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any,TYPE_CHECKING
|
|
6
|
+
from.errors import WorkflowError
|
|
7
|
+
from.remote_agent_protocol import REMOTE_AGENT_OP_ERROR,REMOTE_AGENT_OP_RESULT,create_error_envelope,create_invoke_envelope,create_result_envelope
|
|
8
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier
|
|
9
|
+
DEFAULT_TIMEOUT_MS=120000
|
|
10
|
+
def bare_user_id(app_id,prefixed_sender):
|
|
11
|
+
A=prefixed_sender
|
|
12
|
+
if not A or not isinstance(A,str):return''
|
|
13
|
+
B=f"{app_id}:"
|
|
14
|
+
if A.startswith(B):return A[len(B):]
|
|
15
|
+
C=A.find(':')
|
|
16
|
+
if C>=0:return A[C+1:]
|
|
17
|
+
return A
|
|
18
|
+
def is_remote_agent_reply(data):return isinstance(data,dict)and data.get('op')in(REMOTE_AGENT_OP_RESULT,REMOTE_AGENT_OP_ERROR)
|
|
19
|
+
async def invoke_remote_agent(*,notifier,sender_id,receiver_id,agent_name,input_data=_A,state=_A,trace=_A,timeout_ms=_A):
|
|
20
|
+
E=timeout_ms;D=receiver_id;C=sender_id;B=agent_name;A=notifier
|
|
21
|
+
if not getattr(A,'is_connected',_B):raise WorkflowError('Remote agent invoke requires a connected notifier')
|
|
22
|
+
if not D:raise WorkflowError(f'Remote agent "{B}" is missing receiver_id')
|
|
23
|
+
F=str(uuid.uuid4());G=create_invoke_envelope(correlation_id=F,agent_name=B,input_data=input_data,state=state,trace=trace,reply_to=C);H=A._await_remote_agent_reply(F,timeout_ms=E if E is not _A else DEFAULT_TIMEOUT_MS,agent_name=B);await A.send(sender_id=C,receiver_id=D,data=G,save_history=_B);return await H
|
|
24
|
+
async def send_remote_agent_result(notifier,*,sender_id,receiver_id,correlation_id,output=_A,state=_A):await notifier.send(sender_id=sender_id,receiver_id=receiver_id,data=create_result_envelope(correlation_id=correlation_id,output=output,state=state),save_history=_B)
|
|
25
|
+
async def send_remote_agent_error(notifier,*,sender_id,receiver_id,correlation_id,message):await notifier.send(sender_id=sender_id,receiver_id=receiver_id,data=create_error_envelope(correlation_id=correlation_id,message=message),save_history=_B)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_F='receiverId'
|
|
3
|
+
_E='receiver_id'
|
|
4
|
+
_D='correlationId'
|
|
5
|
+
_C='op'
|
|
6
|
+
_B='__dnotifier'
|
|
7
|
+
_A=None
|
|
8
|
+
from typing import Any
|
|
9
|
+
REMOTE_AGENT_KIND='dnotifier.remoteAgent'
|
|
10
|
+
REMOTE_AGENT_OP_INVOKE='invoke'
|
|
11
|
+
REMOTE_AGENT_OP_RESULT='result'
|
|
12
|
+
REMOTE_AGENT_OP_ERROR='error'
|
|
13
|
+
def is_remote_agent_envelope(data):A=data;return isinstance(A,dict)and A.get(_B)==REMOTE_AGENT_KIND and isinstance(A.get(_C),str)
|
|
14
|
+
def is_remote_agent_ref(value):
|
|
15
|
+
C=False;A=value
|
|
16
|
+
if not isinstance(A,dict):return C
|
|
17
|
+
if callable(getattr(A,'run',_A)):return C
|
|
18
|
+
B=A.get(_E)or A.get(_F);return isinstance(B,str)and B.strip()!=''
|
|
19
|
+
def remote_receiver_id(value):A=value;B=A.get(_E)or A.get(_F);return str(B).strip()
|
|
20
|
+
def create_invoke_envelope(*,correlation_id,agent_name,reply_to,input_data=_A,state=_A,trace=_A):return{_B:REMOTE_AGENT_KIND,_C:REMOTE_AGENT_OP_INVOKE,_D:correlation_id,'agentName':agent_name,'input':input_data,'state':state,'trace':trace,'replyTo':reply_to}
|
|
21
|
+
def create_result_envelope(*,correlation_id,output=_A,state=_A):return{_B:REMOTE_AGENT_KIND,_C:REMOTE_AGENT_OP_RESULT,_D:correlation_id,'output':output,'state':state}
|
|
22
|
+
def create_error_envelope(*,correlation_id,message):return{_B:REMOTE_AGENT_KIND,_C:REMOTE_AGENT_OP_ERROR,_D:correlation_id,'error':{'message':message}}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
import json,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
|
+
from.step_types import STEP_TYPE_END,STEP_TYPE_ENTRY
|
|
10
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier;from.workflow import Workflow
|
|
11
|
+
def _snapshot(value):
|
|
12
|
+
A=value
|
|
13
|
+
try:
|
|
14
|
+
if A is _A:return
|
|
15
|
+
return json.loads(json.dumps(A,default=str))
|
|
16
|
+
except(TypeError,ValueError):return str(A)
|
|
17
|
+
class WorkflowRunner:
|
|
18
|
+
def __init__(A,notifier,sender_id,workflow):
|
|
19
|
+
D=workflow;C=sender_id;B=notifier
|
|
20
|
+
if not B:raise WorkflowError('WorkflowRunner requires a DNotifier instance')
|
|
21
|
+
if not C:raise WorkflowError('WorkflowRunner requires sender_id')
|
|
22
|
+
if not D:raise WorkflowError('WorkflowRunner requires a workflow')
|
|
23
|
+
A.notifier=B;A.sender_id=C;A.workflow=D
|
|
24
|
+
async def run(A,input_data=_A):
|
|
25
|
+
a='result';Z='parentStepId';Y='latencyMs';X='label';W='kind';V='type';U='startedAt';T='input';N='error';M='state';L='status';K='executionId';G=input_data;F='ok';H={};B=_A;C=_A;O=_A;P=int(time.time()*1000)
|
|
26
|
+
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);Q=await B.start_execution({'workflowId':A.workflow.name,'userId':A.sender_id,T:G});C=Q[K];O=Q[U]
|
|
27
|
+
R=_A
|
|
28
|
+
if B and C:b=await B.record_step(C,{V:STEP_TYPE_ENTRY,W:'entry',X:A.workflow.name,L:F,Y:0,Z:_A,T:_snapshot(G),M:_snapshot(H)});R=b.get('id')
|
|
29
|
+
I=WorkflowContext(notifier=A.notifier,sender_id=A.sender_id,workflow=A.workflow,input_data=G,state=H,observability=B,execution_id=C,entry_step_id=R);E=_A;D=F;J=_A
|
|
30
|
+
try:E=await A.workflow.entry(I)
|
|
31
|
+
except Exception as c:D=N;J={'message':str(c)};raise
|
|
32
|
+
finally:
|
|
33
|
+
if B and C:await B.record_step(C,{V:STEP_TYPE_END,W:'end',X:A.workflow.name,L:D,Y:int(time.time()*1000)-P,Z:I.get_end_parent_step_id(),'output':_snapshot(E if D==F else _A),M:_snapshot(I.state),N:J});d=datetime.now(timezone.utc).isoformat();e=B.get_step_count();await B.complete_execution({K:C,L:D,'endedAt':d,'durationMs':int(time.time()*1000)-P,'stepCount':e,N:J,a:E if D==F else _A,U:O})
|
|
34
|
+
S={a:E,M:H}
|
|
35
|
+
if C:S[K]=C
|
|
36
|
+
return S
|
|
@@ -10,7 +10,11 @@ _C='fetchAIHistory'
|
|
|
10
10
|
_B='search'
|
|
11
11
|
_A='sendAI'
|
|
12
12
|
STEP_TYPE_AGENT='Agent'
|
|
13
|
+
STEP_TYPE_PARALLEL='Parallel'
|
|
14
|
+
STEP_TYPE_ENTRY='Entry'
|
|
15
|
+
STEP_TYPE_END='End'
|
|
13
16
|
STEP_TYPE_CUSTOM='Custom'
|
|
17
|
+
STRUCTURAL_STEP_TYPES=[STEP_TYPE_AGENT,STEP_TYPE_PARALLEL,STEP_TYPE_ENTRY,STEP_TYPE_END,STEP_TYPE_CUSTOM]
|
|
14
18
|
SDK_API_STEP_TYPES=[_A,_C,_B,_D,_E,_F,_G,_H]
|
|
15
19
|
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
20
|
LEGACY_KIND_TO_API={'ai_call':_A,'vector_call':_B}
|
|
@@ -30,11 +34,12 @@ def resolve_api_name(step):
|
|
|
30
34
|
if A in LEGACY_KIND_TO_API:return LEGACY_KIND_TO_API[A]
|
|
31
35
|
if A in('agent',_I)or A in SDK_API_STEP_TYPES:return A
|
|
32
36
|
def resolve_step_type(step):
|
|
33
|
-
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
if
|
|
37
|
-
|
|
37
|
+
B=step.get('type')
|
|
38
|
+
if B is not None and str(B).strip():
|
|
39
|
+
A=str(B).strip()
|
|
40
|
+
if A in STRUCTURAL_STEP_TYPES:return A
|
|
41
|
+
if A in LOG_TYPE_BY_API.values():return A
|
|
42
|
+
return to_log_step_type(A)
|
|
38
43
|
C=resolve_api_name(step)
|
|
39
44
|
if C:return to_log_step_type(C)
|
|
40
45
|
return STEP_TYPE_CUSTOM
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
from typing import Any,Awaitable,Callable,TYPE_CHECKING,Union
|
|
4
|
+
from.agent import Agent
|
|
5
|
+
from.errors import WorkflowError
|
|
6
|
+
from.remote_agent_protocol import is_remote_agent_ref,remote_receiver_id
|
|
7
|
+
if TYPE_CHECKING:from..dnotifier import DNotifier;from.context import WorkflowContext
|
|
8
|
+
WorkflowEntry=Callable[['WorkflowContext'],Awaitable]
|
|
9
|
+
AgentRegistration=Union[Agent,dict[str,Any]]
|
|
10
|
+
def _normalize_description(description):
|
|
11
|
+
A=description
|
|
12
|
+
if A is _A or A=='':return
|
|
13
|
+
if not isinstance(A,str):raise WorkflowError('Workflow description must be a string')
|
|
14
|
+
B=A.strip()
|
|
15
|
+
if len(B)>100:raise WorkflowError('Workflow description must be at most 100 characters')
|
|
16
|
+
return B if B else _A
|
|
17
|
+
class Workflow:
|
|
18
|
+
def __init__(A,name,entry,description=_A,observability=False):
|
|
19
|
+
C=entry;B=name
|
|
20
|
+
if not B or not isinstance(B,str):raise WorkflowError('Workflow name is required')
|
|
21
|
+
if not callable(C):raise WorkflowError('Workflow entry must be a callable')
|
|
22
|
+
A.name=B.strip();A.description=_normalize_description(description);A.observability=bool(observability);A.entry=C;A._agents={};A._registered_with_observability=False;A._listen_notifier=_A
|
|
23
|
+
def register_agent(C,name,agent):
|
|
24
|
+
B=name;A=agent
|
|
25
|
+
if not B or not isinstance(B,str):raise WorkflowError('Agent registration name is required')
|
|
26
|
+
if is_remote_agent_ref(A):assert isinstance(A,dict);C._agents[B]={'receiver_id':remote_receiver_id(A)};return C
|
|
27
|
+
if not A or not callable(getattr(A,'run',_A)):raise WorkflowError(f'Invalid agent for "{B}" (expected Agent or {{"receiver_id": ...}})')
|
|
28
|
+
C._agents[B]=A;return C
|
|
29
|
+
def register_agents(A,agents):
|
|
30
|
+
for(B,C)in agents.items():A.register_agent(B,C)
|
|
31
|
+
return A
|
|
32
|
+
def get_agent(B,name):
|
|
33
|
+
A=B._agents.get(name)
|
|
34
|
+
if A is _A or is_remote_agent_ref(A):return
|
|
35
|
+
return A
|
|
36
|
+
def get_agent_registration(A,name):return A._agents.get(name)
|
|
37
|
+
def has_local_agent(A,name):return A.get_agent(name)is not _A
|
|
38
|
+
def listen(A,notifier):
|
|
39
|
+
B=notifier
|
|
40
|
+
if B is _A:raise WorkflowError('workflow.listen requires a DNotifier instance')
|
|
41
|
+
if A._listen_notifier is not _A and A._listen_notifier is not B:A._listen_notifier._unregister_workflow_listener(A)
|
|
42
|
+
A._listen_notifier=B;B._register_workflow_listener(A);return A
|
|
43
|
+
async def _handle_remote_invoke(D,notifier,msg,data):
|
|
44
|
+
O='input';N='trace';C=data;A=notifier;from.context import WorkflowContext as P;from.observability import create_workflow_observability as Q;from.remote_agent_client import bare_user_id as R,send_remote_agent_error as I,send_remote_agent_result as S;G=C.get('agentName');E=C.get('correlationId');B=C.get('replyTo')
|
|
45
|
+
if not isinstance(B,str)or not B.strip():T=getattr(getattr(msg,'metadata',_A),'sender',_A);B=R(A.app_id,T)
|
|
46
|
+
else:B=B.strip()
|
|
47
|
+
if not E or not B:return
|
|
48
|
+
if not D.has_local_agent(str(G)):await I(A,sender_id=A.user_id,receiver_id=B,correlation_id=str(E),message=f"Agent not hosted: {G}");return
|
|
49
|
+
J={};K=C.get('state')
|
|
50
|
+
if isinstance(K,dict):J=dict(K)
|
|
51
|
+
H=_A;F=C.get(N)if isinstance(C.get(N),dict)else _A;L=F.get('executionId')if F else _A
|
|
52
|
+
if L and D.observability:H=Q(app_id=A.app_id,token=A.auth_token,append_only=True);await H.register_workflow(D)
|
|
53
|
+
M=P(notifier=A,sender_id=A.user_id,workflow=D,input_data=C.get(O),state=J,observability=H,execution_id=L,trace_parent_step_id=F.get('parentStepId')if F else _A)
|
|
54
|
+
try:U=await M.run_agent(str(G),input_data=C.get(O));await S(A,sender_id=A.user_id,receiver_id=B,correlation_id=str(E),output=U,state=M.state)
|
|
55
|
+
except Exception as V:await I(A,sender_id=A.user_id,receiver_id=B,correlation_id=str(E),message=str(V))
|
|
@@ -32,9 +32,15 @@ class WorkflowObservabilityClient:
|
|
|
32
32
|
await E._post(DNotifierEndpoints.workflow_register_url,C);return{'ok':True}
|
|
33
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
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(
|
|
36
|
-
|
|
35
|
+
async def add_step(V,execution_id,step):
|
|
36
|
+
U='branchKey';T='siblingIndex';S='parentStepId';R='state';Q='output';P='tokensOut';O='tokensIn';N='latencyMs';M='executedAt';L='label';K='kind';G=execution_id;F='model';D='apiName';C='agentName';A=step;H=resolve_step_type(A);W=A.get(C)or A.get(K)or A.get(D);B={_B:A[_B],_I:G,K:W,L:A.get(L),'type':H,'step_type':H,_C:A.get(_C),M:A.get(M),N:A.get(N,0),O:A.get(O,0),P:A.get(P,0)}
|
|
37
37
|
if A.get(D):B[D]=A[D]
|
|
38
38
|
if A.get(C):B[C]=A[C]
|
|
39
|
-
if A.get(
|
|
40
|
-
set_json_column(B,_F,A.get(_F));set_json_column(B,
|
|
39
|
+
if A.get(F):B[F]=str(A[F])
|
|
40
|
+
set_json_column(B,_F,A.get(_F));set_json_column(B,Q,A.get(Q));set_json_column(B,R,A.get(R));set_json_column(B,_G,A.get(_G));I=A.get(S)
|
|
41
|
+
if I:B[S]=I
|
|
42
|
+
E=A.get(T)
|
|
43
|
+
if E is not _A and E!='':B[T]=E
|
|
44
|
+
J=A.get(U)
|
|
45
|
+
if J:B[U]=J
|
|
46
|
+
await V._post(DNotifierEndpoints.workflow_execution_steps_url(G),B);return{'ok':True}
|
|
@@ -1,68 +0,0 @@
|
|
|
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))})
|
|
@@ -1,36 +0,0 @@
|
|
|
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)
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
|
@@ -1,28 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|