kiln_ai.adapters.prompt_builders

  1from abc import ABCMeta, abstractmethod
  2from dataclasses import dataclass
  3
  4from kiln_ai.datamodel import PromptGenerators, PromptId, Task, TaskRun
  5from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
  6
  7
  8@dataclass
  9class PromptExample:
 10    """A simple input/output example for use in prompt building."""
 11
 12    input: str
 13    output: str
 14
 15
 16class BasePromptBuilder(metaclass=ABCMeta):
 17    """Base class for building prompts from tasks.
 18
 19    Provides the core interface and basic functionality for prompt builders.
 20    """
 21
 22    def __init__(self, task: Task):
 23        """Initialize the prompt builder with a task.
 24
 25        Args:
 26            task (Task): The task containing instructions and requirements.
 27        """
 28        self.task = task
 29
 30    def prompt_id(self) -> str | None:
 31        """Returns the ID of the prompt, scoped to this builder.
 32
 33        Returns:
 34            str | None: The ID of the prompt, or None if not set.
 35        """
 36        return None
 37
 38    def build_prompt(self, include_json_instructions) -> str:
 39        """Build and return the complete prompt string.
 40
 41        Returns:
 42            str: The constructed prompt.
 43        """
 44        prompt = self.build_base_prompt()
 45
 46        if include_json_instructions and self.task.output_schema():
 47            prompt = (
 48                prompt
 49                + f"\n\n# Format Instructions\n\nReturn a JSON object conforming to the following schema:\n```\n{self.task.output_schema()}\n```"
 50            )
 51
 52        return prompt
 53
 54    @abstractmethod
 55    def build_base_prompt(self) -> str:
 56        """Build and return the complete prompt string.
 57
 58        Returns:
 59            str: The constructed prompt.
 60        """
 61        pass
 62
 63    def chain_of_thought_prompt(self) -> str | None:
 64        """Build and return the chain of thought prompt string.
 65
 66        Returns:
 67            str: The constructed chain of thought prompt.
 68        """
 69        return None
 70
 71    def build_prompt_for_ui(self) -> str:
 72        """Build a prompt for the UI. It includes additional instructions (like chain of thought), even if they are passed to the model in stages.
 73
 74        Designed for end-user consumption, not for model consumption.
 75
 76        Returns:
 77            str: The constructed prompt string.
 78        """
 79        base_prompt = self.build_prompt(include_json_instructions=False)
 80        cot_prompt = self.chain_of_thought_prompt()
 81        if cot_prompt:
 82            base_prompt += "\n\n# Thinking Instructions\n\n" + cot_prompt
 83        return base_prompt
 84
 85
 86class SimplePromptBuilder(BasePromptBuilder):
 87    """A basic prompt builder that combines task instruction with requirements."""
 88
 89    def build_base_prompt(self) -> str:
 90        """Build a simple prompt with instruction and requirements.
 91
 92        Returns:
 93            str: The constructed prompt string.
 94        """
 95        base_prompt = self.task.instruction
 96
 97        if len(self.task.requirements) > 0:
 98            base_prompt += (
 99                "\n\nYour response should respect the following requirements:\n"
100            )
101            # iterate requirements, formatting them in numbereed list like 1) task.instruction\n2)...
102            for i, requirement in enumerate(self.task.requirements):
103                base_prompt += f"{i + 1}) {requirement.instruction}\n"
104
105        return base_prompt
106
107
108class MultiShotPromptBuilder(BasePromptBuilder):
109    """A prompt builder that includes multiple examples in the prompt."""
110
111    @classmethod
112    def example_count(cls) -> int:
113        """Get the maximum number of examples to include in the prompt.
114
115        Returns:
116            int: The maximum number of examples (default 25).
117        """
118        return 25
119
120    def build_instruction_and_requirements(self) -> str:
121        """Build the instruction and requirements section of the prompt.
122
123        Returns:
124            str: The instruction and requirements sections.
125        """
126        base_prompt = f"# Instruction\n\n{self.task.instruction}\n\n"
127
128        if len(self.task.requirements) > 0:
129            base_prompt += "# Requirements\n\nYour response should respect the following requirements:\n"
130            for i, requirement in enumerate(self.task.requirements):
131                base_prompt += f"{i + 1}) {requirement.instruction}\n"
132            base_prompt += "\n"
133
134        return base_prompt
135
136    def build_base_prompt(self) -> str:
137        """Build a prompt with instruction, requirements, and multiple examples.
138
139        Returns:
140            str: The constructed prompt string with examples.
141        """
142        base_prompt = self.build_instruction_and_requirements()
143
144        valid_examples = self.collect_examples()
145
146        if len(valid_examples) == 0:
147            return base_prompt
148
149        base_prompt += "# Example Outputs\n\n"
150        for i, example in enumerate(valid_examples):
151            base_prompt += self.prompt_section_for_example(i, example)
152
153        return base_prompt
154
155    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
156        # Prefer repaired output if it exists, otherwise use the regular output
157        output = example.repaired_output or example.output
158        return f"## Example {index + 1}\n\nInput: {example.input}\nOutput: {output.output}\n\n"
159
160    def collect_examples(self) -> list[TaskRun]:
161        valid_examples: list[TaskRun] = []
162        runs = self.task.runs(readonly=True)
163
164        # first pass, we look for repaired outputs. These are the best examples.
165        for run in runs:
166            if len(valid_examples) >= self.__class__.example_count():
167                break
168            if run.repaired_output is not None:
169                valid_examples.append(run)
170
171        # second pass, we look for high quality outputs (rating based)
172        # Minimum is "high_quality" (4 star in star rating scale), then sort by rating
173        # exclude repaired outputs as they were used above
174        runs_with_rating = [
175            run
176            for run in runs
177            if run.output.rating is not None
178            and run.output.rating.value is not None
179            and run.output.rating.is_high_quality()
180            and run.repaired_output is None
181        ]
182        runs_with_rating.sort(
183            key=lambda x: (x.output.rating and x.output.rating.value) or 0, reverse=True
184        )
185        for run in runs_with_rating:
186            if len(valid_examples) >= self.__class__.example_count():
187                break
188            valid_examples.append(run)
189        return valid_examples
190
191
192class FewShotPromptBuilder(MultiShotPromptBuilder):
193    """A prompt builder that includes a small number of examples in the prompt."""
194
195    @classmethod
196    def example_count(cls) -> int:
197        """Get the maximum number of examples to include in the prompt.
198
199        Returns:
200            int: The maximum number of examples (4).
201        """
202        return 4
203
204
205class CustomExamplePromptBuilder(FewShotPromptBuilder):
206    """A prompt builder that uses custom examples instead of collecting from the dataset."""
207
208    def __init__(self, task: Task, examples: list[PromptExample] | None = None):
209        super().__init__(task)
210        self._custom_examples = examples or []
211
212    def collect_examples(self) -> list[TaskRun]:
213        """Override to return an empty list - we handle examples separately."""
214        return []
215
216    def build_base_prompt(self) -> str:
217        """Build a prompt with instruction, requirements, and custom examples."""
218        base_prompt = self.build_instruction_and_requirements()
219
220        if self._custom_examples:
221            base_prompt += "# Example Outputs\n\n"
222            for i, example in enumerate(self._custom_examples):
223                base_prompt += f"## Example {i + 1}\n\nInput: {example.input}\nOutput: {example.output}\n\n"
224
225        return base_prompt
226
227
228class RepairsPromptBuilder(MultiShotPromptBuilder):
229    """A prompt builder that includes multiple examples in the prompt, including repaired instructions describing what was wrong, and how it was fixed."""
230
231    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
232        if (
233            not example.repaired_output
234            or not example.repair_instructions
235            or not example.repaired_output.output
236        ):
237            return super().prompt_section_for_example(index, example)
238
239        prompt_section = f"## Example {index + 1}\n\nInput: {example.input}\n\n"
240        prompt_section += (
241            f"Initial Output Which Was Insufficient: {example.output.output}\n\n"
242        )
243        prompt_section += f"Instructions On How to Improve the Initial Output: {example.repair_instructions}\n\n"
244        prompt_section += (
245            f"Repaired Output Which is Sufficient: {example.repaired_output.output}\n\n"
246        )
247        return prompt_section
248
249
250def chain_of_thought_prompt(task: Task) -> str:
251    """Standard implementation to build and return the chain of thought prompt string.
252
253    Returns:
254        str: The constructed chain of thought prompt.
255    """
256
257    cot_instruction = task.thinking_instruction
258    if not cot_instruction:
259        cot_instruction = "Think step by step, explaining your reasoning."
260
261    return cot_instruction
262
263
264class SimpleChainOfThoughtPromptBuilder(SimplePromptBuilder):
265    """A prompt builder that includes a chain of thought prompt on top of the simple prompt."""
266
267    def chain_of_thought_prompt(self) -> str | None:
268        return chain_of_thought_prompt(self.task)
269
270
271class FewShotChainOfThoughtPromptBuilder(FewShotPromptBuilder):
272    """A prompt builder that includes a chain of thought prompt on top of the few shot prompt."""
273
274    def chain_of_thought_prompt(self) -> str | None:
275        return chain_of_thought_prompt(self.task)
276
277
278class MultiShotChainOfThoughtPromptBuilder(MultiShotPromptBuilder):
279    """A prompt builder that includes a chain of thought prompt on top of the multi shot prompt."""
280
281    def chain_of_thought_prompt(self) -> str | None:
282        return chain_of_thought_prompt(self.task)
283
284
285class SavedPromptBuilder(BasePromptBuilder):
286    """A prompt builder that looks up a static prompt."""
287
288    def __init__(self, task: Task, prompt_id: str):
289        super().__init__(task)
290        prompt_model = next(
291            (
292                prompt
293                for prompt in task.prompts(readonly=True)
294                if prompt.id == prompt_id
295            ),
296            None,
297        )
298        if not prompt_model:
299            raise ValueError(f"Prompt ID not found: {prompt_id}")
300        self.prompt_model = prompt_model
301
302    def prompt_id(self) -> str | None:
303        return self.prompt_model.id
304
305    def build_base_prompt(self) -> str:
306        """Returns a saved prompt.
307
308        Returns:
309            str: The prompt string.
310        """
311        return self.prompt_model.prompt
312
313    def chain_of_thought_prompt(self) -> str | None:
314        return self.prompt_model.chain_of_thought_instructions
315
316
317class TaskRunConfigPromptBuilder(BasePromptBuilder):
318    """A prompt builder that looks up a static prompt in a task run config."""
319
320    def __init__(self, task: Task, run_config_prompt_id: str):
321        parts = run_config_prompt_id.split("::")
322        if len(parts) != 4:
323            raise ValueError(
324                f"Invalid task run config prompt ID: {run_config_prompt_id}. Expected format: 'task_run_config::[project_id]::[task_id]::[run_config_id]'."
325            )
326
327        task_id = parts[2]
328        if task_id != task.id:
329            raise ValueError(
330                f"Task run config prompt ID: {run_config_prompt_id}. Task ID mismatch. Expected: {task.id}, got: {task_id}."
331            )
332
333        run_config_id = parts[3]
334        run_config = next(
335            (
336                run_config
337                for run_config in task.run_configs(readonly=True)
338                if run_config.id == run_config_id
339            ),
340            None,
341        )
342        if not run_config:
343            raise ValueError(
344                f"Task run config ID not found: {run_config_id} for prompt id {run_config_prompt_id}"
345            )
346        if run_config.prompt is None:
347            raise ValueError(
348                f"Task run config ID {run_config_id} does not have a stored prompt. Used as prompt id {run_config_prompt_id}"
349            )
350
351        # Load the prompt from the model
352        self.prompt = run_config.prompt.prompt
353        self.cot_prompt = run_config.prompt.chain_of_thought_instructions
354        self.id = run_config_prompt_id
355
356        super().__init__(task)
357
358    def prompt_id(self) -> str | None:
359        return self.id
360
361    def build_base_prompt(self) -> str:
362        return self.prompt
363
364    def chain_of_thought_prompt(self) -> str | None:
365        return self.cot_prompt
366
367
368class FineTunePromptBuilder(BasePromptBuilder):
369    """A prompt builder that looks up a fine-tune prompt."""
370
371    def __init__(self, task: Task, nested_fine_tune_id: str):
372        super().__init__(task)
373
374        # IDs are in project_id::task_id::fine_tune_id format
375        self.full_fine_tune_id = nested_fine_tune_id
376        parts = nested_fine_tune_id.split("::")
377        if len(parts) != 3:
378            raise ValueError(
379                f"Invalid fine-tune ID format. Expected 'project_id::task_id::fine_tune_id', got: {nested_fine_tune_id}"
380            )
381        fine_tune_id = parts[2]
382
383        fine_tune_model = next(
384            (
385                fine_tune
386                for fine_tune in task.finetunes(readonly=True)
387                if fine_tune.id == fine_tune_id
388            ),
389            None,
390        )
391        if not fine_tune_model:
392            raise ValueError(f"Fine-tune ID not found: {fine_tune_id}")
393        self.fine_tune_model = fine_tune_model
394
395    def prompt_id(self) -> str | None:
396        return self.full_fine_tune_id
397
398    def build_base_prompt(self) -> str:
399        return self.fine_tune_model.system_message
400
401    def chain_of_thought_prompt(self) -> str | None:
402        return self.fine_tune_model.thinking_instructions
403
404
405# Our UI has some names that are not the same as the class names, which also hint parameters.
406def prompt_builder_from_id(prompt_id: PromptId, task: Task) -> BasePromptBuilder:
407    """Convert a name used in the UI to the corresponding prompt builder class.
408
409    Args:
410        prompt_id (PromptId): The prompt ID.
411
412    Returns:
413        type[BasePromptBuilder]: The corresponding prompt builder class.
414
415    Raises:
416        ValueError: If the UI name is not recognized.
417    """
418
419    # Saved prompts are prefixed with "id::"
420    if prompt_id.startswith("id::"):
421        prompt_id = prompt_id[4:]
422        return SavedPromptBuilder(task, prompt_id)
423
424    # Task run config prompts are prefixed with "task_run_config::"
425    # task_run_config::[project_id]::[task_id]::[run_config_id]
426    if prompt_id.startswith("task_run_config::"):
427        return TaskRunConfigPromptBuilder(task, prompt_id)
428
429    # Fine-tune prompts are prefixed with "fine_tune_prompt::"
430    if prompt_id.startswith("fine_tune_prompt::"):
431        prompt_id = prompt_id[18:]
432        return FineTunePromptBuilder(task, prompt_id)
433
434    # Check if the prompt_id matches any enum value
435    if prompt_id not in [member.value for member in PromptGenerators]:
436        raise ValueError(f"Unknown prompt generator: {prompt_id}")
437    typed_prompt_generator = PromptGenerators(prompt_id)
438
439    match typed_prompt_generator:
440        case PromptGenerators.SIMPLE:
441            return SimplePromptBuilder(task)
442        case PromptGenerators.FEW_SHOT:
443            return FewShotPromptBuilder(task)
444        case PromptGenerators.MULTI_SHOT:
445            return MultiShotPromptBuilder(task)
446        case PromptGenerators.REPAIRS:
447            return RepairsPromptBuilder(task)
448        case PromptGenerators.SIMPLE_CHAIN_OF_THOUGHT:
449            return SimpleChainOfThoughtPromptBuilder(task)
450        case PromptGenerators.FEW_SHOT_CHAIN_OF_THOUGHT:
451            return FewShotChainOfThoughtPromptBuilder(task)
452        case PromptGenerators.MULTI_SHOT_CHAIN_OF_THOUGHT:
453            return MultiShotChainOfThoughtPromptBuilder(task)
454        case _:
455            # Type checking will find missing cases
456            raise_exhaustive_enum_error(typed_prompt_generator)
@dataclass
class PromptExample:
 9@dataclass
