conversimple-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.
@@ -0,0 +1,26 @@
1
+ """
2
+ Conversimple SDK - Python client library for the Conversational AI Platform.
3
+
4
+ This SDK enables customers to build and deploy AI agents that integrate with
5
+ the Conversimple platform's WebRTC infrastructure and conversation management.
6
+ """
7
+
8
+ from .agent import ConversimpleAgent
9
+ from .tools import tool, tool_async
10
+ from .callbacks import (
11
+ ConversationLifecycleEvent,
12
+ ToolCallEvent,
13
+ ErrorEvent,
14
+ ConfigUpdateEvent
15
+ )
16
+
17
+ __version__ = "0.1.0"
18
+ __all__ = [
19
+ "ConversimpleAgent",
20
+ "tool",
21
+ "tool_async",
22
+ "ConversationLifecycleEvent",
23
+ "ToolCallEvent",
24
+ "ErrorEvent",
25
+ "ConfigUpdateEvent"
26
+ ]
conversimple/agent.py ADDED
@@ -0,0 +1,354 @@
1
+ """
2
+ Main ConversimpleAgent class for customer workflow integration.
3
+
4
+ Provides WebSocket-based connection to the Conversimple platform with:
5
+ - Tool registration and execution
6
+ - Conversation lifecycle management
7
+ - Real-time event streaming
8
+ - Fault-tolerant connection handling
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import os
15
+ import uuid
16
+ from typing import Dict, List, Optional, Callable, Any, Union
17
+ from datetime import datetime
18
+ import datetime as dt
19
+
20
+ from .connection import WebSocketConnection
21
+ from .tools import ToolRegistry, ToolCall, auto_register_tools
22
+ from .callbacks import CallbackManager
23
+ from .utils import setup_logging
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ConversimpleAgent:
29
+ """
30
+ Main agent class for Conversimple platform integration.
31
+
32
+ Follows the agent session model - one instance per conversation.
33
+ Each agent instance handles a single conversation lifecycle.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str,
39
+ customer_id: Optional[str] = None,
40
+ platform_url: str = "ws://localhost:4000/sdk/websocket"
41
+ ):
42
+ """
43
+ Initialize Conversimple agent.
44
+
45
+ Args:
46
+ api_key: Customer authentication token
47
+ customer_id: Customer identifier (derived from API key if not provided)
48
+ platform_url: WebSocket URL for platform connection
49
+ """
50
+ self.api_key = api_key
51
+ self.customer_id = customer_id or self._derive_customer_id(api_key)
52
+ self.platform_url = platform_url
53
+
54
+ # Core components
55
+ self.connection = WebSocketConnection(
56
+ url=platform_url,
57
+ api_key=api_key,
58
+ customer_id=self.customer_id
59
+ )
60
+ self.tool_registry = ToolRegistry()
61
+ self.callback_manager = CallbackManager()
62
+
63
+ # State management
64
+ self.conversation_id: Optional[str] = None
65
+ self.connection_state = "disconnected"
66
+ self.registered_tools: List[Dict] = []
67
+ self.pending_tool_calls: Dict[str, ToolCall] = {}
68
+
69
+ # Event callbacks
70
+ self.callbacks: Dict[str, Callable] = {}
71
+
72
+ # Setup logging
73
+ setup_logging()
74
+
75
+ def _derive_customer_id(self, api_key: str) -> str:
76
+ """Derive customer ID from API key if not provided."""
77
+ # In production, this would decode/validate the API key
78
+ # For now, use a hash-based approach
79
+ import hashlib
80
+ return hashlib.md5(api_key.encode()).hexdigest()[:12]
81
+
82
+ async def start(self, conversation_id: Optional[str] = None) -> None:
83
+ """
84
+ Start the agent and connect to the platform.
85
+
86
+ Args:
87
+ conversation_id: Unique conversation identifier (generated if not provided)
88
+ """
89
+ self.conversation_id = conversation_id or f"conv_{uuid.uuid4().hex[:8]}"
90
+
91
+ logger.info(f"Starting agent for conversation: {self.conversation_id}")
92
+
93
+ # Set up message handlers
94
+ self.connection.set_message_handler(self._handle_platform_message)
95
+ self.connection.set_connection_handler(self._handle_connection_event)
96
+
97
+ # Connect to platform
98
+ await self.connection.connect()
99
+ self.connection_state = "connected"
100
+
101
+ # Auto-register tools from decorated methods
102
+ auto_register_tools(self)
103
+
104
+ # Store tools for later registration when real conversations start
105
+ # Don't register immediately since we don't have a real conversation ID yet
106
+ await self._register_tools()
107
+
108
+ logger.info(f"Agent started successfully for conversation: {self.conversation_id}")
109
+
110
+ async def stop(self) -> None:
111
+ """Stop the agent and disconnect from platform."""
112
+ logger.info(f"Stopping agent for conversation: {self.conversation_id}")
113
+
114
+ self.connection_state = "disconnecting"
115
+ await self.connection.disconnect()
116
+ self.connection_state = "disconnected"
117
+
118
+ # Trigger conversation ended callback
119
+ await self.callback_manager.trigger_conversation_ended(self.conversation_id)
120
+
121
+ async def _register_tools(self) -> None:
122
+ """Store discovered tools for registration when conversations start."""
123
+ tools = self.tool_registry.get_registered_tools()
124
+
125
+ if not tools:
126
+ logger.info("No tools discovered")
127
+ return
128
+
129
+ self.registered_tools = tools
130
+ logger.info(f"Discovered {len(tools)} tools, will register when conversations start")
131
+
132
+ async def _register_conversation_tools(self, conversation_id: str, tools: list) -> None:
133
+ """Register tools for a specific conversation."""
134
+ message = {
135
+ "conversation_id": conversation_id,
136
+ "tools": tools
137
+ }
138
+
139
+ await self.connection.send_message("register_conversation_tools", message)
140
+ logger.info(f"Registered {len(tools)} tools for conversation {conversation_id}")
141
+
142
+ async def _handle_platform_message(self, event: str, payload: Dict) -> None:
143
+ """Handle incoming messages from the platform."""
144
+ logger.info(f"📨 Received platform message: {event} - {payload}")
145
+
146
+ handlers = {
147
+ "config_update": self._handle_config_update,
148
+ "analytics_update": self._handle_analytics_update,
149
+ "tool_call_request": self._handle_tool_call_request,
150
+ "conversation_lifecycle": self._handle_conversation_lifecycle,
151
+ "hook_event": self._handle_hook_event,
152
+ "connection_warning": self._handle_connection_warning,
153
+ "error_notification": self._handle_error_notification,
154
+ "conversation_ready": self._handle_conversation_ready
155
+ }
156
+
157
+ handler = handlers.get(event)
158
+ if handler:
159
+ try:
160
+ await handler(payload)
161
+ except Exception as e:
162
+ logger.error(f"Error handling {event}: {e}")
163
+ await self._send_error_response(event, str(e))
164
+ else:
165
+ logger.warning(f"Unhandled platform message: {event}")
166
+
167
+ async def _handle_config_update(self, payload: Dict) -> None:
168
+ """Handle configuration updates from platform."""
169
+ logger.info(f"Configuration updated for customer: {payload.get('customer_id')}")
170
+ await self.callback_manager.trigger_config_update(payload)
171
+
172
+ async def _handle_analytics_update(self, payload: Dict) -> None:
173
+ """Handle analytics updates from platform."""
174
+ logger.debug(f"Analytics update received: {payload}")
175
+ # Analytics updates are typically fire-and-forget
176
+
177
+ async def _handle_conversation_ready(self, payload: Dict) -> None:
178
+ """Handle conversation ready events from platform."""
179
+ conversation_id = payload.get('conversation_id')
180
+ customer_id = payload.get('customer_id')
181
+
182
+ logger.info(f"🔧 AGENT_FLOW: Conversation ready event received for {conversation_id}")
183
+
184
+ if not conversation_id:
185
+ logger.error("🔧 AGENT_FLOW: No conversation_id in conversation_ready event")
186
+ return
187
+
188
+ if not self.registered_tools:
189
+ logger.warning("🔧 AGENT_FLOW: No tools available to register for conversation")
190
+ return
191
+
192
+ # Register tools for this specific conversation
193
+ logger.info(f"🔧 AGENT_FLOW: Registering {len(self.registered_tools)} tools for conversation {conversation_id}")
194
+ await self._register_conversation_tools(conversation_id, self.registered_tools)
195
+
196
+ # Trigger conversation started callback
197
+ await self.callback_manager.trigger_conversation_started(conversation_id)
198
+
199
+ async def _handle_tool_call_request(self, payload: Dict) -> None:
200
+ """Handle tool execution requests from platform."""
201
+ call_id = payload.get("call_id")
202
+ tool_name = payload.get("tool_name")
203
+ arguments = payload.get("arguments", {})
204
+
205
+ logger.info(f"Tool call requested: {tool_name} (call_id: {call_id})")
206
+
207
+ if not call_id or not tool_name:
208
+ await self._send_tool_error(call_id or "unknown", "Missing call_id or tool_name")
209
+ return
210
+
211
+ # Create tool call object
212
+ tool_call = ToolCall(
213
+ call_id=call_id,
214
+ tool_name=tool_name,
215
+ arguments=arguments,
216
+ conversation_id=self.conversation_id,
217
+ timeout_seconds=payload.get("timeout_seconds", 30)
218
+ )
219
+
220
+ # Store as pending
221
+ self.pending_tool_calls[call_id] = tool_call
222
+
223
+ # Trigger callback
224
+ await self.callback_manager.trigger_tool_called(tool_call)
225
+
226
+ try:
227
+ # Execute the tool
228
+ result = await self.tool_registry.execute_tool(tool_name, arguments)
229
+
230
+ # Send successful result
231
+ await self._send_tool_result(call_id, result)
232
+
233
+ # Trigger completion callback
234
+ await self.callback_manager.trigger_tool_completed(call_id, result)
235
+
236
+ except Exception as e:
237
+ logger.error(f"Tool execution failed for {tool_name}: {e}")
238
+ await self._send_tool_error(call_id, str(e))
239
+ finally:
240
+ # Clean up pending call
241
+ self.pending_tool_calls.pop(call_id, None)
242
+
243
+ async def _handle_conversation_lifecycle(self, payload: Dict) -> None:
244
+ """Handle conversation lifecycle events."""
245
+ event = payload.get("event")
246
+ conversation_id = payload.get("conversation_id")
247
+
248
+ logger.info(f"🎭 Conversation lifecycle: {event} for {conversation_id}")
249
+ logger.info(f"🎭 Full payload: {payload}")
250
+
251
+ if event == "conversation_started":
252
+ # Auto-register tools for this conversation
253
+ if conversation_id and self.registered_tools:
254
+ logger.info(f"🔧 Auto-registering {len(self.registered_tools)} tools for conversation {conversation_id}")
255
+ try:
256
+ await self._register_conversation_tools(conversation_id, self.registered_tools)
257
+ logger.info(f"✅ Successfully auto-registered tools for conversation {conversation_id}")
258
+ except Exception as e:
259
+ logger.error(f"❌ Failed to auto-register tools for conversation {conversation_id}: {e}")
260
+ else:
261
+ logger.warning(f"⚠️ Cannot auto-register tools - conversation_id: {conversation_id}, tools available: {bool(self.registered_tools)}")
262
+
263
+ await self.callback_manager.trigger_conversation_started(conversation_id, payload)
264
+ elif event == "conversation_ended":
265
+ logger.info(f"🏁 Conversation ended: {conversation_id}")
266
+ await self.callback_manager.trigger_conversation_ended(conversation_id, payload)
267
+ else:
268
+ logger.warning(f"❓ Unknown lifecycle event: {event}")
269
+
270
+ async def _handle_hook_event(self, payload: Dict) -> None:
271
+ """Handle hook events from platform."""
272
+ event_type = payload.get("event_type")
273
+ logger.debug(f"Hook event received: {event_type}")
274
+
275
+ # Hook events are typically informational
276
+ # Customers can subscribe to specific hook events if needed
277
+
278
+ async def _handle_connection_warning(self, payload: Dict) -> None:
279
+ """Handle connection warnings from platform."""
280
+ message = payload.get("message", "Connection warning")
281
+ logger.warning(f"Platform connection warning: {message}")
282
+
283
+ async def _handle_error_notification(self, payload: Dict) -> None:
284
+ """Handle error notifications from platform."""
285
+ error_type = payload.get("error_type")
286
+ error_message = payload.get("error_message")
287
+
288
+ logger.error(f"Platform error ({error_type}): {error_message}")
289
+ await self.callback_manager.trigger_error(error_type, error_message, payload)
290
+
291
+ async def _handle_connection_event(self, event: str, data: Any = None) -> None:
292
+ """Handle WebSocket connection events."""
293
+ logger.info(f"Connection event: {event}")
294
+
295
+ if event == "connected":
296
+ self.connection_state = "connected"
297
+ elif event == "disconnected":
298
+ self.connection_state = "disconnected"
299
+ elif event == "error":
300
+ logger.error(f"Connection error: {data}")
301
+ self.connection_state = "error"
302
+
303
+ async def _send_tool_result(self, call_id: str, result: Any) -> None:
304
+ """Send tool execution result to platform."""
305
+ message = {
306
+ "call_id": call_id,
307
+ "result": result
308
+ }
309
+
310
+ await self.connection.send_message("tool_call_response", message)
311
+ logger.debug(f"Sent tool result for call: {call_id}")
312
+
313
+ async def _send_tool_error(self, call_id: str, error: str) -> None:
314
+ """Send tool execution error to platform."""
315
+ message = {
316
+ "call_id": call_id,
317
+ "error": {
318
+ "message": error,
319
+ "timestamp": datetime.now(dt.timezone.utc).isoformat()
320
+ }
321
+ }
322
+
323
+ await self.connection.send_message("tool_call_error", message)
324
+ logger.debug(f"Sent tool error for call: {call_id}")
325
+
326
+ async def _send_error_response(self, event: str, error: str) -> None:
327
+ """Send error response for event handling failure."""
328
+ logger.error(f"Failed to handle event {event}: {error}")
329
+ # Platform will handle this through connection monitoring
330
+
331
+ # Event callback registration methods
332
+ def on_conversation_started(self, callback: Callable[[str], None]) -> None:
333
+ """Register callback for conversation started events."""
334
+ self.callback_manager.on_conversation_started = callback
335
+
336
+ def on_conversation_ended(self, callback: Callable[[str], None]) -> None:
337
+ """Register callback for conversation ended events."""
338
+ self.callback_manager.on_conversation_ended = callback
339
+
340
+ def on_tool_called(self, callback: Callable[[ToolCall], None]) -> None:
341
+ """Register callback for tool call events."""
342
+ self.callback_manager.on_tool_called = callback
343
+
344
+ def on_tool_completed(self, callback: Callable[[str, Any], None]) -> None:
345
+ """Register callback for tool completion events."""
346
+ self.callback_manager.on_tool_completed = callback
347
+
348
+ def on_error(self, callback: Callable[[str, str, Dict], None]) -> None:
349
+ """Register callback for error events."""
350
+ self.callback_manager.on_error = callback
351
+
352
+ def on_config_update(self, callback: Callable[[Dict], None]) -> None:
353
+ """Register callback for configuration updates."""
354
+ self.callback_manager.on_config_update = callback
@@ -0,0 +1,174 @@
1
+ """
2
+ Event callbacks and lifecycle management for Conversimple SDK.
3
+
4
+ Provides structured event handling for:
5
+ - Conversation lifecycle events
6
+ - Tool execution events
7
+ - Configuration updates
8
+ - Error notifications
9
+ """
10
+
11
+ import logging
12
+ from typing import Dict, Optional, Callable, Any
13
+ from dataclasses import dataclass
14
+ from datetime import datetime
15
+
16
+ from .tools import ToolCall
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class ConversationLifecycleEvent:
23
+ """Represents a conversation lifecycle event."""
24
+ event_type: str # "started" or "ended"
25
+ conversation_id: str
26
+ timestamp: datetime
27
+ metadata: Dict[str, Any]
28
+
29
+
30
+ @dataclass
31
+ class ToolCallEvent:
32
+ """Represents a tool call event."""
33
+ tool_call: ToolCall
34
+ event_type: str # "called", "completed", "failed"
35
+ timestamp: datetime
36
+ result: Optional[Any] = None
37
+ error: Optional[str] = None
38
+
39
+
40
+ @dataclass
41
+ class ErrorEvent:
42
+ """Represents an error event."""
43
+ error_type: str
44
+ error_message: str
45
+ conversation_id: Optional[str]
46
+ timestamp: datetime
47
+ details: Dict[str, Any]
48
+
49
+
50
+ @dataclass
51
+ class ConfigUpdateEvent:
52
+ """Represents a configuration update event."""
53
+ customer_id: str
54
+ configuration: Dict[str, Any]
55
+ timestamp: datetime
56
+
57
+
58
+ class CallbackManager:
59
+ """
60
+ Manages event callbacks for the Conversimple agent.
61
+
62
+ Provides a centralized way to register and trigger callbacks
63
+ for various platform events.
64
+ """
65
+
66
+ def __init__(self):
67
+ # Event callbacks
68
+ self.on_conversation_started: Optional[Callable[[str], None]] = None
69
+ self.on_conversation_ended: Optional[Callable[[str], None]] = None
70
+ self.on_tool_called: Optional[Callable[[ToolCall], None]] = None
71
+ self.on_tool_completed: Optional[Callable[[str, Any], None]] = None
72
+ self.on_error: Optional[Callable[[str, str, Dict], None]] = None
73
+ self.on_config_update: Optional[Callable[[Dict], None]] = None
74
+
75
+ async def trigger_conversation_started(
76
+ self,
77
+ conversation_id: str,
78
+ metadata: Optional[Dict] = None
79
+ ) -> None:
80
+ """Trigger conversation started callback."""
81
+ if self.on_conversation_started:
82
+ try:
83
+ if asyncio.iscoroutinefunction(self.on_conversation_started):
84
+ await self.on_conversation_started(conversation_id)
85
+ else:
86
+ self.on_conversation_started(conversation_id)
87
+
88
+ logger.debug(f"Triggered conversation_started callback for {conversation_id}")
89
+
90
+ except Exception as e:
91
+ logger.error(f"Error in conversation_started callback: {e}")
92
+
93
+ async def trigger_conversation_ended(
94
+ self,
95
+ conversation_id: str,
96
+ metadata: Optional[Dict] = None
97
+ ) -> None:
98
+ """Trigger conversation ended callback."""
99
+ if self.on_conversation_ended:
100
+ try:
101
+ if asyncio.iscoroutinefunction(self.on_conversation_ended):
102
+ await self.on_conversation_ended(conversation_id)
103
+ else:
104
+ self.on_conversation_ended(conversation_id)
105
+
106
+ logger.debug(f"Triggered conversation_ended callback for {conversation_id}")
107
+
108
+ except Exception as e:
109
+ logger.error(f"Error in conversation_ended callback: {e}")
110
+
111
+ async def trigger_tool_called(self, tool_call: ToolCall) -> None:
112
+ """Trigger tool called callback."""
113
+ if self.on_tool_called:
114
+ try:
115
+ if asyncio.iscoroutinefunction(self.on_tool_called):
116
+ await self.on_tool_called(tool_call)
117
+ else:
118
+ self.on_tool_called(tool_call)
119
+
120
+ logger.debug(f"Triggered tool_called callback for {tool_call.tool_name}")
121
+
122
+ except Exception as e:
123
+ logger.error(f"Error in tool_called callback: {e}")
124
+
125
+ async def trigger_tool_completed(self, call_id: str, result: Any) -> None:
126
+ """Trigger tool completed callback."""
127
+ if self.on_tool_completed:
128
+ try:
129
+ if asyncio.iscoroutinefunction(self.on_tool_completed):
130
+ await self.on_tool_completed(call_id, result)
131
+ else:
132
+ self.on_tool_completed(call_id, result)
133
+
134
+ logger.debug(f"Triggered tool_completed callback for {call_id}")
135
+
136
+ except Exception as e:
137
+ logger.error(f"Error in tool_completed callback: {e}")
138
+
139
+ async def trigger_error(
140
+ self,
141
+ error_type: str,
142
+ error_message: str,
143
+ details: Dict[str, Any]
144
+ ) -> None:
145
+ """Trigger error callback."""
146
+ if self.on_error:
147
+ try:
148
+ if asyncio.iscoroutinefunction(self.on_error):
149
+ await self.on_error(error_type, error_message, details)
150
+ else:
151
+ self.on_error(error_type, error_message, details)
152
+
153
+ logger.debug(f"Triggered error callback for {error_type}")
154
+
155
+ except Exception as e:
156
+ logger.error(f"Error in error callback: {e}")
157
+
158
+ async def trigger_config_update(self, config: Dict[str, Any]) -> None:
159
+ """Trigger configuration update callback."""
160
+ if self.on_config_update:
161
+ try:
162
+ if asyncio.iscoroutinefunction(self.on_config_update):
163
+ await self.on_config_update(config)
164
+ else:
165
+ self.on_config_update(config)
166
+
167
+ logger.debug("Triggered config_update callback")
168
+
169
+ except Exception as e:
170
+ logger.error(f"Error in config_update callback: {e}")
171
+
172
+
173
+ # Import asyncio for callback detection
174
+ import asyncio