mirror of
https://github.com/wassname/Unsupervised-Elicitation.git
synced 2026-08-20 12:10:10 +08:00
initial commit
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
ruff.toml
|
||||
tensorboard/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
.idea
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.vite/
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
SECRETS
|
||||
data/
|
||||
.DS_Store
|
||||
pyrightconfig.json
|
||||
*.db
|
||||
|
||||
|
||||
prompt_history/*
|
||||
prompt_history
|
||||
wandb/
|
||||
.hydra/
|
||||
|
||||
# API keys
|
||||
.env
|
||||
*api_key*
|
||||
|
||||
|
||||
# results
|
||||
src/results
|
||||
results*
|
||||
logs
|
||||
@@ -0,0 +1,83 @@
|
||||
## Unsupervised Elicitation of Language Models
|
||||
|
||||
We introduce a new unsupervised algorithm for eliciting skills from pretrained language models. This algorithm is competitive with training on human labels on common misconceptions (TruthfulQA), math (GSM8k-verification), and helpfulness reward modeling (Alpaca). Without supervision, we train a helpful chat assistant from the Haiku 3.5 base model that outperforms a similarly trained human-supervised baseline.
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" src="figures/llama_performance.png">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" src="figures/claude_performance.png">
|
||||
</p>
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
### Environment
|
||||
|
||||
1. create conda environment: `conda env create -f env.yaml`
|
||||
|
||||
2. install package `pip install -e .`
|
||||
|
||||
### API for Pretrained Base Models
|
||||
|
||||
You should have access to an API for pretrained base models, which can return top-K (e.g. 20) logprobs.
|
||||
|
||||
Since most public api servers (e.g. openrouter) only support post-trained chat models, you probably need to deploy pretrained base models yourself. For example, we use vllm to deploy llama models in our experiments.
|
||||
|
||||
In particular, we highly recommend activating the `prefix caching` feature to accelerate the experiments, because our algorithm will create many API queries with similar prefixes.
|
||||
|
||||
|
||||
### Secrets
|
||||
|
||||
You should create a file called SECRETS at the root of the repository with the following contents:
|
||||
```
|
||||
LLAMA_API_BASE=<your_api_base_url>
|
||||
NYU_ORG=None
|
||||
ARG_ORG=None
|
||||
API_KEY=None
|
||||
```
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
### ICM
|
||||
<p align="center">
|
||||
<img width="100%" src="figures/algorithm.png">
|
||||
</p>
|
||||
|
||||
|
||||
The main script is located in `src/experiments/ICM.py`
|
||||
An example command for labeling truthfulQA data:
|
||||
```
|
||||
cd src/experiments
|
||||
python ICM.py --testbed truthfulQA --alpha 50
|
||||
```
|
||||
|
||||
Arguments:
|
||||
|
||||
- `--seed`: random seed
|
||||
- `--alpha`: the coefficient for mutual predictability in our scoring function
|
||||
- `--testbed`: name of the testbed, e.g., alpaca, truthfulqa, gsm8k
|
||||
- `--model`: name of the pretrained base model, e.g., meta-llama/Llama-3.1-70B
|
||||
- `--batch_size`: size of a minibatch when running ICM on large datasets that cannot be fit in to the context all at once[^1].
|
||||
[^1]: Since ICM relies on in-context learning, it might not be able to fix all datapoints in the context at once. In our experiments, we split the whole dataset into $N$ batches (e.g., each batch consists of 256 datapoints) based on the context limit and data length, and run ICM independently on each batch.
|
||||
- `--num_seed`: number of randomly labeled datapoints in the beginning.
|
||||
- `--K`: max iteration
|
||||
- `--consistency_fix_K`: max iteration for consistencyfix
|
||||
- `--decay`: decay rate for simulating annealing
|
||||
- `--initial_T`: initial temprature for simulated annealing
|
||||
- `--final_T`: final temperature for simulated annealing
|
||||
- `--scheduler`: decay scheduler for simulated annealing
|
||||
|
||||
### Iterative Fine-tuning
|
||||
|
||||
Instead of using the initial pretrained model ($M_0$) to label all $N$ batches, we do iterative fine-tuning:
|
||||
|
||||
- fine-tune the pretrained model on the first $j$ batches to obtain $M_j$
|
||||
|
||||
- use $M_j$ to label the $j+1$-th batch.
|
||||
|
||||
We use [axolotl](https://github.com/axolotl-ai-cloud/axolotl) for fine-tuning.
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from traceback import format_exc
|
||||
from typing import Optional, Union
|
||||
|
||||
import attrs
|
||||
from anthropic import AsyncAnthropic
|
||||
from anthropic.types import ContentBlock as AnthropicContentBlock
|
||||
from termcolor import cprint
|
||||
|
||||
from core.llm_api.base_llm import PRINT_COLORS, LLMResponse, ModelAPIProtocol
|
||||
from core.llm_api.openai_llm import OAIChatPrompt
|
||||
|
||||
ANTHROPIC_MODELS = {
|
||||
"claude-instant-1",
|
||||
"claude-2.0",
|
||||
"claude-v1.3",
|
||||
"claude-2.1",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-haiku-20241022"
|
||||
}
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ACCEPTED_ARGS = [
|
||||
"model",
|
||||
"messages",
|
||||
"max_tokens",
|
||||
"system",
|
||||
"max_tokens",
|
||||
"stop_sequences",
|
||||
"stream",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
]
|
||||
|
||||
|
||||
def extract_system_prompt(messages: list) -> str:
|
||||
sys_prompt = ""
|
||||
for message in messages:
|
||||
if message["role"] == "system":
|
||||
if sys_prompt:
|
||||
raise ValueError(
|
||||
"Multiple system messages found in the prompt. Only one is allowed."
|
||||
)
|
||||
sys_prompt = message["content"]
|
||||
return sys_prompt
|
||||
|
||||
|
||||
def transform_messages(messages: list) -> list:
|
||||
_messages = []
|
||||
for message in messages:
|
||||
role = message["role"]
|
||||
|
||||
if role == "system":
|
||||
continue
|
||||
content = message["content"]
|
||||
_messages.append({"role": role, "content": [{"type": "text", "text": content}]})
|
||||
return _messages
|
||||
|
||||
|
||||
def count_tokens(prompt: str) -> int:
|
||||
return len(prompt.split())
|
||||
|
||||
|
||||
def price_per_token(model_id: str) -> tuple[float, float]:
|
||||
"""
|
||||
Returns the (input token, output token) price for the given model id.
|
||||
"""
|
||||
return 0, 0
|
||||
|
||||
|
||||
@attrs.define()
|
||||
class AnthropicChatModel(ModelAPIProtocol):
|
||||
num_threads: int
|
||||
print_prompt_and_response: bool = False
|
||||
client: AsyncAnthropic = attrs.field(
|
||||
init=False, default=attrs.Factory(AsyncAnthropic)
|
||||
)
|
||||
available_requests: asyncio.BoundedSemaphore = attrs.field(init=False)
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
self.available_requests = asyncio.BoundedSemaphore(int(self.num_threads))
|
||||
|
||||
@staticmethod
|
||||
def _create_prompt_history_file(prompt):
|
||||
filename = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}_prompt.txt"
|
||||
with open(os.path.join("prompt_history", filename), "w") as f:
|
||||
json_str = json.dumps(prompt, indent=4)
|
||||
json_str = json_str.replace("\\n", "\n")
|
||||
f.write(json_str)
|
||||
|
||||
return filename
|
||||
|
||||
@staticmethod
|
||||
def _add_response_to_prompt_file(prompt_file, response):
|
||||
with open(os.path.join("prompt_history", prompt_file), "a") as f:
|
||||
f.write("\n\n======RESPONSE======\n\n")
|
||||
json_str = json.dumps(response.to_dict(), indent=4)
|
||||
json_str = json_str.replace("\\n", "\n")
|
||||
f.write(json_str)
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
model_ids: list[str],
|
||||
prompt: Union[str, OAIChatPrompt],
|
||||
print_prompt_and_response: bool,
|
||||
max_attempts: int,
|
||||
**kwargs,
|
||||
) -> list[LLMResponse]:
|
||||
start = time.time()
|
||||
assert (
|
||||
len(model_ids) == 1
|
||||
), "Anthropic implementation only supports one model at a time."
|
||||
model_id = model_ids[0]
|
||||
max_tokens = kwargs.pop("max_tokens", 2048)
|
||||
LOGGER.debug(f"Making {model_id} call")
|
||||
response: Optional[AnthropicContentBlock] = None
|
||||
duration = None
|
||||
|
||||
kwargs = {k: v for k, v in kwargs.items() if k in ACCEPTED_ARGS}
|
||||
system_prompt = extract_system_prompt(prompt)
|
||||
raw_prompt = prompt
|
||||
|
||||
if "tool" in prompt[0]:
|
||||
kwargs["tools"] = prompt[0]["tool"]
|
||||
kwargs["tool_choice"] = prompt[0]["tool_choice"]
|
||||
|
||||
prompt = transform_messages(prompt)
|
||||
# prompt_file = self._create_prompt_history_file([system_prompt] + prompt)
|
||||
for i in range(max_attempts):
|
||||
try:
|
||||
async with self.available_requests:
|
||||
api_start = time.time()
|
||||
response = await self.client.messages.create(
|
||||
messages=prompt,
|
||||
model=model_id,
|
||||
system=system_prompt,
|
||||
max_tokens=max_tokens,
|
||||
**kwargs,
|
||||
)
|
||||
api_duration = time.time() - api_start
|
||||
except Exception as e:
|
||||
error_info = f"Exception Type: {type(e).__name__}, Error Details: {str(e)}, Traceback: {format_exc()}"
|
||||
LOGGER.warn(
|
||||
f"Encountered API error: {error_info}.\nRetrying now. (Attempt {i})"
|
||||
)
|
||||
await asyncio.sleep(1.5**i)
|
||||
else:
|
||||
break
|
||||
|
||||
if response is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to get a response from the API after {max_attempts} attempts."
|
||||
)
|
||||
|
||||
num_context_tokens, num_completion_tokens = (
|
||||
response.usage.input_tokens,
|
||||
response.usage.output_tokens,
|
||||
)
|
||||
|
||||
context_token_cost, completion_token_cost = price_per_token(model_id)
|
||||
cost = (
|
||||
num_context_tokens * context_token_cost
|
||||
+ num_completion_tokens * completion_token_cost
|
||||
)
|
||||
duration = time.time() - start
|
||||
LOGGER.debug(f"Completed call to {model_id} in {duration}s")
|
||||
|
||||
if "tools" in kwargs:
|
||||
completion = response.content[0].input
|
||||
else:
|
||||
completion = response.content[0].text
|
||||
|
||||
llm_response = LLMResponse(
|
||||
model_id=model_id,
|
||||
completion=completion,
|
||||
stop_reason=response.stop_reason,
|
||||
duration=duration,
|
||||
api_duration=api_duration,
|
||||
cost=cost,
|
||||
)
|
||||
|
||||
if self.print_prompt_and_response or print_prompt_and_response:
|
||||
cprint("Prompt:", "white")
|
||||
cprint("System: " + system_prompt, PRINT_COLORS["system"])
|
||||
for message in prompt:
|
||||
role = message["role"]
|
||||
content = message["content"]
|
||||
tag = "Human: " if role == "user" else "Assistant: "
|
||||
cprint(tag, PRINT_COLORS[role], end="")
|
||||
cprint(content, PRINT_COLORS[role])
|
||||
|
||||
cprint(f"Response ({llm_response.model_id}):", "white")
|
||||
cprint(
|
||||
f"{llm_response.completion}", PRINT_COLORS["assistant"], attrs=["bold"]
|
||||
)
|
||||
print()
|
||||
|
||||
return [{"prompt": raw_prompt, "response": llm_response.to_dict()}]
|
||||
@@ -0,0 +1,137 @@
|
||||
import json
|
||||
import logging
|
||||
from enum import Enum, auto
|
||||
from typing import Dict, List, Optional, Protocol
|
||||
|
||||
import attrs
|
||||
import numpy as np
|
||||
from anthropic import AI_PROMPT, HUMAN_PROMPT
|
||||
from pydantic import BaseModel
|
||||
|
||||
PRINT_COLORS = {"user": "cyan", "system": "magenta", "assistant": "light_green"}
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PromptConfig(BaseModel):
|
||||
partials: Dict[str, str] = {}
|
||||
word_limit: Optional[int] = 100
|
||||
messages: List[Dict[str, str]] = []
|
||||
messages1: List[Dict[str, str]] = []
|
||||
messages2: List[Dict[str, str]] = []
|
||||
vars: Dict[str, str] = {}
|
||||
|
||||
|
||||
class LanguageModelConfig(BaseModel):
|
||||
model: str
|
||||
temperature: float = 0.2
|
||||
top_p: float = 1.0
|
||||
max_tokens: Optional[int] = None
|
||||
max_words: int = 10000
|
||||
min_words: int = 0
|
||||
num_candidates_per_completion: int = 1
|
||||
timeout: int = 120
|
||||
logit_bias: Optional[dict] = None
|
||||
|
||||
|
||||
class StopReason(Enum):
|
||||
MAX_TOKENS = auto()
|
||||
STOP_SEQUENCE = auto()
|
||||
TOOL_USE = auto()
|
||||
|
||||
@classmethod
|
||||
def factory(cls, stop_reason: str) -> "StopReason":
|
||||
"""
|
||||
Parses the openai and anthropic stop reasons into a StopReason enum.
|
||||
"""
|
||||
if stop_reason in ["max_tokens", "length"]:
|
||||
return cls.MAX_TOKENS
|
||||
elif stop_reason in ["stop_sequence", "stop", "end_turn", "eos"]:
|
||||
return cls.STOP_SEQUENCE
|
||||
elif stop_reason in ['tool_use', "tool_calls"]:
|
||||
return cls.TOOL_USE
|
||||
raise ValueError(f"Invalid stop reason: {stop_reason}")
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@attrs.frozen()
|
||||
class LLMResponse:
|
||||
model_id: str
|
||||
completion: str
|
||||
stop_reason: StopReason = attrs.field(converter=StopReason.factory)
|
||||
cost: float
|
||||
duration: Optional[float] = None
|
||||
api_duration: Optional[float] = None
|
||||
logprobs: Optional[list[dict[str, float]]] = None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"model_id": self.model_id,
|
||||
"completion": self.completion,
|
||||
"stop_reason": self.stop_reason.__repr__(), # Convert to some JSON-serializable format.
|
||||
"duration": self.duration,
|
||||
"api_duration": self.api_duration,
|
||||
"cost": self.cost,
|
||||
"logprobs": self.logprobs,
|
||||
}
|
||||
|
||||
|
||||
class ModelAPIProtocol(Protocol):
|
||||
async def __call__(
|
||||
self,
|
||||
model_ids: list[str],
|
||||
prompt,
|
||||
print_prompt_and_response: bool,
|
||||
max_attempts: int,
|
||||
**kwargs,
|
||||
) -> list[LLMResponse]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def messages_to_single_prompt(messages) -> str:
|
||||
if (
|
||||
len(messages) >= 2
|
||||
and messages[0]["role"] == "system"
|
||||
and messages[1]["role"] == "user"
|
||||
):
|
||||
combined_content = messages[0]["content"] + " " + messages[1]["content"]
|
||||
messages = [{"role": "user", "content": combined_content}] + messages[2:]
|
||||
prompt = ""
|
||||
for message in messages:
|
||||
role = message["role"]
|
||||
content = message["content"]
|
||||
tag = AI_PROMPT if role == "assistant" else HUMAN_PROMPT
|
||||
prompt += f"{tag} {content}"
|
||||
if tag != AI_PROMPT:
|
||||
prompt += f"{AI_PROMPT}"
|
||||
return prompt.strip()
|
||||
|
||||
|
||||
def convert_to_prob(log_prob: dict, tokens: list) -> tuple[float, float, float]:
|
||||
logit1 = log_prob.get(tokens[0], None)
|
||||
logit2 = log_prob.get(tokens[1], None)
|
||||
|
||||
if logit1 is None:
|
||||
rating = -100
|
||||
LOGGER.warning(
|
||||
f"Missing token0 {tokens[0]} in log_prob, setting rating to -100.0"
|
||||
)
|
||||
else:
|
||||
rating = logit1
|
||||
|
||||
if logit1 is None:
|
||||
logit1 = -100
|
||||
if logit2 is None:
|
||||
logit2 = -100
|
||||
|
||||
return rating, logit1, logit2
|
||||
|
||||
|
||||
def add_assistant_message(messages: list[dict], assistant_message: str):
|
||||
last_role = messages[-1]["role"]
|
||||
if last_role == "assistant":
|
||||
messages[-1]["content"] += assistant_message
|
||||
else:
|
||||
messages.append({"role": "assistant", "content": assistant_message})
|
||||
return messages
|
||||
@@ -0,0 +1,316 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Callable, Literal, Optional, Union
|
||||
|
||||
import attrs
|
||||
|
||||
from core.llm_api.anthropic_llm import ANTHROPIC_MODELS, AnthropicChatModel
|
||||
from core.llm_api.base_llm import LLMResponse, ModelAPIProtocol
|
||||
from core.llm_api.openai_llm import (
|
||||
BASE_MODELS,
|
||||
GPT_CHAT_MODELS,
|
||||
OAIBasePrompt,
|
||||
OAIChatPrompt,
|
||||
OpenAIBaseModel,
|
||||
OpenAIChatModel,
|
||||
)
|
||||
from core.utils import load_secrets
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attrs.define()
|
||||
class ModelAPI:
|
||||
anthropic_num_threads: int = 2 # current redwood limit is 5
|
||||
openai_fraction_rate_limit: float = attrs.field(
|
||||
default=0.99, validator=attrs.validators.lt(1)
|
||||
)
|
||||
organization: str = "NYU_ORG"
|
||||
print_prompt_and_response: bool = False
|
||||
|
||||
_openai_base: OpenAIBaseModel = attrs.field(init=False)
|
||||
_openai_base_arg: OpenAIBaseModel = attrs.field(init=False)
|
||||
_openai_chat: OpenAIChatModel = attrs.field(init=False)
|
||||
_anthropic_chat: AnthropicChatModel = attrs.field(init=False)
|
||||
|
||||
running_cost: float = attrs.field(init=False, default=0)
|
||||
model_timings: dict[str, list[float]] = attrs.field(init=False, default={})
|
||||
model_wait_times: dict[str, list[float]] = attrs.field(init=False, default={})
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
secrets = load_secrets()
|
||||
if self.organization is None:
|
||||
self.organization = "NYU_ORG"
|
||||
self._openai_base = OpenAIBaseModel(
|
||||
frac_rate_limit=self.openai_fraction_rate_limit,
|
||||
organization=secrets[self.organization],
|
||||
print_prompt_and_response=self.print_prompt_and_response,
|
||||
)
|
||||
self._openai_base_arg = OpenAIBaseModel(
|
||||
frac_rate_limit=self.openai_fraction_rate_limit,
|
||||
organization=secrets["ARG_ORG"],
|
||||
print_prompt_and_response=self.print_prompt_and_response,
|
||||
)
|
||||
self._openai_chat = OpenAIChatModel(
|
||||
frac_rate_limit=self.openai_fraction_rate_limit,
|
||||
organization=secrets[self.organization],
|
||||
print_prompt_and_response=self.print_prompt_and_response,
|
||||
)
|
||||
self._anthropic_chat = AnthropicChatModel(
|
||||
num_threads=self.anthropic_num_threads,
|
||||
print_prompt_and_response=self.print_prompt_and_response,
|
||||
)
|
||||
Path("./prompt_history").mkdir(exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _load_from_cache(save_file):
|
||||
if not os.path.exists(save_file):
|
||||
return None
|
||||
else:
|
||||
with open(save_file) as f:
|
||||
cache = json.load(f)
|
||||
return cache
|
||||
|
||||
async def call_single(
|
||||
self,
|
||||
model_ids: Union[str, list[str]],
|
||||
prompt: Union[list[dict[str, str]], str],
|
||||
max_tokens: int,
|
||||
print_prompt_and_response: bool = False,
|
||||
n: int = 1,
|
||||
max_attempts_per_api_call: int = 10,
|
||||
num_candidates_per_completion: int = 1,
|
||||
# is_valid: Callable[[str], bool] = lambda _: True,
|
||||
parse_fn=lambda _: True,
|
||||
insufficient_valids_behaviour: Literal[
|
||||
"error", "continue", "pad_invalids"
|
||||
] = "error",
|
||||
**kwargs,
|
||||
) -> str:
|
||||
assert n == 1, f"Expected a single response. {n} responses were requested."
|
||||
responses = await self(
|
||||
model_ids,
|
||||
prompt,
|
||||
max_tokens,
|
||||
print_prompt_and_response,
|
||||
n,
|
||||
max_attempts_per_api_call,
|
||||
num_candidates_per_completion,
|
||||
parse_fn,
|
||||
insufficient_valids_behaviour,
|
||||
**kwargs,
|
||||
)
|
||||
assert len(responses) == 1, "Expected a single response."
|
||||
return responses[0].completion
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
model_ids: Union[str, list[str]],
|
||||
prompt: Union[list[dict[str, str]], str],
|
||||
print_prompt_and_response: bool = False,
|
||||
n: int = 1,
|
||||
max_attempts_per_api_call: int = 50,
|
||||
num_candidates_per_completion: int = 1,
|
||||
parse_fn=None,
|
||||
use_cache: bool = True,
|
||||
file_sem: asyncio.Semaphore = None,
|
||||
insufficient_valids_behaviour: Literal[
|
||||
"error", "continue", "pad_invalids"
|
||||
] = "error",
|
||||
**kwargs,
|
||||
) -> list[LLMResponse]:
|
||||
"""
|
||||
Make maximally efficient API requests for the specified model(s) and prompt.
|
||||
|
||||
Args:
|
||||
model_ids: The model(s) to call. If multiple models are specified, the output will be sampled from the
|
||||
cheapest model that has capacity. All models must be from the same class (e.g. OpenAI Base,
|
||||
OpenAI Chat, or Anthropic Chat). Anthropic chat will error if multiple models are passed in.
|
||||
Passing in multiple models could speed up the response time if one of the models is overloaded.
|
||||
prompt: The prompt to send to the model(s). Type should match what's expected by the model(s).
|
||||
max_tokens: The maximum number of tokens to request from the API (argument added to
|
||||
standardize the Anthropic and OpenAI APIs, which have different names for this).
|
||||
print_prompt_and_response: Whether to print the prompt and response to stdout.
|
||||
n: The number of completions to request.
|
||||
max_attempts_per_api_call: Passed to the underlying API call. If the API call fails (e.g. because the
|
||||
API is overloaded), it will be retried this many times. If still fails, an exception will be raised.
|
||||
num_candidates_per_completion: How many candidate completions to generate for each desired completion. n*num_candidates_per_completion completions will be generated, then is_valid is applied as a filter, then the remaining completions are returned up to a maximum of n.
|
||||
parse_fn: post-processing on the generated response
|
||||
save_path: cache path
|
||||
use_cache: whether to load from the cache or overwrite it
|
||||
"""
|
||||
|
||||
assert (
|
||||
"max_tokens_to_sample" not in kwargs
|
||||
), "max_tokens_to_sample should be passed in as max_tokens."
|
||||
|
||||
if isinstance(model_ids, str):
|
||||
model_ids = [model_ids]
|
||||
# # trick to double rate limit for most recent model only
|
||||
|
||||
def model_id_to_class(model_id: str) -> ModelAPIProtocol:
|
||||
if model_id in ["gpt-4-base", "gpt-3.5-turbo-instruct"]:
|
||||
return (
|
||||
self._openai_base_arg
|
||||
) # NYU ARG is only org with access to this model
|
||||
elif model_id in BASE_MODELS:
|
||||
return self._openai_base
|
||||
elif model_id in GPT_CHAT_MODELS or "ft:gpt-3.5-turbo" in model_id:
|
||||
return self._openai_chat
|
||||
elif model_id in ANTHROPIC_MODELS:
|
||||
return self._anthropic_chat
|
||||
raise ValueError(f"Invalid model id: {model_id}")
|
||||
|
||||
model_classes = [model_id_to_class(model_id) for model_id in model_ids]
|
||||
# assert model_classes == self._openai_base
|
||||
# if model_classes == self._openai_base:
|
||||
# assert "gpt" not in model_ids[0]
|
||||
# kwargs['api_base'] = "https://5jfmglryfots6s-8000.proxy.runpod.net/v1"
|
||||
|
||||
if len(set(str(type(x)) for x in model_classes)) != 1:
|
||||
raise ValueError("All model ids must be of the same type.")
|
||||
|
||||
max_tokens = (
|
||||
kwargs.get("max_tokens") if kwargs.get("max_tokens") is not None else 2000
|
||||
)
|
||||
model_class = model_classes[0]
|
||||
if isinstance(model_class, AnthropicChatModel):
|
||||
kwargs["max_tokens_to_sample"] = max_tokens
|
||||
else:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
# Check if current prompt has already been saved in the save file
|
||||
# If so, directly return previous result
|
||||
responses = None
|
||||
if use_cache and kwargs.get("save_path") is not None:
|
||||
try:
|
||||
responses = self._load_from_cache(kwargs.get("save_path"))
|
||||
except:
|
||||
logging.error(f"invalid cache data: {kwargs.get('save_path')}")
|
||||
|
||||
# After loading cache, we do not directly return previous results,
|
||||
# but continue running it through parse_fn and re-save it.
|
||||
# This is because we may frequently update the parse_fn during development
|
||||
if responses is None:
|
||||
num_candidates = num_candidates_per_completion * n
|
||||
if isinstance(model_class, AnthropicChatModel):
|
||||
responses = list(
|
||||
chain.from_iterable(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
model_class(
|
||||
model_ids,
|
||||
prompt,
|
||||
print_prompt_and_response,
|
||||
max_attempts_per_api_call,
|
||||
**kwargs,
|
||||
)
|
||||
for _ in range(num_candidates)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
responses = await model_class(
|
||||
model_ids,
|
||||
prompt,
|
||||
print_prompt_and_response,
|
||||
max_attempts_per_api_call,
|
||||
n=num_candidates,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
modified_responses = []
|
||||
for response in responses:
|
||||
self.running_cost += response["response"]["cost"]
|
||||
if kwargs.get("metadata") is not None:
|
||||
response["metadata"] = kwargs.get("metadata")
|
||||
if parse_fn is not None:
|
||||
response = parse_fn(response)
|
||||
|
||||
self.model_timings.setdefault(response["response"]["model_id"], []).append(
|
||||
response["response"]["api_duration"]
|
||||
)
|
||||
self.model_wait_times.setdefault(
|
||||
response["response"]["model_id"], []
|
||||
).append(
|
||||
response["response"]["duration"] - response["response"]["api_duration"]
|
||||
)
|
||||
modified_responses.append(response)
|
||||
|
||||
if kwargs.get("save_path") is not None:
|
||||
if file_sem is not None:
|
||||
async with file_sem:
|
||||
with open(kwargs.get("save_path"), "w") as f:
|
||||
json.dump(modified_responses, f, indent=2)
|
||||
else:
|
||||
with open(kwargs.get("save_path"), "w") as f:
|
||||
json.dump(modified_responses, f, indent=2)
|
||||
return modified_responses[:n]
|
||||
|
||||
def reset_cost(self):
|
||||
self.running_cost = 0
|
||||
|
||||
|
||||
async def demo():
|
||||
model_api = ModelAPI(anthropic_num_threads=2, openai_fraction_rate_limit=0.99)
|
||||
anthropic_requests = [
|
||||
model_api(
|
||||
"claude-3-5-sonnet-20240620",
|
||||
[
|
||||
{"role": "system", "content": "You are Claude."},
|
||||
{"role": "user", "content": "who are you!"},
|
||||
],
|
||||
max_tokens=20,
|
||||
print_prompt_and_response=False,
|
||||
)
|
||||
]
|
||||
oai_chat_messages = [
|
||||
[
|
||||
{"role": "system", "content": "You are gpt-3.5-turbo."},
|
||||
{"role": "user", "content": "who are you!"},
|
||||
],
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are gpt-4",
|
||||
},
|
||||
{"role": "user", "content": "who are you!"},
|
||||
],
|
||||
]
|
||||
oai_chat_models = ["gpt-3.5-turbo-16k"]
|
||||
oai_chat_requests = [
|
||||
model_api(
|
||||
oai_chat_models,
|
||||
prompt=message,
|
||||
max_tokens=16_000,
|
||||
n=1,
|
||||
print_prompt_and_response=False,
|
||||
)
|
||||
for message in oai_chat_messages
|
||||
]
|
||||
answer = await asyncio.gather(*anthropic_requests, *oai_chat_requests)
|
||||
|
||||
for responses in answer:
|
||||
for i in responses:
|
||||
print(i.completion)
|
||||
print("=" * 100)
|
||||
|
||||
costs = defaultdict(int)
|
||||
for responses in answer:
|
||||
for response in responses:
|
||||
costs[response.model_id] += response.cost
|
||||
|
||||
print("-" * 80)
|
||||
print("Costs:")
|
||||
for model_id, cost in costs.items():
|
||||
print(f"{model_id}: ${cost}")
|
||||
return answer
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(demo())
|
||||
@@ -0,0 +1,631 @@
|
||||
# %%
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime
|
||||
from itertools import cycle
|
||||
from traceback import format_exc
|
||||
from typing import Optional, Union
|
||||
|
||||
import attrs
|
||||
import openai
|
||||
import requests
|
||||
import tiktoken
|
||||
from openai.openai_object import OpenAIObject as OpenAICompletion
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
from termcolor import cprint
|
||||
|
||||
from core.llm_api.base_llm import (
|
||||
PRINT_COLORS,
|
||||
LLMResponse,
|
||||
ModelAPIProtocol,
|
||||
messages_to_single_prompt,
|
||||
)
|
||||
|
||||
OAIChatPrompt = list[dict[str, str]]
|
||||
OAIBasePrompt = Union[str, list[str]]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def count_tokens(text: str) -> int:
|
||||
return len(tiktoken.get_encoding("cl100k_base").encode(text))
|
||||
|
||||
|
||||
def price_per_token(model_id: str) -> tuple[float, float]:
|
||||
"""
|
||||
Returns the (input token, output token) price for the given model id.
|
||||
"""
|
||||
if model_id == "gpt-4-1106-preview":
|
||||
prices = 0.01, 0.03
|
||||
elif model_id == "gpt-3.5-turbo-1106":
|
||||
prices = 0.001, 0.002
|
||||
elif model_id.startswith("gpt-4"):
|
||||
prices = 0.03, 0.06
|
||||
elif model_id.startswith("gpt-4-32k"):
|
||||
prices = 0.06, 0.12
|
||||
elif model_id.startswith("gpt-3.5-turbo-16k"):
|
||||
prices = 0.003, 0.004
|
||||
elif model_id.startswith("gpt-3.5-turbo"):
|
||||
prices = 0.0015, 0.002
|
||||
elif model_id == "davinci-002":
|
||||
prices = 0.002, 0.002
|
||||
elif model_id == "babbage-002":
|
||||
prices = 0.0004, 0.0004
|
||||
elif model_id == "text-davinci-003" or model_id == "text-davinci-002":
|
||||
prices = 0.02, 0.02
|
||||
elif "ft:gpt-3.5-turbo" in model_id:
|
||||
prices = 0.012, 0.016
|
||||
elif "llama" in model_id.lower() or "mixtral" in model_id.lower():
|
||||
prices = 0.0015, 0.002
|
||||
elif "o1" in model_id.lower():
|
||||
prices = 0.01, 0.03
|
||||
else:
|
||||
prices = 0, 0
|
||||
# raise ValueError(f"Invalid model id: {model_id}")
|
||||
|
||||
return tuple(price / 1000 for price in prices)
|
||||
|
||||
|
||||
@attrs.define()
|
||||
class Resource:
|
||||
"""
|
||||
A resource that is consumed over time and replenished at a constant rate.
|
||||
"""
|
||||
|
||||
refresh_rate: float = (
|
||||
attrs.field()
|
||||
) # How many units of the resource are replenished per minute
|
||||
value: float = attrs.field(init=False)
|
||||
total: float = 0
|
||||
throughput: float = 0
|
||||
last_update_time: float = attrs.field(init=False, factory=time.time)
|
||||
start_time: float = attrs.field(init=False, factory=time.time)
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
self.value = self.refresh_rate
|
||||
|
||||
def _replenish(self):
|
||||
"""
|
||||
Updates the value of the resource based on the time since the last update.
|
||||
"""
|
||||
curr_time = time.time()
|
||||
self.value = min(
|
||||
self.refresh_rate,
|
||||
self.value + (curr_time - self.last_update_time) * self.refresh_rate / 60,
|
||||
)
|
||||
self.last_update_time = curr_time
|
||||
self.throughput = self.total / (curr_time - self.start_time) * 60
|
||||
|
||||
def geq(self, amount: float) -> bool:
|
||||
self._replenish()
|
||||
return self.value >= amount
|
||||
|
||||
def consume(self, amount: float):
|
||||
"""
|
||||
Consumes the given amount of the resource.
|
||||
"""
|
||||
assert self.geq(
|
||||
amount
|
||||
), f"Resource does not have enough capacity to consume {amount} units"
|
||||
self.value -= amount
|
||||
self.total += amount
|
||||
|
||||
|
||||
@attrs.define
|
||||
class OpenAIModel(ModelAPIProtocol):
|
||||
frac_rate_limit: float
|
||||
organization: str
|
||||
print_prompt_and_response: bool = False
|
||||
model_ids: set[str] = attrs.field(init=False, default=attrs.Factory(set))
|
||||
|
||||
# rate limit
|
||||
token_capacity: dict[str, Resource] = attrs.field(
|
||||
init=False, default=attrs.Factory(dict)
|
||||
)
|
||||
request_capacity: dict[str, Resource] = attrs.field(
|
||||
init=False, default=attrs.Factory(dict)
|
||||
)
|
||||
lock_add: asyncio.Lock = attrs.field(
|
||||
init=False, default=attrs.Factory(asyncio.Lock)
|
||||
)
|
||||
lock_consume: asyncio.Lock = attrs.field(
|
||||
init=False, default=attrs.Factory(asyncio.Lock)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _assert_valid_id(model_id: str):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
async def _get_dummy_response_header(model_id: str):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def _count_prompt_token_capacity(prompt, **kwargs) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
async def _make_api_call(self, prompt, model_id, **params) -> list[LLMResponse]:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def _print_prompt_and_response(prompt, responses):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def _create_prompt_history_file(prompt):
|
||||
filename = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}_prompt.txt"
|
||||
with open(os.path.join("prompt_history", filename), "w") as f:
|
||||
json_str = json.dumps(prompt, indent=4)
|
||||
json_str = json_str.replace("\\n", "\n")
|
||||
f.write(json_str)
|
||||
|
||||
return filename
|
||||
|
||||
@staticmethod
|
||||
def _add_response_to_prompt_file(prompt_file, responses):
|
||||
with open(os.path.join("prompt_history", prompt_file), "a") as f:
|
||||
f.write("\n\n======RESPONSE======\n\n")
|
||||
json_str = json.dumps(
|
||||
[response.to_dict() for response in responses], indent=4
|
||||
)
|
||||
json_str = json_str.replace("\\n", "\n")
|
||||
f.write(json_str)
|
||||
|
||||
async def add_model_id(self, model_id: str):
|
||||
self._assert_valid_id(model_id)
|
||||
if model_id in self.model_ids:
|
||||
return
|
||||
|
||||
# make dummy request to get token and request capacity
|
||||
model_metadata = await self._get_dummy_response_header(model_id)
|
||||
token_capacity = int(model_metadata["x-ratelimit-limit-tokens"])
|
||||
request_capacity = int(model_metadata["x-ratelimit-limit-requests"])
|
||||
print(
|
||||
f"got capacities for model {model_id}: {token_capacity}, {request_capacity}"
|
||||
)
|
||||
tokens_consumed = token_capacity - int(
|
||||
model_metadata["x-ratelimit-remaining-tokens"]
|
||||
)
|
||||
requests_consumed = request_capacity - int(
|
||||
model_metadata["x-ratelimit-remaining-requests"]
|
||||
)
|
||||
print(
|
||||
f"consumed capacities for model {model_id}: {tokens_consumed}, {requests_consumed}"
|
||||
)
|
||||
token_cap = token_capacity * self.frac_rate_limit
|
||||
request_cap = request_capacity * self.frac_rate_limit
|
||||
if model_id in BASE_MODELS:
|
||||
token_cap *= (
|
||||
10000 # openai does not track token limit so we can increase it
|
||||
)
|
||||
|
||||
print(f"setting cap for model {model_id}: {token_cap}, {request_cap}")
|
||||
self.model_ids.add(model_id)
|
||||
token_capacity = Resource(token_cap)
|
||||
request_capacity = Resource(request_cap)
|
||||
token_capacity.consume(min(token_cap, tokens_consumed))
|
||||
request_capacity.consume(min(request_cap, requests_consumed))
|
||||
self.token_capacity[model_id] = token_capacity
|
||||
self.request_capacity[model_id] = request_capacity
|
||||
|
||||
async def __llama_call__(
|
||||
self,
|
||||
model_ids: list[str],
|
||||
prompt,
|
||||
print_prompt_and_response: bool,
|
||||
max_attempts: int,
|
||||
**kwargs,
|
||||
) -> list[LLMResponse]:
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in ("save_path", "metadata")
|
||||
}
|
||||
|
||||
start = time.time()
|
||||
|
||||
async def attempt_api_call():
|
||||
api_base_list = [os.environ['LLAMA_API_BASE']]
|
||||
|
||||
kwargs["api_base"] = random.choice(api_base_list)
|
||||
for model_id in cycle(model_ids):
|
||||
return await asyncio.wait_for(
|
||||
self._make_api_call(prompt, model_id, start, **kwargs),
|
||||
timeout=100, # cloudflare has a 100-second limit for a connection to remain open: https://docs.runpod.io/pods/configuration/expose-ports
|
||||
)
|
||||
|
||||
model_ids.sort(
|
||||
key=lambda model_id: price_per_token(model_id)[0]
|
||||
) # Default to cheapest model
|
||||
model_id = model_ids[0]
|
||||
prompt = self._process_prompt(prompt)
|
||||
# prompt_file = self._create_prompt_history_file(prompt)
|
||||
responses: Optional[list[LLMResponse]] = None
|
||||
for i in range(max_attempts):
|
||||
try:
|
||||
responses = await attempt_api_call()
|
||||
except Exception as e:
|
||||
error_info = f"Exception Type: {type(e).__name__}, Error Details: {str(e)}, Traceback: {format_exc()}"
|
||||
LOGGER.warn(
|
||||
f"Encountered API error: {error_info}.\nRetrying now. (Attempt {i})"
|
||||
)
|
||||
await asyncio.sleep(1.5**i)
|
||||
else:
|
||||
break
|
||||
|
||||
if responses is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to get a response from the API after {max_attempts} attempts."
|
||||
)
|
||||
|
||||
if self.print_prompt_and_response or print_prompt_and_response:
|
||||
self._print_prompt_and_response(prompt, responses)
|
||||
|
||||
end = time.time()
|
||||
LOGGER.debug(f"Completed call to {model_id} in {end - start}s.")
|
||||
return [
|
||||
{"prompt": prompt, "response": response.to_dict()} for response in responses
|
||||
]
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
model_ids: list[str],
|
||||
prompt,
|
||||
print_prompt_and_response: bool,
|
||||
max_attempts: int,
|
||||
**kwargs,
|
||||
) -> list[LLMResponse]:
|
||||
if "gpt" not in model_ids[0]:
|
||||
return await self.__llama_call__(
|
||||
model_ids, prompt, print_prompt_and_response, max_attempts, **kwargs
|
||||
)
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in ("save_path", "metadata")
|
||||
}
|
||||
start = time.time()
|
||||
|
||||
async def attempt_api_call():
|
||||
for model_id in cycle(model_ids):
|
||||
async with self.lock_consume:
|
||||
request_capacity, token_capacity = (
|
||||
self.request_capacity[model_id],
|
||||
self.token_capacity[model_id],
|
||||
)
|
||||
if request_capacity.geq(1) and token_capacity.geq(token_count):
|
||||
request_capacity.consume(1)
|
||||
token_capacity.consume(token_count)
|
||||
else:
|
||||
await asyncio.sleep(0.01)
|
||||
continue # Skip this iteration if the condition isn't met
|
||||
|
||||
# Make the API call outside the lock
|
||||
return await asyncio.wait_for(
|
||||
self._make_api_call(prompt, model_id, start, **kwargs), timeout=120
|
||||
)
|
||||
|
||||
model_ids.sort(
|
||||
key=lambda model_id: price_per_token(model_id)[0]
|
||||
) # Default to cheapest model
|
||||
async with self.lock_add:
|
||||
for model_id in model_ids:
|
||||
await self.add_model_id(model_id)
|
||||
if "tool" in prompt[0]:
|
||||
kwargs["tools"] = prompt[0]["tool"]
|
||||
if "response_format" in prompt[0]:
|
||||
kwargs['response_format'] = prompt[0]['response_format']
|
||||
prompt = self._process_prompt(prompt)
|
||||
|
||||
token_count = self._count_prompt_token_capacity(prompt, **kwargs)
|
||||
assert (
|
||||
max(self.token_capacity[model_id].refresh_rate for model_id in model_ids)
|
||||
>= token_count
|
||||
), "Prompt is too long for any model to handle."
|
||||
# prompt_file = self._create_prompt_history_file(prompt)
|
||||
responses: Optional[list[LLMResponse]] = None
|
||||
for i in range(max_attempts):
|
||||
try:
|
||||
responses = await attempt_api_call()
|
||||
except Exception as e:
|
||||
error_info = f"Exception Type: {type(e).__name__}, Error Details: {str(e)}, Traceback: {format_exc()}"
|
||||
LOGGER.warn(
|
||||
f"Encountered API error: {error_info}.\nRetrying now. (Attempt {i})"
|
||||
)
|
||||
await asyncio.sleep(1.5**i)
|
||||
else:
|
||||
break
|
||||
|
||||
if responses is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to get a response from the API after {max_attempts} attempts."
|
||||
)
|
||||
|
||||
if self.print_prompt_and_response or print_prompt_and_response:
|
||||
self._print_prompt_and_response(prompt, responses)
|
||||
|
||||
end = time.time()
|
||||
LOGGER.debug(f"Completed call to {model_id} in {end - start}s.")
|
||||
return [
|
||||
{"prompt": prompt, "response": response.to_dict()} for response in responses
|
||||
]
|
||||
|
||||
|
||||
_GPT_4_MODELS = [
|
||||
"gpt-4o",
|
||||
"gpt-4",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-4-32k",
|
||||
"gpt-4-32k-0314",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"gpt-4o-2024-11-20",
|
||||
"o1-preview-2024-09-12",
|
||||
"o1-mini-2024-09-12",
|
||||
"deepseek/deepseek-chat",
|
||||
"meta-llama/llama-3.2-3b-instruct",
|
||||
"meta-llama/llama-3.2-1b-instruct",
|
||||
"meta-llama/llama-3.3-70b-instruct",
|
||||
"mistralai/mistral-7b-instruct",
|
||||
"meta-llama/llama-3-8b-instruct",
|
||||
"allenai/olmo-7b-instruct",
|
||||
"01-ai/yi-large",
|
||||
"meta-llama/llama-2-70b-chat",
|
||||
"meta-llama/llama-3.1-8b-instruct",
|
||||
"meta-llama/llama-3.1-70b-instruct",
|
||||
"meta-llama/llama-3.1-405b-instruct",
|
||||
"qwen/qwen-2.5-7b-instruct",
|
||||
"openai/gpt-4o",
|
||||
"openchat/openchat-7b",
|
||||
"ai21/jamba-instruct",
|
||||
"neversleep/llama-3.1-lumimaid-8b",
|
||||
"mistralai/mixtral-8x7b-instruct:nitro",
|
||||
"deepseek/deepseek-r1",
|
||||
"deepseek/deepseek-r1-distill-llama-70b",
|
||||
"minimax/minimax-01",
|
||||
"microsoft/phi-4",
|
||||
"qwen/qvq-72b-preview",
|
||||
]
|
||||
_GPT_TURBO_MODELS = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-3.5-turbo-1106",
|
||||
"gpt-3.5-turbo-0125",
|
||||
]
|
||||
GPT_CHAT_MODELS = set(_GPT_4_MODELS + _GPT_TURBO_MODELS)
|
||||
|
||||
|
||||
class OpenAIChatModel(OpenAIModel):
|
||||
def _process_prompt(self, prompt: OAIChatPrompt) -> OAIChatPrompt:
|
||||
return prompt
|
||||
|
||||
def _assert_valid_id(self, model_id: str):
|
||||
if "ft:" in model_id:
|
||||
model_id = model_id.split(":")[1]
|
||||
assert model_id in GPT_CHAT_MODELS, f"Invalid model id: {model_id}"
|
||||
|
||||
@retry(stop=stop_after_attempt(8), wait=wait_fixed(2))
|
||||
async def _get_dummy_response_header(self, model_id: str):
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {openai.api_key}",
|
||||
"OpenAI-Organization": self.organization,
|
||||
}
|
||||
data = {
|
||||
"model": model_id,
|
||||
"messages": [{"role": "user", "content": "Say 1"}],
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
if "x-ratelimit-limit-tokens" not in response.headers:
|
||||
raise RuntimeError("Failed to get dummy response header")
|
||||
return response.headers
|
||||
|
||||
@staticmethod
|
||||
def _count_prompt_token_capacity(prompt: OAIChatPrompt, **kwargs) -> int:
|
||||
# The magic formula is: .25 * (total number of characters) + (number of messages) + (max_tokens, or 15 if not specified)
|
||||
BUFFER = 5 # A bit of buffer for some error margin
|
||||
MIN_NUM_TOKENS = 20
|
||||
|
||||
num_tokens = 0
|
||||
for message in prompt:
|
||||
num_tokens += 1
|
||||
num_tokens += len(message["content"]) / 4
|
||||
|
||||
return max(
|
||||
MIN_NUM_TOKENS,
|
||||
int(num_tokens + BUFFER)
|
||||
+ kwargs.get("n", 1) * kwargs.get("max_tokens", 15),
|
||||
)
|
||||
|
||||
def convert_top_logprobs(self, data):
|
||||
# Initialize the new structure with only top_logprobs
|
||||
top_logprobs = []
|
||||
|
||||
for item in data["content"]:
|
||||
# Prepare a dictionary for top_logprobs
|
||||
top_logprob_dict = {}
|
||||
for top_logprob in item["top_logprobs"]:
|
||||
top_logprob_dict[top_logprob["token"]] = top_logprob["logprob"]
|
||||
|
||||
top_logprobs.append(top_logprob_dict)
|
||||
|
||||
return top_logprobs
|
||||
|
||||
async def _make_api_call(
|
||||
self, prompt: OAIChatPrompt, model_id, start_time, **params
|
||||
) -> list[LLMResponse]:
|
||||
LOGGER.debug(f"Making {model_id} call with {self.organization}")
|
||||
|
||||
if params.get("logprobs", None):
|
||||
params["top_logprobs"] = params["logprobs"]
|
||||
params["logprobs"] = True
|
||||
|
||||
api_start = time.time()
|
||||
api_response: OpenAICompletion = await openai.ChatCompletion.acreate(messages=prompt, model=model_id, organization=self.organization, **params) # type: ignore
|
||||
api_duration = time.time() - api_start
|
||||
duration = time.time() - start_time
|
||||
context_token_cost, completion_token_cost = price_per_token(model_id)
|
||||
context_cost = api_response.usage.prompt_tokens * context_token_cost
|
||||
completion_cost = api_response.usage.completion_tokens * completion_token_cost
|
||||
return [
|
||||
LLMResponse(
|
||||
model_id=model_id,
|
||||
completion=choice.message.content
|
||||
if "tools" not in params
|
||||
else choice.message.tool_calls[0]["function"]["arguments"],
|
||||
stop_reason=choice.finish_reason,
|
||||
api_duration=api_duration,
|
||||
duration=duration,
|
||||
cost=context_cost + completion_cost,
|
||||
logprobs=self.convert_top_logprobs(choice.logprobs)
|
||||
if choice.logprobs is not None
|
||||
else None,
|
||||
)
|
||||
for choice in api_response.choices
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _print_prompt_and_response(
|
||||
prompts: OAIChatPrompt, responses: list[LLMResponse]
|
||||
):
|
||||
for prompt in prompts:
|
||||
role, text = prompt["role"], prompt["content"]
|
||||
cprint(f"=={role.upper()}:", "white")
|
||||
cprint(text, PRINT_COLORS[role])
|
||||
for i, response in enumerate(responses):
|
||||
if len(responses) > 1:
|
||||
cprint(f"==RESPONSE {i + 1} ({response.model_id}):", "white")
|
||||
cprint(response.completion, PRINT_COLORS["assistant"], attrs=["bold"])
|
||||
print()
|
||||
|
||||
|
||||
BASE_MODELS = {
|
||||
"meta-llama/Llama-3.1-8B",
|
||||
"meta-llama/Llama-3.1-70B",
|
||||
}
|
||||
|
||||
|
||||
class OpenAIBaseModel(OpenAIModel):
|
||||
def _process_prompt(
|
||||
self, prompt: Union[OAIBasePrompt, OAIChatPrompt]
|
||||
) -> OAIBasePrompt:
|
||||
if isinstance(prompt, list) and isinstance(prompt[0], dict):
|
||||
return messages_to_single_prompt(prompt)
|
||||
return prompt
|
||||
|
||||
def _assert_valid_id(self, model_id: str):
|
||||
assert model_id in BASE_MODELS, f"Invalid model id: {model_id}"
|
||||
|
||||
@retry(stop=stop_after_attempt(8), wait=wait_fixed(2))
|
||||
async def _get_dummy_response_header(self, model_id: str):
|
||||
url = "https://api.openai.com/v1/completions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {openai.api_key}",
|
||||
"OpenAI-Organization": self.organization,
|
||||
}
|
||||
data = {"model": model_id, "prompt": "a", "max_tokens": 1}
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
if "gpt" in model_id and "x-ratelimit-limit-tokens" not in response.headers:
|
||||
raise RuntimeError("Failed to get dummy response header")
|
||||
return response.headers
|
||||
|
||||
@staticmethod
|
||||
def _count_prompt_token_capacity(prompt: OAIBasePrompt, **kwargs) -> int:
|
||||
max_tokens = kwargs.get("max_tokens", 15)
|
||||
n = kwargs.get("n", 1)
|
||||
completion_tokens = n * max_tokens
|
||||
|
||||
tokenizer = tiktoken.get_encoding("cl100k_base")
|
||||
if isinstance(prompt, str):
|
||||
prompt_tokens = len(tokenizer.encode(prompt))
|
||||
return prompt_tokens + completion_tokens
|
||||
else:
|
||||
prompt_tokens = sum(len(tokenizer.encode(p)) for p in prompt)
|
||||
return prompt_tokens + completion_tokens
|
||||
|
||||
async def _make_api_call(
|
||||
self, prompt: OAIBasePrompt, model_id, start_time, **params
|
||||
) -> list[LLMResponse]:
|
||||
LOGGER.debug(f"Making {model_id} call with {self.organization}")
|
||||
api_start = time.time()
|
||||
api_response: OpenAICompletion = await openai.Completion.acreate(prompt=prompt, model=model_id, organization=self.organization, **params) # type: ignore
|
||||
api_duration = time.time() - api_start
|
||||
duration = time.time() - start_time
|
||||
if "gpt" not in model_id:
|
||||
return [
|
||||
LLMResponse(
|
||||
model_id=model_id,
|
||||
completion=choice.text,
|
||||
stop_reason=choice.finish_reason,
|
||||
api_duration=api_duration,
|
||||
duration=duration,
|
||||
cost=0,
|
||||
logprobs=choice.logprobs.top_logprobs
|
||||
if choice.logprobs is not None
|
||||
else None,
|
||||
)
|
||||
for choice in api_response.choices
|
||||
]
|
||||
else:
|
||||
context_token_cost, completion_token_cost = price_per_token(model_id)
|
||||
context_cost = api_response.usage.prompt_tokens * context_token_cost
|
||||
return [
|
||||
LLMResponse(
|
||||
model_id=model_id,
|
||||
completion=choice.text,
|
||||
stop_reason=choice.finish_reason,
|
||||
api_duration=api_duration,
|
||||
duration=duration,
|
||||
cost=context_cost / len(api_response.choices)
|
||||
+ count_tokens(choice.message.content) * completion_token_cost,
|
||||
logprobs=choice.logprobs.top_logprobs
|
||||
if choice.logprobs is not None
|
||||
else None,
|
||||
)
|
||||
for choice in api_response.choices
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _print_prompt_and_response(prompt: OAIBasePrompt, responses: list[LLMResponse]):
|
||||
prompt_list = prompt if isinstance(prompt, list) else [prompt]
|
||||
responses_per_prompt = len(responses) // len(prompt_list)
|
||||
responses_list = [
|
||||
responses[i : i + responses_per_prompt]
|
||||
for i in range(0, len(responses), responses_per_prompt)
|
||||
]
|
||||
for i, (prompt, response_list) in enumerate(zip(prompt_list, responses_list)):
|
||||
if len(prompt_list) > 1:
|
||||
cprint(f"==PROMPT {i + 1}", "white")
|
||||
if len(response_list) == 1:
|
||||
cprint(f"=={response_list[0].model_id}", "white")
|
||||
cprint(prompt, PRINT_COLORS["user"], end="")
|
||||
cprint(
|
||||
response_list[0].completion,
|
||||
PRINT_COLORS["assistant"],
|
||||
attrs=["bold"],
|
||||
)
|
||||
else:
|
||||
cprint(prompt, PRINT_COLORS["user"])
|
||||
for j, response in enumerate(response_list):
|
||||
cprint(f"==RESPONSE {j + 1} ({response.model_id}):", "white")
|
||||
cprint(
|
||||
response.completion, PRINT_COLORS["assistant"], attrs=["bold"]
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
# %%
|
||||
@@ -0,0 +1,73 @@
|
||||
import concurrent.futures
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
from core.utils import setup_environment
|
||||
|
||||
|
||||
def can_claude_api_take_n_more_concurrents(n: int) -> bool:
|
||||
def ping_claude__is_ratelimited() -> Optional[bool]:
|
||||
data = {
|
||||
"model": "claude-2.0",
|
||||
"prompt": "\n\nHuman: Count to 50.\n\nAssistant:",
|
||||
"max_tokens_to_sample": 1000,
|
||||
}
|
||||
headers = {
|
||||
"content-type": "application/json",
|
||||
"accept": "application/json",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"x-api-key": f"{os.getenv('ANTHROPIC_API_KEY')}",
|
||||
}
|
||||
response = requests.post(
|
||||
"https://api.anthropic.com/v1/complete",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return False
|
||||
elif response.status_code == 429:
|
||||
return True
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
# launch n threads, each of which tries to ping claude
|
||||
print(f"Checking if claude can currently take {n} more concurrent requests...")
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=n) as executor:
|
||||
futures = []
|
||||
for _ in range(n):
|
||||
futures.append(executor.submit(ping_claude__is_ratelimited))
|
||||
results = [f.result() for f in futures]
|
||||
|
||||
result = not any(results) # if any result is true, is rate limited
|
||||
print(
|
||||
f"Claude currently {'can' if result else 'cannot'} take {n} more concurrent requests"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def binary_search_for_max_concurrent_claude_requests() -> int:
|
||||
min_max_new_concurrent_requests = 1
|
||||
max_max_new_concurrent_requests = 100
|
||||
while (
|
||||
min_max_new_concurrent_requests + 5 < max_max_new_concurrent_requests
|
||||
): # plus five because we don't need it accurate
|
||||
mid_test_number = (
|
||||
min_max_new_concurrent_requests + max_max_new_concurrent_requests
|
||||
) // 2
|
||||
if can_claude_api_take_n_more_concurrents(mid_test_number):
|
||||
min_max_new_concurrent_requests = mid_test_number
|
||||
else:
|
||||
max_max_new_concurrent_requests = mid_test_number
|
||||
print(
|
||||
f"\n\nFinal result: Claude can currently take roughly {min_max_new_concurrent_requests} more concurrent requests.\n"
|
||||
)
|
||||
return min_max_new_concurrent_requests
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_environment()
|
||||
binary_search_for_max_concurrent_claude_requests()
|
||||
@@ -0,0 +1,91 @@
|
||||
import logging
|
||||
|
||||
import openai
|
||||
import requests
|
||||
|
||||
from core.utils import setup_environment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_org_ids = {
|
||||
"NYU": "org-rRALD2hkdlmLWNVCKk9PG5Xq",
|
||||
"FAR": "org-AFgHGbU3MeFr5M5QFwrBET31",
|
||||
"ARG": "org-4L2GWAH28buzKOIhEAb3L5aq",
|
||||
}
|
||||
|
||||
|
||||
def extract_usage(response):
|
||||
requests_left = float(response.headers["x-ratelimit-remaining-requests"])
|
||||
requests_limit = float(response.headers["x-ratelimit-limit-requests"])
|
||||
request_usage = 1 - (requests_left / requests_limit)
|
||||
tokens_left = float(response.headers["x-ratelimit-remaining-tokens"])
|
||||
tokens_limit = float(response.headers["x-ratelimit-limit-tokens"])
|
||||
token_usage = 1 - (tokens_left / tokens_limit)
|
||||
overall_usage = max(request_usage, token_usage)
|
||||
return overall_usage
|
||||
|
||||
|
||||
def get_ratelimit_usage(data, org_id, endpoint):
|
||||
try:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {openai.api_key}",
|
||||
"OpenAI-Organization": org_id,
|
||||
}
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
return extract_usage(response)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching ratelimit usage: {e}")
|
||||
return -1
|
||||
|
||||
|
||||
def fetch_ratelimit_usage(org_id, model_name) -> float:
|
||||
data = {
|
||||
"model": model_name,
|
||||
"messages": [{"role": "user", "content": "Say 1"}],
|
||||
}
|
||||
return get_ratelimit_usage(
|
||||
data, org_id, "https://api.openai.com/v1/chat/completions"
|
||||
)
|
||||
|
||||
|
||||
def fetch_ratelimit_usage_base(org_id, model_name) -> float:
|
||||
data = {"model": model_name, "prompt": "a", "max_tokens": 1}
|
||||
return get_ratelimit_usage(data, org_id, "https://api.openai.com/v1/completions")
|
||||
|
||||
|
||||
def get_current_openai_model_usage() -> None:
|
||||
models_to_check = [
|
||||
"gpt-3.5-turbo-instruct",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-base",
|
||||
]
|
||||
org_names = ["NYU", "FAR", "ARG"]
|
||||
result_str = (
|
||||
"\nModel usage: 1 is hitting rate limits, 0 is not in use. -1 is error.\n"
|
||||
)
|
||||
for org in org_names:
|
||||
result_str += f"\n{org}:\n"
|
||||
for model_name in models_to_check:
|
||||
if model_name == "gpt-4-base" or model_name == "gpt-3.5-turbo-instruct":
|
||||
if org == "ARG":
|
||||
usage = fetch_ratelimit_usage_base(_org_ids[org], model_name)
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
usage = fetch_ratelimit_usage(_org_ids[org], model_name)
|
||||
result_str += f"\t{model_name}:\t{usage:.2f}\n"
|
||||
result_str += "\n"
|
||||
print(result_str)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_environment()
|
||||
get_current_openai_model_usage()
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from functools import lru_cache, wraps
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import openai
|
||||
import pandas as pd
|
||||
import replicate
|
||||
import typer
|
||||
import yaml
|
||||
from tenacity import retry, retry_if_result, stop_after_attempt
|
||||
|
||||
typer.main.get_command_name = lambda name: name
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
SEPARATOR = "---------------------------------------------\n\n"
|
||||
SEPARATOR_CONVERSATIONAL_TURNS = "=============================================\n\n"
|
||||
PROMPT_HISTORY = "prompt_history"
|
||||
SECRETS_FILE_PATH = Path(__file__).parent.parent / "SECRETS"
|
||||
|
||||
LOGGING_LEVELS = {
|
||||
"critical": logging.CRITICAL,
|
||||
"error": logging.ERROR,
|
||||
"warning": logging.WARNING,
|
||||
"info": logging.INFO,
|
||||
"debug": logging.DEBUG,
|
||||
}
|
||||
|
||||
|
||||
def setup_environment(
|
||||
anthropic_tag: str = "ANTHROPIC_API_KEY",
|
||||
logger_level: str = "info",
|
||||
openai_tag: str = "API_KEY",
|
||||
mistral_tag: str = "MISTRAL_API_KEY",
|
||||
replicate_tag: str = "REPLICATE_API_KEY",
|
||||
organization: str = None,
|
||||
):
|
||||
setup_logging(logger_level)
|
||||
load_secrets(
|
||||
SECRETS_FILE_PATH,
|
||||
anthropic_tag,
|
||||
logger_level,
|
||||
openai_tag,
|
||||
mistral_tag,
|
||||
replicate_tag,
|
||||
organization,
|
||||
)
|
||||
|
||||
|
||||
def setup_logging(level_str):
|
||||
level = LOGGING_LEVELS.get(
|
||||
level_str.lower(), logging.INFO
|
||||
) # default to INFO if level_str is not found
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="%(asctime)s [%(levelname)s] (%(name)s) %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
|
||||
# Disable logging from noisy libraries
|
||||
logging.getLogger("openai").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("httpx").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("matplotlib").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("anthropic").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("httpcore").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
||||
LOGGER.info(f"Logging level set to {level_str}")
|
||||
|
||||
|
||||
def load_secrets(
|
||||
file_path=SECRETS_FILE_PATH,
|
||||
anthropic_tag: str = "ANTHROPIC_API_KEY",
|
||||
logger_level: str = "info",
|
||||
openai_tag: str = "API_KEY",
|
||||
mistral_tag: str = "MISTRAL_API_KEY",
|
||||
replicate_tag: str = "REPLICATE_API_KEY",
|
||||
organization: str = None,
|
||||
):
|
||||
secrets = {}
|
||||
with open(file_path) as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split("=", 1)
|
||||
secrets[key] = value
|
||||
|
||||
openai.api_key = secrets[openai_tag]
|
||||
os.environ['LLAMA_API_BASE'] = secrets['LLAMA_API_BASE']
|
||||
# replicate.api_token = secrets[replicate_tag]
|
||||
# os.environ["ANTHROPIC_API_KEY"] = secrets[anthropic_tag]
|
||||
# os.environ["MISTRAL_API_KEY"] = secrets[mistral_tag]
|
||||
# os.environ["REPLICATE_API_KEY"] = secrets[replicate_tag]
|
||||
|
||||
if organization is not None:
|
||||
openai.organization = secrets[organization]
|
||||
if secrets.get("API_BASE") is not None:
|
||||
openai.api_base = secrets['API_BASE']
|
||||
return secrets
|
||||
|
||||
|
||||
def load_yaml(file_path):
|
||||
with open(file_path) as f:
|
||||
content = yaml.safe_load(f)
|
||||
return content
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def load_yaml_cached(file_path):
|
||||
with open(file_path) as f:
|
||||
content = yaml.safe_load(f)
|
||||
return content
|
||||
|
||||
|
||||
def save_yaml(file_path, data):
|
||||
with open(file_path, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
|
||||
def load_jsonl(file_path):
|
||||
data = []
|
||||
with open(file_path, "r") as f:
|
||||
for line in f:
|
||||
json_obj = json.loads(line)
|
||||
data.append(json_obj)
|
||||
return data
|
||||
|
||||
|
||||
def save_jsonl(file_path, data):
|
||||
with open(file_path, "w") as f:
|
||||
for line in data:
|
||||
json.dump(line, f)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def delete_old_prompt_files(
|
||||
path: str = PROMPT_HISTORY, max_age_minutes: int = 60, keep_recent: int = 50
|
||||
):
|
||||
"""
|
||||
Delete all files in the folder that:
|
||||
- Are more than max_age_minutes old
|
||||
- AND are not one of the keep_recent most recent files
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
# Get all files in the folder with their full paths and creation times
|
||||
files = [
|
||||
{
|
||||
"path": os.path.join(path, filename),
|
||||
"ctime": os.path.getctime(os.path.join(path, filename)),
|
||||
}
|
||||
for filename in os.listdir(path)
|
||||
if os.path.isfile(os.path.join(path, filename))
|
||||
]
|
||||
|
||||
# Sort files by creation time
|
||||
files.sort(key=lambda f: f["ctime"], reverse=True)
|
||||
|
||||
# Current time in seconds since epoch
|
||||
now = time.time()
|
||||
|
||||
deleted_count = 0
|
||||
for index, file_info in enumerate(files):
|
||||
# File age in minutes
|
||||
age_minutes = (now - file_info["ctime"]) / 60
|
||||
|
||||
# If file is older than x_minutes and is not one of the y_most_recent files, delete it
|
||||
if age_minutes > max_age_minutes and index >= keep_recent:
|
||||
os.remove(file_info["path"])
|
||||
deleted_count += 1
|
||||
|
||||
if deleted_count > 0:
|
||||
print(f"Deleted {deleted_count} old prompt files")
|
||||
|
||||
|
||||
def typer_async(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError: # No event loop running
|
||||
loop = None
|
||||
|
||||
if loop is None:
|
||||
return asyncio.run(f(*args, **kwargs))
|
||||
else:
|
||||
return f(*args, **kwargs) # Return coroutine to be awaited
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(16),
|
||||
retry=retry_if_result(lambda result: result is not True),
|
||||
)
|
||||
def function_with_retry(function, *args, **kwargs):
|
||||
return function(*args, **kwargs)
|
||||
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(16),
|
||||
retry=retry_if_result(lambda result: result is not True),
|
||||
)
|
||||
async def async_function_with_retry(function, *args, **kwargs):
|
||||
return await function(*args, **kwargs)
|
||||
|
||||
|
||||
def log_model_timings(api_handler, save_location="./model_timings.png"):
|
||||
if len(api_handler.model_timings) > 0:
|
||||
plt.figure(figsize=(10, 6))
|
||||
for model in api_handler.model_timings:
|
||||
timings = np.array(api_handler.model_timings[model])
|
||||
wait_times = np.array(api_handler.model_wait_times[model])
|
||||
LOGGER.info(
|
||||
f"{model}: response {timings.mean():.3f}, waiting {wait_times.mean():.3f} (max {wait_times.max():.3f}, min {wait_times.min():.3f})"
|
||||
)
|
||||
plt.plot(
|
||||
timings, label=f"{model} - Response Time", linestyle="-", linewidth=2
|
||||
)
|
||||
plt.plot(
|
||||
wait_times, label=f"{model} - Waiting Time", linestyle="--", linewidth=2
|
||||
)
|
||||
plt.legend()
|
||||
plt.title("Model Performance: Response and Waiting Times")
|
||||
plt.xlabel("Sample Number")
|
||||
plt.ylabel("Time (seconds)")
|
||||
plt.savefig(save_location, dpi=300)
|
||||
plt.close()
|
||||
|
||||
|
||||
def softmax(x):
|
||||
return np.exp(x) / np.sum(np.exp(x), axis=0)
|
||||
@@ -0,0 +1,176 @@
|
||||
name: UE
|
||||
channels:
|
||||
- conda-forge
|
||||
- defaults
|
||||
dependencies:
|
||||
- _libgcc_mutex=0.1=main
|
||||
- _openmp_mutex=5.1=1_gnu
|
||||
- bzip2=1.0.8=h5eee18b_6
|
||||
- ca-certificates=2024.7.2=h06a4308_0
|
||||
- expat=2.6.2=h6a678d5_0
|
||||
- ld_impl_linux-64=2.38=h1181459_1
|
||||
- libffi=3.4.4=h6a678d5_1
|
||||
- libgcc-ng=11.2.0=h1234567_1
|
||||
- libgomp=11.2.0=h1234567_1
|
||||
- libstdcxx-ng=11.2.0=h1234567_1
|
||||
- libuuid=1.41.5=h5eee18b_0
|
||||
- ncurses=6.4=h6a678d5_0
|
||||
- openssl=3.0.14=h5eee18b_0
|
||||
- pip=24.2=py312h06a4308_0
|
||||
- python=3.12.4=h5148396_1
|
||||
- readline=8.2=h5eee18b_0
|
||||
- setuptools=72.1.0=py312h06a4308_0
|
||||
- shortuuid=1.0.13=pyhd8ed1ab_0
|
||||
- sqlite=3.45.3=h5eee18b_0
|
||||
- tk=8.6.14=h39e8969_0
|
||||
- wheel=0.43.0=py312h06a4308_0
|
||||
- xz=5.4.6=h5eee18b_1
|
||||
- zlib=1.2.13=h5eee18b_1
|
||||
- pip:
|
||||
- adjusttext==1.2.0
|
||||
- aiohappyeyeballs==2.4.3
|
||||
- aiohttp==3.10.10
|
||||
- aiosignal==1.3.1
|
||||
- alembic==1.13.3
|
||||
- altair==5.4.1
|
||||
- annotated-types==0.7.0
|
||||
- anthropic==0.37.1
|
||||
- antlr4-python3-runtime==4.9.3
|
||||
- anyio==4.6.2.post1
|
||||
- attrs==24.2.0
|
||||
- beautifulsoup4==4.12.3
|
||||
- blobfile==3.0.0
|
||||
- bs4==0.0.2
|
||||
- cattrs==24.1.2
|
||||
- certifi==2024.8.30
|
||||
- cfgv==3.4.0
|
||||
- charset-normalizer==3.4.0
|
||||
- click==8.1.7
|
||||
- contourpy==1.3.0
|
||||
- cycler==0.12.1
|
||||
- datasets==3.0.2
|
||||
- dill==0.3.8
|
||||
- distlib==0.3.9
|
||||
- distro==1.9.0
|
||||
- docker-pycreds==0.4.0
|
||||
- eval-type-backport==0.2.0
|
||||
- fastapi==0.100.0
|
||||
- filelock==3.16.1
|
||||
- fire==0.7.0
|
||||
- fonttools==4.54.1
|
||||
- frozenlist==1.4.1
|
||||
- fsspec==2024.9.0
|
||||
- gitdb==4.0.11
|
||||
- gitpython==3.1.43
|
||||
- greenlet==3.1.1
|
||||
- h11==0.14.0
|
||||
- httpcore==1.0.6
|
||||
- httptools==0.6.4
|
||||
- httpx==0.27.2
|
||||
- huggingface-hub==0.26.1
|
||||
- hydra-core==1.3.2
|
||||
- identify==2.6.1
|
||||
- idna==3.10
|
||||
- jinja2==3.1.4
|
||||
- jiter==0.6.1
|
||||
- joblib==1.4.2
|
||||
- jsonpath-python==1.0.6
|
||||
- jsonschema==4.23.0
|
||||
- jsonschema-specifications==2024.10.1
|
||||
- kiwisolver==1.4.7
|
||||
- lxml==5.3.0
|
||||
- mako==1.3.6
|
||||
- markdown-it-py==3.0.0
|
||||
- markupsafe==3.0.2
|
||||
- matplotlib==3.9.2
|
||||
- mdurl==0.1.2
|
||||
- mistralai==1.1.0
|
||||
- mpmath==1.3.0
|
||||
- multidict==6.1.0
|
||||
- multiprocess==0.70.16
|
||||
- mypy-extensions==1.0.0
|
||||
- narwhals==1.10.0
|
||||
- networkx==3.4.2
|
||||
- nodeenv==1.9.1
|
||||
- numpy==2.1.2
|
||||
- nvidia-cublas-cu12==12.4.5.8
|
||||
- nvidia-cuda-cupti-cu12==12.4.127
|
||||
- nvidia-cuda-nvrtc-cu12==12.4.127
|
||||
- nvidia-cuda-runtime-cu12==12.4.127
|
||||
- nvidia-cudnn-cu12==9.1.0.70
|
||||
- nvidia-cufft-cu12==11.2.1.3
|
||||
- nvidia-curand-cu12==10.3.5.147
|
||||
- nvidia-cusolver-cu12==11.6.1.9
|
||||
- nvidia-cusparse-cu12==12.3.1.170
|
||||
- nvidia-nccl-cu12==2.21.5
|
||||
- nvidia-nvjitlink-cu12==12.4.127
|
||||
- nvidia-nvtx-cu12==12.4.127
|
||||
- omegaconf==2.3.0
|
||||
- openai==0.28.0
|
||||
- packaging==24.1
|
||||
- pandas==2.2.3
|
||||
- pebble==5.0.7
|
||||
- pillow==10.4.0
|
||||
- platformdirs==4.3.6
|
||||
- polars==1.10.0
|
||||
- pre-commit==4.0.1
|
||||
- propcache==0.2.0
|
||||
- protobuf==5.28.2
|
||||
- psutil==6.1.0
|
||||
- pyarrow==17.0.0
|
||||
- pycryptodomex==3.21.0
|
||||
- pydantic==2.9.2
|
||||
- pydantic-core==2.23.4
|
||||
- pygments==2.18.0
|
||||
- pyparsing==3.2.0
|
||||
- python-dateutil==2.8.2
|
||||
- python-dotenv==1.0.1
|
||||
- pytz==2024.2
|
||||
- pyyaml==6.0.2
|
||||
- referencing==0.35.1
|
||||
- regex==2024.9.11
|
||||
- replicate==1.0.2
|
||||
- requests==2.32.3
|
||||
- rich==13.9.3
|
||||
- rpds-py==0.20.0
|
||||
- safetensors==0.4.5
|
||||
- scikit-learn==1.5.2
|
||||
- scipy==1.14.1
|
||||
- seaborn==0.13.2
|
||||
- sentry-sdk==2.17.0
|
||||
- setproctitle==1.3.3
|
||||
- shellingham==1.5.4
|
||||
- six==1.16.0
|
||||
- smmap==5.0.1
|
||||
- sniffio==1.3.1
|
||||
- soupsieve==2.6
|
||||
- sqlalchemy==2.0.18
|
||||
- starlette==0.27.0
|
||||
- sympy==1.13.1
|
||||
- tabulate==0.9.0
|
||||
- tenacity==9.0.0
|
||||
- termcolor==2.5.0
|
||||
- threadpoolctl==3.5.0
|
||||
- tiktoken==0.8.0
|
||||
- together==1.3.4
|
||||
- tokenizers==0.20.1
|
||||
- torch==2.5.1
|
||||
- tqdm==4.66.5
|
||||
- transformers==4.46.3
|
||||
- triton==3.1.0
|
||||
- trueskill==0.4.5
|
||||
- typer==0.12.5
|
||||
- typing-extensions==4.12.2
|
||||
- typing-inspect==0.9.0
|
||||
- tzdata==2024.2
|
||||
- urllib3==2.2.3
|
||||
- uvicorn==0.22.0
|
||||
- uvloop==0.21.0
|
||||
- virtualenv==20.27.0
|
||||
- wandb==0.18.5
|
||||
- watchfiles==0.24.0
|
||||
- websockets==13.1
|
||||
- xxhash==3.5.0
|
||||
- yarl==1.16.0
|
||||
- zstandard==0.23.0
|
||||
prefix: /home/wenjiaxin/miniconda3/envs/UE
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
@@ -0,0 +1,31 @@
|
||||
adjustText
|
||||
alembic
|
||||
altair
|
||||
anthropic
|
||||
cattrs
|
||||
datasets
|
||||
fastapi==0.100.0
|
||||
fire
|
||||
hydra-core
|
||||
matplotlib
|
||||
mistralai
|
||||
openai==0.28.0
|
||||
pandas
|
||||
pebble
|
||||
polars
|
||||
pre-commit
|
||||
pydantic
|
||||
replicate
|
||||
scikit-learn
|
||||
scipy
|
||||
seaborn
|
||||
sqlalchemy==2.0.18
|
||||
tabulate
|
||||
tenacity
|
||||
termcolor
|
||||
tiktoken
|
||||
tqdm
|
||||
trueskill
|
||||
typer[all]
|
||||
uvicorn[standard]==0.22.0
|
||||
wandb
|
||||
@@ -0,0 +1,11 @@
|
||||
# setup.py
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="my_package",
|
||||
version="0.1",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
# List your package dependencies here
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
__all__ = ["Language", "PromptType"]
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Language(Enum):
|
||||
PYTHON = ("python", "Python")
|
||||
CPP = ("cpp", "C++")
|
||||
|
||||
def __init__(self, code, text):
|
||||
self.code = code
|
||||
self.text = text
|
||||
|
||||
@staticmethod
|
||||
def from_code(code):
|
||||
if code == "python":
|
||||
return Language.PYTHON
|
||||
elif code == "cpp":
|
||||
return Language.CPP
|
||||
else:
|
||||
raise Exception(f"Unknown code langauge: {code}")
|
||||
|
||||
|
||||
class PromptType(Enum):
|
||||
SOLUTION = "solution_generation"
|
||||
BLUE_TEAM = "blue_team"
|
||||
RED_TEAM = "red_team"
|
||||
EVAL = "eval"
|
||||
|
||||
|
||||
class DifficultyEstimationType(Enum):
|
||||
PROBLEM_ONLY = "problem_only"
|
||||
PROBLEM_SOLUTION = "problem_solution"
|
||||
PROBLEM_SOLUTION_EXECUTION = "problem_solution_execution"
|
||||
@@ -0,0 +1,560 @@
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
from tqdm import tqdm
|
||||
import numpy as np
|
||||
from datasets import load_dataset
|
||||
import argparse
|
||||
|
||||
from core.llm_api.llm import ModelAPI
|
||||
from core.utils import setup_environment
|
||||
from src.experiments.ICM_tools import (
|
||||
propose_consistencyfix,
|
||||
run_consistencyfix,
|
||||
pick_two_inconsistent_claims,
|
||||
update_assign_based_on_decision,
|
||||
)
|
||||
from src.model_querying.prompt_creation import (
|
||||
get_decision_prompt,
|
||||
get_judge_prompt_fewshot,
|
||||
)
|
||||
from src.model_querying.solution_extraction import (
|
||||
extract_claim_logprobs,
|
||||
extract_decision_logprobs,
|
||||
)
|
||||
from src.pipeline.pipeline import Pipeline, PipelineConfig
|
||||
from src.tools.dataloaders import (
|
||||
load_assignments,
|
||||
load_problems_from_json,
|
||||
load_problems_from_json_ids,
|
||||
)
|
||||
from src.tools.path_utils import get_default_results_directory, get_root_directory
|
||||
|
||||
|
||||
def calculate_accuracy(train_data, inconsistent_pairs):
|
||||
train_probs = []
|
||||
for i in train_data.values():
|
||||
if i["label"] is None:
|
||||
continue
|
||||
if i["label"] == 1:
|
||||
train_probs.append(i["score"])
|
||||
else:
|
||||
train_probs.append(-i["score"])
|
||||
if len(train_probs) == 0:
|
||||
train_prob = 0
|
||||
else:
|
||||
train_prob = np.mean(train_probs)
|
||||
|
||||
return {
|
||||
"train_accuracy": 0
|
||||
if len(train_data) == 0
|
||||
else np.mean([i["label"] == i["vanilla_label"] for i in train_data.values()]),
|
||||
"train_label_distribution": Counter(
|
||||
[i["vanilla_label"] for i in train_data.values()]
|
||||
),
|
||||
"train_predict_distribution": Counter(
|
||||
[i["label"] for i in train_data.values()]
|
||||
),
|
||||
"train_prob": train_prob,
|
||||
"train_size": len(train_data),
|
||||
"inconsistent_num": len(inconsistent_pairs),
|
||||
}
|
||||
|
||||
|
||||
def update_assign(data):
|
||||
for key, value in data.items():
|
||||
if value["score"] > 0:
|
||||
value["label"] = 1
|
||||
else:
|
||||
value["label"] = 0
|
||||
return data
|
||||
|
||||
|
||||
def fix_inconsistency(demonstrations, cur_metric, name, alpha, iter=0, K=20):
|
||||
backup_metric = deepcopy(cur_metric)
|
||||
if cur_metric["inconsistent_num"] == 0:
|
||||
return demonstrations, cur_metric
|
||||
|
||||
cur_pool = {k: v for k, v in demonstrations.items() if v["label"] is not None}
|
||||
assignment = cur_pool
|
||||
|
||||
best_metric = cur_metric
|
||||
best_assignment = assignment
|
||||
best_decision_id = None
|
||||
for k in range(K):
|
||||
pipeline = propose_consistencyfix(
|
||||
args.model,
|
||||
name=name,
|
||||
iter=f"{iter}-{k}",
|
||||
assignment=assignment,
|
||||
)
|
||||
results = asyncio.run(pipeline.run())
|
||||
decisions = results["decisions"]
|
||||
assignment = results["get_assign"]
|
||||
for decision_id, decision in enumerate(decisions.values()):
|
||||
tmp_decision_metric_list = []
|
||||
tmp_decision_assignment_list = []
|
||||
for score_idx, score in enumerate([0, 1]):
|
||||
tmp_decision = deepcopy(decision)
|
||||
tmp_decision["score"] = score
|
||||
tmp_assignment = update_assign_based_on_decision(
|
||||
deepcopy(assignment), tmp_decision
|
||||
)
|
||||
tmp_pipeline = run_consistencyfix(
|
||||
model=args.model,
|
||||
name=name,
|
||||
iter=f"{iter}-{k}-{decision_id}-{score_idx}",
|
||||
assignment=tmp_assignment,
|
||||
)
|
||||
tmp_results = asyncio.run(tmp_pipeline.run())
|
||||
tmp_metric = tmp_results["evaluate"]
|
||||
tmp_decision_metric_list.append(tmp_metric)
|
||||
tmp_decision_assignment_list.append(tmp_assignment)
|
||||
tmp_best_decision_id = np.argmax(
|
||||
[get_energy(i, args.alpha) for i in tmp_decision_metric_list]
|
||||
)
|
||||
tmp_assignment = tmp_decision_assignment_list[tmp_best_decision_id]
|
||||
tmp_metric = tmp_decision_metric_list[tmp_best_decision_id]
|
||||
|
||||
if get_energy(tmp_metric, args.alpha) >= get_energy(best_metric, args.alpha):
|
||||
best_decision_id = decision_id
|
||||
best_metric = tmp_metric
|
||||
best_assignment = tmp_assignment
|
||||
break
|
||||
if best_decision_id is None:
|
||||
break
|
||||
elif best_metric["inconsistent_num"] == 0:
|
||||
assignment = best_assignment
|
||||
break
|
||||
else:
|
||||
assignment = best_assignment
|
||||
|
||||
for k in assignment:
|
||||
demonstrations[k] = assignment[k]
|
||||
|
||||
return demonstrations, best_metric
|
||||
|
||||
|
||||
def get_pipeline(
|
||||
model,
|
||||
name=None,
|
||||
use_cache=True,
|
||||
num_problems=None,
|
||||
decision_id=None,
|
||||
iter=None,
|
||||
assignment=None,
|
||||
):
|
||||
pipeline_name = f"iterative-truth-assign-iter-{iter}"
|
||||
if decision_id is not None:
|
||||
pipeline_name += f"-{decision_id}"
|
||||
if name is not None:
|
||||
pipeline_name += "-" + name
|
||||
|
||||
ROOT_DIR = get_root_directory()
|
||||
DATA_DIR = ROOT_DIR / "data"
|
||||
|
||||
|
||||
pipeline_config = PipelineConfig(
|
||||
pipeline_name,
|
||||
anthropic_num_threads=40,
|
||||
openai_fraction_rate_limit=0.99,
|
||||
num_problems=num_problems,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
pipeline = Pipeline(pipeline_config)
|
||||
|
||||
assert assignment is not None
|
||||
initial_assign = pipeline.add_load_data_step(
|
||||
"get_assign", load_assignments, assignment
|
||||
)
|
||||
|
||||
def add_train_demonstrations(train_data):
|
||||
copy_data = deepcopy(train_data)
|
||||
copy_data = {k: v for k, v in copy_data.items() if v["label"] is not None}
|
||||
keys = list(copy_data.keys())
|
||||
values = list(copy_data.values())
|
||||
saved_keys = [
|
||||
"prompt",
|
||||
"question",
|
||||
"choice",
|
||||
"choice_2",
|
||||
"consistency_id",
|
||||
"consistency_key",
|
||||
"source",
|
||||
"label",
|
||||
"vanilla_label",
|
||||
]
|
||||
values = []
|
||||
for i in copy_data.values():
|
||||
values.append({saved_key: i[saved_key] for saved_key in saved_keys if saved_key in i})
|
||||
|
||||
for idx, key in enumerate(keys):
|
||||
tmp_keys, tmp_values = [], []
|
||||
for j, (prev_key, prev_value) in enumerate(zip(keys, values)):
|
||||
if j != idx:
|
||||
tmp_keys.append(prev_key)
|
||||
tmp_values.append(prev_value)
|
||||
|
||||
demos = {
|
||||
prev_key: prev_value
|
||||
for j, (prev_key, prev_value) in enumerate(zip(tmp_keys, tmp_values))
|
||||
}
|
||||
|
||||
sorted_demos = {}
|
||||
for k, v in demos.items():
|
||||
q = v["consistency_id"]
|
||||
if q not in sorted_demos:
|
||||
sorted_demos[q] = []
|
||||
sorted_demos[q].append((k, v))
|
||||
|
||||
out_sorted_demos = {}
|
||||
for group in sorted_demos.values():
|
||||
for k, v in group:
|
||||
out_sorted_demos[k] = v
|
||||
|
||||
copy_data[key]["demonstration"] = out_sorted_demos
|
||||
|
||||
return copy_data
|
||||
|
||||
merged_train_data = pipeline.add_transformation_step(
|
||||
"add_train_demonstration",
|
||||
add_train_demonstrations,
|
||||
dependencies=[initial_assign],
|
||||
)
|
||||
|
||||
get_train_preds = pipeline.add_query_step(
|
||||
"get_train_preds",
|
||||
model,
|
||||
get_judge_prompt_fewshot,
|
||||
extract_claim_logprobs,
|
||||
dependencies=[merged_train_data],
|
||||
logprobs=20,
|
||||
max_tokens=1,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
|
||||
pick_claims = pipeline.add_transformation_step(
|
||||
"pick_two_inconsistent_claims",
|
||||
pick_two_inconsistent_claims,
|
||||
dependencies=[initial_assign],
|
||||
)
|
||||
|
||||
eval_preds = pipeline.add_eval_step(
|
||||
"evaluate",
|
||||
calculate_accuracy,
|
||||
dependencies=[get_train_preds, pick_claims],
|
||||
)
|
||||
return pipeline
|
||||
|
||||
|
||||
async def predict_assignment(model, example, demonstrations):
|
||||
demos = [
|
||||
v
|
||||
for k, v in demonstrations.items()
|
||||
if k != example["uid"] and v["label"] is not None
|
||||
]
|
||||
anthropic_requests = [
|
||||
model_api(
|
||||
model,
|
||||
get_judge_prompt_fewshot(
|
||||
example,
|
||||
demos,
|
||||
pipeline=False,
|
||||
),
|
||||
logprobs=20,
|
||||
max_tokens=1,
|
||||
parse_fn=extract_claim_logprobs,
|
||||
)
|
||||
]
|
||||
responses = await asyncio.gather(*anthropic_requests)
|
||||
score = responses[0][0]["score"]
|
||||
new_label = score > 0
|
||||
return int(new_label)
|
||||
|
||||
|
||||
def get_temperature(
|
||||
iteration, initial_temp, final_temp, decay_rate, schedule="exp"
|
||||
):
|
||||
"""
|
||||
Calculate the temperature for simulated annealing.
|
||||
|
||||
Parameters:
|
||||
- iteration: Current iteration number.
|
||||
- initial_temp: Initial temperature.
|
||||
- decay_rate: Rate at which the temperature decreases.
|
||||
|
||||
Returns:
|
||||
- Current temperature.
|
||||
"""
|
||||
if schedule == "exp":
|
||||
return max(final_temp, initial_temp * (decay_rate**iteration))
|
||||
elif schedule == "log":
|
||||
return max(final_temp, initial_temp / (1 + 2 * np.log(1 + iteration)))
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
def get_energy(metric, alpha):
|
||||
return alpha * metric["train_prob"] - metric["inconsistent_num"]
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--alpha", type=float, default=30)
|
||||
parser.add_argument("--seed", type=int, default=27565976)
|
||||
parser.add_argument("--testbed", type=str, default="gsm8k")
|
||||
parser.add_argument("--model", type=str, default="meta-llama/Llama-3.1-70B")
|
||||
parser.add_argument("--batch_size", type=int, default=256)
|
||||
parser.add_argument("--num_seed", type=int, default=8)
|
||||
parser.add_argument("--K", type=int, default=3000)
|
||||
parser.add_argument("--consistency_fix_K", type=int, default=10)
|
||||
parser.add_argument("--decay", type=float, default=0.99)
|
||||
parser.add_argument("--initial_T", type=float, default=10)
|
||||
parser.add_argument("--final_T", type=float, default=0.01)
|
||||
parser.add_argument("--scheduler", type=str, default="log")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def load_data(args):
|
||||
if args.testbed == "alpaca":
|
||||
with open(get_root_directory() / "data/train_alpaca.json") as f:
|
||||
train = json.load(f)
|
||||
template = """Human: {question}
|
||||
Response 1: {choice}
|
||||
Response 2: {choice_2}
|
||||
Claim: Response 1 is more helpful and harmless than Response 2.
|
||||
I think this claim is """
|
||||
for i in train:
|
||||
i['source'] = 'alpaca'
|
||||
i['consistency_key'] = 'A>B' if i['label'] else 'B>A'
|
||||
i['prompt'] = template.format(question=i['question'], choice=i['choice'].replace("\n", ""), choice_2=i['choice_2'].replace("\n", ""))
|
||||
|
||||
args.GROUP_SIZE = 2
|
||||
|
||||
elif args.testbed == 'gsm8k':
|
||||
with open(get_root_directory() / "data/train_gsm8k.json") as f:
|
||||
train = json.load(f)
|
||||
template = """Question: {question}
|
||||
Claim: {answer}
|
||||
I think this claim is """
|
||||
|
||||
for i in train:
|
||||
i['source'] = 'gsm8k'
|
||||
i['consistency_key'] = i['answer']
|
||||
i['prompt'] = template.format(question=i['question'], answer=i['choice'])
|
||||
|
||||
args.GROUP_SIZE = 4
|
||||
|
||||
elif args.testbed == 'truthfulQA':
|
||||
with open(get_root_directory() / "data/train_truthfulqa.json") as f:
|
||||
train = json.load(f)
|
||||
template = """Question: {question}
|
||||
Claim: {answer}
|
||||
I think this claim is """
|
||||
|
||||
for i in train:
|
||||
i['source'] = 'truthfulQA'
|
||||
i['consistency_key'] = 'A' if i['label'] else 'B'
|
||||
i['prompt'] = template.format(question=i['question'], answer=i['choice'])
|
||||
args.GROUP_SIZE = 4
|
||||
|
||||
elif args.testbed == 'truthfulQA-preference':
|
||||
with open(get_root_directory() / "data/train_truthfulqa_preference.json") as f:
|
||||
train = json.load(f)
|
||||
template = """Question: {question}
|
||||
Answer 1: {choice}
|
||||
Answer 2: {choice_2}
|
||||
Claim: Answer 1 is more truthful than Answer 2.
|
||||
I think this claim is """
|
||||
|
||||
for i in train:
|
||||
i['source'] = 'truthfulQA-preference'
|
||||
i['consistency_key'] = 'A>B' if i['label'] else 'B>A'
|
||||
i['prompt'] = template.format(question=i['question'], choice=i['choice'], choice_2=i['choice_2'])
|
||||
args.GROUP_SIZE = 2
|
||||
|
||||
train_map = {}
|
||||
for i in train:
|
||||
if i['consistency_id'] not in train_map:
|
||||
train_map[i['consistency_id']] = []
|
||||
train_map[i['consistency_id']].append(i)
|
||||
|
||||
out = []
|
||||
for key in train_map:
|
||||
out += train_map[key]
|
||||
train = out
|
||||
|
||||
# sample a batch of batch_size datapoints
|
||||
fewshot_ids = random.sample(
|
||||
list(range(len(train)// args.GROUP_SIZE)), args.batch_size // args.GROUP_SIZE
|
||||
)
|
||||
fewshot_ids = [
|
||||
i * args.GROUP_SIZE + j for i in fewshot_ids for j in range(args.GROUP_SIZE)
|
||||
]
|
||||
|
||||
return train, fewshot_ids
|
||||
|
||||
def initialize(train, fewshot_ids, args):
|
||||
demonstrations = {}
|
||||
unlabeled_ids = []
|
||||
whole_ids = []
|
||||
seed_ids = []
|
||||
|
||||
random_init_labels = [1] * (args.num_seed // 2) + [0] * (args.num_seed // 2)
|
||||
random.shuffle(random_init_labels)
|
||||
|
||||
for id, i in enumerate(fewshot_ids):
|
||||
item = train[i]
|
||||
item["vanilla_label"] = item["label"] # store dataset labels to measure agreement during the searching process
|
||||
item["uid"] = id
|
||||
whole_ids.append(item["uid"])
|
||||
if id >= args.num_seed: # set labels to None
|
||||
item["label"] = None
|
||||
item["type"] = "predict"
|
||||
unlabeled_ids.append(item["uid"])
|
||||
else: # set random labels
|
||||
item["type"] = "seed"
|
||||
item["label"] = random_init_labels[id]
|
||||
seed_ids.append(item["uid"])
|
||||
demonstrations[id] = item
|
||||
|
||||
return demonstrations, unlabeled_ids, whole_ids, seed_ids
|
||||
|
||||
|
||||
def main(args):
|
||||
train, fewshot_ids = load_data(args)
|
||||
|
||||
demonstrations, unlabeled_ids, whole_ids, seed_ids = initialize(train, fewshot_ids, args)
|
||||
|
||||
cur_metric = {
|
||||
"train_prob": -1e6,
|
||||
"inconsistent_num": 100000,
|
||||
"train_accuracy": 1.0,
|
||||
"train_predict_distribution": {"0": 0, "1": 0},
|
||||
"train_label_distribution": {"0": 0, "1": 0},
|
||||
}
|
||||
|
||||
print('init random labels = ', Counter([i['label'] for i in demonstrations.values() if i['type'] == 'seed']), 'init label acc = ', np.mean([i['label'] == i['vanilla_label'] for i in demonstrations.values() if i['type'] == 'seed']))
|
||||
name = f"{args.testbed}-llama70b-K{args.K}-bc{args.batch_size}_seed{args.seed}-initialsize{args.num_seed}-weighted{args.alpha}-decay{args.decay}-initialT{args.initial_T}-finalT{args.final_T}-scheduler{args.scheduler}"
|
||||
|
||||
iter = 0
|
||||
flip_cnt = 0
|
||||
example_id = 0
|
||||
|
||||
for _ in tqdm(range(args.K), desc="searching"):
|
||||
cur_pool = {
|
||||
k: v for k, v in demonstrations.items() if v["label"] is not None
|
||||
}
|
||||
initial_demos = deepcopy(demonstrations)
|
||||
if iter == 0:
|
||||
pipeline = get_pipeline(
|
||||
args.model,
|
||||
name=name,
|
||||
num_problems=None,
|
||||
iter=iter,
|
||||
assignment=cur_pool,
|
||||
)
|
||||
results = asyncio.run(pipeline.run())
|
||||
cur_metric = results["evaluate"]
|
||||
|
||||
demonstrations, cur_metric = fix_inconsistency(
|
||||
demonstrations, cur_metric, name, args.alpha, iter=iter, K=args.consistency_fix_K
|
||||
)
|
||||
|
||||
cur_pool = {
|
||||
k: v for k, v in demonstrations.items() if v["label"] is not None
|
||||
}
|
||||
|
||||
while True: # weighted sampling
|
||||
candidates_ids = whole_ids
|
||||
weights = [1 for _ in range(len(candidates_ids))]
|
||||
for i in candidates_ids:
|
||||
if i in cur_pool:
|
||||
same_consistency_group_ids = [j for j in candidates_ids if demonstrations[j]["consistency_id"] == demonstrations[i]["consistency_id"]]
|
||||
for j in same_consistency_group_ids:
|
||||
if j not in cur_pool:
|
||||
weights[j] = 100
|
||||
|
||||
example_id = random.choices(candidates_ids, k=1, weights=weights)[0]
|
||||
break
|
||||
|
||||
new_label = asyncio.run(
|
||||
predict_assignment(
|
||||
args.model,
|
||||
demonstrations[example_id],
|
||||
cur_pool,
|
||||
)
|
||||
)
|
||||
|
||||
if demonstrations[example_id]["label"] != new_label:
|
||||
tmp_demonstrations = deepcopy(demonstrations)
|
||||
tmp_demonstrations[example_id]["label"] = new_label
|
||||
dummy_metric = {
|
||||
"train_prob": -1e6,
|
||||
"inconsistent_num": 100000,
|
||||
"train_accuracy": 1.0,
|
||||
"train_predict_distribution": {"0": 0, "1": 0},
|
||||
"train_label_distribution": {"0": 0, "1": 0},
|
||||
}
|
||||
|
||||
tmp_demonstrations, _ = fix_inconsistency(
|
||||
tmp_demonstrations,
|
||||
dummy_metric,
|
||||
name + "newlabelexplore",
|
||||
args.alpha,
|
||||
iter=iter,
|
||||
K=10,
|
||||
)
|
||||
|
||||
tmp_pool = {
|
||||
k: v
|
||||
for k, v in tmp_demonstrations.items()
|
||||
if v["label"] is not None
|
||||
}
|
||||
pipeline = get_pipeline(
|
||||
model=args.model,
|
||||
name=name,
|
||||
num_problems=None,
|
||||
iter=iter,
|
||||
assignment=tmp_pool,
|
||||
)
|
||||
results = asyncio.run(pipeline.run())
|
||||
metric = results["evaluate"]
|
||||
T = get_temperature(
|
||||
flip_cnt, args.initial_T, args.final_T, args.decay, schedule=args.scheduler
|
||||
)
|
||||
print(f"iter = {iter}, pool size = {len(cur_pool)}, cur acc = {cur_metric['train_accuracy']}, new acc = {metric['train_accuracy']}, cur score = {get_energy(cur_metric, args.alpha)}, new score = {get_energy(metric, args.alpha)}, cur inconsistent num = {cur_metric['inconsistent_num']}, new inconsistent num = {metric['inconsistent_num']}")
|
||||
print('cur label distribution = ', Counter([i['label'] for i in demonstrations.values() if i['label'] is not None]))
|
||||
print('new label distribution = ', Counter([i['label'] for i in tmp_demonstrations.values() if i['label'] is not None]))
|
||||
|
||||
accept_prob = math.exp((get_energy(metric, args.alpha) - get_energy(cur_metric, args.alpha)) / T)
|
||||
print("accept prob = ", accept_prob)
|
||||
if random.random() < accept_prob:
|
||||
print("accept")
|
||||
demonstrations = tmp_demonstrations
|
||||
flip_cnt += 1
|
||||
cur_metric = metric
|
||||
with open(f"log_{name}.jsonl", "a") as f:
|
||||
f.write(json.dumps({
|
||||
"iter": iter,
|
||||
"flip_cnt": flip_cnt,
|
||||
"acc": cur_metric['train_accuracy'],
|
||||
"score": get_energy(cur_metric, args.alpha),
|
||||
}) + "\n")
|
||||
else:
|
||||
print("reject")
|
||||
|
||||
print("=" * 100)
|
||||
iter += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_environment(logger_level="error")
|
||||
model_api = ModelAPI(anthropic_num_threads=20, openai_fraction_rate_limit=0.99)
|
||||
args = get_args()
|
||||
print("task: ", args.testbed)
|
||||
random.seed(args.seed)
|
||||
main(args)
|
||||
@@ -0,0 +1,238 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
from datasets import load_dataset
|
||||
|
||||
from src.model_querying.prompt_creation import (
|
||||
get_decision_prompt,
|
||||
get_judge_prompt_fewshot,
|
||||
)
|
||||
from src.model_querying.solution_extraction import (
|
||||
extract_claim_logprobs,
|
||||
extract_decision_logprobs,
|
||||
)
|
||||
from src.pipeline.pipeline import Pipeline, PipelineConfig
|
||||
from src.tools.dataloaders import (
|
||||
load_assignments,
|
||||
load_problems_from_json,
|
||||
load_problems_from_json_ids,
|
||||
)
|
||||
from src.tools.path_utils import get_default_results_directory, get_root_directory
|
||||
|
||||
|
||||
def calculate_accuracy(train_data, inconsistent_pairs):
|
||||
return {
|
||||
"train_predict_distribution": Counter(
|
||||
[i["label"] for i in train_data.values()]
|
||||
),
|
||||
"train_label_distribution": Counter(
|
||||
[i["vanilla_label"] for i in train_data.values()]
|
||||
),
|
||||
"train_accuracy": np.mean(
|
||||
[i["label"] == i["vanilla_label"] for i in train_data.values()]
|
||||
),
|
||||
"train_prob": np.mean(
|
||||
[
|
||||
i["score"] if i["label"] == 1 else -i["score"]
|
||||
for i in train_data.values()
|
||||
]
|
||||
),
|
||||
"train_size": len(train_data),
|
||||
"inconsistent_num": len(inconsistent_pairs),
|
||||
}
|
||||
|
||||
|
||||
def update_assign_based_on_decision(data, decision):
|
||||
if decision["type"] == "contradiction":
|
||||
if decision["score"] > 0:
|
||||
data[decision["claim_1"]["uid"]]["label"] = 1
|
||||
data[decision["claim_2"]["uid"]]["label"] = 0
|
||||
else:
|
||||
data[decision["claim_1"]["uid"]]["label"] = 0
|
||||
data[decision["claim_2"]["uid"]]["label"] = 1
|
||||
else:
|
||||
assert decision["type"] == "implication"
|
||||
if decision["score"] > 0:
|
||||
data[decision["claim_1"]["uid"]]["label"] = 1
|
||||
data[decision["claim_2"]["uid"]]["label"] = 1
|
||||
else:
|
||||
data[decision["claim_1"]["uid"]]["label"] = 0
|
||||
data[decision["claim_2"]["uid"]]["label"] = 0
|
||||
return data
|
||||
|
||||
|
||||
def pick_two_inconsistent_claims(data):
|
||||
claims = list(data.values())
|
||||
|
||||
consistency_groups = {}
|
||||
for claim in claims:
|
||||
cid = claim["consistency_id"]
|
||||
if cid not in consistency_groups:
|
||||
consistency_groups[cid] = []
|
||||
consistency_groups[cid].append(claim)
|
||||
|
||||
inconsistent_pairs = {}
|
||||
for group in consistency_groups.values():
|
||||
labels = [claim["vanilla_label"] for claim in group]
|
||||
for i in range(len(group)):
|
||||
for j in range(i + 1, len(group)):
|
||||
if (group[i]['consistency_key'] != group[j]['consistency_key']) and (
|
||||
(group[i]['label'] == group[j]['label'] == 1) or
|
||||
(
|
||||
(group[i]['consistency_key'] in ['A>B', 'B>A']) and (group[i]['label'] == group[j]['label'] == 0) # in comparative tasks, at least one of the two claims is correct
|
||||
)
|
||||
):
|
||||
# if (group[i]["vanilla_label"] != group[j]["vanilla_label"]) and (
|
||||
# group[i]["label"] == group[j]["label"]
|
||||
# ):
|
||||
inconsistent_pairs[len(inconsistent_pairs)] = {
|
||||
"claim_1": group[i],
|
||||
"claim_2": group[j],
|
||||
"consistency_id": group[i]["consistency_id"],
|
||||
"type": "contradiction",
|
||||
}
|
||||
elif (group[i]["consistency_key"] == group[j]["consistency_key"]) and (
|
||||
group[i]["label"] != group[j]["label"]
|
||||
):
|
||||
inconsistent_pairs[len(inconsistent_pairs)] = {
|
||||
"claim_1": group[i],
|
||||
"claim_2": group[j],
|
||||
"consistency_id": group[i]["consistency_id"],
|
||||
"type": "implication",
|
||||
}
|
||||
random.shuffle(inconsistent_pairs)
|
||||
return inconsistent_pairs
|
||||
|
||||
|
||||
def propose_consistencyfix(
|
||||
model,
|
||||
name=None,
|
||||
iter=None,
|
||||
assignment=None,
|
||||
use_cache=True,
|
||||
):
|
||||
pipeline_name = f"propose-consistencyfix-iter-{iter}"
|
||||
if name is not None:
|
||||
pipeline_name += "-" + name
|
||||
pipeline_config = PipelineConfig(
|
||||
pipeline_name,
|
||||
anthropic_num_threads=40,
|
||||
openai_fraction_rate_limit=0.99,
|
||||
num_problems=None,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
pipeline = Pipeline(pipeline_config)
|
||||
|
||||
initial_assign = pipeline.add_load_data_step(
|
||||
"get_assign", load_assignments, assignment
|
||||
)
|
||||
|
||||
pick_claims = pipeline.add_transformation_step(
|
||||
"pick_two_inconsistent_claims",
|
||||
pick_two_inconsistent_claims,
|
||||
dependencies=[initial_assign],
|
||||
)
|
||||
|
||||
get_decision = pipeline.add_query_step(
|
||||
"decisions",
|
||||
model,
|
||||
get_decision_prompt,
|
||||
extract_decision_logprobs,
|
||||
dependencies=[pick_claims],
|
||||
logprobs=20,
|
||||
max_tokens=1,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
return pipeline
|
||||
|
||||
def run_consistencyfix(
|
||||
model,
|
||||
name=None,
|
||||
use_cache=True,
|
||||
decision_id=None,
|
||||
decision=None,
|
||||
iter=None,
|
||||
assignment=None,
|
||||
):
|
||||
pipeline_name = f"consistencyfix-iter-{iter}"
|
||||
if decision_id is not None:
|
||||
pipeline_name += f"-{decision_id}"
|
||||
if name is not None:
|
||||
pipeline_name += "-" + name
|
||||
|
||||
pipeline_config = PipelineConfig(
|
||||
pipeline_name,
|
||||
anthropic_num_threads=40,
|
||||
openai_fraction_rate_limit=0.99,
|
||||
num_problems=None,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
pipeline = Pipeline(pipeline_config)
|
||||
|
||||
assert assignment is not None
|
||||
initial_assign = pipeline.add_load_data_step(
|
||||
"get_assign", load_assignments, assignment
|
||||
)
|
||||
|
||||
pick_claims = pipeline.add_transformation_step(
|
||||
"pick_two_inconsistent_claims",
|
||||
pick_two_inconsistent_claims,
|
||||
dependencies=[initial_assign],
|
||||
)
|
||||
|
||||
def add_train_demonstrations(train_data):
|
||||
copy_data = deepcopy(train_data)
|
||||
keys = list(copy_data.keys())
|
||||
values = list(copy_data.values())
|
||||
saved_keys = [
|
||||
"prompt",
|
||||
"question",
|
||||
"choice",
|
||||
"choice_2",
|
||||
"consistency_id",
|
||||
"source",
|
||||
"label",
|
||||
"vanilla_label",
|
||||
]
|
||||
values = []
|
||||
for i in copy_data.values():
|
||||
values.append(
|
||||
{saved_key: i[saved_key] for saved_key in saved_keys if saved_key in i}
|
||||
)
|
||||
|
||||
for idx, key in enumerate(keys):
|
||||
train_data[key]["demonstration"] = {
|
||||
prev_key: prev_value
|
||||
for j, (prev_key, prev_value) in enumerate(zip(keys, values))
|
||||
if j != idx
|
||||
}
|
||||
return train_data
|
||||
|
||||
merged_train_data = pipeline.add_transformation_step(
|
||||
"add_train_demonstration",
|
||||
add_train_demonstrations,
|
||||
dependencies=[initial_assign],
|
||||
)
|
||||
|
||||
get_train_preds = pipeline.add_query_step(
|
||||
"get_train_preds",
|
||||
model,
|
||||
get_judge_prompt_fewshot,
|
||||
extract_claim_logprobs,
|
||||
dependencies=[merged_train_data],
|
||||
logprobs=20,
|
||||
max_tokens=1,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
|
||||
eval_preds = pipeline.add_eval_step(
|
||||
"evaluate",
|
||||
calculate_accuracy,
|
||||
dependencies=[get_train_preds, pick_claims],
|
||||
)
|
||||
|
||||
return pipeline
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{"iter": 2, "flip_cnt": 1, "acc": 0.7777777777777778, "score": 59.95838513901126}
|
||||
{"iter": 4, "flip_cnt": 2, "acc": 0.8, "score": 62.33137680262533}
|
||||
{"iter": 7, "flip_cnt": 3, "acc": 0.9090909090909091, "score": 71.59853190532465}
|
||||
{"iter": 8, "flip_cnt": 4, "acc": 0.9166666666666666, "score": 79.43930320079279}
|
||||
{"iter": 10, "flip_cnt": 5, "acc": 0.9230769230769231, "score": 82.68683171642135}
|
||||
{"iter": 11, "flip_cnt": 6, "acc": 0.9285714285714286, "score": 86.60106027413124}
|
||||
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
from matplotlib import pyplot as plt
|
||||
import os
|
||||
|
||||
for file in os.listdir("."):
|
||||
if file.startswith("log_"):
|
||||
print(file)
|
||||
with open(file, "r") as f:
|
||||
data = [json.loads(i) for i in f]
|
||||
data = data[:60]
|
||||
x = list(range(8, 8 + len(data)))
|
||||
y_score = [i['score'] for i in data]
|
||||
y_acc = [i['acc'] for i in data]
|
||||
# plt.subplot(1, 2, 1)
|
||||
plt.plot(x, y_score, label=f'Acc {max(y_acc):.2f}')
|
||||
plt.xlabel("# Searched Claims")
|
||||
plt.ylabel("Score")
|
||||
# plt.subplot(1, 2, 2)
|
||||
# plt.plot(x, y_acc)
|
||||
# plt.xlabel("# Searched Claims")
|
||||
# plt.ylabel("Accuracy")
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
plt.savefig("log.png", dpi=500)
|
||||
@@ -0,0 +1,45 @@
|
||||
__all__ = ["get_apps_solution_prompt", "get_two_shot_prompt", "get_backdoor_prompt"]
|
||||
|
||||
import json
|
||||
import random
|
||||
|
||||
from src.datatypes.enums import Language, PromptType
|
||||
from src.tools.dataloaders import load_prompts
|
||||
from src.tools.string_manipulation import format_key_suffix, strip_comments_from_string
|
||||
|
||||
SOLUTION_PROMPTS = load_prompts(PromptType.SOLUTION)
|
||||
RED_TEAM_PROMPTS = load_prompts(PromptType.RED_TEAM)
|
||||
BLUE_TEAM_PROMPTS = load_prompts(PromptType.BLUE_TEAM)
|
||||
EVAL_PROMPTS = load_prompts(PromptType.EVAL)
|
||||
|
||||
|
||||
class Prompt:
|
||||
@staticmethod
|
||||
def empty():
|
||||
return Prompt([])
|
||||
|
||||
def __init__(self, text, logit_bias=None):
|
||||
self.text = text
|
||||
self.logit_bias = logit_bias
|
||||
|
||||
|
||||
def get_decision_prompt(example):
|
||||
prompt = 'dummy prompt'
|
||||
return Prompt(prompt)
|
||||
|
||||
|
||||
def get_judge_prompt_fewshot(example, demonstrations=None, pipeline=True):
|
||||
if demonstrations is None:
|
||||
demonstrations = list(example["demonstration"].values())
|
||||
prompt = ""
|
||||
for i in demonstrations:
|
||||
prompt += i['prompt']
|
||||
prompt += "True" if i["label"] else "False"
|
||||
prompt += "\n\n"
|
||||
|
||||
prompt += example['prompt']
|
||||
|
||||
if pipeline:
|
||||
return Prompt(prompt)
|
||||
else:
|
||||
return prompt
|
||||
@@ -0,0 +1,55 @@
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from copy import copy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_yes_no(x):
|
||||
x = x.lower()
|
||||
y = "true" in x
|
||||
n = "false" in x
|
||||
if y == n:
|
||||
return None
|
||||
return y
|
||||
|
||||
|
||||
def get_yes_no_diff_logprobs(logprobs):
|
||||
eps = 1e-5
|
||||
prob_sums = {False: eps, True: eps}
|
||||
for k, v in logprobs.items():
|
||||
o = get_yes_no(k)
|
||||
if o is None:
|
||||
continue
|
||||
prob_sums[o] += math.exp(v)
|
||||
|
||||
if prob_sums[False] == eps and prob_sums[True] == eps:
|
||||
return 0
|
||||
else:
|
||||
return math.log(prob_sums[True]) - math.log(prob_sums[False])
|
||||
|
||||
|
||||
def extract_claim_logprobs(response):
|
||||
response = response.copy()
|
||||
try:
|
||||
logprobs = response["response"]["logprobs"][0]
|
||||
response[f"score"] = get_yes_no_diff_logprobs(logprobs)
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
f"Problem {response['metadata']['uid']}: Error extracting judgment: {repr(e)}"
|
||||
)
|
||||
response["score"] = 0
|
||||
return response
|
||||
|
||||
def extract_decision_logprobs(response):
|
||||
response = response.copy()
|
||||
try:
|
||||
logprobs = response["response"]["logprobs"][0]
|
||||
response[f"score"] = get_yes_no_diff_logprobs(logprobs)
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
f"Problem {response['metadata']['uid']}: Error extracting decision: {repr(e)}"
|
||||
)
|
||||
response["score"] = 0
|
||||
return response
|
||||
@@ -0,0 +1,27 @@
|
||||
How to use the pipeline:
|
||||
First, outline the graph you would like to execute, including the following:
|
||||
* Data Loading
|
||||
* Model queries
|
||||
* Code Execution Eval
|
||||
* Transformations
|
||||
* Monitoring
|
||||
|
||||
Next, convert each of the nodes in that graph into the corresponding helper function:
|
||||
* add_load_data_step
|
||||
* add_query_step
|
||||
* add_code_evaluation_step
|
||||
* add_transformation_step
|
||||
* add_monitoring_step
|
||||
|
||||
Each of these takes different parameters that you can see in the method signatures. The important ones to know are these:
|
||||
LoadData takes either a data-loading function and a location, or it takes a dataset
|
||||
Queries take a prompt function that they pass the incoming data into to create the associated prompt, and a parse function that they use to parse the LLM response
|
||||
Code Evals take an executor function that executes all of the code in the Solution objects on the associated test cases.
|
||||
Transforms take arbitrary functions that they apply to the data as a whole.
|
||||
Monitoring steps take arbitrary monitoring steps. I may eventually enforce that all pipelines end in one of these because it's really what we care about.
|
||||
|
||||
Finally, put the dependencies of each step into their dependencies parameter. This is how execution order is determined and how data flows between steps. If you rely on more than one step, the data will be passed to ordered args in the same order as the list of dependencies.
|
||||
|
||||
You will also need to include a PipelineConfig parameter that contains metadata around how many concurrents to use and similar.
|
||||
|
||||
Once you have a pipeline definition, call the pipeline.run() function on it to execute the graph. This method returns the Pipeline.Results object back, which holds the output of each step in a dictionary.
|
||||
@@ -0,0 +1,314 @@
|
||||
__all__ = ["PipelineConfig", "Pipeline"]
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
from core.llm_api.llm import ModelAPI
|
||||
from src.datatypes.enums import Language
|
||||
from src.runners.query_model import QueryConfigBuilder, query_model
|
||||
from src.tools.dataloaders import read_from_cache, save_to_cache
|
||||
from src.tools.path_utils import get_root_directory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def in_notebook():
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
if get_ipython() is None or "IPKernelApp" not in get_ipython().config:
|
||||
return False
|
||||
except ImportError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Task:
|
||||
def __init__(self, name, func, use_cache, dependencies=[]):
|
||||
self.name = name
|
||||
self.func = func
|
||||
self.use_cache = use_cache
|
||||
self.index = None
|
||||
self.dependencies = dependencies
|
||||
self.dependents = []
|
||||
self.result = None
|
||||
for dep in dependencies:
|
||||
dep.dependents.append(self)
|
||||
|
||||
async def execute(self, results):
|
||||
if self.result is None:
|
||||
dep_results = [results[dep.name] for dep in self.dependencies]
|
||||
if asyncio.iscoroutinefunction(self.func):
|
||||
self.result = await self.func(
|
||||
*dep_results, use_cache=self.use_cache, index=self.index
|
||||
)
|
||||
else:
|
||||
self.result = self.func(
|
||||
*dep_results, use_cache=self.use_cache, index=self.index
|
||||
)
|
||||
return self.result
|
||||
|
||||
|
||||
class PipelineConfig:
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
anthropic_num_threads=2,
|
||||
openai_fraction_rate_limit=0.99,
|
||||
use_cache=True,
|
||||
language=Language.PYTHON,
|
||||
num_problems=None,
|
||||
problem_ids=None,
|
||||
num_open_files=1000000,
|
||||
organization="NYU_ORG",
|
||||
print_prompt_and_response=False,
|
||||
api_base=None,
|
||||
):
|
||||
self.name = name
|
||||
self.anthropic_num_threads = anthropic_num_threads
|
||||
self.openai_fraction_rate_limit = openai_fraction_rate_limit
|
||||
self.organization = organization
|
||||
self.print_prompt_and_response = print_prompt_and_response
|
||||
self.use_cache = use_cache
|
||||
self.language = language
|
||||
self.num_problems = num_problems
|
||||
self.problem_ids = problem_ids
|
||||
self.num_open_files = num_open_files
|
||||
self.api_base = api_base
|
||||
self.play_sound = in_notebook()
|
||||
|
||||
|
||||
class Pipeline:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.steps = []
|
||||
self.step_names = set()
|
||||
self.results = {}
|
||||
self.model_api = ModelAPI(
|
||||
self.config.anthropic_num_threads,
|
||||
self.config.openai_fraction_rate_limit,
|
||||
self.config.organization,
|
||||
self.config.print_prompt_and_response,
|
||||
)
|
||||
self.file_sem = asyncio.BoundedSemaphore(self.config.num_open_files)
|
||||
self.cost = {"red": 0, "blue": 0}
|
||||
|
||||
def add_load_data_step(
|
||||
self, name, dataloader_fn, data_location, dependencies=[], use_cache=None
|
||||
):
|
||||
if name in self.step_names:
|
||||
raise ValueError(f"Step name {name} already exists")
|
||||
self.step_names.add(name)
|
||||
|
||||
def call(*args, use_cache, index):
|
||||
return dataloader_fn(
|
||||
data_location,
|
||||
num_problems=self.config.num_problems,
|
||||
problem_ids=self.config.problem_ids,
|
||||
)
|
||||
|
||||
task = Task(name, call, use_cache, dependencies)
|
||||
self.steps.append(task)
|
||||
return task
|
||||
|
||||
def add_query_step(
|
||||
self,
|
||||
name,
|
||||
model,
|
||||
prompt_fn,
|
||||
parse_fn,
|
||||
dependencies=[],
|
||||
use_cache=None,
|
||||
temperature=None,
|
||||
logprobs=None,
|
||||
team=None,
|
||||
max_tokens=4096,
|
||||
bon=1,
|
||||
):
|
||||
if name in self.step_names:
|
||||
raise ValueError(f"Step name {name} already exists")
|
||||
self.step_names.add(name)
|
||||
|
||||
query_config_builder = (
|
||||
QueryConfigBuilder()
|
||||
.with_model_to_test(model)
|
||||
.with_prompt_fn(lambda x: prompt_fn(x))
|
||||
.with_parse_fn(lambda x: parse_fn(x))
|
||||
.with_num_problems(self.config.num_problems)
|
||||
.with_max_tokens(max_tokens)
|
||||
.with_temperature(temperature)
|
||||
.with_logprobs(logprobs)
|
||||
.with_bon(bon)
|
||||
)
|
||||
|
||||
async def call(data, use_cache, index):
|
||||
response_dict = await query_model(
|
||||
self.model_api,
|
||||
self.file_sem,
|
||||
query_config_builder.with_experiment_name(
|
||||
f"{self.config.name}/{index:02d}-{name}"
|
||||
)
|
||||
.with_use_cache(use_cache)
|
||||
.with_data(data)
|
||||
.build(),
|
||||
)
|
||||
self.add_cost_data(team, response_dict)
|
||||
return response_dict
|
||||
|
||||
step = Task(name, call, use_cache, dependencies)
|
||||
self.steps.append(step)
|
||||
return step
|
||||
|
||||
def add_transformation_step(
|
||||
self,
|
||||
name,
|
||||
transformation_fn,
|
||||
dependencies=[],
|
||||
use_cache=None,
|
||||
strong_model=None,
|
||||
weak_model=None,
|
||||
read_cache=False,
|
||||
):
|
||||
if name in self.step_names:
|
||||
raise ValueError(f"Step name {name} already exists")
|
||||
self.step_names.add(name)
|
||||
|
||||
async def call(*args, use_cache, index):
|
||||
incoming_problem_ids = set().union(*[arg.keys() for arg in args])
|
||||
if use_cache and read_cache:
|
||||
logger.debug(
|
||||
f"Reading from cache for transformation: {self.config.name}/{index:02d}-{name}/{strong_model}{'+' if strong_model and weak_model else ''}{weak_model}"
|
||||
)
|
||||
data, cached_problem_ids = read_from_cache(
|
||||
f"{self.config.name}/{index:02d}-{name}/{strong_model}{'+' if strong_model and weak_model else ''}{weak_model}"
|
||||
)
|
||||
if incoming_problem_ids.issubset(set(cached_problem_ids)):
|
||||
return {k: v for k, v in data.items() if k in incoming_problem_ids}
|
||||
|
||||
if asyncio.iscoroutinefunction(transformation_fn):
|
||||
output = await transformation_fn(*args)
|
||||
else:
|
||||
output = transformation_fn(*args)
|
||||
|
||||
async with self.file_sem:
|
||||
save_to_cache(
|
||||
output,
|
||||
f"{self.config.name}/{index:02d}-{name}/{strong_model}{'+' if strong_model and weak_model else ''}{weak_model}",
|
||||
delete_existing=read_cache,
|
||||
incoming_problem_ids=incoming_problem_ids,
|
||||
)
|
||||
return output
|
||||
|
||||
step = Task(name, call, use_cache, dependencies)
|
||||
self.steps.append(step)
|
||||
return step
|
||||
|
||||
def add_eval_step(
|
||||
self,
|
||||
name,
|
||||
eval_fn,
|
||||
dependencies=[],
|
||||
strong_model=None,
|
||||
weak_model=None,
|
||||
):
|
||||
if name in self.step_names:
|
||||
raise ValueError(f"Step name {name} already exists")
|
||||
self.step_names.add(name)
|
||||
|
||||
async def call(*args, use_cache, index):
|
||||
output = eval_fn(*args)
|
||||
cache_obj = {"summary": output}
|
||||
async with self.file_sem:
|
||||
save_to_cache(
|
||||
cache_obj,
|
||||
f"{self.config.name}/{index:02d}-{name}/{strong_model}{'+' if strong_model and weak_model else ''}{weak_model}",
|
||||
)
|
||||
return output
|
||||
|
||||
step = Task(name, call, None, dependencies)
|
||||
self.steps.append(step)
|
||||
return step
|
||||
|
||||
def topological_sort_tasks(self, tasks):
|
||||
in_degree = {task: len(task.dependencies) for task in tasks}
|
||||
|
||||
queue = deque([task for task in tasks if in_degree[task] == 0])
|
||||
sorted_tasks = []
|
||||
task_order = {task: i for i, task in enumerate(tasks)}
|
||||
|
||||
while queue:
|
||||
task = queue.popleft()
|
||||
sorted_tasks.append(task)
|
||||
for dependent in task.dependents:
|
||||
in_degree[dependent] -= 1
|
||||
if in_degree[dependent] == 0:
|
||||
queue.append(dependent)
|
||||
queue = deque(sorted(queue, key=lambda t: task_order[t]))
|
||||
|
||||
for i, task in enumerate(sorted_tasks):
|
||||
task.index = i
|
||||
return sorted_tasks
|
||||
|
||||
def add_cost_data(self, team, response_dict):
|
||||
cost = sum(
|
||||
[response["response"]["cost"] for response in response_dict.values()]
|
||||
)
|
||||
if team is not None:
|
||||
if team not in self.cost:
|
||||
self.cost[team] = 0
|
||||
self.cost[team] += cost
|
||||
overall_team = team.split("_")[0]
|
||||
if overall_team != team:
|
||||
self.cost[overall_team] += cost
|
||||
|
||||
def set_use_cache(self, tasks):
|
||||
# This is called after tasks.sort, so we are guaranteed to process all
|
||||
# dependencies before each task itself.
|
||||
for task in tasks:
|
||||
if not self.config.use_cache:
|
||||
task.use_cache = False
|
||||
continue
|
||||
|
||||
if task.use_cache is None:
|
||||
task.use_cache = True
|
||||
|
||||
for dep in task.dependencies:
|
||||
if not dep.use_cache:
|
||||
task.use_cache = False
|
||||
|
||||
def speak(self, message):
|
||||
if self.config.play_sound:
|
||||
from IPython.display import Javascript, display
|
||||
|
||||
display(
|
||||
Javascript(
|
||||
f"""
|
||||
if(window.speechSynthesis) {{
|
||||
var synth = window.speechSynthesis;
|
||||
synth.speak(new window.SpeechSynthesisUtterance('{message}'));
|
||||
}}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
steps = self.topological_sort_tasks(self.steps)
|
||||
self.set_use_cache(steps)
|
||||
for task in steps:
|
||||
logger.info(
|
||||
f"Starting step {task.index}: {task.name} - Using cache: {task.use_cache}"
|
||||
)
|
||||
try:
|
||||
self.results[task.name] = await task.execute(self.results)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in step {task.index}: {task.name}")
|
||||
logger.error(e)
|
||||
self.speak("Pipeline failed sad face")
|
||||
raise e
|
||||
logger.info(f"Finished step {task.index}: {task.name}")
|
||||
self.speak("Jobs done")
|
||||
logger.info("Run complete!! Nice!! 🚀🚀")
|
||||
return self.results
|
||||
@@ -0,0 +1,191 @@
|
||||
__all__ = [
|
||||
"EvalConfig",
|
||||
"EvalConfigBuilder",
|
||||
"evaluate_solutions",
|
||||
"examine_solution",
|
||||
"print_eval",
|
||||
]
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from src.code_evaluation.test_results import Solution
|
||||
|
||||
import src.tools.path_utils as path_utils
|
||||
|
||||
DEFAULT_RESULTS_DIR = path_utils.get_default_results_directory()
|
||||
|
||||
|
||||
class EvalConfig:
|
||||
def __init__(
|
||||
self,
|
||||
experiment_name,
|
||||
model_to_test,
|
||||
executor_fn,
|
||||
language,
|
||||
use_cache=True,
|
||||
data=None,
|
||||
dataloader_fn=None,
|
||||
data_location=None,
|
||||
):
|
||||
self.experiment_name = experiment_name
|
||||
self.model_to_test = model_to_test
|
||||
if dataloader_fn is not None:
|
||||
self.data = dataloader_fn(data_location)
|
||||
elif isinstance(data, str):
|
||||
with open(json.load(data), "r") as f:
|
||||
self.data = json.load(f)
|
||||
else:
|
||||
self.data = data
|
||||
self.executor_fn = executor_fn
|
||||
self.language = language
|
||||
self.use_cache = use_cache
|
||||
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class EvalConfigBuilder:
|
||||
def __init__(self):
|
||||
self.experiment_name = None
|
||||
self.model_to_test = None
|
||||
self.executor_fn = None
|
||||
self.language = None
|
||||
self.data = None
|
||||
self.use_cache = None
|
||||
self.dataloader_fn = None
|
||||
self.data_location = None
|
||||
|
||||
def with_experiment_name(self, experiment_name):
|
||||
self.experiment_name = experiment_name
|
||||
return self
|
||||
|
||||
def with_executor_fn(self, executor_fn):
|
||||
self.executor_fn = executor_fn
|
||||
return self
|
||||
|
||||
def with_language(self, language):
|
||||
self.language = language
|
||||
return self
|
||||
|
||||
def with_model_to_test(self, model_to_test):
|
||||
self.model_to_test = model_to_test
|
||||
return self
|
||||
|
||||
def with_use_cache(self, use_cache):
|
||||
self.use_cache = use_cache
|
||||
return self
|
||||
|
||||
def with_data(self, data):
|
||||
self.data = data
|
||||
return self
|
||||
|
||||
def with_dataloader_fn(self, dataloader_fn):
|
||||
self.dataloader_fn = dataloader_fn
|
||||
return self
|
||||
|
||||
def with_data_location(self, data_location):
|
||||
self.data_location = data_location
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
assert self.experiment_name is not None, "Experiment name must be set"
|
||||
assert self.executor_fn is not None, "Executor function must be set"
|
||||
assert self.language is not None, "Language must be set"
|
||||
assert (self.data is not None) or (
|
||||
self.dataloader_fn is not None and self.data_location is not None
|
||||
), "Data must be set"
|
||||
return EvalConfig(
|
||||
self.experiment_name,
|
||||
self.model_to_test,
|
||||
self.executor_fn,
|
||||
self.language,
|
||||
self.use_cache,
|
||||
self.data,
|
||||
self.dataloader_fn,
|
||||
self.data_location,
|
||||
)
|
||||
|
||||
|
||||
def evaluate_solutions(eval_config):
|
||||
if isinstance(eval_config.data, list):
|
||||
eval_config.data = {
|
||||
index: response[0] for index, response in enumerate(eval_config.data)
|
||||
}
|
||||
|
||||
eval_data = []
|
||||
for problem_id, response in eval_config.data.items():
|
||||
if response == {}:
|
||||
item = Solution.no_solution(problem_id)
|
||||
else:
|
||||
item = Solution.from_response(problem_id, response, eval_config.language)
|
||||
eval_data.append(item)
|
||||
|
||||
results_dir = DEFAULT_RESULTS_DIR
|
||||
save_dir = results_dir / eval_config.experiment_name / eval_config.model_to_test
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
# load caching results
|
||||
if eval_config.use_cache and save_dir is not None:
|
||||
for idx, solution in enumerate(eval_data):
|
||||
if solution.correct is not None: # already have runtime eval results
|
||||
continue
|
||||
save_path = f"{save_dir}/{solution.question_id}.json"
|
||||
if os.path.exists(save_path):
|
||||
eval_data[idx] = Solution.from_cache_file(save_path)
|
||||
|
||||
executor_results = eval_config.executor_fn(eval_data)
|
||||
|
||||
for problem_id, problem_data in eval_config.data.items():
|
||||
if problem_id not in executor_results:
|
||||
executor_results[problem_id] = {}
|
||||
for field, value in problem_data.items():
|
||||
if field not in executor_results[problem_id]:
|
||||
executor_results[problem_id][field] = value
|
||||
|
||||
# save caching results
|
||||
if save_dir is not None:
|
||||
for question_id, solution in executor_results.items():
|
||||
save_path = f"{save_dir}/{question_id}.json"
|
||||
with open(save_path, "w") as f:
|
||||
json.dump(solution, f, indent=2)
|
||||
|
||||
return executor_results
|
||||
|
||||
|
||||
def examine_solution(solutions, index):
|
||||
print(
|
||||
f"Difficulty:\n{solutions[index][0]['metadata']['difficulty']}\n----------------------------"
|
||||
)
|
||||
print(solutions[index][0]["metadata"]["question"])
|
||||
print(solutions[index][0]["solution"])
|
||||
print("test cases:")
|
||||
for test in solutions[index][0]["metadata"]["test_cases"]:
|
||||
print(f"{test['input']}{test['output']}")
|
||||
|
||||
|
||||
def print_eval(results):
|
||||
correct_tests = 0
|
||||
total_tests = 0
|
||||
correct_problems = 0
|
||||
total_problems = len(results.keys())
|
||||
for problem_id, problem_result in results.items():
|
||||
correct_tests_local = sum(
|
||||
[
|
||||
1
|
||||
for test_result in problem_result["test_cases"]
|
||||
if test_result["correct"]
|
||||
]
|
||||
)
|
||||
total_tests_local = len(problem_result["test_cases"])
|
||||
# print(f"Problem ID: {problem_id}\nTest Results:\n\tCorrect: {correct_tests_local}\n\tTotal: {total_tests_local}\n\tAccuracy: {(correct_tests_local * 100.)/total_tests_local}\nOverall Correct: {problem_result.correct}")
|
||||
correct_tests += correct_tests_local
|
||||
total_tests += total_tests_local
|
||||
if problem_result["correct"]:
|
||||
correct_problems += 1
|
||||
print(
|
||||
f"Number of Problems: {total_problems}\nNumber Correct: {correct_problems}\nAccuracy: {(correct_problems * 100.)/total_problems}\nNumber of Tests: {total_tests}\nNumber Correct: {correct_tests}\nAccuracy: {(correct_tests * 100.)/total_tests}"
|
||||
)
|
||||
@@ -0,0 +1,276 @@
|
||||
__all__ = ["QueryConfig", "QueryConfigBuilder", "query_model"]
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import tiktoken
|
||||
|
||||
import src.tools.path_utils as path_utils
|
||||
|
||||
ROOT_DIR = path_utils.get_root_directory()
|
||||
DEFAULT_RESULTS_DIR = path_utils.get_default_results_directory()
|
||||
|
||||
|
||||
class QueryConfig:
|
||||
def __init__(
|
||||
self,
|
||||
experiment_name,
|
||||
model_to_test,
|
||||
dataloader_fn,
|
||||
data_location,
|
||||
data,
|
||||
prompt_fn,
|
||||
parse_fn=None,
|
||||
use_cache=False,
|
||||
num_problems=None,
|
||||
max_tokens=4096,
|
||||
results_dir=None,
|
||||
temperature=None,
|
||||
logprobs=None,
|
||||
bon=1,
|
||||
):
|
||||
assert isinstance(model_to_test, str)
|
||||
self.experiment_name = experiment_name
|
||||
self.model_to_test = model_to_test
|
||||
self.dataloader_fn = dataloader_fn
|
||||
self.data_location = data_location
|
||||
self.data = data
|
||||
self.prompt_fn = prompt_fn
|
||||
self.use_cache = use_cache
|
||||
self.parse_fn = parse_fn
|
||||
self.num_problems = num_problems
|
||||
self.max_tokens = max_tokens
|
||||
self.results_dir = results_dir
|
||||
self.temperature = temperature if temperature is not None else 0.0
|
||||
self.logprobs = logprobs
|
||||
self.bon = bon
|
||||
|
||||
def get_data(self):
|
||||
assert self.data is not None
|
||||
return self.data
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"QueryConfig("
|
||||
f"experiment_name={self.experiment_name}, "
|
||||
f"model_to_test={self.model_to_test}, "
|
||||
f"dataloader_fn={self.dataloader_fn}, "
|
||||
f"data_location={self.data_location}, "
|
||||
f"data={self.data}, "
|
||||
f"prompt_fn={self.prompt_fn}, "
|
||||
f"use_cache={self.use_cache}, "
|
||||
f"num_problems={self.num_problems}, "
|
||||
f"max_tokens={self.max_tokens}, "
|
||||
f"results_dir={self.results_dir}, "
|
||||
f"temperature={self.temperature}, "
|
||||
f"logprobs={self.logprobs}"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class QueryConfigBuilder:
|
||||
def __init__(self):
|
||||
self.experiment_name = None
|
||||
self.model_to_test = None
|
||||
self.dataloader_fn = None
|
||||
self.data_location = None
|
||||
self.data = None
|
||||
self.prompt_fn = None
|
||||
self.parse_fn = None
|
||||
self.use_cache = False
|
||||
self.num_problems = None
|
||||
self.max_tokens = 4096
|
||||
self.results_dir = None
|
||||
self.temperature = 0.0
|
||||
self.logprobs = None
|
||||
self.bon = 1
|
||||
|
||||
def with_experiment_name(self, experiment_name):
|
||||
self.experiment_name = experiment_name
|
||||
return self
|
||||
|
||||
def with_bon(self, bon):
|
||||
self.bon = bon
|
||||
return self
|
||||
|
||||
def with_model_to_test(self, model_to_test):
|
||||
assert isinstance(model_to_test, str)
|
||||
self.model_to_test = model_to_test
|
||||
return self
|
||||
|
||||
def with_dataloader_fn(self, dataloader_fn):
|
||||
self.dataloader_fn = dataloader_fn
|
||||
return self
|
||||
|
||||
def with_data_location(self, data_location):
|
||||
self.data_location = data_location
|
||||
return self
|
||||
|
||||
def with_data(self, data):
|
||||
self.data = data
|
||||
return self
|
||||
|
||||
def with_prompt_fn(self, prompt_fn):
|
||||
self.prompt_fn = prompt_fn
|
||||
return self
|
||||
|
||||
def with_parse_fn(self, parse_fn):
|
||||
self.parse_fn = parse_fn
|
||||
return self
|
||||
|
||||
def with_use_cache(self, use_cache):
|
||||
self.use_cache = use_cache
|
||||
return self
|
||||
|
||||
def with_num_problems(self, num_problems):
|
||||
self.num_problems = num_problems
|
||||
return self
|
||||
|
||||
def with_max_tokens(self, max_tokens):
|
||||
self.max_tokens = max_tokens
|
||||
return self
|
||||
|
||||
def with_results_dir(self, results_dir):
|
||||
self.results_dir = results_dir
|
||||
return self
|
||||
|
||||
def with_temperature(self, temperature):
|
||||
self.temperature = temperature
|
||||
return self
|
||||
|
||||
def with_logprobs(self, logprobs):
|
||||
self.logprobs = logprobs
|
||||
if logprobs is not None:
|
||||
assert "claude" not in self.model_to_test
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
assert self.experiment_name is not None, "Experiment name must be set"
|
||||
assert self.model_to_test is not None, "Model to test must be set"
|
||||
assert self.prompt_fn is not None, "Prompt function must be set"
|
||||
assert (self.data is not None) or (
|
||||
self.dataloader_fn is not None and self.data_location is not None
|
||||
), "Data must be set"
|
||||
assert (self.data is None) or (
|
||||
self.dataloader_fn is None and self.data_location is None
|
||||
), "Data and dataloader_fn/data_location cannot both be set"
|
||||
return QueryConfig(
|
||||
self.experiment_name,
|
||||
self.model_to_test,
|
||||
self.dataloader_fn,
|
||||
self.data_location,
|
||||
self.data,
|
||||
self.prompt_fn,
|
||||
self.parse_fn,
|
||||
self.use_cache,
|
||||
self.num_problems,
|
||||
self.max_tokens,
|
||||
self.results_dir,
|
||||
self.temperature,
|
||||
self.logprobs,
|
||||
self.bon,
|
||||
)
|
||||
|
||||
|
||||
def _get_prompts(problems, prompt_fn):
|
||||
prompts = {}
|
||||
for problem_id, problem in problems.items():
|
||||
prompt = prompt_fn(problem)
|
||||
if prompt.text:
|
||||
prompts[problem_id] = prompt
|
||||
return prompts
|
||||
|
||||
|
||||
def get_save_dir(query_config):
|
||||
results_dir = (
|
||||
ROOT_DIR / query_config.results_dir
|
||||
if query_config.results_dir is not None
|
||||
else DEFAULT_RESULTS_DIR
|
||||
)
|
||||
save_dir = results_dir / query_config.experiment_name / query_config.model_to_test
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
return save_dir
|
||||
|
||||
|
||||
def move_data_into_metadata(data):
|
||||
for data_id, value in data.items():
|
||||
filtered_val = {
|
||||
k: v
|
||||
for k, v in value.items()
|
||||
if k not in ("metadata", "prompt", "response")
|
||||
}
|
||||
metadata = value.get("metadata", {})
|
||||
new_metadata = metadata | filtered_val
|
||||
data[data_id]["metadata"] = new_metadata
|
||||
|
||||
|
||||
def format_response(data, model_responses_map, query_config):
|
||||
model_responses_flattened = {}
|
||||
for data_id, response in model_responses_map.items():
|
||||
if query_config.bon == 1:
|
||||
model_responses_flattened[f"{data_id}"] = data[data_id] | response[0]
|
||||
continue
|
||||
for resp_id, resp in enumerate(response):
|
||||
model_responses_flattened[f"{data_id}-{resp_id}"] = data[data_id] | resp
|
||||
return model_responses_flattened
|
||||
|
||||
|
||||
def tokenize_logit_bias(logit_bias, model):
|
||||
tokenizer = tiktoken.encoding_for_model(model)
|
||||
tokenized_bias = {}
|
||||
for k, v in logit_bias.items():
|
||||
tokenized = tokenizer.encode(k)
|
||||
assert len(tokenized) == 1, f"Tokenized bias key {k} is not a single token"
|
||||
tokenized_bias[tokenized[0]] = v
|
||||
return tokenized_bias
|
||||
|
||||
|
||||
async def query_model(model_api, file_sem, query_config):
|
||||
data = query_config.get_data()
|
||||
prompts = _get_prompts(data, query_config.prompt_fn)
|
||||
save_dir = get_save_dir(query_config)
|
||||
|
||||
move_data_into_metadata(data)
|
||||
|
||||
model_requests = [
|
||||
model_api(
|
||||
query_config.model_to_test,
|
||||
prompts[data_id].text,
|
||||
max_tokens=query_config.max_tokens,
|
||||
temperature=query_config.temperature,
|
||||
n=query_config.bon,
|
||||
top_p=1.0,
|
||||
logprobs=query_config.logprobs,
|
||||
use_cache=query_config.use_cache,
|
||||
metadata=data[data_id]["metadata"],
|
||||
parse_fn=query_config.parse_fn,
|
||||
save_path=f"{save_dir}/{data_id}.json",
|
||||
file_sem=file_sem,
|
||||
**(
|
||||
{
|
||||
"logit_bias": tokenize_logit_bias(
|
||||
prompts[data_id].logit_bias, query_config.model_to_test
|
||||
)
|
||||
}
|
||||
if prompts[data_id].logit_bias is not None
|
||||
else {}
|
||||
),
|
||||
)
|
||||
for data_id in prompts.keys()
|
||||
]
|
||||
|
||||
model_responses = await asyncio.gather(*model_requests)
|
||||
|
||||
# pass through data that wasn't modified by the request
|
||||
model_responses_map = {
|
||||
data_id: response for data_id, response in zip(prompts.keys(), model_responses)
|
||||
}
|
||||
for key in data.keys():
|
||||
if key not in prompts:
|
||||
model_responses_map[key] = [data[key]]
|
||||
|
||||
response = format_response(data, model_responses_map, query_config)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,264 @@
|
||||
__all__ = ["load_prompts", "load_problems", "load_problems_from_json", "load_solutions"]
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .path_utils import get_default_results_directory, get_root_directory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT_DIR = get_root_directory()
|
||||
DATA_DIR = ROOT_DIR / "data" / "APPS"
|
||||
PROMPTS_DIR = ROOT_DIR / "src" / "prompts"
|
||||
|
||||
|
||||
def get_data_dir():
|
||||
return DATA_DIR
|
||||
|
||||
|
||||
def load_prompts(prompt_type):
|
||||
files = [f for f in (PROMPTS_DIR / prompt_type.value).glob("*") if f.is_file()]
|
||||
prompts = {}
|
||||
for file in files:
|
||||
with file.open("r") as f:
|
||||
prompts[file.name] = f.read()
|
||||
return prompts
|
||||
|
||||
|
||||
def load_problem_subset(subset, require_solutions=False, problem_ids=None):
|
||||
def load_problems(dir, num_problems=None):
|
||||
if problem_ids:
|
||||
problem_dirs = problem_ids
|
||||
else:
|
||||
problem_dirs = os.listdir(dir)
|
||||
problems = {}
|
||||
added = 0
|
||||
for problem_dir in problem_dirs:
|
||||
problem_path = dir / problem_dir
|
||||
problem = {}
|
||||
with (problem_path / "metadata.json").open("r") as f:
|
||||
problem["metadata"] = json.load(f)
|
||||
if (
|
||||
subset != "ALL"
|
||||
and problem["metadata"]["difficulty"].lower() != subset.lower()
|
||||
):
|
||||
continue
|
||||
|
||||
if require_solutions and not (problem_path / "solutions.json").exists():
|
||||
logger.debug(
|
||||
f"Skipping problem {problem_dir} because it does not have solutions"
|
||||
)
|
||||
continue
|
||||
|
||||
with (problem_path / "question.txt").open("r") as f:
|
||||
problem["question"] = f.read()
|
||||
|
||||
problem["uid"] = problem_dir
|
||||
problems[problem_dir] = problem
|
||||
added += 1
|
||||
if added >= num_problems:
|
||||
break
|
||||
|
||||
return problems
|
||||
|
||||
return load_problems
|
||||
|
||||
|
||||
def load_problems(dir, num_problems=None):
|
||||
return load_problem_subset("ALL")(dir, num_problems)
|
||||
|
||||
|
||||
def load_problems_from_json(path, num_problems=None, problem_ids=None):
|
||||
problems = {}
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
print('read data error: ', e)
|
||||
data = path
|
||||
if num_problems is not None:
|
||||
data = data[:num_problems]
|
||||
|
||||
for i, item in enumerate(data):
|
||||
item["uid"] = i
|
||||
if 'vanilla_label' not in item:
|
||||
item["vanilla_label"] = item["label"]
|
||||
problems[f"{i}"] = item
|
||||
return problems
|
||||
|
||||
|
||||
def load_problems_from_json_ids(path, num_problems=None, problem_ids=None):
|
||||
problems = {}
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
except:
|
||||
data = path
|
||||
|
||||
if problem_ids is not None:
|
||||
data = [data[i] for i in problem_ids]
|
||||
|
||||
for i, item in enumerate(data):
|
||||
item["uid"] = i
|
||||
if 'vanilla_label' not in item:
|
||||
item["vanilla_label"] = item["label"]
|
||||
# acc.append(item['label'] == item['vanilla_label'])
|
||||
problems[f"{i}"] = item
|
||||
|
||||
return problems
|
||||
|
||||
|
||||
def load_assignments(path, num_problems=None, problem_ids=None):
|
||||
return path
|
||||
|
||||
|
||||
def load_solutions(dir, num_problems=None):
|
||||
solutions = {}
|
||||
files = dir.glob("*")
|
||||
files = [f for f in files if f.name != "incoming_problem_ids.json"]
|
||||
if num_problems is not None:
|
||||
files = list(files)[:num_problems]
|
||||
|
||||
for file in files:
|
||||
with file.open("r") as f:
|
||||
solution = json.load(f)
|
||||
# Assume 1 solution per file
|
||||
if isinstance(solution, list):
|
||||
solutions[file.stem] = solution[0]
|
||||
else:
|
||||
solutions[file.stem] = solution
|
||||
return solutions
|
||||
|
||||
|
||||
def load_multiple_solutions(dir, num_problems=None, problem_ids=None):
|
||||
solutions = {}
|
||||
files = dir.glob("*")
|
||||
files = [f for f in files if f.name != "incoming_problem_ids.json"]
|
||||
|
||||
for file in files:
|
||||
with file.open("r") as f:
|
||||
solution = json.load(f)
|
||||
if "metadata" in solution:
|
||||
solution.pop("metadata")
|
||||
if "demonstration" in solution:
|
||||
solution.pop("demonstration")
|
||||
solutions[file.stem] = solution
|
||||
return solutions
|
||||
|
||||
def load_multiple_solutions_w2s(dir, num_problems=None, problem_ids=None):
|
||||
solutions = {}
|
||||
files = dir.glob("*")
|
||||
files = [f for f in files if f.name != "incoming_problem_ids.json"]
|
||||
|
||||
for file in files:
|
||||
with file.open("r") as f:
|
||||
solution = json.load(f)
|
||||
metadata = solution[0]['metadata']
|
||||
if "demonstration" in metadata:
|
||||
metadata.pop("demonstration")
|
||||
metadata['label'] = solution[0]['score'] > 0
|
||||
solutions[file.stem] = metadata
|
||||
return solutions
|
||||
|
||||
|
||||
def save_to_cache(data, name, delete_existing=False, incoming_problem_ids=None):
|
||||
dir = get_default_results_directory() / name
|
||||
|
||||
# Delete all files in the directory first
|
||||
if delete_existing and os.path.exists(dir):
|
||||
for file in os.listdir(dir):
|
||||
file_path = os.path.join(dir, file)
|
||||
if os.path.isfile(file_path):
|
||||
os.unlink(file_path)
|
||||
|
||||
os.makedirs(dir, exist_ok=True)
|
||||
for k, v in data.items():
|
||||
if isinstance(v, list):
|
||||
to_write = [
|
||||
{
|
||||
key: value
|
||||
for key, value in item.items()
|
||||
if key not in ["prompt", "response"]
|
||||
}
|
||||
for item in v
|
||||
]
|
||||
else:
|
||||
to_write = {
|
||||
key: value
|
||||
for key, value in v.items()
|
||||
if key not in ["prompt", "response"]
|
||||
}
|
||||
with open(dir / f"{k}.json", "w") as f:
|
||||
json.dump(to_write, f, indent=4)
|
||||
|
||||
if incoming_problem_ids:
|
||||
with open(dir / "incoming_problem_ids.json", "w") as f:
|
||||
json.dump({"problem_ids": list(incoming_problem_ids)}, f, indent=4)
|
||||
|
||||
|
||||
def read_from_cache(name):
|
||||
dir = get_default_results_directory() / name
|
||||
data = {}
|
||||
incoming_problem_ids = []
|
||||
|
||||
for file in dir.glob("*.json"):
|
||||
if file.name == "incoming_problem_ids.json":
|
||||
with file.open("r") as f:
|
||||
incoming_problem_ids = json.load(f).get("problem_ids", [])
|
||||
else:
|
||||
with file.open("r") as f:
|
||||
value = json.load(f)
|
||||
if not value.get("metadata"):
|
||||
value["metadata"] = {k: v for k, v in value.items()}
|
||||
data[file.stem] = value
|
||||
|
||||
return data, incoming_problem_ids
|
||||
|
||||
|
||||
def load_ground_truth_solutions(problem_ids):
|
||||
output = {}
|
||||
for problem_id in problem_ids:
|
||||
with open(get_data_dir() / "test" / problem_id / "solutions.json", "r") as f:
|
||||
solutions = json.load(f)
|
||||
cleaned_solutions = []
|
||||
for solution in solutions:
|
||||
# Remove unwanted lines from the solution
|
||||
cleaned_solution = []
|
||||
for line in solution.split("\n"):
|
||||
if (
|
||||
not line.strip().startswith("#!")
|
||||
and " input=" not in line
|
||||
and "sys.stdin" not in line
|
||||
):
|
||||
cleaned_solution.append(line)
|
||||
cleaned_solutions.append("\n".join(cleaned_solution).strip())
|
||||
output[problem_id] = cleaned_solutions
|
||||
return output
|
||||
|
||||
|
||||
def load_test_case(problem_id):
|
||||
problem_id = problem_id.split("-")[0]
|
||||
with open(get_data_dir() / "test" / problem_id / "input_output.json", "r") as f:
|
||||
data = json.load(f)
|
||||
return [
|
||||
{"input": i, "output": o} for (i, o) in zip(data["inputs"], data["outputs"])
|
||||
]
|
||||
|
||||
|
||||
def load_test_cases(problem_ids):
|
||||
output = {}
|
||||
for problem_id in problem_ids:
|
||||
output[problem_id] = load_test_case(problem_id)
|
||||
return output
|
||||
|
||||
|
||||
loaded_test_cases = {}
|
||||
|
||||
|
||||
def get_test_cases_for_single_problem(problem_id):
|
||||
global loaded_test_cases
|
||||
if problem_id not in loaded_test_cases:
|
||||
loaded_test_cases[problem_id] = load_test_case(problem_id)
|
||||
|
||||
return loaded_test_cases[problem_id]
|
||||
@@ -0,0 +1,11 @@
|
||||
__all__ = ["get_root_directory", "get_default_results_directory"]
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_root_directory():
|
||||
return Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
def get_default_results_directory():
|
||||
return get_root_directory() / "results"
|
||||
@@ -0,0 +1,134 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from anytree import Node, RenderTree
|
||||
from anytree.exporter import DotExporter
|
||||
|
||||
from src.tools.path_utils import get_default_results_directory
|
||||
|
||||
|
||||
def print_experiment_log(experiment_name, strong_model, weak_model, problem_number):
|
||||
results_dir = get_default_results_directory()
|
||||
experiment_dir = results_dir / experiment_name
|
||||
|
||||
# Get all step directories
|
||||
step_dirs = [d for d in experiment_dir.iterdir() if d.is_dir()]
|
||||
|
||||
# Sort step directories by step number and exclude "merged_results"
|
||||
step_dirs = [d for d in step_dirs if d.name != "merged_results"]
|
||||
step_dirs.sort(key=lambda x: int(x.name.split("-")[0]))
|
||||
|
||||
for step_dir in step_dirs:
|
||||
# Check for both strong and weak model directories
|
||||
for model in [strong_model, weak_model, f"{strong_model}+{weak_model}"]:
|
||||
model_dir = step_dir / model
|
||||
if not model_dir.exists():
|
||||
continue
|
||||
|
||||
ignore_keys = ["metadata", "prompt", "response"]
|
||||
if model == f"{strong_model}+{weak_model}":
|
||||
ignore_keys.extend(["question", "test_cases", "uid"])
|
||||
|
||||
# Find matching problem files
|
||||
problem_files = list(model_dir.glob(f"{problem_number}*.json"))
|
||||
|
||||
for problem_file in problem_files:
|
||||
with open(problem_file, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"Step: {step_dir.name}")
|
||||
print(f"Model: {model}")
|
||||
print(f"Problem: {problem_file.stem}")
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
|
||||
print("\nPrompt:")
|
||||
prompt_array = data[0].get("prompt")
|
||||
if prompt_array is None:
|
||||
print("No prompt available")
|
||||
else:
|
||||
for text in prompt_array:
|
||||
print(f"Role: {text['role']}")
|
||||
print(f"Content: {text['content']}")
|
||||
|
||||
for response in data:
|
||||
print("\nResponse:")
|
||||
print(
|
||||
response.get("response", {}).get(
|
||||
"completion", "No response available"
|
||||
)
|
||||
)
|
||||
print("\nOther Fields:")
|
||||
for key, value in response.items():
|
||||
if key not in ignore_keys:
|
||||
print(f"{key}: {value}")
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
|
||||
|
||||
def show_pipeline_graph(pipeline):
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
|
||||
# Create a directed graph
|
||||
G = nx.DiGraph()
|
||||
|
||||
# Add nodes and edges
|
||||
for task in pipeline.steps:
|
||||
G.add_node(task.name)
|
||||
for dep in task.dependencies:
|
||||
G.add_edge(dep.name, task.name)
|
||||
|
||||
# Print the graph structure
|
||||
print("Pipeline Dependency Graph:")
|
||||
for node in nx.topological_sort(G):
|
||||
predecessors = list(G.predecessors(node))
|
||||
successors = list(G.successors(node))
|
||||
print(f"{node}:")
|
||||
if predecessors:
|
||||
print(f" Parents: {', '.join(predecessors)}")
|
||||
if successors:
|
||||
print(f" Children: {', '.join(successors)}")
|
||||
|
||||
# Generate a DOT file for visualization
|
||||
output_dir = get_default_results_directory() / pipeline.config.name
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
dot_file = output_dir / "pipeline_graph.dot"
|
||||
png_file = output_dir / "pipeline_graph.png"
|
||||
|
||||
nx.drawing.nx_pydot.write_dot(G, str(dot_file))
|
||||
print(f"DOT file generated at: {dot_file}")
|
||||
|
||||
# Generate PNG file using Graphviz
|
||||
try:
|
||||
subprocess.run(["dot", "-Tpng", str(dot_file), "-o", str(png_file)], check=True)
|
||||
print(f"PNG file generated at: {png_file}")
|
||||
except subprocess.CalledProcessError:
|
||||
print(
|
||||
"Error: Failed to generate PNG. Make sure Graphviz is installed and accessible in your PATH."
|
||||
)
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"Error: Graphviz not found. Please install Graphviz to generate PNG files."
|
||||
)
|
||||
|
||||
# Optionally, you can also use matplotlib to visualize the graph
|
||||
plt.figure(figsize=(12, 8))
|
||||
pos = nx.spring_layout(G)
|
||||
nx.draw(
|
||||
G,
|
||||
pos,
|
||||
with_labels=True,
|
||||
node_color="lightblue",
|
||||
node_size=2000,
|
||||
font_size=8,
|
||||
arrows=True,
|
||||
)
|
||||
plt.title("Pipeline Dependency Graph")
|
||||
plt.axis("off")
|
||||
plt.tight_layout()
|
||||
plt.savefig(str(output_dir / "pipeline_graph_matplotlib.png"))
|
||||
print(
|
||||
f"Matplotlib graph generated at: {output_dir / 'pipeline_graph_matplotlib.png'}"
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
import re
|
||||
|
||||
|
||||
def format_key_suffix(key_suffix):
|
||||
if key_suffix:
|
||||
if key_suffix[0] != "_":
|
||||
key_suffix = f"_{key_suffix}"
|
||||
else:
|
||||
key_suffix = ""
|
||||
return key_suffix
|
||||
|
||||
|
||||
COMMENTS_REGEX = r"""
|
||||
^ # Begin of line.
|
||||
(?:
|
||||
# A) Capturing group n°1: Full-line comment followed by empty lines.
|
||||
(
|
||||
[ \t]* # Optional spaces or tabs.
|
||||
\#[^\r\n]*\r?\n # The comment and the new line.
|
||||
(?:[ \t]*\r?\n)* # Optional empty lines (perhaps with spaces/tabs).
|
||||
)
|
||||
|
|
||||
# B) Statement and optional comment at the end.
|
||||
(?:
|
||||
( # Capturing group n°2 : The statement
|
||||
(?:
|
||||
# Multi-line strings with \"\"\" or '''.
|
||||
# Capturing group n°3 : The triple quotes.
|
||||
(['\"]{3})[\s\S]*?\3
|
||||
|
|
||||
# Double-quoted string "It's ok".
|
||||
\"(?: \\. | [^\"] )*\"
|
||||
|
|
||||
# Single-quoted string 'I\'ll say "Hello!"'.
|
||||
'(?: \\. | [^'] )*'
|
||||
|
|
||||
# Any chars, except spaces, hashtag, quotes and new lines.
|
||||
[^ \t#\"'\r\n]+
|
||||
|
|
||||
# Horizontal spaces, but not followed by a comment, because
|
||||
# we want the spaces in front of the comment to be matched
|
||||
# together with the optional comment we want to get rid of.
|
||||
[ \t]+(?![ \t]*\#)
|
||||
)+
|
||||
)
|
||||
# Capturing group n°4: An optional comment at the end of a statement.
|
||||
(
|
||||
[ \t]*\#[^\r\n]*
|
||||
)?
|
||||
)+
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def strip_comments_from_string(string):
|
||||
return re.sub(
|
||||
COMMENTS_REGEX,
|
||||
"\\2",
|
||||
string,
|
||||
0,
|
||||
re.MULTILINE | re.VERBOSE | re.UNICODE,
|
||||
)
|
||||
Reference in New Issue
Block a user