10class PromptExample:
11    """A simple input/output example for use in prompt building."""
12
13    input: str
14    output: str

A simple input/output example for use in prompt building.

PromptExample(input: str, output: str)
input: str
output: str
class BasePromptBuilder:
17class BasePromptBuilder(metaclass=ABCMeta):
18    """Base class for building prompts from tasks.
19
20    Provides the core interface and basic functionality for prompt builders.
21    """
22
23    def __init__(self, task: Task):
24        """Initialize the prompt builder with a task.
25
26        Args:
27            task (Task): The task containing instructions and requirements.
28        """
29        self.task = task
30
31    def prompt_id(self) -> str | None:
32        """Returns the ID of the prompt, scoped to this builder.
33
34        Returns:
35            str | None: The ID of the prompt, or None if not set.
36        """
37        return None
38
39    def build_prompt(self, include_json_instructions) -> str:
40        """Build and return the complete prompt string.
41
42        Returns:
43            str: The constructed prompt.
44        """
45        prompt = self.build_base_prompt()
46
47        if include_json_instructions and self.task.output_schema():
48            prompt = (
49                prompt
50                + f"\n\n# Format Instructions\n\nReturn a JSON object conforming to the following schema:\n```\n{self.task.output_schema()}\n```"
51            )
52
53        return prompt
54
55    @abstractmethod
56    def build_base_prompt(self) -> str:
57        """Build and return the complete prompt string.
58
59        Returns:
60            str: The constructed prompt.
61        """
62        pass
63
64    def chain_of_thought_prompt(self) -> str | None:
65        """Build and return the chain of thought prompt string.
66
67        Returns:
68            str: The constructed chain of thought prompt.
69        """
70        return None
71
72    def build_prompt_for_ui(self) -> str:
73        """Build a prompt for the UI. It includes additional instructions (like chain of thought), even if they are passed to the model in stages.
74
75        Designed for end-user consumption, not for model consumption.
76
77        Returns:
78            str: The constructed prompt string.
79        """
80        base_prompt = self.build_prompt(include_json_instructions=False)
81        cot_prompt = self.chain_of_thought_prompt()
82        if cot_prompt:
83            base_prompt += "\n\n# Thinking Instructions\n\n" + cot_prompt
84        return base_prompt

