├── .gitignore ├── LICENSE ├── README.md ├── monolithic-app ├── .env-example ├── README.md ├── data.json-example ├── images │ └── MonolithArchitecture.png ├── index.js └── package.json ├── stage1-architecture ├── README.md ├── data.json-example ├── delivery-agent │ ├── .env-example │ ├── agent.js │ ├── index.js │ ├── package.json │ └── schema.json ├── images │ └── ArchitectureRethink1.png ├── inventory-tool-service │ ├── .env-example │ ├── index.js │ ├── package.json │ ├── schema.json │ └── svc.js ├── product-advisor-agent │ ├── .env-example │ ├── agent.js │ ├── index.js │ ├── package.json │ └── schema.json ├── purchasing-tool-service │ ├── .env-example │ ├── index.js │ ├── package.json │ ├── schema.json │ └── svc.js ├── stage_reset.sh └── stage_setup.sh ├── stage2-consistency ├── README.md ├── data.json-example ├── delivery-agent │ ├── .env-example │ ├── agent.js │ ├── index.js │ ├── package.json │ └── schema.json ├── images │ └── ReliableState.png ├── inventory-tool-service │ ├── .env-example │ ├── index.js │ ├── package.json │ ├── schema.json │ └── svc.js ├── product-advisor-agent │ ├── .env-example │ ├── agent.js │ ├── index.js │ ├── package.json │ └── schema.json ├── purchasing-tool-service │ ├── .env-example │ ├── index.js │ ├── package.json │ ├── schema.json │ └── svc.js ├── stage_reset.sh └── stage_setup.sh └── stage3-grounding ├── README.md ├── assist-grounding.yaml ├── data.json-example ├── delivery-agent ├── .env-example ├── agent.js ├── index.js ├── package.json └── schema.json ├── images └── ReliablePlans.png ├── inventory-tool-service ├── .env-example ├── index.js ├── package.json ├── schema.json └── svc.js ├── product-advisor-agent ├── .env-example ├── agent.js ├── index.js ├── package.json └── schema.json ├── purchasing-tool-service ├── .env-example ├── index.js ├── package.json ├── schema.json └── svc.js ├── stage_reset.sh └── stage_setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | .env.production 9 | 10 | # Build files 11 | dist/ 12 | build/ 13 | 14 | # Logs 15 | logs/ 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Runtime data 22 | .orra-data/ 23 | .orra-service-key.json 24 | 25 | # OS specific files 26 | .DS_Store 27 | Thumbs.db 28 | 29 | # Data 30 | monolithic-app/data.json 31 | stage1-architecture/data.json 32 | stage2-consistency/data.json 33 | stage3-grounding/data.json 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 orra-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # From Fragile to Production-Ready Multi-Agent App 2 | 3 | This guide demonstrates how to transform an AI-powered Marketplace Assistant into a production-ready multi-agent application using [orra](https://github.com/orra-dev/orra). 4 | 5 | ## Overview 6 | 7 | We'll explore how to build a marketplace assistant that helps users find and purchase products. This guide progressively improves the application by addressing common production challenges in multi-agent AI systems. 8 | 9 | This guide is a compliment to the [Wrangling Wild Agents](https://docs.google.com/presentation/d/1hTegIOTg4tuzU2EJck_dkWYUqw9VsthHtKBmNNJJ1vI/edit?usp=sharing) talk by the orra team, presented at the [AI in Production](https://home.mlops.community/home/videos/wrangling-wild-agents-ezo-saleh-and-aisha-yusaf-ai-in-production-2025-03-21) 2025 conference. We'll be using various sections of that talk in this guide. 10 | 11 | ## Guide Progression: 3 Stages to Production Readiness 12 | 13 | ### [Stage 0: Monolithic Agent](./monolithic-app) 14 | - The original AI Marketplace Agent implementation 15 | - [View implementation and details](./monolithic-app/README.md) 16 | 17 | ### [Stage 1: Architecture Re-think with orra](./stage1-architecture) 18 | - Build a distributed system with specialized agents and tools as services 19 | - Integrate with orra for coordination, optimal performance, reduced costs and out of the box reliability 20 | - Implement efficient communication between components with execution plans 21 | - [View implementation and details](./stage1-architecture/README.md) 22 | 23 | ### [Stage 2: Reliable Consistency with orra](./stage2-consistency) 24 | - Add compensation handlers for critical operations 25 | - Ensure system consistency during failures 26 | - Implement automatic recovery mechanisms 27 | - [View implementation and details](./stage2-consistency/README.md) 28 | 29 | ### [Stage 3: Reliable Planning with orra](./stage3-grounding) 30 | - Ground all jobs and actions in your intended domain only 31 | - Define use cases with clear capability patterns 32 | - Prevent hallucinated plans and invalid actions - before plans are executed 33 | - [View implementation and details](./stage3-grounding/README.md) 34 | 35 | ## Guide Components 36 | 37 | Each component demonstrates orra's capabilities: 38 | 39 | - **Product Advisor Agent**: LLM-powered product recommendation engine 40 | - **Inventory Service**: Simulated inventory database with holds and releases 41 | - **Delivery Agent**: Estimates delivery times based on various factors 42 | - **Purchasing Service**: A product purchasing that creates orders, makes payments with occasional failures and notifies users 43 | 44 | ## Getting Started 45 | 46 | 1. Make sure you have Node.js installed (v18 or later) 47 | 2. Clone this repository 48 | 3. Follow the orra's [installation instructions](https://github.com/orra-dev/orra?tab=readme-ov-file#installation). 49 | 4. Follow the instructions in each stage's README.md file 50 | 5. Run the provided scripts in each stage to see the improvements in action 51 | 52 | ## Example User Interaction 53 | 54 | ``` 55 | User: "I need a used laptop for college that is powerful enough for programming, under $800." 56 | 57 | System: 58 | - Product Advisor Agent analyzes request, understands parameters 59 | - Inventory Service checks available options 60 | - Product Advisor Agent recommends: "I found a Dell XPS 13 (2022) with 16GB RAM, 512GB SSD for $750." 61 | 62 | User: "That sounds good. Can I get it delivered by next week?" 63 | 64 | System: 65 | - Delivery Agent calculates real-time delivery options 66 | - "Yes, it can be delivered by Thursday. Would you like to proceed with purchase?" 67 | 68 | User: "Yes, let's do it." 69 | 70 | System: 71 | - Orchestrates parallel execution: 72 | - Inventory Service places hold on laptop 73 | - Delivery Agent provides delivery estimate 74 | - Purchasing service places order and notifies user 75 | ``` 76 | 77 | ## Guide Structure 78 | 79 | Each stage builds upon the previous one and includes: 80 | - A dedicated folder with complete code 81 | - A detailed README explaining: 82 | - The problem being addressed 83 | - The solution implemented with orra 84 | - Key benefits and improvements 85 | - Architecture diagrams and examples 86 | - Runnable code to demonstrate each concept 87 | 88 | ## Key Takeaways 89 | 90 | - Multi-agent systems require careful orchestration 91 | - Production-ready AI applications need reliable transaction handling 92 | - Domain grounding prevents hallucinated plans and actions 93 | - An audit trail is essential for system reliability 94 | - orra provides a comprehensive platform for building robust AI applications 95 | 96 |
97 | Discuss on Hacker News 98 | -------------------------------------------------------------------------------- /monolithic-app/.env-example: -------------------------------------------------------------------------------- 1 | # LLM API Keys 2 | OPENAI_API_KEY=xxx 3 | 4 | # Application Settings 5 | PORT=3000 6 | -------------------------------------------------------------------------------- /monolithic-app/README.md: -------------------------------------------------------------------------------- 1 | # Marketplace Assistant - Initial Monolithic Application 2 | 3 | This is the initial implementation of our AI-powered Marketplace Assistant. It's a monolithic application that handles all aspects of the shopping experience, from product recommendations to delivery estimation, purchase processing, and notifications. 4 | 5 | ## Architecture 6 | 7 | This a classic Agent setup where an LLM uses tool calling to find info and execute tasks. 8 | 9 | There's a minor twist where we also call another Agent using a tool call (simulated for delivery estimates). 10 | 11 | - Understanding user requests 12 | - Product recommendations 13 | - Inventory management 14 | - Purchase processing 15 | - Delivery estimation 16 | - Customer notifications 17 | 18 | _We can break this into a crew of agents, but we’d still be dealing with similar limitations for production!_ 19 | 20 | ![](images/MonolithArchitecture.png) 21 | 22 | ## Design Limitations 23 | 24 | This initial implementation has several limitations: 25 | 26 | 1. **High Latency**: The monolithic agent must process all aspects of a user request sequentially. 27 | 2. **Token Inefficiency**: The whole context is passed to the LLM for every operation, even for deterministic tasks. 28 | 3. **Reliability Issues**: A failure in any component impacts the entire application. 29 | 4. **Debugging Complexity**: Difficult to isolate issues within the monolith. 30 | 5. **Scalability Challenges**: The entire application needs to scale together. 31 | 6. **Managing State Across failures is hard**: When failures occur mid-transaction, the system can be left in an inconsistent state. 32 | 33 | ### Critical Bug: Inventory Inconsistency 34 | 35 | The application has a critical issue that demonstrates the need for proper transaction handling: 36 | 37 | 1. When a user attempts to purchase a product, the inventory is reduced BEFORE payment processing 38 | 2. If the payment fails (simulated 50% of the time) because the Payment Gateway is down, the inventory remains reduced 39 | 3. This creates a data inconsistency where products appear out of stock but were never actually purchased 40 | 4. In a production system, this would require manual intervention to fix 41 | 42 | This bug was deliberately included to demonstrate why compensation handling is necessary in distributed systems, which we'll address in Stage 2 of the guide. 43 | 44 | **NOTE**: We've kept the failure quite simple here. Payments can also be rejected quite a bit later in the future - in that case more complicated recovery is required. 45 | 46 | ## Running the Application 47 | 48 | 1. Install dependencies: 49 | ```shell 50 | npm install 51 | ``` 52 | 53 | 2. Setup the appplication's data: 54 | ```shell 55 | cp data.json-example data.json 56 | ``` 57 | 58 | 3. Start the application: 59 | ```shell 60 | npm start 61 | ``` 62 | 63 | 4. Interact with the assistant via the terminal interface, [using this interaction script](../README.md#example-user-interaction). 64 | 65 | ## Next Steps 66 | 67 | In the next stage, we'll refactor this monolithic application into a multi-agent architecture using [orra](https://github.com/orra-dev/orra) to orchestrate different specialised components. 68 | -------------------------------------------------------------------------------- /monolithic-app/data.json-example: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "laptop-1", 5 | "name": "Dell XPS 13 (2022)", 6 | "description": "Used Dell XPS 13 laptop with 16GB RAM, 512GB SSD, Intel i7 processor", 7 | "price": 750, 8 | "condition": "excellent", 9 | "category": "laptops", 10 | "tags": ["programming", "college", "portable"], 11 | "inStock": 1, 12 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 13 | }, 14 | { 15 | "id": "laptop-2", 16 | "name": "MacBook Air M1", 17 | "description": "Used MacBook Air with M1 chip, 8GB RAM, 256GB SSD", 18 | "price": 650, 19 | "condition": "good", 20 | "category": "laptops", 21 | "tags": ["college", "portable", "mac"], 22 | "inStock": 2, 23 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 24 | }, 25 | { 26 | "id": "laptop-3", 27 | "name": "Lenovo ThinkPad X1 Carbon", 28 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 29 | "price": 820, 30 | "condition": "excellent", 31 | "category": "laptops", 32 | "tags": ["business", "programming", "durable"], 33 | "inStock": 0, 34 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 35 | }, 36 | { 37 | "id": "laptop-4", 38 | "name": "Lenovo ThinkPad X1 Carbon", 39 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 40 | "price": 500, 41 | "condition": "fair", 42 | "category": "laptops", 43 | "tags": ["business", "programming", "durable"], 44 | "inStock": 4, 45 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 46 | } 47 | ], 48 | "orders": [], 49 | "users": [ 50 | { 51 | "id": "user-1", 52 | "name": "John Doe", 53 | "email": "john@example.com", 54 | "address": "1a Goldsmiths Row, London E2 8QA" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /monolithic-app/images/MonolithArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orra-dev/agent-fragile-to-prod-guide/3f1875ef08881c71cb09e49d4034952781e9440b/monolithic-app/images/MonolithArchitecture.png -------------------------------------------------------------------------------- /monolithic-app/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import readline from 'readline-sync'; 3 | import OpenAI from 'openai'; 4 | import { JSONFilePreset } from "lowdb/node"; 5 | import * as path from "node:path"; 6 | 7 | dotenv.config(); 8 | 9 | const db = await JSONFilePreset(path.join("data.json"), { products: [], users: [] }); 10 | 11 | // Simulated delivery estimation 12 | function estimateDelivery(warehouseAddress, userAddress) { 13 | console.log(`estimating delivery from warehouse [${warehouseAddress}] to user address [${userAddress}]`); 14 | 15 | // In a real application, this would make external API calls to shipping providers 16 | const baseDeliveryDays = 3; 17 | const randomAddition = Math.floor(Math.random() * 4); // 0-3 additional days 18 | const estimatedDays = baseDeliveryDays + randomAddition; 19 | 20 | const today = new Date(); 21 | const deliveryDate = new Date(today); 22 | deliveryDate.setDate(today.getDate() + estimatedDays); 23 | 24 | return { 25 | estimatedDays, 26 | deliveryDate: deliveryDate.toISOString().split('T')[0] 27 | }; 28 | } 29 | 30 | // Simulated payment processing 31 | function processPayment(userId, productId, amount) { 32 | // In a real application, this would call a payment gateway 33 | console.log(`Processing payment of ${amount} for product ${productId} by user ${userId}`); 34 | 35 | // Simulate more frequent payment failures (50% chance) to demonstrate the inconsistency problem 36 | const failureChance = Math.random(); 37 | if (failureChance < 0.5) { 38 | console.log("Payment failed!"); 39 | return { 40 | success: false, 41 | message: "Payment processing failed. Please try again." 42 | }; 43 | } 44 | 45 | return { 46 | success: true, 47 | transactionId: `trans-${Date.now()}`, 48 | timestamp: new Date().toISOString() 49 | }; 50 | } 51 | 52 | // Simulated notification 53 | function sendNotification(userId, message) { 54 | // In a real application, this would send an email or push notification 55 | console.log(`Notification to user ${userId}: ${message}`); 56 | return { 57 | success: true, 58 | timestamp: new Date().toISOString() 59 | }; 60 | } 61 | 62 | // Initialize the OpenAI client 63 | const openai = new OpenAI({ 64 | apiKey: process.env.OPENAI_API_KEY 65 | }); 66 | 67 | // Define available functions 68 | const availableFunctions = { 69 | searchProducts: (args) => { 70 | const { category, priceMax, tags, condition } = args; 71 | 72 | let filteredProducts = [...db.data.products]; 73 | 74 | if (category) { 75 | filteredProducts = filteredProducts.filter(p => p.category === category); 76 | } 77 | 78 | if (priceMax) { 79 | filteredProducts = filteredProducts.filter(p => p.price <= priceMax); 80 | } 81 | 82 | if (tags && tags.length > 0) { 83 | filteredProducts = filteredProducts.filter(p => 84 | tags.some(tag => p.tags.includes(tag)) 85 | ); 86 | } 87 | 88 | if (condition) { 89 | filteredProducts = filteredProducts.filter(p => p.condition === condition); 90 | } 91 | 92 | return filteredProducts; 93 | }, 94 | 95 | checkProductAvailability: (args) => { 96 | const { productId } = args; 97 | const product = db.data.products.find(p => p.id === productId); 98 | 99 | if (!product) { 100 | return { 101 | available: false, 102 | message: "Product not found" 103 | }; 104 | } 105 | 106 | return { 107 | available: product.inStock > 0, 108 | inStock: product.inStock, 109 | product: product 110 | }; 111 | }, 112 | 113 | processPurchase: (args) => { 114 | const { productId, userId } = args; 115 | 116 | // Get the product and user 117 | const product = db.data.products.find(p => p.id === productId); 118 | const user = db.data. users.find(u => u.id === userId); 119 | 120 | if (!product || !user) { 121 | return { 122 | success: false, 123 | message: "Product or user not found" 124 | }; 125 | } 126 | 127 | if (product.inStock <= 0) { 128 | return { 129 | success: false, 130 | message: "Product is out of stock" 131 | }; 132 | } 133 | 134 | // CRITICAL ISSUE: Reserve the product BEFORE payment processing 135 | // This creates the potential for inconsistent state 136 | console.log(`Reserving product ${product.name} (inventory reduced from ${product.inStock} to ${product.inStock - 1})`); 137 | product.inStock -= 1; 138 | 139 | // Process the payment 140 | // In a real application, this would call a payment gateway which leads to an asynchronous flow. 141 | // Typically, a webhook has to be setup to accept the final payment state. 142 | const paymentResult = processPayment(userId, productId, product.price); 143 | if (!paymentResult.success) { 144 | // CRITICAL ISSUE: The payment failed, but we've already reserved the product! 145 | // In a monolithic system without compensation handling, this creates an inconsistent state 146 | console.log(`INCONSISTENT STATE: Product ${product.id} is reserved but payment failed!`); 147 | console.log(`This would require manual intervention to fix the inventory.`); 148 | // We don't restore inventory here, demonstrating the problem 149 | 150 | return { 151 | success: false, 152 | message: paymentResult.message 153 | }; 154 | } 155 | 156 | // Estimate delivery 157 | const deliveryEstimate = estimateDelivery(productId, user.address); 158 | 159 | // Create the order 160 | const order = { 161 | id: `order-${Date.now()}`, 162 | userId, 163 | productId, 164 | productName: product.name, 165 | price: product.price, 166 | transactionId: paymentResult.transactionId, 167 | createdAt: new Date().toISOString(), 168 | deliveryEstimate 169 | }; 170 | 171 | // Add to orders 172 | db?.data?.orders?.push(order); 173 | 174 | // Send notification 175 | sendNotification(userId, `Your order for ${product.name} has been confirmed! Estimated delivery: ${deliveryEstimate.deliveryDate}`); 176 | 177 | return { 178 | success: true, 179 | order 180 | }; 181 | }, 182 | 183 | getDeliveryEstimate: (args) => { 184 | const { productId, userId } = args; 185 | 186 | const product = db.data.products.find(p => p.id === productId); 187 | const user = db.data.users.find(u => u.id === userId); 188 | 189 | if (!product || !user) { 190 | return { 191 | success: false, 192 | message: "Product or user not found" 193 | }; 194 | } 195 | 196 | return { 197 | success: true, 198 | estimate: estimateDelivery(productId, user.address) 199 | }; 200 | } 201 | }; 202 | 203 | // Define function specs for the OpenAI API 204 | const functionSpecs = [ 205 | { 206 | name: "searchProducts", 207 | description: "Search for products based on criteria", 208 | parameters: { 209 | type: "object", 210 | properties: { 211 | category: { 212 | type: "string", 213 | description: "Product category (e.g., 'laptops')" 214 | }, 215 | priceMax: { 216 | type: "number", 217 | description: "Maximum price" 218 | }, 219 | tags: { 220 | type: "array", 221 | items: { 222 | type: "string" 223 | }, 224 | description: "Tags to filter by (e.g., ['programming', 'college'])" 225 | }, 226 | condition: { 227 | type: "string", 228 | description: "Product condition ('excellent', 'good', 'fair')" 229 | } 230 | } 231 | } 232 | }, 233 | { 234 | name: "checkProductAvailability", 235 | description: "Check if a product is available", 236 | parameters: { 237 | type: "object", 238 | properties: { 239 | productId: { 240 | type: "string", 241 | description: "ID of the product to check" 242 | } 243 | }, 244 | required: ["productId"] 245 | } 246 | }, 247 | { 248 | name: "processPurchase", 249 | description: "Process a purchase for a product", 250 | parameters: { 251 | type: "object", 252 | properties: { 253 | productId: { 254 | type: "string", 255 | description: "ID of the product to purchase" 256 | }, 257 | userId: { 258 | type: "string", 259 | description: "ID of the user making the purchase" 260 | } 261 | }, 262 | required: ["productId", "userId"] 263 | } 264 | }, 265 | { 266 | name: "getDeliveryEstimate", 267 | description: "Get delivery estimate for a product", 268 | parameters: { 269 | type: "object", 270 | properties: { 271 | productId: { 272 | type: "string", 273 | description: "ID of the product" 274 | }, 275 | userId: { 276 | type: "string", 277 | description: "ID of the user for delivery address" 278 | } 279 | }, 280 | required: ["productId", "userId"] 281 | } 282 | } 283 | ]; 284 | 285 | // Enhanced assistant function with function calling 286 | async function marketplaceAssistant(userInput, conversationHistory = []) { 287 | try { 288 | // Add the user input to the conversation history 289 | conversationHistory.push({ role: "user", content: userInput }); 290 | 291 | // Define the system message 292 | const systemMessage = { 293 | role: "system", 294 | content: `You are an AI marketplace assistant helping users find, purchase, and arrange delivery for products. 295 | 296 | Always be helpful, concise, and provide specific product recommendations that match user criteria. 297 | 298 | The current user is John Doe (user-1). When processing purchases or checking delivery estimates, 299 | always use this user ID unless otherwise specified. 300 | 301 | If the user expresses a clear intent to purchase, use the processPurchase function directly. 302 | If the user wants to know about delivery times, use the getDeliveryEstimate function.` 303 | }; 304 | 305 | // Create the messages array for the API call 306 | const messages = [systemMessage, ...conversationHistory]; 307 | 308 | // Step 1: Call OpenAI API with function definitions 309 | const response = await openai.chat.completions.create({ 310 | model: "gpt-4o", 311 | messages: messages, 312 | functions: functionSpecs, 313 | function_call: "auto", 314 | temperature: 0.7, 315 | }); 316 | 317 | const responseMessage = response.choices[0].message; 318 | 319 | // Step 2: Check if the model wants to call a function 320 | if (responseMessage.function_call) { 321 | const functionName = responseMessage.function_call.name; 322 | const functionArgs = JSON.parse(responseMessage.function_call.arguments); 323 | 324 | console.log(`\nCalling function: ${functionName} with args:`, functionArgs); 325 | 326 | // Call the function 327 | const functionResponse = availableFunctions[functionName](functionArgs); 328 | 329 | // Step 3: Append function response to messages 330 | conversationHistory.push(responseMessage); // Add assistant's function call to history 331 | 332 | // Add the function response to chat history 333 | conversationHistory.push({ 334 | role: "function", 335 | name: functionName, 336 | content: JSON.stringify(functionResponse) 337 | }); 338 | 339 | // Step 4: Get a new response from the model with the function response 340 | const secondResponse = await openai.chat.completions.create({ 341 | model: "gpt-4o", 342 | messages: [...messages, responseMessage, { 343 | role: "function", 344 | name: functionName, 345 | content: JSON.stringify(functionResponse) 346 | }], 347 | functions: functionSpecs, 348 | function_call: "auto", 349 | temperature: 0.7, 350 | }); 351 | 352 | const secondResponseMessage = secondResponse.choices[0].message; 353 | 354 | // Handle nested function calls if needed 355 | if (secondResponseMessage.function_call) { 356 | const secondFunctionName = secondResponseMessage.function_call.name; 357 | const secondFunctionArgs = JSON.parse(secondResponseMessage.function_call.arguments); 358 | 359 | console.log(`\nCalling second function: ${secondFunctionName} with args:`, secondFunctionArgs); 360 | 361 | const secondFunctionResponse = availableFunctions[secondFunctionName](secondFunctionArgs); 362 | 363 | conversationHistory.push(secondResponseMessage); 364 | 365 | conversationHistory.push({ 366 | role: "function", 367 | name: secondFunctionName, 368 | content: JSON.stringify(secondFunctionResponse) 369 | }); 370 | 371 | // Get final response from the model 372 | const finalResponse = await openai.chat.completions.create({ 373 | model: "gpt-4o", 374 | messages: [...messages, responseMessage, { 375 | role: "function", 376 | name: functionName, 377 | content: JSON.stringify(functionResponse) 378 | }, secondResponseMessage, { 379 | role: "function", 380 | name: secondFunctionName, 381 | content: JSON.stringify(secondFunctionResponse) 382 | }], 383 | temperature: 0.7, 384 | }); 385 | 386 | const finalResponseMessage = finalResponse.choices[0].message; 387 | conversationHistory.push(finalResponseMessage); 388 | 389 | return { 390 | response: finalResponseMessage.content, 391 | conversationHistory 392 | }; 393 | } 394 | 395 | conversationHistory.push(secondResponseMessage); 396 | 397 | return { 398 | response: secondResponseMessage.content, 399 | conversationHistory 400 | }; 401 | } 402 | 403 | // If no function call, just return the response 404 | conversationHistory.push(responseMessage); 405 | 406 | return { 407 | response: responseMessage.content, 408 | conversationHistory 409 | }; 410 | } catch (error) { 411 | console.error("Error in marketplace assistant:", error); 412 | return { 413 | response: "I'm sorry, I encountered an error while processing your request. Please try again.", 414 | conversationHistory 415 | }; 416 | } 417 | } 418 | 419 | // Simple CLI interface 420 | // Function to display current inventory status 421 | function showInventoryStatus() { 422 | console.log("\n----- CURRENT INVENTORY STATUS -----"); 423 | db.data.products.forEach(product => { 424 | console.log(`${product.name}: ${product.inStock} in stock`); 425 | }); 426 | console.log("------------------------------------\n"); 427 | } 428 | 429 | async function runCLI() { 430 | console.log("Welcome to the Marketplace Assistant!"); 431 | console.log("Ask about products, availability, or make a purchase."); 432 | console.log("-----------------------------------------------------------------"); 433 | console.log("Example queries:"); 434 | console.log("- I need a laptop for programming under $800"); 435 | console.log("- Is the Dell XPS 13 in stock?"); 436 | console.log("- I'd like to buy the Dell XPS 13"); 437 | console.log("- When would the MacBook Air be delivered?"); 438 | console.log("Special commands:"); 439 | console.log("- 'status' - Show current inventory status"); 440 | console.log("- 'exit' - Quit the application"); 441 | console.log("-----------------------------------------------------------------"); 442 | 443 | let conversationHistory = []; 444 | let running = true; 445 | 446 | // Display initial inventory 447 | showInventoryStatus(); 448 | 449 | while (running) { 450 | const userInput = readline.question("\nYou: "); 451 | 452 | if (userInput.toLowerCase() === 'exit') { 453 | running = false; 454 | continue; 455 | } 456 | 457 | if (userInput.toLowerCase() === 'status') { 458 | showInventoryStatus(); 459 | continue; 460 | } 461 | 462 | console.log("\nAssistant is thinking..."); 463 | const result = await marketplaceAssistant(userInput, conversationHistory); 464 | conversationHistory = result.conversationHistory; 465 | 466 | console.log(`\nAssistant: ${result.response}`); 467 | 468 | // After each interaction that might modify inventory, show the status 469 | if (userInput.toLowerCase().includes("buy") || 470 | userInput.toLowerCase().includes("purchase") || 471 | result.response.toLowerCase().includes("payment")) { 472 | showInventoryStatus(); 473 | } 474 | } 475 | 476 | console.log("\nThank you for using the Marketplace Assistant!"); 477 | } 478 | 479 | // Start the CLI 480 | runCLI().catch(console.error); 481 | -------------------------------------------------------------------------------- /monolithic-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marketplace-assistant", 3 | "version": "1.0.0", 4 | "description": "A monolithic AI-powered marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "ai", 13 | "marketplace", 14 | "assistant", 15 | "llm" 16 | ], 17 | "author": "Team orra", 18 | "license": "MIT", 19 | "dependencies": { 20 | "express": "^4.18.2", 21 | "dotenv": "^16.3.1", 22 | "openai": "^4.11.0", 23 | "readline-sync": "^1.4.10", 24 | "lowdb": "^7.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stage1-architecture/README.md: -------------------------------------------------------------------------------- 1 | # Stage 1: Architecture Re-think with orra 2 | 3 | In this stage, we address the key challenges in our Marketplace Assistant: 4 | - High latency and token usage due to monolithic design 5 | - Poor separation of concerns 6 | - Sequential processing of tasks 7 | - Cost inefficiency for deterministic operations 8 | 9 | ## What Changed 10 | 11 | We've transformed our application in two significant ways (with orra's Plan Engine driving coordination and reliability). 12 | 13 | ![](images/ArchitectureRethink1.png) 14 | 15 | ### 1. Split the Monolith into Specialized Components 16 | 17 | First, we divided the monolithic assistant into four specialized components: 18 | 19 | 1. **Product Advisor Agent**: An LLM-powered agent that understands complex user needs and recommends products 20 | 2. **Inventory Tool as Service**: Checks real-time product availability, and reserves/releases product stock 21 | 3. **Purchasing Tool as Service**: Handles product purchase processing for users 22 | 4. **Delivery Agent**: Uses real-time data to estimate delivery times 23 | 24 | ### 2. Migrated Tool Calls to Dedicated Services 25 | 26 | We made a critical architectural improvement by migrating the monolith's function/tool calls to dedicated services: 27 | 28 | - **Tool Calls in Monolith**: The original design used LLM function calling for all operations, even simple deterministic ones like inventory operations and purchase processing 29 | - **Tools as Services**: We extracted these tool functions into proper standalone services that can be directly coordinated 30 | 31 | This creates a clear distinction between: 32 | 33 | - **Agents** (LLM-powered): For tasks requiring complex reasoning and human-like responses 34 | - **(Tools as) Services** (Deterministic): For predictable operations with consistent input/output patterns 35 | 36 | We converted these tool functions into dedicated services: 37 | - **Inventory**: Directly handles inventory operations (previously a function call) 38 | - **Purchasing**: Handles purchase processing including creating orders, making payments and notifying users (previously a function call) 39 | 40 | We kept the Product Advisor and Delivery as LLM-powered agents since they benefit from complex reasoning capabilities. 41 | 42 | This architectural shift is enabled by orra's Plan Engine, which operates at the application level rather than just the agent level. This higher-level orchestration allows direct coordination between services, eliminating the need to tunnel all interactions through LLM function-calling. The Plan Engine understands and coordinates the entire workflow across both LLM-powered agents and deterministic services. 43 | 44 | ## Run this stage 45 | 46 | ### Prerequisites 47 | - Node.js (v18+) 48 | - orra [Plan Engine running and CLI installed](https://github.com/orra-dev/orra/tree/main#installation) 49 | - [OpenAI API key](https://platform.openai.com/docs/api-reference/authentication) 50 | 51 | ### Setup & Run 52 | 53 | 1. **Initialize orra configuration** 54 | ```bash 55 | ./stage_setup.sh # Sets up project, webhooks, and API keys 56 | 57 | 2. **Configure OpenAI API key in each component's `.env` file** 58 | ```shell 59 | OPENAI_API_KEY=your_openai_api_key_here 60 | ``` 61 | 3. **Start each component (in separate terminals)** 62 | ```shell 63 | cd [component-directory] # Run for each component 64 | npm install 65 | npm start 66 | ``` 67 | 4. **Start webhook simulator (in a separate terminal)** 68 | ```bash 69 | orra verify webhooks start http://localhost:3000/webhook 70 | ``` 71 | ### Using the Application 72 | 73 | Let's run the expected AI Marketplace Assistant interactions [described here](../README.md#example-user-interaction). 74 | 75 | We'll be using the [CLI](https://github.com/orra-dev/orra/blob/main/docs/cli.md)'s `orra verify` command to understand how the Plan Engine is coordinating our components to complete system actions. 76 | 77 | The assumption here is that there's a chat UI interface that forwards requests to the Plan Engine. 78 | 79 | We use lowdb to query and update data in our [data.json](data.json) file - basically a simple JSON based DB. This data is shared against all the components. 80 | 81 | 1. **Ask for a product recommendation** 82 | 83 | ```bash 84 | orra verify run 'Recommend a product' \ 85 | -d query:'I need a used laptop for college that is powerful enough for programming, under $800.' 86 | ``` 87 | [Follow these instructions](https://github.com/orra-dev/orra/blob/main/docs/cli.md#orchestration-actions) on how to inspect the orchestrated action. 88 | 89 | In this case, you should see just the `Product Advisor Agent` only executing and dealing with this action. Any interim errors are handled by orra. 90 | 91 | 2. **Enquire about delivery for the recommended product** 92 | 93 | ```bash 94 | orra verify run 'Can I get it delivered by next week?' \ 95 | -d 'productId:laptop-1' \ 96 | -d 'userId:user-1' 97 | ``` 98 | 99 | In this case, there should be 100 | - an inventory check to ensure the product is in-stock 101 | - if yes, a delivery estimate is provided 102 | - Any interim errors are handled by orra 103 | 104 | 3. **Purchase a recommended product** 105 | 106 | ```bash 107 | orra verify run 'Purchase product' \ 108 | -d 'productId:laptop-1' \ 109 | -d 'userId:user-1' 110 | ``` 111 | 112 | In this case, there should be 113 | - an inventory check to ensure the product is in-stock 114 | - an inventory reserve request if the product is in-stock - this lowers the stock count 115 | - A delivery estimate is provided 116 | - The product is purchased - causing an order to be placed 117 | - Any interim errors are handled by orra 118 | 119 | Navigate to the [data.json](data.json) file to view the placed `order` in the `orders` list. 120 | 121 | ### Reset Environment 122 | 123 | 1. **Clear Plan Engine configurations and reset data** 124 | ```bash 125 | ./stage_reset.sh # Clears configurations and data 126 | ``` 127 | 128 | 2. **Stop all the running components and kill all the terminal window** 129 | 130 | 3. **Shutdown the Plan Engine** 131 | 132 | ## Benefits 133 | 134 | 1. **Reduced Latency**: 135 | - **orra** automatically parallelises appropriate tasks 136 | - Overall response time improved by ~60% - esp. after caching execution plans 137 | - Services respond faster than LLM-based agents (40% improvement for deterministic operations) 138 | 139 | 2. **Lower Token Usage**: 140 | - Specialised agents reduce token consumption by ~40% 141 | - Converting tool to services reduces token usage by ~80% for inventory and purchasing operations 142 | - Significant cost savings in production 143 | 144 | 3. **Improved Maintainability**: 145 | - Each component has a single responsibility 146 | - Easier to update, debug, and enhance individual components 147 | - Clear separation between reasoning and deterministic parts 148 | 149 | 4. **Better Reliability**: 150 | - Issues in one component don't necessarily impact others 151 | - Deterministic services have fewer failure modes than LLM-based agents 152 | 153 | ## How orra Helps 154 | 155 | - **Automatic Orchestration**: orra handles the coordination between components based on the user or application's intent 156 | - **Parallel Execution**: Where possible, orra executes non-dependent tasks in parallel 157 | - **Service Discovery**: Components register with orra, which then routes requests appropriately 158 | - **Seamless Integration**: orra orchestrates between agents and services without code changes 159 | - **Execution Timeouts**: Set execution timeout duration per service/agent - works around Agents just spinning their wheels 160 | - **High-Level Error Handling**: Retrying execution on all errors - upto 5 retries 161 | - **Configurable Health Monitoring**: orra pauses orchestrations due to unhealthy services and resumes them when health is restored 162 | 163 | ## Next Steps 164 | 165 | Our application is now more efficient, but it still lacks robust error handling. In the [stage 2](../stage2-consistency), we'll implement compensation mechanisms to handle failures and ensure state/transaction integrity. 166 | -------------------------------------------------------------------------------- /stage1-architecture/data.json-example: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "laptop-1", 5 | "name": "Dell XPS 13 (2022)", 6 | "description": "Used Dell XPS 13 laptop with 16GB RAM, 512GB SSD, Intel i7 processor", 7 | "price": 750, 8 | "condition": "excellent", 9 | "category": "laptops", 10 | "tags": ["programming", "college", "portable"], 11 | "inStock": 1, 12 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 13 | }, 14 | { 15 | "id": "laptop-2", 16 | "name": "MacBook Air M1", 17 | "description": "Used MacBook Air with M1 chip, 8GB RAM, 256GB SSD", 18 | "price": 650, 19 | "condition": "good", 20 | "category": "laptops", 21 | "tags": ["college", "portable", "mac"], 22 | "inStock": 2, 23 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 24 | }, 25 | { 26 | "id": "laptop-3", 27 | "name": "Lenovo ThinkPad X1 Carbon", 28 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 29 | "price": 820, 30 | "condition": "excellent", 31 | "category": "laptops", 32 | "tags": ["business", "programming", "durable"], 33 | "inStock": 0, 34 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 35 | }, 36 | { 37 | "id": "laptop-4", 38 | "name": "Lenovo ThinkPad X1 Carbon", 39 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 40 | "price": 500, 41 | "condition": "fair", 42 | "category": "laptops", 43 | "tags": ["business", "programming", "durable"], 44 | "inStock": 4, 45 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 46 | } 47 | ], 48 | "orders": [], 49 | "users": [ 50 | { 51 | "id": "user-1", 52 | "name": "John Doe", 53 | "email": "john@example.com", 54 | "address": "1a Goldsmiths Row, London E2 8QA" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /stage1-architecture/delivery-agent/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | DELIVERY_AGENT_PORT=3004 10 | -------------------------------------------------------------------------------- /stage1-architecture/delivery-agent/agent.js: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { JSONFilePreset } from "lowdb/node"; 3 | import * as path from "node:path"; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY 10 | }); 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [], users: [] }); 13 | 14 | export const supportedStatuses = [ 15 | 'unknown-product', 16 | 'unknown-user', 17 | 'delivery-estimated', 18 | ] 19 | 20 | // Simulated traffic and logistics data 21 | const trafficConditions = { 22 | "route_segments": [ 23 | { 24 | "segment_id": "A90-ABD-DND", 25 | "name": "A90 Aberdeen to Dundee", 26 | "length_km": 108, 27 | "current_average_speed_kph": 95, 28 | "normal_average_speed_kph": 100, 29 | "congestion_level": "light", 30 | "incidents": [] 31 | }, 32 | { 33 | "segment_id": "M90-PER-EDI", 34 | "name": "M90 Perth to Edinburgh", 35 | "length_km": 45, 36 | "current_average_speed_kph": 110, 37 | "normal_average_speed_kph": 110, 38 | "congestion_level": "none", 39 | "incidents": [ 40 | { 41 | "type": "roadworks", 42 | "location": "Junction 3", 43 | "description": "Lane closure for resurfacing", 44 | "delay_minutes": 10 45 | } 46 | ] 47 | }, 48 | { 49 | "segment_id": "A1-NCL-YRK", 50 | "name": "A1 Newcastle to York", 51 | "length_km": 140, 52 | "current_average_speed_kph": 100, 53 | "normal_average_speed_kph": 110, 54 | "congestion_level": "moderate", 55 | "incidents": [ 56 | { 57 | "type": "accident", 58 | "location": "Near Darlington", 59 | "description": "Multi-vehicle collision", 60 | "delay_minutes": 25 61 | } 62 | ] 63 | } 64 | ], 65 | "weather_conditions": [ 66 | { 67 | "location": "Northeast", 68 | "condition": "light_rain", 69 | "temperature_celsius": 12 70 | }, 71 | { 72 | "location": "Midwest", 73 | "condition": "cloudy", 74 | "temperature_celsius": 14 75 | }, 76 | { 77 | "location": "West Coast", 78 | "condition": "clear", 79 | "temperature_celsius": 20 80 | }, 81 | { 82 | "location": "Southeast", 83 | "condition": "partly_cloudy", 84 | "temperature_celsius": 22 85 | } 86 | ], 87 | "vehicles": [ 88 | { 89 | "type": "van", 90 | "capacity_cubic_meters": 15, 91 | "max_range_km": 500, 92 | "average_speed_kph": 90, 93 | "availability": "high" 94 | }, 95 | { 96 | "type": "truck", 97 | "capacity_cubic_meters": 40, 98 | "max_range_km": 800, 99 | "average_speed_kph": 80, 100 | "availability": "medium" 101 | } 102 | ] 103 | }; 104 | 105 | // Function to generate delivery estimates using LLM 106 | export async function generateDeliveryEstimates(userId, productId, inStock=0) { 107 | if (inStock && inStock < 1) { 108 | throw new Error('CannotEstimateDeliveryForOutOfStockProduct'); 109 | } 110 | 111 | const user = db.data. users.find(u => u.id === userId); 112 | const product = db.data.products.find(p => p.id === productId); 113 | 114 | if (!user) { 115 | return { 116 | success: false, 117 | status: supportedStatuses[1] 118 | }; 119 | } 120 | 121 | if (!product) { 122 | return { 123 | success: false, 124 | status: supportedStatuses[0] 125 | }; 126 | } 127 | 128 | // Use LLM to generate intelligent delivery estimates 129 | const systemPrompt = `You are a delivery logistics expert with 20 years of experience. 130 | Your task is to provide realistic delivery estimates for products being shipped from a warehouse to a customer. 131 | Consider all relevant factors including traffic conditions, weather, distance, and product characteristics. 132 | Always provide both best-case and worst-case scenarios with confidence levels.`; 133 | 134 | const userPrompt = `Create delivery estimates for the following: 135 | 136 | WAREHOUSE ADDRESS: ${product.warehouseAddress} 137 | CUSTOMER ADDRESS: ${user.address} 138 | 139 | Current traffic and logistics data: 140 | ${JSON.stringify(trafficConditions, null, 2)} 141 | 142 | Your response should include: 143 | 1. A best-case delivery estimate (duration in hours and delivery date) 144 | 2. A worst-case delivery estimate (duration in hours and delivery date) 145 | 3. Confidence levels for each estimate (low/moderate/high) 146 | 4. A brief explanation of your reasoning 147 | 148 | Respond in JSON format with these components. 149 | 150 | USE THIS SCHEMA FOR THE FINAL ANSWER: 151 | { 152 | "bestCase": { 153 | "estimatedDurationHours": "expected duration as decimal value, e.g. 7.5", 154 | "estimatedDeliveryDate": "estimated delivery date in the future as a timestamp, e.g. 2024-10-02T21:15:00Z", 155 | "confidenceLevel": "how confident you are. one of: low, moderate or high" 156 | }, 157 | "worstCase": { 158 | "estimatedDurationHours": "expected duration as decimal value, e.g. 7.5", 159 | "estimatedDeliveryDate": "estimated delivery date in the future as a timestamp, e.g. 2024-10-02T21:15:00Z", 160 | "confidenceLevel": "how confident you are. one of: low, moderate or high" 161 | }, 162 | "explanation": "Delivery estimate based on current traffic and weather conditions. Factors include road conditions, distance, and typical shipping times." 163 | }`; 164 | 165 | try { 166 | const response = await openai.chat.completions.create({ 167 | model: "gpt-4o", 168 | messages: [ 169 | { role: "system", content: systemPrompt }, 170 | { role: "user", content: userPrompt } 171 | ], 172 | response_format: { type: "json_object" } 173 | }); 174 | 175 | const content = JSON.parse(response.choices[0].message.content); 176 | console.log("Generated delivery estimates:", content); 177 | 178 | const fallbackDeliveryEstimatedHours = 72 179 | const fallbackDeliveryDate = new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString().split('T')[0] 180 | const fallbackExplanation = "Delivery estimate based on current traffic and weather conditions." 181 | 182 | return { 183 | status: supportedStatuses[2], 184 | success: true, 185 | estimatedDays: hoursToDays(content?.worstCase?.estimatedDays || fallbackDeliveryEstimatedHours), 186 | deliveryDate: content?.worstCase?.estimatedDeliveryDate?.split('T')[0] || fallbackDeliveryDate, 187 | explanation: content?.explanation || fallbackExplanation, 188 | }; 189 | } catch (error) { 190 | console.error("Error generating delivery estimates:", error); 191 | throw error; 192 | } 193 | } 194 | 195 | function hoursToDays(hours) { 196 | if (typeof hours !== "number" || isNaN(hours)) { 197 | throw new Error("Please provide a valid number of hours."); 198 | } 199 | return hours / 24; 200 | } 201 | -------------------------------------------------------------------------------- /stage1-architecture/delivery-agent/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initAgent } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { generateDeliveryEstimates } from "./agent.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.DELIVERY_AGENT_PORT || 3004; 11 | 12 | // Initialize the orra agent 13 | const deliveryAgent = initAgent({ 14 | name: 'delivery-agent', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/delivery-agent-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | 29 | async function startService() { 30 | try { 31 | // Register agent with orra 32 | await deliveryAgent.register({ 33 | description: 'An agent that provides intelligent delivery estimates based on product, location, and current conditions.', 34 | schema 35 | }); 36 | 37 | // Start handling tasks 38 | deliveryAgent.start(async (task) => { 39 | console.log('Processing delivery estimation task:', task.id); 40 | console.log('Input:', task.input); 41 | 42 | const { userId, productId, inStock } = task.input; 43 | 44 | // Use LLM to generate delivery estimates 45 | const result = await generateDeliveryEstimates(userId, productId, inStock); 46 | // FEATURE COMING SOON: 47 | // if (result.status !== 'success') { 48 | // return task.abort(result); 49 | // } 50 | return result; 51 | }); 52 | 53 | console.log('Delivery Agent started successfully'); 54 | } catch (error) { 55 | console.error('Failed to start Delivery Agent:', error); 56 | process.exit(1); 57 | } 58 | } 59 | 60 | // Start the Express server and the agent 61 | app.listen(port, () => { 62 | console.log(`Delivery Agent listening on port ${port}`); 63 | startService().catch(console.error); 64 | }); 65 | -------------------------------------------------------------------------------- /stage1-architecture/delivery-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delivery-agent", 3 | "version": "1.0.0", 4 | "description": "LLM-powered delivery estimation agent for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "openai": "^4.11.0", 17 | "lowdb": "^7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stage1-architecture/delivery-agent/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "userId": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | }, 11 | "inStock": { 12 | "type": "number" 13 | } 14 | }, 15 | "required": ["userId", "productId"] 16 | }, 17 | "output": { 18 | "type": "object", 19 | "properties": { 20 | "status": { 21 | "type": "string" 22 | }, 23 | "success": { 24 | "type": "boolean" 25 | }, 26 | "estimatedDays": { 27 | "type": "number" 28 | }, 29 | "deliveryDate": { 30 | "type": "string" 31 | }, 32 | "explanation": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["estimatedDays", "deliveryDate", "explanation"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stage1-architecture/images/ArchitectureRethink1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orra-dev/agent-fragile-to-prod-guide/3f1875ef08881c71cb09e49d4034952781e9440b/stage1-architecture/images/ArchitectureRethink1.png -------------------------------------------------------------------------------- /stage1-architecture/inventory-tool-service/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | INVENTORY_SERVICE_PORT=3002 10 | -------------------------------------------------------------------------------- /stage1-architecture/inventory-tool-service/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initService } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { execInventory } from "./svc.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.INVENTORY_SERVICE_PORT || 3002; 11 | 12 | // Initialize the orra service 13 | const inventoryService = initService({ 14 | name: 'inventory-service', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/inventory-service-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | async function startService() { 29 | try { 30 | // Register service with orra 31 | await inventoryService.register({ 32 | description: `A service that manages product inventory, checks availability and reserves products. 33 | Supported actions: checkAvailability (gets product status), reserveProduct (reduces inventory), and releaseProduct (returns inventory).`, 34 | schema 35 | }); 36 | 37 | // Start handling tasks 38 | inventoryService.start(async (task) => { 39 | console.log('Processing inventory task:', task.id); 40 | console.log('Input:', task.input); 41 | 42 | const { action, productId } = task.input; 43 | const result = await execInventory(action, productId); 44 | 45 | // FEATURE COMING SOON: 46 | // if (result.status !== 'success') { 47 | // return task.abort(result); 48 | // } 49 | 50 | return result; 51 | }); 52 | 53 | console.log('Inventory Service started successfully'); 54 | } catch (error) { 55 | console.error('Failed to start Inventory Service:', error); 56 | process.exit(1); 57 | } 58 | } 59 | 60 | // Start the Express server and the service 61 | app.listen(port, () => { 62 | console.log(`Inventory Service listening on port ${port}`); 63 | startService().catch(console.error); 64 | }); 65 | -------------------------------------------------------------------------------- /stage1-architecture/inventory-tool-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory-tool-service", 3 | "version": "1.0.0", 4 | "description": "Inventory management previously a tool now a service for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "lowdb": "^7.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage1-architecture/inventory-tool-service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "action": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | } 11 | }, 12 | "required": ["action", "productId"] 13 | }, 14 | "output": { 15 | "type": "object", 16 | "properties": { 17 | "action": { 18 | "type": "string" 19 | }, 20 | "productId": { 21 | "type": "string" 22 | }, 23 | "status": { 24 | "type": "string" 25 | }, 26 | "success": { 27 | "type": "boolean" 28 | }, 29 | "inStock": { 30 | "type": "number" 31 | }, 32 | "message": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["action", "productId"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stage1-architecture/inventory-tool-service/svc.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | 4 | export const supportedStatuses = [ 5 | 'unknown-product', 6 | 'product-available', 7 | 'product-out-of-stock', 8 | 'product-reserved', 9 | 'product-released' 10 | ] 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [] }); 13 | 14 | export async function execInventory(action, productId) { 15 | console.log('executing inventory action: ', action, ' for product: ', productId); 16 | switch (action) { 17 | case 'checkAvailability': 18 | return checkAvailability(productId); 19 | case 'reserveProduct': 20 | return await reserveProduct(productId); 21 | case 'releaseProduct': 22 | return releaseProduct(productId); 23 | default: 24 | throw new Error(`Unknown action: ${action}`); 25 | } 26 | } 27 | 28 | // Service functions 29 | function checkAvailability(productId) { 30 | const product = db.data.products.find(p => p.id === productId); 31 | 32 | if (!product) { 33 | return { 34 | action: "checkAvailability", 35 | productId, 36 | status: supportedStatuses[0], 37 | success: false, 38 | inStock: 0, 39 | message: "Product not found" 40 | }; 41 | } 42 | 43 | return { 44 | action: "checkAvailability", 45 | productId, 46 | status: product.inStock > 0 ? supportedStatuses[1] : supportedStatuses[2], 47 | success: true, 48 | inStock: product.inStock, 49 | message: "Product in stock" 50 | }; 51 | } 52 | 53 | async function reserveProduct(productId, quantity = 1) { 54 | const product = db.data.products.find(p => p.id === productId); 55 | 56 | if (!product) { 57 | return { 58 | action: "reserveProduct", 59 | productId, 60 | status: supportedStatuses[0], 61 | success: false, 62 | inStock: 0, 63 | message: "Product not found" 64 | }; 65 | } 66 | 67 | if (product.inStock < quantity) { 68 | return { 69 | action: "reserveProduct", 70 | productId, 71 | status: supportedStatuses[2], 72 | success: false, 73 | inStock: product.inStock, 74 | message: `Insufficient stock. Requested: ${quantity}, Available: ${product.inStock}` 75 | }; 76 | } 77 | 78 | // Reserve the product 79 | product.inStock -= quantity; 80 | await db.write() 81 | 82 | return { 83 | action: "reserveProduct", 84 | productId, 85 | status: supportedStatuses[3], 86 | success: true, 87 | inStock: product.inStock, 88 | message: `Successfully reserved ${quantity} units of ${product.name}` 89 | }; 90 | } 91 | 92 | async function releaseProduct(productId, quantity = 1) { 93 | const product = db.data.products.find(p => p.id === productId); 94 | 95 | if (!product) { 96 | return { 97 | action: "releaseProduct", 98 | productId, 99 | status: supportedStatuses[0], 100 | success: false, 101 | inStock: 0, 102 | message: "Product not found" 103 | }; 104 | } 105 | 106 | // Release the reservation 107 | product.inStock += quantity; 108 | await db.write() 109 | 110 | console.log(`Released ${quantity} units of ${product.name}. New stock: ${product.inStock}`); 111 | 112 | return { 113 | action: "releaseProduct", 114 | productId, 115 | status: supportedStatuses[4], 116 | success: true, 117 | inStock: product.inStock, 118 | message: `Successfully released ${quantity} units of ${product.name}` 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /stage1-architecture/product-advisor-agent/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxx 7 | 8 | # Ports 9 | PRODUCT_ADVISOR_PORT=3001 10 | -------------------------------------------------------------------------------- /stage1-architecture/product-advisor-agent/agent.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | import OpenAI from "openai"; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY 10 | }); 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [] }); 13 | 14 | const functionSpecs = [ 15 | { 16 | name: "searchProducts", 17 | description: "Search for products based on criteria", 18 | parameters: { 19 | type: "object", 20 | properties: { 21 | category: { 22 | type: "string", 23 | description: "Product category (e.g., 'laptops')" 24 | }, 25 | priceMax: { 26 | type: "number", 27 | description: "Maximum price" 28 | }, 29 | tags: { 30 | type: "array", 31 | items: { 32 | type: "string" 33 | }, 34 | description: "Tags to filter by (e.g., ['programming', 'college'])" 35 | }, 36 | condition: { 37 | type: "string", 38 | description: "Product condition ('excellent', 'good', 'fair')" 39 | } 40 | } 41 | } 42 | } 43 | ]; 44 | 45 | const availableFunctions = { 46 | searchProducts: (args) => { 47 | const { category, priceMax, tags, condition } = args; 48 | 49 | let filteredProducts = [...db.data.products]; 50 | 51 | if (category) { 52 | filteredProducts = filteredProducts.filter(p => p.category === category); 53 | } 54 | 55 | if (priceMax) { 56 | filteredProducts = filteredProducts.filter(p => p.price <= priceMax); 57 | } 58 | 59 | if (tags && tags.length > 0) { 60 | filteredProducts = filteredProducts.filter(p => 61 | tags.some(tag => p.tags.includes(tag)) 62 | ); 63 | } 64 | 65 | if (condition) { 66 | filteredProducts = filteredProducts.filter(p => p.condition === condition); 67 | } 68 | 69 | return filteredProducts; 70 | }, 71 | }; 72 | 73 | export async function recommendProduct(query) { 74 | const data = await runAdvisor(query); 75 | return extractAndParseJson(data.response); 76 | } 77 | 78 | async function runAdvisor(query, conversationHistory = []) { 79 | try { 80 | // Add the user input to the conversation history 81 | conversationHistory.push({ role: "user", content: query }); 82 | 83 | // Define the system message 84 | const systemMessage = { 85 | role: "system", 86 | content: `You are an AI marketplace assistant. Given a user query and product data, recommend the most suitable products. 87 | For each recommendation, provide reasoning based on the user's needs. 88 | 89 | Always be helpful, concise, and provide specific product recommendations that match user criteria. 90 | 91 | Focus ONLY on products that are in stock (inStock > 0). 92 | 93 | Generate recommendations in JSON format with reasoning for why each product matches the user's needs. 94 | 95 | USE THIS SCHEMA FOR THE FINAL ANSWER: 96 | { 97 | "recommendations": [ 98 | { 99 | "id": "the recommended product's Id", 100 | "name": "the recommended product's name", 101 | "description": "the recommended product's description", 102 | "reasoning": "the reason for recommending the product", 103 | } 104 | ] 105 | } 106 | 107 | If there are NO matching products that are in stock (inStock > 0) return an empty recommendations array.` 108 | }; 109 | 110 | // Create the messages array for the API call 111 | const messages = [systemMessage, ...conversationHistory]; 112 | 113 | // Step 1: Call OpenAI API with function definitions 114 | const response = await openai.chat.completions.create({ 115 | model: "gpt-4o", 116 | messages: messages, 117 | functions: functionSpecs, 118 | function_call: "auto", 119 | response_format: { type: "json_object" }, 120 | temperature: 0.7, 121 | }); 122 | 123 | const responseMessage = response.choices[0].message; 124 | 125 | // Step 2: Check if the model wants to call a function 126 | if (responseMessage.function_call) { 127 | const functionName = responseMessage.function_call.name; 128 | const functionArgs = JSON.parse(responseMessage.function_call.arguments); 129 | 130 | console.log(`\nCalling function: ${functionName} with args:`, functionArgs); 131 | 132 | // Call the function 133 | const functionResponse = availableFunctions[functionName](functionArgs); 134 | 135 | // Step 3: Append function response to messages 136 | conversationHistory.push(responseMessage); // Add assistant's function call to history 137 | 138 | // Add the function response to chat history 139 | conversationHistory.push({ 140 | role: "function", 141 | name: functionName, 142 | content: JSON.stringify(functionResponse) 143 | }); 144 | 145 | // Step 4: Get a new response from the model with the function response 146 | const secondResponse = await openai.chat.completions.create({ 147 | model: "gpt-4o", 148 | messages: [...messages, responseMessage, { 149 | role: "function", 150 | name: functionName, 151 | content: JSON.stringify(functionResponse) 152 | }], 153 | functions: functionSpecs, 154 | function_call: "auto", 155 | temperature: 0.7, 156 | }); 157 | 158 | const secondResponseMessage = secondResponse.choices[0].message; 159 | 160 | // Handle nested function calls if needed 161 | if (secondResponseMessage.function_call) { 162 | const secondFunctionName = secondResponseMessage.function_call.name; 163 | const secondFunctionArgs = JSON.parse(secondResponseMessage.function_call.arguments); 164 | 165 | console.log(`\nCalling second function: ${secondFunctionName} with args:`, secondFunctionArgs); 166 | 167 | const secondFunctionResponse = availableFunctions[secondFunctionName](secondFunctionArgs); 168 | 169 | conversationHistory.push(secondResponseMessage); 170 | 171 | conversationHistory.push({ 172 | role: "function", 173 | name: secondFunctionName, 174 | content: JSON.stringify(secondFunctionResponse) 175 | }); 176 | 177 | // Get final response from the model 178 | const finalResponse = await openai.chat.completions.create({ 179 | model: "gpt-4o", 180 | messages: [...messages, responseMessage, { 181 | role: "function", 182 | name: functionName, 183 | content: JSON.stringify(functionResponse) 184 | }, secondResponseMessage, { 185 | role: "function", 186 | name: secondFunctionName, 187 | content: JSON.stringify(secondFunctionResponse) 188 | }], 189 | temperature: 0.7, 190 | }); 191 | 192 | const finalResponseMessage = finalResponse.choices[0].message; 193 | conversationHistory.push(finalResponseMessage); 194 | 195 | return { 196 | response: finalResponseMessage.content, 197 | conversationHistory 198 | }; 199 | } 200 | 201 | conversationHistory.push(secondResponseMessage); 202 | 203 | return { 204 | response: secondResponseMessage.content, 205 | conversationHistory 206 | }; 207 | } 208 | 209 | // If no function call, just return the response 210 | conversationHistory.push(responseMessage); 211 | 212 | return { 213 | response: responseMessage.content, 214 | conversationHistory 215 | }; 216 | } catch (error) { 217 | console.error("Error in marketplace assistant:", error); 218 | throw error; 219 | } 220 | } 221 | 222 | function extractAndParseJson(input) { 223 | // Extract the JSON content 224 | const jsonMatch = input.match(/```json\n([\s\S]*?)\n```/); 225 | 226 | if (!jsonMatch) { 227 | throw new Error("No JSON content found between ```json``` tags"); 228 | } 229 | 230 | const jsonString = jsonMatch[1]; 231 | 232 | // Parse the JSON 233 | try { 234 | return JSON.parse(jsonString); 235 | } catch (error) { 236 | throw new Error(`Failed to parse JSON: ${error.message}`); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /stage1-architecture/product-advisor-agent/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initAgent } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { recommendProduct } from "./agent.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.PRODUCT_ADVISOR_PORT || 3001; 11 | 12 | // Initialize the orra agent 13 | const productAdvisor = initAgent({ 14 | name: 'product-advisor', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/product-advisor-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | // Function to generate product recommendations using LLM 29 | 30 | 31 | async function startService() { 32 | try { 33 | // Register agent with orra 34 | await productAdvisor.register({ 35 | description: 'An agent that helps users find products based on their needs and preferences.', 36 | schema 37 | }); 38 | 39 | // Start handling tasks 40 | productAdvisor.start(async (task) => { 41 | console.log('Processing product advisory task:', task.id); 42 | console.log('Input:', task.input); 43 | 44 | const { query } = task.input; 45 | 46 | // Use LLM to generate recommendations 47 | return await recommendProduct(query); 48 | }); 49 | 50 | console.log('Product Advisor agent started successfully'); 51 | } catch (error) { 52 | console.error('Failed to start Product Advisor agent:', error); 53 | process.exit(1); 54 | } 55 | } 56 | 57 | // Start the Express server and the agent 58 | app.listen(port, () => { 59 | console.log(`Product Advisor listening on port ${port}`); 60 | startService().catch(console.error); 61 | }); 62 | -------------------------------------------------------------------------------- /stage1-architecture/product-advisor-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product-advisor", 3 | "version": "1.0.0", 4 | "description": "Product advisor agent for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "openai": "^4.11.0", 17 | "lowdb": "^7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stage1-architecture/product-advisor-agent/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "string" 7 | } 8 | }, 9 | "required": ["query"] 10 | }, 11 | "output": { 12 | "type": "object", 13 | "properties": { 14 | "recommendations": { 15 | "type": "array", 16 | "items": { 17 | "type": "object", 18 | "properties": { 19 | "id": { 20 | "type": "string" 21 | }, 22 | "name": { 23 | "type": "string" 24 | }, 25 | "description": { 26 | "type": "string" 27 | }, 28 | "reasoning": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "required": ["recommendations"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stage1-architecture/purchasing-tool-service/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | PAYMENT_SERVICE_PORT=3003 10 | -------------------------------------------------------------------------------- /stage1-architecture/purchasing-tool-service/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initService } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { purchaseProduct } from './svc.js'; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.PAYMENT_SERVICE_PORT || 3003; 11 | 12 | // Initialize the orra service 13 | const purchaseService = initService({ 14 | name: 'purchase-service', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/purchase-service-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | async function startService() { 29 | try { 30 | // Register service with orra 31 | await purchaseService.register({ 32 | description: 'A service that makes marketplace product purchases on behalf of a user. It creates purchase orders that include shipping details, makes payments against external payment gateways and notifies users.', 33 | schema 34 | }); 35 | 36 | // Start handling tasks 37 | purchaseService.start(async (task) => { 38 | console.log('Processing purchase task:', task.id); 39 | console.log('Input:', task.input); 40 | 41 | const { userId, productId, deliveryDate } = task.input; 42 | 43 | // Process the purchase order 44 | const result = purchaseProduct(userId, productId, deliveryDate); 45 | // FEATURE COMING SOON: 46 | // if (result.status !== 'success') { 47 | // return task.abort(result); 48 | // } 49 | return result; 50 | }); 51 | 52 | console.log('Payment Service started successfully'); 53 | } catch (error) { 54 | console.error('Failed to start Payment Service:', error); 55 | process.exit(1); 56 | } 57 | } 58 | 59 | // Start the Express server and the service 60 | app.listen(port, () => { 61 | console.log(`Payment Service listening on port ${port}`); 62 | startService().catch(console.error); 63 | }); 64 | -------------------------------------------------------------------------------- /stage1-architecture/purchasing-tool-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purchase-tool-service", 3 | "version": "1.0.0", 4 | "description": "Purchase processing previously a tool now a service for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "lowdb": "^7.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage1-architecture/purchasing-tool-service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "userId": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | }, 11 | "deliveryDate": { 12 | "type": "string" 13 | } 14 | }, 15 | "required": [ 16 | "userId", 17 | "productId", 18 | "deliveryDate" 19 | ] 20 | }, 21 | "output": { 22 | "type": "object", 23 | "properties": { 24 | "order": { 25 | "type": "object", 26 | "properties": { 27 | "id": { 28 | "type": "string" 29 | }, 30 | "userId": { 31 | "type": "string" 32 | }, 33 | "productId": { 34 | "type": "string" 35 | }, 36 | "productName": { 37 | "type": "string" 38 | }, 39 | "price": { 40 | "type": "number" 41 | }, 42 | "transactionId": { 43 | "type": "string" 44 | }, 45 | "status": { 46 | "type": "string" 47 | }, 48 | "createdAt": { 49 | "type": "string" 50 | }, 51 | "deliveryDate": { 52 | "type": "string" 53 | } 54 | } 55 | }, 56 | "success": { 57 | "type": "boolean" 58 | } 59 | }, 60 | "required": [ 61 | "order", 62 | "success" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /stage1-architecture/purchasing-tool-service/svc.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | 4 | const db = await JSONFilePreset(path.join("..", "data.json"), { users: [], products: [] }); 5 | 6 | export const supportedStatuses = [ 7 | 'unknown-product', 8 | 'unknown-user', 9 | 'payment-failed', 10 | 'order-processed' 11 | ]; 12 | 13 | export async function purchaseProduct(userId, productId, deliveryDate) { 14 | // Get the user and product 15 | const user = db.data.users.find(u => u.id === userId); 16 | const product = db.data.products.find(p => p.id === productId); 17 | 18 | console.log('Found user:', user); 19 | console.log('Found product:', product); 20 | 21 | if (!user) { 22 | return { 23 | success: false, 24 | status: supportedStatuses[1] 25 | }; 26 | } 27 | 28 | if (!product) { 29 | return { 30 | success: false, 31 | status: supportedStatuses[0] 32 | }; 33 | } 34 | 35 | const transactionId = processPayment(userId, productId, product.price); 36 | 37 | 38 | // Create the order 39 | const order = { 40 | id: `order-${Date.now()}`, 41 | userId, 42 | productId, 43 | productName: product.name, 44 | price: product.price, 45 | transactionId: transactionId, 46 | status: supportedStatuses[3], 47 | createdAt: new Date().toISOString(), 48 | deliveryDate: deliveryDate 49 | }; 50 | 51 | // Add to orders 52 | db.data.orders.push(order); 53 | await db.write() 54 | 55 | // Send notification 56 | sendNotification(userId, `Your order for ${product.name} has been confirmed! Estimated delivery: ${deliveryDate}`); 57 | 58 | return { 59 | success: true, 60 | order 61 | }; 62 | } 63 | 64 | // Payment processing function 65 | function processPayment(userId, productId, amount) { 66 | console.log(`Processing payment of $${amount} for product ${productId} by user ${userId}`); 67 | 68 | const failureChance = Math.random(); 69 | if (failureChance <= 0.5) { 70 | console.log("Payment processing failed - Payment gateway is down!"); 71 | throw new Error('PaymentGatewayDown'); 72 | } 73 | 74 | // OTHER PAYMENT ERRORS: 75 | // In a real application, payment processing requires calling a payment gateway which leads to an asynchronous flow. 76 | // Typically, a webhook has to be setup to accept the final payment state. 77 | // TO KEEP THIS WORKSHOP SIMPLE WE WILL NOT SHOWCASE HOW THESE ARE HANDLED. 78 | // A FUTURE WORKSHOP WILL SHOWCASE HOW YOU CAN MAKE THIS WORK WITH ORRA. 79 | 80 | // Create transaction record 81 | const transactionId = `trans-${Date.now()}-${userId.substring(0, 4)}-${productId.substring(0, 4)}`; 82 | 83 | console.log(`Payment successful! Transaction ID: ${transactionId}`); 84 | 85 | return transactionId; 86 | } 87 | 88 | // Simulated notification 89 | function sendNotification(userId, message) { 90 | // In a real application, this would send an email or push notification 91 | console.log(`Notification to user ${userId}: ${message}`); 92 | return { 93 | success: true, 94 | timestamp: new Date().toISOString() 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /stage1-architecture/stage_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Starting reset process..." 7 | 8 | # Define the base directory as the current directory 9 | BASE_DIR="$(pwd)" 10 | echo "Base directory: $BASE_DIR" 11 | 12 | # Step 1: Reset orra configuration 13 | echo "Resetting orra configuration..." 14 | orra config reset 15 | 16 | # Step 2: Remove dbstore 17 | echo "Removing dbstore..." 18 | rm -rf "$HOME/.orra/dbstore" 19 | 20 | # Step 3: Remove .orra-data directories from top-level subdirectories 21 | echo "Removing .orra-data directories..." 22 | for dir in "$BASE_DIR"/*; do 23 | if [ -d "$dir" ]; then 24 | ORRA_DATA_DIR="$dir/.orra-data" 25 | if [ -d "$ORRA_DATA_DIR" ]; then 26 | echo "Removing $ORRA_DATA_DIR" 27 | rm -rf "$ORRA_DATA_DIR" 28 | fi 29 | fi 30 | done 31 | 32 | # Step 4: Clear ORRA_API_KEY in .env files 33 | echo "Clearing ORRA_API_KEY in .env files..." 34 | for dir in "$BASE_DIR"/*; do 35 | if [ -d "$dir" ]; then 36 | ENV_FILE="$dir/.env" 37 | if [ -f "$ENV_FILE" ]; then 38 | echo "Updating $ENV_FILE" 39 | # Replace the ORRA_API_KEY line with an empty value 40 | sed -i '' 's/^ORRA_API_KEY=.*$/ORRA_API_KEY=/' "$ENV_FILE" 41 | fi 42 | fi 43 | done 44 | 45 | # Step 5: Remove data.json file if it exists 46 | DATA_JSON="$BASE_DIR/data.json" 47 | if [ -f "$DATA_JSON" ]; then 48 | echo "Removing $DATA_JSON" 49 | rm -f "$DATA_JSON" 50 | fi 51 | 52 | echo "Reset completed successfully!" 53 | -------------------------------------------------------------------------------- /stage1-architecture/stage_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Starting setup process..." 7 | 8 | # Define the base directory as the current directory 9 | BASE_DIR="$(pwd)" 10 | echo "Base directory: $BASE_DIR" 11 | 12 | # Step 1: Add a project to the orra plan engine 13 | echo "Adding project to orra plan engine..." 14 | orra projects add assistant 15 | 16 | # Step 2: Add a webhook to the project 17 | echo "Adding webhook to the project..." 18 | orra webhooks add http://localhost:3000/webhook 19 | 20 | # Step 3: Generate a new API key for the project 21 | echo "Generating new API key..." 22 | API_KEY_OUTPUT=$(orra api-keys gen assist-key) 23 | echo "$API_KEY_OUTPUT" 24 | 25 | # Step 4: Extract the API key from the output 26 | # Extract the API key from the output, ensuring whitespace is trimmed 27 | ORRA_API_KEY=$(echo "$API_KEY_OUTPUT" | grep "KEY:" | sed 's/^[[:space:]]*KEY:[[:space:]]*//' | tr -d '[:space:]') 28 | 29 | if [ -z "$ORRA_API_KEY" ]; then 30 | echo "Error: Could not extract API key from output" 31 | exit 1 32 | fi 33 | 34 | echo "Extracted API key: $ORRA_API_KEY" 35 | 36 | # Step 5: Check for .env-example files and copy to .env if needed 37 | echo "Checking for .env-example files..." 38 | for dir in "$BASE_DIR"/*; do 39 | if [ -d "$dir" ]; then 40 | ENV_FILE="$dir/.env" 41 | ENV_EXAMPLE_FILE="$dir/.env-example" 42 | 43 | # If .env doesn't exist but .env-example does, copy it 44 | if [ ! -f "$ENV_FILE" ] && [ -f "$ENV_EXAMPLE_FILE" ]; then 45 | echo "Creating $ENV_FILE from $ENV_EXAMPLE_FILE" 46 | cp "$ENV_EXAMPLE_FILE" "$ENV_FILE" 47 | fi 48 | fi 49 | done 50 | 51 | # Step 6: Add the ORRA_API_KEY to all .env files 52 | echo "Adding ORRA_API_KEY to .env files..." 53 | for dir in "$BASE_DIR"/*; do 54 | if [ -d "$dir" ]; then 55 | ENV_FILE="$dir/.env" 56 | if [ -f "$ENV_FILE" ]; then 57 | echo "Updating $ENV_FILE" 58 | # Check if ORRA_API_KEY line exists in the file 59 | if grep -q "^ORRA_API_KEY=" "$ENV_FILE"; then 60 | # Replace existing ORRA_API_KEY line 61 | sed -i '' "s|^ORRA_API_KEY=.*$|ORRA_API_KEY=$ORRA_API_KEY|" "$ENV_FILE" 62 | else 63 | # Add ORRA_API_KEY line if it doesn't exist 64 | echo "ORRA_API_KEY=$ORRA_API_KEY" >> "$ENV_FILE" 65 | fi 66 | else 67 | # Create new .env file if it doesn't exist 68 | echo "Creating new $ENV_FILE" 69 | echo "ORRA_API_KEY=$ORRA_API_KEY" > "$ENV_FILE" 70 | fi 71 | fi 72 | done 73 | 74 | # Step 7: Create data.json file by copying from example 75 | DATA_JSON_EXAMPLE="$BASE_DIR/data.json-example" 76 | DATA_JSON="$BASE_DIR/data.json" 77 | 78 | if [ -f "$DATA_JSON_EXAMPLE" ]; then 79 | echo "Creating data.json from example..." 80 | cp "$DATA_JSON_EXAMPLE" "$DATA_JSON" 81 | echo "data.json created successfully" 82 | else 83 | echo "Warning: data.json-example not found, skipping data.json creation" 84 | fi 85 | 86 | echo "Setup completed successfully!" 87 | -------------------------------------------------------------------------------- /stage2-consistency/README.md: -------------------------------------------------------------------------------- 1 | # Stage 2: Reliable Consistency with orra 2 | 3 | In this stage, we address the critical issue of ensuring consistent state in our marketplace assistant by implementing compensation handlers for critical operations. 4 | 5 | ## The Problem: Inconsistent State 6 | 7 | Our Stage 1 architecture had a significant reliability flaw: 8 | 9 | 1. When purchasing an item, inventory was reserved (stock reduced) BEFORE payment processing 10 | 2. If the payment failed, the inventory remained reduced, creating an inconsistent state 11 | 3. Products appeared out of stock even though they were never purchased 12 | 4. This required manual intervention to fix the inventory 13 | 14 | This issue is particularly problematic because payment processing is inherently unreliable. Payment gateways can fail for numerous reasons, from network issues to bank verification problems. While our current implementation focuses on immediate payment failures, real-world systems must also handle delayed rejections that occur well after the initial transaction attempt. 15 | 16 | This problem demonstrates a classic challenge in distributed systems: maintaining consistent state across services when operations can fail at any point in a transaction flow. 17 | 18 | This stage rigs the **Purchasing Tool as Service** payment processing to **ALWAYS fail**. 19 | 20 | ## What Changed 21 | 22 | We've implemented compensation handlers for critical operations: 23 | 24 | **Purchasing Payment Failure Compensation**: If a payment fails during purchasing, we automatically restore inventory 25 | 26 | ![](images/ReliableState.png) 27 | 28 | ## Run this stage 29 | 30 | ### Prerequisites 31 | - Node.js (v18+) 32 | - orra [Plan Engine running and CLI installed](https://github.com/orra-dev/orra/tree/main#installation) 33 | - [OpenAI API key](https://platform.openai.com/docs/api-reference/authentication) 34 | 35 | ### Setup & Run 36 | 37 | 1. **Initialize orra configuration** 38 | ```bash 39 | ./stage_setup.sh # Sets up project, webhooks, and API keys 40 | 41 | 2. **Configure OpenAI API key in each component's `.env` file** 42 | ```shell 43 | OPENAI_API_KEY=your_openai_api_key_here 44 | ``` 45 | 3. **Start each component (in separate terminals)** 46 | ```shell 47 | cd [component-directory] # Run for each component 48 | npm install 49 | npm start 50 | ``` 51 | 4. **Start webhook simulator (in a separate terminal)** 52 | ```bash 53 | orra verify webhooks start http://localhost:3000/webhook 54 | ``` 55 | ### Using the Application 56 | 57 | In this case we just want to demonstrate how the AI Marketplace Assistant can handle payment failures in our purchasing flow. 58 | 59 | Again, we'll be using the [CLI](https://github.com/orra-dev/orra/blob/main/docs/cli.md)'s `orra verify` command to understand how the Plan Engine is coordinating our components to complete system actions. 60 | 61 | The assumption here is that there's a chat UI interface that forwards requests to the Plan Engine. 62 | 63 | We use lowdb to query and update data in our [data.json](data.json) file - basically a simple JSON based DB. This data is shared against all the components. 64 | 65 | 1. **Purchase a recommended product** 66 | 67 | ```bash 68 | orra verify run 'Purchase product' \ 69 | -d 'productId:laptop-1' \ 70 | -d 'userId:user-1' 71 | ``` 72 | 73 | In this case, there should be 74 | - an inventory check to ensure the product is in-stock 75 | - an inventory reserve request if the product is in-stock - this lowers the stock count 76 | - A delivery estimate is provided 77 | - A product purchase is attempted and fails after retries 78 | - The reserved product is released using [orra compensations](https://github.com/orra-dev/orra/blob/main/docs/compensations.md) running the [onRevert Handler](#compensation-handler-definition). 79 | 80 | Navigate to the [data.json](data.json) file there should be NO placed `order` in the `orders` list. 81 | 82 | ### Reset Environment 83 | 84 | 1. **Clear Plan Engine configurations and reset data** 85 | ```bash 86 | ./stage_reset.sh # Clears configurations and data 87 | ``` 88 | 89 | 2. **Stop all the running components and kill all the terminal window** 90 | 91 | 3. **Shutdown the Plan Engine** 92 | 93 | ## Benefits 94 | 95 | 1. **Consistent State**: The system maintains data consistency even when operations fail 96 | 2. **Automatic Recovery**: No manual intervention needed to fix data issues 97 | 3. **Increased Reliability**: Users can trust that products won't disappear incorrectly 98 | 4. **Audit Trail**: Complete history of operations and compensations 99 | 100 | ## How orra Helps 101 | 102 | - **Compensation Framework**: orra provides built-in support for defining compensation handlers 103 | - **Automatic Triggering**: Compensation is automatically triggered when operations fail 104 | - **Orchestration**: orra manages the complex flow of operations and compensations 105 | 106 | ## Implementation Details 107 | 108 | ### Compensation Handler Definition 109 | 110 | Example inventory release compensation: 111 | 112 | ```javascript 113 | // Register compensation handler for inventory reservation 114 | inventoryService.onRevert(async (task, result) => { 115 | // Only process compensations for reserveProduct actions 116 | if (task.input.action === 'reserveProduct' && result.success) { 117 | // Compensation logic: release the product that was reserved 118 | const releaseResult = releaseProduct(result.productId, 1); 119 | console.log('Inventory compensation completed:', releaseResult); 120 | } 121 | }); 122 | ``` 123 | 124 | ### Handling Compensation 125 | 126 | 1. **Begin Transaction**: orra starts tracking a transaction 127 | 2. **Register Operations**: Each operation registers potential compensation 128 | 3. **Execute Operations**: Normal flow proceeds 129 | 4. **Handle Failures**: If an operation fails, orra automatically: 130 | - Stops forward progress 131 | - Executes compensation handlers in reverse order 132 | - Records the compensation actions 133 | - Notifies appropriate services/agents 134 | 135 | ## Next Steps 136 | 137 | While our system is now more reliable with compensation handlers, it can still allow agents to perform operations outside our business domain constraints. In [Stage 3](../stage3-grounding), we'll implement domain guardrails to prevent hallucinations and enforce business rules. 138 | -------------------------------------------------------------------------------- /stage2-consistency/data.json-example: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "laptop-1", 5 | "name": "Dell XPS 13 (2022)", 6 | "description": "Used Dell XPS 13 laptop with 16GB RAM, 512GB SSD, Intel i7 processor", 7 | "price": 750, 8 | "condition": "excellent", 9 | "category": "laptops", 10 | "tags": ["programming", "college", "portable"], 11 | "inStock": 1, 12 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 13 | }, 14 | { 15 | "id": "laptop-2", 16 | "name": "MacBook Air M1", 17 | "description": "Used MacBook Air with M1 chip, 8GB RAM, 256GB SSD", 18 | "price": 650, 19 | "condition": "good", 20 | "category": "laptops", 21 | "tags": ["college", "portable", "mac"], 22 | "inStock": 2, 23 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 24 | }, 25 | { 26 | "id": "laptop-3", 27 | "name": "Lenovo ThinkPad X1 Carbon", 28 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 29 | "price": 820, 30 | "condition": "excellent", 31 | "category": "laptops", 32 | "tags": ["business", "programming", "durable"], 33 | "inStock": 0, 34 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 35 | }, 36 | { 37 | "id": "laptop-4", 38 | "name": "Lenovo ThinkPad X1 Carbon", 39 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 40 | "price": 500, 41 | "condition": "fair", 42 | "category": "laptops", 43 | "tags": ["business", "programming", "durable"], 44 | "inStock": 4, 45 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 46 | } 47 | ], 48 | "orders": [], 49 | "users": [ 50 | { 51 | "id": "user-1", 52 | "name": "John Doe", 53 | "email": "john@example.com", 54 | "address": "1a Goldsmiths Row, London E2 8QA" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /stage2-consistency/delivery-agent/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | DELIVERY_AGENT_PORT=3004 10 | -------------------------------------------------------------------------------- /stage2-consistency/delivery-agent/agent.js: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { JSONFilePreset } from "lowdb/node"; 3 | import * as path from "node:path"; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY 10 | }); 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [], users: [] }); 13 | 14 | export const supportedStatuses = [ 15 | 'unknown-product', 16 | 'unknown-user', 17 | 'delivery-estimated', 18 | ] 19 | 20 | // Simulated traffic and logistics data 21 | const trafficConditions = { 22 | "route_segments": [ 23 | { 24 | "segment_id": "A90-ABD-DND", 25 | "name": "A90 Aberdeen to Dundee", 26 | "length_km": 108, 27 | "current_average_speed_kph": 95, 28 | "normal_average_speed_kph": 100, 29 | "congestion_level": "light", 30 | "incidents": [] 31 | }, 32 | { 33 | "segment_id": "M90-PER-EDI", 34 | "name": "M90 Perth to Edinburgh", 35 | "length_km": 45, 36 | "current_average_speed_kph": 110, 37 | "normal_average_speed_kph": 110, 38 | "congestion_level": "none", 39 | "incidents": [ 40 | { 41 | "type": "roadworks", 42 | "location": "Junction 3", 43 | "description": "Lane closure for resurfacing", 44 | "delay_minutes": 10 45 | } 46 | ] 47 | }, 48 | { 49 | "segment_id": "A1-NCL-YRK", 50 | "name": "A1 Newcastle to York", 51 | "length_km": 140, 52 | "current_average_speed_kph": 100, 53 | "normal_average_speed_kph": 110, 54 | "congestion_level": "moderate", 55 | "incidents": [ 56 | { 57 | "type": "accident", 58 | "location": "Near Darlington", 59 | "description": "Multi-vehicle collision", 60 | "delay_minutes": 25 61 | } 62 | ] 63 | } 64 | ], 65 | "weather_conditions": [ 66 | { 67 | "location": "Northeast", 68 | "condition": "light_rain", 69 | "temperature_celsius": 12 70 | }, 71 | { 72 | "location": "Midwest", 73 | "condition": "cloudy", 74 | "temperature_celsius": 14 75 | }, 76 | { 77 | "location": "West Coast", 78 | "condition": "clear", 79 | "temperature_celsius": 20 80 | }, 81 | { 82 | "location": "Southeast", 83 | "condition": "partly_cloudy", 84 | "temperature_celsius": 22 85 | } 86 | ], 87 | "vehicles": [ 88 | { 89 | "type": "van", 90 | "capacity_cubic_meters": 15, 91 | "max_range_km": 500, 92 | "average_speed_kph": 90, 93 | "availability": "high" 94 | }, 95 | { 96 | "type": "truck", 97 | "capacity_cubic_meters": 40, 98 | "max_range_km": 800, 99 | "average_speed_kph": 80, 100 | "availability": "medium" 101 | } 102 | ] 103 | }; 104 | 105 | // Function to generate delivery estimates using LLM 106 | export async function generateDeliveryEstimates(userId, productId, inStock=0) { 107 | if (inStock && inStock < 1) { 108 | throw new Error('CannotEstimateDeliveryForOutOfStockProduct'); 109 | } 110 | 111 | const user = db.data. users.find(u => u.id === userId); 112 | const product = db.data.products.find(p => p.id === productId); 113 | 114 | if (!user) { 115 | return { 116 | success: false, 117 | status: supportedStatuses[1] 118 | }; 119 | } 120 | 121 | if (!product) { 122 | return { 123 | success: false, 124 | status: supportedStatuses[0] 125 | }; 126 | } 127 | 128 | // Use LLM to generate intelligent delivery estimates 129 | const systemPrompt = `You are a delivery logistics expert with 20 years of experience. 130 | Your task is to provide realistic delivery estimates for products being shipped from a warehouse to a customer. 131 | Consider all relevant factors including traffic conditions, weather, distance, and product characteristics. 132 | Always provide both best-case and worst-case scenarios with confidence levels.`; 133 | 134 | const userPrompt = `Create delivery estimates for the following: 135 | 136 | WAREHOUSE ADDRESS: ${product.warehouseAddress} 137 | CUSTOMER ADDRESS: ${user.address} 138 | 139 | Current traffic and logistics data: 140 | ${JSON.stringify(trafficConditions, null, 2)} 141 | 142 | Your response should include: 143 | 1. A best-case delivery estimate (duration in hours and delivery date) 144 | 2. A worst-case delivery estimate (duration in hours and delivery date) 145 | 3. Confidence levels for each estimate (low/moderate/high) 146 | 4. A brief explanation of your reasoning 147 | 148 | Respond in JSON format with these components. 149 | 150 | USE THIS SCHEMA FOR THE FINAL ANSWER: 151 | { 152 | "bestCase": { 153 | "estimatedDurationHours": "expected duration as decimal value, e.g. 7.5", 154 | "estimatedDeliveryDate": "estimated delivery date in the future as a timestamp, e.g. 2024-10-02T21:15:00Z", 155 | "confidenceLevel": "how confident you are. one of: low, moderate or high" 156 | }, 157 | "worstCase": { 158 | "estimatedDurationHours": "expected duration as decimal value, e.g. 7.5", 159 | "estimatedDeliveryDate": "estimated delivery date in the future as a timestamp, e.g. 2024-10-02T21:15:00Z", 160 | "confidenceLevel": "how confident you are. one of: low, moderate or high" 161 | }, 162 | "explanation": "Delivery estimate based on current traffic and weather conditions. Factors include road conditions, distance, and typical shipping times." 163 | }`; 164 | 165 | try { 166 | const response = await openai.chat.completions.create({ 167 | model: "gpt-4o", 168 | messages: [ 169 | { role: "system", content: systemPrompt }, 170 | { role: "user", content: userPrompt } 171 | ], 172 | response_format: { type: "json_object" } 173 | }); 174 | 175 | const content = JSON.parse(response.choices[0].message.content); 176 | console.log("Generated delivery estimates:", content); 177 | 178 | const fallbackDeliveryEstimatedHours = 72 179 | const fallbackDeliveryDate = new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString().split('T')[0] 180 | const fallbackExplanation = "Delivery estimate based on current traffic and weather conditions." 181 | 182 | return { 183 | status: supportedStatuses[2], 184 | success: true, 185 | estimatedDays: hoursToDays(content?.worstCase?.estimatedDays || fallbackDeliveryEstimatedHours), 186 | deliveryDate: content?.worstCase?.estimatedDeliveryDate?.split('T')[0] || fallbackDeliveryDate, 187 | explanation: content?.explanation || fallbackExplanation, 188 | }; 189 | } catch (error) { 190 | console.error("Error generating delivery estimates:", error); 191 | throw error; 192 | } 193 | } 194 | 195 | function hoursToDays(hours) { 196 | if (typeof hours !== "number" || isNaN(hours)) { 197 | throw new Error("Please provide a valid number of hours."); 198 | } 199 | return hours / 24; 200 | } 201 | -------------------------------------------------------------------------------- /stage2-consistency/delivery-agent/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initAgent } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { generateDeliveryEstimates } from "./agent.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.DELIVERY_AGENT_PORT || 3004; 11 | 12 | // Initialize the orra agent 13 | const deliveryAgent = initAgent({ 14 | name: 'delivery-agent', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/delivery-agent-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | 29 | async function startService() { 30 | try { 31 | // Register agent with orra 32 | await deliveryAgent.register({ 33 | description: 'An agent that provides intelligent delivery estimates based on product, location, and current conditions.', 34 | schema 35 | }); 36 | 37 | // Start handling tasks 38 | deliveryAgent.start(async (task) => { 39 | console.log('Processing delivery estimation task:', task.id); 40 | console.log('Input:', task.input); 41 | 42 | const { userId, productId, inStock } = task.input; 43 | 44 | // Use LLM to generate delivery estimates 45 | const result = await generateDeliveryEstimates(userId, productId, inStock); 46 | // FEATURE COMING SOON: 47 | // if (result.status !== 'success') { 48 | // return task.abort(result); 49 | // } 50 | return result; 51 | }); 52 | 53 | console.log('Delivery Agent started successfully'); 54 | } catch (error) { 55 | console.error('Failed to start Delivery Agent:', error); 56 | process.exit(1); 57 | } 58 | } 59 | 60 | // Start the Express server and the agent 61 | app.listen(port, () => { 62 | console.log(`Delivery Agent listening on port ${port}`); 63 | startService().catch(console.error); 64 | }); 65 | -------------------------------------------------------------------------------- /stage2-consistency/delivery-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delivery-agent", 3 | "version": "1.0.0", 4 | "description": "LLM-powered delivery estimation agent for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "openai": "^4.11.0", 17 | "lowdb": "^7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stage2-consistency/delivery-agent/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "userId": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | }, 11 | "inStock": { 12 | "type": "number" 13 | } 14 | }, 15 | "required": ["userId", "productId"] 16 | }, 17 | "output": { 18 | "type": "object", 19 | "properties": { 20 | "status": { 21 | "type": "string" 22 | }, 23 | "success": { 24 | "type": "boolean" 25 | }, 26 | "estimatedDays": { 27 | "type": "number" 28 | }, 29 | "deliveryDate": { 30 | "type": "string" 31 | }, 32 | "explanation": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["estimatedDays", "deliveryDate", "explanation"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stage2-consistency/images/ReliableState.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orra-dev/agent-fragile-to-prod-guide/3f1875ef08881c71cb09e49d4034952781e9440b/stage2-consistency/images/ReliableState.png -------------------------------------------------------------------------------- /stage2-consistency/inventory-tool-service/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | INVENTORY_SERVICE_PORT=3002 10 | -------------------------------------------------------------------------------- /stage2-consistency/inventory-tool-service/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initService } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { execInventory, releaseProduct } from "./svc.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.INVENTORY_SERVICE_PORT || 3002; 11 | 12 | // Initialize the orra service 13 | const inventoryService = initService({ 14 | name: 'inventory-service', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/inventory-service-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | async function startService() { 29 | try { 30 | // Register service with orra 31 | await inventoryService.register({ 32 | description: `A service that manages product inventory, checks availability and reserves products. 33 | Supported actions: checkAvailability (gets product status), reserveProduct (reduces inventory), and releaseProduct (returns inventory).`, 34 | schema, 35 | revertible: true // Enable compensations 36 | }); 37 | 38 | // Register compensation handler 39 | inventoryService.onRevert(async (task, result) => { 40 | // Only process compensations for reserveProduct actions 41 | if (task.input.action === 'reserveProduct' && result.success) { 42 | console.log('Reverting inventory product for task:', task.id); 43 | console.log('Reverting inventory product hold for product:', result.productId); 44 | 45 | // Compensation logic: release the product that was reserved 46 | const releaseResult = releaseProduct(result.productId, 1); 47 | console.log('Inventory compensation completed:', JSON.stringify(releaseResult)); 48 | } 49 | }); 50 | 51 | // Start handling tasks 52 | inventoryService.start(async (task) => { 53 | console.log('Processing inventory task:', task.id); 54 | console.log('Input:', task.input); 55 | 56 | const { action, productId } = task.input; 57 | const result = await execInventory(action, productId); 58 | 59 | // FEATURE COMING SOON: 60 | // if (result.status !== 'success') { 61 | // return task.abort(result); 62 | // } 63 | 64 | return result; 65 | }); 66 | 67 | console.log('Inventory Service started successfully'); 68 | } catch (error) { 69 | console.error('Failed to start Inventory Service:', error); 70 | process.exit(1); 71 | } 72 | } 73 | 74 | // Start the Express server and the service 75 | app.listen(port, () => { 76 | console.log(`Inventory Service listening on port ${port}`); 77 | startService().catch(console.error); 78 | }); 79 | -------------------------------------------------------------------------------- /stage2-consistency/inventory-tool-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory-tool-service", 3 | "version": "1.0.0", 4 | "description": "Inventory management previously a tool now a service for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "lowdb": "^7.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage2-consistency/inventory-tool-service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "action": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | } 11 | }, 12 | "required": ["action", "productId"] 13 | }, 14 | "output": { 15 | "type": "object", 16 | "properties": { 17 | "action": { 18 | "type": "string" 19 | }, 20 | "productId": { 21 | "type": "string" 22 | }, 23 | "status": { 24 | "type": "string" 25 | }, 26 | "success": { 27 | "type": "boolean" 28 | }, 29 | "inStock": { 30 | "type": "number" 31 | }, 32 | "message": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["action", "productId"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stage2-consistency/inventory-tool-service/svc.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | 4 | export const supportedStatuses = [ 5 | 'unknown-product', 6 | 'product-available', 7 | 'product-out-of-stock', 8 | 'product-reserved', 9 | 'product-released' 10 | ] 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [] }); 13 | 14 | export async function execInventory(action, productId) { 15 | console.log('executing inventory action: ', action, ' for product: ', productId); 16 | switch (action) { 17 | case 'checkAvailability': 18 | return checkAvailability(productId); 19 | case 'reserveProduct': 20 | return await reserveProduct(productId); 21 | case 'releaseProduct': 22 | return releaseProduct(productId); 23 | default: 24 | throw new Error(`Unknown action: ${action}`); 25 | } 26 | } 27 | 28 | // Service functions 29 | function checkAvailability(productId) { 30 | const product = db.data.products.find(p => p.id === productId); 31 | 32 | if (!product) { 33 | return { 34 | action: "checkAvailability", 35 | productId, 36 | status: supportedStatuses[0], 37 | success: false, 38 | inStock: 0, 39 | message: "Product not found" 40 | }; 41 | } 42 | 43 | return { 44 | action: "checkAvailability", 45 | productId, 46 | status: product.inStock > 0 ? supportedStatuses[1] : supportedStatuses[2], 47 | success: true, 48 | inStock: product.inStock, 49 | message: "Product in stock" 50 | }; 51 | } 52 | 53 | async function reserveProduct(productId, quantity = 1) { 54 | const product = db.data.products.find(p => p.id === productId); 55 | 56 | if (!product) { 57 | return { 58 | action: "reserveProduct", 59 | productId, 60 | status: supportedStatuses[0], 61 | success: false, 62 | inStock: 0, 63 | message: "Product not found" 64 | }; 65 | } 66 | 67 | if (product.inStock < quantity) { 68 | return { 69 | action: "reserveProduct", 70 | productId, 71 | status: supportedStatuses[2], 72 | success: false, 73 | inStock: product.inStock, 74 | message: `Insufficient stock. Requested: ${quantity}, Available: ${product.inStock}` 75 | }; 76 | } 77 | 78 | // Reserve the product 79 | product.inStock -= quantity; 80 | await db.write() 81 | 82 | return { 83 | action: "reserveProduct", 84 | productId, 85 | status: supportedStatuses[3], 86 | success: true, 87 | inStock: product.inStock, 88 | message: `Successfully reserved ${quantity} units of ${product.name}` 89 | }; 90 | } 91 | 92 | export async function releaseProduct(productId, quantity = 1) { 93 | const product = db.data.products.find(p => p.id === productId); 94 | 95 | if (!product) { 96 | return { 97 | action: "releaseProduct", 98 | productId, 99 | status: supportedStatuses[0], 100 | success: false, 101 | inStock: 0, 102 | message: "Product not found" 103 | }; 104 | } 105 | 106 | // Release the reservation 107 | product.inStock += quantity; 108 | await db.write() 109 | 110 | console.log(`Released ${quantity} units of ${product.name}. New stock: ${product.inStock}`); 111 | 112 | return { 113 | action: "releaseProduct", 114 | productId, 115 | status: supportedStatuses[4], 116 | success: true, 117 | inStock: product.inStock, 118 | message: `Successfully released ${quantity} units of ${product.name}` 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /stage2-consistency/product-advisor-agent/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxx 7 | 8 | # Ports 9 | PRODUCT_ADVISOR_PORT=3001 10 | -------------------------------------------------------------------------------- /stage2-consistency/product-advisor-agent/agent.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | import OpenAI from "openai"; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY 10 | }); 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [] }); 13 | 14 | const functionSpecs = [ 15 | { 16 | name: "searchProducts", 17 | description: "Search for products based on criteria", 18 | parameters: { 19 | type: "object", 20 | properties: { 21 | category: { 22 | type: "string", 23 | description: "Product category (e.g., 'laptops')" 24 | }, 25 | priceMax: { 26 | type: "number", 27 | description: "Maximum price" 28 | }, 29 | tags: { 30 | type: "array", 31 | items: { 32 | type: "string" 33 | }, 34 | description: "Tags to filter by (e.g., ['programming', 'college'])" 35 | }, 36 | condition: { 37 | type: "string", 38 | description: "Product condition ('excellent', 'good', 'fair')" 39 | } 40 | } 41 | } 42 | } 43 | ]; 44 | 45 | const availableFunctions = { 46 | searchProducts: (args) => { 47 | const { category, priceMax, tags, condition } = args; 48 | 49 | let filteredProducts = [...db.data.products]; 50 | 51 | if (category) { 52 | filteredProducts = filteredProducts.filter(p => p.category === category); 53 | } 54 | 55 | if (priceMax) { 56 | filteredProducts = filteredProducts.filter(p => p.price <= priceMax); 57 | } 58 | 59 | if (tags && tags.length > 0) { 60 | filteredProducts = filteredProducts.filter(p => 61 | tags.some(tag => p.tags.includes(tag)) 62 | ); 63 | } 64 | 65 | if (condition) { 66 | filteredProducts = filteredProducts.filter(p => p.condition === condition); 67 | } 68 | 69 | return filteredProducts; 70 | }, 71 | }; 72 | 73 | export async function recommendProduct(query) { 74 | const data = await runAdvisor(query); 75 | return extractAndParseJson(data.response); 76 | } 77 | 78 | async function runAdvisor(query, conversationHistory = []) { 79 | try { 80 | // Add the user input to the conversation history 81 | conversationHistory.push({ role: "user", content: query }); 82 | 83 | // Define the system message 84 | const systemMessage = { 85 | role: "system", 86 | content: `You are an AI marketplace assistant. Given a user query and product data, recommend the most suitable products. 87 | For each recommendation, provide reasoning based on the user's needs. 88 | 89 | Always be helpful, concise, and provide specific product recommendations that match user criteria. 90 | 91 | Focus ONLY on products that are in stock (inStock > 0). 92 | 93 | Generate recommendations in JSON format with reasoning for why each product matches the user's needs. 94 | 95 | USE THIS SCHEMA FOR THE FINAL ANSWER: 96 | { 97 | "recommendations": [ 98 | { 99 | "id": "the recommended product's Id", 100 | "name": "the recommended product's name", 101 | "description": "the recommended product's description", 102 | "reasoning": "the reason for recommending the product", 103 | } 104 | ] 105 | } 106 | 107 | If there are NO matching products that are in stock (inStock > 0) return an empty recommendations array.` 108 | }; 109 | 110 | // Create the messages array for the API call 111 | const messages = [systemMessage, ...conversationHistory]; 112 | 113 | // Step 1: Call OpenAI API with function definitions 114 | const response = await openai.chat.completions.create({ 115 | model: "gpt-4o", 116 | messages: messages, 117 | functions: functionSpecs, 118 | function_call: "auto", 119 | response_format: { type: "json_object" }, 120 | temperature: 0.7, 121 | }); 122 | 123 | const responseMessage = response.choices[0].message; 124 | 125 | // Step 2: Check if the model wants to call a function 126 | if (responseMessage.function_call) { 127 | const functionName = responseMessage.function_call.name; 128 | const functionArgs = JSON.parse(responseMessage.function_call.arguments); 129 | 130 | console.log(`\nCalling function: ${functionName} with args:`, functionArgs); 131 | 132 | // Call the function 133 | const functionResponse = availableFunctions[functionName](functionArgs); 134 | 135 | // Step 3: Append function response to messages 136 | conversationHistory.push(responseMessage); // Add assistant's function call to history 137 | 138 | // Add the function response to chat history 139 | conversationHistory.push({ 140 | role: "function", 141 | name: functionName, 142 | content: JSON.stringify(functionResponse) 143 | }); 144 | 145 | // Step 4: Get a new response from the model with the function response 146 | const secondResponse = await openai.chat.completions.create({ 147 | model: "gpt-4o", 148 | messages: [...messages, responseMessage, { 149 | role: "function", 150 | name: functionName, 151 | content: JSON.stringify(functionResponse) 152 | }], 153 | functions: functionSpecs, 154 | function_call: "auto", 155 | temperature: 0.7, 156 | }); 157 | 158 | const secondResponseMessage = secondResponse.choices[0].message; 159 | 160 | // Handle nested function calls if needed 161 | if (secondResponseMessage.function_call) { 162 | const secondFunctionName = secondResponseMessage.function_call.name; 163 | const secondFunctionArgs = JSON.parse(secondResponseMessage.function_call.arguments); 164 | 165 | console.log(`\nCalling second function: ${secondFunctionName} with args:`, secondFunctionArgs); 166 | 167 | const secondFunctionResponse = availableFunctions[secondFunctionName](secondFunctionArgs); 168 | 169 | conversationHistory.push(secondResponseMessage); 170 | 171 | conversationHistory.push({ 172 | role: "function", 173 | name: secondFunctionName, 174 | content: JSON.stringify(secondFunctionResponse) 175 | }); 176 | 177 | // Get final response from the model 178 | const finalResponse = await openai.chat.completions.create({ 179 | model: "gpt-4o", 180 | messages: [...messages, responseMessage, { 181 | role: "function", 182 | name: functionName, 183 | content: JSON.stringify(functionResponse) 184 | }, secondResponseMessage, { 185 | role: "function", 186 | name: secondFunctionName, 187 | content: JSON.stringify(secondFunctionResponse) 188 | }], 189 | temperature: 0.7, 190 | }); 191 | 192 | const finalResponseMessage = finalResponse.choices[0].message; 193 | conversationHistory.push(finalResponseMessage); 194 | 195 | return { 196 | response: finalResponseMessage.content, 197 | conversationHistory 198 | }; 199 | } 200 | 201 | conversationHistory.push(secondResponseMessage); 202 | 203 | return { 204 | response: secondResponseMessage.content, 205 | conversationHistory 206 | }; 207 | } 208 | 209 | // If no function call, just return the response 210 | conversationHistory.push(responseMessage); 211 | 212 | return { 213 | response: responseMessage.content, 214 | conversationHistory 215 | }; 216 | } catch (error) { 217 | console.error("Error in marketplace assistant:", error); 218 | throw error; 219 | } 220 | } 221 | 222 | function extractAndParseJson(input) { 223 | // Extract the JSON content 224 | const jsonMatch = input.match(/```json\n([\s\S]*?)\n```/); 225 | 226 | if (!jsonMatch) { 227 | throw new Error("No JSON content found between ```json``` tags"); 228 | } 229 | 230 | const jsonString = jsonMatch[1]; 231 | 232 | // Parse the JSON 233 | try { 234 | return JSON.parse(jsonString); 235 | } catch (error) { 236 | throw new Error(`Failed to parse JSON: ${error.message}`); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /stage2-consistency/product-advisor-agent/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initAgent } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { recommendProduct } from "./agent.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.PRODUCT_ADVISOR_PORT || 3001; 11 | 12 | // Initialize the orra agent 13 | const productAdvisor = initAgent({ 14 | name: 'product-advisor', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/product-advisor-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | // Function to generate product recommendations using LLM 29 | 30 | 31 | async function startService() { 32 | try { 33 | // Register agent with orra 34 | await productAdvisor.register({ 35 | description: 'An agent that helps users find products based on their needs and preferences.', 36 | schema 37 | }); 38 | 39 | // Start handling tasks 40 | productAdvisor.start(async (task) => { 41 | console.log('Processing product advisory task:', task.id); 42 | console.log('Input:', task.input); 43 | 44 | const { query } = task.input; 45 | 46 | // Use LLM to generate recommendations 47 | return await recommendProduct(query); 48 | }); 49 | 50 | console.log('Product Advisor agent started successfully'); 51 | } catch (error) { 52 | console.error('Failed to start Product Advisor agent:', error); 53 | process.exit(1); 54 | } 55 | } 56 | 57 | // Start the Express server and the agent 58 | app.listen(port, () => { 59 | console.log(`Product Advisor listening on port ${port}`); 60 | startService().catch(console.error); 61 | }); 62 | -------------------------------------------------------------------------------- /stage2-consistency/product-advisor-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product-advisor", 3 | "version": "1.0.0", 4 | "description": "Product advisor agent for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "openai": "^4.11.0", 17 | "lowdb": "^7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stage2-consistency/product-advisor-agent/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "string" 7 | } 8 | }, 9 | "required": ["query"] 10 | }, 11 | "output": { 12 | "type": "object", 13 | "properties": { 14 | "recommendations": { 15 | "type": "array", 16 | "items": { 17 | "type": "object", 18 | "properties": { 19 | "id": { 20 | "type": "string" 21 | }, 22 | "name": { 23 | "type": "string" 24 | }, 25 | "description": { 26 | "type": "string" 27 | }, 28 | "reasoning": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "required": ["recommendations"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stage2-consistency/purchasing-tool-service/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | PAYMENT_SERVICE_PORT=3003 10 | -------------------------------------------------------------------------------- /stage2-consistency/purchasing-tool-service/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initService } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { purchaseProduct } from './svc.js'; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.PAYMENT_SERVICE_PORT || 3003; 11 | 12 | // Initialize the orra service 13 | const purchaseService = initService({ 14 | name: 'purchase-service', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/purchase-service-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | async function startService() { 29 | try { 30 | // Register service with orra 31 | await purchaseService.register({ 32 | description: 'A service that makes marketplace product purchases on behalf of a user. It creates purchase orders that include shipping details, makes payments against external payment gateways and notifies users.', 33 | schema 34 | }); 35 | 36 | // Start handling tasks 37 | purchaseService.start(async (task) => { 38 | console.log('Processing purchase task:', task.id); 39 | console.log('Input:', task.input); 40 | 41 | const { userId, productId, deliveryDate } = task.input; 42 | 43 | // Process the purchase order 44 | const result = purchaseProduct(userId, productId, deliveryDate); 45 | // FEATURE COMING SOON: 46 | // if (result.status !== 'success') { 47 | // return task.abort(result); 48 | // } 49 | return result; 50 | }); 51 | 52 | console.log('Payment Service started successfully'); 53 | } catch (error) { 54 | console.error('Failed to start Payment Service:', error); 55 | process.exit(1); 56 | } 57 | } 58 | 59 | // Start the Express server and the service 60 | app.listen(port, () => { 61 | console.log(`Payment Service listening on port ${port}`); 62 | startService().catch(console.error); 63 | }); 64 | -------------------------------------------------------------------------------- /stage2-consistency/purchasing-tool-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purchase-tool-service", 3 | "version": "1.0.0", 4 | "description": "Purchase processing previously a tool now a service for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "lowdb": "^7.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage2-consistency/purchasing-tool-service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "userId": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | }, 11 | "deliveryDate": { 12 | "type": "string" 13 | } 14 | }, 15 | "required": [ 16 | "userId", 17 | "productId", 18 | "deliveryDate" 19 | ] 20 | }, 21 | "output": { 22 | "type": "object", 23 | "properties": { 24 | "order": { 25 | "type": "object", 26 | "properties": { 27 | "id": { 28 | "type": "string" 29 | }, 30 | "userId": { 31 | "type": "string" 32 | }, 33 | "productId": { 34 | "type": "string" 35 | }, 36 | "productName": { 37 | "type": "string" 38 | }, 39 | "price": { 40 | "type": "number" 41 | }, 42 | "transactionId": { 43 | "type": "string" 44 | }, 45 | "status": { 46 | "type": "string" 47 | }, 48 | "createdAt": { 49 | "type": "string" 50 | }, 51 | "deliveryDate": { 52 | "type": "string" 53 | } 54 | } 55 | }, 56 | "success": { 57 | "type": "boolean" 58 | } 59 | }, 60 | "required": [ 61 | "order", 62 | "success" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /stage2-consistency/purchasing-tool-service/svc.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | 4 | const db = await JSONFilePreset(path.join("..", "data.json"), { users: [], products: [] }); 5 | 6 | export const supportedStatuses = [ 7 | 'unknown-product', 8 | 'unknown-user', 9 | 'payment-failed', 10 | 'order-processed' 11 | ]; 12 | 13 | export async function purchaseProduct(userId, productId, deliveryDate) { 14 | // Get the user and product 15 | const user = db.data.users.find(u => u.id === userId); 16 | const product = db.data.products.find(p => p.id === productId); 17 | 18 | console.log('Found user:', user); 19 | console.log('Found product:', product); 20 | 21 | if (!user) { 22 | return { 23 | success: false, 24 | status: supportedStatuses[1] 25 | }; 26 | } 27 | 28 | if (!product) { 29 | return { 30 | success: false, 31 | status: supportedStatuses[0] 32 | }; 33 | } 34 | 35 | const transactionId = processPayment(userId, productId, product.price); 36 | 37 | 38 | // Create the order 39 | const order = { 40 | id: `order-${Date.now()}`, 41 | userId, 42 | productId, 43 | productName: product.name, 44 | price: product.price, 45 | transactionId: transactionId, 46 | status: supportedStatuses[3], 47 | createdAt: new Date().toISOString(), 48 | deliveryDate: deliveryDate 49 | }; 50 | 51 | // Add to orders 52 | db.data.orders.push(order); 53 | await db.write() 54 | 55 | // Send notification 56 | sendNotification(userId, `Your order for ${product.name} has been confirmed! Estimated delivery: ${deliveryDate}`); 57 | 58 | return { 59 | success: true, 60 | order 61 | }; 62 | } 63 | 64 | // Payment processing function 65 | function processPayment() { 66 | throw new Error('PaymentGatewayDown'); 67 | } 68 | 69 | // Simulated notification 70 | function sendNotification(userId, message) { 71 | // In a real application, this would send an email or push notification 72 | console.log(`Notification to user ${userId}: ${message}`); 73 | return { 74 | success: true, 75 | timestamp: new Date().toISOString() 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /stage2-consistency/stage_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Starting reset process..." 7 | 8 | # Define the base directory as the current directory 9 | BASE_DIR="$(pwd)" 10 | echo "Base directory: $BASE_DIR" 11 | 12 | # Step 1: Reset orra configuration 13 | echo "Resetting orra configuration..." 14 | orra config reset 15 | 16 | # Step 2: Remove dbstore 17 | echo "Removing dbstore..." 18 | rm -rf "$HOME/.orra/dbstore" 19 | 20 | # Step 3: Remove .orra-data directories from top-level subdirectories 21 | echo "Removing .orra-data directories..." 22 | for dir in "$BASE_DIR"/*; do 23 | if [ -d "$dir" ]; then 24 | ORRA_DATA_DIR="$dir/.orra-data" 25 | if [ -d "$ORRA_DATA_DIR" ]; then 26 | echo "Removing $ORRA_DATA_DIR" 27 | rm -rf "$ORRA_DATA_DIR" 28 | fi 29 | fi 30 | done 31 | 32 | # Step 4: Clear ORRA_API_KEY in .env files 33 | echo "Clearing ORRA_API_KEY in .env files..." 34 | for dir in "$BASE_DIR"/*; do 35 | if [ -d "$dir" ]; then 36 | ENV_FILE="$dir/.env" 37 | if [ -f "$ENV_FILE" ]; then 38 | echo "Updating $ENV_FILE" 39 | # Replace the ORRA_API_KEY line with an empty value 40 | sed -i '' 's/^ORRA_API_KEY=.*$/ORRA_API_KEY=/' "$ENV_FILE" 41 | fi 42 | fi 43 | done 44 | 45 | # Step 5: Remove data.json file if it exists 46 | DATA_JSON="$BASE_DIR/data.json" 47 | if [ -f "$DATA_JSON" ]; then 48 | echo "Removing $DATA_JSON" 49 | rm -f "$DATA_JSON" 50 | fi 51 | 52 | echo "Reset completed successfully!" 53 | -------------------------------------------------------------------------------- /stage2-consistency/stage_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Starting setup process..." 7 | 8 | # Define the base directory as the current directory 9 | BASE_DIR="$(pwd)" 10 | echo "Base directory: $BASE_DIR" 11 | 12 | # Step 1: Add a project to the orra plan engine 13 | echo "Adding project to orra plan engine..." 14 | orra projects add assistant 15 | 16 | # Step 2: Add a webhook to the project 17 | echo "Adding webhook to the project..." 18 | orra webhooks add http://localhost:3000/webhook 19 | 20 | # Step 3: Generate a new API key for the project 21 | echo "Generating new API key..." 22 | API_KEY_OUTPUT=$(orra api-keys gen assist-key) 23 | echo "$API_KEY_OUTPUT" 24 | 25 | # Step 4: Extract the API key from the output 26 | # Extract the API key from the output, ensuring whitespace is trimmed 27 | ORRA_API_KEY=$(echo "$API_KEY_OUTPUT" | grep "KEY:" | sed 's/^[[:space:]]*KEY:[[:space:]]*//' | tr -d '[:space:]') 28 | 29 | if [ -z "$ORRA_API_KEY" ]; then 30 | echo "Error: Could not extract API key from output" 31 | exit 1 32 | fi 33 | 34 | echo "Extracted API key: $ORRA_API_KEY" 35 | 36 | # Step 5: Check for .env-example files and copy to .env if needed 37 | echo "Checking for .env-example files..." 38 | for dir in "$BASE_DIR"/*; do 39 | if [ -d "$dir" ]; then 40 | ENV_FILE="$dir/.env" 41 | ENV_EXAMPLE_FILE="$dir/.env-example" 42 | 43 | # If .env doesn't exist but .env-example does, copy it 44 | if [ ! -f "$ENV_FILE" ] && [ -f "$ENV_EXAMPLE_FILE" ]; then 45 | echo "Creating $ENV_FILE from $ENV_EXAMPLE_FILE" 46 | cp "$ENV_EXAMPLE_FILE" "$ENV_FILE" 47 | fi 48 | fi 49 | done 50 | 51 | # Step 6: Add the ORRA_API_KEY to all .env files 52 | echo "Adding ORRA_API_KEY to .env files..." 53 | for dir in "$BASE_DIR"/*; do 54 | if [ -d "$dir" ]; then 55 | ENV_FILE="$dir/.env" 56 | if [ -f "$ENV_FILE" ]; then 57 | echo "Updating $ENV_FILE" 58 | # Check if ORRA_API_KEY line exists in the file 59 | if grep -q "^ORRA_API_KEY=" "$ENV_FILE"; then 60 | # Replace existing ORRA_API_KEY line 61 | sed -i '' "s|^ORRA_API_KEY=.*$|ORRA_API_KEY=$ORRA_API_KEY|" "$ENV_FILE" 62 | else 63 | # Add ORRA_API_KEY line if it doesn't exist 64 | echo "ORRA_API_KEY=$ORRA_API_KEY" >> "$ENV_FILE" 65 | fi 66 | else 67 | # Create new .env file if it doesn't exist 68 | echo "Creating new $ENV_FILE" 69 | echo "ORRA_API_KEY=$ORRA_API_KEY" > "$ENV_FILE" 70 | fi 71 | fi 72 | done 73 | 74 | # Step 7: Create data.json file by copying from example 75 | DATA_JSON_EXAMPLE="$BASE_DIR/data.json-example" 76 | DATA_JSON="$BASE_DIR/data.json" 77 | 78 | if [ -f "$DATA_JSON_EXAMPLE" ]; then 79 | echo "Creating data.json from example..." 80 | cp "$DATA_JSON_EXAMPLE" "$DATA_JSON" 81 | echo "data.json created successfully" 82 | else 83 | echo "Warning: data.json-example not found, skipping data.json creation" 84 | fi 85 | 86 | echo "Setup completed successfully!" 87 | -------------------------------------------------------------------------------- /stage3-grounding/README.md: -------------------------------------------------------------------------------- 1 | # Stage 3: Reliable Plans with orra 2 | 3 | In this stage, we address the challenge of LLM plan hallucinations by implementing domain grounding. 4 | 5 | ## The Problem: Potential Plan Engine Hallucinations 6 | 7 | Even with reliable compensation mechanisms, LLM-powered plan engines can still: 8 | 9 | 1. Hallucinate execution plans with non-existent services or capabilities 10 | 2. Generate plans that don't match the user's actual intent 11 | 3. Create invalid action sequences that can't be executed properly 12 | 4. Make incorrect assumptions about service capabilities 13 | 5. Design plans that require impossible state transitions 14 | 15 | ## What Changed 16 | 17 | We've implemented domain grounding for our marketplace assistant: 18 | 19 | 1. **Use Case Definitions**: Clearly defined use cases of actions and the expected capabilities they require 20 | 2. **Semantic Verification**: The planning system ensures all actions align with real capabilities 21 | 3. **PDDL Validation**: Execution plans are formally validated before execution 22 | 23 | ![](images/ReliablePlans.png) 24 | 25 | ## Domain Grounding Definition 26 | 27 | See [assist-grounding.yaml](assist-grounding.yaml). 28 | 29 | ```yaml 30 | # Domain grounding definition 31 | name: "marketplace-assistant" 32 | domain: "ecommerce" 33 | version: "1.0" 34 | 35 | use-cases: 36 | - action: "Recommend products" 37 | params: 38 | query: "Second hand laptop under $1000 for coding" 39 | capabilities: 40 | - "Product recommendation based on needs" 41 | - "Product finder based on user preferences" 42 | intent: "Customer wants to find products matching specific criteria" 43 | 44 | - action: "Purchase a product" 45 | params: 46 | productId: "laptop-1" 47 | userId: "user-1" 48 | capabilities: 49 | - "Inventory availability check" 50 | - "Inventory reserve product" 51 | - "Estimate delivery date" 52 | - "Purchase processing" 53 | intent: "Customer wants to purchase a specific product" 54 | 55 | - action: "Can I get it delivered soon?" 56 | params: 57 | productId: "laptop-1" 58 | userId: "user-1" 59 | capabilities: 60 | - "Inventory availability check" 61 | - "Delivery estimation" 62 | intent: "Customer wants to know potential dates for delivery" 63 | 64 | constraints: 65 | - "Verify product availability before processing purchase" 66 | - "Provide delivery estimates based on inventory location" 67 | ``` 68 | 69 | ## Run this stage 70 | 71 | ### Prerequisites 72 | - Node.js (v18+) 73 | - orra [Plan Engine running and CLI installed](https://github.com/orra-dev/orra/tree/main#installation) 74 | - [OpenAI API key](https://platform.openai.com/docs/api-reference/authentication) 75 | 76 | ### Setup & Run 77 | 78 | 1. **Initialize orra configuration** 79 | ```bash 80 | ./stage_setup.sh # Sets up project, webhooks, and API keys 81 | 82 | 2. **Configure OpenAI API key in each component's `.env` file** 83 | ```shell 84 | OPENAI_API_KEY=your_openai_api_key_here 85 | ``` 86 | 3. **Start each component (in separate terminals)** 87 | ```shell 88 | cd [component-directory] # Run for each component 89 | npm install 90 | npm start 91 | ``` 92 | 4. **Start webhook simulator (in a separate terminal)** 93 | ```bash 94 | orra verify webhooks start http://localhost:3000/webhook 95 | ``` 96 | ### Using the Application 97 | 98 | In this case we just want to demonstrate how grounding works. 99 | 100 | Again, we'll be using the [CLI](https://github.com/orra-dev/orra/blob/main/docs/cli.md)'s `orra verify` command to understand how the Plan Engine is coordinating our components to complete system actions. 101 | 102 | The assumption here is that there's a chat UI interface that forwards requests to the Plan Engine. 103 | 104 | 1. **Apply the grounding** 105 | 106 | ```bash 107 | orra grounding apply -f stage3-grounding/assist-grounding.yaml 108 | ``` 109 | 110 | 2. **Ensure the domain is locked** 111 | 112 | ```bash 113 | orra verify run 'Refund product' \ 114 | -d 'productId:laptop-1' \ 115 | -d 'userId:user-1' 116 | ``` 117 | 118 | This should be rejected. 119 | 120 | 3. **Purchase a recommended product - with grounding** 121 | 122 | ```bash 123 | orra verify run 'Purchase product' \ 124 | -d 'productId:laptop-1' \ 125 | -d 'userId:user-1' 126 | ``` 127 | 128 | Now extra checks are enforced to stop Plan Hallucinations and impossible state transitions are guarded against. 129 | 130 | Invalid plans will NEVER run. 131 | 132 | ### Reset Environment 133 | 134 | 1. **Clear Plan Engine configurations and reset data** 135 | ```bash 136 | ./stage_reset.sh # Clears configurations and data 137 | ``` 138 | 139 | 2. **Stop all the running components and kill all the terminal window** 140 | 141 | 3. **Shutdown the Plan Engine** 142 | 143 | ## Benefits 144 | 145 | 1. **Reduced Plan Hallucinations**: The plan engine cannot generate invalid execution plans 146 | 2. **Stronger Reliability**: All plans are grounded in real service capabilities 147 | 3. **Consistent Execution**: Plans align with well-defined use cases 148 | 4. **Clear Intent Mapping**: User requests map to verified execution patterns 149 | 5. **Formal Validation**: PDDL validation ensures logical correctness of plans 150 | 151 | ## How orra Helps 152 | 153 | - **Plan Engine**: Validates all execution plans against domain grounding 154 | - **Embedding-based Matching**: Intelligently maps user intents to grounded use cases 155 | - **PDDL Validation**: Ensures plans satisfy logical constraints and capabilities 156 | 157 | ## Plan Validation Process 158 | 159 | 1. **User Request Processing**: 160 | - The system receives a user request 161 | - The request is analyzed to determine the user's intent 162 | 163 | 2. **Plan Generation**: 164 | - The Plan Engine generates an initial execution plan 165 | - The plan is based on the available services and capabilities 166 | 167 | 3. **Grounding Validation**: 168 | - The plan is checked against domain grounding examples 169 | - Both semantic matching and formal PDDL validation are performed 170 | - The system verifies capability requirements and execution constraints 171 | 172 | 4. **Execution or Rejection**: 173 | - Valid plans are executed 174 | - Invalid plans are rejected 175 | 176 | ## Hallucination Prevention Example 177 | 178 | Consider this scenario: 179 | 180 | 1. **User Request**: "I want to cancel my order and get a refund" 181 | 2. **Without Grounding**: The plan engine might hallucinate a non-existent "refund-service" in the plan 182 | 3. **With Grounding**: The plan is validated against known capabilities and rejected, with the system explaining, "I'm sorry, but our system doesn't currently support order cancellations and refunds" 183 | 184 | ## Done 185 | 186 | Our application is now more reliable with grounded planning! 187 | -------------------------------------------------------------------------------- /stage3-grounding/assist-grounding.yaml: -------------------------------------------------------------------------------- 1 | name: "marketplace-assistant" 2 | domain: "ecommerce" 3 | version: "1.0" 4 | 5 | use-cases: 6 | - action: "Recommend products" 7 | params: 8 | query: "Second hand laptop under $1000 for coding" 9 | capabilities: 10 | - "Product recommendation based on needs" 11 | - "Product finder based on user preferences" 12 | intent: "Customer wants to find products matching specific criteria" 13 | 14 | - action: "Purchase a product" 15 | params: 16 | productId: "laptop-1" 17 | userId: "user-1" 18 | capabilities: 19 | - "Inventory availability check" 20 | - "Inventory reserve product" 21 | - "Estimate delivery date" 22 | - "Purchase processing" 23 | intent: "Customer wants to purchase a specific product" 24 | 25 | - action: "Can I get it delivered soon?" 26 | params: 27 | productId: "laptop-1" 28 | userId: "user-1" 29 | capabilities: 30 | - "Inventory availability check" 31 | - "Delivery estimation" 32 | intent: "Customer wants to know potential dates for delivery" 33 | 34 | constraints: 35 | - "Verify product availability before processing purchase" 36 | - "Provide delivery estimates based on inventory location" 37 | -------------------------------------------------------------------------------- /stage3-grounding/data.json-example: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "laptop-1", 5 | "name": "Dell XPS 13 (2022)", 6 | "description": "Used Dell XPS 13 laptop with 16GB RAM, 512GB SSD, Intel i7 processor", 7 | "price": 750, 8 | "condition": "excellent", 9 | "category": "laptops", 10 | "tags": ["programming", "college", "portable"], 11 | "inStock": 1, 12 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 13 | }, 14 | { 15 | "id": "laptop-2", 16 | "name": "MacBook Air M1", 17 | "description": "Used MacBook Air with M1 chip, 8GB RAM, 256GB SSD", 18 | "price": 650, 19 | "condition": "good", 20 | "category": "laptops", 21 | "tags": ["college", "portable", "mac"], 22 | "inStock": 2, 23 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 24 | }, 25 | { 26 | "id": "laptop-3", 27 | "name": "Lenovo ThinkPad X1 Carbon", 28 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 29 | "price": 820, 30 | "condition": "excellent", 31 | "category": "laptops", 32 | "tags": ["business", "programming", "durable"], 33 | "inStock": 0, 34 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 35 | }, 36 | { 37 | "id": "laptop-4", 38 | "name": "Lenovo ThinkPad X1 Carbon", 39 | "description": "Used ThinkPad X1 Carbon with 16GB RAM, 1TB SSD, Intel i7 processor", 40 | "price": 500, 41 | "condition": "fair", 42 | "category": "laptops", 43 | "tags": ["business", "programming", "durable"], 44 | "inStock": 4, 45 | "warehouseAddress": "Unit 1 Cairnrobin Way, Portlethen, Aberdeen AB12 4NJ" 46 | } 47 | ], 48 | "orders": [], 49 | "users": [ 50 | { 51 | "id": "user-1", 52 | "name": "John Doe", 53 | "email": "john@example.com", 54 | "address": "1a Goldsmiths Row, London E2 8QA" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /stage3-grounding/delivery-agent/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | DELIVERY_AGENT_PORT=3004 10 | -------------------------------------------------------------------------------- /stage3-grounding/delivery-agent/agent.js: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { JSONFilePreset } from "lowdb/node"; 3 | import * as path from "node:path"; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY 10 | }); 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [], users: [] }); 13 | 14 | export const supportedStatuses = [ 15 | 'unknown-product', 16 | 'unknown-user', 17 | 'delivery-estimated', 18 | ] 19 | 20 | // Simulated traffic and logistics data 21 | const trafficConditions = { 22 | "route_segments": [ 23 | { 24 | "segment_id": "A90-ABD-DND", 25 | "name": "A90 Aberdeen to Dundee", 26 | "length_km": 108, 27 | "current_average_speed_kph": 95, 28 | "normal_average_speed_kph": 100, 29 | "congestion_level": "light", 30 | "incidents": [] 31 | }, 32 | { 33 | "segment_id": "M90-PER-EDI", 34 | "name": "M90 Perth to Edinburgh", 35 | "length_km": 45, 36 | "current_average_speed_kph": 110, 37 | "normal_average_speed_kph": 110, 38 | "congestion_level": "none", 39 | "incidents": [ 40 | { 41 | "type": "roadworks", 42 | "location": "Junction 3", 43 | "description": "Lane closure for resurfacing", 44 | "delay_minutes": 10 45 | } 46 | ] 47 | }, 48 | { 49 | "segment_id": "A1-NCL-YRK", 50 | "name": "A1 Newcastle to York", 51 | "length_km": 140, 52 | "current_average_speed_kph": 100, 53 | "normal_average_speed_kph": 110, 54 | "congestion_level": "moderate", 55 | "incidents": [ 56 | { 57 | "type": "accident", 58 | "location": "Near Darlington", 59 | "description": "Multi-vehicle collision", 60 | "delay_minutes": 25 61 | } 62 | ] 63 | } 64 | ], 65 | "weather_conditions": [ 66 | { 67 | "location": "Northeast", 68 | "condition": "light_rain", 69 | "temperature_celsius": 12 70 | }, 71 | { 72 | "location": "Midwest", 73 | "condition": "cloudy", 74 | "temperature_celsius": 14 75 | }, 76 | { 77 | "location": "West Coast", 78 | "condition": "clear", 79 | "temperature_celsius": 20 80 | }, 81 | { 82 | "location": "Southeast", 83 | "condition": "partly_cloudy", 84 | "temperature_celsius": 22 85 | } 86 | ], 87 | "vehicles": [ 88 | { 89 | "type": "van", 90 | "capacity_cubic_meters": 15, 91 | "max_range_km": 500, 92 | "average_speed_kph": 90, 93 | "availability": "high" 94 | }, 95 | { 96 | "type": "truck", 97 | "capacity_cubic_meters": 40, 98 | "max_range_km": 800, 99 | "average_speed_kph": 80, 100 | "availability": "medium" 101 | } 102 | ] 103 | }; 104 | 105 | // Function to generate delivery estimates using LLM 106 | export async function generateDeliveryEstimates(userId, productId, inStock=0) { 107 | if (inStock && inStock < 1) { 108 | throw new Error('CannotEstimateDeliveryForOutOfStockProduct'); 109 | } 110 | 111 | const user = db.data. users.find(u => u.id === userId); 112 | const product = db.data.products.find(p => p.id === productId); 113 | 114 | if (!user) { 115 | return { 116 | success: false, 117 | status: supportedStatuses[1] 118 | }; 119 | } 120 | 121 | if (!product) { 122 | return { 123 | success: false, 124 | status: supportedStatuses[0] 125 | }; 126 | } 127 | 128 | // Use LLM to generate intelligent delivery estimates 129 | const systemPrompt = `You are a delivery logistics expert with 20 years of experience. 130 | Your task is to provide realistic delivery estimates for products being shipped from a warehouse to a customer. 131 | Consider all relevant factors including traffic conditions, weather, distance, and product characteristics. 132 | Always provide both best-case and worst-case scenarios with confidence levels.`; 133 | 134 | const userPrompt = `Create delivery estimates for the following: 135 | 136 | WAREHOUSE ADDRESS: ${product.warehouseAddress} 137 | CUSTOMER ADDRESS: ${user.address} 138 | 139 | Current traffic and logistics data: 140 | ${JSON.stringify(trafficConditions, null, 2)} 141 | 142 | Your response should include: 143 | 1. A best-case delivery estimate (duration in hours and delivery date) 144 | 2. A worst-case delivery estimate (duration in hours and delivery date) 145 | 3. Confidence levels for each estimate (low/moderate/high) 146 | 4. A brief explanation of your reasoning 147 | 148 | Respond in JSON format with these components. 149 | 150 | USE THIS SCHEMA FOR THE FINAL ANSWER: 151 | { 152 | "bestCase": { 153 | "estimatedDurationHours": "expected duration as decimal value, e.g. 7.5", 154 | "estimatedDeliveryDate": "estimated delivery date in the future as a timestamp, e.g. 2024-10-02T21:15:00Z", 155 | "confidenceLevel": "how confident you are. one of: low, moderate or high" 156 | }, 157 | "worstCase": { 158 | "estimatedDurationHours": "expected duration as decimal value, e.g. 7.5", 159 | "estimatedDeliveryDate": "estimated delivery date in the future as a timestamp, e.g. 2024-10-02T21:15:00Z", 160 | "confidenceLevel": "how confident you are. one of: low, moderate or high" 161 | }, 162 | "explanation": "Delivery estimate based on current traffic and weather conditions. Factors include road conditions, distance, and typical shipping times." 163 | }`; 164 | 165 | try { 166 | const response = await openai.chat.completions.create({ 167 | model: "gpt-4o", 168 | messages: [ 169 | { role: "system", content: systemPrompt }, 170 | { role: "user", content: userPrompt } 171 | ], 172 | response_format: { type: "json_object" } 173 | }); 174 | 175 | const content = JSON.parse(response.choices[0].message.content); 176 | console.log("Generated delivery estimates:", content); 177 | 178 | const fallbackDeliveryEstimatedHours = 72 179 | const fallbackDeliveryDate = new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString().split('T')[0] 180 | const fallbackExplanation = "Delivery estimate based on current traffic and weather conditions." 181 | 182 | return { 183 | status: supportedStatuses[2], 184 | success: true, 185 | estimatedDays: hoursToDays(content?.worstCase?.estimatedDays || fallbackDeliveryEstimatedHours), 186 | deliveryDate: content?.worstCase?.estimatedDeliveryDate?.split('T')[0] || fallbackDeliveryDate, 187 | explanation: content?.explanation || fallbackExplanation, 188 | }; 189 | } catch (error) { 190 | console.error("Error generating delivery estimates:", error); 191 | throw error; 192 | } 193 | } 194 | 195 | function hoursToDays(hours) { 196 | if (typeof hours !== "number" || isNaN(hours)) { 197 | throw new Error("Please provide a valid number of hours."); 198 | } 199 | return hours / 24; 200 | } 201 | -------------------------------------------------------------------------------- /stage3-grounding/delivery-agent/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initAgent } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { generateDeliveryEstimates } from "./agent.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.DELIVERY_AGENT_PORT || 3004; 11 | 12 | // Initialize the orra agent 13 | const deliveryAgent = initAgent({ 14 | name: 'delivery-agent', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/delivery-agent-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | 29 | async function startService() { 30 | try { 31 | // Register agent with orra 32 | await deliveryAgent.register({ 33 | description: 'An agent that provides intelligent delivery estimates based on product, location, and current conditions.', 34 | schema 35 | }); 36 | 37 | // Start handling tasks 38 | deliveryAgent.start(async (task) => { 39 | console.log('Processing delivery estimation task:', task.id); 40 | console.log('Input:', task.input); 41 | 42 | const { userId, productId, inStock } = task.input; 43 | 44 | // Use LLM to generate delivery estimates 45 | const result = await generateDeliveryEstimates(userId, productId, inStock); 46 | // FEATURE COMING SOON: 47 | // if (result.status !== 'success') { 48 | // return task.abort(result); 49 | // } 50 | return result; 51 | }); 52 | 53 | console.log('Delivery Agent started successfully'); 54 | } catch (error) { 55 | console.error('Failed to start Delivery Agent:', error); 56 | process.exit(1); 57 | } 58 | } 59 | 60 | // Start the Express server and the agent 61 | app.listen(port, () => { 62 | console.log(`Delivery Agent listening on port ${port}`); 63 | startService().catch(console.error); 64 | }); 65 | -------------------------------------------------------------------------------- /stage3-grounding/delivery-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delivery-agent", 3 | "version": "1.0.0", 4 | "description": "LLM-powered delivery estimation agent for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "openai": "^4.11.0", 17 | "lowdb": "^7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stage3-grounding/delivery-agent/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "userId": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | }, 11 | "inStock": { 12 | "type": "number" 13 | } 14 | }, 15 | "required": ["userId", "productId"] 16 | }, 17 | "output": { 18 | "type": "object", 19 | "properties": { 20 | "status": { 21 | "type": "string" 22 | }, 23 | "success": { 24 | "type": "boolean" 25 | }, 26 | "estimatedDays": { 27 | "type": "number" 28 | }, 29 | "deliveryDate": { 30 | "type": "string" 31 | }, 32 | "explanation": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["estimatedDays", "deliveryDate", "explanation"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stage3-grounding/images/ReliablePlans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orra-dev/agent-fragile-to-prod-guide/3f1875ef08881c71cb09e49d4034952781e9440b/stage3-grounding/images/ReliablePlans.png -------------------------------------------------------------------------------- /stage3-grounding/inventory-tool-service/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | INVENTORY_SERVICE_PORT=3002 10 | -------------------------------------------------------------------------------- /stage3-grounding/inventory-tool-service/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initService } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { execInventory } from "./svc.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.INVENTORY_SERVICE_PORT || 3002; 11 | 12 | // Initialize the orra service 13 | const inventoryService = initService({ 14 | name: 'inventory-service', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/inventory-service-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | async function startService() { 29 | try { 30 | // Register service with orra 31 | await inventoryService.register({ 32 | description: `A service that manages product inventory, checks availability and reserves products. 33 | Supported actions: checkAvailability (gets product status), reserveProduct (reduces inventory), and releaseProduct (returns inventory).`, 34 | schema, 35 | revertible: true 36 | }); 37 | 38 | inventoryService.onRevert(async (task, result) => { 39 | // Only process compensations for reserveProduct actions 40 | if (task.input.action === 'reserveProduct' && result.success) { 41 | console.log('Reverting inventory product for task:', task.id); 42 | console.log('Reverting inventory product hold for product:', result.productId); 43 | 44 | // Compensation logic: release the product that was reserved 45 | const releaseResult = releaseProduct(result.productId, 1); 46 | console.log('Inventory compensation completed:', JSON.stringify(releaseResult)); 47 | } 48 | }); 49 | 50 | // Start handling tasks 51 | inventoryService.start(async (task) => { 52 | console.log('Processing inventory task:', task.id); 53 | console.log('Input:', task.input); 54 | 55 | const { action, productId } = task.input; 56 | const result = await execInventory(action, productId); 57 | 58 | // FEATURE COMING SOON: 59 | // if (result.status !== 'success') { 60 | // return task.abort(result); 61 | // } 62 | 63 | return result; 64 | }); 65 | 66 | console.log('Inventory Service started successfully'); 67 | } catch (error) { 68 | console.error('Failed to start Inventory Service:', error); 69 | process.exit(1); 70 | } 71 | } 72 | 73 | // Start the Express server and the service 74 | app.listen(port, () => { 75 | console.log(`Inventory Service listening on port ${port}`); 76 | startService().catch(console.error); 77 | }); 78 | -------------------------------------------------------------------------------- /stage3-grounding/inventory-tool-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory-tool-service", 3 | "version": "1.0.0", 4 | "description": "Inventory management previously a tool now a service for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "lowdb": "^7.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage3-grounding/inventory-tool-service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "action": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | } 11 | }, 12 | "required": ["action", "productId"] 13 | }, 14 | "output": { 15 | "type": "object", 16 | "properties": { 17 | "action": { 18 | "type": "string" 19 | }, 20 | "productId": { 21 | "type": "string" 22 | }, 23 | "status": { 24 | "type": "string" 25 | }, 26 | "success": { 27 | "type": "boolean" 28 | }, 29 | "inStock": { 30 | "type": "number" 31 | }, 32 | "message": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["action", "productId"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stage3-grounding/inventory-tool-service/svc.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | 4 | export const supportedStatuses = [ 5 | 'unknown-product', 6 | 'product-available', 7 | 'product-out-of-stock', 8 | 'product-reserved', 9 | 'product-released' 10 | ] 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [] }); 13 | 14 | export async function execInventory(action, productId) { 15 | console.log('executing inventory action: ', action, ' for product: ', productId); 16 | switch (action) { 17 | case 'checkAvailability': 18 | return checkAvailability(productId); 19 | case 'reserveProduct': 20 | return await reserveProduct(productId); 21 | case 'releaseProduct': 22 | return releaseProduct(productId); 23 | default: 24 | throw new Error(`Unknown action: ${action}`); 25 | } 26 | } 27 | 28 | // Service functions 29 | function checkAvailability(productId) { 30 | const product = db.data.products.find(p => p.id === productId); 31 | 32 | if (!product) { 33 | return { 34 | action: "checkAvailability", 35 | productId, 36 | status: supportedStatuses[0], 37 | success: false, 38 | inStock: 0, 39 | message: "Product not found" 40 | }; 41 | } 42 | 43 | return { 44 | action: "checkAvailability", 45 | productId, 46 | status: product.inStock > 0 ? supportedStatuses[1] : supportedStatuses[2], 47 | success: true, 48 | inStock: product.inStock, 49 | message: "Product in stock" 50 | }; 51 | } 52 | 53 | async function reserveProduct(productId, quantity = 1) { 54 | const product = db.data.products.find(p => p.id === productId); 55 | 56 | if (!product) { 57 | return { 58 | action: "reserveProduct", 59 | productId, 60 | status: supportedStatuses[0], 61 | success: false, 62 | inStock: 0, 63 | message: "Product not found" 64 | }; 65 | } 66 | 67 | if (product.inStock < quantity) { 68 | return { 69 | action: "reserveProduct", 70 | productId, 71 | status: supportedStatuses[2], 72 | success: false, 73 | inStock: product.inStock, 74 | message: `Insufficient stock. Requested: ${quantity}, Available: ${product.inStock}` 75 | }; 76 | } 77 | 78 | // Reserve the product 79 | product.inStock -= quantity; 80 | await db.write() 81 | 82 | return { 83 | action: "reserveProduct", 84 | productId, 85 | status: supportedStatuses[3], 86 | success: true, 87 | inStock: product.inStock, 88 | message: `Successfully reserved ${quantity} units of ${product.name}` 89 | }; 90 | } 91 | 92 | async function releaseProduct(productId, quantity = 1) { 93 | const product = db.data.products.find(p => p.id === productId); 94 | 95 | if (!product) { 96 | return { 97 | action: "releaseProduct", 98 | productId, 99 | status: supportedStatuses[0], 100 | success: false, 101 | inStock: 0, 102 | message: "Product not found" 103 | }; 104 | } 105 | 106 | // Release the reservation 107 | product.inStock += quantity; 108 | await db.write() 109 | 110 | console.log(`Released ${quantity} units of ${product.name}. New stock: ${product.inStock}`); 111 | 112 | return { 113 | action: "releaseProduct", 114 | productId, 115 | status: supportedStatuses[4], 116 | success: true, 117 | inStock: product.inStock, 118 | message: `Successfully released ${quantity} units of ${product.name}` 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /stage3-grounding/product-advisor-agent/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxx 7 | 8 | # Ports 9 | PRODUCT_ADVISOR_PORT=3001 10 | -------------------------------------------------------------------------------- /stage3-grounding/product-advisor-agent/agent.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | import OpenAI from "openai"; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: process.env.OPENAI_API_KEY 10 | }); 11 | 12 | const db = await JSONFilePreset(path.join("..", "data.json"), { products: [] }); 13 | 14 | const functionSpecs = [ 15 | { 16 | name: "searchProducts", 17 | description: "Search for products based on criteria", 18 | parameters: { 19 | type: "object", 20 | properties: { 21 | category: { 22 | type: "string", 23 | description: "Product category (e.g., 'laptops')" 24 | }, 25 | priceMax: { 26 | type: "number", 27 | description: "Maximum price" 28 | }, 29 | tags: { 30 | type: "array", 31 | items: { 32 | type: "string" 33 | }, 34 | description: "Tags to filter by (e.g., ['programming', 'college'])" 35 | }, 36 | condition: { 37 | type: "string", 38 | description: "Product condition ('excellent', 'good', 'fair')" 39 | } 40 | } 41 | } 42 | } 43 | ]; 44 | 45 | const availableFunctions = { 46 | searchProducts: (args) => { 47 | const { category, priceMax, tags, condition } = args; 48 | 49 | let filteredProducts = [...db.data.products]; 50 | 51 | if (category) { 52 | filteredProducts = filteredProducts.filter(p => p.category === category); 53 | } 54 | 55 | if (priceMax) { 56 | filteredProducts = filteredProducts.filter(p => p.price <= priceMax); 57 | } 58 | 59 | if (tags && tags.length > 0) { 60 | filteredProducts = filteredProducts.filter(p => 61 | tags.some(tag => p.tags.includes(tag)) 62 | ); 63 | } 64 | 65 | if (condition) { 66 | filteredProducts = filteredProducts.filter(p => p.condition === condition); 67 | } 68 | 69 | return filteredProducts; 70 | }, 71 | }; 72 | 73 | export async function recommendProduct(query) { 74 | const data = await runAdvisor(query); 75 | return extractAndParseJson(data.response); 76 | } 77 | 78 | async function runAdvisor(query, conversationHistory = []) { 79 | try { 80 | // Add the user input to the conversation history 81 | conversationHistory.push({ role: "user", content: query }); 82 | 83 | // Define the system message 84 | const systemMessage = { 85 | role: "system", 86 | content: `You are an AI marketplace assistant. Given a user query and product data, recommend the most suitable products. 87 | For each recommendation, provide reasoning based on the user's needs. 88 | 89 | Always be helpful, concise, and provide specific product recommendations that match user criteria. 90 | 91 | Focus ONLY on products that are in stock (inStock > 0). 92 | 93 | Generate recommendations in JSON format with reasoning for why each product matches the user's needs. 94 | 95 | USE THIS SCHEMA FOR THE FINAL ANSWER: 96 | { 97 | "recommendations": [ 98 | { 99 | "id": "the recommended product's Id", 100 | "name": "the recommended product's name", 101 | "description": "the recommended product's description", 102 | "reasoning": "the reason for recommending the product", 103 | } 104 | ] 105 | } 106 | 107 | If there are NO matching products that are in stock (inStock > 0) return an empty recommendations array.` 108 | }; 109 | 110 | // Create the messages array for the API call 111 | const messages = [systemMessage, ...conversationHistory]; 112 | 113 | // Step 1: Call OpenAI API with function definitions 114 | const response = await openai.chat.completions.create({ 115 | model: "gpt-4o", 116 | messages: messages, 117 | functions: functionSpecs, 118 | function_call: "auto", 119 | response_format: { type: "json_object" }, 120 | temperature: 0.7, 121 | }); 122 | 123 | const responseMessage = response.choices[0].message; 124 | 125 | // Step 2: Check if the model wants to call a function 126 | if (responseMessage.function_call) { 127 | const functionName = responseMessage.function_call.name; 128 | const functionArgs = JSON.parse(responseMessage.function_call.arguments); 129 | 130 | console.log(`\nCalling function: ${functionName} with args:`, functionArgs); 131 | 132 | // Call the function 133 | const functionResponse = availableFunctions[functionName](functionArgs); 134 | 135 | // Step 3: Append function response to messages 136 | conversationHistory.push(responseMessage); // Add assistant's function call to history 137 | 138 | // Add the function response to chat history 139 | conversationHistory.push({ 140 | role: "function", 141 | name: functionName, 142 | content: JSON.stringify(functionResponse) 143 | }); 144 | 145 | // Step 4: Get a new response from the model with the function response 146 | const secondResponse = await openai.chat.completions.create({ 147 | model: "gpt-4o", 148 | messages: [...messages, responseMessage, { 149 | role: "function", 150 | name: functionName, 151 | content: JSON.stringify(functionResponse) 152 | }], 153 | functions: functionSpecs, 154 | function_call: "auto", 155 | temperature: 0.7, 156 | }); 157 | 158 | const secondResponseMessage = secondResponse.choices[0].message; 159 | 160 | // Handle nested function calls if needed 161 | if (secondResponseMessage.function_call) { 162 | const secondFunctionName = secondResponseMessage.function_call.name; 163 | const secondFunctionArgs = JSON.parse(secondResponseMessage.function_call.arguments); 164 | 165 | console.log(`\nCalling second function: ${secondFunctionName} with args:`, secondFunctionArgs); 166 | 167 | const secondFunctionResponse = availableFunctions[secondFunctionName](secondFunctionArgs); 168 | 169 | conversationHistory.push(secondResponseMessage); 170 | 171 | conversationHistory.push({ 172 | role: "function", 173 | name: secondFunctionName, 174 | content: JSON.stringify(secondFunctionResponse) 175 | }); 176 | 177 | // Get final response from the model 178 | const finalResponse = await openai.chat.completions.create({ 179 | model: "gpt-4o", 180 | messages: [...messages, responseMessage, { 181 | role: "function", 182 | name: functionName, 183 | content: JSON.stringify(functionResponse) 184 | }, secondResponseMessage, { 185 | role: "function", 186 | name: secondFunctionName, 187 | content: JSON.stringify(secondFunctionResponse) 188 | }], 189 | temperature: 0.7, 190 | }); 191 | 192 | const finalResponseMessage = finalResponse.choices[0].message; 193 | conversationHistory.push(finalResponseMessage); 194 | 195 | return { 196 | response: finalResponseMessage.content, 197 | conversationHistory 198 | }; 199 | } 200 | 201 | conversationHistory.push(secondResponseMessage); 202 | 203 | return { 204 | response: secondResponseMessage.content, 205 | conversationHistory 206 | }; 207 | } 208 | 209 | // If no function call, just return the response 210 | conversationHistory.push(responseMessage); 211 | 212 | return { 213 | response: responseMessage.content, 214 | conversationHistory 215 | }; 216 | } catch (error) { 217 | console.error("Error in marketplace assistant:", error); 218 | throw error; 219 | } 220 | } 221 | 222 | function extractAndParseJson(input) { 223 | // Extract the JSON content 224 | const jsonMatch = input.match(/```json\n([\s\S]*?)\n```/); 225 | 226 | if (!jsonMatch) { 227 | throw new Error("No JSON content found between ```json``` tags"); 228 | } 229 | 230 | const jsonString = jsonMatch[1]; 231 | 232 | // Parse the JSON 233 | try { 234 | return JSON.parse(jsonString); 235 | } catch (error) { 236 | throw new Error(`Failed to parse JSON: ${error.message}`); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /stage3-grounding/product-advisor-agent/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initAgent } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { recommendProduct } from "./agent.js"; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.PRODUCT_ADVISOR_PORT || 3001; 11 | 12 | // Initialize the orra agent 13 | const productAdvisor = initAgent({ 14 | name: 'product-advisor', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/product-advisor-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | // Function to generate product recommendations using LLM 29 | 30 | 31 | async function startService() { 32 | try { 33 | // Register agent with orra 34 | await productAdvisor.register({ 35 | description: 'An agent that helps users find products based on their needs and preferences.', 36 | schema 37 | }); 38 | 39 | // Start handling tasks 40 | productAdvisor.start(async (task) => { 41 | console.log('Processing product advisory task:', task.id); 42 | console.log('Input:', task.input); 43 | 44 | const { query } = task.input; 45 | 46 | // Use LLM to generate recommendations 47 | return await recommendProduct(query); 48 | }); 49 | 50 | console.log('Product Advisor agent started successfully'); 51 | } catch (error) { 52 | console.error('Failed to start Product Advisor agent:', error); 53 | process.exit(1); 54 | } 55 | } 56 | 57 | // Start the Express server and the agent 58 | app.listen(port, () => { 59 | console.log(`Product Advisor listening on port ${port}`); 60 | startService().catch(console.error); 61 | }); 62 | -------------------------------------------------------------------------------- /stage3-grounding/product-advisor-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product-advisor", 3 | "version": "1.0.0", 4 | "description": "Product advisor agent for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "openai": "^4.11.0", 17 | "lowdb": "^7.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stage3-grounding/product-advisor-agent/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "query": { 6 | "type": "string" 7 | } 8 | }, 9 | "required": ["query"] 10 | }, 11 | "output": { 12 | "type": "object", 13 | "properties": { 14 | "recommendations": { 15 | "type": "array", 16 | "items": { 17 | "type": "object", 18 | "properties": { 19 | "id": { 20 | "type": "string" 21 | }, 22 | "name": { 23 | "type": "string" 24 | }, 25 | "description": { 26 | "type": "string" 27 | }, 28 | "reasoning": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "required": ["recommendations"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stage3-grounding/purchasing-tool-service/.env-example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=xxxx 3 | 4 | # orra Configuration 5 | ORRA_URL=http://localhost:8005 6 | ORRA_API_KEY=xxxx 7 | 8 | # Ports 9 | PAYMENT_SERVICE_PORT=3003 10 | -------------------------------------------------------------------------------- /stage3-grounding/purchasing-tool-service/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { initService } from '@orra.dev/sdk'; 3 | import dotenv from 'dotenv'; 4 | import schema from './schema.json' assert { type: 'json' }; 5 | import { purchaseProduct } from './svc.js'; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | const port = process.env.PAYMENT_SERVICE_PORT || 3003; 11 | 12 | // Initialize the orra service 13 | const purchaseService = initService({ 14 | name: 'purchase-service', 15 | orraUrl: process.env.ORRA_URL, 16 | orraKey: process.env.ORRA_API_KEY, 17 | persistenceOpts: { 18 | method: 'file', 19 | filePath: './.orra-data/purchase-service-key.json' 20 | } 21 | }); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'healthy' }); 26 | }); 27 | 28 | async function startService() { 29 | try { 30 | // Register service with orra 31 | await purchaseService.register({ 32 | description: 'A service that makes marketplace product purchases on behalf of a user. It creates purchase orders that include shipping details, makes payments against external payment gateways and notifies users.', 33 | schema 34 | }); 35 | 36 | // Start handling tasks 37 | purchaseService.start(async (task) => { 38 | console.log('Processing purchase task:', task.id); 39 | console.log('Input:', task.input); 40 | 41 | const { userId, productId, deliveryDate } = task.input; 42 | 43 | // Process the purchase order 44 | const result = purchaseProduct(userId, productId, deliveryDate); 45 | // FEATURE COMING SOON: 46 | // if (result.status !== 'success') { 47 | // return task.abort(result); 48 | // } 49 | return result; 50 | }); 51 | 52 | console.log('Payment Service started successfully'); 53 | } catch (error) { 54 | console.error('Failed to start Payment Service:', error); 55 | process.exit(1); 56 | } 57 | } 58 | 59 | // Start the Express server and the service 60 | app.listen(port, () => { 61 | console.log(`Payment Service listening on port ${port}`); 62 | startService().catch(console.error); 63 | }); 64 | -------------------------------------------------------------------------------- /stage3-grounding/purchasing-tool-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purchase-tool-service", 3 | "version": "1.0.0", 4 | "description": "Purchase processing previously a tool now a service for the marketplace assistant", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Team orra", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@orra.dev/sdk": "^0.2.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "lowdb": "^7.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage3-grounding/purchasing-tool-service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "object", 4 | "properties": { 5 | "userId": { 6 | "type": "string" 7 | }, 8 | "productId": { 9 | "type": "string" 10 | }, 11 | "deliveryDate": { 12 | "type": "string" 13 | } 14 | }, 15 | "required": [ 16 | "userId", 17 | "productId", 18 | "deliveryDate" 19 | ] 20 | }, 21 | "output": { 22 | "type": "object", 23 | "properties": { 24 | "order": { 25 | "type": "object", 26 | "properties": { 27 | "id": { 28 | "type": "string" 29 | }, 30 | "userId": { 31 | "type": "string" 32 | }, 33 | "productId": { 34 | "type": "string" 35 | }, 36 | "productName": { 37 | "type": "string" 38 | }, 39 | "price": { 40 | "type": "number" 41 | }, 42 | "transactionId": { 43 | "type": "string" 44 | }, 45 | "status": { 46 | "type": "string" 47 | }, 48 | "createdAt": { 49 | "type": "string" 50 | }, 51 | "deliveryDate": { 52 | "type": "string" 53 | } 54 | } 55 | }, 56 | "success": { 57 | "type": "boolean" 58 | } 59 | }, 60 | "required": [ 61 | "order", 62 | "success" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /stage3-grounding/purchasing-tool-service/svc.js: -------------------------------------------------------------------------------- 1 | import { JSONFilePreset } from "lowdb/node"; 2 | import * as path from "node:path"; 3 | 4 | const db = await JSONFilePreset(path.join("..", "data.json"), { users: [], products: [] }); 5 | 6 | export const supportedStatuses = [ 7 | 'unknown-product', 8 | 'unknown-user', 9 | 'payment-failed', 10 | 'order-processed' 11 | ]; 12 | 13 | export async function purchaseProduct(userId, productId, deliveryDate) { 14 | // Get the user and product 15 | const user = db.data.users.find(u => u.id === userId); 16 | const product = db.data.products.find(p => p.id === productId); 17 | 18 | console.log('Found user:', user); 19 | console.log('Found product:', product); 20 | 21 | if (!user) { 22 | return { 23 | success: false, 24 | status: supportedStatuses[1] 25 | }; 26 | } 27 | 28 | if (!product) { 29 | return { 30 | success: false, 31 | status: supportedStatuses[0] 32 | }; 33 | } 34 | 35 | const transactionId = processPayment(userId, productId, product.price); 36 | 37 | 38 | // Create the order 39 | const order = { 40 | id: `order-${Date.now()}`, 41 | userId, 42 | productId, 43 | productName: product.name, 44 | price: product.price, 45 | transactionId: transactionId, 46 | status: supportedStatuses[3], 47 | createdAt: new Date().toISOString(), 48 | deliveryDate: deliveryDate 49 | }; 50 | 51 | // Add to orders 52 | db.data.orders.push(order); 53 | await db.write() 54 | 55 | // Send notification 56 | sendNotification(userId, `Your order for ${product.name} has been confirmed! Estimated delivery: ${deliveryDate}`); 57 | 58 | return { 59 | success: true, 60 | order 61 | }; 62 | } 63 | 64 | // Payment processing function 65 | function processPayment(userId, productId, amount) { 66 | console.log(`Processing payment of $${amount} for product ${productId} by user ${userId}`); 67 | 68 | const failureChance = Math.random(); 69 | if (failureChance <= 0.5) { 70 | console.log("Payment processing failed - Payment gateway is down!"); 71 | throw new Error('PaymentGatewayDown'); 72 | } 73 | 74 | // OTHER PAYMENT ERRORS: 75 | // In a real application, payment processing requires calling a payment gateway which leads to an asynchronous flow. 76 | // Typically, a webhook has to be setup to accept the final payment state. 77 | // TO KEEP THIS WORKSHOP SIMPLE WE WILL NOT SHOWCASE HOW THESE ARE HANDLED. 78 | // A FUTURE WORKSHOP WILL SHOWCASE HOW YOU CAN MAKE THIS WORK WITH ORRA. 79 | 80 | // Create transaction record 81 | const transactionId = `trans-${Date.now()}-${userId.substring(0, 4)}-${productId.substring(0, 4)}`; 82 | 83 | console.log(`Payment successful! Transaction ID: ${transactionId}`); 84 | 85 | return transactionId; 86 | } 87 | 88 | // Simulated notification 89 | function sendNotification(userId, message) { 90 | // In a real application, this would send an email or push notification 91 | console.log(`Notification to user ${userId}: ${message}`); 92 | return { 93 | success: true, 94 | timestamp: new Date().toISOString() 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /stage3-grounding/stage_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Starting reset process..." 7 | 8 | # Define the base directory as the current directory 9 | BASE_DIR="$(pwd)" 10 | echo "Base directory: $BASE_DIR" 11 | 12 | # Step 1: Reset orra configuration 13 | echo "Resetting orra configuration..." 14 | orra config reset 15 | 16 | # Step 2: Remove dbstore 17 | echo "Removing dbstore..." 18 | rm -rf "$HOME/.orra/dbstore" 19 | 20 | # Step 3: Remove .orra-data directories from top-level subdirectories 21 | echo "Removing .orra-data directories..." 22 | for dir in "$BASE_DIR"/*; do 23 | if [ -d "$dir" ]; then 24 | ORRA_DATA_DIR="$dir/.orra-data" 25 | if [ -d "$ORRA_DATA_DIR" ]; then 26 | echo "Removing $ORRA_DATA_DIR" 27 | rm -rf "$ORRA_DATA_DIR" 28 | fi 29 | fi 30 | done 31 | 32 | # Step 4: Clear ORRA_API_KEY in .env files 33 | echo "Clearing ORRA_API_KEY in .env files..." 34 | for dir in "$BASE_DIR"/*; do 35 | if [ -d "$dir" ]; then 36 | ENV_FILE="$dir/.env" 37 | if [ -f "$ENV_FILE" ]; then 38 | echo "Updating $ENV_FILE" 39 | # Replace the ORRA_API_KEY line with an empty value 40 | sed -i '' 's/^ORRA_API_KEY=.*$/ORRA_API_KEY=/' "$ENV_FILE" 41 | fi 42 | fi 43 | done 44 | 45 | # Step 5: Remove data.json file if it exists 46 | DATA_JSON="$BASE_DIR/data.json" 47 | if [ -f "$DATA_JSON" ]; then 48 | echo "Removing $DATA_JSON" 49 | rm -f "$DATA_JSON" 50 | fi 51 | 52 | echo "Reset completed successfully!" 53 | -------------------------------------------------------------------------------- /stage3-grounding/stage_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | echo "Starting setup process..." 7 | 8 | # Define the base directory as the current directory 9 | BASE_DIR="$(pwd)" 10 | echo "Base directory: $BASE_DIR" 11 | 12 | # Step 1: Add a project to the orra plan engine 13 | echo "Adding project to orra plan engine..." 14 | orra projects add assistant 15 | 16 | # Step 2: Add a webhook to the project 17 | echo "Adding webhook to the project..." 18 | orra webhooks add http://localhost:3000/webhook 19 | 20 | # Step 3: Generate a new API key for the project 21 | echo "Generating new API key..." 22 | API_KEY_OUTPUT=$(orra api-keys gen assist-key) 23 | echo "$API_KEY_OUTPUT" 24 | 25 | # Step 4: Extract the API key from the output 26 | # Extract the API key from the output, ensuring whitespace is trimmed 27 | ORRA_API_KEY=$(echo "$API_KEY_OUTPUT" | grep "KEY:" | sed 's/^[[:space:]]*KEY:[[:space:]]*//' | tr -d '[:space:]') 28 | 29 | if [ -z "$ORRA_API_KEY" ]; then 30 | echo "Error: Could not extract API key from output" 31 | exit 1 32 | fi 33 | 34 | echo "Extracted API key: $ORRA_API_KEY" 35 | 36 | # Step 5: Check for .env-example files and copy to .env if needed 37 | echo "Checking for .env-example files..." 38 | for dir in "$BASE_DIR"/*; do 39 | if [ -d "$dir" ]; then 40 | ENV_FILE="$dir/.env" 41 | ENV_EXAMPLE_FILE="$dir/.env-example" 42 | 43 | # If .env doesn't exist but .env-example does, copy it 44 | if [ ! -f "$ENV_FILE" ] && [ -f "$ENV_EXAMPLE_FILE" ]; then 45 | echo "Creating $ENV_FILE from $ENV_EXAMPLE_FILE" 46 | cp "$ENV_EXAMPLE_FILE" "$ENV_FILE" 47 | fi 48 | fi 49 | done 50 | 51 | # Step 6: Add the ORRA_API_KEY to all .env files 52 | echo "Adding ORRA_API_KEY to .env files..." 53 | for dir in "$BASE_DIR"/*; do 54 | if [ -d "$dir" ]; then 55 | ENV_FILE="$dir/.env" 56 | if [ -f "$ENV_FILE" ]; then 57 | echo "Updating $ENV_FILE" 58 | # Check if ORRA_API_KEY line exists in the file 59 | if grep -q "^ORRA_API_KEY=" "$ENV_FILE"; then 60 | # Replace existing ORRA_API_KEY line 61 | sed -i '' "s|^ORRA_API_KEY=.*$|ORRA_API_KEY=$ORRA_API_KEY|" "$ENV_FILE" 62 | else 63 | # Add ORRA_API_KEY line if it doesn't exist 64 | echo "ORRA_API_KEY=$ORRA_API_KEY" >> "$ENV_FILE" 65 | fi 66 | else 67 | # Create new .env file if it doesn't exist 68 | echo "Creating new $ENV_FILE" 69 | echo "ORRA_API_KEY=$ORRA_API_KEY" > "$ENV_FILE" 70 | fi 71 | fi 72 | done 73 | 74 | # Step 7: Create data.json file by copying from example 75 | DATA_JSON_EXAMPLE="$BASE_DIR/data.json-example" 76 | DATA_JSON="$BASE_DIR/data.json" 77 | 78 | if [ -f "$DATA_JSON_EXAMPLE" ]; then 79 | echo "Creating data.json from example..." 80 | cp "$DATA_JSON_EXAMPLE" "$DATA_JSON" 81 | echo "data.json created successfully" 82 | else 83 | echo "Warning: data.json-example not found, skipping data.json creation" 84 | fi 85 | 86 | echo "Setup completed successfully!" 87 | --------------------------------------------------------------------------------