task-completion-text.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { FmodeChatCompletion } from 'fmode-ng';
  2. import { FlowTask, FlowTaskOptions } from '../../flow.task';
  3. import { Subject } from 'rxjs';
  4. import { takeUntil } from 'rxjs/operators';
  5. export interface TextCompletionOptions extends FlowTaskOptions {
  6. promptTemplate: string; // Required: Your prompt template with {{variables}}
  7. outputProperty: string; // Required: Where to store the result in task.data
  8. inputVariables?: string[]; // Optional: List of required variables (for validation)
  9. modelOptions?: Record<string, any>; // Optional: Model configuration
  10. strictValidation?: boolean; // Optional: Whether to fail on missing variables
  11. }
  12. export class TaskCompletionText extends FlowTask {
  13. promptTemplate: string;
  14. outputProperty: string;
  15. inputVariables: string[];
  16. strictPromptValidation: boolean = false;
  17. modelOptions: Record<string, any>;
  18. strictValidation: boolean;
  19. destroy$ = new Subject<void>();
  20. constructor(options: TextCompletionOptions) {
  21. super({
  22. title: options.title || 'Text Generation Task',
  23. output: options.output,
  24. initialData: options.initialData
  25. });
  26. if (!options.promptTemplate) throw new Error('promptTemplate is required');
  27. if (!options.outputProperty) throw new Error('outputProperty is required');
  28. this.promptTemplate = options.promptTemplate;
  29. this.outputProperty = options.outputProperty;
  30. this.inputVariables = options.inputVariables || this.extractPromptVariables();
  31. this.modelOptions = options.modelOptions || {};
  32. this.strictValidation = options.strictValidation ?? true;
  33. }
  34. override async handle(): Promise<void> {
  35. // 1. Validate all required prompt variables exist in task.data
  36. this.validatePromptVariables();
  37. // 2. Prepare the prompt with variable substitution
  38. const fullPrompt = this.renderPromptTemplate();
  39. // 3. Call the LLM for text completion
  40. await this.callModelCompletion(fullPrompt);
  41. }
  42. validatePromptVariables(): void {
  43. const requiredVariables = this.extractPromptVariables();
  44. const missingVariables: string[] = [];
  45. const undefinedVariables: string[] = [];
  46. requiredVariables.forEach(variable => {
  47. if (!(variable in this.data)) {
  48. missingVariables.push(variable);
  49. } else if (this.data[variable] === undefined) {
  50. undefinedVariables.push(variable);
  51. }
  52. });
  53. const errors: string[] = [];
  54. if (missingVariables.length > 0) {
  55. errors.push(`Missing required variables in task.data: ${missingVariables.join(', ')}`);
  56. }
  57. if (undefinedVariables.length > 0) {
  58. errors.push(`Variables with undefined values: ${undefinedVariables.join(', ')}`);
  59. }
  60. if (errors.length > 0 && this.strictPromptValidation) {
  61. throw new Error(`Prompt variable validation failed:\n${errors.join('\n')}`);
  62. } else if (errors.length > 0) {
  63. console.warn(`Prompt variable warnings:\n${errors.join('\n')}`);
  64. }
  65. }
  66. extractPromptVariables(): string[] {
  67. const matches = this.promptTemplate.match(/\{\{\w+\}\}/g) || [];
  68. const uniqueVariables = new Set<string>();
  69. matches.forEach(match => {
  70. const key = match.replace(/\{\{|\}\}/g, '');
  71. uniqueVariables.add(key);
  72. });
  73. return Array.from(uniqueVariables);
  74. }
  75. renderPromptTemplate(): string {
  76. let result = this.promptTemplate;
  77. const variables = this.extractPromptVariables();
  78. variables.forEach(variable => {
  79. if (this.data[variable] !== undefined) {
  80. result = result.replace(new RegExp(`\\{\\{${variable}\\}\\}`, 'g'), this.data[variable]);
  81. }
  82. });
  83. return result;
  84. }
  85. async callModelCompletion(prompt: string): Promise<void> {
  86. return new Promise((resolve, reject) => {
  87. const messages = [{
  88. role: "user",
  89. content: prompt
  90. }];
  91. const completion = new FmodeChatCompletion(messages);
  92. let accumulatedContent = '';
  93. completion.sendCompletion({
  94. ...this.modelOptions,
  95. onComplete: (message: any) => {
  96. console.log("onComplete", message);
  97. }
  98. })
  99. .pipe(takeUntil(this.destroy$))
  100. .subscribe({
  101. next: (message: any) => {
  102. if (message.content && typeof message.content === 'string') {
  103. accumulatedContent = message.content;
  104. this.setProgress(0.3 + (accumulatedContent.length / 1000) * 0.7);
  105. }
  106. if (message.complete) {
  107. try {
  108. // Store the complete generated text in the specified property
  109. this.updateData(this.outputProperty, accumulatedContent);
  110. this.setProgress(1);
  111. resolve();
  112. } catch (error) {
  113. this.handleError(error as Error);
  114. reject(error);
  115. }
  116. }
  117. },
  118. error: (error) => {
  119. this.handleError(error);
  120. reject(error);
  121. }
  122. });
  123. });
  124. }
  125. handleError(error: Error): void {
  126. this.updateData('error', {
  127. message: error.message,
  128. stack: error.stack,
  129. timestamp: new Date().toISOString()
  130. });
  131. this._status = 'failed';
  132. }
  133. onDestroy(): void {
  134. this.destroy$.next();
  135. this.destroy$.complete();
  136. }
  137. }