# Development Source: https://docs.laragent.ai/guides/development Contribute to LarAgent development # Contributing to LarAgent We welcome contributions to LarAgent! Whether it's improving documentation, fixing bugs, or adding new features, your help is appreciated. This guide will walk you through the process of setting up your development environment and submitting contributions. Need help in contributing? Join our [Discord community](https://discord.gg/NAczq2T9F8), send "@Maintainer 🛠️ onboard me" in any channel and we will help you to get started. ## Development Setup Follow these steps to set up your local development environment: 1. **Fork the repository** on GitHub 2. **Clone your fork**: ```bash theme={null} git clone https://github.com/YOUR_USERNAME/LarAgent.git cd LarAgent ``` 3. **Install dependencies**: ```bash theme={null} composer install ``` 4. **Create a new branch** for your feature or bugfix: ```bash theme={null} git checkout -b feature/your-feature-name ``` ## Coding Guidelines When contributing to LarAgent, please follow these guidelines to ensure your code meets our standards: ### Code Style * Use type hints and return types where possible * Add PHPDoc blocks for classes and methods * Keep methods focused and concise * Follow PSR-12 coding standards LarAgent uses PHP CS Fixer to maintain code style. You can run it with: ```bash theme={null} composer format ``` ### Testing All new features and bug fixes should include tests. LarAgent uses [PEST](https://pestphp.com/) for testing. * Add tests for new features * Ensure all tests pass before submitting: ```bash theme={null} composer test ``` * Maintain or improve code coverage ### Documentation Good documentation is crucial for any project: * Add PHPDoc blocks for new classes and methods * Include examples for new features * Consider updating the [official documentation](https://github.com/MaestroError/docs) for major changes ### Commit Guidelines * Use clear, descriptive commit messages * Reference issues and pull requests in your commits * Keep commits focused and atomic ## Pull Request Process Follow these steps to submit your contributions: 1. **Update your fork** with the latest changes from main: ```bash theme={null} git remote add upstream https://github.com/MaestroError/LarAgent.git git fetch upstream git rebase upstream/main ``` 2. **Push your changes** to your fork: ```bash theme={null} git push origin feature/your-feature-name ``` 3. **Create a Pull Request** with: * Clear title and description * List of changes and impact * Any breaking changes highlighted * Screenshots/examples if relevant The maintainers aim to review all pull requests within 2 weeks. ## Getting Help If you need assistance while contributing: * Open an issue for bugs or feature requests * Join discussions in existing issues * Join our [Discord community](https://discord.gg/NAczq2T9F8) * Reach out to maintainers for guidance ## Security Vulnerabilities If you discover a security vulnerability, please review [our security policy](https://github.com/MaestroError/LarAgent/security/policy) on how to report it properly. ## Running Tests You can run the test suite with: ```bash theme={null} composer test ``` For more specific tests: ```bash theme={null} # Run a specific test file ./vendor/bin/pest tests/YourTestFile.php # Run with coverage report composer test-coverage ``` Thank you for contributing to LarAgent! Your efforts help make the package better for everyone. # Guides Introduction Source: https://docs.laragent.ai/guides/introduction Comprehensive guides to help you build powerful AI agents with LarAgent. From basic implementations to advanced patterns and real-world use cases. Welcome to the LarAgent Guides! These step-by-step tutorials will help you master LarAgent's capabilities and build production-ready AI agents for your Laravel applications. ## What You'll Find Here The Guides section provides practical, hands-on tutorials that complement the core documentation. While the [Core Concepts](/core-concepts/agents) explain *what* LarAgent can do, these guides show you *how* to implement real-world solutions. Build intelligent document search and retrieval systems More guides are being prepared ## Guide Categories ### 🔍 Retrieval-Augmented Generation (RAG) Learn how to enhance your AI agents with external knowledge sources: * **[Vector-Based RAG](/guides/rag/vector-based)** - Implement semantic search using vector embeddings for document retrieval ### 🚀 More Guides Coming Soon We're actively working on additional guides covering: * **Multi-Agent Systems** - Orchestrating multiple agents for complex workflows * **Custom Tool Development** - Building specialized tools for your domain * **Production Deployment** - Best practices for scaling LarAgent in production * **Integration Patterns** - Common patterns for integrating with existing Laravel applications * **Performance Optimization** - Tips for optimizing agent response times and resource usage ## How to Use These Guides Each guide follows a consistent structure to help you learn effectively: What you need to know or have installed before starting Detailed instructions with code examples How to verify your implementation works correctly Suggestions for extending or improving the implementation ## Before You Begin Make sure you've completed the [Quickstart](/quickstart) tutorial and have a basic understanding of: * [Agents](/core-concepts/agents) - The foundation of LarAgent * [Tools](/core-concepts/tools) - How agents interact with external systems * [Chat History](/core-concepts/chat-history) - Managing conversation context ## Getting Help If you encounter issues while following these guides: Report bugs or request new guides Get help from the community ## Contributing to Guides Found an error or want to suggest improvements? We welcome contributions to make these guides better for everyone. You can: * Submit issues for unclear instructions * Propose new guide topics * Share your own implementation examples *** Ready to dive in? Start with [Vector-Based RAG](/guides/rag/vector-based) to learn how to build knowledge-enhanced AI agents, or explore the [Core Concepts](/core-concepts/agents) if you need to review the fundamentals. # Retrieval-as-Tool RAG Source: https://docs.laragent.ai/guides/rag/retrieval-as-tool Learn how to implement advanced RAG by giving your agent tools to retrieve information on-demand from both structured databases and document collections. This guide is designed for **educational purposes** to help you understand RAG concepts and how they work in LarAgent and AI development. The prompts, configurations, and implementations provided here are **not fine-tuned or extensively tested** for production use. Use this guide to learn and experiment, then build upon it with production-grade practices. Retrieval-as-Tool is an advanced RAG approach where the AI agent **decides when and what to retrieve** based on the conversation context. Unlike traditional RAG that retrieves context for every query, this method gives the agent tools to fetch information only when needed, making it more efficient and context-aware. In this guide, we'll implement a **smart support agent** with two retrieval tools: 1. **SQL Query Tool** - For retrieving structured data from databases (users, orders, settings, etc.) 2. **Document Search Tool** - For searching unstructured documentation using vector embeddings The agent will intelligently choose which tool (or both) to use based on the user's question. ## How Retrieval-as-Tool Works ```mermaid theme={null} graph TD A[User Query] --> B[Agent Analyzes Query] B --> C{Needs Data?} C -->|Yes - Structured Data| D[Call SQL Query Tool] C -->|Yes - Documentation| E[Call Document Search Tool] C -->|Yes - Both| F[Call Both Tools] C -->|No| G[Direct Response] D --> H[Execute with Guardrails] E --> I[Vector Search] H --> J[Return Results to Agent] I --> J F --> J J --> K[Agent Generates Response] G --> K K --> L[User Receives Answer] ``` The key difference: * **Traditional RAG**: Retrieves context for *every* query in the `prompt` method * **Retrieval-as-Tool**: Agent *decides* when to retrieve and what data source to query ## Understanding the SQL Query Approach The SQL query tool in this guide is designed for **structured data retrieval** (database records like users, orders, products) and **NOT for document retrieval**. For unstructured documents and FAQs, we'll use the Document Search tool with vector embeddings. This dual-tool approach allows your agent to: * Query database tables for precise, structured information * Search documentation for conceptual knowledge and procedures * Combine both when needed (e.g., "Show me user John's order history and the return policy") ## Prerequisites Before starting this guide, make sure you have: You should have LarAgent installed and configured. If not, check the [Quickstart](/quickstart) guide. ```bash theme={null} composer require maestroerror/laragent ``` Ensure your Laravel application has database access configured. We'll be using `DB::select()` for safe, read-only queries. For the document search tool, you'll need a vector database: * **[Qdrant](https://github.com/hkulekci/qdrant-php)** * **[Pinecone](https://github.com/probots-io/pinecone-php)** * **[pgvector](https://github.com/pgvector/pgvector)** We recommend using `openai-php/client` since LarAgent already provides it. ```bash theme={null} # Usually already installed with LarAgent composer require openai-php/client ``` Make sure you have some data in your database and documents indexed in your vector database for testing. ## Implementation Steps ### Step 1: Create Your Support Agent Create a new agent using the artisan command: ```bash theme={null} php artisan make:agent SmartSupportAgent ``` This will generate a new agent class at `app/AiAgents/SmartSupportAgent.php`. ### Step 2: Define Agent Instructions Create a blade template for your agent's instructions: ```blade resources/views/prompts/smart_support_instructions.blade.php theme={null} # Purpose You are an intelligent customer support agent with access to both database and documentation. Your role is to assist users by: - Retrieving relevant information from the database when needed (user data, orders, settings) - Searching documentation for policies, procedures, and how-to guides - Combining information from multiple sources when necessary **Important Guidelines:** - Use the `queryDatabase` tool for structured data queries (users, orders, products, etc.) - Use the `searchDocumentation` tool for unstructured information (FAQs, guides, policies) - Only retrieve data when necessary to answer the question - Be helpful, accurate, and professional - Never make up information - only use data from tools or general knowledge Current Date: {{ $date }} ## Current User Context Name: {{ $user->name ?? 'Guest' }} Email: {{ $user->email ?? 'N/A' }} User ID: {{ $user->id ?? 'N/A' }} ## Database Schema You have access to the following database tables for queries: ### users - id (int, primary key) - name (string) - email (string) - email_verified_at (timestamp) - subscription_type (string: 'free', 'basic', 'premium') - created_at (timestamp) - updated_at (timestamp) ### orders - id (int, primary key) - user_id (int, foreign key to users.id) - total (decimal) - status (string: 'pending', 'completed', 'cancelled', 'refunded') - created_at (timestamp) - updated_at (timestamp) ### products - id (int, primary key) - name (string) - description (text) - price (decimal) - category (string) - stock (int) - is_active (boolean) - created_at (timestamp) - updated_at (timestamp) ### order_items - id (int, primary key) - order_id (int, foreign key to orders.id) - product_id (int, foreign key to products.id) - quantity (int) - price (decimal) - created_at (timestamp) - updated_at (timestamp) **Schema Notes:** - Always use proper JOIN statements when querying related tables - Use appropriate WHERE clauses to filter results - Include LIMIT clauses to prevent large result sets - Remember to respect user privacy and only query data relevant to the question ``` ### Step 3: Create the SQL Query Guardrail Agent Before implementing the main agent, create a guardrail agent that validates SQL queries for safety: ```bash theme={null} php artisan make:agent SqlGuardAgent ``` Update the `SqlGuardAgent.php`: ````php app/AiAgents/SqlGuardAgent.php theme={null} 'sql_validation', 'schema' => [ 'type' => 'object', 'properties' => [ 'is_safe' => [ 'type' => 'boolean', 'description' => 'Whether the query is safe (read-only)', ], 'reason' => [ 'type' => 'string', 'description' => 'Explanation of why the query is safe or unsafe', ], 'detected_operations' => [ 'type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of SQL operations detected in the query', ], ], 'required' => ['is_safe', 'reason', 'detected_operations'], 'additionalProperties' => false, ], 'strict' => true, ]; public function instructions() { return view('prompts.detailed_SQL_checking_instruction'); } /** * Validate a SQL query for safety * * @param string $query The SQL query to validate * @return array Validation result with is_safe, reason, and detected_operations */ public function validateQuery(string $query): array { $response = $this->respond("Validate this SQL query:\n\n```sql\n{$query}\n```"); return $response; } } ```` ### Step 4: Implement SQL Query Tool in Your Agent Now, let's implement the SQL query tool using the `#[Tool]` attribute: ```php app/AiAgents/SmartSupportAgent.php theme={null} now()->format('F j, Y'), 'user' => auth()->user(), ])->render(); } /** * Query the database for structured data * * This tool allows retrieval of structured information like users, orders, products, etc. * The query must be read-only (SELECT statements only). */ #[Tool( 'Query the database to retrieve structured data like users, orders, products, or settings. Only use for structured data queries.', [ 'query' => 'A read-only SQL SELECT query. Must not contain INSERT, UPDATE, DELETE, or other modification statements.', ] )] public function queryDatabase(string $query): string { try { // Validate query using guardrail agent $validation = SqlGuardAgent::for("check")->validateQuery($query); // Check if query is safe if (!$validation['is_safe']) { return "Query rejected: {$validation['reason']}\n" . "Detected operations: " . implode(', ', $validation['detected_operations']) . "\n" . "Only SELECT queries are allowed for data retrieval."; } // Execute the query $results = DB::select($query); // Format results if (empty($results)) { return "Query executed successfully but returned no results."; } // Convert to array and return as JSON for better parsing $formattedResults = json_encode($results, JSON_PRETTY_PRINT); return "Query executed successfully. Results:\n{$formattedResults}"; } catch (\Exception $e) { return "Error executing query: " . $e->getMessage() . "\nPlease check your SQL syntax and try again."; } } } ``` The `queryDatabase` tool validates every SQL query through the `SqlGuardAgent` before execution, ensuring only safe SELECT statements are processed. This prevents any data modification attempts. ### Step 5: Add Document Search Tool with Enum Constraints Now let's add the document search tool. First, create an Enum to constrain the limit parameter: ```php app/Enums/DocumentLimit.php theme={null} Using an Enum for the `limit` parameter **frames the LLM's ability to choose** by providing a predefined set of valid options. Instead of allowing any integer (which could lead to values like 1, 100, or even negative numbers), the LLM can only select from the three specific cases: 3, 4, or 5. This ensures more predictable behavior and prevents edge cases while still giving the agent flexibility to adjust the number of retrieved documents based on the query complexity. Now add the document search tool to your agent: ```php theme={null} use App\Enums\DocumentLimit; use App\Services\QdrantSearchService; // Add this method to your SmartSupportAgent class /** * Search documentation for unstructured information * * This tool searches through documentation, FAQs, guides, and policies * using semantic vector search. */ #[Tool( 'Search the documentation for unstructured information like FAQs, guides, policies, and procedures. Use this for conceptual questions or how-to queries.', [ 'query' => 'The search query or question to find relevant documentation for.', 'limit' => 'Number of documents to retrieve. Choose based on query complexity.', ] )] public function searchDocumentation(string $query, DocumentLimit $limit = DocumentLimit::THREE): string { try { // Search using Qdrant $searchService = new QdrantSearchService(); $documents = $searchService->search($query, $limit->value); // Check if we found any relevant documents if (empty($documents)) { return "No relevant documentation found for the query: {$query}"; } // Format and return results directly return json_encode($documents, JSON_PRETTY_PRINT); } catch (\Exception $e) { return "Error searching documentation: " . $e->getMessage(); } } ``` The simplified implementation returns raw JSON results, allowing the agent to interpret and present the information in the most appropriate way based on the conversation context. The Enum constraint ensures the agent can only request 3, 4, or 5 documents, preventing excessive retrieval while maintaining flexibility. #### Understanding Enum Benefits The `DocumentLimit` Enum provides several advantages: Prevents invalid values at the language level LLM sees exactly what choices are available Removes need for min/max boundary checks Agent understands these are the only valid options When the LLM receives the tool definition, it sees: ```json theme={null} { "limit": { "type": "integer", "enum": [3, 4, 5], "description": "Number of documents to retrieve. Choose based on query complexity." } } ``` This constraint guides the agent to make appropriate choices: * Simple questions → 3 documents (default) * Moderate complexity → 4 documents * Complex or multi-faceted queries → 5 documents ## Testing Your Implementation ### Interactive Testing Test your agent using the built-in chat command: ```bash theme={null} php artisan agent:chat SmartSupportAgent ``` Try different types of questions to test both tools: ```text Database Queries theme={null} "Show me details for user with email john@example.com" "What are my recent orders?" "List all active products in the electronics category" "How many users registered this month?" ``` ```text Documentation Queries theme={null} "What is the return policy?" "How do I reset my password?" "What are the available pricing plans?" "How does the referral program work?" ``` ```text Combined Queries theme={null} "Show me user Sarah's order history and explain the return policy" "What are the requirements for premium features and how many premium users do we have?" ``` ```text Expected Behavior theme={null} ✅ Agent decides which tool(s) to use ✅ SQL queries are validated by guardrail agent ✅ Unsafe queries are rejected ✅ Document search returns relevant results ✅ Agent synthesizes information from multiple sources ``` ### Testing SQL Guardrails The guardrail agent should reject unsafe queries: ```text Safe Queries (Should Pass) theme={null} ✅ "SELECT * FROM users WHERE email = 'test@example.com'" ✅ "SELECT COUNT(*) FROM orders WHERE created_at > '2024-01-01'" ✅ "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id" ``` ```text Unsafe Queries (Should Be Rejected) theme={null} ❌ "DELETE FROM users WHERE id = 1" ❌ "UPDATE users SET role = 'admin' WHERE id = 1" ❌ "DROP TABLE users" ❌ "INSERT INTO users (name) VALUES ('hacker')" ❌ "SELECT * FROM users; DROP TABLE users;" ``` ### Programmatic Testing You can also test programmatically: ```php theme={null} use App\AiAgents\SmartSupportAgent; // Test with authenticated user $response = SmartSupportAgent::forUser(auth()->user()) ->respond('Show me my order history'); str_contains($response, "test_string"); // Test documentation search $response = SmartSupportAgent::for('test_session') ->respond('What is your refund policy?'); str_contains($response, "test_string"); // Test combined retrieval $response = SmartSupportAgent::forUser(auth()->user()) ->respond('Show my account details and explain how to upgrade to premium'); str_contains($response, "test_string"); ``` ### Debugging Tips Add logging to see which tools are being called: ```php theme={null} public function queryDatabase(string $query): string { \Log::info('Database query tool called', ['query' => $query]); // ... rest of implementation } ``` Log guardrail agent decisions: ```php theme={null} $validation = $guardAgent->validateQuery($query); \Log::info('SQL validation result', json_encode($validation)); ``` Set File chat history and Check in `storage/app/private` json file to learn if the agent is choosing the right tools for different question types. Tool calls can increase token consumption significantly. ```php theme={null} protected $contextWindowSize = 8000; // Adjust based on needs ``` ## Advanced: Combining Results The agent will automatically synthesize information from multiple tool calls. For example: **User Question:** "Show me the top 5 customers and explain the loyalty program benefits" **Agent's Process:** 1. Calls `queryDatabase` with: `SELECT name, email, total_purchases FROM customers ORDER BY total_purchases DESC LIMIT 5` 2. Calls `searchDocumentation` with: "loyalty program benefits" 3. Combines results into a comprehensive answer ## Next Steps Extend with API calls, external services, or specialized data sources Cache frequent queries to improve performance Add table-level permissions and query complexity limits Track tool usage patterns and optimize performance ### Comparing RAG Approaches **Vector-Based RAG (Traditional):** * ✅ Simpler to implement * ✅ Consistent context injection * ❌ Retrieves for every query (less efficient) * ❌ No selective retrieval **Retrieval-as-Tool:** * ✅ Agent decides when to retrieve * ✅ More efficient (only retrieves when needed) * ✅ Can combine multiple data sources * ❌ More complex implementation * ❌ Requires careful tool design **Use Vector-Based RAG when:** * Every query needs context from documentation * Building a simple FAQ bot * Working with a single knowledge source **Use Retrieval-as-Tool when:** * Queries vary significantly in data needs * Multiple data sources (DB + docs + APIs) * Need fine-grained control over retrieval * Building complex conversational agents *** For more information about RAG fundamentals in LarAgent, check the [RAG Core Concept](/core-concepts/rag) and [Vector-Based RAG guide](/guides/rag/vector-based). ## Summary You've now implemented a sophisticated Retrieval-as-Tool RAG system with LarAgent! Your agent can: * ✅ **Intelligently decide** when to retrieve information * ✅ **Query databases** safely with SQL guardrails * ✅ **Search documentation** using vector embeddings * ✅ **Combine multiple sources** for comprehensive answers * ✅ **Validate and secure** all data access This approach provides maximum flexibility and efficiency, allowing your agent to handle both structured data queries and unstructured document searches while maintaining security through intelligent guardrails. ### Key Takeaways 1. **Tools as Retrieval Methods**: Using `#[Tool]` attribute makes retrieval explicit and controllable 2. **Dual Data Sources**: Structured (SQL) and unstructured (vectors) data serve different purposes 3. **Security First**: Guardrail agents validate operations before execution 4. **Smart Decisions**: The agent chooses when and what to retrieve based on context 5. **Extensibility**: Easy to add more tools for APIs, external services, or specialized sources # Vector-Based RAG Source: https://docs.laragent.ai/guides/rag/vector-based Learn how to implement traditional RAG using vector embeddings to build a knowledge-enhanced customer support agent. This guide is designed for **educational purposes** to help you understand RAG concepts and how they work in LarAgent and AI development. The prompts, configurations, and implementations provided here are **not fine-tuned or extensively tested** for production use. Use this guide to learn and experiment, then build upon it with production-grade practices. Vector-based RAG (Retrieval-Augmented Generation), often referred to as Traditional RAG, is a powerful technique that stores text documents as vectors: the same mathematical representation that LLMs use for understanding words and concepts - and compares user queries against these vectors to retrieve relevant context. In this guide, we'll implement a **customer support agent** that answers questions based on FAQ documents stored in a vector database. The agent will intelligently retrieve relevant documentation and use it to provide accurate, context-aware responses. ## How Vector-Based RAG Works ```mermaid theme={null} graph LR A[User Query] --> B[Generate Query Embedding] B --> C[Search Vector DB] C --> D[Retrieve Similar Documents] D --> E[Add Context to Prompt] E --> F[LLM Generates Response] F --> G[User Receives Answer] ``` The process flow: 1. User asks a question 2. The question is converted into a vector embedding 3. The vector database finds documents with similar embeddings 4. Retrieved documents are added as context 5. The LLM generates a response based on the context 6. User receives an accurate, documentation-based answer ## Prerequisites Before starting this guide, make sure you have: You should have LarAgent installed and configured. If not, check the [Quickstart](/quickstart) guide. ```bash theme={null} composer require maestroerror/laragent ``` You can choose any vector database solution, but if you're not sure what to pick, these three are recommended: * **[Qdrant](https://github.com/hkulekci/qdrant-php)** * **[Pinecone](https://github.com/probots-io/pinecone-php)** * **[pgvector](https://github.com/pgvector/pgvector)** * If your project uses PostgreSQL, the pgvector extension is also a good option We recommend using `openai-php/client` since LarAgent already provides it as a dependency, so you won't need to install anything extra. However, you can use any embeddings generator, including open-source models running locally. Just make sure you use the same generator for user queries as you use for generating the documents vector representation Make sure you have your vector database running and accessible, and that you have API keys configured for your chosen embeddings provider. ## Implementation Steps ### Step 1: Create Your Agent First, create a new agent using the artisan command: ```bash theme={null} php artisan make:agent SupportAgent ``` This will generate a new agent class at `app/AiAgents/SupportAgent.php`. ### Step 2: Define Instructions with Blade Template Create a blade template for your agent's instructions. This makes it easy to maintain and allows for dynamic content. Create a new file at `resources/views/prompts/support_agent_instructions.blade.php`: ```blade resources/views/prompts/support_agent_instructions.blade.php theme={null} # Purpose You are a customer support agent of SaaS platform. Your role is to assist users with their questions and issues related to the platform. Answer questions based on the following Context provided to you. If the Context does not contain information relevant to the user's question, respond with: "I'm sorry, I don't have that information right now. Please contact our support team at support@helloworld.com for further assistance." **Important Guidelines:** - Only answer based on the provided Context - Be helpful, friendly, and professional - If you're unsure, avoid guessing, ask to contact the support team - Provide clear, concise answers Current Date: {{ $date }} ## Current User Name: {{ $user->name ?? 'Valued Customer' }} Email: {{ $user->email ?? 'N/A' }} Account Type: {{ $user->subscription_type ?? 'Free' }} ``` Now, update your `SupportAgent.php` to use this template: ```php app/AiAgents/SupportAgent.php theme={null} now()->format('F j, Y'), 'user' => auth()->user(), ])->render(); } public function prompt($message) { // We'll add RAG logic here in the next steps return $message; } } ``` ### Step 3: Create a Search Service Create a service to handle vector search operations. We'll use `QdrantSearchService` as an example which has following API: ```php app/Services/QdrantSearchService.php theme={null} namespace App\Services; class QdrantSearchService { public function convertToEmbeddings(string $text): array; public function search(string $query, int $limit = 10): array; public function importDocumentToCollection(array $document, string $collectionName): bool; } ``` For vector databases (including Pinecone or Qdrant), the search logic will be similar but with different client implementations: The key is to generate embeddings on each new document added and perform similarity search or them when agent gets a question. ### Step 4: Configure Environment Variables Add your vector database and OpenAI credentials to `.env`: ```env .env theme={null} OPENAI_API_KEY=your_openai_api_key_here # Qdrant Configuration QDRANT_HOST=http://localhost:6333 QDRANT_API_KEY=your_qdrant_api_key ``` ### Step 5: Implement RAG in the Prompt Method Now, integrate the search service into your agent's `prompt` method. Create a context template first: ```blade resources/views/prompts/support_agent_context.blade.php theme={null} # Context The following documents from our knowledge base may be relevant to the user's question: @foreach($documents as $index => $doc) ## Document {{ $index + 1 }}: {{ $doc['title'] }} {{ $doc['content'] }} --- @endforeach Use this context to answer the user's question accurately. If the context doesn't contain relevant information, let the user know. ``` Update your `SupportAgent.php` to use RAG: ```php app/AiAgents/SupportAgent.php theme={null} now()->format('F j, Y'), 'user' => auth()->user(), ])->render(); } public function prompt($message) { // Search for relevant documents $searchService = new QdrantSearchService(); $documents = $searchService->search($message, limit: 3); // Only add context if we found relevant documents if (!empty($documents)) { // Format the context using blade template $context = view('prompts.support_agent_context', [ 'documents' => $documents, ])->render(); // Add context as a developer message $devMsg = new DeveloperMessage($context); $this->chatHistory()->addMessage($devMsg); } return $message; } } ``` The `DeveloperMessage` role is perfect for RAG context because it can be inserted at any point in the chat history sequence without disrupting the conversation flow between user and assistant messages. ## Testing Your RAG Implementation ### Interactive Testing Test your agent using the built-in chat command: ```bash theme={null} php artisan agent:chat SupportAgent ``` Try asking questions from your documentation: ```text Example Questions theme={null} "How do I create a new user?" "Who has invented the lightbulb?" "What are the pricing plans available?" "What's the difference between Basic and Pro plans?" ``` ```text Expected Behavior theme={null} ✅ Agent retrieves relevant FAQ documents ✅ Provides accurate answers based on context ✅ Admits when information is not available ✅ Maintains conversation context across messages ``` ### Programmatic Testing You can also test programmatically in your application: ```php theme={null} use App\AiAgents\SupportAgent; // For authenticated users $response = SupportAgent::forUser(auth()->user()) ->respond('How do I create a new email campaign?'); str_contains($response, "test_string"); // For named sessions $response = SupportAgent::for('test_session') ->respond('What are the pricing plans?'); str_contains($response, "test_string"); ``` ### Debugging Tips Add logging to see what documents are being retrieved: `php $documents = $searchService->search($message, limit: 3); \Log::info('Retrieved documents:', $documents); ` Ensure your embeddings are being generated correctly and match the dimensions expected by your vector database. Keep an eye on token consumption, especially when adding multiple documents as context. `protected $contextWindowSize = 4000; // Adjust based on your needs ` ## Next Steps Implement safeguards to prevent hallucination and keep conversations on-topic Learn about advanced RAG techniques like retrieval-as-tool or hybrid search Fine-tune your vector search parameters and caching strategies Track answer quality and user satisfaction metrics ### Adding Guardrails To prevent hallucinations and off-topic questions, consider: 1. **Score Thresholding**: Only use documents with similarity scores above a threshold: ```php theme={null} $documents = array_filter($documents, function($doc) { return $doc['score'] > 0.7; // Adjust threshold as needed }); ``` 2. **Explicit Instructions**: Make your system prompt very clear about staying on topic: ```blade theme={null} If the user asks about topics unrelated to HelloWorld or our services, politely redirect them: "I'm specialized in helping with HelloWorld platform questions. For other topics, please visit our general contact page." ``` 3. **Content Filtering**: Pre-filter your document collection to only include appropriate content. ### Exploring Other RAG Approaches Now that you've mastered vector-based RAG, consider exploring: * **Hybrid Search**: Combining vector similarity with keyword search for better accuracy * **Re-ranking**: Using a second model to re-rank retrieved documents * **Retrieval-as-Tool**: Letting the agent decide when to retrieve information * **Multi-modal RAG**: Including images and other media in your knowledge base *** For more information about RAG fundamentals in LarAgent, check the [RAG Core Concept](/core-concepts/rag) documentation. ## Summary You've now implemented a fully functional vector-based RAG system with LarAgent! Your support agent can: * ✅ Retrieve relevant documentation based on user queries * ✅ Provide accurate, context-aware responses * ✅ Maintain conversation history * ✅ Gracefully handle questions without available context This foundation can be extended and customized for various use cases beyond customer support, such as internal knowledge bases, educational assistants, or technical documentation helpers. # Upgrade to v1.0 Source: https://docs.laragent.ai/guides/upgrade-to-v1 Complete migration guide from LarAgent v0.8 to v1.0 with step-by-step instructions This guide provides comprehensive instructions for migrating your LarAgent project from v0.8 to v1.0. Follow the sections in order, starting with the most critical changes that affect the public API. ## Critical Changes These changes **will break your code** if not addressed. Review and update these first before upgrading. ### Message Factory API Changes The `Message` class is now a **pure factory class** with only typed static factory methods. The following methods have been removed: | Removed Method | Status | | ---------------------- | --------- | | `Message::create()` | ❌ Removed | | `Message::fromArray()` | ❌ Removed | | `Message::fromJSON()` | ❌ Removed | Run this command to locate all usages in your codebase: ```bash theme={null} grep -rn "Message::create" --include="*.php" ``` Update your code to use the new typed factory methods: ```php Before (v0.8) theme={null} $message = Message::create('user', 'Hello'); $message = Message::create('assistant', 'Hi there'); $message = Message::create('system', 'You are helpful'); ``` ```php After (v1.0) theme={null} $message = Message::user('Hello'); $message = Message::assistant('Hi there'); $message = Message::system('You are helpful'); ``` Find occurrences: ```bash theme={null} grep -rn "Message::fromArray" --include="*.php" ``` ```php Before (v0.8) theme={null} $message = Message::fromArray(['role' => 'user', 'content' => 'Hello']); ``` ```php After (v1.0) theme={null} // Use the specific message class: $message = UserMessage::fromArray(['role' => 'user', 'content' => 'Hello']); // Or use MessageArray for collections: $messages = MessageArray::fromArray($arrayOfMessages); ``` Find occurrences: ```bash theme={null} grep -rn "Message::fromJSON" --include="*.php" ``` ```php Before (v0.8) theme={null} $message = Message::fromJSON($jsonString); ``` ```php After (v1.0) theme={null} $data = json_decode($jsonString, true); $message = UserMessage::fromArray($data); // or appropriate message class ``` #### Available Factory Methods | Method | Description | | ------------------------------------------------------------------ | -------------------------- | | `Message::user($content, $metadata)` | Create user message | | `Message::assistant($content, $metadata)` | Create assistant message | | `Message::system($content, $metadata)` | Create system message | | `Message::developer($content, $metadata)` | Create developer message | | `Message::toolCall($toolCalls, $metadata)` | Create tool call message | | `Message::toolResult($content, $toolCallId, $toolName, $metadata)` | Create tool result message | *** ### ToolResultMessage Constructor Signature The `ToolResultMessage` constructor signature has changed completely. You must provide `$toolName` for Gemini driver compatibility. The constructor now accepts `$toolName` as an optional third parameter. ```php Constructor v0.8 theme={null} public function __construct(array $message, array $metadata = []) ``` ```php Constructor v1.0 theme={null} public function __construct( ToolResultContent|string $content, string $toolCallId, string $toolName = '', array $metadata = [] ) ``` ```bash theme={null} grep -rn "new ToolResultMessage" --include="*.php" ``` ```php Before (v0.8) theme={null} new ToolResultMessage($resultArray, $metadata); // or new ToolResultMessage(['content' => $result, 'tool_call_id' => $id], $metadata); ``` ```php After (v1.0) theme={null} new ToolResultMessage($content, $toolCallId, $toolName, $metadata); // Or use the factory method (recommended): Message::toolResult($content, $toolCallId, $toolName, $metadata); ``` *** ### ToolCallMessage Constructor Signature The `ToolCallMessage` constructor no longer accepts the `$message` array parameter. ```php Constructor v0.8 theme={null} public function __construct(array $toolCalls, array $message, array $metadata = []) ``` ```php Constructor v1.0 theme={null} public function __construct(ToolCallArray|array $toolCalls, array $metadata = []) ``` ```bash theme={null} grep -rn "new ToolCallMessage" --include="*.php" ``` ```php Before (v0.8) theme={null} new ToolCallMessage($toolCalls, $messageArray, $metadata); // $messageArray was typically the raw API response array ``` ```php After (v1.0) theme={null} new ToolCallMessage($toolCalls, $metadata); // Or use the factory method (recommended): Message::toolCall($toolCalls, $metadata); ``` *** ### ChatHistory Interface Changes If you have custom ChatHistory implementations or directly use ChatHistory methods, this section is critical. #### Key Changes 1. `getMessages()` now returns `MessageArray` instead of `array` 2. Several methods have been removed from the interface 3. New truncation system replaces manual context window management #### Removed Methods | Removed Method | Status | | ----------------------------------------- | --------- | | `setContextWindow(int $tokens)` | ❌ Removed | | `exceedsContextWindow(int $tokens)` | ❌ Removed | | `truncateOldMessages(int $messagesCount)` | ❌ Removed | | `saveKeyToMemory()` | ❌ Removed | | `loadKeysFromMemory()` | ❌ Removed | | `removeChatFromMemory(string $key)` | ❌ Removed | ```php Before (v0.8) theme={null} $messages = $chatHistory->getMessages(); // returns array foreach ($messages as $msg) { ... } ``` ```php After (v1.0) theme={null} $messages = $chatHistory->getMessages(); // returns MessageArray foreach ($messages as $msg) { ... } // Still iterable count($messages); // Still countable $messages->all(); // Get as array if needed $messages->first(); // Get first message $messages->last(); // Get last message ``` ```php Before (v0.8) theme={null} $chatHistory->setContextWindow(50000); if ($chatHistory->exceedsContextWindow($tokens)) { $chatHistory->truncateOldMessages(5); } ``` ```php After (v1.0) theme={null} // Configure truncation in Agent class or config: protected $enableTruncation = true; protected $truncationThreshold = 50000; // Truncation is handled automatically by the truncation strategy ``` Use the new Context facade or Agent methods: ```php theme={null} use LarAgent\Facades\Context; use App\AiAgents\MyAgent; // Option 1: Inside Agent - get chat keys via Agent methods $agent = MyAgent::for('user-123'); $chatKeys = $agent->getChatKeys(); // Get all chat history keys $allStorageKeys = $agent->getStorageKeys(); // Get all storage keys $chatIdentities = $agent->getChatIdentities(); // Get chat identities // Option 2: Inside Agent - get current session key $sessionKey = $this->context()->getIdentity()->getKey(); $chatName = $this->context()->getIdentity()->getChatName(); // Option 3: Outside Agent - use Context facade with agent class $chatKeys = Context::of(MyAgent::class)->getChatKeys(); $storageKeys = Context::of(MyAgent::class)->getStorageKeys(); // Option 4: Outside Agent - use Context facade with agent name $chatKeys = Context::named('MyAgent')->getChatKeys(); $storageKeys = Context::named('MyAgent')->getStorageKeys(); ``` The Context facade provides powerful filtering and iteration capabilities. See the [New Features](#context-facade) section for more details. *** ## Agent Class Changes ### Removed Properties and Methods #### Removed Properties | Property | Replacement | | ------------------------------ | --------------------------------------------------------- | | `$includeModelInChatSessionId` | Removed - not supported | | `$saveChatKeys` | Automatic - handled by Context system's `IdentityStorage` | | `$contextWindowSize` | Use `$truncationThreshold` | #### Removed Methods | Method | Replacement | | -------------------------------------- | ------------------------------------ | | `keyIncludesModelName()` | Removed - not supported | | `withModelInChatSessionId()` | Removed - not supported | | `withoutModelInChatSessionId()` | Removed - not supported | | `createChatHistory(string $sessionId)` | `createChatHistory()` (no parameter) | #### Renamed Methods The old method names still work but are deprecated. Update to the new names when possible. | Old Method (v0.8) | New Method (v1.0) | Returns | Description | | -------------------- | ----------------- | -------- | --------------------------------------------------------- | | `getChatSessionId()` | `getSessionId()` | `string` | Full storage key (e.g., `AgentName_chatHistory_user-123`) | | `getChatKey()` | `getSessionKey()` | `string` | Session key passed to `for()` (e.g., `user-123`) | #### Available Methods These methods from `HasContext` trait are available: | Method | Returns | Description | | ----------------- | --------- | ------------------------------------ | | `getSessionId()` | `string` | Full storage key | | `getSessionKey()` | `string` | Session key passed to `for()` | | `getUserId()` | `?string` | User ID if `usesUserId()` was called | | `getAgentName()` | `string` | Agent class name | | `group()` | `?string` | Group name if set | | `context()` | `Context` | Access to context object | ```php Before (v0.8) - deprecated theme={null} $fullKey = $this->getChatSessionId(); $chatKey = $this->getChatKey(); ``` ```php After (v1.0) - recommended theme={null} $fullKey = $this->getSessionId(); // Full storage key $sessionKey = $this->getSessionKey(); // Session key portion $userId = $this->getUserId(); // User ID if using forUser() $agentName = $this->getAgentName(); // Agent class name // Or access via context identity: $identity = $this->context()->getIdentity(); $fullKey = $identity->getKey(); $chatName = $identity->getChatName(); $userId = $identity->getUserId(); $agentName = $identity->getAgentName(); ``` ```php Before (v0.8) theme={null} protected $includeModelInChatSessionId = true; // and $agent->withModelInChatSessionId(); $agent->withoutModelInChatSessionId(); ``` ```php After (v1.0) theme={null} // This feature has been removed. If you need model-specific sessions, // include the model in your chat key manually: YourAgent::for($userId . '-' . $model); ``` ```php Before (v0.8) theme={null} protected $contextWindowSize = 50000; ``` ```php After (v1.0) theme={null} protected $truncationThreshold = 50000; protected $enableTruncation = true; // Must enable truncation ``` *** ### New Context System LarAgent v1.0 introduces a new **Context System** that manages all storages (chat history, state, identities) through a unified interface. #### New Properties | Property | Type | Description | | ------------------- | ------- | -------------------------------------- | | `$storage` | `array` | Default storage drivers for context | | `$forceReadHistory` | `bool` | Force read history on construction | | `$forceSaveHistory` | `bool` | Force save history after each response | | `$forceReadContext` | `bool` | Force read context on construction | | `$trackUsage` | `bool` | Enable token usage tracking | | `$usageStorage` | `array` | Storage drivers for usage data | | `$enableTruncation` | `bool` | Enable automatic truncation | ```php Before (v0.8) theme={null} $this->chatHistory->addMessage($message); ``` ```php After (v1.0) theme={null} $this->chatHistory()->addMessage($message); // or $this->context()->getStorage(ChatHistoryStorage::class)->addMessage($message); ``` Configure storage drivers in your Agent class: ```php theme={null} protected $storage = [ \LarAgent\Context\Drivers\CacheStorage::class, ]; // Or use the built-in history drivers: protected $history = 'cache'; // Still works ``` *** ### Config Property Renames Update your published config file (`config/laragent.php`): | v0.8 Key | v1.0 Key | | ---------------------------- | ------------------------------ | | `default_context_window` | `default_truncation_threshold` | | `chat_history` (in provider) | `history` | ```php Before (v0.8) theme={null} 'providers' => [ 'default' => [ // ... 'default_context_window' => 50000, 'chat_history' => \LarAgent\History\CacheChatHistory::class, ], ], ``` ```php After (v1.0) theme={null} 'providers' => [ 'default' => [ // ... 'default_truncation_threshold' => 50000, 'history' => \LarAgent\History\CacheChatHistory::class, ], ], ``` Alternatively, republish the config file: ```bash theme={null} php artisan vendor:publish --tag=laragent-config --force ``` *** ## Driver and Configuration Changes ### DriverConfig DTO for Custom Drivers This section only affects custom LLM driver implementations. Driver configurations now use `DriverConfig` DTO internally instead of plain arrays. ```php Before (v0.8) theme={null} class MyCustomDriver extends LlmDriver { public function __construct(array $settings = []) { $this->settings = $settings; } } ``` ```php After (v1.0) theme={null} use LarAgent\Core\DTO\DriverConfig; class MyCustomDriver extends LlmDriver { public function __construct(DriverConfig|array $settings = []) { parent::__construct($settings); // Required! // Custom initialization here } } ``` ```php Before (v0.8) theme={null} $model = $this->settings['model']; $apiKey = $this->settings['api_key']; ``` ```php After (v1.0) theme={null} // Array access still works $model = $this->getSettings()['model']; // Typed access (recommended) $model = $this->getDriverConfig()->model; $apiKey = $this->getDriverConfig()->apiKey; ``` ```php Before (v0.8) theme={null} public function sendMessage(array $messages, array $options = []): AssistantMessage ``` ```php After (v1.0) theme={null} public function sendMessage(array $messages, DriverConfig|array $overrideSettings = []): AssistantMessage ``` *** ### Custom ChatHistory Implementations This section only affects custom ChatHistory classes. Chat history classes should now extend `ChatHistoryStorage` instead of the old `ChatHistory` abstract class. ```php Before (v0.8) theme={null} use LarAgent\Core\Abstractions\ChatHistory; class MyCustomHistory extends ChatHistory { public function readFromMemory(): void { /* ... */ } public function writeToMemory(): void { /* ... */ } public function saveKeyToMemory(): void { /* ... */ } public function loadKeysFromMemory(): array { /* ... */ } public function removeChatFromMemory(string $key): void { /* ... */ } protected function removeChatKey(string $key): void { /* ... */ } } ``` ```php After (v1.0) theme={null} use LarAgent\Context\Storages\ChatHistoryStorage; use LarAgent\Context\Drivers\YourCustomStorageDriver; class MyCustomHistory extends ChatHistoryStorage { protected array $defaultDrivers = [YourCustomStorageDriver::class]; } ``` If you need custom storage logic, create a custom StorageDriver: ```php theme={null} use LarAgent\Context\Contracts\StorageDriver; use LarAgent\Context\Contracts\SessionIdentity; class MyStorageDriver implements StorageDriver { public function readFromMemory(SessionIdentity $identity): ?array { // Your read logic } public function writeToMemory(SessionIdentity $identity, array $data): bool { // Your write logic } public function removeFromMemory(SessionIdentity $identity): bool { // Your remove logic } public static function make(?array $config = null): static { return new static(); } } ``` *** ## New Features These features are new in v1.0 and don't require migration. Consider using them to enhance your agents. ### Message IDs and Timestamps All messages now have unique IDs and timestamps: ```php theme={null} $message = Message::user('Hello'); $id = $message->getId(); // e.g., 'msg_abc123...' $created = $message->getCreatedAt(); // ISO 8601 timestamp ``` ### Message Extras Store driver-specific or custom fields: ```php theme={null} $message->setExtra('custom_field', 'value'); $value = $message->getExtra('custom_field'); $all = $message->getExtras(); ``` ### Usage Tracking Enable automatic token usage tracking: ```php theme={null} class MyAgent extends Agent { protected $trackUsage = true; } ``` ### Truncation Strategies Automatic conversation truncation when context exceeds threshold: ```php theme={null} class MyAgent extends Agent { protected $enableTruncation = true; protected $truncationThreshold = 50000; // Override to use custom strategy protected function truncationStrategy() { return new SummarizationStrategy(); } } ``` ### Context Facade New facade for managing storage outside of agents: ```php theme={null} use LarAgent\Facades\Context; use LarAgent\Context\Storages\ChatHistoryStorage; use App\AiAgents\MyAgent; // ======================================== // Entry Point 1: Context::of(AgentClass::class) // Full agent-based context access (creates temporary agent) // ======================================== // Get all chat keys for an agent $chatKeys = Context::of(MyAgent::class)->getChatKeys(); $storageKeys = Context::of(MyAgent::class)->getStorageKeys(); // Fluent filtering API $count = Context::of(MyAgent::class) ->forUser('user-123') ->forStorage(ChatHistoryStorage::class) ->count(); // Iterate with full agent access Context::of(MyAgent::class) ->forUser('user-123') ->each(function ($identity, $agent) { // $identity is SessionIdentityContract // $agent is fully initialized Agent instance $agent->chatHistory()->clear(); }); // Clear/remove operations Context::of(MyAgent::class)->clearAllChats(); // Clear all chat data Context::of(MyAgent::class)->removeAllChats(); // Remove all chat entries Context::of(MyAgent::class)->clearAllChatsByUser('user-123'); Context::of(MyAgent::class)->removeAllChatsByUser('user-123'); // Get first matching agent $agent = Context::of(MyAgent::class)->forUser('user-123')->firstAgent(); // ======================================== // Entry Point 2: Context::named('AgentName') // Lightweight access (no agent initialization) // ======================================== // Get keys without creating agent instances $chatKeys = Context::named('MyAgent')->getChatKeys(); $storageKeys = Context::named('MyAgent')->getStorageKeys(); // Custom driver configuration $count = Context::named('MyAgent') ->withDrivers([CacheStorage::class]) ->forUser('user-123') ->count(); // Clear chats (lightweight - no agent needed) Context::named('MyAgent')->clearAllChats(); Context::named('MyAgent')->removeAllChats(); ``` ### DataModel Classes Use DataModels for structured output: ```php theme={null} class MyResponse extends DataModel { #[Desc('The answer to the question')] public string $answer; #[Desc('Confidence level 0-100')] public int $confidence; } class MyAgent extends Agent { protected $responseSchema = MyResponse::class; } // Response is automatically reconstructed as MyResponse instance $response = MyAgent::ask('What is 2+2?'); $response->answer; // "4" $response->confidence; // 95 ``` *** ## Quick Migration Checklist Use this checklist to ensure you've covered all migration steps. * [ ] Replace all `Message::create()` with typed factory methods * [ ] Replace all `Message::fromArray()` with specific message class `fromArray()` * [ ] Update `ToolResultMessage` constructor calls to include `$toolName` * [ ] Update `ToolCallMessage` constructor calls to remove `$message` parameter * [ ] Replace `$contextWindowSize` with `$truncationThreshold` * [ ] Remove `$saveChatKeys` (now automatic via Context system) * [ ] Remove `$includeModelInChatSessionId` and related method calls * [ ] Update provider config `default_context_window` → `default_truncation_threshold` * [ ] Update provider config `chat_history` → `history` * [ ] If custom drivers: update constructor to call `parent::__construct($settings)` * [ ] If custom chat history: refactor to use `ChatHistoryStorage` with custom driver *** ## Getting Help If you encounter issues during migration: Open an issue for bugs or migration problems Get help from the community # Introduction Source: https://docs.laragent.ai/introduction LarAgent brings Laravel-grade productivity to the world of AI agents - combining the speed and structure Laravel developers love with powerful agentic capabilities. Together, Laravel + LarAgent form the most productive stack available for building AI agents today. It is backed by [Redberry](https://redberry.international/?utm_source=documentation\&utm_medium=documentation_introduction\&utm_campaign=AI+service+campaign), one of only 10 Diamond-tier Laravel partners. This partnership allows LarAgent to scale development and stay open-source - while also giving teams the option to get hands-on help with AI agent implementation. > Need help building your AI agents? [Talk to us](https://redberry.international/ai-agent-development/?utm_source=documentation\&utm_medium=documentation_introduction\&utm_campaign=AI+service+campaign) about our 5-week PoC sprint for AI agent development. Get started with LarAgent in minutes Learn how to create agents and tools Learn about structured output Check out our blog for tutorials and updates ## What is LarAgent? What if you can create AI agents just like you create any other Eloquent model? Why not?! 👇 ```bash theme={null} php artisan make:agent YourAgentName ``` And it looks familiar, isn't it? ```php theme={null} namespace App\AiAgents; use LarAgent\Agent; class YourAgentName extends Agent { protected $model = 'gpt-4'; protected $history = 'in_memory'; protected $provider = 'default'; protected $tools = []; public function instructions() { return "Define your agent's instructions here."; } public function prompt($message) { return $message; } } ``` And you can tweak the configs, like `history` ```php theme={null} // ... protected $history = \LarAgent\History\CacheChatHistory::class; // ... ``` Or add `temperature`: ```php theme={null} // ... protected $temperature = 0.5; // ... ``` Oh, and add a new tool: ```php theme={null} // ... #[Tool('Get the current weather in a given location')] public function exampleWeatherTool($location, $unit = 'celsius') { return 'The weather in '.$location.' is '.'20'.' degrees '.$unit; } // ... ``` And run it, per user: ```php theme={null} Use App\AiAgents\YourAgentName; // ... YourAgentName::forUser(auth()->user())->respond($message); ``` Or use your custom name for the chat history: ```php theme={null} Use App\AiAgents\YourAgentName; // ... YourAgentName::for("custom_history_name")->respond($message); ``` Let's check the [quickstart](/quickstart) page 👍 # Quickstart Source: https://docs.laragent.ai/quickstart Get started with LarAgent in minutes ## Requirements Before installing LarAgent, make sure your environment meets the following requirements: * Laravel 10.x or higher * PHP 8.3 or higher * OpenAI API key or other [supported LLM provider](/core-concepts/llm-drivers#available-drivers) API key ## Installation You can install LarAgent via Composer: ```bash theme={null} composer require maestroerror/laragent ``` After installing the package, publish the configuration file: ```bash theme={null} php artisan vendor:publish --tag="laragent-config" ``` This will create a `config/laragent.php` file in your application. ## Configuration ### OpenAi If you are using OpenAI API, just set your API key in your `.env` file and you are good to go: ``` OPENAI_API_KEY=your-openai-api-key ``` ### Configuration file The published configuration file contains the following settings and default providers: Configuration file provided below is an example, please refer to the [latest version](https://github.com/MaestroError/LarAgent/blob/main/config/laragent.php) of config file. ```php theme={null} // config for Maestroerror/LarAgent return [ 'default_driver' => \LarAgent\Drivers\OpenAi\OpenAiCompatible::class, 'default_chat_history' => \LarAgent\History\InMemoryChatHistory::class, 'namespaces' => [ 'App\\AiAgents\\', 'App\\Agents\\', ], 'providers' => [ 'default' => [ 'label' => 'openai', 'api_key' => env('OPENAI_API_KEY'), 'driver' => \LarAgent\Drivers\OpenAi\OpenAiDriver::class, 'default_truncation_threshold' => 50000, 'default_max_completion_tokens' => 10000, 'default_temperature' => 1, ], 'gemini' => [ 'label' => 'gemini', 'api_key' => env('GEMINI_API_KEY'), 'driver' => \LarAgent\Drivers\OpenAi\GeminiDriver::class, 'default_truncation_threshold' => 1000000, 'default_max_completion_tokens' => 10000, 'default_temperature' => 1, ], 'gemini_native' => [ 'label' => 'gemini', 'api_key' => env('GEMINI_API_KEY'), 'driver' => \LarAgent\Drivers\Gemini\GeminiDriver::class, 'default_truncation_threshold' => 1000000, 'default_max_completion_tokens' => 10000, 'default_temperature' => 1, ], 'groq' => [ 'label' => 'groq', 'api_key' => env('GROQ_API_KEY'), 'driver' => \LarAgent\Drivers\Groq\GroqDriver::class, 'default_truncation_threshold' => 131072, 'default_max_completion_tokens' => 131072, 'default_temperature' => 1, ], 'claude' => [ 'label' => 'claude', 'api_key' => env('ANTHROPIC_API_KEY'), 'model' => 'claude-3-7-sonnet-latest', 'driver' => \LarAgent\Drivers\Anthropic\ClaudeDriver::class, 'default_truncation_threshold' => 200000, 'default_max_completion_tokens' => 8192, 'default_temperature' => 1, ], 'openrouter' => [ 'label' => 'openrouter', 'api_key' => env('OPENROUTER_API_KEY'), 'model' => 'openai/gpt-oss-20b:free', 'driver' => \LarAgent\Drivers\OpenAi\OpenRouter::class, 'default_truncation_threshold' => 200000, 'default_max_completion_tokens' => 8192, 'default_temperature' => 1, ], 'ollama' => [ 'label' => 'ollama', 'driver' => \LarAgent\Drivers\OpenAi\OllamaDriver::class, 'default_truncation_threshold' => 131072, 'default_max_completion_tokens' => 131072, 'default_temperature' => 0.8, ], ], 'fallback_provider' => null, ]; ``` ### Custom Providers You can configure additional providers with custom settings: ```php theme={null} // Example custom provider with all possible configurations 'custom_provider' => [ // Just name for reference, changes nothing 'label' => 'custom', 'api_key' => env('PROVIDER_API_KEY'), 'api_url' => env('PROVIDER_API_URL'), // Defaults (Can be overriden per agent) 'model' => 'your-provider-model', 'driver' => \LarAgent\Drivers\OpenAi\OpenAiDriver::class, 'chat_history' => \LarAgent\History\InMemoryChatHistory::class, 'default_truncation_threshold' => 15000, 'default_max_completion_tokens' => 100, 'default_temperature' => 0.7, // Enable/disable parallel tool calls 'parallel_tool_calls' => true, // Store metadata with messages 'store_meta' => true, // Save chat keys to memory via chatHistory 'save_chat_keys' => true, ], ``` ## Creating Your First Agent ### Using Artisan Command The quickest way to create a new agent is using the provided Artisan command: ```bash theme={null} php artisan make:agent WeatherAgent ``` This will create a new agent class in the `App\AiAgents` directory with all the necessary boilerplate code. ## Basic Usage ### Simple Response ```php with chat name theme={null} use App\AiAgents\WeatherAgent; // Create an instance for a specific user or chat session $agent = WeatherAgent::for('user-123'); // Get a response $response = $agent->respond("What's the weather like in Boston?"); echo $response; // "The weather in Boston is currently..." ``` ```php with random chat theme={null} use App\AiAgents\WeatherAgent; // Create instance with random chat history name $response = WeatherAgent::make() ->withTemperature(0.7) ->message("What's the weather like in Boston?") ->respond(); echo $response; // "The weather in Boston is currently..." ``` ```php shorthand theme={null} use App\AiAgents\WeatherAgent; // Create instance with random chat history name $response = WeatherAgent::ask("What's the weather like in Boston?"); echo $response; // "The weather in Boston is currently..." ``` ### Adding Tools Tools allow your agent to perform actions and access external data: ```php theme={null} namespace App\AiAgents; use LarAgent\Agent; use LarAgent\Attributes\Tool; class WeatherAgent extends Agent { // ... other properties #[Tool('Get the current weather in a given location')] public function getCurrentWeather($location, $unit = 'celsius') { // Call a weather API or service return "The weather in {$location} is 22 degrees {$unit}."; } } ``` ## Next Steps Now that you have LarAgent set up, you can explore more advanced features: Learn more about creating and configuring agents Discover how to create powerful tools for your agents Get responses in structured formats like JSON Stream responses in real-time for better UX # Expose Agents via API Source: https://docs.laragent.ai/v1/agents/agent-via-api This document describes the feature introduced in the v0.5 and explains how to expose your agents through an OpenAI-compatible endpoint. ## Expose API in Laravel `LarAgent\API\Completions` handles OpenAI compatible chat completion requests. The class expects a valid `Illuminate\Http\Request` and an agent class name: ```php theme={null} use LarAgent\API\Completions; public function completion(Request $request) { $response = Completions::make($request, MyAgent::class); // Your code } ``` Where `$response` is either an `array` (For non-streaming responses) or a `Generator` with chunks (for streaming responses). When exposing agents via API, we are using [Phantom Tools](/v1/tools/phantom-tools) internally to handle tool calls by your client application rather than executed server-side. ### Base Controllers To not bother you with building the controllers with `Completions` class we create abstract classes, So that you can use the provided base controllers to create endpoints quickly by extending them. Both controllers implement a `completion(Request $request)` method that delegates work to `Completions::make()` and automatically handles SSE streaming or JSON responses compatible with OpenAI API. #### SingleAgentController Simple controller for exposing a single agent providing `completion` and `models` methods. Once you have your agent created, 3 steps is enough to expose it via API. Extend [`SingleAgentController`](https://github.com/MaestroError/LarAgent/tree/main/src/API/Completion/Controllers/SingleAgentController.php) when exposing a single agent: 1. Set `protected ?string $agentClass` property to specify the agent class. 2. Set `protected ?array $models` property to specify the models. Controller Example: ```php theme={null} namespace App\Http\Controllers; use LarAgent\API\Completion\Controllers\SingleAgentController; class MyAgentApiController extends SingleAgentController { protected ?string $agentClass = \App\AiAgents\MyAgent::class; protected ?array $models = ['gpt-4o-mini']; } ``` 3. Define the API routes in your Laravel application Routes example: ```php theme={null} Route::post('/v1/chat/completions', [MyAgentApiController::class, 'completion']); Route::get('/v1/models', [MyAgentApiController::class, 'models']); ``` #### MultiAgentController When several agents share one endpoint extend [`MultiAgentController`](https://github.com/MaestroError/LarAgent/tree/main/src/API/Completion/Controllers/MultiAgentController.php): 1. Set `protected ?array $agents` property to specify the agent classes. 2. Set `protected ?array $models` property to specify the models. ```php theme={null} namespace App\Http\Controllers; use LarAgent\API\Completion\Controllers\MultiAgentController; class AgentsController extends MultiAgentController { protected ?array $agents = [ \App\AiAgents\ChatAgent::class, \App\AiAgents\SupportAgent::class, ]; protected ?array $models = [ 'ChatAgent/gpt-4o-mini', 'SupportAgent/gpt-4.1-mini', 'SupportAgent', ]; } ``` The client specifies `model` as `AgentName/model` or as `AgentName` (Default model is used defined in Agent class or provider). 3. Define the API routes in your Laravel application Routes example: ```php theme={null} Route::post('/v1/chat/completions', [AgentsController::class, 'completion']); Route::get('/v1/models', [AgentsController::class, 'models']); ``` ### Storing chat histories Since the most of clients manage the chat history on their side, this method **is not necessary** if you don't want to store chats. Without this method, the session id will be random string per each request, you can easily set "in\_memory" as a chat history type of your exposed agent and forget about it. But if you want to store the chat histories and maintain the state on your side, you will need to set the session id for the agent using `setSessionId` method in `SingleAgentController` or `MultiAgentController`. ```php theme={null} // @return string protected function setSessionId() { $user = auth()->user(); if ($user) { return (string) $user->id; } return "OpenWebUi-LarAgent"; } ``` ### Streaming response Streaming responses are sent as Server-Sent Events where each event contains a JSON chunk matching OpenAI's streaming format. Including `"stream": true` in request returns a `text/event-stream` where each chunk matches the OpenAI format and includes: ```php theme={null} echo "event: chunk\n"; echo 'data: '.json_encode($chunk)."\n\n"; ``` Example of chunk: ```json theme={null} { "id": "ApiAgent_OpenWebUi-LarAgent", "object": "chat.completion.chunk", "created": 1753446654, "model": "gpt-4.1-nano", "choices": [ { "index": 0, "delta": { "role": "assistant", "content": " can" }, "logprobs": null, "finish_reason": null } ], "usage": null } ``` Note that the `usage` data is included only in the last chunk as in OpenAI API. Use either controller according to your needs and point your OpenAI compatible client to these routes. ### Calling from a Custom Controller If you need more control you may call `Completions::make()` directly: ```php theme={null} use Illuminate\Http\Request; use LarAgent\API\Completions; class CustomController { public function chat(Request $request) { $response = Completions::make($request, \App\AiAgents\MyAgent::class); if ($response instanceof \Generator) { // stream Server-Sent Events return response()->stream(function () use ($response) { foreach ($response as $chunk) { echo "event: chunk\n"; echo 'data: '.json_encode($chunk)."\n\n"; ob_flush(); flush(); } }, 200, ['Content-Type' => 'text/event-stream']); } return response()->json($response); } } ``` For more references see [Completions](https://github.com/MaestroError/LarAgent/tree/main/src/API/Completions.php), [SingleAgentController](https://github.com/MaestroError/LarAgent/tree/main/src/API/Completion/Controllers/SingleAgentController.php), [MultiAgentController](https://github.com/MaestroError/LarAgent/tree/main/src/API/Completion/Controllers/MultiAgentController.php). ### Example Request ```bash theme={null} curl -X POST /v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{ "model": "MyAgent/gpt-4o-mini", "messages": [ {"role":"user","content":"Hello"} ], }' ``` ### Example Response ```json theme={null} { "id": "MyAgent_abcd1234", "object": "chat.completion", "created": 1753357877, "model": "gpt-4o-mini", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hi!" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 } } ``` # Create & Configure Source: https://docs.laragent.ai/v1/agents/creation Learn how to create agent classes and configure their behavior, model settings, and provider connections. ## Creating an Agent The recommended way to create a new agent is using the artisan command: ```bash theme={null} php artisan make:agent MyAgent ``` This generates a new agent class in the `App\AiAgents` directory with all the necessary boilerplate: ```php App/AiAgents/MyAgent.php theme={null} Name your agents descriptively based on their purpose. `CustomerSupportAgent` is clearer than `Agent1` or `MyAgent`. *** ## Core Methods Override these methods to customize your agent's behavior: ### instructions() The system prompt that shapes how your agent responds. This defines the agent's personality, role, and guidelines. ```php theme={null} public function instructions() { return "You are an expert financial advisor. Provide clear, actionable advice while always recommending users consult with a certified professional for major decisions."; } ``` For simple, static instructions you can use the `$instructions` property instead of the method. For complex or dynamic instructions, consider using Blade template files. This keeps your agent class clean and allows you to leverage Blade's templating features like conditionals and loops. ### prompt() Process or augment the user's message before sending it to the LLM. Useful for adding context, formatting, or implementing RAG patterns. ```php theme={null} public function prompt($message) { $context = $this->retrieveRelevantContext($message); return "Context: {$context}\n\nUser question: {$message}"; } ``` ### model() Dynamically determine which model to use based on custom logic: ```php theme={null} public function model() { // Use a more powerful model for complex queries if ($this->isComplexQuery()) { return 'gpt-4o'; } return 'gpt-4o-mini'; } ``` ### API Key and URL Override these methods to dynamically set API credentials: ```php theme={null} public function getApiKey() { // Use different API keys per tenant return auth()->user()->team->openai_api_key; } public function getApiUrl() { return config('services.openai.url'); } ``` *** ## Configuration Properties ### Model & Provider | Property | Type | Description | | ----------- | -------- | ------------------------------------------------------ | | `$model` | `string` | The LLM model to use (e.g., `gpt-4o-mini`, `gpt-4o`) | | `$provider` | `string` | Provider configuration name from `config/laragent.php` | | `$driver` | `string` | Driver class for LLM communication | ```php theme={null} class MyAgent extends Agent { protected $model = 'gpt-4o'; protected $provider = 'openai'; protected $driver = \LarAgent\Drivers\OpenAi\OpenAiDriver::class; } ``` LarAgent supports multiple AI providers including OpenAI, Anthropic, Gemini, Groq, and Ollama. See [LLM Drivers](/v1/agents/llm-drivers) for setup instructions and available options. ### Response Settings Control how the LLM generates responses: Controls randomness in responses. Range: `0.0` (focused/deterministic) to `2.0` (creative/random). ```php theme={null} protected $temperature = 0.7; // Balanced ``` Maximum number of tokens in the AI's response. ```php theme={null} protected $maxCompletionTokens = 2000; ``` Generate multiple completion choices. Note: You'll be charged for all generated tokens. ```php theme={null} protected $n = 3; // Returns array of 3 responses ``` Alternative to temperature. The model considers tokens with top\_p probability mass. ```php theme={null} protected $topP = 0.1; // Only top 10% probability tokens ``` Use either `$temperature` or `$topP`, not both. Penalizes tokens based on their frequency in the text. Range: `-2.0` to `2.0`. ```php theme={null} protected $frequencyPenalty = 0.5; // Reduces verbatim repetition ``` Penalizes tokens based on whether they've appeared. Range: `-2.0` to `2.0`. ```php theme={null} protected $presencePenalty = 0.5; // Encourages discussing new topics ``` All configuration properties can also be set via the provider settings in `config/laragent.php`. ### History & Tools | Property | Type | Description | | ---------- | -------- | ------------------------------------------------------------- | | `$history` | `string` | Chat history storage: `in_memory`, `session`, `cache`, `file` | | `$tools` | `array` | Array of tool classes the agent can use | ```php theme={null} class MyAgent extends Agent { protected $history = 'cache'; protected $tools = [ \App\Tools\WeatherTool::class, \App\Tools\CalculatorTool::class, ]; } ``` Both **Context** (chat history, data models) and **Tools** are core features of LarAgent with extensive configuration options. The properties shown here are just the basics. * Learn about context management, storage drivers, and truncation strategies in the [Context page](/v1/context/overview) * Explore tool creation, parameters, and execution in the [Tools Section](/v1/tools/overview) *** ## Arbitrary Configuration Pass custom configuration values to the driver for provider-specific settings or experimental features: ```php Property theme={null} class MyAgent extends Agent { protected $configs = [ 'reasoning_effort' => 'high', 'custom_header' => 'value', ]; } ``` ```php Runtime theme={null} $agent = MyAgent::for('chat-123') ->withConfigs([ 'reasoning_effort' => 'high', 'timeout' => 60, ]) ->respond('Analyze this problem'); ``` ```php Individual theme={null} // Set single config $agent->setConfig('custom_key', 'value'); // Get config $value = $agent->getConfig('custom_key'); // Check existence if ($agent->hasConfig('custom_key')) { // ... } // Remove config $agent->removeConfig('custom_key'); ``` Custom configurations are passed directly to the driver, allowing you to leverage provider-specific features without modifying the agent's core properties. *** ## Runtime Configuration Override any configuration at runtime using chainable methods: ```php theme={null} $response = WeatherAgent::for('user-123') ->withModel('gpt-4o') ->temperature(0.9) ->maxCompletionTokens(1000) ->respond('Write a creative story'); ``` All properties have corresponding chainable methods: * `$temperature` → `temperature(float $temp)` * `$maxCompletionTokens` → `maxCompletionTokens(int $tokens)` * `$topP` → `topP(float $value)` * And so on... ## Next Steps Learn how to interact with agents and handle their responses. Stream responses in real-time for better user experience. Give your agents capabilities to perform actions and retrieve data. Manage conversation history and context persistence. # Agent Hooks Source: https://docs.laragent.ai/v1/agents/hooks Learn how to use lifecycle events and engine hooks to customize agent behavior LarAgent provides a comprehensive hook system that allows you to intercept and customize behavior at various stages of the agent's lifecycle and conversation flow. LarAgent also dispatches **Laravel events** for all hook points, providing more flexibility for cross-cutting concerns like logging and analytics. See the [Event Setup Guide](/v1/customization/events/setup) and [Agent Events](/v1/customization/events/agent) for details. ## Lifecycle Hooks Lifecycle hooks focus on the agent's initialization, conversation flow, and termination. They are perfect for setting up agent-specific configurations, handling conversation state, and managing cleanup operations. ### onInitialize Called when the agent is fully initialized. Use this to set up initial state or configurations. ```php theme={null} protected function onInitialize() { if (auth()->check() && auth()->user()->prefersCreative()) { $this->temperature(1.4); } } ``` ### onConversationStart Triggered at the beginning of each `respond` method call. Use this to prepare conversation-specific resources or logging. ```php theme={null} protected function onConversationStart() { Log::info('Starting new conversation', [ 'agent' => self::class, 'message' => $this->currentMessage() ]); } ``` ### onConversationEnd Called at the end of each `respond` method. For streaming, it runs when the last chunk is received. ```php theme={null} /** @param MessageInterface|array|null $message */ protected function onConversationEnd($message) { $this->clear(); DB::table('chat_histories')->insert([ 'chat_session_id' => $this->chatHistory()->getIdentifier(), 'message' => $message, ]); } ``` ### onToolChange Triggered when a tool is added to or removed from the agent. ```php theme={null} /** * @param ToolInterface $tool * @param bool $added */ protected function onToolChange($tool, $added = true) { if ($added && $tool->getName() == 'my_tool') { $newMetaData = ['using_in' => self::class, ...$tool->getMetaData()]; $tool->setMetaData($newMetaData); } } ``` ### onClear Triggered before the agent's chat history is cleared. ```php theme={null} protected function onClear() { file_put_contents('backup.json', json_encode($this->chatHistory()->toArrayWithMeta())); } ``` ### onTerminate Called when the agent is being terminated. Ideal for final cleanup or saving state. ```php theme={null} protected function onTerminate() { Log::info('Agent terminated successfully'); } ``` ### onEngineError Called when the provider fails to process a request, before trying the fallback provider. ```php theme={null} protected function onEngineError(\Throwable $th) { Log::info('Provider failed', ['error' => $th->getMessage()]); } ``` ## Engine Hooks Engine hooks provide fine-grained control over the conversation processing pipeline, allowing you to intercept and modify behavior at crucial points. Each engine hook returns a boolean value where `true` allows the operation to proceed and `false` prevents it. Prefer throwing exceptions with clear messages instead of returning `false`, since returning `false` silently stops execution. ### beforeReinjectingInstructions Called before the engine reinjects system instructions into the chat history. Instructions are always injected at the beginning of the chat history. The `$reinjectInstructionsPer` property defines when to reinject instructions again. By default, it is set to `0` (disabled). ```php theme={null} /** * @param ChatHistoryInterface $chatHistory * @return bool */ protected function beforeReinjectingInstructions($chatHistory) { if ($chatHistory->count() > 1000) { $this->instructions = view("agents/new_instructions", ['user' => auth()->user()])->render(); } return true; } ``` ### beforeSend & afterSend Called before and after a message is added to the chat history. ```php theme={null} /** * @param ChatHistoryInterface $history * @param MessageInterface|null $message * @return bool */ protected function beforeSend($history, $message) { if ($message && Checker::containsSensitiveData($message->getContent())) { throw new \Exception("Message contains sensitive data"); } return true; } protected function afterSend($history, $message) { Log::info('Message sent', [ 'session' => $history->getIdentifier(), 'content_length' => Tokenizer::count($message->getContent()) ]); return true; } ``` ### beforeSaveHistory Triggered before the chat history is saved. ```php theme={null} protected function beforeSaveHistory($history) { $updatedMeta = [ 'saved_at' => now()->timestamp, 'message_count' => $history->count(), ...$history->getMetadata() ]; $history->getLastMessage()->setMetadata($updatedMeta); return true; } ``` ### beforeResponse & afterResponse Called before sending a message to the LLM and after receiving its response. ```php theme={null} /** * @param ChatHistoryInterface $history * @param MessageInterface|null $message */ protected function beforeResponse($history, $message) { if ($message) { Log::info('User message: ' . $message->getContent()); } return true; } /** * @param MessageInterface $message */ protected function afterResponse($message) { if (is_array($message->getContent())) { Log::info('Structured response received'); } return true; } ``` ### beforeToolExecution & afterToolExecution Triggered before and after a tool is executed. ```php theme={null} /** * @param ToolInterface $tool * @param ToolCallInterface $toolCall * @return bool */ protected function beforeToolExecution($tool, $toolCall) { if (!$this->hasToolPermission($tool->getName())) { Log::warning("Unauthorized tool execution attempt: {$tool->getName()}"); return false; } return true; } /** * @param ToolInterface $tool * @param ToolCallInterface $toolCall * @param mixed &$result * @return bool */ protected function afterToolExecution($tool, $toolCall, &$result) { if (is_array($result)) { $result = array_map(fn($item) => trim($item), $result); } return true; } ``` ### beforeStructuredOutput Called before processing structured output. ```php theme={null} protected function beforeStructuredOutput(array &$response) { if (!$this->checkArrayContent($response)) { return false; } $response['timestamp'] = now()->timestamp; return true; } ``` ## Best Practices Each hook - single responsibility. Throw exceptions with clear messages instead of silently returning `false`. Avoid heavy processing in hooks that run frequently. When modifying referenced parameters (like `&$result`), understand the implications. # Supported Providers Source: https://docs.laragent.ai/v1/agents/llm-drivers Connect to different AI providers like OpenAI, Anthropic, Gemini, and more while maintaining a consistent API across your application. LLM Drivers provide a standardized interface for interacting with different language model providers. Switch between providers without changing your application code. ## Available Drivers Default driver for OpenAI API. Works with GPT-4, GPT-4o, and other OpenAI models. Native support for Claude models via the Anthropic API. Native Gemini driver for Google's AI models. Ultra-fast inference with Groq's LPU platform. Run local LLMs with Ollama integration. Access multiple providers through OpenRouter's unified API. All drivers are pre-configured in `config/laragent.php`. Add your API key and you're ready to go. *** ## Quick Setup ### OpenAI (Default) ```env .env theme={null} OPENAI_API_KEY=your-api-key ``` ```php App/AiAgents/MyAgent.php theme={null} class MyAgent extends Agent { protected $provider = 'default'; // Uses OpenAI protected $model = 'gpt-4o-mini'; } ``` ### Anthropic (Claude) ```env .env theme={null} ANTHROPIC_API_KEY=your-api-key ``` ```php App/AiAgents/MyAgent.php theme={null} class MyAgent extends Agent { protected $provider = 'claude'; protected $model = 'claude-sonnet-4-20250514'; } ``` ### Google Gemini ```env .env theme={null} GEMINI_API_KEY=your-api-key ``` ```php App/AiAgents/MyAgent.php theme={null} class MyAgent extends Agent { protected $provider = 'gemini'; protected $model = 'gemini-2.5-pro-preview-03-25'; } ``` ### Groq ```env .env theme={null} GROQ_API_KEY=your-api-key ``` ```php App/AiAgents/MyAgent.php theme={null} class MyAgent extends Agent { protected $provider = 'groq'; protected $model = 'llama-3.3-70b-versatile'; } ``` ### Ollama (Local) ```php App/AiAgents/MyAgent.php theme={null} class MyAgent extends Agent { protected $provider = 'ollama'; protected $model = 'llama2'; // Any model installed in Ollama } ``` ### OpenRouter ```env .env theme={null} OPENROUTER_API_KEY=your-api-key ``` ```php App/AiAgents/MyAgent.php theme={null} class MyAgent extends Agent { protected $provider = 'openrouter'; protected $model = 'anthropic/claude-3-opus'; } ``` *** ## Configuration ### Global Configuration Configure providers in `config/laragent.php`: ```php config/laragent.php theme={null} 'providers' => [ 'default' => [ 'label' => 'openai', 'api_key' => env('OPENAI_API_KEY'), 'driver' => \LarAgent\Drivers\OpenAi\OpenAiDriver::class, 'default_temperature' => 1, 'default_max_completion_tokens' => 2048, ], 'claude' => [ 'label' => 'anthropic', 'api_key' => env('ANTHROPIC_API_KEY'), 'driver' => \LarAgent\Drivers\Anthropic\ClaudeDriver::class, ], ], ``` ### Per-Agent Configuration Override the driver directly in your agent: ```php theme={null} use LarAgent\Drivers\OpenAi\OpenAiCompatible; class MyAgent extends Agent { protected $driver = OpenAiCompatible::class; protected $provider = 'custom-provider'; } ``` Agent-level driver configuration overrides the global provider settings. *** ## Fallback Provider Configure a fallback provider that activates when the primary provider fails: ```php config/laragent.php theme={null} return [ 'providers' => [ 'default' => [ 'label' => 'openai', 'model' => 'gpt-4o-mini', 'api_key' => env('OPENAI_API_KEY'), 'driver' => \LarAgent\Drivers\OpenAi\OpenAiDriver::class, ], 'gemini' => [ 'label' => 'gemini', 'model' => 'gemini-2.0-flash', 'api_key' => env('GEMINI_API_KEY'), 'driver' => \LarAgent\Drivers\Gemini\GeminiDriver::class, ], ], // Use Gemini as fallback when OpenAI fails 'fallback_provider' => 'gemini', ]; ``` Always set a `model` in the fallback provider configuration to ensure it works correctly. *** ## Driver Reference | Driver | Class | Provider Key | | ----------------- | ------------------------------------------ | ------------ | | OpenAI | `LarAgent\Drivers\OpenAi\OpenAiDriver` | `default` | | OpenAI Compatible | `LarAgent\Drivers\OpenAi\OpenAiCompatible` | — | | Anthropic | `LarAgent\Drivers\Anthropic\ClaudeDriver` | `claude` | | Gemini | `LarAgent\Drivers\Gemini\GeminiDriver` | `gemini` | | Groq | `LarAgent\Drivers\Groq\GroqDriver` | `groq` | | Ollama | `LarAgent\Drivers\OpenAi\OllamaDriver` | `ollama` | | OpenRouter | `LarAgent\Drivers\OpenAi\OpenAiCompatible` | `openrouter` | The `OpenAiCompatible` driver works with any API that follows the OpenAI format, making it easy to integrate with custom or self-hosted solutions. *** ## Best Practices Store API keys in environment variables — never hardcode them. Configure a fallback provider for production reliability. Not all providers support the same features. Check provider documentation for streaming, tool calling, and structured output support. ## Next Steps Configure agents with different providers. Enable real-time streaming responses. Get typed, validated responses from your agents. Manage conversation history and agent state. # What are agents? Source: https://docs.laragent.ai/v1/agents/overview Agents are the core building blocks of LarAgent, representing AI-powered assistants that can interact with users, execute tools, and maintain conversation context. Agent classes are the foundation of everything in LarAgent. They define how your AI assistants behave, what capabilities they have, and how they process information. Other LarAgent features like Tools, Storage, Prompts, etc all revolve around agents. ## What are Agents? In LarAgent, an **Agent** is a PHP class that represents an AI-powered assistant. Each agent encapsulates: * **Instructions** — The system prompt that defines the agent's personality, role, and behavior * **Model configuration** — Which LLM to use and how it should respond * **Tools** — Functions the agent can call to perform actions or retrieve information * **Context** — How conversations (chat history) and other data models are stored and retrieved Think of an agent as a specialized AI assistant tailored to a specific task or domain. You might have a `CustomerSupportAgent` for handling support tickets, a `DataAnalysisAgent` for processing reports, or a `SchedulingAgent` for managing appointments. Or one agent that ochestrates all mentioned above! ## Why Agents? LarAgent's agent-centric architecture provides several benefits: Define an agent once and use it across your application with different chat sessions. Keep all AI-related configuration in one place — instructions, tools, and settings. Test your agents in isolation with predictable inputs and outputs. Override any configuration at runtime for specific use cases. ## Agent Lifecycle When you interact with an agent, here's what happens: The agent is created with its configured instructions, model, and tools. Your message passes through the `prompt()` method, where you can add context or transform the input. The agent sends the conversation history and your message to the configured LLM. If the LLM requests tool calls, the agent executes them and continues the conversation. The final response is returned and stored in the chat history. ## Quick Example Here's what a simple agent looks like: ```php theme={null} namespace App\AiAgents; use LarAgent\Agent; class AssistantAgent extends Agent { protected $model = 'gpt-4o-mini'; protected $history = 'cache'; public function instructions() { return "You are a helpful assistant. Be concise and friendly."; } } ``` Using the agent is straightforward: ```php theme={null} // Start or continue a conversation $response = AssistantAgent::for('user-123')->respond('Hello, how can you help me?'); // Or for one-off interactions without persisted history $response = AssistantAgent::ask('What is 2 + 2?'); ``` ## Next Steps Learn how to create agent classes and configure their behavior. Learn how to interact with agents and handle their responses. Stream responses in real-time for better user experience. Extend your agents with custom tools and capabilities. # RAG Source: https://docs.laragent.ai/v1/agents/rag Guide about implementation of Retrival Augmented Generation in LarAgent. LarAgent doesn't contain any built-in Implementations of RAG, instead it gives you a solid foundation and a frame to inject your prefered RAG approach elegantly Since there are multiple approaches and multiple implementations per each (including in PHP), we decided to not reinvent the wheel. For more info check RAG section in [Guides](/guides/introduction). ## Prompt method of an Agent The best place to inject your RAG solution is the `prompt` method of Agent class, which initially looks like this: ```php theme={null} public function prompt($message) { return $message; } ``` When using Agent, the message from `respond` method is going through prompt method at first, so here you can mutate/enhance the user message as well as **query the database** for extra context. Let's imagine we have (any type of) RAG implemented as `RetrivalService` with `Search` method. ```php highlight={7} theme={null} use App\Services\RetrivalService; // ... public function prompt($message) { $results = RetrivalService::search($message); return $message; } ``` Additionally, we can format it as well structured context using blade template: ```php highlight={8} theme={null} use App\Services\RetrivalService; // ... public function prompt($message) { $results = RetrivalService::search($message); $context = view('prompts.rag_context', ["result" => $results]); return $message; } ``` The most comfortable way to add given context in chat sequence, is the "Developer" message role, since it is allowed to be added in any point at chat history sequence as well as doesn't requires any further maintenance ```php highlight={2,10-12} theme={null} use App\Services\RetrivalService; use LarAgent\Messages\DeveloperMessage; // ... public function prompt($message) { $results = RetrivalService::search($message); $context = view('prompts.rag_context', ["result" => $results])->render(); $devMsg = new DeveloperMessage(view('prompts.support_agent_context', [ 'documents' => $docs, ])->render()); return $message; } ``` And the only thing we are still missing is actual passing the \$devMsg to the chat history, which is pretty simple: ```php highlight={13} theme={null} use App\Services\RetrivalService; use LarAgent\Messages\DeveloperMessage; // ... public function prompt($message) { $results = RetrivalService::search($message); $context = view('prompts.rag_context', ["result" => $results])->render(); $devMsg = new DeveloperMessage(view('prompts.support_agent_context', [ 'documents' => $docs, ])->render()); $this->chatHistory()->addMessage($devMsg); return $message; } ``` That's it! Your agent now has needed context to answer the user's question! For more specific types of RAG implementations please check the [Guides](/guides/introduction). # Data Model Source: https://docs.laragent.ai/v1/context/data-model Create structured data objects with automatic validation, serialization, and OpenAPI schema generation The `DataModel` system in LarAgent provides a robust foundation for handling structured data. It ensures strict typing, automatic validation, serialization, and OpenAPI schema generation - flexible enough for simple DTOs while powerful enough for complex, nested, and polymorphic data structures. Used in storage, tool arguments and structured output with LLMs, the `DataModel` system is a core part of LarAgent's architecture. ## Core Features All data models extend the `LarAgent\Core\Abstractions\DataModel` abstract class, which provides: Populate objects from arrays using `fill()` or `fromArray()` Automatically generate OpenAPI/JSON Schemas using PHP types and attributes Convert objects back to arrays or JSON Uses static runtime caching to minimize Reflection overhead ## Basic Usage When creating a DataModel for use with LLMs (structured output or tool arguments), use the `#[Desc]` attribute to provide context that helps the AI understand the purpose and expected format of each field. ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; class SearchQuery extends DataModel { #[Desc('The search term to look for')] public string $query; #[Desc('The maximum number of results to return')] public int $limit = 10; } ``` ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; class SearchQuery extends DataModel { public function __construct( #[Desc('The search term to look for')] public string $query, #[Desc('The maximum number of results to return')] public int $limit = 10, ) {} } ``` ```php theme={null} // Create from array $query = SearchQuery::fromArray([ 'query' => 'Laravel AI agents', 'limit' => 5 ]); echo $query->query; // 'Laravel AI agents' // Generate JSON schema automatically $schema = $query->toSchema(); ``` Even when you don't need LLM integration, you can still use `DataModel` instead of a plain DTO to benefit from the built-in `fromArray()` and `toArray()` methods. ## Polymorphic Arrays To handle lists of different model types (e.g., a message containing both text and images), extend `DataModelArray`. DataModelArray allows you to define a set of allowed models and a discriminator field to determine which model to instantiate for each item in the array. It's like a collection, but strictly typed to only allow specific DataModels based on a discriminator field. ```php theme={null} use LarAgent\Core\Abstractions\DataModelArray; class MessageContent extends DataModelArray { // Define allowed types and their mapping public static function allowedModels(): array { return [ 'text' => TextContent::class, 'image_url' => ImageContent::class, ]; } // Define the field used to distinguish types (default is 'type') public function discriminator(): string { return 'type'; } } ``` You can instantiate polymorphic arrays in multiple ways: ```php theme={null} // From array $content = new MessageContent([ ['type' => 'text', 'text' => 'Hello'], ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/img.png']] ]); // Or using variadic objects $content = new MessageContent( new TextContent(['text' => 'Hello']), new ImageContent(['image_url' => ['url' => 'https://example.com/img.png']]) ); ``` ### Differentiation Sometime one discriminator field is not enough to differentiate between multiple models. In such cases, you can override the `matchesArray()` method in DataModel. For example, in a chat message array where both `AssistantMessage` and `ToolCallMessage` share the same `role` value of `assistant`, you can implement custom logic in `matchesArray` to check for the presence of specific fields: ```php theme={null} class MessageArray extends DataModelArray { public static function allowedModels(): array { return [ 'user' => UserMessage::class, 'system' => SystemMessage::class, 'developer' => DeveloperMessage::class, 'tool' => ToolResultMessage::class, 'assistant' => [ ToolCallMessage::class, // Check first (has specific condition via matchesArray) AssistantMessage::class, // Default fallback ], ]; } public function discriminator(): string { return 'role'; } } ``` In `ToolCallMessage` & `AssistantMessage`, you can implement custom logic: ```php ToolCallMessage.php theme={null} // ... /** * Check if array data matches ToolCallMessage (has tool_calls). */ public static function matchesArray(array $data): bool { return ! empty($data['tool_calls']); } ``` ```php AssistantMessage.php theme={null} // ... /** * Check if array data matches AssistantMessage (no tool_calls). */ public static function matchesArray(array $data): bool { return empty($data['tool_calls']); } ``` Where the \$data is the raw array being hydrated. This allows you to implement any complex differentiation logic based on the presence/absence or value of certain fields. ## Supported Property Types DataModel supports various types for properties that are automatically mapped to Schema types, automatically validated, and serialized: `string`, `int`, `float`, `bool` `?string`, `?int`, etc. - marks property as optional in schema `array` - simple arrays without type hints Other `DataModel` classes as properties PHP `Enum` for constrained values `DataModelArray` for typed collections ### Nested Data Models DataModels can contain other DataModels as properties, enabling you to build complex, hierarchical data structures. Nested models are automatically hydrated when using `fromArray()` and properly serialized with `toArray()`. ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; class ImageContent extends DataModel { #[Desc('The type of the content')] public string $type = 'image_url'; #[Desc('The image URL information')] public ImageUrl $image_url; } // Nested DataModel class ImageUrl extends DataModel { #[Desc('The URL of the image')] public string $url; #[Desc('The detail level for image processing')] public string $detail = 'auto'; } ``` ```php theme={null} // Nested hydration works automatically $content = ImageContent::fromArray([ 'type' => 'image_url', 'image_url' => [ 'url' => 'https://example.com/image.png', 'detail' => 'high' ] ]); echo $content->image_url->url; // 'https://example.com/image.png' ``` ### Enums PHP backed enums are fully supported and automatically converted to JSON Schema `enum` constraints. This is useful when a property should only accept specific values. ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; enum Sentiment: string { case Positive = 'positive'; case Neutral = 'neutral'; case Negative = 'negative'; } enum Priority: int { case Low = 1; case Medium = 2; case High = 3; } class ContentAnalysis extends DataModel { #[Desc('The sentiment of the analyzed content')] public Sentiment $sentiment; #[Desc('Priority level for follow-up')] public Priority $priority; #[Desc('Summary of the content')] public string $summary; } ``` ```php theme={null} // Usage $analysis = ContentAnalysis::fromArray([ 'sentiment' => 'positive', // or Sentiment::Positive 'priority' => 2, // or Priority::Medium 'summary' => 'Great product review' ]); echo $analysis->sentiment->value; // 'positive' echo $analysis->priority->value; // 2 // Schema generation includes enum values $schema = $analysis->toSchema(); // The 'sentiment' property will have: "enum": ["positive", "neutral", "negative"] ``` Use string-backed enums for human-readable values that the LLM can easily understand. Integer-backed enums work well for numeric scales or priority levels. ### DataModelArray as Property A `DataModelArray` can be used as a property within a `DataModel`, enabling typed collections of polymorphic items. ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Core\Abstractions\DataModelArray; use LarAgent\Attributes\Desc; // Define the polymorphic array class MessageContent extends DataModelArray { public static function allowedModels(): array { return [ 'text' => TextContent::class, 'image_url' => ImageContent::class, ]; } public function discriminator(): string { return 'type'; } } // Use it as a property in another DataModel class ChatMessage extends DataModel { #[Desc('The role of the message sender')] public string $role; #[Desc('The content of the message, which can include text and images')] public MessageContent $content; } ``` ```php theme={null} // Hydration works automatically for nested DataModelArrays $message = ChatMessage::fromArray([ 'role' => 'user', 'content' => [ ['type' => 'text', 'text' => 'What is in this image?'], ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/photo.jpg']] ] ]); // Access the typed content foreach ($message->content as $item) { if ($item instanceof TextContent) { echo $item->text; } elseif ($item instanceof ImageContent) { echo $item->image_url->url; } } // Serialization preserves the structure $array = $message->toArray(); ``` When a `DataModelArray` is used as a property, it behaves like any nested DataModel—automatically hydrated from arrays and serialized back to arrays. ## DTO-Style Data Models For simple data transfer objects that don't need schema generation (For example, when you use it only in storage), you can use a lightweight DTO approach: It's good practice to override `fromArray()` and `toArray()` methods for DataModels that are heavily used. Static method resolution is faster than the dynamic reflection-based approach used by default, which can make a significant difference at scale. ```php theme={null} use LarAgent\Core\Abstractions\DataModel; class SessionIdentity extends DataModel { public function __construct( public readonly string $agentName, public readonly ?string $chatKey = null, public readonly ?string $userId = null, public readonly ?string $group = null ) {} public static function fromArray(array $data): self { return new self( agentName: $data['agentName'], chatKey: $data['chatKey'] ?? '', userId: $data['userId'] ?? '', group: $data['group'] ?? '' ); } public function toArray(): array { return [ 'agentName' => $this->agentName, 'chatKey' => $this->chatKey, 'userId' => $this->userId, 'group' => $this->group, ]; } } ``` ## Performance Optimization The `DataModel` class uses **Reflection** to inspect properties and types. While LarAgent implements **static runtime caching** to mitigate the cost, Reflection is still slower than native code. You are **not required** to override `fromArray()` or `toArray()` methods. The base implementation works perfectly for 90% of use cases. ### When to Override Override `fromArray()` and `toArray()` **only if**: 1. The model is instantiated frequently (thousands of times in a loop) 2. The model is part of a core hot path (like `MessageContent` in a streaming response) 3. You need custom transformation logic that standard casting doesn't support ### Performance-Optimized Example Premature optimization can make your code harder to maintain. Start with the default implementation and optimize only when necessary. ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; class ImageContent extends DataModel { #[Desc('The type of the content')] public string $type = 'image_url'; #[Desc('The image URL information')] public ImageUrl $image_url; // Override for performance (bypasses Reflection) public function toArray(): array { return [ 'type' => $this->type, 'image_url' => $this->image_url->toArray(), ]; } // Override for performance public static function fromArray(array $attributes): static { $instance = new static(); if (isset($attributes['type'])) { $instance->type = $attributes['type']; } if (isset($attributes['image_url'])) { // Handle nested hydration manually $instance->image_url = is_array($attributes['image_url']) ? ImageUrl::fromArray($attributes['image_url']) : $attributes['image_url']; } return $instance; } } ``` ## Summary | Approach | Use Case | Description Required | | ---------------------------- | ----------------------- | -------------------- | | Property Promotion | Simple DTOs | No | | Public Properties | API mapping | Recommended | | With `#[Desc]` | LLM interactions | Yes | | Custom `fromArray`/`toArray` | High-performance paths | Optional | | `DataModelArray` | Polymorphic collections | Depends on contents | # Context Facade Source: https://docs.laragent.ai/v1/context/facade Manage agent contexts, chat histories, and storage operations with an Eloquent-like fluent API The Context Facade provides an Eloquent-like fluent API for managing agent contexts, chat histories, and storage operations. It offers two distinct approaches for accessing context data, each suited for different use cases. ## Access Methods The Context Facade provides two entry points, each returning a different manager type: Full agent access with configuration and callbacks receiving agent instances Lightweight access using just an agent name string, ideal for admin tools ### Choosing the Right Approach | Feature | `of()` / `agent()` | `named()` | | ------------------------- | ---------------------- | ---------------------------- | | Returns | `ContextManager` | `NamedContextManager` | | Requires agent class | Yes | No | | Initializes agent | Yes (temp instance) | No | | Needs `withDrivers()` | No (uses agent config) | Yes (or uses config default) | | `each()` callback args | `($identity, $agent)` | `($identity)` | | `map()` callback args | `($identity, $agent)` | `($identity)` | | `firstAgent()` method | Available | Not available | | `clearAllChats()` returns | `static` (chainable) | `int` (count) | It is recommended to use `named()` for lightweight access, since it does not require initializing agent instances, works directly with context and drivers - can be more efficient for most of the cases. Use `of()` only when you need full agent functionality or access to agent-defined configuration. *** ## Using `Context::of()` The `of()` method (aliased as `agent()`) creates a `ContextManager` that requires a full agent class. This approach initializes a temporary agent instance internally, giving you access to agent configuration and the ability to work with agent instances in callbacks. ```php theme={null} use LarAgent\Facades\Context; use App\AiAgents\SupportAgent; // Using of() Context::of(SupportAgent::class)->clearAllChats(); // Using agent() - alias for of() Context::agent(SupportAgent::class)->clearAllChats(); ``` Use `of()` when you need full agent functionality, want to interact with agents, or when agent configuration matters. ### Filter Methods All filter methods are chainable and create immutable instances (the original instance remains unchanged). ```php theme={null} // Filter by user Context::of(SupportAgent::class) ->forUser('user-123') ->clearAllChats(); // Filter by chat/session name Context::of(SupportAgent::class) ->forChat('support-ticket-456') ->clear(); // Filter by group Context::of(SupportAgent::class) ->forGroup('premium') ->each(function ($identity, $agent) { // Process premium user chats }); // Filter by storage type use LarAgent\Context\Storages\ChatHistoryStorage; Context::of(SupportAgent::class) ->forStorage(ChatHistoryStorage::class) ->count(); // Custom filter with callback Context::of(SupportAgent::class) ->filter(function ($identity) { return str_starts_with($identity->getChatName(), 'vip-'); }) ->count(); ``` #### Chaining Multiple Filters Filters can be chained for complex queries (all filters are AND-ed together): ```php theme={null} Context::of(SupportAgent::class) ->forUser('user-123') ->forGroup('premium') ->forStorage(ChatHistoryStorage::class) ->filter(fn($identity) => $identity->getChatName() !== 'archived') ->each(function ($identity, $agent) { // Process matching identities }); ``` ### Query Methods ```php theme={null} // Count matching identities $totalChats = Context::of(SupportAgent::class)->count(); $userChats = Context::of(SupportAgent::class) ->forUser('user-123') ->count(); // Check if any identities exist $hasChats = Context::of(SupportAgent::class) ->forUser('user-123') ->exists(); // Get first matching identity $identity = Context::of(SupportAgent::class) ->forUser('user-123') ->first(); if ($identity) { echo $identity->getChatName(); echo $identity->getUserId(); echo $identity->getGroup(); } // Get first matching identity as an agent instance $agent = Context::of(SupportAgent::class) ->forUser('user-123') ->firstAgent(); if ($agent) { $response = $agent->respond('Hello!'); } // Get all matching identities as array $identities = Context::of(SupportAgent::class) ->forUser('user-123') ->all(); // Get as SessionIdentityArray collection $identities = Context::of(SupportAgent::class) ->forUser('user-123') ->getIdentities(); // Get chat-specific identities $chatIdentities = Context::of(SupportAgent::class) ->getChatIdentities(); // Get storage keys $keys = Context::of(SupportAgent::class)->getStorageKeys(); $chatKeys = Context::of(SupportAgent::class)->getChatKeys(); ``` ### Iteration Methods With `of()`, callbacks receive both the identity and the agent instance: ```php theme={null} // Iterate with each() Context::of(SupportAgent::class) ->forUser('user-123') ->each(function ($identity, $agent) { echo "Chat: " . $identity->getChatName(); // $agent is a fully initialized agent instance $messages = $agent->getMessages(); }); // Map and collect results $results = Context::of(SupportAgent::class) ->forUser('user-123') ->map(function ($identity, $agent) { return [ 'chat' => $identity->getChatName(), 'messageCount' => count($agent->getMessages()), ]; }); ``` ### Action Methods ```php theme={null} // Clear data from matching storages (keys remain tracked) Context::of(SupportAgent::class) ->forUser('user-123') ->forStorage(ChatHistoryStorage::class) ->clear(); // Remove storages entirely (data and tracking keys removed) Context::of(SupportAgent::class) ->forUser('user-123') ->forStorage(ChatHistoryStorage::class) ->remove(); // Clear all chat histories (chainable) Context::of(SupportAgent::class) ->clearAllChats() ->removeAllChats(); // Shorthand methods for user-specific operations Context::of(SupportAgent::class) ->clearAllChatsByUser('user-123'); Context::of(SupportAgent::class) ->removeAllChatsByUser('user-123'); ``` *** ## Using `Context::named()` The `named()` method creates a `NamedContextManager` using just an agent name string. This lightweight approach doesn't initialize any agent class—it only works with identities and requires explicit driver configuration. ```php theme={null} use LarAgent\Facades\Context; use LarAgent\Context\Drivers\CacheStorage; Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->clearAllChats(); ``` Use `named()` for administrative tasks, cleanup scripts, operations outside agent context, or when you don't need agent functionality. ### Driver Configuration Since `named()` doesn't have access to agent configuration, you must specify drivers explicitly: ```php theme={null} use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\FileStorage; // Single driver Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->clearAllChats(); // Multiple drivers Context::named('SupportAgent') ->withDrivers([CacheStorage::class, FileStorage::class]) ->clearAllChats(); ``` If `withDrivers()` is not called, the facade will attempt to use the default drivers from your `config/laragent.php` configuration. ### Filter Methods The same filter methods are available, but callbacks only receive identities: ```php theme={null} // Filter by user Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forUser('user-123') ->clearAllChats(); // Filter by chat Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forChat('support-ticket-456') ->clearAllChats(); // Filter by group Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forGroup('premium') ->each(function ($identity) { // Only identity available, no agent }); // Custom filter Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->filter(fn($identity) => $identity->getUserId() !== null) ->count(); ``` ### Query Methods ```php theme={null} // Count $count = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->count(); // Check existence $exists = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forUser('user-123') ->exists(); // Check if empty (opposite of exists) $isEmpty = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forUser('user-123') ->isEmpty(); // Get first identity $identity = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->first(); // Get last identity $lastIdentity = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->last(); // Get all identities $identities = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->all(); ``` After you get needed identity, you can initialize agent instances with it using `fromIdentity()` method: `supportAgent::fromIdentity($identity)`. ### Iteration Methods With `named()`, callbacks only receive the identity (no agent instance): ```php theme={null} // Iterate with each() Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forUser('user-123') ->each(function ($identity) { echo "Chat: " . $identity->getChatName(); // No agent available }); // Map and collect $chatNames = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->map(fn($identity) => $identity->getChatName()); ``` ### Action Methods Action methods return counts instead of being chainable: ```php theme={null} // Clear all chats - returns count $clearedCount = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->clearAllChats(); echo "Cleared $clearedCount chats"; // Remove all chats - returns count $removedCount = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->forUser('user-123') ->removeAllChats(); // Clear all storages $count = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->clearAll(); // Remove all storage entries $count = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->removeAll(); ``` ### Additional Methods ```php theme={null} // Get agent name $manager = Context::named('SupportAgent'); echo $manager->getAgentName(); // "SupportAgent" // Get drivers configuration $manager = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]); $drivers = $manager->getDriversConfig(); // Access underlying Context instance $context = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->context(); ``` *** ## Filter Immutability Filters create new instances, leaving the original unchanged. This allows building reusable query bases: ```php theme={null} $base = Context::of(SupportAgent::class); $filtered = $base->forUser('user-123'); // $base still has no filters echo $base->count(); // All identities echo $filtered->count(); // Only user-123's identities // Build reusable bases $premiumBase = Context::of(SupportAgent::class)->forGroup('premium'); $premiumUser1 = $premiumBase->forUser('user-1')->count(); $premiumUser2 = $premiumBase->forUser('user-2')->count(); $allPremium = $premiumBase->count(); ``` *** ## Common Use Cases ```php theme={null} // Using named() for admin tool (no agent class needed) $cleared = Context::named('SupportAgent') ->withDrivers([CacheStorage::class]) ->filter(function ($identity) { // Add your date/condition logic here return true; }) ->clearAllChats(); Log::info("Cleared $cleared old chat sessions"); ``` ```php theme={null} // Using of() to remove all user data across agents Context::of(SupportAgent::class) ->forUser($userId) ->removeAllChats(); Context::of(BillingAgent::class) ->forUser($userId) ->removeAllChats(); ``` ```php theme={null} $exports = Context::of(SupportAgent::class) ->forUser($userId) ->map(function ($identity, $agent) { return [ 'chat_name' => $identity->getChatName(), 'messages' => $agent->getMessages(), 'created_at' => $identity->getKey(), ]; }); ``` ```php theme={null} $hasActiveSessions = Context::of(SupportAgent::class) ->forUser($userId) ->forGroup('active') ->exists(); if ($hasActiveSessions) { // User has active chat sessions } ``` ```php theme={null} Context::of(SupportAgent::class) ->forGroup('premium') ->each(function ($identity, $agent) { // Apply premium processing to each chat $agent->addTool(new PremiumSupportTool()); }); ``` ## Method Reference ### Filter Methods (Both Managers) | Method | Description | | ---------------------------------- | --------------------------- | | `forUser(string $userId)` | Filter by user ID | | `forChat(string $chatName)` | Filter by chat/session name | | `forGroup(string $group)` | Filter by group | | `forStorage(string $storageClass)` | Filter by storage type | | `filter(callable $callback)` | Custom filter callback | ### Query Methods (Both Managers) | Method | Description | | ----------------- | ----------------------------- | | `count()` | Count matching identities | | `exists()` | Check if any match | | `first()` | Get first matching identity | | `all()` | Get all as array | | `getIdentities()` | Get as `SessionIdentityArray` | ### ContextManager Only | Method | Description | | ------------------------ | --------------------------------- | | `firstAgent()` | Get first match as agent instance | | `clear()` | Clear data (keep keys) | | `remove()` | Remove entirely | | `clearAllChatsByUser()` | Clear user's chat history | | `removeAllChatsByUser()` | Remove user's chat history | ### NamedContextManager Only | Method | Description | | ----------------------------- | ---------------------------------- | | `withDrivers(array $drivers)` | Set driver configuration | | `isEmpty()` | Check if no matches | | `last()` | Get last matching identity | | `clearAll()` | Clear all storages (returns count) | | `removeAll()` | Remove all entries (returns count) | | `getAgentName()` | Get agent name string | | `context()` | Get underlying Context instance | # Chat History Source: https://docs.laragent.ai/v1/context/history Configure and manage conversation history storage with flexible drivers and truncation strategies The Chat History system in LarAgent stores and manages conversation messages between users and AI agents. It provides automatic persistence, lazy loading, and flexible storage driver configuration. ## Overview Chat history is managed through the `ChatHistoryStorage` class, which automatically: * Persists messages across requests * Maintains message order * Supports multiple storage drivers (cache, file, database, session) * Provides lazy loading for performance * Tracks dirty state to avoid unnecessary writes ## Configuration LarAgent provides multiple levels of configuration, from global defaults to per-agent customization. ### Global Configuration Set default history storage drivers for all agents in `config/laragent.php`: ```php theme={null} // config/laragent.php return [ /** * Default chat history storage drivers * Uses driver chain - first driver is primary, others are fallback */ 'default_history_storage' => [ \LarAgent\Context\Drivers\CacheStorage::class, // Primary \LarAgent\Context\Drivers\FileStorage::class, // Fallback ], ]; ``` ### Per-Provider Configuration Configure history storage for specific providers: ```php theme={null} // config/laragent.php 'providers' => [ 'default' => [ 'label' => 'openai', 'api_key' => env('OPENAI_API_KEY'), 'driver' => \LarAgent\Drivers\OpenAi\OpenAiDriver::class, // Provider-specific history storage 'history' => [ \LarAgent\Context\Drivers\EloquentStorage::class, ], ], ], ``` ### Per-Agent Configuration Set storage drivers directly in your agent class using the `$history` property: ```php theme={null} class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; // Use string alias for built-in storage protected $history = 'cache'; } ``` ```php theme={null} use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\FileStorage; class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; // Single driver or array with fallback chain protected $history = [ CacheStorage::class, // Primary FileStorage::class, // Fallback ]; } ``` ```php theme={null} class CustomAgent extends Agent { protected $instructions = 'You are a custom agent.'; protected function historyStorageDrivers(): string|array { return [ new EloquentStorage(\App\Models\CustomMessage::class), ]; } } ``` ### Available Storage Aliases | Alias | Driver Class | Description | | -------------------- | ----------------------- | -------------------------------------- | | `'in_memory'` | `InMemoryStorage` | No persistence, lost on request end | | `'session'` | `SessionStorage` | PHP session storage | | `'cache'` | `CacheStorage` | Laravel cache (Redis, Memcached, etc.) | | `'file'` or `'json'` | `FileStorage` | JSON files on disk | | `'database'` | `EloquentStorage` | Separate row per message | | `'database-simple'` | `SimpleEloquentStorage` | JSON column storage | ## Driver Chain (Fallback Pattern) LarAgent supports configuring multiple drivers in a chain. The first driver is the **primary**, and subsequent drivers serve as **fallbacks**: ```php theme={null} use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\FileStorage; class SupportAgent extends Agent { protected $history = [ CacheStorage::class, // Primary: read first, write first FileStorage::class, // Fallback: used if primary fails on read ]; } ``` **How it works:** * **Reading**: Tries the primary driver first. If it returns no data, falls back to the next driver. * **Writing**: Writes to **all** drivers in the chain to keep them synchronized. * **Removing**: Removes from **all** drivers. Use the fallback pattern for high availability. For example, cache for speed with file storage as a durable backup. Learn about all available storage drivers, their configuration options, and custom model examples. ## Working with Chat History ### Accessing Chat History ```php theme={null} // Get the chat history instance $chatHistory = $agent->chatHistory(); // Get all messages $messages = $chatHistory->getMessages(); // Get the last message $lastMessage = $chatHistory->getLastMessage(); // Get message count $count = $chatHistory->count(); // Convert to array $messagesArray = $chatHistory->toArray(); ``` ### Adding Messages Manually ```php theme={null} use LarAgent\Message; // Add a user message $agent->addMessage(Message::user('Hello, I need help.')); // Add an assistant message $agent->addMessage(Message::assistant('Of course! How can I help you?')); // Add a system message $agent->addMessage(Message::system('Additional context...')); ``` All messages support metadata. Use `$message->addMeta([...])` to attach custom data or pass it during creation as second argument `Message::user('text', ['key' => 'value'])`. #### Metadata Storage By default, only message content is stored. Enable metadata storage to persist additional information like agent name, model used, and custom data. ##### Enable via Property ```php theme={null} class SupportAgent extends Agent { protected $storeMeta = true; } ``` ##### Enable via Provider Config ```php theme={null} 'providers' => [ 'default' => [ 'store_meta' => true, // ... ], ], ``` ##### Metadata Contents ```php theme={null} // Metadata automatically added to messages [ 'agent' => 'SupportAgent', // Agent name 'model' => 'gpt-4', // Model used // Custom metadata added via $message->addMeta([...]) ] ``` ### Clearing Chat History ```php theme={null} // Clear all messages (keeps the session) $agent->clear(); // Using Context Facade for bulk operations use LarAgent\Facades\Context; // Clear all chats for an agent Context::of(SupportAgent::class)->clearAllChats(); // Clear all chats for a specific user Context::of(SupportAgent::class) ->forUser('user-123') ->clearAllChats(); ``` Check out the [Context Facade](/v1/context/facade) for more on using the `Context` facade. You can also use artisan commands during development to clear chat histories. ### Manual Read/Save Operations ```php theme={null} // Read/refresh from storage $agent->chatHistory()->read(); // Save to storage (only if there are changes) $agent->chatHistory()->save(); // Force write to storage (bypasses dirty check) $agent->chatHistory()->writeToMemory(); // Force read from storage drivers (bypasses lazy loading) $agent->chatHistory()->readFromMemory(); ``` ## Force Read/Save Flags Control when chat history is synchronized with storage: ```php theme={null} class SupportAgent extends Agent { /** * Force read history from storage on agent initialization * Default: false (uses lazy loading) */ protected $forceReadHistory = false; /** * Force save history after each agent response * Default: false (saves at end of request lifecycle) */ protected $forceSaveHistory = false; } ``` ### Default Behavior By default, LarAgent uses **lazy loading** for reading and **end-of-request saving**: * **Reading**: Chat history is loaded from storage only when first accessed * **Saving**: Chat history is saved automatically when the request ends ### Long-Running Processes In long-running environments (Laravel Octane, FrankenPHP, Swoole), agent instances may persist across requests. Use both flags to ensure data freshness: ```php theme={null} class OctaneSafeAgent extends Agent { protected $instructions = 'You are a helpful assistant.'; // Always read fresh data protected $forceReadHistory = true; // Save immediately protected $forceSaveHistory = true; } ``` ## Truncation Strategies When conversations exceed the model's context window, truncation strategies automatically reduce the conversation size while preserving important context. ### Enabling Truncation ```php theme={null} // config/laragent.php return [ // Enable truncation globally 'enable_truncation' => true, // Set default strategy and config 'default_truncation_strategy' => \LarAgent\Context\Truncation\SimpleTruncationStrategy::class, 'default_truncation_config' => [ 'keep_messages' => 10, 'preserve_system' => true, ], 'truncation_buffer' => 0.2, // Reserve 20% for new content ]; ``` ```php theme={null} 'providers' => [ 'default' => [ 'enable_truncation' => true, 'default_truncation_threshold' => 50000, ], ], ``` ```php theme={null} class LongConversationAgent extends Agent { protected $enableTruncation = true; protected $truncationThreshold = 30000; } ``` #### Full Control via Method Override Override the `truncationStrategy()` method for full control: ```php theme={null} use LarAgent\Context\Truncation\SummarizationStrategy; class CustomTruncationAgent extends Agent { protected $instructions = 'You are a helpful assistant.'; /** * Override to provide a custom truncation strategy. */ protected function truncationStrategy(): ?\LarAgent\Context\Contracts\TruncationStrategy { return new SummarizationStrategy([ 'keep_messages' => 20, 'summary_agent' => \App\AiAgents\CustomSummarizerAgent::class, 'summary_title' => 'Previous conversation context', 'preserve_system' => true, ]); } /** * Override to check if truncation should be enabled. */ public function shouldTruncate(): bool { // Custom logic - e.g., only truncate in production if (app()->environment('testing')) { return false; } return parent::shouldTruncate(); } /** * Override to set a custom truncation threshold. */ public function getTruncationThreshold(): int { // Dynamic threshold based on model return match ($this->model()) { 'gpt-4' => 50000, 'gpt-3.5-turbo' => 10000, default => 30000, }; } } ``` ### Built-in Strategies #### SimpleTruncationStrategy Keeps the last N messages, discarding older ones. Fast and simple. ```php theme={null} $strategy = new SimpleTruncationStrategy([ 'keep_messages' => 10, 'preserve_system' => true, ]); ``` #### SummarizationStrategy Summarizes removed messages using an AI agent, preserving context. ```php theme={null} $strategy = new SummarizationStrategy([ 'keep_messages' => 5, 'summary_agent' => ChatSummarizerAgent::class, 'summary_title' => 'Summary of previous conversation', 'preserve_system' => true, ]); ``` #### SymbolizationStrategy Creates brief "symbols" for each removed message, providing a timeline. ```php theme={null} $strategy = new SymbolizationStrategy([ 'keep_messages' => 5, 'summary_agent' => ChatSymbolizerAgent::class, 'symbol_title' => 'Conversation symbols', 'preserve_system' => true, 'batch_size' => 10, ]); ``` ### Runtime Configuration ```php theme={null} // Enable/disable dynamically $agent = SupportAgent::for('session-123') ->enableTruncation(true); // Configure via context $agent->context() ->setTruncationStrategy(new SymbolizationStrategy([ 'keep_messages' => 10, ])) ->setTruncationThreshold(40000) ->setTruncationBuffer(0.25); ``` ### Understanding Thresholds The `truncationThreshold` is NOT the model's context window. Set it to 30-50% of the model's actual context window to leave room for system prompts, tools, and responses. **Effective threshold calculation:** ``` effective_threshold = truncation_threshold × (1 - buffer) Example: truncation_threshold = 50000 buffer = 0.2 (20%) effective_threshold = 50000 × 0.8 = 40000 tokens ``` **Recommendations:** * **Maximum**: 80% of model's context window (aggressive) * **Recommended**: 30-50% (balanced) * **Conservative**: 20-30% (for agents with large tool outputs) ### Creating Custom Strategies Use the artisan command to scaffold a custom strategy: ```bash theme={null} php artisan make:truncation-strategy CustomTruncation ``` This creates `app/TruncationStrategies/CustomTruncationStrategy.php`: ```php theme={null} true, ]; } public function truncate(MessageArray $messages, int $truncationThreshold, int $currentTokens): MessageArray { // Implement your truncation logic return $messages; } } ``` Keep messages marked as important: ```php theme={null} 10, 'preserve_system' => true, 'priority_metadata_key' => 'priority', 'high_priority_value' => 'high', ]; } public function truncate(MessageArray $messages, int $truncationThreshold, int $currentTokens): MessageArray { $keepMessages = $this->getConfig('keep_messages', 10); $priorityKey = $this->getConfig('priority_metadata_key'); $highPriorityValue = $this->getConfig('high_priority_value'); if ($messages->count() <= $keepMessages) { return $messages; } $newMessages = new MessageArray; $allMessages = $messages->all(); // Separate by type $systemMessages = []; $highPriorityMessages = []; $regularMessages = []; foreach ($allMessages as $message) { if ($this->shouldPreserve($message)) { $systemMessages[] = $message; } elseif ($this->isHighPriority($message, $priorityKey, $highPriorityValue)) { $highPriorityMessages[] = $message; } else { $regularMessages[] = $message; } } // Build result: system → high priority → recent regular foreach ($systemMessages as $message) { $newMessages->add($message); } foreach ($highPriorityMessages as $message) { $newMessages->add($message); } $regularToKeep = max(0, $keepMessages - count($highPriorityMessages)); $recentRegular = array_slice($regularMessages, -$regularToKeep); foreach ($recentRegular as $message) { $newMessages->add($message); } return $newMessages; } protected function isHighPriority($message, string $key, string $value): bool { if (!method_exists($message, 'getMetadata')) { return false; } return ($message->getMetadata()[$key] ?? null) === $value; } } ``` ## Events Chat history operations emit events that you can listen to for logging, validation, or custom behavior. Available events include: * `MessageAdding` / `MessageAdded` * `ChatHistorySaving` / `ChatHistorySaved` * `ChatHistoryLoaded` * `ChatHistoryTruncated` Learn how to listen to and handle chat history events for custom behavior. ## Next Steps Understand the Context and Identity system for storage isolation. Track token usage and costs across agent interactions. # Identity Source: https://docs.laragent.ai/v1/context/identity Understand the Context and Identity system for managing storage isolation, session tracking, and data scoping in LarAgent The Context system in LarAgent provides a unified way to manage storage isolation, session tracking, and data scoping across agents and users. It serves as the central orchestration layer that connects agents with their storage backends. ## Core Components The Context system consists of four key components: Uniquely identifies a storage session using agent name, user ID, chat name, group, and scope. Central orchestration layer that manages multiple storage instances for an agent. Tracks all storage identities registered within a context for discovery and management. Abstract base for all storage implementations (chat history, usage, custom). ## Session Identity A `SessionIdentity` uniquely identifies a storage key using these components: | Component | Description | Example | | ----------- | ----------------------------------------- | -------------------------- | | `agentName` | Name of the agent class | `'SupportAgent'` | | `chatName` | Session/chat identifier | `'support-ticket-123'` | | `userId` | User identifier (when using `forUser()`) | `'user-456'` | | `group` | Grouping for shared context across agents | `'FAQ-team'` | | `scope` | Storage type scope | `'chatHistory'`, `'usage'` | ### Key Generation The identity key is generated as: ``` {scope}_{group|agentName}_{userId|chatName|'default'} ``` * `chatHistory_SupportAgent_user-123` — User-based chat history * `chatHistory_SupportAgent_session-abc` — Session-based chat history * `usage_sql_user-456` — User usage in SQL group ## Creating Agents with Identities LarAgent provides several ways to create agents with different identity configurations. ### Session-based Creation Use session-based creation when you need to manage conversations by a custom session key rather than user identity. This is ideal for anonymous users, guest sessions, or when you want explicit control over session naming. ```php theme={null} // Create agent with a specific session key $agent = SupportAgent::for('session-123'); // Create agent with random session key $agent = SupportAgent::make(); // Access identity $identity = $agent->context()->getIdentity(); echo $identity->getChatName(); // 'session-123' echo $identity->getAgentName(); // 'SupportAgent' ``` ### User-based Creation Use user-based creation to automatically tie conversations to authenticated users. The identity key will include the user ID, enabling easy querying and management of all conversations for a specific user. ```php theme={null} // Create agent for authenticated user $agent = SupportAgent::forUser($request->user()); // Create agent for specific user ID $agent = SupportAgent::forUserId('user-123'); // Access identity $identity = $agent->context()->getIdentity(); echo $identity->getUserId(); // 'user-123' echo $identity->getAgentName(); // 'SupportAgent' ``` ### Reconstructing from Identity Use `fromIdentity()` to recreate an agent instance from a previously tracked identity. This is useful for admin panels, background jobs, or any scenario where you need to operate on existing conversations. ```php theme={null} use LarAgent\Facades\Context; // Get an identity from storage $identity = Context::of(SupportAgent::class) ->forUser('user-123') ->first(); // Reconstruct the agent from identity $agent = SupportAgent::fromIdentity($identity); // The agent is now configured with the same session context $response = $agent->respond('Continue our conversation...'); ``` ## Grouping Groups enable **shared context** between multiple agents. When a group is set, the storage key uses the group name **instead of** the agent name. **Key format without group:** `{scope}_{agentName}_{userId|chatName|'default'}` **Key format with group:** `{scope}_{group}_{userId|chatName|'default'}` This is useful for multi-tenant applications where agents should share context within a tenant, or when you want multiple agent types to work with the same conversation history. ### Static Group Assignment ```php theme={null} class TenantSupportAgent extends Agent { protected $group = 'tenant-123'; } ``` ### Dynamic Group Resolution For runtime group resolution (e.g., based on current tenant): ```php theme={null} class TenantSupportAgent extends Agent { public function group(): ?string { return tenant()->id ?? null; } } ``` The group setting applies to **all** storages by default. To use different groups for specific storages (e.g., group usage but not chat history), override `createChatHistory()` or `createUsageStorage()` with a custom identity. ```php theme={null} use LarAgent\Context\SessionIdentity; use LarAgent\Context\Storages\ChatHistoryStorage; class MyAgent extends Agent { protected $group = 'shared-group'; // Default group for all storages // Override to use agent-specific identity for chat history (no grouping) public function createChatHistory() { $identity = new SessionIdentity( agentName: $this->getAgentName(), chatName: $this->getChatKey(), userId: $this->getUserId(), group: null, // No group - uses agentName instead ); return new ChatHistoryStorage( $identity, $this->historyStorageDrivers(), $this->storeMeta ?? false ); } } ``` Or, alternatively, you can override to set group only for specific storage instead using \$group property. ## Context Operations ### Accessing Context You can access the context object directly from an agent: ```php theme={null} // Get the context instance $context = $agent->context(); // Get the base identity $identity = $context->getIdentity(); // Get the context identity (used for IdentityStorage) $contextIdentity = $context->getContextIdentity(); ``` The `contextIdentity` is the general identity of the agent. It is stored in IdentityStorage and holds all other storage identities associated with the agent. ### Bulk Operations ```php theme={null} // Save all dirty storages $context->save(); // Read all storages from drivers $context->read(); // Clear all storages (marks as dirty, sets to empty) $context->clear(); // Remove all storages completely $context->remove(); ``` ### Getting Tracked Keys ```php theme={null} // Get all tracked storage keys $keys = $agent->getStorageKeys(); // ['chatHistory_SupportAgent_user-123', 'usage_SupportAgent_user-123', ...] // Get chat history keys only $chatKeys = $agent->getChatKeys(); // Get chat history identities $chatIdentities = $agent->getChatIdentities(); ``` ## Temporary Sessions Sessions prefixed with `_temp` are not tracked in IdentityStorage: ```php theme={null} // This session won't be tracked $agent = SupportAgent::for('_temp_preview'); ``` This is useful for: * Preview/demo sessions * Test sessions * One-time interactions ## Context Events Listen to context lifecycle events for custom behavior: ```php theme={null} use LarAgent\Events\Context\ContextCreated; use LarAgent\Events\Context\ContextSaving; use LarAgent\Events\Context\ContextSaved; use LarAgent\Events\Context\ContextReading; use LarAgent\Events\Context\ContextRead; use LarAgent\Events\Context\ContextClearing; use LarAgent\Events\Context\ContextCleared; use LarAgent\Events\Context\StorageRegistered; // Context created Event::listen(ContextCreated::class, function ($event) { $context = $event->context; Log::info('Context created', ['identity' => $context->getIdentity()->getKey()]); }); // Storage registered Event::listen(StorageRegistered::class, function ($event) { $context = $event->context; $prefix = $event->prefix; $storage = $event->storage; Log::info('Storage registered', ['prefix' => $prefix]); }); // Before saving Event::listen(ContextSaving::class, function ($event) { // Last chance to modify before persistence }); // After saving Event::listen(ContextSaved::class, function ($event) { // Trigger post-save actions }); ``` ### Identity Storage Events ```php theme={null} use LarAgent\Events\IdentityStorage\IdentityAdding; use LarAgent\Events\IdentityStorage\IdentityAdded; use LarAgent\Events\IdentityStorage\IdentityStorageSaving; use LarAgent\Events\IdentityStorage\IdentityStorageSaved; use LarAgent\Events\IdentityStorage\IdentityStorageLoaded; // Before identity is added Event::listen(IdentityAdding::class, function ($event) { $identity = $event->identity; // Can modify or validate }); // After identity is added (only when actually new) Event::listen(IdentityAdded::class, function ($event) { $identity = $event->identity; Log::info('New session tracked', ['key' => $identity->getKey()]); }); ``` ## Next Steps Learn how to configure and manage conversation history storage. Track token usage and costs across agent interactions. Create typed, structured data for custom storage implementations. Manage context window limits with truncation strategies. # Overview Source: https://docs.laragent.ai/v1/context/overview Configure default storage drivers for agent context persistence Every agent in LarAgent has a **Context** that manages multiple storage instances — chat history, usage tracking, identity storage, and any custom storages you register. You can configure which storage drivers these use at the agent level. ## Configuration Hierarchy Storage drivers are resolved in the following order (highest priority first): 1. **Storage-specific method** (e.g., `historyStorageDrivers()`) 2. **Storage-specific property** (e.g., `$history`) 3. **Agent default method** (`defaultStorageDrivers()`) 4. **Agent default property** (`$storage`) 5. **Provider configuration** (`config/laragent.php`) 6. **Global configuration** (`config/laragent.php`) ## Default Storage Drivers Set default drivers for **all** storages in an agent's context using the `$storage` property or `defaultStorageDrivers()` method. ### Using Property ```php theme={null} use LarAgent\Agent; use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\FileStorage; class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; // Default drivers for all storages (chat history, usage, custom, etc.) protected $storage = [ CacheStorage::class, // Primary FileStorage::class, // Fallback ]; } ``` ### Using Method Override For dynamic configuration, override the `defaultStorageDrivers()` method: ```php theme={null} use LarAgent\Agent; use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\EloquentStorage; class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; protected function defaultStorageDrivers(): array { // Use database in production, cache in development if (app()->environment('production')) { return [ new EloquentStorage(), ]; } return [ new CacheStorage('redis'), ]; } } ``` ## Storage-Specific Drivers Override the default for specific storage types when you need different persistence strategies. ### Chat History Storage Use `$history` property or `historyStorageDrivers()` method: ```php theme={null} class SupportAgent extends Agent { // Default for all storages protected $storage = [CacheStorage::class]; // Override for chat history only protected $history = 'database'; } ``` ```php theme={null} class SupportAgent extends Agent { protected $storage = [CacheStorage::class]; // Override for chat history with custom model protected function historyStorageDrivers(): string|array { return [ new EloquentStorage(\App\Models\ChatMessage::class), ]; } } ``` For chat history-specific configuration like truncation strategies, metadata storage, and force read/save flags, see the [Chat History](/v1/context/history) documentation. ### Usage Storage Use `$usageStorage` property or `usageStorageDrivers()` method: ```php theme={null} class SupportAgent extends Agent { protected $storage = [CacheStorage::class]; protected $trackUsage = true; // Override for usage tracking only protected $usageStorage = [EloquentStorage::class]; } ``` ```php theme={null} class SupportAgent extends Agent { protected $storage = [CacheStorage::class]; protected $trackUsage = true; protected function usageStorageDrivers(): array { return [ new EloquentStorage(\App\Models\UsageRecord::class), ]; } } ``` For detailed usage tracking configuration, cost calculation, and custom models, see the [Usage Tracking](/v1/context/usage-tracking) documentation. ## String Aliases Both `$usageStorage` and `$history` properties accept string aliases for convenience: ```php theme={null} class SupportAgent extends Agent { // Using alias for usage protected $usageStorage = 'cache'; // Using alias for history protected $history = 'database'; } ``` | Alias | Driver Class | | -------------------- | ----------------------- | | `'in_memory'` | `InMemoryStorage` | | `'session'` | `SessionStorage` | | `'cache'` | `CacheStorage` | | `'file'` or `'json'` | `FileStorage` | | `'database'` | `EloquentStorage` | | `'database-simple'` | `SimpleEloquentStorage` | For detailed information about each driver's constructor arguments, custom model examples, and the fallback pattern, see the [Storage Drivers](/v1/context/storage-drivers) reference. ## Configuration Examples ### Simple: Single Driver for Everything ```php theme={null} use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\EloquentStorage; class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; // All storages use cache and database drivers protected $storage = [ CacheStorage::class, EloquentStorage::class, ]; } ``` ### Mixed: Different Drivers per Storage Type ```php theme={null} use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\EloquentStorage; class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; // Default: cache for most storages protected $storage = [CacheStorage::class]; // Chat history: database for queryability protected $history = 'database'; // Usage tracking: database for analytics protected $usageStorage = 'database-simple'; } ``` ### High Availability: Fallback Chain ```php theme={null} use LarAgent\Context\Drivers\CacheStorage; use LarAgent\Context\Drivers\FileStorage; use LarAgent\Context\Drivers\EloquentStorage; class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; // Cache with file fallback for general storages protected $storage = [ CacheStorage::class, FileStorage::class, ]; // Database for chat history (no fallback needed) protected $history = [ EloquentStorage::class ]; } ``` ### Dynamic: Environment-Based Configuration ```php theme={null} class SupportAgent extends Agent { protected $instructions = 'You are a helpful support agent.'; protected function defaultStorageDrivers(): array { return match (app()->environment()) { 'testing' => [new InMemoryStorage()], 'local' => [new CacheStorage()], default => [ new CacheStorage('redis'), new FileStorage('s3', 'agent_backup'), ], }; } protected function historyStorageDrivers(): string|array { // Always use database for chat history in non-test environments if (app()->environment('testing')) { return [new InMemoryStorage()]; } return [new EloquentStorage()]; } protected function usageStorageDrivers(): string|array { // Always use database for chat history in non-test environments if (app()->environment('testing')) { return [new InMemoryStorage()]; } return [new EloquentStorage()]; } } ``` ## How It Works When a storage is created (e.g., `ChatHistoryStorage`), it resolves drivers in this order: ``` historyStorageDrivers() → $history → defaultStorageDrivers() → $storage → config ``` This allows you to: 1. Set sensible defaults at the agent level (`$storage` / `defaultStorageDrivers()`) 2. Override specific storages when needed (`$history` / `historyStorageDrivers()`) 3. Fall back to global config if nothing is specified Use `$storage` for simple cases and `defaultStorageDrivers()` when you need dynamic logic like environment checks or custom driver instances. ## Custom Storages Beyond chat history and usage tracking, you can create custom storages for any data your agent needs to persist — customer notes, session metadata, conversation tags, and more. Custom storages use: * **DataModel** — Typed, structured data classes for your storage items * **Storage** — Abstract base class that handles persistence, lazy loading, and dirty tracking * **Storage Drivers** — Pluggable backends (cache, file, database, etc.) Create custom storages with DataModels for type-safe persistence. Build custom storage drivers for specialized backends. ## Next Steps Learn about all available storage drivers and their configuration options. Configure chat history storage and truncation strategies. Track token usage and costs across agent interactions. Understand the Context and Identity system for storage isolation. # Storage Drivers Source: https://docs.laragent.ai/v1/context/storage-drivers Configure persistence backends for chat history, usage tracking, and custom storages LarAgent provides several storage drivers for persisting data. Each driver can be used with any storage type — chat history, usage tracking, identity storage, or custom storages. ## Available Drivers | Driver | Use Case | Persistence | Performance | | ----------------------- | ---------------------------------- | ---------------- | ----------------- | | `InMemoryStorage` | Testing, single-request processing | None | Fastest | | `SessionStorage` | Web requests, user sessions | Session lifetime | Fast | | `CacheStorage` | Temporary storage with TTL | Configurable | Fast | | `FileStorage` | Simple file-based persistence | Permanent | Moderate | | `SimpleEloquentStorage` | Database (JSON blob) | Permanent | Moderate | | `EloquentStorage` | Database (normalized rows) | Permanent | Best for querying | ## Driver Chain (Fallback Pattern) LarAgent `Storage` classes support configuring multiple drivers in a chain. The first driver is the **primary**, and subsequent drivers serve as **fallbacks**: ```php theme={null} protected $history = [ CacheStorage::class, // Primary: read first, write first FileStorage::class, // Fallback: used if primary fails on read ]; ``` **How it works:** * **Reading**: Tries the primary driver first. If it returns no data, falls back to the next driver. * **Writing**: Writes to **all** drivers in the chain to keep them synchronized. * **Removing**: Removes from **all** drivers. Use the fallback pattern for high availability. For example, cache for speed with file storage as a durable backup. *** ## InMemoryStorage Stores data in PHP memory. Data is lost after the request ends. Ideal for testing or single-request agents. ```php theme={null} protected $history = 'in_memory'; ``` There is no point in using InMemoryStorage with other storage drivers in a fallback chain. *** ## SessionStorage Uses Laravel's session to store data. Perfect for web applications where context should persist across user requests. ```php theme={null} protected $history = 'session'; ``` Requires an active Laravel session. Not suitable for CLI or queue workers. *** ## CacheStorage Uses Laravel's cache system. Supports different cache stores like Redis, Memcached, or file. ```php theme={null} protected $history = 'cache'; ``` ```php MyAgent theme={null} use LarAgent\Context\Drivers\CacheStorage; protected function defaultStorageDrivers(): array { return [ // Use default cache store new CacheStorage(), // Or use a specific cache store new CacheStorage('redis'), ]; } ``` **Constructor arguments:** | Parameter | Type | Default | Description | | --------- | --------- | ------- | -------------------------------------------------------------------------------------- | | `$store` | `?string` | `null` | Cache store name (e.g., `'redis'`, `'memcached'`). Uses default cache store if `null`. | *** ## FileStorage Stores data as JSON files on disk using Laravel's filesystem. ```php theme={null} protected $history = 'file'; ``` ```php MyAgent theme={null} use LarAgent\Context\Drivers\FileStorage; protected function defaultStorageDrivers(): array { return [ // Use default disk and folder new FileStorage(), // Use specific disk new FileStorage('local'), // Use specific disk and custom folder new FileStorage('s3', 'agent_storage'), ]; } ``` **Constructor arguments:** | Parameter | Type | Default | Description | | --------- | --------- | -------------------- | ------------------------------------------------------------------ | | `$disk` | `?string` | `null` | Storage disk name. Uses `config('filesystems.default')` if `null`. | | `$folder` | `string` | `'laragent_storage'` | Folder path within the disk where files are stored. | *** ## EloquentStorage Stores each item as a separate database row. Best for applications that need to query individual messages or require advanced database features. ```bash theme={null} # Publish and run migration php artisan la:publish eloquent-storage php artisan migrate ``` ```php theme={null} protected $history = 'database'; ``` Example here uses `historyStorageDrivers` instead of `defaultStorageDrivers` to show chat history specific laravel model override. ```php MyAgent theme={null} use LarAgent\Context\Drivers\EloquentStorage; protected function historyStorageDrivers(): string|array { // Use default model $storage = new EloquentStorage(); // Or use custom model $storage = new EloquentStorage(\App\Models\ChatMessage::class); // With custom column names $storage = (new EloquentStorage(\App\Models\ChatMessage::class)) ->setKeyColumn('chat_session_id') ->setPositionColumn('order'); return [$storage]; } ``` **Constructor arguments:** | Parameter | Type | Default | Description | | --------- | --------- | ------- | ---------------------------------------------------------------- | | `$model` | `?string` | `null` | Eloquent model class name. Defaults to `LaragentMessage::class`. | **Additional methods:** | Method | Description | | ----------------------------------- | ----------------------------- | | `setKeyColumn(string $column)` | Column for session identifier | | `setPositionColumn(string $column)` | Column for item ordering | ```php theme={null} belongsTo(User::class); } // Add scopes public function scopeByAgent($query, string $agentName) { return $query->where('agent_name', $agentName); } } ``` *** ## SimpleEloquentStorage Stores all data as a JSON blob in a single database row. Simple setup, good for basic use cases. ```bash theme={null} # Publish and run migration php artisan la:publish simple-eloquent-storage php artisan migrate ``` ```php theme={null} protected $history = 'database-simple'; ``` ```php MyAgent theme={null} use LarAgent\Context\Drivers\SimpleEloquentStorage; protected function defaultStorageDrivers(): array { return [ // Use default model new SimpleEloquentStorage(), // Or use custom model new SimpleEloquentStorage(\App\Models\CustomStorage::class), ]; } ``` **Constructor arguments:** | Parameter | Type | Default | Description | | --------- | --------- | ------- | ---------------------------------------------------------------- | | `$model` | `?string` | `null` | Eloquent model class name. Defaults to `LaragentStorage::class`. | ```php theme={null} 'array', ]; public function user() { return $this->belongsTo(User::class); } } ``` *** ## Selection Guide | Use Case | Recommended Driver(s) | | ---------------------- | --------------------------------- | | Development/Testing | `in_memory` | | Simple web app | `session` or `cache` | | Production with Redis | `cache` (redis) | | Need to query data | `database` (EloquentStorage) | | Simple DB persistence | `database-simple` | | High availability | `cache` + `file` (fallback chain) | | Serverless/Lambda | `database` or `database-simple` | | Long-running processes | `cache` + `database` | ## Next Steps Configure chat history storage and truncation strategies. Track token usage and costs with storage drivers. # Usage Tracking Source: https://docs.laragent.ai/v1/context/usage-tracking Monitor and analyze token consumption metrics from AI model responses in LarAgent Usage tracking in LarAgent automatically captures token consumption metrics, helping you monitor API usage, estimate costs, and optimize your AI agent implementations. ## What Gets Tracked LarAgent's usage tracking system captures: * **Prompt tokens** - Tokens used in the input/request * **Completion tokens** - Tokens used in the output/response * **Total tokens** - Combined prompt + completion tokens * **Timestamps** - When each response was generated * **Model and provider information** - Which model and provider generated the response * **Agent and user context** - Which agent and user triggered the response ## Configuration Usage tracking can be configured at multiple levels, with the following priority order: **Priority:** Agent property > Provider config > Global config ### Global Configuration Enable usage tracking for all agents in `config/laragent.php`: ```php config/laragent.php theme={null} return [ /** * Enable usage tracking globally for all agents. * Can be overridden per-provider or per-agent. */ 'track_usage' => false, /** * Default storage drivers for usage tracking. * If null, uses 'default_storage' configuration. */ 'default_usage_storage' => null, // Or specify explicit drivers: // 'default_usage_storage' => [ // \LarAgent\Context\Drivers\CacheStorage::class, // \LarAgent\Context\Drivers\FileStorage::class, // ], ]; ``` ### Provider Configuration Configure usage tracking for specific providers: ```php config/laragent.php theme={null} return [ 'providers' => [ 'default' => [ 'label' => 'openai', 'api_key' => env('OPENAI_API_KEY'), 'driver' => \LarAgent\Drivers\OpenAi\OpenAiDriver::class, // Enable usage tracking for this provider 'track_usage' => true, // Provider-specific usage storage 'usage_storage' => [ \LarAgent\Usage\Drivers\EloquentUsageDriver::class, ], ], 'gemini' => [ 'label' => 'gemini', 'api_key' => env('GEMINI_API_KEY'), // Disable tracking for this provider 'track_usage' => false, ], ], ]; ``` ### Agent Configuration Set usage tracking directly in your agent class using properties: ```php app/AiAgents/MyAgent.php theme={null} | Alias | Driver Class | Description | | ------------------- | ----------------------- | ------------------------------------- | | `'in_memory'` | `InMemoryStorage` | No persistence | | `'session'` | `SessionStorage` | Session-based storage | | `'cache'` | `CacheStorage` | Laravel cache storage | | `'file'` | `FileStorage` | File-based storage | | `'database'` | `EloquentUsageDriver` | Database storage (requires migration) | | `'database-simple'` | `SimpleEloquentStorage` | Simplified database storage | ### Method Override For complete control, override methods in your agent class: ```php app/AiAgents/CustomTrackingAgent.php theme={null} environment('production'); } /** * Create a custom usage storage instance. */ public function createUsageStorage(): UsageStorage { return new UsageStorage( $this->context()->getIdentity(), $this->usageStorageDrivers(), $this->model(), $this->providerName ); } } ``` ### Runtime Configuration Enable or disable tracking dynamically at runtime: ```php theme={null} // Enable tracking for a specific request $agent = SupportAgent::for('session-123') ->trackUsage(true) ->respond('Hello!'); // Disable tracking $agent->trackUsage(false); ``` ## Database Setup To persist usage data in a database while using `database` driver, publish and run the migration: ```bash theme={null} php artisan la:publish usage-storage ``` ```bash theme={null} php artisan migrate ``` This creates the `laragent_usage` table with columns for all tracked metrics. ## Working with Usage Data The methods below allow you to access and analyze usage data tracked by LarAgent for any driver, but if you use the `EloquentUsageDriver` (or `"database"` alias), you also have direct access to the underlying [database model](#direct-eloquent-model-usage) for advanced queries. ### Accessing Usage Storage ```php theme={null} // Get the usage storage instance $usageStorage = $agent->usageStorage(); // Returns null if tracking is disabled if ($usageStorage === null) { // Tracking is disabled } ``` ### Getting Usage Records ```php theme={null} // Get all usage records for this agent/user $usage = $agent->getUsage(); // Get usage with filters $usage = $agent->getUsage([ 'model_name' => 'gpt-4', 'date' => '2024-01-15', ]); ``` | Filter | Description | | ----------------- | ---------------------------------------------- | | `'agent_name'` | Filter by agent class name | | `'user_id'` | Filter by user ID (null for non-user sessions) | | `'group'` | Filter by group | | `'model_name'` | Filter by model name | | `'provider_name'` | Filter by provider label | | `'date'` | Filter by specific date (Y-m-d) | | `'date_from'` | Filter from date | | `'date_to'` | Filter to date | ### Aggregating Usage Statistics ```php theme={null} // Get aggregate statistics $stats = $agent->getUsageAggregate(); // Returns: // [ // 'total_prompt_tokens' => 1500, // 'total_completion_tokens' => 800, // 'total_tokens' => 2300, // 'record_count' => 10, // ] // Aggregate with filters $stats = $agent->getUsageAggregate([ 'date_from' => '2024-01-01', 'date_to' => '2024-01-31', ]); ``` ### Grouping Usage Data ```php theme={null} // Group usage by model $byModel = $agent->getUsageGroupedBy('model_name'); // Returns: // [ // 'gpt-4' => ['total_tokens' => 1500, 'record_count' => 5], // 'gpt-3.5-turbo' => ['total_tokens' => 800, 'record_count' => 10], // ] // Group by provider $byProvider = $agent->getUsageGroupedBy('provider_name'); // Group by agent $byAgent = $agent->getUsageGroupedBy('agent_name'); // Group by user $byUser = $agent->getUsageGroupedBy('user_id'); // With filters $byModel = $agent->getUsageGroupedBy('model_name', [ 'date_from' => '2024-01-01', ]); ``` ### Getting Usage Identities ```php theme={null} // Get all tracked usage identities for this agent class $identities = $agent->getUsageIdentities(); foreach ($identities as $identity) { echo "User: " . $identity->getUserId(); echo "Chat: " . $identity->getChatName(); } ``` ### Clearing Usage Data ```php theme={null} // Clear all usage records for this identity $agent->clearUsage(); ``` ## Usage Data Structure Each usage record contains: ```php theme={null} [ 'agent_name' => 'SupportAgent', 'user_id' => 'user-123', // null for non-user sessions 'group' => null, // Optional group identifier 'model_name' => 'gpt-4', 'provider_name' => 'openai', 'prompt_tokens' => 150, 'completion_tokens' => 75, 'total_tokens' => 225, 'recorded_at' => '2024-01-15T10:30:00Z', ] ``` ## Direct Eloquent Model Usage When using the `EloquentUsageDriver` (database storage), you have direct access to the `LaragentUsage` Eloquent model for complex queries and reporting. ```php theme={null} use LarAgent\Usage\Models\LaragentUsage; ``` ### Database Schema | Column | Type | Description | | ------------------- | ----------- | --------------------------------------- | | `session_key` | string | Unique identifier for the agent session | | `record_id` | string | Unique identifier for the usage record | | `agent_name` | string | Name of the agent class | | `user_id` | string/null | User ID (if user-based session) | | `group` | string/null | Group identifier | | `chat_name` | string/null | Chat/session name | | `model_name` | string | AI model name (e.g., gpt-4) | | `provider_name` | string | Provider label (e.g., openai) | | `prompt_tokens` | integer | Tokens used in input | | `completion_tokens` | integer | Tokens used in output | | `total_tokens` | integer | Total tokens consumed | | `recorded_at` | datetime | When the usage was recorded | ### Query Scopes The model includes convenient query scopes for filtering: ```php theme={null} use LarAgent\Usage\Models\LaragentUsage; // Filter by agent LaragentUsage::forAgent('SupportAgent')->get(); // Filter by user LaragentUsage::forUser('user-123')->get(); // Filter by model LaragentUsage::forModel('gpt-4')->get(); // Filter by provider LaragentUsage::forProvider('openai')->get(); // Filter by group LaragentUsage::forGroup('premium-users')->get(); // Filter by date range LaragentUsage::betweenDates('2024-01-01', '2024-01-31')->get(); // Filter by specific date LaragentUsage::onDate('2024-01-15')->get(); // Chain multiple scopes LaragentUsage::forAgent('SupportAgent') ->forProvider('openai') ->betweenDates('2024-01-01', '2024-01-31') ->get(); ``` ### Aggregation Methods ```php theme={null} use LarAgent\Usage\Models\LaragentUsage; // Get aggregate totals for all records $totals = LaragentUsage::aggregate(); // Returns: // [ // 'total_prompt_tokens' => 15000, // 'total_completion_tokens' => 8000, // 'total_tokens' => 23000, // 'record_count' => 150, // ] // Aggregate with filters (pass a query builder) $query = LaragentUsage::forAgent('SupportAgent') ->betweenDates('2024-01-01', '2024-01-31'); $totals = LaragentUsage::aggregate($query); // Group by a column $byModel = LaragentUsage::groupByColumn('model_name'); // Returns Collection keyed by model_name ``` ### Advanced Queries Leverage Laravel's query builder for complex analytics: ```php theme={null} // Get daily token usage for the last 30 days $dailyUsage = LaragentUsage::query() ->where('recorded_at', '>=', now()->subDays(30)) ->selectRaw('DATE(recorded_at) as date, SUM(total_tokens) as tokens') ->groupBy('date') ->orderBy('date') ->get(); ``` ```php theme={null} // Get top users by token consumption $topUsers = LaragentUsage::query() ->whereNotNull('user_id') ->selectRaw('user_id, SUM(total_tokens) as total_tokens, COUNT(*) as requests') ->groupBy('user_id') ->orderByDesc('total_tokens') ->limit(10) ->get(); ``` ```php theme={null} // Calculate estimated costs $costs = LaragentUsage::query() ->forProvider('openai') ->betweenDates('2024-01-01', '2024-01-31') ->selectRaw(" model_name, SUM(prompt_tokens) as prompt_tokens, SUM(completion_tokens) as completion_tokens, CASE model_name WHEN 'gpt-4' THEN (SUM(prompt_tokens) / 1000 * 0.03) + (SUM(completion_tokens) / 1000 * 0.06) WHEN 'gpt-3.5-turbo' THEN (SUM(prompt_tokens) / 1000 * 0.001) + (SUM(completion_tokens) / 1000 * 0.002) ELSE 0 END as estimated_cost ") ->groupBy('model_name') ->get(); ``` ## Real-World Example: Cost Monitoring Dashboard Here's a complete example of building a cost monitoring dashboard with usage tracking. Configure tracking in your `config/laragent.php`: ```php config/laragent.php theme={null} return [ 'track_usage' => true, 'default_usage_storage' => [ \LarAgent\Usage\Drivers\EloquentUsageDriver::class, ], ]; ``` ```php app/Services/UsageAnalyticsService.php theme={null} 0, 'completion_tokens' => 0, 'total_tokens' => 0, 'record_count' => 0, ]; foreach ($this->agents as $agentClass) { $agent = $agentClass::make(); $stats = $agent->getUsageAggregate([ 'date_from' => $from, 'date_to' => $to, ]); if ($stats) { $totals['prompt_tokens'] += $stats['total_prompt_tokens'] ?? 0; $totals['completion_tokens'] += $stats['total_completion_tokens'] ?? 0; $totals['total_tokens'] += $stats['total_tokens'] ?? 0; $totals['record_count'] += $stats['record_count'] ?? 0; } } return $totals; } /** * Get usage breakdown by provider. */ public function getUsageByProvider(): array { $byProvider = []; foreach ($this->agents as $agentClass) { $agent = $agentClass::make(); $grouped = $agent->getUsageGroupedBy('provider_name'); if ($grouped) { foreach ($grouped as $provider => $stats) { if (!isset($byProvider[$provider])) { $byProvider[$provider] = [ 'total_tokens' => 0, 'record_count' => 0, ]; } $byProvider[$provider]['total_tokens'] += $stats['total_tokens'] ?? 0; $byProvider[$provider]['record_count'] += $stats['record_count'] ?? 0; } } } return $byProvider; } /** * Estimate cost based on token pricing. */ public function estimateCost(string $from, string $to): array { $pricing = [ 'openai' => [ 'gpt-4' => ['prompt' => 0.03, 'completion' => 0.06], 'gpt-3.5-turbo' => ['prompt' => 0.001, 'completion' => 0.002], ], ]; $costs = []; foreach ($this->agents as $agentClass) { $agent = $agentClass::make(); $usage = $agent->getUsage([ 'date_from' => $from, 'date_to' => $to, ]); if ($usage) { foreach ($usage as $record) { $provider = $record->providerName ?? 'unknown'; $model = $record->modelName ?? 'unknown'; if (isset($pricing[$provider][$model])) { $rates = $pricing[$provider][$model]; $cost = (($record->promptTokens / 1000) * $rates['prompt']) + (($record->completionTokens / 1000) * $rates['completion']); $key = "{$provider}:{$model}"; $costs[$key] = ($costs[$key] ?? 0) + $cost; } } } } return $costs; } } ``` ```php app/Http/Controllers/Admin/UsageController.php theme={null} input('from', now()->startOfMonth()->format('Y-m-d')); $to = $request->input('to', now()->format('Y-m-d')); return response()->json([ 'total_usage' => $this->analytics->getTotalUsage($from, $to), 'by_provider' => $this->analytics->getUsageByProvider(), 'estimated_cost' => $this->analytics->estimateCost($from, $to), ]); } } ``` ```php app/Jobs/CheckUsageThresholds.php theme={null} format('Y-m-d'); $usage = $analytics->getTotalUsage($today, $today); if ($usage['total_tokens'] > $this->dailyThreshold) { $admin = \App\Models\User::find(1); $admin->notify(new UsageThresholdExceeded($usage)); } } } ``` ```php app/Console/Kernel.php theme={null} protected function schedule(Schedule $schedule) { $schedule->job(new CheckUsageThresholds)->hourly(); } ``` Your cost monitoring dashboard is now ready to track usage and alert on thresholds. For production applications, consider setting up usage alerts at different thresholds (50%, 75%, 90%) to proactively manage costs before hitting limits. # Custom Storage 🚧 Source: https://docs.laragent.ai/v1/customization/context/storage Create custom storages with DataModels for type-safe persistence This page is under construction. Check back soon for detailed documentation on creating custom storages. ## Overview Custom storages allow you to persist any structured data within your agent's context — customer notes, conversation tags, session metadata, and more. ## Key Concepts * **DataModel** — Typed data classes that define your storage items * **DataModelArray** — Collections of DataModels with filtering and transformation methods * **Storage** — Abstract base class handling persistence, lazy loading, and dirty tracking ## Coming Soon * Creating a DataModel for your storage items * Extending the Storage abstract class * Registering custom storages with the Context * Querying and filtering stored data # Custom Storage Driver 🚧 Source: https://docs.laragent.ai/v1/customization/context/storage-driver Build custom storage drivers for specialized persistence backends This page is under construction. Check back soon for detailed documentation on creating custom storage drivers. ## Overview Storage drivers handle the actual persistence of data to various backends. You can create custom drivers for specialized storage needs — custom databases, external APIs, cloud services, and more. ## Key Concepts * **StorageDriver** — Interface for persistence backends * **readFromMemory()** — Load data from the storage backend * **writeToMemory()** — Save data to the storage backend * **removeFromMemory()** — Delete data from the storage backend ## Coming Soon * Implementing the StorageDriver interface * Integrating with external services * Handling serialization and deserialization * Error handling and fallback strategies # Agent Events Source: https://docs.laragent.ai/v1/customization/events/agent Events dispatched during agent lifecycle and conversation flow Agent events track the main lifecycle stages of an agent conversation, from initialization through tool execution to termination. This page documents Laravel events that LarAgent dispatches. For in-class hooks that can modify agent behavior, see [Agent Hooks](/v1/agents/hooks). ## Lifecycle Events ### AgentInitialized Dispatched when the agent completes its initialization process and is ready to handle conversations. ```php theme={null} LarAgent\Events\AgentInitialized ``` Complete agent configuration including provider, tools, instructions, and response schema. ```php theme={null} public function handle(AgentInitialized $event): void { $provider = $event->agentDto->provider; $tools = $event->agentDto->tools; } ``` ### ConversationStarted Dispatched when the `respond()` method begins execution, marking the start of a new conversation turn. ```php theme={null} LarAgent\Events\ConversationStarted ``` Current agent configuration at the start of the conversation. ```php theme={null} public function handle(ConversationStarted $event): void { $message = $event->agentDto->message; // Log conversation start, initialize metrics, etc. } ``` ### ConversationEnded Dispatched when the `respond()` method completes execution. ```php theme={null} LarAgent\Events\ConversationEnded ``` Final agent configuration at the end of the conversation. The final response message or null if no response was generated. Message instance includes the token usage data. Use `toArrayWithMeta` to get it as an array. ```php theme={null} public function handle(ConversationEnded $event): void { $response = $event->message; // Log conversation end, calculate metrics, cleanup } ``` ### ToolChanged Dispatched when tools are dynamically added to or removed from the agent during runtime. ```php theme={null} LarAgent\Events\ToolChanged ``` Current agent configuration. The tool instance that was added or removed. `true` if the tool was added, `false` if it was removed. ```php theme={null} public function handle(ToolChanged $event): void { $toolName = $event->tool->getName(); $action = $event->added ? 'added' : 'removed'; } ``` ### AgentCleared Dispatched when the agent's state is cleared, typically resetting conversation history and context. ```php theme={null} LarAgent\Events\AgentCleared ``` Agent configuration at the time of clearing. ### EngineError Dispatched when an error occurs in the LLM engine during processing. ```php theme={null} LarAgent\Events\EngineError ``` Agent configuration when the error occurred. The exception that was thrown by the LLM engine. ```php theme={null} public function handle(EngineError $event): void { $error = $event->exception->getMessage(); $provider = $event->agentDto->provider; // Log error, send alerts, implement fallback logic } ``` ## Hook Events These events provide hooks before and after critical operations. ### BeforeReinjectingInstructions Dispatched before instructions are reinjected into the conversation history. ```php theme={null} LarAgent\Events\BeforeReinjectingInstructions ``` Current agent configuration. The chat history interface before instruction reinjection. ### BeforeSend / AfterSend Dispatched before and after adding a message to chat history. ```php theme={null} LarAgent\Events\BeforeSend LarAgent\Events\AfterSend ``` Current agent configuration. The conversation history. The message being sent. ### BeforeResponse / AfterResponse Dispatched before sending to and after receiving from the LLM. ```php theme={null} LarAgent\Events\BeforeResponse LarAgent\Events\AfterResponse ``` Current agent configuration. The message (user message for Before, LLM response for After). ### BeforeToolExecution / AfterToolExecution Dispatched before and after a tool is executed. ```php theme={null} LarAgent\Events\BeforeToolExecution LarAgent\Events\AfterToolExecution ``` Current agent configuration. The tool instance being executed. The tool call with `getId`, `getToolName`, and `getArguments` methods. (AfterToolExecution only) The result returned by the tool. ### BeforeSaveHistory Dispatched before persisting the conversation history to storage. ```php theme={null} LarAgent\Events\BeforeSaveHistory ``` Current agent configuration. The conversation history about to be saved. ### BeforeStructuredOutput Dispatched before processing structured output from the LLM response. ```php theme={null} LarAgent\Events\BeforeStructuredOutput ``` Current agent configuration including the response schema. The raw structured response array before processing. # Context Events 🚧 Source: https://docs.laragent.ai/v1/customization/events/context Events dispatched during context operations and state management Context events are dispatched during context storage operations, allowing you to monitor and react to context state changes. This page is a placeholder. Context events documentation is coming soon. ## Overview Context events will cover: * Context initialization and loading * Context data modifications * Context persistence operations * Context clearing and cleanup # Chat History Events 🚧 Source: https://docs.laragent.ai/v1/customization/events/history Listen to chat history lifecycle events for logging, validation, and custom behavior This page is under construction. Check back soon for detailed documentation on chat history events. ## Available Events * `MessageAdding` - Before a message is added * `MessageAdded` - After a message is added * `ChatHistorySaving` - Before saving to storage * `ChatHistorySaved` - After saving to storage * `ChatHistoryLoaded` - After loading from storage * `ChatHistoryTruncated` - After truncation is applied # Identity Events 🚧 Source: https://docs.laragent.ai/v1/customization/events/identity Events dispatched during identity resolution and management Identity events are dispatched during identity resolution operations, allowing you to monitor and customize identity handling. This page is a placeholder. Identity events documentation is coming soon. ## Overview Identity events will cover: * Identity resolution process * Identity creation and updates * Identity-based context binding * Multi-identity scenarios # Event Setup Guide Source: https://docs.laragent.ai/v1/customization/events/setup Learn how to listen to and handle LarAgent events in your Laravel application LarAgent dispatches Laravel events at key points during agent execution. This allows you to hook into the agent lifecycle using Laravel's native event system for logging, monitoring, analytics, and more. ## Quickstart ```bash theme={null} php artisan make:listener AgentListener ``` ```php theme={null} namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; use LarAgent\Events\AgentInitialized; class AgentListener { public function __construct() { // } public function handle(AgentInitialized $event): void { // Access the agent DTO dd('Agent has been initialized:', $event->agentDto); } } ``` Open `app/Providers/AppServiceProvider.php` and register the event with its listener: ```php theme={null} namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Event; use LarAgent\Events\AgentInitialized; use App\Listeners\AgentListener; class AppServiceProvider extends ServiceProvider { public function register(): void { // } public function boot(): void { Event::listen( AgentInitialized::class, AgentListener::class ); } } ``` Now whenever an agent is initialized, your `handle` method will be executed. ## AgentDTO Structure Each event that includes `agentDto` uses the following Data Transfer Object structure: ```php theme={null} namespace LarAgent\Core\DTO; class AgentDTO { public function __construct( public readonly string $provider, public readonly string $providerName, public readonly ?string $message, public readonly array $tools = [], public readonly ?string $instructions = null, public readonly ?array $responseSchema = [], public readonly array $configuration = [] ) {} public function toArray(): array { return [ 'provider' => $this->provider, 'providerName' => $this->providerName, 'message' => $this->message, 'tools' => $this->tools, 'instructions' => $this->instructions, 'responseSchema' => $this->responseSchema, 'configuration' => $this->configuration, ]; } } ``` ## Available Events LarAgent provides events across different categories: Lifecycle events for agent initialization, conversation flow, and termination. Events for context storage operations and state changes. Events for chat history operations and message handling. ## Events vs Agent Hooks LarAgent provides two ways to customize behavior: | Feature | Agent Hooks | Laravel Events | | ------------------- | ------------------------------- | ------------------------- | | **Definition** | Override methods in agent class | Separate listener classes | | **Scope** | Single agent class | Application-wide | | **Async support** | No | Yes (via queues) | | **Modify behavior** | Can return `false` to halt | Observe only | | **Use case** | Agent-specific logic | Cross-cutting concerns | Use **agent hooks** for agent-specific customizations that need to modify behavior. Use **Laravel events** for logging, monitoring, analytics, and other cross-cutting concerns that should be decoupled from your agent classes. ## Queueable Listeners For heavy processing, implement `ShouldQueue` to handle events asynchronously: ```php theme={null} namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueue; use LarAgent\Events\ConversationEnded; class LogConversationMetrics implements ShouldQueue { public function handle(ConversationEnded $event): void { // This runs in the background $metrics = [ 'provider' => $event->agentDto->provider, 'tools_used' => count($event->agentDto->tools), 'response_length' => strlen($event->message?->getContent() ?? ''), ]; Analytics::track('conversation_completed', $metrics); } } ``` # Overview Source: https://docs.laragent.ai/v1/responses/overview Learn how to interact with agents, handle responses, and use chainable methods to customize behavior at runtime. ## Getting Responses There are multiple ways to interact with your agent and get responses. ### Using `for()`: Named Sessions Use `for()` to specify a chat session identifier. This enables conversation persistence based on your configured history storage: ```php theme={null} $response = WeatherAgent::for('weather-chat')->respond('What is the weather like?'); ``` The session ID creates a unique conversation thread. Use meaningful identifiers like user IDs, ticket numbers, or conversation UUIDs. ### Using `forUser()`: User-Specific Sessions Pass a Laravel `Authenticatable` object directly to create user-specific sessions: ```php theme={null} $response = WeatherAgent::forUser(auth()->user())->respond('What is the weather like?'); // Or with any Authenticatable model $response = WeatherAgent::forUser($customer)->respond('Check my order status'); ``` This automatically uses the user's identifier to create a unique session, making it easy to maintain per-user conversation history. Learn more about session management and history storage options in [Context & History](/v1/context/history). ### Using `ask()`: Quick One-Off For simple, stateless interactions where you don't need conversation history: ```php theme={null} $response = WeatherAgent::ask('What is 2 + 2?'); ``` `ask()` uses in-memory history that's discarded after the response. Perfect for single-turn interactions. ### Using `make()`: Instance Without Session Create an agent instance without a named session: ```php theme={null} $response = WeatherAgent::make() ->temperature(0.7) ->respond('Tell me a joke'); ``` *** ## Chainable Methods Build complex requests using the fluent API: ```php theme={null} $response = WeatherAgent::for('user-123') ->message('What is the weather like?') // Set message ->temperature(0.7) // Adjust creativity ->withModel('gpt-4o') // Override model ->respond(); // Execute ``` ### Setting the Message ```php theme={null} // Simple string message $agent->message('Your question here')->respond(); // Or pass message directly to respond() $agent->respond('Your question here'); ``` #### Using UserMessage Objects For more control, create a `UserMessage` instance with metadata and bypass prompt processing: ```php theme={null} use LarAgent\Message; $userMessage = Message::user('What is the weather?', [ 'requestId' => $requestId, 'userId' => auth()->id(), ]); $response = WeatherAgent::for('session')->message($userMessage)->respond(); ``` When using a `UserMessage` instance, the `prompt()` method is bypassed. The message is sent directly to the LLM. *** ## Response Types By default, `respond()` returns different types based on your configuration: | Configuration | Return Type | | -------------------------------- | -------------------- | | Standard request | `string` | | `$n` > 1 | `array` of strings | | Structured output (array schema) | Associative `array` | | Structured output (DataModel) | `DataModel` instance | ### Getting the Raw Message Object To get the full `AssistantMessage` object instead of just the content: ```php theme={null} $message = WeatherAgent::for('session') ->returnMessage() ->respond('What is the weather?'); // Access message properties $content = $message->getContent(); $role = $message->getRole(); $metadata = $message->getMetadata(); ``` Use `returnMessage()` when you need access to message metadata or want to inspect the full response object. *** ## Multimodal Input ### Images Pass image URLs or base64-encoded images for vision-capable models: ```php theme={null} // URL-based images $response = VisionAgent::for('analysis') ->withImages([ 'https://example.com/image1.jpg', 'https://example.com/image2.jpg', ]) ->respond('What do you see in these images?'); // Base64-encoded images $base64Image = base64_encode(file_get_contents('photo.jpg')); $response = VisionAgent::for('analysis') ->withImages(['data:image/jpeg;base64,' . $base64Image]) ->respond('Describe this image'); ``` ### Audio Pass base64-encoded audio for audio-capable models: ```php theme={null} $audioData = base64_encode(file_get_contents('recording.mp3')); $response = AudioAgent::for('transcription') ->withAudios([ [ 'format' => 'mp3', // wav, mp3, ogg, flac, m4a, webm 'data' => $audioData, ] ]) ->respond('Transcribe this audio'); ``` *** ## Runtime Mutators Override agent configuration for specific requests: ```php theme={null} $agent->withModel('gpt-4o')->respond('Complex question'); ``` ```php theme={null} // 0.0 = focused, 2.0 = creative $agent->temperature(1.5)->respond('Write a poem'); ``` ```php theme={null} $agent->maxCompletionTokens(500)->respond('Summarize this'); ``` ```php theme={null} $agent->withTool(new CalculatorTool())->respond('What is 15% of 230?'); ``` ```php theme={null} $agent->removeTool('web_search')->respond('Answer from your knowledge only'); ``` ```php theme={null} use LarAgent\Message; $agent->addMessage(Message::system('Be extra concise')) ->respond('Explain quantum computing'); ``` ```php theme={null} $agent->clear()->respond("Let's start fresh"); ``` *** ## Accessors Inspect agent state and retrieve information: ```php theme={null} $sessionId = $agent->getChatSessionId(); // Returns: "WeatherAgent_gpt-4o-mini_user-123" ``` ```php theme={null} $history = $agent->chatHistory(); $messageCount = $history->count(); ``` ```php theme={null} $last = $agent->lastMessage(); echo $last->getRole(); // 'assistant' echo $last->getContent(); // Response text ``` ```php theme={null} $current = $agent->currentMessage(); ``` ```php theme={null} foreach ($agent->getTools() as $tool) { echo $tool->getName(); } ``` ```php theme={null} $keys = $agent->getChatKeys(); // Returns: ["user-1", "user-2", "user-3"] ``` ```php theme={null} $provider = $agent->getProviderName(); // Returns: "openai" ``` *** ## Structured Output For predictable, type-safe responses, you can define a response schema. The agent will return data matching your defined structure instead of free-form text. ```php theme={null} class ProductExtractor extends Agent { protected $responseSchema = ProductInfo::class; } $product = ProductExtractor::ask('Extract: iPhone 15 Pro costs $999'); // Returns a ProductInfo instance echo $product->name; // 'iPhone 15 Pro' echo $product->price; // 999 ``` Learn how to define schemas and work with DataModels for type-safe responses. ## Next Steps Stream responses in real-time for better UX. Get type-safe, predictable responses with schemas. # Streaming Source: https://docs.laragent.ai/v1/responses/streaming Receive AI responses in real-time chunks rather than waiting for the complete response, improving user experience for long interactions. Streaming allows your application to display AI responses as they're being generated, creating a more responsive and engaging user experience. ## Basic Streaming The simplest way to use streaming is with the `respondStreamed` method: ```php theme={null} $agent = WeatherAgent::for('user-123'); $stream = $agent->respondStreamed('What\'s the weather like in Boston and Los Angeles?'); foreach ($stream as $chunk) { if ($chunk instanceof \LarAgent\Messages\StreamedAssistantMessage) { echo $chunk->getLastChunk(); // Output each new piece of content } } ``` ## Streaming with Callback You can also provide a callback function to process each chunk: ```php theme={null} $agent = WeatherAgent::for('user-123'); $stream = $agent->respondStreamed( 'What\'s the weather like in Boston and Los Angeles?', function ($chunk) { if ($chunk instanceof \LarAgent\Messages\StreamedAssistantMessage) { echo $chunk->getLastChunk(); // Flush output buffer to send content to the browser immediately ob_flush(); flush(); } } ); // Consume the stream to ensure it completes foreach ($stream as $_) { // The callback handles the output } ``` ## Understanding Stream Chunks The stream can yield three types of chunks: Regular text content chunks from the AI assistant Tool call messages (handled internally by LarAgent) Final chunk when structured output is enabled For most use cases, you only need to handle `StreamedAssistantMessage` chunks as shown in the examples above. Tool calls are processed automatically by LarAgent. ## Laravel HTTP Streaming For Laravel applications, LarAgent provides the `streamResponse` method that returns a Laravel `StreamedResponse`, making it easy to integrate with your controllers: ```php theme={null} // In a controller public function chat(Request $request) { $message = $request->input('message'); $agent = WeatherAgent::for(auth()->id()); // Return a streamable response return $agent->streamResponse($message, 'plain'); } ``` The `streamResponse` method supports three formats: * Plain Text * JSON * Server-Sent Events (SSE) ```php theme={null} // Simple text output return $agent->streamResponse($message, 'plain'); ``` Frontend implementation (JavaScript): ```javascript theme={null} fetch('/chat?message=What is the weather in Boston?') .then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder(); function read() { return reader.read().then(({ done, value }) => { if (done) return; const text = decoder.decode(value); document.getElementById('output').textContent += text; return read(); }); } return read(); }); ``` ```php theme={null} // Structured JSON with delta and content return $agent->streamResponse($message, 'json'); ``` Example output: ```json theme={null} {"delta":"Hello","content":"Hello"} {"delta":" there","content":"Hello there"} {"delta":"!","content":"Hello there!"} ``` Frontend implementation: ```javascript theme={null} fetch('/chat?message=Greet me&format=json') .then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder(); function read() { return reader.read().then(({ done, value }) => { if (done) return; const text = decoder.decode(value); const lines = text.split('\n').filter(line => line.trim()); lines.forEach(line => { try { const data = JSON.parse(line); document.getElementById('output').textContent = data.content; } catch (e) { console.error('Error parsing JSON:', e); } }); return read(); }); } return read(); }); ``` ```php theme={null} // Server-Sent Events format with event types return $agent->streamResponse($message, 'sse'); ``` Example output: ``` event: content data: {"delta":"Hello","content":"Hello"} event: content data: {"delta":" there","content":"Hello there"} event: content data: {"delta":"!","content":"Hello there!"} event: complete data: {"content":"Hello there!"} ``` Frontend implementation: ```javascript theme={null} const eventSource = new EventSource('/chat?message=Greet me&format=sse'); eventSource.addEventListener('content', function(e) { const data = JSON.parse(e.data); document.getElementById('output').textContent = data.content; }); eventSource.addEventListener('complete', function(e) { eventSource.close(); }); eventSource.addEventListener('error', function(e) { console.error('EventSource error:', e); eventSource.close(); }); ``` *** ## Streaming with Structured Output When using structured output with streaming, you'll receive text chunks during generation, and the final structured data at the end: ```php theme={null} $agent = ProfileAgent::for('user-123'); $stream = $agent->respondStreamed('Generate a profile for John Doe'); $finalStructuredData = null; foreach ($stream as $chunk) { if ($chunk instanceof \LarAgent\Messages\StreamedAssistantMessage) { echo $chunk->getLastChunk(); // Part of JSON } elseif (is_array($chunk)) { // This is the final structured data $finalStructuredData = $chunk; } } // Now $finalStructuredData is array which contains the structured output // For example: ['name' => 'John Doe', 'age' => 30, 'interests' => [...]] ``` When using SSE format with structured output, you'll receive a special event: ``` event: structured data: {"type":"structured","delta":"","content":{"name":"John Doe","age":30,"interests":["coding","reading","hiking"]},"complete":true} event: complete data: {"content":{"name":"John Doe","age":30,"interests":["coding","reading","hiking"]}} ``` ## Best Practices **Do** use streaming for long responses to improve user experience **Do** handle both text chunks and structured output appropriately **Do** implement proper error handling in your streaming code **Don't** forget to consume the entire stream, even when using callbacks **Don't** rely on specific timing of chunks, as they can vary based on network conditions # Structured Output Source: https://docs.laragent.ai/v1/responses/structured-output Define response schemas to receive type-safe, predictable data from your AI agents using DataModels or array schemas. Structured output constrains the LLM to return JSON matching your defined schema. Instead of parsing free-form text, you get predictable data structures that integrate seamlessly with your application. ## Quick Start Define a DataModel and use it as your response schema: ```php theme={null} use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; class WeatherInfo extends DataModel { #[Desc('The city name')] public string $city; #[Desc('Temperature in Celsius')] public float $temperature; #[Desc('Weather condition')] public string $condition; } ``` ```php theme={null} class WeatherAgent extends Agent { protected $responseSchema = WeatherInfo::class; public function instructions() { return 'Provide weather information for the requested location.'; } } ``` ```php theme={null} $weather = WeatherAgent::ask('What is the weather in Paris?'); echo $weather->city; // 'Paris' echo $weather->temperature; // 18.5 echo $weather->condition; // 'Partly cloudy' ``` *** ## Setting Up Schemas ### Using a DataModel Class The recommended approach for type-safe responses: ```php theme={null} class ProductExtractor extends Agent { protected $responseSchema = ProductInfo::class; } // Response is automatically a ProductInfo instance $product = ProductExtractor::ask('Extract: MacBook Air M3 - $1,099'); echo $product->name; // 'MacBook Air M3' echo $product->price; // 1099 ``` ### Using an Array Schema For simple cases or dynamic schemas: ```php theme={null} class ProductExtractor extends Agent { protected $responseSchema = [ 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string', 'description' => 'Product name'], 'price' => ['type' => 'number', 'description' => 'Price in dollars'], ], 'required' => ['name', 'price'], 'additionalProperties' => false, ]; } // Response is an associative array $product = ProductExtractor::ask('Extract: MacBook Air M3 - $1,099'); // ['name' => 'MacBook Air M3', 'price' => 1099] ``` ### Using the `structuredOutput()` Method For dynamic schemas or complex logic, override the `structuredOutput()` method: ```php Using DataModel Schema theme={null} class DynamicAgent extends Agent { public function structuredOutput() { // Dynamically choose schema based on context if ($this->requiresDetailedResponse()) { return DetailedReport::generateSchema(); } return SummaryReport::generateSchema(); } // Enable automatic DataModel reconstruction public function getResponseSchemaClass(): ?string { return $this->requiresDetailedResponse() ? DetailedReport::class : SummaryReport::class; } } ``` ```php Using Array Schema theme={null} class DynamicAgent extends Agent { public function structuredOutput() { return [ 'type' => 'object', 'properties' => $this->buildPropertiesBasedOnContext(), 'required' => ['field1', 'field2'], 'additionalProperties' => false, ]; } } ``` When you override `structuredOutput()`, also override `getResponseSchemaClass()` to enable automatic DataModel reconstruction. Or skip if you want response returned as array. ### Runtime Schema Set the schema at runtime using the fluent API: ```php theme={null} $response = MyAgent::for('session') ->responseSchema(ProductInfo::class) ->respond('Extract product info from: iPhone 15 - $799'); ``` *** ## DataModel Basics DataModels are PHP classes that define your expected response structure. ### Property Types ```php theme={null} class UserProfile extends DataModel { public string $name; // Text public int $age; // Integer public float $score; // Decimal public bool $isActive; // Boolean public array $tags; // Generic array public ?string $nickname; // Nullable (optional) public UserStatus $status; // Enum (string-backed or int-backed) public Address $address; // Nested DataModel public SkillArray $skills; // DataModelArray: collection of DataModels } ``` ### The `#[Desc]` Attribute Add descriptions to guide the LLM on what each field should contain: ```php theme={null} use LarAgent\Attributes\Desc; class ContactInfo extends DataModel { #[Desc('Full legal name of the person')] public string $name; #[Desc('Valid email address format')] public string $email; #[Desc('Phone number with country code')] public ?string $phone = null; } ``` Descriptive `#[Desc]` attributes significantly improve the quality and accuracy of extracted data. ### Optional Properties Use nullable types or default values: ```php theme={null} class Article extends DataModel { #[Desc('Article title')] public string $title; #[Desc('Article subtitle (optional)')] public ?string $subtitle = null; #[ExcludeFromSchema] public string $status = 'draft'; } ``` Use `#[ExcludeFromSchema]` to exclude properties from the schema passed to the LLM. These properties won't be affected by the AI response — they remain completely controlled by you with their default values and defined methods. *** ## Advanced DataModels DataModels support powerful features for complex data structures: * **Nested DataModels** — Embed DataModels within other DataModels for hierarchical data * **DataModelArray** — Type-safe collections of DataModels * **Polymorphic Collections** — Collections containing different DataModel types with discriminators Learn about nested structures, collections, polymorphic arrays, and advanced DataModel patterns. *** ## Real-World Example A complete example analyzing product reviews: ```php App/DataModels/ReviewSentiment.php theme={null} // Represents sentiment analysis for a single review class ReviewSentiment extends DataModel { #[Desc('Overall sentiment: positive, negative, or neutral')] public string $sentiment; #[Desc('Confidence score from 0.0 to 1.0')] public float $confidence; #[Desc('Key points from the review')] public array $keyPoints; } ``` ```php App/DataModels/ReviewSentimentArray.php theme={null} // Typed collection of ReviewSentiment instances class ReviewSentimentArray extends DataModelArray { public static function allowedModels(): array { return [ReviewSentiment::class]; } } ``` ```php App/DataModels/ReviewAnalysis.php theme={null} // Complete analysis result containing all reviews and summary class ReviewAnalysis extends DataModel { #[Desc('Product being reviewed')] public string $productName; #[Desc('Average sentiment score')] public float $averageScore; #[Desc('Individual review analyses')] public ReviewSentimentArray $reviews; #[Desc('Summary recommendation')] public string $recommendation; } ``` ```php App/AiAgents/ReviewAnalyzerAgent.php theme={null} // Agent that analyzes product reviews using the ReviewAnalysis schema class ReviewAnalyzerAgent extends Agent { protected $model = 'gpt-4o'; protected $responseSchema = ReviewAnalysis::class; public function instructions() { return 'Analyze product reviews and provide sentiment analysis.'; } } ``` ```php theme={null} $reviews = <<productName}\n"; echo "Average Score: {$analysis->averageScore}\n"; echo "Recommendation: {$analysis->recommendation}\n"; foreach ($analysis->reviews as $review) { echo "- {$review->sentiment} ({$review->confidence})\n"; } ``` *** ## Best Practices Use DataModels for reusable, type-safe schemas with IDE autocompletion. Add descriptive `#[Desc]` attributes to guide the LLM. Use `DataModelArray` instead of raw arrays for typed collections. Keep DataModels focused — split complex structures into nested models. Avoid deeply nested schemas (4+ levels) as they can reduce response accuracy. Always include critical fields in `required` — only omit truly optional properties. ## Next Steps Stream responses in real-time. Extend agents with custom tools. # Attribute Tools Source: https://docs.laragent.ai/v1/tools/attribute-tools Create tools by adding the #[Tool] attribute to methods in your agent class — the simplest and most flexible way to define agent capabilities. The `#[Tool]` attribute is the recommended way to create tools in LarAgent. It transforms regular PHP methods into tools that your agent can invoke, with automatic schema generation from type hints and descriptions. ## Basic Usage Add the `#[Tool]` attribute to any method in your agent class: ```php theme={null} use LarAgent\Agent; use LarAgent\Attributes\Tool; class WeatherAgent extends Agent { protected $model = 'gpt-4o-mini'; public function instructions() { return 'You are a helpful weather assistant.'; } #[Tool('Get the current weather for a location')] public function getWeather(string $location, string $unit = 'celsius'): string { $weather = WeatherService::get($location, $unit); return "The weather in {$location} is {$weather['temp']} degrees {$unit}"; } } ``` LarAgent automatically: * Registers the method as a tool with the given description * Extracts parameter names and types from the method signature * Generates a JSON schema for the LLM * Handles tool invocation and response processing ## Parameter Descriptions Provide descriptions for each parameter to help the LLM understand what values to pass: ```php theme={null} #[Tool( description: 'Get the current weather for a location', parameterDescriptions: [ 'location' => 'The city and state, e.g. San Francisco, CA', 'unit' => 'Temperature unit: celsius or fahrenheit' ] )] public function getWeather(string $location, string $unit = 'celsius'): string { return WeatherService::get($location, $unit); } ``` Clear parameter descriptions significantly improve the LLM's ability to call tools correctly. Describing the expected format and providing examples is generally considered as best practice. ## Using Enums PHP Enums constrain parameter values to a specific set of options: ```php theme={null} // app/Enums/TemperatureUnit.php namespace App\Enums; enum TemperatureUnit: string { case Celsius = 'celsius'; case Fahrenheit = 'fahrenheit'; } ``` ```php theme={null} use App\Enums\TemperatureUnit; #[Tool( description: 'Get the current weather for a location' )] public function getWeather(string $location, TemperatureUnit $unit = TemperatureUnit::Celsius): string { return WeatherService::get($location, $unit->value); } ``` LarAgent automatically generates an `enum` constraint in the OpenAPI schema before sending to the LLM: ```json theme={null} { "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit" } } ``` ## DataModel Parameters Use [DataModel](/v1/context/data-model) classes as parameters for complex, structured input: ```php theme={null} // app/DataModels/Address.php namespace App\DataModels; use LarAgent\Core\Abstractions\DataModel; use LarAgent\Attributes\Desc; class Address extends DataModel { #[Desc('Street address')] public string $street; #[Desc('City name')] public string $city; #[Desc('State or province')] public string $state; #[Desc('Postal/ZIP code')] public string $postalCode; #[Desc('Country code (ISO 3166-1 alpha-2)')] public string $country = 'US'; } ``` ```php theme={null} use App\DataModels\Address; #[Tool('Calculate shipping cost to an address')] public function calculateShipping(Address $address, float $weight): string { // $address is automatically hydrated as an Address instance $zone = $this->getShippingZone($address->country, $address->state); $cost = $this->calculateCost($zone, $weight); return "Shipping to {$address->city}, {$address->state} costs \${$cost}"; } ``` LarAgent automatically: 1. Detects the DataModel type hint 2. Generates a nested JSON schema from the DataModel properties 3. Converts the LLM's array response back to a DataModel instance ### Nested DataModels DataModels can contain other DataModels for deeply structured data: ```php theme={null} class ContactInfo extends DataModel { #[Desc('Email address')] public string $email; #[Desc('Phone number')] public ?string $phone = null; } class Customer extends DataModel { #[Desc('Customer full name')] public string $name; #[Desc('Contact information')] public ContactInfo $contact; #[Desc('Shipping address')] public Address $shippingAddress; } ``` ```php theme={null} #[Tool('Create a new customer order')] public function createOrder(Customer $customer, array $items): string { // Access nested DataModels directly $email = $customer->contact->email; $city = $customer->shippingAddress->city; return "Order created for {$customer->name} in {$city}"; } ``` ### DataModels with Enums Combine DataModels and Enums for type-safe structured input: ```php theme={null} enum Priority: string { case Low = 'low'; case Medium = 'medium'; case High = 'high'; case Urgent = 'urgent'; } class SupportTicket extends DataModel { #[Desc('Brief description of the issue')] public string $title; #[Desc('Detailed description')] public string $description; #[Desc('Priority level')] public Priority $priority = Priority::Medium; } ``` ```php theme={null} #[Tool('Create a support ticket')] public function createTicket(SupportTicket $ticket): string { if ($ticket->priority === Priority::Urgent) { $this->notifyOnCall($ticket); } return "Ticket created: {$ticket->title} [{$ticket->priority->value}]"; } ``` ### DataModel Arrays Use `DataModelArray` for parameters that accept multiple items: `DataModelArray` is like a collection for DataModels, but strictly typed. See [DataModel documentation](/v1/context/data-model) for details. ```php theme={null} use LarAgent\Core\Abstractions\DataModelArray; class LineItem extends DataModel { #[Desc('Product SKU or ID')] public string $productId; #[Desc('Quantity to order')] public int $quantity; #[Desc('Unit price')] public float $price; } class LineItemArray extends DataModelArray { public static function allowedModels(): array { return [LineItem::class]; } public function getTotal(): float { return array_reduce($this->items, fn($sum, $item) => $sum + ($item->quantity * $item->price), 0); } } ``` ```php theme={null} #[Tool('Process an order with multiple items')] public function processOrder(Customer $customer, LineItemArray $items): string { $total = $items->getTotal(); return "Processing order for {$customer->name}: {$items->count()} items, total: \${$total}"; } ``` `DataModelArray` also supports polymorphic arrays with multiple DataModel types using discriminator fields. See [DataModel documentation](/v1/context/data-model) for details. ## Optional Parameters Use nullable types and/or default values for optional parameters: ```php theme={null} #[Tool('Search for products')] public function searchProducts( string $query, ?string $category = null, int $limit = 10 ): array { $results = Product::search($query); if ($category !== null) { $results = $results->where('category', $category); } return $results->take($limit)->get()->toArray(); } ``` ## Union Types PHP 8 union types allow parameters to accept multiple types: ```php theme={null} #[Tool('Process payment')] public function processPayment( string $orderId, CreditCard|BankAccount|PayPalAccount $paymentMethod ): string { // LarAgent generates a schema with oneOf for union types if ($paymentMethod instanceof CreditCard) { return $this->chargeCreditCard($orderId, $paymentMethod); } // Handle other payment types... } ``` ## Static vs Instance Methods Both static and instance methods work as tools: Use instance methods when you need access to the agent instance (`$this`): ```php theme={null} #[Tool('Get user preferences')] public function getUserPreferences(): array { // Access agent properties or methods return $this->context->get('user_preferences', []); } ``` Use static methods for self-contained tools that don't need agent context: ```php theme={null} #[Tool('Calculate tax')] public static function calculateTax(float $amount, string $state): float { return TaxService::calculate($amount, $state); } ``` Prefer static methods whenever possible — they're slightly more performant and make dependencies explicit. ## Injecting External Data When your tools need data from outside the agent (e.g., from a controller), use custom setter methods: ```php theme={null} namespace App\AiAgents; use LarAgent\Agent; use LarAgent\Attributes\Tool; use App\Models\User; class OrderAgent extends Agent { protected $model = 'gpt-4o-mini'; protected ?User $currentUser = null; public function setUser(User $user): self { $this->currentUser = $user; return $this; } public function instructions() { return 'You help users manage their orders.'; } #[Tool('Get my recent orders')] public function getMyOrders(int $limit = 5): array { if (!$this->currentUser) { return ['error' => 'No user context available']; } return $this->currentUser->orders() ->latest() ->take($limit) ->get() ->toArray(); } #[Tool('Get my account balance')] public function getMyBalance(): string { if (!$this->currentUser) { return 'Unable to retrieve balance: no user context'; } return "Your current balance is \${$this->currentUser->balance}"; } } ``` Call the setter before interacting with the agent: ```php theme={null} // In your controller public function chat(Request $request) { $response = OrderAgent::for($request->user()->id) ->setUser($request->user()) ->respond($request->message); return response()->json(['message' => $response]); } ``` Return `$this` from setter methods to enable fluent chaining with other agent methods. ## Tools Without Parameters Some Tools don't require parameters: ```php theme={null} #[Tool('Get the current server time')] public function getServerTime(): string { return now()->toIso8601String(); } #[Tool('Get available inventory count')] public function getInventoryCount(): int { return Product::where('in_stock', true)->count(); } ``` ## Reusable Tool Traits It's good practice to extract tool groups into traits for reusability and better organization: ```php theme={null} // app/AiAgents/Traits/WeatherTools.php namespace App\AiAgents\Traits; use LarAgent\Attributes\Tool; use App\Enums\TemperatureUnit; trait WeatherTools { #[Tool( description: 'Get the current weather for a location', parameterDescriptions: [ 'location' => 'The city and state, e.g. San Francisco, CA', 'unit' => 'Temperature unit' ] )] public function getWeather(string $location, TemperatureUnit $unit = TemperatureUnit::Celsius): string { return WeatherService::get($location, $unit->value); } #[Tool('Get the weather forecast for the next 5 days')] public function getForecast(string $location): array { return WeatherService::forecast($location, days: 5); } } ``` Use traits in your agents: ```php theme={null} namespace App\AiAgents; use LarAgent\Agent; use App\AiAgents\Traits\WeatherTools; use App\AiAgents\Traits\CalendarTools; class PersonalAssistant extends Agent { use WeatherTools, CalendarTools; protected $model = 'gpt-4o'; public function instructions() { return 'You are a personal assistant that can check weather and manage calendar events.'; } } ``` ```php theme={null} class TravelAgent extends Agent { use WeatherTools; // Reuse weather tools protected $model = 'gpt-4o'; public function instructions() { return 'You help users plan trips and check destination weather.'; } #[Tool('Search for flights')] public function searchFlights(string $origin, string $destination, string $date): array { return FlightService::search($origin, $destination, $date); } } ``` Organizing tools into traits by domain (e.g., `WeatherTools`, `CalendarTools`, `PaymentTools`) creates a library of reusable capabilities you can mix and match across agents. ## Return Values Tools can return various types — LarAgent converts them to strings for the LLM: ```php theme={null} // String - returned as-is #[Tool('Get greeting')] public function getGreeting(): string { return 'Hello, world!'; } // Array - JSON encoded #[Tool('Get user data')] public function getUserData(int $userId): array { return User::find($userId)->toArray(); } // Object with __toString - converted to string #[Tool('Get order status')] public function getOrderStatus(string $orderId): Order { return Order::find($orderId); } ``` Keep tool responses concise. Large responses consume tokens and may confuse the LLM. Return only the information needed to continue the conversation. ## Best Practices Tool and parameter descriptions are the LLM's only guide for when and how to use tools. Be specific about: * What the tool does * Expected input formats * What the tool returns ```php theme={null} // ❌ Vague #[Tool('Get data')] public function getData($id) { ... } // ✅ Clear #[Tool('Retrieve customer order history by customer ID')] public function getOrderHistory( string $customerId ): array { ... } ``` Leverage PHP's type system to constrain inputs: * Use `int`, `float`, `bool` for primitives * Use Enums for fixed option sets * Use DataModels for complex structures * Use nullable types for optional parameters ```php theme={null} // ✅ Strong typing #[Tool('Set ticket priority')] public function setPriority(int $ticketId, Priority $priority): string { ... } ``` Return informative error messages to LLM instead of throwing exceptions: ```php theme={null} #[Tool('Get user by email')] public function getUser(string $email): string { $user = User::where('email', $email)->first(); if (!$user) { return "No user found with email: {$email}"; } return json_encode($user->only(['id', 'name', 'email'])); } ``` Each tool should do one thing well. Split complex operations into multiple tools: ```php theme={null} // ❌ Too broad #[Tool('Manage orders')] public function manageOrder($action, $orderId, $data) { ... } // ✅ Focused #[Tool('Create a new order')] public function createOrder(Customer $customer, LineItemArray $items) { ... } #[Tool('Cancel an existing order')] public function cancelOrder(string $orderId, string $reason) { ... } #[Tool('Get order status')] public function getOrderStatus(string $orderId) { ... } ``` Too many tools can overwhelm the LLM and lead to poor tool selection. As a general guideline: * **Small models** (e.g., GPT-4o-mini, Claude Haiku): Up to **10 tools** * **Large models** (e.g., GPT-4o, Claude Sonnet/Opus): Up to **30 tools** If you need more tools, consider: * Grouping related functionality into fewer, more versatile tools * Using different agents for different domains * Dynamically registering only relevant tools based on context * Orchestrating multiple agents with specialized toolsets ## Next Steps Create reusable tool classes or build tools dynamically at runtime. Configure tool choice, parallel execution, and more. Learn more about DataModel classes for structured data. # Configuration & Runtime Source: https://docs.laragent.ai/v1/tools/configuration Configure tool behavior including tool choice, parallel execution, and runtime tool management. ## Agent Properties Configure tool behavior using these properties in your agent class: ```php theme={null} class MyAgent extends Agent { /** @var array - Tool classes to register with the agent */ protected $tools = [ WeatherTool::class, CalendarTool::class, ]; /** @var bool|null - Whether tools can execute in parallel */ protected $parallelToolCalls = true; /** @var string - Tool selection mode: 'auto', 'none', or 'required' */ protected $toolChoice = 'auto'; } ``` Set `$parallelToolCalls` to `null` or remove it to remove parameter from the request entirely. Some models like `o1` don't support parallel tool calls. ## Tool Choice Control how the LLM selects and uses tools for each request. ### Available Modes | Mode | Description | | ---------- | -------------------------------------------------------------------- | | `auto` | LLM decides whether to use tools (default when tools are registered) | | `none` | Disable tool usage for this request | | `required` | Force the LLM to call at least one tool | ### Runtime Methods ```php theme={null} // Disable tools for this specific call WeatherAgent::for('chat-123') ->toolNone() ->respond('What is your name?'); // Require at least one tool call WeatherAgent::for('chat-123') ->toolRequired() ->respond('What is the weather?'); // Force a specific tool to be used WeatherAgent::for('chat-123') ->forceTool('get_weather') ->respond('Check the weather in Paris'); // Set tool choice programmatically $agent->setToolChoice('none'); $agent->setToolChoice('required'); $agent->setToolChoice('auto'); ``` `toolRequired()` and `forceTool()` only apply to the first LLM call. After that, tool choice automatically switches to `auto` to prevent infinite loops. `forceTool()` requires the tool's name as a parameter, not the method or class name. ## Parallel Tool Calls When enabled, the LLM can request multiple tool calls in a single response, which LarAgent executes together before continuing. ```php theme={null} // Enable parallel tool calls (default) protected $parallelToolCalls = true; // Disable parallel tool calls protected $parallelToolCalls = false; // Remove from request (for models that don't support it) protected $parallelToolCalls = null; ``` At runtime: ```php theme={null} // Enable parallel execution $agent->parallelToolCalls(true); // Disable parallel execution $agent->parallelToolCalls(false); // Remove from request $agent->parallelToolCalls(null); ``` ## Runtime Tool Management Add or remove tools dynamically during execution. ### Adding Tools ```php theme={null} use LarAgent\Tool; // Add a tool class $agent->withTool(WeatherTool::class); // Add an inline tool instance $tool = Tool::create('get_time', 'Get current server time') ->setCallback(fn() => now()->toIso8601String()); $agent->withTool($tool); ``` ### Removing Tools ```php theme={null} // Remove by tool name $agent->removeTool('get_weather'); // Remove by class name $agent->removeTool(WeatherTool::class); // Remove by instance $agent->removeTool($tool); ``` ### Conditional Tools Register tools based on runtime conditions: ```php theme={null} $user = auth()->user(); $agent = MyAgent::for($user->id); // Add tools based on user permissions if ($user->can('manage_calendar')) { $agent->withTool(CalendarTool::class); } if ($user->isAdmin()) { $agent->withTool(AdminTool::class); } $response = $agent->respond($message); ``` ## Next Steps Tools that return control to your application for external handling. Create tools using the #\[Tool] attribute. Build reusable tool classes or create tools dynamically. # Model Context Protocol (MCP) Source: https://docs.laragent.ai/v1/tools/mcp Integrate external MCP servers to dynamically extend your agent with tools and resources ## Overview The Model Context Protocol (MCP) integration enables your Agents to dynamically discover and use **tools** and **resources** provided by external MCP servers. This powerful feature allows you to extend your agent's capabilities without writing custom tool implementations. MCP support is provided through the `redberry/mcp-client-laravel` package, which is included as a dependency. ## Configuration Configure MCP servers in your `config/laragent.php` file under the `mcp_servers` key. LarAgent supports two transport types: HTTP and STDIO. ### HTTP Transport Use HTTP transport for MCP servers accessible via HTTP endpoints: ```php config/laragent.php theme={null} 'mcp_servers' => [ 'github' => [ 'type' => \Redberry\MCPClient\Enums\Transporters::HTTP, 'base_url' => 'https://api.githubcopilot.com/mcp', 'timeout' => 30, // Shorthand for bearer auth 'token' => env('GITHUB_API_TOKEN', null), 'headers' => [ // Add custom headers here ], // 'string' or 'int' - controls JSON-RPC id type (default: 'int') 'id_type' => 'int', ], ], ``` Everything except "type" & "base\_url" are optional, use only when you need them. ### STDIO Transport Use STDIO transport for command-line MCP servers: ```php config/laragent.php theme={null} 'mcp_servers' => [ 'mcp_server_memory' => [ 'type' => \Redberry\MCPClient\Enums\Transporters::STDIO, 'command' => [ 'npx', '-y', '@modelcontextprotocol/server-memory', ], 'timeout' => 30, 'cwd' => base_path(), // milliseconds - delay after process start (default: 100) 'startup_delay' => 100, // milliseconds - polling interval for response (default: 20) 'poll_interval' => 20, ], ], ``` Everything, except "type", "cwd" & "command", are optional, use only when you need them. You can configure multiple MCP servers simultaneously by adding more entries to the `mcp_servers` array. ## Registering MCP Servers in Agents Once configured, register MCP servers in your agent class using either a property or a method. ### Using Property The simplest approach is to define the `$mcpServers` property: ```php theme={null} use LarAgent\Agent; class MyAgent extends Agent { protected $mcpServers = [ 'github', 'mcp_server_memory', ]; } ``` ### Using Method For more control and dynamic nature, implement the `registerMcpServers()` method: ```php theme={null} use LarAgent\Agent; class MyAgent extends Agent { public function registerMcpServers() { return [ 'github', 'mcp_server_memory', ]; } } ``` ## Filtering Tools and Resources By default, LarAgent fetches only **tools** from MCP servers. You can explicitly control what to fetch using filters. ### Fetch Specific Types Use the `:tools` or `:resources` suffix to specify what to fetch: ```php theme={null} protected $mcpServers = [ 'mcp_server_memory:tools', 'mcp_everything:resources', ]; ``` ### Include/Exclude Specific Items Use `only` and `except` filters to control which tools or resources are registered: ```php Only specific tools theme={null} public function registerMcpServers() { return [ 'github:tools|only:search_repositories,get_issues', ]; } ``` ```php Exclude specific tools theme={null} public function registerMcpServers() { return [ 'mcp_server_memory:tools|except:delete_entities,delete_observations,delete_relations', ]; } ``` ```php Resources with filters theme={null} public function registerMcpServers() { return [ 'mcp_everything:resources|only:Resource 1,Resource 2', ]; } ``` When using filters, separate multiple items with commas and ensure there are no spaces after the commas. ## Working with Resources Resources are data sources provided by MCP servers. While tools are automatically registered, resources need to be explicitly requested via `"mcp_server_name:resources"`. Alternatively, resources and tools can be accessed manually through the `mcpClient` property. ### Manual Resource Access Access resources directly in your agent methods: ```php theme={null} use LarAgent\Agent; class MyAgent extends Agent { protected $mcpServers = ['mcp_everything']; public function instructions() { // Read a resource from the MCP server $resourceData = $this->mcpClient ->connect('mcp_everything') ->readResource('test://static/resource/1'); // Use resource data in your instructions $context = $resourceData['contents'][0]['text']; return "You are a helpful assistant. Context: {$context}"; } } ``` ### Resource Response Structure The `readResource()` method returns an array with the following structure: ```php theme={null} [ "contents" => [ [ "uri" => "test://static/resource/1", "name" => "Resource 1", "mimeType" => "text/plain", "text" => "Resource 1: This is a plaintext resource" ] ] ] ``` Array containing resource content objects. Unique identifier for the resource following URI format. Human-readable name of the resource. MIME type indicating the resource content format (e.g., `text/plain`, `application/json`). The actual resource content as text. ### Registering Resources as Tools You can make resources available as tools by explicitly requesting them: ```php theme={null} protected $mcpServers = [ 'mcp_everything:resources', ]; ``` When registered this way, resources become callable tools that your agent can use during conversations. Description of resource is appended with "Read the resource: ". So, if resource description is "Revenue report 2025", the tool description will be: "Read the resource: Revenue report 2025" ### Manual Tool Access You can run MCP tools manually via `mcpClient` property and `callTool` method: ```php theme={null} #[Tool("Get issue details from LarAgent repository")] public function readTheIssue(int $issue_number) { $args = [ "issue_number" => $issue_number, "owner" => "maestroerror", "repo" => "LarAgent" ]; return $this->mcpClient->connect("github")->callTool("get_issue", $args); } ``` This way, you can write your own descriptions to the MCP tools, also hard-code/restrict the arguments while making the error surface smaller. In this example above, if I am building an agent to work only with LarAgent issues, why should LLM generate the same "owner" and "repo" parameters for MCP tool each time? They are the static and should be in every request. ## How MCP Tools Work MCP tools are dynamically constructed based on metadata from the MCP server: LarAgent connects to the configured MCP server and retrieves available tools and their schemas. Tools are automatically registered with your agent, including their parameters, descriptions, and required fields. When the LLM decides to use an MCP tool, LarAgent handles the invocation and returns results to the conversation. MCP tools integrate seamlessly with your agent's existing tool system, appearing alongside custom tools. Initialization -> fetching -> registration of tools: The process is pretty intensive, using more than 3 mcp servers per Agent can dramatically decrease the performance / response time ## Complete Example Here's a comprehensive example combining multiple MCP servers with filtering: ```php theme={null} mcpClient ->connect('knowledge_base') ->readResource('docs://api_reference'); $context = $apiDocs['contents'][0]['text'] ?? ''; return "You are a research assistant with access to GitHub and memory storage. Use the available tools to search repositories, track information, and answer questions. API Reference: {$context}"; } } ``` ## Tool Caching MCP tools require network calls to fetch tool definitions from MCP servers, which can add significant latency to agent initialization. LarAgent provides automatic tool caching to dramatically improve performance for subsequent requests. ### Configuration Configure MCP tool caching in your `config/laragent.php` file: ```php config/laragent.php theme={null} 'mcp_tool_caching' => [ 'enabled' => env('MCP_TOOL_CACHE_ENABLED', false), 'ttl' => env('MCP_TOOL_CACHE_TTL', 3600), // 1 hour 'store' => env('MCP_TOOL_CACHE_STORE', null), // Cache store to use (null = default) ], ``` Enable or disable MCP tool caching. Cache time-to-live in seconds. Default is 1 hour (3600 seconds). The cache store to use. Set to `null` to use Laravel's default cache store, or specify a dedicated store like `redis`. ### Enabling Caching Enable caching globally in your `.env` file: ```bash .env theme={null} MCP_TOOL_CACHE_ENABLED=true MCP_TOOL_CACHE_TTL=3600 MCP_TOOL_CACHE_STORE=redis # Optional: use dedicated store ``` ### Per-Agent Configuration You can also configure tool caching on individual agents by setting properties directly in your agent class: ```php theme={null} use LarAgent\Agent; class ResearchAgent extends Agent { protected $mcpServers = ['github', 'mcp_server_memory']; // Enable caching for this agent protected $toolCaching = true; // Cache TTL in seconds (default: 3600) protected $toolCacheTtl = 7200; // Cache store to use (null = default store) protected $toolCacheStore = 'redis'; } ``` Per-agent configuration overrides the global settings from `config/laragent.php`, allowing you to fine-tune caching behavior for specific agents. ### How It Works Once enabled, caching happens automatically with no manual intervention needed: ```php theme={null} use App\Agents\ResearchAgent; // First call: fetches from MCP server + caches tools $agent = ResearchAgent::for('user_123'); $response = $agent->respond('Hello'); // Tools fetched & cached // Subsequent calls: uses cache (no network call) $agent2 = ResearchAgent::for('user_456'); $response2 = $agent2->respond('Hi'); // Tools loaded from cache ⚡ ``` The cache is shared across all users and agent instances. Tool definitions are cached based on the MCP server name and the fetch method (tools/resources). ### Clearing the Cache Clear the MCP tool cache when you need to refresh tool definitions: ```bash theme={null} php artisan agent:tool-clear ``` Run this command after updating your MCP servers or when tool definitions change. The clear command is production-safe for Redis. It uses `SCAN` instead of `KEYS` to prevent blocking and correctly handles Laravel's cache prefix. ## Best Practices * Use **HTTP transport** for cloud-based or remote MCP servers * Use **STDIO transport** for local command-line tools and npm packages * Consider timeout values based on expected response times Use `except` to exclude dangerous or unnecessary operations (like delete functions). Use `only` when you need a specific subset of tools for focused agents. Start with all tools and refine based on actual usage patterns Always use environment variables for API tokens and sensitive credentials; Never hardcode tokens directly in configuration files; Review MCP server documentation for required authentication * Load resources in `instructions()` when context is needed * Cache resource data when appropriate to avoid repeated fetches * Consider resource size impact on token usage ## Troubleshooting Ensure MCP server commands are available in your environment. For STDIO transport with npm packages, verify Node.js and npm/npx are installed and accessible. **For HTTP transport:** * Verify the `base_url` is correct and accessible * Check authentication tokens are valid * Ensure firewall rules allow outbound connections **For STDIO transport:** * Confirm the command exists (`npx`, `node`, etc.) * Verify the working directory (`cwd`) has proper permissions * Check the command array syntax is correct **Tools and other issues:** * Verify the MCP server name matches your configuration exactly * Check filter syntax if using `only` or `except` * Ensure the MCP server actually provides tools (not just resources) * Review logs for connection or parsing errors * Increase the `timeout` value in your MCP server configuration * For STDIO servers, ensure the command starts quickly * Consider network latency for HTTP servers ## Related Resources Learn how custom tools work alongside MCP tools Understand the agent architecture and configuration # Classes & Inline Tools Source: https://docs.laragent.ai/v1/tools/other-tools Create reusable tool classes for complex functionality or build tools dynamically at runtime using the fluent API. While the [#\[Tool\] attribute](/v1/tools/attribute-tools) is recommended for most use cases, Tool Classes and Inline Tools offer additional flexibility for reusable or dynamically generated tools. ## Tool Classes Tool classes are standalone PHP classes that encapsulate tool logic. They're ideal for: * Complex tools with extensive logic * Tools shared across multiple agents * Tools requiring their own dependencies or configuration ### Creating a Tool Class ```php theme={null} namespace App\AiTools; use LarAgent\Tool; class WeatherTool extends Tool { protected string $name = 'get_weather'; protected string $description = 'Get the current weather for a location'; protected array $properties = [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA', ], 'unit' => [ 'type' => 'string', 'description' => 'Temperature unit', 'enum' => ['celsius', 'fahrenheit'], ], ]; protected array $required = ['location']; public function execute(array $input): mixed { $location = $input['location']; $unit = $input['unit'] ?? 'celsius'; return WeatherService::get($location, $unit); } } ``` ### Registering Tool Classes Add tool classes to the `$tools` property in your agent: ```php theme={null} class WeatherAgent extends Agent { protected $tools = [ \App\Tools\WeatherTool::class, \App\Tools\LocationTool::class, ]; } ``` Or register them in the `registerTools()` method by instantiating: ```php theme={null} public function registerTools() { return [ new \App\AiTools\WeatherTool(), new \App\AiTools\LocationTool(), ]; } ``` Or add them at runtime: ```php theme={null} $agent->withTool(WeatherTool::class); // or $agent->withTool(new WeatherTool()); ``` ### Tool Class Properties | Property | Type | Description | | -------------- | -------- | ------------------------------------------------- | | `$name` | `string` | Unique identifier for the tool | | `$description` | `string` | Description shown to the LLM | | `$properties` | `array` | Parameter definitions with types and descriptions | | `$required` | `array` | List of required parameter names | | `$metaData` | `array` | Optional metadata (not sent to LLM) | ### Property Definitions Properties are OpenAPI compatible schemas. Each property in `$properties` can have: ```php theme={null} protected array $properties = [ 'param_name' => [ 'type' => 'string', // string, number, integer, boolean, array, object 'description' => 'Help text', 'enum' => ['a', 'b', 'c'], // Optional: restrict to specific values ], ]; ``` *** ## Inline Tools Inline tools are created programmatically using the fluent `Tool` API. They're ideal for: * Tools that depend on runtime context (user state, request data) * Dynamically generated tools based on configuration * Quick prototyping before extracting to a class ### Creating Inline Tools Use the `registerTools()` method in your agent: ```php theme={null} use LarAgent\Tool; class MyAgent extends Agent { public function registerTools() { $user = auth()->user(); return [ Tool::create('get_user_location', "Get the current user's location") ->setCallback(fn() => $user->location->city), Tool::create('get_weather', 'Get weather for a location') ->addProperty('location', 'string', 'City name') ->addProperty('unit', 'string', 'Temperature unit', ['celsius', 'fahrenheit']) ->setRequired('location') ->setCallback(fn($location, $unit = 'celsius') => WeatherService::get($location, $unit) ), ]; } } ``` ### Fluent API Reference ```php theme={null} Tool::create(string $name, string $description) ->addProperty(string $name, string $type, string $description, ?array $enum = null) ->setRequired(string|array $properties) ->setCallback(callable $callback); ``` ### Callback Options The `setCallback()` method accepts any PHP callable, including: ```php theme={null} // Closure ->setCallback(fn($param) => doSomething($param)) // Function name ->setCallback('myGlobalFunction') // Class method (array syntax) ->setCallback([$this, 'methodName']) ``` ### Runtime Tool Addition Add inline tools at runtime using `withTool()`: ```php theme={null} $tool = Tool::create('custom_tool', 'Do something custom') ->addProperty('input', 'string', 'The input value') ->setCallback(fn($input) => processInput($input)); $response = MyAgent::for('user-123') ->withTool($tool) ->respond('Use the custom tool'); ``` ### Context-Aware Tools Example ```php theme={null} public function registerTools() { $user = auth()->user(); $tools = []; // Always available $tools[] = Tool::create('get_time', 'Get current server time') ->setCallback(fn() => now()->toIso8601String()); // User-specific tool $tools[] = Tool::create('get_my_orders', 'Get my recent orders') ->addProperty('limit', 'integer', 'Number of orders to return') ->setCallback(fn($limit = 5) => $user->orders()->latest()->take($limit)->get()->toArray() ); // Permission-based tool if ($user->can('view_reports')) { $tools[] = Tool::create('get_sales_report', 'Get sales report') ->addProperty('period', 'string', 'Time period', ['day', 'week', 'month']) ->setCallback(fn($period) => ReportService::sales($period)); } return $tools; } ``` *** ## Choosing the Right Approach | Approach | Best For | | ---------------------------------------------------- | -------------------------------------------------- | | **[#\[Tool\] Attribute](/v1/tools/attribute-tools)** | Most use cases — simple, type-safe, IDE support | | **Tool Classes** | Complex, reusable tools with multiple dependencies | | **Inline Tools** | Dynamic, context-dependent, or quick prototypes | Start with the `#[Tool]` attribute. Use Inline Tools when the tool depends on runtime context. ## Next Steps The recommended way to create tools. Configure tool choice and parallel execution. # Overview Source: https://docs.laragent.ai/v1/tools/overview Tools extend your agent's capabilities, allowing them to perform actions like calling APIs, querying databases, or executing any custom logic. Tools (also known as function calling) allow your AI agents to interact with external systems and services, expanding their capabilities beyond text generation. When an LLM needs to perform an action, it calls a tool and uses the result to formulate its response. ## What are Tools? In LarAgent, a **Tool** is a function or class that an agent can invoke to perform actions or retrieve information. Tools bridge the gap between AI reasoning and real-world operations. Common use cases include: * Fetching data from APIs or databases * Sending emails or notifications * Performing calculations * Interacting with external services When an agent determines it needs information or needs to take action, it calls the appropriate tool, receives the result, and uses it to continue the conversation. ## Creating Tools LarAgent offers three ways to define tools, each suited for different use cases: **Recommended.** Transform agent methods into tools with a simple attribute — best for most use cases. Create reusable tool classes or build tools dynamically at runtime. ### Quick Examples The simplest approach — add the `#[Tool]` attribute to any method in your agent class: ```php theme={null} use LarAgent\Attributes\Tool; #[Tool('Get the current weather for a location')] public function getWeather(string $location, string $unit = 'celsius'): string { return WeatherService::get($location, $unit); } ``` Create a dedicated class for complex or reusable tools: ```php theme={null} class WeatherTool extends LarAgent\Tool { protected string $name = 'get_weather'; protected string $description = 'Get the current weather for a location'; protected array $properties = [ 'location' => [ 'type' => 'string', 'description' => 'City and state, e.g. San Francisco, CA', ], ]; public function execute(array $input): mixed { return WeatherService::get($input['location']); } } ``` Build tools dynamically using the fluent API: ```php theme={null} use LarAgent\Tool; Tool::create('get_weather', 'Get the current weather') ->addProperty('location', 'string', 'City name') ->setCallback(fn($location) => WeatherService::get($location)); ``` ## Tool Execution Flow When an agent uses tools: The LLM analyzes the conversation and determines a tool call is needed. LarAgent automatically executes the requested tool with the provided arguments. The tool's return value is sent back to the LLM as context. The LLM uses the tool result to formulate its final response. LarAgent handles the entire tool execution loop automatically — you just define your tools and the agent does the rest. ## Next Steps Learn how to create tools using the #\[Tool] attribute on agent methods. Build reusable tool classes or create tools dynamically. Get type-safe responses using DataModels and schemas. # Phantom Tools Source: https://docs.laragent.ai/v1/tools/phantom-tools Phantom Tools return control to your application instead of executing automatically, enabling external handling, user confirmation, and API integration. Phantom Tools follow the same interface as regular tools but instead of automatic execution, they return the tool call details to your application — giving you full control over when and how the tool is executed. ## What are Phantom Tools? When the LLM calls a regular tool, LarAgent automatically executes it and feeds the result back to the LLM. **Phantom Tools** break this cycle — they return the tool call details to your application, allowing you to: * Handle execution externally (different service, API, etc.) * Request user confirmation before proceeding * Expose tool calls through your own API * Queue execution for background processing ## Creating Phantom Tools ### At Runtime ```php theme={null} use LarAgent\PhantomTool; $phantomTool = PhantomTool::create('process_payment', 'Process a payment transaction') ->addProperty('amount', 'number', 'Payment amount in cents') ->addProperty('currency', 'string', 'Currency code (e.g., USD, EUR)') ->addProperty('description', 'string', 'Payment description') ->setRequired('amount') ->setRequired('currency'); $agent->withTool($phantomTool); ``` ### In Agent Class Define phantom tools in the `registerTools()` method: ```php theme={null} use LarAgent\Agent; use LarAgent\PhantomTool; class PaymentAgent extends Agent { protected $model = 'gpt-4o'; public function instructions() { return 'You help users process payments and manage transactions.'; } public function registerTools() { return [ PhantomTool::create('process_payment', 'Process a payment transaction') ->addProperty('amount', 'number', 'Payment amount in cents') ->addProperty('currency', 'string', 'Currency code') ->setRequired('amount') ->setRequired('currency'), PhantomTool::create('refund_payment', 'Refund a previous payment') ->addProperty('transaction_id', 'string', 'Original transaction ID') ->addProperty('reason', 'string', 'Reason for refund') ->setRequired('transaction_id'), ]; } } ``` ## Handling Phantom Tool Calls When the LLM decides to call a phantom tool, the agent returns an array with `tool_calls` instead of a text response (or `ToolCallMessage` when using `->returnMessage()`): ```php theme={null} use LarAgent\Message; use LarAgent\Messages\ToolCallMessage; $agent = PaymentAgent::for('user-123'); $response = $agent->respond('Charge $50 for the subscription'); // Response is an array when phantom tools are called if (is_array($response) && isset($response['tool_calls'])) { foreach ($response['tool_calls'] as $toolCall) { $id = $toolCall['id']; // Tool call ID $name = $toolCall['function']['name']; // 'process_payment' $args = json_decode($toolCall['function']['arguments'], true); // ['amount' => 5000, 'currency' => 'USD'] // Handle the tool execution externally $result = match($name) { 'process_payment' => $this->paymentService->charge($args), 'refund_payment' => $this->paymentService->refund($args), default => ['error' => 'Unknown tool'], }; // Add tool result to chat history and continue conversation $agent->addMessage(Message::toolResult(json_encode($result), $id, $name)); } // Continue the conversation with tool results $finalResponse = $agent->respond(); } ``` Use `->returnMessage()` before `respond()` to get a `ToolCallMessage` instance instead of an array, giving you access to the `getToolCalls()` method with `ToolCall` objects. ### Multiple Phantom Tool Calls When parallel tool calls are enabled, the LLM may request multiple phantom tools at once: ```php theme={null} use LarAgent\Message; $agent = PaymentAgent::for('user-123'); $response = $agent->respond('Charge $50 and send a receipt email'); if (is_array($response) && isset($response['tool_calls'])) { // Process all tool calls foreach ($response['tool_calls'] as $toolCall) { $id = $toolCall['id']; $name = $toolCall['function']['name']; $args = json_decode($toolCall['function']['arguments'], true); $result = $this->executeExternally($name, $args); // Add each tool result to the chat history (result must be a string) $agent->addMessage(Message::toolResult(json_encode($result), $id, $name)); } // Continue conversation after all tool results are added $finalResponse = $agent->respond(); } ``` ## Use Cases Phantom Tools where created to support user provided external tools while [exposing Agents via API](v1/agents/agent-via-api), but they can be useful in many scenarios Hand off execution to external APIs or microservices that require special authentication or handling. Pause for user approval before executing sensitive or irreversible actions. Make tool calls available through your API for frontend or mobile app handling. Queue tool execution for background processing with job queues. ### User Confirmation Example ```php theme={null} use LarAgent\Message; // Controller method public function chat(Request $request) { $agent = PaymentAgent::for($request->user()->id); $response = $agent->respond($request->message); if (is_array($response) && isset($response['tool_calls'])) { $toolCall = $response['tool_calls'][0]; // Get first tool call $args = json_decode($toolCall['function']['arguments'], true); // Store tool call info in session for later confirmation session(['pending_tool_call' => [ 'id' => $toolCall['id'], 'name' => $toolCall['function']['name'], 'arguments' => $args, ]]); // Return tool call details to frontend for confirmation return response()->json([ 'requires_confirmation' => true, 'tool_call_id' => $toolCall['id'], 'tool_name' => $toolCall['function']['name'], 'arguments' => $args, 'confirmation_message' => "Process payment of \${$args['amount'] / 100}?", ]); } return response()->json(['message' => $response]); } // Confirmation endpoint public function confirmToolCall(Request $request) { $pending = session('pending_tool_call'); $result = $this->paymentService->charge($pending['arguments']); $agent = PaymentAgent::for($request->user()->id); $agent->addMessage(Message::toolResult( json_encode($result), $pending['id'], $pending['name'] )); $response = $agent->respond(); session()->forget('pending_tool_call'); return response()->json(['message' => $response]); } ``` ### Background Processing Example ```php theme={null} use App\Jobs\ProcessToolCall; use LarAgent\Message; $agent = PaymentAgent::for($chatId); $response = $agent->respond($message); if (is_array($response) && isset($response['tool_calls'])) { foreach ($response['tool_calls'] as $toolCall) { // Dispatch each tool call to queue ProcessToolCall::dispatch( agentClass: PaymentAgent::class, chatId: $chatId, toolCallId: $toolCall['id'], toolName: $toolCall['function']['name'], arguments: json_decode($toolCall['function']['arguments'], true), ); } return 'Your request is being processed...'; } ``` The job handler would then: ```php theme={null} // In ProcessToolCall job public function handle() { $result = $this->executeToolExternally($this->toolName, $this->arguments); $agent = ($this->agentClass)::for($this->chatId); $agent->addMessage(Message::toolResult( json_encode($result), $this->toolCallId, $this->toolName )); // Continue conversation or notify user $response = $agent->respond(); // ... notify user of completion } ``` ## Mixing Regular and Phantom Tools You can use both regular and phantom tools in the same agent: ```php theme={null} use LarAgent\Agent; use LarAgent\PhantomTool; use LarAgent\Attributes\Tool; class AssistantAgent extends Agent { // Regular tool - executes automatically #[Tool('Get the current time')] public function getTime(): string { return now()->toIso8601String(); } // Regular tool - executes automatically #[Tool('Search for products')] public function searchProducts(string $query): array { return Product::search($query)->take(5)->get()->toArray(); } public function registerTools() { return [ // Phantom tool - returns for external handling PhantomTool::create('place_order', 'Place an order for products') ->addProperty('product_ids', 'array', 'Product IDs to order') ->addProperty('quantities', 'array', 'Quantities for each product') ->setRequired('product_ids') ->setRequired('quantities'), ]; } } ``` When mixing regular and phantom tools: if the LLM calls both types in parallel, regular tools execute first, then the phantom tool call is returned. The conversation only returns to you when all regular tool executions are complete and at least one phantom tool was called. Use regular tools for safe, read-only operations and phantom tools for actions that modify state, cost money, or require confirmation. ## Using returnMessage() for Type-Safe Access For more control, use `returnMessage()` to get a `ToolCallMessage` instance instead of an array: ```php theme={null} use LarAgent\Message; use LarAgent\Messages\ToolCallMessage; $agent = PaymentAgent::for('user-123')->returnMessage(); $response = $agent->respond('Process my payment'); if ($response instanceof ToolCallMessage) { $toolCalls = $response->getToolCalls(); // ToolCallArray collection foreach ($toolCalls as $toolCall) { $id = $toolCall->getId(); // string $name = $toolCall->getToolName(); // string $args = json_decode($toolCall->getArguments(), true); $result = $this->executeExternally($name, $args); $agent->addMessage(Message::toolResult(json_encode($result), $id, $name)); } $finalResponse = $agent->respond(); } ``` ## Phantom Tool Properties Phantom tools support the same property definitions as regular tools: ```php theme={null} PhantomTool::create('create_invoice', 'Create and send an invoice') ->addProperty('customer_id', 'string', 'Customer ID') ->addProperty('items', 'array', 'Line items for the invoice') ->addProperty('due_date', 'string', 'Due date in ISO 8601 format') ->addProperty('send_email', 'boolean', 'Whether to send email notification') ->setRequired('customer_id') ->setRequired('items'); ``` ### With Enum Constraints ```php theme={null} PhantomTool::create('update_status', 'Update order status') ->addProperty('order_id', 'string', 'Order ID') ->addProperty('status', 'string', 'New status', ['pending', 'processing', 'shipped', 'delivered']) ->setRequired('order_id') ->setRequired('status'); ``` ## Next Steps Configure tool choice, parallel execution, and runtime management. Create tools using the #\[Tool] attribute. Build reusable tool classes or create tools dynamically. Learn about handling agent responses.