44
55namespace WordPress \AiClient \Providers \Models \DTO ;
66
7+ use InvalidArgumentException ;
78use WordPress \AiClient \Common \AbstractDataTransferObject ;
8- use WordPress \AiClient \Common \Exception \InvalidArgumentException ;
9+ use WordPress \AiClient \Messages \DTO \Message ;
10+ use WordPress \AiClient \Messages \Enums \ModalityEnum ;
911use WordPress \AiClient \Providers \Models \Enums \CapabilityEnum ;
12+ use WordPress \AiClient \Providers \Models \Enums \OptionEnum ;
1013
1114/**
1215 * Represents requirements that implementing code has for AI model selection.
@@ -88,6 +91,290 @@ public function getRequiredOptions(): array
8891 return $ this ->requiredOptions ;
8992 }
9093
94+ /**
95+ * Checks whether the given model metadata meets these requirements.
96+ *
97+ * @since n.e.x.t
98+ *
99+ * @param ModelMetadata $metadata The model metadata to check against.
100+ * @return bool True if the model meets all requirements, false otherwise.
101+ */
102+ public function areMetBy (ModelMetadata $ metadata ): bool
103+ {
104+ // Create lookup maps for better performance (instead of nested foreach loops)
105+ $ capabilitiesMap = [];
106+ foreach ($ metadata ->getSupportedCapabilities () as $ capability ) {
107+ $ capabilitiesMap [$ capability ->value ] = $ capability ;
108+ }
109+
110+ $ optionsMap = [];
111+ foreach ($ metadata ->getSupportedOptions () as $ option ) {
112+ $ optionsMap [$ option ->getName ()->value ] = $ option ;
113+ }
114+
115+ // Check if all required capabilities are supported using map lookup
116+ foreach ($ this ->requiredCapabilities as $ requiredCapability ) {
117+ if (!isset ($ capabilitiesMap [$ requiredCapability ->value ])) {
118+ return false ;
119+ }
120+ }
121+
122+ // Check if all required options are supported with the specified values
123+ foreach ($ this ->requiredOptions as $ requiredOption ) {
124+ // Use map lookup instead of linear search
125+ if (!isset ($ optionsMap [$ requiredOption ->getName ()->value ])) {
126+ return false ;
127+ }
128+
129+ $ supportedOption = $ optionsMap [$ requiredOption ->getName ()->value ];
130+
131+ // Check if the required value is supported by this option
132+ if (!$ supportedOption ->isSupportedValue ($ requiredOption ->getValue ())) {
133+ return false ;
134+ }
135+ }
136+
137+ return true ;
138+ }
139+
140+ /**
141+ * Creates ModelRequirements from prompt data and model configuration.
142+ *
143+ * @since n.e.x.t
144+ *
145+ * @param CapabilityEnum $capability The capability the model must support.
146+ * @param list<Message> $messages The messages in the conversation.
147+ * @param ModelConfig $modelConfig The model configuration.
148+ * @return self The created requirements.
149+ */
150+ public static function fromPromptData (CapabilityEnum $ capability , array $ messages , ModelConfig $ modelConfig ): self
151+ {
152+ // Start with base capability
153+ $ capabilities = [$ capability ];
154+ $ inputModalities = [];
155+
156+ // Check if we have chat history (multiple messages)
157+ if (count ($ messages ) > 1 ) {
158+ $ capabilities [] = CapabilityEnum::chatHistory ();
159+ }
160+
161+ // Analyze all messages to determine required input modalities
162+ $ hasFunctionMessageParts = false ;
163+ foreach ($ messages as $ message ) {
164+ foreach ($ message ->getParts () as $ part ) {
165+ // Check for text input
166+ if ($ part ->getType ()->isText ()) {
167+ $ inputModalities [] = ModalityEnum::text ();
168+ }
169+
170+ // Check for file inputs
171+ if ($ part ->getType ()->isFile ()) {
172+ $ file = $ part ->getFile ();
173+
174+ if ($ file !== null ) {
175+ if ($ file ->isImage ()) {
176+ $ inputModalities [] = ModalityEnum::image ();
177+ } elseif ($ file ->isAudio ()) {
178+ $ inputModalities [] = ModalityEnum::audio ();
179+ } elseif ($ file ->isVideo ()) {
180+ $ inputModalities [] = ModalityEnum::video ();
181+ } elseif ($ file ->isDocument () || $ file ->isText ()) {
182+ $ inputModalities [] = ModalityEnum::document ();
183+ }
184+ }
185+ }
186+
187+ // Check for function calls/responses (these might require special capabilities)
188+ if ($ part ->getType ()->isFunctionCall () || $ part ->getType ()->isFunctionResponse ()) {
189+ $ hasFunctionMessageParts = true ;
190+ }
191+ }
192+ }
193+
194+ // Convert ModelConfig to RequiredOptions
195+ $ requiredOptions = self ::toRequiredOptions ($ modelConfig );
196+
197+ // Add additional options based on message analysis
198+ if ($ hasFunctionMessageParts ) {
199+ $ requiredOptions = self ::includeInRequiredOptions (
200+ $ requiredOptions ,
201+ new RequiredOption (OptionEnum::functionDeclarations (), true )
202+ );
203+ }
204+
205+ // Add input modalities if we have any inputs
206+ if (!empty ($ inputModalities )) {
207+ // Remove duplicates
208+ $ inputModalities = array_unique ($ inputModalities , SORT_REGULAR );
209+ $ requiredOptions = self ::includeInRequiredOptions (
210+ $ requiredOptions ,
211+ new RequiredOption (OptionEnum::inputModalities (), array_values ($ inputModalities ))
212+ );
213+ }
214+
215+ // Step 6: Return new ModelRequirements
216+ return new self ($ capabilities , $ requiredOptions );
217+ }
218+
219+ /**
220+ * Converts ModelConfig to an array of RequiredOptions.
221+ *
222+ * @since n.e.x.t
223+ *
224+ * @param ModelConfig $modelConfig The model configuration.
225+ * @return list<RequiredOption> The required options.
226+ */
227+ private static function toRequiredOptions (ModelConfig $ modelConfig ): array
228+ {
229+ $ requiredOptions = [];
230+
231+ // Map properties that have corresponding OptionEnum values
232+ if ($ modelConfig ->getOutputModalities () !== null ) {
233+ $ requiredOptions [] = new RequiredOption (
234+ OptionEnum::outputModalities (),
235+ $ modelConfig ->getOutputModalities ()
236+ );
237+ }
238+
239+ if ($ modelConfig ->getSystemInstruction () !== null ) {
240+ $ requiredOptions [] = new RequiredOption (
241+ OptionEnum::systemInstruction (),
242+ $ modelConfig ->getSystemInstruction ()
243+ );
244+ }
245+
246+ if ($ modelConfig ->getCandidateCount () !== null ) {
247+ $ requiredOptions [] = new RequiredOption (
248+ OptionEnum::candidateCount (),
249+ $ modelConfig ->getCandidateCount ()
250+ );
251+ }
252+
253+ if ($ modelConfig ->getMaxTokens () !== null ) {
254+ $ requiredOptions [] = new RequiredOption (
255+ OptionEnum::maxTokens (),
256+ $ modelConfig ->getMaxTokens ()
257+ );
258+ }
259+
260+ if ($ modelConfig ->getTemperature () !== null ) {
261+ $ requiredOptions [] = new RequiredOption (
262+ OptionEnum::temperature (),
263+ $ modelConfig ->getTemperature ()
264+ );
265+ }
266+
267+ if ($ modelConfig ->getTopP () !== null ) {
268+ $ requiredOptions [] = new RequiredOption (
269+ OptionEnum::topP (),
270+ $ modelConfig ->getTopP ()
271+ );
272+ }
273+
274+ if ($ modelConfig ->getTopK () !== null ) {
275+ $ requiredOptions [] = new RequiredOption (
276+ OptionEnum::topK (),
277+ $ modelConfig ->getTopK ()
278+ );
279+ }
280+
281+ if ($ modelConfig ->getOutputMimeType () !== null ) {
282+ $ requiredOptions [] = new RequiredOption (
283+ OptionEnum::outputMimeType (),
284+ $ modelConfig ->getOutputMimeType ()
285+ );
286+ }
287+
288+ if ($ modelConfig ->getOutputSchema () !== null ) {
289+ $ requiredOptions [] = new RequiredOption (
290+ OptionEnum::outputSchema (),
291+ $ modelConfig ->getOutputSchema ()
292+ );
293+ }
294+
295+ // Handle properties without OptionEnum values as custom options
296+ if ($ modelConfig ->getStopSequences () !== null ) {
297+ $ requiredOptions [] = new RequiredOption (OptionEnum::stopSequences (), $ modelConfig ->getStopSequences ());
298+ }
299+
300+ if ($ modelConfig ->getPresencePenalty () !== null ) {
301+ $ requiredOptions [] = new RequiredOption (OptionEnum::presencePenalty (), $ modelConfig ->getPresencePenalty ());
302+ }
303+
304+ if ($ modelConfig ->getFrequencyPenalty () !== null ) {
305+ $ requiredOptions [] = new RequiredOption (
306+ OptionEnum::frequencyPenalty (),
307+ $ modelConfig ->getFrequencyPenalty ()
308+ );
309+ }
310+
311+ if ($ modelConfig ->getLogprobs () !== null ) {
312+ $ requiredOptions [] = new RequiredOption (OptionEnum::logprobs (), $ modelConfig ->getLogprobs ());
313+ }
314+
315+ if ($ modelConfig ->getTopLogprobs () !== null ) {
316+ $ requiredOptions [] = new RequiredOption (OptionEnum::topLogprobs (), $ modelConfig ->getTopLogprobs ());
317+ }
318+
319+ if ($ modelConfig ->getFunctionDeclarations () !== null ) {
320+ $ requiredOptions [] = new RequiredOption (OptionEnum::functionDeclarations (), true );
321+ }
322+
323+ if ($ modelConfig ->getWebSearch () !== null ) {
324+ $ requiredOptions [] = new RequiredOption (OptionEnum::webSearch (), true );
325+ }
326+
327+ if ($ modelConfig ->getOutputFileType () !== null ) {
328+ $ requiredOptions [] = new RequiredOption (OptionEnum::outputFileType (), $ modelConfig ->getOutputFileType ());
329+ }
330+
331+ if ($ modelConfig ->getOutputMediaOrientation () !== null ) {
332+ $ requiredOptions [] = new RequiredOption (
333+ OptionEnum::outputMediaOrientation (),
334+ $ modelConfig ->getOutputMediaOrientation ()
335+ );
336+ }
337+
338+ if ($ modelConfig ->getOutputMediaAspectRatio () !== null ) {
339+ $ requiredOptions [] = new RequiredOption (
340+ OptionEnum::outputMediaAspectRatio (),
341+ $ modelConfig ->getOutputMediaAspectRatio ()
342+ );
343+ }
344+
345+ // Add custom options as individual RequiredOptions
346+ foreach ($ modelConfig ->getCustomOptions () as $ key => $ value ) {
347+ $ requiredOptions [] = new RequiredOption (OptionEnum::customOptions (), [$ key => $ value ]);
348+ }
349+
350+ return $ requiredOptions ;
351+ }
352+
353+ /**
354+ * Includes a RequiredOption in the array, ensuring no duplicates based on option name.
355+ *
356+ * @since n.e.x.t
357+ *
358+ * @param list<RequiredOption> $requiredOptions The existing required options.
359+ * @param RequiredOption $newOption The new option to include.
360+ * @return list<RequiredOption> The updated required options array.
361+ */
362+ private static function includeInRequiredOptions (array $ requiredOptions , RequiredOption $ newOption ): array
363+ {
364+ // Check if we already have this option name
365+ foreach ($ requiredOptions as $ index => $ existingOption ) {
366+ if ($ existingOption ->getName ()->equals ($ newOption ->getName ())) {
367+ // Replace existing option with new one
368+ $ requiredOptions [$ index ] = $ newOption ;
369+ return $ requiredOptions ;
370+ }
371+ }
372+
373+ // Option not found, add it
374+ $ requiredOptions [] = $ newOption ;
375+ return $ requiredOptions ;
376+ }
377+
91378 /**
92379 * {@inheritDoc}
93380 *
@@ -157,4 +444,4 @@ public static function fromArray(array $array): self
157444 )
158445 );
159446 }
160- }
447+ }
0 commit comments