simpleflow-sdk 0.1.0__py3-none-any.whl
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.
- simpleflow_sdk/__init__.py +37 -0
- simpleflow_sdk/auth.py +88 -0
- simpleflow_sdk/client.py +873 -0
- simpleflow_sdk/contracts.py +104 -0
- simpleflow_sdk-0.1.0.dist-info/METADATA +7 -0
- simpleflow_sdk-0.1.0.dist-info/RECORD +8 -0
- simpleflow_sdk-0.1.0.dist-info/WHEEL +5 -0
- simpleflow_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .auth import InvokeTokenVerifier
|
|
2
|
+
from .client import (
|
|
3
|
+
SimpleFlowAuthenticationError,
|
|
4
|
+
SimpleFlowAuthorizationError,
|
|
5
|
+
SimpleFlowClient,
|
|
6
|
+
SimpleFlowLifecycleError,
|
|
7
|
+
SimpleFlowRequestError,
|
|
8
|
+
TelemetryBridge,
|
|
9
|
+
)
|
|
10
|
+
from .contracts import (
|
|
11
|
+
ChatHistoryMessage,
|
|
12
|
+
ChatMessageWrite,
|
|
13
|
+
InvokeTrace,
|
|
14
|
+
QueueContract,
|
|
15
|
+
RuntimeEvent,
|
|
16
|
+
RuntimeRegistration,
|
|
17
|
+
TelemetrySpan,
|
|
18
|
+
WorkflowTraceTenant,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"SimpleFlowClient",
|
|
23
|
+
"TelemetryBridge",
|
|
24
|
+
"SimpleFlowRequestError",
|
|
25
|
+
"SimpleFlowAuthenticationError",
|
|
26
|
+
"SimpleFlowAuthorizationError",
|
|
27
|
+
"SimpleFlowLifecycleError",
|
|
28
|
+
"InvokeTokenVerifier",
|
|
29
|
+
"RuntimeEvent",
|
|
30
|
+
"InvokeTrace",
|
|
31
|
+
"ChatHistoryMessage",
|
|
32
|
+
"ChatMessageWrite",
|
|
33
|
+
"QueueContract",
|
|
34
|
+
"RuntimeRegistration",
|
|
35
|
+
"TelemetrySpan",
|
|
36
|
+
"WorkflowTraceTenant",
|
|
37
|
+
]
|
simpleflow_sdk/auth.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import jwt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvokeTokenVerifier:
|
|
9
|
+
"""Verify invoke tokens using HS256 shared keys or RS256 public keys.
|
|
10
|
+
|
|
11
|
+
You can pass a verification key per call via ``verify(token, key=...)`` or
|
|
12
|
+
configure a default key using ``for_hs256_shared_key`` / ``for_rs256_public_key``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
issuer: str,
|
|
18
|
+
audience: str,
|
|
19
|
+
algorithms: list[str] | None = None,
|
|
20
|
+
verification_key: str | bytes | Any | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
if issuer.strip() == "":
|
|
23
|
+
raise ValueError("simpleflow sdk auth config error: issuer is required")
|
|
24
|
+
if audience.strip() == "":
|
|
25
|
+
raise ValueError("simpleflow sdk auth config error: audience is required")
|
|
26
|
+
|
|
27
|
+
self._issuer = issuer
|
|
28
|
+
self._audience = audience
|
|
29
|
+
self._algorithms = algorithms if algorithms is not None else ["HS256", "RS256"]
|
|
30
|
+
self._verification_key = verification_key
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def for_hs256_shared_key(
|
|
34
|
+
cls,
|
|
35
|
+
*,
|
|
36
|
+
issuer: str,
|
|
37
|
+
audience: str,
|
|
38
|
+
shared_key: str | bytes,
|
|
39
|
+
) -> "InvokeTokenVerifier":
|
|
40
|
+
return cls(
|
|
41
|
+
issuer=issuer,
|
|
42
|
+
audience=audience,
|
|
43
|
+
algorithms=["HS256"],
|
|
44
|
+
verification_key=shared_key,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def for_rs256_public_key(
|
|
49
|
+
cls,
|
|
50
|
+
*,
|
|
51
|
+
issuer: str,
|
|
52
|
+
audience: str,
|
|
53
|
+
public_key: str | bytes,
|
|
54
|
+
) -> "InvokeTokenVerifier":
|
|
55
|
+
return cls(
|
|
56
|
+
issuer=issuer,
|
|
57
|
+
audience=audience,
|
|
58
|
+
algorithms=["RS256"],
|
|
59
|
+
verification_key=public_key,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def verify(
|
|
63
|
+
self, token: str, key: str | bytes | Any | None = None
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
if token.strip() == "":
|
|
66
|
+
raise ValueError("simpleflow sdk auth error: token is required")
|
|
67
|
+
verification_key = key if key is not None else self._verification_key
|
|
68
|
+
if verification_key is None:
|
|
69
|
+
raise ValueError("simpleflow sdk auth error: verification key is required")
|
|
70
|
+
|
|
71
|
+
claims = jwt.decode(
|
|
72
|
+
token,
|
|
73
|
+
key=verification_key,
|
|
74
|
+
algorithms=self._algorithms,
|
|
75
|
+
audience=self._audience,
|
|
76
|
+
issuer=self._issuer,
|
|
77
|
+
options={"require": ["exp", "iat", "iss", "aud"]},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
str(claims.get("agent_id", "")).strip() == ""
|
|
82
|
+
or str(claims.get("org_id", "")).strip() == ""
|
|
83
|
+
):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"simpleflow sdk auth error: token missing required agent_id or org_id"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return claims
|
simpleflow_sdk/client.py
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from math import isfinite
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SimpleFlowRequestError(RuntimeError):
|
|
14
|
+
def __init__(self, *, status_code: int, detail: str, path: str) -> None:
|
|
15
|
+
super().__init__(
|
|
16
|
+
f"simpleflow sdk request error: status={status_code} path={path} detail={detail}"
|
|
17
|
+
)
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.detail = detail
|
|
20
|
+
self.path = path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SimpleFlowAuthenticationError(SimpleFlowRequestError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimpleFlowAuthorizationError(SimpleFlowRequestError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SimpleFlowLifecycleError(SimpleFlowRequestError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _normalize_payload(payload: Any) -> dict[str, Any]:
|
|
36
|
+
if payload is None:
|
|
37
|
+
return {}
|
|
38
|
+
if hasattr(payload, "__dataclass_fields__"):
|
|
39
|
+
return asdict(payload)
|
|
40
|
+
if isinstance(payload, dict):
|
|
41
|
+
return payload
|
|
42
|
+
raise TypeError(
|
|
43
|
+
"simpleflow sdk payload error: payload must be a dataclass, dict, or None"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_sample_rate(sample_rate: float | None) -> None:
|
|
48
|
+
if sample_rate is None:
|
|
49
|
+
return
|
|
50
|
+
if not isfinite(sample_rate) or sample_rate < 0.0 or sample_rate > 1.0:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"simpleflow sdk config error: telemetry sample_rate must be a finite value between 0.0 and 1.0"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _count_workflow_events_by_type(workflow_result: dict[str, Any]) -> dict[str, int]:
|
|
57
|
+
events = workflow_result.get("events")
|
|
58
|
+
if not isinstance(events, list):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
counts: dict[str, int] = {}
|
|
62
|
+
for event in events:
|
|
63
|
+
if not isinstance(event, dict):
|
|
64
|
+
continue
|
|
65
|
+
event_type = str(event.get("event_type", "")).strip()
|
|
66
|
+
if event_type == "":
|
|
67
|
+
continue
|
|
68
|
+
counts[event_type] = counts.get(event_type, 0) + 1
|
|
69
|
+
return counts
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_workflow_nerdstats(
|
|
73
|
+
workflow_result: dict[str, Any],
|
|
74
|
+
) -> dict[str, Any] | None:
|
|
75
|
+
events = workflow_result.get("events")
|
|
76
|
+
if not isinstance(events, list):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
for event in reversed(events):
|
|
80
|
+
if not isinstance(event, dict):
|
|
81
|
+
continue
|
|
82
|
+
if str(event.get("event_type", "")).strip() != "workflow_completed":
|
|
83
|
+
continue
|
|
84
|
+
metadata = event.get("metadata")
|
|
85
|
+
if not isinstance(metadata, dict):
|
|
86
|
+
continue
|
|
87
|
+
nerdstats = metadata.get("nerdstats")
|
|
88
|
+
if isinstance(nerdstats, dict):
|
|
89
|
+
return nerdstats
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_trace_url(
|
|
94
|
+
trace_id: str, trace_ui_base_url: str = "http://localhost:16686"
|
|
95
|
+
) -> str:
|
|
96
|
+
normalized_trace_id = trace_id.strip()
|
|
97
|
+
if normalized_trace_id == "":
|
|
98
|
+
return ""
|
|
99
|
+
normalized_base_url = trace_ui_base_url.strip().rstrip("/")
|
|
100
|
+
if normalized_base_url == "":
|
|
101
|
+
normalized_base_url = "http://localhost:16686"
|
|
102
|
+
return f"{normalized_base_url}/trace/{normalized_trace_id}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _should_sample(trace_id: str, sample_rate: float | None) -> bool:
|
|
106
|
+
if sample_rate is None:
|
|
107
|
+
return True
|
|
108
|
+
if sample_rate <= 0.0:
|
|
109
|
+
return False
|
|
110
|
+
if sample_rate >= 1.0:
|
|
111
|
+
return True
|
|
112
|
+
digest = sha256(trace_id.encode("utf-8")).digest()
|
|
113
|
+
value = int.from_bytes(digest[0:8], byteorder="big", signed=False)
|
|
114
|
+
ratio = value / ((1 << 64) - 1)
|
|
115
|
+
return ratio <= sample_rate
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SimpleFlowClient:
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
base_url: str,
|
|
122
|
+
api_token: str | None = None,
|
|
123
|
+
oauth_client_id: str | None = None,
|
|
124
|
+
oauth_client_secret: str | None = None,
|
|
125
|
+
oauth_token_path: str = "/v1/oauth/token",
|
|
126
|
+
oauth_token_leeway_seconds: int = 30,
|
|
127
|
+
auth_sessions_path: str = "/v1/auth/sessions",
|
|
128
|
+
auth_current_session_path: str = "/v1/auth/sessions/current",
|
|
129
|
+
auth_me_path: str = "/v1/me",
|
|
130
|
+
runtime_register_path: str = "/v1/runtime/registrations",
|
|
131
|
+
runtime_invoke_path: str = "/v1/runtime/invoke",
|
|
132
|
+
runtime_events_path: str = "/v1/runtime/events",
|
|
133
|
+
runtime_activate_path: str = "/v1/runtime/registrations/{registration_id}/activate",
|
|
134
|
+
runtime_deactivate_path: str = "/v1/runtime/registrations/{registration_id}/deactivate",
|
|
135
|
+
runtime_validate_path: str = "/v1/runtime/registrations/{registration_id}/validate",
|
|
136
|
+
chat_messages_path: str = "/v1/runtime/chat/messages",
|
|
137
|
+
queue_contracts_path: str = "/v1/runtime/queue/contracts",
|
|
138
|
+
chat_sessions_path: str = "/v1/chat/history/sessions",
|
|
139
|
+
chat_history_path: str = "/v1/chat/history/messages",
|
|
140
|
+
timeout_seconds: float = 10.0,
|
|
141
|
+
) -> None:
|
|
142
|
+
if base_url.strip() == "":
|
|
143
|
+
raise ValueError("simpleflow sdk config error: base_url is required")
|
|
144
|
+
self._base_url = base_url.rstrip("/")
|
|
145
|
+
self._api_token = api_token.strip() if api_token is not None else ""
|
|
146
|
+
self._oauth_client_id = (
|
|
147
|
+
oauth_client_id.strip() if oauth_client_id is not None else ""
|
|
148
|
+
)
|
|
149
|
+
self._oauth_client_secret = (
|
|
150
|
+
oauth_client_secret.strip() if oauth_client_secret is not None else ""
|
|
151
|
+
)
|
|
152
|
+
self._oauth_token_path = oauth_token_path
|
|
153
|
+
self._oauth_token_leeway_seconds = max(0, int(oauth_token_leeway_seconds))
|
|
154
|
+
self._oauth_access_token = ""
|
|
155
|
+
self._oauth_access_token_expires_at_unix = 0.0
|
|
156
|
+
self._auth_sessions_path = auth_sessions_path
|
|
157
|
+
self._auth_current_session_path = auth_current_session_path
|
|
158
|
+
self._auth_me_path = auth_me_path
|
|
159
|
+
self._runtime_register_path = runtime_register_path
|
|
160
|
+
self._runtime_invoke_path = runtime_invoke_path
|
|
161
|
+
self._runtime_events_path = runtime_events_path
|
|
162
|
+
self._runtime_activate_path = runtime_activate_path
|
|
163
|
+
self._runtime_deactivate_path = runtime_deactivate_path
|
|
164
|
+
self._runtime_validate_path = runtime_validate_path
|
|
165
|
+
self._chat_messages_path = chat_messages_path
|
|
166
|
+
self._queue_contracts_path = queue_contracts_path
|
|
167
|
+
self._chat_sessions_path = chat_sessions_path
|
|
168
|
+
self._chat_history_path = chat_history_path
|
|
169
|
+
self._client = httpx.Client(timeout=timeout_seconds)
|
|
170
|
+
|
|
171
|
+
def close(self) -> None:
|
|
172
|
+
self._client.close()
|
|
173
|
+
|
|
174
|
+
def create_session(self, email: str, password: str) -> dict[str, Any]:
|
|
175
|
+
payload = {"email": email, "password": password}
|
|
176
|
+
return self._post(self._auth_sessions_path, payload, auth_token="")
|
|
177
|
+
|
|
178
|
+
def delete_current_session(self, auth_token: str | None = None) -> None:
|
|
179
|
+
self._delete(self._auth_current_session_path, auth_token=auth_token)
|
|
180
|
+
|
|
181
|
+
def get_me(self, auth_token: str | None = None) -> dict[str, Any]:
|
|
182
|
+
return self._get(self._auth_me_path, auth_token=auth_token)
|
|
183
|
+
|
|
184
|
+
def register_runtime(
|
|
185
|
+
self, registration: Any, auth_token: str | None = None
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
return self._post(
|
|
188
|
+
self._runtime_register_path, registration, auth_token=auth_token
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def list_runtime_registrations(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
agent_id: str,
|
|
195
|
+
agent_version: str,
|
|
196
|
+
auth_token: str | None = None,
|
|
197
|
+
) -> list[dict[str, Any]]:
|
|
198
|
+
path = self._path_with_query(
|
|
199
|
+
self._runtime_register_path,
|
|
200
|
+
{
|
|
201
|
+
"agent_id": agent_id,
|
|
202
|
+
"agent_version": agent_version,
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
response = self._get(path, auth_token=auth_token)
|
|
206
|
+
registrations = response.get("registrations")
|
|
207
|
+
if isinstance(registrations, list):
|
|
208
|
+
return [item for item in registrations if isinstance(item, dict)]
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
def activate_runtime_registration(
|
|
212
|
+
self, registration_id: str, auth_token: str | None = None
|
|
213
|
+
) -> None:
|
|
214
|
+
self._post(
|
|
215
|
+
self._runtime_registration_action_path(
|
|
216
|
+
self._runtime_activate_path, registration_id
|
|
217
|
+
),
|
|
218
|
+
{},
|
|
219
|
+
auth_token=auth_token,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def deactivate_runtime_registration(
|
|
223
|
+
self, registration_id: str, auth_token: str | None = None
|
|
224
|
+
) -> None:
|
|
225
|
+
self._post(
|
|
226
|
+
self._runtime_registration_action_path(
|
|
227
|
+
self._runtime_deactivate_path, registration_id
|
|
228
|
+
),
|
|
229
|
+
{},
|
|
230
|
+
auth_token=auth_token,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def validate_runtime_registration(
|
|
234
|
+
self, registration_id: str, auth_token: str | None = None
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
return self._post(
|
|
237
|
+
self._runtime_registration_action_path(
|
|
238
|
+
self._runtime_validate_path, registration_id
|
|
239
|
+
),
|
|
240
|
+
{},
|
|
241
|
+
auth_token=auth_token,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def ensure_runtime_registration_active(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
registration: Any,
|
|
248
|
+
auth_token: str | None = None,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
payload = _normalize_payload(registration)
|
|
251
|
+
requested_agent_id = str(payload.get("agent_id", "")).strip()
|
|
252
|
+
requested_agent_version = str(payload.get("agent_version", "")).strip()
|
|
253
|
+
if requested_agent_id == "" or requested_agent_version == "":
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"simpleflow sdk payload error: registration agent_id and agent_version are required"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
existing = self.list_runtime_registrations(
|
|
259
|
+
agent_id=requested_agent_id,
|
|
260
|
+
agent_version=requested_agent_version,
|
|
261
|
+
auth_token=auth_token,
|
|
262
|
+
)
|
|
263
|
+
for item in existing:
|
|
264
|
+
status = str(item.get("status", "")).strip().lower()
|
|
265
|
+
if status == "active":
|
|
266
|
+
return {
|
|
267
|
+
"status": "active",
|
|
268
|
+
"registration": item,
|
|
269
|
+
"registration_id": str(
|
|
270
|
+
item.get("id", item.get("registration_id", ""))
|
|
271
|
+
).strip(),
|
|
272
|
+
"created": False,
|
|
273
|
+
"validated": False,
|
|
274
|
+
"activated": False,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
target = existing[0] if len(existing) > 0 else None
|
|
278
|
+
created = False
|
|
279
|
+
registration_id = ""
|
|
280
|
+
if target is None:
|
|
281
|
+
target = self.register_runtime(payload, auth_token=auth_token)
|
|
282
|
+
created = True
|
|
283
|
+
|
|
284
|
+
registration_id = str(
|
|
285
|
+
target.get("id", target.get("registration_id", ""))
|
|
286
|
+
).strip()
|
|
287
|
+
if registration_id == "":
|
|
288
|
+
raise SimpleFlowLifecycleError(
|
|
289
|
+
status_code=502,
|
|
290
|
+
detail="registration response did not include registration id",
|
|
291
|
+
path=self._runtime_register_path,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
validation = self.validate_runtime_registration(
|
|
295
|
+
registration_id, auth_token=auth_token
|
|
296
|
+
)
|
|
297
|
+
self.activate_runtime_registration(registration_id, auth_token=auth_token)
|
|
298
|
+
return {
|
|
299
|
+
"status": "active",
|
|
300
|
+
"registration": target,
|
|
301
|
+
"registration_id": registration_id,
|
|
302
|
+
"validation": validation,
|
|
303
|
+
"created": created,
|
|
304
|
+
"validated": True,
|
|
305
|
+
"activated": True,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def invoke(self, request: Any, auth_token: str | None = None) -> dict[str, Any]:
|
|
309
|
+
response = self._post(self._runtime_invoke_path, request, auth_token=auth_token)
|
|
310
|
+
return response
|
|
311
|
+
|
|
312
|
+
def write_event(self, event: Any) -> None:
|
|
313
|
+
body = _normalize_payload(event)
|
|
314
|
+
event_type = str(body.get("event_type", "")).strip()
|
|
315
|
+
if event_type == "":
|
|
316
|
+
event_type = str(body.get("type", "")).strip()
|
|
317
|
+
body["event_type"] = event_type
|
|
318
|
+
body.pop("type", None)
|
|
319
|
+
idempotency_key = str(body.pop("idempotency_key", "")).strip()
|
|
320
|
+
allowed_keys = {
|
|
321
|
+
"agent_id",
|
|
322
|
+
"organization_id",
|
|
323
|
+
"run_id",
|
|
324
|
+
"event_type",
|
|
325
|
+
"trace_id",
|
|
326
|
+
"conversation_id",
|
|
327
|
+
"request_id",
|
|
328
|
+
"payload",
|
|
329
|
+
}
|
|
330
|
+
body = {key: value for key, value in body.items() if key in allowed_keys}
|
|
331
|
+
headers: dict[str, str] = {}
|
|
332
|
+
if idempotency_key != "":
|
|
333
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
334
|
+
self._post(self._runtime_events_path, body, extra_headers=headers)
|
|
335
|
+
|
|
336
|
+
def report_runtime_event(self, event: Any) -> None:
|
|
337
|
+
self.write_event(event)
|
|
338
|
+
|
|
339
|
+
def write_chat_message(self, message: Any) -> None:
|
|
340
|
+
body = _normalize_payload(message)
|
|
341
|
+
idempotency_key = str(body.pop("idempotency_key", "")).strip()
|
|
342
|
+
body.pop("created_at_ms", None)
|
|
343
|
+
allowed_keys = {
|
|
344
|
+
"agent_id",
|
|
345
|
+
"organization_id",
|
|
346
|
+
"run_id",
|
|
347
|
+
"chat_id",
|
|
348
|
+
"message_id",
|
|
349
|
+
"role",
|
|
350
|
+
"direction",
|
|
351
|
+
"content",
|
|
352
|
+
"metadata",
|
|
353
|
+
}
|
|
354
|
+
body = {key: value for key, value in body.items() if key in allowed_keys}
|
|
355
|
+
direction = str(body.get("direction", "")).strip()
|
|
356
|
+
if direction == "":
|
|
357
|
+
body["direction"] = "outbound"
|
|
358
|
+
if body.get("content") is None:
|
|
359
|
+
body["content"] = {}
|
|
360
|
+
if body.get("metadata") is None:
|
|
361
|
+
body["metadata"] = {}
|
|
362
|
+
headers: dict[str, str] = {}
|
|
363
|
+
if idempotency_key != "":
|
|
364
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
365
|
+
self._post(self._chat_messages_path, body, extra_headers=headers)
|
|
366
|
+
|
|
367
|
+
def publish_queue_contract(self, contract: Any) -> None:
|
|
368
|
+
body = _normalize_payload(contract)
|
|
369
|
+
if str(body.get("contract_name", "")).strip() == "":
|
|
370
|
+
body["contract_name"] = (
|
|
371
|
+
str(body.get("message_id", "")).strip() or "runtime.queue.contract"
|
|
372
|
+
)
|
|
373
|
+
if str(body.get("contract_version", "")).strip() == "":
|
|
374
|
+
body["contract_version"] = "v1"
|
|
375
|
+
if str(body.get("status", "")).strip() == "":
|
|
376
|
+
body["status"] = "draft"
|
|
377
|
+
if body.get("schema") is None:
|
|
378
|
+
body["schema"] = body.get("payload") or {}
|
|
379
|
+
if body.get("transport") is None:
|
|
380
|
+
body["transport"] = {}
|
|
381
|
+
idempotency_key = str(body.pop("idempotency_key", "")).strip()
|
|
382
|
+
headers: dict[str, str] = {}
|
|
383
|
+
if idempotency_key != "":
|
|
384
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
385
|
+
self._post(self._queue_contracts_path, body, extra_headers=headers)
|
|
386
|
+
|
|
387
|
+
def list_chat_history_messages(
|
|
388
|
+
self,
|
|
389
|
+
*,
|
|
390
|
+
agent_id: str,
|
|
391
|
+
chat_id: str,
|
|
392
|
+
user_id: str,
|
|
393
|
+
limit: int = 50,
|
|
394
|
+
auth_token: str | None = None,
|
|
395
|
+
) -> list[dict[str, Any]]:
|
|
396
|
+
path = self._path_with_query(
|
|
397
|
+
self._chat_history_path,
|
|
398
|
+
{
|
|
399
|
+
"agent_id": agent_id,
|
|
400
|
+
"chat_id": chat_id,
|
|
401
|
+
"user_id": user_id,
|
|
402
|
+
"limit": limit,
|
|
403
|
+
},
|
|
404
|
+
)
|
|
405
|
+
response = self._get(path, auth_token=auth_token)
|
|
406
|
+
messages = response.get("messages")
|
|
407
|
+
if isinstance(messages, list):
|
|
408
|
+
return [item for item in messages if isinstance(item, dict)]
|
|
409
|
+
return []
|
|
410
|
+
|
|
411
|
+
def list_chat_sessions(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
agent_id: str,
|
|
415
|
+
user_id: str,
|
|
416
|
+
status: str = "active",
|
|
417
|
+
limit: int = 50,
|
|
418
|
+
auth_token: str | None = None,
|
|
419
|
+
) -> list[dict[str, Any]]:
|
|
420
|
+
path = self._path_with_query(
|
|
421
|
+
self._chat_sessions_path,
|
|
422
|
+
{
|
|
423
|
+
"agent_id": agent_id,
|
|
424
|
+
"user_id": user_id,
|
|
425
|
+
"status": status,
|
|
426
|
+
"limit": limit,
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
response = self._get(path, auth_token=auth_token)
|
|
430
|
+
sessions = response.get("sessions")
|
|
431
|
+
if isinstance(sessions, list):
|
|
432
|
+
return [item for item in sessions if isinstance(item, dict)]
|
|
433
|
+
return []
|
|
434
|
+
|
|
435
|
+
def create_chat_history_message(
|
|
436
|
+
self, message: Any, auth_token: str | None = None
|
|
437
|
+
) -> dict[str, Any]:
|
|
438
|
+
return self._post(self._chat_history_path, message, auth_token=auth_token)
|
|
439
|
+
|
|
440
|
+
def update_chat_history_message(
|
|
441
|
+
self,
|
|
442
|
+
*,
|
|
443
|
+
message_id: str,
|
|
444
|
+
agent_id: str,
|
|
445
|
+
chat_id: str,
|
|
446
|
+
user_id: str,
|
|
447
|
+
content: Any,
|
|
448
|
+
metadata: Any,
|
|
449
|
+
auth_token: str | None = None,
|
|
450
|
+
) -> dict[str, Any]:
|
|
451
|
+
payload = {
|
|
452
|
+
"agent_id": agent_id,
|
|
453
|
+
"chat_id": chat_id,
|
|
454
|
+
"user_id": user_id,
|
|
455
|
+
"content": _normalize_payload(content),
|
|
456
|
+
"metadata": _normalize_payload(metadata),
|
|
457
|
+
}
|
|
458
|
+
return self._patch(
|
|
459
|
+
f"{self._chat_history_path}/{message_id}",
|
|
460
|
+
payload,
|
|
461
|
+
auth_token=auth_token,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def write_event_from_workflow_result(
|
|
465
|
+
self,
|
|
466
|
+
*,
|
|
467
|
+
agent_id: str,
|
|
468
|
+
workflow_result: Any,
|
|
469
|
+
event_type: str = "runtime.workflow.completed",
|
|
470
|
+
) -> None:
|
|
471
|
+
normalized_result = _normalize_payload(workflow_result)
|
|
472
|
+
metadata = normalized_result.get("metadata")
|
|
473
|
+
metadata_dict = metadata if isinstance(metadata, dict) else {}
|
|
474
|
+
telemetry = metadata_dict.get("telemetry")
|
|
475
|
+
telemetry_dict = telemetry if isinstance(telemetry, dict) else {}
|
|
476
|
+
trace = metadata_dict.get("trace")
|
|
477
|
+
trace_dict = trace if isinstance(trace, dict) else {}
|
|
478
|
+
tenant = trace_dict.get("tenant")
|
|
479
|
+
tenant_dict = tenant if isinstance(tenant, dict) else {}
|
|
480
|
+
|
|
481
|
+
conversation_id = str(tenant_dict.get("conversation_id", "")).strip()
|
|
482
|
+
if conversation_id == "":
|
|
483
|
+
conversation_id = str(trace_dict.get("conversation_id", "")).strip()
|
|
484
|
+
request_id = str(tenant_dict.get("request_id", "")).strip()
|
|
485
|
+
if request_id == "":
|
|
486
|
+
request_id = str(trace_dict.get("request_id", "")).strip()
|
|
487
|
+
run_id = str(tenant_dict.get("run_id", "")).strip()
|
|
488
|
+
if run_id == "":
|
|
489
|
+
run_id = str(normalized_result.get("run_id", "")).strip()
|
|
490
|
+
trace_id = str(telemetry_dict.get("trace_id", "")).strip()
|
|
491
|
+
sampled_value = telemetry_dict.get("sampled")
|
|
492
|
+
sampled = sampled_value if isinstance(sampled_value, bool) else None
|
|
493
|
+
|
|
494
|
+
self.write_event(
|
|
495
|
+
{
|
|
496
|
+
"event_type": event_type,
|
|
497
|
+
"agent_id": agent_id,
|
|
498
|
+
"run_id": run_id,
|
|
499
|
+
"conversation_id": conversation_id,
|
|
500
|
+
"request_id": request_id,
|
|
501
|
+
"trace_id": trace_id,
|
|
502
|
+
"sampled": sampled,
|
|
503
|
+
"payload": normalized_result,
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def write_chat_message_from_workflow_result(
|
|
508
|
+
self,
|
|
509
|
+
*,
|
|
510
|
+
agent_id: str,
|
|
511
|
+
organization_id: str,
|
|
512
|
+
run_id: str,
|
|
513
|
+
role: str,
|
|
514
|
+
workflow_result: Any,
|
|
515
|
+
trace_id: str = "",
|
|
516
|
+
span_id: str = "",
|
|
517
|
+
tenant_id: str = "",
|
|
518
|
+
trace_ui_base_url: str = "http://localhost:16686",
|
|
519
|
+
chat_id: str | None = None,
|
|
520
|
+
message_id: str | None = None,
|
|
521
|
+
direction: str | None = None,
|
|
522
|
+
created_at_ms: int | None = None,
|
|
523
|
+
idempotency_key: str | None = None,
|
|
524
|
+
) -> None:
|
|
525
|
+
normalized_result = _normalize_payload(workflow_result)
|
|
526
|
+
normalized_trace_id = trace_id.strip()
|
|
527
|
+
events = normalized_result.get("events")
|
|
528
|
+
event_counts = _count_workflow_events_by_type(normalized_result)
|
|
529
|
+
nerdstats = _extract_workflow_nerdstats(normalized_result)
|
|
530
|
+
|
|
531
|
+
content = {
|
|
532
|
+
"reply": normalized_result.get("terminal_output"),
|
|
533
|
+
"terminal_output": normalized_result.get("terminal_output"),
|
|
534
|
+
"workflow": {
|
|
535
|
+
"workflow_id": normalized_result.get("workflow_id"),
|
|
536
|
+
"terminal_node": normalized_result.get("terminal_node"),
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
metadata: dict[str, Any] = {
|
|
541
|
+
"source": "runtime.workflow.invoke",
|
|
542
|
+
"workflow_id": normalized_result.get("workflow_id"),
|
|
543
|
+
"terminal_node": normalized_result.get("terminal_node"),
|
|
544
|
+
"trace": normalized_result.get("trace", []),
|
|
545
|
+
"step_timings": normalized_result.get("step_timings", []),
|
|
546
|
+
"event_counts": event_counts,
|
|
547
|
+
"nerdstats": nerdstats,
|
|
548
|
+
"llm_node_metrics": normalized_result.get("llm_node_metrics", {}),
|
|
549
|
+
"total_elapsed_ms": normalized_result.get("total_elapsed_ms"),
|
|
550
|
+
"trace_context": {
|
|
551
|
+
"trace_id": normalized_trace_id,
|
|
552
|
+
"span_id": span_id.strip(),
|
|
553
|
+
"tenant_id": tenant_id.strip(),
|
|
554
|
+
"trace_url": _build_trace_url(
|
|
555
|
+
normalized_trace_id, trace_ui_base_url=trace_ui_base_url
|
|
556
|
+
),
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
if isinstance(events, list):
|
|
560
|
+
metadata["events"] = events
|
|
561
|
+
|
|
562
|
+
self.write_chat_message(
|
|
563
|
+
{
|
|
564
|
+
"agent_id": agent_id,
|
|
565
|
+
"organization_id": organization_id,
|
|
566
|
+
"run_id": run_id,
|
|
567
|
+
"role": role,
|
|
568
|
+
"chat_id": chat_id,
|
|
569
|
+
"message_id": message_id,
|
|
570
|
+
"direction": direction,
|
|
571
|
+
"content": content,
|
|
572
|
+
"metadata": metadata,
|
|
573
|
+
"idempotency_key": idempotency_key,
|
|
574
|
+
"created_at_ms": created_at_ms,
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
def with_telemetry(
|
|
579
|
+
self,
|
|
580
|
+
*,
|
|
581
|
+
mode: str = "simpleflow",
|
|
582
|
+
sample_rate: float | None = None,
|
|
583
|
+
otlp_sink: Any | None = None,
|
|
584
|
+
default_trace: dict[str, str] | None = None,
|
|
585
|
+
) -> TelemetryBridge:
|
|
586
|
+
return TelemetryBridge(
|
|
587
|
+
client=self,
|
|
588
|
+
mode=mode,
|
|
589
|
+
sample_rate=sample_rate,
|
|
590
|
+
otlp_sink=otlp_sink,
|
|
591
|
+
default_trace=default_trace,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def _post(
|
|
595
|
+
self,
|
|
596
|
+
path: str,
|
|
597
|
+
payload: Any,
|
|
598
|
+
extra_headers: dict[str, str] | None = None,
|
|
599
|
+
auth_token: str | None = None,
|
|
600
|
+
) -> dict[str, Any]:
|
|
601
|
+
normalized_path = path if path.startswith("/") else f"/{path}"
|
|
602
|
+
url = f"{self._base_url}{normalized_path}"
|
|
603
|
+
body = _normalize_payload(payload)
|
|
604
|
+
|
|
605
|
+
headers = {"Content-Type": "application/json"}
|
|
606
|
+
headers.update(self._authorization_headers(auth_token))
|
|
607
|
+
if extra_headers is not None:
|
|
608
|
+
headers.update(extra_headers)
|
|
609
|
+
|
|
610
|
+
response = self._client.post(url, json=body, headers=headers)
|
|
611
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
612
|
+
self._raise_request_error(
|
|
613
|
+
path=normalized_path,
|
|
614
|
+
status_code=response.status_code,
|
|
615
|
+
body=response.text,
|
|
616
|
+
)
|
|
617
|
+
if response.text.strip() == "":
|
|
618
|
+
return {}
|
|
619
|
+
try:
|
|
620
|
+
decoded = response.json()
|
|
621
|
+
except ValueError as exc:
|
|
622
|
+
raise RuntimeError(
|
|
623
|
+
"simpleflow sdk request error: expected JSON response body"
|
|
624
|
+
) from exc
|
|
625
|
+
if isinstance(decoded, dict):
|
|
626
|
+
return decoded
|
|
627
|
+
raise RuntimeError(
|
|
628
|
+
"simpleflow sdk request error: expected JSON object response body"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def _get(self, path: str, auth_token: str | None = None) -> dict[str, Any]:
|
|
632
|
+
normalized_path = path if path.startswith("/") else f"/{path}"
|
|
633
|
+
url = f"{self._base_url}{normalized_path}"
|
|
634
|
+
headers = self._authorization_headers(auth_token)
|
|
635
|
+
response = self._client.get(url, headers=headers)
|
|
636
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
637
|
+
self._raise_request_error(
|
|
638
|
+
path=normalized_path,
|
|
639
|
+
status_code=response.status_code,
|
|
640
|
+
body=response.text,
|
|
641
|
+
)
|
|
642
|
+
if response.text.strip() == "":
|
|
643
|
+
return {}
|
|
644
|
+
decoded = response.json()
|
|
645
|
+
if isinstance(decoded, dict):
|
|
646
|
+
return decoded
|
|
647
|
+
raise RuntimeError(
|
|
648
|
+
"simpleflow sdk request error: expected JSON object response body"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def _patch(
|
|
652
|
+
self, path: str, payload: Any, auth_token: str | None = None
|
|
653
|
+
) -> dict[str, Any]:
|
|
654
|
+
normalized_path = path if path.startswith("/") else f"/{path}"
|
|
655
|
+
url = f"{self._base_url}{normalized_path}"
|
|
656
|
+
body = _normalize_payload(payload)
|
|
657
|
+
headers = {"Content-Type": "application/json"}
|
|
658
|
+
headers.update(self._authorization_headers(auth_token))
|
|
659
|
+
response = self._client.patch(url, json=body, headers=headers)
|
|
660
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
661
|
+
self._raise_request_error(
|
|
662
|
+
path=normalized_path,
|
|
663
|
+
status_code=response.status_code,
|
|
664
|
+
body=response.text,
|
|
665
|
+
)
|
|
666
|
+
if response.text.strip() == "":
|
|
667
|
+
return {}
|
|
668
|
+
decoded = response.json()
|
|
669
|
+
if isinstance(decoded, dict):
|
|
670
|
+
return decoded
|
|
671
|
+
raise RuntimeError(
|
|
672
|
+
"simpleflow sdk request error: expected JSON object response body"
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _delete(self, path: str, auth_token: str | None = None) -> None:
|
|
676
|
+
normalized_path = path if path.startswith("/") else f"/{path}"
|
|
677
|
+
url = f"{self._base_url}{normalized_path}"
|
|
678
|
+
headers = self._authorization_headers(auth_token)
|
|
679
|
+
response = self._client.delete(url, headers=headers)
|
|
680
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
681
|
+
self._raise_request_error(
|
|
682
|
+
path=normalized_path,
|
|
683
|
+
status_code=response.status_code,
|
|
684
|
+
body=response.text,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def _authorization_headers(self, auth_token: str | None = None) -> dict[str, str]:
|
|
688
|
+
token = ""
|
|
689
|
+
if auth_token is None:
|
|
690
|
+
token = self._api_token
|
|
691
|
+
if (
|
|
692
|
+
token == ""
|
|
693
|
+
and self._oauth_client_id != ""
|
|
694
|
+
and self._oauth_client_secret != ""
|
|
695
|
+
):
|
|
696
|
+
token = self._ensure_oauth_access_token()
|
|
697
|
+
else:
|
|
698
|
+
token = auth_token.strip()
|
|
699
|
+
if token == "":
|
|
700
|
+
return {}
|
|
701
|
+
return {"Authorization": f"Bearer {token}"}
|
|
702
|
+
|
|
703
|
+
def _ensure_oauth_access_token(self) -> str:
|
|
704
|
+
now = time.time()
|
|
705
|
+
if (
|
|
706
|
+
self._oauth_access_token != ""
|
|
707
|
+
and now + float(self._oauth_token_leeway_seconds)
|
|
708
|
+
< self._oauth_access_token_expires_at_unix
|
|
709
|
+
):
|
|
710
|
+
return self._oauth_access_token
|
|
711
|
+
|
|
712
|
+
normalized_path = (
|
|
713
|
+
self._oauth_token_path
|
|
714
|
+
if self._oauth_token_path.startswith("/")
|
|
715
|
+
else f"/{self._oauth_token_path}"
|
|
716
|
+
)
|
|
717
|
+
url = f"{self._base_url}{normalized_path}"
|
|
718
|
+
response = self._client.post(
|
|
719
|
+
url,
|
|
720
|
+
json={
|
|
721
|
+
"grant_type": "client_credentials",
|
|
722
|
+
"client_id": self._oauth_client_id,
|
|
723
|
+
"client_secret": self._oauth_client_secret,
|
|
724
|
+
},
|
|
725
|
+
headers={"Content-Type": "application/json"},
|
|
726
|
+
)
|
|
727
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
728
|
+
self._raise_request_error(
|
|
729
|
+
path=normalized_path,
|
|
730
|
+
status_code=response.status_code,
|
|
731
|
+
body=response.text,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
payload = response.json()
|
|
735
|
+
if not isinstance(payload, dict):
|
|
736
|
+
raise RuntimeError(
|
|
737
|
+
"simpleflow sdk oauth error: expected JSON object token response"
|
|
738
|
+
)
|
|
739
|
+
access_token = str(payload.get("access_token", "")).strip()
|
|
740
|
+
if access_token == "":
|
|
741
|
+
raise RuntimeError(
|
|
742
|
+
"simpleflow sdk oauth error: token response missing access_token"
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
expires_in_value = payload.get("expires_in", 0)
|
|
746
|
+
expires_in = 0.0
|
|
747
|
+
try:
|
|
748
|
+
expires_in = float(expires_in_value)
|
|
749
|
+
except (TypeError, ValueError):
|
|
750
|
+
expires_in = 0.0
|
|
751
|
+
if not isfinite(expires_in) or expires_in <= 0.0:
|
|
752
|
+
expires_in = 60.0
|
|
753
|
+
|
|
754
|
+
self._oauth_access_token = access_token
|
|
755
|
+
self._oauth_access_token_expires_at_unix = now + expires_in
|
|
756
|
+
return self._oauth_access_token
|
|
757
|
+
|
|
758
|
+
def _runtime_registration_action_path(
|
|
759
|
+
self, path_template: str, registration_id: str
|
|
760
|
+
) -> str:
|
|
761
|
+
trimmed_id = registration_id.strip()
|
|
762
|
+
if trimmed_id == "":
|
|
763
|
+
raise ValueError(
|
|
764
|
+
"simpleflow sdk payload error: registration_id is required"
|
|
765
|
+
)
|
|
766
|
+
if "{registration_id}" in path_template:
|
|
767
|
+
return path_template.replace("{registration_id}", trimmed_id)
|
|
768
|
+
if path_template.endswith("/"):
|
|
769
|
+
return f"{path_template}{trimmed_id}"
|
|
770
|
+
return f"{path_template}/{trimmed_id}"
|
|
771
|
+
|
|
772
|
+
def _path_with_query(self, path: str, query: dict[str, Any]) -> str:
|
|
773
|
+
encoded = urlencode(query)
|
|
774
|
+
if encoded == "":
|
|
775
|
+
return path
|
|
776
|
+
return f"{path}?{encoded}"
|
|
777
|
+
|
|
778
|
+
def _raise_request_error(self, *, path: str, status_code: int, body: str) -> None:
|
|
779
|
+
detail = body.strip()
|
|
780
|
+
if detail == "":
|
|
781
|
+
detail = "request failed"
|
|
782
|
+
if status_code == 401:
|
|
783
|
+
raise SimpleFlowAuthenticationError(
|
|
784
|
+
status_code=status_code,
|
|
785
|
+
detail=detail,
|
|
786
|
+
path=path,
|
|
787
|
+
)
|
|
788
|
+
if status_code == 403:
|
|
789
|
+
raise SimpleFlowAuthorizationError(
|
|
790
|
+
status_code=status_code,
|
|
791
|
+
detail=detail,
|
|
792
|
+
path=path,
|
|
793
|
+
)
|
|
794
|
+
if self._is_lifecycle_path(path):
|
|
795
|
+
raise SimpleFlowLifecycleError(
|
|
796
|
+
status_code=status_code,
|
|
797
|
+
detail=detail,
|
|
798
|
+
path=path,
|
|
799
|
+
)
|
|
800
|
+
raise SimpleFlowRequestError(status_code=status_code, detail=detail, path=path)
|
|
801
|
+
|
|
802
|
+
def _is_lifecycle_path(self, path: str) -> bool:
|
|
803
|
+
normalized = path if path.startswith("/") else f"/{path}"
|
|
804
|
+
registration_root = self._runtime_register_path.rstrip("/")
|
|
805
|
+
return normalized.startswith(registration_root)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
class TelemetryBridge:
|
|
809
|
+
def __init__(
|
|
810
|
+
self,
|
|
811
|
+
*,
|
|
812
|
+
client: SimpleFlowClient,
|
|
813
|
+
mode: str,
|
|
814
|
+
sample_rate: float | None,
|
|
815
|
+
otlp_sink: Any | None,
|
|
816
|
+
default_trace: dict[str, str] | None,
|
|
817
|
+
) -> None:
|
|
818
|
+
normalized_mode = mode.strip().lower()
|
|
819
|
+
if normalized_mode not in {"simpleflow", "otlp"}:
|
|
820
|
+
raise ValueError(
|
|
821
|
+
"simpleflow sdk config error: telemetry mode must be one of simpleflow or otlp"
|
|
822
|
+
)
|
|
823
|
+
_validate_sample_rate(sample_rate)
|
|
824
|
+
self._client = client
|
|
825
|
+
self._mode = normalized_mode
|
|
826
|
+
self._sample_rate = sample_rate
|
|
827
|
+
self._otlp_sink = otlp_sink
|
|
828
|
+
self._default_trace = default_trace if default_trace is not None else {}
|
|
829
|
+
|
|
830
|
+
def emit_span(
|
|
831
|
+
self,
|
|
832
|
+
*,
|
|
833
|
+
span: Any,
|
|
834
|
+
agent_id: str = "",
|
|
835
|
+
run_id: str = "",
|
|
836
|
+
trace_id: str = "",
|
|
837
|
+
request_id: str = "",
|
|
838
|
+
conversation_id: str = "",
|
|
839
|
+
) -> None:
|
|
840
|
+
normalized_span = _normalize_payload(span)
|
|
841
|
+
if trace_id.strip() != "" and not _should_sample(trace_id, self._sample_rate):
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
if self._mode == "otlp":
|
|
845
|
+
if callable(self._otlp_sink):
|
|
846
|
+
self._otlp_sink(normalized_span)
|
|
847
|
+
return
|
|
848
|
+
|
|
849
|
+
fallback_agent_id = str(self._default_trace.get("agent_id", "")).strip()
|
|
850
|
+
fallback_run_id = str(self._default_trace.get("run_id", "")).strip()
|
|
851
|
+
fallback_request_id = str(self._default_trace.get("request_id", "")).strip()
|
|
852
|
+
fallback_conversation_id = str(
|
|
853
|
+
self._default_trace.get("conversation_id", "")
|
|
854
|
+
).strip()
|
|
855
|
+
|
|
856
|
+
self._client.write_event(
|
|
857
|
+
{
|
|
858
|
+
"type": "runtime.telemetry.span",
|
|
859
|
+
"agent_id": agent_id.strip()
|
|
860
|
+
if agent_id.strip() != ""
|
|
861
|
+
else fallback_agent_id,
|
|
862
|
+
"run_id": run_id.strip() if run_id.strip() != "" else fallback_run_id,
|
|
863
|
+
"request_id": request_id.strip()
|
|
864
|
+
if request_id.strip() != ""
|
|
865
|
+
else fallback_request_id,
|
|
866
|
+
"conversation_id": conversation_id.strip()
|
|
867
|
+
if conversation_id.strip() != ""
|
|
868
|
+
else fallback_conversation_id,
|
|
869
|
+
"trace_id": trace_id.strip(),
|
|
870
|
+
"sampled": True,
|
|
871
|
+
"payload": {"span": normalized_span},
|
|
872
|
+
}
|
|
873
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(slots=True)
|
|
6
|
+
class InvokeTrace:
|
|
7
|
+
trace_id: str
|
|
8
|
+
span_id: str
|
|
9
|
+
tenant_id: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class WorkflowTraceTenant:
|
|
14
|
+
conversation_id: str | None = None
|
|
15
|
+
request_id: str | None = None
|
|
16
|
+
run_id: str | None = None
|
|
17
|
+
agent_id: str | None = None
|
|
18
|
+
organization_id: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class RuntimeRegistration:
|
|
23
|
+
agent_id: str | None = None
|
|
24
|
+
agent_version: str | None = None
|
|
25
|
+
execution_mode: str | None = None
|
|
26
|
+
endpoint_url: str | None = None
|
|
27
|
+
auth_mode: str | None = None
|
|
28
|
+
capabilities: list[str] | None = None
|
|
29
|
+
metadata: dict[str, str] | None = None
|
|
30
|
+
runtime_id: str | None = None
|
|
31
|
+
runtime_version: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class RuntimeEvent:
|
|
36
|
+
agent_id: str
|
|
37
|
+
event_type: str | None = None
|
|
38
|
+
type: str | None = None
|
|
39
|
+
agent_version: str | None = None
|
|
40
|
+
run_id: str | None = None
|
|
41
|
+
conversation_id: str | None = None
|
|
42
|
+
request_id: str | None = None
|
|
43
|
+
trace_id: str | None = None
|
|
44
|
+
sampled: bool | None = None
|
|
45
|
+
organization_id: str | None = None
|
|
46
|
+
user_id: str | None = None
|
|
47
|
+
timestamp_ms: int | None = None
|
|
48
|
+
idempotency_key: str | None = None
|
|
49
|
+
payload: dict[str, Any] | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class TelemetrySpan:
|
|
54
|
+
name: str
|
|
55
|
+
start_time_ms: int
|
|
56
|
+
end_time_ms: int
|
|
57
|
+
kind: str | None = None
|
|
58
|
+
attributes: dict[str, Any] | None = None
|
|
59
|
+
status: str | None = None
|
|
60
|
+
status_detail: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(slots=True)
|
|
64
|
+
class ChatMessageWrite:
|
|
65
|
+
agent_id: str
|
|
66
|
+
organization_id: str
|
|
67
|
+
run_id: str
|
|
68
|
+
role: str
|
|
69
|
+
chat_id: str | None = None
|
|
70
|
+
message_id: str | None = None
|
|
71
|
+
direction: str | None = None
|
|
72
|
+
content: Any = None
|
|
73
|
+
metadata: dict[str, Any] | None = None
|
|
74
|
+
idempotency_key: str | None = None
|
|
75
|
+
created_at_ms: int | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(slots=True)
|
|
79
|
+
class QueueContract:
|
|
80
|
+
queue_name: str
|
|
81
|
+
message_id: str
|
|
82
|
+
idempotency_key: str
|
|
83
|
+
retry_attempt: int
|
|
84
|
+
max_retry_attempt: int
|
|
85
|
+
agent_id: str | None = None
|
|
86
|
+
organization_id: str | None = None
|
|
87
|
+
run_id: str | None = None
|
|
88
|
+
contract_name: str | None = None
|
|
89
|
+
contract_version: str | None = None
|
|
90
|
+
schema: dict[str, Any] | None = None
|
|
91
|
+
transport: dict[str, Any] | None = None
|
|
92
|
+
status: str | None = None
|
|
93
|
+
payload: dict[str, Any] | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(slots=True)
|
|
97
|
+
class ChatHistoryMessage:
|
|
98
|
+
agent_id: str
|
|
99
|
+
chat_id: str
|
|
100
|
+
message_id: str
|
|
101
|
+
user_id: str
|
|
102
|
+
role: str | None = None
|
|
103
|
+
content: dict[str, Any] | None = None
|
|
104
|
+
metadata: dict[str, Any] | None = None
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
simpleflow_sdk/__init__.py,sha256=aB9VSQemN76Jgja2EILErL0cpcJ5mf7ynU0wCdZo_zE,837
|
|
2
|
+
simpleflow_sdk/auth.py,sha256=CUcwL7zwe4OK-7lBN5bLt0--OqNDw8LzYrQeSbNQrbk,2656
|
|
3
|
+
simpleflow_sdk/client.py,sha256=SCIKI43fEQ2AzSVdqBb86hz-aVcE5RAyQLe5Pwci_Qc,31919
|
|
4
|
+
simpleflow_sdk/contracts.py,sha256=xGOc-uaFKuE5WMMwryBCEM-i2BgTmPHNkTeeNL_lqgQ,2622
|
|
5
|
+
simpleflow_sdk-0.1.0.dist-info/METADATA,sha256=ZbL9e6ddEk4z0gjKGXTQqnzrLfXYfGcYi1b-ZgoyPsQ,204
|
|
6
|
+
simpleflow_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
simpleflow_sdk-0.1.0.dist-info/top_level.txt,sha256=fWOaP2mG9_aU-fCd1OdZ-qnBJbsC_JC6bVh1gQSVMhA,15
|
|
8
|
+
simpleflow_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simpleflow_sdk
|