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# 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 ShortPromptBuilder(BasePromptBuilder):
109    """A prompt builder that includes a the base prompt but excludes the requirements."""
110
111    def build_base_prompt(self) -> str:
112        """Build a short prompt with just the base prompt, no requirements.
113
114        Returns:
115            str: The constructed prompt string.
116        """
117        return self.task.instruction
118
119
120class MultiShotPromptBuilder(BasePromptBuilder):
121    """A prompt builder that includes multiple examples in the prompt."""
122
123    @classmethod
124    def example_count(cls) -> int:
125        """Get the maximum number of examples to include in the prompt.
126
127        Returns:
128            int: The maximum number of examples (default 25).
129        """
130        return 25
131
132    def build_instruction_and_requirements(self) -> str:
133        """Build the instruction and requirements section of the prompt.
134
135        Returns:
136            str: The instruction and requirements sections.
137        """
138        base_prompt = f"# Instruction\n\n{self.task.instruction}\n\n"
139
140        if len(self.task.requirements) > 0:
141            base_prompt += "# Requirements\n\nYour response should respect the following requirements:\n"
142            for i, requirement in enumerate(self.task.requirements):
143                base_prompt += f"{i + 1}) {requirement.instruction}\n"
144            base_prompt += "\n"
145
146        return base_prompt
147
148    def build_base_prompt(self) -> str:
149        """Build a prompt with instruction, requirements, and multiple examples.
150
151        Returns:
152            str: The constructed prompt string with examples.
153        """
154        base_prompt = self.build_instruction_and_requirements()
155
156        valid_examples = self.collect_examples()
157
158        if len(valid_examples) == 0:
159            return base_prompt
160
161        base_prompt += "# Example Outputs\n\n"
162        for i, example in enumerate(valid_examples):
163            base_prompt += self.prompt_section_for_example(i, example)
164
165        return base_prompt
166
167    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
168        # Prefer repaired output if it exists, otherwise use the regular output
169        output = example.repaired_output or example.output
170        return f"## Example {index + 1}\n\nInput: {example.input}\nOutput: {output.output}\n\n"
171
172    def collect_examples(self) -> list[TaskRun]:
173        valid_examples: list[TaskRun] = []
174        runs = self.task.runs(readonly=True)
175
176        # first pass, we look for repaired outputs. These are the best examples.
177        for run in runs:
178            if len(valid_examples) >= self.__class__.example_count():
179                break
180            if run.repaired_output is not None:
181                valid_examples.append(run)
182
183        # second pass, we look for high quality outputs (rating based)
184        # Minimum is "high_quality" (4 star in star rating scale), then sort by rating
185        # exclude repaired outputs as they were used above
186        runs_with_rating = [
187            run
188            for run in runs
189            if run.output.rating is not None
190            and run.output.rating.value is not None
191            and run.output.rating.is_high_quality()
192            and run.repaired_output is None
193        ]
194        runs_with_rating.sort(
195            key=lambda x: (x.output.rating and x.output.rating.value) or 0, reverse=True
196        )
197        for run in runs_with_rating:
198            if len(valid_examples) >= self.__class__.example_count():
199                break
200            valid_examples.append(run)
201        return valid_examples
202
203
204class FewShotPromptBuilder(MultiShotPromptBuilder):
205    """A prompt builder that includes a small number of examples in the prompt."""
206
207    @classmethod
208    def example_count(cls) -> int:
209        """Get the maximum number of examples to include in the prompt.
210
211        Returns:
212            int: The maximum number of examples (4).
213        """
214        return 4
215
216
217class CustomExamplePromptBuilder(FewShotPromptBuilder):
218    """A prompt builder that uses custom examples instead of collecting from the dataset."""
219
220    def __init__(self, task: Task, examples: list[PromptExample] | None = None):
221        super().__init__(task)
222        self._custom_examples = examples or []
223
224    def collect_examples(self) -> list[TaskRun]:
225        """Override to return an empty list - we handle examples separately."""
226        return []
227
228    def build_base_prompt(self) -> str:
229        """Build a prompt with instruction, requirements, and custom examples."""
230        base_prompt = self.build_instruction_and_requirements()
231
232        if self._custom_examples:
233            base_prompt += "# Example Outputs\n\n"
234            for i, example in enumerate(self._custom_examples):
235                base_prompt += f"## Example {i + 1}\n\nInput: {example.input}\nOutput: {example.output}\n\n"
236
237        return base_prompt
238
239
240class RepairsPromptBuilder(MultiShotPromptBuilder):
241    """A prompt builder that includes multiple examples in the prompt, including repaired instructions describing what was wrong, and how it was fixed."""
242
243    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
244        if (
245            not example.repaired_output
246            or not example.repair_instructions
247            or not example.repaired_output.output
248        ):
249            return super().prompt_section_for_example(index, example)
250
251        prompt_section = f"## Example {index + 1}\n\nInput: {example.input}\n\n"
252        prompt_section += (
253            f"Initial Output Which Was Insufficient: {example.output.output}\n\n"
254        )
255        prompt_section += f"Instructions On How to Improve the Initial Output: {example.repair_instructions}\n\n"
256        prompt_section += (
257            f"Repaired Output Which is Sufficient: {example.repaired_output.output}\n\n"
258        )
259        return prompt_section
260
261
262def chain_of_thought_prompt(task: Task) -> str:
263    """Standard implementation to build and return the chain of thought prompt string.
264
265    Returns:
266        str: The constructed chain of thought prompt.
267    """
268
269    cot_instruction = task.thinking_instruction
270    if not cot_instruction:
271        cot_instruction = "Think step by step, explaining your reasoning."
272
273    return cot_instruction
274
275
276class SimpleChainOfThoughtPromptBuilder(SimplePromptBuilder):
277    """A prompt builder that includes a chain of thought prompt on top of the simple prompt."""
278
279    def chain_of_thought_prompt(self) -> str | None:
280        return chain_of_thought_prompt(self.task)
281
282
283class FewShotChainOfThoughtPromptBuilder(FewShotPromptBuilder):
284    """A prompt builder that includes a chain of thought prompt on top of the few shot prompt."""
285
286    def chain_of_thought_prompt(self) -> str | None:
287        return chain_of_thought_prompt(self.task)
288
289
290class MultiShotChainOfThoughtPromptBuilder(MultiShotPromptBuilder):
291    """A prompt builder that includes a chain of thought prompt on top of the multi shot prompt."""
292
293    def chain_of_thought_prompt(self) -> str | None:
294        return chain_of_thought_prompt(self.task)
295
296
297class SavedPromptBuilder(BasePromptBuilder):
298    """A prompt builder that looks up a static prompt."""
299
300    def __init__(self, task: Task, prompt_id: str):
301        super().__init__(task)
302        prompt_model = next(
303            (
304                prompt
305                for prompt in task.prompts(readonly=True)
306                if prompt.id == prompt_id
307            ),
308            None,
309        )
310        if not prompt_model:
311            raise ValueError(f"Prompt ID not found: {prompt_id}")
312        self.prompt_model = prompt_model
313
314    def prompt_id(self) -> str | None:
315        return self.prompt_model.id
316
317    def build_base_prompt(self) -> str:
318        """Returns a saved prompt.
319
320        Returns:
321            str: The prompt string.
322        """
323        return self.prompt_model.prompt
324
325    def chain_of_thought_prompt(self) -> str | None:
326        return self.prompt_model.chain_of_thought_instructions
327
328
329class TaskRunConfigPromptBuilder(BasePromptBuilder):
330    """A prompt builder that looks up a static prompt in a task run config."""
331
332    def __init__(self, task: Task, run_config_prompt_id: str):
333        parts = run_config_prompt_id.split("::")
334        if len(parts) != 4:
335            raise ValueError(
336                f"Invalid task run config prompt ID: {run_config_prompt_id}. Expected format: 'task_run_config::[project_id]::[task_id]::[run_config_id]'."
337            )
338
339        task_id = parts[2]
340        if task_id != task.id:
341            raise ValueError(
342                f"Task run config prompt ID: {run_config_prompt_id}. Task ID mismatch. Expected: {task.id}, got: {task_id}."
343            )
344
345        run_config_id = parts[3]
346        run_config = next(
347            (
348                run_config
349                for run_config in task.run_configs(readonly=True)
350                if run_config.id == run_config_id
351            ),
352            None,
353        )
354        if not run_config:
355            raise ValueError(
356                f"Task run config ID not found: {run_config_id} for prompt id {run_config_prompt_id}"
357            )
358        if run_config.prompt is None:
359            raise ValueError(
360                f"Task run config ID {run_config_id} does not have a stored prompt. Used as prompt id {run_config_prompt_id}"
361            )
362
363        # Load the prompt from the model
364        self.prompt = run_config.prompt.prompt
365        self.cot_prompt = run_config.prompt.chain_of_thought_instructions
366        self.id = run_config_prompt_id
367
368        super().__init__(task)
369
370    def prompt_id(self) -> str | None:
371        return self.id
372
373    def build_base_prompt(self) -> str:
374        return self.prompt
375
376    def chain_of_thought_prompt(self) -> str | None:
377        return self.cot_prompt
378
379
380class FineTunePromptBuilder(BasePromptBuilder):
381    """A prompt builder that looks up a fine-tune prompt."""
382
383    def __init__(self, task: Task, nested_fine_tune_id: str):
384        super().__init__(task)
385
386        # IDs are in project_id::task_id::fine_tune_id format
387        self.full_fine_tune_id = nested_fine_tune_id
388        parts = nested_fine_tune_id.split("::")
389        if len(parts) != 3:
390            raise ValueError(
391                f"Invalid fine-tune ID format. Expected 'project_id::task_id::fine_tune_id', got: {nested_fine_tune_id}"
392            )
393        fine_tune_id = parts[2]
394
395        fine_tune_model = next(
396            (
397                fine_tune
398                for fine_tune in task.finetunes(readonly=True)
399                if fine_tune.id == fine_tune_id
400            ),
401            None,
402        )
403        if not fine_tune_model:
404            raise ValueError(f"Fine-tune ID not found: {fine_tune_id}")
405        self.fine_tune_model = fine_tune_model
406
407    def prompt_id(self) -> str | None:
408        return self.full_fine_tune_id
409
410    def build_base_prompt(self) -> str:
411        return self.fine_tune_model.system_message
412
413    def chain_of_thought_prompt(self) -> str | None:
414        return self.fine_tune_model.thinking_instructions
415
416
417# Our UI has some names that are not the same as the class names, which also hint parameters.
418def prompt_builder_from_id(prompt_id: PromptId, task: Task) -> BasePromptBuilder:
419    """Convert a name used in the UI to the corresponding prompt builder class.
420
421    Args:
422        prompt_id (PromptId): The prompt ID.
423
424    Returns:
425        type[BasePromptBuilder]: The corresponding prompt builder class.
426
427    Raises:
428        ValueError: If the UI name is not recognized.
429    """
430
431    # Saved prompts are prefixed with "id::"
432    if prompt_id.startswith("id::"):
433        prompt_id = prompt_id[4:]
434        return SavedPromptBuilder(task, prompt_id)
435
436    # Task run config prompts are prefixed with "task_run_config::"
437    # task_run_config::[project_id]::[task_id]::[run_config_id]
438    if prompt_id.startswith("task_run_config::"):
439        return TaskRunConfigPromptBuilder(task, prompt_id)
440
441    # Fine-tune prompts are prefixed with "fine_tune_prompt::"
442    if prompt_id.startswith("fine_tune_prompt::"):
443        prompt_id = prompt_id[18:]
444        return FineTunePromptBuilder(task, prompt_id)
445
446    # Check if the prompt_id matches any enum value
447    if prompt_id not in [member.value for member in PromptGenerators]:
448        raise ValueError(f"Unknown prompt generator: {prompt_id}")
449    typed_prompt_generator = PromptGenerators(prompt_id)
450
451    match typed_prompt_generator:
452        case PromptGenerators.SIMPLE:
453            return SimplePromptBuilder(task)
454        case PromptGenerators.SHORT:
455            return ShortPromptBuilder(task)
456        case PromptGenerators.FEW_SHOT:
457            return FewShotPromptBuilder(task)
458        case PromptGenerators.MULTI_SHOT:
459            return MultiShotPromptBuilder(task)
460        case PromptGenerators.REPAIRS:
461            return RepairsPromptBuilder(task)
462        case PromptGenerators.SIMPLE_CHAIN_OF_THOUGHT:
463            return SimpleChainOfThoughtPromptBuilder(task)
464        case PromptGenerators.FEW_SHOT_CHAIN_OF_THOUGHT:
465            return FewShotChainOfThoughtPromptBuilder(task)
466        case PromptGenerators.MULTI_SHOT_CHAIN_OF_THOUGHT:
467            return MultiShotChainOfThoughtPromptBuilder(task)
468        case _:
469            # Type checking will find missing cases
470            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# 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# 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 ShortPromptBuilder(BasePromptBuilder):
109class ShortPromptBuilder(BasePromptBuilder):
110    """A prompt builder that includes a the base prompt but excludes the requirements."""
111
112    def build_base_prompt(self) -> str:
113        """Build a short prompt with just the base prompt, no requirements.
114
115        Returns:
116            str: The constructed prompt string.
117        """
118        return self.task.instruction