Base class for building prompts from tasks.

Provides the core interface and basic functionality for prompt builders.

BasePromptBuilder(task: kiln_ai.datamodel.Task)
23    def __init__(self, task: Task):
24        """Initialize the prompt builder with a task.
25
26        Args:
27            task (Task): The task containing instructions and requirements.
28        """
29        self.task = task

Initialize the prompt builder with a task.

Args: task (Task): The task containing instructions and requirements.

task
def prompt_id(self) -> str | None:
31    def prompt_id(self) -> str | None:
32        """Returns the ID of the prompt, scoped to this builder.
33
34        Returns:
35            str | None: The ID of the prompt, or None if not set.
36        """
37        return None

Returns the ID of the prompt, scoped to this builder.

Returns: str | None: The ID of the prompt, or None if not set.

def build_prompt(self, include_json_instructions) -> str:
39    def build_prompt(self, include_json_instructions) -> str:
40        """Build and return the complete prompt string.
41
42        Returns:
43            str: The constructed prompt.
44        """
45        prompt = self.build_base_prompt()
46
47        if include_json_instructions and self.task.output_schema():
48            prompt = (
49                prompt
50                + f"\n\n# Format Instructions\n\nReturn a JSON object conforming to the following schema:\n```\n{self.task.output_schema()}\n```"
51            )
52
53        return prompt

