dnotifier 1.0.2__tar.gz → 1.0.3__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 (35) hide show
  1. dnotifier-1.0.3/CHANGELOG.md +6 -0
  2. {dnotifier-1.0.2 → dnotifier-1.0.3}/PKG-INFO +1 -1
  3. {dnotifier-1.0.2 → dnotifier-1.0.3}/pyproject.toml +1 -1
  4. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/dnotifier.py +32 -23
  5. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/utils/logs.py +5 -5
  6. dnotifier-1.0.3/src/dnotifier/workflow/agent.py +13 -0
  7. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/context.py +66 -62
  8. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/workflow_observability_client.py +10 -9
  9. dnotifier-1.0.2/CHANGELOG.md +0 -7
  10. dnotifier-1.0.2/src/dnotifier/workflow/agent.py +0 -12
  11. {dnotifier-1.0.2 → dnotifier-1.0.3}/.gitignore +0 -0
  12. {dnotifier-1.0.2 → dnotifier-1.0.3}/LICENSE +0 -0
  13. {dnotifier-1.0.2 → dnotifier-1.0.3}/README.md +0 -0
  14. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/__init__.py +0 -0
  15. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/config/__init__.py +0 -0
  16. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/config/endpoints.py +0 -0
  17. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/transport/__init__.py +0 -0
  18. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/transport/http_transport.py +0 -0
  19. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/transport/websocket_transport.py +0 -0
  20. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/utils/__init__.py +0 -0
  21. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/utils/buffer.py +0 -0
  22. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/utils/frame.py +0 -0
  23. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/utils/id.py +0 -0
  24. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/utils/payload.py +0 -0
  25. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/__init__.py +0 -0
  26. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/errors.py +0 -0
  27. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/graph_parents.py +0 -0
  28. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/ids.py +0 -0
  29. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/json_payload.py +0 -0
  30. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/observability.py +0 -0
  31. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/remote_agent_client.py +0 -0
  32. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/remote_agent_protocol.py +0 -0
  33. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/runner.py +0 -0
  34. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/step_types.py +0 -0
  35. {dnotifier-1.0.2 → dnotifier-1.0.3}/src/dnotifier/workflow/workflow.py +0 -0
@@ -0,0 +1,6 @@
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, messaging, AI, RAG documents, chat history, session logging, and workflows.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dnotifier
3
- Version: 1.0.2
3
+ Version: 1.0.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dnotifier"
7
- version = "1.0.2"
7
+ version = "1.0.3"
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" }
@@ -15,9 +15,9 @@ _U='messageId'
15
15
  _T='receivers'
16
16
  _S='future'
17
17
  _R='loop'
18
- _Q='text'
19
- _P='ai'
20
- _O='sender_id required'
18
+ _Q='ai'
19
+ _P='sender_id required'
20
+ _O='text'
21
21
  _N='handle'
22
22
  _M=False
23
23
  _L=True
@@ -50,6 +50,15 @@ from.workflow.observability import create_workflow_observability
50
50
  _DEFAULT_WS_URL=DNotifierEndpoints.ws_url
51
51
  _DEFAULT_AUTH_URL=DNotifierEndpoints.auth_url
52
52
  _DEFAULT_HTTP_URL=DNotifierEndpoints.rpc_send_url
53
+ def _build_ai_send_data(message,*,provider=_A,model=_A):
54
+ D=model;C=provider;A=message;E=bool(C and str(C).strip());F=bool(D and str(D).strip())
55
+ if not E and not F:return A
56
+ if isinstance(A,dict):B=dict(A)
57
+ elif A is not _A:B={_O:A}
58
+ else:B={}
59
+ if E:B['provider']=C
60
+ if F:B['model']=D
61
+ return B
53
62
  @dataclass
54
63
  class DNotifierMessageMetadata:id:Optional[str];sender:str;timestamp:int;type:Optional[str]=_A
55
64
  @dataclass
@@ -119,7 +128,7 @@ class DNotifier:
119
128
  B=A.is_connected;A.is_connected=_M
120
129
  if B:A.on_disconnected(code=code,reason=reason or'Disconnected')
121
130
  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
