Python: fixes for hugging face and working integration tests (#4927)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
Fixes #4652 
and reenabled integration tests for hugging face
found we never added the prompt execution settings class into the right
place in the HF class, fixed now.

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
This commit is contained in:
Eduard van Valkenburg
2024-02-09 13:33:52 +00:00
committed by GitHub
parent 8d5417486f
commit 9ec32347ed
9 changed files with 133 additions and 376 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: |
python -m pip install poetry pytest
cd python
poetry install --without chromadb --without hugging_face --without azure_cognitive_search --without weaviate --without pinecone --without postgres --without qdrant --without redis
poetry install --without chromadb --without azure_cognitive_search --without weaviate --without pinecone --without postgres --without qdrant --without redis
- name: Test with pytest
run: |
cd python && poetry run pytest ./tests/unit
+2
View File
@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true
@@ -26,15 +26,10 @@ class HuggingFacePromptExecutionSettings(PromptExecutionSettings):
def prepare_settings_dict(self, **kwargs) -> Dict[str, Any]:
gen_config = self.get_generation_config()
if "prompt" in kwargs and kwargs["prompt"] is not None:
return {
"text_inputs": kwargs["prompt"],
"generation_config": gen_config,
"num_return_sequences": self.num_return_sequences,
"do_sample": self.do_sample,
}
return {
settings = {
"generation_config": gen_config,
"num_return_sequences": self.num_return_sequences,
"do_sample": self.do_sample,
}
settings.update(kwargs)
return settings
@@ -2,10 +2,10 @@
import logging
from threading import Thread
from typing import Any, AsyncIterable, Dict, List, Literal, Optional
from typing import TYPE_CHECKING, Any, AsyncIterable, Dict, List, Literal, Optional
import torch
import transformers
from transformers import AutoTokenizer, TextIteratorStreamer, pipeline
from semantic_kernel.connectors.ai.ai_exception import AIException
from semantic_kernel.connectors.ai.ai_service_client_base import AIServiceClientBase
@@ -18,6 +18,9 @@ from semantic_kernel.connectors.ai.text_completion_client_base import (
from semantic_kernel.models.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.models.contents.text_content import TextContent
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
@@ -31,7 +34,6 @@ class HuggingFaceTextCompletion(TextCompletionClientBase, AIServiceClientBase):
ai_model_id: str,
task: Optional[str] = "text2text-generation",
device: Optional[int] = -1,
log: Optional[Any] = None,
model_kwargs: Optional[Dict[str, Any]] = None,
pipeline_kwargs: Optional[Dict[str, Any]] = None,
) -> None:
@@ -59,20 +61,19 @@ class HuggingFaceTextCompletion(TextCompletionClientBase, AIServiceClientBase):
Note that this model will be downloaded from the Hugging Face model hub.
"""
generator = pipeline(
task=task,
model=ai_model_id,
device=device,
model_kwargs=model_kwargs,
**pipeline_kwargs or {},
)
super().__init__(
ai_model_id=ai_model_id,
task=task,
device=(f"cuda:{device}" if device >= 0 and torch.cuda.is_available() else "cpu"),
generator=transformers.pipeline(
task=task,
model=ai_model_id,
device=device,
model_kwargs=model_kwargs,
**pipeline_kwargs or {},
),
generator=generator,
)
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
async def complete(
self,
@@ -93,7 +94,7 @@ class HuggingFaceTextCompletion(TextCompletionClientBase, AIServiceClientBase):
if kwargs.get("logger"):
logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.")
try:
results = self.generator(**settings.prepare_settings_dict(prompt=prompt))
results = self.generator(prompt, **settings.prepare_settings_dict())
except Exception as e:
raise AIException("Hugging Face completion failed", e)
if isinstance(results, list):
@@ -133,18 +134,11 @@ class HuggingFaceTextCompletion(TextCompletionClientBase, AIServiceClientBase):
If you need multiple responses, please use the complete method.",
)
try:
tokenizer = transformers.AutoTokenizer.from_pretrained(self.ai_model_id)
streamer = transformers.TextIteratorStreamer(tokenizer)
args = {prompt}
kwargs = {
"num_return_sequences": settings.num_return_sequences,
"generation_config": settings.get_generation_config(),
"streamer": streamer,
"do_sample": settings.do_sample,
}
streamer = TextIteratorStreamer(AutoTokenizer.from_pretrained(self.ai_model_id))
# See https://github.com/huggingface/transformers/blob/main/src/transformers/generation/streamers.py#L159
thread = Thread(target=self.generator, args=args, kwargs=kwargs)
thread = Thread(
target=self.generator, args={prompt}, kwargs=settings.prepare_settings_dict(streamer=streamer)
)
thread.start()
for new_text in streamer:
@@ -158,3 +152,7 @@ class HuggingFaceTextCompletion(TextCompletionClientBase, AIServiceClientBase):
except Exception as e:
raise AIException("Hugging Face completion failed", e)
def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings":
"""Create a request settings object."""
return HuggingFacePromptExecutionSettings
@@ -4,77 +4,10 @@ import sys
import pytest
import semantic_kernel.connectors.ai.hugging_face as sk_hf
if sys.version_info >= (3, 9):
import semantic_kernel.connectors.ai.google_palm as sk_gp
@pytest.fixture(
scope="module",
params=[
("google/flan-t5-base", "text2text-generation"),
("facebook/bart-large-cnn", "summarization"),
],
)
def setup_hf_text_completion_function(create_kernel, request):
kernel = create_kernel
# Configure LLM service
kernel.add_text_completion_service(
request.param[0],
sk_hf.HuggingFaceTextCompletion(ai_model_id=request.param[0], task=request.param[1]),
)
# Define semantic function using SK prompt template language
sk_prompt = "Hello, I like {{$input}}{{$input2}}"
# Create the semantic function
text2text_function = kernel.create_semantic_function(sk_prompt, max_tokens=25, temperature=0.7, top_p=0.5)
# User input
simple_input = "sleeping and "
yield kernel, text2text_function, simple_input
@pytest.fixture(scope="module")
def setup_summarize_function(create_kernel):
# User input (taken from https://en.wikipedia.org/wiki/Whale)
text_to_summarize = """
Whales are fully aquatic, open-ocean animals:
they can feed, mate, give birth, suckle and raise their young at sea.
Whales range in size from the 2.6 metres (8.5 ft) and 135 kilograms (298 lb)
dwarf sperm whale to the 29.9 metres (98 ft) and 190 tonnes (210 short tons) blue whale,
which is the largest known animal that has ever lived. The sperm whale is the largest
toothed predator on Earth. Several whale species exhibit sexual dimorphism,
in that the females are larger than males.
"""
additional_text = """
The word "whale" comes from the Old English hwæl, from Proto-Germanic *hwalaz,
from Proto-Indo-European *(s)kwal-o-, meaning "large sea fish".[3][4]
The Proto-Germanic *hwalaz is also the source of Old Saxon hwal,
Old Norse hvalr, hvalfiskr, Swedish val, Middle Dutch wal, walvisc, Dutch walvis,
Old High German wal, and German Wal.[3] Other archaic English forms include wal,
wale, whal, whalle, whaille, wheal, etc.[5]
"""
# Define semantic function using SK prompt template language
sk_prompt = "{{$input}} {{$input2}}"
kernel = create_kernel
# Configure LLM service
kernel.add_text_completion_service(
"facebook/bart-large-cnn",
sk_hf.HuggingFaceTextCompletion(ai_model_id="facebook/bart-large-cnn", task="summarization"),
)
# Create the semantic function
summarize_function = kernel.create_semantic_function(sk_prompt, max_tokens=80, temperature=0, top_p=0.5)
yield kernel, summarize_function, text_to_summarize, additional_text
@pytest.fixture(scope="module")
def setup_tldr_function_for_oai_models(create_kernel):
kernel = create_kernel
@@ -1,147 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from transformers import AutoTokenizer
import semantic_kernel as sk
import semantic_kernel.connectors.ai.hugging_face as sk_hf
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text2text_generation_input_str(setup_hf_text_completion_function):
kernel, text2text_function, simple_input = setup_hf_text_completion_function
# Complete input string and print
summary = await kernel.run(text2text_function, input_str=simple_input)
output = str(summary).strip()
print(f"Completion using input string: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text2text_generation_input_vars(setup_hf_text_completion_function):
kernel, text2text_function, simple_input = setup_hf_text_completion_function
# Complete input as context variable and print
context_vars = sk.ContextVariables(simple_input)
summary = await kernel.run(text2text_function, input_vars=context_vars)
output = str(summary).strip()
print(f"Completion using context variables: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text2text_generation_input_context(setup_hf_text_completion_function):
kernel, text2text_function, simple_input = setup_hf_text_completion_function
# Complete input context and print
context = kernel.create_new_context()
context["input"] = simple_input
summary = await kernel.run(text2text_function, input_context=context)
output = str(summary).strip()
print(f"Completion using input context: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text2text_generation_input_context_with_vars(
setup_hf_text_completion_function,
):
kernel, text2text_function, simple_input = setup_hf_text_completion_function
# Complete input context with additional variables and print
context = kernel.create_new_context()
context["input"] = simple_input
context_vars = sk.ContextVariables("running and")
summary = await kernel.run(text2text_function, input_context=context, input_vars=context_vars)
output = str(summary).strip()
print(f"Completion using context and additional variables: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text2text_generation_input_context_with_str(
setup_hf_text_completion_function,
):
kernel, text2text_function, simple_input = setup_hf_text_completion_function
# Complete input context with additional input string and print
context = kernel.create_new_context()
context["input"] = simple_input
summary = await kernel.run(text2text_function, input_context=context, input_str="running and")
output = str(summary).strip()
print(f"Completion using context and additional string: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text2text_generation_input_context_with_vars_and_str(
setup_hf_text_completion_function,
):
kernel, text2text_function, simple_input = setup_hf_text_completion_function
# Complete input context with additional variables and string and print
context = kernel.create_new_context()
context["input"] = simple_input
context_vars = sk.ContextVariables(variables={"input2": "running and"})
summary = await kernel.run(
text2text_function,
input_context=context,
input_vars=context_vars,
input_str="new text",
)
output = str(summary).strip()
print(f"Completion using context, additional variables, and additional string: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_text_generation_with_kwargs():
simple_input = "sleeping and "
model_name = "google/flan-t5-base"
tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model_name, trust_remote_code=True)
hf_model = sk_hf.HuggingFaceTextCompletion(
model_name,
task="text2text-generation",
model_kwargs={"repetition_penalty": 0.2},
pipeline_kwargs={"tokenizer": tokenizer, "trust_remote_code": True},
)
kernel = sk.Kernel()
# Configure LLM service
kernel.add_text_completion_service("hf-local", hf_model)
# Define semantic function using SK prompt template language
sk_prompt = "Hello, I like {{$input}}{{$input2}}"
text2text_function = kernel.create_semantic_function(sk_prompt, max_tokens=25, temperature=0.2, top_p=0.5)
# Complete input context with additional variables and string and print
context = kernel.create_new_context()
context["input"] = simple_input
context_vars = sk.ContextVariables(variables={"input2": "running and"})
summary = await kernel.run(
text2text_function,
input_context=context,
input_vars=context_vars,
input_str="new text",
)
output = str(summary).strip()
print(f"Completion using context, additional variables, and additional string: '{output}'")
assert len(output) > 0
@@ -1,129 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
import semantic_kernel as sk
@pytest.mark.asyncio
@pytest.mark.skip
async def test_summarize_input_str(setup_summarize_function):
(
kernel,
summarize_function,
text_to_summarize,
additional_text,
) = setup_summarize_function
# Summarize input string and print
summary = await kernel.run(summarize_function, input_str=text_to_summarize)
output = str(summary).strip()
print(f"Summary using input string: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_summarize_input_vars(setup_summarize_function):
(
kernel,
summarize_function,
text_to_summarize,
additional_text,
) = setup_summarize_function
# Summarize input as context variable and print
context_vars = sk.ContextVariables(text_to_summarize)
summary = await kernel.run(summarize_function, input_vars=context_vars)
output = str(summary).strip()
print(f"Summary using context variables: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_summarize_input_context(setup_summarize_function):
(
kernel,
summarize_function,
text_to_summarize,
additional_text,
) = setup_summarize_function
# Summarize input context and print
context = kernel.create_new_context()
context["input"] = text_to_summarize
summary = await kernel.run(summarize_function, input_context=context)
output = str(summary).strip()
print(f"Summary using input context: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_summarize_input_context_with_vars(setup_summarize_function):
(
kernel,
summarize_function,
text_to_summarize,
additional_text,
) = setup_summarize_function
# Summarize input context with additional variables and print
context = kernel.create_new_context()
context["input"] = text_to_summarize
context_vars = sk.ContextVariables(additional_text)
summary = await kernel.run(summarize_function, input_context=context, input_vars=context_vars)
output = str(summary).strip()
print(f"Summary using context and additional variables: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_summarize_input_context_with_str(setup_summarize_function):
(
kernel,
summarize_function,
text_to_summarize,
additional_text,
) = setup_summarize_function
# Summarize input context with additional input string and print
context = kernel.create_new_context()
context["input"] = text_to_summarize
summary = await kernel.run(summarize_function, input_context=context, input_str=additional_text)
output = str(summary).strip()
print(f"Summary using context and additional string: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.skip
async def test_summarize_input_context_with_vars_and_str(setup_summarize_function):
(
kernel,
summarize_function,
text_to_summarize,
additional_text,
) = setup_summarize_function
# Summarize input context with additional variables and string and print
context = kernel.create_new_context()
context["input"] = text_to_summarize
context_vars = sk.ContextVariables(variables={"input2": additional_text})
summary = await kernel.run(
summarize_function,
input_context=context,
input_vars=context_vars,
input_str="new text",
)
output = str(summary).strip()
print(f"Summary using context, additional variables, and additional string: '{output}'")
assert len(output) > 0
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
import semantic_kernel.connectors.ai.hugging_face as sk_hf
from semantic_kernel.kernel import Kernel
@pytest.mark.asyncio
@pytest.mark.parametrize(
("model_name", "task", "input_str"),
[
(
"patrickvonplaten/t5-tiny-random",
"text2text-generation",
"translate English to Dutch: Hello, how are you?",
),
(
"jotamunz/billsum_tiny_summarization",
"summarization",
"""
Summarize: Whales are fully aquatic, open-ocean animals:
they can feed, mate, give birth, suckle and raise their young at sea.
Whales range in size from the 2.6 metres (8.5 ft) and 135 kilograms (298 lb)
dwarf sperm whale to the 29.9 metres (98 ft) and 190 tonnes (210 short tons) blue whale,
which is the largest known animal that has ever lived. The sperm whale is the largest
toothed predator on Earth. Several whale species exhibit sexual dimorphism,
in that the females are larger than males.
""",
),
("HuggingFaceM4/tiny-random-LlamaForCausalLM", "text-generation", "Hello, I like sleeping and "),
],
ids=["text2text-generation", "summarization", "text-generation"],
)
async def test_text_completion(model_name, task, input_str):
kernel = Kernel()
# Configure LLM service
kernel.add_text_completion_service(
service_id=model_name,
service=sk_hf.HuggingFaceTextCompletion(ai_model_id=model_name, task=task),
)
# Define semantic function using SK prompt template language
sk_prompt = "{{$input}}"
# Create the semantic function
function = kernel.create_semantic_function(sk_prompt, max_tokens=25, temperature=0.7, top_p=0.5)
summary = await kernel.run(function, input_str=input_str)
output = str(summary).strip()
print(f"Completion using input string: '{output}'")
assert len(output) > 0
@pytest.mark.asyncio
@pytest.mark.parametrize(
("model_name", "task", "input_str"),
[
(
"patrickvonplaten/t5-tiny-random",
"text2text-generation",
"translate English to Dutch: Hello, how are you?",
),
(
"jotamunz/billsum_tiny_summarization",
"summarization",
"""
Summarize: Whales are fully aquatic, open-ocean animals:
they can feed, mate, give birth, suckle and raise their young at sea.
Whales range in size from the 2.6 metres (8.5 ft) and 135 kilograms (298 lb)
dwarf sperm whale to the 29.9 metres (98 ft) and 190 tonnes (210 short tons) blue whale,
which is the largest known animal that has ever lived. The sperm whale is the largest
toothed predator on Earth. Several whale species exhibit sexual dimorphism,
in that the females are larger than males.
""",
),
# skipped for now, as it takes too long
("HuggingFaceM4/tiny-random-LlamaForCausalLM", "text-generation", "Hello, I like sleeping and "),
],
ids=["text2text-generation", "summarization", "text-generation"],
)
async def test_text_completion_stream(model_name, task, input_str):
kernel = Kernel()
# Configure LLM service
kernel.add_text_completion_service(
service_id=model_name,
service=sk_hf.HuggingFaceTextCompletion(ai_model_id=model_name, task=task),
)
# Define semantic function using SK prompt template language
sk_prompt = "{{$input}}"
# Create the semantic function
function = kernel.create_semantic_function(sk_prompt, max_tokens=25, temperature=0.7, top_p=0.5)
summary = ""
async for text in kernel.run_stream(function, input_str=input_str):
summary += str(text[0])
output = str(summary).strip()
print(f"Completion using input string: '{output}'")
assert len(output) > 0