Build and return the complete prompt string.

Returns: str: The constructed prompt.

@abstractmethod
def build_base_prompt(self) -> str:
55    @abstractmethod
56    def build_base_prompt(self) -> str:
57        """Build and return the complete prompt string.
58
59        Returns:
60            str: The constructed prompt.
61        """
62        pass

Build and return the complete prompt string.

Returns: str: The constructed prompt.

def chain_of_thought_prompt(self) -> str | None:
64    def chain_of_thought_prompt(self) -> str | None:
65        """Build and return the chain of thought prompt string.
66
67        Returns:
68            str: The constructed chain of thought prompt.
69        """
70        return None

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

def build_prompt_for_ui(self) -> str:
72    def build_prompt_for_ui(self) -> str:
73        """Build a prompt for the UI. It includes additional instructions (like chain of thought), even if they are passed to the model in stages.
74
75        Designed for end-user consumption, not for model consumption.
76
77        Returns:
78            str: The constructed prompt string.
79        """
80        base_prompt = self.build_prompt(include_json_instructions=False)
81        cot_prompt = self.chain_of_thought_prompt()
82        if cot_prompt:
83            base_prompt += "\n\n# Thinking Instructions\n\n" + cot_prompt
84        return base_prompt

Build a prompt for the UI. It includes additional instructions (like chain of thought), even if they are passed to the model in stages.

Designed for end-user consumption, not for model consumption.

Returns: str: The constructed prompt string.

class SimplePromptBuilder(BasePromptBuilder):
 87class SimplePromptBuilder(BasePromptBuilder):
 88    """A basic prompt builder that combines task instruction with requirements."""
 89
 90    def build_base_prompt(self) -> str:
 91        """Build a simple prompt with instruction and requirements.
 92
 93        Returns:
 94            str: The constructed prompt string.
 95        """
 96        base_prompt = self.task.instruction
 97
 98        if len(self.task.requirements) > 0:
 99            base_prompt += (
100                "\n\nYour response should respect the following requirements:\n"
101            )
102            # iterate requirements, formatting them in numbereed list like 1) task.instruction\n2)...
103            for i, requirement in enumerate(self.task.requirements):
104                base_prompt += f"{i + 1}) {requirement.instruction}\n"
105
106        return base_prompt

A basic prompt builder that combines task instruction with requirements.

def build_base_prompt(self) -> str:
 90    def build_base_prompt(self) -> str:
 91        """Build a simple prompt with instruction and requirements.
 92
 93        Returns:
 94            str: The constructed prompt string.
 95        """
 96        base_prompt = self.task.instruction
 97
 98        if len(self.task.requirements) > 0:
 99            base_prompt += (
100                "\n\nYour response should respect the following requirements:\n"
101            )
102            # iterate requirements, formatting them in numbereed list like 1) task.instruction\n2)...
103            for i, requirement in enumerate(self.task.requirements):
104                base_prompt += f"{i + 1}) {requirement.instruction}\n"
105
106        return base_prompt

Build a simple prompt with instruction and requirements.

Returns: str: The constructed prompt string.

class MultiShotPromptBuilder(BasePromptBuilder):
109class MultiShotPromptBuilder(BasePromptBuilder):
110    """A prompt builder that includes multiple examples in the prompt."""
111
112    @classmethod
113    def example_count(cls) -> int:
114        """Get the maximum number of examples to include in the prompt.
115
116        Returns:
117            int: The maximum number of examples (default 25).
118        """
119        return 25
120
121    def build_instruction_and_requirements(self) -> str:
122        """Build the instruction and requirements section of the prompt.
123
124        Returns:
125            str: The instruction and requirements sections.
126        """
127        base_prompt = f"# Instruction\n\n{self.task.instruction}\n\n"
128
129        if len(self.task.requirements) > 0:
130            base_prompt += "# Requirements\n\nYour response should respect the following requirements:\n"
131            for i, requirement in enumerate(self.task.requirements):
132                base_prompt += f"{i + 1}) {requirement.instruction}\n"
133            base_prompt += "\n"
134
135        return base_prompt
136
137    def build_base_prompt(self) -> str:
138        """Build a prompt with instruction, requirements, and multiple examples.
139
140        Returns:
141            str: The constructed prompt string with examples.
142        """
143        base_prompt = self.build_instruction_and_requirements()
144
145        valid_examples = self.collect_examples()
146
147        if len(valid_examples) == 0:
148            return base_prompt
149
150        base_prompt += "# Example Outputs\n\n"
151        for i, example in enumerate(valid_examples):
152            base_prompt += self.prompt_section_for_example(i, example)
153
154        return base_prompt
155
156    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
157        # Prefer repaired output if it exists, otherwise use the regular output
158        output = example.repaired_output or example.output
159        return f"## Example {index + 1}\n\nInput: {example.input}\nOutput: {output.output}\n\n"
160
161    def collect_examples(self) -> list[TaskRun]:
162        valid_examples: list[TaskRun] = []
163        runs = self.task.runs(readonly=True)
164
165        # first pass, we look for repaired outputs. These are the best examples.
166        for run in runs:
167            if len(valid_examples) >= self.__class__.example_count():
168                break
169            if run.repaired_output is not None:
170                valid_examples.append(run)
171
172        # second pass, we look for high quality outputs (rating based)
173        # Minimum is "high_quality" (4 star in star rating scale), then sort by rating
174        # exclude repaired outputs as they were used above
175        runs_with_rating = [
176            run
177            for run in runs
178            if run.output.rating is not None
179            and run.output.rating.value is not None
180            and run.output.rating.is_high_quality()
181            and run.repaired_output is None
182        ]
183        runs_with_rating.sort(
184            key=lambda x: (x.output.rating and x.output.rating.value) or 0, reverse=True
185        )
186        for run in runs_with_rating:
187            if len(valid_examples) >= self.__class__.example_count():
188                break
189            valid_examples.append(run)
190        return valid_examples