A prompt builder that includes a the base prompt but excludes the requirements.

def build_base_prompt(self) -> str:
112    def build_base_prompt(self) -> str:
113        """Build a short prompt with just the base prompt, no requirements.
114
115        Returns:
116            str: The constructed prompt string.
117        """
118        return self.task.instruction

Build a short prompt with just the base prompt, no requirements.

Returns: str: The constructed prompt string.

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

A prompt builder that includes multiple examples in the prompt.

@classmethod
def example_count(cls) -> int:
124    @classmethod
125    def example_count(cls) -> int:
126        """Get the maximum number of examples to include in the prompt.
127
128        Returns:
129            int: The maximum number of examples (default 25).
130        """
131        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:
133    def build_instruction_and_requirements(self) -> str:
134        """Build the instruction and requirements section of the prompt.
135
136        Returns:
137            str: The instruction and requirements sections.
138        """
139        base_prompt = f"# Instruction\n\n{self.task.instruction}\n\n"
140
141        if len(self.task.requirements) > 0:
142            base_prompt += "# Requirements\n\nYour response should respect the following requirements:\n"
143            for i, requirement in enumerate(self.task.requirements):
144                base_prompt += f"{i + 1}) {requirement.instruction}\n"
145            base_prompt += "\n"
146
147        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:
149    def build_base_prompt(self) -> str:
150        """Build a prompt with instruction, requirements, and multiple examples.
151
152        Returns:
153            str: The constructed prompt string with examples.
154        """
155        base_prompt = self.build_instruction_and_requirements()
156
157        valid_examples = self.collect_examples()
158
159        if len(valid_examples) == 0:
160            return base_prompt
161
162        base_prompt += "# Example Outputs\n\n"
163        for i, example in enumerate(valid_examples):
164            base_prompt += self.prompt_section_for_example(i, example)
165
166        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:
168    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
169        # Prefer repaired output if it exists, otherwise use the regular output
170        output = example.repaired_output or example.output
171        return f"## Example {index + 1}\n\nInput: {example.input}\nOutput: {output.output}\n\n"
def collect_examples(self) -> list[kiln_ai.datamodel.TaskRun]:
173    def collect_examples(self) -> list[TaskRun]:
174        valid_examples: list[TaskRun] = []
175        runs = self.task.runs(readonly=True)
176
177        # first pass, we look for repaired outputs. These are the best examples.
178        for run in runs:
179            if len(valid_examples) >= self.__class__.example_count():
180                break
181            if run.repaired_output is not None:
182                valid_examples.append(run)
183
184        # second pass, we look for high quality outputs (rating based)
185        # Minimum is "high_quality" (4 star in star rating scale), then sort by rating
186        # exclude repaired outputs as they were used above
187        runs_with_rating = [
188            run
189            for run in runs
190            if run.output.rating is not None
191            and run.output.rating.value is not None
192            and run.output.rating.is_high_quality()
193            and run.repaired_output is None
194        ]
195        runs_with_rating.sort(
196            key=lambda x: (x.output.rating and x.output.rating.value) or 0, reverse=True
197        )
198        for run in runs_with_rating:
199            if len(valid_examples) >= self.__class__.example_count():
200                break
201            valid_examples.append(run)
202        return valid_examples
class FewShotPromptBuilder(MultiShotPromptBuilder):
205class FewShotPromptBuilder(MultiShotPromptBuilder):
206    """A prompt builder that includes a small number of examples in the prompt."""
207
208    @classmethod
209    def example_count(cls) -> int:
210        """Get the maximum number of examples to include in the prompt.
211
212        Returns:
213            int: The maximum number of examples (4).
214        """
215        return 4

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

