Skip to content

Commit ed8346d

Browse files
committed
Bring prompt_utlis back
1 parent 7fc9900 commit ed8346d

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

.fernignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
## Custom code
55

66
src/humanloop/evals
7+
src/humanloop/prompt_utils.py
78
src/humanloop/client.py
89
src/humanloop/overload.py
910
src/humanloop/context.py

src/humanloop/client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from humanloop.otel import instrument_provider
2323
from humanloop.otel.exporter import HumanloopSpanExporter
2424
from humanloop.otel.processor import HumanloopSpanProcessor
25+
from humanloop.prompt_utils import populate_template
26+
from humanloop.prompts.client import PromptsClient
2527

2628

2729
class ExtendedEvalsClient(EvaluationsClient):
@@ -68,6 +70,14 @@ def run(
6870
)
6971

7072

73+
class ExtendedPromptsClient(PromptsClient):
74+
"""
75+
Adds utility for populating Prompt template inputs.
76+
"""
77+
78+
populate_template = staticmethod(populate_template) # type: ignore [assignment]
79+
80+
7181
class Humanloop(BaseHumanloop):
7282
"""
7383
See docstring of :class:`BaseHumanloop`.
@@ -109,6 +119,7 @@ def __init__(
109119
eval_client = ExtendedEvalsClient(client_wrapper=self._client_wrapper)
110120
eval_client.client = self
111121
self.evaluations = eval_client
122+
self.prompts = ExtendedPromptsClient(client_wrapper=self._client_wrapper)
112123

113124
# Overload the .log method of the clients to be aware of Evaluation Context
114125
# and the @flow decorator providing the trace_id

src/humanloop/prompt_utils.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import copy
2+
from typing import Any, Dict, List, Optional, TypeVar, Sequence
3+
import logging
4+
5+
import re
6+
7+
from .requests.chat_message import ChatMessageParams
8+
from .prompts.requests.prompt_request_template import (
9+
PromptRequestTemplateParams,
10+
)
11+
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class PromptVariablesNotFoundError(ValueError):
17+
"""Raised when inputs do not satisfy prompt variables."""
18+
19+
missing_variables: List[str]
20+
"""Missing variables"""
21+
22+
def __init__(self, missing_variables: List[str]) -> None:
23+
self.missing_variables = missing_variables
24+
super().__init__(f"Prompt requires inputs for the following " f"variables: {self.missing_variables}")
25+
26+
27+
def populate_prompt_template(
28+
template: str,
29+
inputs: Optional[Dict[str, Any]],
30+
) -> str:
31+
"""Interpolate a string template with kwargs, where template variables
32+
are specified using double curly bracket syntax: {{variable}}.
33+
34+
args:
35+
template: str - string template where template variables are specified
36+
using double curly bracket syntax: {{variable}}.
37+
38+
inputs - represent the key, value string pairs to inject into the template
39+
variables, where key corresponds to the template variable name and
40+
value to the variable value to inject
41+
42+
return:
43+
The interpolated template string
44+
45+
raises:
46+
PromptVariablesNotFoundError - if any variables are missing from inputs
47+
"""
48+
template_variables: List[str] = re.findall(
49+
# Matching variables: `{{ variable_2 }}`
50+
r"{{\s?([a-zA-Z_\d\.\[\]]+)\s?}}",
51+
template,
52+
) + re.findall(
53+
# Matching tools: `{{ tool_2("all characters$#@$!") }}`
54+
# https://regexr.com/7nvrf
55+
r"\{\{\s?([a-zA-Z_\-\d]+\([a-zA-Z_\-\d,\s\"]+\))\s?\}\}",
56+
template,
57+
)
58+
59+
# populate the template variables, tracking if any are missing
60+
prompt = template
61+
missing_vars = []
62+
63+
if inputs is None:
64+
inputs = {}
65+
66+
# e.g. var: input_name, sig(input_name), sig(other_name), sig("string")
67+
for var in template_variables:
68+
text: Optional[str] = None
69+
70+
if var in inputs:
71+
text = inputs[var]
72+
73+
if text is None:
74+
missing_vars.append(var)
75+
else:
76+
if not isinstance(text, str):
77+
logger.info(f"Converting input value for variable '{var}' to string for prompt template: " f"{text}")
78+
text = str(text)
79+
replacement = sanitize_prompt(prompt=text) if text else text
80+
prompt = re.sub(
81+
r"{{\s?" + re.escape(var) + r"\s?}}",
82+
replacement,
83+
prompt,
84+
)
85+
86+
if missing_vars:
87+
missing_vars.sort()
88+
raise PromptVariablesNotFoundError(
89+
missing_variables=missing_vars,
90+
)
91+
92+
return prompt
93+
94+
95+
def sanitize_prompt(prompt: str):
96+
return prompt.replace("\\", "\\\\")
97+
98+
99+
def populate_chat_template(
100+
chat_template: Sequence[ChatMessageParams],
101+
inputs: Optional[Dict[str, str]] = None,
102+
) -> List[ChatMessageParams]:
103+
"""Interpolate a chat template with kwargs, where template variables."""
104+
messages = []
105+
message: ChatMessageParams
106+
for message in chat_template:
107+
if "content" not in message:
108+
messages.append(message)
109+
continue
110+
111+
message_content = copy.deepcopy(message["content"])
112+
if isinstance(message_content, str):
113+
message_content = populate_prompt_template(
114+
template=message_content,
115+
inputs=inputs,
116+
)
117+
elif isinstance(message_content, list):
118+
for j, content_item in enumerate(message_content):
119+
if content_item["type"] == "text":
120+
content_item_text = content_item["text"]
121+
(content_item_text,) = populate_prompt_template(
122+
template=content_item_text,
123+
inputs=inputs,
124+
)
125+
content_item["text"] = content_item_text
126+
messages.append(
127+
ChatMessageParams(
128+
role=message["role"],
129+
content=message_content,
130+
)
131+
)
132+
return messages
133+
134+
135+
T = TypeVar("T", bound=PromptRequestTemplateParams)
136+
137+
138+
def populate_template(template: T, inputs: Dict[str, str]) -> T:
139+
"""Populate a Prompt's template with the given inputs.
140+
141+
Humanloop supports insertion of variables of the form `{{variable}}` in
142+
Prompt templates.
143+
E.g. If you provide the template `Hello {{name}}` and the input
144+
`{"name": "Alice"}`, the populated template will be `Hello Alice`.
145+
146+
This function supports both completion and chat models. For completion
147+
models, provide template as a string. For chat models, provide template
148+
as a list of messages.
149+
"""
150+
if isinstance(template, str):
151+
return populate_prompt_template(
152+
template=template,
153+
inputs=inputs,
154+
)
155+
return populate_chat_template(
156+
chat_template=template,
157+
inputs=inputs,
158+
)

0 commit comments

Comments
 (0)