A prompt builder that includes multiple examples in the prompt.

@classmethod
def example_count(cls) -> int:
112    @classmethod
113    def example_count(cls) -> int:
114        """Get the maximum number of examples to include in the prompt.
115
116        Returns:
117            int: The maximum number of examples (default 25).
118        """
119        return 25

Get the maximum number of examples to include in the prompt.

Returns: int: The maximum number of examples (default 25).

def build_instruction_and_requirements(self) -> str:
121    def build_instruction_and_requirements(self) -> str:
122        """Build the instruction and requirements section of the prompt.
123
124        Returns:
125            str: The instruction and requirements sections.
126        """
127        base_prompt = f"# Instruction\n\n{self.task.instruction}\n\n"
128
129        if len(self.task.requirements) > 0:
130            base_prompt += "# Requirements\n\nYour response should respect the following requirements:\n"
131            for i, requirement in enumerate(self.task.requirements):
132                base_prompt += f"{i + 1}) {requirement.instruction}\n"
133            base_prompt += "\n"
134
135        return base_prompt

Build the instruction and requirements section of the prompt.

Returns: str: The instruction and requirements sections.

def build_base_prompt(self) -> str:
137    def build_base_prompt(self) -> str:
138        """Build a prompt with instruction, requirements, and multiple examples.
139
140        Returns:
141            str: The constructed prompt string with examples.
142        """
143        base_prompt = self.build_instruction_and_requirements()
144
145        valid_examples = self.collect_examples()
146
147        if len(valid_examples) == 0:
148            return base_prompt
149
150        base_prompt += "# Example Outputs\n\n"
151        for i, example in enumerate(valid_examples):
152            base_prompt += self.prompt_section_for_example(i, example)
153
154        return base_prompt

Build a prompt with instruction, requirements, and multiple examples.

Returns: str: The constructed prompt string with examples.

def prompt_section_for_example(self, index: int, example: kiln_ai.datamodel.TaskRun) -> str:
156    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
157        # Prefer repaired output if it exists, otherwise use the regular output
158        output = example.repaired_output or example.output
159        return f"## Example {index + 1}\n\nInput: {example.input}\nOutput: {output.output}\n\n"
def collect_examples(self) -> list[kiln_ai.datamodel.TaskRun]:
161    def collect_examples(self) -> list[TaskRun]:
162        valid_examples: list[TaskRun] = []
163        runs = self.task.runs(readonly=True)
164
165        # first pass, we look for repaired outputs. These are the best examples.
166        for run in runs:
167            if len(valid_examples) >= self.__class__.example_count():
168                break
169            if run.repaired_output is not None:
170                valid_examples.append(run)
171
172        # second pass, we look for high quality outputs (rating based)
173        # Minimum is "high_quality" (4 star in star rating scale), then sort by rating
174        # exclude repaired outputs as they were used above
175        runs_with_rating = [
176            run
177            for run in runs
178            if run.output.rating is not None
179            and run.output.rating.value is not None
180            and run.output.rating.is_high_quality()
181            and run.repaired_output is None
182        ]
183        runs_with_rating.sort(
184            key=lambda x: (x.output.rating and x.output.rating.value) or 0, reverse=True
185        )
186        for run in runs_with_rating:
187            if len(valid_examples) >= self.__class__.example_count():
188                break
189            valid_examples.append(run)
190        return valid_examples
class FewShotPromptBuilder(MultiShotPromptBuilder):
193class FewShotPromptBuilder(MultiShotPromptBuilder):
194    """A prompt builder that includes a small number of examples in the prompt."""
195
196    @classmethod
197    def example_count(cls) -> int:
198        """Get the maximum number of examples to include in the prompt.
199
200        Returns:
201            int: The maximum number of examples (4).
202        """
203        return 4

A prompt builder that includes a small number of examples in the prompt.

@classmethod
def example_count(cls) -> int:
196    @classmethod
197    def example_count(cls) -> int:
198        """Get the maximum number of examples to include in the prompt.
199
200        Returns:
201            int: The maximum number of examples (4).
202        """
203        return 4

Get the maximum number of examples to include in the prompt.

Returns: int: The maximum number of examples (4).

class CustomExamplePromptBuilder(FewShotPromptBuilder):
206class CustomExamplePromptBuilder(FewShotPromptBuilder):
207    """A prompt builder that uses custom examples instead of collecting from the dataset."""
208
209    def __init__(self, task: Task, examples: list[PromptExample] | None = None):
210        super().__init__(task)
211        self._custom_examples = examples or []
212
213    def collect_examples(self) -> list[TaskRun]:
214        """Override to return an empty list - we handle examples separately."""
215        return []
216
217    def build_base_prompt(self) -> str:
218        """Build a prompt with instruction, requirements, and custom examples."""
219        base_prompt = self.build_instruction_and_requirements()
220
221        if self._custom_examples:
222            base_prompt += "# Example Outputs\n\n"
223            for i, example in enumerate(self._custom_examples):
224                base_prompt += f"## Example {i + 1}\n\nInput: {example.input}\nOutput: {example.output}\n\n"
225
226        return base_prompt