131
+ B.on_message=J;B.on_close=K;B.on_error=L;await B.connect(A.url);M={_C:_O,_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
132
  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
133
  def _dispatch_inbound_message(B,msg):
125
134
  O='agentName';N='output';M='state';I='correlationId';F=msg
@@ -184,9 +193,9 @@ class DNotifier:
184
193
  async def send(A,sender_id,data,receiver_id=_A,receiver_ids=_A,save_history=_L):
185
194
  I=receiver_ids;H=receiver_id;G=sender_id
186
195
  if not A.is_connected:raise RuntimeError(_K)
187
- if not G:raise ValueError(_O)
196
+ if not G:raise ValueError(_P)
188
197
  if not H and not I:raise ValueError('receiver_id or receiver_ids required')
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}
198
+ K=I or[H];B=to_bytes(json.dumps(data));D=A.message_size_limit or 32768;C=_random_uuid();E={_J:C,_C:_O,_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}
190
199
  if A._http is not _A:return await A._http.send({_D:E,_G:data})
191
200
  F=A._ws
192
201
  if F is _A:raise RuntimeError('WebSocket not available')
@@ -194,48 +203,48 @@ class DNotifier:
194
203
  J=[B[A:A+D]for A in range(0,len(B),D)];L=len(J)
195
204
  for(M,N)in enumerate(J):O={**E,_J:C,_Z:L,_a:M+1};await F.send_binary(frame_packet(O,N))
196
205
  return C
197
- async def send_ai(A,sender_id,message,save_history=_L,session_id=_A):
198
- E=session_id;D=message;C=sender_id
206
+ async def send_ai(A,sender_id,message,save_history=_L,session_id=_A,provider=_A,model=_A):
207
+ F=model;E=session_id;D=sender_id
199
208
  try:
200
209
  if not A.is_connected:raise RuntimeError(_K)
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))
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)
204
- if A._ws is not _A:H=to_bytes(json.dumps(D));await A._ws.send_binary(frame_packet(F,H))
205
- return B
206
- except Exception as I:print(f"error in send_ai: {I}");return
210
+ if not D:raise ValueError(_P)
211
+ C=E or _random_uuid();G={_J:C,_C:_Q,_I:300,_H:A.app_id,_B:prefix_id(A.app_id,D),'receiverId':prefix_id(A.app_id,D),_F:int(time.time()*1000),_E:A.auth_token,_e:save_history};B=_build_ai_send_data(message,provider=provider,model=F);await A._logs_client.start_ai_logging(session_id=C,prompts=B,is_existing_session=bool(E),model=(B.get('model')if isinstance(B,dict)else _A)or F)
212
+ if A._http is not _A:H=await A._http.send({_D:G,_G:B});await A._logs_client.complete_ai_logging(session_id=C,response=H);return A._normalize_rpc_response(H)
213
+ if A._ws is not _A:I=to_bytes(json.dumps(B));await A._ws.send_binary(frame_packet(G,I))
214
+ return C
215
+ except Exception as J:print(f"error in send_ai: {J}");return
207
216
  async def fetch_ai_history(A,sender_id):
208
217
  B=sender_id
209
218
  if not A.is_connected:raise RuntimeError(_K)
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}
219
+ if not B:raise ValueError(_P)
220
+ C={_J:_random_uuid(),_C:_Q,_I:301,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
212
221
  if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:{}}))
213
222
  if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes('{}')))
214
223
  async def fetch_chat_history(A,sender_id,receiver_ids):
215
224
  C=receiver_ids;B=sender_id
216
225
  if not A.is_connected:raise RuntimeError(_K)
217
- if not B:raise ValueError(_O)
226
+ if not B:raise ValueError(_P)
218
227
  if not C:raise ValueError('receiver_ids required')
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}
228
+ D={_J:_random_uuid(),_C:_O,_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}
220
229
  if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:D,_G:{}}))
221
230
  if A._ws is not _A:await A._ws.send_binary(frame_packet(D,to_bytes('{}')))
222
231
  async def delete_ai_history_message(A,sender_id,message_id):
223
232
  B=message_id
224
233
  if not A.is_connected:raise RuntimeError(_K)
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}
234
+ C={_J:_random_uuid(),_C:_Q,_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}
226
235
  if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:D}))
227
236
  if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(D))))
228
237
  async def delete_chat_history_message(A,sender_id,message_id):
229
238
  B=message_id
230
239
  if not A.is_connected:raise RuntimeError(_K)
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}
240
+ C={_J:_random_uuid(),_C:_O,_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}
232
241
  if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:D}))
233
242
  if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(D))))
234
243
  async def search(A,sender_id,query,limit=_A,min_similarity=_A,filterby_source=_A):
235
244
  F=filterby_source;E=min_similarity;D=limit;C=query
236
245
  if not A.is_connected:raise RuntimeError(_K)
237
246
  if not C:raise ValueError('query required')
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}
247
+ G={_J:_random_uuid(),_C:_Q,_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}
239
248
  if D is not _A:B['limit']=D
240
249
  if E is not _A:B['minSimilarity']=E
241
250
  if F is not _A:B['filterbySource']=F