@classmethod
def example_count(cls) -> int:
208    @classmethod
209    def example_count(cls) -> int:
210        """Get the maximum number of examples to include in the prompt.
211
212        Returns:
213            int: The maximum number of examples (4).
214        """
215        return 4

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

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

class CustomExamplePromptBuilder(FewShotPromptBuilder):
218class CustomExamplePromptBuilder(FewShotPromptBuilder):
219    """A prompt builder that uses custom examples instead of collecting from the dataset."""
220
221    def __init__(self, task: Task, examples: list[PromptExample] | None = None):
222        super().__init__(task)
223        self._custom_examples = examples or []
224
225    def collect_examples(self) -> list[TaskRun]:
226        """Override to return an empty list - we handle examples separately."""
227        return []
228
229    def build_base_prompt(self) -> str:
230        """Build a prompt with instruction, requirements, and custom examples."""
231        base_prompt = self.build_instruction_and_requirements()
232
233        if self._custom_examples:
234            base_prompt += "# Example Outputs\n\n"
235            for i, example in enumerate(self._custom_examples):
236                base_prompt += f"## Example {i + 1}\n\nInput: {example.input}\nOutput: {example.output}\n\n"
237
238        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)
221    def __init__(self, task: Task, examples: list[PromptExample] | None = None):
222        super().__init__(task)
223        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]:
225    def collect_examples(self) -> list[TaskRun]:
226        """Override to return an empty list - we handle examples separately."""
227        return []

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