A prompt builder that uses custom examples instead of collecting from the dataset.

CustomExamplePromptBuilder( task: kiln_ai.datamodel.Task, examples: list[PromptExample] | None = None)
209    def __init__(self, task: Task, examples: list[PromptExample] | None = None):
210        super().__init__(task)
211        self._custom_examples = examples or []

Initialize the prompt builder with a task.

Args: task (Task): The task containing instructions and requirements.

def collect_examples(self) -> list[kiln_ai.datamodel.TaskRun]:
213    def collect_examples(self) -> list[TaskRun]:
214        """Override to return an empty list - we handle examples separately."""
215        return []

Override to return an empty list - we handle examples separately.

def build_base_prompt(self) -> str:
217    def build_base_prompt(self) -> str:
218        """Build a prompt with instruction, requirements, and custom examples."""
219        base_prompt = self.build_instruction_and_requirements()
220
221        if self._custom_examples:
222            base_prompt += "# Example Outputs\n\n"
223            for i, example in enumerate(self._custom_examples):
224                base_prompt += f"## Example {i + 1}\n\nInput: {example.input}\nOutput: {example.output}\n\n"
225
226        return base_prompt

Build a prompt with instruction, requirements, and custom examples.

class RepairsPromptBuilder(MultiShotPromptBuilder):
229class RepairsPromptBuilder(MultiShotPromptBuilder):
230    """A prompt builder that includes multiple examples in the prompt, including repaired instructions describing what was wrong, and how it was fixed."""
231
232    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
233        if (
234            not example.repaired_output
235            or not example.repair_instructions
236            or not example.repaired_output.output
237        ):
238            return super().prompt_section_for_example(index, example)
239
240        prompt_section = f"## Example {index + 1}\n\nInput: {example.input}\n\n"
241        prompt_section += (
242            f"Initial Output Which Was Insufficient: {example.output.output}\n\n"
243        )
244        prompt_section += f"Instructions On How to Improve the Initial Output: {example.repair_instructions}\n\n"
245        prompt_section += (
246            f"Repaired Output Which is Sufficient: {example.repaired_output.output}\n\n"
247        )
248        return prompt_section

A prompt builder that includes multiple examples in the prompt, including repaired instructions describing what was wrong, and how it was fixed.

def prompt_section_for_example(self, index: int, example: kiln_ai.datamodel.TaskRun) -> str:
232    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
233        if (
234            not example.repaired_output
235            or not example.repair_instructions
236            or not example.repaired_output.output
237        ):
238            return super().prompt_section_for_example(index, example)
239
240        prompt_section = f"## Example {index + 1}\n\nInput: {example.input}\n\n"
241        prompt_section += (
242            f"Initial Output Which Was Insufficient: {example.output.output}\n\n"
243        )
244        prompt_section += f"Instructions On How to Improve the Initial Output: {example.repair_instructions}\n\n"
245        prompt_section += (
246            f"Repaired Output Which is Sufficient: {example.repaired_output.output}\n\n"
247        )
248        return prompt_section
def chain_of_thought_prompt(task: kiln_ai.datamodel.Task) -> str:
251def chain_of_thought_prompt(task: Task) -> str:
252    """Standard implementation to build and return the chain of thought prompt string.
253
254    Returns:
255        str: The constructed chain of thought prompt.
256    """
257
258    cot_instruction = task.thinking_instruction
259    if not cot_instruction:
260        cot_instruction = "Think step by step, explaining your reasoning."
261
262    return cot_instruction

Standard implementation to build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class SimpleChainOfThoughtPromptBuilder(SimplePromptBuilder):
265class SimpleChainOfThoughtPromptBuilder(SimplePromptBuilder):
266    """A prompt builder that includes a chain of thought prompt on top of the simple prompt."""
267
268    def chain_of_thought_prompt(self) -> str | None:
269        return chain_of_thought_prompt(self.task)

A prompt builder that includes a chain of thought prompt on top of the simple prompt.

def chain_of_thought_prompt(self) -> str | None:
268    def chain_of_thought_prompt(self) -> str | None:
269        return chain_of_thought_prompt(self.task)

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class FewShotChainOfThoughtPromptBuilder(FewShotPromptBuilder):
272class FewShotChainOfThoughtPromptBuilder(FewShotPromptBuilder):
273    """A prompt builder that includes a chain of thought prompt on top of the few shot prompt."""
274
275    def chain_of_thought_prompt(self) -> str | None:
276        return chain_of_thought_prompt(self.task)

A prompt builder that includes a chain of thought prompt on top of the few shot prompt.

def chain_of_thought_prompt(self) -> str | None:
275    def chain_of_thought_prompt(self) -> str | None:
276        return chain_of_thought_prompt(self.task)

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class MultiShotChainOfThoughtPromptBuilder(MultiShotPromptBuilder):
279class MultiShotChainOfThoughtPromptBuilder(MultiShotPromptBuilder):
280    """A prompt builder that includes a chain of thought prompt on top of the multi shot prompt."""
281
282    def chain_of_thought_prompt(self) -> str | None:
283        return chain_of_thought_prompt(self.task)

A prompt builder that includes a chain of thought prompt on top of the multi shot prompt.