@@ -274,8 +283,8 @@ class DNotifier:
274
283
  async def _send_document_rpc(A,sender_id,code,data):
275
284
  B=sender_id
276
285
  if not A.is_connected:raise RuntimeError(_K)
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}
286
+ if not B:raise ValueError(_P)
287
+ C={_J:_random_uuid(),_C:_Q,_I:code,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
279
288
  if A._http is not _A:return A._normalize_rpc_response(await A._http.send({_D:C,_G:data}))
280
289
  if A._ws is not _A:await A._ws.send_binary(frame_packet(C,to_bytes(json.dumps(data))))
281
290
  async def send_binary(A,sender_id,receiver_ids,buffer,msg_type='binary'):
@@ -23,13 +23,13 @@ class LogsClient:
23
23
  async def create_log(A,payload):return await A._transport(DNotifierEndpoints.add_log_url).send(payload)
24
24
  async def update_log(A,payload):return await A._transport(DNotifierEndpoints.update_log_status_url).send(payload)
25
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
26
+ async def start_ai_logging(A,session_id,prompts,is_existing_session=False,model=_A):
27
+ I='createdDate';H='model';G='appId';E=model;D='InProgress';B=session_id
28
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()
29
+ C=datetime.now(timezone.utc).isoformat();F=E if E else A.model
30
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}
31
+ else:await A.create_log({G:A.app_id,_C:B,_B:D,H:F,'calls':1,I:C,'step':1})
32
+ _session_step_tracker[B]={G:A.app_id,_C:B,_F:0,_B:D,_E:prompts,I:C,H:F}
33
33
  async def complete_ai_logging(B,session_id,response=_A):
34
34
  K='usage';J='data';I='Completed';C=session_id;A=response
35
35
  if not B.enabled or not B.token or not C:return
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+ _A=None
3
+ from typing import Awaitable,Callable,Optional,TYPE_CHECKING
4
+ from.errors import WorkflowError
5
+ if TYPE_CHECKING:from.context import WorkflowContext
6
+ AgentRun=Callable[['WorkflowContext'],Awaitable]
7
+ class Agent:
8
+ def __init__(A,name,run,*,provider=_A,model=_A):
9
+ D=model;C=provider;B=name
10
+ if not B or not isinstance(B,str):raise WorkflowError('Agent name is required')
11
+ if not callable(run):raise WorkflowError(f'Agent "{B}": run must be a callable')
12
+ A.name=B;A.run=run;A.provider=C if C else _A;A.model=D if D else _A
13
+ def define_agent(name,run,*,provider=_A,model=_A):return Agent(name=name,run=run,provider=provider,model=model)
@@ -1,27 +1,28 @@
1
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'
2
+ _a='usesTokens'
3
+ _Z='workflowId'
4
+ _Y='executionId'
5
+ _X='siblingIndex'
6
+ _W='branchKey'
7
+ _V='apiName'
8
+ _U='message'
9
+ _T='stepId'
10
+ _S='latencyMs'
11
+ _R='status'
12
+ _Q='ok'
13
+ _P='type'
14
+ _O='kind'
15
+ _N='agentName'
16
+ _M='key'
17
+ _L='state'
18
+ _K='error'
19
+ _J='tokensOut'
20
+ _I='tokensIn'
21
+ _H='provider'
22
+ _G='output'
23
+ _F='name'
24
+ _E='parentStepId'
25
+ _D='model'
25
26
  _C='input'
26
27
  _B='label'
27
28
  _A=None
@@ -39,13 +40,13 @@ def _usage_from_response(response):
39
40
  C=B.get(E)
40
41
  if isinstance(C,dict):A=C.get(F)
41
42
  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
43
+ if not isinstance(A,dict):return{_I:0,_J:0,_D:_A}
44
+ G=A.get('prompt_tokens')or A.get('input_tokens')or A.get(_I)or 0;H=A.get('completion_tokens')or A.get('output_tokens')or A.get('total_tokens')or A.get(_J)or 0;D=_A
44
45
  if isinstance(B,dict):
45
46
  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}
47
+ if isinstance(C,dict):D=C.get(_D)
48
+ if D is _A:D=A.get(_D)
49
+ return{_I:G,_J:H,_D:D}
49
50
  def _snapshot(value):
50
51
  A=value
51
52
  try:
@@ -54,25 +55,28 @@ def _snapshot(value):
54
55
  except(TypeError,ValueError):return str(A)
55
56
  def _api_input(opts):
56
57
  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
