This is an automated email from the ASF dual-hosted git repository. xtsong pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/flink-agents.git
commit 66ed0d05ac37adc606316ad76850700cf3562ba3 Author: WenjinXie <wenjin...@gmail.com> AuthorDate: Tue Aug 5 10:19:41 2025 +0800 [api][python] Introduce Prompt in python. --- python/flink_agents/api/chat_message.py | 69 +++++++++++++++ python/flink_agents/api/prompts/__init__.py | 17 ++++ python/flink_agents/api/prompts/prompt.py | 81 ++++++++++++++++++ python/flink_agents/api/prompts/utils.py | 38 +++++++++ python/flink_agents/api/tests/test_prompt.py | 123 +++++++++++++++++++++++++++ 5 files changed, 328 insertions(+) diff --git a/python/flink_agents/api/chat_message.py b/python/flink_agents/api/chat_message.py new file mode 100644 index 0000000..edad466 --- /dev/null +++ b/python/flink_agents/api/chat_message.py @@ -0,0 +1,69 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################# +from enum import Enum +from typing import Any, Dict, List + +from pydantic import BaseModel, Field + + +class MessageRole(str, Enum): + """Message role. + + Attributes: + ---------- + SYSTEM : str + Used to tell the chat model how to behave and provide additional context. + USER : str + Represents input from a user interacting with the model. + ASSISTANT : str + Represents a response from the model, which can include text or a + request to invoke tools. + TOOL : str + A message used to pass the results of a tools invocation back to the model. + """ + + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +class ChatMessage(BaseModel): + """Chat message. + + ChatMessages are the inputs and outputs of ChatModels. + + Attributes: + ---------- + role : MessageRole + The message productor or purpose. + content : str + The content of the message. + tool_calls: List[Dict[str, Any]] + The tools call information. + extra_args : dict[str, Any] + Additional information about the message. + """ + + role: MessageRole = MessageRole.USER + content: str = Field(default_factory=str) + tool_calls: List[Dict[str, Any]] = Field(default_factory=list) + extra_args: Dict[str, Any] = Field(default_factory=dict) + + def __str__(self) -> str: + return f"{self.role.value}: {self.content}" diff --git a/python/flink_agents/api/prompts/__init__.py b/python/flink_agents/api/prompts/__init__.py new file mode 100644 index 0000000..e154fad --- /dev/null +++ b/python/flink_agents/api/prompts/__init__.py @@ -0,0 +1,17 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################# diff --git a/python/flink_agents/api/prompts/prompt.py b/python/flink_agents/api/prompts/prompt.py new file mode 100644 index 0000000..1635606 --- /dev/null +++ b/python/flink_agents/api/prompts/prompt.py @@ -0,0 +1,81 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################# +from typing import List, Sequence, Union + +from flink_agents.api.chat_message import ChatMessage, MessageRole +from flink_agents.api.prompts.utils import FORMATTER +from flink_agents.api.resource import ResourceType, SerializableResource + + +class Prompt(SerializableResource): + """Prompt for a language model. + + Attributes: + ---------- + template : Union[Sequence[ChatMessage], str] + The prompt template. + """ + + template: Union[Sequence[ChatMessage], str] + + @staticmethod + def from_messages(name: str, messages: Sequence[ChatMessage]) -> "Prompt": + """Create prompt from sequence of ChatMessage.""" + return Prompt(name=name, template=messages) + + @staticmethod + def from_text(name: str, text: str) -> "Prompt": + """Create prompt from text string.""" + return Prompt(name=name, template=text) + + @classmethod + def resource_type(cls) -> ResourceType: + """Get the resource type.""" + return ResourceType.PROMPT + + def format_string(self, **kwargs: str) -> str: + """Generate text string from template with input arguments.""" + if isinstance(self.template, str): + return FORMATTER.format(self.template, **kwargs) + else: + msgs = [] + for m in self.template: + msg = f"{m.role.value}: {FORMATTER.format(m.content, **kwargs)}" + if m.extra_args is not None and len(m.extra_args) > 0: + msg += f"{m.extra_args}" + msgs.append(msg) + return "\n".join(msgs) + + def format_messages( + self, role: MessageRole = MessageRole.SYSTEM, **kwargs: str + ) -> List[ChatMessage]: + """Generate list of ChatMessage from template with input arguments.""" + if isinstance(self.template, str): + return [ + ChatMessage( + role=role, content=FORMATTER.format(self.template, **kwargs) + ) + ] + else: + msgs = [] + for m in self.template: + msg = ChatMessage( + role=m.role, content=FORMATTER.format(m.content, **kwargs) + ) + msgs.append(msg) + return msgs diff --git a/python/flink_agents/api/prompts/utils.py b/python/flink_agents/api/prompts/utils.py new file mode 100644 index 0000000..527fdd5 --- /dev/null +++ b/python/flink_agents/api/prompts/utils.py @@ -0,0 +1,38 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################# +from string import Formatter +from typing import Any + +from typing_extensions import override + + +class SafeFormatter(Formatter): + """Safe string formatter that does not raise KeyError if key is missing.""" + + @override + def get_value(self, key: Any, args: Any, kwargs: Any) -> Any: + if isinstance(key, int): + return args[key] + else: + if key in kwargs: + return kwargs[key] + else: + return str(key) + + +FORMATTER = SafeFormatter() diff --git a/python/flink_agents/api/tests/test_prompt.py b/python/flink_agents/api/tests/test_prompt.py new file mode 100644 index 0000000..de9996a --- /dev/null +++ b/python/flink_agents/api/tests/test_prompt.py @@ -0,0 +1,123 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################# +import pytest + +from flink_agents.api.chat_message import ChatMessage, MessageRole +from flink_agents.api.prompts.prompt import Prompt + + +@pytest.fixture(scope="module") +def text_prompt() -> Prompt: # noqa: D103 + template = ( + "You ara a product review analyzer, please generate a score and the dislike reasons" + "(if any) for the review. " + "The product {product_id} is {description}, and user review is '{review}'." + ) + + return Prompt.from_text(name="prompt", text=template) + + +def test_prompt_from_text_to_string(text_prompt: Prompt) -> None: # noqa: D103 + assert text_prompt.format_string( + product_id="12345", + description="wireless noise-canceling headphones with 20-hour battery life", + review="The headphones broke after one week of use. Very poor quality", + ) == ( + "You ara a product review analyzer, please generate a score and the " + "dislike reasons(if any) for the review. The product 12345 is wireless " + "noise-canceling headphones with 20-hour battery life, and user review is " + "'The headphones broke after one week of use. Very poor quality'." + ) + + +def test_prompt_from_text_to_messages(text_prompt: Prompt) -> None: # noqa: D103 + assert text_prompt.format_messages( + product_id="12345", + description="wireless noise-canceling headphones with 20-hour battery life", + review="The headphones broke after one week of use. Very poor quality", + ) == [ + ChatMessage( + role=MessageRole.SYSTEM, + content="You ara a product review analyzer, please generate a score and the " + "dislike reasons(if any) for the review. The product 12345 is wireless " + "noise-canceling headphones with 20-hour battery life, and user review is " + "'The headphones broke after one week of use. Very poor quality'.", + ) + ] + + +@pytest.fixture(scope="module") +def messages_prompt() -> Prompt: # noqa: D103 + template = [ + ChatMessage( + role=MessageRole.SYSTEM, + content="You ara a product review analyzer, please generate a score and the dislike reasons" + "(if any) for the review.", + ), + ChatMessage( + role=MessageRole.USER, + content="The product {product_id} is {description}, and user review is '{review}'.", + ), + ] + + return Prompt.from_messages(name="prompt", messages=template) + + +def test_prompt_from_messages_to_string(messages_prompt: Prompt) -> None: # noqa: D103 + assert messages_prompt.format_string( + product_id="12345", + description="wireless noise-canceling headphones with 20-hour battery life", + review="The headphones broke after one week of use. Very poor quality", + ) == ( + "system: You ara a product review analyzer, please generate a score and the " + "dislike reasons(if any) for the review.\n" + "user: The product 12345 is wireless " + "noise-canceling headphones with 20-hour battery life, and user review is " + "'The headphones broke after one week of use. Very poor quality'." + ) + + +def test_prompt_from_messages_to_messages(messages_prompt: Prompt) -> None: # noqa: D103 + assert messages_prompt.format_messages( + product_id="12345", + description="wireless noise-canceling headphones with 20-hour battery life", + review="The headphones broke after one week of use. Very poor quality", + ) == [ + ChatMessage( + role=MessageRole.SYSTEM, + content="You ara a product review analyzer, please generate a score and the " + "dislike reasons(if any) for the review.", + ), + ChatMessage( + role=MessageRole.USER, + content="The product 12345 is wireless " + "noise-canceling headphones with 20-hour battery life, and user review is " + "'The headphones broke after one week of use. Very poor quality'.", + ), + ] + + +def test_prompt_lack_one_argument(text_prompt: Prompt) -> None: #noqa: D103 + assert text_prompt.format_string( + product_id="12345", + review="The headphones broke after one week of use. Very poor quality", + ) == ( + "You ara a product review analyzer, please generate a score and the " + "dislike reasons(if any) for the review. The product 12345 is description, " + "and user review is 'The headphones broke after one week of use. Very poor quality'." + )