def chain_of_thought_prompt(self) -> str | None:
282    def chain_of_thought_prompt(self) -> str | None:
283        return chain_of_thought_prompt(self.task)

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class SavedPromptBuilder(BasePromptBuilder):
286class SavedPromptBuilder(BasePromptBuilder):
287    """A prompt builder that looks up a static prompt."""
288
289    def __init__(self, task: Task, prompt_id: str):
290        super().__init__(task)
291        prompt_model = next(
292            (
293                prompt
294                for prompt in task.prompts(readonly=True)
295                if prompt.id == prompt_id
296            ),
297            None,
298        )
299        if not prompt_model:
300            raise ValueError(f"Prompt ID not found: {prompt_id}")
301        self.prompt_model = prompt_model
302
303    def prompt_id(self) -> str | None:
304        return self.prompt_model.id
305
306    def build_base_prompt(self) -> str:
307        """Returns a saved prompt.
308
309        Returns:
310            str: The prompt string.
311        """
312        return self.prompt_model.prompt
313
314    def chain_of_thought_prompt(self) -> str | None:
315        return self.prompt_model.chain_of_thought_instructions

A prompt builder that looks up a static prompt.

SavedPromptBuilder(task: kiln_ai.datamodel.Task, prompt_id: str)
289    def __init__(self, task: Task, prompt_id: str):
290        super().__init__(task)
291        prompt_model = next(
292            (
293                prompt
294                for prompt in task.prompts(readonly=True)
295                if prompt.id == prompt_id
296            ),
297            None,
298        )
299        if not prompt_model:
300            raise ValueError(f"Prompt ID not found: {prompt_id}")
301        self.prompt_model = prompt_model

Initialize the prompt builder with a task.

Args: task (Task): The task containing instructions and requirements.

prompt_model
def prompt_id(self) -> str | None:
303    def prompt_id(self) -> str | None:
304        return self.prompt_model.id

Returns the ID of the prompt, scoped to this builder.

Returns: str | None: The ID of the prompt, or None if not set.

def build_base_prompt(self) -> str:
306    def build_base_prompt(self) -> str:
307        """Returns a saved prompt.
308
309        Returns:
310            str: The prompt string.
311        """
312        return self.prompt_model.prompt

Returns a saved prompt.

Returns: str: The prompt string.

def chain_of_thought_prompt(self) -> str | None:
314    def chain_of_thought_prompt(self) -> str | None:
315        return self.prompt_model.chain_of_thought_instructions

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class TaskRunConfigPromptBuilder(BasePromptBuilder):
318class TaskRunConfigPromptBuilder(BasePromptBuilder):
319    """A prompt builder that looks up a static prompt in a task run config."""
320
321    def __init__(self, task: Task, run_config_prompt_id: str):
322        parts = run_config_prompt_id.split("::")
323        if len(parts) != 4:
324            raise ValueError(
325                f"Invalid task run config prompt ID: {run_config_prompt_id}. Expected format: 'task_run_config::[project_id]::[task_id]::[run_config_id]'."
326            )
327
328        task_id = parts[2]
329        if task_id != task.id:
330            raise ValueError(
331                f"Task run config prompt ID: {run_config_prompt_id}. Task ID mismatch. Expected: {task.id}, got: {task_id}."
332            )
333
334        run_config_id = parts[3]
335        run_config = next(
336            (
337                run_config
338                for run_config in task.run_configs(readonly=True)
339                if run_config.id == run_config_id
340            ),
341            None,
342        )
343        if not run_config:
344            raise ValueError(
345                f"Task run config ID not found: {run_config_id} for prompt id {run_config_prompt_id}"
346            )
347        if run_config.prompt is None:
348            raise ValueError(
349                f"Task run config ID {run_config_id} does not have a stored prompt. Used as prompt id {run_config_prompt_id}"
350            )
351
352        # Load the prompt from the model
353        self.prompt = run_config.prompt.prompt
354        self.cot_prompt = run_config.prompt.chain_of_thought_instructions
355        self.id = run_config_prompt_id
356
357        super().__init__(task)
358
359    def prompt_id(self) -> str | None:
360        return self.id
361
362    def build_base_prompt(self) -> str:
363        return self.prompt
364
365    def chain_of_thought_prompt(self) -> str | None:
366        return self.cot_prompt

A prompt builder that looks up a static prompt in a task run config.

TaskRunConfigPromptBuilder(task: kiln_ai.datamodel.Task, run_config_prompt_id: str)
321    def __init__(self, task: Task, run_config_prompt_id: str):
322        parts = run_config_prompt_id.split("::")
323        if len(parts) != 4:
324            raise ValueError(
325                f"Invalid task run config prompt ID: {run_config_prompt_id}. Expected format: 'task_run_config::[project_id]::[task_id]::[run_config_id]'."
326            )
327
328        task_id = parts[2]
329        if task_id != task.id:
330            raise ValueError(
331                f"Task run config prompt ID: {run_config_prompt_id}. Task ID mismatch. Expected: {task.id}, got: {task_id}."
332            )
333
334        run_config_id = parts[3]
335        run_config = next(
336            (
337                run_config
338                for run_config in task.run_configs(readonly=True)
339                if run_config.id == run_config_id
340            ),
341            None,
342        )
343        if not run_config:
344            raise ValueError(
345                f"Task run config ID not found: {run_config_id} for prompt id {run_config_prompt_id}"
346            )
347        if run_config.prompt is None:
348            raise ValueError(
349                f"Task run config ID {run_config_id} does not have a stored prompt. Used as prompt id {run_config_prompt_id}"
350            )
351
352        # Load the prompt from the model
353        self.prompt = run_config.prompt.prompt
354        self.cot_prompt = run_config.prompt.chain_of_thought_instructions
355        self.id = run_config_prompt_id
356
357        super().__init__(task)

Initialize the prompt builder with a task.

Args: task (Task): The task containing instructions and requirements.

prompt
cot_prompt
id
def prompt_id(self) -> str | None:
359    def prompt_id(self) -> str | None:
360        return self.id

Returns the ID of the prompt, scoped to this builder.

Returns: str | None: The ID of the prompt, or None if not set.

def build_base_prompt(self) -> str:
362    def build_base_prompt(self) -> str:
363        return self.prompt

Build and return the complete prompt string.

Returns: str: The constructed prompt.

