hunch-jev 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.
@@ -0,0 +1,5 @@
1
+ TYPESAFE_API_KEY=
2
+ TYPESAFE_DEFAULT_MODEL=jev-latest
3
+ CEREBRAS_API_KEY=
4
+ OPENAI_API_KEY=
5
+ OPENROUTER_API_KEY=
@@ -0,0 +1,32 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install -e ".[dev]"
17
+ - run: pytest
18
+
19
+ pypi:
20
+ needs: test
21
+ runs-on: ubuntu-latest
22
+ environment: pypi
23
+ permissions:
24
+ id-token: write
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: "3.12"
30
+ - run: pip install build
31
+ - run: python -m build
32
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+ .coverage
15
+ htmlcov/
16
+ .DS_Store
17
+ .hunch/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steven Shoemaker
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,140 @@
1
+ Metadata-Version: 2.5
2
+ Name: hunch-jev
3
+ Version: 0.1.0
4
+ Summary: Ask Jev over columns of data: closed-set questions, cached and joined back.
5
+ Project-URL: Homepage, https://github.com/steven-shoemaker/hunch
6
+ Project-URL: Repository, https://github.com/steven-shoemaker/hunch
7
+ Author: Steven Shoemaker
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: pandas>=2.0
12
+ Requires-Dist: typesafe-sdk>=0.5.7
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # hunch
18
+
19
+ Jev as a column primitive.
20
+
21
+ [Jev](https://docs.typesafe.ai) is TypeSafe’s System One model: you send state and typed questions, and you get labels, scores, and yes/no probabilities back. **hunch** is a small Python layer on that API. Code owns the table and the side effects. Jev only judges. An optional LLM role may *propose* strings; it never closes.
22
+
23
+ ```python
24
+ import hunch
25
+ from hunch import ask, over, rate
26
+
27
+ jev = hunch.connect() # TYPESAFE_API_KEY
28
+
29
+ @over(prospects, "JOB_TITLE")
30
+ def classify(title):
31
+ function = ask(
32
+ title,
33
+ "what business function does this title state?",
34
+ among=functions,
35
+ by=function_rules,
36
+ )
37
+ level = ask(
38
+ title,
39
+ "what level of rank does this title state?",
40
+ among=levels,
41
+ by=seniority_rules,
42
+ )
43
+ seniority = level.on(
44
+ sure=level.top,
45
+ torn=lambda: ask(
46
+ title,
47
+ "which of these two fits better?",
48
+ among=level.top2,
49
+ by=seniority_rules,
50
+ ).top,
51
+ lost=lambda: (
52
+ "Individual Contributor"
53
+ if title.feels("a rank word governing a product, program, or account rather than people")
54
+ else "review"
55
+ ),
56
+ )
57
+ keep = rate(
58
+ title,
59
+ "How important is this account?",
60
+ ["disposable", "nice to have", "should keep", "must keep"],
61
+ )
62
+ return {
63
+ "function": function.top,
64
+ "function_shape": function.shape,
65
+ "seniority": seniority,
66
+ "seniority_shape": level.shape,
67
+ "keep": keep.level,
68
+ }
69
+
70
+ results = classify.run(jev)
71
+ ```
72
+
73
+ Independent `ask` / `rate` / `feels` calls on the same value go in **one** Jev request. `@over` classifies each distinct value once, caches it, and left-joins onto the frame.
74
+
75
+ ## Install
76
+
77
+ ```sh
78
+ pip install hunch-jev
79
+ # or from a clone:
80
+ pip install -e ".[dev]"
81
+ ```
82
+
83
+ Requires Python 3.10+. Set `TYPESAFE_API_KEY`. Do not put keys in source.
84
+
85
+ ```sh
86
+ pytest
87
+ ```
88
+
89
+ ## Verbs
90
+
91
+ | Call | Jev primitive | You get |
92
+ | --- | --- | --- |
93
+ | `ask(state, question, among=..., by=...)` | Choice | `.top`, `.top2`, `.p`, `.confidence`, `.shape` |
94
+ | `rate(state, question, levels)` | Score | `.score`, `.level`, `.shape` |
95
+ | `value.feels("...")` | Noul | truthy when P(yes) ≥ 0.5 |
96
+
97
+ `.shape` is **your** policy on the distribution, not a Jev field:
98
+
99
+ | Shape | Meaning |
100
+ | --- | --- |
101
+ | `sure` | One option dominates |
102
+ | `torn` | Two options are close |
103
+ | `lost` | Flat or weak evidence |
104
+
105
+ Cutoffs live on `ShapePolicy`. Confidence is how peaked the distribution is, not whether the label is true.
106
+
107
+ `connect(cache="~/.cache/hunch")` persists answers on disk. `jev.usage` reports calls, cache hits, tokens, and the model name.
108
+
109
+ ## LLM roles
110
+
111
+ A role may only propose text or a list of labels. `ask` still decides.
112
+
113
+ ```python
114
+ from hunch import ask, draft, openrouter, role
115
+
116
+ jev = hunch.connect(llm=openrouter()) # OPENROUTER_API_KEY
117
+ taxonomist = role(
118
+ "Propose 8–16 kebab-case folder names. Include junk. No review pile.",
119
+ emit=list[str],
120
+ )
121
+
122
+ with jev.session():
123
+ taxonomy = draft(listing, taxonomist).labels
124
+
125
+ folder = ask(name, "which folder?", among=taxonomy)
126
+ ```
127
+
128
+ `hunch.openai`, `hunch.cerebras`, and `hunch.openrouter` are OpenAI-compatible adapters. `via=` on a role overrides `llm=` on `connect()`. A role stops after `max_loops` (default 5) in one session.
129
+
130
+ ## Example
131
+
132
+ [`examples/organize_downloads.py`](examples/organize_downloads.py) files a Downloads folder: one LLM taxonomy, then Jev assigns each loose file, then the script moves. Destination folders are skipped on later runs. `--dry-run` prints the plan.
133
+
134
+ ## What this is not
135
+
136
+ Jev does not invent labels. `among=` is the whole set of allowed answers. Roles invent candidates; they have no tools and do not move files. Open-ended writing and multi-step agents are out of scope.
137
+
138
+ ## License
139
+
140
+ MIT. Jev and TypeSafe are [typesafe.ai](https://typesafe.ai); this library is not affiliated.
@@ -0,0 +1,124 @@
1
+ # hunch
2
+
3
+ Jev as a column primitive.
4
+
5
+ [Jev](https://docs.typesafe.ai) is TypeSafe’s System One model: you send state and typed questions, and you get labels, scores, and yes/no probabilities back. **hunch** is a small Python layer on that API. Code owns the table and the side effects. Jev only judges. An optional LLM role may *propose* strings; it never closes.
6
+
7
+ ```python
8
+ import hunch
9
+ from hunch import ask, over, rate
10
+
11
+ jev = hunch.connect() # TYPESAFE_API_KEY
12
+
13
+ @over(prospects, "JOB_TITLE")
14
+ def classify(title):
15
+ function = ask(
16
+ title,
17
+ "what business function does this title state?",
18
+ among=functions,
19
+ by=function_rules,
20
+ )
21
+ level = ask(
22
+ title,
23
+ "what level of rank does this title state?",
24
+ among=levels,
25
+ by=seniority_rules,
26
+ )
27
+ seniority = level.on(
28
+ sure=level.top,
29
+ torn=lambda: ask(
30
+ title,
31
+ "which of these two fits better?",
32
+ among=level.top2,
33
+ by=seniority_rules,
34
+ ).top,
35
+ lost=lambda: (
36
+ "Individual Contributor"
37
+ if title.feels("a rank word governing a product, program, or account rather than people")
38
+ else "review"
39
+ ),
40
+ )
41
+ keep = rate(
42
+ title,
43
+ "How important is this account?",
44
+ ["disposable", "nice to have", "should keep", "must keep"],
45
+ )
46
+ return {
47
+ "function": function.top,
48
+ "function_shape": function.shape,
49
+ "seniority": seniority,
50
+ "seniority_shape": level.shape,
51
+ "keep": keep.level,
52
+ }
53
+
54
+ results = classify.run(jev)
55
+ ```
56
+
57
+ Independent `ask` / `rate` / `feels` calls on the same value go in **one** Jev request. `@over` classifies each distinct value once, caches it, and left-joins onto the frame.
58
+
59
+ ## Install
60
+
61
+ ```sh
62
+ pip install hunch-jev
63
+ # or from a clone:
64
+ pip install -e ".[dev]"
65
+ ```
66
+
67
+ Requires Python 3.10+. Set `TYPESAFE_API_KEY`. Do not put keys in source.
68
+
69
+ ```sh
70
+ pytest
71
+ ```
72
+
73
+ ## Verbs
74
+
75
+ | Call | Jev primitive | You get |
76
+ | --- | --- | --- |
77
+ | `ask(state, question, among=..., by=...)` | Choice | `.top`, `.top2`, `.p`, `.confidence`, `.shape` |
78
+ | `rate(state, question, levels)` | Score | `.score`, `.level`, `.shape` |
79
+ | `value.feels("...")` | Noul | truthy when P(yes) ≥ 0.5 |
80
+
81
+ `.shape` is **your** policy on the distribution, not a Jev field:
82
+
83
+ | Shape | Meaning |
84
+ | --- | --- |
85
+ | `sure` | One option dominates |
86
+ | `torn` | Two options are close |
87
+ | `lost` | Flat or weak evidence |
88
+
89
+ Cutoffs live on `ShapePolicy`. Confidence is how peaked the distribution is, not whether the label is true.
90
+
91
+ `connect(cache="~/.cache/hunch")` persists answers on disk. `jev.usage` reports calls, cache hits, tokens, and the model name.
92
+
93
+ ## LLM roles
94
+
95
+ A role may only propose text or a list of labels. `ask` still decides.
96
+
97
+ ```python
98
+ from hunch import ask, draft, openrouter, role
99
+
100
+ jev = hunch.connect(llm=openrouter()) # OPENROUTER_API_KEY
101
+ taxonomist = role(
102
+ "Propose 8–16 kebab-case folder names. Include junk. No review pile.",
103
+ emit=list[str],
104
+ )
105
+
106
+ with jev.session():
107
+ taxonomy = draft(listing, taxonomist).labels
108
+
109
+ folder = ask(name, "which folder?", among=taxonomy)
110
+ ```
111
+
112
+ `hunch.openai`, `hunch.cerebras`, and `hunch.openrouter` are OpenAI-compatible adapters. `via=` on a role overrides `llm=` on `connect()`. A role stops after `max_loops` (default 5) in one session.
113
+
114
+ ## Example
115
+
116
+ [`examples/organize_downloads.py`](examples/organize_downloads.py) files a Downloads folder: one LLM taxonomy, then Jev assigns each loose file, then the script moves. Destination folders are skipped on later runs. `--dry-run` prints the plan.
117
+
118
+ ## What this is not
119
+
120
+ Jev does not invent labels. `among=` is the whole set of allowed answers. Roles invent candidates; they have no tools and do not move files. Open-ended writing and multi-step agents are out of scope.
121
+
122
+ ## License
123
+
124
+ MIT. Jev and TypeSafe are [typesafe.ai](https://typesafe.ai); this library is not affiliated.
@@ -0,0 +1,218 @@
1
+ """Sort the Downloads folder: an LLM proposes folders, Jev assigns, this script moves."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import shutil
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ import pandas as pd
13
+
14
+ from hunch import ask, connect, draft, openrouter, over, role
15
+
16
+ SKIP_NAMES = {".DS_Store", ".localized", "$RECYCLE.BIN"}
17
+
18
+
19
+ def load_env(path: Path) -> None:
20
+ if not path.is_file():
21
+ return
22
+ for line in path.read_text().splitlines():
23
+ stripped = line.strip()
24
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
25
+ continue
26
+ key, value = stripped.split("=", 1)
27
+ os.environ.setdefault(key.strip(), value.strip())
28
+
29
+
30
+ def list_downloads(root: Path) -> pd.DataFrame:
31
+ rows: list[dict[str, object]] = []
32
+ for path in root.iterdir():
33
+ if path.name.startswith(".") or path.name in SKIP_NAMES:
34
+ continue
35
+ try:
36
+ info = path.stat()
37
+ except OSError:
38
+ continue
39
+ rows.append(
40
+ {
41
+ "name": path.name,
42
+ "kind": "dir" if path.is_dir() else "file",
43
+ "ext": "" if path.is_dir() else path.suffix.lower(),
44
+ "bytes": info.st_size,
45
+ "modified": datetime.fromtimestamp(info.st_mtime, tz=timezone.utc).isoformat(),
46
+ "path": str(path),
47
+ }
48
+ )
49
+ return pd.DataFrame(rows).sort_values("modified", ascending=False).reset_index(drop=True)
50
+
51
+
52
+ def unique_dest(folder: Path, name: str) -> Path:
53
+ dest = folder / name
54
+ if not dest.exists():
55
+ return dest
56
+ stem = Path(name).stem
57
+ suffix = Path(name).suffix
58
+ index = 1
59
+ while True:
60
+ candidate = folder / f"{stem} ({index}){suffix}"
61
+ if not candidate.exists():
62
+ return candidate
63
+ index += 1
64
+
65
+
66
+ def move_item(source: Path, dest: Path) -> Path:
67
+ dest.parent.mkdir(parents=True, exist_ok=True)
68
+ shutil.move(str(source), str(dest))
69
+ return dest
70
+
71
+
72
+ def main() -> None:
73
+ parser = argparse.ArgumentParser(description="Sort Downloads into Jev-chosen folders.")
74
+ parser.add_argument("--root", type=Path, default=Path.home() / "Downloads")
75
+ parser.add_argument("--limit", type=int, default=0, help="Newest items only. 0 = all.")
76
+ parser.add_argument("--workers", type=int, default=8)
77
+ parser.add_argument("--dry-run", action="store_true")
78
+ parser.add_argument("--refresh-taxonomy", action="store_true")
79
+ parser.add_argument(
80
+ "--cache",
81
+ type=Path,
82
+ default=Path.home() / ".cache" / "hunch" / "downloads",
83
+ )
84
+ parser.add_argument("--env", type=Path, default=Path(__file__).resolve().parents[1] / ".env")
85
+ args = parser.parse_args()
86
+
87
+ load_env(args.env)
88
+ items = list_downloads(args.root)
89
+ if items.empty:
90
+ raise SystemExit(f"No visible items in {args.root}")
91
+
92
+ listing = [
93
+ f"{row.kind}\t{row.ext}\t{row.name}"
94
+ for row in items.head(200).itertuples(index=False)
95
+ ]
96
+ jev = connect(llm=openrouter(model="z-ai/glm-5.3-flash"), cache=args.cache)
97
+ taxonomist = role(
98
+ "Propose 8–16 kebab-case folder names that cover this Downloads pile. "
99
+ "Group by meaning (work docs, personal paperwork, books, design assets, "
100
+ "installers, archives, media, project folders), not by file extension alone. "
101
+ "Include junk for disposable leftovers. Do not include review, other, or misc — "
102
+ "every item must have a real home. No duplicates.",
103
+ emit=list[str],
104
+ )
105
+
106
+ print(f"Listed {len(items)} top-level items in {args.root}", flush=True)
107
+ saved = None if args.refresh_taxonomy else _read_taxonomy(args.cache)
108
+ if saved:
109
+ taxonomy = saved
110
+ print("Using saved taxonomy:", ", ".join(taxonomy), flush=True)
111
+ else:
112
+ print("Asking OpenRouter for a taxonomy…", flush=True)
113
+ with jev.session():
114
+ taxonomy = draft(listing, taxonomist).labels
115
+ taxonomy = [label for label in taxonomy if label not in {"review", "other", "misc", "unsorted"}]
116
+ if "junk" not in taxonomy:
117
+ taxonomy.append("junk")
118
+ _write_taxonomy(args.cache, taxonomy)
119
+ print("Taxonomy:", ", ".join(taxonomy), flush=True)
120
+
121
+ dest_names = set(taxonomy)
122
+ movable = items[~items["name"].isin(dest_names) | (items["kind"] != "dir")].copy()
123
+ sample = movable if args.limit <= 0 else movable.head(args.limit)
124
+ if sample.empty:
125
+ print("Nothing new to file. Destination folders were left alone.")
126
+ print(f"Jev usage: {jev.usage.calls} calls, {jev.usage.hits} cache hits.")
127
+ return
128
+ lookup = {row["name"]: row for row in sample.to_dict(orient="records")}
129
+ print(f"Classifying {len(sample)} new items with Jev ({args.workers} workers)…", flush=True)
130
+
131
+ @over(sample, "name")
132
+ def classify(name):
133
+ row = lookup[str(name)]
134
+ state = {
135
+ "name": row["name"],
136
+ "kind": row["kind"],
137
+ "ext": row["ext"],
138
+ "bytes": row["bytes"],
139
+ "modified": row["modified"],
140
+ }
141
+ folder = ask(
142
+ state,
143
+ "Which folder should this Downloads item go in?",
144
+ among=taxonomy,
145
+ by=(
146
+ "Pick the best fitting folder even if the name is thin. "
147
+ "Camera-roll stills (IMG_, DSC_, screenshots) go in a photos or screenshots folder. "
148
+ "App disk images and installers go in installers. "
149
+ "Named project folders and their zips go in side-projects or dev-projects. "
150
+ "Use junk only for obvious leftovers or disposable exports. "
151
+ "Never pick a review, other, or unsorted pile."
152
+ ),
153
+ )
154
+ return {
155
+ "folder": folder.top,
156
+ "shape": folder.shape,
157
+ "confidence": round(folder.confidence, 3),
158
+ "p": round(folder.p, 3),
159
+ }
160
+
161
+ results = classify.run(
162
+ jev,
163
+ max_workers=args.workers,
164
+ on_item=lambda done, total, _value: print(f" classified {done}/{total}", flush=True)
165
+ if done == total or done % 25 == 0
166
+ else None,
167
+ )
168
+
169
+ moved = 0
170
+ skipped = 0
171
+ for row in results.itertuples(index=False):
172
+ folder = getattr(row, "folder", None)
173
+ if not isinstance(folder, str) or not folder:
174
+ skipped += 1
175
+ continue
176
+ source = Path(row.path)
177
+ if not source.exists():
178
+ skipped += 1
179
+ continue
180
+ dest_dir = args.root / folder
181
+ if source.resolve() == dest_dir.resolve():
182
+ skipped += 1
183
+ continue
184
+ dest = unique_dest(dest_dir, source.name)
185
+ print(f"{source.name} -> {folder}/{dest.name}", flush=True)
186
+ if not args.dry_run:
187
+ move_item(source, dest)
188
+ moved += 1
189
+
190
+ print()
191
+ print(results.groupby("folder").size().sort_values(ascending=False).to_string())
192
+ print()
193
+ action = "Would move" if args.dry_run else "Moved"
194
+ used = jev.usage
195
+ print(f"{action} {moved} items. Skipped {skipped}.")
196
+ print(
197
+ f"Jev {used.model or 'jev'}: {used.calls} calls, {used.hits} cache hits, "
198
+ f"{used.input_tokens} in / {used.output_tokens} out tokens."
199
+ )
200
+
201
+
202
+ def _read_taxonomy(cache: Path) -> list[str] | None:
203
+ path = cache / "taxonomy.json"
204
+ if not path.is_file():
205
+ return None
206
+ labels = json.loads(path.read_text())
207
+ if not isinstance(labels, list) or not all(isinstance(item, str) for item in labels):
208
+ return None
209
+ return labels
210
+
211
+
212
+ def _write_taxonomy(cache: Path, labels: list[str]) -> None:
213
+ cache.mkdir(parents=True, exist_ok=True)
214
+ (cache / "taxonomy.json").write_text(json.dumps(labels, indent=2) + "\n")
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "hunch-jev"
3
+ version = "0.1.0"
4
+ description = "Ask Jev over columns of data: closed-set questions, cached and joined back."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Steven Shoemaker" }]
9
+ dependencies = [
10
+ "pandas>=2.0",
11
+ "typesafe-sdk>=0.5.7",
12
+ ]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/steven-shoemaker/hunch"
16
+ Repository = "https://github.com/steven-shoemaker/hunch"
17
+
18
+ [project.optional-dependencies]
19
+ dev = ["pytest>=8.0"]
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/hunch"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ pythonpath = ["src", "."]
31
+
32
+ [tool.ruff]
33
+ target-version = "py310"
34
+ line-length = 100
@@ -0,0 +1,41 @@
1
+ """hunch — Jev as a column primitive, with optional LLM roles that only propose."""
2
+
3
+ from hunch.answer import Answer, Feeling, Rating
4
+ from hunch.client import Hunch, connect
5
+ from hunch.exceptions import HunchError, NoSessionError
6
+ from hunch.llm import LanguageModel, OpenAICompat, cerebras, openai, openrouter
7
+ from hunch.over import OverJob, over
8
+ from hunch.role import Draft, Role, draft, role
9
+ from hunch.session import ask, rate
10
+ from hunch.shapes import Shape, ShapePolicy
11
+ from hunch.subject import Subject
12
+ from hunch.usage import Usage
13
+
14
+ __all__ = [
15
+ "Answer",
16
+ "Draft",
17
+ "Feeling",
18
+ "Hunch",
19
+ "HunchError",
20
+ "LanguageModel",
21
+ "NoSessionError",
22
+ "OpenAICompat",
23
+ "OverJob",
24
+ "Rating",
25
+ "Role",
26
+ "Shape",
27
+ "ShapePolicy",
28
+ "Subject",
29
+ "Usage",
30
+ "ask",
31
+ "cerebras",
32
+ "connect",
33
+ "draft",
34
+ "openai",
35
+ "openrouter",
36
+ "over",
37
+ "rate",
38
+ "role",
39
+ ]
40
+
41
+ __version__ = "0.1.0"