1+ // Licensed to the .NET Foundation under one or more agreements.
2+ // The .NET Foundation licenses this file to you under the MIT license.
3+ // See the LICENSE file in the project root for more information.
4+
5+ using DevProxy . Abstractions . Utils ;
6+ using Microsoft . Extensions . AI ;
7+ using Microsoft . Extensions . Logging ;
8+ using PromptyCore = Prompty . Core ;
9+ using System . Collections . Concurrent ;
10+
11+ namespace DevProxy . Abstractions . LanguageModel ;
12+
13+ public abstract class BaseLanguageModelClient ( ILogger logger ) : ILanguageModelClient
14+ {
15+ private readonly ILogger _logger = logger ;
16+ private readonly ConcurrentDictionary < string , ( IEnumerable < ILanguageModelChatCompletionMessage > ? , CompletionOptions ? ) > _promptCache = new ( ) ;
17+
18+ public virtual async Task < ILanguageModelCompletionResponse ? > GenerateChatCompletionAsync ( string promptFileName , Dictionary < string , object > parameters )
19+ {
20+ ArgumentNullException . ThrowIfNull ( promptFileName , nameof ( promptFileName ) ) ;
21+
22+ if ( ! promptFileName . EndsWith ( ".prompty" , StringComparison . OrdinalIgnoreCase ) )
23+ {
24+ _logger . LogDebug ( "Prompt file name '{PromptFileName}' does not end with '.prompty'. Appending the extension." , promptFileName ) ;
25+ promptFileName += ".prompty" ;
26+ }
27+ var ( messages , options ) = _promptCache . GetOrAdd ( promptFileName , _ =>
28+ LoadPrompt ( promptFileName , parameters ) ) ;
29+
30+ if ( messages is null || ! messages . Any ( ) )
31+ {
32+ return null ;
33+ }
34+
35+ return await GenerateChatCompletionAsync ( messages , options ) ;
36+ }
37+
38+ public virtual Task < ILanguageModelCompletionResponse ? > GenerateChatCompletionAsync ( IEnumerable < ILanguageModelChatCompletionMessage > messages , CompletionOptions ? options = null ) => throw new NotImplementedException ( ) ;
39+
40+ public virtual Task < ILanguageModelCompletionResponse ? > GenerateCompletionAsync ( string prompt , CompletionOptions ? options = null ) => throw new NotImplementedException ( ) ;
41+
42+ public virtual Task < bool > IsEnabledAsync ( ) => throw new NotImplementedException ( ) ;
43+
44+ protected virtual IEnumerable < ILanguageModelChatCompletionMessage > ConvertMessages ( ChatMessage [ ] messages ) => throw new NotImplementedException ( ) ;
45+
46+ private ( IEnumerable < ILanguageModelChatCompletionMessage > ? , CompletionOptions ? ) LoadPrompt ( string promptFileName , Dictionary < string , object > parameters )
47+ {
48+ _logger . LogDebug ( "Prompt file {PromptFileName} not in the cache. Loading..." , promptFileName ) ;
49+
50+ var filePath = Path . Combine ( ProxyUtils . AppFolder ! , "prompts" , promptFileName ) ;
51+ if ( ! File . Exists ( filePath ) )
52+ {
53+ throw new FileNotFoundException ( $ "Prompt file '{ filePath } ' not found.") ;
54+ }
55+
56+ _logger . LogDebug ( "Loading prompt file: {FilePath}" , filePath ) ;
57+ var promptContents = File . ReadAllText ( filePath ) ;
58+
59+ var prompty = PromptyCore . Prompty . Load ( promptContents , [ ] ) ;
60+ if ( prompty . Prepare ( parameters ) is not ChatMessage [ ] promptyMessages ||
61+ promptyMessages . Length == 0 )
62+ {
63+ _logger . LogError ( "No messages found in the prompt file: {FilePath}" , filePath ) ;
64+ return ( null , null ) ;
65+ }
66+
67+ var messages = ConvertMessages ( promptyMessages ) ;
68+
69+ var options = new CompletionOptions ( ) ;
70+ if ( prompty . Model ? . Options is not null )
71+ {
72+ if ( prompty . Model . Options . TryGetValue ( "temperature" , out var temperature ) )
73+ {
74+ options . Temperature = temperature as double ? ;
75+ }
76+ if ( prompty . Model . Options . TryGetValue ( "top_p" , out var topP ) )
77+ {
78+ options . TopP = topP as double ? ;
79+ }
80+ }
81+
82+ return ( messages , options ) ;
83+ }
84+ }
0 commit comments