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.
Files changed (37) hide show
  1. {dnotifier-1.0.0 → dnotifier-1.0.2}/CHANGELOG.md +0 -1
  2. {dnotifier-1.0.0 → dnotifier-1.0.2}/PKG-INFO +1 -26
  3. {dnotifier-1.0.0 → dnotifier-1.0.2}/README.md +0 -25
  4. {dnotifier-1.0.0 → dnotifier-1.0.2}/pyproject.toml +6 -1
  5. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/config/endpoints.py +1 -1
  6. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/dnotifier.py +137 -67
  7. dnotifier-1.0.2/src/dnotifier/workflow/context.py +144 -0
  8. dnotifier-1.0.2/src/dnotifier/workflow/graph_parents.py +18 -0
  9. dnotifier-1.0.2/src/dnotifier/workflow/observability.py +42 -0
  10. dnotifier-1.0.2/src/dnotifier/workflow/remote_agent_client.py +25 -0
  11. dnotifier-1.0.2/src/dnotifier/workflow/remote_agent_protocol.py +22 -0
  12. dnotifier-1.0.2/src/dnotifier/workflow/runner.py +36 -0
  13. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/step_types.py +10 -5
  14. dnotifier-1.0.2/src/dnotifier/workflow/workflow.py +55 -0
  15. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/workflow_observability_client.py +10 -4
  16. dnotifier-1.0.0/src/dnotifier/workflow/context.py +0 -68
  17. dnotifier-1.0.0/src/dnotifier/workflow/observability.py +0 -36
  18. dnotifier-1.0.0/src/dnotifier/workflow/runner.py +0 -27
  19. dnotifier-1.0.0/src/dnotifier/workflow/workflow.py +0 -28
  20. {dnotifier-1.0.0 → dnotifier-1.0.2}/.gitignore +0 -0
  21. {dnotifier-1.0.0 → dnotifier-1.0.2}/LICENSE +0 -0
  22. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/__init__.py +0 -0
  23. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/config/__init__.py +0 -0
  24. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/transport/__init__.py +0 -0
  25. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/transport/http_transport.py +0 -0
  26. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/transport/websocket_transport.py +0 -0
  27. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/__init__.py +0 -0
  28. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/buffer.py +0 -0
  29. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/frame.py +0 -0
  30. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/id.py +0 -0
  31. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/logs.py +0 -0
  32. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/utils/payload.py +0 -0
  33. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/__init__.py +0 -0
  34. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/agent.py +0 -0
  35. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/errors.py +0 -0
  36. {dnotifier-1.0.0 → dnotifier-1.0.2}/src/dnotifier/workflow/ids.py +0 -0
  37. {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.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.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,5 +1,5 @@
1
1
  from __future__ import annotations
2
- BASE_HOST='https://api.dnotifier.com'
2
+ BASE_HOST = "https://api.dnotifier.com"
3
3
  _WS_HOST=BASE_HOST.replace('http','ws',1)
4
4
  LOG_HOST=f"{BASE_HOST}/ecs"
5
5
  WORKFLOW_SDK_BASE=f"{LOG_HOST}/api/v1/log/sdk/workflow"
@@ -1,22 +1,26 @@
1
1
  from __future__ import annotations
2
- _c='content'
3
- _b='content or text is required'
4
- _a='saveHistory'
5
- _Z='aiEnabled'
6
- _Y='limits'
7
- _X='currentPacket'
8
- _W='totalPackets'
9
- _V='_responseData'
10
- _U='recordId'
11
- _T='record_id is required'
12
- _S='record_id'
13
- _R='messageId'
14
- _Q='receivers'
15
- _P='text'
16
- _O='ai'
17
- _N='sender_id required'
18
- _M=True
19
- _L=False
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=_L;max_users:Optional[int]=_A;max_rows_per_user:Optional[int]=_A
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=_L,url=_DEFAULT_WS_URL,on_connected=_A,on_message=_A,on_disconnected=_A):
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=_L;A.ai_enabled=_L;A.message_size_limit=32768;A._logs_enabled=bool(logs);A._logs_client=LogsClient(enabled=A._logs_enabled,app_id=A.app_id);A._auth_response=_A;A.auth_token=_A;A.current_model=_A;A._ws=_A;A._http=_A
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(_V)
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(_V,'Authentication failed')))
85
+ if E==400:raise RuntimeError(str(D.get(_Y,'Authentication failed')))
79
86
  raise RuntimeError('Authentication failed.')
80
- C=D.get(_V)
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(_Y)or{};A.ai_enabled=bool(F.get(_Z));A.message_size_limit=int(F.get('messageSizeKB',32))*1024;A._logs_client.configure(token=A.auth_token,model=A.current_model)
85
- if A._http is not _A:A.is_connected=_M;A.on_connected();return _M
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=_L;H={}
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=_M;A.is_connected=_M;A.on_connected()
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(_W)or 1);J=int(B.get(_X)or 1);P=B.get(_J);D=str(P)if P else _A;K=D or f"{B.get(_B)}_{B.get(_F)}_{F}"
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=_L
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=_L;A.on_disconnected(code=500,reason=str(err))
115
- B.on_message=J;B.on_close=K;B.on_error=L;await B.connect(A.url);M={_C:_P,_I:101,_H:A.app_id,_B:prefix_id(A.app_id,A.user_id),_F:int(time.time()*1000),_E:A.auth_token};N=to_bytes(json.dumps({_E:A.auth_token}));await B.send_binary(frame_packet(M,N));return _M
116
- def _emit_message(B,packet,data):A=packet;B.on_message(DNotifierMessage(metadata=DNotifierMessageMetadata(id=A.get(_J),sender=str(A.get(_B,'')),timestamp=int(A.get(_F)or 0),type=A.get(_C)or A.get(_B)),payload=Payload(data)))
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(_Y)
181
+ A=(C._auth_response or{}).get(_b)
120
182
  if not isinstance(A,dict):return
