Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,108 @@
[![ITMO](https://raw.githubusercontent.com/aimclub/open-source-ops/43bb283758b43d75ec1df0a6bb4ae3eb20066323/badges/ITMO_badge.svg)](https://itmo.ru/)
[![Telegram Channel](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/+0kMcymeAQrczN2Fi)

---

<div align="center">

# 🧬 CoEvo — структурная ко-эволюция промптов

**Выпускная квалификационная работа**

Метод автоматической оптимизации промптов, реализованный внутри фреймворка CoolPrompt

</div>

> Этот раздел (ветка `role_based`) описывает мой дипломный метод **CoEvo** и его усиленную версию **CoEvo-M**: идею, результаты, запускаемое демо и список ключевых файлов. Общее описание фреймворка CoolPrompt — [ниже](#coolprompt-framework).

## В чём идея

Классические методы оптимизируют промпт как **единый кусок текста**. CoEvo представляет промпт как **три независимых поля** и эволюционирует каждое из них:

| Поле | Куда подставляется | За что отвечает |
|------|--------------------|-----------------|
| `role` (`system_behavior`) | **system**-сообщение | роль и поведение модели |
| `task` (`task_description`) | **user**-сообщение | что именно нужно сделать |
| `constraints` (`output_constraints`) | **user**-сообщение | формат и ограничения ответа |

1. **Декомпозиция.** На старте один вызов LLM-оптимизатора раскладывает исходный промпт на тройку `role / task / constraints` (структурированный JSON).
2. **Эволюция.** Популяция таких троек оптимизируется генетически: рулеточный отбор → рефлексия → кроссовер → мутация → softmax-выживание. Рефлексия объясняет модели, *чем* удачные варианты лучше неудачных.
3. **Отбор полей.** В конце ablation на валидации выбирает лучшую комбинацию полей (task / task+role / task+role+constraints).

**CoEvo-M** — усиленная версия: штраф за длину роли и за смысловое дублирование `role`/`task` (sentence-transformers), hall-of-fame лучших особей, «плохие примеры» в мутации и форсированный элитизм.

## Результаты

Сравнение с базовым ReflectivePrompt на 6 датасетах (BERTScore / метрика задачи):

<p align="center">
<img alt="CoEvo benchmark" src="notebooks/examples/benchmark_coevo.png" width="85%">
</p>

| Датасет | ReflectivePrompt | CoEvo | CoEvo-M |
|---------|:---:|:---:|:---:|
| TweetEval | 0.705 | **0.726** | 0.719 |
| SQuAD v2 | 0.878 | 0.907 | **0.929** |
| CommonGen | 0.808 | **0.809** | 0.807 |
| MEDIQA | 0.688 | 0.700 | **0.703** |
| GSM8K | 0.919 | **0.927** | 0.926 |
| XSum | 0.730 | **0.736** | 0.734 |
| **Среднее** | 0.788 | 0.801 | **0.803** |

## Запускаемое демо

📓 **[notebooks/examples/coevo_demo.ipynb](notebooks/examples/coevo_demo.ipynb)** — CoEvo end-to-end улучшает промпт для QA по SQuAD v2.

Идея сценария: сильный оптимизатор (`gpt-4o-mini`) переписывает промпт для дешёвой продакшн-модели (`gpt-4.1-nano`).

<p align="center">
<img alt="CoEvo demo result" src="notebooks/examples/coevo_demo_result.png" width="55%">
</p>

Из простого `"Answer the question based on the context."` метод за 5 эпох собирает структурированный промпт и поднимает BERTScore **0.823 → 0.896 (+0.073)**.

## Быстрый старт CoEvo

```python
from coolprompt.assistant import PromptTuner

tuner = PromptTuner() # OPENAI_API_KEY в окружении

tuner.run(
start_prompt="Answer the question based on the context.",
task="generation",
metric="bertscore",
dataset=dataset, # список входов
target=target, # список эталонных ответов
method="coevo", # CoEvo-M по умолчанию; use_enhancements=False базовый CoEvo
)

# CoEvo возвращает три поля:
print(tuner.final_role) # роль
print(tuner.final_prompt) # задача
print(tuner.final_constraints) # ограничения формата
```

## Мой вклад и ключевые файлы

Реализация методов CoEvo / CoEvo-M поверх фреймворка CoolPrompt:

- **[`coolprompt/optimizer/reflective_prompt/coevo_base_evoluter.py`](coolprompt/optimizer/reflective_prompt/coevo_base_evoluter.py)** — ядро метода: декомпозиция промпта на 3 поля, эволюционный цикл, рефлексия, отбор полей.
- **[`coolprompt/optimizer/reflective_prompt/coevo_evoluter.py`](coolprompt/optimizer/reflective_prompt/coevo_evoluter.py)** — операторы кроссовера и мутации на уровне полей.
- **[`coolprompt/optimizer/reflective_prompt/factorized_evoluter.py`](coolprompt/optimizer/reflective_prompt/factorized_evoluter.py)** — факторизованная эволюция по отдельным полям.
- **[`coolprompt/optimizer/reflective_prompt/run.py`](coolprompt/optimizer/reflective_prompt/run.py)** — `CoevoMethod`, интеграция в публичный API (`method="coevo"`).
- **[`coolprompt/utils/prompt_templates/`](coolprompt/utils/prompt_templates/)** — мета-промпты CoEvo: `reflective_templates_coevo_enhanced.py`, `reflective_templates_coevo_per_field.py`, `reflective_templates_coevolution.py`.


## Материалы
- 📓 Демо-ноутбук: [coevo_demo.ipynb](notebooks/examples/coevo_demo.ipynb).

---

<a name="coolprompt-framework"></a>

# CoolPrompt — фреймворк автопромптинга

CoolPrompt is a framework for automatic prompt creation and optimization.

### Join our [telegram](https://t.me/+0kMcymeAQrczN2Fi) channel to be in touch.
Expand Down
9 changes: 9 additions & 0 deletions coolprompt/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def __init__(
self.init_prompt = None
self.final_metric = None
self.final_prompt = None
self.final_role = None
self.final_constraints = None
self.assistant_feedback = None

self.synthetic_dataset = None
Expand Down Expand Up @@ -323,6 +325,11 @@ def run(
**kwargs,
)

self.final_role = getattr(method_impl, "last_role", "") or None
self.final_constraints = (
getattr(method_impl, "last_constraints", "") or None
)

logger.info("Running the prompt format checking...")
final_prompt = correct(
prompt=final_prompt,
Expand All @@ -346,6 +353,8 @@ def run(
dataset=dataset_split[1],
targets=dataset_split[3],
template=template,
system_role=self.final_role,
constraints=self.final_constraints,
)
logger.info(
f"Initial {base_metric} score: {self.init_metric}, "
Expand Down
37 changes: 31 additions & 6 deletions coolprompt/evaluator/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from langchain_core.language_models.base import BaseLanguageModel
from langchain_core.messages.ai import AIMessage
from langchain_core.messages import SystemMessage, HumanMessage
import numpy as np
from coolprompt.evaluator.metrics import BaseMetric
from coolprompt.utils.logging_config import logger
Expand Down Expand Up @@ -67,6 +68,8 @@ def evaluate(
targets: list[str | int],
template: Optional[str] = None,
failed_examples: Optional[int] = None,
system_role: Optional[str] = None,
constraints: Optional[str] = None,
*,
return_detailed: bool = False,
save_model_answers: bool = False,
Expand Down Expand Up @@ -112,7 +115,9 @@ def evaluate(
if self.task == Task.CLASSIFICATION:
self.metric.extract_labels(targets)
full_prompts = [
self._get_full_prompt(prompt, sample, template)
self._get_full_prompt(
prompt, sample, template, system_role, constraints
)
for sample in dataset
]

Expand Down Expand Up @@ -203,7 +208,9 @@ def _get_full_prompt(
prompt: str,
sample: str,
template: Optional[str] = None,
) -> str:
system_role: Optional[str] = None,
constraints: Optional[str] = None,
) -> str | list:
"""Inserts parts of the prompt into the task template.

Args:
Expand All @@ -212,25 +219,43 @@ def _get_full_prompt(
template (Optional[str]):
Prompt template for defined task type.
If None, uses default template.
system_role (Optional[str]): system behavior prepended as a
SystemMessage (CoEvo). Defaults to None.
constraints (Optional[str]): output format constraints appended
to the prompt (CoEvo). Defaults to None.

Raises:
ValueError: if type of task is not supported

Returns:
str: the full prompt to be passed to the model
str | list: the full prompt string, or a list of
SystemMessage + HumanMessage if system_role is set.
"""

if template is None:
template = self._get_default_template()

effective_prompt = prompt
if constraints:
effective_prompt = f"{prompt}\n\n{constraints}"

match self.task:
case Task.CLASSIFICATION:
labels = ", ".join(map(str, self.metric.label_to_id.keys()))
return template.format(
PROMPT=prompt, LABELS=labels, INPUT=sample
formatted = template.format(
PROMPT=effective_prompt, LABELS=labels, INPUT=sample
)
case Task.GENERATION:
return template.format(PROMPT=prompt, INPUT=sample)
formatted = template.format(
PROMPT=effective_prompt, INPUT=sample
)

if system_role:
return [
SystemMessage(content=system_role),
HumanMessage(content=formatted),
]
return formatted

def _get_default_template(self) -> str:
"""Returns the default template for the task type."""
Expand Down
4 changes: 4 additions & 0 deletions coolprompt/evaluator/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ def _compute_raw(
List[float]: List of float metrics (for each model answer).
"""

outputs = [
"none" if isinstance(o, str) and not o.strip() else o
for o in outputs
]
return [
self._postprocessing(
self._metric.compute(
Expand Down
22 changes: 19 additions & 3 deletions coolprompt/optimizer/reflective_prompt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
from coolprompt.optimizer.reflective_prompt.run import ReflectiveMethod, reflectiveprompt

__all__ = ["reflectiveprompt", "ReflectiveMethod"]
from coolprompt.optimizer.reflective_prompt.run import (
ReflectiveMethod,
reflectiveprompt,
coevo,
CoevoMethod,
)
from coolprompt.optimizer.reflective_prompt.factorized_evoluter import (
FactorizedEvoluter,
)
from coolprompt.optimizer.reflective_prompt.coevo_evoluter import CoevoEvoluter

__all__ = [
"reflectiveprompt",
"ReflectiveMethod",
"coevo",
"CoevoMethod",
"FactorizedEvoluter",
"CoevoEvoluter",
]
Loading