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 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.
@@ -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,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,6 @@
1
+ """hardHats - the crew framework. coldHat is for play; hardHats are for work."""
2
+ from .backends import BACKENDS, chat
3
+ from .crew import Crew, Hat
4
+
5
+ __all__ = ["Crew", "Hat", "chat", "BACKENDS"]
6
+ __version__ = "0.1.0"
@@ -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
+ 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()