def chain_of_thought_prompt(self) -> str | None:
365    def chain_of_thought_prompt(self) -> str | None:
366        return self.cot_prompt

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class FineTunePromptBuilder(BasePromptBuilder):
369class FineTunePromptBuilder(BasePromptBuilder):
370    """A prompt builder that looks up a fine-tune prompt."""
371
372    def __init__(self, task: Task, nested_fine_tune_id: str):
373        super().__init__(task)
374
375        # IDs are in project_id::task_id::fine_tune_id format
376        self.full_fine_tune_id = nested_fine_tune_id
377        parts = nested_fine_tune_id.split("::")
378        if len(parts) != 3:
379            raise ValueError(
380                f"Invalid fine-tune ID format. Expected 'project_id::task_id::fine_tune_id', got: {nested_fine_tune_id}"
381            )
382        fine_tune_id = parts[2]
383
384        fine_tune_model = next(
385            (
386                fine_tune
387                for fine_tune in task.finetunes(readonly=True)
388                if fine_tune.id == fine_tune_id
389            ),
390            None,
391        )
392        if not fine_tune_model:
393            raise ValueError(f"Fine-tune ID not found: {fine_tune_id}")
394        self.fine_tune_model = fine_tune_model
395
396    def prompt_id(self) -> str | None:
397        return self.full_fine_tune_id
398
399    def build_base_prompt(self) -> str:
400        return self.fine_tune_model.system_message
401
402    def chain_of_thought_prompt(self) -> str | None:
403        return self.fine_tune_model.thinking_instructions

A prompt builder that looks up a fine-tune prompt.

FineTunePromptBuilder(task: kiln_ai.datamodel.Task, nested_fine_tune_id: str)
372    def __init__(self, task: Task, nested_fine_tune_id: str):
373        super().__init__(task)
374
375        # IDs are in project_id::task_id::fine_tune_id format
376        self.full_fine_tune_id = nested_fine_tune_id
377        parts = nested_fine_tune_id.split("::")
378        if len(parts) != 3:
379            raise ValueError(
380                f"Invalid fine-tune ID format. Expected 'project_id::task_id::fine_tune_id', got: {nested_fine_tune_id}"
381            )
382        fine_tune_id = parts[2]
383
384        fine_tune_model = next(
385            (
386                fine_tune
387                for fine_tune in task.finetunes(readonly=True)
388                if fine_tune.id == fine_tune_id
389            ),
390            None,
391        )
392        if not fine_tune_model:
393            raise ValueError(f"Fine-tune ID not found: {fine_tune_id}")
394        self.fine_tune_model = fine_tune_model

Initialize the prompt builder with a task.

Args: task (Task): The task containing instructions and requirements.

full_fine_tune_id
fine_tune_model
def prompt_id(self) -> str | None:
396    def prompt_id(self) -> str | None:
397        return self.full_fine_tune_id

Returns the ID of the prompt, scoped to this builder.

Returns: str | None: The ID of the prompt, or None if not set.

def build_base_prompt(self) -> str:
399    def build_base_prompt(self) -> str:
400        return self.fine_tune_model.system_message

Build and return the complete prompt string.

Returns: str: The constructed prompt.

def chain_of_thought_prompt(self) -> str | None:
402    def chain_of_thought_prompt(self) -> str | None:
403        return self.fine_tune_model.thinking_instructions

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

def prompt_builder_from_id( prompt_id: Annotated[str, AfterValidator(func=<function <lambda>>)], task: kiln_ai.datamodel.Task) -> BasePromptBuilder:
407def prompt_builder_from_id(prompt_id: PromptId, task: Task) -> BasePromptBuilder:
408    """Convert a name used in the UI to the corresponding prompt builder class.
409
410    Args:
411        prompt_id (PromptId): The prompt ID.
412
413    Returns:
414        type[BasePromptBuilder]: The corresponding prompt builder class.
415
416    Raises:
417        ValueError: If the UI name is not recognized.
418    """
419
420    # Saved prompts are prefixed with "id::"
421    if prompt_id.startswith("id::"):
422        prompt_id = prompt_id[4:]
423        return SavedPromptBuilder(task, prompt_id)
424
425    # Task run config prompts are prefixed with "task_run_config::"
426    # task_run_config::[project_id]::[task_id]::[run_config_id]
427    if prompt_id.startswith("task_run_config::"):
428        return TaskRunConfigPromptBuilder(task, prompt_id)
429
430    # Fine-tune prompts are prefixed with "fine_tune_prompt::"
431    if prompt_id.startswith("fine_tune_prompt::"):
432        prompt_id = prompt_id[18:]
433        return FineTunePromptBuilder(task, prompt_id)
434
435    # Check if the prompt_id matches any enum value
436    if prompt_id not in [member.value for member in PromptGenerators]:
437        raise ValueError(f"Unknown prompt generator: {prompt_id}")
438    typed_prompt_generator = PromptGenerators(prompt_id)
439
440    match typed_prompt_generator:
441        case PromptGenerators.SIMPLE:
442            return SimplePromptBuilder(task)
443        case PromptGenerators.FEW_SHOT:
444            return FewShotPromptBuilder(task)
445        case PromptGenerators.MULTI_SHOT:
446            return MultiShotPromptBuilder(task)
447        case PromptGenerators.REPAIRS:
448            return RepairsPromptBuilder(task)
449        case PromptGenerators.SIMPLE_CHAIN_OF_THOUGHT:
450            return SimpleChainOfThoughtPromptBuilder(task)
451        case PromptGenerators.FEW_SHOT_CHAIN_OF_THOUGHT:
452            return FewShotChainOfThoughtPromptBuilder(task)
453        case PromptGenerators.MULTI_SHOT_CHAIN_OF_THOUGHT:
454            return MultiShotChainOfThoughtPromptBuilder(task)
455        case _:
456            # Type checking will find missing cases
457            raise_exhaustive_enum_error(typed_prompt_generator)

Convert a name used in the UI to the corresponding prompt builder class.

Args: prompt_id (PromptId): The prompt ID.

Returns: type[BasePromptBuilder]: The corresponding prompt builder class.

Raises: ValueError: If the UI name is not recognized.