+ B={_B,'dependsOn','parentStepIds',_W,_X,_E,_C,_H,_D};A={A:C for(A,C)in opts.items()if A not in B};return A if A else _A
59
+ def _first_non_empty(*B):
60
+ for A in B:
61
+ if A is not _A and str(A).strip()!='':return A
58
62
  def _normalize_run_agents_spec(spec):
59
63
  D=spec
60
64
  if isinstance(D,list):
61
65
  A=[]
62
66
  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)})
67
+ if isinstance(B,str):A.append({_F:B,_C:_A,_M:str(F)})
68
+ elif isinstance(B,dict)and B.get(_F):A.append({_F:B[_F],_C:B.get(_C),_M:str(F)})
65
69
  else:raise WorkflowError('run_agents entry requires name')
66
70
  return A
67
71
  if isinstance(D,dict):
68
72
  A=[]
69
73
  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})
74
+ if isinstance(C,dict)and _F in C:A.append({_F:C[_F],_C:C.get(_C),_M:E})
75
+ else:A.append({_F:E,_C:C,_M:E})
72
76
  return A
73
77
  raise WorkflowError('run_agents expects a list or dict spec')
74
78
  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=[]
79
+ 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,provider=_A,model=_A):H=model;G=provider;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=[];A._ai_provider=G if G else _A;A._ai_model=H if H else _A
76
80
  def get_end_parent_step_id(A):
77
81
  if A._terminal_step_ids:return format_parent_step_id(A._terminal_step_ids)
78
82
  if A._entry_step_id:return A._entry_step_id
@@ -81,56 +85,56 @@ class WorkflowContext:
81
85
  A._terminal_step_ids=[str(A)for A in step_ids if A is not _A and str(A).strip()]
82
86
  def _resolve_parent_step_id(A,options=_A):
83
87
  B=options;B=B or{}
84
- if _D in B:return B[_D]
88
+ if _E in B:return B[_E]
85
89
  if A._current_step_id:return A._current_step_id
86
90
  if A._trace_parent_step_id:return A._trace_parent_step_id
87
91
  if A._chain_head_step_id:return A._chain_head_step_id
88
92
  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}
93
+ def export_trace_context(A):return{_Y:A._execution_id,_Z:A.workflow.name,_E:A._current_step_id or A._chain_head_step_id or A._trace_parent_step_id or A._entry_step_id}
90
94
  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)
95
+ D=api_name;B=meta;B=B or{};I=int(time.time()*1000);E=_Q;C=_A;F=_A;G=to_log_step_type(D)
92
96
  try:C=await invoke();return C
93
- except Exception as I:E=_J;F={_T:str(I)};raise
97
+ except Exception as J:E=_K;F={_U:str(J)};raise
94
98
  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})
99
+ if A._observability_enabled and A._observability and A._execution_id:H=_usage_from_response(C)if B.get(_a)else{_I:0,_J:0,_D:_A};K=_first_non_empty(B.get(_D),H.get(_D),getattr(A.notifier,'current_model',_A));L=_first_non_empty(B.get(_H));M=A.agent_name or D;await A._observability.record_step(A._execution_id,{_V:D,_N:A.agent_name,_O:M,_P:G,_B:B.get(_B)or G,_R:E,_S:int(time.time()*1000)-I,_E:format_parent_step_id(A._current_step_id),**H,_D:K,_H:L,_C:_snapshot(B.get(_C)),_G:_snapshot(C),_L:_snapshot(A.state),_K:F})
96
100
  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)
101
+ K=input_data;F=parent_step_id;C=name;D=A.workflow.get_agent_registration(C)
98
102
  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
103
+ G=A.input if K is...else K;L={}
104
+ if F is not...:L[_E]=F
105
+ Q=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(L));B=A._observability.allocate_step_id()if A._observability_enabled and A._observability else _A;R=int(time.time()*1000);H=_Q;E=_A;M=_A
102
106
  try:
103
107
  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
108
+ assert isinstance(D,dict);N=_A
109
+ if A._execution_id:N={_Y:A._execution_id,_Z:A.workflow.name,_E:B or A._current_step_id or A._chain_head_step_id or A._trace_parent_step_id or A._entry_step_id}
110
+ O=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=N,timeout_ms=timeout_ms);P=O.get(_L)
111
+ if isinstance(P,dict):A.state.update(P)
112
+ E=O.get(_G);return{_G:E,_T:B}
113
+ I=D;J=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,provider=getattr(I,_H,_A),model=getattr(I,_D,_A));J._current_step_id=B;E=await I.run(J)
114
+ if B:J.root_step_id=B
115
+ return{_G:E,_T:B}
116
+ except Exception as S:H=_K;M={_U:str(S)};raise
113
117
  finally:
114
118
  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
