GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALAI.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Linq;
20using System.Net.Http;
22using static GenerallyPositive.Enums;
23
24namespace GenerallyPositive
25{
27 {
28 public string[] Labels { get; set; }
29 public string PromptTemplate { get; set; }
31 //public double? ConfidenceThreshold { get; set; }
32 public int? MaxWords { get; set; }
33 public bool? IncludeKeywords { get; set; }
34 public Dictionary<string, object> AdditionalParameters { get; set; } = new Dictionary<string, object>();
35 }
36 public delegate int AIResponseDelegate(IGPALAI ai, IGPALGrid<string> results);
37 public class GPALAI : IGPALAI
38 {
39 private static readonly Dictionary<AIClassificationType, ClassificationConfig> ClassificationConfigs = new Dictionary<AIClassificationType, ClassificationConfig>
40 {
41 { AIClassificationType.Sentiment, new ClassificationConfig { Labels = new[] { "Strong Positive", "Weak Positive", "Neutral", "Weak Negative", "Strong Negative" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
42 { AIClassificationType.Spam, new ClassificationConfig { Labels = new[] { "Spam", "Not Spam" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
43 { AIClassificationType.Topic, new ClassificationConfig { Labels = new[] { "Technology", "Politics", "Sports", "Other" }, PromptTemplate = "Classify {0} into {1}", MaxWords = 100 } },
44 { AIClassificationType.Intent, new ClassificationConfig { Labels = new[] { "Request", "Complaint", "Inquiry", "Moderate", "Other" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
45 { AIClassificationType.Emotion, new ClassificationConfig { Labels = new[] { "Happy", "Sad", "Angry", "Neutral" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
46 { AIClassificationType.Toxicity, new ClassificationConfig { Labels = new[] { "Toxic", "Non-Toxic" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
47 { AIClassificationType.Language, new ClassificationConfig { Labels = new[] { "English", "Spanish", "French", "Other" }, PromptTemplate = "Identify the language of {0} as {1}", MaxWords = 100 } },
48 { AIClassificationType.Category, new ClassificationConfig { Labels = new[] { "General", "Specific", "Miscellaneous" }, PromptTemplate = "Classify {0} into {1}", MaxWords = 100 } },
49 { AIClassificationType.Priority, new ClassificationConfig { Labels = new[] { "High", "Medium", "Low", "Urgent" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
50 { AIClassificationType.Relevance, new ClassificationConfig { Labels = new[] { "Relevant", "Irrelevant" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
51 { AIClassificationType.Tone, new ClassificationConfig { Labels = new[] { "Formal", "Informal", "Sarcastic", "Humorous" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
52 { AIClassificationType.Urgency, new ClassificationConfig { Labels = new[] { "Urgent", "Non-Urgent" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
53 { AIClassificationType.IntentComplexity, new ClassificationConfig { Labels = new[] { "Simple", "Complex" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
54 { AIClassificationType.Risk, new ClassificationConfig { Labels = new[] { "High Risk", "Low Risk", "Acceptable" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
55 { AIClassificationType.ContentType, new ClassificationConfig { Labels = new[] { "News", "Opinion", "Advertisement", "Technical" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
56 { AIClassificationType.Engagement, new ClassificationConfig { Labels = new[] { "High Engagement", "Low Engagement" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
57 { AIClassificationType.Authenticity, new ClassificationConfig { Labels = new[] { "Authentic", "Fake" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
58 { AIClassificationType.LanguageProficiency, new ClassificationConfig { Labels = new[] { "Beginner", "Intermediate", "Advanced" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
59 { AIClassificationType.SentimentConfidence, new ClassificationConfig { Labels = new[] { "High Confidence", "Low Confidence" }, PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } },
60 { AIClassificationType.Custom, new ClassificationConfig { PromptTemplate = "Classify {0} as {1}", MaxWords = 100 } }
61 };
62
63 private AIProviderType? _provider;
64 private AIModel? _model;
65 private ICredentials _credentials;
66 private AITask _task;
67 private AIClassificationType? _classificationType;
68 private List<ClassificationConfig> _configs = new List<ClassificationConfig>();
69 private List<object> _inputSources = new List<object>();
70 private List<object> _outputTargets = new List<object>();
71 private AIResponseDelegate _callback;
72 private GPALForm.GPALStatusStrip _statusStrip;
73 private AIProvider _aiProvider;
74 private bool _isExecuted;
75
76 internal GPALAI() { }
77
78 public IGPALAI ToGPALObject() => this;
79
80 public IAllowAITask WithProvider(AIProviderType provider)
81 {
82 if (_provider.HasValue)
83 {
84 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Provider already set and cannot be changed.", this, GPALObjectType.GPALAI);
85 return this;
86 }
87 _provider = provider;
88 return this;
89 }
90
91 public IAllowAITask WithModel(AIModel model)
92 {
93 _model = model;
94 return this;
95 }
96
97 public IAllowAITask WithCredentials(ICredentials credentials)
98 {
99 if (credentials == null)
100 {
101 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Credentials is null.", this, GPALObjectType.GPALAI);
102 return this;
103 }
104 _credentials = credentials;
105 _aiProvider = null;
106 return this;
107 }
108
109 public IAllowAITask WithCredentials(string staticApiKey) =>
110 WithCredentials(GPAL.CredentialsFor(CredentialServiceType.StaticKey).WithServiceKey(staticApiKey).ToGPALObject());
111
112 public IAllowAIConfig WithTask(AITask task)
113 {
114 if (_isExecuted)
115 {
116 _task = default;
117 _isExecuted = false;
118 }
119 _task = task;
120 return this;
121 }
122
123 public IAllowAIConfig WithTaskConfig(ClassificationConfig config)
124 {
125 if (_isExecuted)
126 {
127 _configs.Clear();
128 _isExecuted = false;
129 }
130 if (config == null)
131 {
132 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Task config is null.", this, GPALObjectType.GPALAI);
133 return this;
134 }
135 _configs.Add(config);
136 return this;
137 }
138
139 public IAllowAIConfig WithClassificationType(AIClassificationType type)
140 {
141 if (_isExecuted)
142 {
143 _classificationType = null;
144 _isExecuted = false;
145 }
146 if (_task != AITask.Classification)
147 {
148 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "WithClassificationType is only valid for Classification task.", this, GPALObjectType.GPALAI);
149 return this;
150 }
151 _classificationType = type;
152 return this;
153 }
154
155 public IAllowAIConfig WithCustomClassification(ClassificationConfig config)
156 {
157 if (_isExecuted)
158 {
159 _configs.Clear();
160 _isExecuted = false;
161 }
162 if (_task != AITask.Classification || _classificationType != AIClassificationType.Custom)
163 {
164 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "WithCustomClassification requires Classification task and Custom type.", this, GPALObjectType.GPALAI);
165 return this;
166 }
167 if (config == null || config.Labels == null || config.PromptTemplate == null)
168 {
169 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Custom classification requires Labels and PromptTemplate.", this, GPALObjectType.GPALAI);
170 return this;
171 }
172 _configs.Add(config);
173 return this;
174 }
175
176 public IAllowAIOutput WithLiveInputFrom(GPALForm.GPALInput input, int debounceMilliseconds = 500)
177 {
178 WithInputFrom(input);
179 WireLiveCallback(input, debounceMilliseconds);
180 return this;
181 }
182
183 public IAllowAIOutput WithLiveInputFrom(GPALForm.GPALTextArea textArea, int debounceMilliseconds = 500)
184 {
185 WithInputFrom(textArea);
186 WireLiveCallback(textArea, debounceMilliseconds);
187 return this;
188 }
189
190 private void WireLiveCallback(GPALForm.GPALControl control, int debounceMilliseconds)
191 {
192 var timer = new System.Windows.Forms.Timer { Interval = Math.Max(1, debounceMilliseconds) };
193 timer.Tick += (s, e) =>
194 {
195 timer.Stop();
196 Execute();
197 };
198 control.WithCallback(new EventHandler((s, e) =>
199 {
200 timer.Stop();
201 timer.Start();
202 }));
203 }
204
205 public IAllowAIOutput WithInputFrom(object source)
206 {
207 if (_isExecuted)
208 {
209 _inputSources.Clear();
210 _isExecuted = false;
211 }
212 if (source == null)
213 {
214 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Input source cannot be null.", this, GPALObjectType.GPALAI);
215 return this;
216 }
217 if (!(source is GPALForm.GPALInput || source is GPALForm.GPALTextArea || source is IGPALGrid<string> ||
218 source is GPALFile || source is GPALDatabase || source is string || source is Uri))
219 {
220 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unsupported input source type.", this, GPALObjectType.GPALAI);
221 return this;
222 }
223 _inputSources.Add(source);
224 return this;
225 }
226
227 public IAllowAIExecution WithOutputTo(out IGPALGrid<string> target)
228 {
229 if (_isExecuted)
230 {
231 _outputTargets.Clear();
232 _isExecuted = false;
233 }
234 target = new GPALGrid<string>();
235 _outputTargets.Add(target);
236 return this;
237 }
238
239 public IAllowAIExecution WithOutputTo(object target)
240 {
241 if (_isExecuted)
242 {
243 _outputTargets.Clear();
244 _isExecuted = false;
245 }
246 if (target == null)
247 {
248 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Output target cannot be null.", this, GPALObjectType.GPALAI);
249 return this;
250 }
251 if (!(target is GPALForm.GPALTextArea || target is GPALForm.GPALRichTextBox || target is GPALDatabase || target is IGPALGrid<string> || target is GPALFile))
252 {
253 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unsupported output target type.", this, GPALObjectType.GPALAI);
254 return this;
255 }
256 _outputTargets.Add(target);
257 return this;
258 }
259
260 public IAllowAIConfig CallAfterAIResponse(AIResponseDelegate callback)
261 {
262 if (callback == null)
263 {
264 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Callback is null.", this, GPALObjectType.GPALAI);
265 return this;
266 }
267 _callback = callback;
268 return this;
269 }
270
271 public IAllowAIExecution WithStatusTo(GPALForm.GPALStatusStrip statusStrip)
272 {
273 _statusStrip = statusStrip;
274 return this;
275 }
276
281 private void SetStatus(string text)
282 {
283 if (null == _statusStrip) return;
284 _statusStrip.Text = text;
285 System.Windows.Forms.Application.DoEvents();
286 }
287
288 public IAllowAIConfig Execute()
289 {
290 if (!_provider.HasValue)
291 {
292 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Provider must be set before execution.", this, GPALObjectType.GPALAI);
293 return this;
294 }
295 ExecuteInternal(false);
296 _isExecuted = true;
297 return this;
298 }
299
300 private void ExecuteInternal(bool isInteractive)
301 {
302 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Starting AI task for [{_provider}][{_model}][{_task}]", this, GPALObjectType.GPALAI);
303 IGPALGrid<string> results = new GPALGrid<string>();
304 try
305 {
306 _task = _task == default ? AITask.Classification : _task;
307 _classificationType = _classificationType ?? AIClassificationType.Custom;
308 if (!_inputSources.Any())
309 {
310 _inputSources.Add("");
311 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Using default empty input.", this, GPALObjectType.GPALAI);
312 }
313 if (!_outputTargets.Any())
314 {
315 _outputTargets.Add(results);
316 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Using default grid output.", this, GPALObjectType.GPALAI);
317 }
318
319 var mergedConfig = MergeConfigs();
320 if (_model.HasValue)
321 mergedConfig.AdditionalParameters["Model"] = _model.Value.ToString();
322
323 if (_aiProvider == null || _credentials != null)
324 {
325 _aiProvider = new AIProvider(_provider.Value, new RESTClient(), _credentials, mergedConfig);
326 }
327
328 string promptTemplate = GeneratePromptTemplate(mergedConfig);
329 if (string.IsNullOrEmpty(promptTemplate))
330 {
331 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Failed to generate prompt template.", this, GPALObjectType.GPALAI);
332 return;
333 }
334
335 List<string> inputs = ProcessInputs();
336 if (!inputs.Any())
337 {
338 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "No valid inputs provided.", this, GPALObjectType.GPALAI);
339 return;
340 }
341
342 foreach (var input in inputs)
343 {
344 string prompt = string.Format(promptTemplate, input, mergedConfig.Labels?.Length > 0 ? string.Join(", ", mergedConfig.Labels) : "");
345
346 SetStatus("Sending request...");
347 SetStatus("Waiting for response...");
348 string result = _aiProvider.Call(prompt);
349
350 if (!string.IsNullOrEmpty(result))
351 {
352 results.AddRow(new List<string> { result });
353 SetStatus("Response received");
354 }
355 else
356 {
357 SetStatus("Error: no response from AI provider - see log for details");
358 }
359 }
360
361 foreach (var target in _outputTargets)
362 {
363 if (target is IGPALGrid<string> grid)
364 {
365 foreach (var row in results)
366 grid.AddRow(row);
367 }
368 else if (target is GPALFile file)
369 {
370 if (file.Filenames.Count == 0)
371 {
372 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "No filenames specified for GPALFile output. Skipping.", this, GPALObjectType.GPALAI);
373 continue;
374 }
375 GPAL.Converter.WithInput(results).AppendTo(file);
376 }
377 else if (target is GPALDatabase db)
378 {
379 if (string.IsNullOrEmpty(db.DatabaseSettings.TableName))
380 {
381 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "TableName must be set for GPALDatabase output.", this, GPALObjectType.GPALAI);
382 continue;
383 }
384 GPAL.Converter.WithInput(results).SaveTo(db);
385 }
386 else if (target is GPALForm.GPALTextArea textArea)
387 {
388 textArea.Text = string.Join("\n", results.Select(row => string.Join(" ", row)));
389 }
390 else if (target is GPALForm.GPALRichTextBox richTextBox)
391 {
392 richTextBox.Text = string.Join("\n", results.Select(row => string.Join(" ", row)));
393 }
394 else
395 {
396 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unsupported output target.", this, GPALObjectType.GPALAI);
397 }
398 }
399
400 if (_callback != null)
401 {
402 int callbackResult = _callback(this, results);
403 if (callbackResult == -1)
404 {
405 string str = "AI Callback requested application exit.";
406 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, this, GPALObjectType.GPALAI);
407 throw new GPALException(str);
408 }
409 }
410 }
411 catch (Exception ex)
412 {
413 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "AI execution failed.", this, GPALObjectType.GPALAI, ex);
414 SetStatus($"Error: {ex.Message}");
415 }
416 }
417
418 private ClassificationConfig MergeConfigs()
419 {
420 var merged = new ClassificationConfig();
421 if (_task == AITask.Classification && _classificationType.HasValue && ClassificationConfigs.TryGetValue(_classificationType.Value, out var defaultConfig))
422 {
423 merged.Labels = defaultConfig.Labels;
424 merged.PromptTemplate = defaultConfig.PromptTemplate;
425 merged.MaxWords = defaultConfig.MaxWords;
426 }
427
428 foreach (var config in _configs)
429 {
430 if (config.MaxWords.HasValue) merged.MaxWords = config.MaxWords;
431 if (config.IncludeKeywords.HasValue) merged.IncludeKeywords = config.IncludeKeywords;
432 if (config.Labels != null) merged.Labels = config.Labels;
433 if (config.PromptTemplate != null) merged.PromptTemplate = config.PromptTemplate;
434 if (config.AdditionalParameters != null)
435 {
436 merged.AdditionalParameters = new Dictionary<string, object>(config.AdditionalParameters);
437 }
438 }
439 return merged;
440 }
441
442 private string GeneratePromptTemplate(ClassificationConfig config)
443 {
444 switch (_task)
445 {
446 case AITask.Summarization:
447 return $"Summarize {{0}} in {config.MaxWords ?? 100} words{(config.IncludeKeywords == true ? ", including keywords" : "")}";
448 case AITask.Classification:
449 if (_classificationType == AIClassificationType.Custom && string.IsNullOrEmpty(config.PromptTemplate))
450 {
451 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Custom classification requires a PromptTemplate.", this, GPALObjectType.GPALAI);
452 return string.Empty;
453 }
454 return config.PromptTemplate ?? (ClassificationConfigs.TryGetValue(_classificationType ?? AIClassificationType.Custom, out var defaultConfig) ? defaultConfig.PromptTemplate : string.Empty);
455 case AITask.TextGeneration:
456 return config.PromptTemplate ?? "Generate text based on {0}";
457 case AITask.DataAugmentation:
458 return config.PromptTemplate ?? "Augment data for {0}";
459 default:
460 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unsupported AI task.", this, GPALObjectType.GPALAI);
461 return string.Empty;
462 }
463 }
464
465 private List<string> ProcessInputs()
466 {
467 var inputs = new List<string>();
468 try
469 {
470 foreach (var source in _inputSources)
471 {
472 switch (source)
473 {
474 case GPALForm.GPALInput input:
475 inputs.Add(input.Text);
476 break;
477 case GPALForm.GPALTextArea textArea:
478 inputs.Add(textArea.Text);
479 break;
480 case IGPALGrid<string> grid:
481 foreach (var row in grid) inputs.Add(string.Join(" ", row));
482 break;
483 case GPALFile file:
484 if (!FileHelper.TokenizeFile(file, null))
485 {
486 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Failed to tokenize file.", this, GPALObjectType.GPALAI);
487 continue;
488 }
489 foreach (var row in ((IGPALFileInternal)file).TokenList)
490 inputs.Add(string.Join(" ", row));
491 break;
492 case GPALDatabase database:
493 DatabaseHelper.TokenizeDatabase(null, database);
494 foreach (var row in database.Tokens)
495 inputs.Add(string.Join(" ", row));
496 break;
497 case string text:
498 inputs.Add(text);
499 break;
500 case Uri uri:
501 inputs.Add(FetchWebContent(uri));
502 break;
503 default:
504 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unsupported input source.", this, GPALObjectType.GPALAI);
505 continue;
506 }
507 }
508 }
509 catch (Exception ex)
510 {
511 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Failed to process inputs.", this, GPALObjectType.GPALAI, ex);
512 }
513 return inputs;
514 }
515
516 private string FetchWebContent(Uri uri)
517 {
518 try
519 {
520 using (var client = new HttpClient())
521 {
522 client.DefaultRequestHeaders.Add("User-Agent", BrowserHelper.GetUserAgent(GPAL.Browsers[0])); // NOTE: hardcoded value
523
524 string content = client.GetStringAsync(uri).GetAwaiter().GetResult();
525 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Fetched web content from [{uri}]", this, GPALObjectType.GPALAI);
526 return content;
527 }
528 }
529 catch (Exception ex)
530 {
531 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to fetch web content", this, GPALObjectType.GPALAI, ex);
532 return string.Empty;
533 }
534 }
535 }
536}
IAllowAIOutput WithLiveInputFrom(GPALForm.GPALInput input, int debounceMilliseconds=500)
Like WithInputFrom(object), but also wires the control's TextChanged event (debounced) to call Execut...
Definition GPALAI.cs:176
IAllowAIOutput WithLiveInputFrom(GPALForm.GPALTextArea textArea, int debounceMilliseconds=500)
Like WithInputFrom(object), but also wires the control's TextChanged event (debounced) to call Execut...
Definition GPALAI.cs:183
Base class for all GPAL form controls.
A GPAL single line input instantiated with GPAL.Input for use on GPAL forms. Callback EventHandler i...
A status bar (usually docked at the bottom of the form) that displays real-time messages,...
A GPAL multiline textare instantiated with GPAL.TextArea for use on GPAL forms. Callback EventHandle...
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568