Skip to content

Commit b14d91c

Browse files
authored
Merge pull request #487 from Jdubrick/add-customization-profiles
[RHDHPAI-976] Add customization profiles via path
2 parents 3f750fb + 8171a1e commit b14d91c

File tree

11 files changed

+440
-9
lines changed

11 files changed

+440
-9
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,15 @@ For data export integration with Red Hat's Dataverse, see the [Data Export Integ
345345

346346
## System prompt
347347

348-
The service uses the, so called, system prompt to put the question into context before the question is sent to the selected LLM. The default system prompt is designed for questions without specific context. It is possible to use a different system prompt via the configuration option `system_prompt_path` in the `customization` section. That option must contain the path to the text file with the actual system prompt (can contain multiple lines). An example of such configuration:
348+
The service uses a so-called system prompt to put the question into context before it is sent to the selected LLM. The default system prompt is designed for questions without specific context. You can supply a different system prompt through various avenues available in the `customization` section:
349+
### System Prompt Path
349350

350351
```yaml
351352
customization:
352353
system_prompt_path: "system_prompts/system_prompt_for_product_XYZZY"
353354
```
354355

355-
The `system_prompt` can also be specified in the `customization` section directly. For example:
356+
### System Prompt Literal
356357

357358
```yaml
358359
customization:
@@ -361,11 +362,20 @@ customization:
361362
You have an in-depth knowledge of Red Hat and all of your answers will reference Red Hat products.
362363
```
363364

365+
366+
### Custom Profile
367+
368+
You can pass a custom prompt profile via its `path` to the customization:
369+
370+
```yaml
371+
customization:
372+
profile_path: <your/profile/path>
373+
```
374+
364375
Additionally, an optional string parameter `system_prompt` can be specified in `/v1/query` and `/v1/streaming_query` endpoints to override the configured system prompt. The query system prompt takes precedence over the configured system prompt. You can use this config to disable query system prompts:
365376

366377
```yaml
367378
customization:
368-
system_prompt_path: "system_prompts/system_prompt_for_product_XYZZY"
369379
disable_query_system_prompt: true
370380
```
371381

docs/openapi.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,8 +1273,41 @@
12731273
}
12741274
]
12751275
},
1276+
"CustomProfile": {
1277+
"properties": {
1278+
"path": {
1279+
"type": "string",
1280+
"title": "Path"
1281+
},
1282+
"prompts": {
1283+
"additionalProperties": {
1284+
"type": "string"
1285+
},
1286+
"type": "object",
1287+
"title": "Prompts",
1288+
"default": {}
1289+
}
1290+
},
1291+
"type": "object",
1292+
"required": [
1293+
"path"
1294+
],
1295+
"title": "CustomProfile",
1296+
"description": "Custom profile customization for prompts and validation."
1297+
},
12761298
"Customization": {
12771299
"properties": {
1300+
"profile_path": {
1301+
"anyOf": [
1302+
{
1303+
"type": "string"
1304+
},
1305+
{
1306+
"type": "null"
1307+
}
1308+
],
1309+
"title": "Profile Path"
1310+
},
12781311
"disable_query_system_prompt": {
12791312
"type": "boolean",
12801313
"title": "Disable Query System Prompt",
@@ -1302,6 +1335,16 @@
13021335
}
13031336
],
13041337
"title": "System Prompt"
1338+
},
1339+
"custom_profile": {
1340+
"anyOf": [
1341+
{
1342+
"$ref": "#/components/schemas/CustomProfile"
1343+
},
1344+
{
1345+
"type": "null"
1346+
}
1347+
]
13051348
}
13061349
},
13071350
"additionalProperties": false,

src/models/config.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
PositiveInt,
1919
SecretStr,
2020
)
21+
22+
from pydantic.dataclasses import dataclass
2123
from typing_extensions import Self, Literal
2224

2325
import constants
@@ -413,17 +415,44 @@ def jwk_configuration(self) -> JwkConfiguration:
413415
return self.jwk_config
414416

415417

