hardhats 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hardhats-0.1.0/LICENSE +21 -0
- hardhats-0.1.0/PKG-INFO +66 -0
- hardhats-0.1.0/README.md +49 -0
- hardhats-0.1.0/hardhats/__init__.py +6 -0
- hardhats-0.1.0/hardhats/backends.py +193 -0
- hardhats-0.1.0/hardhats/crew.py +89 -0
- hardhats-0.1.0/hardhats.egg-info/PKG-INFO +66 -0
- hardhats-0.1.0/hardhats.egg-info/SOURCES.txt +11 -0
- hardhats-0.1.0/hardhats.egg-info/dependency_links.txt +1 -0
- hardhats-0.1.0/hardhats.egg-info/top_level.txt +1 -0
- hardhats-0.1.0/pyproject.toml +27 -0
- hardhats-0.1.0/setup.cfg +4 -0
- hardhats-0.1.0/tests/test_crew.py +63 -0
hardhats-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vincent Gonzalez / F-Keys Creative LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hardhats-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hardhats
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: hardHats: a framework for crews of agents, each wearing a hat — an LLM instruction or a deterministic gate — run across a federation of free model backends.
|
|
5
|
+
Author: Vincent Gonzalez
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vince-gonzalez/hardhats
|
|
8
|
+
Project-URL: Repository, https://github.com/vince-gonzalez/hardhats
|
|
9
|
+
Keywords: agents,llm,orchestration,multi-model,crew,factory
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# hardHats
|
|
19
|
+
|
|
20
|
+
**coldHat is for play; hardHats are for work.** hardHats is the framework — the
|
|
21
|
+
factory that makes crews. A crew is a line-up of agents, each **wearing a hat**,
|
|
22
|
+
run across a federation of free model backends.
|
|
23
|
+
|
|
24
|
+
A **hat** is one of two things:
|
|
25
|
+
- an **LLM hat** — a model wearing an instruction (a role, a method, an output rule), or
|
|
26
|
+
- a **gate hat** — a deterministic function: a real check, a linter, a measurement.
|
|
27
|
+
|
|
28
|
+
The same engine runs both, so a crew can put an LLM drafter and a deterministic
|
|
29
|
+
verifier on the same line. That is the point: the LLM hats do the judgement, the
|
|
30
|
+
gate hats do the facts, and nobody has to trust a model for something a rule can decide.
|
|
31
|
+
|
|
32
|
+
## A crew is config, not code
|
|
33
|
+
```python
|
|
34
|
+
from hardhats import Hat, Crew
|
|
35
|
+
|
|
36
|
+
crew = Crew("grant", [
|
|
37
|
+
Hat("researcher", model=("groq", "openai/gpt-oss-20b"),
|
|
38
|
+
instruction="List what a Statement of Need must establish."),
|
|
39
|
+
Hat("writer", model=("github", "openai/gpt-4o"),
|
|
40
|
+
instruction="Draft it in a cold, plain register. No AI-tell words."),
|
|
41
|
+
Hat("critic", gate=slopgate), # a deterministic gate hat
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
# pre-flight the crew against a conflict graph before it runs
|
|
45
|
+
conflicts = crew.preflight(load("dataset/prompt-conflicts.json"))
|
|
46
|
+
|
|
47
|
+
outputs = crew.run("Write a Statement of Need for an accessibility tool.")
|
|
48
|
+
```
|
|
49
|
+
Each hat sees the task and every prior hat's output, down the line.
|
|
50
|
+
|
|
51
|
+
## The federation
|
|
52
|
+
`hardhats.backends` speaks to one `chat()` over many OpenAI-compatible (and Gemini)
|
|
53
|
+
backends — Gemini, Groq, OpenRouter, Ollama, plus GitHub Models, Cloudflare Workers
|
|
54
|
+
AI, Cerebras, Mistral, NVIDIA NIM, SambaNova and Cohere — each read from the
|
|
55
|
+
environment, keys never printed. Add a key, gain a perspective.
|
|
56
|
+
|
|
57
|
+
## Crews in the family
|
|
58
|
+
- **a11yhat** — the WCAG accessibility machine, four crew leads as deterministic gate hats.
|
|
59
|
+
- a grant crew, a compliance crew, a prompt-check crew — each a few lines of config.
|
|
60
|
+
|
|
61
|
+
## Tests
|
|
62
|
+
`python -m unittest discover -s tests` — the engine (pipeline + pre-flight) is
|
|
63
|
+
verified with gate hats only, so it proves out with zero model calls.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
MIT. Copyright Vincent Gonzalez / F-Keys Creative LLC.
|
hardhats-0.1.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# hardHats
|
|
2
|
+
|
|
3
|
+
**coldHat is for play; hardHats are for work.** hardHats is the framework — the
|
|
4
|
+
factory that makes crews. A crew is a line-up of agents, each **wearing a hat**,
|
|
5
|
+
run across a federation of free model backends.
|
|
6
|
+
|
|
7
|
+
A **hat** is one of two things:
|
|
8
|
+
- an **LLM hat** — a model wearing an instruction (a role, a method, an output rule), or
|
|
9
|
+
- a **gate hat** — a deterministic function: a real check, a linter, a measurement.
|
|
10
|
+
|
|
11
|
+
The same engine runs both, so a crew can put an LLM drafter and a deterministic
|
|
12
|
+
verifier on the same line. That is the point: the LLM hats do the judgement, the
|
|
13
|
+
gate hats do the facts, and nobody has to trust a model for something a rule can decide.
|
|
14
|
+
|
|
15
|
+
## A crew is config, not code
|
|
16
|
+
```python
|
|
17
|
+
from hardhats import Hat, Crew
|
|
18
|
+
|
|
19
|
+
crew = Crew("grant", [
|
|
20
|
+
Hat("researcher", model=("groq", "openai/gpt-oss-20b"),
|
|
21
|
+
instruction="List what a Statement of Need must establish."),
|
|
22
|
+
Hat("writer", model=("github", "openai/gpt-4o"),
|
|
23
|
+
instruction="Draft it in a cold, plain register. No AI-tell words."),
|
|
24
|
+
Hat("critic", gate=slopgate), # a deterministic gate hat
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
# pre-flight the crew against a conflict graph before it runs
|
|
28
|
+
conflicts = crew.preflight(load("dataset/prompt-conflicts.json"))
|
|
29
|
+
|
|
30
|
+
outputs = crew.run("Write a Statement of Need for an accessibility tool.")
|
|
31
|
+
```
|
|
32
|
+
Each hat sees the task and every prior hat's output, down the line.
|
|
33
|
+
|
|
34
|
+
## The federation
|
|
35
|
+
`hardhats.backends` speaks to one `chat()` over many OpenAI-compatible (and Gemini)
|
|
36
|
+
backends — Gemini, Groq, OpenRouter, Ollama, plus GitHub Models, Cloudflare Workers
|
|
37
|
+
AI, Cerebras, Mistral, NVIDIA NIM, SambaNova and Cohere — each read from the
|
|
38
|
+
environment, keys never printed. Add a key, gain a perspective.
|
|
39
|
+
|
|
40
|
+
## Crews in the family
|
|
41
|
+
- **a11yhat** — the WCAG accessibility machine, four crew leads as deterministic gate hats.
|
|
42
|
+
- a grant crew, a compliance crew, a prompt-check crew — each a few lines of config.
|
|
43
|
+
|
|
44
|
+
## Tests
|
|
45
|
+
`python -m unittest discover -s tests` — the engine (pipeline + pre-flight) is
|
|
46
|
+
verified with gate hats only, so it proves out with zero model calls.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
MIT. Copyright Vincent Gonzalez / F-Keys Creative LLC.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-backend model caller for the atom-effectiveness study.
|
|
3
|
+
|
|
4
|
+
One chat() over four free backends: Gemini (native), and Groq, OpenRouter and
|
|
5
|
+
Ollama (all OpenAI-compatible, so one adapter). Keys are read from the
|
|
6
|
+
environment or, failing that, the Windows user settings that `setx` writes to -
|
|
7
|
+
the same fallback the atom pipeline uses - and are NEVER printed. Standard
|
|
8
|
+
library only; nothing is pip-installed into his environment.
|
|
9
|
+
|
|
10
|
+
A per-backend daily call budget guards each free tier: call budget_left() /
|
|
11
|
+
spend() around real runs so a study can never overrun a free quota.
|
|
12
|
+
|
|
13
|
+
python tools/backends.py # smoke-test connectivity (one tiny call each)
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
|
|
21
|
+
# --- keys: environment first, then HKCU\Environment (what setx writes) ---------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_key(name: str) -> str | None:
|
|
25
|
+
key = os.environ.get(name)
|
|
26
|
+
if key:
|
|
27
|
+
return key
|
|
28
|
+
try:
|
|
29
|
+
import winreg
|
|
30
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as k:
|
|
31
|
+
return winreg.QueryValueEx(k, name)[0] or None
|
|
32
|
+
except (ImportError, OSError, FileNotFoundError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- backend registry ----------------------------------------------------------
|
|
37
|
+
# kind "gemini" uses Google's native API; "openai" uses the chat/completions
|
|
38
|
+
# shape that Groq, OpenRouter and Ollama all speak. Models are candidates: the
|
|
39
|
+
# smoke test reports which resolve, so no name is trusted without a check.
|
|
40
|
+
BACKENDS: dict[str, dict] = {
|
|
41
|
+
"gemini": {
|
|
42
|
+
"kind": "gemini", "key_env": "GEMINI_API_KEY",
|
|
43
|
+
"models": ["gemini-flash-lite-latest", "gemini-3.5-flash"],
|
|
44
|
+
},
|
|
45
|
+
"groq": {
|
|
46
|
+
"kind": "openai", "base": "https://api.groq.com/openai/v1", "key_env": "GROQ_API_KEY",
|
|
47
|
+
# Live on this account's free tier 2026-10-16. gpt-oss is OpenAI's open model.
|
|
48
|
+
"models": ["openai/gpt-oss-20b", "openai/gpt-oss-120b"],
|
|
49
|
+
},
|
|
50
|
+
"openrouter": {
|
|
51
|
+
"kind": "openai", "base": "https://openrouter.ai/api/v1", "key_env": "OPENROUTER_API_KEY",
|
|
52
|
+
# Live :free ids 2026-10-16 (OpenRouter rotates these; refresh from /models).
|
|
53
|
+
"models": ["google/gemma-4-31b-it:free", "nvidia/nemotron-3-super-120b-a12b:free",
|
|
54
|
+
"google/gemma-4-26b-a4b-it:free"],
|
|
55
|
+
},
|
|
56
|
+
"ollama": {
|
|
57
|
+
"kind": "openai", "base": "http://localhost:11434/v1", "key_env": None,
|
|
58
|
+
"models": ["llama3.2", "qwen2.5"],
|
|
59
|
+
},
|
|
60
|
+
# --- additional free perspectives (OpenAI-compatible); add keys to enable ---
|
|
61
|
+
"github": { # GitHub Models: free frontier (GPT-4o, Llama) via a GitHub PAT
|
|
62
|
+
"kind": "openai", "base": "https://models.github.ai/inference", "key_env": "GITHUB_MODELS_TOKEN",
|
|
63
|
+
"models": ["openai/gpt-4o-mini", "openai/gpt-4o", "meta/Llama-3.3-70B-Instruct"],
|
|
64
|
+
},
|
|
65
|
+
"cloudflare": { # Workers AI; base carries the account id, filled from env
|
|
66
|
+
"kind": "openai", "base": "https://api.cloudflare.com/client/v4/accounts/{acct}/ai/v1",
|
|
67
|
+
"key_env": "CLOUDFLARE_API_TOKEN",
|
|
68
|
+
"models": ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/openai/gpt-oss-120b"],
|
|
69
|
+
},
|
|
70
|
+
"cerebras": {
|
|
71
|
+
"kind": "openai", "base": "https://api.cerebras.ai/v1", "key_env": "CEREBRAS_API_KEY",
|
|
72
|
+
"models": ["llama-3.3-70b", "llama3.1-8b"],
|
|
73
|
+
},
|
|
74
|
+
"mistral": {
|
|
75
|
+
"kind": "openai", "base": "https://api.mistral.ai/v1", "key_env": "MISTRAL_API_KEY",
|
|
76
|
+
"models": ["mistral-small-latest", "open-mistral-nemo"],
|
|
77
|
+
},
|
|
78
|
+
"nvidia": {
|
|
79
|
+
"kind": "openai", "base": "https://integrate.api.nvidia.com/v1", "key_env": "NVIDIA_API_KEY",
|
|
80
|
+
"models": ["meta/llama-3.3-70b-instruct", "nvidia/llama-3.1-nemotron-70b-instruct"],
|
|
81
|
+
},
|
|
82
|
+
"sambanova": {
|
|
83
|
+
"kind": "openai", "base": "https://api.sambanova.ai/v1", "key_env": "SAMBANOVA_API_KEY",
|
|
84
|
+
"models": ["Meta-Llama-3.3-70B-Instruct"],
|
|
85
|
+
},
|
|
86
|
+
"cohere": { # non-commercial free tier - perspective/research only, not paid products
|
|
87
|
+
"kind": "openai", "base": "https://api.cohere.ai/compatibility/v1", "key_env": "COHERE_API_KEY",
|
|
88
|
+
"models": ["command-r-plus", "command-r"],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# A real browser User-Agent: Groq (and other Cloudflare-fronted APIs) answer 403
|
|
94
|
+
# "error 1010" to urllib's default agent. This is the same bot-screen seen on the
|
|
95
|
+
# worker's /verify. Sending a normal UA clears it.
|
|
96
|
+
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
97
|
+
"(KHTML, like Gecko) Chrome/131.0 Safari/537.36")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _post(url: str, body: bytes, headers: dict, timeout: int = 120) -> dict:
|
|
101
|
+
headers = {"user-agent": UA, **headers}
|
|
102
|
+
req = urllib.request.Request(url, data=body, headers=headers)
|
|
103
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
104
|
+
return json.load(r)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _openai_chat(base: str, key: str | None, model: str, prompt: str,
|
|
108
|
+
temperature: float, max_tokens: int) -> str:
|
|
109
|
+
headers = {"content-type": "application/json"}
|
|
110
|
+
if key:
|
|
111
|
+
headers["authorization"] = f"Bearer {key}"
|
|
112
|
+
if "openrouter" in base: # optional attribution headers OpenRouter asks for
|
|
113
|
+
headers["HTTP-Referer"] = "https://coldhat.f-keys.com"
|
|
114
|
+
headers["X-Title"] = "coldHat atom study"
|
|
115
|
+
body = json.dumps({
|
|
116
|
+
"model": model, "temperature": temperature, "max_tokens": max_tokens,
|
|
117
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
118
|
+
}).encode()
|
|
119
|
+
data = _post(base + "/chat/completions", body, headers)
|
|
120
|
+
msg = data["choices"][0]["message"]
|
|
121
|
+
return msg.get("content") or msg.get("reasoning") or ""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _gemini_chat(key: str, model: str, prompt: str,
|
|
125
|
+
temperature: float, max_tokens: int) -> str:
|
|
126
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
|
|
127
|
+
body = json.dumps({
|
|
128
|
+
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
|
|
129
|
+
"generationConfig": {"temperature": temperature, "maxOutputTokens": max_tokens},
|
|
130
|
+
}).encode()
|
|
131
|
+
data = _post(url, body, {"content-type": "application/json", "x-goog-api-key": key})
|
|
132
|
+
return data["candidates"][0]["content"]["parts"][0]["text"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def chat(backend: str, prompt: str, model: str | None = None,
|
|
136
|
+
temperature: float = 0.0, max_tokens: int = 700) -> str:
|
|
137
|
+
"""One completion. Falls back across the backend's candidate models on a
|
|
138
|
+
bad-model error; waits out rate limits. Raises RuntimeError on total failure."""
|
|
139
|
+
spec = BACKENDS[backend]
|
|
140
|
+
key = read_key(spec["key_env"]) if spec["key_env"] else None
|
|
141
|
+
if spec["key_env"] and not key:
|
|
142
|
+
raise RuntimeError(f"{backend}: {spec['key_env']} is not set")
|
|
143
|
+
base = spec.get("base", "")
|
|
144
|
+
if "{acct}" in base:
|
|
145
|
+
acct = read_key("CLOUDFLARE_ACCOUNT_ID")
|
|
146
|
+
if not acct:
|
|
147
|
+
raise RuntimeError(f"{backend}: CLOUDFLARE_ACCOUNT_ID is not set")
|
|
148
|
+
base = base.replace("{acct}", acct)
|
|
149
|
+
models = [model] if model else spec["models"]
|
|
150
|
+
last = ""
|
|
151
|
+
for m in models:
|
|
152
|
+
for attempt in range(4):
|
|
153
|
+
try:
|
|
154
|
+
if spec["kind"] == "gemini":
|
|
155
|
+
return _gemini_chat(key, m, prompt, temperature, max_tokens)
|
|
156
|
+
return _openai_chat(base, key, m, prompt, temperature, max_tokens)
|
|
157
|
+
except urllib.error.HTTPError as e:
|
|
158
|
+
last = f"{backend}/{m} HTTP {e.code}"
|
|
159
|
+
if e.code in (429, 500, 502, 503) and attempt < 3:
|
|
160
|
+
time.sleep(12 * (attempt + 1))
|
|
161
|
+
continue
|
|
162
|
+
break # 400/401/404 -> try the next candidate model
|
|
163
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
|
164
|
+
last = f"{backend}/{m} unreadable: {e}"
|
|
165
|
+
break
|
|
166
|
+
except (TimeoutError, urllib.error.URLError) as e:
|
|
167
|
+
last = f"{backend}/{m} no answer: {type(e).__name__}"
|
|
168
|
+
if attempt < 2:
|
|
169
|
+
time.sleep(8)
|
|
170
|
+
continue
|
|
171
|
+
break
|
|
172
|
+
raise RuntimeError(last or f"{backend}: all models failed")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def smoke() -> None:
|
|
176
|
+
"""One tiny call per backend; reports which backend/model pair answers."""
|
|
177
|
+
for name, spec in BACKENDS.items():
|
|
178
|
+
key_ok = not spec["key_env"] or read_key(spec["key_env"])
|
|
179
|
+
if not key_ok:
|
|
180
|
+
print(f" {name:11} SKIP ({spec['key_env']} not set)")
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
t0 = time.time()
|
|
184
|
+
reply = chat(name, "Reply with exactly: OK", max_tokens=10)
|
|
185
|
+
dt = time.time() - t0
|
|
186
|
+
print(f" {name:11} OK {dt:4.1f}s reply={reply.strip()[:30]!r}")
|
|
187
|
+
except Exception as e: # noqa: BLE001 - report, don't crash the sweep
|
|
188
|
+
print(f" {name:11} FAIL {e}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
print("backends smoke test (one call each):")
|
|
193
|
+
smoke()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hardHats - the crew engine. A crew is config, not code: an ordered list of hats.
|
|
3
|
+
A hat is either an LLM hat (a model wearing an instruction) or a gate hat (a
|
|
4
|
+
deterministic function - a real check, a linter, a measurement). The same engine
|
|
5
|
+
runs both, so a crew can mix an LLM drafter with a deterministic verifier.
|
|
6
|
+
|
|
7
|
+
from hardhats import Hat, Crew
|
|
8
|
+
|
|
9
|
+
crew = Crew("grant", [
|
|
10
|
+
Hat("researcher", model=("groq", "openai/gpt-oss-20b"),
|
|
11
|
+
instruction="List what a Statement of Need must establish."),
|
|
12
|
+
Hat("writer", model=("groq", "openai/gpt-oss-120b"),
|
|
13
|
+
instruction="Draft it in a cold, plain register. No AI-tell words."),
|
|
14
|
+
Hat("critic", gate=my_slopgate_function), # deterministic hat
|
|
15
|
+
])
|
|
16
|
+
outputs = crew.run("Write a Statement of Need for ...")
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
from .backends import chat
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Hat:
|
|
28
|
+
"""A role in the crew. LLM hat: set model + instruction. Gate hat: set gate."""
|
|
29
|
+
name: str
|
|
30
|
+
instruction: str = ""
|
|
31
|
+
model: tuple | None = None # (backend, model)
|
|
32
|
+
gate: object = None # callable(payload: str) -> str
|
|
33
|
+
temperature: float = 0.4
|
|
34
|
+
max_tokens: int = 700
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def kind(self) -> str:
|
|
38
|
+
return "gate" if self.gate else "llm"
|
|
39
|
+
|
|
40
|
+
def wear(self, payload: str) -> str:
|
|
41
|
+
if self.gate:
|
|
42
|
+
return str(self.gate(payload))
|
|
43
|
+
if not self.model:
|
|
44
|
+
raise ValueError(f"hat '{self.name}' has neither a model nor a gate")
|
|
45
|
+
prompt = f"{self.instruction}\n\n---\n{payload}" if self.instruction else payload
|
|
46
|
+
return chat(self.model[0], prompt, model=self.model[1],
|
|
47
|
+
temperature=self.temperature, max_tokens=self.max_tokens)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Crew:
|
|
51
|
+
"""An ordered pipeline of hats. Each hat sees the task and every prior output."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, name: str, hats: list[Hat]):
|
|
54
|
+
self.name = name
|
|
55
|
+
self.hats = hats
|
|
56
|
+
|
|
57
|
+
def preflight(self, conflicts: list | None = None):
|
|
58
|
+
"""Check the crew's LLM hats for conflicting instructions before they run.
|
|
59
|
+
`conflicts` is a list of (text_a, text_b, reason); supply the atom conflict
|
|
60
|
+
graph (dataset/prompt-conflicts.json) to use the measured one."""
|
|
61
|
+
if not conflicts:
|
|
62
|
+
return []
|
|
63
|
+
hats = [h for h in self.hats if h.kind == "llm"]
|
|
64
|
+
|
|
65
|
+
def present(atom, text):
|
|
66
|
+
words = {w for w in re.findall(r"[a-z]{4,}", atom.lower())
|
|
67
|
+
if w not in ("output", "section", "list", "only", "nothing", "your", "with")}
|
|
68
|
+
if not words:
|
|
69
|
+
return False
|
|
70
|
+
return sum(1 for w in words if w in text.lower()) >= max(1, len(words) // 2)
|
|
71
|
+
|
|
72
|
+
found = []
|
|
73
|
+
for a, b, reason in conflicts:
|
|
74
|
+
for i, ha in enumerate(hats):
|
|
75
|
+
for hb in hats[i + 1:]:
|
|
76
|
+
if present(a, ha.instruction) and present(b, hb.instruction):
|
|
77
|
+
found.append((ha.name, hb.name, reason))
|
|
78
|
+
return found
|
|
79
|
+
|
|
80
|
+
def run(self, task: str, log=print) -> dict:
|
|
81
|
+
context = f"Task: {task}"
|
|
82
|
+
outputs: dict[str, str] = {}
|
|
83
|
+
for hat in self.hats:
|
|
84
|
+
if log:
|
|
85
|
+
log(f"[{hat.name}] ({hat.kind}) ...")
|
|
86
|
+
out = hat.wear(context).strip()
|
|
87
|
+
outputs[hat.name] = out
|
|
88
|
+
context += f"\n\n{hat.name} produced:\n{out}"
|
|
89
|
+
return outputs
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hardhats
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: hardHats: a framework for crews of agents, each wearing a hat — an LLM instruction or a deterministic gate — run across a federation of free model backends.
|
|
5
|
+
Author: Vincent Gonzalez
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vince-gonzalez/hardhats
|
|
8
|
+
Project-URL: Repository, https://github.com/vince-gonzalez/hardhats
|
|
9
|
+
Keywords: agents,llm,orchestration,multi-model,crew,factory
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# hardHats
|
|
19
|
+
|
|
20
|
+
**coldHat is for play; hardHats are for work.** hardHats is the framework — the
|
|
21
|
+
factory that makes crews. A crew is a line-up of agents, each **wearing a hat**,
|
|
22
|
+
run across a federation of free model backends.
|
|
23
|
+
|
|
24
|
+
A **hat** is one of two things:
|
|
25
|
+
- an **LLM hat** — a model wearing an instruction (a role, a method, an output rule), or
|
|
26
|
+
- a **gate hat** — a deterministic function: a real check, a linter, a measurement.
|
|
27
|
+
|
|
28
|
+
The same engine runs both, so a crew can put an LLM drafter and a deterministic
|
|
29
|
+
verifier on the same line. That is the point: the LLM hats do the judgement, the
|
|
30
|
+
gate hats do the facts, and nobody has to trust a model for something a rule can decide.
|
|
31
|
+
|
|
32
|
+
## A crew is config, not code
|
|
33
|
+
```python
|
|
34
|
+
from hardhats import Hat, Crew
|
|
35
|
+
|
|
36
|
+
crew = Crew("grant", [
|
|
37
|
+
Hat("researcher", model=("groq", "openai/gpt-oss-20b"),
|
|
38
|
+
instruction="List what a Statement of Need must establish."),
|
|
39
|
+
Hat("writer", model=("github", "openai/gpt-4o"),
|
|
40
|
+
instruction="Draft it in a cold, plain register. No AI-tell words."),
|
|
41
|
+
Hat("critic", gate=slopgate), # a deterministic gate hat
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
# pre-flight the crew against a conflict graph before it runs
|
|
45
|
+
conflicts = crew.preflight(load("dataset/prompt-conflicts.json"))
|
|
46
|
+
|
|
47
|
+
outputs = crew.run("Write a Statement of Need for an accessibility tool.")
|
|
48
|
+
```
|
|
49
|
+
Each hat sees the task and every prior hat's output, down the line.
|
|
50
|
+
|
|
51
|
+
## The federation
|
|
52
|
+
`hardhats.backends` speaks to one `chat()` over many OpenAI-compatible (and Gemini)
|
|
53
|
+
backends — Gemini, Groq, OpenRouter, Ollama, plus GitHub Models, Cloudflare Workers
|
|
54
|
+
AI, Cerebras, Mistral, NVIDIA NIM, SambaNova and Cohere — each read from the
|
|
55
|
+
environment, keys never printed. Add a key, gain a perspective.
|
|
56
|
+
|
|
57
|
+
## Crews in the family
|
|
58
|
+
- **a11yhat** — the WCAG accessibility machine, four crew leads as deterministic gate hats.
|
|
59
|
+
- a grant crew, a compliance crew, a prompt-check crew — each a few lines of config.
|
|
60
|
+
|
|
61
|
+
## Tests
|
|
62
|
+
`python -m unittest discover -s tests` — the engine (pipeline + pre-flight) is
|
|
63
|
+
verified with gate hats only, so it proves out with zero model calls.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
MIT. Copyright Vincent Gonzalez / F-Keys Creative LLC.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
hardhats/__init__.py
|
|
5
|
+
hardhats/backends.py
|
|
6
|
+
hardhats/crew.py
|
|
7
|
+
hardhats.egg-info/PKG-INFO
|
|
8
|
+
hardhats.egg-info/SOURCES.txt
|
|
9
|
+
hardhats.egg-info/dependency_links.txt
|
|
10
|
+
hardhats.egg-info/top_level.txt
|
|
11
|
+
tests/test_crew.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hardhats
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hardhats"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "hardHats: a framework for crews of agents, each wearing a hat — an LLM instruction or a deterministic gate — run across a federation of free model backends."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Vincent Gonzalez" }]
|
|
12
|
+
keywords = ["agents", "llm", "orchestration", "multi-model", "crew", "factory"]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Software Development :: Libraries",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/vince-gonzalez/hardhats"
|
|
24
|
+
Repository = "https://github.com/vince-gonzalez/hardhats"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["hardhats"]
|
hardhats-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine tests for hardHats - gate hats only, so the pipeline and pre-flight are
|
|
3
|
+
verified with zero model calls. LLM hats are exercised separately against the
|
|
4
|
+
live federation.
|
|
5
|
+
|
|
6
|
+
python -m unittest discover -s tests
|
|
7
|
+
"""
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
from hardhats import Crew, Hat
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Pipeline(unittest.TestCase):
|
|
14
|
+
def test_gate_hats_pass_outputs_down_the_line(self):
|
|
15
|
+
seen = {}
|
|
16
|
+
|
|
17
|
+
def upper(payload):
|
|
18
|
+
seen["upper"] = payload
|
|
19
|
+
return "A, B, C"
|
|
20
|
+
|
|
21
|
+
def count(payload):
|
|
22
|
+
seen["count"] = payload # must contain the first hat's output
|
|
23
|
+
return f"items={payload.count(',') + 1 if ',' in payload else 0}"
|
|
24
|
+
|
|
25
|
+
crew = Crew("demo", [
|
|
26
|
+
Hat("lister", gate=upper),
|
|
27
|
+
Hat("counter", gate=count),
|
|
28
|
+
])
|
|
29
|
+
out = crew.run("make a list", log=None)
|
|
30
|
+
self.assertEqual(out["lister"], "A, B, C")
|
|
31
|
+
self.assertIn("A, B, C", seen["count"], "the counter should see the lister's output")
|
|
32
|
+
self.assertIn("items=", out["counter"])
|
|
33
|
+
|
|
34
|
+
def test_hat_kind(self):
|
|
35
|
+
self.assertEqual(Hat("g", gate=lambda p: p).kind, "gate")
|
|
36
|
+
self.assertEqual(Hat("l", model=("groq", "x"), instruction="hi").kind, "llm")
|
|
37
|
+
|
|
38
|
+
def test_hat_without_model_or_gate_raises(self):
|
|
39
|
+
with self.assertRaises(ValueError):
|
|
40
|
+
Hat("bad").wear("x")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Preflight(unittest.TestCase):
|
|
44
|
+
def test_detects_conflicting_instructions_across_hats(self):
|
|
45
|
+
crew = Crew("c", [
|
|
46
|
+
Hat("a", model=("groq", "x"), instruction="Use a bulleted list for the output."),
|
|
47
|
+
Hat("b", model=("groq", "x"), instruction="Use a numbered list for the output."),
|
|
48
|
+
])
|
|
49
|
+
conflicts = [("Use a bulleted list", "Use a numbered list", "cannot be both")]
|
|
50
|
+
found = crew.preflight(conflicts)
|
|
51
|
+
self.assertTrue(found, "bulleted-vs-numbered hats should be flagged")
|
|
52
|
+
self.assertEqual(found[0][2], "cannot be both")
|
|
53
|
+
|
|
54
|
+
def test_clean_crew_has_no_conflicts(self):
|
|
55
|
+
crew = Crew("c", [
|
|
56
|
+
Hat("a", model=("groq", "x"), instruction="Think step by step."),
|
|
57
|
+
Hat("b", model=("groq", "x"), instruction="Write in a plain register."),
|
|
58
|
+
])
|
|
59
|
+
self.assertEqual(crew.preflight([("Use a bulleted list", "Use a numbered list", "x")]), [])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
unittest.main()
|