GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
AIProvider.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.Text.Json;
20using static GenerallyPositive.Enums;
21
22namespace GenerallyPositive
23{
24 public class AIProvider
25 {
26 private readonly AIProviderType _providerType;
27 private readonly IRESTClient _restClient;
28 private readonly ICredentials _credentials;
29 private readonly ClassificationConfig _config;
30 private readonly AIProviderDefinition _definition;
31
32 // a 429 is the provider saying the account is over its rate limit for the moment, not that the call was
33 // wrong. Waiting the Retry-After out and asking again turns a burst into a slower run rather than a
34 // failed one. The wait is capped so a provider asking for an hour does not hang the workflow
35 private const int RATE_LIMIT_ATTEMPTS = 3;
36 private const int RATE_LIMIT_DEFAULT_WAIT = 5;
37 private const int RATE_LIMIT_MAX_WAIT = 60;
38
39 public AIProvider(AIProviderType providerType, IRESTClient restClient, ICredentials credentials, ClassificationConfig config)
40 {
41 _providerType = providerType;
42 _restClient = restClient ?? throw new ArgumentNullException(nameof(restClient));
43 _credentials = credentials;
44 _config = config ?? new ClassificationConfig();
46 _definition = AIProvidersConfig.Current.Find(providerType);
47 if (_definition == null)
48 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No provider definition found for [{providerType}]. Add it to AIProvidersConfig.yaml.", null, GPALObjectType.AIProvider);
49 }
50
51 public string Call(string prompt)
52 {
53 try
54 {
55 if (string.IsNullOrEmpty(prompt))
56 {
57 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Prompt cannot be empty.", this, GPALObjectType.AIProvider);
58 return string.Empty;
59 }
60
61 string endpoint = GetEndpoint();
62 string apiBase = GetApiBase();
63
64 string accessToken = null;
65 if (_credentials != null)
66 {
67 _credentials.FetchAccessToken(out accessToken);
68 if (string.IsNullOrEmpty(accessToken))
69 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to login using credentials. No access token granted.", this, GPALObjectType.AIProvider);
70 }
71
72 var body = BuildRequestBody(prompt);
73
74 string response = Send(apiBase, endpoint, accessToken, body);
75
76 int attempt = 1;
77
78 while (429 == _restClient.StatusCode && attempt < RATE_LIMIT_ATTEMPTS)
79 {
80 int wait = RateLimitWait();
81
82 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{_providerType}] is rate limiting. Waiting [{wait}] seconds, then attempt [{attempt + 1}] of [{RATE_LIMIT_ATTEMPTS}]", this, GPALObjectType.AIProvider);
83
84 System.Threading.Thread.Sleep(wait * 1000);
85
86 response = Send(apiBase, endpoint, accessToken, body);
87 attempt++;
88 }
89
90 if (429 == _restClient.StatusCode)
91 {
92 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"[{_providerType}] is still rate limiting after [{RATE_LIMIT_ATTEMPTS}] attempts.", this, GPALObjectType.AIProvider);
93 return string.Empty;
94 }
95
96 if (string.IsNullOrEmpty(response))
97 {
98 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Empty response from AI provider.", this, GPALObjectType.AIProvider);
99 return string.Empty;
100 }
101
102 string completion = ExtractCompletion(response);
103 if (string.IsNullOrEmpty(completion))
104 {
105 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to extract completion from [{_providerType}] response: [{response}]", this, GPALObjectType.AIProvider);
106 return string.Empty;
107 }
108
109 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Received response from [{_providerType}]", this, GPALObjectType.AIProvider);
110 return completion;
111 }
112 catch (Exception ex)
113 {
114 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to call AI provider", this, GPALObjectType.AIProvider, ex);
115 return string.Empty;
116 }
117 }
118
128 private string Send(string apiBase, string endpoint, string accessToken, Dictionary<string, object> body)
129 {
130 // NOTE: WithEndpoint() resets the client's headers, so headers must be added
131 // after WithAPIBase/WithEndpoint, not before.
132 var requestBuilder = (IAllowRESTParametersOrExecution)_restClient
133 .WithAPIBase(apiBase, System.Threading.Timeout.InfiniteTimeSpan)
134 .WithEndpoint(endpoint);
135
136 if (!string.IsNullOrEmpty(accessToken))
137 {
138 var headerName = _definition?.AuthHeader ?? "Authorization";
139 var prefix = _definition?.AuthPrefix ?? "Bearer ";
140 _restClient.WithHeader(headerName, string.IsNullOrEmpty(prefix) ? accessToken : $"{prefix}{accessToken}");
141 }
142
143 if (_definition?.AdditionalHeaders != null)
144 {
145 foreach (var h in _definition.AdditionalHeaders)
146 _restClient.WithHeader(h.Key, h.Value);
147 }
148
149 string response = requestBuilder
150 .WithParameters(body)
151 .Execute();
152
153 return response;
154 }
155
160 private int RateLimitWait()
161 {
162 int wait = _restClient.RetryAfterSeconds;
163
164 if (0 >= wait)
165 wait = RATE_LIMIT_DEFAULT_WAIT;
166
167 if (RATE_LIMIT_MAX_WAIT < wait)
168 wait = RATE_LIMIT_MAX_WAIT;
169
170 return wait;
171 }
172
173 private Dictionary<string, object> BuildRequestBody(string prompt)
174 {
175 var messages = new List<object> { new Dictionary<string, object> { ["role"] = "user", ["content"] = prompt } };
176
177 var body = new Dictionary<string, object>
178 {
179 ["model"] = GetModel(),
180 ["messages"] = messages
181 };
182
183 if (_definition?.ResponseFormat == "anthropic")
184 {
185 body["max_tokens"] = GetMaxTokens();
186 }
187
188 if (_config.AdditionalParameters != null)
189 {
190 foreach (var param in _config.AdditionalParameters)
191 {
192 if (IsReservedParameter(param.Key))
193 continue;
194 body[param.Key] = param.Value;
195 }
196 }
197
198 return body;
199 }
200
201 private static bool IsReservedParameter(string key)
202 {
203 return key == "Endpoint" || key == "Model" || key == "MaxTokens" || key == "AnthropicVersion";
204 }
205
206 private int GetMaxTokens()
207 {
208 if (_config?.AdditionalParameters?.ContainsKey("MaxTokens") == true
209 && int.TryParse(_config.AdditionalParameters["MaxTokens"].ToString(), out int maxTokens))
210 {
211 return maxTokens;
212 }
213 return 1024;
214 }
215
216 private string GetModel()
217 {
218 if (_config?.AdditionalParameters?.ContainsKey("Model") == true)
219 return _config.AdditionalParameters["Model"].ToString();
220 return _definition?.DefaultModel;
221 }
222
223 private string GetApiBase() => _definition?.BaseUrl;
224
225 private string GetEndpoint()
226 {
227 if (_config?.AdditionalParameters?.ContainsKey("Endpoint") == true)
228 return _config.AdditionalParameters["Endpoint"].ToString();
229 return _definition?.ChatEndpoint ?? "/chat/completions";
230 }
231
232 private string ExtractCompletion(string response)
233 {
234 using var doc = JsonDocument.Parse(response);
235 var root = doc.RootElement;
236
237 if (_definition?.ResponseFormat == "anthropic")
238 {
239 if (root.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array && content.GetArrayLength() > 0)
240 {
241 var first = content[0];
242 if (first.TryGetProperty("text", out var text))
243 return text.GetString();
244 }
245 return null;
246 }
247
248 if (root.TryGetProperty("choices", out var choices) && choices.ValueKind == JsonValueKind.Array && choices.GetArrayLength() > 0)
249 {
250 var first = choices[0];
251 if (first.TryGetProperty("message", out var message) && message.TryGetProperty("content", out var msgContent))
252 return msgContent.GetString();
253 }
254 return null;
255 }
256 }
257}
Per-provider connection definition: base URL, endpoint, default model, auth style,...
Loads and saves the AI provider definitions used by GPAL.AI.WithProvider. Stored as ....
static AIProvidersConfig Load(GPALFile file=null)
Loads AI provider definitions from AIProvidersConfig.yaml (or a custom path). If the file is missing,...
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