# Tool collections Source: https://slide.mintlify.app/api-reference/lye-collections Detailed reference for all Lye tool collections ## Overview This page provides detailed documentation for each tool collection in Lye, including parameters, return values, and usage examples. ## WEB\_TOOLS Tools for web browsing, searching, and content extraction. ### web-fetch\_page Fetches content from a web page in text or HTML format. The URL to fetch Output format - "text" for readable content, "html" for raw HTML Custom headers to send with request ```python theme={null} # Example usage thread.add_message(Message( role="user", content="Get the content from https://example.com" )) ``` ### web-search Search the web using Google. The search query Number of results to return (max 10) ```python theme={null} # Example usage thread.add_message(Message( role="user", content="Search for recent AI developments" )) ``` ### web-download\_file Download files from URLs. URL of the file to download Custom headers for the request Returns downloaded file as attachment. ## FILES\_TOOLS Tools for file system operations. ### files-read\_file Read contents of a file. Path to the file to read File encoding ### files-write\_file Write content to a file. Path where to write the file Content to write File encoding Create parent directories if they don't exist ### files-list\_directory List contents of a directory. Directory path to list List subdirectories recursively Include hidden files (starting with .) ### files-move\_file Move or rename a file. Source file path Destination file path ### files-copy\_file Copy a file. Source file path Destination file path ### files-delete\_file Delete a file. Path to file to delete ### files-create\_directory Create a directory. Directory path to create Create parent directories if needed ### files-search\_files Search for files matching a pattern. Search pattern (supports wildcards) Directory to search in Search subdirectories ## IMAGE\_TOOLS Tools for image generation and analysis. ### image-generate\_image Generate images using DALL-E 3. Description of the image to generate Model to use: "dall-e-3" or "dall-e-2" Image size: "1024x1024", "1792x1024", or "1024x1792" (DALL-E 3) Image quality: "standard" or "hd" (DALL-E 3 only) Style: "vivid" or "natural" (DALL-E 3 only) Returns generated image as attachment. ### image-analyze\_image Analyze images using GPT-4 Vision. Path to image file or URL Question about the image Analysis detail: "low", "high", or "auto" ### image-create\_qr\_code Generate QR codes. Data to encode in QR code QR code size in pixels Border size in modules Returns QR code image as attachment. ### image-resize\_image Resize images. Path to image file Target width (maintains aspect if height not specified) Target height (maintains aspect if width not specified) Resize mode: "contain", "cover", "fill", or "exact" ### image-convert\_image Convert between image formats. Path to source image Target format: "JPEG", "PNG", "WEBP", "BMP", "GIF" Output quality (1-100, for lossy formats) ## AUDIO\_TOOLS Tools for audio processing. ### audio-transcribe Transcribe audio using OpenAI Whisper. Path to audio file Whisper model to use Language code (e.g., "en", "es") Sampling temperature (0-1) ## COMMAND\_LINE\_TOOLS System command execution. ### run\_terminal\_command Execute shell commands. Command to execute Directory to run command in Command timeout in seconds Run command in shell ## SLACK\_TOOLS Slack integration tools. ### slack-send\_message Send messages to Slack. Channel ID or name (e.g., "C1234567890" or "#general") Message text Thread timestamp to reply to Rich message blocks ### slack-get\_messages Retrieve messages from a channel. Channel ID Number of messages to retrieve Oldest message timestamp Latest message timestamp ### slack-list\_channels List available Slack channels. Channel types: "public\_channel", "private\_channel", "mpim", "im" Exclude archived channels ### slack-react\_to\_message Add emoji reaction to a message. Channel ID Message timestamp Emoji name (without colons) ## NOTION\_TOOLS Notion workspace integration. ### notion-create\_page Create a new Notion page. Parent page or database ID Page title Page content (text or blocks) Page properties for database items ### notion-update\_page Update existing Notion page. Page ID to update New content Properties to update ### notion-search Search Notion workspace. Search query Filter by object type: "page" or "database" Sort results by "last\_edited\_time" ## BROWSER\_TOOLS Browser automation with Playwright. ### browser-navigate Navigate to a URL. URL to navigate to Wait condition: "load", "domcontentloaded", "networkidle" ### browser-click Click an element. CSS selector or text Timeout in milliseconds ### browser-type Type text into an input. Input selector Text to type Delay between keystrokes (ms) ### browser-screenshot Take a screenshot. Save screenshot to path Capture full page Capture specific element ## WANDB\_TOOLS Weights & Biases integration. ### wandb-create\_workspace Create a W\&B workspace. Workspace name W\&B project name Workspace configuration ### wandb-log\_metrics Log metrics to W\&B. Metrics to log Step number Commit immediately ## Usage Examples ### Web research agent ```python theme={null} from tyler import Agent, Thread, Message from lye import WEB_TOOLS, FILES_TOOLS agent = Agent( name="researcher", tools=[*WEB_TOOLS, *FILES_TOOLS] ) thread = Thread() thread.add_message(Message( role="user", content="Research the latest developments in quantum computing and save a summary to research.md" )) result = await agent.run(thread) ``` ### Creative assistant ```python theme={null} from lye import IMAGE_TOOLS, AUDIO_TOOLS agent = Agent( name="creative", tools=[*IMAGE_TOOLS, *AUDIO_TOOLS] ) thread.add_message(Message( role="user", content="Generate an image of a futuristic city and transcribe this audio note", attachments=[audio_file] )) ``` ### DevOps Agent ```python theme={null} from lye import COMMAND_LINE_TOOLS, FILES_TOOLS, SLACK_TOOLS agent = Agent( name="devops", tools=[*COMMAND_LINE_TOOLS, *FILES_TOOLS, *SLACK_TOOLS] ) thread.add_message(Message( role="user", content="Check system status, create a report, and notify the team on Slack" )) ``` # Tool format Source: https://slide.mintlify.app/api-reference/lye-tool-format Understanding Lye tool structure and usage ## Overview Lye tools follow a specific format that makes them compatible with Tyler agents and LLM function calling. Each tool consists of a definition (for the LLM) and an implementation (the actual function). ## Tool Structure Each Lye tool is a dictionary with the following structure: ```python theme={null} { "definition": { "type": "function", "function": { "name": "tool-name", "description": "What the tool does", "parameters": { "type": "object", "properties": { "param1": { "type": "string", "description": "Parameter description" } }, "required": ["param1"] } } }, "implementation": tool_function, "type": "standard", # Optional metadata "timeout": 30.0 # Optional: max execution time in seconds } ``` | Field | Required | Description | | ---------------- | -------- | --------------------------------------------------------------------- | | `definition` | Yes | OpenAI function calling format definition | | `implementation` | Yes | The Python function to execute | | `type` | No | Optional metadata (e.g., "standard") | | `timeout` | No | Maximum execution time in seconds. If exceeded, raises `TimeoutError` | ## Tool Implementation Tool functions follow specific patterns: **Automatic Weave Tracing**: Tyler automatically wraps all tool implementations with `weave.op()` when they are registered. You don't need to add `@weave.op()` decorators to your tools - they will appear in Weave traces automatically with the tool name as the operation name. ### Basic Tool ```python theme={null} def tool_function(*, param1: str, param2: Optional[int] = None) -> str: """ Tool implementation. Args: param1: Required parameter param2: Optional parameter with default Returns: String result for the agent """ # Tool logic here result = f"Processed {param1}" return result ``` ### Tool with Files Tools that return files use a tuple format: ```python theme={null} def create_file(*, filename: str, content: str) -> Tuple[str, List[Dict[str, Any]]]: """ Create a file and return it. Returns: Tuple containing: - Status message - List of file dictionaries """ files = [{ "filename": filename, "content": base64.b64encode(content.encode()).decode(), "mime_type": "text/plain" }] return f"Created {filename}", files ``` ## Parameter Types ### Basic Types ```python theme={null} "parameters": { "type": "object", "properties": { "text": { "type": "string", "description": "Input text" }, "count": { "type": "integer", "description": "Number of items" }, "enabled": { "type": "boolean", "description": "Feature flag" }, "threshold": { "type": "number", "description": "Decimal threshold" } } } ``` ### Enums ```python theme={null} "format": { "type": "string", "description": "Output format", "enum": ["text", "json", "html"], "default": "text" } ``` ### Arrays ```python theme={null} "tags": { "type": "array", "items": { "type": "string" }, "description": "List of tags" } ``` ### Optional Parameters ```python theme={null} "parameters": { "type": "object", "properties": { "required_param": {...}, "optional_param": {...} }, "required": ["required_param"] # Only list required params } ``` ## Return Formats ### Simple string return ```python theme={null} def simple_tool(*, input: str) -> str: return f"Processed: {input}" ``` ### Structured return with files ```python theme={null} def file_tool(*, data: str) -> Tuple[str, List[Dict[str, Any]]]: # Process data result_content = process_data(data) files = [{ "filename": "result.txt", "content": base64.b64encode(result_content.encode()).decode(), "mime_type": "text/plain" }] return "Processing complete", files ``` ### Error Handling ```python theme={null} def safe_tool(*, url: str) -> str: try: # Attempt operation result = fetch_data(url) return f"Success: {result}" except Exception as e: # Return error message for agent return f"Error: {str(e)}" ``` ## Tool Naming Conventions Lye follows consistent naming patterns: * **Format**: `category-action_target` * **Examples**: * `web-fetch_page` * `files-read_file` * `image-generate_image` * `slack-send_message` ## Using tools with agents ### Direct Usage ```python theme={null} from tyler import Agent from lye import WEB_TOOLS agent = Agent( name="web-researcher", tools=WEB_TOOLS ) # Agent automatically uses tools based on user requests ``` ### Tool Selection ```python theme={null} from lye import TOOLS # Filter tools by name pattern web_tools = [t for t in TOOLS if t["definition"]["function"]["name"].startswith("web-")] # Select specific tools selected_tools = [ t for t in TOOLS if t["definition"]["function"]["name"] in ["web-search", "files-write_file"] ] agent = Agent( name="selective-agent", tools=selected_tools ) ``` ## Tool Execution Flow 1. **User Request**: User asks agent to perform a task 2. **Tool Selection**: Agent selects appropriate tool based on description 3. **Parameter Extraction**: Agent extracts parameters from context 4. **Validation**: Parameters are validated against schema 5. **Execution**: Tool function is called with parameters 6. **Result Processing**: Result is returned to agent 7. **Response**: Agent incorporates result into response ## Best practices ### Clear descriptions ```python theme={null} # Good "description": "Search the web using Google and return relevant results with titles, snippets, and URLs" # Bad "description": "Search tool" ``` ### Parameter descriptions ```python theme={null} # Good "url": { "type": "string", "description": "The complete URL to fetch, including protocol (http:// or https://)" } # Bad "url": { "type": "string", "description": "URL" } ``` ### Error messages ```python theme={null} # Good return "Error: Unable to connect to https://example.com - Connection timeout after 30 seconds" # Bad return "Error" ``` ### File returns ```python theme={null} # Always include metadata files = [{ "filename": "report.pdf", "content": base64_content, "mime_type": "application/pdf" }] # Include helpful status message return f"Generated PDF report with {page_count} pages", files ``` ## Tool Timeout For tools that may take a long time (external API calls, file processing), set a `timeout` value: ```python theme={null} SLOW_API_TOOL = { "definition": { "type": "function", "function": { "name": "slow_api_call", "description": "Call an external API that may be slow", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "API query"} }, "required": ["query"] } } }, "implementation": slow_api_call, "timeout": 60.0 # 60 second timeout } ``` When a tool exceeds its timeout: 1. A `TimeoutError` is raised with the message: `Tool 'tool_name' timed out after X seconds` 2. The error is returned to the LLM as the tool result 3. The LLM can then decide how to proceed ## Example: Complete Tool ```python theme={null} import requests from typing import Tuple, List, Dict, Any import base64 # Tool definition WEB_DOWNLOAD_TOOL = { "definition": { "type": "function", "function": { "name": "web-download_file", "description": "Download a file from a URL and return it as an attachment", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "The URL of the file to download" }, "timeout": { "type": "integer", "description": "Timeout in seconds", "default": 30 } }, "required": ["url"] } } }, "implementation": download_file, "type": "standard", "timeout": 120.0 # Tool-level timeout (2 minutes max) } # Tool implementation (automatically traced by Tyler) def download_file(*, url: str, timeout: int = 30) -> Tuple[str, List[Dict[str, Any]]]: """Download a file from URL.""" try: response = requests.get(url, timeout=timeout) response.raise_for_status() # Get filename from URL filename = url.split('/')[-1] or "download" # Encode content content = base64.b64encode(response.content).decode() # Detect MIME type mime_type = response.headers.get('content-type', 'application/octet-stream') files = [{ "filename": filename, "content": content, "mime_type": mime_type }] size_mb = len(response.content) / (1024 * 1024) return f"Downloaded {filename} ({size_mb:.2f} MB)", files except requests.exceptions.Timeout: return f"Error: Download timed out after {timeout} seconds", [] except requests.exceptions.RequestException as e: return f"Error downloading file: {str(e)}", [] except Exception as e: return f"Unexpected error: {str(e)}", [] ``` ## Testing tools ```python theme={null} # Test tool directly result = await download_file(url="https://example.com/file.pdf") print(result[0]) # Status message print(len(result[1])) # Number of files # Test with agent from tyler import Agent, Thread, Message agent = Agent(tools=[WEB_DOWNLOAD_TOOL]) thread = Thread() thread.add_message(Message( role="user", content="Download the PDF from https://example.com/report.pdf" )) result = await agent.run(thread) ``` # Attachment Source: https://slide.mintlify.app/api-reference/narrator-attachment Represents a file attached to a message ## Overview The `Attachment` class handles file attachments on messages, supporting various file types with automatic processing for text extraction, MIME type detection, and integration with FileStore for persistence. ## Creating Attachments ### From Raw Content ```python theme={null} from narrator import Attachment # Create from bytes pdf_attachment = Attachment( filename="report.pdf", content=pdf_bytes, mime_type="application/pdf" ) # Create from base64 string image_attachment = Attachment( filename="screenshot.png", content="iVBORw0KGgoAAAANS...", # base64 string mime_type="image/png" ) # MIME type auto-detection attachment = Attachment( filename="document.pdf", content=file_bytes ) attachment.detect_mime_type() # Sets mime_type automatically ``` ### From File Path ```python theme={null} # Load from file system attachment = Attachment.from_file_path("/path/to/document.pdf") # Automatically sets filename, content, and mime_type ``` ### From Data URLs ```python theme={null} # Handle data URLs (e.g., from web uploads) attachment = Attachment( filename="upload.jpg", content="data:image/jpeg;base64,/9j/4AAQSkZJRg..." ) ``` ## Key properties Name of the file File content as bytes or base64-encoded string MIME type of the file (auto-detected if not provided) Processed content and metadata (e.g., extracted text, image info) Unique identifier when stored in FileStore Path where file is stored in FileStore Current processing status Auto-generated unique ID based on content hash ## Processing and Storage Attachments can be processed to extract content and stored persistently: ```python theme={null} from narrator import FileStore # Create file store file_store = await FileStore.create("./attachments") # Process and store attachment await attachment.process_and_store(file_store) # After processing: print(f"File ID: {attachment.file_id}") print(f"Storage path: {attachment.storage_path}") print(f"Status: {attachment.status}") # "stored" print(f"Extracted content: {attachment.attributes}") ``` ## Content Processing Different file types are automatically processed: ### Text Files ```python theme={null} text_attachment = Attachment( filename="notes.txt", content=b"Meeting notes: Project update..." ) await text_attachment.process_and_store(file_store) # attributes["text"] contains the file content ``` ### PDFs ```python theme={null} pdf_attachment = Attachment( filename="report.pdf", content=pdf_bytes ) await pdf_attachment.process_and_store(file_store) # attributes["text"] contains extracted text from all pages ``` ### Images ```python theme={null} image_attachment = Attachment( filename="diagram.png", content=image_bytes ) await image_attachment.process_and_store(file_store) # attributes["type"] = "image" # attributes["url"] contains the file URL for viewing ``` ### JSON Files ```python theme={null} json_attachment = Attachment( filename="config.json", content=b'{"setting": "value"}' ) await json_attachment.process_and_store(file_store) # attributes["parsed_content"] contains the parsed JSON object ``` ### Audio Files ```python theme={null} audio_attachment = Attachment( filename="recording.mp3", content=audio_bytes, mime_type="audio/mpeg" ) await audio_attachment.process_and_store(file_store) # attributes["type"] = "audio" ``` ## Retrieving Content ```python theme={null} # Get content as bytes (handles all encoding types) content_bytes = await attachment.get_content_bytes() # If stored in FileStore, pass it to retrieve content_bytes = await attachment.get_content_bytes(file_store=file_store) # Access processed attributes if attachment.attributes.get("type") == "text": text_content = attachment.attributes["text"] elif attachment.attributes.get("type") == "image": image_url = attachment.attributes["url"] ``` ## Serialization ```python theme={null} # Convert to dictionary (excludes content for efficiency) attachment_dict = attachment.model_dump() # { # "filename": "report.pdf", # "mime_type": "application/pdf", # "file_id": "abc123", # "storage_path": "/files/abc123_report.pdf", # "status": "stored", # "attributes": {...} # } # Content is not included in serialization to avoid large payloads # Retrieve content separately using get_content_bytes() ``` ## File Size and Type Validation When used with FileStore, attachments are validated: ```python theme={null} try: # FileStore enforces size limits (default 50MB) await large_attachment.process_and_store(file_store) except FileTooLargeError: print("File exceeds size limit") try: # FileStore checks allowed MIME types await executable.process_and_store(file_store) except UnsupportedFileTypeError: print("File type not allowed") ``` ## Example: Document Processing Pipeline ```python theme={null} from narrator import Message, Attachment, FileStore # Initialize storage file_store = await FileStore.create("./documents") # Create message with multiple attachments message = Message( role="user", content="Please review these documents", attachments=[ Attachment( filename="contract.pdf", content=contract_bytes ), Attachment( filename="requirements.txt", content=b"Project requirements:\n1. Feature A\n2. Feature B" ), Attachment.from_file_path("./data/analytics.json") ] ) # Process all attachments for attachment in message.attachments: try: await attachment.process_and_store(file_store) print(f"Processed {attachment.filename}:") if attachment.attributes.get("text"): print(f" Extracted text: {len(attachment.attributes['text'])} chars") elif attachment.attributes.get("parsed_content"): print(f" Parsed JSON with {len(attachment.attributes['parsed_content'])} keys") print(f" Stored at: {attachment.storage_path}") print(f" URL: {attachment.attributes.get('url')}") except Exception as e: print(f"Failed to process {attachment.filename}: {e}") # Access processed content for attachment in message.attachments: if attachment.status == "stored": # Retrieve content when needed content = await attachment.get_content_bytes(file_store) print(f"{attachment.filename}: {len(content)} bytes") ``` ## Integration with Messages Attachments are designed to work seamlessly with messages: ```python theme={null} # Attachments are included in message serialization msg_dict = message.model_dump() attachments_data = msg_dict["attachments"] # Each attachment includes metadata but not content for att_data in attachments_data: print(f"File: {att_data['filename']}") print(f"Type: {att_data['mime_type']}") print(f"Status: {att_data['status']}") if att_data.get('attributes', {}).get('url'): print(f"URL: {att_data['attributes']['url']}") ``` # FileStore Source: https://slide.mintlify.app/api-reference/narrator-filestore File storage system with validation, processing, and retrieval ## Overview The `FileStore` class provides secure file storage with automatic validation, MIME type detection, and configurable limits. It's designed to handle attachments and files in conversational AI applications. ## Creating a FileStore ### Recommended: Factory Method ```python theme={null} from narrator import FileStore # Default configuration store = await FileStore.create() # Custom directory store = await FileStore.create("/path/to/files") # Full configuration store = await FileStore.create( base_path="/var/app/files", max_file_size=100*1024*1024, # 100MB max_storage_size=10*1024*1024*1024, # 10GB allowed_mime_types={"image/jpeg", "image/png", "application/pdf"} ) ``` The factory method validates storage access immediately, ensuring the directory is writable. ### Direct Constructor ```python theme={null} # Creates store without validation store = FileStore("/path/to/files") # Validation happens on first use file_id = await store.save(content, "file.pdf") # Validates here ``` ## Configuration Options Base directory for file storage Maximum file size in bytes (default: 50MB) Maximum total storage in bytes (default: 5GB) Set of allowed MIME types. Default includes documents, images, archives, and audio formats. ## Default Allowed File Types ```python theme={null} # Documents 'application/pdf' 'application/msword' 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 'text/plain' 'text/csv' 'application/json' # Images 'image/jpeg' 'image/png' 'image/gif' 'image/webp' 'image/svg+xml' # Archives 'application/zip' 'application/x-tar' 'application/gzip' # Audio 'audio/mpeg' 'audio/mp3' 'audio/wav' 'audio/ogg' # ... and more ``` ## Saving Files ```python theme={null} # Save from bytes file_info = await store.save( content=pdf_bytes, filename="report.pdf", mime_type="application/pdf" # Optional, auto-detected if not provided ) print(f"File ID: {file_info['id']}") print(f"Stored at: {file_info['storage_path']}") print(f"Size: {file_info['size']} bytes") # Save from base64 import base64 encoded = base64.b64encode(image_bytes).decode() file_info = await store.save( content=encoded, filename="photo.jpg" ) # Save with metadata file_info = await store.save( content=data, filename="document.docx", metadata={ "author": "John Doe", "department": "Sales", "version": "1.2" } ) ``` ## Retrieving Files ```python theme={null} # Get file content file_id = "abc123..." storage_path = "2024/01/15/abc123_report.pdf" content = await store.get(file_id, storage_path) # Returns bytes # Get file URL for web access file_url = FileStore.get_file_url(storage_path) # Returns: "/files/2024/01/15/abc123_report.pdf" # Get file metadata metadata = await store.get_metadata(file_id, storage_path) print(f"Filename: {metadata['filename']}") print(f"Size: {metadata['size']}") print(f"MIME type: {metadata['mime_type']}") print(f"Created: {metadata['created_at']}") ``` ## Deleting Files ```python theme={null} # Delete a file deleted = await store.delete(file_id, storage_path) print(f"Deleted: {deleted}") # Delete all files (careful!) await store.delete_all() ``` ## Storage Management ```python theme={null} # Check storage usage current_size = await store.get_storage_size() print(f"Using {current_size / 1024 / 1024:.2f} MB") # List all files files = await store.list_files() for file_info in files: print(f"{file_info['filename']}: {file_info['size']} bytes") # Get file statistics stats = await store.get_stats() print(f"Total files: {stats['total_files']}") print(f"Total size: {stats['total_size_mb']:.2f} MB") print(f"Average size: {stats['average_size_mb']:.2f} MB") print(f"Storage used: {stats['storage_percentage']:.1f}%") # File type breakdown for mime_type, count in stats['mime_type_counts'].items(): print(f"{mime_type}: {count} files") ``` ## Error handling FileStore includes specific exceptions for different error cases: ```python theme={null} from narrator.storage.file_store import ( FileTooLargeError, UnsupportedFileTypeError, StorageFullError, FileNotFoundError ) try: # Save a large file await store.save(large_content, "huge.zip") except FileTooLargeError as e: print(f"File too large: {e}") try: # Save unsupported type await store.save(exe_content, "app.exe") except UnsupportedFileTypeError as e: print(f"File type not allowed: {e}") try: # Storage limit exceeded await store.save(content, "file.pdf") except StorageFullError as e: print(f"Storage full: {e}") try: # Retrieve non-existent file await store.get("bad-id", "bad-path") except FileNotFoundError as e: print(f"File not found: {e}") ``` ## Integration with Attachments FileStore is designed to work with the Attachment class: ```python theme={null} from narrator import Attachment, FileStore # Create store store = await FileStore.create("./uploads") # Process attachment attachment = Attachment( filename="presentation.pdf", content=pdf_bytes ) # Store the attachment await attachment.process_and_store(store) # Attachment now has storage info print(f"Stored at: {attachment.storage_path}") print(f"File ID: {attachment.file_id}") print(f"URL: {attachment.attributes.get('url')}") # Later: retrieve content content = await attachment.get_content_bytes(file_store=store) ``` ## File Organization Files are organized by date for easy management: ``` narrator_files/ ├── 2024/ │ ├── 01/ │ │ ├── 15/ │ │ │ ├── abc123_report.pdf │ │ │ ├── def456_image.jpg │ │ │ └── metadata/ │ │ │ ├── abc123.json │ │ │ └── def456.json ``` ## Example: Document Management System ```python theme={null} from narrator import FileStore import asyncio class DocumentManager: def __init__(self, store: FileStore): self.store = store async def upload_document(self, content: bytes, filename: str, department: str, doc_type: str): """Upload a document with metadata""" try: file_info = await self.store.save( content=content, filename=filename, metadata={ "department": department, "doc_type": doc_type, "uploaded_by": "current_user", "upload_time": datetime.now().isoformat() } ) return { "success": True, "file_id": file_info["id"], "url": FileStore.get_file_url(file_info["storage_path"]) } except FileTooLargeError: return {"success": False, "error": "File exceeds size limit"} except UnsupportedFileTypeError: return {"success": False, "error": "File type not allowed"} async def get_department_documents(self, department: str): """Get all documents for a department""" all_files = await self.store.list_files() dept_files = [] for file_info in all_files: metadata = await self.store.get_metadata( file_info["id"], file_info["storage_path"] ) if metadata.get("metadata", {}).get("department") == department: dept_files.append({ "filename": metadata["filename"], "size": metadata["size"], "uploaded": metadata["created_at"], "type": metadata.get("metadata", {}).get("doc_type"), "url": FileStore.get_file_url(file_info["storage_path"]) }) return dept_files async def cleanup_old_files(self, days: int = 30): """Remove files older than specified days""" cutoff = datetime.now() - timedelta(days=days) all_files = await self.store.list_files() deleted = 0 for file_info in all_files: metadata = await self.store.get_metadata( file_info["id"], file_info["storage_path"] ) if metadata["created_at"] < cutoff: await self.store.delete( file_info["id"], file_info["storage_path"] ) deleted += 1 return deleted # Usage store = await FileStore.create( base_path="/var/documents", max_file_size=25*1024*1024, # 25MB max_storage_size=100*1024*1024*1024 # 100GB ) doc_manager = DocumentManager(store) # Upload a document result = await doc_manager.upload_document( content=pdf_bytes, filename="Q4_Report.pdf", department="Finance", doc_type="quarterly_report" ) # Get department documents finance_docs = await doc_manager.get_department_documents("Finance") for doc in finance_docs: print(f"{doc['filename']} - {doc['type']} - {doc['url']}") # Storage maintenance stats = await store.get_stats() if stats["storage_percentage"] > 80: deleted = await doc_manager.cleanup_old_files(60) print(f"Cleaned up {deleted} old files") ``` ## Best practices 1. **Use the factory method** for immediate validation of storage access 2. **Set appropriate limits** based on your application needs 3. **Monitor storage usage** to prevent running out of space 4. **Use metadata** to organize and search files 5. **Handle errors gracefully** - files can fail validation 6. **Clean up old files** periodically to manage storage 7. **Back up the file directory** - FileStore doesn't handle backups # Message Source: https://slide.mintlify.app/api-reference/narrator-message Represents a single message within a conversation thread ## Overview The `Message` class represents individual messages in a conversation, supporting various roles (system, user, assistant, tool), rich content types, attachments, and detailed metrics. ## Creating Messages ### Basic messages ```python theme={null} from narrator import Message # User message user_msg = Message( role="user", content="Hello, how can I analyze this data?" ) # Assistant message assistant_msg = Message( role="assistant", content="I can help you analyze your data in several ways..." ) # System message system_msg = Message( role="system", content="You are a data analysis expert." ) ``` ### Tool messages ```python theme={null} # Tool response message tool_msg = Message( role="tool", name="data_analyzer", tool_call_id="call_abc123", content="Analysis complete: 500 records processed" ) # Assistant message with tool calls assistant_with_tools = Message( role="assistant", content="Let me analyze that data for you.", tool_calls=[{ "id": "call_abc123", "type": "function", "function": { "name": "data_analyzer", "arguments": '{"dataset": "sales_2024"}' } }] ) ``` ## Key properties Unique identifier based on content hash The role of the message sender Message content - can be text or multimodal content Name identifier for tool messages Required for tool role messages to link to the tool call Tool calls made by assistant messages When the message was created (UTC) Message order within the thread (set automatically) Conversational turn number (set automatically) File attachments on the message Emoji reactions - maps emoji to list of user IDs Information about who/what created this message Platform-specific identifiers (e.g., Slack message ID) Performance and usage metrics for the message ## Multimodal Content Messages support text and image content: ```python theme={null} # Text with images multimodal_msg = Message( role="user", content=[ {"type": "text", "text": "What's in this image?"}, {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} ] ) # Check content type if message.is_multimodal_content(): print("Message contains images") ``` ## Attachments Messages can have file attachments: ```python theme={null} from narrator import Message, Attachment # Create message with attachment message = Message( role="user", content="Please review this document", attachments=[ Attachment( filename="report.pdf", content=pdf_bytes, mime_type="application/pdf" ) ] ) # Or create from file during initialization message = Message( role="user", content="Here's the data", filename="data.csv", file_content=csv_bytes ) ``` ## Message Source Track who created a message: ```python theme={null} # User-created message user_message = Message( role="user", content="Hello", source={ "type": "user", "id": "user_123", "name": "John Doe", "attributes": {"email": "john@example.com"} } ) # Agent-created message agent_message = Message( role="assistant", content="Hello! How can I help?", source={ "type": "agent", "id": "agent_xyz", "name": "Support Bot" } ) # Tool-created message tool_message = Message( role="tool", name="calculator", content="Result: 42", source={ "type": "tool", "id": "calc_v1", "name": "Calculator Tool" } ) ``` ## Metrics Messages track detailed performance metrics: ```python theme={null} message = Message( role="assistant", content="Here's your answer...", metrics={ "model": "gpt-4", "timing": { "started_at": "2024-01-01T00:00:00Z", "ended_at": "2024-01-01T00:00:02Z", "latency": 2000 # milliseconds }, "usage": { "completion_tokens": 150, "prompt_tokens": 500, "total_tokens": 650 }, "weave_call": { "id": "call_abc123", "ui_url": "https://wandb.ai/..." } } ) # Access metrics print(f"Model used: {message.metrics['model']}") print(f"Latency: {message.metrics['timing']['latency']}ms") print(f"Tokens: {message.metrics['usage']['total_tokens']}") ``` ## Reactions Add emoji reactions to messages: ```python theme={null} # Add a reaction added = message.add_reaction("thumbsup", "user_123") # Remove a reaction removed = message.remove_reaction("thumbsup", "user_123") # Get all reactions reactions = message.get_reactions() # Returns: {"thumbsup": ["user_123", "user_456"], "heart": ["user_789"]} # Check if user reacted with specific emoji has_reacted = message.user_has_reacted("thumbsup", "user_123") ``` ## Platform References Link messages to external platforms: ```python theme={null} message = Message( role="user", content="Hello from Slack!", platforms={ "slack": { "channel": "C123456", "ts": "1234567890.123456", "thread_ts": "1234567890.000000" } } ) ``` ## Conversion for APIs ```python theme={null} # Convert to chat completion format chat_msg = message.to_chat_completion_message() # Returns: {"role": "user", "content": "Hello"} # For messages with attachments (requires FileStore) chat_msg = message.to_chat_completion_message(file_store=file_store) # Serialize to dictionary msg_dict = message.model_dump(mode="json") # Dates as ISO strings msg_dict = message.model_dump(mode="python") # Dates as datetime objects ``` ## Validation Messages include automatic validation: ```python theme={null} # Tool messages require tool_call_id try: msg = Message(role="tool", content="Result") # Raises ValueError except ValueError as e: print("Tool messages need tool_call_id") # Tool calls must have proper structure msg = Message( role="assistant", tool_calls=[{ "id": "123", "type": "function", "function": { "name": "get_weather", "arguments": "{}" } }] ) ``` ## Example: Complete Message Flow ```python theme={null} from narrator import Message, Attachment import datetime # User sends message with attachment user_msg = Message( role="user", content="Can you analyze this sales data?", attachments=[ Attachment( filename="sales_q4.csv", content=csv_data, mime_type="text/csv" ) ], source={ "type": "user", "id": "user_123", "name": "Alice Smith" } ) # Assistant responds with tool use assistant_msg = Message( role="assistant", content="I'll analyze your Q4 sales data.", tool_calls=[{ "id": "analysis_001", "type": "function", "function": { "name": "analyze_csv", "arguments": '{"file_id": "sales_q4.csv"}' } }], source={ "type": "agent", "id": "analyst_bot", "name": "Data Analyst" } ) # Tool provides results tool_msg = Message( role="tool", name="analyze_csv", tool_call_id="analysis_001", content="Total sales: $1.2M, Top product: Widget Pro", metrics={ "timing": { "latency": 1500 } } ) # Assistant summarizes summary_msg = Message( role="assistant", content="Your Q4 sales totaled $1.2M, with Widget Pro as your top product!", metrics={ "model": "gpt-4", "usage": { "total_tokens": 245 } } ) # User reacts user_msg.add_reaction("thumbsup", "user_123") ``` # Thread Source: https://slide.mintlify.app/api-reference/narrator-thread Represents a conversation thread containing messages ## Overview The `Thread` class represents a conversation containing multiple messages. It provides rich functionality for managing conversation state, tracking metrics, and organizing messages by turns. ## Creating a Thread ```python theme={null} from narrator import Thread, Message # Create a new thread thread = Thread() # Create with a specific ID and title thread = Thread( id="my-thread-123", title="Customer Support Chat" ) # Create with platform references thread = Thread( title="Slack Conversation", platforms={ "slack": { "channel": "C123456", "thread_ts": "1234567890.123" } } ) ``` ## Key properties Unique identifier for the thread Human-readable title for the thread List of messages in the thread Timestamp when the thread was created (UTC) Timestamp when the thread was last updated (UTC) Custom metadata for the thread References to where this thread exists on external platforms (e.g., Slack, Discord) ## Adding Messages ### Single message ```python theme={null} # Add a user message thread.add_message(Message( role="user", content="Hello, I need help with my order" )) # Add an assistant message thread.add_message(Message( role="assistant", content="I'd be happy to help you with your order." )) # Messages in the same turn (e.g., tool calls) thread.add_message(tool_message, same_turn=True) ``` ### Batch messages ```python theme={null} # Add multiple messages with the same turn number messages = [ Message(role="assistant", content="Let me check that for you."), Message(role="tool", name="order_lookup", content="Order #12345 found"), Message(role="assistant", content="I found your order!") ] thread.add_messages_batch(messages) ``` ## Accessing Messages ```python theme={null} # Get all messages for chat completion (excludes system messages) messages = await thread.get_messages_for_chat_completion() # Get last message by role last_user_msg = thread.get_last_message_by_role("user") last_assistant_msg = thread.get_last_message_by_role("assistant") # Get system message if exists system_msg = thread.get_system_message() # Get messages by turn turn_messages = thread.get_messages_by_turn(3) # Get current turn number current_turn = thread.get_current_turn() # Get message by ID message = thread.get_message_by_id("msg-123") ``` ## Thread Analytics ### Token usage ```python theme={null} # Get total token usage tokens = thread.get_total_tokens() print(f"Total tokens: {tokens['overall']['total_tokens']}") print(f"By model: {tokens['by_model']}") # Get usage for specific model gpt4_usage = thread.get_model_usage("gpt-4") print(f"GPT-4 calls: {gpt4_usage['calls']}") print(f"GPT-4 tokens: {gpt4_usage['total_tokens']}") ``` ### Message statistics ```python theme={null} # Get message counts by role counts = thread.get_message_counts() print(f"User messages: {counts['user']}") print(f"Assistant messages: {counts['assistant']}") # Get timing statistics timing = thread.get_message_timing_stats() print(f"Average latency: {timing['average_latency']}ms") # Get tool usage tools = thread.get_tool_usage() print(f"Tools used: {tools['tools']}") print(f"Total tool calls: {tools['total_calls']}") # Get turns summary turns = thread.get_turns_summary() for turn_num, info in turns.items(): print(f"Turn {turn_num}: {info['message_count']} messages") ``` ## Thread Management ```python theme={null} # Generate title from first message thread.generate_title() # Sets title based on first user message # Clear all messages thread.clear_messages() # Access messages in sequence order ordered_messages = thread.get_messages_in_sequence() ``` ## Reactions Threads support emoji reactions on messages: ```python theme={null} # Add a reaction success = thread.add_reaction( message_id="msg-123", emoji="thumbsup", user_id="user-456" ) # Remove a reaction removed = thread.remove_reaction( message_id="msg-123", emoji="thumbsup", user_id="user-456" ) # Get all reactions for a message reactions = thread.get_reactions("msg-123") # Returns: {"thumbsup": ["user-456", "user-789"], "heart": ["user-456"]} ``` ## Serialization ```python theme={null} # Convert to dictionary for JSON thread_dict = thread.model_dump(mode="json") # Dates as ISO strings thread_dict = thread.model_dump(mode="python") # Dates as datetime objects # The thread is Pydantic-based, so standard serialization works import json json_str = thread.model_dump_json() ``` ## Turn Management Messages are organized into turns, representing conversation rounds: ```python theme={null} # Messages added separately get sequential turns thread.add_message(user_msg) # Turn 1 thread.add_message(assistant_msg) # Turn 2 # Messages added together share a turn thread.add_message(assistant_msg) # Turn 3 thread.add_message(tool_msg, same_turn=True) # Also turn 3 # Or use batch for multiple messages in one turn thread.add_messages_batch([msg1, msg2, msg3]) # All get same turn ``` ## Example: Building a Conversation ```python theme={null} from narrator import Thread, Message # Create thread thread = Thread(title="Technical Support") # Add system message (always turn 0) thread.add_message(Message( role="system", content="You are a helpful technical support agent." )) # User asks question (turn 1) thread.add_message(Message( role="user", content="My computer won't start. The screen is black." )) # Assistant responds with tool use (turn 2) thread.add_message(Message( role="assistant", content="I'll help you troubleshoot this issue.", tool_calls=[{ "id": "call_123", "type": "function", "function": { "name": "diagnose_computer", "arguments": '{"symptom": "black screen"}' } }] )) # Tool response (same turn as assistant) thread.add_message(Message( role="tool", name="diagnose_computer", tool_call_id="call_123", content="Possible causes: 1) Power issue, 2) Display cable..." ), same_turn=True) # Assistant final response (still same turn) thread.add_message(Message( role="assistant", content="Based on the diagnosis, let's start by checking your power connection." ), same_turn=True) # Check thread state print(f"Total messages: {len(thread.messages)}") print(f"Current turn: {thread.get_current_turn()}") print(f"Token usage: {thread.get_total_tokens()}") ``` # ThreadStore Source: https://slide.mintlify.app/api-reference/narrator-threadstore Thread storage with support for memory, SQLite, and PostgreSQL backends ## Overview The `ThreadStore` class provides a unified interface for persisting conversation threads with support for multiple storage backends: in-memory (for development), SQLite (for local storage), and PostgreSQL (for production). ## Creating a ThreadStore ### Recommended: Factory Method ```python theme={null} from narrator import ThreadStore # In-memory storage (default) store = await ThreadStore.create() # SQLite storage store = await ThreadStore.create("sqlite+aiosqlite:///threads.db") # PostgreSQL storage store = await ThreadStore.create("postgresql+asyncpg://user:pass@localhost/dbname") ``` The factory method validates the connection immediately, catching configuration errors early. ### Direct Constructor ```python theme={null} # Creates store but doesn't connect until first operation store = ThreadStore("postgresql+asyncpg://localhost/db") # Connection happens on first use thread = await store.get("thread-123") # Connects here ``` ## Storage Backends ### In-Memory (Default) ```python theme={null} # No configuration needed store = await ThreadStore.create() # Perfect for: # - Development and testing # - Temporary conversation storage # - Single-instance applications ``` ### SQLite ```python theme={null} # Local file storage store = await ThreadStore.create("sqlite+aiosqlite:///path/to/threads.db") # In-memory SQLite (for tests) store = await ThreadStore.create("sqlite+aiosqlite:///:memory:") # Perfect for: # - Desktop applications # - Single-user apps # - Local development with persistence ``` ### PostgreSQL ```python theme={null} # Production database store = await ThreadStore.create( "postgresql+asyncpg://user:password@host:5432/database" ) # With connection pooling (configured via environment) # NARRATOR_DB_POOL_SIZE=10 # NARRATOR_DB_MAX_OVERFLOW=20 # NARRATOR_DB_POOL_TIMEOUT=30 # NARRATOR_DB_POOL_RECYCLE=300 # Perfect for: # - Multi-user applications # - Production deployments # - Horizontal scaling ``` #### Quick Docker Setup For local development with PostgreSQL: ```bash theme={null} # One-command setup (starts PostgreSQL and initializes tables) uv run narrator docker-setup # Then use in your code: store = await ThreadStore.create( "postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator" ) ``` To manage the database: ```bash theme={null} # Stop container (preserves data) uv run narrator docker-stop # Stop and remove all data uv run narrator docker-stop --remove-volumes ``` ## Connection Pool Configuration Configure connection pooling via environment variables: Maximum number of connections to maintain in the pool Maximum overflow connections above pool\_size Seconds to wait for a connection from the pool Seconds after which to recycle connections ## Basic operations ### Saving Threads ```python theme={null} from narrator import Thread, Message # Create a thread thread = Thread(title="Customer Support") thread.add_message(Message(role="user", content="I need help")) thread.add_message(Message(role="assistant", content="I'm here to help!")) # Save to storage saved_thread = await store.save(thread) # Note: System messages are NOT persisted by design # They remain in memory but aren't stored in the database ``` ### Retrieving Threads ```python theme={null} # Get by ID thread = await store.get("thread-123") if thread: print(f"Found thread: {thread.title}") else: print("Thread not found") # Get or create thread = await store.get_or_create("thread-123") # Returns existing thread or creates new one with given ID ``` ### Listing Threads ```python theme={null} # Get recent threads threads = await store.list(limit=10) for thread in threads: print(f"{thread.title} - {thread.updated_at}") # Pagination page1 = await store.list(limit=20, offset=0) page2 = await store.list(limit=20, offset=20) ``` ### Searching Threads ```python theme={null} # Search in thread titles results = await store.search_by_title("customer") for thread in results: print(f"Found: {thread.title}") # Search in message content results = await store.search_by_content("refund", limit=5) for thread in results: print(f"Thread {thread.id} contains 'refund'") # Search by platform reference slack_threads = await store.search_by_platform("slack", "C123456") for thread in slack_threads: print(f"Slack thread: {thread.title}") ``` ### Deleting Threads ```python theme={null} # Delete a single thread success = await store.delete("thread-123") print(f"Deleted: {success}") # Delete multiple threads await store.delete_many(["thread-1", "thread-2", "thread-3"]) ``` ## Thread Aliases ThreadStore supports aliases - alternative IDs for threads: ```python theme={null} # Create thread with Slack channel as alias thread = Thread(title="Team Discussion") await store.save(thread) await store.save_thread_alias(thread.id, "slack-C123456") # Retrieve using alias thread = await store.get_by_alias("slack-C123456") # Manage aliases await store.delete_thread_alias("slack-C123456") aliases = await store.get_thread_aliases(thread.id) ``` ## Usage Statistics ```python theme={null} # Get storage statistics stats = await store.get_usage_stats() print(f"Total threads: {stats['total_threads']}") print(f"Total messages: {stats['total_messages']}") print(f"Average messages per thread: {stats['avg_messages_per_thread']}") # Token usage across all threads print(f"Total tokens used: {stats['total_tokens']['total']}") print(f"By model: {stats['total_tokens']['by_model']}") # Recent activity print(f"Threads updated in last hour: {stats['threads_last_hour']}") print(f"Threads updated in last 24h: {stats['threads_last_24h']}") print(f"Threads updated in last 7d: {stats['threads_last_7d']}") ``` ## Example: Multi-User Chat Application ```python theme={null} from narrator import ThreadStore, Thread, Message import os # Initialize store with PostgreSQL for production DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///chat.db") store = await ThreadStore.create(DATABASE_URL) # Create a support thread thread = Thread( title="Order #12345 Support", attributes={ "customer_id": "cust_789", "order_id": "12345", "priority": "high" }, platforms={ "slack": { "channel": "C123456", "thread_ts": "1234567890.123" } } ) # Add conversation thread.add_message(Message( role="user", content="My order hasn't arrived yet", source={"type": "user", "id": "cust_789", "name": "John Doe"} )) thread.add_message(Message( role="assistant", content="I'll check on that order for you right away.", source={"type": "agent", "id": "support_bot", "name": "Support Bot"} )) # Save thread await store.save(thread) # Create alias for easy lookup await store.save_thread_alias(thread.id, f"order-{thread.attributes['order_id']}") # Later: retrieve by order order_thread = await store.get_by_alias("order-12345") # Search for high priority threads high_priority = await store.search_by_content("priority.*high", limit=10) # Get usage stats stats = await store.get_usage_stats() print(f"Active support threads: {stats['total_threads']}") ``` ## Error handling ```python theme={null} try: store = await ThreadStore.create("postgresql+asyncpg://bad-host/db") except RuntimeError as e: print(f"Failed to connect: {e}") # Fall back to in-memory store = await ThreadStore.create() # Handle individual operation failures try: thread = await store.get("thread-123") except Exception as e: print(f"Failed to retrieve thread: {e}") ``` ## Best practices 1. **Use the factory method** (`ThreadStore.create()`) for immediate validation 2. **Configure connection pools** for production PostgreSQL deployments 3. **Use aliases** for integrating with external systems (Slack, Discord, etc.) 4. **Don't store sensitive data** in thread attributes - they're not encrypted 5. **System messages aren't persisted** - add them dynamically when needed 6. **Use search sparingly** - it can be expensive on large datasets ## Migration Between Backends ```python theme={null} # Export from SQLite sqlite_store = await ThreadStore.create("sqlite+aiosqlite:///old.db") threads = await sqlite_store.list(limit=1000) # Import to PostgreSQL pg_store = await ThreadStore.create("postgresql+asyncpg://localhost/new_db") for thread in threads: await pg_store.save(thread) print(f"Migrated {len(threads)} threads") ``` # SlackApp Source: https://slide.mintlify.app/api-reference/space-monkey-slackapp Main class for deploying Tyler agents as Slack bots ## Overview The `SlackApp` class provides a clean interface for running Tyler agents as Slack bots with intelligent message routing, thread management, and health monitoring. ## Creating a SlackApp ```python theme={null} from space_monkey import SlackApp from tyler import Agent from narrator import ThreadStore, FileStore # Initialize storage thread_store = await ThreadStore.create() file_store = await FileStore.create() # Create your agent agent = Agent( name="slack-assistant", model_name="gpt-4o", purpose="To help Slack users with their questions", tools=[...] # Optional tools ) # Create Slack app app = SlackApp( agent=agent, thread_store=thread_store, file_store=file_store, response_topics="technical questions and code help" # Optional ) # Start the app await app.start(port=3000) ``` ## Key Parameters The Tyler agent that will handle conversations ThreadStore instance for conversation persistence across Slack channels FileStore instance for handling file uploads and attachments Simple sentence describing what topics the bot should respond to. Used for intelligent message filtering when the bot is in channels. ## Environment Variables SlackApp requires these environment variables to be set: Bot User OAuth Token (starts with `xoxb-`) App-level token for Socket Mode (starts with `xapp-`) OpenAI API key for the agent (or other LLM provider keys) URL to ping for health monitoring Interval in seconds between health check pings Weights & Biases project name for Weave tracing Weights & Biases API key for Weave tracing ## Starting the App The `start()` method launches the Slack bot server: ```python theme={null} await app.start( host="0.0.0.0", # Host to bind to port=8000 # Port to listen on ) ``` Host address to bind the server to Port number for the server ## Message Routing SlackApp automatically handles different types of Slack events: ### Direct messages All direct messages to the bot are processed automatically. ### Channel messages In channels, the bot responds to: * Messages where the bot is @mentioned * Thread replies where the bot has previously participated * Messages matching the configured `response_topics` (if set) ### Thread management Each Slack channel gets its own persistent conversation thread, maintaining context across messages. ## Features ### Intelligent message classification When `response_topics` is configured, SlackApp uses an AI classifier to determine whether to respond to messages based on relevance. ### File handling Automatically downloads and processes files shared in Slack when the agent has file-handling tools. ### Health monitoring Built-in health check endpoint at `/health` and optional external health check pings. ### Graceful shutdown Proper signal handling for clean shutdown in containerized environments. ## Example with Advanced Features ```python theme={null} import os from space_monkey import SlackApp from tyler import Agent from narrator import ThreadStore, FileStore from lye import WEB_TOOLS, IMAGE_TOOLS async def create_advanced_bot(): # PostgreSQL persistence thread_store = await ThreadStore.create( os.getenv("DATABASE_URL", "postgresql://localhost/slackbot") ) file_store = await FileStore.create("./slack_files") # Create agent with tools agent = Agent( name="team-assistant", model_name="gpt-4o", purpose="To help the team with research and image analysis", tools=[*WEB_TOOLS, *IMAGE_TOOLS] ) # Create app with topic filtering app = SlackApp( agent=agent, thread_store=thread_store, file_store=file_store, response_topics="technical questions, research requests, and image analysis" ) return app # Run the bot app = await create_advanced_bot() await app.start(port=3000) ``` ## Server Endpoints SlackApp runs a FastAPI server with these endpoints: ### `GET /health` Health check endpoint returning server status. ### `GET /` Root endpoint with basic server information. ## Error handling SlackApp includes comprehensive error handling: * Automatic retries for transient Slack API errors * Graceful handling of missing threads or messages * Detailed logging for debugging * User-friendly error messages in Slack ## Docker Support SlackApp is designed for easy containerization: ```dockerfile theme={null} FROM python:3.13-slim WORKDIR /app # Install uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY . . RUN uv sync --frozen CMD ["uv", "run", "your_bot.py"] ``` See the [Slack Agent Guide](/apps/slack-agent) for complete deployment instructions. # Agent Source: https://slide.mintlify.app/api-reference/tyler-agent Core agent class for building AI assistants ## Overview The `Agent` class is the central component of Tyler, providing a flexible interface for creating AI agents with tool use, delegation capabilities, and conversation management. ## Creating an Agent ```python theme={null} from tyler import Agent agent = Agent( name="MyAssistant", model_name="gpt-4o", purpose="To help users with their tasks", temperature=0.7, tools=[...], # Optional tools agents=[...] # Optional sub-agents for delegation ) ``` ## All Parameters The name of your agent. This is used in the system prompt to give the agent an identity. The LLM model to use. Supports any LiteLLM compatible model including OpenAI, Anthropic, Gemini, and more. The agent's purpose or system prompt. Can be a string or a Tyler Prompt object for more complex prompts. Controls randomness in responses. Range is 0.0 to 2.0, where lower values make output more focused and deterministic. Whether to automatically drop unsupported parameters for specific models. When True, parameters like temperature are automatically removed for models that don't support them (e.g., O-series models). This ensures seamless compatibility across different model providers without requiring model-specific configuration. List of tools available to the agent. Can include: * Direct tool function references (callables) * Tool module namespaces (modules like `web`, `files`) * Built-in tool module names (strings like `"web"`, `"files"`) * Custom tool definitions (dicts with 'definition', 'implementation', and optional 'attributes' keys) For module names, you can specify specific tools using `'module:tool1,tool2'` format. List of sub-agents that this agent can delegate tasks to. Enables multi-agent systems and task delegation. Maximum number of tool calls allowed per conversation turn. Prevents infinite loops in tool usage. Custom API base URL for the model provider (e.g., for using alternative inference services). You can also use `base_url` as an alias for this parameter. Alias for `api_base`. Either parameter can be used to specify a custom API endpoint. API key for the model provider. If not provided, LiteLLM will use environment variables (e.g., OPENAI\_API\_KEY, WANDB\_API\_KEY). Use this when you need to explicitly pass an API key, such as with W\&B Inference or custom providers. Additional headers to include in API requests. Useful for authentication tokens, API keys, or tracking headers. Enable reasoning/thinking tokens for supported models (OpenAI o1/o3, DeepSeek-R1, Claude with extended thinking). * String: `'low'`, `'medium'`, `'high'` (recommended for most use cases) * Dict: Provider-specific config (e.g., `{'type': 'enabled', 'budget_tokens': 1024}` for Anthropic) When enabled, the model will show its internal reasoning process before generating the final answer. If True, the `step()` method will raise exceptions instead of returning error messages. Used for backward compatibility and custom error handling. Supporting notes to help the agent accomplish its purpose. These are included in the system prompt and can provide additional context or instructions. Version identifier for the agent. Useful for tracking agent iterations and changes. Thread store instance for managing conversation threads. If not provided, uses the default thread store. This parameter is excluded from serialization. File store instance for managing file attachments. If not provided, uses the default file store. This parameter is excluded from serialization. Custom message factory for creating standardized messages. Advanced users can provide a custom implementation to control message formatting and structure. If not provided, uses the default factory. This parameter is excluded from serialization (recreated on deserialization). Custom completion handler for LLM communication. Advanced users can provide a custom implementation to modify how the agent communicates with LLMs. If not provided, uses the default handler. This parameter is excluded from serialization (recreated on deserialization). Optional Pydantic model to enforce structured output from the LLM. Uses the **output-tool pattern**: your Pydantic schema is registered as a special tool, and `tool_choice="required"` forces the model to call it. The validated model instance is available in `AgentResult.structured_data`. This approach allows regular tools to work alongside structured output. LiteLLM automatically translates `tool_choice` for different providers (OpenAI, Anthropic, Gemini, Bedrock, etc.). Can be set at the agent level (default for all runs) or overridden per `run()` call. See the [Structured Output Guide](/guides/structured-output) for details. Configuration for automatic retry behavior. When enabled with structured output, the agent will automatically retry LLM calls if validation fails, providing error feedback to help the LLM correct its output. ```python theme={null} from tyler import RetryConfig agent = Agent( retry_config=RetryConfig( max_retries=3, retry_on_validation_error=True, backoff_base_seconds=1.0 ) ) ``` See [RetryConfig](/api-reference/tyler-retryconfig) for all options. Default request identity passed to tools via the `ctx` parameter. Primarily used for user/org/session identity that answers "who is making this request?" Can be set at the agent level (for system agents or default identity) or per `run()` call (typical for user requests). When both are provided, run-level merges with and overrides agent-level for conflicting keys. ```python theme={null} # System agent with fixed identity system_agent = Agent( model_name="gpt-4o", tool_context={"user_id": "system", "role": "admin"} ) # User-facing agent - identity passed per request agent = Agent(model_name="gpt-4o", tools=my_tools) await agent.run(thread, tool_context={ "user_id": request.user.id, "org_id": request.user.org_id, "permissions": request.user.permissions }) ``` Infrastructure (database clients, API clients) should be closed over at tool definition time, not passed in context. See [ToolContext](/api-reference/tyler-toolcontext) for the recommended pattern. Simple JSON mode for when you want any valid JSON without schema validation. Pass `response_format="json"` to `run()` to force JSON output. ```python theme={null} # Get any valid JSON (no schema validation) result = await agent.run(thread, response_format="json") data = json.loads(result.content) # Parse yourself ``` Cannot be used with `response_type`. Use `response_type` for schema validation, or `response_format="json"` for simple JSON without validation. ## Creating from Config Files Create an Agent from a YAML configuration file. Enables reusing the same configuration between CLI and Python code. ```python theme={null} from tyler import Agent # Auto-discover config (searches standard locations) agent = Agent.from_config() # Load from specific path agent = Agent.from_config("my-config.yaml") # With parameter overrides agent = Agent.from_config( "config.yaml", temperature=0.9, model_name="gpt-4o" ) ``` ### Parameters Path to YAML config file (.yaml or .yml). If None, searches standard locations: 1. `./tyler-chat-config.yaml` (current directory) 2. `~/.tyler/chat-config.yaml` (user home) 3. `/etc/tyler/chat-config.yaml` (system-wide) Override any config values. These replace (not merge) config file values using shallow dict update semantics. Examples: * `tools=["web"]` replaces entire tools list * `temperature=0.9` replaces temperature value * `mcp={...}` replaces entire mcp dict (not merged) ### Config File Format ```yaml theme={null} # Agent Identity name: "MyAgent" purpose: "To help with tasks" notes: "Additional instructions" # Model Configuration model_name: "gpt-4.1" temperature: 0.7 max_tool_iterations: 10 reasoning: "low" # For models supporting thinking tokens # Tools tools: - "web" # Built-in tool module - "slack" - "./my_tools.py" # Custom tool file (relative to config) # MCP Servers (optional) mcp: servers: - name: "docs" transport: "streamablehttp" url: "https://example.com/mcp" timeout_seconds: 10 sse_read_timeout_seconds: 300 tool_timeout_seconds: 30 include_tools: ["search"] headers: Authorization: "Bearer ${MCP_TOKEN}" - name: "local_files" transport: "stdio" command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] cwd: "/tmp" encoding: "utf-8" # Environment variables (keeps secrets safe) api_key: "${OPENAI_API_KEY}" # Reads from environment ``` Config files use the same format as `tyler-chat` CLI configs, so you can share configurations between interactive CLI sessions and your Python code. ### Advanced Config Loading For more control over config loading, use `load_config()` directly: ```python theme={null} from tyler import load_config, Agent # Load config into dict config = load_config("config.yaml") # Inspect and modify before creating agent print(f"Model: {config['model_name']}") config["temperature"] = 0.9 config["notes"] += "\nModified programmatically" # Create agent from modified config agent = Agent(**config) ``` ## Processing Conversations Use `stream(...)` for interactive agent applications. It yields real-time events while the LLM and tools run, so callers can render partial text, show tool activity, and collect progress metadata as the agent works. Use `run(...)` when you want to wait for completion and inspect the final `AgentResult`. The backwards-compatible `go(...)` alias still maps to `run(...)`. ### Event Streaming Mode Stream responses as high-level `ExecutionEvent` objects with full observability: ```python theme={null} from tyler import EventType async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: print(event.data["content_chunk"], end="", flush=True) elif event.type == EventType.TOOL_SELECTED: print(f"Using tool: {event.data['tool_name']}") elif event.type == EventType.TOOL_RESULT: print(f"Tool result: {event.data['result']}") elif event.type == EventType.EXECUTION_COMPLETE: print(f"Done in {event.data['duration_ms']}ms") ``` ### Non-Streaming Mode ```python theme={null} from tyler import Thread, Message # Create a conversation thread thread = Thread() thread.add_message(Message(role="user", content="Hello!")) # Process the thread result = await agent.run(thread) # Access the response print(result.content) # The agent's final response print(result.thread) # Updated thread with all messages print(result.new_messages) # New messages added in this turn print(result.execution.total_tokens) # Execution summary ``` ### With Structured Output Get type-safe, validated responses using Pydantic models: ```python theme={null} from pydantic import BaseModel from tyler import Agent, Thread, Message, RetryConfig class SupportTicket(BaseModel): priority: str category: str summary: str agent = Agent( name="classifier", model_name="gpt-4o", retry_config=RetryConfig(max_retries=2) # Retry on validation failure ) thread = Thread() thread.add_message(Message(role="user", content="My payment failed!")) # Get structured output result = await agent.run(thread, response_type=SupportTicket) # Access the validated Pydantic model ticket: SupportTicket = result.structured_data print(f"Priority: {ticket.priority}") print(f"Category: {ticket.category}") ``` ### With Tool Context (Request Identity) Pass request-scoped identity to your tools: ```python theme={null} # Define tools with infrastructure closed over def create_order_tools(db): async def get_user_orders(ctx: ToolContext, limit: int = 10) -> str: user_id = ctx["user_id"] # Identity from context org_id = ctx["org_id"] # Tenant isolation orders = await db.get_orders(user_id, org_id, limit) return f"Found {len(orders)} orders" return [get_user_orders] # Create agent with tools (db already closed over) agent = Agent(model_name="gpt-4o", tools=create_order_tools(database)) # Run with identity context result = await agent.run( thread, tool_context={ "user_id": current_user.id, "org_id": current_user.org_id, "permissions": current_user.permissions } ) ``` See [ToolContext](/api-reference/tyler-toolcontext) for the recommended pattern. ### OpenAI Streaming Mode (mode="openai") Raw mode is for advanced use cases requiring OpenAI compatibility. Tools ARE executed, but you only receive raw LiteLLM chunks (no ExecutionEvents for observability). Stream raw LiteLLM chunks in OpenAI-compatible format for direct integration: ```python theme={null} # Get raw chunks for OpenAI compatibility async for chunk in agent.stream(thread, mode="openai"): # chunk is a raw LiteLLM object with OpenAI structure if hasattr(chunk, 'choices') and chunk.choices: delta = chunk.choices[0].delta # Access content if hasattr(delta, 'content') and delta.content: print(delta.content, end="", flush=True) # Tool call deltas are visible in raw chunks. Tyler executes tools # between LLM iterations, then streams the next LLM response. if hasattr(delta, 'tool_calls') and delta.tool_calls: print(f"\nTool call delta: {delta.tool_calls}") # Usage info in final chunk if hasattr(chunk, 'usage') and chunk.usage: print(f"\nTokens: {chunk.usage.total_tokens}") ``` **When to use openai mode:** * Building OpenAI API proxies or gateways * Direct integration with OpenAI-compatible clients * Debugging provider-specific behavior * Minimal latency requirements (no transformation overhead) **How openai mode works:** * ✅ Tools ARE executed (fully agentic behavior) * ✅ Multi-turn iteration supported (continues until task complete) * ✅ Raw chunks show tool calls via `finish_reason: "tool_calls"` * ⚠️ No ExecutionEvent telemetry (only raw chunks) * ⚠️ Silent during tool execution (brief pauses between chunk streams) * ⚠️ Consumer handles chunk formatting (SSE serialization, etc.) The pattern matches [OpenAI's Agents SDK](https://openai.github.io/openai-agents-python/streaming/): chunks → finish\_reason="tool\_calls" → \[tools execute silently] → more chunks → repeat until done See the [streaming guide](/guides/streaming-responses) for more details and examples. ### Vercel AI SDK Streaming Mode (mode="vercel") Perfect for React/Next.js frontends using `@ai-sdk/react`'s `useChat` hook. Stream responses as SSE-formatted strings compatible with the [Vercel AI SDK Data Stream Protocol](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol): ```python theme={null} from tyler import VERCEL_STREAM_HEADERS # Stream in Vercel AI SDK format async for sse_chunk in agent.stream(thread, mode="vercel"): print(sse_chunk, end="") # Pre-formatted SSE strings # In a FastAPI endpoint: from fastapi.responses import StreamingResponse @app.post("/api/chat") async def chat(request: Request): # ... setup thread from request ... async def generate(): async for sse_chunk in agent.stream(thread, mode="vercel"): yield sse_chunk return StreamingResponse( generate(), media_type="text/event-stream", headers=VERCEL_STREAM_HEADERS # Includes x-vercel-ai-ui-message-stream: v1 ) ``` **When to use vercel mode:** * Building React/Next.js chat interfaces with `@ai-sdk/react` * Direct integration with Vercel's AI SDK ecosystem * Need SSE streams compatible with `useChat` hook **How vercel mode works:** * ✅ Tools ARE executed (fully agentic behavior) * ✅ Thinking/reasoning tokens supported * ✅ Pre-formatted SSE strings ready for HTTP response * ✅ Compatible with `x-vercel-ai-ui-message-stream: v1` protocol See the [streaming guide](/guides/streaming-responses#vercel-ai-sdk-streaming) for complete examples including React frontend code. ## Return Values ### AgentResult (Non-Streaming) ```python theme={null} @dataclass class AgentResult: thread: Thread # Updated thread with all messages new_messages: List[Message] # New messages from this execution content: Optional[str] # The final assistant response structured_data: Optional[BaseModel] # Validated Pydantic model (if response_type used) validation_retries: int = 0 # Structured output retry count retry_history: Optional[List[Dict[str, Any]]] = None execution: ExecutionDetails # Events, duration, tokens, tool summaries ``` `execution` is appended after the existing optional fields to preserve positional `AgentResult(...)` compatibility. `result.success` is `True` when no `EXECUTION_ERROR` event was emitted. When using `response_type`, the `structured_data` field contains the validated Pydantic model instance. See [AgentResult](/api-reference/tyler-agentresult) for full documentation. ### ExecutionEvent (Streaming) ```python theme={null} @dataclass class ExecutionEvent: type: EventType # Type of event timestamp: datetime # When the event occurred data: Dict[str, Any] # Event-specific data attributes: Optional[Dict[str, Any]] # Additional metadata ``` ## Event Types * `ITERATION_START` - New iteration beginning * `LLM_REQUEST` - Request sent to LLM * `LLM_RESPONSE` - Complete response received * `LLM_STREAM_CHUNK` - Streaming content chunk * `TOOL_SELECTED` - Tool about to be called * `TOOL_RESULT` - Tool execution completed * `TOOL_ERROR` - Tool execution failed * `MESSAGE_CREATED` - New message added * `EXECUTION_COMPLETE` - All processing done * `EXECUTION_ERROR` - Processing failed * `ITERATION_LIMIT` - Max iterations reached ## Execution Details `AgentResult.execution` provides the full execution summary: ```python theme={null} print(f"Success: {result.success}") print(f"Duration: {result.execution.duration_ms:.0f}ms") print(f"Total tokens: {result.execution.total_tokens}") for tool_call in result.execution.tool_calls: print(f"{tool_call.tool_name}: {tool_call.result or tool_call.error}") ``` ## Working with Tools ```python theme={null} from lye import WEB_TOOLS, FILES_TOOLS agent = Agent( name="ResearchAssistant", model_name="gpt-4o", purpose="To research topics and create reports", tools=[*WEB_TOOLS, *FILES_TOOLS] ) # The agent can now browse the web and work with files result = await agent.run(thread) # Check which tools were used for tool_call in result.execution.tool_calls: print(f"Used {tool_call.tool_name}: {tool_call.result}") ``` ## Agent Delegation ```python theme={null} researcher = Agent( name="Researcher", purpose="To find information", tools=[*WEB_TOOLS] ) writer = Agent( name="Writer", purpose="To create content", tools=[*FILES_TOOLS] ) coordinator = Agent( name="Coordinator", purpose="To manage research and writing tasks", agents=[researcher, writer] # Can delegate to these agents ) # The coordinator can now delegate tasks result = await coordinator.run(thread) ``` ## Custom Configuration ### Using custom API endpoints ```python theme={null} # Use a custom API endpoint agent = Agent( model_name="gpt-4", api_base="https://your-api.com/v1", extra_headers={"Authorization": "Bearer token"} ) ``` ### W\&B Inference configuration ```python theme={null} import os # Use W&B Inference with DeepSeek-R1 (thinking tokens) agent = Agent( model_name="openai/deepseek-ai/DeepSeek-R1-0528", base_url="https://api.inference.wandb.ai/v1", api_key=os.getenv("WANDB_API_KEY"), # Your W&B API key extra_headers={ "HTTP-Referer": "https://wandb.ai/my-team/my-project", "X-Project-Name": "my-team/my-project" }, reasoning="low", # Enable thinking tokens temperature=0.7 ) ``` For W\&B Inference, you can also use YAML config with environment variable substitution: ```yaml theme={null} model_name: "openai/deepseek-ai/DeepSeek-R1-0528" base_url: "https://api.inference.wandb.ai/v1" api_key: "${WANDB_API_KEY}" # Reads from environment ``` ### Custom storage configuration ```python theme={null} from narrator import ThreadStore, FileStore agent = Agent( thread_store=ThreadStore(backend="postgresql"), file_store=FileStore(path="/custom/path") ) ``` ### Weave Tracing and Serialization Tyler Agent uses `@weave.op()` decorators for comprehensive tracing. When you initialize Weave, all agent method calls are automatically traced and logged to your Weave dashboard. ```python theme={null} import weave from tyler import Agent # Initialize Weave - enables automatic tracing weave.init("my-project") # Create an agent agent = Agent( name="MyAgent", model_name="gpt-4o", temperature=0.7 ) # Agent calls and tool calls are traced to Weave. When the installed Weave # version supports Agents tracing, Tyler also emits session, turn, LLM, and # tool spans. result = await agent.run(thread) # View traces at: https://wandb.ai//my-project/weave ``` **Don't use `weave.publish(agent)`** - Published Agent objects cannot be retrieved and used (Weave returns an unusable `ObjectRecord`). For reproducibility, publish your configuration instead: ```python theme={null} # ✅ DO: Publish config dict for reproducibility config = { "name": "MyAgent", "model_name": "gpt-4o", "temperature": 0.7, "tools": ["web", "files"] } weave.publish(config, name="my-agent-config") # Later, retrieve and recreate config_ref = weave.ref("my-agent-config:v1").get() agent = Agent(**config_ref) # ❌ DON'T: Publish agent object directly # weave.publish(agent) # The result cannot be used! ``` #### Pydantic Serialization Agents inherit from `pydantic.BaseModel` and support standard Pydantic serialization: ```python theme={null} # Serialize to dict agent_dict = agent.model_dump() # Serialize to JSON agent_json = agent.model_dump_json() # Deserialize (helper objects are automatically recreated) restored_agent = Agent(**agent_dict) # restored_agent works exactly like the original result = await restored_agent.run(thread) ``` The following attributes are excluded from serialization and automatically recreated: * `thread_store` - Database connections * `file_store` - File system state * `message_factory` - Message creation helper * `completion_handler` - LLM communication helper If you provide custom helpers, they will be preserved during initialization but not serialized. ## Best practices 1. **Clear Purpose**: Define a specific, focused purpose for each agent 2. **Tool Selection**: Only include tools the agent actually needs 3. **Temperature**: Use lower values (0.0-0.3) for consistency, higher (0.7-1.0) for creativity 4. **Error Handling**: Always handle potential errors in production 5. **Token Limits**: Monitor token usage to avoid hitting limits 6. **Streaming**: Use streaming for better user experience in interactive applications ## Example: Complete Application ```python theme={null} import asyncio from tyler import Agent, Thread, Message, EventType from lye import WEB_TOOLS async def main(): # Create agent agent = Agent( name="WebAssistant", model_name="gpt-4o", purpose="To help users find information online", tools=WEB_TOOLS, temperature=0.3 ) # Create thread thread = Thread() # Add user message thread.add_message(Message( role="user", content="What's the latest news about AI?" )) # Process with streaming print("Assistant: ", end="", flush=True) async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: print(event.data["content_chunk"], end="", flush=True) elif event.type == EventType.TOOL_SELECTED: print(f"\n[Searching: {event.data['tool_name']}...]\n", end="", flush=True) print("\n") if __name__ == "__main__": asyncio.run(main()) ``` # AgentResult Source: https://slide.mintlify.app/api-reference/tyler-agentresult Result object from non-streaming agent execution ## Overview The `AgentResult` class encapsulates the complete result of an agent's execution in non-streaming mode. It provides access to the updated thread, new messages, final output, structured output data, and execution telemetry. ## Class Definition ```python theme={null} @dataclass class AgentResult: thread: Thread # Updated thread with new messages new_messages: List[Message] # New messages added during execution content: Optional[str] # Final assistant response content structured_data: Optional[BaseModel] # Validated Pydantic model (if response_type used) validation_retries: int = 0 # Structured output retry count retry_history: Optional[List[Dict[str, Any]]] = None execution: ExecutionDetails # Events, duration, tokens, and tool summaries ``` `execution` is appended after the existing optional fields so existing positional `AgentResult(...)` construction keeps the same argument order. In application code, prefer reading fields by name from the object returned by `agent.run(...)`. ## Properties The updated thread containing all messages including those added during this execution List of new messages added during this execution (excludes pre-existing messages) The final assistant response content. None if no assistant message was generated When `response_type` is provided to `agent.run()`, this field contains a validated instance of the Pydantic model. Internally, the agent uses the **output-tool pattern**: your schema becomes a special tool, and when the LLM calls it, the arguments are validated against your model. ```python theme={null} from pydantic import BaseModel class Invoice(BaseModel): vendor: str total: float result = await agent.run(thread, response_type=Invoice) invoice: Invoice = result.structured_data print(f"Vendor: {invoice.vendor}, Total: ${invoice.total}") ``` Returns `None` if `response_type` was not provided. See the [Structured Output Guide](/guides/structured-output) for complete usage. Number of structured output validation retry attempts. This is only relevant when `response_type` and retry configuration are used. Validation retry details, including attempt numbers, validation errors, and response previews. Execution telemetry collected during this run, including: * `events`: ordered `ExecutionEvent` objects * `duration_ms`: total execution time * `total_tokens`: total token usage reported by the LLM * `tool_calls`: structured tool call summaries with name, arguments, result or error, duration, and success Property that returns `True` when the execution completed without any `EXECUTION_ERROR` events. ## Usage Examples ### Basic Usage ```python theme={null} from tyler import Agent, Thread, Message agent = Agent(name="MyAgent", purpose="To help users") thread = Thread() thread.add_message(Message(role="user", content="Hello!")) # Execute and get result result = await agent.run(thread) # Access the response print(f"Response: {result.content}") ``` ### Accessing Metrics ```python theme={null} print(f"Success: {result.success}") print(f"Tokens used: {result.execution.total_tokens}") print(f"Duration: {result.execution.duration_ms:.0f}ms") for tool_call in result.execution.tool_calls: print(f"{tool_call.tool_name}: {tool_call.result or tool_call.error}") ``` ### Working with Messages ```python theme={null} # Access all new messages for message in result.new_messages: print(f"{message.role}: {message.content}") # Check for tool calls if message.tool_calls: print(f" Tool calls: {len(message.tool_calls)}") # Check metrics if message.metrics: tokens = message.metrics.get("usage", {}) print(f" Tokens: {tokens.get('total_tokens', 0)}") ``` ### Error Handling ```python theme={null} try: result = await agent.run(thread) print(f"Response: {result.content}") except Exception as e: print(f"Error: {e}") # The thread may still have partial messages if thread.messages: last_message = thread.messages[-1] print(f"Last message: {last_message.content}") ``` ### Thread Management ```python theme={null} # The thread is updated in-place original_message_count = len(thread.messages) result = await agent.run(thread) new_message_count = len(result.thread.messages) print(f"Added {new_message_count - original_message_count} messages") # You can also access the thread directly assert result.thread is thread # Same object, modified in-place ``` ## Common Patterns ### Conversation Loop ```python theme={null} async def chat_loop(agent: Agent, thread: Thread): while True: user_input = input("You: ") if user_input.lower() == 'quit': break thread.add_message(Message(role="user", content=user_input)) result = await agent.run(thread) print(f"Assistant: {result.content}") print(f"(Took {result.execution.duration_ms:.0f}ms)") ``` ### Result Analysis ```python theme={null} def analyze_result(result: AgentResult): """Analyze agent execution results""" stats = { "success": result.success, "duration_ms": result.execution.duration_ms, "tokens": result.execution.total_tokens, "tool_calls": len(result.execution.tool_calls), "messages_added": len(result.new_messages), "has_content": result.content is not None, "used_tools": bool(result.execution.tool_calls), } return stats ``` ### Persisting Results ```python theme={null} from narrator import ThreadStore store = ThreadStore() # Execute agent result = await agent.run(thread) # Save the updated thread await store.save(result.thread) await store.update_metadata( thread_id=result.thread.id, metadata={ "last_execution_ms": result.execution.duration_ms, "last_tokens_used": result.execution.total_tokens, "last_response": result.content, "last_success": result.success, } ) ``` ## Structured Output Usage When using structured output, access the validated data through `structured_data`: ```python theme={null} from pydantic import BaseModel, Field from typing import List, Literal class SupportTicket(BaseModel): priority: Literal["low", "medium", "high"] category: str summary: str = Field(max_length=500) requires_escalation: bool # Run with response_type result = await agent.run(thread, response_type=SupportTicket) # Access structured data if result.structured_data: ticket: SupportTicket = result.structured_data print(f"Priority: {ticket.priority}") print(f"Category: {ticket.category}") print(f"Needs escalation: {ticket.requires_escalation}") # Raw content is still available print(f"Raw JSON: {result.content}") ``` ### Handling Errors When structured output validation fails after all retries, `StructuredOutputError` is raised: ```python theme={null} from tyler import StructuredOutputError try: result = await agent.run(thread, response_type=SupportTicket) except StructuredOutputError as e: print(f"Failed: {e.message}") print(f"Validation errors: {e.validation_errors}") print(f"Last response: {e.last_response}") ``` See the [Structured Output Guide](/guides/structured-output) for complete documentation. ## See Also * [Thread](/api-reference/narrator-thread) - Conversation management * [Agent](/api-reference/tyler-agent) - The main agent class * [RetryConfig](/api-reference/tyler-retryconfig) - Retry configuration for structured output * [StructuredOutputError](/api-reference/tyler-structuredoutputerror) - Error when validation fails * [ExecutionEvent](/api-reference/tyler-executionevent) - Individual execution events used by streaming and `AgentResult.execution.events` # EventType Source: https://slide.mintlify.app/api-reference/tyler-eventtype Enumeration of all event types emitted during agent execution ## Overview The `EventType` enum defines all possible events that can be emitted during agent execution. These events provide granular visibility into the agent's processing, enabling real-time streaming, debugging, and monitoring. ## Event Categories ### LLM Interaction Events Emitted when a request is sent to the language model. **Event Data:** * `message_count` (int): Number of messages in the context * `model` (str): The model being used * `temperature` (float): Temperature setting for the request Emitted when a complete response is received from the language model. **Event Data:** * `content` (str): The response content * `tool_calls` (List\[Dict]): Any tool calls in the response * `tokens` (Dict): Token usage with `prompt_tokens`, `completion_tokens`, `total_tokens` * `latency_ms` (float): Response time in milliseconds Emitted for each chunk of content during streaming responses. **Event Data:** * `content_chunk` (str): The partial content chunk ### Tool execution events Emitted when a tool is selected for execution. **Event Data:** * `tool_name` (str): Name of the selected tool * `arguments` (Dict): Arguments passed to the tool * `tool_call_id` (str): Unique identifier for this tool call Emitted when tool execution begins. **Event Data:** * `tool_name` (str): Name of the executing tool * `tool_call_id` (str): Tool call identifier Emitted when a tool execution completes successfully. **Event Data:** * `tool_name` (str): Name of the tool * `result` (Any): The tool's return value * `duration_ms` (float): Execution time in milliseconds * `tool_call_id` (str): Tool call identifier Emitted when a tool execution fails. **Event Data:** * `tool_name` (str): Name of the tool * `error` (str): Error message * `tool_call_id` (str): Tool call identifier ### Message Management Events Emitted when a new message is added to the thread. **Event Data:** * `message` (Message): The complete message object ### Control Flow Events Emitted at the beginning of each agent iteration. **Event Data:** * `iteration_number` (int): Current iteration number (0-based) * `max_iterations` (int): Maximum allowed iterations Emitted when the maximum iteration limit is reached. **Event Data:** * `iterations_used` (int): Total number of iterations used Emitted when an error occurs during execution. **Event Data:** * `error_type` (str): Type of error (e.g., exception class name) * `message` (str): Error message * `traceback` (Optional\[str]): Stack trace if available Emitted when agent execution completes. **Event Data:** * `duration_ms` (float): Total execution time in milliseconds * `total_tokens` (int): Total tokens used across all LLM calls ## Usage Examples ### Basic Event Handling ```python theme={null} from tyler import Agent, Thread, EventType async def handle_events(agent: Agent, thread: Thread): async for event in agent.stream(thread): match event.type: case EventType.LLM_STREAM_CHUNK: print(event.data["content_chunk"], end="") case EventType.TOOL_SELECTED: print(f"\nUsing tool: {event.data['tool_name']}") case EventType.EXECUTION_ERROR: print(f"\nError: {event.data['message']}") ``` ### Event Counting ```python theme={null} from collections import Counter async def count_events(agent: Agent, thread: Thread) -> Counter: event_counts = Counter() async for event in agent.stream(thread): event_counts[event.type] += 1 return event_counts # Usage counts = await count_events(agent, thread) print(f"LLM requests: {counts[EventType.LLM_REQUEST]}") print(f"Tool calls: {counts[EventType.TOOL_SELECTED]}") ``` ### Performance Monitoring ```python theme={null} async def monitor_performance(agent: Agent, thread: Thread): metrics = { "llm_requests": 0, "tool_calls": 0, "errors": 0, "total_latency_ms": 0 } async for event in agent.stream(thread): if event.type == EventType.LLM_REQUEST: metrics["llm_requests"] += 1 elif event.type == EventType.LLM_RESPONSE: metrics["total_latency_ms"] += event.data["latency_ms"] elif event.type == EventType.TOOL_SELECTED: metrics["tool_calls"] += 1 elif event.type == EventType.EXECUTION_ERROR: metrics["errors"] += 1 return metrics ``` ### Custom Event Handlers ```python theme={null} class EventHandler: def __init__(self): self.handlers = { EventType.LLM_REQUEST: self.on_llm_request, EventType.TOOL_SELECTED: self.on_tool_selected, EventType.EXECUTION_ERROR: self.on_error, EventType.EXECUTION_COMPLETE: self.on_complete } async def handle(self, event: ExecutionEvent): handler = self.handlers.get(event.type) if handler: await handler(event) async def on_llm_request(self, event: ExecutionEvent): print(f"🤖 Thinking with {event.data['model']}...") async def on_tool_selected(self, event: ExecutionEvent): print(f"🔧 Using {event.data['tool_name']}") async def on_error(self, event: ExecutionEvent): print(f"❌ Error: {event.data['message']}") async def on_complete(self, event: ExecutionEvent): print(f"✅ Done in {event.data['duration_ms']:.0f}ms") # Usage handler = EventHandler() async for event in agent.stream(thread): await handler.handle(event) ``` ## Event Flow The typical sequence of events during agent execution: 1. `ITERATION_START` - Processing begins 2. `LLM_REQUEST` - Request sent to language model 3. `LLM_STREAM_CHUNK` (multiple) - If streaming, content chunks arrive 4. `LLM_RESPONSE` - Complete response received 5. `MESSAGE_CREATED` - Assistant message added to thread 6. If tool calls: * `TOOL_SELECTED` - For each tool to be called * `TOOL_EXECUTING` - Tool execution begins * `TOOL_RESULT` or `TOOL_ERROR` - Tool completes * `MESSAGE_CREATED` - Tool message added 7. Repeat from step 2 if more iterations needed 8. `EXECUTION_COMPLETE` - All processing finished ## See Also * [ExecutionEvent](/api-reference/tyler-executionevent) - The event object structure * [Agent](/api-reference/tyler-agent) - Agent streaming documentation * [Thread](/api-reference/tyler-thread) - Thread methods for accessing execution information # ExecutionEvent Source: https://slide.mintlify.app/api-reference/tyler-executionevent Atomic unit of execution information emitted during agent processing ## Overview The `ExecutionEvent` class represents individual events that occur during agent execution. These events provide real-time visibility into what the agent is doing, making them essential for streaming responses, debugging, and monitoring. ## Class Definition ```python theme={null} @dataclass class ExecutionEvent: type: EventType # The type of event timestamp: datetime # When the event occurred data: Dict[str, Any] # Event-specific data metadata: Optional[Dict[str, Any]] = None # Additional metadata ``` ## Properties The type of event that occurred (e.g., LLM\_REQUEST, TOOL\_SELECTED) UTC timestamp of when the event occurred Event-specific data. Structure varies by event type Optional additional metadata for the event ## Event Types and Data Each event type includes specific data fields: ### LLM Events ```python theme={null} # LLM_REQUEST { "message_count": int, # Number of messages in context "model": str, # Model being used "temperature": float # Temperature setting } # LLM_RESPONSE { "content": str, # Response content "tool_calls": List[Dict], # Tool calls if any "tokens": { # Token usage "prompt_tokens": int, "completion_tokens": int, "total_tokens": int }, "latency_ms": float # Response time } # LLM_STREAM_CHUNK { "content_chunk": str # Streaming content chunk } ``` ### Tool Events ```python theme={null} # TOOL_SELECTED { "tool_name": str, # Name of the tool "arguments": Dict, # Tool arguments "tool_call_id": str # Unique tool call ID } # TOOL_RESULT { "tool_name": str, # Name of the tool "result": Any, # Tool execution result "duration_ms": float, # Execution time "tool_call_id": str # Tool call ID } # TOOL_ERROR { "tool_name": str, # Name of the tool "error": str, # Error message "tool_call_id": str # Tool call ID } ``` ### Message Events ```python theme={null} # MESSAGE_CREATED { "message": Message # The created message object } ``` ### Control Flow Events ```python theme={null} # ITERATION_START { "iteration_number": int, # Current iteration "max_iterations": int # Maximum allowed } # ITERATION_LIMIT { "iterations_used": int # Total iterations used } # EXECUTION_ERROR { "error_type": str, # Type of error "message": str, # Error message "traceback": Optional[str] # Stack trace if available } # EXECUTION_COMPLETE { "duration_ms": float, # Total execution time "total_tokens": int # Total tokens used } ``` ## Usage Examples ### Streaming Responses ```python theme={null} from tyler import Agent, Thread, EventType async def stream_response(agent: Agent, thread: Thread): async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: # Print content as it arrives print(event.data["content_chunk"], end="", flush=True) elif event.type == EventType.TOOL_SELECTED: print(f"\n[Using {event.data['tool_name']}...]\n") elif event.type == EventType.EXECUTION_COMPLETE: print(f"\n[Done in {event.data['duration_ms']:.0f}ms]") ``` ### Event Logging ```python theme={null} import logging async def log_execution(agent: Agent, thread: Thread): async for event in agent.stream(thread): # Log different event types if event.type == EventType.LLM_REQUEST: logging.info(f"Sending request to {event.data['model']}") elif event.type == EventType.TOOL_RESULT: logging.info( f"Tool {event.data['tool_name']} completed in " f"{event.data['duration_ms']:.0f}ms" ) elif event.type == EventType.EXECUTION_ERROR: logging.error( f"Error: {event.data['error_type']} - " f"{event.data['message']}" ) ``` ### Building Event History ```python theme={null} async def collect_events(agent: Agent, thread: Thread) -> List[ExecutionEvent]: """Collect all events from an execution""" events = [] async for event in agent.stream(thread): events.append(event) # Process specific events if event.type == EventType.MESSAGE_CREATED: msg = event.data["message"] print(f"New {msg.role} message added") return events ``` ### Real-time UI Updates ```python theme={null} class AgentUI: async def process_with_ui_updates(self, agent: Agent, thread: Thread): async for event in agent.stream(thread): await self.handle_event(event) async def handle_event(self, event: ExecutionEvent): if event.type == EventType.LLM_STREAM_CHUNK: await self.append_to_output(event.data["content_chunk"]) elif event.type == EventType.TOOL_SELECTED: await self.show_tool_indicator( event.data["tool_name"], event.data["arguments"] ) elif event.type == EventType.TOOL_RESULT: await self.hide_tool_indicator() await self.show_tool_result(event.data["result"]) elif event.type == EventType.EXECUTION_ERROR: await self.show_error(event.data["message"]) ``` ## Event Filtering ```python theme={null} from typing import AsyncGenerator async def filter_events( agent: Agent, thread: Thread, event_types: List[EventType] ) -> AsyncGenerator[ExecutionEvent, None]: """Filter events by type""" async for event in agent.stream(thread): if event.type in event_types: yield event # Only get tool and error events async for event in filter_events( agent, thread, [EventType.TOOL_SELECTED, EventType.TOOL_ERROR] ): print(f"{event.type}: {event.data}") ``` ## Timing Analysis ```python theme={null} def analyze_timing(events: List[ExecutionEvent]) -> Dict[str, float]: """Analyze timing from events""" timings = {} # Find start and end start = events[0].timestamp end = events[-1].timestamp timings["total_ms"] = (end - start).total_seconds() * 1000 # LLM timing llm_starts = {} for event in events: if event.type == EventType.LLM_REQUEST: llm_starts[event.timestamp] = event elif event.type == EventType.LLM_RESPONSE: timings["llm_latency_ms"] = event.data["latency_ms"] # Tool timing tool_times = [] for event in events: if event.type == EventType.TOOL_RESULT: tool_times.append(event.data["duration_ms"]) if tool_times: timings["tool_total_ms"] = sum(tool_times) timings["tool_average_ms"] = sum(tool_times) / len(tool_times) return timings ``` ## See Also * [EventType](/api-reference/tyler-eventtype) - All available event types * [Thread](/api-reference/tyler-thread) - Thread methods for accessing aggregated information * [Agent](/api-reference/tyler-agent) - The main agent class # RetryConfig Source: https://slide.mintlify.app/api-reference/tyler-retryconfig Configuration for automatic retry behavior in agent execution ## Overview The `RetryConfig` class configures automatic retry behavior for agent operations, particularly useful when using structured output that may fail validation. ## Class Definition ```python theme={null} from pydantic import BaseModel, Field class RetryConfig(BaseModel): """Configuration for retry behavior in agent execution.""" max_retries: int = Field(default=3, ge=0) retry_on_validation_error: bool = Field(default=True) retry_on_tool_error: bool = Field(default=False) backoff_base_seconds: float = Field(default=1.0, ge=0) model_config = {"frozen": True} # Immutable ``` ## Properties Maximum number of retry attempts for a failed operation. Set to 0 to disable retries entirely. * Minimum value: 0 * Each retry includes the error message as feedback to the LLM When True, the agent will automatically retry if structured output validation fails. This handles: * `JSONDecodeError` when the LLM returns invalid JSON * `ValidationError` when JSON doesn't match the Pydantic schema When True, the agent will retry if a tool execution fails with an exception. Use with caution—some tool errors are not recoverable by retry (e.g., authentication failures). Base delay in seconds for exponential backoff between retries. The actual delay is: `backoff_base_seconds * retry_attempt` * Retry 1: 1.0s delay * Retry 2: 2.0s delay * Retry 3: 3.0s delay ## Creating RetryConfig ```python theme={null} from tyler import RetryConfig # Default configuration (3 retries, validation retry enabled) config = RetryConfig() # Custom configuration config = RetryConfig( max_retries=5, retry_on_validation_error=True, retry_on_tool_error=True, backoff_base_seconds=2.0 ) # Disable retries config = RetryConfig(max_retries=0) ``` ## Usage with Agent ### At Agent Creation ```python theme={null} from tyler import Agent, RetryConfig agent = Agent( name="DataExtractor", model_name="gpt-4o", purpose="To extract structured data from documents", retry_config=RetryConfig( max_retries=3, retry_on_validation_error=True ) ) ``` ### At Runtime The agent's `retry_config` is used automatically when `response_type` is provided: ```python theme={null} from pydantic import BaseModel class Invoice(BaseModel): vendor: str total: float items: list[str] # Retry config is used if structured output validation fails result = await agent.run(thread, response_type=Invoice) ``` ## Retry Flow When a retry occurs: 1. **Error Detection**: The agent catches `JSONDecodeError` or `ValidationError` 2. **Feedback Message**: An error message is added to the thread explaining what went wrong 3. **Backoff Delay**: The agent waits `backoff_base_seconds * attempt_number` seconds 4. **Retry Attempt**: A new LLM call is made with the feedback in context 5. **Repeat or Fail**: Steps 1-4 repeat until success or `max_retries` is exhausted ```python theme={null} # Example retry flow with max_retries=2 # # Attempt 1: LLM returns invalid JSON → JSONDecodeError # → Add error feedback to thread # → Wait 1.0s # # Attempt 2: LLM returns JSON but wrong schema → ValidationError # → Add error feedback to thread # → Wait 2.0s # # Attempt 3: LLM returns valid structured output → Success! ``` ## Error Handling When all retries are exhausted, `StructuredOutputError` is raised: ```python theme={null} from tyler import Agent, RetryConfig, StructuredOutputError agent = Agent( name="extractor", retry_config=RetryConfig(max_retries=2) ) try: result = await agent.run(thread, response_type=MyModel) except StructuredOutputError as e: print(f"Failed: {e.message}") print(f"Validation errors: {e.validation_errors}") print(f"Last response: {e.last_response}") ``` ## Immutability `RetryConfig` instances are immutable (frozen). Create a new instance to change values: ```python theme={null} config = RetryConfig(max_retries=3) # ❌ This will raise an error config.max_retries = 5 # ✅ Create a new instance instead new_config = RetryConfig( max_retries=5, retry_on_validation_error=config.retry_on_validation_error, retry_on_tool_error=config.retry_on_tool_error, backoff_base_seconds=config.backoff_base_seconds ) ``` ## Best Practices ### Choosing max\_retries ```python theme={null} # Simple schemas - fewer retries needed RetryConfig(max_retries=2) # Complex schemas - may need more attempts RetryConfig(max_retries=5) # Critical operations - more retries with longer backoff RetryConfig(max_retries=5, backoff_base_seconds=2.0) ``` ### When to Enable tool\_error Retry ```python theme={null} # ✅ Good candidates for tool retry # - Network timeouts # - Rate limiting # - Transient API errors # ❌ Bad candidates (won't help) # - Authentication failures # - Invalid parameters # - Permission denied ``` ### Production Configuration ```python theme={null} # Development - fast iteration dev_config = RetryConfig( max_retries=1, backoff_base_seconds=0.5 ) # Production - reliability focused prod_config = RetryConfig( max_retries=3, retry_on_validation_error=True, retry_on_tool_error=False, # Only retry known-recoverable errors backoff_base_seconds=1.0 ) ``` ## See Also * [Agent](/api-reference/tyler-agent) - The main agent class * [StructuredOutputError](/api-reference/tyler-structuredoutputerror) - Exception raised when retries are exhausted * [Structured Output Guide](/guides/structured-output) - Complete guide to structured output # StructuredOutputError Source: https://slide.mintlify.app/api-reference/tyler-structuredoutputerror Exception raised when structured output validation fails after all retry attempts ## Overview `StructuredOutputError` is raised when the agent fails to produce valid structured output after exhausting all retry attempts. It provides detailed information about what went wrong, including validation errors and the last LLM response. ## Class Definition ```python theme={null} class StructuredOutputError(Exception): """Exception raised when structured output validation fails.""" message: str validation_errors: List[Dict[str, Any]] last_response: Any ``` ## Properties A human-readable description of the failure, including the number of attempts made. List of Pydantic validation errors or custom error dictionaries explaining what was invalid. For Pydantic validation errors, each dict typically contains: * `type`: The error type (e.g., `"missing"`, `"type_error"`) * `loc`: The location in the schema where the error occurred * `msg`: A human-readable error message * `input`: The invalid input value The raw content from the last LLM response before the error was raised. Useful for debugging what the LLM actually returned. ## Basic Usage ```python theme={null} from tyler import Agent, StructuredOutputError, RetryConfig from pydantic import BaseModel class UserProfile(BaseModel): name: str age: int email: str agent = Agent( name="extractor", model_name="gpt-4o", retry_config=RetryConfig(max_retries=2) ) try: result = await agent.run(thread, response_type=UserProfile) profile = result.structured_data except StructuredOutputError as e: print(f"Error: {e.message}") # "Failed to get valid structured output after 3 attempts" print(f"Validation errors: {e.validation_errors}") # [{"type": "missing", "loc": ["email"], "msg": "Field required"}] print(f"Last response: {e.last_response}") # '{"name": "John", "age": 30}' # Missing email field ``` ## Error Types ### JSON Decode Errors When the LLM returns invalid JSON: ```python theme={null} try: result = await agent.run(thread, response_type=MyModel) except StructuredOutputError as e: if any(err.get("type") == "json_decode_error" for err in e.validation_errors): print("LLM returned invalid JSON") print(f"Raw response: {e.last_response}") ``` ### Schema Validation Errors When JSON is valid but doesn't match the Pydantic schema: ```python theme={null} try: result = await agent.run(thread, response_type=MyModel) except StructuredOutputError as e: for error in e.validation_errors: print(f"Field: {error.get('loc')}") print(f"Error: {error.get('msg')}") ``` ### Unexpected Errors For other errors during structured output processing: ```python theme={null} try: result = await agent.run(thread, response_type=MyModel) except StructuredOutputError as e: if any(err.get("type") == "unexpected_error" for err in e.validation_errors): print(f"Unexpected error: {e.validation_errors[0].get('msg')}") ``` ## Handling Strategies ### Graceful Fallback ```python theme={null} async def extract_with_fallback(thread, model_class): try: result = await agent.run(thread, response_type=model_class) return result.structured_data except StructuredOutputError as e: # Fall back to unstructured response result = await agent.run(thread) # No response_type return {"raw_content": result.content, "error": e.message} ``` ### Retry with Simpler Schema ```python theme={null} class DetailedProfile(BaseModel): name: str email: str phone: str address: str preferences: dict class SimpleProfile(BaseModel): name: str email: str async def extract_profile(thread): try: result = await agent.run(thread, response_type=DetailedProfile) return result.structured_data except StructuredOutputError: # Try with simpler schema result = await agent.run(thread, response_type=SimpleProfile) return result.structured_data ``` ### Logging and Monitoring ```python theme={null} import logging logger = logging.getLogger(__name__) async def extract_data(thread, response_type): try: result = await agent.run(thread, response_type=response_type) return result.structured_data except StructuredOutputError as e: logger.error( "Structured output failed", extra={ "message": e.message, "validation_errors": e.validation_errors, "last_response_preview": str(e.last_response)[:200], "response_type": response_type.__name__ } ) raise ``` ## Common Causes | Cause | Solution | | -------------------------- | ----------------------------------------------------------- | | Schema too complex | Simplify the Pydantic model or break into smaller models | | Missing field descriptions | Add `Field(description="...")` to help the LLM | | Strict constraints | Use looser constraints or handle in post-processing | | Model limitations | Use a more capable model (e.g., gpt-4o instead of gpt-3.5) | | Ambiguous prompts | Clarify what data you want extracted | | Wrong data type | Ensure the input contains the data you're trying to extract | ## Debugging Tips 1. **Check the last response**: The `last_response` field shows exactly what the LLM returned: ```python theme={null} except StructuredOutputError as e: print("LLM returned:", repr(e.last_response)) ``` 2. **Inspect validation errors**: The errors show exactly what failed: ```python theme={null} except StructuredOutputError as e: for err in e.validation_errors: print(f"{err.get('loc')}: {err.get('msg')}") ``` 3. **Test your schema manually**: ```python theme={null} from pydantic import ValidationError test_json = '{"name": "John"}' # Your LLM's response try: MyModel.model_validate_json(test_json) except ValidationError as e: print(e.errors()) ``` ## See Also * [RetryConfig](/api-reference/tyler-retryconfig) - Configure retry behavior * [AgentResult](/api-reference/tyler-agentresult) - Successful result with structured\_data * [Structured Output Guide](/guides/structured-output) - Complete usage guide # ToolContext Source: https://slide.mintlify.app/api-reference/tyler-toolcontext Request-scoped identity and metadata for tool execution ## Overview `ToolContext` carries **request-scoped identity** to your tools—answering the question "who is making this request?" It provides user IDs, organization IDs, session information, and auth claims that tools need to act on behalf of specific users. **Design principle**: ToolContext answers "who is making this request?" not "what infrastructure does this tool need?" Infrastructure dependencies (database clients, API clients, caches) are better captured at tool definition time via closures. This keeps your tools testable and your context lightweight. ## Class Definition ```python theme={null} from dataclasses import dataclass, field from typing import Dict, Any, Optional, Callable, Awaitable @dataclass class ToolContext: """Context passed to tools during execution.""" tool_name: Optional[str] = None # Name of the tool being executed tool_call_id: Optional[str] = None # Unique ID for this tool call deps: Dict[str, Any] = field(default_factory=dict) # User dependencies progress_callback: Optional[Callable[[int, int, str], Awaitable[None]]] = None # For progress updates ``` ## Features * **Request identity**: Carry user\_id, org\_id, session\_id, and auth claims * **Typed metadata fields**: Access `tool_name` and `tool_call_id` directly * **Dict-style access**: Simple `ctx["key"]` syntax for accessing identity data * **Lightweight**: Only request-scoped data, not infrastructure ## Using ToolContext ### The Recommended Pattern **Close over infrastructure at tool definition time, receive identity at runtime:** ```python theme={null} from tyler import ToolContext # Infrastructure is captured when the tool is defined def create_order_tools(db, payment_api): """Create order tools with infrastructure closed over.""" async def get_my_orders(ctx: ToolContext, status: str = "all") -> str: """Get orders for the current user.""" # Identity from context - "who is asking?" user_id = ctx["user_id"] org_id = ctx.get("org_id") # Infrastructure from closure - captured at definition time orders = await db.query( "SELECT * FROM orders WHERE user_id = ? AND org_id = ?", [user_id, org_id] ) return json.dumps(orders) async def create_order(ctx: ToolContext, product_id: str, quantity: int) -> str: """Create an order for the current user.""" user_id = ctx["user_id"] # Use closed-over infrastructure order = await db.insert("orders", { "user_id": user_id, "product_id": product_id, "quantity": quantity }) await payment_api.authorize(user_id, order.total) return f"Order {order.id} created" return [get_my_orders, create_order] # At startup: create tools with infrastructure db = Database(connection_string) payment = PaymentAPI(api_key) order_tools = create_order_tools(db, payment) agent = Agent(model_name="gpt-4o", tools=order_tools) # At request time: pass only identity await agent.run(thread, tool_context={ "user_id": current_user.id, "org_id": current_user.org_id, "permissions": current_user.permissions }) ``` ### Accessing Identity Data Tools access request-scoped identity using dict-style syntax: ```python theme={null} from tyler import ToolContext async def get_user_preferences(ctx: ToolContext) -> str: """Get preferences for the current user.""" user_id = ctx["user_id"] # Required - raises KeyError if missing org_id = ctx.get("org_id") # Optional - returns None if missing session_id = ctx.get("session_id") # Optional # ... fetch preferences for this user ``` ### Accessing Metadata Fields Tools can also access typed metadata about the current execution: ```python theme={null} async def audited_action(ctx: ToolContext, action: str) -> str: """Perform an action with audit logging.""" # Request identity user_id = ctx["user_id"] # Execution metadata (auto-populated) tool_name = ctx.tool_name # e.g., "audited_action" call_id = ctx.tool_call_id # e.g., "call_abc123" # Log the audit trail await audit_log.write( user=user_id, action=action, tool=tool_name, call_id=call_id ) return f"Completed: {action}" ``` ### Passing Context to Agent Context is provided per-request with the identity of who is making the request: ```python theme={null} from tyler import Agent # Agent setup (tools have infrastructure closed over) agent = Agent( model_name="gpt-4o", tools=order_tools # Already have db, api clients via closures ) # Each request passes identity result = await agent.run( thread, tool_context={ "user_id": request.user.id, "org_id": request.user.org_id, "session_id": request.session_id, "permissions": request.user.permissions } ) ``` #### Agent-Level Context (Shared Identity Defaults) For scenarios where some identity data is constant (e.g., service accounts, system agents): ```python theme={null} # System agent that always acts as a specific service account system_agent = Agent( model_name="gpt-4o", tools=admin_tools, tool_context={ "user_id": "system", "role": "admin" } ) # No per-request context needed for system operations await system_agent.run(thread) ``` #### Context Merging When both agent-level and run-level contexts are provided, they merge with run-level taking precedence: ```python theme={null} agent = Agent( tool_context={"org_id": "default_org", "tier": "free"} ) # Final context: org_id overridden, tier inherited, user_id added await agent.run(thread, tool_context={ "org_id": "premium_org", # Overrides agent default "user_id": "user_123" # Added for this request }) # Result: {"org_id": "premium_org", "tier": "free", "user_id": "user_123"} ``` ## Parameter Naming Convention The tool runner looks for specific parameter names to identify context parameters: Preferred short form. Must be the first parameter. ```python theme={null} async def my_tool(ctx: ToolContext, param: str) -> str: ... ``` Alternative longer form. Must be the first parameter. ```python theme={null} async def my_tool(context: ToolContext, param: str) -> str: ... ``` The context parameter **must** be the first parameter in your function signature. If it appears elsewhere, it won't receive the injected context. ## Typed Fields These fields are automatically populated by the agent: | Field | Type | Description | | ------------------- | ------------------ | ------------------------------------------------- | | `tool_name` | `str \| None` | Name of the tool being executed | | `tool_call_id` | `str \| None` | Unique identifier for this tool call | | `deps` | `Dict[str, Any]` | User-provided dependencies | | `progress_callback` | `Callable \| None` | Async callback for reporting progress (MCP tools) | ```python theme={null} async def my_tool(ctx: ToolContext, param: str) -> str: # Typed field access print(f"Running: {ctx.tool_name}") # e.g., "my_tool" print(f"Call ID: {ctx.tool_call_id}") # e.g., "call_abc123" # Dict access to user deps db = ctx["db"] # Accesses ctx.deps["db"] return "done" ``` ## Dict-Style Access Methods `ToolContext` supports full dict-style access for backward compatibility: | Method | Example | Description | | ---------- | ------------------------- | --------------------------------------- | | `[]` | `ctx["key"]` | Get value, raises `KeyError` if missing | | `[]=` | `ctx["key"] = val` | Set value | | `get()` | `ctx.get("key", default)` | Get with default | | `in` | `"key" in ctx` | Check key exists | | `keys()` | `ctx.keys()` | Iterate over keys | | `items()` | `ctx.items()` | Iterate over key-value pairs | | `values()` | `ctx.values()` | Iterate over values | | `len()` | `len(ctx)` | Count of deps | ## Common Context Keys Here are common keys used in ToolContext—all represent **request identity**, not infrastructure: | Key | Type | Description | | ------------- | ---------- | ------------------------------ | | `user_id` | str | Current user's ID | | `org_id` | str | Organization or tenant ID | | `session_id` | str | Current session identifier | | `permissions` | list\[str] | User's permissions/scopes | | `roles` | list\[str] | User's roles | | `auth_claims` | dict | JWT claims or auth metadata | | `request_id` | str | Trace ID for logging/debugging | | `locale` | str | User's locale preference | **Avoid passing infrastructure in context.** Database connections, API clients, caches, and loggers should be closed over when defining tools, not passed per-request. ## Examples ### Multi-Tenant Data Access ```python theme={null} def create_data_tools(db): """Tools with database closed over, identity from context.""" async def query_orders(ctx: ToolContext, status: str, limit: int = 10) -> str: """Query orders for the current user.""" user_id = ctx["user_id"] org_id = ctx["org_id"] # Tenant isolation orders = await db.query( "SELECT * FROM orders WHERE user_id = ? AND org_id = ? AND status = ? LIMIT ?", [user_id, org_id, status, limit] ) return json.dumps([dict(o) for o in orders]) return [query_orders] ``` ### Permission-Gated Actions ```python theme={null} def create_admin_tools(db, notification_service): """Admin tools that check permissions from context.""" async def delete_user(ctx: ToolContext, target_user_id: str) -> str: """Delete a user (admin only).""" permissions = ctx.get("permissions", []) if "admin:delete_users" not in permissions: return "Error: Insufficient permissions" admin_id = ctx["user_id"] # Who is performing the action await db.delete("users", target_user_id) await notification_service.notify_admins( f"User {target_user_id} deleted by {admin_id}" ) return f"User {target_user_id} deleted" return [delete_user] ``` ### Personalized Recommendations ```python theme={null} def create_recommendation_tools(recommender_api): """Recommendation tools with API client closed over.""" async def get_recommendations(ctx: ToolContext, category: str) -> str: """Get personalized recommendations for the current user.""" user_id = ctx["user_id"] locale = ctx.get("locale", "en-US") recs = await recommender_api.get_recommendations( user_id=user_id, category=category, locale=locale ) return json.dumps(recs) return [get_recommendations] ``` ### Audit Logging ```python theme={null} def create_audited_tools(db, audit_log): """Tools that log who performed each action.""" async def update_settings(ctx: ToolContext, settings: dict) -> str: """Update account settings with audit trail.""" user_id = ctx["user_id"] session_id = ctx.get("session_id") request_id = ctx.get("request_id") await db.update("settings", user_id, settings) await audit_log.record({ "action": "update_settings", "user_id": user_id, "session_id": session_id, "request_id": request_id, "changes": settings }) return "Settings updated" return [update_settings] ``` ## Error Handling ### Missing Context When a tool expects identity but none is provided: ```python theme={null} from tyler import ToolContextError async def requires_user(ctx: ToolContext, action: str) -> str: user_id = ctx["user_id"] # KeyError if not in context ... # This raises ToolContextError try: result = await agent.run(thread) # No tool_context! except ToolContextError as e: print(f"Missing context: {e}") ``` ### Missing Identity Keys Handle optional identity gracefully: ```python theme={null} async def flexible_tool(ctx: ToolContext, action: str) -> str: # Required identity if "user_id" not in ctx: raise ValueError("This tool requires user_id in context") user_id = ctx["user_id"] # Optional identity with defaults org_id = ctx.get("org_id", "default") locale = ctx.get("locale", "en-US") permissions = ctx.get("permissions", []) # Check permissions before proceeding if "write" not in permissions: return "Error: Write permission required" # ... rest of implementation ``` ## Backward Compatibility ### Tools Without Context Tools without a context parameter work normally: ```python theme={null} # This tool doesn't need identity async def simple_math(a: int, b: int) -> str: return str(a + b) # Context is ignored for this tool await agent.run( thread, tool_context={"user_id": "123"} # Passed but not used ) ``` ### Existing Code Works Unchanged The `ToolContext` dataclass is fully backward compatible. Existing tools using dict-style access continue to work: ```python theme={null} # This code works the same before and after the update async def existing_tool(ctx: ToolContext, query: str) -> str: user_id = ctx["user_id"] # Still works org_id = ctx.get("org_id") # Still works if "permissions" in ctx: # Still works permissions = ctx["permissions"] return "done" ``` ## Testing with Context Testing is clean because infrastructure is separate from identity: ```python theme={null} import pytest from unittest.mock import AsyncMock @pytest.mark.asyncio async def test_query_orders(): # Mock the infrastructure (closed over at tool creation) mock_db = AsyncMock() mock_db.query.return_value = [ {"id": 1, "status": "pending"}, {"id": 2, "status": "pending"} ] # Create tool with mock infrastructure tools = create_data_tools(mock_db) query_orders = tools[0] # Test with identity context only ctx = {"user_id": "user_123", "org_id": "org_456"} result = await query_orders(ctx, status="pending", limit=10) # Verify assert "pending" in result mock_db.query.assert_called_once() # Verify user_id was used in query call_args = mock_db.query.call_args[0] assert "user_123" in call_args[1] assert "org_456" in call_args[1] ``` ### Testing Permission Checks ```python theme={null} @pytest.mark.asyncio async def test_admin_action_requires_permission(): mock_db = AsyncMock() tools = create_admin_tools(mock_db, AsyncMock()) delete_user = tools[0] # Test without admin permission ctx = {"user_id": "user_123", "permissions": ["read"]} result = await delete_user(ctx, target_user_id="user_456") assert "Insufficient permissions" in result mock_db.delete.assert_not_called() # Test with admin permission ctx = {"user_id": "admin_1", "permissions": ["admin:delete_users"]} result = await delete_user(ctx, target_user_id="user_456") assert "deleted" in result mock_db.delete.assert_called_once() ``` ## Best Practices ### Close Over Infrastructure Capture databases, API clients, and other infrastructure when defining tools: ```python theme={null} # ✅ Good: Infrastructure closed over at definition time def create_tools(db, cache, external_api): async def fetch_data(ctx: ToolContext, query: str) -> str: user_id = ctx["user_id"] # Identity from context return await db.query(query, user_id) # Infrastructure from closure return [fetch_data] # ❌ Avoid: Infrastructure in context async def fetch_data(ctx: ToolContext, query: str) -> str: db = ctx["db"] # Infrastructure shouldn't be here return await db.query(query, ctx["user_id"]) ``` ### Document Expected Identity Document what identity keys your tools expect: ```python theme={null} async def sensitive_action(ctx: ToolContext, action: str) -> str: """ Perform a sensitive action. Args: ctx: Request context containing: - user_id (str): Required. The authenticated user's ID. - org_id (str): Required. The user's organization. - permissions (list[str]): Required. User's permission scopes. - session_id (str): Optional. For audit logging. action: The action to perform. Returns: Result of the action. """ ``` ### Validation Helper Create a validation helper for required identity: ```python theme={null} def require_identity(ctx: ToolContext, *keys: str) -> None: """Validate that context contains required identity keys.""" missing = [key for key in keys if key not in ctx] if missing: raise ValueError(f"Missing required identity: {missing}") async def my_tool(ctx: ToolContext, param: str) -> str: require_identity(ctx, "user_id", "org_id") ... ``` ### Identity Factory Create identity context consistently from your auth layer: ```python theme={null} class IdentityContext: """Build tool context from authenticated requests.""" @staticmethod def from_request(request) -> dict: """Extract identity from an HTTP request.""" return { "user_id": request.user.id, "org_id": request.user.org_id, "permissions": request.user.permissions, "session_id": request.session.id, "request_id": request.headers.get("X-Request-ID"), "locale": request.headers.get("Accept-Language", "en-US") } @staticmethod def from_jwt(claims: dict) -> dict: """Extract identity from JWT claims.""" return { "user_id": claims["sub"], "org_id": claims.get("org"), "permissions": claims.get("scope", "").split(), "roles": claims.get("roles", []) } # Usage in a web framework @app.post("/chat") async def chat(request: Request, message: str): identity = IdentityContext.from_request(request) result = await agent.run(thread, tool_context=identity) return result ``` ## See Also * [Agent](/api-reference/tyler-agent) - Agent.run() with tool\_context * [Adding Tools](/guides/adding-tools) - Creating custom tools * [Structured Output Guide](/guides/structured-output) - Complete guide including tool context # Narrator CLI Source: https://slide.mintlify.app/apps/narrator-cli Command-line interface for managing Narrator database storage The Narrator CLI provides database management tools for the Narrator storage system. It includes commands for initializing database tables and checking database status. ## Installation The Narrator CLI is automatically installed when you install the Narrator package: ```bash theme={null} uv add slide-narrator ``` ```bash theme={null} pip install slide-narrator ``` After installation, the `narrator` command will be available (run with `uv run narrator` if using uv). ## Commands ### Database Commands #### narrator init Initialize database tables for thread and message storage: ```bash theme={null} # Initialize with explicit database URL uv run narrator init --database-url "postgresql+asyncpg://user:pass@localhost/dbname" # Initialize using environment variable export NARRATOR_DATABASE_URL="postgresql+asyncpg://user:pass@localhost/dbname" uv run narrator init ``` This command creates the necessary database tables: * `threads` - Stores conversation threads * `messages` - Stores messages within threads #### narrator status Check database connection and display basic statistics: ```bash theme={null} # Check status with explicit database URL uv run narrator status --database-url "postgresql+asyncpg://user:pass@localhost/dbname" # Check status using environment variable export NARRATOR_DATABASE_URL="postgresql+asyncpg://user:pass@localhost/dbname" uv run narrator status ``` The status command shows: * Database connection status * Number of recent threads * Basic health check information ### Docker Commands #### narrator docker-setup One-command setup that starts PostgreSQL and initializes tables: ```bash theme={null} # Quick setup with defaults uv run narrator docker-setup # Use a custom port uv run narrator docker-setup --port 5433 ``` This command: 1. Starts a PostgreSQL container with the correct configuration 2. Waits for the database to be ready 3. Initializes the required tables 4. Provides the connection string to use #### narrator docker-start Start a PostgreSQL container for Narrator: ```bash theme={null} # Start with defaults (port 5432, detached) uv run narrator docker-start # Use a custom port uv run narrator docker-start --port 5433 # Run in foreground (useful for debugging) uv run narrator docker-start --no-detach ``` #### narrator docker-stop Stop the PostgreSQL container: ```bash theme={null} # Stop container (preserves data) uv run narrator docker-stop # Stop and remove all data uv run narrator docker-stop --remove-volumes ``` ## Quick Start with Docker The fastest way to get started with PostgreSQL for Narrator: ```bash theme={null} # One command to set up everything uv run narrator docker-setup # This will: # 1. Start a PostgreSQL container # 2. Wait for it to be ready # 3. Initialize the database tables # 4. Show you the connection string # The database is now available at: # postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator ``` That's it! Your database is ready to use. ## Configuration ### Environment Variables The Narrator CLI respects these environment variables: #### Database Connection | Variable | Description | Default | | ----------------------- | -------------------------- | --------------------- | | `NARRATOR_DATABASE_URL` | Database connection string | None (uses in-memory) | #### Docker Configuration These variables affect the `docker-*` commands: | Variable | Description | Default | | ---------------------- | ------------------------- | ------------- | | `NARRATOR_DB_NAME` | Database name | narrator | | `NARRATOR_DB_USER` | Database username | narrator | | `NARRATOR_DB_PASSWORD` | Database password | narrator\_dev | | `NARRATOR_DB_PORT` | Port to expose PostgreSQL | 5432 | #### Connection Pool Settings For PostgreSQL connections: | Variable | Description | Default | | -------------------------- | --------------------------------- | ------- | | `NARRATOR_DB_POOL_SIZE` | Connection pool size | 5 | | `NARRATOR_DB_MAX_OVERFLOW` | Max overflow connections | 10 | | `NARRATOR_DB_POOL_TIMEOUT` | Connection timeout (seconds) | 30 | | `NARRATOR_DB_POOL_RECYCLE` | Connection recycle time (seconds) | 300 | | `NARRATOR_DB_ECHO` | Enable SQL logging | false | ### Database URLs Narrator supports different database backends: ```bash theme={null} # PostgreSQL (recommended for production) postgresql+asyncpg://user:password@localhost:5432/dbname # SQLite (good for development) sqlite+aiosqlite:///path/to/database.db # In-memory (default, no persistence) # Just omit the database URL ``` ## Examples ### Local Development Setup ```bash theme={null} # 1. Navigate to your project cd my-tyler-project # 2. Set up local PostgreSQL with Docker (default settings) uv run narrator docker-setup # 3. Use in your code export NARRATOR_DATABASE_URL="postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator" python your_agent.py ``` ### Custom Docker Configuration ```bash theme={null} # Set custom database configuration export NARRATOR_DB_NAME=myapp export NARRATOR_DB_USER=myapp_user export NARRATOR_DB_PASSWORD=secure_password export NARRATOR_DB_PORT=5433 # Start Docker with these settings uv run narrator docker-setup # Your database will be available at: # postgresql+asyncpg://myapp_user:secure_password@localhost:5433/myapp ``` ### Production Setup ```bash theme={null} # 1. Set production database URL export NARRATOR_DATABASE_URL="postgresql+asyncpg://prod_user:prod_pass@prod_host:5432/prod_db" # 2. Initialize tables (one-time setup) uv run narrator init # 3. Verify connection uv run narrator status ``` ### CI/CD Pipeline ```yaml theme={null} # Example GitHub Actions workflow - name: Setup Database run: | # Use SQLite for tests export NARRATOR_DATABASE_URL="sqlite+aiosqlite:///test.db" uv run narrator init uv run narrator status - name: Run Tests run: | export NARRATOR_DATABASE_URL="sqlite+aiosqlite:///test.db" pytest tests/ ``` ## Integration with Tyler When using Tyler agents with persistent storage: ```python theme={null} from tyler import Agent, ThreadStore # The database must be initialized first with: # uv run narrator init # Then use in your agent thread_store = await ThreadStore.create( "postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator" ) agent = Agent( name="my-agent", thread_store=thread_store ) ``` ## Troubleshooting ### Connection Errors **"Failed to connect to database"**: Check your database URL and ensure the database server is running: ```bash theme={null} # For Docker setup, ensure container is running docker ps | grep narrator-postgres # Test connection with psql psql postgresql://narrator:narrator_dev@localhost:5432/narrator ``` **"No such command 'init'"**: Make sure you're using the correct command: ```bash theme={null} # Correct uv run narrator init # Incorrect (old command name) uv run narrator-db init ``` ### Permission Errors **"Permission denied"**: Ensure your database user has CREATE TABLE permissions: ```sql theme={null} -- Grant permissions (run as superuser) GRANT ALL PRIVILEGES ON DATABASE narrator TO narrator; ``` ### Migration Issues **"Table already exists"**: The init command is idempotent and will skip existing tables. This is not an error. ### Environment Variable Issues **"No database URL provided"**: Set the environment variable: ```bash theme={null} # Bash/Zsh export NARRATOR_DATABASE_URL="your-database-url" # Or use .env file with python-dotenv echo 'NARRATOR_DATABASE_URL="your-database-url"' >> .env ``` ## Best Practices 1. **Always initialize before use**: Run `narrator init` before using ThreadStore with a database 2. **Use environment variables**: Avoid hardcoding database URLs in your code 3. **Use connection pooling**: For production, configure pool settings appropriately 4. **Regular backups**: Set up automated backups for production databases 5. **Monitor connections**: Keep an eye on connection pool usage in production ## Advanced Usage ### Custom Database Configuration Create a `.env` file for your project: ```bash theme={null} # .env NARRATOR_DATABASE_URL=postgresql+asyncpg://user:pass@localhost/myapp NARRATOR_DB_POOL_SIZE=20 NARRATOR_DB_MAX_OVERFLOW=40 NARRATOR_DB_POOL_TIMEOUT=60 NARRATOR_DB_POOL_RECYCLE=600 ``` Then load it in your application: ```python theme={null} from dotenv import load_dotenv load_dotenv() # Now narrator commands will use these settings ``` ### Database Migrations For schema changes between Narrator versions: ```bash theme={null} # 1. Backup your database pg_dump -U narrator -h localhost narrator > backup.sql # 2. Update Narrator uv add slide-narrator@latest # 3. Re-initialize (safe - won't drop existing data) uv run narrator init # 4. Verify uv run narrator status ``` ## Next Steps Learn about the ThreadStore API Build agents with persistent memory Create and chat with agents Deploy agents to production # Building Slack agents Source: https://slide.mintlify.app/apps/slack-agent Deploy your AI agents as Slack agents Space Monkey makes it easy to deploy your Slide agents as Slack agents. In this guide, you'll learn how to create a Slack agent that can respond to messages, handle events, and interact with your workspace. ## Prerequisites Before starting, you'll need: 1. A Slack workspace where you can install apps 2. Slack app credentials (we'll create these) ## Quick start ### Step 1: Install Space Monkey ```bash theme={null} uv add slide-space-monkey ``` ### Step 2: Create Your Agent ```python theme={null} import asyncio from space_monkey import SlackApp from tyler import Agent from narrator import ThreadStore, FileStore from lye import WEB_TOOLS # Set environment variables first: # export SLACK_BOT_TOKEN=xoxb-your-bot-token # export SLACK_APP_TOKEN=xapp-your-app-token async def main(): # Initialize storage thread_store = await ThreadStore.create() file_store = await FileStore.create() # Create your agent agent = Agent( name="slack-assistant", model_name="gpt-4", purpose="To help Slack users with their questions", tools=WEB_TOOLS ) # Create Slack app app = SlackApp( agent=agent, thread_store=thread_store, file_store=file_store ) # Start the app await app.start(port=3000) # Run the app if __name__ == "__main__": asyncio.run(main()) ``` ## Setting Up Slack App ### 1. Create a Slack App 1. Go to [api.slack.com/apps](https://api.slack.com/apps) 2. Click "Create New App" → "From scratch" 3. Name your app and select your workspace ### 2. Configure Agent User 1. Go to "OAuth & Permissions" 2. Add these Bot Token Scopes: * `app_mentions:read` - Read mentions * `chat:write` - Send messages * `channels:history` - Read channel messages * `groups:history` - Read private channel messages * `im:history` - Read direct messages * `mpim:history` - Read group DMs * `files:read` - Read files (if using file tools) * `files:write` - Upload files (if creating files) ### 3. Install to Workspace 1. Click "Install to Workspace" 2. Authorize the app 3. Copy the Bot User OAuth Token (starts with `xoxb-`) ### 4. Enable Socket Mode 1. Go to "Socket Mode" in the sidebar 2. Enable Socket Mode 3. Create an app-level token with `connections:write` scope 4. Copy the token (starts with `xapp-`) ### 5. Enable Events 1. Go to "Event Subscriptions" 2. Turn on "Enable Events" 3. Subscribe to agent events: * `app_mention` * `message.channels` * `message.groups` * `message.im` * `message.mpim` ### 6. Set Environment Variables Set these environment variables before running your agent: ```bash theme={null} export SLACK_BOT_TOKEN=xoxb-your-bot-token export SLACK_APP_TOKEN=xapp-your-app-token export OPENAI_API_KEY=sk-your-openai-key ``` ## Advanced Agent Features ### Conversation Persistence Give your agent persistence across conversations: ```python theme={null} from narrator import ThreadStore, FileStore, Thread, Message async def create_bot_with_persistence(): # Set up persistent storage thread_store = await ThreadStore.create("postgresql://localhost/slackbot") file_store = await FileStore.create("./slack_files") # Create agent with persistence agent = Agent( name="persistent-agent", model_name="gpt-4", purpose="To be a helpful Slack assistant with conversation history", thread_store=thread_store, file_store=file_store, tools=WEB_TOOLS ) return SlackApp( agent=agent, thread_store=thread_store, file_store=file_store ) app = await create_bot_with_persistence() @app.event("app_mention") async def handle_mention_with_persistence(event, say): # Use channel ID as thread ID for conversation continuity thread_id = f"slack-{event['channel']}" try: thread = await app.agent.thread_store.get_thread(thread_id) except: thread = Thread(id=thread_id) # Add user message user_text = event["text"].replace(f"<@{event['user']}>", "").strip() message = Message( role="user", content=user_text, metadata={"slack_user": event["user"]} ) thread.add_message(message) # Process and save result = await app.agent.run(thread) await app.agent.thread_store.save_thread(result.thread) # Respond await say(result.new_messages[-1].content) ``` ### Message Routing SlackApp automatically handles: * Direct messages * App mentions in channels * Thread replies * File uploads * Intelligent routing based on context The agent will respond to: 1. All direct messages 2. Messages where the agent is @mentioned 3. Replies in threads where the agent has participated 4. Messages matching the configured `response_topics` ### File Handling SlackApp automatically processes files shared in Slack when the agent has file tools: ```python theme={null} from lye import IMAGE_TOOLS, FILES_TOOLS agent = Agent( name="file-processor", model_name="gpt-4", purpose="To help analyze files and images", tools=[*IMAGE_TOOLS, *FILES_TOOLS] ) app = SlackApp( agent=agent, thread_store=thread_store, file_store=file_store ) ``` Users can share files directly with the agent, and it will automatically download and process them. ## Production Deployment ### Environment Variables ```bash theme={null} # .env file SLACK_BOT_TOKEN=xoxb-your-bot-token SLACK_APP_TOKEN=xapp-your-app-token OPENAI_API_KEY=sk-your-openai-key DATABASE_URL=postgresql://user:pass@localhost/slackbot ``` ### Docker Deployment Space Monkey includes Docker support for easy deployment: ```bash theme={null} # Clone your agent code cd your-slack-agent/ # Build the Docker image docker build -t my-slack-agent . # Run with environment variables docker run -d \ --name slack-agent \ -p 8000:8000 \ -e SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN \ -e SLACK_APP_TOKEN=$SLACK_APP_TOKEN \ -e OPENAI_API_KEY=$OPENAI_API_KEY \ my-slack-agent ``` #### Using Docker Compose For easier local development: ```bash theme={null} # Copy the example environment file cp .env.example .env # Edit .env with your credentials # Start the agent docker-compose up -d # View logs docker-compose logs -f # Stop the agent docker-compose down ``` To use PostgreSQL for persistence: ```bash theme={null} # Start with PostgreSQL docker-compose --profile with-postgres up -d ``` ### Health Checks SlackApp includes built-in health monitoring: ```python theme={null} # Set environment variables for health monitoring export HEALTH_CHECK_URL=http://healthcheck:8000/ping-receiver export HEALTH_PING_INTERVAL_SECONDS=120 # The app will automatically ping the health check URL ``` The app also provides a built-in health endpoint at `/health`. ## Real-World Example: Team Assistant Agent ```python theme={null} import os import asyncio from space_monkey import SlackApp from tyler import Agent from narrator import ThreadStore, FileStore from lye import WEB_TOOLS, FILES_TOOLS, IMAGE_TOOLS async def create_team_assistant(): # Initialize storage thread_store = await ThreadStore.create( os.getenv("DATABASE_URL", "sqlite+aiosqlite:///slack_bot.db") ) file_store = await FileStore.create("./team_files") # Create agent agent = Agent( name="team-assistant", model_name="gpt-4", purpose="""To be a helpful team assistant that can: - Answer questions about any topic - Research information - Analyze images and files - Remember context within channels """, tools=[*WEB_TOOLS, *FILES_TOOLS, *IMAGE_TOOLS], thread_store=thread_store, file_store=file_store ) # Create Slack app app = SlackApp( agent=agent, thread_store=thread_store, file_store=file_store, response_topics="technical questions, research requests, and team productivity" ) return app # Run the agent async def main(): app = await create_team_assistant() await app.start(port=3000) if __name__ == "__main__": asyncio.run(main()) ``` ## Monitoring and Logging SlackApp includes built-in logging. To configure logging levels: ```python theme={null} import logging # Set logging level for space_monkey logging.getLogger("space_monkey").setLevel(logging.INFO) ``` For production monitoring, you can also set up Weave tracing: ```python theme={null} # Set environment variables export WANDB_API_KEY=your-wandb-key export WANDB_PROJECT=slack-agent-prod ``` ## Troubleshooting 1. Check your agent is online: Look for green dot in Slack 2. Verify tokens are correct 3. Check ngrok is running (for development) 4. Ensure agent is invited to channel 5. Check logs for errors 1. Verify request URL is correct 2. Check signing secret matches 3. Ensure your server is accessible from internet 4. Try re-verifying the URL in Slack settings Review OAuth scopes - you may need additional permissions: * `channels:read` for channel info * `users:read` for user info * `chat:write.public` for posting to channels agent isn't in ## Next steps Deep dive into Space Monkey More Slack agent examples # Tyler CLI Source: https://slide.mintlify.app/apps/tyler-cli Command-line interface for Tyler agent development The Tyler CLI provides tools for creating and interacting with Tyler agents. It includes commands for scaffolding new projects and chatting with agents interactively. ## Installation The Tyler CLI is automatically installed when you install the Tyler package: ```bash theme={null} uv add slide-tyler ``` ```bash theme={null} pip install slide-tyler ``` After installation, the `tyler` command will be available in your terminal. ## Commands ### tyler init Create a new Tyler agent project with all the necessary scaffolding: ```bash theme={null} tyler init my-agent tyler init "Research Assistant" --purpose "To help with academic research" ``` This creates a project structure with: * `agent.py` - Main agent configuration * `tyler-config.yaml` - Chat configuration * `.env.example` - API key template * `pyproject.toml` - Project dependencies * `tools/` - Directory for custom tools * `README.md` - Project documentation ### tyler chat Start an interactive chat session with an agent: ```bash theme={null} tyler chat tyler chat --config my-agent-config.yaml tyler chat --title "Research Session" ``` This launches an interactive chat where you can: * Type messages and see streaming responses * Use commands to manage your conversation * Switch between different conversation threads ## Configuration Tyler Chat can be configured using a YAML or JSON file to customize the agent's behavior, tools, and parameters. ### Using a configuration file ```bash theme={null} tyler chat --config my-agent-config.yaml # or tyler chat -c config.json ``` ### Configuration file format Create a `tyler-config.yaml` file: ```yaml theme={null} # Agent Identity name: "Tyler" purpose: "To be a helpful AI assistant with access to various tools and capabilities." notes: | - Prefer clear, concise communication - Use tools when appropriate to enhance responses - Maintain context across conversations # Model Configuration model_name: "gpt-4o" # or any LiteLLM-compatible model temperature: 0.7 max_tool_iterations: 10 # Instruction Configuration # AGENTS.md is auto-discovered by default from this config directory upward. # agents_md: false # instruction_role: "developer" # optional; default is "system" # Tool Configuration tools: # Built-in tool modules - "web" # Web search and browsing - "files" # File operations - "slack" # Slack integration - "notion" # Notion integration - "command_line" # System commands - "image" # Image processing - "audio" # Audio processing # Custom tool files - "./my_custom_tools.py" - "~/tools/special_tool.py" # Skills are explicit and each path must contain SKILL.md skills: - "./skills/code-review" # MCP Server Configuration (optional) # Connect to reviewed external docs, APIs, databases. # Use streamablehttp for remote servers, stdio for local servers, # and sse only for legacy compatibility. # mcp: # servers: # - name: docs # transport: streamablehttp # url: https://docs.example.com/mcp # include_tools: ["search"] ``` See the [MCP Integration Guide](/guides/mcp-integration) for full MCP configuration options. ### Environment variable substitution Config files support environment variable substitution using `${VAR_NAME}` syntax. This is useful for securely referencing API keys: ```yaml theme={null} # Example: W&B Inference configuration model_name: "openai/deepseek-ai/DeepSeek-R1-0528" base_url: "https://api.inference.wandb.ai/v1" api_key: "${WANDB_API_KEY}" # Reads from environment extra_headers: HTTP-Referer: "https://wandb.ai/my-team/my-project" ``` Never hardcode API keys in config files. Always use environment variables via `${VAR_NAME}` syntax or store them in your `.env` file. ### Command line options ```bash theme={null} # Specify a configuration file tyler chat --config path/to/config.yaml # Set an initial thread title tyler chat --title "Research Session" # Combine options tyler chat -c my-config.yaml -t "Project Discussion" ``` ## Chat commands During a chat session, you can use these special commands: | Command | Description | | ------------------ | -------------------------------- | | `/help` | Show available commands | | `/quit` or `/exit` | Exit the chat | | `/new` | Create a new conversation thread | | `/threads` | List all conversation threads | | `/switch ` | Switch to a different thread | | `/save` | Save the current thread | | `/clear` | Clear the screen | ### Command examples ``` You: /threads ╭─────────────── Threads ───────────────╮ │ 1. Research Session (2 messages) │ │ 2. Code Review (5 messages) │ │ 3. Project Planning (3 messages) │ ╰──────────────────────────────────────╯ You: /switch 2 Switched to thread: Code Review You: /new Created new thread: Untitled Thread ``` ## Features ### Streaming responses Tyler Chat displays responses in real-time as they're generated, providing immediate feedback and a more interactive experience. ### Thread persistence Conversations are automatically saved and can be resumed later. By default, threads are stored in memory during the session. ### Rich formatting * **Markdown support**: Responses are rendered with proper formatting * **Syntax highlighting**: Code blocks are displayed with syntax colors * **Structured output**: Tables, lists, and other elements are properly formatted ### Tool integration When your agent uses tools, you'll see real-time updates: ``` You: Search for the latest AI news Agent: Let me search for the latest AI news for you. [🔧 Using tool: web-search] Here's what I found about the latest AI developments... ``` ## Advanced usage ### Persistent storage To enable persistent storage across sessions, set up a database: ```bash theme={null} # Set environment variable export NARRATOR_DATABASE_URL="sqlite:///tyler_chat.db" # Then run tyler chat tyler chat ``` ### Custom tools Create a Python file with your custom tools: ```python theme={null} # my_tools.py from lye import tool @tool def calculate_compound_interest( principal: float, rate: float, time: int, compounds_per_year: int = 12 ) -> float: """Calculate compound interest""" amount = principal * (1 + rate/compounds_per_year) ** (compounds_per_year * time) return round(amount, 2) # Export tools TOOLS = [calculate_compound_interest] ``` Then reference it in your config: ```yaml theme={null} tools: - "./my_tools.py" ``` ### Environment variables Tyler Chat respects these environment variables: | Variable | Description | | ----------------------- | ------------------------------------------------------------------------------- | | `OPENAI_API_KEY` | API key for OpenAI models | | `ANTHROPIC_API_KEY` | API key for Anthropic models | | `WANDB_API_KEY` | W\&B API key (for W\&B Inference or Weave tracking) | | `WANDB_PROJECT` | W\&B project for Weave tracking (optional - if not set, Weave won't initialize) | | `NARRATOR_DATABASE_URL` | Database URL for thread persistence | To enable Weave tracing for observability, set `WANDB_PROJECT` to your desired project name. If not set, the CLI runs without tracing overhead for faster startup. ## Troubleshooting ### Clean output mode Tyler Chat automatically suppresses noisy output from third-party libraries. If you need to see debug information: ```bash theme={null} # Enable debug mode TYLER_DEBUG=1 tyler chat ``` ### Common issues **"Module not found" errors**: Make sure all dependencies are installed: ```bash theme={null} uv add slide-tyler[all] ``` **API key errors**: Ensure your API keys are set in environment variables or `.env` file **Database errors**: Check your `NARRATOR_DATABASE_URL` is correctly formatted ## Examples ### Research assistant ```yaml theme={null} # research-assistant-config.yaml name: "Research Assistant" purpose: "To help with in-depth research and analysis" model_name: "gpt-4o" tools: - "web" - "files" notes: | - Always cite sources - Create organized reports - Fact-check information ``` ```bash theme={null} tyler chat -c research-assistant-config.yaml -t "Climate Research" ``` ### Code helper ```yaml theme={null} # code-helper-config.yaml name: "Code Helper" purpose: "To assist with programming tasks" temperature: 0.3 # Lower temperature for more consistent code tools: - "files" - "command_line" notes: | - Write clean, well-commented code - Follow best practices - Include error handling ``` ### Quick project setup Use `tyler init` to scaffold a complete project: ```bash theme={null} # Create a new research assistant project tyler init research-bot --purpose "To help with academic research and paper analysis" cd research-bot # Set up environment cp .env.example .env # Edit .env with your API keys # Start chatting with your configured agent tyler chat --config tyler-config.yaml ``` ## Next steps Learn to create agents programmatically Extend agent capabilities with custom tools Run your agent as a Slack bot Explore advanced agent patterns # Agent-to-Agent (A2A) protocol Source: https://slide.mintlify.app/concepts/a2a Using A2A Protocol v0.3.0 to enable multi-agent coordination and delegation across platforms ## What is A2A? The Agent-to-Agent (A2A) Protocol is an open standard that enables AI agents from different platforms and frameworks to communicate and collaborate effectively. Slide's Tyler package includes comprehensive A2A v0.3.0 support, allowing your agents to: * Delegate tasks to remote A2A-compatible agents * Expose Tyler agents as A2A endpoints for other systems * **Stream responses in real-time** with token-level streaming via SSE * Exchange files, structured data, and text between agents * Produce and consume Artifacts as task deliverables * Group related tasks with context IDs * Receive real-time updates via push notifications * Enable seamless interoperability between different agent frameworks Use A2A for agent-to-agent delegation and interoperability. For general external tool or data-source integration, use [MCP](/concepts/mcp). **💻 Code Examples** Expose a Tyler agent via A2A Connect to remote A2A agents Coordinate multiple A2A agents ## A2A Architecture in Slide ```mermaid theme={null} graph LR A[Tyler Agent] --> B[A2A Adapter] B --> C[A2A Client] C --> D[Remote A2A Agent 1] C --> E[Remote A2A Agent 2] C --> F[Remote A2A Agent N] G[Other A2A Clients] --> H[A2A Server] H --> I[Tyler Agent] D --> J[Specialized Tools] E --> K[External APIs] F --> L[Enterprise Services] ``` ## Quick start ### Delegating to Remote A2A Agents ```python theme={null} import asyncio from tyler import Agent, Thread, Message from tyler.a2a import A2AAdapter async def main(): # Create A2A adapter adapter = A2AAdapter() # Connect to remote A2A agents await adapter.connect("research_agent", "https://research-service.example.com") await adapter.connect("analysis_agent", "https://analysis-service.example.com") # Create Tyler agent with delegation capabilities agent = Agent( name="Project Coordinator", model_name="gpt-4.1", purpose="To coordinate complex projects using specialized remote agents", tools=adapter.get_tools_for_agent() # Gets delegation tools ) # Create a complex request that will be delegated thread = Thread() message = Message( role="user", content="Research quantum computing trends and analyze business opportunities" ) thread.add_message(message) # Agent will automatically delegate to appropriate remote agents result = await agent.run(thread) if __name__ == "__main__": asyncio.run(main()) ``` ### Exposing Tyler Agents via A2A ```python theme={null} import asyncio from tyler import Agent from tyler.a2a import A2AServer from lye import WEB_TOOLS, FILES_TOOLS async def main(): # Create a Tyler agent with specific capabilities tyler_agent = Agent( name="Research Assistant", model_name="gpt-4.1", purpose="Advanced research specialist with web search and document processing", tools=[*WEB_TOOLS, *FILES_TOOLS] ) # Create A2A server to expose the agent with authentication server = A2AServer( tyler_agent=tyler_agent, agent_card={ "name": "Tyler Research Assistant", "description": "AI research specialist with web and document capabilities", "capabilities": ["web_research", "document_processing", "data_analysis"], "version": "1.0.0" }, authentication={ "schemes": ["bearer"], "required": True } ) # Start the A2A server await server.start_server(host="0.0.0.0", port=8000) # Agent is now accessible at http://localhost:8000 if __name__ == "__main__": asyncio.run(main()) ``` ## A2A Protocol v0.3.0 Features Tyler fully supports the A2A Protocol v0.3.0 specification, including: ### 1. All Part Types A2A messages can contain multiple types of content: ```python theme={null} from tyler.a2a import TextPart, FilePart, DataPart # Text content text = TextPart(text="Analyze this document") # File content (inline with bytes) with open("document.pdf", "rb") as f: file_data = f.read() file_part = FilePart( name="document.pdf", media_type="application/pdf", file_with_bytes=file_data ) # File content (by URI reference) remote_file = FilePart( name="report.xlsx", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file_with_uri="https://cdn.example.com/files/report.xlsx" ) # Structured data data_part = DataPart(data={ "analysis_type": "financial", "parameters": {"period": "Q4", "metrics": ["revenue", "profit"]} }) ``` ### 2. Artifacts Artifacts are the formal deliverables of a task, providing structured outputs: ```python theme={null} from tyler.a2a import Artifact, TextPart, DataPart # Create an artifact with task results artifact = Artifact.create( name="Market Analysis Report", parts=[ TextPart(text="Executive Summary: Market growth projected at 15%..."), DataPart(data={"growth_rate": 0.15, "confidence": 0.92}), ], metadata={"analysis_version": "2.0"} ) # Access artifact properties print(f"Artifact ID: {artifact.artifact_id}") print(f"Created at: {artifact.created_at}") ``` ### 3. Context ID for Task Grouping Group related tasks together using context IDs: ```python theme={null} from tyler.a2a import A2AClient client = A2AClient() await client.connect("agent", "https://agent.example.com") # Create multiple related tasks in the same context context_id = "project-alpha-research" task1_id = await client.create_task( "agent", "Research market trends", context_id=context_id ) task2_id = await client.create_task( "agent", "Analyze competitor landscape", context_id=context_id ) # Get all tasks in a context related_tasks = client.get_tasks_by_context(context_id) ``` ### 4. Real-Time Token Streaming A2A clients can receive response tokens as they're generated via Server-Sent Events (SSE): ```python theme={null} from tyler.a2a import A2AClient client = A2AClient() await client.connect("agent", "https://agent.example.com") # Create a task task_id = await client.create_task("agent", "Write a detailed analysis") # Stream response tokens in real-time async for message in client.stream_task_messages("agent", task_id): # Tokens arrive as they're generated by the LLM print(message.get("content", ""), end="", flush=True) ``` The A2A server uses Tyler's streaming internally for all requests. The A2A SDK handles delivery: * **`message/send`**: Returns aggregated response after completion * **`message/stream`**: Streams tokens via SSE as they're generated For non-streaming requests, use `send_task` which waits for completion: ```python theme={null} from tyler.a2a import A2AClient client = A2AClient() await client.connect("agent", "https://agent.example.com") # Send task and wait for complete response (non-streaming) result = await client.send_task("agent", "Summarize this document") # Result contains the full response and artifacts print(f"Status: {result.status}") for artifact in result.artifacts: print(f"Artifact: {artifact.name}") for part in artifact.parts: print(part.text) ``` ### 5. Push Notifications Receive real-time updates for long-running tasks via webhooks: ```python theme={null} from tyler.a2a import A2AClient, PushNotificationConfig client = A2AClient() await client.connect("agent", "https://agent.example.com") # Configure push notifications push_config = PushNotificationConfig( webhook_url="https://your-service.com/webhooks/a2a", events=["task.created", "task.updated", "task.completed", "task.artifact"], headers={"Authorization": "Bearer your-webhook-token"}, secret="your-hmac-secret" # For signature verification ) # Create task with push notifications task_id = await client.create_task( "agent", "Generate comprehensive market report", push_notification_config=push_config ) # Your webhook will receive events like: # { # "event_type": "task.completed", # "task_id": "...", # "timestamp": "...", # "data": {"status": "completed", "artifacts": [...]} # } ``` ### 6. Authentication Declaration Declare authentication requirements in your Agent Card: ```python theme={null} from tyler.a2a import A2AServer server = A2AServer( tyler_agent=my_agent, authentication={ "schemes": ["bearer", "api_key"], "required": True } ) ``` ## A2A Protocol Features ### Agent Discovery A2A uses **Agent Cards** to describe agent capabilities: ```python theme={null} agent_card = { "name": "Tyler Research Assistant", "version": "1.0.0", "description": "Specialized research agent with web search capabilities", "protocol_version": "0.3.0", "capabilities": [ "web_research", "fact_checking", "data_analysis", "document_processing", "artifacts" ], "push_notifications": { "supported": True, "events": ["task.created", "task.updated", "task.completed", "task.artifact"] }, "authentication": { "schemes": ["bearer"], "required": False }, "contact": { "name": "Your Organization", "email": "contact@yourorg.com" } } ``` ### Task Management A2A provides full task lifecycle management: ```python theme={null} # Create and track tasks task_id = await adapter.client.create_task("research_agent", "Analyze market trends") # Get task status status = await adapter.client.get_task_status("research_agent", task_id) print(f"Task status: {status['status']}") print(f"Has artifacts: {status['has_artifacts']}") # Get task artifacts artifacts = await adapter.client.get_task_artifacts("research_agent", task_id) for artifact in artifacts: print(f"Artifact: {artifact.name}") for part in artifact.parts: if isinstance(part, TextPart): print(f" Text: {part.text[:100]}...") # Stream real-time responses async for message in adapter.client.stream_task_messages("research_agent", task_id): print(f"Agent response: {message['content']}") ``` ### Secure Communication A2A supports standard web security practices: ```python theme={null} # Connect with authentication await adapter.connect( "secure_agent", "https://secure-service.example.com", headers={"Authorization": "Bearer your-token"} ) ``` ## Multi-Agent Coordination ### Creating Specialized Agent Networks ```python theme={null} import asyncio from tyler import Agent from tyler.a2a import A2AAdapter, A2AServer from lye import WEB_TOOLS, FILES_TOOLS async def create_agent_network(): """Create a network of specialized A2A agents.""" # Create specialized agents research_agent = Agent( name="Research Specialist", tools=[*WEB_TOOLS], purpose="Web research and information gathering expert" ) analysis_agent = Agent( name="Analysis Specialist", tools=[*FILES_TOOLS], purpose="Data analysis and strategic insights expert" ) # Expose agents as A2A servers research_server = A2AServer(research_agent) analysis_server = A2AServer(analysis_agent) # Start servers (in production, these would be separate services) await research_server.start_server(port=8001) await analysis_server.start_server(port=8002) # Create coordinator that uses both agents adapter = A2AAdapter() await adapter.connect("research", "http://localhost:8001") await adapter.connect("analysis", "http://localhost:8002") coordinator = Agent( name="Project Coordinator", purpose="Orchestrate complex projects using specialized agents", tools=adapter.get_tools_for_agent() ) return coordinator ``` ### Task Delegation with Files and Data ```python theme={null} from tyler.a2a import A2AAdapter, FilePart, DataPart adapter = A2AAdapter() await adapter.connect("analysis_agent", "https://analysis.example.com") # Create task with file attachment task_id = await adapter.create_task_with_files( "analysis_agent", "Analyze the attached financial data", files=["./data/q4_report.xlsx", "./data/projections.csv"], context_id="financial-analysis-2024" ) # Create task with structured data task_id = await adapter.create_task_with_data( "analysis_agent", "Run analysis with these parameters", data={ "analysis_type": "trend", "time_range": {"start": "2024-01", "end": "2024-12"}, "metrics": ["revenue", "growth", "churn"] } ) ``` ## A2A vs MCP Comparison | Feature | A2A Protocol | MCP Protocol | | ----------------- | ---------------------------- | --------------------------- | | **Purpose** | Agent-to-agent communication | Model-to-tool communication | | **Scope** | Multi-agent coordination | Tool integration | | **Communication** | Task delegation & responses | Tool calls & results | | **Architecture** | Agent ↔ Agent | Model ↔ Tools | | **Use Cases** | Distributed agent systems | Tool ecosystem integration | ## Production Deployment ### Server Configuration ```python theme={null} # Production A2A server setup server = A2AServer( tyler_agent=production_agent, agent_card={ "name": "Production Research Agent", "version": "2.0.0", "description": "Enterprise research capabilities", "capabilities": ["web_research", "document_analysis", "market_intelligence", "artifacts"], "contact": {"email": "api-support@yourcompany.com"} }, authentication={ "schemes": ["bearer"], "required": True } ) # Start with production settings await server.start_server( host="0.0.0.0", port=8000, ) ``` ## Installation and Setup ### Install A2A Dependencies ```bash theme={null} # Install Tyler (includes A2A support) uv add slide-tyler # A2A SDK is included as a dependency ``` ### Environment Setup ```python theme={null} # Optional: Configure A2A settings import os # Set A2A agent base URL for discovery os.environ["A2A_BASE_URL"] = "https://your-agent-service.com" # Configure authentication if needed os.environ["A2A_AUTH_TOKEN"] = "your-auth-token" ``` ## Security Considerations **Production Security**: Always use HTTPS endpoints, implement proper authentication, and validate agent cards in production environments. ### Authentication Methods ```python theme={null} # Token-based authentication await adapter.connect( "secure_agent", "https://secure-agent.example.com", headers={"Authorization": "Bearer your-secure-token"} ) # Custom headers for API keys await adapter.connect( "api_agent", "https://api-agent.example.com", headers={ "X-API-Key": "your-api-key", "X-Client-Version": "1.0.0" } ) ``` ### Webhook Security ```python theme={null} from tyler.a2a import PushNotificationConfig # Configure secure webhooks with HMAC signing push_config = PushNotificationConfig( webhook_url="https://your-service.com/webhooks/a2a", secret="your-hmac-secret", # Webhook payloads will be signed events=["task.completed", "task.artifact"] ) # Verify webhook signature in your handler import hmac import hashlib def verify_webhook(payload: str, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature) ``` ## Best practices * **Specialized Agents**: Create agents with focused, well-defined capabilities * **Clear Interfaces**: Use descriptive agent cards and task descriptions * **Use Artifacts**: Structure task outputs as formal Artifacts for better interoperability * **Context Grouping**: Use context IDs to group related tasks for better tracking * **Push Notifications**: Use webhooks for long-running tasks instead of polling * **Error Handling**: Implement robust connection and task failure handling * **Monitoring**: Track agent health and task completion rates * **Security**: Always use secure connections and proper authentication ## Next steps Step-by-step A2A integration tutorial Learn advanced delegation patterns Read the official A2A protocol specification Understand Tyler's agent architecture # Architecture overview Source: https://slide.mintlify.app/concepts/architecture Understanding the Slide framework architecture ## Slide Architecture Slide is designed as a collection of modular packages that work seamlessly together while remaining independent. This architecture provides flexibility and allows you to use only what you need. ## Core Components Slide Architecture Overview ### Tyler - The Agent Core Tyler is the heart of Slide, providing: * Agent orchestration and LLM integration * Tool execution framework * Streaming and async support * Model Context Protocol (MCP) compatibility * Evaluation and testing framework ### Lye - The Tool Library Lye provides ready-to-use tools organized by capability: * **Web Tools**: Search, fetch, scrape * **Image Tools**: Analyze, extract text, process * **Audio Tools**: Transcribe, text-to-speech * **File Tools**: Read, write, manipulate * **Browser Tools**: Screenshots, extraction ### Narrator - The Persistence Layer Narrator handles conversation and file persistence: * Thread management (conversation history) * File storage for attachments * Support for multiple backends: * In-memory (testing) * SQLite (local development) * PostgreSQL (production) ### Space Monkey - The Slack Bridge Space Monkey enables Slack deployment: * Event handling and routing * Message formatting * Thread management * File handling ## How Components Work Together ```python theme={null} # Tyler provides the agent from tyler import Agent, Thread, Message # Lye provides tools from lye import WEB_TOOLS, IMAGE_TOOLS # Narrator provides persistence from tyler import ThreadStore, FileStore # They work together seamlessly thread_store = await ThreadStore.create("sqlite+aiosqlite:///db.sqlite") file_store = await FileStore.create("./files") agent = Agent( name="my-agent", model_name="gpt-4", tools=[*WEB_TOOLS, *IMAGE_TOOLS], thread_store=thread_store, file_store=file_store ) ``` ## Design Principles ### 1. Modularity Each package is independent and can be used separately: * Use Tyler alone for simple agents * Use Lye in any Python project for tool utilities * Use Narrator for any conversation management needs ### 2. Composability Components are designed to work together: * Tyler + Lye = Powerful agents with tools * Tyler + Narrator = Agents with conversation persistence * All together = Production-ready AI systems ### 3. Extensibility Every component is designed for extension: * Create custom tools * Add new storage backends * Integrate with any LLM provider * Connect to MCP servers ### 4. Production-Ready Built with real-world use in mind: * Comprehensive error handling * Structured logging * Testing frameworks * Performance optimization ## Data Flow ```mermaid theme={null} graph LR User[User Input] --> Thread[Thread/Message] Thread --> Agent[Tyler Agent] Agent --> LLM[LLM Provider] LLM --> Tools{Tool Calls?} Tools -->|Yes| Lye[Lye Tools] Tools -->|No| Response[Response] Lye --> Agent Agent --> Narrator[Narrator Storage] Narrator --> Response Response --> User ``` ## When to Use Each Component ### Just Tyler Perfect for: * Simple conversational agents * Prototyping and experiments * Custom tool implementations ### Tyler + Lye Ideal for: * Agents that interact with external systems * Research and analysis tasks * Automation workflows ### Tyler + Narrator Best for: * Customer service bots * Long-running conversations * Applications needing context persistence ### Tyler + Lye + Narrator Recommended for: * Production applications * Complex agent systems * Multi-user environments ### Space Monkey Use when: * Deploying to Slack * Building team collaboration tools * Integrating with existing Slack workflows ## Next steps Deep dive into agent internals Learn about the tool architecture # Agents Source: https://slide.mintlify.app/concepts/how-agents-work Understanding agents in the Slide framework **💻 Code Examples** Minimal agent setup YAML configuration Real-time responses ## How the agent works Tyler uses an iterative approach to process messages and execute tools. Here's how it works: ```mermaid theme={null} graph LR A[Thread] --> B[Agent.go] B --> C[Agent.step] C --> D[LLM Call] D --> E{Has Tool Calls?} E -->|No| F[Complete Response] E -->|Yes| G[Execute Tools] G --> C ``` ### Processing Flow When you call `agent.run()` (with or without streaming), Tyler follows these steps: 1. **Message Processing** * Loads the conversation thread * Processes any attached files (images, PDFs, etc.) * Ensures the system prompt is set 2. **Step Execution** * Makes an LLM call with the current context * Processes the response for content and tool calls * Streams responses in real-time (if using `stream=True`) 3. **Tool Execution** * If tool calls are present, executes them in parallel * Adds tool results back to the conversation * Returns to step execution if more tools are needed 4. **Completion** * Saves the final thread state * Returns the processed thread and new messages ### Key Components * **ToolRunner**: Manages the registry of available tools and handles execution * **Thread**: Maintains conversation history and context * **Message**: Represents user, assistant, and tool messages * **ExecutionEvent**: Provides detailed execution telemetry and streaming updates ### Error Handling & Limits Tyler includes built-in safeguards: * Maximum tool iteration limit (default: 10) * Automatic error recovery * Structured error responses * Tool execution timeout handling ## Creating an Agent ### Basic Agent ```python theme={null} from tyler import Agent # Minimal agent configuration agent = Agent( name="assistant", model_name="gpt-4", purpose="To be a helpful assistant" ) # With additional configuration agent = Agent( name="gpt4-assistant", model_name="gpt-4", purpose="To assist with various tasks", temperature=0.7 ) ``` ### Agent with Tools ```python theme={null} from tyler import Agent from lye import WEB_TOOLS, FILES_TOOLS, IMAGE_TOOLS from lye.web import search, fetch from lye.files import read_file, write_file from lye.image import analyze_image # Using tool groups agent = Agent( name="research-assistant", model_name="gpt-4", purpose="To help with research tasks", tools=[ *WEB_TOOLS, # All web tools *FILES_TOOLS, # All file tools analyze_image # Specific image tool ] ) # Or using specific tools agent = Agent( name="focused-assistant", model_name="gpt-4", purpose="To search and save information", tools=[search, fetch, read_file, write_file] ) ``` ## Agent Capabilities ### 1. Tool Usage Agents can intelligently select and use tools based on the task: ```python theme={null} from tyler import Agent, Thread, Message # Create thread and message thread = Thread() message = Message( role="user", content="Search for recent AI developments and save a summary" ) thread.add_message(message) # Agent automatically chooses the right tools result = await agent.run(thread) # Agent will: 1) Use web.search, 2) Use web.fetch for details, 3) Use files.write ``` ### 2. Multi-step Reasoning Agents can break down complex tasks: ```python theme={null} thread = Thread() message = Message( role="user", content=""" 1. Find the top 3 Python web frameworks 2. Compare their features 3. Create a comparison chart 4. Save the analysis """ ) thread.add_message(message) result = await agent.run(thread) ``` ### 3. Context Awareness With proper thread management, agents maintain conversation context: ```python theme={null} from tyler import Agent, Thread, Message, ThreadStore # Set up persistent storage thread_store = await ThreadStore.create("sqlite+aiosqlite:///conversations.db") agent = Agent( name="assistant", model_name="gpt-4", thread_store=thread_store ) # Create a thread thread = Thread(id="research-session") # First query message1 = Message(role="user", content="What is FastAPI?") thread.add_message(message1) result = await agent.run(thread) # Save the thread await thread_store.save_thread(result.thread) # Follow-up uses context message2 = Message(role="user", content="How does it compare to Flask?") result.thread.add_message(message2) final_result = await agent.run(result.thread) # Agent knows we're talking about FastAPI ``` ## Advanced features ### Streaming Responses For long-running tasks or real-time interaction: ```python theme={null} from tyler import Agent, Thread, Message from tyler.models.execution import ExecutionEvent, EventType thread = Thread() message = Message(role="user", content="Write a detailed analysis of...") thread.add_message(message) async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: print(event.data.get("content_chunk", ""), end="", flush=True) elif event.type == EventType.TOOL_SELECTED: print(f"\n[Using tool: {event.data.get('tool_name', '')}]") ``` ### Custom System Prompts Fine-tune agent behavior: ```python theme={null} agent = Agent( name="code-reviewer", model_name="gpt-4", purpose="""You are an expert code reviewer. Focus on: - Security vulnerabilities - Performance optimizations - Best practices Always explain your reasoning.""" ) ``` ### Tool Configuration Control how agents use tools: ```python theme={null} from lye import FILES_TOOLS agent = Agent( name="safe-agent", model_name="gpt-4", purpose="To safely read and analyze files", tools=[FILES_TOOLS[0]], # Just read_file tool tool_choice="auto", # or "none", "required", or specific tool name parallel_tool_calls=True # Enable parallel execution ) ``` ## Agent Patterns ### 1. Supervisor Pattern ```python theme={null} # Note: This is a conceptual pattern - implement delegate_task and create_sub_agent supervisor = Agent( name="supervisor", model_name="gpt-4", purpose="You coordinate work between specialized agents", tools=[delegate_task, create_sub_agent] ) researcher = Agent( name="researcher", model_name="gpt-4", purpose="To conduct research", tools=[*WEB_TOOLS] ) writer = Agent( name="writer", model_name="gpt-4", purpose="To write content", tools=[*FILES_TOOLS] ) ``` ### 2. Tool Specialist Pattern ```python theme={null} from lye import IMAGE_TOOLS from lye.files import read_csv, write_file # Image specialist image_agent = Agent( name="image-expert", model_name="gpt-4", purpose="You are an expert at image analysis and manipulation", tools=IMAGE_TOOLS ) # Data specialist data_agent = Agent( name="data-analyst", model_name="gpt-4", purpose="You are a data analysis expert", tools=[read_csv, write_file] # Add your data analysis tools ) ``` ### 3. Validation Pattern ```python theme={null} # Note: validation_fn is not a current Tyler feature # Instead, use the evaluation framework for validation from tyler.eval import AgentEval, Conversation, Expectation eval = AgentEval( name="validation_test", conversations=[ Conversation( user="Analyze this data", expect=Expectation( custom=lambda response: len(response["content"]) > 100 ) ) ] ) ``` ## Best practices Use clear, descriptive names that indicate the agent's purpose: ```python theme={null} # Good agent = Agent( name="customer-support-agent", model_name="gpt-4", purpose="To help customers with product questions" ) # Not as clear agent = Agent(name="agent1", model_name="gpt-4") ``` Only provide tools the agent actually needs: ```python theme={null} from lye.slack import send_message, read_channel from lye.notion import search_pages # Good - specific tools for the task email_agent = Agent( name="email-assistant", model_name="gpt-4", purpose="To manage email communications", tools=[send_message, read_channel, search_pages] ) # Avoid - too many unnecessary tools from lye import TOOLS # All available tools email_agent = Agent( name="email-assistant", model_name="gpt-4", tools=TOOLS # Includes unrelated tools ) ``` Match model capabilities to task complexity: ```python theme={null} # Simple tasks simple_agent = Agent( name="formatter", model_name="gpt-3.5-turbo", purpose="To format text" ) # Complex reasoning complex_agent = Agent( name="analyzer", model_name="gpt-4", purpose="To perform deep analysis" ) ``` Always implement error handling: ```python theme={null} from tyler.exceptions import AgentError, ToolError try: thread = Thread() message = Message(role="user", content=task) thread.add_message(message) result = await agent.run(thread) except ToolError as e: print(f"Tool failed: {e}") # Retry with different approach except AgentError as e: print(f"Agent error: {e}") # Log and handle appropriately ``` ## Testing Agents Tyler provides a comprehensive evaluation framework for testing your agents: ```python theme={null} from tyler.eval import AgentEval, Conversation, Expectation, ToolUsageScorer # Define test scenarios eval = AgentEval( name="agent_test", conversations=[ Conversation( user="Calculate the sum of 15 and 27", expect=Expectation( mentions=["42"], completes_task=True ) ) ], scorers=[ToolUsageScorer()] ) # Run tests with mock tools results = await eval.run(agent) ``` Key testing features: * **Mock Tools**: Prevent real API calls during testing * **Flexible Expectations**: Test content, behavior, and tool usage * **Multiple Scorers**: Evaluate tone, task completion, and more * **Multi-turn Conversations**: Test complex interaction flows Always test your agents with the evaluation framework before deployment. See the [full evaluation guide](/packages/tyler/evaluations) for details. ## Performance Considerations * **Token Usage**: Monitor and optimize prompts to reduce token consumption * **Tool Calls**: Minimize unnecessary tool invocations * **Caching**: Use Narrator's thread system to avoid redundant work * **Parallel Execution**: Enable `parallel_tool_calls` when tools can run concurrently ## Next steps Learn about tools and how to create custom ones Understand Model Context Protocol integration See practical agent examples Detailed API documentation # Model Context Protocol (MCP) Source: https://slide.mintlify.app/concepts/mcp Using MCP to extend Slide agents with external tools and services **Recommended Reading**: For practical implementation details, see the [MCP Integration Guide](/guides/mcp-integration). **💻 Code Examples** Get started with MCP Multiple servers & filtering ## What is MCP? The Model Context Protocol (MCP) is an open standard that enables seamless communication between AI applications and external tools. Slide's Tyler package includes first-class MCP support, allowing your agents to: * Connect to any MCP-compatible server * Use tools from external services * Share context across applications * Build interoperable AI systems Tyler follows the MCP `2025-11-25` stable transport framing: Streamable HTTP is the default for remote servers, `stdio` is for local subprocess servers, and SSE is legacy compatibility. ## MCP Architecture in Slide ```mermaid theme={null} graph LR A[Slide Agent] --> B[MCP SDK] B --> C[ClientSessionGroup] C --> D[MCP Server 1] C --> E[MCP Server 2] C --> F[MCP Server N] D --> G[External Tools] E --> H[Databases] F --> I[APIs] ``` Tyler uses the official MCP SDK's `ClientSessionGroup` to manage connections to multiple MCP servers simultaneously. This provides: * Automatic session lifecycle management * Tool discovery and aggregation across servers * Tool execution routing to the correct server * Standard MCP transports (`stdio`, Streamable HTTP) plus legacy SSE compatibility ## Quick Start ### Python API (Recommended) ```python theme={null} import asyncio from tyler import Agent, Thread, Message async def main(): # Create agent with MCP config (validates schema immediately) agent = Agent( name="Tyler", model_name="gpt-4.1", tools=["web"], mcp={ "servers": [{ "name": "docs", "transport": "streamablehttp", # Use for Mintlify and hosted servers "url": "https://slide.mintlify.app/mcp" }] } ) try: # Connect to MCP servers (fail fast!) await agent.connect_mcp() # Use the agent - MCP tools are now available thread = Thread() thread.add_message(Message( role="user", content="How do I create a Tyler agent?" )) result = await agent.run(thread) print(result.content) finally: await agent.cleanup() if __name__ == "__main__": asyncio.run(main()) ``` ### Using MCP Tools Once connected, MCP tools are automatically available to your agent with namespaced names: ```python theme={null} # Tools from server "docs" become available as: # - docs_SearchSlideFramework # - docs_GetDocument # etc. # The agent automatically selects and uses appropriate tools thread = Thread() thread.add_message(Message( role="user", content="Search the Slide documentation for information about streaming" )) result = await agent.run(thread) ``` ## MCP Server Types ### 1. Stdio Servers Local processes that communicate via standard input/output. Great for local tools and development: ```python theme={null} mcp={ "servers": [{ "name": "filesystem", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "env": {"NODE_ENV": "production"} # Optional environment }] } ``` ### 2. Streamable HTTP Servers HTTP-based servers using the current remote transport. Recommended for hosted MCP servers: ```python theme={null} mcp={ "servers": [{ "name": "docs", "transport": "streamablehttp", "url": "https://slide.mintlify.app/mcp", "headers": {"X-API-Key": "${API_KEY}"} # Optional auth }] } ``` ### 3. SSE (Server-Sent Events) Servers Legacy HTTP transport for backward compatibility with older MCP servers: ```python theme={null} mcp={ "servers": [{ "name": "legacy", "transport": "sse", "url": "https://legacy.example.com/mcp" }] } ``` ## Creating MCP Servers ### Using FastMCP (Python) Create an MCP server that exposes tools to Slide: ```python theme={null} from fastmcp import FastMCP # Create MCP server mcp = FastMCP() @mcp.tool() async def search_knowledge_base(query: str) -> str: """Search internal knowledge base.""" results = await db.search(query) return format_results(results) @mcp.tool() async def execute_sql(query: str, database: str = "main") -> dict: """Execute SQL query safely.""" if not is_safe_query(query): return {"error": "Unsafe query"} conn = get_connection(database) results = await conn.execute(query) return {"data": results} # Run the server if __name__ == "__main__": mcp.run() ``` ## Advanced MCP Usage ### Multiple Server Connections Connect to multiple MCP servers simultaneously: ```python theme={null} agent = Agent( name="multi-server-agent", model_name="gpt-4.1", mcp={ "servers": [ { "name": "docs", "transport": "streamablehttp", "url": "https://slide.mintlify.app/mcp", "prefix": "slide" # Custom prefix }, { "name": "github", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"}, "prefix": "gh", "fail_silent": True # Continue if GitHub unavailable }, { "name": "filesystem", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "exclude_tools": ["write_file", "delete_file"] # Read-only } ] } ) try: await agent.connect_mcp() # Agent now has tools from all servers: # - slide_SearchSlideFramework # - gh_search_repos # - filesystem_read_file, filesystem_list_directory finally: await agent.cleanup() ``` ### Tool Filtering Control which tools are available: ```python theme={null} { "name": "filesystem", "transport": "stdio", "command": "mcp-server-filesystem", "include_tools": ["read_file", "list_directory"], # Whitelist "exclude_tools": ["delete_file"] # Blacklist (applied after include) } ``` ### Custom Prefixes Override the default namespace prefix for cleaner tool names: ```python theme={null} { "name": "wandb_documentation_server", # Long server name "prefix": "docs", # Short, clean prefix "transport": "streamablehttp", "url": "https://docs.wandb.ai/mcp" } # Tools become: docs_search, docs_query (not wandb_documentation_server_search) ``` ### Environment Variable Substitution Use environment variables for secrets (recommended!): ```python theme={null} mcp={ "servers": [{ "name": "api", "transport": "streamablehttp", "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer ${API_TOKEN}", # Substituted from env "X-API-Key": "${API_KEY}" } }] } ``` ### Graceful Degradation Control failure behavior per server: ```python theme={null} { "servers": [ { "name": "critical", "url": "...", "fail_silent": False # Fail startup if unavailable }, { "name": "optional", "url": "...", "fail_silent": True # Continue if unavailable (default) } ] } ``` ## Best Practices Always cleanup MCP connections: ```python theme={null} agent = Agent(mcp={...}) try: await agent.connect_mcp() # Use agent finally: await agent.cleanup() ``` Never hardcode secrets: ```python theme={null} # Good "headers": {"Authorization": "Bearer ${API_TOKEN}"} # Bad "headers": {"Authorization": "Bearer sk-1234567890"} ``` Only connect to reviewed MCP servers. MCP tools execute with your agent's permissions, local `stdio` servers run with the same privileges as Tyler, and tool annotations/metadata are advisory unless the server is trusted. Use custom prefixes for cleaner tool names: ```python theme={null} { "name": "long_server_name", "prefix": "short", # Tools become short_toolname ... } ``` Use `fail_silent: True` for optional servers: ```python theme={null} { "name": "optional_feature", "fail_silent": True, # Won't break startup ... } ``` ## Troubleshooting Common MCP issues and solutions: * **Connection refused**: Check if MCP server is running and URL is correct * **Tool not found**: Verify tool is exposed by server, check `include_tools`/`exclude_tools` * **Timeout errors**: Increase timeout or check network connectivity * **Permission denied**: Verify authentication credentials * **Environment variable not substituted**: Ensure variable is set before running ## Next Steps Detailed configuration reference See MCP integration examples Build your own MCP server Read the MCP specification # Tools Source: https://slide.mintlify.app/concepts/tools Understanding and creating tools for Slide agents ## What are Tools? Tools in Slide are the capabilities that agents can use to interact with the world. They bridge the gap between an agent's decision-making and actual actions. The Lye package provides a comprehensive set of pre-built tools, and you can easily create custom ones. **💻 Code Examples** Get started with tools Read and write files Analyze images Transcribe and synthesize ## Built-in Tools Slide comes with a rich set of tools organized by category: ```python theme={null} from lye import AUDIO_TOOLS from lye.audio import transcribe, text_to_speech # Use all audio tools agent = Agent(tools=AUDIO_TOOLS) # Or specific tools agent = Agent(tools=[transcribe, text_to_speech]) ``` ```python theme={null} from lye import BROWSER_TOOLS from lye.browser import screenshot, extract_text # Use all browser tools agent = Agent(tools=BROWSER_TOOLS) # Or specific tools agent = Agent(tools=[screenshot, extract_text]) ``` ```python theme={null} from lye import FILES_TOOLS from lye.files import read_file, write_file, list_directory # Use all file tools agent = Agent(tools=FILES_TOOLS) # Or specific tools agent = Agent(tools=[read_file, write_file]) ``` ```python theme={null} from lye import IMAGE_TOOLS from lye.image import analyze_image, extract_text_from_image # Use all image tools agent = Agent(tools=IMAGE_TOOLS) # Or specific tools agent = Agent(tools=[analyze_image, extract_text_from_image]) ``` ```python theme={null} from lye import WEB_TOOLS from lye.web import search, fetch_page # Use all web tools agent = Agent(tools=WEB_TOOLS) # Or specific tools agent = Agent(tools=[search, fetch_page]) ``` ## Tool Structure Every tool in Slide follows the OpenAI function calling format: ```python theme={null} # Example of a Lye tool definition def custom_tool_implementation(param1: str, param2: int = 10) -> str: """Implementation of the tool.""" # Your logic here return f"Processed {param1} with value {param2}" # Tool definition custom_tool = { "definition": { "type": "function", "function": { "name": "custom_tool", "description": "Does something useful with the input", "parameters": { "type": "object", "properties": { "param1": { "type": "string", "description": "The main input parameter" }, "param2": { "type": "integer", "description": "Optional configuration value", "default": 10 } }, "required": ["param1"] } } }, "implementation": custom_tool_implementation } ``` ## Creating custom tools ### Simple function tool The easiest way to create a tool: ```python theme={null} def word_counter_implementation(text: str) -> str: """Count words in the provided text.""" words = text.split() result = { "word_count": len(words), "character_count": len(text), "unique_words": len(set(words)) } return str(result) # Create tool definition word_counter = { "definition": { "type": "function", "function": { "name": "word_counter", "description": "Count words in text", "parameters": { "type": "object", "properties": { "text": { "type": "string", "description": "The text to analyze" } }, "required": ["text"] } } }, "implementation": word_counter_implementation } # Use with an agent from tyler import Agent agent = Agent( name="text-analyzer", model_name="gpt-4", purpose="To analyze text", tools=[word_counter] ) ``` ### Advanced tool example For more complex tools with external dependencies: ```python theme={null} import aiohttp import json async def weather_tool_implementation(city: str, units: str = "celsius") -> str: """Get current weather for a city.""" api_key = "your-api-key" # In practice, use environment variables async with aiohttp.ClientSession() as session: url = f"https://api.weather.com/v1/current" params = { "city": city, "units": units, "api_key": api_key } try: async with session.get(url, params=params) as response: if response.status == 200: data = await response.json() return json.dumps({ "temperature": data["temp"], "conditions": data["conditions"], "humidity": data["humidity"] }) else: return json.dumps({"error": f"API returned status {response.status}"}) except Exception as e: return json.dumps({"error": str(e)}) # Tool definition weather_tool = { "definition": { "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a city", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "City name" }, "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" } }, "required": ["city"] } } }, "implementation": weather_tool_implementation } ``` ## Tool patterns ### 1. Validation Pattern Always validate inputs: ```python theme={null} def safe_calculator_implementation(expression: str) -> str: # Validate input allowed_chars = set("0123456789+-*/()., ") if not all(c in allowed_chars for c in expression): return json.dumps({"error": "Invalid characters in expression"}) try: # Safe evaluation after validation result = eval(expression) return json.dumps({"result": result}) except Exception as e: return json.dumps({"error": str(e)}) safe_calculator = { "definition": { "type": "function", "function": { "name": "safe_calculator", "description": "Safe math calculations", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "Mathematical expression to evaluate" } }, "required": ["expression"] } } }, "implementation": safe_calculator_implementation } ``` ### 2. Resource Management Pattern Properly manage external resources: ```python theme={null} import asyncpg import json async def database_query_implementation(query: str, database: str = "main") -> str: """Execute a database query safely.""" conn = None try: # Connect to database conn = await asyncpg.connect(f"postgresql://localhost/{database}") # Execute query result = await conn.fetch(query) # Convert to JSON-serializable format data = [dict(record) for record in result] return json.dumps({"data": data, "count": len(data)}) except Exception as e: return json.dumps({"error": str(e)}) finally: if conn: await conn.close() # Always cleanup database_tool = { "definition": { "type": "function", "function": { "name": "query_database", "description": "Execute a database query", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "SQL query to execute" }, "database": { "type": "string", "description": "Database name", "default": "main" } }, "required": ["query"] } } }, "implementation": database_query_implementation } ``` ### 3. Error Handling Pattern Provide meaningful error messages: ```python theme={null} import aiohttp import json async def api_caller_implementation(url: str, method: str = "GET", data: dict = None) -> str: """Make API calls with proper error handling.""" try: async with aiohttp.ClientSession() as session: kwargs = {"url": url} if data and method in ["POST", "PUT", "PATCH"]: kwargs["json"] = data async with session.request(method, **kwargs) as response: response_text = await response.text() if response.status >= 400: return json.dumps({ "error": f"API error: {response.status}", "details": response_text }) try: response_data = json.loads(response_text) return json.dumps({"data": response_data}) except json.JSONDecodeError: return json.dumps({"data": response_text}) except aiohttp.ClientError as e: return json.dumps({"error": f"Network error: {str(e)}"}) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}) api_tool = { "definition": { "type": "function", "function": { "name": "api_caller", "description": "Make API calls", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "API endpoint URL" }, "method": { "type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"], "default": "GET" }, "data": { "type": "object", "description": "Request body data" } }, "required": ["url"] } } }, "implementation": api_caller_implementation } ``` ### 4. Rate Limiting Implement rate limiting for external services: ```python theme={null} import asyncio from datetime import datetime, timedelta import json class RateLimiter: def __init__(self, max_calls: int, time_window: timedelta): self.max_calls = max_calls self.time_window = time_window self.calls = [] self.lock = asyncio.Lock() async def acquire(self): async with self.lock: now = datetime.now() # Remove old calls outside the time window self.calls = [call_time for call_time in self.calls if now - call_time < self.time_window] if len(self.calls) >= self.max_calls: # Calculate wait time oldest_call = min(self.calls) wait_time = (oldest_call + self.time_window - now).total_seconds() if wait_time > 0: await asyncio.sleep(wait_time) return await self.acquire() # Retry self.calls.append(now) # Global rate limiter for API calls api_limiter = RateLimiter(max_calls=60, time_window=timedelta(minutes=1)) async def rate_limited_api_implementation(endpoint: str) -> str: """Make rate-limited API calls.""" await api_limiter.acquire() # Make the actual API call async with aiohttp.ClientSession() as session: async with session.get(f"https://api.example.com/{endpoint}") as response: data = await response.json() return json.dumps(data) ``` ## Tool Best Practices Write clear, concise descriptions that help agents understand when to use the tool: ```python theme={null} # Good weather_tool = { "definition": { "type": "function", "function": { "name": "get_weather", "description": "Get current weather conditions for any city worldwide" } } } # Too vague weather_tool = { "definition": { "type": "function", "function": { "name": "weather", "description": "Weather tool" } } } ``` Document all parameters thoroughly: ```python theme={null} "parameters": { "type": "object", "properties": { "source_lang": { "type": "string", "description": "Source language code (e.g., 'en', 'es', 'fr')" }, "target_lang": { "type": "string", "description": "Target language code (e.g., 'en', 'es', 'fr')" }, "text": { "type": "string", "description": "Text to translate (max 5000 characters)" } }, "required": ["text", "target_lang"] } ``` Make tools idempotent when possible: ```python theme={null} import os import json def create_file_implementation(path: str, content: str) -> str: """Create a file if it doesn't exist.""" if os.path.exists(path): return json.dumps({"status": "already_exists", "path": path}) with open(path, 'w') as f: f.write(content) return json.dumps({"status": "created", "path": path}) ``` Always return strings (JSON) from tool implementations: ```python theme={null} # Good - returns JSON string def tool_implementation(param: str) -> str: result = {"data": process(param)} return json.dumps(result) # Bad - returns dict def tool_implementation(param: str) -> dict: return {"data": process(param)} # Will cause errors! ``` ## Testing tools Always test your custom tools: ```python theme={null} import pytest import json @pytest.mark.asyncio async def test_weather_tool(): # Mock the implementation for testing async def mock_weather_implementation(city: str, units: str = "celsius") -> str: if city == "London": return json.dumps({ "temperature": 15, "conditions": "Cloudy", "humidity": 70 }) else: return json.dumps({"error": "City not found"}) # Replace implementation for testing weather_tool["implementation"] = mock_weather_implementation # Test successful call result = await weather_tool["implementation"]("London") data = json.loads(result) assert "temperature" in data assert "conditions" in data assert data["temperature"] == 15 @pytest.mark.asyncio async def test_weather_tool_error(): result = await weather_tool["implementation"]("InvalidCity") data = json.loads(result) assert "error" in data ``` ## Tool Composition Combine multiple tools for complex operations: ```python theme={null} from tyler import Agent, Thread, Message from lye import WEB_TOOLS, FILES_TOOLS, IMAGE_TOOLS # Create a research agent with multiple tool categories agent = Agent( name="researcher", model_name="gpt-4", purpose="To conduct comprehensive research", tools=[ *WEB_TOOLS, # search, fetch *FILES_TOOLS, # read_file, write_file *IMAGE_TOOLS # analyze_image, extract_text_from_image ] ) # Example usage thread = Thread() message = Message( role="user", content=""" 1. Search for information about the James Webb telescope 2. Fetch detailed content from NASA's website 3. Analyze any images found 4. Save a comprehensive report """ ) thread.add_message(message) result = await agent.run(thread) ``` ## Next steps Explore all available tools Use tools from MCP servers See practical tool examples Detailed tool API docs # A2A integration guide Source: https://slide.mintlify.app/guides/a2a-integration Step-by-step guide to integrating Agent-to-Agent (A2A) Protocol v0.3.0 with Tyler agents This guide walks you through integrating the Agent-to-Agent (A2A) Protocol v0.3.0 with your Tyler agents, enabling multi-agent coordination and delegation across different platforms. Use A2A when one agent delegates to or exposes another agent. Use [MCP](/guides/mcp-integration) for external tools, services, and data sources. **💻 Code Examples** Expose a Tyler agent via A2A Connect to remote A2A agents Coordinate multiple A2A agents ## Prerequisites Before starting, ensure you have: * Tyler installed and working * Basic understanding of Tyler agents * Python 3.11+ environment ## Installation Tyler includes A2A support out of the box: ```bash theme={null} # Install Tyler (includes a2a-sdk) uv add slide-tyler # For server functionality, these are already included: # - fastapi # - uvicorn ``` The A2A integration is fully supported in Tyler. All dependencies are included automatically. ## Part 1: Connecting to Remote A2A Agents (Client Mode) ### Step 1: Create an A2A Adapter ```python theme={null} from tyler.a2a import A2AAdapter # Create adapter for connecting to remote agents adapter = A2AAdapter() ``` ### Step 2: Connect to Remote Agents ```python theme={null} import asyncio async def connect_to_agents(): # Connect to a research specialist agent research_connected = await adapter.connect( name="research_agent", base_url="https://research-ai.example.com" ) # Connect to an analysis specialist agent analysis_connected = await adapter.connect( name="analysis_agent", base_url="https://analysis-ai.example.com" ) if research_connected and analysis_connected: print("Connected to both remote agents") # Check agent capabilities for agent_name in ["research_agent", "analysis_agent"]: info = adapter.client.get_connection_info(agent_name) print(f"{agent_name}:") print(f" Protocol version: {info['protocol_version']}") print(f" Capabilities: {info['capabilities']}") print(f" Push notifications: {info['push_notifications_supported']}") else: print("Failed to connect to one or more agents") asyncio.run(connect_to_agents()) ``` ### Step 3: Create Tyler Agent with Delegation Tools ```python theme={null} from tyler import Agent # Get delegation tools from connected agents delegation_tools = adapter.get_tools_for_agent() # Create Tyler agent that can delegate tasks coordinator = Agent( name="Project Coordinator", model_name="gpt-4.1", purpose="""You coordinate complex projects by delegating specialized tasks to remote agents. You have access to: - Research agent: For web research, fact-checking, and information gathering - Analysis agent: For data analysis, insights, and strategic recommendations Use these agents strategically to break down complex requests.""", tools=delegation_tools ) ``` ### Step 4: Use the Coordinating Agent ```python theme={null} from tyler import Thread, Message async def coordinate_project(): # Create a complex request thread = Thread() thread.add_message(Message( role="user", content="""I need a comprehensive market analysis for electric vehicle charging stations. Please: 1. Research current market size, key players, and growth trends 2. Analyze competitive landscape and identify opportunities 3. Provide strategic recommendations for market entry """ )) # The coordinator will automatically delegate to appropriate agents result = await coordinator.run(thread) # Print the coordinated response for message in result.thread.messages: if message.role == "assistant": print(f"Coordinator: {message.content}") asyncio.run(coordinate_project()) ``` ## Part 2: Exposing Tyler Agents via A2A (Server Mode) ### Step 1: Create a Specialized Tyler Agent ```python theme={null} from tyler import Agent from lye import WEB_TOOLS, FILES_TOOLS # Create a specialized research agent research_agent = Agent( name="Research Specialist", model_name="gpt-4.1", purpose="""You are an expert research specialist with web search and document processing capabilities. Your expertise includes: - Comprehensive web research and fact-finding - Academic and market research - Document analysis and summarization - Competitive intelligence gathering Always provide well-sourced, accurate information.""", tools=[*WEB_TOOLS, *FILES_TOOLS] ) ``` ### Step 2: Create A2A Server with Authentication ```python theme={null} from tyler.a2a import A2AServer # Create server to expose the agent server = A2AServer( tyler_agent=research_agent, agent_card={ "name": "Tyler Research Specialist", "version": "1.0.0", "description": "AI research specialist with web search and document processing", "capabilities": [ "web_research", "fact_checking", "document_analysis", "market_research", "competitive_intelligence", "artifacts" ], "contact": { "name": "Your Organization", "email": "ai-support@yourorg.com" }, "vendor": "Tyler Framework" }, authentication={ "schemes": ["bearer"], "required": True } ) ``` ### Step 3: Start the A2A Server ```python theme={null} async def start_research_service(): print("Starting Tyler Research Specialist A2A Server...") print("Other agents can connect at: http://localhost:8000") print("Agent Card available at: http://localhost:8000/.well-known/agent-card.json") # Start the server (this will run indefinitely) await server.start_server(host="0.0.0.0", port=8000) # Run the server if __name__ == "__main__": try: asyncio.run(start_research_service()) except KeyboardInterrupt: print("\nServer stopped by user") ``` ## Part 3: Working with Part Types ### Sending Files to Remote Agents ```python theme={null} from tyler.a2a import A2AAdapter, FilePart adapter = A2AAdapter() await adapter.connect("document_processor", "https://docs.example.com") # Send a file for processing task_id = await adapter.create_task_with_files( "document_processor", "Summarize the key points from this document", files=["./reports/annual_report_2024.pdf"], context_id="annual-review" ) # Or create FilePart manually for more control file_part = FilePart.from_path("./data/analysis.xlsx") print(f"File: {file_part.name}, Size: {len(file_part.file_with_bytes)} bytes") ``` ### Sending Structured Data ```python theme={null} from tyler.a2a import A2AAdapter, DataPart adapter = A2AAdapter() await adapter.connect("analysis_agent", "https://analysis.example.com") # Send structured data for analysis task_id = await adapter.create_task_with_data( "analysis_agent", "Perform trend analysis on this data", data={ "dataset": "sales_2024", "metrics": ["revenue", "units", "margin"], "grouping": "monthly", "filters": { "region": ["NA", "EU"], "product_category": "electronics" } }, context_id="sales-analysis" ) ``` ## Part 4: Working with Artifacts ### Retrieving Task Artifacts ```python theme={null} from tyler.a2a import A2AClient, TextPart, DataPart client = A2AClient() await client.connect("agent", "https://agent.example.com") # Create a task task_id = await client.create_task("agent", "Generate a comprehensive report") # Wait for completion (in production, use push notifications instead) import asyncio while True: status = await client.get_task_status("agent", task_id) if status["status"] in ["completed", "error"]: break await asyncio.sleep(1) # Get artifacts artifacts = await client.get_task_artifacts("agent", task_id) for artifact in artifacts: print(f"Artifact: {artifact.name}") print(f" ID: {artifact.artifact_id}") print(f" Created: {artifact.created_at}") for part in artifact.parts: if isinstance(part, TextPart): print(f" Text content: {part.text[:200]}...") elif isinstance(part, DataPart): print(f" Data: {part.data}") ``` ## Part 5: Context-Based Task Grouping ```python theme={null} from tyler.a2a import A2AClient client = A2AClient() await client.connect("agent", "https://agent.example.com") # Define a context for related tasks context_id = "market-research-project-q4" # Create multiple related tasks tasks = [ ("Research competitor pricing", "phase1-research"), ("Analyze market trends", "phase2-analysis"), ("Generate recommendations", "phase3-synthesis"), ] task_ids = [] for description, tag in tasks: task_id = await client.create_task( "agent", f"[{tag}] {description}", context_id=context_id ) task_ids.append(task_id) # Get all tasks in the context related_task_ids = client.get_tasks_by_context(context_id) print(f"Tasks in context: {len(related_task_ids)}") ``` ## Part 6: Push Notifications for Long-Running Tasks ### Configuring Push Notifications ```python theme={null} from tyler.a2a import A2AClient, PushNotificationConfig client = A2AClient() await client.connect("agent", "https://agent.example.com") # Configure webhook for notifications push_config = PushNotificationConfig( webhook_url="https://your-app.com/webhooks/a2a-events", events=[ "task.created", "task.updated", "task.completed", "task.failed", "task.artifact" ], headers={ "Authorization": "Bearer your-webhook-secret", "X-App-ID": "my-coordinator" }, secret="hmac-signing-secret" # For payload verification ) # Create task with push notifications task_id = await client.create_task( "agent", "Perform comprehensive analysis (this may take a while)", push_notification_config=push_config ) print(f"Task {task_id} created. Updates will be sent to your webhook.") ``` ### Handling Webhook Events ```python theme={null} from fastapi import FastAPI, Request, HTTPException import hmac import hashlib app = FastAPI() WEBHOOK_SECRET = "hmac-signing-secret" @app.post("/webhooks/a2a-events") async def handle_a2a_event(request: Request): # Verify signature signature = request.headers.get("X-A2A-Signature", "") body = await request.body() expected = hmac.new( WEBHOOK_SECRET.encode(), body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(f"sha256={expected}", signature): raise HTTPException(status_code=401, detail="Invalid signature") # Process event event = await request.json() event_type = event["event_type"] task_id = event["task_id"] if event_type == "task.created": print(f"Task {task_id} created") elif event_type == "task.updated": print(f"Task {task_id} status: {event['data']['status']}") elif event_type == "task.completed": print(f"Task {task_id} completed!") artifacts = event["data"].get("artifacts", []) print(f" Produced {len(artifacts)} artifacts") elif event_type == "task.failed": print(f"Task {task_id} failed: {event['data']['error']}") elif event_type == "task.artifact": artifact = event["data"]["artifact"] print(f"New artifact: {artifact['name']}") return {"status": "received"} ``` ## Part 7: Advanced Multi-Agent Patterns ### Creating Agent Networks ```python theme={null} import asyncio from typing import Dict, Any from tyler.a2a import A2AAdapter class AgentNetwork: """Manages a network of specialized A2A agents.""" def __init__(self): self.adapter = A2AAdapter() self.agents: Dict[str, Any] = {} async def add_agent(self, name: str, base_url: str, specialization: str): """Add a specialized agent to the network.""" connected = await self.adapter.connect(name, base_url) if connected: info = self.adapter.client.get_connection_info(name) self.agents[name] = { "base_url": base_url, "specialization": specialization, "status": "connected", "protocol_version": info.get("protocol_version"), "capabilities": info.get("capabilities", []), } print(f"Added {name} ({specialization})") else: print(f"Failed to connect to {name}") async def create_coordinator(self) -> "Agent": """Create a coordinator agent for this network.""" from tyler import Agent tools = self.adapter.get_tools_for_agent() # Build purpose description based on connected agents specializations = [agent["specialization"] for agent in self.agents.values()] purpose = f"""You coordinate complex tasks across a network of specialized agents. Available specialists: {', '.join(specializations)} Break down complex requests and delegate appropriately to leverage each agent's expertise.""" return Agent( name="Network Coordinator", model_name="gpt-4.1", purpose=purpose, tools=tools ) async def health_check(self): """Check the health of all connected agents.""" for name, info in self.agents.items(): try: status = await self.adapter.get_agent_status(name) if status: print(f"{name}: healthy ({status.get('active_tasks', 0)} active tasks)") else: print(f"{name}: unreachable") except Exception as e: print(f"{name}: error - {e}") # Example usage async def setup_enterprise_network(): network = AgentNetwork() # Add specialized agents await network.add_agent("research", "https://research.corp.com", "Research & Intelligence") await network.add_agent("analysis", "https://analytics.corp.com", "Data Analysis") await network.add_agent("compliance", "https://compliance.corp.com", "Legal & Compliance") # Create coordinator coordinator = await network.create_coordinator() # Health check await network.health_check() return coordinator, network ``` ### Task Streaming and Monitoring Tyler's A2A server supports both streaming and non-streaming request modes: #### Non-Streaming (message/send) For simple requests where you want to wait for the complete response: ```python theme={null} from tyler.a2a import A2AClient async def send_remote_task(): """Send a task and wait for complete response.""" client = A2AClient() await client.connect("agent", "https://agent.example.com") # Send task and wait for full response result = await client.send_task( "agent", "Create a brief executive summary" ) # Access the complete response print(f"Status: {result.status}") for artifact in result.artifacts: for part in artifact.parts: print(part.text) ``` #### Real-Time Streaming (message/stream) When clients call `message/stream`, they receive response tokens as they're generated by the LLM via Server-Sent Events (SSE): ```python theme={null} from tyler.a2a import A2AClient async def stream_remote_task(): """Stream responses from a remote A2A agent.""" client = A2AClient() await client.connect("agent", "https://agent.example.com") # Create a task task_id = await client.create_task( "agent", "Create a comprehensive business plan for a new AI startup" ) print("Streaming response...") print("=" * 50) # Stream tokens as they arrive async for message in client.stream_task_messages("agent", task_id): content = message.get("content", "") print(content, end="", flush=True) print("\n\nTask complete!") ``` For local coordinating agents, you can also stream using Tyler's native streaming: ```python theme={null} from tyler.models.execution import EventType async def stream_coordinated_task(): """Example of streaming responses from coordinated agents.""" thread = Thread() thread.add_message(Message( role="user", content="Create a comprehensive business plan for a new AI startup" )) print("Starting coordinated task execution...") print("=" * 50) async for update in coordinator.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: print(update.data.get("content_chunk", ""), end="", flush=True) elif update.type == EventType.TOOL_SELECTED: tool_name = update.data.get("tool_name", "") if "delegate_to_" in tool_name: agent_name = tool_name.replace("delegate_to_", "") print(f"\n\nDelegating to {agent_name}...") print() elif update.type == EventType.EXECUTION_COMPLETE: print("\n\nTask coordination complete!") ``` ## Part 8: Production Considerations ### Security and Authentication ```python theme={null} # Secure A2A connections async def secure_connection(): adapter = A2AAdapter() # Connect with authentication await adapter.connect( name="secure_agent", base_url="https://secure-agents.company.com", headers={ "Authorization": "Bearer your-secure-token", "X-API-Version": "1.0" } ) ``` ### Error Handling and Resilience ```python theme={null} from typing import Optional class ResilientA2AAdapter: """A2A adapter with built-in resilience patterns.""" def __init__(self): self.adapter = A2AAdapter() self.fallback_agents: Dict[str, str] = {} async def connect_with_fallback( self, primary_url: str, fallback_url: Optional[str] = None ): """Connect with automatic fallback.""" try: success = await self.adapter.connect("primary", primary_url) if success: return True except Exception as e: print(f"Primary connection failed: {e}") if fallback_url: try: return await self.adapter.connect("fallback", fallback_url) except Exception as e: print(f"Fallback connection failed: {e}") return False async def robust_delegation(self, task: str, max_retries: int = 3): """Delegate with retry logic.""" for attempt in range(max_retries): try: return await self._execute_task(task) except Exception as e: if attempt == max_retries - 1: raise print(f"Attempt {attempt + 1} failed: {e}. Retrying...") await asyncio.sleep(2 ** attempt) # Exponential backoff ``` ## Troubleshooting ### Common Issues **Problem**: Getting import errors when using A2A features. **Solution**: A2A SDK is included with Tyler. Ensure you have the latest version: ```bash theme={null} uv add slide-tyler --upgrade ``` **Problem**: Cannot connect to remote A2A agents. **Solutions**: * Verify the agent URL is correct and accessible * Check network connectivity and firewall settings * Ensure the remote agent is running and healthy * Verify authentication credentials if required ```python theme={null} # Test connection manually try: connected = await adapter.connect("test", "https://agent.example.com") if connected: info = adapter.client.get_connection_info("test") print(f"Connected! Protocol: {info['protocol_version']}") else: print("Connection failed - check agent status") except Exception as e: print(f"Connection error: {e}") ``` **Problem**: Webhook notifications are not being received. **Solutions**: * Verify webhook URL is accessible from the agent's network * Check that the URL uses HTTPS (required for security) * Ensure webhook server returns 2xx status codes * Verify HMAC signature validation if using secrets ```python theme={null} # Debug webhook configuration from tyler.a2a import validate_webhook_url url = "https://your-service.com/webhook" if validate_webhook_url(url): print("URL is valid") else: print("URL validation failed - check HTTPS and accessibility") ``` **Problem**: Cannot retrieve artifacts from completed tasks. **Solutions**: * Ensure task has actually completed * Check that the remote agent supports artifacts * Verify connection is still active ```python theme={null} # Check task status before retrieving artifacts status = await client.get_task_status("agent", task_id) print(f"Status: {status['status']}") print(f"Has artifacts: {status['has_artifacts']}") if status["status"] == "completed" and status["has_artifacts"]: artifacts = await client.get_task_artifacts("agent", task_id) ``` ## Best Practices Summary **Connection Management** * Always handle connection failures gracefully * Implement health checks for connected agents * Use connection pooling for high-throughput scenarios **Task Delegation** * Be specific in task descriptions for remote agents * Use context IDs to group related tasks * Monitor task progress with push notifications for long-running tasks * Retrieve and process artifacts for structured results **Security** * Always use HTTPS in production * Implement proper authentication and authorization * Use HMAC signing for webhook verification * Validate agent cards and capabilities **Performance** * Monitor delegation latency and success rates * Use streaming for long-running tasks * Implement caching where appropriate **Error Handling** * Plan for network failures and agent unavailability * Log delegation attempts and outcomes * Provide meaningful error messages to users ## Next steps Now that you have A2A integration working, explore these advanced topics: Deep dive into A2A protocol concepts Advanced agent delegation patterns Official A2A Protocol v0.3.0 specification Deploy A2A agents to production # Adding tools to agents Source: https://slide.mintlify.app/guides/adding-tools Learn how to give your agents new capabilities with tools Tools are what make agents powerful. They allow your agent to interact with the world - searching the web, processing files, analyzing images, and much more. In this guide, you'll learn how to add tools to your agents. **💻 Code Examples** Get started with agent tools Pick specific tools from groups Import and combine tool groups Analyze images with agents ## Understanding Tools in Slide Tools in Slide follow the OpenAI function calling format. Each tool has: * A **definition** that describes what it does * An **implementation** that executes the action ## Using Lye's built-in tools The easiest way to add tools is using Lye's pre-built tool groups: ```python theme={null} from tyler import Agent from lye import WEB_TOOLS, IMAGE_TOOLS, FILES_TOOLS, AUDIO_TOOLS, BROWSER_TOOLS # Agent with all capabilities agent = Agent( name="powerful-assistant", model_name="gpt-4", purpose="To help with any task", tools=[ *WEB_TOOLS, # search, fetch *IMAGE_TOOLS, # analyze_image, extract_text_from_image *FILES_TOOLS, # read_file, write_file, list_files *AUDIO_TOOLS, # transcribe, text_to_speech *BROWSER_TOOLS # screenshot, extract_text_from_webpage ] ) ``` ## Tool Groups Explained ### Web Tools Perfect for research and information gathering: ```python theme={null} from lye import WEB_TOOLS # Includes: # - search: Search the web for information # - fetch: Get content from a specific URL ``` ### Image Tools For visual analysis and OCR: ```python theme={null} from lye import IMAGE_TOOLS # Includes: # - analyze_image: Describe images and answer questions # - extract_text_from_image: OCR text extraction ``` ### File Tools For reading and writing files: ```python theme={null} from lye import FILES_TOOLS # Includes: # - read_file: Read file contents # - write_file: Create or update files # - list_files: List directory contents ``` ### Audio Tools For speech processing: ```python theme={null} from lye import AUDIO_TOOLS # Includes: # - transcribe: Convert speech to text # - text_to_speech: Generate speech from text ``` ### Browser Tools For web automation: ```python theme={null} from lye import BROWSER_TOOLS # Includes: # - screenshot: Capture webpage screenshots # - extract_text_from_webpage: Get clean text from pages ``` ## Selective Tool Usage Sometimes you only need specific tools: ```python theme={null} from lye.web import search from lye.files import write_file from lye.image import analyze_image agent = Agent( name="research-writer", model_name="gpt-4", purpose="To research topics and write reports", tools=[search, write_file, analyze_image] # Only what's needed ) ``` ## Creating custom tools You can create your own tools by following the OpenAI function format: ```python theme={null} def get_weather(location: str, unit: str = "celsius") -> str: """Get the current weather for a location.""" # Your implementation here return f"The weather in {location} is sunny and 22°{unit[0].upper()}" # Tool definition weather_tool = { "definition": { "name": "get_weather", "description": "Get the current weather for a location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and country, e.g. San Francisco, USA" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit" } }, "required": ["location"] } }, "implementation": get_weather } # Use your custom tool agent = Agent( name="weather-assistant", model_name="gpt-4", purpose="To provide weather information", tools=[weather_tool] ) ``` ## Advanced Custom Tools Here's a more complex example with async support and error handling: ```python theme={null} import aiohttp import json async def call_api(endpoint: str, method: str = "GET", data: dict = None) -> str: """Make an API call to an external service.""" async with aiohttp.ClientSession() as session: try: if method == "GET": async with session.get(endpoint) as response: result = await response.json() else: async with session.post(endpoint, json=data) as response: result = await response.json() return json.dumps(result, indent=2) except Exception as e: return f"Error calling API: {str(e)}" api_tool = { "definition": { "name": "call_api", "description": "Make HTTP API calls to external services", "parameters": { "type": "object", "properties": { "endpoint": { "type": "string", "description": "The API endpoint URL" }, "method": { "type": "string", "enum": ["GET", "POST"], "description": "HTTP method" }, "data": { "type": "object", "description": "Data to send with POST request" } }, "required": ["endpoint"] } }, "implementation": call_api } ``` ## Tool Combinations for Common Tasks ### Research assistant ```python theme={null} from lye import WEB_TOOLS, FILES_TOOLS research_agent = Agent( name="researcher", model_name="gpt-4", purpose="To conduct thorough research and create reports", tools=[*WEB_TOOLS, *FILES_TOOLS] ) ``` ### Content Analyzer ```python theme={null} from lye import IMAGE_TOOLS, AUDIO_TOOLS, FILES_TOOLS analyzer_agent = Agent( name="content-analyzer", model_name="gpt-4", purpose="To analyze multimedia content", tools=[*IMAGE_TOOLS, *AUDIO_TOOLS, *FILES_TOOLS] ) ``` ### Web Scraper ```python theme={null} from lye import WEB_TOOLS, BROWSER_TOOLS, FILES_TOOLS scraper_agent = Agent( name="web-scraper", model_name="gpt-4", purpose="To extract and save information from websites", tools=[*WEB_TOOLS, *BROWSER_TOOLS, *FILES_TOOLS] ) ``` ## Tool Timeouts For tools that might take a long time (API calls, database queries), you can set a timeout using the low-level tool runner: ```python theme={null} from tyler.utils.tool_runner import tool_runner async def slow_api_call(query: str) -> str: """Call an external API that might be slow.""" # ... implementation pass # Register with a 30-second timeout tool_runner.register_tool( name="slow_api_call", implementation=slow_api_call, definition={ "name": "slow_api_call", "description": "Call external API", "parameters": { "type": "object", "properties": { "query": {"type": "string"} }, "required": ["query"] } }, timeout=30.0 # Timeout in seconds ) ``` ### Timeout Behavior When a tool exceeds its timeout: 1. A `TimeoutError` is raised with the message: `Tool 'tool_name' timed out after X seconds` 2. The error is returned to the LLM as the tool result 3. The LLM can then decide how to proceed (retry, try a different approach, or inform the user) Timeouts work for both async and synchronous tools. For sync tools, the operation runs in a thread pool with the timeout applied to the entire operation. For synchronous tools, the underlying thread cannot be forcibly stopped after a timeout—it will continue running in the background. For truly cancellable long-running operations, implement your tools as async functions. You can also specify timeouts in the lye tool format: ```python theme={null} my_tool = { "definition": { "type": "function", "function": { "name": "my_slow_tool", "description": "A tool that might take a while", "parameters": {...} } }, "implementation": my_slow_function, "timeout": 60.0 # 60 second timeout } ``` ## Best practices ### 1. Tool Selection Only give your agent the tools it needs: ```python theme={null} # ❌ Too many tools tools=[*WEB_TOOLS, *IMAGE_TOOLS, *AUDIO_TOOLS, *FILES_TOOLS, *BROWSER_TOOLS] # ✅ Just what's needed tools=[*WEB_TOOLS, *FILES_TOOLS] ``` ### 2. Clear Tool Descriptions When creating custom tools, write clear descriptions: ```python theme={null} # ❌ Vague "description": "Does something with data" # ✅ Clear "description": "Fetches user data from the API and returns formatted profile information" ``` ### 3. Error Handling Always handle errors in custom tools: ```python theme={null} async def safe_api_call(url: str) -> str: try: # API call logic return result except Exception as e: return f"Error: {str(e)}" ``` ### 4. Tool Composition Combine tools for complex workflows: ```python theme={null} # Agent that can research, analyze, and report agent = Agent( name="analyst", model_name="gpt-4", purpose="To analyze data from multiple sources", tools=[ *WEB_TOOLS, # Gather data *IMAGE_TOOLS, # Analyze visuals *FILES_TOOLS # Save results ] ) ``` ## Troubleshooting For OCR support with image-based PDFs, install poppler: * macOS: `brew install poppler` * Ubuntu: `sudo apt-get install poppler-utils` ## Next steps Advanced patterns for tool usage Test agents with mock tools Use MCP tools with your agents Explore all built-in tools # Agent delegation Source: https://slide.mintlify.app/guides/agent-delegation Build multi-agent systems where specialized agents work together Agent delegation is a powerful feature in Tyler that allows you to create specialized agents that can work together to solve complex problems. This pattern enables you to build sophisticated AI systems where each agent has a specific role and expertise. **💻 Code Examples** Multi-agent coordination patterns Cross-platform agent networks ## Overview Agent delegation allows one agent (the coordinator) to delegate tasks to other specialized agents. This is useful when: * Different parts of a task require different expertise * You want to separate concerns and create modular systems * You need different models or configurations for different subtasks * You want to build scalable, maintainable AI systems ## Basic Delegation Here's how to create a simple multi-agent system: ```python theme={null} from tyler import Agent from lye import WEB_TOOLS, FILES_TOOLS, IMAGE_TOOLS # Create specialized agents research_agent = Agent( name="Researcher", model_name="gpt-4", purpose="To conduct thorough research on any topic", tools=WEB_TOOLS ) writer_agent = Agent( name="Writer", model_name="gpt-4", purpose="To write clear, engaging content based on research", tools=FILES_TOOLS ) # Create coordinator that can delegate coordinator = Agent( name="Project Manager", model_name="gpt-4", purpose="To coordinate research and writing tasks", agents=[research_agent, writer_agent] ) # The coordinator automatically delegates based on the task thread = Thread() thread.add_message(Message( role="user", content="Research the latest AI trends and write a report" )) result = await coordinator.run(thread) ``` ## How Delegation Works When an agent has access to other agents: 1. **Automatic Detection**: The coordinator analyzes the task to determine if delegation would be helpful 2. **Agent Selection**: It chooses the most appropriate agent based on their purpose and capabilities 3. **Task Delegation**: The coordinator formulates a clear request for the specialized agent 4. **Result Integration**: The coordinator receives and integrates the results into its response ## Advanced patterns ### Hierarchical teams Create multi-level agent hierarchies: ```python theme={null} # Data team data_analyst = Agent( name="Data Analyst", model_name="gpt-4", purpose="To analyze data and create visualizations", tools=[*IMAGE_TOOLS, "python_repl"] ) data_engineer = Agent( name="Data Engineer", model_name="gpt-3.5-turbo", purpose="To handle data pipelines and ETL processes", tools=FILES_TOOLS ) data_lead = Agent( name="Data Team Lead", model_name="gpt-4", purpose="To coordinate data projects", agents=[data_analyst, data_engineer] ) # Engineering team backend_dev = Agent( name="Backend Developer", model_name="gpt-4", purpose="To develop backend services and APIs", tools=["python_repl", *FILES_TOOLS] ) frontend_dev = Agent( name="Frontend Developer", model_name="gpt-4", purpose="To build user interfaces", tools=FILES_TOOLS ) tech_lead = Agent( name="Tech Lead", model_name="gpt-4", purpose="To coordinate development tasks", agents=[backend_dev, frontend_dev] ) # Project coordinator with access to team leads project_manager = Agent( name="Project Manager", model_name="gpt-4", purpose="To manage complex projects requiring multiple teams", agents=[data_lead, tech_lead] ) ``` ### Specialized agent networks Create agents that can collaborate peer-to-peer: ```python theme={null} # Create a network of specialized agents agents = { "researcher": Agent( name="Researcher", model_name="gpt-4", purpose="To find and synthesize information", tools=WEB_TOOLS ), "fact_checker": Agent( name="Fact Checker", model_name="gpt-4", purpose="To verify claims and check sources", tools=WEB_TOOLS ), "editor": Agent( name="Editor", model_name="gpt-4", purpose="To improve clarity and correctness", tools=[] ) } # Give each agent access to others for collaboration for agent_name, agent in agents.items(): agent.agents = [a for n, a in agents.items() if n != agent_name] # Use any agent as entry point thread = Thread() thread.add_message(Message( role="user", content="Write a fact-checked article about quantum computing" )) # The researcher might delegate to fact_checker and editor result = await agents["researcher"].run(thread) ``` ### Dynamic agent selection Choose agents based on task requirements: ```python theme={null} class DynamicCoordinator: def __init__(self): self.agent_pool = { "creative": Agent( name="Creative", model_name="gpt-4", temperature=0.9, purpose="For creative and imaginative tasks" ), "analytical": Agent( name="Analytical", model_name="gpt-4", temperature=0.1, purpose="For precise, logical analysis" ), "coder": Agent( name="Coder", model_name="gpt-4", purpose="For programming tasks", tools=["python_repl", *FILES_TOOLS] ) } async def handle_task(self, task: str) -> Any: # Analyze task to select agents selected_agents = [] task_lower = task.lower() if any(word in task_lower for word in ["create", "imagine", "design"]): selected_agents.append(self.agent_pool["creative"]) if any(word in task_lower for word in ["analyze", "compare", "evaluate"]): selected_agents.append(self.agent_pool["analytical"]) if any(word in task_lower for word in ["code", "program", "implement"]): selected_agents.append(self.agent_pool["coder"]) # Create coordinator with selected agents coordinator = Agent( name="Dynamic Coordinator", model_name="gpt-4", purpose=f"To coordinate: {task}", agents=selected_agents ) thread = Thread() thread.add_message(Message(role="user", content=task)) return await coordinator.run(thread) ``` ## Best practices ### 1. Clear Purpose Definition Each agent should have a well-defined purpose: ```python theme={null} # Good - specific and clear agent = Agent( name="Python Expert", purpose="To write, review, and debug Python code following PEP 8 standards" ) # Too vague agent = Agent( name="Helper", purpose="To help with stuff" ) ``` ### 2. Appropriate Tool Assignment Give agents only the tools they need: ```python theme={null} # Security researcher doesn't need file write access security_agent = Agent( name="Security Auditor", purpose="To analyze code for security vulnerabilities", tools=["read_file", "search"] # Read-only tools ) # Code fixer needs write access fix_agent = Agent( name="Security Fixer", purpose="To fix identified security issues", tools=FILES_TOOLS # Full file access ) ``` ### 3. Model Selection Choose appropriate models for each agent: ```python theme={null} # Use more powerful model for complex reasoning architect = Agent( name="System Architect", model_name="gpt-4", purpose="To design complex system architectures" ) # Use faster/cheaper model for simple tasks formatter = Agent( name="Code Formatter", model_name="gpt-3.5-turbo", purpose="To format code according to style guides" ) ``` ### 4. Delegation Depth Limit delegation depth to avoid complexity: ```python theme={null} # Configure maximum delegation depth coordinator = Agent( name="Coordinator", agents=[agent1, agent2], metadata={"max_delegation_depth": 2} ) ``` ## Common Patterns ### Research and Analysis Pipeline ```python theme={null} # Sequential pipeline of agents pipeline = [ Agent(name="Gatherer", purpose="To collect raw information", tools=WEB_TOOLS), Agent(name="Analyzer", purpose="To analyze and extract insights"), Agent(name="Reporter", purpose="To create final report", tools=FILES_TOOLS) ] async def run_pipeline(task: str): context = task for agent in pipeline: thread = Thread() thread.add_message(Message(role="user", content=context)) result = await agent.run(thread) context = result.new_messages[-1].content return context ``` ### Consensus building Multiple agents provide input: ```python theme={null} # Get perspectives from different agents perspectives = [] for agent in [optimist_agent, pessimist_agent, realist_agent]: thread = Thread() thread.add_message(Message( role="user", content="What are the implications of AGI?" )) result = await agent.run(thread) perspectives.append({ "agent": agent.name, "view": result.new_messages[-1].content }) # Synthesizer agent combines perspectives synthesizer = Agent( name="Synthesizer", purpose="To combine multiple viewpoints into balanced analysis" ) thread = Thread() thread.add_message(Message( role="user", content=f"Synthesize these perspectives on AGI: {perspectives}" )) result = await synthesizer.run(thread) ``` ## Performance Considerations ### 1. Parallel Delegation When agents work independently, run them in parallel: ```python theme={null} import asyncio async def parallel_research(topics: List[str]): research_tasks = [] for topic in topics: agent = Agent( name=f"Researcher-{topic}", purpose=f"To research {topic}", tools=WEB_TOOLS ) thread = Thread() thread.add_message(Message( role="user", content=f"Research {topic} and summarize findings" )) research_tasks.append(agent.run(thread)) # Run all research in parallel results = await asyncio.gather(*research_tasks) return results ``` ### 2. Caching Agent Responses Cache responses from specialized agents: ```python theme={null} from functools import lru_cache class CachedCoordinator: def __init__(self): self.agents = {} self.cache = {} async def delegate_with_cache(self, agent_name: str, task: str): cache_key = f"{agent_name}:{task}" if cache_key in self.cache: return self.cache[cache_key] result = await self.agents[agent_name].go_for_task(task) self.cache[cache_key] = result return result ``` ### 3. Agent Pool Management Reuse agent instances: ```python theme={null} class AgentPool: def __init__(self, agent_config: Dict): self.available = {name: Agent(**config) for name, config in agent_config.items()} self.busy = set() async def get_agent(self, agent_type: str): if agent_type in self.available: agent = self.available.pop(agent_type) self.busy.add(agent) return agent else: # Wait for agent to become available await asyncio.sleep(0.1) return await self.get_agent(agent_type) def release_agent(self, agent: Agent): self.busy.remove(agent) self.available[agent.name] = agent ``` ## Error handling Handle delegation failures gracefully: ```python theme={null} class ResilientCoordinator: def __init__(self, agents: List[Agent], fallback_agent: Agent): self.agents = agents self.fallback = fallback_agent async def delegate_with_fallback(self, task: str): for agent in self.agents: try: thread = Thread() thread.add_message(Message(role="user", content=task)) result = await agent.run(thread) return result except Exception as e: print(f"Agent {agent.name} failed: {e}") continue # All agents failed, use fallback thread = Thread() thread.add_message(Message(role="user", content=task)) return await self.fallback.run(thread) ``` ## Testing multi-agent systems ```python theme={null} from tyler.eval import AgentEval, Conversation, Turn, Expectation # Test agent delegation eval = AgentEval( name="delegation_test", conversations=[ Conversation( id="multi_step_task", turns=[ Turn( role="user", content="Research Python async patterns and write example code", expect=Expectation( uses_delegation=["Researcher", "Coder"], completes_task=True ) ) ] ) ] ) # Run evaluation results = await eval.run(coordinator) ``` ## Next steps Add tools to your specialized agents Test your multi-agent systems Optimize agent coordination See agent delegation examples # Conversation persistence Source: https://slide.mintlify.app/guides/conversation-persistence Build agents that store conversations and maintain context across sessions Conversation persistence is crucial for building agents that maintain context across sessions. With Slide's Narrator integration, your agents can store and retrieve past interactions, maintain conversation history, and provide contextual experiences. **💻 Code Examples** Store and resume conversations Handle files in conversations ## Why Conversation Persistence Matters Without persistence, every interaction starts from scratch: * No context from previous messages * Can't remember user preferences * Can't track ongoing tasks * Poor user experience With persistence, your agent becomes truly useful: * Stores conversation history * Maintains context across sessions * Can resume interrupted tasks * Provides contextual responses ## Quick Start with Persistence ```python theme={null} import asyncio from tyler import Agent, Thread, Message, ThreadStore, FileStore async def create_agent_with_persistence(): # Set up persistent storage # Option 1: SQLite (simple, local) thread_store = await ThreadStore.create("sqlite+aiosqlite:///conversations.db") # Option 2: PostgreSQL with Docker (production-ready) # Run: uv run narrator docker-setup # thread_store = await ThreadStore.create( # "postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator" # ) file_store = await FileStore.create(base_path="./conversation_files") # Create agent with persistence agent = Agent( name="assistant", model_name="gpt-4", purpose="To be a helpful assistant that maintains conversation history", thread_store=thread_store, file_store=file_store ) return agent, thread_store, file_store # Use the agent async def main(): agent, thread_store, _ = await create_agent_with_persistence() # Create or resume a thread thread_id = "user-123-main" try: thread = await thread_store.get_thread(thread_id) print("Welcome back!") except: thread = Thread(id=thread_id) print("Nice to meet you!") # Continue the conversation... ``` ## Storage Backends Narrator supports multiple storage backends: ### SQLite (Development) Perfect for local development and single-user applications: ```python theme={null} thread_store = await ThreadStore.create("sqlite+aiosqlite:///app.db") ``` ### PostgreSQL (Production) Scalable for multi-user applications: ```python theme={null} thread_store = await ThreadStore.create( "postgresql+asyncpg://user:pass@localhost/dbname" ) ``` #### Quick PostgreSQL Setup with Docker Narrator includes built-in Docker commands for easy PostgreSQL setup: ```bash theme={null} # One-command setup that starts PostgreSQL and initializes tables uv run narrator docker-setup # This creates a PostgreSQL database at: # postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator ``` To manage the database: ```bash theme={null} # Stop container (keeps data) uv run narrator docker-stop # Stop and remove all data uv run narrator docker-stop --remove-volumes # Start again uv run narrator docker-start ``` ### In-Memory (Testing) For unit tests and temporary storage: ```python theme={null} thread_store = await ThreadStore.create() # No URL = in-memory ``` ## Thread Management Threads are containers for conversations. Each thread has a unique ID and contains messages. ### Creating Threads ```python theme={null} # Auto-generated ID thread = Thread() # Custom ID (useful for user-specific threads) thread = Thread(id="user-123-support") # With metadata thread = Thread( id="project-research", metadata={"project": "quantum-computing", "created_by": "alice"} ) ``` ### Saving and Loading Threads ```python theme={null} # Save a thread await thread_store.save_thread(thread) # Load a thread thread = await thread_store.get_thread("thread-id") # List all threads threads = await thread_store.list_threads() # Delete a thread await thread_store.delete_thread("thread-id") ``` ## Message History Messages in threads maintain full conversation context: ```python theme={null} # Add messages to thread thread.add_message(Message(role="user", content="What's the capital of France?")) thread.add_message(Message(role="assistant", content="The capital of France is Paris.")) thread.add_message(Message(role="user", content="What about Germany?")) # Access message history for msg in thread.messages: print(f"{msg.role}: {msg.content}") # The agent sees all previous messages when processing result = await agent.run(thread) ``` ## Conversation Patterns ### Pattern 1: User-Specific Threads ```python theme={null} async def get_user_thread(user_id: str, thread_store: ThreadStore): thread_id = f"user-{user_id}-main" try: return await thread_store.get_thread(thread_id) except: return Thread(id=thread_id) # Usage thread = await get_user_thread("alice@example.com", thread_store) ``` ### Pattern 2: Topic-Based Threads ```python theme={null} async def create_research_thread(topic: str, thread_store: ThreadStore): thread = Thread( id=f"research-{topic.lower().replace(' ', '-')}", metadata={"type": "research", "topic": topic} ) # Add initial context thread.add_message(Message( role="system", content=f"This thread is for researching: {topic}" )) await thread_store.save_thread(thread) return thread ``` ### Pattern 3: Session Management ```python theme={null} class ConversationSession: def __init__(self, agent, thread_store): self.agent = agent self.thread_store = thread_store self.thread = None async def start_or_resume(self, session_id: str): try: self.thread = await self.thread_store.get_thread(session_id) return "resumed" except: self.thread = Thread(id=session_id) return "new" async def send_message(self, content: str): message = Message(role="user", content=content) self.thread.add_message(message) result = await self.agent.run(self.thread) self.thread = result.thread await self.thread_store.save_thread(self.thread) return result.new_messages ``` ## File Attachments FileStore handles attachments in conversations: ```python theme={null} from tyler import Attachment # Create agent with file storage file_store = await FileStore.create(base_path="./uploads") agent = Agent( name="file-assistant", model_name="gpt-4", purpose="To help with file processing", file_store=file_store ) # Add message with attachment message = Message( role="user", content="Please analyze this image" ) with open("chart.png", "rb") as f: attachment = Attachment( filename="chart.png", content=f.read(), mime_type="image/png" ) message.add_attachment(attachment) thread.add_message(message) # Files are automatically saved and managed result = await agent.run(thread) ``` ## Advanced Persistence Patterns ### Conversation Summarization ```python theme={null} async def summarize_old_conversations(thread: Thread, max_messages: int = 50): if len(thread.messages) > max_messages: # Get messages to summarize old_messages = thread.messages[:-max_messages] # Create summary request summary_thread = Thread() summary_content = "\n".join([ f"{msg.role}: {msg.content}" for msg in old_messages ]) summary_thread.add_message(Message( role="user", content=f"Summarize this conversation:\n\n{summary_content}" )) # Get summary summary_agent = Agent( name="summarizer", model_name="gpt-3.5-turbo", purpose="To create concise summaries" ) summary_result = await summary_agent.run(summary_thread) # Replace old messages with summary thread.messages = [ Message( role="system", content=f"Previous conversation summary: {summary_result.new_messages[-1].content}" ) ] + thread.messages[-max_messages:] ``` ### Context Injection ```python theme={null} async def inject_user_context(thread: Thread, user_id: str): # Load user preferences user_prefs = await load_user_preferences(user_id) # Add context at the beginning context_message = Message( role="system", content=f"User preferences: {json.dumps(user_prefs)}" ) thread.messages.insert(0, context_message) return thread ``` ## Persistence Management Tips ### 1. Thread Naming Conventions ```python theme={null} # Good thread IDs "user-123-main" # User-specific main thread "support-ticket-456" # Support conversation "research-2024-01-15" # Date-based research # Avoid "thread1" # Not descriptive "my-thread" # Not unique ``` ### 2. Metadata Usage ```python theme={null} thread = Thread( id="customer-support-789", metadata={ "customer_id": "cust-123", "issue_type": "billing", "priority": "high", "created_at": datetime.now().isoformat() } ) ``` ### 3. Cleanup Strategies ```python theme={null} async def cleanup_old_threads(thread_store: ThreadStore, days: int = 30): threads = await thread_store.list_threads() cutoff = datetime.now() - timedelta(days=days) for thread in threads: if thread.metadata.get("last_updated") < cutoff.isoformat(): await thread_store.delete_thread(thread.id) ``` ## Real-World Example: Customer Support Agent ```python theme={null} import asyncio from datetime import datetime from tyler import Agent, Thread, Message, ThreadStore, FileStore from lye import WEB_TOOLS class SupportAgent: def __init__(self): self.agent = None self.thread_store = None self.file_store = None async def initialize(self): # Use Narrator's Docker PostgreSQL (after running: uv run narrator docker-setup) self.thread_store = await ThreadStore.create( "postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator" ) # Or use your own PostgreSQL instance: # self.thread_store = await ThreadStore.create( # "postgresql+asyncpg://localhost/support" # ) self.file_store = await FileStore.create("./support_files") self.agent = Agent( name="support-agent", model_name="gpt-4", purpose="To help customers with product issues", tools=[*WEB_TOOLS], thread_store=self.thread_store, file_store=self.file_store ) async def handle_ticket(self, ticket_id: str, customer_id: str, issue: str): # Create thread for this ticket thread = Thread( id=f"ticket-{ticket_id}", metadata={ "ticket_id": ticket_id, "customer_id": customer_id, "status": "open", "created_at": datetime.now().isoformat() } ) # Add initial message thread.add_message(Message( role="user", content=f"Customer {customer_id} reports: {issue}" )) # Process with agent result = await self.agent.run(thread) thread = result.thread # Save thread await self.thread_store.save_thread(thread) # Return response return messages[-1].content if messages else "No response" async def get_ticket_history(self, ticket_id: str): thread = await self.thread_store.get_thread(f"ticket-{ticket_id}") return thread.messages # Usage agent = SupportAgent() await agent.initialize() response = await agent.handle_ticket( ticket_id="12345", customer_id="cust-789", issue="Cannot login to my account" ) ``` ## Database Setup and Configuration ### Environment Variables When using PostgreSQL, you can configure the connection via environment variables: ```bash theme={null} # Set the database URL export NARRATOR_DATABASE_URL="postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator" # Then initialize tables (one-time setup) uv run narrator init # Check database status uv run narrator status ``` ### Connection Pooling For production applications, Narrator automatically configures connection pooling: ```bash theme={null} # Optional pool configuration export NARRATOR_DB_POOL_SIZE=5 # Max connections (default: 5) export NARRATOR_DB_MAX_OVERFLOW=10 # Max overflow connections (default: 10) export NARRATOR_DB_POOL_TIMEOUT=30 # Connection timeout in seconds (default: 30) export NARRATOR_DB_POOL_RECYCLE=300 # Recycle connections after seconds (default: 300) ``` ## Performance Considerations ### 1. Message Limits Keep threads manageable: ```python theme={null} MAX_MESSAGES = 100 if len(thread.messages) > MAX_MESSAGES: # Archive old messages or summarize thread.messages = thread.messages[-MAX_MESSAGES:] ``` ### 2. Batch Operations ```python theme={null} # Save multiple threads efficiently threads = [thread1, thread2, thread3] await asyncio.gather(*[ thread_store.save_thread(t) for t in threads ]) ``` ### 3. Caching ```python theme={null} from functools import lru_cache @lru_cache(maxsize=100) async def get_cached_thread(thread_id: str): return await thread_store.get_thread(thread_id) ``` ## Next steps Deep dive into Narrator features Test agents with conversation persistence Build Slack agents with persistence Complex persistence patterns # MCP Integration Source: https://slide.mintlify.app/guides/mcp-integration Connect your agents to Model Context Protocol servers using declarative config Model Context Protocol (MCP) is an open standard for connecting AI applications to external data sources and tools. Tyler has first-class support for MCP through declarative configuration based on the MCP `2025-11-25` stable framing: Streamable HTTP for remote servers, `stdio` for local servers, and SSE only for legacy compatibility. **💻 Code Examples** Get started with MCP servers Multiple servers with filtering ## Quick Start ### Python API ```python theme={null} from tyler import Agent, Thread, Message # Create agent with MCP config agent = Agent( name="Tyler", model_name="gpt-4.1", tools=["web"], mcp={ "servers": [{ "name": "slide_docs", "transport": "streamablehttp", # Mintlify uses streamablehttp "url": "https://slide.mintlify.app/mcp" }] } ) try: # Connect to MCP servers (fail fast!) await agent.connect_mcp() # Use normally - MCP tools are now available thread = Thread() thread.add_message(Message(role="user", content="How do I create a Tyler agent?")) result = await agent.run(thread) finally: await agent.cleanup() ``` ### CLI (tyler-chat) Add this to your `tyler-chat-config.yaml`: ```yaml theme={null} name: "Tyler" model_name: "gpt-4.1" tools: - "web" mcp: servers: - name: slide_docs transport: streamablehttp # Mintlify uses streamablehttp url: https://slide.mintlify.app/mcp ``` Then run: ```bash theme={null} tyler chat # MCP servers connect automatically on startup! ``` ## How It Works 1. **Agent Creation**: `Agent(mcp={...})` validates config schema immediately (fail fast!) 2. **Connection**: `await agent.connect_mcp()` connects to servers and discovers tools 3. **Tool Registration**: Discovered tools are namespaced (`servername_toolname`) and merged with built-in tools 4. **Usage**: Tools are available in `agent.run()` like any other tool 5. **Cleanup**: `await agent.cleanup()` disconnects MCP servers ## Configuration Reference ### Server Configuration Each MCP server requires: ```python theme={null} { "name": str, # Required: Unique server identifier "transport": str, # Required: "stdio", "streamablehttp", or "sse" # Transport-specific fields (choose based on transport): "url": str, # Required for streamablehttp/sse "command": str, # Required for stdio "args": List[str], # Optional for stdio "env": Dict, # Optional for stdio "cwd": str, # Optional for stdio "encoding": str, # Optional for stdio "encoding_error_handler": str, # Optional for stdio: strict, ignore, replace # Optional fields: "headers": Dict, # Custom HTTP headers (for streamablehttp/sse) "include_tools": List[str], # Whitelist specific tools "exclude_tools": List[str], # Blacklist specific tools "prefix": str, # Custom namespace (default: server name) "fail_silent": bool, # Continue if connection fails (default: true) "max_retries": int, # Positive retry count (default: 3) "timeout_seconds": float, # Connection/request timeout "sse_read_timeout_seconds": float, # Stream read timeout "terminate_on_close": bool, # Streamable HTTP session termination (default: true) "tool_timeout_seconds": float # Per-tool execution/read timeout } ``` ### Transport Types **Streamable HTTP** - For HTTP-based MCP servers (Mintlify, hosted servers): ```python theme={null} { "name": "slide_docs", "transport": "streamablehttp", "url": "https://slide.mintlify.app/mcp" } ``` **stdio** - For local process MCP servers: ```python theme={null} { "name": "filesystem", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] } ``` **SSE (Server-Sent Events)** - Legacy HTTP transport for backward compatibility: ```python theme={null} { "name": "legacy_server", "transport": "sse", "url": "https://legacy.example.com/mcp" } ``` ## Advanced Usage ### Multiple Servers Connect to multiple MCP servers simultaneously: ```python theme={null} agent = Agent( mcp={ "servers": [ {"name": "docs", "transport": "streamablehttp", "url": "https://docs.example.com/mcp"}, {"name": "db", "transport": "streamablehttp", "url": "https://db.example.com/mcp"}, {"name": "files", "transport": "stdio", "command": "mcp-server-files"} ] } ) try: await agent.connect_mcp() finally: await agent.cleanup() ``` ### Tool Filtering Control which tools are registered: ```python theme={null} { "name": "filesystem", "transport": "stdio", "command": "mcp-server-filesystem", "include_tools": ["read_file", "list_directory"], # Whitelist "exclude_tools": ["write_file", "delete_file"] # Blacklist (applied after include) } ``` ### Custom Namespace Override the default namespace prefix: ```python theme={null} { "name": "wandb_documentation_server", # Long name "prefix": "docs", # Short, clean prefix "transport": "streamablehttp", "url": "https://docs.wandb.ai/mcp" } # Tools: docs_search, docs_query (instead of wandb_documentation_server_search) ``` ### Environment Variables Use environment variables for secrets (recommended!): ```yaml theme={null} mcp: servers: - name: api transport: streamablehttp url: https://api.example.com/mcp headers: Authorization: "Bearer ${API_TOKEN}" # Substituted from env ``` Set the environment variable: ```bash theme={null} export API_TOKEN=your_secret_token tyler chat ``` ### Graceful Degradation Control failure behavior per server: ```python theme={null} { "servers": [ { "name": "critical_server", "url": "...", "fail_silent": False # Fail startup if unavailable }, { "name": "optional_server", "url": "...", "fail_silent": True # Continue if unavailable (default) } ] } ``` ## Resource Management ### When to Use `cleanup()` Use `try/finally` and call `await agent.cleanup()` whenever an agent connects to MCP servers. Cleanup disconnects SDK sessions and removes dynamically registered MCP tools from the agent so reconnects do not duplicate definitions. **Long-running applications:** ```python theme={null} # Web server creating agents per request @app.post("/chat") async def chat_endpoint(request): agent = Agent(mcp={...}) try: await agent.connect_mcp() result = await agent.run(thread) return result finally: await agent.cleanup() ``` **Testing or batch processing:** ```python theme={null} # Creating/destroying many agents for task in tasks: agent = Agent(mcp={...}) try: await agent.connect_mcp() await agent.run(thread) finally: await agent.cleanup() ``` Streamable HTTP uses asyncio task groups and remote sessions that especially need explicit cleanup. Using the same pattern for `stdio` and legacy `sse` keeps scripts and tests predictable. ## Security Best Practices Never hardcode API keys or secrets in config files! Always use environment variable substitution (`${VAR}`). **DO:** ```yaml theme={null} mcp: servers: - name: api url: https://api.example.com/mcp headers: Authorization: "Bearer ${API_TOKEN}" # ✓ Good ``` **DON'T:** ```yaml theme={null} mcp: servers: - name: api url: https://api.example.com/mcp headers: Authorization: "Bearer sk-1234567890" # ✗ Bad - secret exposed! ``` **Harden MCP usage:** * Trust only reviewed MCP servers; local `stdio` servers run with the same privileges as the Tyler process. * Treat MCP tool descriptions, annotations, icons, and `_meta` as advisory/untrusted unless the server is trusted. * Prefer `include_tools` and `exclude_tools` so agents only see the tools they need. * Avoid always-on large MCP server catalogs; connect narrow server sets per agent or workflow. * Keep secrets in environment variables and use `${VAR}` substitution in config. ## Troubleshooting ### Connection refused **Error:** `Failed to connect to MCP server 'xyz': Connection refused` **Solutions:** 1. Verify the server URL is correct 2. Check if the MCP server is running 3. For stdio servers, verify the command path is correct 4. Check firewall settings ### Invalid config schema **Error:** `Server 'xyz' with transport 'sse' requires 'url' field` **Solution:** Ensure required fields are present for the transport type: * streamablehttp/SSE: `url` required * stdio: `command` required ### Tools not discovered **Error:** MCP connects but no tools available **Solutions:** 1. Verify the MCP server is functioning (check server logs) 2. Check if tools are being filtered out (remove `include_tools`/`exclude_tools`) 3. Try connecting to the server manually to verify it exposes tools ### Environment variable not substituted **Error:** Connection fails with literal `${VAR}` in URL **Solution:** Ensure the environment variable is set before running: ```bash theme={null} export MY_VAR=value python my_script.py ``` **Under the Hood:** Tyler uses the official MCP SDK's `ClientSessionGroup` to manage connections to MCP servers. The declarative config approach is recommended for all users. See the [API Reference](/api-reference/tyler-agent) for details. ## Next Steps Read the full MCP specification Browse available MCP servers Build custom tools for agents Use MCP with tyler-chat CLI # Advanced patterns Source: https://slide.mintlify.app/guides/patterns Design patterns and best practices for building sophisticated AI applications This guide covers advanced patterns for building production-ready AI applications with Slide. These patterns have been tested in real-world scenarios and represent best practices from the community. ## Configuration Management ### Configuration file pattern Use YAML configuration files to manage agent settings, especially useful for: * Sharing configuration between CLI and Python code * Environment-specific configurations (dev, staging, prod) * Team collaboration (version control) * Quick experimentation without code changes Create `agent-config.yaml`: ```yaml theme={null} name: "research-assistant" model_name: "gpt-4o" temperature: 0.7 purpose: "To help with research by finding, analyzing, and summarizing information" notes: | - Be thorough and cite sources - Save reports to files when requested tools: - "web" - "files" # MCP servers (optional) mcp: servers: - name: "docs" transport: "streamablehttp" url: "https://slide.mintlify.app/mcp" ``` Use `Agent.from_config()` to create your agent: ```python theme={null} from tyler import Agent # Auto-discover (searches ./tyler-chat-config.yaml, ~/.tyler/chat-config.yaml, /etc/tyler/chat-config.yaml) agent = Agent.from_config() # Load from specific path agent = Agent.from_config("agent-config.yaml") # Override specific settings agent = Agent.from_config( "agent-config.yaml", temperature=0.9, # Override config value model_name="gpt-4.1" ) ``` Keep secrets safe using environment variable substitution: ```yaml theme={null} name: "secure-agent" api_key: "${OPENAI_API_KEY}" # Reads from environment base_url: "${API_BASE_URL}" model_name: "gpt-4o" extra_headers: Authorization: "Bearer ${AUTH_TOKEN}" ``` The `${VAR_NAME}` syntax automatically substitutes values from your environment variables. Load custom tools from Python files: ```yaml theme={null} tools: - "web" # Built-in module - "./my_tools.py" # Relative to config file - "~/shared/tools.py" # Home directory - "/opt/tools/company.py" # Absolute path ``` Tool files should export a `TOOLS` list: ```python theme={null} # my_tools.py TOOLS = [ { "definition": { "type": "function", "function": { "name": "my_tool", "description": "Custom tool", "parameters": {...} } }, "implementation": my_tool_function } ] ``` ### Advanced: Programmatic config manipulation For maximum flexibility, load and modify configs before creating the agent: ```python theme={null} from tyler import load_config, Agent # Load config config = load_config("base-config.yaml") # Inspect and modify print(f"Base model: {config['model_name']}") config["temperature"] = 0.9 config["notes"] += "\nCustomized for production" # Add tools programmatically if "tools" not in config: config["tools"] = [] config["tools"].append("slack") # Create agent from modified config agent = Agent(**config) ``` ### Multi-environment pattern Use different configs for different environments: ```python theme={null} import os from tyler import Agent # Select config based on environment env = os.getenv("ENVIRONMENT", "dev") config_map = { "dev": "configs/dev-agent.yaml", "staging": "configs/staging-agent.yaml", "prod": "configs/prod-agent.yaml" } agent = Agent.from_config(config_map[env]) ``` Or use environment variables in a single config: ```yaml theme={null} # Single config for all environments name: "agent-${ENVIRONMENT}" model_name: "${MODEL_NAME}" # Different per environment temperature: 0.7 api_base: "${API_BASE_URL}" # dev vs prod endpoints ``` See `examples/003_agent_from_config.py` for complete runnable examples and the [Agent API reference](/api-reference/tyler-agent#creating-from-config-files) for full documentation. ## Agent Patterns ### Tool selection pattern Dynamically select tools based on the task: ```python theme={null} from typing import List, Dict from lye import WEB_TOOLS, FILES_TOOLS, IMAGE_TOOLS, AUDIO_TOOLS class AdaptiveAgent: def __init__(self): self.tool_sets = { "research": WEB_TOOLS, "files": FILES_TOOLS, "visual": IMAGE_TOOLS, "audio": AUDIO_TOOLS } async def create_agent_for_task(self, task: str) -> Agent: """Create an agent with appropriate tools for the task""" # Analyze task to determine needed tools tools_needed = [] task_lower = task.lower() if any(word in task_lower for word in ["search", "research", "find"]): tools_needed.extend(self.tool_sets["research"]) if any(word in task_lower for word in ["save", "write", "read", "file"]): tools_needed.extend(self.tool_sets["files"]) if any(word in task_lower for word in ["image", "picture", "visual", "analyze"]): tools_needed.extend(self.tool_sets["visual"]) if any(word in task_lower for word in ["audio", "speech", "transcribe"]): tools_needed.extend(self.tool_sets["audio"]) # Create agent with selected tools return Agent( name="adaptive-agent", model_name="gpt-4", purpose=f"To help with: {task}", tools=tools_needed ) # Usage adapter = AdaptiveAgent() agent = await adapter.create_agent_for_task("Research AI and create a visual report") ``` ### Validation pattern Ensure agent outputs meet requirements: ```python theme={null} from typing import Callable, Any import json class ValidatedAgent: def __init__(self, agent: Agent, validators: Dict[str, Callable]): self.agent = agent self.validators = validators async def go_with_validation(self, thread: Thread) -> tuple[Thread, List[Message]]: """Process thread with output validation""" # Process normally result = await self.agent.run(thread) result_thread = result.thread messages = result.new_messages # Validate outputs for message in messages: if message.role == "assistant": validation_errors = self.validate_content(message.content) if validation_errors: # Add correction request thread.add_message(Message( role="system", content=f"Please correct these issues: {', '.join(validation_errors)}" )) # Retry return await self.agent.run(thread) return result_thread, messages def validate_content(self, content: str) -> List[str]: """Run validators on content""" errors = [] for name, validator in self.validators.items(): try: if not validator(content): errors.append(f"Failed {name} validation") except Exception as e: errors.append(f"Validation error in {name}: {str(e)}") return errors # Example validators def has_json_output(content: str) -> bool: """Check if response contains valid JSON""" try: # Find JSON in content start = content.find('{') end = content.rfind('}') + 1 if start != -1 and end > start: json.loads(content[start:end]) return True except: pass return False def meets_length_requirement(content: str, min_length: int = 100) -> bool: """Check if response meets minimum length""" return len(content) >= min_length # Usage agent = Agent(name="reporter", model_name="gpt-4", purpose="To create reports") validated_agent = ValidatedAgent( agent, validators={ "json_output": has_json_output, "length": lambda c: meets_length_requirement(c, 200) } ) ``` ## Persistence Patterns ### Context window management Manage long conversations efficiently: ```python theme={null} from narrator import Thread, Message, ThreadStore class ContextManager: def __init__(self, max_messages: int = 50, summary_threshold: int = 100): self.max_messages = max_messages self.summary_threshold = summary_threshold async def manage_context(self, thread: Thread, agent: Agent) -> Thread: """Manage context window for long conversations""" if len(thread.messages) > self.summary_threshold: # Create summary of older messages summary = await self.summarize_messages( thread.messages[:-self.max_messages], agent ) # Create new thread with summary new_thread = Thread(id=thread.id, metadata=thread.metadata) # Add summary as system message new_thread.add_message(Message( role="system", content=f"Previous conversation summary: {summary}" )) # Add recent messages for msg in thread.messages[-self.max_messages:]: new_thread.add_message(msg) return new_thread return thread async def summarize_messages(self, messages: List[Message], agent: Agent) -> str: """Create summary of messages""" # Format messages for summarization conversation = "\n".join([ f"{msg.role}: {msg.content[:200]}..." for msg in messages ]) # Create summary request summary_thread = Thread() summary_thread.add_message(Message( role="user", content=f"Summarize this conversation concisely:\n\n{conversation}" )) # Get summary result = await agent.run(summary_thread) return result.new_messages[-1].content if result.new_messages else "No summary available" ``` ### Branching conversations Support multiple conversation branches: ```python theme={null} class ConversationTree: def __init__(self, thread_store: ThreadStore): self.thread_store = thread_store self.branches = {} # branch_id -> parent_thread_id async def create_branch(self, parent_thread_id: str, branch_point: int) -> Thread: """Create a new branch from a conversation""" # Load parent thread parent_thread = await self.thread_store.get_thread(parent_thread_id) # Create new thread with messages up to branch point branch_id = f"{parent_thread_id}-branch-{len(self.branches)}" branch_thread = Thread(id=branch_id) # Copy messages up to branch point for i, msg in enumerate(parent_thread.messages): if i <= branch_point: branch_thread.add_message(msg) # Save branch await self.thread_store.save_thread(branch_thread) self.branches[branch_id] = parent_thread_id return branch_thread async def merge_branch(self, branch_id: str, target_thread_id: str): """Merge a branch back into target thread""" branch_thread = await self.thread_store.get_thread(branch_id) target_thread = await self.thread_store.get_thread(target_thread_id) # Find divergence point divergence_point = self.find_divergence_point(branch_thread, target_thread) # Add new messages from branch for msg in branch_thread.messages[divergence_point + 1:]: target_thread.add_message(msg) # Save merged thread await self.thread_store.save_thread(target_thread) ``` ## Tool patterns ### Retry with Fallback Implement robust tool execution: ```python theme={null} from typing import List, Callable, Any import asyncio class RobustToolExecutor: def __init__(self, max_retries: int = 3, fallback_tools: Dict[str, List[Callable]] = None): self.max_retries = max_retries self.fallback_tools = fallback_tools or {} async def execute_with_fallback( self, primary_tool: Callable, tool_name: str, *args, **kwargs ) -> Any: """Execute tool with retry and fallback logic""" # Try primary tool for attempt in range(self.max_retries): try: result = await primary_tool(*args, **kwargs) return result except Exception as e: if attempt == self.max_retries - 1: # Try fallback tools if tool_name in self.fallback_tools: for fallback in self.fallback_tools[tool_name]: try: return await fallback(*args, **kwargs) except: continue raise e # Exponential backoff await asyncio.sleep(2 ** attempt) # Example usage from lye.web import search, fetch async def search_duckduckgo(query: str) -> str: """Fallback search using DuckDuckGo""" # Implementation pass executor = RobustToolExecutor( fallback_tools={ "web-search": [search_duckduckgo], "web-fetch": [lambda url: fetch(f"https://archive.org/wayback/{url}")] } ) # Execute with automatic fallback result = await executor.execute_with_fallback(search, "web-search", "AI news") ``` ### Tool composition Create complex tools from simple ones: ```python theme={null} class CompositeTools: @staticmethod def create_research_tool(search_fn, fetch_fn, write_fn): """Create a composite research tool""" async def research_and_save(topic: str, output_file: str) -> str: # Search for information search_results = await search_fn(topic) # Extract URLs (simplified) urls = extract_urls(search_results)[:5] # Fetch content contents = [] for url in urls: try: content = await fetch_fn(url) contents.append({"url": url, "content": content}) except: continue # Create report report = f"# Research Report: {topic}\n\n" for item in contents: report += f"## Source: {item['url']}\n{item['content'][:500]}...\n\n" # Save report await write_fn(output_file, report) return f"Research report saved to {output_file}" return { "definition": { "name": "research_and_save", "description": "Research a topic and save findings to a file", "parameters": { "type": "object", "properties": { "topic": {"type": "string"}, "output_file": {"type": "string"} }, "required": ["topic", "output_file"] } }, "implementation": research_and_save } ``` ## Streaming Patterns ### Buffered Streaming Optimize streaming for better UX: ```python theme={null} from tyler.models.execution import EventType class StreamBuffer: def __init__(self, buffer_size: int = 10, flush_interval: float = 0.5): self.buffer = [] self.buffer_size = buffer_size self.flush_interval = flush_interval self.last_flush = asyncio.get_event_loop().time() async def process_stream(self, agent: Agent, thread: Thread): """Process stream with intelligent buffering""" async for update in agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: self.buffer.append(update.data.get("content_chunk", "")) # Flush if buffer full or timeout current_time = asyncio.get_event_loop().time() if (len(self.buffer) >= self.buffer_size or current_time - self.last_flush > self.flush_interval): yield "".join(self.buffer) self.buffer = [] self.last_flush = current_time elif update.type == EventType.EXECUTION_COMPLETE: # Flush remaining buffer if self.buffer: yield "".join(self.buffer) # Yield completion yield {"type": "complete", "thread": update.data} ``` ### Progress Tracking Track progress for long-running operations: ```python theme={null} class ProgressTracker: def __init__(self): self.stages = [] self.current_stage = 0 self.listeners = [] def add_stage(self, name: str, weight: float = 1.0): """Add a progress stage""" self.stages.append({"name": name, "weight": weight, "progress": 0}) def add_listener(self, callback: Callable): """Add progress listener""" self.listeners.append(callback) async def update_progress(self, stage_progress: float): """Update current stage progress""" if self.current_stage < len(self.stages): self.stages[self.current_stage]["progress"] = stage_progress # Calculate total progress total_weight = sum(s["weight"] for s in self.stages) total_progress = sum( s["weight"] * s["progress"] for s in self.stages ) / total_weight # Notify listeners for listener in self.listeners: await listener({ "stage": self.stages[self.current_stage]["name"], "stage_progress": stage_progress, "total_progress": total_progress }) async def next_stage(self): """Move to next stage""" if self.current_stage < len(self.stages): self.stages[self.current_stage]["progress"] = 1.0 self.current_stage += 1 await self.update_progress(0) # Usage with agent tracker = ProgressTracker() tracker.add_stage("Research", weight=2) tracker.add_stage("Analysis", weight=3) tracker.add_stage("Report Generation", weight=1) async def progress_callback(update): print(f"{update['stage']}: {update['total_progress']:.0%}") tracker.add_listener(progress_callback) ``` ## Error handling patterns ### Graceful Degradation Handle errors without failing completely: ```python theme={null} class GracefulAgent: def __init__(self, agent: Agent): self.agent = agent self.error_handlers = {} def register_error_handler(self, error_type: type, handler: Callable): """Register handler for specific error type""" self.error_handlers[error_type] = handler async def go_with_graceful_degradation(self, thread: Thread): """Process with graceful error handling""" try: return await self.agent.run(thread) except Exception as e: # Find appropriate handler for error_type, handler in self.error_handlers.items(): if isinstance(e, error_type): return await handler(thread, e) # Default handler return await self.default_error_handler(thread, e) async def default_error_handler(self, thread: Thread, error: Exception): """Default error handling""" error_thread = Thread(id=thread.id) # Copy messages for msg in thread.messages: error_thread.add_message(msg) # Add error message error_thread.add_message(Message( role="assistant", content=f"I encountered an error: {str(error)}. Let me try a different approach." )) return error_thread, [] # Usage agent = Agent(name="worker", model_name="gpt-4", purpose="To complete tasks") graceful = GracefulAgent(agent) # Register specific handlers async def handle_tool_error(thread, error): # Retry without tools no_tool_agent = Agent( name="worker", model_name="gpt-4", purpose="To complete tasks without tools" ) return await no_tool_agent.run(thread) graceful.register_error_handler(ToolExecutionError, handle_tool_error) ``` ## Production Patterns ### Health Monitoring Monitor agent health in production: ```python theme={null} from datetime import datetime, timedelta import statistics class AgentHealthMonitor: def __init__(self, window_size: int = 100): self.window_size = window_size self.response_times = [] self.error_count = 0 self.success_count = 0 self.last_health_check = datetime.now() async def monitor_execution(self, agent: Agent, thread: Thread): """Execute with monitoring""" start_time = datetime.now() try: result = await agent.run(thread) self.success_count += 1 # Record response time response_time = (datetime.now() - start_time).total_seconds() self.response_times.append(response_time) # Keep window size if len(self.response_times) > self.window_size: self.response_times.pop(0) return result except Exception as e: self.error_count += 1 raise e def get_health_metrics(self) -> Dict[str, Any]: """Get current health metrics""" metrics = { "status": "healthy", "success_rate": self.success_count / (self.success_count + self.error_count) if (self.success_count + self.error_count) > 0 else 0, "error_count": self.error_count, "success_count": self.success_count } if self.response_times: metrics.update({ "avg_response_time": statistics.mean(self.response_times), "p95_response_time": statistics.quantiles(self.response_times, n=20)[18], "p99_response_time": statistics.quantiles(self.response_times, n=100)[98] }) # Determine health status if metrics["success_rate"] < 0.95: metrics["status"] = "degraded" if metrics["success_rate"] < 0.8: metrics["status"] = "unhealthy" return metrics ``` ## Next steps Test these patterns Optimize pattern performance See patterns in action Complete API docs # Skills & AGENTS.md Source: https://slide.mintlify.app/guides/skills Give your agents reusable instructions — on-demand or always-on Slide supports two complementary ways to inject instructions into your agent's system prompt: * **Skills** — progressively disclosed on-demand via the `activate_skill` tool * **AGENTS.md** — eagerly loaded into the system prompt at init time Both follow open standards: [Open Agent Skills](https://openagentskills.dev/docs/specification) and [AGENTS.md](https://agents.md). **Code Examples** Progressive skill disclosure in action Project instructions in action ## Skills Skills let you package reusable instructions that agents load only when they need them. Instead of stuffing everything into the system prompt, skills keep the prompt small and focused — the agent sees a short menu of available skills and activates the ones relevant to the current task. ### How skills work 1. You point the agent at one or more skill directories 2. At init time, only each skill's **name** and **description** appear in the system prompt 3. When the agent decides it needs a skill, it calls the `activate_skill` tool 4. The skill root path and full instructions from `SKILL.md` are returned to the agent as a tool result ### Creating a skill A skill is a directory containing a `SKILL.md` file. The file has YAML frontmatter (name + description) followed by markdown instructions: ``` my-project/ └── skills/ ├── code-review/ │ └── SKILL.md └── testing/ └── SKILL.md ``` Example `SKILL.md`: ```markdown theme={null} --- name: code-review description: Guidelines for performing thorough code reviews --- # Code Review Skill ## What to look for - Correctness: Does the code do what it's supposed to? - Readability: Is the code easy to understand? - Performance: Are there unnecessary allocations or O(n²) loops? - Security: Are inputs validated? Are there injection risks? ## How to format feedback - Use inline comments for specific issues - Summarize overall impressions at the top - Always mention what was done well ``` #### Frontmatter requirements | Field | Rules | | ------------- | ------------------------------------------------------------------- | | `name` | Lowercase alphanumeric + hyphens, max 64 chars (e.g. `code-review`) | | `description` | Plain text, max 1024 chars | ### Using skills with an agent ```python theme={null} from tyler import Agent agent = Agent( model_name="gpt-4.1", purpose="A helpful coding assistant", skills=[ "./skills/code-review", "./skills/testing", ], ) ``` The agent's system prompt will include something like: ``` # Available Skills Use the `activate_skill` tool to load full instructions for any skill. - **code-review**: Guidelines for performing thorough code reviews - **testing**: Guidelines for writing comprehensive tests ``` When the agent encounters a task that matches a skill, it will call `activate_skill` with the skill name and receive the full instructions. ### Using skills with config files ```yaml theme={null} name: "MyAgent" model_name: "gpt-4.1" purpose: "A helpful assistant" skills: - "./skills/code-review" - "./skills/testing" - "~/shared-skills/documentation" ``` Relative paths are resolved relative to the config file's directory. *** ## AGENTS.md AGENTS.md files provide project-level instructions that are eagerly loaded into the agent's system prompt at init time. Unlike skills (which are progressively disclosed), AGENTS.md content is always present — making it ideal for coding standards, API conventions, and other rules that should always apply. ### How it works 1. Auto-discovery is enabled by default, or you point the agent at one or more `AGENTS.md` files 2. At init time, the file contents are loaded and placed in a `` block in the system prompt 3. The agent sees these instructions on every interaction ### Creating an AGENTS.md file Create an `AGENTS.md` file in your project root (or any directory). No special frontmatter or formatting is required — it's just markdown: ```markdown theme={null} # Project Guidelines ## Code Style - Use type hints for all function signatures - Follow PEP 8 naming conventions - Prefer `async`/`await` over threads for I/O-bound operations ## Error Handling - Always use specific exception types (never bare `except:`) - Include meaningful error messages with context - Use `logging` instead of `print` for diagnostics ## API Conventions - Use `httpx` for HTTP requests (async-native) - Always set timeouts on external calls - Return typed dataclasses or Pydantic models, not raw dicts ``` ### Using AGENTS.md with an agent #### Default auto-discovery By default, Tyler discovers `AGENTS.md` files by walking upward from the current working directory: ```python theme={null} from tyler import Agent agent = Agent( model_name="gpt-4.1", purpose="A helpful coding assistant", ) ``` #### Explicit path ```python theme={null} from tyler import Agent agent = Agent( model_name="gpt-4.1", purpose="A helpful coding assistant", agents_md="./AGENTS.md", ) ``` #### Auto-discovery You can also set `agents_md=True` explicitly. `Agent.from_config()` starts discovery from the config file directory when `agents_md` is omitted or set to `true`: ```python theme={null} agent = Agent( model_name="gpt-4.1", purpose="A helpful coding assistant", agents_md=True, ) ``` This is useful in monorepos where you might have: ``` project-root/AGENTS.md # Company-wide rules project-root/backend/AGENTS.md # Backend-specific rules ``` Files are loaded root-first, so the closest file's instructions appear last (taking natural precedence). #### Disable loading ```python theme={null} agent = Agent( model_name="gpt-4.1", purpose="A helpful coding assistant", agents_md=False, ) ``` #### Multiple files ```python theme={null} agent = Agent( model_name="gpt-4.1", agents_md=["./AGENTS.md", "./docs/coding-standards.md"], ) ``` Multiple files are joined with `---` separators. ### Using AGENTS.md with config files ```yaml theme={null} name: "MyAgent" model_name: "gpt-4.1" purpose: "A helpful assistant" # Default: auto-discover from this config directory upward # agents_md: true # Disable loading # agents_md: false # Explicit path # agents_md: "./AGENTS.md" # Multiple files # agents_md: # - "./AGENTS.md" # - "./docs/coding-standards.md" # Keep "system" unless your provider supports developer messages instruction_role: "system" ``` Relative paths are resolved relative to the config file's directory. ### Size limits AGENTS.md content is guarded against oversized files: * Individual files larger than 100,000 characters are skipped with a warning * Combined content from multiple files is truncated at 100,000 characters If your instructions exceed this limit, consider moving task-specific content into skills instead. *** ## When to use which | | Skills | AGENTS.md | | ----------------- | -------------------------------------------------------- | ------------------------------------ | | **Loading** | On-demand (progressive disclosure) | Eager (always in prompt) | | **Best for** | Task-specific instructions the agent may or may not need | Project-wide rules that always apply | | **Prompt impact** | Minimal — only name + description until activated | Full content always present | | **Format** | `SKILL.md` with YAML frontmatter | Plain markdown | Use **AGENTS.md** for short, universal project rules that should always be in context. Use **skills** for detailed, task-specific instructions that only matter sometimes. You can use both together. ## Next steps Give your agents more capabilities Connect to external tool servers # Streaming responses Source: https://slide.mintlify.app/guides/streaming-responses Build real-time interactive agents with streaming Streaming responses enable your agents to provide real-time feedback, making interactions feel more natural and responsive. Instead of waiting for the entire response, users see content as it's generated. **💻 Code Examples** Stream events with tool tracking OpenAI-compatible chunks Stream reasoning content ## Why Use Streaming? Traditional (non-streaming) approach: * User waits for entire response * No feedback during processing * Can feel slow for long responses Streaming approach: * Immediate visual feedback * See responses as they're generated * Better user experience * Can see tool usage in real-time ## Streaming Modes Tyler supports multiple streaming modes through `agent.stream(thread, mode=...)`: | Mode | Value | Output Type | Use Case | | -------------------- | -------------------- | ----------------------- | ---------------------------------------- | | **Event Streaming** | `"events"` (default) | `ExecutionEvent` | Rich observability, tool tracking | | **OpenAI Streaming** | `"openai"` | Raw LiteLLM chunks | OpenAI compatibility, proxying | | **Vercel Streaming** | `"vercel"` | SSE strings | React/Next.js with AI SDK | | **Vercel Objects** | `"vercel_objects"` | Vercel protocol objects | Python-native Vercel AI SDK integrations | Use `await agent.run(thread)` for non-streaming execution when you want a completed `AgentResult`. ### Event Streaming (Recommended) The default streaming mode with full observability: ```python theme={null} import asyncio from tyler import Agent, Thread, Message, EventType agent = Agent( name="streaming-assistant", model_name="gpt-4", purpose="To provide real-time responses" ) async def stream_response(): thread = Thread() message = Message(role="user", content="Tell me a story about space exploration") thread.add_message(message) print("🤖 Assistant: ", end="", flush=True) async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: print(event.data.get("content_chunk", ""), end="", flush=True) print() # New line at the end asyncio.run(stream_response()) ``` ### OpenAI Streaming (Advanced) OpenAI mode is for advanced use cases. Tools ARE executed for full agentic behavior, but you only receive raw chunks (no ExecutionEvents). Stream raw LiteLLM chunks for OpenAI compatibility: ```python theme={null} import asyncio from tyler import Agent, Thread, Message agent = Agent( name="proxy-assistant", model_name="gpt-4o", purpose="OpenAI-compatible streaming" ) async def openai_stream_response(): thread = Thread() message = Message(role="user", content="Hello!") thread.add_message(message) # Get raw OpenAI-compatible chunks async for chunk in agent.stream(thread, mode="openai"): # chunk is a raw LiteLLM object if hasattr(chunk, 'choices') and chunk.choices: delta = chunk.choices[0].delta # Delta can be dict or object depending on LiteLLM version if isinstance(delta, dict): content = delta.get('content') else: content = getattr(delta, 'content', None) if content: print(content, end="", flush=True) # Usage info in final chunk if hasattr(chunk, 'usage') and chunk.usage: print(f"\n\nTokens: {chunk.usage.total_tokens}") asyncio.run(openai_stream_response()) ``` **When to use openai mode:** * Building OpenAI API proxies or gateways * Direct integration with OpenAI-compatible clients * Minimal latency requirements (no transformation overhead) **How it works:** * ✅ Tools ARE executed (fully agentic behavior) * ✅ Multi-turn iteration supported * ✅ Frontend sees `finish_reason: "tool_calls"` in chunks * ⚠️ No ExecutionEvent telemetry (only raw chunks) * ⚠️ Silent during tool execution (brief pauses expected) * ⚠️ Consumer must handle chunk formatting (SSE serialization) Matches the pattern from [OpenAI's Agents SDK](https://openai.github.io/openai-agents-python/streaming/): Raw chunks → finish\_reason="tool\_calls" → \[agent executes tools] → more raw chunks → repeat **SSE Serialization Example:** ```python theme={null} import json def serialize_chunk_to_sse(chunk) -> str: """Convert raw chunk to Server-Sent Events format""" chunk_dict = { "id": getattr(chunk, 'id', 'unknown'), "object": getattr(chunk, 'object', 'chat.completion.chunk'), "created": getattr(chunk, 'created', 0), "model": getattr(chunk, 'model', 'unknown'), "choices": [] } if hasattr(chunk, 'choices') and chunk.choices: for choice in chunk.choices: choice_dict = { "index": getattr(choice, 'index', 0), "delta": {}, "finish_reason": getattr(choice, 'finish_reason', None) } if hasattr(choice, 'delta'): delta = choice.delta if isinstance(delta, dict): choice_dict["delta"] = delta else: if hasattr(delta, 'content') and delta.content: choice_dict["delta"]["content"] = delta.content if hasattr(delta, 'role') and delta.role: choice_dict["delta"]["role"] = delta.role chunk_dict["choices"].append(choice_dict) if hasattr(chunk, 'usage') and chunk.usage: chunk_dict["usage"] = { "prompt_tokens": chunk.usage.prompt_tokens, "completion_tokens": chunk.usage.completion_tokens, "total_tokens": chunk.usage.total_tokens } return f"data: {json.dumps(chunk_dict)}\n\n" # Use in a FastAPI endpoint from fastapi import FastAPI from fastapi.responses import StreamingResponse app = FastAPI() @app.get("/v1/chat/completions") async def openai_compatible_endpoint(messages: list): thread = Thread() for msg in messages: thread.add_message(Message(role=msg["role"], content=msg["content"])) async def generate(): async for chunk in agent.stream(thread, mode="openai"): yield serialize_chunk_to_sse(chunk) return StreamingResponse(generate(), media_type="text/event-stream") ``` See `examples/005_openai_streaming.py` for a complete working example. ### Vercel AI SDK Streaming Perfect for React/Next.js frontends using `@ai-sdk/react`'s `useChat` hook. Tyler supports the [Vercel AI SDK Data Stream Protocol](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol), enabling seamless integration with modern React frontends. ```python theme={null} import asyncio from tyler import Agent, Thread, Message, VERCEL_STREAM_HEADERS agent = Agent( name="vercel-assistant", model_name="gpt-4.1", purpose="To provide AI-powered responses" ) async def vercel_stream_response(): thread = Thread() message = Message(role="user", content="Hello!") thread.add_message(message) # Stream in Vercel AI SDK format async for sse_chunk in agent.stream(thread, mode="vercel"): print(sse_chunk, end="") # SSE-formatted strings asyncio.run(vercel_stream_response()) ``` **When to use Vercel mode:** * Building React/Next.js chat interfaces with `@ai-sdk/react` * Direct integration with Vercel's AI SDK ecosystem * Need SSE streams compatible with `useChat` hook **How it works:** * ✅ Tools ARE executed (fully agentic behavior) * ✅ Thinking/reasoning tokens supported * ✅ Pre-formatted SSE strings ready for HTTP response * ✅ Compatible with `x-vercel-ai-ui-message-stream: v1` protocol **FastAPI Integration Example:** ```python theme={null} from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse from tyler import Agent, Thread, Message, VERCEL_STREAM_HEADERS app = FastAPI() agent = Agent(name="chat", model_name="gpt-4.1", purpose="Chat assistant") @app.post("/api/chat") async def chat(request: Request): body = await request.json() messages = body.get("messages", []) thread = Thread() for msg in messages: thread.add_message(Message(role=msg["role"], content=msg["content"])) async def generate(): async for sse_chunk in agent.stream(thread, mode="vercel"): yield sse_chunk return StreamingResponse( generate(), media_type="text/event-stream", headers=VERCEL_STREAM_HEADERS # Includes x-vercel-ai-ui-message-stream: v1 ) ``` **React Frontend with @ai-sdk/react:** ```tsx theme={null} import { useChat } from '@ai-sdk/react'; export default function Chat() { const { messages, sendMessage, input, setInput } = useChat({ api: '/api/chat', // Points to your Tyler backend }); return (
{messages.map(message => (
{message.role}: {message.parts.map((part, i) => part.type === 'text' ? {part.text} : null )}
))}
{ e.preventDefault(); sendMessage({ text: input }); setInput(''); }}> setInput(e.target.value)} />
); } ``` See `examples/007_vercel_streaming.py` for a complete working example. ## Understanding Execution Events ExecutionEvent objects provide detailed information about the agent's execution: ```python theme={null} from tyler import EventType async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: # Text being generated print(event.data.get("content_chunk", ""), end="", flush=True) elif event.type == EventType.TOOL_SELECTED: # Tool is about to be called print(f"\n🔧 Calling tool: {event.data['tool_name']}") elif event.type == EventType.MESSAGE_CREATED: # New message added to thread msg = event.data["message"] if msg.role == "tool": print(f"\n✅ Tool {msg.name} completed") elif event.type == EventType.EXECUTION_COMPLETE: # All processing complete print(f"\n✅ Complete in {event.data['duration_ms']:.0f}ms!") ``` ## Thinking Tokens (Reasoning Content) Requires LiteLLM >= 1.63.0 and a reasoning-capable model like OpenAI o1 or Anthropic Claude with extended thinking. Models like OpenAI o1 and Anthropic Claude can emit their reasoning process as separate "thinking tokens" alongside the response content. Tyler's streaming API exposes these as dedicated `LLM_THINKING_CHUNK` events, allowing you to display reasoning separately from the final answer. ### Why Use Thinking Tokens? * **Transparency**: Show users how the AI arrived at its answer * **Debugging**: Trace model reasoning for better agent development * **UX**: Display thinking in a collapsible section or different style * **Trust**: Users can verify the model's reasoning process ### Event Streaming with Thinking ```python theme={null} from tyler import Agent, Thread, Message, EventType agent = Agent( name="thinking-agent", model_name="anthropic/claude-3-7-sonnet-20250219", # or "o1-preview" purpose="To demonstrate thinking tokens" ) thread = Thread() thread.add_message(Message( role="user", content="What's 137 * 284? Show your thinking." )) print("💭 Thinking: ", end="", flush=True) print("\n💬 Response: ", end="", flush=True) async for event in agent.stream(thread): if event.type == EventType.LLM_THINKING_CHUNK: # Thinking/reasoning tokens (separate from content) thinking = event.data['thinking_chunk'] thinking_type = event.data['thinking_type'] # "reasoning", "thinking", etc. print(f"\n[{thinking_type}] {thinking}", flush=True) elif event.type == EventType.LLM_STREAM_CHUNK: # Regular response content print(event.data['content_chunk'], end="", flush=True) ``` **Output:** ``` 💭 Thinking: [reasoning] Let me calculate 137 * 284 step by step... [reasoning] 137 * 284 = 137 * (280 + 4) = 137 * 280 + 137 * 4... 💬 Response: The answer is 38,908. ``` ### Thinking in Message Object After streaming completes, thinking content is stored as a top-level field on the message: ```python theme={null} async for event in agent.stream(thread): if event.type == EventType.MESSAGE_CREATED: msg = event.data['message'] if msg.role == "assistant": # Access complete reasoning (top-level field) if msg.reasoning_content: print(f"Full reasoning: {msg.reasoning_content}") ``` ### OpenAI Streaming with Thinking OpenAI mode preserves all thinking fields from LiteLLM: ```python theme={null} async for chunk in agent.stream(thread, mode="openai"): if hasattr(chunk, 'choices') and chunk.choices: delta = chunk.choices[0].delta # LiteLLM standardized field (v1.63.0+) if hasattr(delta, 'reasoning_content') and delta.reasoning_content: print(f"[Reasoning] {delta.reasoning_content}") # Anthropic-specific field if hasattr(delta, 'thinking') and delta.thinking: print(f"[Thinking] {delta.thinking}") # Regular content if hasattr(delta, 'content') and delta.content: print(delta.content, end="") ``` ### UI Pattern: Separated Display A common pattern is showing thinking in a collapsible section: ```python theme={null} thinking_section = [] response_section = [] async for event in agent.stream(thread): if event.type == EventType.LLM_THINKING_CHUNK: thinking_section.append(event.data['thinking_chunk']) elif event.type == EventType.LLM_STREAM_CHUNK: response_section.append(event.data['content_chunk']) # Display in UI print("─── Thinking Process (click to expand) ───") print(''.join(thinking_section)) print("─── Response ───") print(''.join(response_section)) ``` ### Supported Models **OpenAI:** * `o1-preview` (reasoning\_content) * `o1-mini` (reasoning\_content) **Anthropic:** * `claude-3-7-sonnet-20250219` with extended thinking * Future Claude models with thinking capability **Other Providers via LiteLLM:** * Deepseek * XAI * Google AI Studio * Perplexity (Magistral models) * Groq See [LiteLLM docs](https://docs.litellm.ai/docs/reasoning_content) for full list. ### Backward Compatibility Models without thinking support work unchanged - no `LLM_THINKING_CHUNK` events are emitted: ```python theme={null} agent = Agent(name="regular", model_name="gpt-4o") # No thinking async for event in agent.stream(thread): if event.type == EventType.LLM_THINKING_CHUNK: # This won't execute for non-reasoning models pass elif event.type == EventType.LLM_STREAM_CHUNK: # Regular content streaming works as before print(event.data['content_chunk'], end="") ``` See `packages/tyler/examples/006_thinking_tokens.py` for complete working examples. ## Streaming with Tools See tool usage in real-time: ```python theme={null} from lye import WEB_TOOLS, FILES_TOOLS agent = Agent( name="research-assistant", model_name="gpt-4", purpose="To research and create reports", tools=[*WEB_TOOLS, *FILES_TOOLS] ) async def research_with_streaming(topic: str): thread = Thread() message = Message( role="user", content=f"Research {topic} and create a summary" ) thread.add_message(message) async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: print(event.data.get("content_chunk", ""), end="", flush=True) elif event.type == EventType.TOOL_SELECTED: tool_name = event.data["tool_name"] print(f"\n\n🔧 Using {tool_name}...", flush=True) elif event.type == EventType.TOOL_RESULT: result = event.data["result"] # Show abbreviated tool results if len(result) > 100: print(f" Result: {result[:100]}...") else: print(f" Result: {result}") print("\n🤖 ", end="", flush=True) ``` ## Building interactive applications ### Terminal chat interface ```python theme={null} import asyncio from tyler import Agent, Thread, Message, ThreadStore from tyler.models.execution import ExecutionEvent, EventType class StreamingChat: def __init__(self): self.agent = None self.thread_store = None self.thread = None async def initialize(self): self.thread_store = await ThreadStore.create("sqlite+aiosqlite:///chat.db") self.agent = Agent( name="chat-assistant", model_name="gpt-4", purpose="To have helpful conversations", thread_store=self.thread_store ) async def start_session(self, session_id: str): try: self.thread = await self.thread_store.get_thread(session_id) print("📚 Resuming conversation...") except: self.thread = Thread(id=session_id) print("🆕 Starting new conversation...") async def send_message(self, content: str): message = Message(role="user", content=content) self.thread.add_message(message) print("\n🤖 ", end="", flush=True) async for update in self.agent.stream(self.thread): if update.type == EventType.LLM_STREAM_CHUNK: print(update.data.get("content_chunk", ""), end="", flush=True) elif update.type == EventType.EXECUTION_COMPLETE: # Thread is already updated in place await self.thread_store.save_thread(self.thread) print("\n") # Usage async def main(): chat = StreamingChat() await chat.initialize() await chat.start_session("main-chat") while True: user_input = input("\nYou: ") if user_input.lower() in ['exit', 'quit']: break await chat.send_message(user_input) asyncio.run(main()) ``` ### Web application streaming For web applications, you can stream to a WebSocket or Server-Sent Events: ```python theme={null} # FastAPI example with WebSocket from fastapi import FastAPI, WebSocket import json app = FastAPI() @app.websocket("/ws/chat") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() agent = Agent( name="web-assistant", model_name="gpt-4", purpose="To assist web users" ) thread = Thread() while True: # Receive message from client data = await websocket.receive_text() message_data = json.loads(data) # Add to thread message = Message(role="user", content=message_data["content"]) thread.add_message(message) # Stream response async for update in agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: await websocket.send_json({ "type": "content", "data": update.data }) elif update.type == EventType.TOOL_SELECTED: await websocket.send_json({ "type": "tool", "name": update.data.get("tool_name", ""), "content": str(update.data.get("arguments", ""))[:100] + "..." }) elif update.type == EventType.EXECUTION_COMPLETE: # Thread is already updated in place await websocket.send_json({"type": "complete"}) ``` ## Advanced Streaming Patterns ### Progress indicators Show progress for long-running tasks: ```python theme={null} async def stream_with_progress(): thread = Thread() message = Message( role="user", content="Analyze these 10 websites and create a report" ) thread.add_message(message) tool_count = 0 content_buffer = [] async for update in agent.stream(thread): if update.type == EventType.TOOL_SELECTED: tool_count += 1 print(f"\r⏳ Processing... ({tool_count} tools used)", end="", flush=True) elif update.type == EventType.LLM_STREAM_CHUNK: content_buffer.append(update.data.get("content_chunk", "")) elif update.type == EventType.EXECUTION_COMPLETE: print("\r✅ Complete!" + " " * 30) # Clear progress print("\n🤖 " + "".join(content_buffer)) ``` ### Buffered streaming For smoother output, buffer chunks: ```python theme={null} class BufferedStreamer: def __init__(self, buffer_size: int = 5): self.buffer = [] self.buffer_size = buffer_size async def stream(self, agent, thread): async for update in agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: self.buffer.append(update.data.get("content_chunk", "")) if len(self.buffer) >= self.buffer_size: yield "".join(self.buffer) self.buffer = [] elif update.type == EventType.EXECUTION_COMPLETE: if self.buffer: yield "".join(self.buffer) yield {"type": "complete", "thread": update.data} # Usage streamer = BufferedStreamer() async for chunk in streamer.stream(agent, thread): if isinstance(chunk, str): print(chunk, end="", flush=True) else: # Handle completion pass ``` ### Cancellable streaming Allow users to stop generation: ```python theme={null} import asyncio class CancellableStream: def __init__(self): self.cancelled = False async def stream_with_cancel(self, agent, thread): try: async for update in agent.stream(thread): if self.cancelled: print("\n\n⚠️ Generation cancelled by user") break if update.type == EventType.LLM_STREAM_CHUNK: print(update.data.get("content_chunk", ""), end="", flush=True) except asyncio.CancelledError: print("\n\n⚠️ Stream interrupted") def cancel(self): self.cancelled = True # Usage with keyboard interrupt import signal stream_handler = CancellableStream() def signal_handler(sig, frame): stream_handler.cancel() signal.signal(signal.SIGINT, signal_handler) ``` ## Streaming UI Components ### Rich terminal UI Using the `rich` library for better terminal output: ```python theme={null} from rich.console import Console from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel console = Console() async def rich_streaming(): thread = Thread() message = Message(role="user", content="Explain quantum computing") thread.add_message(message) content = "" with Live(Panel("", title="🤖 Assistant"), refresh_per_second=10) as live: async for update in agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: content += update.data.get("content_chunk", "") live.update(Panel(Markdown(content), title="🤖 Assistant")) elif update.type == EventType.TOOL_SELECTED: tool_panel = Panel( f"Using: {update.data.get('tool_name', '')}", title="🔧 Tool", style="yellow" ) console.print(tool_panel) ``` ### Token counting Track tokens during streaming: ```python theme={null} class TokenCounter: def __init__(self): self.total_tokens = 0 self.chunks = [] async def count_stream(self, agent, thread): async for update in agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: self.chunks.append(update.data.get("content_chunk", "")) # Rough estimate: 1 token ≈ 4 characters self.total_tokens += len(update.data) // 4 yield update print(f"\n\n📊 Approximate tokens used: {self.total_tokens}") ``` ## Performance tips ### 1. Chunk Size Optimization Larger chunks reduce overhead but decrease responsiveness: ```python theme={null} # Configure in your agent if supported agent = Agent( name="optimized-streamer", model_name="gpt-4", purpose="To stream efficiently", # streaming_chunk_size=10 # If available ) ``` ### 2. Async Processing Process streams asynchronously for better performance: ```python theme={null} async def process_multiple_streams(): tasks = [] for query in queries: thread = Thread() thread.add_message(Message(role="user", content=query)) task = asyncio.create_task(collect_stream(agent, thread)) tasks.append(task) results = await asyncio.gather(*tasks) return results async def collect_stream(agent, thread): content = [] async for update in agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: content.append(update.data.get("content_chunk", "")) return "".join(content) ``` ### 3. Error Handling in Streams ```python theme={null} from datetime import datetime async def safe_stream(agent, thread): try: async for update in agent.stream(thread): yield update except asyncio.TimeoutError: yield ExecutionEvent( type=EventType.EXECUTION_ERROR, timestamp=datetime.now(), data={"message": "Stream timed out"} ) except Exception as e: yield ExecutionEvent( type=EventType.EXECUTION_ERROR, timestamp=datetime.now(), data={"message": f"Stream error: {str(e)}"} ) ``` ## Real-World Example: Live Research Assistant ```python theme={null} import asyncio from datetime import datetime from tyler import Agent, Thread, Message from tyler.models.execution import ExecutionEvent, EventType from lye import WEB_TOOLS, FILES_TOOLS class LiveResearchAssistant: def __init__(self): self.agent = Agent( name="live-researcher", model_name="gpt-4", purpose="To conduct research and provide real-time updates", tools=[*WEB_TOOLS, *FILES_TOOLS] ) async def research(self, topic: str, save_to_file: bool = True): thread = Thread() message = Message( role="user", content=f""" Research '{topic}' comprehensively: 1. Search for recent information 2. Analyze multiple sources 3. Create a detailed report {"4. Save the report to a file" if save_to_file else ""} """ ) thread.add_message(message) print(f"\n{'='*50}") print(f"🔍 Researching: {topic}") print(f"⏰ Started: {datetime.now().strftime('%H:%M:%S')}") print(f"{'='*50}\n") content_buffer = [] tool_uses = [] async for update in self.agent.stream(thread): if update.type == EventType.LLM_STREAM_CHUNK: chunk = update.data.get("content_chunk", "") content_buffer.append(chunk) print(chunk, end="", flush=True) elif update.type == EventType.TOOL_SELECTED: tool_name = update.data.get("tool_name", "") tool_uses.append(tool_name) # Show tool use inline print(f"\n\n[🔧 {tool_name}]", end="") if tool_name == "web-search": print(f" Searching for information...") elif tool_name == "files-write": print(f" Saving report...") print("\n", end="") elif update.type == EventType.EXECUTION_COMPLETE: print(f"\n\n{'='*50}") print(f"✅ Research Complete!") print(f"📊 Tools used: {', '.join(set(tool_uses))}") print(f"📝 Total length: {len(''.join(content_buffer))} characters") print(f"⏰ Finished: {datetime.now().strftime('%H:%M:%S')}") print(f"{'='*50}") return update.data # Usage async def main(): assistant = LiveResearchAssistant() topics = [ "Latest breakthroughs in quantum computing", "Climate change solutions for 2024", "AI safety research progress" ] for topic in topics: await assistant.research(topic, save_to_file=True) print("\n" + "="*70 + "\n") asyncio.run(main()) ``` ## Next steps Stream responses in Slack Complex streaming patterns # Structured output & dependency injection Source: https://slide.mintlify.app/guides/structured-output Get type-safe, validated data from your agents and inject runtime dependencies into tools This guide covers two powerful features that enhance agent reliability and code organization: structured output for type-safe LLM responses, and tool context for dependency injection. **💻 Code Examples** Type-safe agent responses with Pydantic Inject dependencies into tools ## Structured Output Structured output lets you define a Pydantic model as the expected response format from your agent. The LLM will return data that exactly matches your schema, giving you type-safe, validated data directly. ### Basic Usage You can set `response_type` on the agent (as a default for all runs) or pass it per-run: ```python theme={null} from pydantic import BaseModel, Field from typing import Literal from tyler import Agent, Thread, Message # 1. Define your output schema class SupportTicket(BaseModel): priority: Literal["low", "medium", "high"] category: str summary: str = Field(max_length=500) requires_escalation: bool # 2. Create agent with default response_type agent = Agent( name="ticket-classifier", model_name="gpt-4o", purpose="To classify support tickets", response_type=SupportTicket # Default for all runs ) # 3. Run - uses agent's default response_type thread = Thread() thread.add_message(Message( role="user", content="My payment failed and I'm locked out!" )) result = await agent.run(thread) # No need to pass response_type # 4. Access the validated data ticket: SupportTicket = result.structured_data print(f"Priority: {ticket.priority}") # "high" ``` ```python theme={null} from pydantic import BaseModel, Field from typing import Literal, List from tyler import Agent, Thread, Message class SupportTicket(BaseModel): priority: Literal["low", "medium", "high"] category: str summary: str = Field(max_length=500) requires_escalation: bool class Invoice(BaseModel): invoice_id: str total: float items: List[str] # Agent without default - flexible for multiple schemas agent = Agent( name="data-extractor", model_name="gpt-4o", purpose="To extract structured data" ) # Extract a support ticket result1 = await agent.run(thread1, response_type=SupportTicket) ticket = result1.structured_data # Same agent, different schema result2 = await agent.run(thread2, response_type=Invoice) invoice = result2.structured_data ``` Per-run `response_type` always overrides the agent's default. This lets you have a sensible default while retaining flexibility. ### How It Works When you provide a `response_type`, Slide uses the **output-tool pattern**: 1. **Schema as Tool**: Your Pydantic model is registered as a special "output tool" 2. **Forced Tool Call**: The LLM is called with `tool_choice="required"`, ensuring it must call a tool 3. **Validation**: When the LLM calls the output tool, its arguments are validated against your schema 4. **Retry on Failure**: If validation fails, an error message is added and the LLM retries This approach has key advantages over simple JSON mode: * **Tools + Structured Output**: Regular tools work alongside structured output in the same conversation * **Reliable Output**: `tool_choice="required"` forces the model to respond with structured data * **Cross-Provider Support**: LiteLLM translates `tool_choice` for each provider (OpenAI, Anthropic, Gemini, Bedrock, etc.) **Azure OpenAI Limitation**: Azure does not currently support `tool_choice="required"`. If using Azure, set `drop_params=True` on your agent to gracefully fall back. ### Complex Schemas Structured output supports all Pydantic features: ```python theme={null} from pydantic import BaseModel, Field from typing import List, Optional, Literal from datetime import date class LineItem(BaseModel): """A single item in an invoice""" description: str quantity: int = Field(ge=1) unit_price: float = Field(ge=0) @property def total(self) -> float: return self.quantity * self.unit_price class Invoice(BaseModel): """Extracted invoice data""" vendor_name: str invoice_number: str invoice_date: date due_date: Optional[date] = None items: List[LineItem] currency: Literal["USD", "EUR", "GBP"] = "USD" notes: Optional[str] = None @property def subtotal(self) -> float: return sum(item.total for item in self.items) # Use it result = await agent.run(thread, response_type=Invoice) invoice = result.structured_data print(f"Invoice #{invoice.invoice_number}") print(f"Subtotal: {invoice.currency} {invoice.subtotal:.2f}") for item in invoice.items: print(f" - {item.description}: {item.quantity} x {item.unit_price}") ``` ### Default Response Type You can set a default `response_type` on the agent itself: ```python theme={null} # Agent-level default agent = Agent( name="extractor", model_name="gpt-4o", purpose="To extract structured data", response_type=Invoice # Default for all runs ) # Uses agent's default response_type result = await agent.run(thread) # Override for specific run result = await agent.run(thread, response_type=SupportTicket) ``` ## Automatic Retry on Validation Failure Sometimes the LLM might return invalid JSON or data that doesn't match your schema. Use `RetryConfig` to automatically retry with feedback: ```python theme={null} from tyler import Agent, RetryConfig, StructuredOutputError agent = Agent( name="extractor", model_name="gpt-4o", purpose="To extract structured data", retry_config=RetryConfig( max_retries=3, retry_on_validation_error=True, backoff_base_seconds=1.0 ) ) try: result = await agent.run(thread, response_type=Invoice) invoice = result.structured_data except StructuredOutputError as e: print(f"Failed after {e.message}") print(f"Validation errors: {e.validation_errors}") print(f"Last response: {e.last_response}") ``` ### How Retry Works When validation fails and retry is enabled: 1. Slide catches the `ValidationError` or `JSONDecodeError` from the output tool arguments 2. A tool result message with the error details is added to the thread 3. The LLM is called again with `tool_choice="required"` to try again 4. This repeats until validation succeeds or `max_retries` is exhausted 5. If all retries fail, `StructuredOutputError` is raised with the validation history ### RetryConfig Options ```python theme={null} class RetryConfig(BaseModel): max_retries: int = 3 # Number of retry attempts (0 = no retries) retry_on_validation_error: bool = True # Retry on schema validation failure retry_on_tool_error: bool = False # Retry on tool execution failure backoff_base_seconds: float = 1.0 # Delay between retries (exponential) ``` The backoff is exponential: 1s → 2s → 4s. This prevents overwhelming the API during transient issues. ## Tool Context (Dependency Injection) Tool context lets you inject runtime dependencies (databases, API clients, user info) into your tools without hardcoding them. ### Basic Usage ```python theme={null} from tyler import Agent, Thread, Message, ToolContext from typing import Dict, Any # 1. Define a tool that accepts context async def get_user_orders(ctx: ToolContext, limit: int = 10) -> str: """Get orders for the current user.""" db = ctx["db"] # Injected database user_id = ctx["user_id"] # Injected user ID orders = await db.get_orders(user_id, limit) return f"Found {len(orders)} orders" # 2. Register the tool from tyler.utils.tool_runner import tool_runner tool_runner.register_tool( name="get_user_orders", implementation=get_user_orders, definition={ "type": "function", "function": { "name": "get_user_orders", "description": "Get orders for the current user", "parameters": { "type": "object", "properties": { "limit": {"type": "integer", "default": 10} } } } } ) # 3. Create agent with the tool agent = Agent( name="order-assistant", model_name="gpt-4o", purpose="To help users with their orders", tools=[tool_runner.get_tool_definition("get_user_orders")] ) # 4. Run with tool_context result = await agent.run( thread, tool_context={ "db": database_connection, "user_id": current_user.id, "config": app_config } ) ``` ### Context Parameter Convention Tools receive context through a special first parameter. Use either: * `ctx: ToolContext` (recommended) * `ctx: Dict[str, Any]` * `context: ToolContext` * `context: Dict[str, Any]` ```python theme={null} # Both work identically async def my_tool(ctx: ToolContext, param: str) -> str: ... async def my_tool(context: Dict[str, Any], param: str) -> str: ... ``` The context parameter MUST be the first parameter in your tool function signature. If it's not first, the tool won't receive the context. ### Backward Compatibility Tools without a context parameter continue to work normally: ```python theme={null} # This tool doesn't use context - still works! async def simple_tool(query: str) -> str: return f"Searched for: {query}" # Context is passed but ignored for tools that don't need it result = await agent.run( thread, tool_context={"db": database} # simple_tool won't receive this ) ``` ### Error Handling If a tool expects context but none is provided: ```python theme={null} async def requires_db(ctx: ToolContext, user_id: str) -> str: db = ctx["db"] # Will raise if 'db' not in context ... # This will raise ToolContextError result = await agent.run(thread) # No tool_context provided! # Proper usage result = await agent.run(thread, tool_context={"db": my_database}) ``` ### Use Cases for Tool Context ```python theme={null} async def query_database(ctx: ToolContext, sql: str) -> str: db = ctx["db"] results = await db.execute(sql) return json.dumps(results) ``` ```python theme={null} async def get_user_preferences(ctx: ToolContext) -> str: user = ctx["current_user"] return json.dumps({ "name": user.name, "timezone": user.timezone, "language": user.language }) ``` ```python theme={null} async def send_slack_message(ctx: ToolContext, channel: str, text: str) -> str: slack = ctx["slack_client"] await slack.chat_postMessage(channel=channel, text=text) return f"Message sent to {channel}" ``` ```python theme={null} async def process_order(ctx: ToolContext, order_id: str) -> str: config = ctx["config"] if config.get("new_checkout_enabled"): return await new_checkout_flow(order_id) return await legacy_checkout_flow(order_id) ``` ## Combining Features Structured output and tool context work together seamlessly: ```python theme={null} from pydantic import BaseModel from typing import List from tyler import Agent, Thread, Message, RetryConfig, ToolContext class OrderSummary(BaseModel): total_orders: int total_value: float recent_orders: List[str] async def get_order_stats(ctx: ToolContext, user_id: str) -> str: db = ctx["db"] stats = await db.get_user_order_stats(user_id) return json.dumps(stats) agent = Agent( name="order-analyst", model_name="gpt-4o", purpose="To analyze user orders", tools=[order_stats_tool], retry_config=RetryConfig(max_retries=2) ) result = await agent.run( thread, response_type=OrderSummary, # Structured output tool_context={"db": database} # Dependency injection ) summary: OrderSummary = result.structured_data print(f"User has {summary.total_orders} orders worth ${summary.total_value}") ``` ## Streaming with Structured Output Structured output is currently a non-streaming feature. When you use `response_type`, the agent will collect the full response before validating and returning. If you need streaming with structured output, you can: 1. Stream for real-time feedback, then parse the final response: ```python theme={null} full_content = "" async for event in agent.stream(thread): if event.type == EventType.LLM_STREAM_CHUNK: chunk = event.data.get("content_chunk", "") full_content += chunk print(chunk, end="", flush=True) elif event.type == EventType.EXECUTION_COMPLETE: # Parse after streaming completes data = MyModel.model_validate_json(full_content) ``` 2. Or use non-streaming mode for structured data: ```python theme={null} # For structured data, use non-streaming result = await agent.run(thread, response_type=MyModel) ``` ## Best Practices ### Schema Design Define schemas for specific use cases rather than trying to capture everything: ```python theme={null} # ✅ Focused schema class SentimentResult(BaseModel): sentiment: Literal["positive", "negative", "neutral"] confidence: float = Field(ge=0, le=1) # ❌ Too broad class AnalysisResult(BaseModel): sentiment: str entities: List[str] summary: str keywords: List[str] language: str # ... many more fields ``` Pydantic validators help the LLM produce correct output: ```python theme={null} class Rating(BaseModel): score: int = Field(ge=1, le=5) # LLM knows the valid range reason: str = Field(max_length=200) ``` Field descriptions help the LLM understand what you want: ```python theme={null} class ContactInfo(BaseModel): email: str = Field(description="Primary email address") phone: Optional[str] = Field( default=None, description="Phone number in E.164 format, e.g., +1234567890" ) ``` ### Tool Context Even though context is a dict, document expected keys: ```python theme={null} async def my_tool(ctx: ToolContext, param: str) -> str: """ Args: ctx: Must contain 'db' (Database) and 'user' (User) param: The search query """ db: Database = ctx["db"] user: User = ctx["user"] ``` Check for required keys at the start of your tool: ```python theme={null} async def my_tool(ctx: ToolContext, param: str) -> str: if "db" not in ctx: raise ValueError("Tool requires 'db' in context") ... ``` Only pass what's needed: ```python theme={null} # ✅ Minimal context tool_context={"db": db, "user_id": user.id} # ❌ Passing entire app state tool_context={"app": entire_application_instance} ``` ## Error Reference | Error | Cause | Solution | | ----------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `StructuredOutputError` | LLM failed to produce valid structured output after retries | Check your schema complexity, add more retry attempts, or simplify the prompt | | `ToolContextError` | Tool expected context but none provided | Pass `tool_context` to `agent.run()` | | `KeyError` in tool | Required key missing from context | Ensure all required keys are in your `tool_context` dict | | `ValidationError` | LLM output doesn't match Pydantic schema | The retry mechanism will attempt to fix this; if it persists, simplify your schema | ## Next Steps Test structured output and tool context Complete Agent documentation Retry configuration options More advanced usage patterns # Testing agents Source: https://slide.mintlify.app/guides/testing-agents Build reliable agents with comprehensive testing Testing is crucial for building reliable AI agents. Slide's evaluation framework lets you test agent behavior, verify tool usage, and ensure consistent responses across different scenarios. **💻 Code Examples** Basic agent evaluation setup Test with mocked tool responses ## Why Test Agents? AI agents are non-deterministic, making testing challenging but essential: * Verify agents use tools correctly * Ensure consistent behavior patterns * Catch regressions early * Build confidence before deployment ## Quick Start with Testing ```python theme={null} import asyncio from tyler import Agent from tyler.eval import AgentEval, Conversation, Expectation from lye import WEB_TOOLS # Create your agent agent = Agent( name="research-assistant", model_name="gpt-4", purpose="To help with research tasks", tools=WEB_TOOLS ) # Define test scenarios eval = AgentEval( name="research_agent_test", conversations=[ Conversation( user="What's the weather in San Francisco?", expect=Expectation( uses_tools=["web-search"], mentions_any=["weather", "temperature", "San Francisco"], tone="helpful" ) ), Conversation( user="Tell me a joke", expect=Expectation( does_not_use_tools=["web-search"], tone="friendly" ) ) ] ) # Run tests async def run_tests(): results = await eval.run(agent, trials=3) print(f"✅ Pass rate: {results.pass_rate:.0%}") # Detailed results for conv_result in results.conversation_results: print(f"\nTest: {conv_result.conversation.user}") print(f"Passed: {conv_result.passed}") if not conv_result.passed: print(f"Failures: {conv_result.failures}") asyncio.run(run_tests()) ``` ## Mock Tools for Testing Use mock tools to test without making real API calls: ```python theme={null} from tyler.eval import mock_tools # Define mock responses mock_responses = { "web-search": { "weather San Francisco": "Current weather: 68°F, partly cloudy", "latest AI news": "OpenAI announces GPT-5..." }, "files-write": { "report.md": "File saved successfully" } } # Run tests with mocks eval = AgentEval( name="mock_test", conversations=[...], mock_responses=mock_responses ) # Tests run without real API calls results = await eval.run(agent) ``` ## Testing Patterns ### Pattern 1: Tool Usage Verification ```python theme={null} eval = AgentEval( name="tool_usage_test", conversations=[ # Should use search Conversation( user="Find information about quantum computing breakthroughs", expect=Expectation( uses_tools=["web-search"], mentions_any=["quantum", "computing", "breakthrough"] ) ), # Should use multiple tools Conversation( user="Research climate change and save a report", expect=Expectation( uses_tools=["web-search", "files-write"], mentions_all=["climate", "report", "saved"] ) ), # Should NOT use tools Conversation( user="What's 2 + 2?", expect=Expectation( does_not_use_tools=["web-search", "files-write"], mentions="4" ) ) ] ) ``` ### Pattern 2: Response Quality Testing ```python theme={null} eval = AgentEval( name="quality_test", conversations=[ Conversation( user="Explain quantum computing to a 5-year-old", expect=Expectation( mentions_any=["simple", "easy", "like", "imagine"], does_not_mention_any=["superposition", "entanglement", "qubit"], tone="simple" ) ), Conversation( user="Write a professional email declining a meeting", expect=Expectation( mentions_all=["thank you", "unfortunately", "unable"], tone="professional", min_length=50 ) ) ] ) ``` ### Pattern 3: Multi-Turn Conversations ```python theme={null} eval = AgentEval( name="multi_turn_test", conversations=[ Conversation( messages=[ {"role": "user", "content": "My name is Alice"}, {"role": "assistant", "content": "Nice to meet you, Alice!"}, {"role": "user", "content": "What's my name?"} ], expect=Expectation( mentions="Alice", tone="friendly" ) ) ] ) ``` ## Advanced Testing Features ### Custom Expectations Create custom expectation functions: ```python theme={null} def has_valid_json(response: str) -> bool: """Check if response contains valid JSON""" import json try: # Find JSON in response start = response.find('{') end = response.rfind('}') + 1 if start != -1 and end != 0: json.loads(response[start:end]) return True except: pass return False eval = AgentEval( name="json_test", conversations=[ Conversation( user="Return user data as JSON", expect=Expectation( custom_checks=[has_valid_json], mentions="json" ) ) ] ) ``` ### Testing with Different Models Test consistency across models: ```python theme={null} models = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus-20240229"] for model in models: agent = Agent( name="test-agent", model_name=model, purpose="To be helpful", tools=WEB_TOOLS ) results = await eval.run(agent) print(f"{model}: {results.pass_rate:.0%}") ``` ### Performance Testing Measure response times: ```python theme={null} import time class PerformanceEval(AgentEval): async def run(self, agent, trials=1): start_time = time.time() results = await super().run(agent, trials) duration = time.time() - start_time print(f"Total time: {duration:.2f}s") print(f"Avg per conversation: {duration/len(self.conversations):.2f}s") return results ``` ## Integration Testing Test complete workflows: ```python theme={null} import asyncio from tyler import Agent, Thread, Message, ThreadStore from lye import WEB_TOOLS, FILES_TOOLS async def test_research_workflow(): # Setup thread_store = await ThreadStore.create() agent = Agent( name="researcher", model_name="gpt-4", purpose="To research and create reports", tools=[*WEB_TOOLS, *FILES_TOOLS], thread_store=thread_store ) # Test workflow thread = Thread(id="test-research") # Step 1: Research thread.add_message(Message( role="user", content="Research the latest in renewable energy" )) result = await agent.run(thread) thread = result.thread # Verify research was done assert any(msg.role == "tool" and "web-search" in msg.name for msg in thread.messages) # Step 2: Create report thread.add_message(Message( role="user", content="Now create a summary report and save it" )) result = await agent.run(thread) thread = result.thread # Verify file was created assert any(msg.role == "tool" and "files-write" in msg.name for msg in thread.messages) print("✅ Workflow test passed!") asyncio.run(test_research_workflow()) ``` ## Testing Best Practices ### 1. Test Different Scenarios ```python theme={null} eval = AgentEval( name="comprehensive_test", conversations=[ # Happy path Conversation( user="Normal request for information", expect=Expectation(tone="helpful") ), # Edge cases Conversation( user="", # Empty input expect=Expectation( mentions_any=["provide", "help", "question"], tone="helpful" ) ), # Error scenarios Conversation( user="Search for information on nonexistent-topic-12345", expect=Expectation( handles_gracefully=True, mentions_any=["couldn't find", "no results", "try"] ) ) ] ) ``` ### 2. Test Tool Error Handling ```python theme={null} # Mock tool failures mock_responses = { "web-search": { "error_test": Exception("Network error") } } eval = AgentEval( name="error_handling_test", mock_responses=mock_responses, conversations=[ Conversation( user="Search for error_test", expect=Expectation( handles_gracefully=True, mentions_any=["error", "problem", "try again"], does_not_crash=True ) ) ] ) ``` ### 3. Regression Testing ```python theme={null} class RegressionTest: def __init__(self): self.baseline_results = None async def establish_baseline(self, agent, eval): """Run tests and save baseline""" self.baseline_results = await eval.run(agent) return self.baseline_results async def test_regression(self, agent, eval): """Compare against baseline""" current_results = await eval.run(agent) # Compare pass rates baseline_rate = self.baseline_results.pass_rate current_rate = current_results.pass_rate if current_rate < baseline_rate - 0.1: # 10% tolerance print(f"⚠️ Regression detected!") print(f"Baseline: {baseline_rate:.0%}") print(f"Current: {current_rate:.0%}") return False print(f"✅ No regression (Current: {current_rate:.0%})") return True ``` ## CI/CD Integration ### GitHub Actions Example ```yaml theme={null} # .github/workflows/test-agents.yml name: Test Agents on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.13.5' - name: Install dependencies run: | curl -LsSf https://astral.sh/uv/install.sh | sh uv sync --dev - name: Run agent tests env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | python -m pytest tests/test_agents.py -v ``` ### Test File Structure ```python theme={null} # tests/test_agents.py import pytest import asyncio from tyler import Agent from tyler.eval import AgentEval, Conversation, Expectation class TestResearchAgent: @pytest.fixture async def agent(self): return Agent( name="test-researcher", model_name="gpt-3.5-turbo", # Cheaper for tests purpose="To help with research" ) @pytest.mark.asyncio async def test_basic_research(self, agent): eval = AgentEval( name="basic_research", conversations=[ Conversation( user="What is Python?", expect=Expectation( mentions_any=["programming", "language"], min_length=50 ) ) ] ) results = await eval.run(agent) assert results.pass_rate >= 0.8 @pytest.mark.asyncio async def test_tool_usage(self, agent): # Test with mocked tools pass ``` ## Debugging Failed Tests Enable detailed logging: ```python theme={null} import logging # Set up logging logging.basicConfig(level=logging.DEBUG) # Run tests with debugging eval = AgentEval( name="debug_test", conversations=[...], debug=True # Show detailed output ) results = await eval.run(agent) # Inspect failures for conv_result in results.conversation_results: if not conv_result.passed: print(f"\n❌ Failed: {conv_result.conversation.user}") print(f"Response: {conv_result.response}") print(f"Failures: {conv_result.failures}") print(f"Tool calls: {conv_result.tool_calls}") ``` ## Real-World Example: Customer Support Agent Testing ```python theme={null} from tyler import Agent from tyler.eval import AgentEval, Conversation, Expectation from lye import WEB_TOOLS class CustomerSupportTests: def __init__(self): self.agent = Agent( name="support-agent", model_name="gpt-4", purpose="To help customers with product issues", tools=WEB_TOOLS ) def get_test_suite(self): return AgentEval( name="customer_support_suite", conversations=[ # Greeting test Conversation( user="Hi", expect=Expectation( mentions_any=["hello", "hi", "help"], tone="friendly", max_length=200 ) ), # Product inquiry Conversation( user="How do I reset my password?", expect=Expectation( mentions_all=["password", "reset"], mentions_any=["click", "button", "email", "link"], tone="helpful", min_length=50 ) ), # Complaint handling Conversation( user="Your product is terrible and doesn't work!", expect=Expectation( mentions_any=["sorry", "apologize", "understand"], mentions_any=["help", "assist", "resolve"], tone="empathetic", does_not_mention_any=["terrible", "doesn't work"] ) ), # Information lookup Conversation( user="What are your business hours?", expect=Expectation( uses_tools=["web-search"], mentions_any=["hours", "open", "available"], provides_specific_info=True ) ), # Escalation Conversation( user="I want to speak to a human!", expect=Expectation( mentions_any=["representative", "agent", "transfer"], tone="understanding", provides_next_steps=True ) ) ] ) async def run_all_tests(self): eval = self.get_test_suite() # Run multiple trials for consistency results = await eval.run(self.agent, trials=5) print(f"\n📊 Customer Support Agent Test Results") print(f"{'='*50}") print(f"Overall pass rate: {results.pass_rate:.0%}") print(f"Total tests: {len(eval.conversations) * 5}") print(f"Passed: {results.passed_count}") print(f"Failed: {results.failed_count}") # Detailed analysis if results.pass_rate < 1.0: print(f"\n❌ Failed Tests:") for conv_result in results.conversation_results: if not conv_result.passed: print(f"- {conv_result.conversation.user[:50]}...") print(f" Failures: {', '.join(conv_result.failures)}") return results.pass_rate >= 0.9 # 90% threshold # Run tests async def main(): tester = CustomerSupportTests() success = await tester.run_all_tests() if success: print("\n✅ All tests passed! Agent is ready for deployment.") else: print("\n❌ Tests failed. Please review and fix issues.") asyncio.run(main()) ``` ## Next steps Deep dive into evaluation features Automate agent testing # Your first agent Source: https://slide.mintlify.app/guides/your-first-agent Step-by-step guide to building your first AI agent In this guide, we'll build a research assistant agent that can search the web, analyze information, and create reports. By the end, you'll understand the core concepts of building agents with Slide. **💻 Code Examples** Minimal agent setup Real-time responses Add capabilities **Requirements:** Python 3.11 or higher ## What We're Building We'll create an agent that can: * Search for information on any topic * Analyze and summarize findings * Save research reports to files * Remember previous conversations First, create a new project directory: ```bash theme={null} mkdir research-agent cd research-agent ``` Install the required packages: ```bash theme={null} # Initialize project with uv uv init . # Add Slide packages uv add slide-tyler slide-lye slide-narrator ``` ```bash theme={null} # Create virtual environment python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate # Install packages pip install slide-tyler slide-lye slide-narrator ``` Create a file called `agent.py`: ```python theme={null} import asyncio from tyler import Agent, Thread, Message, EventType from lye import WEB_TOOLS, FILES_TOOLS async def main(): # Create your agent agent = Agent( name="research-assistant", model_name="gpt-4o", purpose="To help with research by finding, analyzing, and summarizing information", tools=[ *WEB_TOOLS, # Can search and fetch web content *FILES_TOOLS # Can read and write files ] ) # Create a conversation thread thread = Thread() thread.add_message(Message( role="user", content="Research the latest developments in renewable energy and create a summary" )) # Watch the agent work in real-time print("🔍 Researching renewable energy...\n") async for event in agent.stream(thread): # Show content as it's generated if event.type == EventType.LLM_STREAM_CHUNK: print(event.data['content_chunk'], end="", flush=True) # Show when tools are used elif event.type == EventType.TOOL_SELECTED: print(f"\n\n🔧 Using {event.data['tool_name']}...", flush=True) elif event.type == EventType.TOOL_RESULT: print(f"✓ Done\n", flush=True) if __name__ == "__main__": asyncio.run(main()) ``` **Choosing between `.stream()` and `.run()`**: * **Use `agent.stream(thread)`** for: * Chat interfaces and real-time UIs * Watching the agent work (great for debugging) * Progressive UI updates as content generates * **Use `await agent.run(thread)`** for: * Batch processing and automation * Testing (when you just need the final result) * Simple scripts where streaming isn't needed Most interactive applications will want `.stream()` - it's what makes agents feel alive! ```bash theme={null} uv run agent.py ``` ```bash theme={null} python agent.py ``` When you run the agent, here's what happens: 1. **Thread Creation**: A conversation thread is created to hold messages 2. **Message Processing**: The agent receives your message and plans its approach 3. **Tool Usage**: The agent uses web search tools to find information 4. **Response Generation**: The agent synthesizes findings into a response ## Understanding What Happened When you stream, you get **real-time events** showing everything your agent does: ```python theme={null} async def main(): thread = Thread() thread.add_message(Message( role="user", content="What are the top 3 renewable energy breakthroughs in 2024?" )) # Stream events to see everything in real-time async for event in agent.stream(thread): if event.type == EventType.LLM_REQUEST: print("🤔 Agent is thinking...") elif event.type == EventType.TOOL_SELECTED: print(f"🔧 Calling {event.data['tool_name']}...") elif event.type == EventType.TOOL_RESULT: print(f"✓ Got results") elif event.type == EventType.LLM_STREAM_CHUNK: # Print response as it's generated print(event.data['content_chunk'], end="", flush=True) elif event.type == EventType.EXECUTION_COMPLETE: print("\n\n✅ Done!") ``` **Non-streaming alternative**: If you just want the final result without watching the process: ```python theme={null} result = await agent.run(thread) print(result.content) ``` This is useful for batch processing or automation where you don't need real-time updates. ## Add Persistence to Your Agent Let's upgrade the agent to maintain conversation history: ```python theme={null} import asyncio from tyler import Agent, Thread, Message, ThreadStore, FileStore from lye import WEB_TOOLS, FILES_TOOLS async def create_agent_with_persistence(): # Set up persistent storage thread_store = await ThreadStore.create("sqlite+aiosqlite:///research.db") file_store = await FileStore.create(base_path="./research_files") # Create agent with persistence agent = Agent( name="research-assistant", model_name="gpt-4", purpose="To help with research and maintain our conversation history", tools=[*WEB_TOOLS, *FILES_TOOLS], thread_store=thread_store, file_store=file_store ) return agent, thread_store async def main(): agent, thread_store = await create_agent_with_persistence() # Try to resume previous conversation thread_id = "main-research" try: thread = await thread_store.get_thread(thread_id) print("📚 Resuming previous research session...") print(f" Found {len(thread.messages)} previous messages") except: thread = Thread(id=thread_id) print("🆕 Starting new research session...") # Add new message message = Message( role="user", content="What did we discuss last time? If this is our first conversation, tell me about yourself." ) thread.add_message(message) # Process result = await agent.run(thread) # Save the conversation await thread_store.save_thread(result.thread) # Print response for msg in result.new_messages: if msg.role == "assistant": print(f"\n🤖 {msg.content}") if __name__ == "__main__": asyncio.run(main()) ``` ## Interactive Research Session Let's create an interactive version where you can have a conversation: ```python theme={null} async def interactive_session(): agent, thread_store = await create_agent_with_persistence() # Load or create thread thread_id = "interactive-research" try: thread = await thread_store.get_thread(thread_id) print("📚 Resuming previous session...") except: thread = Thread(id=thread_id) print("🆕 Starting new session...") print("💡 Try asking me to research any topic!") print("\nType 'exit' to end the session\n") while True: # Get user input user_input = input("You: ") if user_input.lower() in ['exit', 'quit']: break # Add message to thread message = Message(role="user", content=user_input) thread.add_message(message) # Process with agent print("\n🤖 Thinking...", end="", flush=True) result = await agent.run(thread) # Clear thinking message print("\r" + " " * 20 + "\r", end="") # Display response for msg in result.new_messages: if msg.role == "assistant": print(f"🤖 Assistant: {msg.content}\n") elif msg.role == "tool": print(f" [Used {msg.name}]") # Save conversation await thread_store.save_thread(result.thread) thread = result.thread print("\n👋 Session saved. See you next time!") if __name__ == "__main__": asyncio.run(interactive_session()) ``` ## Understanding Tools Let's explore what tools your agent can use: ```python theme={null} # See available tools from lye import WEB_TOOLS, FILES_TOOLS, IMAGE_TOOLS print("🔧 Web Tools:") for tool in WEB_TOOLS: print(f" - {tool['definition']['name']}: {tool['definition']['description']}") print("\n📁 File Tools:") for tool in FILES_TOOLS: print(f" - {tool['definition']['name']}: {tool['definition']['description']}") ``` You can also give your agent specific tools: ```python theme={null} from lye.web import search, fetch from lye.files import write_file agent = Agent( name="focused-researcher", model_name="gpt-4", purpose="To search and save information", tools=[search, fetch, write_file] # Only these specific tools ) ``` ## Debugging Your Agent ### Basic Logging Enable detailed logging to see what your agent is doing: ```python theme={null} import logging # Enable debug logging logging.basicConfig(level=logging.INFO) ``` ### Advanced tracing with Weave For comprehensive debugging and observability, Slide integrates with [Weights & Biases Weave](https://weave-docs.wandb.ai/). Weave provides: * **Visual traces** of every agent action and decision * **LLM call tracking** with inputs, outputs, and token usage * **Tool execution monitoring** to see which tools were called and their results * **Performance insights** to identify bottlenecks * **Error tracking** with full context ```python theme={null} import weave # Initialize Weave tracing weave.init("my-research-agent") # Now all agent operations will be traced automatically # View traces at https://wandb.ai/your-username/my-research-agent ``` Weave traces are invaluable for debugging complex agent behaviors. You can see exactly what prompts were sent to the LLM, what tools were called, and how the agent made its decisions. ## Next steps You've built your first agent! Here's what to explore next: Give your agent more capabilities Build real-time interactive agents Ensure your agent behaves correctly Turn your agent into a Slack agent ## Tips for Success The `purpose` parameter significantly affects agent behavior. Be specific: ```python theme={null} # Good purpose="To research technology topics and create detailed, well-sourced reports" # Too vague purpose="To help with stuff" ``` Only give your agent the tools it needs: ```python theme={null} # For a research agent tools=[*WEB_TOOLS, *FILES_TOOLS] # For an image analysis agent tools=[*IMAGE_TOOLS, *FILES_TOOLS] # For a data processing agent tools=[*FILES_TOOLS] ``` Always handle potential errors: ```python theme={null} try: result = await agent.run(thread) except Exception as e: print(f"❌ Error: {e}") # Handle gracefully ``` Your agent can handle various file types out of the box. For enhanced capabilities: * **Scanned PDFs**: Install `poppler` for OCR support This is optional - your agent will work fine without it for most use cases. # Overview Source: https://slide.mintlify.app/introduction A development kit for manifesting AI agents with a complete lack of conventional limitations.
Slide Logo
**Build an agent with just a few lines of code.** ```python theme={null} import asyncio from tyler import Agent, Thread, Message, EventType from lye import WEB_TOOLS # Create an AI agent that can browse the web agent = Agent( name="web_summarizer", model_name="gpt-4o", purpose="To summarize web content clearly and concisely", tools=WEB_TOOLS ) # Ask your agent to visit and summarize a webpage thread = Thread() thread.add_message(Message( role="user", content="Why should I use https://slide.mintlify.app/?" )) # Watch your agent work in real-time async def main(): async for event in agent.stream(thread): # Print content as it's generated if event.type == EventType.LLM_STREAM_CHUNK: print(event.data['content_chunk'], end="", flush=True) # Show when tools are being used elif event.type == EventType.TOOL_SELECTED: print(f"\n🔧 Using {event.data['tool_name']}...") asyncio.run(main()) ``` That's it! Your agent can now search the web, analyze images, and complete complex tasks autonomously—and you can watch it happen in real-time. ## Why use Slide? Slide gives you everything you need to build, test, and deploy intelligent AI agents. It is: * **Extensible and open source**: Built on a foundation you can inspect, customize, and contribute to * **Flexible with model support**: Compatible with any LLM provider supported by LiteLLM (100+ providers including OpenAI, Anthropic, etc.) * **Real-time streaming enabled**: Designed to build interactive applications with streaming responses from both agents and tools * **MCP and A2A compatible**: Seamlessly integrated with Model Context Protocol (MCP) servers and Agent-to-Agent (A2A) protocol for multi-agent interoperability * **Agent delegation built-in**: Create multi-agent systems where specialized agents collaborate to solve complex tasks * **Multimodal by design**: Capable of processing and understanding images, audio, PDFs, and more out of the box * **Persistent with conversations**: Equipped with built-in support for threads, messages, and attachments with flexible storage options (in-memory, SQLite, or PostgreSQL) * **Type-safe structured outputs**: Get validated Pydantic models from your agents with automatic retry on validation failure—no more manual JSON parsing * **Ready-to-use with tools**: Packed with built-in tools for web interaction, file handling, and browser automation—plus dependency injection via `tool_context` for custom tools * **Skills and project instructions**: Progressively disclose reusable [Open Agent Skills](https://openagentskills.dev/docs/specification) on-demand, and eagerly load project-level guidelines via [AGENTS.md](https://agents.md) * **Transparent reasoning**: Access model thinking tokens to see how agents arrive at decisions * **Interactive CLI included**: Chat with your agents instantly using the built-in `tyler chat` command, or scaffold new projects with `tyler init` * **Evaluation-ready**: Equipped with a framework to test agents safely with mock tools, prebuilt LLM judges, and multi-turn conversation scenarios * **Debuggable**: Integrated with W\&B Weave for powerful tracing and debugging capabilities ## Ready to Build? Build your first agent in 5 minutes Step-by-step guide to building agents ## What Can You Build? Autonomous agents that can use tools, make decisions, and complete complex tasks Coordinate specialized agents that work together on complex problems Deploy your agents as Slack agents with built-in message handling Agents that can search, analyze, and synthesize information Extract validated, type-safe structured data from documents and conversations ***
🧩 Advanced: Using Slide's Modular Architecture While most users will want the full agent experience, Slide is built as modular packages that can be used independently: * **Tyler**: Core agent framework * **Lye**: Tool library * **Narrator**: Conversation persistence * **Space Monkey**: Slack integration [Learn more about the architecture →](/concepts/architecture)
# Quickstart Source: https://slide.mintlify.app/quickstart Build your first AI agent in 5 minutes In this quickstart, you'll build an AI agent that can search the web and analyze images. Let's dive in! **Requirements:** Python 3.11 or higher ```bash theme={null} # Install uv if you haven't already curl -LsSf https://astral.sh/uv/install.sh | sh # Create a new project uv init my-agent cd my-agent # Install Slide uv add slide-tyler slide-lye slide-narrator ``` ```bash theme={null} # Create a virtual environment (Python 3.11+) python3 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate # Install Slide pip install slide-tyler slide-lye slide-narrator ``` Your agent needs an API key to use the LLM. Choose your provider: Create your API key from [platform.openai.com](https://platform.openai.com/api-keys) then add it to your environment: ```bash theme={null} export OPENAI_API_KEY="sk-..." ``` Create your API key from [console.anthropic.com](https://console.anthropic.com/) then add it to your environment: ```bash theme={null} export ANTHROPIC_API_KEY="sk-ant-..." ``` Slide supports 100+ providers via LiteLLM. See the [full list](https://docs.litellm.ai/docs/providers). Example for Google: ```bash theme={null} export GOOGLE_API_KEY="..." ``` For production, use a `.env` file. If you choose this approach: 1. Install python-dotenv: `uv add python-dotenv` or `pip install python-dotenv` 2. Create a `.env` file with your API keys 3. Uncomment the dotenv import lines in the code example below Create a file called `agent.py`: ```python theme={null} # Optional: If you're using a .env file for API keys, uncomment these lines # from dotenv import load_dotenv # load_dotenv() import asyncio from tyler import Agent, Thread, Message, EventType from lye import WEB_TOOLS, IMAGE_TOOLS, FILES_TOOLS # Optional: Uncomment these lines if you want observiabiltiy with W&B Weave # import weave # weave.init("wandb-designers/my-agent") async def main(): # Create your agent agent = Agent( name="research-assistant", model_name="gpt-4o", # Use the model for your API key provider purpose="To help with research and analysis tasks", tools=[ *WEB_TOOLS, # Can search and fetch web content *IMAGE_TOOLS # Can analyze and describe images ] ) # Create a conversation thread thread = Thread() thread.add_message(Message( role="user", content="Search for information about the Mars Perseverance rover and create a summary" )) # Watch your agent work in real-time print("🤖 Agent is working...\n") async for event in agent.stream(thread): # Print content as it's generated if event.type == EventType.LLM_STREAM_CHUNK: print(event.data['content_chunk'], end="", flush=True) # Show tool usage elif event.type == EventType.TOOL_SELECTED: print(f"\n\n🔧 Using {event.data['tool_name']}...", flush=True) if __name__ == "__main__": asyncio.run(main()) ``` ```bash theme={null} uv run agent.py ``` ```bash theme={null} python agent.py ``` Your agent will search for information about the Mars rover and create a summary. That's it! 🎉 ## What's Next? Now that you have a working agent, explore these guides to add more capabilities: Detailed walkthrough with persistence and interactive sessions Chat with agents instantly using the interactive CLI Make your agent maintain conversation history See responses as they're generated in real-time Write tests to ensure your agent behaves correctly ## Deploy Your Agent Turn your agent into a Slack agent Add built-in and custom tools to agents ## Troubleshooting If you see errors like "No solution found when resolving dependencies" or "requires Python>=3.11": **For uv users:** ```bash theme={null} # If you initialized with an older Python version, edit your pyproject.toml: # requires-python = ">=3.11" # Then recreate your virtual environment: rm -rf .venv uv sync ``` **For pip users:** We recommend switching to uv for better dependency management: ```bash theme={null} # Install uv curl -LsSf https://astral.sh/uv/install.sh | sh # Recreate your project with uv uv init my-agent cd my-agent uv add slide-tyler slide-lye slide-narrator ``` Make sure to set your OpenAI API key: ```bash theme={null} export OPENAI_API_KEY="sk-..." ``` Or use a different model provider: ```python theme={null} agent = Agent( model_name="claude-3-opus-20240229", # Anthropic # or model_name="gemini-pro", # Google # or model_name="o3", # OpenAI O-series ) ``` Tyler automatically handles model-specific parameter restrictions. For example, O-series models only support `temperature=1`, but Tyler will automatically drop incompatible parameters, so you can use the same agent configuration across all models. Make sure you've installed all packages: ```bash theme={null} uv add slide-tyler slide-lye slide-narrator ``` Remember to use `asyncio.run()` or run in an async context: ```python theme={null} import asyncio async def main(): # Your agent code here pass asyncio.run(main()) ``` # Using Lye Source: https://slide.mintlify.app/standalone-packages/using-lye Powerful tools for any Python application Lye is Slide's comprehensive tool library that provides ready-to-use utilities for web interaction, file handling, image processing, audio manipulation, and more. While designed to work seamlessly with Tyler agents, Lye can be used independently in any Python application. ## Why Use Lye Standalone? * Add powerful capabilities to existing applications * No AI or agent dependencies required * Async-first design for modern Python * Consistent API across all tool categories * Well-tested and production-ready ## Quick Start ### Installation ```bash theme={null} # Using uv (recommended) uv add slide-lye # Using pip (fallback) pip install slide-lye ``` ### Optional system dependencies While Lye works out of the box, some advanced features benefit from additional system libraries: * **PDF OCR**: Install `poppler` to process scanned PDFs with optical character recognition See the [Lye package documentation](/packages/lye/introduction#optional-dependencies) for installation instructions. ### Basic usage ```python theme={null} import asyncio from lye.web import search, fetch async def main(): # Search the web results = await search("Python async programming") print(f"Found: {results}") # Fetch a webpage content = await fetch("https://example.com") print(f"Page content: {content[:200]}...") asyncio.run(main()) ``` ## Tool Categories ### Web Tools Tools for interacting with the web: ```python theme={null} from lye.web import search, fetch from lye import WEB_TOOLS # All web tools as a list # Search the web results = await search("climate change 2024") # Fetch webpage content html = await fetch("https://example.com/article") # Using with rate limiting async def fetch_with_delay(urls): results = [] for url in urls: content = await fetch(url) results.append(content) await asyncio.sleep(1) # Be respectful return results ``` ### File Tools File system operations: ```python theme={null} from lye.files import read_file, write_file, list_files from lye import FILES_TOOLS # Read a file content = await read_file("data.json") # Write to a file await write_file("output.txt", "Hello, World!") # List directory contents files = await list_files("./documents", pattern="*.pdf") # Batch file processing async def process_csv_files(directory): csv_files = await list_files(directory, pattern="*.csv") for file in csv_files: data = await read_file(file) # Process data processed = transform_data(data) await write_file(f"processed_{file}", processed) ``` ### Image Tools Image analysis and processing: ```python theme={null} from lye.image import analyze_image, extract_text_from_image from lye import IMAGE_TOOLS # Analyze an image with open("photo.jpg", "rb") as f: analysis = await analyze_image(f.read()) print(f"Image contains: {analysis}") # Extract text (OCR) with open("document.png", "rb") as f: text = await extract_text_from_image(f.read()) print(f"Extracted text: {text}") # Batch image processing async def catalog_images(image_dir): images = await list_files(image_dir, pattern="*.jpg") catalog = {} for img_path in images: with open(img_path, "rb") as f: image_data = f.read() catalog[img_path] = { "analysis": await analyze_image(image_data), "text": await extract_text_from_image(image_data) } return catalog ``` ### Audio Tools Audio processing and transcription: ```python theme={null} from lye.audio import transcribe, text_to_speech from lye import AUDIO_TOOLS # Transcribe audio with open("recording.mp3", "rb") as f: transcript = await transcribe(f.read()) print(f"Transcript: {transcript}") # Generate speech audio_data = await text_to_speech( "Hello, this is a test of text to speech.", voice="alloy" # or "echo", "fable", "onyx", "nova", "shimmer" ) # Save the audio with open("output.mp3", "wb") as f: f.write(audio_data) # Process podcast episodes async def transcribe_podcast(episode_files): transcripts = {} for episode in episode_files: with open(episode, "rb") as f: audio = f.read() transcript = await transcribe(audio) transcripts[episode] = transcript # Save transcript await write_file( f"{episode}_transcript.txt", transcript ) return transcripts ``` ### Browser Tools Web automation and scraping: ```python theme={null} from lye.browser import screenshot, extract_text_from_webpage from lye import BROWSER_TOOLS # Take a screenshot image_data = await screenshot("https://example.com") with open("screenshot.png", "wb") as f: f.write(image_data) # Extract clean text from webpage text = await extract_text_from_webpage("https://example.com/article") print(f"Article text: {text}") # Monitor website changes async def monitor_website(url, interval=3600): previous_content = None while True: current_content = await extract_text_from_webpage(url) if previous_content and current_content != previous_content: print(f"Website changed at {datetime.now()}") # Send notification, save diff, etc. previous_content = current_content await asyncio.sleep(interval) ``` ## Real-World Applications ### Web Scraper Build a comprehensive web scraper: ```python theme={null} from lye.web import search, fetch from lye.browser import extract_text_from_webpage from lye.files import write_file import json class WebScraper: def __init__(self): self.results = [] async def scrape_topic(self, topic, max_results=10): # Search for URLs search_results = await search(f"{topic} site:medium.com") urls = self.extract_urls(search_results)[:max_results] # Scrape each URL for url in urls: try: content = await extract_text_from_webpage(url) self.results.append({ "url": url, "content": content, "word_count": len(content.split()), "scraped_at": datetime.now().isoformat() }) except Exception as e: print(f"Error scraping {url}: {e}") # Save results await write_file( f"{topic}_articles.json", json.dumps(self.results, indent=2) ) return self.results def extract_urls(self, search_results): # Parse URLs from search results # Implementation depends on search result format return [] # Usage scraper = WebScraper() articles = await scraper.scrape_topic("machine learning", max_results=20) ``` ### Media Processor Process multimedia files: ```python theme={null} from lye.image import analyze_image, extract_text_from_image from lye.audio import transcribe from lye.files import list_files, write_file import os class MediaProcessor: def __init__(self, input_dir, output_dir): self.input_dir = input_dir self.output_dir = output_dir os.makedirs(output_dir, exist_ok=True) async def process_all(self): # Process images images = await list_files(self.input_dir, pattern="*.{jpg,png,jpeg}") for img in images: await self.process_image(img) # Process audio audio_files = await list_files(self.input_dir, pattern="*.{mp3,wav,m4a}") for audio in audio_files: await self.process_audio(audio) async def process_image(self, image_path): with open(image_path, "rb") as f: image_data = f.read() # Analyze and extract text analysis = await analyze_image(image_data) text = await extract_text_from_image(image_data) # Save results base_name = os.path.basename(image_path) output_path = os.path.join(self.output_dir, f"{base_name}_analysis.json") await write_file(output_path, json.dumps({ "file": image_path, "analysis": analysis, "extracted_text": text }, indent=2)) async def process_audio(self, audio_path): with open(audio_path, "rb") as f: audio_data = f.read() # Transcribe transcript = await transcribe(audio_data) # Save transcript base_name = os.path.basename(audio_path) output_path = os.path.join(self.output_dir, f"{base_name}_transcript.txt") await write_file(output_path, transcript) # Usage processor = MediaProcessor("./media_files", "./processed") await processor.process_all() ``` ### Research assistant Automated research tool: ```python theme={null} from lye.web import search, fetch from lye.files import write_file, read_file from lye.browser import extract_text_from_webpage from datetime import datetime import json class ResearchAssistant: def __init__(self, cache_dir="./research_cache"): self.cache_dir = cache_dir os.makedirs(cache_dir, exist_ok=True) async def research_topic(self, topic, questions): research_data = { "topic": topic, "timestamp": datetime.now().isoformat(), "questions": {}, "sources": [] } for question in questions: # Search for answers query = f"{topic} {question}" search_results = await search(query) # Extract URLs and fetch content urls = self.extract_urls(search_results)[:3] answers = [] for url in urls: try: content = await extract_text_from_webpage(url) answers.append({ "url": url, "content": content[:1000], # First 1000 chars "relevance": self.calculate_relevance(content, question) }) research_data["sources"].append(url) except: continue research_data["questions"][question] = answers # Save research filename = f"{topic.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" await write_file( os.path.join(self.cache_dir, filename), json.dumps(research_data, indent=2) ) return research_data def calculate_relevance(self, content, question): # Simple keyword matching (can be improved) keywords = question.lower().split() content_lower = content.lower() matches = sum(1 for keyword in keywords if keyword in content_lower) return matches / len(keywords) # Usage assistant = ResearchAssistant() research = await assistant.research_topic( "renewable energy", [ "What are the latest breakthroughs?", "Which countries lead in adoption?", "What are the main challenges?" ] ) ``` ## Advanced patterns ### Rate Limiting Implement rate limiting for API calls: ```python theme={null} import asyncio from datetime import datetime, timedelta class RateLimiter: def __init__(self, calls_per_minute=60): self.calls_per_minute = calls_per_minute self.calls = [] async def wait_if_needed(self): now = datetime.now() minute_ago = now - timedelta(minutes=1) # Remove old calls self.calls = [call for call in self.calls if call > minute_ago] if len(self.calls) >= self.calls_per_minute: # Wait until the oldest call is more than a minute old wait_time = (self.calls[0] + timedelta(minutes=1) - now).total_seconds() await asyncio.sleep(wait_time) self.calls.append(now) # Use with Lye tools rate_limiter = RateLimiter(calls_per_minute=30) async def search_with_limit(query): await rate_limiter.wait_if_needed() return await search(query) ``` ### Error Handling Robust error handling for tools: ```python theme={null} from typing import Optional import logging logger = logging.getLogger(__name__) async def safe_fetch(url: str, retries: int = 3) -> Optional[str]: """Fetch URL with retry logic""" for attempt in range(retries): try: content = await fetch(url) return content except Exception as e: logger.warning(f"Attempt {attempt + 1} failed for {url}: {e}") if attempt < retries - 1: await asyncio.sleep(2 ** attempt) # Exponential backoff logger.error(f"Failed to fetch {url} after {retries} attempts") return None ``` ### Parallel Processing Process multiple items concurrently: ```python theme={null} async def process_urls_parallel(urls, max_concurrent=5): """Process URLs with controlled concurrency""" semaphore = asyncio.Semaphore(max_concurrent) async def process_with_semaphore(url): async with semaphore: return await extract_text_from_webpage(url) tasks = [process_with_semaphore(url) for url in urls] results = await asyncio.gather(*tasks, return_exceptions=True) # Handle results and errors successful = [] failed = [] for url, result in zip(urls, results): if isinstance(result, Exception): failed.append((url, str(result))) else: successful.append((url, result)) return successful, failed ``` ## Integration Examples ### With FastAPI ```python theme={null} from fastapi import FastAPI, UploadFile from lye.image import analyze_image from lye.audio import transcribe app = FastAPI() @app.post("/analyze-image") async def analyze_uploaded_image(file: UploadFile): contents = await file.read() analysis = await analyze_image(contents) return {"filename": file.filename, "analysis": analysis} @app.post("/transcribe-audio") async def transcribe_uploaded_audio(file: UploadFile): contents = await file.read() transcript = await transcribe(contents) return {"filename": file.filename, "transcript": transcript} ``` ### With Django ```python theme={null} # views.py from django.http import JsonResponse from django.views import View from lye.web import search import asyncio class SearchView(View): def get(self, request): query = request.GET.get('q', '') if not query: return JsonResponse({'error': 'No query provided'}, status=400) # Run async function in sync context loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) results = loop.run_until_complete(search(query)) return JsonResponse({'query': query, 'results': results}) ``` ## Performance tips 1. **Use asyncio.gather() for parallel operations** ```python theme={null} results = await asyncio.gather( search("topic 1"), search("topic 2"), search("topic 3") ) ``` 2. **Implement caching for expensive operations** ```python theme={null} from functools import lru_cache @lru_cache(maxsize=100) async def cached_search(query): return await search(query) ``` 3. **Stream large files** ```python theme={null} async def process_large_file(filepath): # Process in chunks instead of loading entire file chunk_size = 1024 * 1024 # 1MB with open(filepath, 'rb') as f: while chunk := f.read(chunk_size): # Process chunk pass ``` ## Next steps Complete API documentation Use Lye tools with agents Learn how to add custom tools More Lye examples # Using Narrator Source: https://slide.mintlify.app/standalone-packages/using-narrator Add conversation persistence to any AI application with Narrator Narrator is Slide's conversation persistence and storage system. While it integrates seamlessly with Tyler agents, you can also use Narrator independently for any application that needs to manage conversations and files. ## Why Use Narrator Standalone? * Add conversation history to existing AI applications * Manage chat threads across multiple sessions * Store and retrieve file attachments * Switch between storage backends easily * No dependency on Tyler or other Slide packages ## Quick Start ### Installation ```bash theme={null} # Using uv (recommended) uv add slide-narrator # Using pip (fallback) pip install slide-narrator ``` ### Basic usage ```python theme={null} import asyncio from narrator import Thread, Message, ThreadStore async def main(): # Create a thread store (in-memory by default) store = await ThreadStore.create() # Create a conversation thread thread = Thread(id="chat-001") # Add messages thread.add_message(Message(role="user", content="Hello!")) thread.add_message(Message(role="assistant", content="Hi there! How can I help?")) # Save the thread await store.save_thread(thread) # Load it later loaded_thread = await store.get_thread("chat-001") print(f"Messages in thread: {len(loaded_thread.messages)}") asyncio.run(main()) ``` ## Storage Backends ### In-Memory Storage Perfect for testing and temporary conversations: ```python theme={null} # Default - no URL needed store = await ThreadStore.create() # Explicit store = await ThreadStore.create("memory://") ``` ### SQLite Storage Great for local applications and development: ```python theme={null} # SQLite with async support store = await ThreadStore.create("sqlite+aiosqlite:///conversations.db") # Custom options store = await ThreadStore.create( "sqlite+aiosqlite:///app.db", pool_size=5, max_overflow=10 ) ``` ### PostgreSQL Storage For production applications with multiple users: ```python theme={null} # PostgreSQL with asyncpg store = await ThreadStore.create( "postgresql+asyncpg://user:pass@localhost/dbname" ) # With connection pool settings store = await ThreadStore.create( "postgresql+asyncpg://localhost/chat_app", pool_size=20, max_overflow=30, pool_timeout=30 ) ``` ## Thread Management ### Creating Threads ```python theme={null} from narrator import Thread, Message import uuid # Auto-generated ID thread = Thread() print(f"Thread ID: {thread.id}") # Custom ID thread = Thread(id="user-123-support-chat") # With metadata thread = Thread( id=f"session-{uuid.uuid4()}", metadata={ "user_id": "user-123", "channel": "web", "tags": ["support", "billing"], "created_at": datetime.now().isoformat() } ) ``` ### Managing Messages ```python theme={null} # Add messages thread.add_message(Message( role="user", content="What's the weather like?", metadata={"timestamp": datetime.now().isoformat()} )) thread.add_message(Message( role="assistant", content="I'll help you check the weather. What's your location?" )) # Access messages for msg in thread.messages: print(f"{msg.role}: {msg.content}") # Get last message last_message = thread.messages[-1] if thread.messages else None # Filter messages user_messages = [m for m in thread.messages if m.role == "user"] ``` ### Thread Operations ```python theme={null} # Save thread await store.save_thread(thread) # Load thread thread = await store.get_thread("thread-id") # List all threads threads = await store.list_threads() for t in threads: print(f"Thread {t.id}: {len(t.messages)} messages") # Delete thread await store.delete_thread("thread-id") # Search threads by metadata user_threads = await store.list_threads( filters={"metadata.user_id": "user-123"} ) ``` ## File Storage ### Setting Up File Storage ```python theme={null} from narrator import FileStore # Local file storage file_store = await FileStore.create(base_path="./uploads") # S3-compatible storage (requires boto3) file_store = await FileStore.create( backend="s3", bucket="my-app-files", region="us-east-1" ) ``` ### Working with Attachments ```python theme={null} from narrator import Attachment # Create attachment from file with open("document.pdf", "rb") as f: attachment = Attachment( filename="document.pdf", content=f.read(), mime_type="application/pdf" ) # Add to message message = Message( role="user", content="Please review this document" ) message.add_attachment(attachment) # Save with file store await file_store.save_attachment(thread.id, attachment) # Retrieve attachments attachments = await file_store.get_attachments(thread.id) for att in attachments: print(f"File: {att.filename} ({att.mime_type})") ``` ## Integration Examples ### With OpenAI ```python theme={null} import openai from narrator import Thread, Message, ThreadStore async def chat_with_persistence(): store = await ThreadStore.create("sqlite+aiosqlite:///chat.db") thread_id = "user-session-123" # Load or create thread try: thread = await store.get_thread(thread_id) except: thread = Thread(id=thread_id) # Get user input user_input = input("You: ") thread.add_message(Message(role="user", content=user_input)) # Prepare messages for OpenAI messages = [ {"role": msg.role, "content": msg.content} for msg in thread.messages ] # Call OpenAI response = await openai.ChatCompletion.acreate( model="gpt-4", messages=messages ) # Add response to thread assistant_message = Message( role="assistant", content=response.choices[0].message.content ) thread.add_message(assistant_message) # Save thread await store.save_thread(thread) print(f"Assistant: {assistant_message.content}") ``` ### With LangChain ```python theme={null} from langchain.memory import ConversationSummaryBufferMemory from langchain.schema import BaseMessage, HumanMessage, AIMessage from narrator import ThreadStore, Thread, Message class NarratorMemory(ConversationSummaryBufferMemory): def __init__(self, thread_store: ThreadStore, thread_id: str): super().__init__() self.thread_store = thread_store self.thread_id = thread_id async def load_memory(self): try: thread = await self.thread_store.get_thread(self.thread_id) for msg in thread.messages: if msg.role == "user": self.chat_memory.add_user_message(msg.content) elif msg.role == "assistant": self.chat_memory.add_ai_message(msg.content) except: pass async def save_context(self, inputs: dict, outputs: dict): thread = Thread(id=self.thread_id) # Add messages from current context for msg in self.chat_memory.messages: if isinstance(msg, HumanMessage): thread.add_message(Message(role="user", content=msg.content)) elif isinstance(msg, AIMessage): thread.add_message(Message(role="assistant", content=msg.content)) await self.thread_store.save_thread(thread) ``` ### With Custom AI Systems ```python theme={null} from narrator import Thread, Message, ThreadStore from typing import List, Dict class ChatBot: def __init__(self, thread_store: ThreadStore): self.thread_store = thread_store self.active_sessions = {} async def start_session(self, user_id: str) -> str: """Start or resume a chat session""" thread_id = f"user-{user_id}-chat" try: thread = await self.thread_store.get_thread(thread_id) print(f"Resuming session with {len(thread.messages)} messages") except: thread = Thread( id=thread_id, metadata={"user_id": user_id, "started": datetime.now().isoformat()} ) print("Starting new session") self.active_sessions[user_id] = thread return thread_id async def process_message(self, user_id: str, content: str) -> str: """Process a user message and generate response""" thread = self.active_sessions.get(user_id) if not thread: await self.start_session(user_id) thread = self.active_sessions[user_id] # Add user message thread.add_message(Message( role="user", content=content, metadata={"timestamp": datetime.now().isoformat()} )) # Generate response (your AI logic here) response = await self.generate_response(thread.messages) # Add assistant message thread.add_message(Message( role="assistant", content=response, metadata={"timestamp": datetime.now().isoformat()} )) # Save thread await self.thread_store.save_thread(thread) return response async def generate_response(self, messages: List[Message]) -> str: # Your AI logic here return "This is where your AI generates a response" ``` ## Advanced patterns ### Thread Archival ```python theme={null} async def archive_old_threads(store: ThreadStore, days: int = 30): """Archive threads older than specified days""" cutoff = datetime.now() - timedelta(days=days) threads = await store.list_threads() archived_count = 0 for thread in threads: # Check last message time if thread.messages: last_msg_time = thread.messages[-1].metadata.get("timestamp", "") if last_msg_time and datetime.fromisoformat(last_msg_time) < cutoff: # Mark as archived thread.metadata["archived"] = True thread.metadata["archived_at"] = datetime.now().isoformat() await store.save_thread(thread) archived_count += 1 print(f"Archived {archived_count} threads") ``` ### Message Search ```python theme={null} async def search_messages( store: ThreadStore, query: str, user_id: str = None ) -> List[Dict]: """Search messages across threads""" results = [] # Get relevant threads if user_id: threads = await store.list_threads( filters={"metadata.user_id": user_id} ) else: threads = await store.list_threads() # Search messages for thread in threads: for msg in thread.messages: if query.lower() in msg.content.lower(): results.append({ "thread_id": thread.id, "message": msg, "context": thread.metadata }) return results ``` ### Export/Import ```python theme={null} import json async def export_thread(thread: Thread, file_path: str): """Export thread to JSON file""" data = { "id": thread.id, "metadata": thread.metadata, "messages": [ { "role": msg.role, "content": msg.content, "metadata": msg.metadata, "attachments": [ { "filename": att.filename, "mime_type": att.mime_type } for att in msg.attachments ] } for msg in thread.messages ] } with open(file_path, "w") as f: json.dump(data, f, indent=2) async def import_thread(store: ThreadStore, file_path: str) -> Thread: """Import thread from JSON file""" with open(file_path, "r") as f: data = json.load(f) thread = Thread(id=data["id"], metadata=data["metadata"]) for msg_data in data["messages"]: message = Message( role=msg_data["role"], content=msg_data["content"], metadata=msg_data.get("metadata", {}) ) thread.add_message(message) await store.save_thread(thread) return thread ``` ## Performance tips ### 1. Connection Pooling ```python theme={null} # Configure connection pool for PostgreSQL store = await ThreadStore.create( "postgresql+asyncpg://localhost/chat", pool_size=20, # Number of connections max_overflow=10, # Additional connections when needed pool_timeout=30, # Timeout for getting connection pool_recycle=3600 # Recycle connections after 1 hour ) ``` ### 2. Batch Operations ```python theme={null} # Save multiple threads efficiently threads = [thread1, thread2, thread3] await asyncio.gather(*[ store.save_thread(thread) for thread in threads ]) ``` ### 3. Lazy Loading ```python theme={null} # Load only thread metadata first thread_list = await store.list_threads(load_messages=False) # Load full thread only when needed full_thread = await store.get_thread(thread_id) ``` ## Migration Guide ### From File-based Storage ```python theme={null} async def migrate_from_files(file_dir: str, store: ThreadStore): """Migrate from file-based storage to database""" import os import json for filename in os.listdir(file_dir): if filename.endswith(".json"): with open(os.path.join(file_dir, filename), "r") as f: data = json.load(f) thread = Thread(id=data["id"]) for msg in data["messages"]: thread.add_message(Message( role=msg["role"], content=msg["content"] )) await store.save_thread(thread) print(f"Migrated thread {thread.id}") ``` ## Next steps Advanced persistence patterns Complete API documentation Use with Tyler agents See more examples