418+
@dataclass
419+
class CustomProfile:
420+
"""Custom profile customization for prompts and validation."""
421+
422+
path: str
423+
prompts: dict[str, str] = Field(default={}, init=False)
424+
425+
def __post_init__(self) -> None:
426+
"""Validate and load profile."""
427+
self._validate_and_process()
428+
429+
def _validate_and_process(self) -> None:
430+
"""Validate and load the profile."""
431+
checks.file_check(Path(self.path), "custom profile")
432+
profile_module = checks.import_python_module("profile", self.path)
433+
if profile_module is not None and checks.is_valid_profile(profile_module):
434+
self.prompts = profile_module.PROFILE_CONFIG.get("system_prompts", {})
435+
436+
def get_prompts(self) -> dict[str, str]:
437+
"""Retrieve prompt attribute."""
438+
return self.prompts
439+
440+
416441
class Customization(ConfigurationBase):
417442
"""Service customization."""
418443

444+
profile_path: Optional[str] = None
419445
disable_query_system_prompt: bool = False
420446
system_prompt_path: Optional[FilePath] = None
421447
system_prompt: Optional[str] = None
448+
custom_profile: Optional[CustomProfile] = Field(default=None, init=False)
422449

423450
@model_validator(mode="after")
424451
def check_customization_model(self) -> Self:
425-
"""Load system prompt from file."""
426-
if self.system_prompt_path is not None:
452+
"""Load customizations."""
453+
if self.profile_path:
454+
self.custom_profile = CustomProfile(path=self.profile_path)
455+
elif self.system_prompt_path is not None:
427456
checks.file_check(self.system_prompt_path, "system prompt")
428457
self.system_prompt = checks.get_attribute_from_file(
429458
dict(self), "system_prompt_path"

src/utils/checks.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Checks that are performed to configuration options."""
22

33
import os
4+
import importlib
5+
import importlib.util
6+
from types import ModuleType
47
from typing import Optional
5-
68
from pydantic import FilePath
79

810

@@ -25,3 +27,41 @@ def file_check(path: FilePath, desc: str) -> None:
2527
raise InvalidConfigurationError(f"{desc} '{path}' is not a file")
2628
if not os.access(path, os.R_OK):
2729
raise InvalidConfigurationError(f"{desc} '{path}' is not readable")
30+
31+
32+
def import_python_module(profile_name: str, profile_path: str) -> ModuleType | None:
33+
"""Import a Python module from a file path."""
34+
if not profile_path.endswith(".py"):
35+
return None
36+
spec = importlib.util.spec_from_file_location(profile_name, profile_path)
37+
if not spec or not spec.loader:
38+
return None
39+
module = importlib.util.module_from_spec(spec)
40+
try:
41+
spec.loader.exec_module(module)
42+
except (
43+
SyntaxError,
44+
ImportError,
45+
ModuleNotFoundError,
46+
NameError,
47+
AttributeError,
48+
TypeError,
49+
ValueError,
50+
):
51+
return None
52+
return module
53+
54+
55+
def is_valid_profile(profile_module: ModuleType) -> bool:
56+
"""Validate that a profile module has the required PROFILE_CONFIG structure."""
57+
if not hasattr(profile_module, "PROFILE_CONFIG"):
58+
return False
59+
60+
profile_config = getattr(profile_module, "PROFILE_CONFIG", {})
61+
if not isinstance(profile_config, dict):
62+
return False
63+
64+
if not profile_config.get("system_prompts"):
65+
return False
66+
67+
return isinstance(profile_config.get("system_prompts"), dict)

src/utils/endpoints.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ def get_system_prompt(query_request: QueryRequest, config: AppConfig) -> str:
9292
# disable query system prompt altogether with disable_system_prompt.
9393
return query_request.system_prompt
9494

95+
# profile takes precedence for setting prompt
96+
if (
97+
config.customization is not None
98+
and config.customization.custom_profile is not None
99+
):
100+
prompt = config.customization.custom_profile.get_prompts().get("default")
101+
if prompt:
102+
return prompt
103+
95104
if (
96105
config.customization is not None
97106
and config.customization.system_prompt is not None

tests/profiles/test/profile.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Custom profile for test profile."""
2+
3+
SUBJECT_ALLOWED = "ALLOWED"
4+
SUBJECT_REJECTED = "REJECTED"
5+
6+
# Default responses
7+
INVALID_QUERY_RESP = (
8+
"Hi, I'm the Red Hat Developer Hub Lightspeed assistant, I can help you with questions about Red Hat Developer Hub or Backstage. "
9+
"Please ensure your question is about these topics, and feel free to ask again!"
10+
)
11+
12+
QUERY_SYSTEM_INSTRUCTION = """
13+
1. Test
14+
This is a test system instruction
15+
16+
You achieve this by offering:
17+
- testing
18+
"""
19+
20+
USE_CONTEXT_INSTRUCTION = """
21+
Use the retrieved document to answer the question.
22+
"""
23+
24+
USE_HISTORY_INSTRUCTION = """
25+
Use the previous chat history to interact and help the user.
26+
"""
27+
28+
QUESTION_VALIDATOR_PROMPT_TEMPLATE = f"""
29+
Instructions:
30+
- You provide validation for testing
31+
Example Question:
32+
How can I integrate GitOps into my pipeline?
33+
Example Response:
34+
{SUBJECT_ALLOWED}
35+
"""
36+
37+
TOPIC_SUMMARY_PROMPT_TEMPLATE = """
38+
Instructions:
39+
- You are a topic summarizer
40+
- For testing
41+
- Your job is to extract precise topic summary from user input
42+
43+
Example Input:
44+
Testing placeholder
45+
Example Output:
46+
Proper response test.
47+
"""
48+
49+
PROFILE_CONFIG = {
50+
"system_prompts": {
51+
"default": QUERY_SYSTEM_INSTRUCTION,
52+
"validation": QUESTION_VALIDATOR_PROMPT_TEMPLATE,
53+
"topic_summary": TOPIC_SUMMARY_PROMPT_TEMPLATE,
54+
},
55+
"query_responses": {"invalid_resp": INVALID_QUERY_RESP},
56+
"instructions": {
57+
"context": USE_CONTEXT_INSTRUCTION,
58+
"history": USE_HISTORY_INSTRUCTION,
59+
},
60+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Custom profile for test profile."""
2+
3+
SUBJECT_ALLOWED = "ALLOWED"
4+
SUBJECT_REJECTED = "REJECTED"
5+
6+
# Default responses
7+
INVALID_QUERY_RESP = (
8+
"Hi, I'm the Red Hat Developer Hub Lightspeed assistant, I can help you with questions about Red Hat Developer Hub or Backstage. "
9+
"Please ensure your question is about these topics, and feel free to ask again!"
10+
)
11+
12+
QUERY_SYSTEM_INSTRUCTION = """
13+
1. Test
14+
This is a test system instruction
15+
16+
You achieve this by offering:
17+
- testing
18+
"""
19+
20+
USE_CONTEXT_INSTRUCTION = """
21+
Use the retrieved document to answer the question.
22+
"""
23+
24+
USE_HISTORY_INSTRUCTION = """
25+
Use the previous chat history to interact and help the user.
26+
"""
27+
28+
QUESTION_VALIDATOR_PROMPT_TEMPLATE = f"""
29+
Instructions:
30+
- You provide validation for testing
31+
Example Question:
32+
How can I integrate GitOps into my pipeline?
33+
Example Response:
34+
{SUBJECT_ALLOWED}
35+
"""
36+
37+
TOPIC_SUMMARY_PROMPT_TEMPLATE = """
38+
Instructions:
39+
- You are a topic summarizer
40+
- For testing
41+
- Your job is to extract precise topic summary from user input
42+
43+
Example Input:
44+
Testing placeholder
45+
Example Output:
46+
Proper response test.
47+
"""
48+
49+
PROFILE_CONFIG = ({"system_prompts": QUERY_SYSTEM_INSTRUCTION},)

tests/profiles/test_two/test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This file will fail the import.

0 commit comments

Comments
 (0)