diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml index ba58264325..39f722e5ba 100644 --- a/.github/workflows/python-unit-tests.yml +++ b/.github/workflows/python-unit-tests.yml @@ -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 diff --git a/python/poetry.toml b/python/poetry.toml new file mode 100644 index 0000000000..ab1033bd37 --- /dev/null +++ b/python/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py b/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py index 5bed30a9a5..d4e9c1067e 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py @@ -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 diff --git a/python/semantic_kernel/connectors/ai/hugging_face/services/__init__.py b/python/semantic_kernel/connectors/ai/hugging_face/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py index 27efeb2eb0..9e14ad65b6 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py @@ -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 diff --git a/python/tests/integration/completions/conftest.py b/python/tests/integration/completions/conftest.py index 9a6530646b..72096efeb4 100644 --- a/python/tests/integration/completions/conftest.py +++ b/python/tests/integration/completions/conftest.py @@ -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 diff --git a/python/tests/integration/completions/test_hf_local_text_completions.py b/python/tests/integration/completions/test_hf_local_text_completions.py deleted file mode 100644 index a03acda585..0000000000 --- a/python/tests/integration/completions/test_hf_local_text_completions.py +++ /dev/null @@ -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 diff --git a/python/tests/integration/completions/test_hf_local_text_summarization.py b/python/tests/integration/completions/test_hf_local_text_summarization.py deleted file mode 100644 index 21dad2f1e7..0000000000 --- a/python/tests/integration/completions/test_hf_local_text_summarization.py +++ /dev/null @@ -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 diff --git a/python/tests/unit/ai/hugging_face/test_hf_local_text_completions.py b/python/tests/unit/ai/hugging_face/test_hf_local_text_completions.py new file mode 100644 index 0000000000..024f0a5e70 --- /dev/null +++ b/python/tests/unit/ai/hugging_face/test_hf_local_text_completions.py @@ -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