trodo-python 1.0.0__py3-none-any.whl → 1.2.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.
- trodo/__init__.py +177 -134
- trodo/api/async_client.py +96 -96
- trodo/api/endpoints.py +21 -20
- trodo/api/http_client.py +90 -87
- trodo/auto/auto_event_manager.py +134 -134
- trodo/client.py +318 -195
- trodo/managers/group_manager.py +106 -106
- trodo/managers/people_manager.py +77 -77
- trodo/queue/batch_flusher.py +52 -52
- trodo/queue/event_queue.py +32 -32
- trodo/session/server_session.py +74 -74
- trodo/session/session_manager.py +74 -74
- trodo/types.py +154 -79
- trodo/user_context.py +224 -224
- trodo_python-1.2.0.dist-info/METADATA +358 -0
- trodo_python-1.2.0.dist-info/RECORD +23 -0
- trodo_python-1.0.0.dist-info/METADATA +0 -227
- trodo_python-1.0.0.dist-info/RECORD +0 -23
- {trodo_python-1.0.0.dist-info → trodo_python-1.2.0.dist-info}/WHEEL +0 -0
- {trodo_python-1.0.0.dist-info → trodo_python-1.2.0.dist-info}/top_level.txt +0 -0
trodo/client.py
CHANGED
|
@@ -1,195 +1,318 @@
|
|
|
1
|
-
"""TrodoClient — main SDK class for Python."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any, Dict, List, Optional, Union
|
|
6
|
-
|
|
7
|
-
from .api.http_client import HttpClient
|
|
8
|
-
from .session.session_manager import SessionManager
|
|
9
|
-
from .managers.people_manager import PeopleManager
|
|
10
|
-
from .managers.group_manager import GroupManager, GroupProfile
|
|
11
|
-
from .queue.event_queue import EventQueue
|
|
12
|
-
from .queue.batch_flusher import BatchFlusher
|
|
13
|
-
from .auto.auto_event_manager import AutoEventManager
|
|
14
|
-
from .user_context import UserContext
|
|
15
|
-
from .types import (
|
|
16
|
-
ApiResult,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
self.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def
|
|
122
|
-
return self.for_user(distinct_id).
|
|
123
|
-
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
) -> ApiResult:
|
|
148
|
-
return self.for_user(distinct_id).people.
|
|
149
|
-
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
) ->
|
|
171
|
-
return self.for_user(distinct_id).
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# --------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
"""TrodoClient — main SDK class for Python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .api.http_client import HttpClient
|
|
8
|
+
from .session.session_manager import SessionManager
|
|
9
|
+
from .managers.people_manager import PeopleManager
|
|
10
|
+
from .managers.group_manager import GroupManager, GroupProfile
|
|
11
|
+
from .queue.event_queue import EventQueue
|
|
12
|
+
from .queue.batch_flusher import BatchFlusher
|
|
13
|
+
from .auto.auto_event_manager import AutoEventManager
|
|
14
|
+
from .user_context import UserContext
|
|
15
|
+
from .types import (
|
|
16
|
+
ApiResult,
|
|
17
|
+
ResetResult,
|
|
18
|
+
WalletAddressResult,
|
|
19
|
+
AgentCallProps,
|
|
20
|
+
ToolUseProps,
|
|
21
|
+
AgentResponseProps,
|
|
22
|
+
AgentErrorProps,
|
|
23
|
+
FeedbackProps,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TrodoClient:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
site_id: str,
|
|
31
|
+
api_base: str = "https://sdkapi.trodo.ai",
|
|
32
|
+
timeout: int = 10,
|
|
33
|
+
retries: int = 2,
|
|
34
|
+
batch_enabled: bool = False,
|
|
35
|
+
batch_size: int = 50,
|
|
36
|
+
batch_flush_interval: float = 5.0,
|
|
37
|
+
auto_events: bool = False,
|
|
38
|
+
on_error: Optional[Any] = None,
|
|
39
|
+
debug: bool = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
if not site_id:
|
|
42
|
+
raise ValueError("trodo-python: site_id is required")
|
|
43
|
+
|
|
44
|
+
self.site_id = site_id
|
|
45
|
+
|
|
46
|
+
self._http = HttpClient(
|
|
47
|
+
api_base=api_base,
|
|
48
|
+
site_id=site_id,
|
|
49
|
+
timeout=timeout,
|
|
50
|
+
retries=retries,
|
|
51
|
+
on_error=on_error,
|
|
52
|
+
debug=debug,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self._session_manager = SessionManager()
|
|
56
|
+
|
|
57
|
+
if batch_enabled:
|
|
58
|
+
self._event_queue: Optional[EventQueue] = EventQueue(batch_size)
|
|
59
|
+
self._batch_flusher: Optional[BatchFlusher] = BatchFlusher(
|
|
60
|
+
self._event_queue, self._http, batch_flush_interval
|
|
61
|
+
)
|
|
62
|
+
self._batch_flusher.start()
|
|
63
|
+
else:
|
|
64
|
+
self._event_queue = None
|
|
65
|
+
self._batch_flusher = None
|
|
66
|
+
|
|
67
|
+
self._auto_event_manager = AutoEventManager(
|
|
68
|
+
site_id, self._http, self._session_manager
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if auto_events:
|
|
72
|
+
self._auto_event_manager.enable()
|
|
73
|
+
|
|
74
|
+
self._user_context_cache: Dict[str, UserContext] = {}
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
# Primary pattern: for_user()
|
|
78
|
+
# --------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def for_user(
|
|
81
|
+
self,
|
|
82
|
+
distinct_id: str,
|
|
83
|
+
session_id: Optional[str] = None,
|
|
84
|
+
) -> UserContext:
|
|
85
|
+
if distinct_id in self._user_context_cache:
|
|
86
|
+
return self._user_context_cache[distinct_id]
|
|
87
|
+
|
|
88
|
+
ctx = UserContext(
|
|
89
|
+
distinct_id=distinct_id,
|
|
90
|
+
site_id=self.site_id,
|
|
91
|
+
http_client=self._http,
|
|
92
|
+
session_manager=self._session_manager,
|
|
93
|
+
event_queue=self._event_queue,
|
|
94
|
+
batch_flusher=self._batch_flusher,
|
|
95
|
+
auto_event_manager=self._auto_event_manager,
|
|
96
|
+
session_id=session_id,
|
|
97
|
+
)
|
|
98
|
+
self._user_context_cache[distinct_id] = ctx
|
|
99
|
+
return ctx
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------------------------------
|
|
102
|
+
# Direct-call pattern (distinct_id as first arg)
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def track(
|
|
106
|
+
self,
|
|
107
|
+
distinct_id: str,
|
|
108
|
+
event_name: str,
|
|
109
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
110
|
+
category: str = "custom",
|
|
111
|
+
) -> None:
|
|
112
|
+
self.for_user(distinct_id).track(event_name, properties, category)
|
|
113
|
+
|
|
114
|
+
def identify(self, identify_id: str, session_id: Optional[str] = None) -> "UserContext":
|
|
115
|
+
if identify_id in self._user_context_cache:
|
|
116
|
+
return self._user_context_cache[identify_id]
|
|
117
|
+
ctx = self.for_user(identify_id, session_id)
|
|
118
|
+
ctx.identify(identify_id)
|
|
119
|
+
return ctx
|
|
120
|
+
|
|
121
|
+
def wallet_address(self, distinct_id: str, wallet_addr: str) -> WalletAddressResult:
|
|
122
|
+
return self.for_user(distinct_id).wallet_address(wallet_addr)
|
|
123
|
+
|
|
124
|
+
def reset(self, distinct_id: str) -> ResetResult:
|
|
125
|
+
self._user_context_cache.pop(distinct_id, None)
|
|
126
|
+
return self.for_user(distinct_id).reset()
|
|
127
|
+
|
|
128
|
+
# People (direct)
|
|
129
|
+
def people_set(self, distinct_id: str, properties: Dict[str, Any]) -> ApiResult:
|
|
130
|
+
return self.for_user(distinct_id).people.set(properties)
|
|
131
|
+
|
|
132
|
+
def people_set_once(self, distinct_id: str, properties: Dict[str, Any]) -> ApiResult:
|
|
133
|
+
return self.for_user(distinct_id).people.set_once(properties)
|
|
134
|
+
|
|
135
|
+
def people_unset(self, distinct_id: str, keys: Union[str, List[str]]) -> ApiResult:
|
|
136
|
+
return self.for_user(distinct_id).people.unset(keys)
|
|
137
|
+
|
|
138
|
+
def people_increment(self, distinct_id: str, key: str, amount: float = 1) -> ApiResult:
|
|
139
|
+
return self.for_user(distinct_id).people.increment(key, amount)
|
|
140
|
+
|
|
141
|
+
def people_append(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
|
|
142
|
+
return self.for_user(distinct_id).people.append(key, values)
|
|
143
|
+
|
|
144
|
+
def people_union(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
|
|
145
|
+
return self.for_user(distinct_id).people.union(key, values)
|
|
146
|
+
|
|
147
|
+
def people_remove(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
|
|
148
|
+
return self.for_user(distinct_id).people.remove(key, values)
|
|
149
|
+
|
|
150
|
+
def people_track_charge(
|
|
151
|
+
self,
|
|
152
|
+
distinct_id: str,
|
|
153
|
+
amount: float,
|
|
154
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
155
|
+
) -> ApiResult:
|
|
156
|
+
return self.for_user(distinct_id).people.track_charge(amount, properties)
|
|
157
|
+
|
|
158
|
+
def people_clear_charges(self, distinct_id: str) -> ApiResult:
|
|
159
|
+
return self.for_user(distinct_id).people.clear_charges()
|
|
160
|
+
|
|
161
|
+
def people_delete_user(self, distinct_id: str) -> ApiResult:
|
|
162
|
+
return self.for_user(distinct_id).people.delete_user()
|
|
163
|
+
|
|
164
|
+
# Groups (direct)
|
|
165
|
+
def set_group(
|
|
166
|
+
self, distinct_id: str, group_key: str, group_id: Union[str, List[str]]
|
|
167
|
+
) -> ApiResult:
|
|
168
|
+
return self.for_user(distinct_id).set_group(group_key, group_id)
|
|
169
|
+
|
|
170
|
+
def add_group(self, distinct_id: str, group_key: str, group_id: str) -> ApiResult:
|
|
171
|
+
return self.for_user(distinct_id).add_group(group_key, group_id)
|
|
172
|
+
|
|
173
|
+
def remove_group(self, distinct_id: str, group_key: str, group_id: str) -> ApiResult:
|
|
174
|
+
return self.for_user(distinct_id).remove_group(group_key, group_id)
|
|
175
|
+
|
|
176
|
+
def get_group(
|
|
177
|
+
self, distinct_id: str, group_key: str, group_id: str
|
|
178
|
+
) -> GroupProfile:
|
|
179
|
+
return self.for_user(distinct_id).get_group(group_key, group_id)
|
|
180
|
+
|
|
181
|
+
# --------------------------------------------------------------------------
|
|
182
|
+
# Auto events
|
|
183
|
+
# --------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def enable_auto_events(self) -> None:
|
|
186
|
+
self._auto_event_manager.enable()
|
|
187
|
+
|
|
188
|
+
def disable_auto_events(self) -> None:
|
|
189
|
+
self._auto_event_manager.disable()
|
|
190
|
+
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
# Lifecycle
|
|
193
|
+
# --------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def flush(self) -> None:
|
|
196
|
+
if self._batch_flusher:
|
|
197
|
+
self._batch_flusher.flush()
|
|
198
|
+
|
|
199
|
+
def shutdown(self) -> None:
|
|
200
|
+
self._auto_event_manager.disable()
|
|
201
|
+
if self._batch_flusher:
|
|
202
|
+
self._batch_flusher.stop()
|
|
203
|
+
self._batch_flusher.flush()
|
|
204
|
+
|
|
205
|
+
# --------------------------------------------------------------------------
|
|
206
|
+
# Agent Analytics
|
|
207
|
+
# --------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def _build_agent_base(
|
|
210
|
+
self,
|
|
211
|
+
agent_id: str,
|
|
212
|
+
conversation_id: str,
|
|
213
|
+
message_id: str,
|
|
214
|
+
event_type: str,
|
|
215
|
+
distinct_id: Optional[str] = None,
|
|
216
|
+
timestamp: Optional[str] = None,
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
payload: Dict[str, Any] = {
|
|
219
|
+
"event_type": event_type,
|
|
220
|
+
"agent_id": agent_id,
|
|
221
|
+
"conversation_id": conversation_id,
|
|
222
|
+
"message_id": message_id,
|
|
223
|
+
}
|
|
224
|
+
if distinct_id is not None:
|
|
225
|
+
payload["distinct_id"] = distinct_id
|
|
226
|
+
if timestamp is not None:
|
|
227
|
+
payload["timestamp"] = timestamp
|
|
228
|
+
return payload
|
|
229
|
+
|
|
230
|
+
def track_agent_call(self, props: AgentCallProps) -> None:
|
|
231
|
+
"""Track an LLM invocation / inbound message."""
|
|
232
|
+
payload = self._build_agent_base(
|
|
233
|
+
props.agent_id, props.conversation_id, props.message_id,
|
|
234
|
+
"agent_call", props.distinct_id, props.timestamp,
|
|
235
|
+
)
|
|
236
|
+
if props.prompt is not None:
|
|
237
|
+
payload["prompt"] = props.prompt
|
|
238
|
+
if props.model is not None:
|
|
239
|
+
payload["model"] = props.model
|
|
240
|
+
if props.temperature is not None:
|
|
241
|
+
payload["temperature"] = props.temperature
|
|
242
|
+
if props.system_prompt_version is not None:
|
|
243
|
+
payload["system_prompt_version"] = props.system_prompt_version
|
|
244
|
+
if props.provider is not None:
|
|
245
|
+
payload["provider"] = props.provider
|
|
246
|
+
if props.properties:
|
|
247
|
+
payload.update(props.properties)
|
|
248
|
+
self._http.post_agent_event(payload)
|
|
249
|
+
|
|
250
|
+
def track_tool_use(self, props: ToolUseProps) -> None:
|
|
251
|
+
"""Track a tool invocation within an agent turn."""
|
|
252
|
+
payload = self._build_agent_base(
|
|
253
|
+
props.agent_id, props.conversation_id, props.message_id,
|
|
254
|
+
"tool_use", props.distinct_id, props.timestamp,
|
|
255
|
+
)
|
|
256
|
+
payload["tool_name"] = props.tool_name
|
|
257
|
+
if props.input is not None:
|
|
258
|
+
payload["input"] = props.input
|
|
259
|
+
if props.output is not None:
|
|
260
|
+
payload["output"] = props.output
|
|
261
|
+
if props.latency_ms is not None:
|
|
262
|
+
payload["latency_ms"] = props.latency_ms
|
|
263
|
+
if props.status is not None:
|
|
264
|
+
payload["status"] = props.status
|
|
265
|
+
if props.properties:
|
|
266
|
+
payload.update(props.properties)
|
|
267
|
+
self._http.post_agent_event(payload)
|
|
268
|
+
|
|
269
|
+
def track_agent_response(self, props: AgentResponseProps) -> None:
|
|
270
|
+
"""Track an LLM response / completion."""
|
|
271
|
+
payload = self._build_agent_base(
|
|
272
|
+
props.agent_id, props.conversation_id, props.message_id,
|
|
273
|
+
"agent_response", props.distinct_id, props.timestamp,
|
|
274
|
+
)
|
|
275
|
+
if props.output is not None:
|
|
276
|
+
payload["output"] = props.output
|
|
277
|
+
if props.model is not None:
|
|
278
|
+
payload["model"] = props.model
|
|
279
|
+
if props.completion_tokens is not None:
|
|
280
|
+
payload["completion_tokens"] = props.completion_tokens
|
|
281
|
+
if props.prompt_tokens is not None:
|
|
282
|
+
payload["prompt_tokens"] = props.prompt_tokens
|
|
283
|
+
if props.total_tokens is not None:
|
|
284
|
+
payload["total_tokens"] = props.total_tokens
|
|
285
|
+
if props.finish_reason is not None:
|
|
286
|
+
payload["finish_reason"] = props.finish_reason
|
|
287
|
+
if props.properties:
|
|
288
|
+
payload.update(props.properties)
|
|
289
|
+
self._http.post_agent_event(payload)
|
|
290
|
+
|
|
291
|
+
def track_agent_error(self, props: AgentErrorProps) -> None:
|
|
292
|
+
"""Track an error during an agent turn."""
|
|
293
|
+
payload = self._build_agent_base(
|
|
294
|
+
props.agent_id, props.conversation_id, props.message_id,
|
|
295
|
+
"agent_error", props.distinct_id, props.timestamp,
|
|
296
|
+
)
|
|
297
|
+
if props.error_type is not None:
|
|
298
|
+
payload["error_type"] = props.error_type
|
|
299
|
+
if props.error_message is not None:
|
|
300
|
+
payload["error_message"] = props.error_message
|
|
301
|
+
if props.failed_tool is not None:
|
|
302
|
+
payload["failed_tool"] = props.failed_tool
|
|
303
|
+
if props.traceback is not None:
|
|
304
|
+
payload["traceback"] = props.traceback
|
|
305
|
+
if props.properties:
|
|
306
|
+
payload.update(props.properties)
|
|
307
|
+
self._http.post_agent_event(payload)
|
|
308
|
+
|
|
309
|
+
def track_feedback(self, props: FeedbackProps) -> None:
|
|
310
|
+
"""Track a user feedback reaction on an agent response."""
|
|
311
|
+
payload = self._build_agent_base(
|
|
312
|
+
props.agent_id, props.conversation_id, props.message_id,
|
|
313
|
+
"feedback", props.distinct_id, props.timestamp,
|
|
314
|
+
)
|
|
315
|
+
payload["feedback"] = props.feedback
|
|
316
|
+
if props.properties:
|
|
317
|
+
payload.update(props.properties)
|
|
318
|
+
self._http.post_agent_event(payload)
|