Skip to content

Commit de8eb02

Browse files
committed
Refactored to make AI control more independent from rest of the project
1 parent 2954aa5 commit de8eb02

8 files changed

Lines changed: 597 additions & 392 deletions

File tree

RegExpressWPFNET/RegExpressLibrary/IRegexEngine.cs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,66 @@ namespace RegExpressLibrary
1515
public delegate void RegexEngineOptionsChanged( IRegexEngine sender, RegexEngineOptionsChangedArgs args );
1616

1717

18-
public interface IRegexEngine
18+
public interface IBaseEngine
19+
{
20+
string Name { get; }
21+
string? ExportOptions( ); // (JSON)
22+
}
23+
24+
public interface IAIEngine
25+
{
26+
public enum REFERENCE_TEXT_REPLACE_ACTION
27+
{
28+
/// <summary>
29+
/// Disable reference text functionality, will not show in UI either
30+
/// </summary>
31+
ReferenceTextDisabled,
32+
/// <summary>
33+
/// This will change the old reference text to a placeholder indicating it has changed and the new version will appear later in the chat history, exact placeholder text can be controlled via PlaceholderTextForReferenceTextRemoval.
34+
/// </summary>
35+
ChangeOldToPlaceholder,
36+
/// <summary>
37+
/// This leaves the old reference text in place in the chat history even if it changes later. It may eat up excess tokens but hopefully shouldn't be too confusing to the AI as it can see the newer version later. It also allows the AI to understand how the reference text has changed over time.
38+
/// </summary>
39+
LeaveOldInplace,
40+
/// <summary>
41+
/// UpdateInPlace means it appears in the chat history where it originally appeared, this might be confusing as the AI's answers were based on the old text and it may confuse the current AI as why the AI previously responded as it did.
42+
/// </summary>
43+
UpdateInPlace,
44+
/// <summary>
45+
/// This deletes the old reference text from the chat history entirely when it changes. This might cause confusion for the current AI as it won't know exactly what the previous AI was basing its responses off.
46+
/// </summary>
47+
DeleteOld,
48+
49+
}
50+
/// <summary>
51+
/// For large reference texts we don't want to keep sending them in the chat history if they change so this controls how we handle them. Depending on the action may cause confusion for the AI or waste tokens, ChangeOldToPlaceholder is likely best.
52+
/// </summary>
53+
REFERENCE_TEXT_REPLACE_ACTION ReplaceAction => REFERENCE_TEXT_REPLACE_ACTION.ChangeOldToPlaceholder;
54+
string GetSystemPrompt( );
55+
/// <summary>
56+
/// This is the message header used at the start of a message to the AI providing some sort of reference text that will appear in a codeblock. For example "Users current webpage html"
57+
/// </summary>
58+
string ReferenceTextHeader => "Reference Text";
59+
string PlaceholderTextForReferenceTextRemoval => $"The old content for {ReferenceTextHeader} was here but changed. It has been removed to shorten history new version found later.";
60+
bool IsReferenceTextEnabled => ReplaceAction != REFERENCE_TEXT_REPLACE_ACTION.ReferenceTextDisabled;
61+
}
62+
public interface IOurAIEngine : IAIEngine, IBaseEngine
63+
{
64+
string AIPatternType => "Regex";
65+
string AIPatternCodeblockType => "regex";
66+
string AIAdditionalSystemPrompt => "If the language supports named capture groups, use these by default. " +
67+
"If the user has ignoring patterned whitespace enabled in the options, use multi-lines and minimal in-regex comments for complex regexes with nice whitespace formatting to make it more readable. ";
68+
69+
string IAIEngine.ReferenceTextHeader => "Users current target text";
70+
string IAIEngine.GetSystemPrompt( ) => $"You are a {Name} {AIPatternType} expert assistant. The user has questions about their {AIPatternType} patterns and target text. " +
71+
$"Provide {AIPatternType} patterns inside Markdown code blocks (```{AIPatternCodeblockType} ... ```). " +
72+
"Explain how the pattern works briefly. " +
73+
AIAdditionalSystemPrompt +
74+
$"They currently have these engine options enabled:\n```json\n{ExportOptions( )}\n```";
75+
}
76+
77+
public interface IRegexEngine : IOurAIEngine
1978
{
2079
event RegexEngineOptionsChanged? OptionsChanged;
2180
event EventHandler? FeatureMatrixReady;
@@ -26,8 +85,6 @@ public interface IRegexEngine
2685

2786
(string Kind, string? Version) CombinedId => (Kind, Version);
2887

29-
string Name { get; }
30-
3188
string Subtitle { get; }
3289

3390
RegexEngineCapabilityEnum Capabilities { get; }
@@ -36,8 +93,6 @@ public interface IRegexEngine
3693

3794
Control GetOptionsControl( );
3895

39-
string? ExportOptions( ); // (JSON)
40-
4196
void ImportOptions( string? json );
4297

4398
RegexMatches GetMatches( ICancellable cnc, [StringSyntax( StringSyntaxAttribute.Regex )] string pattern, string text );

RegExpressWPFNET/RegExpressWPFNET/Code/AIService.cs

Lines changed: 79 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -10,87 +10,71 @@
1010

1111
namespace RegExpressWPFNET.Code
1212
{
13-
13+
public class AiModelProvider
14+
{
15+
public string Id { get; set; }
16+
public string Name { get; set; }
17+
public string Description { get; set; }
18+
public string ModelHint { get; set; }
19+
public string TokenHint { get; set; }
20+
public string TokenHelp { get; set; }
21+
public string EndpointHint { get; set; } = "ie: http://localhost:11434/v1";
22+
23+
public bool IsGithubCopilot { get; set; }
24+
public bool AllowEndpointCustomization { get; set; }
25+
26+
public string Endpoint { get; set; }
27+
public string ModelId { get; set; }
28+
public bool TokenRequired {get;set; } = true;
29+
public string Token { get; set; }
30+
public List<KeyValuePair<string, string>> HTTPHeadersToAdd { get; set; } = new( );
31+
override public string ToString( ) => Name;
32+
}
1433
public class AiService
1534
{
35+
36+
1637
private IChatCompletionService? _chatService;
1738
private Kernel? _kernel;
1839
private HttpClient? _httpClient; // Keep reference to avoid disposal
19-
private ChatHistory? _chatHistory; // Persistent conversation history
40+
private ChatHistory _chatHistory = new( ); // Persistent conversation history
2041
public bool IsConfigured => _chatService != null;
2142

22-
public string EngineName { get; private set; } = string.Empty;
23-
public string? EngineOptions { get; private set; }
2443
public string? CurrentProvider { get; private set; }
2544
public string? CurrentModelId { get; private set; }
45+
public IAIEngine Engine { get; private set; }
46+
public Action<string>? DebugAction;
47+
2648

27-
public void Configure( string provider, string apiKey, string modelId, string endpoint )
49+
public void Configure( AiModelProvider provider )
2850
{
2951
// Dispose previous HttpClient if any
3052
_httpClient?.Dispose( );
3153
_httpClient = null;
3254

3355
var builder = Kernel.CreateBuilder( );
56+
_httpClient = new HttpClient( );
57+
foreach( var header in provider.HTTPHeadersToAdd )
58+
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation( header.Key, header.Value );
59+
60+
builder.AddOpenAIChatCompletion(
61+
modelId: provider.ModelId,
62+
apiKey: provider.Token,
63+
httpClient: _httpClient,
64+
endpoint: string.IsNullOrWhiteSpace( provider.Endpoint ) ? null : new Uri( provider.Endpoint )
65+
);
66+
3467

35-
switch( provider )
36-
{
37-
case "OpenAI":
38-
builder.AddOpenAIChatCompletion( modelId, apiKey );
39-
break;
40-
41-
case "GitHub Copilot":
42-
// GitHub Copilot uses a two-step auth flow:
43-
// 1. OAuth token → API token exchange (done before calling Configure)
44-
// 2. API token + dynamic endpoint for requests
45-
// The apiKey here is the API token (not OAuth), endpoint is from token exchange
46-
if( string.IsNullOrWhiteSpace( modelId ) )
47-
throw new ArgumentException( "GitHub Copilot must have the modelId" );
48-
if( string.IsNullOrWhiteSpace( endpoint ) )
49-
throw new ArgumentException( "GitHub Copilot requires the API endpoint from token exchange" );
50-
51-
_httpClient = new HttpClient { BaseAddress = new Uri( endpoint ) };
52-
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation( "User-Agent", "GitHubCopilotChat/0.24.2025012401" );
53-
_httpClient.DefaultRequestHeaders.Add( "Copilot-Integration-Id", "vscode-chat" );
54-
_httpClient.DefaultRequestHeaders.Add( "Editor-Version", "vscode/1.103.2" );
55-
_httpClient.DefaultRequestHeaders.Add( "x-github-api-version", "2025-05-01" );
56-
builder.AddOpenAIChatCompletion(
57-
modelId: modelId,
58-
apiKey: apiKey,
59-
httpClient: _httpClient
60-
);
61-
break;
62-
63-
case "GitHub Models":
64-
// GitHub Models uses the OpenAI connector but with a specific endpoint
65-
_httpClient = new HttpClient { BaseAddress = new Uri( "https://models.inference.ai.azure.com" ) };
66-
builder.AddOpenAIChatCompletion(
67-
modelId: modelId,
68-
apiKey: apiKey,
69-
httpClient: _httpClient
70-
);
71-
break;
72-
73-
case "Local (Ollama)":
74-
// Connects to local Ollama instance (usually http://localhost:11434/v1)
75-
builder.AddOpenAIChatCompletion(
76-
modelId: modelId, // e.g., "llama3"
77-
apiKey: apiKey, // Ollama ignores the key
78-
endpoint: new Uri( endpoint )
79-
);
80-
break;
81-
}
8268

8369
_kernel = builder.Build( );
8470
_chatService = _kernel.GetRequiredService<IChatCompletionService>( );
85-
CurrentProvider = provider;
86-
CurrentModelId = modelId;
8771

8872
// Initialize fresh chat history with system prompt
8973
_chatHistory = new ChatHistory( );
9074
lastTargetText = string.Empty;
9175
SetSystemPromptIfChanged( true );
9276
}
93-
private string lastPrompt;
77+
private string? lastPrompt;
9478
private void SetSystemPromptIfChanged( bool force = false )
9579
{
9680
var curPrompt = GetSystemPrompt( );
@@ -103,31 +87,23 @@ private void SetSystemPromptIfChanged( bool force = false )
10387
lastPrompt = curPrompt;
10488
}
10589

106-
private string GetSystemPrompt( )
107-
{
108-
return $"You are a {EngineName} Regex expert assistant. The user has questions about their regex patterns and target text. " +
109-
"Provide Regex patterns inside Markdown code blocks (```regex ... ```). " +
110-
"Explain how the pattern works briefly. " +
111-
"If the language supports named capture groups, use these by default. " +
112-
"If the user has ignoring patterned whitespace enabled in the options, use multi-lines and minimal in-regex comments for complex regexes with nice whitespace formatting to make it more readable. " +
113-
$"They currently have these engine options enabled: {EngineOptions}";
114-
}
90+
private string GetSystemPrompt( ) => Engine?.GetSystemPrompt( ) ?? string.Empty;
11591

116-
public async Task<string> GetSuggestionAsync( string userPrompt, string curRegex, string targetText )
92+
public async Task<string> GetSuggestionAsync( string userPrompt, string ReferenceText )
11793
{
118-
if( _chatService == null || _chatHistory == null )
94+
if( _chatService == null )
11995
return "Error: AI Service not configured. Please go to Settings.";
12096

12197
SetSystemPromptIfChanged( );
12298
// Build the user message with current context
123-
SetOrUpdateTargetTextIfChanged( targetText );
124-
string combinedPrompt = $"Current Regex pattern:\n```\n{curRegex}\n```\n\nMy question: {userPrompt}";
99+
SetOrUpdateTargetTextIfChanged( ReferenceText );
125100

126101

127102
// Add user message to history
128-
_chatHistory.AddUserMessage( combinedPrompt );
103+
_chatHistory.AddUserMessage( userPrompt );
104+
129105

130-
if( InternalConfig.DEBUG_LOG_AI_MESSAGES )
106+
if( DebugAction != null )
131107
{
132108
StringBuilder debugSb = new StringBuilder( );
133109
debugSb.AppendLine( "=== AI Chat History ===" );
@@ -136,13 +112,13 @@ public async Task<string> GetSuggestionAsync( string userPrompt, string curRegex
136112
debugSb.AppendLine( $"[{message.Role}] {message.Content}" );
137113
}
138114
debugSb.AppendLine( "=======================" );
139-
System.Diagnostics.Debug.WriteLine( debugSb.ToString( ) );
115+
DebugAction?.Invoke( debugSb.ToString( ) );
140116
}
141117
// Get response
142118
var result = await _chatService.GetChatMessageContentAsync( _chatHistory );
143119
var response = result.Content ?? "No response from AI.";
144-
if( InternalConfig.DEBUG_LOG_AI_MESSAGES )
145-
System.Diagnostics.Debug.WriteLine( $"[AI Response] {response}" );
120+
121+
DebugAction?.Invoke( $"[AI Response] {response}" );
146122

147123
// Add assistant response to history for context in follow-up questions
148124
_chatHistory.AddAssistantMessage( response );
@@ -151,31 +127,44 @@ public async Task<string> GetSuggestionAsync( string userPrompt, string curRegex
151127
}
152128

153129
private string lastTargetText = string.Empty;
154-
private const string CUR_TARGET_TEXT_HEADER = "Current Target text is";
130+
private const string REFERENCE_TEXT_CODEBLOCK_DELIM = "\n```\n";
131+
/// <summary>
132+
/// Sadly right now developer or tool both throw an error...
133+
/// </summary>
134+
private AuthorRole StoreReferenceTextUnder = AuthorRole.User;
155135
private void SetOrUpdateTargetTextIfChanged( string targetText )
156136
{
157-
if( lastTargetText == targetText )
137+
if( lastTargetText == targetText || Engine.ReplaceAction == IAIEngine.REFERENCE_TEXT_REPLACE_ACTION.ReferenceTextDisabled )
158138
return;
159139
lastTargetText = targetText;
160-
var msgStr = $"Current Target text is:\n```\n{targetText}\n```\n\n";
161-
var curMsg = _chatHistory.FirstOrDefault( x => x.Role == AuthorRole.User && x.Content?.StartsWith( CUR_TARGET_TEXT_HEADER ) == true );
162-
163-
164-
165-
140+
var PreTarget = $"{Engine.ReferenceTextHeader}:{REFERENCE_TEXT_CODEBLOCK_DELIM}";
141+
var msgStr = $"{PreTarget}{targetText}{REFERENCE_TEXT_CODEBLOCK_DELIM}\n";
142+
var curMsg = _chatHistory.FirstOrDefault( x => x.Role == StoreReferenceTextUnder && x.Content?.StartsWith( PreTarget ) == true );
166143

167144
if( curMsg != null )
168145
{
169-
if (InternalConfig.DONT_UPDATE_OLD_AI_TEXT_MARK_REMOVED)
170-
curMsg.Content = "The old target text was here but changed, removed to shorten history new version found later.";
171-
else
146+
switch( Engine.ReplaceAction )
147+
{
148+
case IAIEngine.REFERENCE_TEXT_REPLACE_ACTION.UpdateInPlace:
172149
curMsg.Content = msgStr;
150+
return; // Don't add a new message
151+
152+
case IAIEngine.REFERENCE_TEXT_REPLACE_ACTION.ChangeOldToPlaceholder:
153+
curMsg.Content = Engine.PlaceholderTextForReferenceTextRemoval;
154+
break;
155+
156+
case IAIEngine.REFERENCE_TEXT_REPLACE_ACTION.LeaveOldInplace:
157+
break;
158+
159+
case IAIEngine.REFERENCE_TEXT_REPLACE_ACTION.DeleteOld:
160+
_chatHistory.Remove( curMsg );
161+
break;
162+
}
173163
}
174-
if (InternalConfig.DONT_UPDATE_OLD_AI_TEXT_MARK_REMOVED || curMsg == null)
175-
_chatHistory.AddUserMessage( content: msgStr );
176164

165+
// Add the new reference text message
166+
_chatHistory.AddMessage( StoreReferenceTextUnder, content: msgStr );
177167

178-
// Target text:\n```\n{targetText}\n```\n\n
179168
}
180169

181170
/// <summary>
@@ -192,17 +181,16 @@ public void ClearConversation( )
192181
}
193182
}
194183

195-
internal void SetEngineInfo( string name, string? engine_options )
184+
internal void SetEngineInfo( IAIEngine engine )
196185
{
197-
this.EngineName = name;
198-
this.EngineOptions = engine_options;
186+
this.Engine = engine;
199187
}
200188

201189
public void Reset( )
202190
{
203191
_chatService = null;
204192
_kernel = null;
205-
_chatHistory = null;
193+
_chatHistory = new( );
206194
_httpClient?.Dispose( );
207195
_httpClient = null;
208196
CurrentProvider = null;

RegExpressWPFNET/RegExpressWPFNET/Code/CopilotTokenHelper.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public string GetDisplayName( )
6262
if( TokenMultiplier.HasValue )
6363
{
6464
if( TokenMultiplier.Value == 0 )
65-
tags.Add( "FREE" );
65+
tags.Add( "free" );
6666
else if( TokenMultiplier.Value == 1.0 )
6767
tags.Add( "1x" );
6868
else
@@ -760,6 +760,12 @@ public static async Task<FetchModelsResult> FetchAvailableModelsAsync(
760760
if( defaultElement.GetBoolean( ) )
761761
model.IsPreview = false; // Default models are not preview
762762
}
763+
if (model.Name.Contains("beta", StringComparison.CurrentCultureIgnoreCase))
764+
model.IsBeta = true;
765+
string[] replace_strs = ["(preview)","(beta)","preview","beta"];
766+
foreach (var str in replace_strs)
767+
model.Name = model.Name.Replace(str, "", StringComparison.OrdinalIgnoreCase);
768+
model.Name = model.Name.Trim().Replace(" ", " ");
763769

764770
result.Models.Add( model );
765771
}

RegExpressWPFNET/RegExpressWPFNET/Code/TabData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public sealed class TabData
3838
class AllTabData
3939
{
4040
public List<TabData> Tabs { get; set; } = new( );
41-
public AIControlData AI { get; set; } = new( );
41+
public AIUserConfig AI { get; set; } = new( );
4242
public bool AITabOpen { get; set; } = false;
4343
}
4444

0 commit comments

Comments
 (0)