77import logging
88import os
99from copy import copy
10- from typing import Any , AsyncIterable , Callable , Literal , Type , TypeVar
10+ from types import MethodType
11+ from typing import TYPE_CHECKING , Any , AsyncIterable , Callable , ItemsView , Literal , Type , TypeVar , Union
1112
1213import yaml
1314from pydantic import Field , field_validator
1415
1516from semantic_kernel .connectors .ai .prompt_execution_settings import PromptExecutionSettings
16- from semantic_kernel .contents .streaming_content_mixin import StreamingContentMixin
1717from semantic_kernel .events import FunctionInvokedEventArgs , FunctionInvokingEventArgs
1818from semantic_kernel .exceptions import (
1919 FunctionInitializationError ,
4747from semantic_kernel .services .ai_service_selector import AIServiceSelector
4848from semantic_kernel .utils .validation import validate_plugin_name
4949
50+ if TYPE_CHECKING :
51+ from semantic_kernel .connectors .ai .chat_completion_client_base import ChatCompletionClientBase
52+ from semantic_kernel .connectors .ai .embeddings .embedding_generator_base import EmbeddingGeneratorBase
53+ from semantic_kernel .connectors .ai .text_completion_client_base import TextCompletionClientBase
54+ from semantic_kernel .contents .streaming_content_mixin import StreamingContentMixin
55+
5056T = TypeVar ("T" )
5157
52- ALL_SERVICE_TYPES = "TextCompletionClientBase | ChatCompletionClientBase | EmbeddingGeneratorBase"
58+ ALL_SERVICE_TYPES = Union [ "TextCompletionClientBase" , " ChatCompletionClientBase" , " EmbeddingGeneratorBase"]
5359
5460logger : logging .Logger = logging .getLogger (__name__ )
5561
@@ -139,7 +145,7 @@ async def invoke_stream(
139145 plugin_name : str | None = None ,
140146 return_function_results : bool | None = False ,
141147 ** kwargs : Any ,
142- ) -> AsyncIterable [list ["StreamingContentMixin" ] | list [FunctionResult ]]:
148+ ) -> AsyncIterable [list ["StreamingContentMixin" ] | FunctionResult | list [FunctionResult ]]:
143149 """Execute one or more stream functions.
144150
145151 This will execute the functions in the order they are provided, if a list of functions is provided.
@@ -204,25 +210,33 @@ async def invoke_stream(
204210 return
205211 # TODO: decide how to put results into kernelarguments,
206212 # might need to be done as part of the invoked_handler
207- function_result = []
213+ function_result : FunctionResult | list [ list [ "StreamingContentMixin" ] | Any ] = []
208214 exception = None
209215
210216 async for stream_message in stream_function .invoke_stream (self , arguments ):
211217 if isinstance (stream_message , FunctionResult ):
212- exception = stream_message . metadata . get ( "exception" , None )
213- if exception :
214- break
218+ function_result = stream_message
219+ break
220+ assert isinstance ( function_result , list )
215221 function_result .append (stream_message )
216222 yield stream_message
217223
218- output_function_result = []
219- for result in function_result :
220- for choice in result :
221- if len (output_function_result ) <= choice .choice_index :
222- output_function_result .append (copy (choice ))
223- else :
224- output_function_result [choice .choice_index ] += choice
225- func_result = FunctionResult (function = stream_function .metadata , value = output_function_result )
224+ if isinstance (function_result , FunctionResult ):
225+ func_result = function_result
226+ exception = func_result .metadata .get ("exception" , None )
227+ else :
228+ output_function_result : list ["StreamingContentMixin" ] = []
229+ assert isinstance (function_result , list )
230+ for result in function_result :
231+ assert isinstance (result , list )
232+ for choice in result :
233+ if isinstance (choice , FunctionResult ):
234+ continue
235+ if len (output_function_result ) <= choice .choice_index :
236+ output_function_result .append (copy (choice ))
237+ else :
238+ output_function_result [choice .choice_index ] += choice
239+ func_result = FunctionResult (function = stream_function .metadata , value = output_function_result )
226240 function_invoked_args = self .on_function_invoked (
227241 stream_function .metadata ,
228242 arguments ,
@@ -235,7 +249,7 @@ async def invoke_stream(
235249 f"During function invocation:'{ stream_function .plugin_name } .{ stream_function .name } '. "
236250 f"Error description: '{ str (function_invoked_args .exception )} '"
237251 ) from function_invoked_args .exception
238- if return_function_results :
252+ if return_function_results and function_invoked_args . function_result :
239253 results .append (function_invoked_args .function_result )
240254 if function_invoked_args .is_cancel_requested :
241255 logger .info (
@@ -286,7 +300,7 @@ async def invoke(
286300 """
287301 if arguments is None :
288302 arguments = KernelArguments (** kwargs )
289- results = []
303+ results : list [ FunctionResult ] = []
290304 pipeline_step = 0
291305 if not functions :
292306 if not function_name or not plugin_name :
@@ -332,7 +346,10 @@ async def invoke(
332346
333347 # this allows a hook to alter the results before adding.
334348 function_invoked_args = self .on_function_invoked (func .metadata , arguments , function_result , exception )
335- results .append (function_invoked_args .function_result )
349+ if function_invoked_args .function_result :
350+ results .append (function_invoked_args .function_result )
351+ else :
352+ results .append (FunctionResult (function = func .metadata , value = None , metadata = {}))
336353
337354 if function_invoked_args .exception :
338355 raise KernelInvokeException (
@@ -343,7 +360,7 @@ async def invoke(
343360 f"Execution was cancelled on function invoked event of pipeline step "
344361 f"{ pipeline_step } : { func .plugin_name } .{ func .name } ."
345362 )
346- return results if results else None
363+ return results if results else FunctionResult ( function = func . metadata , value = None , metadata = {})
347364 if function_invoked_args .updated_arguments :
348365 logger .info (
349366 f"Arguments updated by function_invoked_handler in pipeline step: "
@@ -491,6 +508,7 @@ def import_plugin_from_object(self, plugin_instance: Any | dict[str, Any], plugi
491508 logger .debug (f"Importing plugin { plugin_name } " )
492509
493510 functions : dict [str , KernelFunction ] = {}
511+ candidates : list [tuple [str , MethodType ]] | ItemsView [str , Any ] = []
494512
495513 if isinstance (plugin_instance , dict ):
496514 candidates = plugin_instance .items ()
@@ -515,7 +533,9 @@ def import_plugin_from_object(self, plugin_instance: Any | dict[str, Any], plugi
515533
516534 return plugin
517535
518- def import_native_plugin_from_directory (self , parent_directory : str , plugin_directory_name : str ) -> KernelPlugin :
536+ def import_native_plugin_from_directory (
537+ self , parent_directory : str , plugin_directory_name : str
538+ ) -> KernelPlugin | None :
519539 MODULE_NAME = "native_function"
520540
521541 validate_plugin_name (plugin_directory_name )
@@ -529,7 +549,10 @@ def import_native_plugin_from_directory(self, parent_directory: str, plugin_dire
529549 plugin_name = os .path .basename (plugin_directory )
530550
531551 spec = importlib .util .spec_from_file_location (MODULE_NAME , native_py_file_path )
552+ if not spec :
553+ raise PluginInitializationError (f"Failed to load plugin: { plugin_name } " )
532554 module = importlib .util .module_from_spec (spec )
555+ assert spec .loader
533556 spec .loader .exec_module (module )
534557
535558 class_name = next (
@@ -616,7 +639,7 @@ def import_plugin_from_prompt_directory(self, parent_directory: str, plugin_dire
616639 prompt = prompt_file .read ()
617640 prompt_template_config .template = prompt
618641
619- prompt_template = TEMPLATE_FORMAT_MAP [prompt_template_config .template_format ](
642+ prompt_template = TEMPLATE_FORMAT_MAP [prompt_template_config .template_format ]( # type: ignore
620643 prompt_template_config = prompt_template_config
621644 )
622645
@@ -806,7 +829,7 @@ def get_service(
806829 self ,
807830 service_id : str | None = None ,
808831 type : Type [ALL_SERVICE_TYPES ] | None = None ,
809- ) -> ALL_SERVICE_TYPES :
832+ ) -> "AIServiceClientBase" :
810833 """Get a service by service_id and type.
811834
812835 Type is optional and when not supplied, no checks are done.
@@ -830,6 +853,7 @@ def get_service(
830853 ValueError: If no service is found that matches the type.
831854
832855 """
856+ service : "AIServiceClientBase | None" = None
833857 if not service_id or service_id == "default" :
834858 if not type :
835859 if default_service := self .services .get ("default" ):
@@ -848,11 +872,11 @@ def get_service(
848872 raise ServiceInvalidTypeError (f"Service with service_id '{ service_id } ' is not of type { type } " )
849873 return service
850874
851- def get_services_by_type (self , type : Type [T ]) -> dict [str , T ]:
875+ def get_services_by_type (self , type : Type [ALL_SERVICE_TYPES ]) -> dict [str , "AIServiceClientBase" ]:
852876 return {service .service_id : service for service in self .services .values () if isinstance (service , type )}
853877
854878 def get_prompt_execution_settings_from_service_id (
855- self , service_id : str , type : Type [T ] | None = None
879+ self , service_id : str , type : Type [ALL_SERVICE_TYPES ] | None = None
856880 ) -> PromptExecutionSettings :
857881 """Get the specific request settings from the service, instantiated with the service_id and ai_model_id."""
858882 service = self .get_service (service_id , type = type )
0 commit comments