semantic-kernel icon indicating copy to clipboard operation
semantic-kernel copied to clipboard

Python: implement Hooks

Open eavanvalkenburg opened this issue 10 months ago • 5 comments

Motivation and Context

This pull request includes significant changes across multiple files, mainly related to the addition of hooks and the modification of function invocations in the semantic_kernel module. The changes also include the addition of a new sample and a YAML file, and modifications to the __init__.py files.

Addition of Hooks:

  • python/semantic_kernel/hooks/__init__.py: Added new context classes, protocol classes, helpers (utils and const) and a hook filter decorator.

Other Additions and Modifications:

  • python/samples/kernel-syntax-examples/azure_chat_gpt_api_with_hooks.py: Added a new Python script that demonstrates the usage of hooks with the Azure Chat GPT API.

  • python/semantic_kernel/connectors/ai/open_ai/utils.py: Modified the _describe_tool_call and parse_param functions to handle more complex parameter types. [1] [2]

  • python/semantic_kernel/functions/kernel_function.py: Modified the invoke and invoke_stream methods to include pre and post hooks. [1] [2] [3] [4]

  • python/semantic_kernel/functions/kernel_function_from_prompt.py: Modified the _invoke_internal and _invoke_internal_stream methods to include pre and post prompt render hooks. [1] [2]

Removals:

  • python/semantic_kernel/events: Removed the previously used events.

New Exceptions:

  • python/semantic_kernel/exceptions/kernel_exceptions.py: Added new exception classes OperationCancelledException and HookInvalidSignatureError. [1] [2]

Changes to Function Decorator:

  • python/semantic_kernel/functions/kernel_function_decorator.py: Modified the _parse_internal_annotation function to handle list and dict types.

Fixes: #3038

Contribution Checklist

eavanvalkenburg avatar Mar 27 '24 16:03 eavanvalkenburg

Py3.8 Test Coverage

Python 3.8 Test Coverage Report •
FileStmtsMissCoverMissing
semantic_kernel
   kernel.py2612889%122, 124, 182–185, 187–191, 193–196, 203, 210–219, 254–257, 267–272, 277, 281–284, 466–467
semantic_kernel/connectors/ai/open_ai
   utils.py462154%27–28, 51–55, 62–63, 104, 142, 144, 146, 148, 150, 155–162
semantic_kernel/events
   function_invoking_event_args.py6183%35
   kernel_events_args.py14379%38, 41–42
semantic_kernel/functions
   function_result.py30970%45, 47–48, 58–63
   kernel_function.py72297%141, 160
   kernel_function_decorator.py67199%101
   kernel_function_from_prompt.py162498%170, 198, 299, 369
TOTAL5545101082% 

Python 3.8 Unit Test Overview

Tests Skipped Failures Errors Time
1211 11 :zzz: 0 :x: 0 :fire: 20.899s :stopwatch:

markwallace-microsoft avatar Mar 28 '24 10:03 markwallace-microsoft

@eavanvalkenburg @moonbox3. I have a general feedback on the hooks implementation. From my point of view as an LLM developer, the most common use case for this is to be able to get the token usage after invoking a semantic function or executing an agent (with multiple tools invocations).

I feel that the mechanism mentioned in the example is somehwat complicated, specially for the inexperienced python developers. I would recommend adopting the with-statement context as it is done in Langchain. So, getting the tokens after an LLM call is as easy as this:

        with get_openai_callback() as callback:
            response = agent.invoke({"input": "What is 2+2"})
            print(f"Prompt Tokens: {callback.prompt_tokens}")
            print(f"Completion Tokens: {callback.completion_tokens}")
            print(f"Total Tokens: {callback.total_tokens}")

This allows new developers to use the 'with' statement and default callback implementation for common use cases like getting tokens but more experienced developers can implement their custom callback functions to do more sophisticated stuff.

bashimr avatar Apr 17 '24 13:04 bashimr

@eavanvalkenburg @moonbox3. I have a general feedback on the hooks implementation. From my point of view as an LLM developer, the most common use case for this is to be able to get the token usage after invoking a semantic function or executing an agent (with multiple tools invocations).