def build_base_prompt(self) -> str:
229    def build_base_prompt(self) -> str:
230        """Build a prompt with instruction, requirements, and custom examples."""
231        base_prompt = self.build_instruction_and_requirements()
232
233        if self._custom_examples:
234            base_prompt += "# Example Outputs\n\n"
235            for i, example in enumerate(self._custom_examples):
236                base_prompt += f"## Example {i + 1}\n\nInput: {example.input}\nOutput: {example.output}\n\n"
237
238        return base_prompt

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

class RepairsPromptBuilder(MultiShotPromptBuilder):
241class RepairsPromptBuilder(MultiShotPromptBuilder):
242    """A prompt builder that includes multiple examples in the prompt, including repaired instructions describing what was wrong, and how it was fixed."""
243
244    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
245        if (
246            not example.repaired_output
247            or not example.repair_instructions
248            or not example.repaired_output.output
249        ):
250            return super().prompt_section_for_example(index, example)
251
252        prompt_section = f"## Example {index + 1}\n\nInput: {example.input}\n\n"
253        prompt_section += (
254            f"Initial Output Which Was Insufficient: {example.output.output}\n\n"
255        )
256        prompt_section += f"Instructions On How to Improve the Initial Output: {example.repair_instructions}\n\n"
257        prompt_section += (
258            f"Repaired Output Which is Sufficient: {example.repaired_output.output}\n\n"
259        )
260        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:
244    def prompt_section_for_example(self, index: int, example: TaskRun) -> str:
245        if (
246            not example.repaired_output
247            or not example.repair_instructions
248            or not example.repaired_output.output
249        ):
250            return super().prompt_section_for_example(index, example)
251
252        prompt_section = f"## Example {index + 1}\n\nInput: {example.input}\n\n"
253        prompt_section += (
254            f"Initial Output Which Was Insufficient: {example.output.output}\n\n"
255        )
256        prompt_section += f"Instructions On How to Improve the Initial Output: {example.repair_instructions}\n\n"
257        prompt_section += (
258            f"Repaired Output Which is Sufficient: {example.repaired_output.output}\n\n"
259        )
260        return prompt_section
def chain_of_thought_prompt(task: kiln_ai.datamodel.Task) -> str:
263def chain_of_thought_prompt(task: Task) -> str:
264    """Standard implementation to build and return the chain of thought prompt string.
265
266    Returns:
267        str: The constructed chain of thought prompt.
268    """
269
270    cot_instruction = task.thinking_instruction
271    if not cot_instruction:
272        cot_instruction = "Think step by step, explaining your reasoning."
273
274    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):
277class SimpleChainOfThoughtPromptBuilder(SimplePromptBuilder):
278    """A prompt builder that includes a chain of thought prompt on top of the simple prompt."""
279
280    def chain_of_thought_prompt(self) -> str | None:
281        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:
280    def chain_of_thought_prompt(self) -> str | None:
281        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):
284class FewShotChainOfThoughtPromptBuilder(FewShotPromptBuilder):
285    """A prompt builder that includes a chain of thought prompt on top of the few shot prompt."""
286
287    def chain_of_thought_prompt(self) -> str | None:
288        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:
287    def chain_of_thought_prompt(self) -> str | None:
288        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):
291class MultiShotChainOfThoughtPromptBuilder(MultiShotPromptBuilder):
292    """A prompt builder that includes a chain of thought prompt on top of the multi shot prompt."""
293
294    def chain_of_thought_prompt(self) -> str | None:
295        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:
294    def chain_of_thought_prompt(self) -> str | None:
295        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):
298class SavedPromptBuilder(BasePromptBuilder):
299    """A prompt builder that looks up a static prompt."""
300
301    def __init__(self, task: Task, prompt_id: str):
302        super().__init__(task)
303        prompt_model = next(
304            (
305                prompt
306                for prompt in task.prompts(readonly=True)
307                if prompt.id == prompt_id
308            ),
309            None,
310        )
311        if not prompt_model:
312            raise ValueError(f"Prompt ID not found: {prompt_id}")
313        self.prompt_model = prompt_model
314
315    def prompt_id(self) -> str | None:
316        return self.prompt_model.id
317
318    def build_base_prompt(self) -> str:
319        """Returns a saved prompt.
320
321        Returns:
322            str: The prompt string.
323        """
324        return self.prompt_model.prompt
325
326    def chain_of_thought_prompt(self) -> str | None:
327        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)
301    def __init__(self, task: Task, prompt_id: str):
302        super().__init__(task)
303        prompt_model = next(
304            (
305                prompt
306                for prompt in task.prompts(readonly=True)
307                if prompt.id == prompt_id
308            ),
309            None,
310        )
311        if not prompt_model:
312            raise ValueError(f"Prompt ID not found: {prompt_id}")
313        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:
315    def prompt_id(self) -> str | None:
316        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:
318    def build_base_prompt(self) -> str:
319        """Returns a saved prompt.
320
321        Returns:
322            str: The prompt string.
323        """
324        return self.prompt_model.prompt

