Skip to content

Commit 132ab1e

Browse files
Release v0.1.0
1 parent 12312c9 commit 132ab1e

193 files changed

Lines changed: 39417 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Checklist generators."""
2+
3+
from .base import (
4+
ChecklistGenerator,
5+
InstanceChecklistGenerator,
6+
CorpusChecklistGenerator,
7+
)
8+
from .instance_level import DirectGenerator, ContrastiveGenerator
9+
from .corpus_level import InductiveGenerator, DeductiveGenerator, InteractiveGenerator
10+
11+
__all__ = [
12+
"ChecklistGenerator",
13+
"InstanceChecklistGenerator",
14+
"CorpusChecklistGenerator",
15+
"DirectGenerator",
16+
"ContrastiveGenerator",
17+
"InductiveGenerator",
18+
"DeductiveGenerator",
19+
"InteractiveGenerator",
20+
]

autochecklist/generators/base.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""Base classes for checklist generators."""
2+
3+
import logging
4+
from abc import ABC, abstractmethod
5+
from typing import Any, Dict, Iterator, List, Optional
6+
7+
from ..config import get_config
8+
from ..models import Checklist
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class ChecklistGenerator(ABC):
14+
"""Base class for all checklist generators."""
15+
16+
def __init__(
17+
self,
18+
model: Optional[str] = None,
19+
temperature: Optional[float] = None,
20+
max_tokens: int = 2048,
21+
api_key: Optional[str] = None,
22+
provider: Optional[str] = None,
23+
base_url: Optional[str] = None,
24+
client: Any = None,
25+
api_format: Optional[str] = None,
26+
reasoning_effort: Optional[str] = None,
27+
):
28+
config = get_config()
29+
self.model = model or config.generator_model.model_id
30+
self.temperature = temperature if temperature is not None else config.generator_model.temperature
31+
self.max_tokens = max_tokens
32+
self.api_key = api_key
33+
self._client = client
34+
self._provider = provider or config.generator_model.provider or "openrouter"
35+
self._base_url = base_url
36+
self._api_format = api_format or "chat"
37+
self.reasoning_effort = reasoning_effort
38+
39+
@property
40+
@abstractmethod
41+
def generation_level(self) -> str:
42+
"""Return 'instance' or 'corpus'."""
43+
pass
44+
45+
@property
46+
@abstractmethod
47+
def method_name(self) -> str:
48+
"""Return the method name (e.g., 'tick', 'rlcf')."""
49+
pass
50+
51+
@abstractmethod
52+
def generate(self, **kwargs: Any) -> Checklist:
53+
"""Generate a checklist."""
54+
pass
55+
56+
def generate_stream(self, **kwargs: Any) -> Iterator[str]:
57+
"""Stream checklist generation (for UI).
58+
59+
Default implementation just yields the final result.
60+
Override for true streaming support.
61+
"""
62+
checklist = self.generate(**kwargs)
63+
yield checklist.to_text()
64+
65+
def _get_or_create_client(self) -> Any:
66+
"""Get injected client or create one from provider settings."""
67+
if self._client is not None:
68+
return self._client
69+
from ..providers.factory import get_client
70+
return get_client(
71+
provider=self._provider,
72+
api_key=self.api_key,
73+
base_url=self._base_url,
74+
model=self.model,
75+
api_format=self._api_format,
76+
)
77+
78+
def _call_model(
79+
self,
80+
prompt: str,
81+
system_prompt: Optional[str] = None,
82+
response_format: Optional[dict] = None,
83+
) -> str:
84+
"""Call the LLM and return the response text.
85+
86+
Args:
87+
prompt: The user prompt text.
88+
system_prompt: Optional system prompt.
89+
response_format: Optional OpenAI-compatible response_format dict
90+
for structured JSON output. When provided, the call is attempted
91+
with ``response_format`` first; if the provider does not support
92+
it, the call is retried without it (fallback to schema-in-prompt).
93+
94+
Returns:
95+
The model's response text.
96+
"""
97+
messages: List[Dict[str, str]] = []
98+
if system_prompt:
99+
messages.append({"role": "system", "content": system_prompt})
100+
messages.append({"role": "user", "content": prompt})
101+
102+
kwargs: Dict[str, Any] = {}
103+
if response_format is not None:
104+
kwargs["response_format"] = response_format
105+
if self.reasoning_effort is not None:
106+
kwargs["reasoning_effort"] = self.reasoning_effort
107+
108+
client = self._get_or_create_client()
109+
110+
try:
111+
response = client.chat_completion(
112+
model=self.model,
113+
messages=messages,
114+
temperature=self.temperature,
115+
max_tokens=self.max_tokens,
116+
**kwargs,
117+
)
118+
except (ValueError, KeyError, TypeError) as e:
119+
# Schema/parsing errors from response_format — fall back to schema-in-prompt
120+
if response_format is not None:
121+
logger.warning(
122+
"Structured output failed (%s), retrying without "
123+
"response_format (fallback to schema-in-prompt).",
124+
e,
125+
)
126+
fallback_kwargs = {k: v for k, v in kwargs.items() if k != "response_format"}
127+
response = client.chat_completion(
128+
model=self.model,
129+
messages=messages,
130+
temperature=self.temperature,
131+
max_tokens=self.max_tokens,
132+
**fallback_kwargs,
133+
)
134+
else:
135+
raise
136+
except Exception as e:
137+
# For HTTP errors (auth, rate limit, server), only fallback on 400 (bad schema)
138+
import httpx
139+
140+
if (
141+
response_format is not None
142+
and isinstance(e, httpx.HTTPStatusError)
143+
and e.response.status_code == 400
144+
):
145+
logger.warning(
146+
"Structured output failed (%s), retrying without "
147+
"response_format (fallback to schema-in-prompt).",
148+
e,
149+
)
150+
fallback_kwargs = {k: v for k, v in kwargs.items() if k != "response_format"}
151+
response = client.chat_completion(
152+
model=self.model,
153+
messages=messages,
154+
temperature=self.temperature,
155+
max_tokens=self.max_tokens,
156+
**fallback_kwargs,
157+
)
158+
else:
159+
raise
160+
161+
return response["choices"][0]["message"]["content"]
162+
163+
164+
class InstanceChecklistGenerator(ChecklistGenerator):
165+
"""Base for instance-level generators (one checklist per input)."""
166+
167+
@property
168+
def generation_level(self) -> str:
169+
return "instance"
170+
171+
@abstractmethod
172+
def generate(
173+
self,
174+
input: str,
175+
target: Optional[str] = None,
176+
reference: Optional[str] = None,
177+
**kwargs: Any,
178+
) -> Checklist:
179+
"""Generate checklist for an input.
180+
181+
Args:
182+
input: The input/query/instruction text
183+
target: Optional target response to evaluate
184+
reference: Optional reference target for comparison
185+
**kwargs: Method-specific arguments
186+
187+
Returns:
188+
Generated Checklist
189+
"""
190+
pass
191+
192+
193+
class CorpusChecklistGenerator(ChecklistGenerator):
194+
"""Base for corpus-level generators (one checklist for entire dataset)."""
195+
196+
@property
197+
def generation_level(self) -> str:
198+
return "corpus"
199+
200+
@abstractmethod
201+
def generate(
202+
self,
203+
inputs: List[Dict[str, Any]],
204+
**kwargs: Any,
205+
) -> Checklist:
206+
"""Generate checklist from corpus inputs.
207+
208+
Args:
209+
inputs: List of input items (feedback, dimensions, think-aloud, etc.)
210+
**kwargs: Method-specific arguments
211+
212+
Returns:
213+
Generated Checklist
214+
"""
215+
pass
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Corpus-level checklist generators (one checklist for entire dataset)."""
2+
3+
from .inductive import InductiveGenerator
4+
from .deductive import DeductiveGenerator, AugmentationMode
5+
from .interactive import InteractiveGenerator
6+
7+
__all__ = ["InductiveGenerator", "DeductiveGenerator", "AugmentationMode", "InteractiveGenerator"]

0 commit comments

Comments
 (0)