21
21
import java .util .Map ;
22
22
import java .util .Set ;
23
23
24
+ import io .micrometer .observation .Observation ;
25
+ import io .micrometer .observation .ObservationRegistry ;
26
+ import io .micrometer .observation .contextpropagation .ObservationThreadLocalAccessor ;
24
27
import org .springframework .ai .chat .messages .AssistantMessage ;
25
28
import org .springframework .ai .chat .messages .SystemMessage ;
26
29
import org .springframework .ai .chat .messages .ToolResponseMessage ;
27
30
import org .springframework .ai .chat .messages .UserMessage ;
28
31
import org .springframework .ai .chat .metadata .ChatGenerationMetadata ;
29
32
import org .springframework .ai .chat .metadata .ChatResponseMetadata ;
30
- import org .springframework .ai .chat .model .AbstractToolCallSupport ;
31
- import org .springframework .ai .chat .model .ChatModel ;
32
- import org .springframework .ai .chat .model .ChatResponse ;
33
- import org .springframework .ai .chat .model .Generation ;
33
+ import org .springframework .ai .chat .model .*;
34
+ import org .springframework .ai .chat .observation .ChatModelObservationContext ;
35
+ import org .springframework .ai .chat .observation .ChatModelObservationConvention ;
36
+ import org .springframework .ai .chat .observation .ChatModelObservationDocumentation ;
37
+ import org .springframework .ai .chat .observation .DefaultChatModelObservationConvention ;
34
38
import org .springframework .ai .chat .prompt .ChatOptions ;
39
+ import org .springframework .ai .chat .prompt .ChatOptionsBuilder ;
35
40
import org .springframework .ai .chat .prompt .Prompt ;
36
41
import org .springframework .ai .model .ModelOptionsUtils ;
37
42
import org .springframework .ai .model .function .FunctionCallback ;
64
69
*/
65
70
public class OllamaChatModel extends AbstractToolCallSupport implements ChatModel {
66
71
72
+ private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention ();
73
+
67
74
/**
68
75
* Low-level Ollama API library.
69
76
*/
@@ -72,61 +79,97 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
72
79
/**
73
80
* Default options to be used for all chat requests.
74
81
*/
75
- private OllamaOptions defaultOptions ;
82
+ private final OllamaOptions defaultOptions ;
83
+
84
+ /**
85
+ * Observation registry used for instrumentation.
86
+ */
87
+ private final ObservationRegistry observationRegistry ;
76
88
77
- public OllamaChatModel (OllamaApi chatApi ) {
78
- this (chatApi , OllamaOptions .create ().withModel (OllamaOptions .DEFAULT_MODEL ));
89
+ /**
90
+ * Conventions to use for generating observations.
91
+ */
92
+ private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION ;
93
+
94
+ public OllamaChatModel (OllamaApi ollamaApi ) {
95
+ this (ollamaApi , OllamaOptions .create ().withModel (OllamaOptions .DEFAULT_MODEL ));
79
96
}
80
97
81
- public OllamaChatModel (OllamaApi chatApi , OllamaOptions defaultOptions ) {
82
- this (chatApi , defaultOptions , null );
98
+ public OllamaChatModel (OllamaApi ollamaApi , OllamaOptions defaultOptions ) {
99
+ this (ollamaApi , defaultOptions , null );
83
100
}
84
101
85
- public OllamaChatModel (OllamaApi chatApi , OllamaOptions defaultOptions ,
102
+ public OllamaChatModel (OllamaApi ollamaApi , OllamaOptions defaultOptions ,
86
103
FunctionCallbackContext functionCallbackContext ) {
87
- this (chatApi , defaultOptions , functionCallbackContext , List .of ());
104
+ this (ollamaApi , defaultOptions , functionCallbackContext , List .of ());
88
105
}
89
106
90
- public OllamaChatModel (OllamaApi chatApi , OllamaOptions defaultOptions ,
107
+ public OllamaChatModel (OllamaApi ollamaApi , OllamaOptions defaultOptions ,
91
108
FunctionCallbackContext functionCallbackContext , List <FunctionCallback > toolFunctionCallbacks ) {
109
+ this (ollamaApi , defaultOptions , functionCallbackContext , toolFunctionCallbacks , ObservationRegistry .NOOP );
110
+ }
111
+
112
+ public OllamaChatModel (OllamaApi chatApi , OllamaOptions defaultOptions ,
113
+ FunctionCallbackContext functionCallbackContext , List <FunctionCallback > toolFunctionCallbacks ,
114
+ ObservationRegistry observationRegistry ) {
92
115
super (functionCallbackContext , defaultOptions , toolFunctionCallbacks );
93
- Assert .notNull (chatApi , "OllamaApi must not be null" );
94
- Assert .notNull (defaultOptions , "DefaultOptions must not be null" );
116
+ Assert .notNull (chatApi , "ollamaApi must not be null" );
117
+ Assert .notNull (defaultOptions , "defaultOptions must not be null" );
118
+ Assert .notNull (observationRegistry , "ObservationRegistry must not be null" );
95
119
this .chatApi = chatApi ;
96
120
this .defaultOptions = defaultOptions ;
121
+ this .observationRegistry = observationRegistry ;
97
122
}
98
123
99
124
@ Override
100
125
public ChatResponse call (Prompt prompt ) {
126
+ OllamaApi .ChatRequest request = ollamaChatRequest (prompt , false );
101
127
102
- OllamaApi .ChatResponse response = this .chatApi .chat (ollamaChatRequest (prompt , false ));
128
+ ChatModelObservationContext observationContext = ChatModelObservationContext .builder ()
129
+ .prompt (prompt )
130
+ .provider (OllamaApi .PROVIDER_NAME )
131
+ .requestOptions (buildRequestOptions (request ))
132
+ .build ();
103
133
104
- List <AssistantMessage .ToolCall > toolCalls = response .message ().toolCalls () == null ? List .of ()
105
- : response .message ()
106
- .toolCalls ()
107
- .stream ()
108
- .map (toolCall -> new AssistantMessage .ToolCall ("" , "function" , toolCall .function ().name (),
109
- ModelOptionsUtils .toJsonString (toolCall .function ().arguments ())))
110
- .toList ();
134
+ ChatResponse response = ChatModelObservationDocumentation .CHAT_MODEL_OPERATION
135
+ .observation (this .observationConvention , DEFAULT_OBSERVATION_CONVENTION , () -> observationContext ,
136
+ this .observationRegistry )
137
+ .observe (() -> {
111
138
112
- var assistantMessage = new AssistantMessage ( response . message (). content (), Map . of (), toolCalls );
139
+ OllamaApi . ChatResponse ollamaResponse = this . chatApi . chat ( request );
113
140
114
- ChatGenerationMetadata generationMetadata = ChatGenerationMetadata .NULL ;
115
- if (response .promptEvalCount () != null && response .evalCount () != null ) {
116
- generationMetadata = ChatGenerationMetadata .from (response .doneReason (), null );
117
- }
141
+ List <AssistantMessage .ToolCall > toolCalls = ollamaResponse .message ().toolCalls () == null ? List .of ()
142
+ : ollamaResponse .message ()
143
+ .toolCalls ()
144
+ .stream ()
145
+ .map (toolCall -> new AssistantMessage .ToolCall ("" , "function" , toolCall .function ().name (),
146
+ ModelOptionsUtils .toJsonString (toolCall .function ().arguments ())))
147
+ .toList ();
148
+
149
+ var assistantMessage = new AssistantMessage (ollamaResponse .message ().content (), Map .of (), toolCalls );
150
+
151
+ ChatGenerationMetadata generationMetadata = ChatGenerationMetadata .NULL ;
152
+ if (ollamaResponse .promptEvalCount () != null && ollamaResponse .evalCount () != null ) {
153
+ generationMetadata = ChatGenerationMetadata .from (ollamaResponse .doneReason (), null );
154
+ }
155
+
156
+ var generator = new Generation (assistantMessage , generationMetadata );
157
+ ChatResponse chatResponse = new ChatResponse (List .of (generator ), from (ollamaResponse ));
158
+
159
+ observationContext .setResponse (chatResponse );
160
+
161
+ return chatResponse ;
118
162
119
- var generator = new Generation (assistantMessage , generationMetadata );
120
- var chatResponse = new ChatResponse (List .of (generator ), from (response ));
163
+ });
121
164
122
- if (isToolCall (chatResponse , Set .of ("stop" ))) {
123
- var toolCallConversation = handleToolCalls (prompt , chatResponse );
165
+ if (response != null && isToolCall (response , Set .of ("stop" ))) {
166
+ var toolCallConversation = handleToolCalls (prompt , response );
124
167
// Recursively call the call method with the tool call message
125
168
// conversation that contains the call responses.
126
169
return this .call (new Prompt (toolCallConversation , prompt .getOptions ()));
127
170
}
128
171
129
- return chatResponse ;
172
+ return response ;
130
173
}
131
174
132
175
public static ChatResponseMetadata from (OllamaApi .ChatResponse response ) {
@@ -147,40 +190,64 @@ public static ChatResponseMetadata from(OllamaApi.ChatResponse response) {
147
190
148
191
@ Override
149
192
public Flux <ChatResponse > stream (Prompt prompt ) {
193
+ return Flux .deferContextual (contextView -> {
194
+ OllamaApi .ChatRequest request = ollamaChatRequest (prompt , true );
195
+
196
+ final ChatModelObservationContext observationContext = ChatModelObservationContext .builder ()
197
+ .prompt (prompt )
198
+ .provider (OllamaApi .PROVIDER_NAME )
199
+ .requestOptions (buildRequestOptions (request ))
200
+ .build ();
201
+
202
+ Observation observation = ChatModelObservationDocumentation .CHAT_MODEL_OPERATION .observation (
203
+ this .observationConvention , DEFAULT_OBSERVATION_CONVENTION , () -> observationContext ,
204
+ this .observationRegistry );
205
+
206
+ observation .parentObservation (contextView .getOrDefault (ObservationThreadLocalAccessor .KEY , null )).start ();
207
+
208
+ Flux <OllamaApi .ChatResponse > ollamaResponse = this .chatApi .streamingChat (request );
209
+
210
+ Flux <ChatResponse > chatResponse = ollamaResponse .map (chunk -> {
211
+ String content = (chunk .message () != null ) ? chunk .message ().content () : "" ;
212
+ List <AssistantMessage .ToolCall > toolCalls = chunk .message ().toolCalls () == null ? List .of ()
213
+ : chunk .message ()
214
+ .toolCalls ()
215
+ .stream ()
216
+ .map (toolCall -> new AssistantMessage .ToolCall ("" , "function" , toolCall .function ().name (),
217
+ ModelOptionsUtils .toJsonString (toolCall .function ().arguments ())))
218
+ .toList ();
219
+
220
+ var assistantMessage = new AssistantMessage (content , Map .of (), toolCalls );
221
+
222
+ ChatGenerationMetadata generationMetadata = ChatGenerationMetadata .NULL ;
223
+ if (chunk .promptEvalCount () != null && chunk .evalCount () != null ) {
224
+ generationMetadata = ChatGenerationMetadata .from (chunk .doneReason (), null );
225
+ }
150
226
151
- Flux <OllamaApi .ChatResponse > ollamaResponse = this .chatApi .streamingChat (ollamaChatRequest (prompt , true ));
152
-
153
- Flux <ChatResponse > chatResponse = ollamaResponse .map (chunk -> {
154
- String content = (chunk .message () != null ) ? chunk .message ().content () : "" ;
155
- List <AssistantMessage .ToolCall > toolCalls = chunk .message ().toolCalls () == null ? List .of ()
156
- : chunk .message ()
157
- .toolCalls ()
158
- .stream ()
159
- .map (toolCall -> new AssistantMessage .ToolCall ("" , "function" , toolCall .function ().name (),
160
- ModelOptionsUtils .toJsonString (toolCall .function ().arguments ())))
161
- .toList ();
162
-
163
- var assistantMessage = new AssistantMessage (content , Map .of (), toolCalls );
164
-
165
- ChatGenerationMetadata generationMetadata = ChatGenerationMetadata .NULL ;
166
- if (chunk .promptEvalCount () != null && chunk .evalCount () != null ) {
167
- generationMetadata = ChatGenerationMetadata .from (chunk .doneReason (), null );
168
- }
169
-
170
- var generator = new Generation (assistantMessage , generationMetadata );
171
- return new ChatResponse (List .of (generator ), from (chunk ));
172
- });
173
-
174
- return chatResponse .flatMap (response -> {
175
- if (isToolCall (response , Set .of ("stop" ))) {
176
- var toolCallConversation = handleToolCalls (prompt , response );
177
- // Recursively call the stream method with the tool call message
178
- // conversation that contains the call responses.
179
- return this .stream (new Prompt (toolCallConversation , prompt .getOptions ()));
180
- }
181
- else {
182
- return Flux .just (response );
183
- }
227
+ var generator = new Generation (assistantMessage , generationMetadata );
228
+ return new ChatResponse (List .of (generator ), from (chunk ));
229
+ });
230
+
231
+ // @formatter:off
232
+ Flux <ChatResponse > chatResponseFlux = chatResponse .flatMap (response -> {
233
+ if (isToolCall (response , Set .of ("stop" ))) {
234
+ var toolCallConversation = handleToolCalls (prompt , response );
235
+ // Recursively call the stream method with the tool call message
236
+ // conversation that contains the call responses.
237
+ return this .stream (new Prompt (toolCallConversation , prompt .getOptions ()));
238
+ }
239
+ else {
240
+ return Flux .just (response );
241
+ }
242
+ })
243
+ .doOnError (observation ::error )
244
+ .doFinally (s -> {
245
+ observation .stop ();
246
+ })
247
+ .contextWrite (ctx -> ctx .put (ObservationThreadLocalAccessor .KEY , observation ));
248
+ // @formatter:on
249
+
250
+ return new MessageAggregator ().aggregate (chatResponseFlux , observationContext ::setResponse );
184
251
});
185
252
}
186
253
@@ -216,13 +283,10 @@ else if (message instanceof AssistantMessage assistantMessage) {
216
283
.build ());
217
284
}
218
285
else if (message instanceof ToolResponseMessage toolMessage ) {
219
-
220
- List <OllamaApi .Message > responseMessages = toolMessage .getResponses ()
286
+ return toolMessage .getResponses ()
221
287
.stream ()
222
288
.map (tr -> OllamaApi .Message .builder (Role .TOOL ).withContent (tr .responseData ()).build ())
223
289
.toList ();
224
-
225
- return responseMessages ;
226
290
}
227
291
throw new IllegalArgumentException ("Unsupported message type: " + message .getMessageType ());
228
292
}).flatMap (List ::stream ).toList ();
@@ -290,9 +354,32 @@ private List<ChatRequest.Tool> getFunctionTools(Set<String> functionNames) {
290
354
}).toList ();
291
355
}
292
356
357
+ private ChatOptions buildRequestOptions (OllamaApi .ChatRequest request ) {
358
+ var options = ModelOptionsUtils .mapToClass (request .options (), OllamaOptions .class );
359
+ return ChatOptionsBuilder .builder ()
360
+ .withModel (request .model ())
361
+ .withFrequencyPenalty (options .getFrequencyPenalty ())
362
+ .withMaxTokens (options .getMaxTokens ())
363
+ .withPresencePenalty (options .getPresencePenalty ())
364
+ .withStopSequences (options .getStopSequences ())
365
+ .withTemperature (options .getTemperature ())
366
+ .withTopK (options .getTopK ())
367
+ .withTopP (options .getTopP ())
368
+ .build ();
369
+ }
370
+
293
371
@ Override
294
372
public ChatOptions getDefaultOptions () {
295
373
return OllamaOptions .fromOptions (this .defaultOptions );
296
374
}
297
375
376
+ /**
377
+ * Use the provided convention for reporting observation data
378
+ * @param observationConvention The provided convention
379
+ */
380
+ public void setObservationConvention (ChatModelObservationConvention observationConvention ) {
381
+ Assert .notNull (observationConvention , "observationConvention cannot be null" );
382
+ this .observationConvention = observationConvention ;
383
+ }
384
+
298
385
}
0 commit comments