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