119
+ await A._observability.record_step(A._execution_id,{'id':B,_E:Q,_W:branch_key,_X:sibling_index,_N:C,_O:C,_P:STEP_TYPE_AGENT,_B:C,_R:H,_S:int(time.time()*1000)-R,_C:_snapshot(G),_G:_snapshot(E),_L:_snapshot(A.state),_K:M});A.root_step_id=B
120
+ if H==_Q and not A._current_step_id:A._chain_head_step_id=B
117
121
  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)
122
+ 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(_T)
119
123
  if D and not A._current_step_id:A._mark_terminal_steps([D])
120
- return C[_F]
124
+ return C[_G]
121
125
  async def wait_for_message(B,*,timeout_ms=_A):
122
126
  A=getattr(B.notifier,'_wait_for_inbound_message',_A)
123
127
  if not callable(A):raise WorkflowError('wait_for_message requires a DNotifier instance')
124
128
  return await A(timeout_ms=timeout_ms)
125
129
  async def run_agents(B,spec,*,parent_step_id=...,label=_A):
126
130
  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
131
+ if A is not...:E[_E]=A
128
132
  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)
133
+ async def K(entry,index):A=entry;return await B._run_agent_observed(A[_F],input_data=A[_C]if A[_C]is not _A else...,parent_step_id=J,sibling_index=index)
134
+ F=await asyncio.gather(*[K(B,A)for(A,B)in enumerate(D)]);G=[A[_G]for A in F];C=[A[_T]for A in F];B._mark_terminal_steps(C)
131
135
  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})
136
+ L={B[_M]:G[A]for(A,B)in enumerate(D)};return{H:L,I:C}
137
+ async def send_ai(B,**A):C=A.get(_H,B._ai_provider);D=A.get(_D,B._ai_model);E={**A,_H:C,_D:D};E.pop(_B,_A);return await B._invoke_sdk_api('sendAI',lambda:B.notifier.send_ai(sender_id=B.sender_id,**E),{_B:A.get(_B),_C:A.get(_U)or _api_input(A),_a:True,_H:C,_D:D})
134
138
  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
139
  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
140
  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)})
@@ -141,4 +145,4 @@ class WorkflowContext:
141
145
  async def record_step(B,step):
142
146
  A=step
143
147
  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)})
148
+ C=to_log_step_type(A.get(_P))if A.get(_P)else STEP_TYPE_CUSTOM;D=A.get(_E);await B._observability.record_step(B._execution_id,{**A,_V:A.get(_V)or A.get(_O),_N:A.get(_N)or B.agent_name,_O:A.get(_N)or B.agent_name or A.get(_O)or'custom',_P:C,_B:A.get(_B)or C,_I:A.get(_I,0),_J:A.get(_J,0),_D:A.get(_D),_S:A.get(_S,0),_R:A.get(_R,_Q),_K:A.get(_K),_E:format_parent_step_id(D)if D is not _A else format_parent_step_id(B._current_step_id),_C:_snapshot(A.get(_C)),_G:_snapshot(A.get(_G)),_L:_snapshot(A[_L]if _L in A else B.state)})
@@ -32,15 +32,16 @@ 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(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)}
35
+ async def add_step(W,execution_id,step):
36
+ V='branchKey';U='siblingIndex';T='parentStepId';S='state';R='output';Q='tokensOut';P='tokensIn';O='latencyMs';N='executedAt';M='label';L='kind';H=execution_id;G='provider';F='model';D='apiName';C='agentName';A=step;I=resolve_step_type(A);X=A.get(C)or A.get(L)or A.get(D);B={_B:A[_B],_I:H,L:X,M:A.get(M),'type':I,'step_type':I,_C:A.get(_C),N:A.get(N),O:A.get(O,0),P:A.get(P,0),Q:A.get(Q,0)}
37
37
  if A.get(D):B[D]=A[D]
38
38
  if A.get(C):B[C]=A[C]
39
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}
40
+ if A.get(G):B[G]=str(A[G])
41
+ set_json_column(B,_F,A.get(_F));set_json_column(B,R,A.get(R));set_json_column(B,S,A.get(S));set_json_column(B,_G,A.get(_G));J=A.get(T)
42
+ if J:B[T]=J
43
+ E=A.get(U)
44
+ if E is not _A and E!='':B[U]=E
45
+ K=A.get(V)
46
+ if K:B[V]=K
47
+ await W._post(DNotifierEndpoints.workflow_execution_steps_url(H),B);return{'ok':True}
@@ -1,7 +0,0 @@
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`.
@@ -1,12 +0,0 @@
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)
File without changes
File without changes
File without changes