mirror of
https://github.com/wassname/Unsupervised-Elicitation.git
synced 2026-08-20 12:10:10 +08:00
fix inconsistents
fix inconsistents as combinatorial with no llm calls
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
.env
|
||||
.anycache/
|
||||
docs/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
This is a simplified fork of unsupervised-elicitation which is a paper that uses in-context metalearning, requiring it to be consistent and mutually predictive. We are using two things here
|
||||
|
||||
Implicit:
|
||||
- in-context metalearning e.g. "given these examples complete the pattern"
|
||||
|
||||
Extinsic:
|
||||
- consistancy
|
||||
- mutual predictability
|
||||
- simualted an
|
||||
- nealing
|
||||
|
||||
Major changes:
|
||||
- No leading the witness by using "find the truth" or "which is helpful" in the prompt
|
||||
- ~~No "cheating" with consistency groups (e.g. one of these must be true)~~ hard to make it work without it
|
||||
|
||||
Fork to
|
||||
- [x] refactor to UV
|
||||
- [x] use openrouter logprob moddels
|
||||
- [x] and simplify
|
||||
- [x] replicate 
|
||||
- [ ] remove bias from the prompts "find truth" "which is helpfull" is too leading for unsupervised IMO
|
||||
- [x] remove bias from the prompts "find truth" "which is helpfull" is too leading for unsupervised IMO
|
||||
- [ ] add moral datasets e.g. daily dilemmas, ETHICS, Machiavelli, moral foundations vignettes
|
||||
|
||||
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
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)
|
||||
@@ -9,6 +9,7 @@ authors = [
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"adjusttext>=1.3.0",
|
||||
"aiocache>=0.12.3",
|
||||
"alembic>=1.16.5",
|
||||
"altair>=5.5.0",
|
||||
"anthropic>=0.69.0",
|
||||
@@ -51,6 +52,10 @@ unsupervised-elicitation = "unsupervised_elicitation:main"
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."] # search the root directory
|
||||
include = ["src*"]
|
||||
|
||||
[tool.uv.sources]
|
||||
openrouter-wrapper = { path = "../llm_moral_lb_v2/openrouter_wrapper", editable = true }
|
||||
|
||||
|
||||
@@ -13,16 +13,17 @@ import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
import math
|
||||
import numpy as np
|
||||
from anycache import anycache
|
||||
from functools import lru_cache
|
||||
import os, sys
|
||||
import pandas as pd
|
||||
from dataclasses import dataclass, asdict
|
||||
import dotenv
|
||||
from loguru import logger
|
||||
from openrouter_wrapper.logprobs import openrouter_completion_wlogprobs, get_logprobs_choices, LogprobsNotSupportedError # User's wrapper
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Callable
|
||||
import asyncio
|
||||
from aiocache import cached
|
||||
from itertools import combinations
|
||||
from copy import deepcopy
|
||||
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
@@ -40,6 +41,9 @@ dotenv.load_dotenv()
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, format="<green>{time:YYYY-MM-DD HH:mm}</green> | <level>{level}</level> | <cyan>{message}</cyan>", colorize=True, level="DEBUG")
|
||||
|
||||
# Global cost tracker
|
||||
total_cost = 0.0
|
||||
|
||||
# %% [code]
|
||||
|
||||
@dataclass
|
||||
@@ -50,8 +54,8 @@ class Config:
|
||||
decay_rate: float = 0.99
|
||||
beta: float = 2.0
|
||||
num_seed: int = 8
|
||||
max_iters: int = 950 # Small for demo; increase for more
|
||||
n_shots: int = 8 # Number of in-context examples
|
||||
max_iters: int = 2500000 # should be at least dataset size X 2
|
||||
n_shots: int = 6 # Number of in-context examples
|
||||
model_id: str = "meta-llama/llama-3.1-8b-instruct" # Logprobs supported
|
||||
provider_whitelist: Tuple[str] = None # None to let OpenRouter choose
|
||||
out_dir: Path = Path("../outputs/icm") # Directory to save outputs
|
||||
@@ -74,40 +78,21 @@ C.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
# )
|
||||
|
||||
logger.info(f"Config: {C}")
|
||||
|
||||
config_dict = asdict(C)
|
||||
config_dict['out_dir'] = str(config_dict['out_dir'])
|
||||
with open(C.out_dir / "icm_config.json", "w") as f:
|
||||
json.dump(config_dict, f, indent=2)
|
||||
|
||||
# %% [code]
|
||||
from datasets import load_dataset
|
||||
from src.data.truthfulqa import load_truthfulqa, is_consistent
|
||||
|
||||
# Load larger HF dataset: Yik/truthfulQA-bool
|
||||
dataset = load_dataset("wassname/truthful_qa_v2", split="validation", name="binary")
|
||||
# semantic_anchor = "0 is false, 1 is true."
|
||||
data = []
|
||||
group_id = 0
|
||||
groups = {}
|
||||
for idx, item in enumerate(dataset):
|
||||
claim = f"Question: {item['Question']}\nClaim: {item['Answer']}"
|
||||
label = item['label']
|
||||
consistency_id = item['question_id']
|
||||
|
||||
|
||||
example = {
|
||||
'uid': idx,
|
||||
'prompt': claim,
|
||||
'vanilla_label': label,
|
||||
'consistency_id': consistency_id,
|
||||
'consistency_key': 'A' if label == 1 else 'B'
|
||||
}
|
||||
data.append(example)
|
||||
|
||||
# HACK: Limit to small batch for demo
|
||||
# data = data[:512]
|
||||
logger.info(f"Loaded {len(data)} examples from TruthfulQA-bool")
|
||||
data = load_truthfulqa()
|
||||
logger.info("Loaded {} examples", len(data))
|
||||
|
||||
# %% [code]
|
||||
# Initialize: Random labels for first num_seed, None for others
|
||||
def initialize_data(data, config):
|
||||
demonstrations = {item['uid']: item.copy() for item in data}
|
||||
demonstrations = {item['uid']: deepcopy(item) for item in data}
|
||||
labeled_uids = random.sample(list(demonstrations.keys()), min(config.num_seed, len(data)))
|
||||
for uid in demonstrations:
|
||||
demonstrations[uid]['label'] = None
|
||||
@@ -125,35 +110,40 @@ logger.info("Initialized labels: {}", {k: v['label'] for k, v in demonstrations.
|
||||
def print_messages(messages):
|
||||
return "\n".join([f"**{m['role'].upper()}**: {m['content']}" for m in messages])
|
||||
|
||||
@cached()
|
||||
async def cached_openrouter_completion_wlogprobs(*args, **kwargs):
|
||||
# TODO: Async caching (e.g., with aiocache)
|
||||
return await openrouter_completion_wlogprobs(*args, **kwargs)
|
||||
|
||||
|
||||
async def predict_label(example_uid, current_demos, config=C, verbose=False):
|
||||
async def predict_label(example_uid, current_demos, config=C, verbose=False, all_demos=None):
|
||||
# simplified from https://github.com/Jiaxin-Wen/Unsupervised-Elicitation/blob/master/src/experiments/ICM.py#L254
|
||||
# Sort by consistency_key for relevance, limit to 16
|
||||
relevant_demos = sorted(
|
||||
[demo for uid, demo in current_demos.items() if uid != example_uid and demo['label'] is not None],
|
||||
key=lambda x: x.get('consistency_key', '')
|
||||
)[:C.n_shots]
|
||||
# TODO: Use all/subsampled demos like original (group/sort by consistency_id for relevance), handling context limits (high value, med complexity).
|
||||
global total_cost
|
||||
# Group and interleave demos by consistency_id like original
|
||||
grouped_demos = {}
|
||||
for uid, demo in current_demos.items():
|
||||
if uid != example_uid and demo['label'] is not None:
|
||||
grouped_demos.setdefault(demo['consistency_id'], []).append(demo)
|
||||
|
||||
# FIXME: some LLM's have a positional bias, so we should randomize order
|
||||
relevant_demos = []
|
||||
for group in grouped_demos.values():
|
||||
random.shuffle(group) # Reduce order bias
|
||||
relevant_demos.extend(group[:2]) # Limit per group for balance
|
||||
|
||||
instruction = "Carefully examine the noisy example pairs of (Candidate, Set). Then as consistently and coherently as possible complete the pattern, you may provide reasoning after.\n\n"
|
||||
# if verbose>1:
|
||||
# instruction = "Examine the examples below and determine the category 0 or 1 for the final claim based on the pattern. After 0 or 1 output a single word to describe how category 1 compares to category 0.\n\n"
|
||||
relevant_demos = relevant_demos[:C.n_shots] # Cap total
|
||||
|
||||
instruction = "Examine these examples and complete the pattern for the new one.\n\n"
|
||||
fewshot = []
|
||||
# FIXME to not lead the unsupervised model, we should avoid true/false or even 0/1 and try to use neutral labels like A/B or similar
|
||||
for idx, demo in enumerate(relevant_demos):
|
||||
for demo in relevant_demos:
|
||||
label_str = "A" if demo['label'] == 1 else "B"
|
||||
fewshot.append(f"\nCandidate: {demo['prompt']}\nSet: {label_str}\n")
|
||||
fewshot.append(f"\n\n## Candidate:\n{demo['prompt']}\n## Set: {label_str}")
|
||||
|
||||
target_prompt = demonstrations[example_uid]['prompt']
|
||||
# Use all_demos if provided (for unlabeled examples), otherwise use current_demos
|
||||
demos_source = all_demos if all_demos is not None else current_demos
|
||||
target_prompt = demos_source[example_uid]['prompt']
|
||||
messages = [
|
||||
{"role": "user", "content": instruction+"".join(fewshot)+f"Candidate: {target_prompt}\n"},
|
||||
{"role": "assistant", "content": "\n\nSet:"} # Assistant prefill to ensure
|
||||
{"role": "user", "content": instruction+"".join(fewshot)+f"\n\n## Candidate:\n{target_prompt}"},
|
||||
{"role": "assistant", "content": "\n## Set:"} # Assistant prefill to ensure
|
||||
]
|
||||
|
||||
|
||||
@@ -161,11 +151,13 @@ async def predict_label(example_uid, current_demos, config=C, verbose=False):
|
||||
model_id=config.model_id,
|
||||
provider_whitelist=config.provider_whitelist,
|
||||
messages=messages,
|
||||
max_completion_tokens=90 if verbose else 5,
|
||||
max_completion_tokens=160 if verbose else 5,
|
||||
temperature=0.4,
|
||||
top_logprobs=8,
|
||||
)
|
||||
|
||||
total_cost += response.get('usage', {}).get('cost', 0.0)
|
||||
|
||||
if verbose:
|
||||
logger.info(f"Debug Prediction - UID {example_uid}:")
|
||||
logger.info(f"messages: {print_messages(messages)}")
|
||||
@@ -229,7 +221,7 @@ def compute_energy(demos, config=C):
|
||||
if len(set(all_labels)) < len(key_groups): # Not all oppose if num unique labels < num keys
|
||||
num_inconsistent += max(0, len(items) - len(set(all_labels)))
|
||||
|
||||
energy = config.alpha * avg_lprob - num_inconsistent
|
||||
energy = config.alpha * avg_lprob - num_inconsistent - (num_inconsistent / max(1, len(labeled))) # Normalized penalty
|
||||
accuracy = np.mean([d['label'] == d['vanilla_label'] for d in labeled])
|
||||
return energy, {
|
||||
'avg_lprob': avg_lprob,
|
||||
@@ -241,60 +233,77 @@ def compute_energy(demos, config=C):
|
||||
logger.info("Initial energy: {}", compute_energy(demonstrations))
|
||||
|
||||
# %% [code]
|
||||
async def fix_inconsistencies_simple(demos, config=C, max_fixes=5):
|
||||
"""Simple consistency fix: for inconsistent pairs, enumerate label combos, re-predict, pick max energy."""
|
||||
|
||||
def get_kflip_neighbors(group_uids, demos, k):
|
||||
"""
|
||||
Generate all label assignments that are k flips away from current.
|
||||
Returns list of [(uid, new_label), ...] tuples.
|
||||
"""
|
||||
labeled_uids = [uid for uid in group_uids if demos[uid]['label'] is not None]
|
||||
neighbors = []
|
||||
|
||||
for combo in combinations(labeled_uids, k):
|
||||
flips = [(uid, 1 - demos[uid]['label']) for uid in combo]
|
||||
neighbors.append(flips)
|
||||
|
||||
return neighbors
|
||||
|
||||
async def fix_inconsistencies_greedy(demos, config=C, max_fixes=20, max_flips=3, is_consistent: Callable = is_consistent):
|
||||
"""
|
||||
Greedy consistency fix: Try k-flip neighborhoods (k=1,2,...) until we find
|
||||
a consistent assignment that improves energy.
|
||||
"""
|
||||
for fix_iter in range(max_fixes):
|
||||
# Find inconsistent pairs
|
||||
# Find inconsistent groups
|
||||
groups = {}
|
||||
for uid, demo in demos.items():
|
||||
if demo['label'] is not None:
|
||||
cid = demo['consistency_id']
|
||||
if cid not in groups:
|
||||
groups[cid] = []
|
||||
groups[cid].append(uid)
|
||||
groups.setdefault(demo['consistency_id'], []).append(uid)
|
||||
|
||||
# Find first inconsistent pair
|
||||
inconsistent_pair = None
|
||||
# Find first inconsistent group
|
||||
inconsistent_group = None
|
||||
for cid, uids in groups.items():
|
||||
labels = [demos[uid]['label'] for uid in uids if demos[uid]['label'] is not None]
|
||||
if len(set(labels)) > 1: # Inconsistent
|
||||
inconsistent_pair = (uids[0], uids[1])
|
||||
if not is_consistent(uids, demos):
|
||||
inconsistent_group = uids
|
||||
break
|
||||
|
||||
if inconsistent_pair is None:
|
||||
break # No more inconsistencies or not yet enough labels to have any effect
|
||||
if not inconsistent_group:
|
||||
break # All consistent!
|
||||
|
||||
uid1, uid2 = inconsistent_pair
|
||||
old_energy, _ = compute_energy(demos, config)
|
||||
best_energy = old_energy
|
||||
best_flips = None
|
||||
|
||||
# Enumerate all 4 label combinations and pick max energy
|
||||
# FIXME doesn't this call predict_label twice per option?
|
||||
options = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
best_energy = float('-inf')
|
||||
best_option = None
|
||||
|
||||
for label1, label2 in options:
|
||||
temp_demos = demos.copy()
|
||||
temp_demos[uid1]['label'] = label1
|
||||
temp_demos[uid2]['label'] = label2
|
||||
# Try k=1, 2, 3, ... flips until we find improvement
|
||||
for k in range(1, min(max_flips + 1, len(inconsistent_group) + 1)):
|
||||
neighbors = get_kflip_neighbors(inconsistent_group, demos, k)
|
||||
|
||||
# Re-predict labels with new context to update scores
|
||||
current_labeled = {k: v for k, v in temp_demos.items() if v['label'] is not None}
|
||||
new_label1, score1 = await predict_label(uid1, current_labeled, config)
|
||||
new_label2, score2 = await predict_label(uid2, current_labeled, config)
|
||||
temp_demos[uid1]['score'] = score1
|
||||
temp_demos[uid2]['score'] = score2
|
||||
for flips in neighbors:
|
||||
# Apply flips temporarily
|
||||
temp_demos = deepcopy(demos)
|
||||
for uid, new_label in flips:
|
||||
temp_demos[uid]['label'] = new_label
|
||||
|
||||
# Check if this is consistent
|
||||
if not is_consistent(inconsistent_group, temp_demos):
|
||||
continue # Skip inconsistent neighbors
|
||||
|
||||
# Score it
|
||||
energy, _ = compute_energy(temp_demos, config)
|
||||
|
||||
if energy > best_energy:
|
||||
best_energy = energy
|
||||
best_flips = flips
|
||||
|
||||
energy, _ = compute_energy(temp_demos, config)
|
||||
|
||||
if energy > best_energy:
|
||||
best_energy = energy
|
||||
best_option = (label1, label2, score1, score2)
|
||||
if best_flips:
|
||||
break # Found improvement with k flips, don't try k+1
|
||||
|
||||
# Apply best option
|
||||
demos[uid1]['label'] = best_option[0]
|
||||
demos[uid1]['score'] = best_option[2]
|
||||
demos[uid2]['label'] = best_option[1]
|
||||
demos[uid2]['score'] = best_option[3]
|
||||
# Apply best flips if improvement found
|
||||
if best_flips and best_energy > old_energy:
|
||||
for uid, new_label in best_flips:
|
||||
demos[uid]['label'] = new_label
|
||||
else:
|
||||
break # No improvement possible, stop trying
|
||||
|
||||
return demos
|
||||
|
||||
@@ -305,7 +314,7 @@ async def run_icm(demonstrations, config=C):
|
||||
accuracies = []
|
||||
|
||||
# Fix any initial inconsistencies from random initialization
|
||||
demonstrations = await fix_inconsistencies_simple(demonstrations, config)
|
||||
demonstrations = await fix_inconsistencies_greedy(demonstrations, config)
|
||||
|
||||
current_labeled = {k: v for k, v in demonstrations.items() if v['label'] is not None}
|
||||
old_energy, old_metrics = compute_energy(demonstrations, config)
|
||||
@@ -360,17 +369,21 @@ async def run_icm(demonstrations, config=C):
|
||||
verbose = 2
|
||||
else:
|
||||
verbose = 0
|
||||
new_label, score = await predict_label(example_uid, current_labeled, config, verbose=verbose)
|
||||
new_label, score = await predict_label(example_uid, current_labeled, config, verbose=verbose, all_demos=demonstrations)
|
||||
|
||||
# Update with new label and fix any inconsistencies
|
||||
temp_demos = demonstrations.copy()
|
||||
# Update with new label and fix any inconsistencies ONLY if label changed
|
||||
temp_demos = deepcopy(demonstrations)
|
||||
temp_demos[example_uid]['label'] = new_label
|
||||
temp_demos[example_uid]['score'] = score
|
||||
temp_demos = await fix_inconsistencies_simple(temp_demos, config)
|
||||
|
||||
# Only fix inconsistencies if the label actually changed
|
||||
if demonstrations[example_uid]['label'] != new_label:
|
||||
temp_demos = await fix_inconsistencies_greedy(temp_demos, config)
|
||||
|
||||
# Compute new energy
|
||||
new_energy, new_metrics = compute_energy(temp_demos, config)
|
||||
delta = new_energy - old_energy
|
||||
temp_demos[example_uid]['deltaE'] = delta
|
||||
|
||||
# Annealing decision
|
||||
T = max(config.final_t, config.initial_t / (1 + config.beta * math.log(1 + iter)))
|
||||
@@ -387,7 +400,7 @@ async def run_icm(demonstrations, config=C):
|
||||
accuracies.append(new_metrics['accuracy'])
|
||||
|
||||
if iter % C.log_interval == 0:
|
||||
logger.info(f"Progress: Labeled {new_metrics['num_labeled']}, Inconsistents: {new_metrics['num_inconsistent']}. Acc: {new_metrics['accuracy']:.2f}, Energy: {old_energy:.2f}")
|
||||
logger.info(f"Progress: Labeled {new_metrics['num_labeled']}, Inconsistents: {new_metrics['num_inconsistent']}. Acc: {new_metrics['accuracy']:.2f}, Energy: {old_energy:.2f}. Cost so far: ${total_cost:.4f}")
|
||||
|
||||
return demonstrations, energies, accuracies
|
||||
|
||||
@@ -398,6 +411,7 @@ final_demos, energies, accuracies = asyncio.run(run_icm(demonstrations, C))
|
||||
# Final metrics
|
||||
final_energy, final_metrics = compute_energy(final_demos, C)
|
||||
logger.info("\nFinal Results:")
|
||||
logger.info("Total cost: ${:.4f}", total_cost)
|
||||
logger.info("Energy: {:.2f}", final_energy)
|
||||
# TODO show vanilla accuracy here for comparison
|
||||
logger.info("Accuracy vs vanilla: {:.2f}, initial {:.2f}", final_metrics['accuracy'], accuracies[0])
|
||||
@@ -408,19 +422,16 @@ logger.info("Inconsistencies: {}", final_metrics['num_inconsistent'])
|
||||
df = pd.DataFrame(final_demos).T
|
||||
df.to_parquet(C.out_dir / "icm_final_labels.parquet")
|
||||
|
||||
logger.info("\nFinal labels:")
|
||||
for uid, demo in final_demos.items():
|
||||
label = demo['label']
|
||||
if label is not None:
|
||||
logger.info(f"UID {uid} ({demo['consistency_id']}): {label} (vanilla: {demo['vanilla_label']})")
|
||||
df_labeled = df.dropna(subset='label').sort_values(by='score', key=np.abs, ascending=False)
|
||||
df_labeled_disagreed = df_labeled[df_labeled['vanilla_label'] != df_labeled['label']]
|
||||
|
||||
print(f"\nFinal labeled examples (total {len(df_labeled)}):")
|
||||
print(df_labeled_disagreed[['consistency_id', 'label', 'vanilla_label', 'score', 'prompt']])
|
||||
|
||||
for uid, row in df_labeled_disagreed.iterrows():
|
||||
print(f"\n## Candidate: {row['prompt']}\nICM Set: {'A' if row['label']==1 else 'B'}, Vanilla Set: {'A' if row['vanilla_label']==1 else 'B'}, score={row['score']}\n")
|
||||
|
||||
|
||||
json.dump(
|
||||
asdict(C),
|
||||
open(C.out_dir / "icm_config.json", "w")
|
||||
)
|
||||
|
||||
# TODO put them in file, only print a top few disagreements
|
||||
|
||||
# %% [code]
|
||||
# Simple visualization (requires matplotlib)
|
||||
@@ -443,27 +454,24 @@ plt.tight_layout()
|
||||
plt.savefig("icm_progress.png")
|
||||
plt.show()
|
||||
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
# ## Next Steps & Limitations
|
||||
# Prioritized by complexity (low/med/high) vs. value (low/med/high) based on paper comparison. Limitations noted with potential fixes.
|
||||
|
||||
# # Existing TODOs:
|
||||
# # Existing TODOs:
|
||||
# - [x] Load larger HF dataset ('Yik/truthfulQA-bool' subset, formatted to messages).
|
||||
# - [x] Refine few-shot prompt from original get_judge_prompt_fewshot.
|
||||
# - [x] Add weighted sampling for inconsistent groups (no longer random).
|
||||
# - [x] Remove biased instruction - pure pattern completion for unsupervised elicitation.
|
||||
# - [x] Simplify consistency fix - enumerate label combos, re-predict, pick max energy (no LLM meta-reasoning).
|
||||
# - [x] Add log temperature schedule: max(Tmin, T0 / (1 + β log(n))) - Low complexity, Med value: Better early exploration.
|
||||
# - [ ] Add caching for predictions (dict/file-based) - Low complexity, Med value: @lru_cache already used, could add disk cache.
|
||||
# - [ ] Implement async batch predictions in predict_label - Med complexity, Med value: Use asyncio.gather for concurrent API calls.
|
||||
# - [ ] Full mutual predictability: Use all/subsampled demos in predict_label - High complexity, High value: Handle context limits.
|
||||
# - [ ] Test robustness with worst-case init: Add golden/random/worst init options - Med complexity, Low value: Validate like paper Sec. 5.
|
||||
# # - [ ] Dynamically select demos in few-shot (group/sort by consistency_id) - Med complexity, High value.
|
||||
# # - [ ] Weight sampling by energy delta potential - Low complexity, Med value.
|
||||
# #
|
||||
# # Limitations (from Paper Sec. 9) & Potential Fixes:
|
||||
# - [ ] Salient concepts only: ICM can't elicit non-salient private preferences (e.g., "sun" poems); fix: Combine with weak supervision.
|
||||
# - [ ] Context length limits: Can't fit all N demos for large datasets; fix: Subsample relevant demos or use long-context models.
|
||||
# - [ ] Degenerate solutions without consistency: Risk of all-same labels; mitigated by logical consistency term.
|
||||
# - [ ] Inference cost: 2-3 fwd passes per point (paper App. B); fix: Caching + batching reduces API hits.
|
||||
# - [ ] I'd like to record what it thinks the labels represent e.g. "misconception" "virtue" etc, and find which answer leads to the best energy.
|
||||
# - [ ] consistency groups rely on known labels, this is a limitation for real unsupervised use
|
||||
@@ -26,6 +26,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/1c/8feedd607cc14c5df9aef74fe3af9a99bf660743b842a9b5b1865326b4aa/adjustText-1.3.0-py3-none-any.whl", hash = "sha256:da23d7b24b6db5ffa039bb136bfa556207365e32f48ac74b07ad26dd485bc691", size = 13154, upload-time = "2024-10-31T16:45:35.227Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
version = "0.12.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/64/b945b8025a9d1e6e2138845f4022165d3b337f55f50984fbc6a4c0a1e355/aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713", size = 132196, upload-time = "2024-09-25T13:20:23.823Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d7/15d67e05b235d1ed8c3ce61688fe4d84130e72af1657acadfaac3479f4cf/aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d", size = 28199, upload-time = "2024-09-25T13:20:22.688Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
@@ -3748,6 +3757,7 @@ version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "adjusttext" },
|
||||
{ name = "aiocache" },
|
||||
{ name = "alembic" },
|
||||
{ name = "altair" },
|
||||
{ name = "anthropic" },
|
||||
@@ -3793,6 +3803,7 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "adjusttext", specifier = ">=1.3.0" },
|
||||
{ name = "aiocache", specifier = ">=0.12.3" },
|
||||
{ name = "alembic", specifier = ">=1.16.5" },
|
||||
{ name = "altair", specifier = ">=5.5.0" },
|
||||
{ name = "anthropic", specifier = ">=0.69.0" },
|
||||
|
||||
Reference in New Issue
Block a user