/ driftkit-chat-assistant-framework / README.md
README.md
1 # DriftKit Chat Assistant Framework 2 3 [](https://adoptium.net/temurin/releases/) 4 [](https://spring.io/projects/spring-boot) 5 [](https://search.maven.org/) 6 7 **DriftKit Chat Assistant Framework** is a powerful and flexible framework for building AI-powered conversational workflows using annotation-based step definitions. It provides a robust foundation for creating complex, multi-step chat interactions with automatic schema generation, asynchronous processing, and composable user interfaces. 8 9 ## π― Key Features 10 11 ### Core Framework Features 12 13 - **π Annotation-based Workflow Definition** - Define complex workflows using simple Java annotations 14 - **π Automatic Schema Generation** - Convert Java classes to AI function schemas automatically 15 - **β‘ Asynchronous Step Execution** - Support for long-running operations with progress tracking 16 - **π§© Composable User Interfaces** - Break complex forms into step-by-step user interactions 17 - **πΎ Persistent Session Management** - Maintain workflow state across user interactions 18 - **π Conditional Flow Control** - Dynamic workflow routing based on user input and conditions 19 - **π οΈ Spring Boot Integration** - Seamless integration with Spring Boot applications 20 - **π Execution History Tracking** - Complete audit trail of workflow execution 21 - **π§ Extensible Architecture** - Easy to extend with custom components and integrations 22 23 ### Advanced Features 24 25 - **π¨ Dynamic Schema Composition** - Create complex forms by combining multiple schema classes 26 - **π Multi-step Workflows** - Chain multiple steps with automatic state management 27 - **π± Multi-modal Support** - Handle text, structured data, and custom input types 28 - **π Internationalization Ready** - Built-in support for multiple languages 29 - **π Expression Language Support** - Use SpEL for dynamic conditions and validations 30 - **π Progress Tracking** - Real-time progress updates for long-running operations 31 - **π‘οΈ Error Handling** - Comprehensive error handling and recovery mechanisms 32 33 ## π¦ Installation 34 35 ### Maven Dependency 36 37 ```xml 38 <dependency> 39 <groupId>ai.driftkit</groupId> 40 <artifactId>driftkit-chat-assistant-framework</artifactId> 41 <version>1.0-SNAPSHOT</version> 42 </dependency> 43 ``` 44 45 ### Gradle Dependency 46 47 ```gradle 48 implementation 'ai.driftkit:driftkit-chat-assistant-framework:1.0-SNAPSHOT' 49 ``` 50 51 ## Spring Boot Initialization 52 53 To use the chat assistant framework in your Spring Boot application: 54 55 ```java 56 @SpringBootApplication 57 @Import(FeignConfig.class) // Import Feign configuration for AI client 58 @ComponentScan(basePackages = {"ai.driftkit.chat.framework"}) // Scan framework components 59 public class YourApplication { 60 public static void main(String[] args) { 61 SpringApplication.run(YourApplication.class, args); 62 } 63 } 64 ``` 65 66 Configuration in `application.yml`: 67 68 ```yaml 69 ai-props: 70 host: "https://your-ai-service-host" 71 username: "${AI_USERNAME}" 72 password: "${AI_PASSWORD}" 73 ``` 74 75 The module provides: 76 - **Feign Configuration**: `FeignConfig` with basic authentication interceptor 77 - **AI Client**: `AiClient` interface for external AI service communication 78 - **Workflow Base Classes**: `AnnotatedWorkflow` for step-based conversational flows 79 - **Annotations**: `@WorkflowStep`, `@AsyncStep` for defining workflow steps 80 81 ## π Quick Start 82 83 ### 1. Implement Required Services 84 85 The framework requires implementations of several key interfaces: 86 87 ```java 88 @Component 89 public class MyWorkflowContextRepository implements WorkflowContextRepository { 90 private final Map<String, WorkflowContext> contexts = new ConcurrentHashMap<>(); 91 92 @Override 93 public Optional<WorkflowContext> findById(String id) { 94 return Optional.ofNullable(contexts.get(id)); 95 } 96 97 @Override 98 public WorkflowContext saveOrUpdate(WorkflowContext context) { 99 contexts.put(context.getContextId(), context); 100 return context; 101 } 102 103 @Override 104 public void deleteById(String id) { 105 contexts.remove(id); 106 } 107 } 108 109 @Component 110 public class MyAsyncResponseTracker implements AsyncResponseTracker { 111 @Override 112 public String generateResponseId() { 113 return UUID.randomUUID().toString(); 114 } 115 116 @Override 117 public void trackResponse(String responseId, ChatResponse response) { 118 // Implementation for tracking responses 119 } 120 121 @Override 122 public void updateResponseStatus(String responseId, ChatResponse response) { 123 // Implementation for updating response status 124 } 125 } 126 127 @Component 128 public class MyChatHistoryService implements ChatHistoryService { 129 @Override 130 public void addRequest(ChatRequest request) { 131 // Implementation for storing chat requests 132 } 133 134 @Override 135 public void updateResponse(ChatResponse response) { 136 // Implementation for updating responses 137 } 138 } 139 140 @Component 141 public class MyChatMessageService implements ChatMessageService { 142 @Override 143 public void addMessage(String chatId, String message, MessageType type) { 144 // Implementation for adding messages 145 } 146 } 147 ``` 148 149 ### 2. Define Schema Classes 150 151 Create data classes that represent the structure of your workflow inputs: 152 153 ```java 154 @SchemaClass( 155 id = "userRegistration", 156 description = "User registration information", 157 composable = true 158 ) 159 public class UserRegistrationInput { 160 @SchemaProperty(description = "User's full name", required = true) 161 private String fullName; 162 163 @SchemaProperty(description = "User's email address", required = true) 164 private String email; 165 166 @SchemaProperty( 167 description = "User's age", 168 required = true, 169 minValue = 18, 170 maxValue = 120 171 ) 172 private Integer age; 173 174 @SchemaProperty( 175 description = "User's role", 176 values = {"USER", "ADMIN", "MODERATOR"} 177 ) 178 private String role; 179 180 // Getters and setters 181 } 182 183 @SchemaClass(id = "confirmationRequest", description = "Confirmation request") 184 public class ConfirmationInput { 185 @SchemaProperty(description = "Confirmation decision", required = true) 186 private Boolean confirmed; 187 188 @SchemaProperty(description = "Additional comments") 189 private String comments; 190 191 // Getters and setters 192 } 193 ``` 194 195 ### 3. Create a Workflow 196 197 Extend `AnnotatedWorkflow` to define your workflow logic: 198 199 ```java 200 @Component 201 public class UserRegistrationWorkflow extends AnnotatedWorkflow { 202 203 @Override 204 public String getWorkflowId() { 205 return "user-registration"; 206 } 207 208 @Override 209 public boolean canHandle(String message, Map<String, String> properties) { 210 return message != null && message.toLowerCase().contains("register"); 211 } 212 213 @WorkflowStep( 214 index = 1, 215 inputClass = UserRegistrationInput.class, 216 description = "Collect user registration information" 217 ) 218 public StepEvent collectUserInfo(UserRegistrationInput input, WorkflowContext context) { 219 // Validate input 220 if (input.getAge() < 18) { 221 return StepEvent.withError("User must be at least 18 years old"); 222 } 223 224 // Store user data in context 225 context.setContextValue("userData", input); 226 227 // Return event with next step 228 return StepEvent.of(input, ConfirmationInput.class); 229 } 230 231 @WorkflowStep( 232 index = 2, 233 inputClass = ConfirmationInput.class, 234 description = "Confirm user registration" 235 ) 236 public StepEvent confirmRegistration(ConfirmationInput input, WorkflowContext context) { 237 UserRegistrationInput userData = context.getContextValue("userData", UserRegistrationInput.class); 238 239 if (input.getConfirmed()) { 240 // Process registration 241 return StepEvent.withMessage("Registration completed successfully for " + userData.getFullName()); 242 } else { 243 return StepEvent.withMessage("Registration cancelled"); 244 } 245 } 246 } 247 ``` 248 249 ### 4. Configuration 250 251 Configure the AI client in your `application.yml`: 252 253 ```yaml 254 ai-props: 255 host: https://api.openai.com 256 username: your-api-key 257 password: your-api-secret 258 259 spring: 260 application: 261 name: my-chat-assistant 262 ``` 263 264 ### 5. Create a REST Controller 265 266 ```java 267 @RestController 268 @RequestMapping("/api/chat") 269 public class ChatController { 270 271 @Autowired 272 private WorkflowRegistry workflowRegistry; 273 274 @PostMapping("/message") 275 public ResponseEntity<ChatResponse> processMessage(@RequestBody ChatRequest request) { 276 // Find appropriate workflow 277 ChatWorkflow workflow = workflowRegistry.findWorkflow(request.getMessage(), request.getPropertiesMap()); 278 279 if (workflow != null) { 280 ChatResponse response = workflow.processChat(request); 281 return ResponseEntity.ok(response); 282 } else { 283 return ResponseEntity.badRequest().build(); 284 } 285 } 286 } 287 ``` 288 289 ## π¨ Advanced Features 290 291 ### Asynchronous Step Execution 292 293 For long-running operations, use async steps: 294 295 ```java 296 @WorkflowStep( 297 index = 1, 298 inputClass = DocumentProcessingInput.class, 299 async = true, 300 description = "Process document asynchronously" 301 ) 302 public AsyncTaskEvent processDocument(DocumentProcessingInput input, WorkflowContext context) { 303 // Return immediately with task information 304 return AsyncTaskEvent.builder() 305 .taskName("documentProcessing") 306 .taskArgs(Map.of("documentId", input.getDocumentId())) 307 .messageId("processing_document") 308 .nextInputSchema(getSchemaFromClass(ProcessingResultInput.class)) 309 .build(); 310 } 311 312 @AsyncStep(forStep = "documentProcessing") 313 public StepEvent executeDocumentProcessing(Map<String, Object> taskArgs, WorkflowContext context) { 314 String documentId = (String) taskArgs.get("documentId"); 315 316 // Simulate long-running processing 317 try { 318 Thread.sleep(5000); // Simulate processing time 319 320 // Update progress 321 return StepEvent.builder() 322 .completed(true) 323 .percentComplete(100) 324 .properties(Map.of("result", "Document processed successfully")) 325 .build(); 326 } catch (InterruptedException e) { 327 return StepEvent.withError("Processing interrupted"); 328 } 329 } 330 ``` 331 332 ### Conditional Flow Control 333 334 Use Spring Expression Language for dynamic workflow routing: 335 336 ```java 337 @WorkflowStep( 338 index = 1, 339 inputClass = AgeVerificationInput.class, 340 condition = "#input.age >= 18", 341 onTrue = "adultStep", 342 onFalse = "minorStep" 343 ) 344 public StepEvent checkAge(AgeVerificationInput input, WorkflowContext context) { 345 return StepEvent.withProperty("age", input.getAge().toString()); 346 } 347 348 @WorkflowStep( 349 index = 2, 350 id = "adultStep", 351 inputClass = AdultServicesInput.class 352 ) 353 public StepEvent handleAdultUser(AdultServicesInput input, WorkflowContext context) { 354 return StepEvent.withMessage("Welcome to adult services"); 355 } 356 357 @WorkflowStep( 358 index = 3, 359 id = "minorStep", 360 inputClass = MinorServicesInput.class 361 ) 362 public StepEvent handleMinorUser(MinorServicesInput input, WorkflowContext context) { 363 return StepEvent.withMessage("Welcome to youth services"); 364 } 365 ``` 366 367 ### Composable Schemas 368 369 Break complex forms into multiple steps: 370 371 ```java 372 @SchemaClass( 373 id = "customerInfo", 374 description = "Complete customer information", 375 composable = true 376 ) 377 public class CustomerInfo { 378 @SchemaProperty(description = "Customer's full name", required = true) 379 private String fullName; 380 381 @SchemaProperty(description = "Customer's email", required = true) 382 private String email; 383 384 @SchemaProperty(description = "Customer's phone number") 385 private String phoneNumber; 386 387 @SchemaProperty(description = "Customer's address") 388 private String address; 389 390 @SchemaProperty(description = "Customer's preferences") 391 private String preferences; 392 393 // Getters and setters 394 } 395 ``` 396 397 When `composable = true`, the framework automatically creates separate interaction steps for each field, making the user experience more conversational. 398 399 ### Multi-Schema Steps 400 401 Handle multiple input types in a single step: 402 403 ```java 404 @WorkflowStep( 405 index = 1, 406 inputClasses = {EmailInput.class, PhoneInput.class, SocialMediaInput.class}, 407 description = "Choose contact method" 408 ) 409 public StepEvent selectContactMethod(Object input, WorkflowContext context) { 410 if (input instanceof EmailInput) { 411 return handleEmailContact((EmailInput) input, context); 412 } else if (input instanceof PhoneInput) { 413 return handlePhoneContact((PhoneInput) input, context); 414 } else if (input instanceof SocialMediaInput) { 415 return handleSocialMediaContact((SocialMediaInput) input, context); 416 } 417 418 return StepEvent.withError("Invalid contact method"); 419 } 420 ``` 421 422 ### Progress Tracking 423 424 Track progress for long-running operations: 425 426 ```java 427 @AsyncStep(forStep = "dataAnalysis") 428 public StepEvent performDataAnalysis(Map<String, Object> taskArgs, WorkflowContext context) { 429 String dataSetId = (String) taskArgs.get("dataSetId"); 430 431 // Initial progress 432 updateProgress(0, "Starting analysis..."); 433 434 // Simulate analysis phases 435 for (int i = 1; i <= 10; i++) { 436 try { 437 Thread.sleep(1000); // Simulate work 438 int progress = i * 10; 439 updateProgress(progress, "Processing phase " + i + "/10"); 440 } catch (InterruptedException e) { 441 return StepEvent.withError("Analysis interrupted"); 442 } 443 } 444 445 return StepEvent.builder() 446 .completed(true) 447 .percentComplete(100) 448 .properties(Map.of("result", "Analysis completed successfully")) 449 .build(); 450 } 451 452 private void updateProgress(int percent, String message) { 453 // Update progress in tracking system 454 // This would typically update a database or cache 455 } 456 ``` 457 458 ### Error Handling and Recovery 459 460 Implement comprehensive error handling: 461 462 ```java 463 @WorkflowStep( 464 index = 1, 465 inputClass = PaymentInput.class, 466 description = "Process payment" 467 ) 468 public StepEvent processPayment(PaymentInput input, WorkflowContext context) { 469 try { 470 // Validate payment information 471 if (!isValidPaymentInfo(input)) { 472 return StepEvent.withError("Invalid payment information. Please check your details."); 473 } 474 475 // Process payment 476 PaymentResult result = paymentService.processPayment(input); 477 478 if (result.isSuccess()) { 479 context.setContextValue("paymentId", result.getPaymentId()); 480 return StepEvent.of(result, ConfirmationInput.class); 481 } else { 482 return StepEvent.withError("Payment failed: " + result.getErrorMessage()); 483 } 484 485 } catch (PaymentException e) { 486 log.error("Payment processing failed", e); 487 return StepEvent.withError("Payment processing temporarily unavailable. Please try again later."); 488 } catch (Exception e) { 489 log.error("Unexpected error during payment processing", e); 490 return StepEvent.withError("An unexpected error occurred. Please contact support."); 491 } 492 } 493 ``` 494 495 ## π οΈ Configuration Options 496 497 ### AI Client Configuration 498 499 ```yaml 500 ai-props: 501 host: https://api.openai.com 502 username: ${OPENAI_API_KEY} 503 password: ${OPENAI_API_SECRET} 504 timeout: 30000 505 max-retries: 3 506 ``` 507 508 ### Workflow Configuration 509 510 ```yaml 511 driftkit: 512 workflows: 513 default-timeout: 300000 # 5 minutes 514 max-steps: 50 515 enable-debug: true 516 session-timeout: 1800000 # 30 minutes 517 ``` 518 519 ### Logging Configuration 520 521 ```yaml 522 logging: 523 level: 524 ai.driftkit.chat.framework: DEBUG 525 ai.driftkit.chat.framework.workflow: INFO 526 ai.driftkit.chat.framework.service: WARN 527 ``` 528 529 ## π Monitoring and Observability 530 531 ### Health Checks 532 533 ```java 534 @Component 535 public class WorkflowHealthIndicator implements HealthIndicator { 536 537 @Autowired 538 private WorkflowRegistry workflowRegistry; 539 540 @Override 541 public Health health() { 542 try { 543 int workflowCount = workflowRegistry.getRegisteredWorkflows().size(); 544 545 return Health.up() 546 .withDetail("registered-workflows", workflowCount) 547 .withDetail("framework-version", "1.0-SNAPSHOT") 548 .build(); 549 } catch (Exception e) { 550 return Health.down() 551 .withDetail("error", e.getMessage()) 552 .build(); 553 } 554 } 555 } 556 ``` 557 558 ### Metrics 559 560 ```java 561 @Component 562 public class WorkflowMetrics { 563 564 private final MeterRegistry meterRegistry; 565 private final Counter workflowExecutions; 566 private final Timer workflowDuration; 567 568 public WorkflowMetrics(MeterRegistry meterRegistry) { 569 this.meterRegistry = meterRegistry; 570 this.workflowExecutions = Counter.builder("workflow.executions") 571 .description("Number of workflow executions") 572 .register(meterRegistry); 573 this.workflowDuration = Timer.builder("workflow.duration") 574 .description("Workflow execution duration") 575 .register(meterRegistry); 576 } 577 578 public void recordExecution(String workflowId, Duration duration) { 579 workflowExecutions.increment(Tags.of("workflow", workflowId)); 580 workflowDuration.record(duration); 581 } 582 } 583 ``` 584 585 ## ποΈ Architecture Overview 586 587 ### Core Components 588 589 ``` 590 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 591 β Framework Architecture β 592 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ 593 β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β 594 β β Annotations β β Events β β Models β β 595 β β β β β β β β 596 β β @WorkflowStep β β StepEvent β βWorkflowContext β β 597 β β @AsyncStep β βAsyncTaskEvent β βStepDefinition β β 598 β β @SchemaClass β β β β β β 599 β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β 600 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ 601 β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β 602 β β Workflows β β Services β β Repositories β β 603 β β β β β β β β 604 β βAnnotatedWorkflowβ βChatHistoryServiceβ βWorkflowContext β β 605 β β WorkflowRegistryβ βChatMessageServiceβ β Repository β β 606 β β β βAsyncResponseTrackerβ β β β 607 β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β 608 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ 609 β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β 610 β β AI Client β β Utilities β β Framework β β 611 β β β β β β β β 612 β β AiClient β β SchemaUtils β β ApplicationContextβ β 613 β β AIFunctionSchemaβ β AIUtils β β Provider β β 614 β β β β β β β β 615 β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β 616 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 617 ``` 618 619 ### Workflow Execution Flow 620 621 ``` 622 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 623 β Workflow Execution Flow β 624 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ 625 β β 626 β 1. Chat Request β 2. Workflow Selection β 3. Session Mgmt β 627 β β 628 β β β β β 629 β β 630 β 4. Step Discovery β 5. Schema Generation β 6. Validation β 631 β β 632 β β β β β 633 β β 634 β 7. Step Execution β 8. Event Processing β 9. Response β 635 β β 636 β β β β β 637 β β 638 β 10. State Update β 11. History Tracking β 12. Completion β 639 β β 640 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 641 ``` 642 643 ## π Integration Examples 644 645 ### Spring Security Integration 646 647 ```java 648 @Component 649 public class SecureWorkflow extends AnnotatedWorkflow { 650 651 @Autowired 652 private SecurityContextHolder securityContextHolder; 653 654 @Override 655 public boolean canHandle(String message, Map<String, String> properties) { 656 // Check user permissions 657 Authentication auth = securityContextHolder.getContext().getAuthentication(); 658 return auth != null && auth.getAuthorities().stream() 659 .anyMatch(a -> a.getAuthority().equals("ROLE_USER")); 660 } 661 662 @WorkflowStep(index = 1, inputClass = SecureInput.class) 663 public StepEvent handleSecureOperation(SecureInput input, WorkflowContext context) { 664 // Access user information 665 String username = securityContextHolder.getContext().getAuthentication().getName(); 666 context.setContextValue("username", username); 667 668 return StepEvent.withMessage("Secure operation completed for " + username); 669 } 670 } 671 ``` 672 673 ### Database Integration 674 675 ```java 676 @Component 677 public class DatabaseIntegratedWorkflow extends AnnotatedWorkflow { 678 679 @Autowired 680 private UserRepository userRepository; 681 682 @Autowired 683 private TransactionTemplate transactionTemplate; 684 685 @WorkflowStep(index = 1, inputClass = UserLookupInput.class) 686 public StepEvent findUser(UserLookupInput input, WorkflowContext context) { 687 return transactionTemplate.execute(status -> { 688 Optional<User> user = userRepository.findByEmail(input.getEmail()); 689 690 if (user.isPresent()) { 691 context.setContextValue("user", user.get()); 692 return StepEvent.of(user.get(), UserDetailsInput.class); 693 } else { 694 return StepEvent.withError("User not found"); 695 } 696 }); 697 } 698 } 699 ``` 700 701 ### External API Integration 702 703 ```java 704 @Component 705 public class ExternalAPIWorkflow extends AnnotatedWorkflow { 706 707 @Autowired 708 private RestTemplate restTemplate; 709 710 @WorkflowStep(index = 1, inputClass = WeatherInput.class, async = true) 711 public AsyncTaskEvent getWeather(WeatherInput input, WorkflowContext context) { 712 return AsyncTaskEvent.builder() 713 .taskName("fetchWeather") 714 .taskArgs(Map.of("location", input.getLocation())) 715 .messageId("fetching_weather") 716 .build(); 717 } 718 719 @AsyncStep(forStep = "fetchWeather") 720 public StepEvent fetchWeatherData(Map<String, Object> taskArgs, WorkflowContext context) { 721 String location = (String) taskArgs.get("location"); 722 723 try { 724 String url = "https://api.weather.com/v1/current?location=" + location; 725 WeatherResponse response = restTemplate.getForObject(url, WeatherResponse.class); 726 727 return StepEvent.builder() 728 .completed(true) 729 .percentComplete(100) 730 .properties(Map.of( 731 "temperature", response.getTemperature().toString(), 732 "condition", response.getCondition() 733 )) 734 .build(); 735 } catch (Exception e) { 736 return StepEvent.withError("Failed to fetch weather data: " + e.getMessage()); 737 } 738 } 739 } 740 ``` 741 742 ## π§ Annotation Reference 743 744 ### @WorkflowStep 745 746 ```java 747 @WorkflowStep( 748 index = 1, // Step order (required) 749 id = "customStepId", // Custom step ID (optional) 750 description = "Step description", // Human-readable description 751 inputClass = InputClass.class, // Single input class 752 inputClasses = {Input1.class, Input2.class}, // Multiple input classes 753 outputClasses = {Output1.class}, // Output classes 754 nextClasses = {NextInput.class}, // Next step input classes 755 nextSteps = {"stepId1", "stepId2"}, // Possible next steps 756 requiresUserInput = true, // Requires user input 757 async = false, // Asynchronous execution 758 condition = "#input.age >= 18", // Condition expression 759 onTrue = "adultStep", // Step if condition is true 760 onFalse = "minorStep", // Step if condition is false 761 inputSchemaId = "customSchema", // Input schema ID 762 outputSchemaId = "outputSchema" // Output schema ID 763 ) 764 ``` 765 766 ### @AsyncStep 767 768 ```java 769 @AsyncStep( 770 forStep = "stepId", // Associated step ID (required) 771 inputClass = InputClass.class, // Input class 772 inputClasses = {Input1.class}, // Multiple input classes 773 outputClass = OutputClass.class, // Output class 774 nextClasses = {NextInput.class} // Next step input classes 775 ) 776 ``` 777 778 ### @SchemaClass 779 780 ```java 781 @SchemaClass( 782 id = "schemaId", // Schema identifier 783 description = "Schema description", // Schema description 784 composable = false // Composable schema flag 785 ) 786 ``` 787 788 ### @SchemaProperty 789 790 ```java 791 @SchemaProperty( 792 nameId = "propertyId", // Property identifier 793 description = "Property description", // Property description 794 required = true, // Required flag 795 defaultValue = "default", // Default value 796 minValue = 0, // Minimum value 797 maxValue = 100, // Maximum value 798 minLength = 1, // Minimum length 799 maxLength = 255, // Maximum length 800 values = {"option1", "option2"}, // Enum values 801 array = false, // Array flag 802 multiSelect = false, // Multi-select flag 803 valueAsNameId = false, // Value as name ID flag 804 type = String.class // Property type 805 ) 806 ``` 807 808 ## π Troubleshooting 809 810 ### Common Issues 811 812 #### 1. Workflow Not Found 813 814 **Problem**: Workflow not being selected for user input. 815 816 **Solution**: 817 ```java 818 @Override 819 public boolean canHandle(String message, Map<String, String> properties) { 820 // Make sure this method returns true for expected inputs 821 return message != null && message.toLowerCase().contains("expected keyword"); 822 } 823 ``` 824 825 #### 2. Schema Generation Errors 826 827 **Problem**: Schema not being generated correctly. 828 829 **Solution**: 830 ```java 831 // Ensure proper annotations 832 @SchemaClass(id = "uniqueId", description = "Clear description") 833 public class MySchema { 834 @SchemaProperty(description = "Field description", required = true) 835 private String field; 836 837 // Public constructor required 838 public MySchema() {} 839 840 // Getters and setters required 841 } 842 ``` 843 844 #### 3. Async Step Not Executing 845 846 **Problem**: Async step not being called. 847 848 **Solution**: 849 ```java 850 // Ensure task name matches 851 @WorkflowStep(async = true) 852 public AsyncTaskEvent startTask() { 853 return AsyncTaskEvent.builder() 854 .taskName("exactTaskName") // Must match @AsyncStep forStep 855 .build(); 856 } 857 858 @AsyncStep(forStep = "exactTaskName") // Must match taskName 859 public StepEvent executeTask() { 860 // Implementation 861 } 862 ``` 863 864 #### 4. Session State Issues 865 866 **Problem**: Session state not persisting. 867 868 **Solution**: 869 ```java 870 @Component 871 public class MyWorkflowContextRepository implements WorkflowContextRepository { 872 // Ensure proper implementation with actual persistence 873 // Don't use in-memory maps in production 874 } 875 ``` 876 877 ### Debug Configuration 878 879 ```yaml 880 logging: 881 level: 882 ai.driftkit.chat.framework: DEBUG 883 ai.driftkit.chat.framework.workflow.AnnotatedWorkflow: TRACE 884 ai.driftkit.chat.framework.util.SchemaUtils: DEBUG 885 ``` 886 887 ### Performance Optimization 888 889 ```java 890 // Cache schema generation 891 @Configuration 892 public class SchemaConfiguration { 893 894 @Bean 895 public CacheManager cacheManager() { 896 return new ConcurrentMapCacheManager("schemas", "workflows"); 897 } 898 899 @Cacheable("schemas") 900 public AIFunctionSchema getSchema(Class<?> clazz) { 901 return SchemaUtils.getSchemaFromClass(clazz); 902 } 903 } 904 ``` 905 906 ## π Performance Considerations 907 908 ### Best Practices 909 910 1. **Schema Caching**: Schemas are automatically cached, but consider external caching for high-load scenarios 911 2. **Async Processing**: Use async steps for operations taking more than 1-2 seconds 912 3. **Database Optimization**: Implement efficient WorkflowContextRepository with proper indexing 913 4. **Memory Management**: Clean up old sessions periodically 914 5. **Connection Pooling**: Configure appropriate connection pools for AI client 915 916 ### Monitoring 917 918 ```java 919 @Component 920 public class WorkflowPerformanceMonitor { 921 922 @EventListener 923 public void handleStepExecution(StepExecutionEvent event) { 924 long duration = event.getExecutionTime(); 925 if (duration > 1000) { // Log slow operations 926 log.warn("Slow step execution: {} took {}ms", 927 event.getStepId(), duration); 928 } 929 } 930 } 931 ``` 932 933 ## π€ Contributing 934 935 1. Fork the repository 936 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 937 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 938 4. Push to the branch (`git push origin feature/amazing-feature`) 939 5. Open a Pull Request 940 941 ## π License 942 943 This project is part of the DriftKit framework and is licensed under the terms specified in the main DriftKit project. 944 945 ## π Support 946 947 - **Documentation**: [DriftKit Documentation](https://driftkit.ai/docs) 948 - **Issues**: [GitHub Issues](https://github.com/driftkit/framework/issues) 949 - **Discussions**: [GitHub Discussions](https://github.com/driftkit/framework/discussions) 950 - **Email**: support@driftkit.ai 951 952 ## π Related Projects 953 954 - [DriftKit Common](../driftkit-common) - Core utilities and shared components 955 - [DriftKit Workflows](../driftkit-workflows) - Workflow execution engine 956 - [DriftKit Context Engineering](../driftkit-context-engineering) - Prompt management system 957 - [DriftKit Vector](../driftkit-vector) - Vector storage and retrieval 958 959 ## π Real-World Demo Examples 960 961 ### 1. E-commerce Order Assistant 962 963 This example demonstrates a complete e-commerce order workflow with product search, cart management, and checkout. 964 965 ```java 966 @Component 967 public class EcommerceOrderWorkflow extends AnnotatedWorkflow { 968 969 @Autowired 970 private ProductService productService; 971 972 @Autowired 973 private InventoryService inventoryService; 974 975 @Autowired 976 private PaymentService paymentService; 977 978 @Autowired 979 private OrderService orderService; 980 981 @Override 982 public String getWorkflowId() { 983 return "ecommerce-order"; 984 } 985 986 @Override 987 public boolean canHandle(String message, Map<String, String> properties) { 988 return message.toLowerCase().contains("order") || 989 message.toLowerCase().contains("buy") || 990 message.toLowerCase().contains("purchase"); 991 } 992 993 @WorkflowStep( 994 index = 1, 995 inputClass = ProductSearchInput.class, 996 description = "Search for products" 997 ) 998 public StepEvent searchProducts(ProductSearchInput input, WorkflowContext context) { 999 List<Product> products = productService.searchProducts(input.getQuery(), input.getCategory()); 1000 1001 if (products.isEmpty()) { 1002 return StepEvent.withMessage("No products found. Try a different search term."); 1003 } 1004 1005 context.setContextValue("searchResults", products); 1006 1007 // Generate product selection options 1008 ProductSelectionInput selectionInput = new ProductSelectionInput(); 1009 selectionInput.setProducts(products.stream() 1010 .map(p -> new ProductOption(p.getId(), p.getName(), p.getPrice())) 1011 .collect(Collectors.toList())); 1012 1013 return StepEvent.of(selectionInput, ProductSelectionInput.class); 1014 } 1015 1016 @WorkflowStep( 1017 index = 2, 1018 inputClass = ProductSelectionInput.class, 1019 description = "Select products and quantities" 1020 ) 1021 public StepEvent addToCart(ProductSelectionInput input, WorkflowContext context) { 1022 Cart cart = context.getContextValue("cart", Cart.class); 1023 if (cart == null) { 1024 cart = new Cart(); 1025 } 1026 1027 // Add selected products to cart 1028 for (SelectedProduct selected : input.getSelectedProducts()) { 1029 Product product = productService.getProduct(selected.getProductId()); 1030 1031 // Check inventory 1032 if (!inventoryService.isAvailable(product.getId(), selected.getQuantity())) { 1033 return StepEvent.withError( 1034 String.format("%s is out of stock. Only %d available.", 1035 product.getName(), 1036 inventoryService.getAvailableQuantity(product.getId())) 1037 ); 1038 } 1039 1040 cart.addItem(product, selected.getQuantity()); 1041 } 1042 1043 context.setContextValue("cart", cart); 1044 1045 // Show cart summary and ask for next action 1046 CartSummary summary = new CartSummary(); 1047 summary.setItems(cart.getItems()); 1048 summary.setSubtotal(cart.getSubtotal()); 1049 summary.setTax(cart.getTax()); 1050 summary.setTotal(cart.getTotal()); 1051 1052 return StepEvent.of(summary, CartActionInput.class); 1053 } 1054 1055 @WorkflowStep( 1056 index = 3, 1057 inputClass = CartActionInput.class, 1058 description = "Handle cart actions", 1059 condition = "#input.action == 'CHECKOUT'", 1060 onTrue = "collectShipping", 1061 onFalse = "continueShopping" 1062 ) 1063 public StepEvent handleCartAction(CartActionInput input, WorkflowContext context) { 1064 return StepEvent.withProperty("action", input.getAction()); 1065 } 1066 1067 @WorkflowStep( 1068 index = 4, 1069 id = "collectShipping", 1070 inputClass = ShippingInput.class, 1071 description = "Collect shipping information" 1072 ) 1073 public StepEvent collectShippingInfo(ShippingInput input, WorkflowContext context) { 1074 // Validate address 1075 if (!addressService.validateAddress(input)) { 1076 return StepEvent.withError("Invalid address. Please check and try again."); 1077 } 1078 1079 // Calculate shipping options 1080 Cart cart = context.getContextValue("cart", Cart.class); 1081 List<ShippingOption> options = shippingService.calculateOptions(cart, input.getAddress()); 1082 1083 context.setContextValue("shippingAddress", input); 1084 1085 return StepEvent.of( 1086 new ShippingSelectionInput(options), 1087 ShippingSelectionInput.class 1088 ); 1089 } 1090 1091 @WorkflowStep( 1092 index = 5, 1093 inputClass = ShippingSelectionInput.class, 1094 description = "Select shipping method" 1095 ) 1096 public StepEvent selectShipping(ShippingSelectionInput input, WorkflowContext context) { 1097 context.setContextValue("shippingMethod", input.getSelectedOption()); 1098 1099 // Update cart with shipping cost 1100 Cart cart = context.getContextValue("cart", Cart.class); 1101 cart.setShippingCost(input.getSelectedOption().getCost()); 1102 1103 return StepEvent.of(new PaymentInput(), PaymentInput.class); 1104 } 1105 1106 @WorkflowStep( 1107 index = 6, 1108 inputClass = PaymentInput.class, 1109 description = "Process payment", 1110 async = true 1111 ) 1112 public AsyncTaskEvent processPayment(PaymentInput input, WorkflowContext context) { 1113 Cart cart = context.getContextValue("cart", Cart.class); 1114 1115 return AsyncTaskEvent.builder() 1116 .taskName("paymentProcessing") 1117 .taskArgs(Map.of( 1118 "paymentMethod", input.getPaymentMethod(), 1119 "amount", cart.getTotal(), 1120 "cartId", cart.getId() 1121 )) 1122 .messageId("processing_payment") 1123 .nextInputSchema(getSchemaFromClass(OrderConfirmationInput.class)) 1124 .build(); 1125 } 1126 1127 @AsyncStep(forStep = "paymentProcessing") 1128 public StepEvent executePayment(Map<String, Object> taskArgs, WorkflowContext context) { 1129 try { 1130 PaymentResult result = paymentService.processPayment( 1131 (String) taskArgs.get("paymentMethod"), 1132 (BigDecimal) taskArgs.get("amount") 1133 ); 1134 1135 if (result.isSuccess()) { 1136 // Create order 1137 Cart cart = context.getContextValue("cart", Cart.class); 1138 ShippingInput shipping = context.getContextValue("shippingAddress", ShippingInput.class); 1139 1140 Order order = orderService.createOrder(cart, shipping, result.getTransactionId()); 1141 context.setContextValue("orderId", order.getId()); 1142 1143 // Reserve inventory 1144 inventoryService.reserveItems(order.getItems()); 1145 1146 return StepEvent.builder() 1147 .completed(true) 1148 .percentComplete(100) 1149 .properties(Map.of( 1150 "orderId", order.getId(), 1151 "orderNumber", order.getOrderNumber(), 1152 "estimatedDelivery", order.getEstimatedDelivery() 1153 )) 1154 .build(); 1155 } else { 1156 return StepEvent.withError("Payment failed: " + result.getErrorMessage()); 1157 } 1158 } catch (Exception e) { 1159 return StepEvent.withError("Payment processing error: " + e.getMessage()); 1160 } 1161 } 1162 1163 @WorkflowStep( 1164 index = 7, 1165 id = "continueShopping", 1166 inputClass = ProductSearchInput.class, 1167 description = "Continue shopping" 1168 ) 1169 public StepEvent continueShopping(ProductSearchInput input, WorkflowContext context) { 1170 // Loop back to product search 1171 return searchProducts(input, context); 1172 } 1173 } 1174 ``` 1175 1176 ### 2. Healthcare Appointment Booking 1177 1178 This example shows a healthcare appointment booking system with doctor selection, availability checking, and confirmation. 1179 1180 ```java 1181 @Component 1182 public class HealthcareAppointmentWorkflow extends AnnotatedWorkflow { 1183 1184 @Autowired 1185 private DoctorService doctorService; 1186 1187 @Autowired 1188 private AppointmentService appointmentService; 1189 1190 @Autowired 1191 private NotificationService notificationService; 1192 1193 @Override 1194 public String getWorkflowId() { 1195 return "healthcare-appointment"; 1196 } 1197 1198 @Override 1199 public boolean canHandle(String message, Map<String, String> properties) { 1200 return message.toLowerCase().contains("appointment") || 1201 message.toLowerCase().contains("doctor") || 1202 message.toLowerCase().contains("schedule"); 1203 } 1204 1205 @WorkflowStep( 1206 index = 1, 1207 inputClass = SymptomInput.class, 1208 description = "Describe your symptoms or reason for visit" 1209 ) 1210 public StepEvent collectSymptoms(SymptomInput input, WorkflowContext context) { 1211 // Analyze symptoms to suggest appropriate specialists 1212 List<String> specialties = symptomAnalyzer.suggestSpecialties(input.getSymptoms()); 1213 1214 context.setContextValue("symptoms", input.getSymptoms()); 1215 context.setContextValue("urgency", input.getUrgencyLevel()); 1216 1217 // For urgent cases, suggest immediate care 1218 if (input.getUrgencyLevel() == UrgencyLevel.EMERGENCY) { 1219 return StepEvent.withMessage( 1220 "This seems urgent. Please visit the emergency room or call 911." 1221 ); 1222 } 1223 1224 // Find available doctors 1225 List<Doctor> doctors = doctorService.findBySpecialties(specialties); 1226 1227 DoctorSelectionInput selection = new DoctorSelectionInput(); 1228 selection.setDoctors(doctors.stream() 1229 .map(d -> new DoctorOption( 1230 d.getId(), 1231 d.getName(), 1232 d.getSpecialty(), 1233 d.getRating(), 1234 d.getNextAvailable() 1235 )) 1236 .collect(Collectors.toList())); 1237 1238 return StepEvent.of(selection, DoctorSelectionInput.class); 1239 } 1240 1241 @WorkflowStep( 1242 index = 2, 1243 inputClass = DoctorSelectionInput.class, 1244 description = "Select a doctor" 1245 ) 1246 public StepEvent selectDoctor(DoctorSelectionInput input, WorkflowContext context) { 1247 Doctor doctor = doctorService.getDoctor(input.getSelectedDoctorId()); 1248 context.setContextValue("selectedDoctor", doctor); 1249 1250 // Get available time slots for next 2 weeks 1251 LocalDate startDate = LocalDate.now().plusDays(1); 1252 LocalDate endDate = startDate.plusWeeks(2); 1253 1254 List<TimeSlot> availableSlots = appointmentService.getAvailableSlots( 1255 doctor.getId(), 1256 startDate, 1257 endDate 1258 ); 1259 1260 if (availableSlots.isEmpty()) { 1261 return StepEvent.withMessage( 1262 "No appointments available in the next 2 weeks. Would you like to see other doctors?" 1263 ); 1264 } 1265 1266 TimeSlotSelectionInput slotSelection = new TimeSlotSelectionInput(); 1267 slotSelection.setTimeSlots(availableSlots); 1268 slotSelection.setDoctorName(doctor.getName()); 1269 1270 return StepEvent.of(slotSelection, TimeSlotSelectionInput.class); 1271 } 1272 1273 @WorkflowStep( 1274 index = 3, 1275 inputClass = TimeSlotSelectionInput.class, 1276 description = "Select appointment time" 1277 ) 1278 public StepEvent selectTimeSlot(TimeSlotSelectionInput input, WorkflowContext context) { 1279 TimeSlot selectedSlot = input.getSelectedSlot(); 1280 Doctor doctor = context.getContextValue("selectedDoctor", Doctor.class); 1281 1282 // Check if slot is still available (prevent race conditions) 1283 if (!appointmentService.isSlotAvailable(doctor.getId(), selectedSlot)) { 1284 return StepEvent.withError( 1285 "Sorry, that time slot was just booked. Please select another time." 1286 ); 1287 } 1288 1289 context.setContextValue("selectedTimeSlot", selectedSlot); 1290 1291 // Collect patient information 1292 return StepEvent.of(new PatientInfoInput(), PatientInfoInput.class); 1293 } 1294 1295 @WorkflowStep( 1296 index = 4, 1297 inputClass = PatientInfoInput.class, 1298 description = "Provide patient information" 1299 ) 1300 public StepEvent collectPatientInfo(PatientInfoInput input, WorkflowContext context) { 1301 // Validate insurance if provided 1302 if (input.hasInsurance()) { 1303 InsuranceValidation validation = insuranceService.validate( 1304 input.getInsuranceProvider(), 1305 input.getInsuranceNumber() 1306 ); 1307 1308 if (!validation.isValid()) { 1309 return StepEvent.withError( 1310 "Insurance validation failed: " + validation.getErrorMessage() 1311 ); 1312 } 1313 1314 context.setContextValue("insuranceCoverage", validation.getCoverageInfo()); 1315 } 1316 1317 context.setContextValue("patientInfo", input); 1318 1319 // Show appointment summary for confirmation 1320 AppointmentSummary summary = buildAppointmentSummary(context); 1321 1322 return StepEvent.of(summary, AppointmentConfirmationInput.class); 1323 } 1324 1325 @WorkflowStep( 1326 index = 5, 1327 inputClass = AppointmentConfirmationInput.class, 1328 description = "Confirm appointment booking", 1329 async = true 1330 ) 1331 public AsyncTaskEvent confirmAppointment(AppointmentConfirmationInput input, WorkflowContext context) { 1332 if (!input.isConfirmed()) { 1333 return AsyncTaskEvent.builder() 1334 .taskName("cancelBooking") 1335 .messageId("booking_cancelled") 1336 .build(); 1337 } 1338 1339 return AsyncTaskEvent.builder() 1340 .taskName("bookAppointment") 1341 .taskArgs(Map.of( 1342 "doctorId", context.getContextValue("selectedDoctor", Doctor.class).getId(), 1343 "timeSlot", context.getContextValue("selectedTimeSlot"), 1344 "patientInfo", context.getContextValue("patientInfo"), 1345 "symptoms", context.getContextValue("symptoms") 1346 )) 1347 .messageId("booking_appointment") 1348 .build(); 1349 } 1350 1351 @AsyncStep(forStep = "bookAppointment") 1352 public StepEvent executeBooking(Map<String, Object> taskArgs, WorkflowContext context) { 1353 try { 1354 // Create appointment 1355 Appointment appointment = appointmentService.createAppointment( 1356 (String) taskArgs.get("doctorId"), 1357 (TimeSlot) taskArgs.get("timeSlot"), 1358 (PatientInfo) taskArgs.get("patientInfo"), 1359 (String) taskArgs.get("symptoms") 1360 ); 1361 1362 // Send confirmation notifications 1363 notificationService.sendAppointmentConfirmation( 1364 appointment, 1365 NotificationChannel.EMAIL, 1366 NotificationChannel.SMS 1367 ); 1368 1369 // Add to calendar 1370 String calendarLink = calendarService.createCalendarEvent(appointment); 1371 1372 return StepEvent.builder() 1373 .completed(true) 1374 .percentComplete(100) 1375 .properties(Map.of( 1376 "appointmentId", appointment.getId(), 1377 "confirmationNumber", appointment.getConfirmationNumber(), 1378 "calendarLink", calendarLink, 1379 "message", "Appointment booked successfully! Confirmation sent to your email and phone." 1380 )) 1381 .build(); 1382 1383 } catch (Exception e) { 1384 log.error("Failed to book appointment", e); 1385 return StepEvent.withError("Failed to book appointment: " + e.getMessage()); 1386 } 1387 } 1388 1389 private AppointmentSummary buildAppointmentSummary(WorkflowContext context) { 1390 Doctor doctor = context.getContextValue("selectedDoctor", Doctor.class); 1391 TimeSlot timeSlot = context.getContextValue("selectedTimeSlot", TimeSlot.class); 1392 PatientInfo patient = context.getContextValue("patientInfo", PatientInfo.class); 1393 InsuranceCoverage coverage = context.getContextValue("insuranceCoverage", InsuranceCoverage.class); 1394 1395 AppointmentSummary summary = new AppointmentSummary(); 1396 summary.setDoctorName(doctor.getName()); 1397 summary.setDoctorSpecialty(doctor.getSpecialty()); 1398 summary.setDateTime(timeSlot.getDateTime()); 1399 summary.setDuration(timeSlot.getDuration()); 1400 summary.setLocation(doctor.getOfficeAddress()); 1401 summary.setPatientName(patient.getFullName()); 1402 summary.setEstimatedCost(calculateEstimatedCost(doctor, coverage)); 1403 summary.setInsuranceCovered(coverage != null); 1404 1405 return summary; 1406 } 1407 } 1408 ``` 1409 1410 ### 3. Banking Virtual Assistant 1411 1412 This example demonstrates a banking assistant that handles account inquiries, transfers, and bill payments. 1413 1414 ```java 1415 @Component 1416 public class BankingAssistantWorkflow extends AnnotatedWorkflow { 1417 1418 @Autowired 1419 private AccountService accountService; 1420 1421 @Autowired 1422 private TransactionService transactionService; 1423 1424 @Autowired 1425 private SecurityService securityService; 1426 1427 @Autowired 1428 private BillPayService billPayService; 1429 1430 @Override 1431 public String getWorkflowId() { 1432 return "banking-assistant"; 1433 } 1434 1435 @Override 1436 public boolean canHandle(String message, Map<String, String> properties) { 1437 // Require authenticated user 1438 return properties.containsKey("userId") && 1439 (message.toLowerCase().contains("account") || 1440 message.toLowerCase().contains("transfer") || 1441 message.toLowerCase().contains("balance") || 1442 message.toLowerCase().contains("pay")); 1443 } 1444 1445 @WorkflowStep( 1446 index = 1, 1447 inputClass = BankingActionInput.class, 1448 description = "What would you like to do?" 1449 ) 1450 public StepEvent selectAction(BankingActionInput input, WorkflowContext context) { 1451 String userId = context.getProperty("userId"); 1452 1453 // Verify user session 1454 if (!securityService.isSessionValid(userId)) { 1455 return StepEvent.withError("Session expired. Please log in again."); 1456 } 1457 1458 switch (input.getAction()) { 1459 case CHECK_BALANCE: 1460 return checkBalance(userId, context); 1461 case TRANSFER_MONEY: 1462 return StepEvent.of(new TransferInput(), TransferInput.class); 1463 case PAY_BILL: 1464 return StepEvent.of(new BillSelectionInput(), BillSelectionInput.class); 1465 case VIEW_TRANSACTIONS: 1466 return viewTransactions(userId, context); 1467 default: 1468 return StepEvent.withError("Unknown action"); 1469 } 1470 } 1471 1472 private StepEvent checkBalance(String userId, WorkflowContext context) { 1473 List<Account> accounts = accountService.getUserAccounts(userId); 1474 1475 BalanceSummary summary = new BalanceSummary(); 1476 summary.setAccounts(accounts.stream() 1477 .map(a -> new AccountBalance( 1478 a.getAccountNumber(), 1479 a.getType(), 1480 a.getBalance(), 1481 a.getAvailableBalance() 1482 )) 1483 .collect(Collectors.toList())); 1484 summary.setTotalBalance(accounts.stream() 1485 .map(Account::getBalance) 1486 .reduce(BigDecimal.ZERO, BigDecimal::add)); 1487 1488 return StepEvent.withMessage( 1489 "Your account balances:\n" + formatBalances(summary) 1490 ); 1491 } 1492 1493 @WorkflowStep( 1494 index = 2, 1495 inputClass = TransferInput.class, 1496 description = "Enter transfer details" 1497 ) 1498 public StepEvent setupTransfer(TransferInput input, WorkflowContext context) { 1499 String userId = context.getProperty("userId"); 1500 1501 // Validate source account 1502 Account sourceAccount = accountService.getAccount(userId, input.getFromAccountId()); 1503 if (sourceAccount == null) { 1504 return StepEvent.withError("Invalid source account"); 1505 } 1506 1507 // Check balance 1508 if (sourceAccount.getAvailableBalance().compareTo(input.getAmount()) < 0) { 1509 return StepEvent.withError( 1510 String.format("Insufficient funds. Available: $%.2f", 1511 sourceAccount.getAvailableBalance()) 1512 ); 1513 } 1514 1515 // Validate destination 1516 TransferValidation validation = transactionService.validateTransfer( 1517 sourceAccount, 1518 input.getToAccount(), 1519 input.getAmount() 1520 ); 1521 1522 if (!validation.isValid()) { 1523 return StepEvent.withError(validation.getErrorMessage()); 1524 } 1525 1526 context.setContextValue("transferDetails", input); 1527 context.setContextValue("sourceAccount", sourceAccount); 1528 1529 // Require 2FA for transfers 1530 return StepEvent.of(new TwoFactorInput(), TwoFactorInput.class); 1531 } 1532 1533 @WorkflowStep( 1534 index = 3, 1535 inputClass = TwoFactorInput.class, 1536 description = "Enter verification code", 1537 async = true 1538 ) 1539 public AsyncTaskEvent verifyAndTransfer(TwoFactorInput input, WorkflowContext context) { 1540 String userId = context.getProperty("userId"); 1541 1542 // Verify 2FA code 1543 if (!securityService.verify2FA(userId, input.getCode())) { 1544 return AsyncTaskEvent.builder() 1545 .taskName("transferFailed") 1546 .messageId("invalid_2fa") 1547 .build(); 1548 } 1549 1550 TransferInput transfer = context.getContextValue("transferDetails", TransferInput.class); 1551 1552 return AsyncTaskEvent.builder() 1553 .taskName("executeTransfer") 1554 .taskArgs(Map.of( 1555 "userId", userId, 1556 "transfer", transfer 1557 )) 1558 .messageId("processing_transfer") 1559 .build(); 1560 } 1561 1562 @AsyncStep(forStep = "executeTransfer") 1563 public StepEvent performTransfer(Map<String, Object> taskArgs, WorkflowContext context) { 1564 try { 1565 String userId = (String) taskArgs.get("userId"); 1566 TransferInput transfer = (TransferInput) taskArgs.get("transfer"); 1567 1568 // Execute transfer 1569 TransactionResult result = transactionService.executeTransfer( 1570 userId, 1571 transfer.getFromAccountId(), 1572 transfer.getToAccount(), 1573 transfer.getAmount(), 1574 transfer.getMemo() 1575 ); 1576 1577 if (result.isSuccess()) { 1578 // Send confirmation 1579 notificationService.sendTransferConfirmation(userId, result); 1580 1581 return StepEvent.builder() 1582 .completed(true) 1583 .percentComplete(100) 1584 .properties(Map.of( 1585 "transactionId", result.getTransactionId(), 1586 "confirmationNumber", result.getConfirmationNumber(), 1587 "message", String.format( 1588 "Transfer of $%.2f completed successfully. Confirmation: %s", 1589 transfer.getAmount(), 1590 result.getConfirmationNumber() 1591 ) 1592 )) 1593 .build(); 1594 } else { 1595 return StepEvent.withError("Transfer failed: " + result.getErrorMessage()); 1596 } 1597 1598 } catch (Exception e) { 1599 log.error("Transfer processing error", e); 1600 return StepEvent.withError("Transfer processing error. Please try again."); 1601 } 1602 } 1603 1604 @WorkflowStep( 1605 index = 2, 1606 inputClass = BillSelectionInput.class, 1607 description = "Select bill to pay" 1608 ) 1609 public StepEvent selectBill(BillSelectionInput input, WorkflowContext context) { 1610 String userId = context.getProperty("userId"); 1611 1612 if (input.getAction() == BillAction.VIEW_BILLS) { 1613 List<Bill> bills = billPayService.getUpcomingBills(userId); 1614 1615 BillListDisplay display = new BillListDisplay(); 1616 display.setBills(bills); 1617 display.setTotalDue(bills.stream() 1618 .map(Bill::getAmountDue) 1619 .reduce(BigDecimal.ZERO, BigDecimal::add)); 1620 1621 return StepEvent.of(display, BillPaymentInput.class); 1622 } else { 1623 // Add new payee 1624 return StepEvent.of(new PayeeInput(), PayeeInput.class); 1625 } 1626 } 1627 } 1628 ``` 1629 1630 ### 4. Travel Planning Assistant 1631 1632 This example shows a comprehensive travel planning workflow with flight search, hotel booking, and itinerary creation. 1633 1634 ```java 1635 @Component 1636 public class TravelPlanningWorkflow extends AnnotatedWorkflow { 1637 1638 @Autowired 1639 private FlightSearchService flightService; 1640 1641 @Autowired 1642 private HotelSearchService hotelService; 1643 1644 @Autowired 1645 private ActivityService activityService; 1646 1647 @Autowired 1648 private ItineraryService itineraryService; 1649 1650 @Override 1651 public String getWorkflowId() { 1652 return "travel-planning"; 1653 } 1654 1655 @WorkflowStep( 1656 index = 1, 1657 inputClass = TravelBasicsInput.class, 1658 description = "Tell me about your travel plans" 1659 ) 1660 public StepEvent collectTravelBasics(TravelBasicsInput input, WorkflowContext context) { 1661 // Validate travel dates 1662 if (input.getDepartureDate().isBefore(LocalDate.now())) { 1663 return StepEvent.withError("Departure date must be in the future"); 1664 } 1665 1666 if (input.getReturnDate().isBefore(input.getDepartureDate())) { 1667 return StepEvent.withError("Return date must be after departure date"); 1668 } 1669 1670 context.setContextValue("travelBasics", input); 1671 1672 // Check if it's a popular destination with package deals 1673 List<TravelPackage> packages = packageService.findPackages( 1674 input.getOrigin(), 1675 input.getDestination(), 1676 input.getDepartureDate(), 1677 input.getReturnDate(), 1678 input.getTravelerCount() 1679 ); 1680 1681 if (!packages.isEmpty()) { 1682 PackageSelectionInput packageInput = new PackageSelectionInput(); 1683 packageInput.setPackages(packages); 1684 packageInput.setAllowCustom(true); 1685 1686 return StepEvent.of(packageInput, PackageSelectionInput.class); 1687 } 1688 1689 // No packages, proceed with custom planning 1690 return StepEvent.of(new FlightPreferenceInput(), FlightPreferenceInput.class); 1691 } 1692 1693 @WorkflowStep( 1694 index = 2, 1695 inputClass = FlightPreferenceInput.class, 1696 description = "Flight preferences", 1697 async = true 1698 ) 1699 public AsyncTaskEvent searchFlights(FlightPreferenceInput input, WorkflowContext context) { 1700 TravelBasicsInput basics = context.getContextValue("travelBasics", TravelBasicsInput.class); 1701 1702 return AsyncTaskEvent.builder() 1703 .taskName("flightSearch") 1704 .taskArgs(Map.of( 1705 "origin", basics.getOrigin(), 1706 "destination", basics.getDestination(), 1707 "departureDate", basics.getDepartureDate(), 1708 "returnDate", basics.getReturnDate(), 1709 "travelers", basics.getTravelerCount(), 1710 "preferences", input 1711 )) 1712 .messageId("searching_flights") 1713 .nextInputSchema(getSchemaFromClass(FlightSelectionInput.class)) 1714 .build(); 1715 } 1716 1717 @AsyncStep(forStep = "flightSearch") 1718 public StepEvent performFlightSearch(Map<String, Object> taskArgs, WorkflowContext context) { 1719 try { 1720 // Search flights with progress updates 1721 updateProgress(0, "Searching flights..."); 1722 1723 FlightSearchCriteria criteria = buildSearchCriteria(taskArgs); 1724 List<FlightOption> flights = flightService.searchFlights(criteria); 1725 1726 updateProgress(50, "Found " + flights.size() + " flights"); 1727 1728 // Sort by price and duration 1729 flights.sort(Comparator 1730 .comparing(FlightOption::getTotalPrice) 1731 .thenComparing(FlightOption::getTotalDuration)); 1732 1733 updateProgress(75, "Analyzing best options..."); 1734 1735 // Get top 5 options 1736 List<FlightOption> topFlights = flights.stream() 1737 .limit(5) 1738 .collect(Collectors.toList()); 1739 1740 context.setContextValue("flightOptions", topFlights); 1741 1742 FlightSelectionInput selection = new FlightSelectionInput(); 1743 selection.setFlights(topFlights); 1744 1745 return StepEvent.builder() 1746 .completed(true) 1747 .percentComplete(100) 1748 .data(selection) 1749 .build(); 1750 1751 } catch (Exception e) { 1752 return StepEvent.withError("Flight search failed: " + e.getMessage()); 1753 } 1754 } 1755 1756 @WorkflowStep( 1757 index = 3, 1758 inputClass = FlightSelectionInput.class, 1759 description = "Select your flights" 1760 ) 1761 public StepEvent selectFlights(FlightSelectionInput input, WorkflowContext context) { 1762 FlightOption selected = input.getSelectedFlight(); 1763 context.setContextValue("selectedFlight", selected); 1764 1765 // Calculate hotel search parameters based on flight times 1766 LocalDateTime arrivalTime = selected.getOutboundArrival(); 1767 LocalDateTime departureTime = selected.getReturnDeparture(); 1768 1769 HotelSearchInput hotelSearch = new HotelSearchInput(); 1770 hotelSearch.setCheckIn(arrivalTime.toLocalDate()); 1771 hotelSearch.setCheckOut(departureTime.toLocalDate()); 1772 hotelSearch.setLocation(selected.getDestinationCity()); 1773 1774 return StepEvent.of(hotelSearch, HotelPreferenceInput.class); 1775 } 1776 1777 @WorkflowStep( 1778 index = 4, 1779 inputClass = HotelPreferenceInput.class, 1780 description = "Hotel preferences" 1781 ) 1782 public StepEvent searchHotels(HotelPreferenceInput input, WorkflowContext context) { 1783 HotelSearchInput searchParams = context.getContextValue("hotelSearch", HotelSearchInput.class); 1784 1785 List<Hotel> hotels = hotelService.searchHotels( 1786 searchParams.getLocation(), 1787 searchParams.getCheckIn(), 1788 searchParams.getCheckOut(), 1789 input 1790 ); 1791 1792 // Filter by preferences 1793 hotels = hotels.stream() 1794 .filter(h -> h.getStarRating() >= input.getMinStarRating()) 1795 .filter(h -> h.getPricePerNight().compareTo(input.getMaxPricePerNight()) <= 0) 1796 .sorted(Comparator.comparing(Hotel::getGuestRating).reversed()) 1797 .limit(5) 1798 .collect(Collectors.toList()); 1799 1800 HotelSelectionInput selection = new HotelSelectionInput(); 1801 selection.setHotels(hotels); 1802 1803 return StepEvent.of(selection, HotelSelectionInput.class); 1804 } 1805 1806 @WorkflowStep( 1807 index = 5, 1808 inputClass = ActivityPreferenceInput.class, 1809 description = "What activities interest you?" 1810 ) 1811 public StepEvent suggestActivities(ActivityPreferenceInput input, WorkflowContext context) { 1812 TravelBasicsInput basics = context.getContextValue("travelBasics", TravelBasicsInput.class); 1813 1814 List<Activity> activities = activityService.findActivities( 1815 basics.getDestination(), 1816 input.getInterests(), 1817 input.getActivityLevel(), 1818 input.getBudgetPerDay() 1819 ); 1820 1821 // Group by day 1822 Map<LocalDate, List<Activity>> dailyActivities = groupActivitiesByDay( 1823 activities, 1824 basics.getDepartureDate(), 1825 basics.getReturnDate() 1826 ); 1827 1828 ItineraryDraft draft = new ItineraryDraft(); 1829 draft.setDailyActivities(dailyActivities); 1830 draft.setFlight(context.getContextValue("selectedFlight", FlightOption.class)); 1831 draft.setHotel(context.getContextValue("selectedHotel", Hotel.class)); 1832 1833 return StepEvent.of(draft, ItineraryConfirmationInput.class); 1834 } 1835 1836 @WorkflowStep( 1837 index = 6, 1838 inputClass = ItineraryConfirmationInput.class, 1839 description = "Review and confirm your itinerary" 1840 ) 1841 public StepEvent confirmItinerary(ItineraryConfirmationInput input, WorkflowContext context) { 1842 if (!input.isConfirmed()) { 1843 return StepEvent.withMessage("No problem! Let me know if you'd like to plan another trip."); 1844 } 1845 1846 // Create final itinerary 1847 Itinerary itinerary = itineraryService.createItinerary(context); 1848 1849 // Generate booking links 1850 BookingLinks links = bookingService.generateBookingLinks(itinerary); 1851 1852 // Create calendar events 1853 String calendarFile = calendarService.exportItinerary(itinerary); 1854 1855 return StepEvent.builder() 1856 .completed(true) 1857 .properties(Map.of( 1858 "itineraryId", itinerary.getId(), 1859 "totalCost", itinerary.getTotalCost(), 1860 "bookingLinks", links, 1861 "calendarFile", calendarFile, 1862 "message", "Your travel itinerary is ready! Check your email for booking links and calendar invites." 1863 )) 1864 .build(); 1865 } 1866 } 1867 ``` 1868 1869 ### 5. HR Onboarding Assistant 1870 1871 This example demonstrates an employee onboarding workflow with document collection, training scheduling, and equipment setup. 1872 1873 ```java 1874 @Component 1875 public class HROnboardingWorkflow extends AnnotatedWorkflow { 1876 1877 @Autowired 1878 private EmployeeService employeeService; 1879 1880 @Autowired 1881 private DocumentService documentService; 1882 1883 @Autowired 1884 private TrainingService trainingService; 1885 1886 @Autowired 1887 private ITService itService; 1888 1889 @Autowired 1890 private FacilitiesService facilitiesService; 1891 1892 @Override 1893 public String getWorkflowId() { 1894 return "hr-onboarding"; 1895 } 1896 1897 @WorkflowStep( 1898 index = 1, 1899 inputClass = NewEmployeeInput.class, 1900 description = "Welcome! Let's get your information" 1901 ) 1902 public StepEvent collectEmployeeInfo(NewEmployeeInput input, WorkflowContext context) { 1903 // Validate employee ID 1904 Employee employee = employeeService.findByEmail(input.getEmail()); 1905 if (employee == null) { 1906 return StepEvent.withError("Employee record not found. Please contact HR."); 1907 } 1908 1909 context.setContextValue("employee", employee); 1910 context.setContextValue("startDate", input.getStartDate()); 1911 1912 // Get required documents for role 1913 List<DocumentRequirement> requiredDocs = documentService.getRequiredDocuments( 1914 employee.getRole(), 1915 employee.getDepartment(), 1916 employee.getLocation() 1917 ); 1918 1919 DocumentCollectionInput docInput = new DocumentCollectionInput(); 1920 docInput.setRequiredDocuments(requiredDocs); 1921 docInput.setEmployeeName(employee.getFullName()); 1922 1923 return StepEvent.of(docInput, DocumentCollectionInput.class); 1924 } 1925 1926 @WorkflowStep( 1927 index = 2, 1928 inputClass = DocumentCollectionInput.class, 1929 description = "Upload required documents", 1930 async = true 1931 ) 1932 public AsyncTaskEvent processDocuments(DocumentCollectionInput input, WorkflowContext context) { 1933 return AsyncTaskEvent.builder() 1934 .taskName("documentProcessing") 1935 .taskArgs(Map.of( 1936 "documents", input.getUploadedDocuments(), 1937 "employeeId", context.getContextValue("employee", Employee.class).getId() 1938 )) 1939 .messageId("processing_documents") 1940 .nextInputSchema(getSchemaFromClass(EmergencyContactInput.class)) 1941 .build(); 1942 } 1943 1944 @AsyncStep(forStep = "documentProcessing") 1945 public StepEvent verifyDocuments(Map<String, Object> taskArgs, WorkflowContext context) { 1946 List<UploadedDocument> documents = (List<UploadedDocument>) taskArgs.get("documents"); 1947 String employeeId = (String) taskArgs.get("employeeId"); 1948 1949 updateProgress(0, "Verifying documents..."); 1950 1951 List<DocumentVerificationResult> results = new ArrayList<>(); 1952 int processed = 0; 1953 1954 for (UploadedDocument doc : documents) { 1955 DocumentVerificationResult result = documentService.verifyDocument(doc); 1956 results.add(result); 1957 1958 processed++; 1959 int progress = (processed * 100) / documents.size(); 1960 updateProgress(progress, "Verified " + doc.getType()); 1961 1962 if (!result.isValid()) { 1963 return StepEvent.withError( 1964 "Document verification failed for " + doc.getType() + ": " + result.getErrorMessage() 1965 ); 1966 } 1967 } 1968 1969 // Store verified documents 1970 documentService.storeEmployeeDocuments(employeeId, documents); 1971 context.setContextValue("documentsVerified", true); 1972 1973 return StepEvent.builder() 1974 .completed(true) 1975 .percentComplete(100) 1976 .build(); 1977 } 1978 1979 @WorkflowStep( 1980 index = 3, 1981 inputClass = EmergencyContactInput.class, 1982 description = "Emergency contact information" 1983 ) 1984 public StepEvent collectEmergencyContacts(EmergencyContactInput input, WorkflowContext context) { 1985 Employee employee = context.getContextValue("employee", Employee.class); 1986 1987 // Save emergency contacts 1988 employeeService.updateEmergencyContacts(employee.getId(), input.getContacts()); 1989 1990 // Determine required training based on role 1991 List<TrainingModule> requiredTraining = trainingService.getRequiredTraining( 1992 employee.getRole(), 1993 employee.getDepartment(), 1994 employee.getLevel() 1995 ); 1996 1997 TrainingScheduleInput trainingInput = new TrainingScheduleInput(); 1998 trainingInput.setModules(requiredTraining); 1999 trainingInput.setStartDate(context.getContextValue("startDate", LocalDate.class)); 2000 trainingInput.setPreferredTimes(employee.getPreferredTrainingTimes()); 2001 2002 return StepEvent.of(trainingInput, TrainingScheduleInput.class); 2003 } 2004 2005 @WorkflowStep( 2006 index = 4, 2007 inputClass = TrainingScheduleInput.class, 2008 description = "Schedule your training" 2009 ) 2010 public StepEvent scheduleTraining(TrainingScheduleInput input, WorkflowContext context) { 2011 Employee employee = context.getContextValue("employee", Employee.class); 2012 2013 // Create training schedule 2014 TrainingSchedule schedule = trainingService.createSchedule( 2015 employee.getId(), 2016 input.getSelectedModules(), 2017 input.getSchedulePreference() 2018 ); 2019 2020 context.setContextValue("trainingSchedule", schedule); 2021 2022 // Prepare equipment request 2023 EquipmentRequestInput equipmentInput = new EquipmentRequestInput(); 2024 equipmentInput.setRole(employee.getRole()); 2025 equipmentInput.setDepartment(employee.getDepartment()); 2026 equipmentInput.setStandardPackages(itService.getStandardPackages(employee.getRole())); 2027 2028 return StepEvent.of(equipmentInput, EquipmentRequestInput.class); 2029 } 2030 2031 @WorkflowStep( 2032 index = 5, 2033 inputClass = EquipmentRequestInput.class, 2034 description = "Select equipment and workspace" 2035 ) 2036 public StepEvent setupWorkspace(EquipmentRequestInput input, WorkflowContext context) { 2037 Employee employee = context.getContextValue("employee", Employee.class); 2038 LocalDate startDate = context.getContextValue("startDate", LocalDate.class); 2039 2040 // Create IT ticket for equipment 2041 ITTicket equipmentTicket = itService.createEquipmentRequest( 2042 employee, 2043 input.getSelectedPackage(), 2044 input.getAdditionalItems(), 2045 startDate 2046 ); 2047 2048 // Assign workspace 2049 WorkspaceAssignment workspace = facilitiesService.assignWorkspace( 2050 employee, 2051 input.getWorkspacePreference() 2052 ); 2053 2054 context.setContextValue("equipmentTicket", equipmentTicket); 2055 context.setContextValue("workspace", workspace); 2056 2057 // Create onboarding summary 2058 OnboardingSummary summary = buildOnboardingSummary(context); 2059 2060 return StepEvent.of(summary, OnboardingConfirmationInput.class); 2061 } 2062 2063 @WorkflowStep( 2064 index = 6, 2065 inputClass = OnboardingConfirmationInput.class, 2066 description = "Review onboarding checklist", 2067 async = true 2068 ) 2069 public AsyncTaskEvent finalizeOnboarding(OnboardingConfirmationInput input, WorkflowContext context) { 2070 if (!input.isConfirmed()) { 2071 return AsyncTaskEvent.builder() 2072 .taskName("onboardingIncomplete") 2073 .messageId("onboarding_incomplete") 2074 .build(); 2075 } 2076 2077 return AsyncTaskEvent.builder() 2078 .taskName("completeOnboarding") 2079 .taskArgs(Map.of("context", context.getAllValues())) 2080 .messageId("completing_onboarding") 2081 .build(); 2082 } 2083 2084 @AsyncStep(forStep = "completeOnboarding") 2085 public StepEvent completeOnboardingProcess(Map<String, Object> taskArgs, WorkflowContext context) { 2086 try { 2087 Employee employee = context.getContextValue("employee", Employee.class); 2088 2089 // Send notifications to relevant parties 2090 notificationService.notifyManager(employee.getManagerId(), employee); 2091 notificationService.notifyIT(context.getContextValue("equipmentTicket", ITTicket.class)); 2092 notificationService.notifyFacilities(context.getContextValue("workspace", WorkspaceAssignment.class)); 2093 notificationService.notifyTraining(context.getContextValue("trainingSchedule", TrainingSchedule.class)); 2094 2095 // Create first day agenda 2096 FirstDayAgenda agenda = onboardingService.createFirstDayAgenda(employee, context); 2097 2098 // Send welcome package 2099 emailService.sendWelcomePackage(employee, agenda); 2100 2101 return StepEvent.builder() 2102 .completed(true) 2103 .percentComplete(100) 2104 .properties(Map.of( 2105 "employeeId", employee.getId(), 2106 "message", "Onboarding completed successfully! Welcome package sent to " + employee.getEmail(), 2107 "firstDayAgenda", agenda 2108 )) 2109 .build(); 2110 2111 } catch (Exception e) { 2112 log.error("Failed to complete onboarding", e); 2113 return StepEvent.withError("Failed to complete onboarding: " + e.getMessage()); 2114 } 2115 } 2116 2117 private OnboardingSummary buildOnboardingSummary(WorkflowContext context) { 2118 OnboardingSummary summary = new OnboardingSummary(); 2119 2120 Employee employee = context.getContextValue("employee", Employee.class); 2121 summary.setEmployeeName(employee.getFullName()); 2122 summary.setStartDate(context.getContextValue("startDate", LocalDate.class)); 2123 summary.setDocumentsVerified(context.getContextValue("documentsVerified", Boolean.class)); 2124 summary.setTrainingSchedule(context.getContextValue("trainingSchedule", TrainingSchedule.class)); 2125 summary.setEquipmentTicket(context.getContextValue("equipmentTicket", ITTicket.class)); 2126 summary.setWorkspace(context.getContextValue("workspace", WorkspaceAssignment.class)); 2127 2128 // Generate checklist 2129 summary.setChecklist(onboardingService.generateChecklist(employee)); 2130 2131 return summary; 2132 } 2133 } 2134 ``` 2135 2136 --- 2137 2138 **Built with β€οΈ by the DriftKit team**