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'."
+    )

Reply via email to