├── .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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------