thunc 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,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: pip install -e ".[anthropic,dev]"
20
+ - run: pytest # offline tests only; live_tests need a model and are not run in CI
21
+ - run: ruff check .
22
+ - run: ruff format --check .
23
+ - run: mypy --strict thunc
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs when you publish a GitHub release (e.g. tag v0.1.0). Uses PyPI Trusted Publishing:
4
+ # no API token is stored anywhere; PyPI trusts this workflow in this repo.
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.13"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write # lets PyPI verify this workflow; required for Trusted Publishing
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
thunc-0.1.0/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ .env
9
+ *.jsonl
thunc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hussein Eltarras
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.
thunc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: thunc
3
+ Version: 0.1.0
4
+ Summary: think + function: call an LLM like a typed Python function.
5
+ Project-URL: Repository, https://github.com/Eltarras/thunc
6
+ Project-URL: Issues, https://github.com/Eltarras/thunc/issues
7
+ Author: Hussein Eltarras
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,claude,llm,prompt,structured-output,typed
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Provides-Extra: anthropic
19
+ Requires-Dist: anthropic; extra == 'anthropic'
20
+ Provides-Extra: dev
21
+ Requires-Dist: basedpyright; extra == 'dev'
22
+ Requires-Dist: mypy; extra == 'dev'
23
+ Requires-Dist: pytest; extra == 'dev'
24
+ Requires-Dist: ruff; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # thunc
28
+
29
+ [![CI](https://github.com/Eltarras/thunc/actions/workflows/ci.yml/badge.svg)](https://github.com/Eltarras/thunc/actions/workflows/ci.yml)
30
+
31
+ **think + function.** Call an LLM like a typed Python function.
32
+
33
+ > **Status: beta (v0.1).** Expect bugs; the API may change. Feedback and issues are welcome.
34
+
35
+ ```python
36
+ import thunc
37
+
38
+ thunc.configure(backend="claude-code")
39
+
40
+
41
+ @thunc.function
42
+ def urgency(ticket: str) -> int:
43
+ """Rate how urgent this ticket is, from 1 (can wait) to 5 (customer is blocked)."""
44
+ ...
45
+
46
+
47
+ urgency("I was charged twice!") # -> 4, a checked int
48
+ ```
49
+
50
+ The answer is parsed into the declared type. If it doesn't fit, the model is asked again, and
51
+ after that `thunc.ThuncError` is raised. The library uses the standard library only and needs
52
+ Python 3.10+.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install thunc # standard library only
58
+ pip install "thunc[anthropic]" # adds the Claude API backend
59
+ ```
60
+
61
+ ## Try it
62
+
63
+ Clone the repo and run the examples from its root. No install is needed; the examples run through
64
+ your local [Claude Code](https://claude.com/claude-code) login:
65
+
66
+ ```bash
67
+ git clone https://github.com/Eltarras/thunc && cd thunc
68
+ python3 -m examples.hello
69
+ python3 -m examples.support_inbox
70
+ THUNC_BACKEND=codex python3 -m examples.log_triage
71
+ ```
72
+
73
+ ## Two ways to write a prompt
74
+
75
+ | | When | |
76
+ |---|---|---|
77
+ | `@thunc.function` | The prompt is fixed and should read like code | The docstring is the prompt, the parameters are the inputs, the return annotation is the type |
78
+ | `thunc.call(...)` | The prompt is built in code (from config, in a loop, loaded from a file) | `thunc.call(f"Translate into {lang}.", {"text": note})` |
79
+
80
+ `@thunc.function(instructions=some_string)` combines the two: a typed, reusable function whose
81
+ prompt is generated.
82
+
83
+ **Keep user data out of the instructions.** Your own text can go in the instructions string.
84
+ Anything from users, files or the web goes in the inputs:
85
+
86
+ - `@thunc.function` does this automatically.
87
+ - With `thunc.call` it's up to you. In a live test, a hostile email pasted in with an f-string
88
+ tricked the model 3 out of 3 times. Passed as an input, it failed 3 out of 3 times.
89
+
90
+ ## API
91
+
92
+ | | |
93
+ |---|---|
94
+ | `@thunc.function` | Turns a signature + docstring into an AI-backed function. Options: `instructions=`, `ensure=`, `retries=`, `backend=`, `model=`. The body must be empty (`...`); real code raises `TypeError`. `async def` works |
95
+ | `thunc.call(instructions, inputs=None, *, returns=str, ensure=None, retries=2, backend=None, model=None)` | One prompt. Inputs are sent separately from the instructions |
96
+ | `thunc.map(func, items, workers=8)` | Runs calls in parallel, keeping the input order. Each call takes 4–8s, so this is the main speed lever |
97
+ | `thunc.configure(backend=, api_key=, model=, timeout=, trace=)` | Process-wide settings. `trace="calls.jsonl"` logs every call |
98
+ | `thunc.ThuncError` | Raised when no valid answer arrives after the retries |
99
+
100
+ **Return types:** `str`, `bool`, `int`, `float`, `Literal[...]`, `list[T]`, `dict[str, T]`,
101
+ `T | None`, and dataclasses (built into real instances).
102
+
103
+ **`ensure=`** adds your own check, for example `ensure=lambda n: 1 <= n <= 5`. A failed check is
104
+ sent back to the model and retried.
105
+
106
+ **Backends:**
107
+ - `anthropic` is the Claude API: `configure(api_key=...)` or `ANTHROPIC_API_KEY`, plus
108
+ `pip install anthropic`.
109
+ - `claude-code` and `codex` call your local CLI login, and are meant for cheap testing.
110
+
111
+ The backend can also be set with `THUNC_BACKEND`.
112
+
113
+ **Type checking:** signatures and return types are visible to mypy and Pyright. mypy reports
114
+ empty bodies; turn that off with `disable_error_code = ["empty-body"]`.
115
+
116
+ ## Examples
117
+
118
+ | | |
119
+ |---|---|
120
+ | [hello.py](https://github.com/Eltarras/thunc/blob/main/examples/hello.py) | The smallest call |
121
+ | [support_inbox.py](https://github.com/Eltarras/thunc/blob/main/examples/support_inbox.py) | Docstring functions returning a `Literal`, an `int` with `ensure=`, a dataclass, and a reply; tickets processed in parallel |
122
+ | [dynamic_prompts.py](https://github.com/Eltarras/thunc/blob/main/examples/dynamic_prompts.py) | Prompts built from a style guide with `thunc.call`, and a grading function generated from a rubric |
123
+ | [log_triage.py](https://github.com/Eltarras/thunc/blob/main/examples/log_triage.py) | Plain Python and AI functions mixed, with tracing |
124
+
125
+ ## Code
126
+
127
+ ```
128
+ thunc/
129
+ __init__.py public API
130
+ decorator.py @thunc.function
131
+ core.py thunc.call, thunc.map, tracing
132
+ schema.py return types: describe, parse, validate
133
+ config.py settings and backend selection
134
+ backends.py anthropic, claude-code, codex
135
+ errors.py ThuncError
136
+ tests/ offline: a fake backend, never a real model
137
+ live_tests/ against a real model: hello, a yes/no decision, messy text to a dict
138
+ examples/
139
+ ```
140
+
141
+ ## Limitations
142
+
143
+ - **There's no caching and no record/replay yet**, so repeated calls cost again.
144
+ - **The API-key backend hasn't been run live yet.** It's only checked against the SDK's types.
145
+ - **`Literal` results from `thunc.call` are typed as `Any`.** `@thunc.function` has no such gap.
146
+ - **Docstrings disappear under `python -OO`.** Use `instructions=` there.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ python3 -m venv .venv && .venv/bin/pip install -e ".[anthropic,dev]"
152
+ .venv/bin/pytest # offline tests (these run in CI)
153
+ .venv/bin/pytest live_tests # real model calls through your Claude Code login; costs quota
154
+ .venv/bin/ruff check . && .venv/bin/mypy --strict thunc
155
+ ```
156
+
157
+ ## License
158
+
159
+ [MIT](https://github.com/Eltarras/thunc/blob/main/LICENSE)
thunc-0.1.0/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # thunc
2
+
3
+ [![CI](https://github.com/Eltarras/thunc/actions/workflows/ci.yml/badge.svg)](https://github.com/Eltarras/thunc/actions/workflows/ci.yml)
4
+
5
+ **think + function.** Call an LLM like a typed Python function.
6
+
7
+ > **Status: beta (v0.1).** Expect bugs; the API may change. Feedback and issues are welcome.
8
+
9
+ ```python
10
+ import thunc
11
+
12
+ thunc.configure(backend="claude-code")
13
+
14
+
15
+ @thunc.function
16
+ def urgency(ticket: str) -> int:
17
+ """Rate how urgent this ticket is, from 1 (can wait) to 5 (customer is blocked)."""
18
+ ...
19
+
20
+
21
+ urgency("I was charged twice!") # -> 4, a checked int
22
+ ```
23
+
24
+ The answer is parsed into the declared type. If it doesn't fit, the model is asked again, and
25
+ after that `thunc.ThuncError` is raised. The library uses the standard library only and needs
26
+ Python 3.10+.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install thunc # standard library only
32
+ pip install "thunc[anthropic]" # adds the Claude API backend
33
+ ```
34
+
35
+ ## Try it
36
+
37
+ Clone the repo and run the examples from its root. No install is needed; the examples run through
38
+ your local [Claude Code](https://claude.com/claude-code) login:
39
+
40
+ ```bash
41
+ git clone https://github.com/Eltarras/thunc && cd thunc
42
+ python3 -m examples.hello
43
+ python3 -m examples.support_inbox
44
+ THUNC_BACKEND=codex python3 -m examples.log_triage
45
+ ```
46
+
47
+ ## Two ways to write a prompt
48
+
49
+ | | When | |
50
+ |---|---|---|
51
+ | `@thunc.function` | The prompt is fixed and should read like code | The docstring is the prompt, the parameters are the inputs, the return annotation is the type |
52
+ | `thunc.call(...)` | The prompt is built in code (from config, in a loop, loaded from a file) | `thunc.call(f"Translate into {lang}.", {"text": note})` |
53
+
54
+ `@thunc.function(instructions=some_string)` combines the two: a typed, reusable function whose
55
+ prompt is generated.
56
+
57
+ **Keep user data out of the instructions.** Your own text can go in the instructions string.
58
+ Anything from users, files or the web goes in the inputs:
59
+
60
+ - `@thunc.function` does this automatically.
61
+ - With `thunc.call` it's up to you. In a live test, a hostile email pasted in with an f-string
62
+ tricked the model 3 out of 3 times. Passed as an input, it failed 3 out of 3 times.
63
+
64
+ ## API
65
+
66
+ | | |
67
+ |---|---|
68
+ | `@thunc.function` | Turns a signature + docstring into an AI-backed function. Options: `instructions=`, `ensure=`, `retries=`, `backend=`, `model=`. The body must be empty (`...`); real code raises `TypeError`. `async def` works |
69
+ | `thunc.call(instructions, inputs=None, *, returns=str, ensure=None, retries=2, backend=None, model=None)` | One prompt. Inputs are sent separately from the instructions |
70
+ | `thunc.map(func, items, workers=8)` | Runs calls in parallel, keeping the input order. Each call takes 4–8s, so this is the main speed lever |
71
+ | `thunc.configure(backend=, api_key=, model=, timeout=, trace=)` | Process-wide settings. `trace="calls.jsonl"` logs every call |
72
+ | `thunc.ThuncError` | Raised when no valid answer arrives after the retries |
73
+
74
+ **Return types:** `str`, `bool`, `int`, `float`, `Literal[...]`, `list[T]`, `dict[str, T]`,
75
+ `T | None`, and dataclasses (built into real instances).
76
+
77
+ **`ensure=`** adds your own check, for example `ensure=lambda n: 1 <= n <= 5`. A failed check is
78
+ sent back to the model and retried.
79
+
80
+ **Backends:**
81
+ - `anthropic` is the Claude API: `configure(api_key=...)` or `ANTHROPIC_API_KEY`, plus
82
+ `pip install anthropic`.
83
+ - `claude-code` and `codex` call your local CLI login, and are meant for cheap testing.
84
+
85
+ The backend can also be set with `THUNC_BACKEND`.
86
+
87
+ **Type checking:** signatures and return types are visible to mypy and Pyright. mypy reports
88
+ empty bodies; turn that off with `disable_error_code = ["empty-body"]`.
89
+
90
+ ## Examples
91
+
92
+ | | |
93
+ |---|---|
94
+ | [hello.py](https://github.com/Eltarras/thunc/blob/main/examples/hello.py) | The smallest call |
95
+ | [support_inbox.py](https://github.com/Eltarras/thunc/blob/main/examples/support_inbox.py) | Docstring functions returning a `Literal`, an `int` with `ensure=`, a dataclass, and a reply; tickets processed in parallel |
96
+ | [dynamic_prompts.py](https://github.com/Eltarras/thunc/blob/main/examples/dynamic_prompts.py) | Prompts built from a style guide with `thunc.call`, and a grading function generated from a rubric |
97
+ | [log_triage.py](https://github.com/Eltarras/thunc/blob/main/examples/log_triage.py) | Plain Python and AI functions mixed, with tracing |
98
+
99
+ ## Code
100
+
101
+ ```
102
+ thunc/
103
+ __init__.py public API
104
+ decorator.py @thunc.function
105
+ core.py thunc.call, thunc.map, tracing
106
+ schema.py return types: describe, parse, validate
107
+ config.py settings and backend selection
108
+ backends.py anthropic, claude-code, codex
109
+ errors.py ThuncError
110
+ tests/ offline: a fake backend, never a real model
111
+ live_tests/ against a real model: hello, a yes/no decision, messy text to a dict
112
+ examples/
113
+ ```
114
+
115
+ ## Limitations
116
+
117
+ - **There's no caching and no record/replay yet**, so repeated calls cost again.
118
+ - **The API-key backend hasn't been run live yet.** It's only checked against the SDK's types.
119
+ - **`Literal` results from `thunc.call` are typed as `Any`.** `@thunc.function` has no such gap.
120
+ - **Docstrings disappear under `python -OO`.** Use `instructions=` there.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ python3 -m venv .venv && .venv/bin/pip install -e ".[anthropic,dev]"
126
+ .venv/bin/pytest # offline tests (these run in CI)
127
+ .venv/bin/pytest live_tests # real model calls through your Claude Code login; costs quota
128
+ .venv/bin/ruff check . && .venv/bin/mypy --strict thunc
129
+ ```
130
+
131
+ ## License
132
+
133
+ [MIT](https://github.com/Eltarras/thunc/blob/main/LICENSE)
@@ -0,0 +1,63 @@
1
+ """String prompts: instructions built in code from data.
2
+
3
+ Rule of thumb: your own text (config, rubric) may go in the instructions; user content goes in
4
+ the inputs, never into the instructions through an f-string.
5
+
6
+ Run from the repo root: python3 -m examples.dynamic_prompts
7
+ """
8
+
9
+ import os
10
+ from dataclasses import dataclass
11
+
12
+ import thunc
13
+
14
+ thunc.configure(backend=os.environ.get("THUNC_BACKEND", "claude-code"))
15
+
16
+ # 1. thunc.call: one prompt per target language, assembled from a style guide.
17
+
18
+ STYLE_GUIDES = {
19
+ "Dutch": "Use informal 'je', not 'u'. Keep product names in English.",
20
+ "German": "Use formal 'Sie'. Keep product names in English.",
21
+ "French": "Use 'vous'. Put a non-breaking space before ! and ?.",
22
+ }
23
+ release_note = "New: export your dashboards to PDF! Find it under Share → Export."
24
+
25
+ for language, rules in STYLE_GUIDES.items():
26
+ translated = thunc.call(
27
+ f"Translate the release note into {language}. Style rules: {rules}", {"release_note": release_note}
28
+ )
29
+ print(f"{language:7} {translated}")
30
+
31
+
32
+ # 2. @thunc.function(instructions=...): a typed, reusable function whose prompt comes from data.
33
+
34
+
35
+ @dataclass
36
+ class Grade:
37
+ score: int
38
+ feedback: str
39
+
40
+
41
+ QUESTION = "Why does HTTPS protect against eavesdropping?"
42
+ CRITERIA = [
43
+ "mentions encryption of the traffic",
44
+ "mentions that the server's identity is verified with a certificate",
45
+ "is at most three sentences",
46
+ ]
47
+ rubric = "\n".join(f"- {c}" for c in CRITERIA)
48
+
49
+
50
+ @thunc.function(
51
+ instructions=f"Grade the student's answer to: {QUESTION}\nAward one point per criterion met:\n{rubric}\n"
52
+ "Give one sentence of feedback naming what is missing, if anything.",
53
+ ensure=lambda g: 0 <= g.score <= len(CRITERIA),
54
+ )
55
+ def grade(answer: str) -> Grade: ...
56
+
57
+
58
+ answers = [
59
+ "Because the data is encrypted so nobody in between can read it.",
60
+ "TLS encrypts traffic, and the certificate proves you're talking to the real server.",
61
+ ]
62
+ for answer, result in zip(answers, thunc.map(grade, answers), strict=True):
63
+ print(f"\n{result.score}/{len(CRITERIA)} {answer}\n {result.feedback}")
@@ -0,0 +1,6 @@
1
+ """The smallest call. Run from the repo root: python3 -m examples.hello"""
2
+
3
+ import thunc
4
+
5
+ thunc.configure(backend="claude-code")
6
+ print(thunc.call("Say hello in five words or fewer."))
@@ -0,0 +1,60 @@
1
+ """Plain Python and AI functions together: triage the errors in a service log, with tracing on.
2
+
3
+ Run from the repo root: python3 -m examples.log_triage
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ import tempfile
10
+ from collections import Counter
11
+ from typing import Literal
12
+
13
+ import thunc
14
+
15
+ trace_file = os.path.join(tempfile.gettempdir(), "thunc_log_triage.jsonl")
16
+ if os.path.exists(trace_file):
17
+ os.remove(trace_file)
18
+ thunc.configure(backend=os.environ.get("THUNC_BACKEND", "claude-code"), trace=trace_file)
19
+
20
+ LOG = """\
21
+ 2026-10-03 09:12:01 INFO api request GET /health 200
22
+ 2026-10-03 09:12:04 ERROR api psycopg.OperationalError: connection to server at "10.0.3.7", port 5432 failed: timeout
23
+ 2026-10-03 09:12:09 ERROR api psycopg.OperationalError: connection to server at "10.0.3.7", port 5432 failed: timeout
24
+ 2026-10-03 09:13:30 ERROR worker KeyError: 'customer_id' in invoices/render.py line 88
25
+ 2026-10-03 09:15:47 ERROR api ssl.SSLCertVerificationError: certificate has expired (hostname='payments.example.com')
26
+ 2026-10-03 09:16:12 ERROR worker KeyError: 'customer_id' in invoices/render.py line 88
27
+ """
28
+
29
+
30
+ @thunc.function
31
+ def owner(error: str) -> Literal["database", "application-code", "certificates", "network", "unknown"]:
32
+ """Which area most likely needs to act on this production error?"""
33
+ ...
34
+
35
+
36
+ @thunc.function
37
+ def first_step(error: str, owner: str) -> str:
38
+ """Suggest the single most useful first debugging step for this error, in one sentence."""
39
+ ...
40
+
41
+
42
+ # Plain Python does the cheap part: find and count the unique errors.
43
+ errors = Counter(m[1] for m in re.finditer(r"ERROR \w+\s+(.*)", LOG))
44
+
45
+
46
+ def analyse(error: str) -> tuple[str, str]:
47
+ area = owner(error)
48
+ return area, first_step(error, area)
49
+
50
+
51
+ for error, (area, step) in zip(errors, thunc.map(analyse, list(errors)), strict=True):
52
+ print(f"{errors[error]}x [{area}] {error[:70]}\n → {step}")
53
+
54
+ # The trace has one JSON line per call: inputs, raw answers, attempts, timing.
55
+ calls = [json.loads(line) for line in open(trace_file, encoding="utf-8")]
56
+ retried = sum(c["attempts"] > 1 for c in calls)
57
+ print(
58
+ f"\n{len(calls)} calls traced to {trace_file}; {retried} needed a retry; "
59
+ f"slowest {max(c['seconds'] for c in calls):.1f}s"
60
+ )
@@ -0,0 +1,63 @@
1
+ """Docstring prompts: triage a support inbox.
2
+
3
+ Run from the repo root: python3 -m examples.support_inbox
4
+ """
5
+
6
+ import os
7
+ from dataclasses import dataclass
8
+ from typing import Literal
9
+
10
+ import thunc
11
+
12
+ thunc.configure(backend=os.environ.get("THUNC_BACKEND", "claude-code"))
13
+
14
+
15
+ @dataclass
16
+ class Order:
17
+ order_id: str
18
+ product: str | None = None
19
+ amount_eur: float | None = None
20
+
21
+
22
+ @thunc.function
23
+ def category(ticket: str) -> Literal["bug", "billing", "feature-request", "other"]:
24
+ """Classify this customer support ticket."""
25
+ ...
26
+
27
+
28
+ @thunc.function(ensure=lambda n: 1 <= n <= 5)
29
+ def urgency(ticket: str) -> int:
30
+ """Rate how urgent this ticket is, from 1 (can wait a week) to 5 (customer is blocked right now)."""
31
+ ...
32
+
33
+
34
+ @thunc.function
35
+ def find_order(ticket: str) -> Order | None:
36
+ """Extract the order this ticket is about, or null if it doesn't mention one."""
37
+ ...
38
+
39
+
40
+ @thunc.function
41
+ def draft_reply(ticket: str, tone: str = "friendly and concise") -> str:
42
+ """Write a short first reply to this ticket in the given tone. Never promise a refund."""
43
+ ...
44
+
45
+
46
+ tickets = [
47
+ "I was charged twice for order #A-1042 (Pro plan, €49). Please fix this today!",
48
+ "It would be great if the dashboard had a dark mode.",
49
+ "Export to CSV crashes with 'unexpected token' since this morning's update.",
50
+ ]
51
+
52
+
53
+ def triage(ticket: str) -> tuple[str, int, Order | None]:
54
+ return category(ticket), urgency(ticket), find_order(ticket)
55
+
56
+
57
+ # The tickets are triaged in parallel; each ticket's three calls run in sequence.
58
+ for ticket, (kind, level, order) in zip(tickets, thunc.map(triage, tickets), strict=True):
59
+ print(f"[{kind:15}] urgency {level} {ticket}")
60
+ if order:
61
+ print(f"{'':19}order: {order}")
62
+
63
+ print("\nDraft reply to the first ticket:\n" + draft_reply(tickets[0]))
@@ -0,0 +1,25 @@
1
+ """Live tests call a real model, so they cost quota and answers can vary. Plain `pytest` skips them.
2
+
3
+ pytest live_tests # through your local Claude Code login
4
+ THUNC_BACKEND=codex pytest live_tests
5
+ THUNC_BACKEND=anthropic pytest live_tests # needs ANTHROPIC_API_KEY and `pip install anthropic`
6
+ """
7
+
8
+ import os
9
+ import shutil
10
+
11
+ import pytest
12
+
13
+ import thunc
14
+
15
+ BACKEND = os.environ.get("THUNC_BACKEND", "claude-code")
16
+ CLI = {"claude-code": "claude", "codex": "codex"}
17
+
18
+
19
+ @pytest.fixture(autouse=True, scope="session")
20
+ def live_backend():
21
+ if BACKEND in CLI and shutil.which(CLI[BACKEND]) is None:
22
+ pytest.skip(f"the `{CLI[BACKEND]}` CLI is not installed")
23
+ if BACKEND == "anthropic" and not os.environ.get("ANTHROPIC_API_KEY"):
24
+ pytest.skip("ANTHROPIC_API_KEY is not set")
25
+ thunc.configure(backend=BACKEND)
@@ -0,0 +1,22 @@
1
+ import thunc
2
+
3
+
4
+ @thunc.function
5
+ def bool_decision() -> bool:
6
+ """is 1 =3?"""
7
+ ...
8
+
9
+
10
+ @thunc.function
11
+ def is_true(statement: str) -> bool:
12
+ """Is this statement true?"""
13
+ ...
14
+
15
+
16
+ def test_false_statement():
17
+ assert bool_decision() is False
18
+
19
+
20
+ def test_true_statement():
21
+ # Without a true case, a model that always answers False would pass.
22
+ assert is_true("2 + 2 = 4") is True
@@ -0,0 +1,35 @@
1
+ """Raw, messy text in; a dict out. The log mixes decimal commas (42,50), "€134,-" for 134.00,
2
+ a "2x ... each" multiplication, and several date and currency spellings."""
3
+
4
+ import pytest
5
+
6
+ import thunc
7
+
8
+ RAW_EXPENSES = """\
9
+ 03/09 - Uber airport -> hotel €42,50 (Sara)
10
+ lunch @ Bistro Amsterdam: 18.90 EUR - Tom
11
+ 2026-09-03 coffee x3 7.20
12
+ Sept 4th, train tickets AMS-RTM 2x €16.80 each
13
+ hotel 2 nights 289.00 eur paid by company card
14
+ 4/9 dinner w/ client €134,-
15
+ taxi back 38 euro (receipt lost)
16
+ coffee 2,40
17
+ Train RTM-AMS €33.60 total
18
+ 04-09 snacks for the booth: €12.35
19
+ """
20
+
21
+ # transport: 42.50 + 2 x 16.80 + 38.00 + 33.60
22
+ # food: 18.90 + 7.20 + 134.00 + 2.40 + 12.35
23
+ # lodging: 289.00
24
+ EXPECTED = {"transport": 147.70, "food": 174.85, "lodging": 289.00}
25
+
26
+
27
+ @thunc.function(ensure=lambda d: set(d) == {"transport", "food", "lodging"})
28
+ def totals_by_category(expenses: str) -> dict[str, float]:
29
+ """Total these trip expenses per category: transport, food and lodging. All amounts are in euros."""
30
+ ...
31
+
32
+
33
+ def test_totals_by_category():
34
+ totals = totals_by_category(RAW_EXPENSES)
35
+ assert totals == pytest.approx(EXPECTED, abs=0.01)
@@ -0,0 +1,13 @@
1
+ import thunc
2
+
3
+
4
+ @thunc.function
5
+ def hello():
6
+ """Hello"""
7
+ ...
8
+
9
+
10
+ def test_hello_returns_text():
11
+ reply = hello()
12
+ assert isinstance(reply, str)
13
+ assert reply.strip()