Returns a saved prompt.

Returns: str: The prompt string.

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

Build and return the complete prompt string.

Returns: str: The constructed prompt.

def chain_of_thought_prompt(self) -> str | None:
377    def chain_of_thought_prompt(self) -> str | None:
378        return self.cot_prompt

Build and return the chain of thought prompt string.

Returns: str: The constructed chain of thought prompt.

class FineTunePromptBuilder(BasePromptBuilder):
381class FineTunePromptBuilder(BasePromptBuilder):
382    """A prompt builder that looks up a fine-tune prompt."""
383
384    def __init__(self, task: Task, nested_fine_tune_id: str):
385        super().__init__(task)
386
387        # IDs are in project_id::task_id::fine_tune_id format
388        self.full_fine_tune_id = nested_fine_tune_id
389        parts = nested_fine_tune_id.split("::")
390        if len(parts) != 3:
391            raise ValueError(
392                f"Invalid fine-tune ID format. Expected 'project_id::task_id::fine_tune_id', got: {nested_fine_tune_id}"
393            )
394        fine_tune_id = parts[2]
395
396        fine_tune_model = next(
397            (
398                fine_tune
399                for fine_tune in task.finetunes(readonly=True)
400                if fine_tune.id == fine_tune_id
401            ),
402            None,
403        )
404        if not fine_tune_model:
405            raise ValueError(f"Fine-tune ID not found: {fine_tune_id}")
406        self.fine_tune_model = fine_tune_model
407
408    def prompt_id(self) -> str | None:
409        return self.full_fine_tune_id
410
411    def build_base_prompt(self) -> str:
412        return self.fine_tune_model.system_message
413
414    def chain_of_thought_prompt(self) -> str | None:
415        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)
384    def __init__(self, task: Task, nested_fine_tune_id: str):
385        super().__init__(task)
386
387        # IDs are in project_id::task_id::fine_tune_id format
388        self.full_fine_tune_id = nested_fine_tune_id
389        parts = nested_fine_tune_id.split("::")
390        if len(parts) != 3:
391            raise ValueError(
392                f"Invalid fine-tune ID format. Expected 'project_id::task_id::fine_tune_id', got: {nested_fine_tune_id}"
393            )
394        fine_tune_id = parts[2]
395
396        fine_tune_model = next(
397            (
398                fine_tune
399                for fine_tune in task.finetunes(readonly=True)
400                if fine_tune.id == fine_tune_id
401            ),
402            None,
403        )
404        if not fine_tune_model:
405            raise ValueError(f"Fine-tune ID not found: {fine_tune_id}")
406        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:
408    def prompt_id(self) -> str | None:
409        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:
411    def build_base_prompt(self) -> str:
412        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:
414    def chain_of_thought_prompt(self) -> str | None:
415        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:
419def prompt_builder_from_id(prompt_id: PromptId, task: Task) -> BasePromptBuilder:
420    """Convert a name used in the UI to the corresponding prompt builder class.
421
422    Args:
423        prompt_id (PromptId): The prompt ID.
424
425    Returns:
426        type[BasePromptBuilder]: The corresponding prompt builder class.
427
428    Raises:
429        ValueError: If the UI name is not recognized.
430    """
431
432    # Saved prompts are prefixed with "id::"
433    if prompt_id.startswith("id::"):
434        prompt_id = prompt_id[4:]
435        return SavedPromptBuilder(task, prompt_id)
436
437    # Task run config prompts are prefixed with "task_run_config::"
438    # task_run_config::[project_id]::[task_id]::[run_config_id]
439    if prompt_id.startswith("task_run_config::"):
440        return TaskRunConfigPromptBuilder(task, prompt_id)
441
442    # Fine-tune prompts are prefixed with "fine_tune_prompt::"
443    if prompt_id.startswith("fine_tune_prompt::"):
444        prompt_id = prompt_id[18:]
445        return FineTunePromptBuilder(task, prompt_id)
446
447    # Check if the prompt_id matches any enum value
448    if prompt_id not in [member.value for member in PromptGenerators]:
449        raise ValueError(f"Unknown prompt generator: {prompt_id}")
450    typed_prompt_generator = PromptGenerators(prompt_id)
451
452    match typed_prompt_generator:
453        case PromptGenerators.SIMPLE:
454            return SimplePromptBuilder(task)
455        case PromptGenerators.SHORT:
456            return ShortPromptBuilder(task)
457        case PromptGenerators.FEW_SHOT:
458            return FewShotPromptBuilder(task)
459        case PromptGenerators.MULTI_SHOT:
460            return MultiShotPromptBuilder(task)
461        case PromptGenerators.REPAIRS:
462            return RepairsPromptBuilder(task)
463        case PromptGenerators.SIMPLE_CHAIN_OF_THOUGHT:
464            return SimpleChainOfThoughtPromptBuilder(task)
465        case PromptGenerators.FEW_SHOT_CHAIN_OF_THOUGHT:
466            return FewShotChainOfThoughtPromptBuilder(task)
467        case PromptGenerators.MULTI_SHOT_CHAIN_OF_THOUGHT:
468            return MultiShotChainOfThoughtPromptBuilder(task)
469        case _:
470            # Type checking will find missing cases
471            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.