I feel that the mechanism mentioned in the example is unnecessarily complicated, specially for the inexperienced python developers. I would recommend adopting the with-statement context as it is done in Langchain. So, getting the tokens after an LLM call is as easy as this:

        with get_openai_callback() as callback:
            response = agent.invoke({"input": "What is 2+2"})
            print(f"Prompt Tokens: {callback.prompt_tokens}")
            print(f"Completion Tokens: {callback.completion_tokens}")
            print(f"Total Tokens: {callback.total_tokens}")

thanks for commenting @bashimr, which new example did you look at?

the latest is the one with function_filter, which is aligned with how the filters are being implemented in dotnet, that will make the above look like this:


class Filter:
  async def function_filter(context, next):
    await next(context)
    if context.result:
      print(f"Prompt tokens: {context.result.metadata.get('usage', {}).get('prompt_tokens', 'unknown')}")
      ...

I would love to hear your thoughts on that one!

FYI @matthewbolanos

eavanvalkenburg avatar Apr 17 '24 13:04 eavanvalkenburg

@eavanvalkenburg @moonbox3. I have a general feedback on the hooks implementation. From my point of view as an LLM developer, the most common use case for this is to be able to get the token usage after invoking a semantic function or executing an agent (with multiple tools invocations). I feel that the mechanism mentioned in the example is unnecessarily complicated, specially for the inexperienced python developers. I would recommend adopting the with-statement context as it is done in Langchain. So, getting the tokens after an LLM call is as easy as this:

        with get_openai_callback() as callback:
            response = agent.invoke({"input": "What is 2+2"})
            print(f"Prompt Tokens: {callback.prompt_tokens}")
            print(f"Completion Tokens: {callback.completion_tokens}")
            print(f"Total Tokens: {callback.total_tokens}")

thanks for commenting @bashimr, which new example did you look at?

the latest is the one with function_filter, which is aligned with how the filters are being implemented in dotnet, that will make the above look like this:

class Filter:
  async def function_filter(context, next):
    await next(context)
    if context.result:
      print(f"Prompt tokens: {context.result.metadata.get('usage', {}).get('prompt_tokens', 'unknown')}")
      ...

I would love to hear your thoughts on that one!

FYI @matthewbolanos

@eavanvalkenburg. Thanks for pointing this out. This is simpler to implement. But, to me, it does not feel very Pythonic. Looks more like dotnet paradigm which probably makes sense as the goal is to achieve parity with dotnet. Python developers are very familiar with 'with-statement' so Langchain's way of implementing callback to get token counts and doing other operations feels a bit more natural from Python's perspective.

bashimr avatar Apr 17 '24 15:04 bashimr

Py3.10 Test Coverage

Python 3.10 Test Coverage Report •
FileStmtsMissCoverMissing
semantic_kernel
   kernel.py2625081%123, 125, 191, 198–207, 237, 247–255, 327–363, 433–434, 661, 682–684, 720, 722, 732
semantic_kernel/connectors/ai/open_ai/services
   azure_config_base.py34488%73–75, 90
   open_ai_chat_completion_base.py2119356%97, 117, 142–146, 170, 174, 190–195, 211–240, 243–254, 272–279, 290–298, 314–321, 342, 350, 356–362, 374–380, 411, 450, 452–453, 458–463, 467–474, 496, 505–514
semantic_kernel/contents
   function_result_content.py571181%51, 56, 66, 81, 106, 111, 118–122
semantic_kernel/functions
   kernel_function.py83298%146, 166
   kernel_function_from_method.py81495%145–146, 152–153
   kernel_function_from_prompt.py157796%149–150, 171, 191, 216, 237, 317
semantic_kernel/planners/function_calling_stepwise_planner
   function_calling_stepwise_planner.py1073964%126–211, 245–250
semantic_kernel/prompt_template/utils
   template_function_helpers.py31197%35
TOTAL6172103283% 

Python 3.10 Unit Test Overview

Tests Skipped Failures Errors Time
1328 1 :zzz: 0 :x: 0 :fire: 12.639s :stopwatch:

markwallace-microsoft avatar May 15 '24 19:05 markwallace-microsoft