121
- B=bool(A.get(_Z));D=A.get('ai_limits')or{};F=A.get('knowledge_base_limits')or{};E=A.get('db_storage_limits')or{};return DNotifierPlanLimits(messages_hard_limit=A.get('messagesHardLimit'),max_ai_requests_per_month=int(D.get('maxRequestsPerMonth',0))if B else 0,max_ai_words_per_month=int(D.get('maxAIWordsPerMonth',0))if B else 0,knowledge_base_max_words=int(F.get('maxWords',0))if B else 0,ai_enabled=B,max_users=E.get('maxUsers'),max_rows_per_user=E.get('maxRowsPerUser'))
122
- async def send(A,sender_id,data,receiver_id=_A,receiver_ids=_A,save_history=_M):
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(_N)
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:_P,_I:200,_W:1,_X:1,_H:A.app_id,_B:prefix_id(A.app_id,G),_Q:[prefix_id(A.app_id,B)for B in K if B],_F:int(time.time()*1000),_E:A.auth_token,_a:save_history}
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,_W:L,_X:M+1};await F.send_binary(frame_packet(O,N))
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=_M,session_id=_A):
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(_N)
140
- B=E or _random_uuid();F={_J:B,_C:_O,_I:300,_H:A.app_id,_B:prefix_id(A.app_id,C),'receiverId':prefix_id(A.app_id,C),_F:int(time.time()*1000),_E:A.auth_token,_a:save_history};await A._logs_client.start_ai_logging(session_id=B,prompts=D,is_existing_session=bool(E))
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(_N)
149
- C={_J:_random_uuid(),_C:_O,_I:301,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
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(_N)
217
+ if not B:raise ValueError(_O)
156
218
  if not C:raise ValueError('receiver_ids required')
157
- D={_J:_random_uuid(),_C:_P,_I:201,_H:A.app_id,_B:prefix_id(A.app_id,B),_Q:[prefix_id(A.app_id,B)for B in C],_F:int(time.time()*1000),_E:A.auth_token}
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:_O,_I:302,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_F:int(time.time()*1000),_E:A.auth_token,_R:B};D={_R:B}
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:_P,_I:202,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_Q:[],_F:int(time.time()*1000),_E:A.auth_token,_R:B};D={_R:B}
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:_O,_I:303,_H:A.app_id,_B:prefix_id(A.app_id,sender_id),_F:int(time.time()*1000),_E:A.auth_token};B={'query':C}
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(_S)or'').strip();F=B if B is not _A else text or''
184
- if not E:raise ValueError(_T)
185
- if not str(F).strip():raise ValueError(_b)
186
- A={_U:E,_c:str(F)}
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(_S)or'').strip();F=B if B is not _A else text or''
192
- if not E:raise ValueError(_T)
193
- if not str(F).strip():raise ValueError(_b)
194
- A={_U:E,_c:str(F)}
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(_S)or'').strip()
200
- if not A:raise ValueError(_T)
201
- return await B._send_document_rpc(sender_id,308,{_U:A})
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(_S)or'').strip()
210
- if not A:raise ValueError(_T)
211
- return await B._send_document_rpc(sender_id,310,{_U:A})
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(_N)
216
- C={_J:_random_uuid(),_C:_O,_I:code,_H:A.app_id,_B:prefix_id(A.app_id,B),_F:int(time.time()*1000),_E:A.auth_token}
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),_Q:[prefix_id(A.app_id,B)for B in receiver_ids],_F:int(time.time()*1000),_E:A.auth_token}
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=_L
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
- A=step.get('type')
34
- if A is not None and str(A).strip():
35
- B=str(A).strip()
36
- if B in LOG_TYPE_BY_API.values():return B
37
- return to_log_step_type(B)
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(O,execution_id,step):
36
- N='output';M='tokensOut';L='tokensIn';K='latencyMs';J='executedAt';I='label';H='kind';F=execution_id;E='model';D='apiName';C='agentName';A=step;G=resolve_step_type(A);P=A.get(C)or A.get(H)or A.get(D);B={_B:A[_B],_I:F,H:P,I:A.get(I),'type':G,'step_type':G,_C:A.get(_C),J:A.get(J),K:A.get(K,0),L:A.get(L,0),M:A.get(M,0)}
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(E):B[E]=str(A[E])
40
- set_json_column(B,_F,A.get(_F));set_json_column(B,N,A.get(N));set_json_column(B,_G,A.get(_G));await O._post(DNotifierEndpoints.workflow_execution_steps_url(F),B);return{'ok':True}
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