From 15e7e5b0e7ef2b9600ad96a873ad2f6d9e74105d Mon Sep 17 00:00:00 2001 From: WenjiaoYue Date: Mon, 7 Jul 2025 17:37:57 +0800 Subject: [PATCH 1/2] Upstream E-RAG's prompt template microservice. Signed-off-by: WenjiaoYue --- .../compose/prompt_template-compose.yaml | 9 + comps/cores/mega/constants.py | 2 +- .../deployment/docker_compose/README.md | 0 .../deployment/docker_compose/compose.yaml | 19 ++ .../deployment/kubernetes/README.md | 0 comps/prompt_template/src/Dockerfile | 34 +++ comps/prompt_template/src/README.md | 199 ++++++++++++ .../src/integrations/__init__.py | 2 + .../src/integrations/native.py | 283 ++++++++++++++++++ .../utils/conversation_history_handler.py | 46 +++ .../src/integrations/utils/prompt.py | 26 ++ .../src/integrations/utils/templates.py | 17 ++ .../src/opea_prompt_template_microservice.py | 72 +++++ comps/prompt_template/src/requirements.in | 18 ++ comps/prompt_template/src/requirements.txt | 272 +++++++++++++++++ tests/prompt_template/test_prompt_template.sh | 129 ++++++++ 16 files changed, 1127 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker/compose/prompt_template-compose.yaml create mode 100644 comps/prompt_template/deployment/docker_compose/README.md create mode 100644 comps/prompt_template/deployment/docker_compose/compose.yaml create mode 100644 comps/prompt_template/deployment/kubernetes/README.md create mode 100644 comps/prompt_template/src/Dockerfile create mode 100644 comps/prompt_template/src/README.md create mode 100644 comps/prompt_template/src/integrations/__init__.py create mode 100644 comps/prompt_template/src/integrations/native.py create mode 100644 comps/prompt_template/src/integrations/utils/conversation_history_handler.py create mode 100644 comps/prompt_template/src/integrations/utils/prompt.py create mode 100644 comps/prompt_template/src/integrations/utils/templates.py create mode 100644 comps/prompt_template/src/opea_prompt_template_microservice.py create mode 100644 comps/prompt_template/src/requirements.in create mode 100644 comps/prompt_template/src/requirements.txt create mode 100644 tests/prompt_template/test_prompt_template.sh diff --git a/.github/workflows/docker/compose/prompt_template-compose.yaml b/.github/workflows/docker/compose/prompt_template-compose.yaml new file mode 100644 index 0000000000..62b7dd770e --- /dev/null +++ b/.github/workflows/docker/compose/prompt_template-compose.yaml @@ -0,0 +1,9 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# this file should be run in the root of the repo +services: + prompt-template: + build: + dockerfile: comps/prompt_template/src/Dockerfile + image: ${REGISTRY:-opea}/prompt-template:${TAG:-latest} diff --git a/comps/cores/mega/constants.py b/comps/cores/mega/constants.py index 33aae3406b..6252b7c770 100644 --- a/comps/cores/mega/constants.py +++ b/comps/cores/mega/constants.py @@ -39,7 +39,7 @@ class ServiceType(Enum): TEXT2KG = 22 STRUCT2GRAPH = 23 LANGUAGE_DETECTION = 24 - + PROMPT_TEMPLATE = 25 class MegaServiceEndpoint(Enum): """The enum of an MegaService endpoint.""" diff --git a/comps/prompt_template/deployment/docker_compose/README.md b/comps/prompt_template/deployment/docker_compose/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comps/prompt_template/deployment/docker_compose/compose.yaml b/comps/prompt_template/deployment/docker_compose/compose.yaml new file mode 100644 index 0000000000..14ec480ae3 --- /dev/null +++ b/comps/prompt_template/deployment/docker_compose/compose.yaml @@ -0,0 +1,19 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +services: + prompt-template: + image: ${REGISTRY:-opea}/prompt-template:${TAG:-latest} + container_name: prompt-template + ports: + - "7900:7900" + environment: + - no_proxy=${no_proxy} + - https_proxy=${https_proxy} + - http_proxy=${http_proxy} + ipc: host + restart: always + +networks: + default: + driver: bridge diff --git a/comps/prompt_template/deployment/kubernetes/README.md b/comps/prompt_template/deployment/kubernetes/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/comps/prompt_template/src/Dockerfile b/comps/prompt_template/src/Dockerfile new file mode 100644 index 0000000000..e8d05e0253 --- /dev/null +++ b/comps/prompt_template/src/Dockerfile @@ -0,0 +1,34 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# FROM python:3.11-slim +FROM python:3.11-slim + +# Set environment variables +ENV LANG=en_US.UTF-8 + +RUN apt-get update -y && \ + apt-get install build-essential -y && \ + apt-get install -y --no-install-recommends --fix-missing \ + vim && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY comps /home/comps + +RUN useradd -m -s /bin/bash user && \ + mkdir -p /home/user && \ + chown -R user /home/user/ + +ARG uvpip='uv pip install --system --no-cache-dir' +RUN pip install --no-cache-dir --upgrade pip setuptools uv && \ + $uvpip -r /home/comps/prompt_template/src/requirements.txt + +ENV PYTHONPATH=$PYTHONPATH:/home + +USER user + +WORKDIR /home/comps/prompt_template/src + +ENTRYPOINT ["python", "opea_prompt_template_microservice.py"] + + diff --git a/comps/prompt_template/src/README.md b/comps/prompt_template/src/README.md new file mode 100644 index 0000000000..5c46f7ee09 --- /dev/null +++ b/comps/prompt_template/src/README.md @@ -0,0 +1,199 @@ +# Prompt Template microservice + +The Prompt Template microservice dynamically generates system and user prompts based on structured inputs and document context. It supports usage in LLM pipelines to customize prompt formatting with reranked documents, conversation history, and user queries. + +## Getting started + +### 🚀1. Start Prompt Template Microservice with Python (Option 1) + +To start the Prompt Template microservice, you need to install Python packages first. + +#### 1.1. Install Requirements + +```bash +pip install -r requirements.txt +``` + +#### 1.2. Start Microservice + +```bash +python opea_prompt_template_microservice.py +``` + +### 🚀2. Start Prompt Template Microservice with Docker (Option 2) + +#### 2.1. Build the Docker Image: + +Use the below docker build command to create the image: + +```bash +cd ../../../ +docker build -t opea/prompt-template:latest -f comps/prompt_template/src/Dockerfile . +``` + +Please note that the building process may take a while to complete. + +#### 2.2. Run the Docker Container: + +```bash +docker run -d --name="prompt-template-microservice" \ + -p 7900:7900 \ + --net=host \ + --ipc=host \ + opea/prompt-template:latest +``` + +### 3. Verify the Prompt Template Microservice + +#### 3.1. Check Status + +```bash +curl http://localhost:7900/v1/health_check \ + -X GET \ + -H 'Content-Type: application/json' +``` + +#### 3.2. Sending a Request + +##### 3.2.1 Default Template Generation + +Generates the prompt using the default template: + +**Example Input** + +```bash +curl -X POST -H "Content-Type: application/json" -d @- http://localhost:7900/v1/prompt_template < None: + """Validate system and user prompt templates. + + Args: + system_prompt_template: System prompt template string. + user_prompt_template: User prompt template string. + placeholders: Required placeholders set. + + Raises: + ValueError: For various validation failures. + """ + if not system_prompt_template.strip() or not user_prompt_template.strip(): + raise ValueError("Prompt templates cannot be empty.") + + system_placeholders = extract_placeholders_from_template(system_prompt_template) + user_placeholders = extract_placeholders_from_template(user_prompt_template) + + if not system_placeholders and not user_placeholders: + raise ValueError("Prompt templates do not contain any placeholders.") + + if not placeholders: + raise ValueError("Expected placeholders set cannot be empty.") + + duplicates = system_placeholders.intersection(user_placeholders) + if duplicates: + raise ValueError(f"System and user prompt templates share placeholders: {duplicates}") + + combined_placeholders = system_placeholders.union(user_placeholders) + + missing = placeholders - combined_placeholders + if missing: + raise ValueError(f"Prompt templates missing required placeholders: {missing}") + + extras = combined_placeholders - placeholders + extras_no_conv = extras - {self._conversation_history_placeholder} + if extras_no_conv: + raise ValueError(f"Prompt templates contain unexpected placeholders: {extras_no_conv}") + + if self._conversation_history_placeholder in extras: + self._if_conv_history_in_prompt = True + else: + logger.warning( + "Placeholder {conversation_history} missing. LLM will not remember previous answers." + " Add {conversation_history} placeholder if conversation history is desired." + ) + self._if_conv_history_in_prompt = False + + def _changed( + self, + new_system_prompt_template: str, + new_user_prompt_template: str, + placeholders: Set[str], + ) -> bool: + """Check if new templates differ and validate them. + + Args: + new_system_prompt_template: New system prompt template. + new_user_prompt_template: New user prompt template. + placeholders: Expected placeholders set. + + Returns: + True if templates changed and valid, False otherwise. + """ + if not new_system_prompt_template.strip() and not new_user_prompt_template.strip(): + logger.info("Empty new prompt templates, no change.") + return False + + if new_system_prompt_template == getattr( + self, "system_prompt_template", None + ) and new_user_prompt_template == getattr(self, "user_prompt_template", None): + logger.info("Prompt templates unchanged.") + return False + + self._validate(new_system_prompt_template, new_user_prompt_template, placeholders) + self.system_prompt_template = new_system_prompt_template + self.user_prompt_template = new_user_prompt_template + return True + + def _get_prompt(self, **kwargs) -> tuple[str, str]: + """Generate formatted prompts with provided kwargs. + + Returns: + Tuple of (system_prompt, user_prompt) + """ + system_prompt = self.system_prompt_template.format(**kwargs).strip() + user_prompt = self.user_prompt_template.format(**kwargs).strip() + return system_prompt, user_prompt + + def _parse_reranked_docs(self, reranked_docs: List[Union[dict, TextDoc]]) -> str: + """Format reranked documents into string. + + Args: + reranked_docs: List of document dicts or TextDoc instances. + + Returns: + Formatted string with sources and sections. + """ + formatted_docs = [] + for doc in reranked_docs: + metadata = None + text = None + + if isinstance(doc, dict): + metadata = doc.get("metadata") + text = doc.get("text") + elif isinstance(doc, TextDoc): + metadata = getattr(doc, "metadata", None) + text = getattr(doc, "text", None) + else: + logger.error(f"Unsupported document type: {type(doc)}") + raise ValueError(f"Unsupported document type: {type(doc)}") + + if not metadata and not text: + logger.error(f"Document {doc} lacks metadata and text.") + raise ValueError(f"Document {doc} lacks metadata and text.") + + file_info = "Unknown Source" + if metadata: + file_info = metadata.get("url") or metadata.get("object_name") or file_info + + headers = [metadata.get(f"Header{i}") for i in range(1, 8) if metadata and metadata.get(f"Header{i}")] + header_part = f" | Section: {' > '.join(headers)}" if headers else "" + + formatted_docs.append(f"[File: {file_info}{header_part}]\n{text or ''}") + + return "\n\n".join(formatted_docs) + + async def invoke(self, input: PromptTemplateInput) -> LLMParamsDoc: + """Entry point for prompt generation. + + Args: + input: PromptTemplateInput instance. + + Returns: + LLMParamsDoc with combined system and user prompts. + """ + keys = set(input.data.keys()) + logger.debug(f"Input data keys: {keys}") + + if input.system_prompt_template and input.user_prompt_template: + if self._changed(input.system_prompt_template, input.user_prompt_template, keys): + logger.info("Prompt templates updated.") + logger.debug(f"System template:\n{self.system_prompt_template}") + logger.debug(f"User template:\n{self.user_prompt_template}") + else: + logger.debug("Prompt templates not updated.") + expected_sys = extract_placeholders_from_template(self.system_prompt_template) + expected_user = extract_placeholders_from_template(self.user_prompt_template) + expected = expected_sys.union(expected_user) - {self._conversation_history_placeholder} + if keys != expected: + logger.error(f"Input keys {keys} do not match expected {expected}") + raise ValueError(f"Input keys {keys} do not match expected {expected}") + + prompt_data = {} + for k, v in input.data.items(): + if k == "reranked_docs": + prompt_data[k] = self._parse_reranked_docs(v) + else: + prompt_data[k] = extract_text_from_nested_dict(v) + logger.debug(f"Extracted text for key '{k}': {prompt_data[k]}") + + if self._if_conv_history_in_prompt: + params = {} + prompt_data[self._conversation_history_placeholder] = self.ch_handler.parse_conversation_history( + input.conversation_history, input.conversation_history_parse_type, params + ) + + try: + system_prompt, user_prompt = self._get_prompt(**prompt_data) + except KeyError as e: + logger.error(f"Missing key in prompt data: {e}") + raise + except Exception as e: + logger.error(f"Error generating prompt: {e}") + raise + + combined_chat_template = system_prompt + "\n" + user_prompt + extract_query = extract_query_from_user_prompt(user_prompt) + return LLMParamsDoc(chat_template=combined_chat_template, query=extract_query) + + def check_health(self) -> bool: + """Check the health status of the prompt template component. + + Returns: + bool: True if healthy, False otherwise. + """ + try: + if not self.system_prompt_template or not self.user_prompt_template: + logger.error("System or user prompt template is empty.") + return False + self._validate(self.system_prompt_template, self.user_prompt_template) + return True + except Exception as e: + logger.error(f"Prompt template health check failed: {e}") + return False + + +def extract_placeholders_from_template(template: str) -> Set[str]: + """Extract placeholders from a template string. + + Args: + template: Template string. + + Returns: + Set of placeholder names. + """ + return set(re.findall(r"\{(\w+)\}", template)) + + +def extract_text_from_nested_dict(data: object) -> str: + """Recursively extract text from nested dicts/lists/TextDoc. + + Args: + data: Input data. + + Returns: + Extracted text string. + """ + if isinstance(data, str): + return data + elif data is None: + return "" + elif isinstance(data, TextDoc): + return data.text + elif isinstance(data, list): + return " ".join(extract_text_from_nested_dict(item) for item in data) + elif isinstance(data, dict): + return " ".join(extract_text_from_nested_dict(v) for v in data.values()) + else: + logger.error(f"Unsupported data type for text extraction: {type(data)}") + raise ValueError(f"Unsupported data type for text extraction: {type(data)}") + + +def extract_query_from_user_prompt(user_prompt: str) -> str: + match = re.search(r"### Question:\s*(.+?)\s*(?:### Answer:|$)", user_prompt, re.DOTALL) + if match: + return match.group(1).strip() + return user_prompt.strip() diff --git a/comps/prompt_template/src/integrations/utils/conversation_history_handler.py b/comps/prompt_template/src/integrations/utils/conversation_history_handler.py new file mode 100644 index 0000000000..63ec56724d --- /dev/null +++ b/comps/prompt_template/src/integrations/utils/conversation_history_handler.py @@ -0,0 +1,46 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from typing import List + +from comps import CustomLogger +from comps.cores.proto.docarray import PrevQuestionDetails + +logger = CustomLogger(f"{__file__.split('comps/')[1].split('/', 1)[0]}_microservice") + + +class ConversationHistoryHandler: + def validate_conversation_history(self, con_history: List[PrevQuestionDetails]): + if con_history is None: + return False + + if len(con_history) == 0: + return False + + if_not_empty_history = False + for h in con_history: + if h.question.strip() != "" or h.answer.strip() != "": + if_not_empty_history = True + return if_not_empty_history + + def parse_conversation_history(self, con_history: List[PrevQuestionDetails], type: str, params: dict = {}) -> str: + if self.validate_conversation_history(con_history) is False: + return "" + + if type.lower() == "naive": + return self._get_history_naive(con_history, **params) + else: + raise ValueError(f"Incorrect ConversationHistoryHandler parsing type. Got: {type}. Expected: [naive, ]") + + def _get_history_naive(self, con_history: List[PrevQuestionDetails], top_k: int = 3) -> str: + if len(con_history) < top_k: + last_k_answers = con_history + else: + last_k_answers = con_history[-top_k:] + + formatted_output = "" + for conv in last_k_answers: + formatted_output += f"User: {conv.question}\nAssistant: {conv.answer}\n" + + logger.info(formatted_output) + return formatted_output.strip() diff --git a/comps/prompt_template/src/integrations/utils/prompt.py b/comps/prompt_template/src/integrations/utils/prompt.py new file mode 100644 index 0000000000..22d3338995 --- /dev/null +++ b/comps/prompt_template/src/integrations/utils/prompt.py @@ -0,0 +1,26 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + + +def generate_prompt_templates(context: str, question: str) -> tuple[str, str]: + """Dynamically generates the system and user prompts based on the given context and question. + + Args: + context (str): The context information to assist in answering the question. + question (str): The user's question. + + Returns: + tuple[str, str]: A tuple containing the system prompt and user prompt. + """ + system_prompt = f"""You are a helpful and knowledgeable assistant. Use the following context to answer the question accurately. + +Context: +{context} + +""" + user_prompt = f"""Question: +{question} + +Answer:""" + + return system_prompt, user_prompt diff --git a/comps/prompt_template/src/integrations/utils/templates.py b/comps/prompt_template/src/integrations/utils/templates.py new file mode 100644 index 0000000000..5de34fa5a3 --- /dev/null +++ b/comps/prompt_template/src/integrations/utils/templates.py @@ -0,0 +1,17 @@ +# Copyright (C) 2024-2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +template_system_english = """ +### You are a helpful, respectful, and honest assistant to help the user with questions. \ +Please refer to the search results obtained from the local knowledge base. \ +Refer also to the conversation history if you think it is relevant to the current question. \ +Ignore all information that you think is not relevant to the question. \ +If you don't know the answer to a question, please don't share false information. \n \ +### Search results: {reranked_docs} +### Conversation history: {conversation_history} \n +""" + +template_user_english = """ +### Question: {user_prompt} \n +### Answer: +""" diff --git a/comps/prompt_template/src/opea_prompt_template_microservice.py b/comps/prompt_template/src/opea_prompt_template_microservice.py new file mode 100644 index 0000000000..273e74bb68 --- /dev/null +++ b/comps/prompt_template/src/opea_prompt_template_microservice.py @@ -0,0 +1,72 @@ +# Copyright (C) 2024-2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os +import time + +from fastapi import HTTPException + +from comps import ( + CustomLogger, + LLMParamsDoc, + OpeaComponentLoader, + PromptTemplateInput, + ServiceType, + opea_microservices, + register_microservice, + register_statistics, + statistics_dict, +) +from comps.prompt_template.src.integrations.native import OPEAPromptTemplateGenerator + +logger = CustomLogger("opea_prompt_template") +component_loader = None + + +@register_microservice( + name="opea_service@prompt_template", + service_type=ServiceType.PROMPT_TEMPLATE, + endpoint="/v1/prompt_template", + host="0.0.0.0", + port=7900, + input_datatype=PromptTemplateInput, + output_datatype=LLMParamsDoc, +) +@register_statistics(names=["opea_service@prompt_template"]) +async def process(input: PromptTemplateInput) -> LLMParamsDoc: + """Process the input document using the OPEALanguageDetector. + + Args: + input (PromptTemplateInput): The input document to be processed. + + Returns: + LLMParamsDoc: The processed document with LLM parameters. + """ + start = time.time() + try: + res = await component_loader.invoke(input) + except ValueError as e: + logger.exception(f"Validation error: {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.exception(f"Unhandled error: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + statistics_dict["opea_service@prompt_template"].append_latency(time.time() - start, None) + return res + + +if __name__ == "__main__": + prompt_templete_component_name = os.getenv("PROMPT_TEMPLATE_COMPONENT_NAME", "OPEA_PROMPT_TEMPLATE") + + try: + component_loader = OpeaComponentLoader( + prompt_templete_component_name, + description=f"Prompt Template Generator Component: {prompt_templete_component_name}", + config={}, + ) + except Exception as e: + logger.error(f"Failed to initialize component: {e}") + exit(1) + + logger.info("Prompt template service started.") + opea_microservices["opea_service@prompt_template"].start() diff --git a/comps/prompt_template/src/requirements.in b/comps/prompt_template/src/requirements.in new file mode 100644 index 0000000000..e93c6d44b2 --- /dev/null +++ b/comps/prompt_template/src/requirements.in @@ -0,0 +1,18 @@ +aiohttp +docarray[full] +fastapi +httpx +opentelemetry-api +opentelemetry-exporter-otlp +opentelemetry-sdk +prometheus-fastapi-instrumentator +pydantic==2.7.2 +pydub +pyyaml +shortuuid +starlette +uvicorn +jinja2 +h11 +setuptools +python-dotenv \ No newline at end of file diff --git a/comps/prompt_template/src/requirements.txt b/comps/prompt_template/src/requirements.txt new file mode 100644 index 0000000000..da9806bd8e --- /dev/null +++ b/comps/prompt_template/src/requirements.txt @@ -0,0 +1,272 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements.in +# +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.13 + # via -r requirements.in +aiosignal==1.3.2 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.9.0 + # via + # httpx + # starlette +attrs==25.3.0 + # via + # aiohttp + # jsonschema + # referencing +av==14.4.0 + # via docarray +certifi==2025.6.15 + # via + # httpcore + # httpx + # requests +charset-normalizer==3.4.2 + # via + # requests + # trimesh +click==8.2.1 + # via uvicorn +colorlog==6.9.0 + # via trimesh +docarray[full]==0.41.0 + # via -r requirements.in +embreex==2.17.7.post6 + # via trimesh +fastapi==0.115.13 + # via -r requirements.in +frozenlist==1.7.0 + # via + # aiohttp + # aiosignal +googleapis-common-protos==1.70.0 + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +grpcio==1.73.0 + # via opentelemetry-exporter-otlp-proto-grpc +h11==0.16.0 + # via + # -r requirements.in + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # -r requirements.in + # trimesh +idna==3.10 + # via + # anyio + # httpx + # requests + # yarl +importlib-metadata==8.7.0 + # via opentelemetry-api +jax==0.5.3 + # via docarray +jaxlib==0.5.3 + # via jax +jinja2==3.1.6 + # via -r requirements.in +jsonschema==4.24.0 + # via trimesh +jsonschema-specifications==2025.4.1 + # via jsonschema +lxml==5.4.0 + # via trimesh +lz4==4.4.4 + # via docarray +manifold3d==3.1.1 + # via trimesh +mapbox-earcut==1.0.3 + # via trimesh +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +ml-dtypes==0.4.1 + # via + # jax + # jaxlib +multidict==6.5.0 + # via + # aiohttp + # yarl +mypy-extensions==1.1.0 + # via typing-inspect +networkx==3.5 + # via trimesh +numpy==1.26.4 + # via + # docarray + # embreex + # jax + # jaxlib + # manifold3d + # mapbox-earcut + # ml-dtypes + # pandas + # pycollada + # scipy + # shapely + # trimesh + # vhacdx +opentelemetry-api==1.34.1 + # via + # -r requirements.in + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-exporter-otlp==1.34.1 + # via -r requirements.in +opentelemetry-exporter-otlp-proto-common==1.34.1 + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.34.1 + # via opentelemetry-exporter-otlp +opentelemetry-exporter-otlp-proto-http==1.34.1 + # via opentelemetry-exporter-otlp +opentelemetry-proto==1.34.1 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.34.1 + # via + # -r requirements.in + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-semantic-conventions==0.55b1 + # via opentelemetry-sdk +opt-einsum==3.4.0 + # via jax +orjson==3.10.18 + # via docarray +pandas==2.3.0 + # via docarray +pillow==11.2.1 + # via + # docarray + # trimesh +prometheus-client==0.22.1 + # via prometheus-fastapi-instrumentator +prometheus-fastapi-instrumentator==7.1.0 + # via -r requirements.in +propcache==0.3.2 + # via + # aiohttp + # yarl +protobuf==5.29.5 + # via + # docarray + # googleapis-common-protos + # opentelemetry-proto +pycollada==0.9 + # via trimesh +pydantic==2.7.2 + # via + # -r requirements.in + # docarray + # fastapi +pydantic-core==2.18.3 + # via pydantic +pydub==0.25.1 + # via + # -r requirements.in + # docarray +pygments==2.19.1 + # via rich +python-dateutil==2.9.0.post0 + # via + # pandas + # pycollada +python-dotenv==1.1.1 + # via -r requirements.in +pytz==2025.2 + # via pandas +pyyaml==6.0.2 + # via -r requirements.in +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.4 + # via opentelemetry-exporter-otlp-proto-http +rich==14.0.0 + # via docarray +rpds-py==0.25.1 + # via + # jsonschema + # referencing +rtree==1.4.0 + # via trimesh +scipy==1.15.3 + # via + # jax + # jaxlib + # trimesh +shapely==2.1.1 + # via trimesh +shortuuid==1.0.13 + # via -r requirements.in +six==1.17.0 + # via python-dateutil +sniffio==1.3.1 + # via anyio +starlette==0.46.2 + # via + # -r requirements.in + # fastapi + # prometheus-fastapi-instrumentator +svg-path==6.3 + # via trimesh +trimesh[easy]==4.6.12 + # via docarray +types-pillow==10.2.0.20240822 + # via docarray +types-requests==2.32.4.20250611 + # via docarray +typing-extensions==4.14.0 + # via + # anyio + # fastapi + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # pydantic + # pydantic-core + # referencing + # typing-inspect +typing-inspect==0.9.0 + # via docarray +tzdata==2025.2 + # via pandas +urllib3==2.4.0 + # via + # requests + # types-requests +uvicorn==0.34.3 + # via -r requirements.in +vhacdx==0.0.8.post2 + # via trimesh +xxhash==3.5.0 + # via trimesh +yarl==1.20.1 + # via aiohttp +zipp==3.23.0 + # via importlib-metadata diff --git a/tests/prompt_template/test_prompt_template.sh b/tests/prompt_template/test_prompt_template.sh new file mode 100644 index 0000000000..780313d08d --- /dev/null +++ b/tests/prompt_template/test_prompt_template.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -x + +WORKPATH=$(dirname "$PWD") +ip_address=$(hostname -I | awk '{print $1}') + +export TAG=comps +export PORT=7900 +export service_name="prompt-template" + +function build_docker_images() { + cd $WORKPATH + echo $(pwd) + docker build --no-cache -t opea/prompt-template:$TAG -f comps/prompt_template/src/Dockerfile . + if [ $? -ne 0 ]; then + echo "opea/prompt-template built fail" + exit 1 + else + echo "opea/prompt-template built successful" + fi +} + +function start_service() { + unset http_proxy + cd $WORKPATH/comps/prompt_template/deployment/docker_compose + docker compose -f compose.yaml up ${service_name} -d + sleep 10s +} + +function validate_microservice() { + local PORT=7900 + local service_name="prompt-template" + echo "🔍 Validating $service_name service on port $PORT..." + + echo "📦 Case 1: Default template..." + result1=$(http_proxy="" curl -s -X POST http://localhost:$PORT/v1/prompt_template \ + -H "Content-Type: application/json" \ + -d '{ + "data": { + "user_prompt": "What is Deep Learning?", + "reranked_docs": [{"text":"Deep Learning is..."}] + }, + "conversation_history": [ + {"question": "Hello", "answer": "Hello as well"}, + {"question": "How are you?", "answer": "I am good, thank you!"}, + {"question": "Who are you?", "answer": "I am a robot"} + ], + "system_prompt_template": "", + "user_prompt_template": "" + }') + + chat_template1=$(echo "$result1" | jq -r '.chat_template') + if [[ "$chat_template1" == *"You are a helpful, respectful, and honest assistant to help the user with questions."* ]]; then + echo "✅ Case 1 passed." + else + echo "❌ Case 1 failed: missing default assistant description." + echo "$chat_template1" + fi + + echo "📦 Case 2: Custom prompt template..." + SYSTEM_PROMPT="### Please refer to the search results obtained from the local knowledge base. But be careful to not incorporate information that you think is not relevant to the question. If you don't know the answer to a question, please don't share false information. ### Search results: {reranked_docs} \n" + USER_PROMPT="### Question: {initial_query} \n### Answer:" + + result2=$(curl -s -X POST http://localhost:$PORT/v1/prompt_template \ + -H "Content-Type: application/json" \ + -d "{ + \"data\": { + \"initial_query\": \"What is Deep Learning?\", + \"reranked_docs\": [{\"text\":\"Deep Learning is...\"}] + }, + \"system_prompt_template\": \"${SYSTEM_PROMPT}\", + \"user_prompt_template\": \"${USER_PROMPT}\" + }") + + chat_template2=$(echo "$result2" | jq -r '.chat_template') + if [[ "$chat_template2" == *"refer to the search results obtained from the local knowledge base"* ]]; then + echo "✅ Case 2 passed." + else + echo "❌ Case 2 failed: missing expected custom prompt content." + echo "$chat_template2" + fi + + echo "📦 Case 3: Translation template..." + SYSTEM_PROMPT="### You are a helpful, respectful, and honest assistant to help the user with translations. Translate this from {source_lang} to {target_lang}.\n" + USER_PROMPT="### Question: {initial_query} \n### Answer:" + + result3=$(curl -s -X POST http://localhost:$PORT/v1/prompt_template \ + -H "Content-Type: application/json" \ + -d "{ + \"data\": { + \"initial_query\":\"什么是深度学习?\", + \"source_lang\": \"chinese\", + \"target_lang\": \"english\" + }, + \"system_prompt_template\": \"${SYSTEM_PROMPT}\", + \"user_prompt_template\": \"${USER_PROMPT}\" + }") + + chat_template3=$(echo "$result3" | jq -r '.chat_template') + if [[ "$chat_template3" == *"Translate this from chinese to english"* ]]; then + echo "✅ Case 3 passed." + else + echo "❌ Case 3 failed: translation instruction missing." + echo "$chat_template3" + fi +} + + +function stop_docker() { + cd $WORKPATH/comps/prompt_template/deployment/docker_compose + docker compose -f compose.yaml down --remove-orphans +} + +function main() { + stop_docker + + build_docker_images + start_service + + validate_microservice + + stop_docker + echo y | docker system prune +} + +main From 354468d96aa1e41586fffce5a9d218adc4a6b871 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:39:55 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- comps/cores/mega/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comps/cores/mega/constants.py b/comps/cores/mega/constants.py index 6252b7c770..38427db3ca 100644 --- a/comps/cores/mega/constants.py +++ b/comps/cores/mega/constants.py @@ -41,6 +41,7 @@ class ServiceType(Enum): LANGUAGE_DETECTION = 24 PROMPT_TEMPLATE = 25 + class MegaServiceEndpoint(Enum): """The enum of an MegaService endpoint."""