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/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
- IdentifyResult,
18
- ResetResult,
19
- WalletAddressResult,
20
- )
21
-
22
-
23
- class TrodoClient:
24
- def __init__(
25
- self,
26
- site_id: str,
27
- api_base: str = "https://sdkapi.trodo.ai",
28
- timeout: int = 10,
29
- retries: int = 2,
30
- batch_enabled: bool = False,
31
- batch_size: int = 50,
32
- batch_flush_interval: float = 5.0,
33
- auto_events: bool = False,
34
- on_error: Optional[Any] = None,
35
- debug: bool = False,
36
- ) -> None:
37
- if not site_id:
38
- raise ValueError("trodo-python: site_id is required")
39
-
40
- self.site_id = site_id
41
-
42
- self._http = HttpClient(
43
- api_base=api_base,
44
- site_id=site_id,
45
- timeout=timeout,
46
- retries=retries,
47
- on_error=on_error,
48
- debug=debug,
49
- )
50
-
51
- self._session_manager = SessionManager()
52
-
53
- if batch_enabled:
54
- self._event_queue: Optional[EventQueue] = EventQueue(batch_size)
55
- self._batch_flusher: Optional[BatchFlusher] = BatchFlusher(
56
- self._event_queue, self._http, batch_flush_interval
57
- )
58
- self._batch_flusher.start()
59
- else:
60
- self._event_queue = None
61
- self._batch_flusher = None
62
-
63
- self._auto_event_manager = AutoEventManager(
64
- site_id, self._http, self._session_manager
65
- )
66
-
67
- if auto_events:
68
- self._auto_event_manager.enable()
69
-
70
- self._user_context_cache: Dict[str, UserContext] = {}
71
-
72
- # --------------------------------------------------------------------------
73
- # Primary pattern: for_user()
74
- # --------------------------------------------------------------------------
75
-
76
- def for_user(
77
- self,
78
- distinct_id: str,
79
- session_id: Optional[str] = None,
80
- ) -> UserContext:
81
- if distinct_id in self._user_context_cache:
82
- return self._user_context_cache[distinct_id]
83
-
84
- ctx = UserContext(
85
- distinct_id=distinct_id,
86
- site_id=self.site_id,
87
- http_client=self._http,
88
- session_manager=self._session_manager,
89
- event_queue=self._event_queue,
90
- batch_flusher=self._batch_flusher,
91
- auto_event_manager=self._auto_event_manager,
92
- session_id=session_id,
93
- )
94
- self._user_context_cache[distinct_id] = ctx
95
- return ctx
96
-
97
- # --------------------------------------------------------------------------
98
- # Direct-call pattern (distinct_id as first arg)
99
- # --------------------------------------------------------------------------
100
-
101
- def track(
102
- self,
103
- distinct_id: str,
104
- event_name: str,
105
- properties: Optional[Dict[str, Any]] = None,
106
- category: str = "custom",
107
- ) -> None:
108
- self.for_user(distinct_id).track(event_name, properties, category)
109
-
110
- def identify(self, distinct_id: str, identify_id: str) -> IdentifyResult:
111
- return self.for_user(distinct_id).identify(identify_id)
112
-
113
- def wallet_address(self, distinct_id: str, wallet_addr: str) -> WalletAddressResult:
114
- return self.for_user(distinct_id).wallet_address(wallet_addr)
115
-
116
- def reset(self, distinct_id: str) -> ResetResult:
117
- self._user_context_cache.pop(distinct_id, None)
118
- return self.for_user(distinct_id).reset()
119
-
120
- # People (direct)
121
- def people_set(self, distinct_id: str, properties: Dict[str, Any]) -> ApiResult:
122
- return self.for_user(distinct_id).people.set(properties)
123
-
124
- def people_set_once(self, distinct_id: str, properties: Dict[str, Any]) -> ApiResult:
125
- return self.for_user(distinct_id).people.set_once(properties)
126
-
127
- def people_unset(self, distinct_id: str, keys: Union[str, List[str]]) -> ApiResult:
128
- return self.for_user(distinct_id).people.unset(keys)
129
-
130
- def people_increment(self, distinct_id: str, key: str, amount: float = 1) -> ApiResult:
131
- return self.for_user(distinct_id).people.increment(key, amount)
132
-
133
- def people_append(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
134
- return self.for_user(distinct_id).people.append(key, values)
135
-
136
- def people_union(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
137
- return self.for_user(distinct_id).people.union(key, values)
138
-
139
- def people_remove(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
140
- return self.for_user(distinct_id).people.remove(key, values)
141
-
142
- def people_track_charge(
143
- self,
144
- distinct_id: str,
145
- amount: float,
146
- properties: Optional[Dict[str, Any]] = None,
147
- ) -> ApiResult:
148
- return self.for_user(distinct_id).people.track_charge(amount, properties)
149
-
150
- def people_clear_charges(self, distinct_id: str) -> ApiResult:
151
- return self.for_user(distinct_id).people.clear_charges()
152
-
153
- def people_delete_user(self, distinct_id: str) -> ApiResult:
154
- return self.for_user(distinct_id).people.delete_user()
155
-
156
- # Groups (direct)
157
- def set_group(
158
- self, distinct_id: str, group_key: str, group_id: Union[str, List[str]]
159
- ) -> ApiResult:
160
- return self.for_user(distinct_id).set_group(group_key, group_id)
161
-
162
- def add_group(self, distinct_id: str, group_key: str, group_id: str) -> ApiResult:
163
- return self.for_user(distinct_id).add_group(group_key, group_id)
164
-
165
- def remove_group(self, distinct_id: str, group_key: str, group_id: str) -> ApiResult:
166
- return self.for_user(distinct_id).remove_group(group_key, group_id)
167
-
168
- def get_group(
169
- self, distinct_id: str, group_key: str, group_id: str
170
- ) -> GroupProfile:
171
- return self.for_user(distinct_id).get_group(group_key, group_id)
172
-
173
- # --------------------------------------------------------------------------
174
- # Auto events
175
- # --------------------------------------------------------------------------
176
-
177
- def enable_auto_events(self) -> None:
178
- self._auto_event_manager.enable()
179
-
180
- def disable_auto_events(self) -> None:
181
- self._auto_event_manager.disable()
182
-
183
- # --------------------------------------------------------------------------
184
- # Lifecycle
185
- # --------------------------------------------------------------------------
186
-
187
- def flush(self) -> None:
188
- if self._batch_flusher:
189
- self._batch_flusher.flush()
190
-
191
- def shutdown(self) -> None:
192
- self._auto_event_manager.disable()
193
- if self._batch_flusher:
194
- self._batch_flusher.stop()
195
- self._batch_flusher.flush()
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)