Javier Montalvo commited on
Commit ·
2a9b8d1
1
Parent(s): f005306
Added more providers, changed entrypoint, removed local llama for now
Browse files- DEMO.md +112 -0
- README.md +20 -21
- frontend/.gitignore +0 -1
- frontend/dist/assets/__vite-browser-external-DYxpcVy9-BIHI7g3E.js +1 -0
- frontend/dist/assets/index-Cs8-YHSt.js +0 -0
- frontend/dist/assets/index-DpGaJEG3.css +1 -0
- frontend/dist/index.html +25 -0
- frontend/src/lib/api.ts +15 -6
- frontend/src/lib/dashboard.tsx +55 -9
- frontend/src/lib/types.ts +12 -3
- frontend/src/modules/rules/RuleStudioPanel.tsx +4 -23
- frontend/src/modules/settings/SettingsPanel.tsx +89 -11
- poetry.lock +207 -107
- pyproject.toml +3 -2
- requirements.txt +2 -0
- server.py +39 -17
- tests/test_automation.py +29 -0
- tests/test_llm.py +230 -58
- tests/test_store.py +6 -2
- tiny_trigger/__init__.py +13 -2
- tiny_trigger/automation.py +3 -3
- tiny_trigger/llm.py +250 -69
- tiny_trigger/store.py +4 -2
DEMO.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tiny Trigger Demo Runbook
|
| 2 |
+
|
| 3 |
+
Use this if you need to run the demo without rebuilding context.
|
| 4 |
+
|
| 5 |
+
## Launch
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install -r requirements.txt
|
| 9 |
+
python server.py
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
Or:
|
| 13 |
+
|
| 14 |
+
```bash
|
| 15 |
+
poetry install
|
| 16 |
+
poetry run python server.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
Open:
|
| 20 |
+
|
| 21 |
+
```text
|
| 22 |
+
http://127.0.0.1:7860
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
For Hugging Face Spaces, the entrypoint is `server.py`; it serves the already
|
| 26 |
+
built frontend from `frontend/dist`.
|
| 27 |
+
|
| 28 |
+
## API Keys
|
| 29 |
+
|
| 30 |
+
Open Settings, choose one compiler provider, and paste its API key:
|
| 31 |
+
|
| 32 |
+
- Replicate: `REPLICATE_API_TOKEN`
|
| 33 |
+
- OpenAI: `OPENAI_API_KEY`
|
| 34 |
+
- Claude: `ANTHROPIC_API_KEY`
|
| 35 |
+
|
| 36 |
+
Keys pasted in the UI are only sent with compile requests. If you own the Space,
|
| 37 |
+
set the matching variable as a Space secret instead.
|
| 38 |
+
|
| 39 |
+
## Demo Video
|
| 40 |
+
|
| 41 |
+
No demo video is checked in yet. Record a short MP4 or MOV with:
|
| 42 |
+
|
| 43 |
+
- one obvious person or hand
|
| 44 |
+
- one clear object such as guitar, monitor, steering wheel, cup, laptop, chair
|
| 45 |
+
- at least one moment where the object appears or disappears
|
| 46 |
+
- 5-15 seconds is enough
|
| 47 |
+
|
| 48 |
+
The detector downloads YOLOE weights on first use, so the first run can be slow.
|
| 49 |
+
|
| 50 |
+
Recommended starting detector settings:
|
| 51 |
+
|
| 52 |
+
- Classes: leave defaults, or add obvious objects from the video.
|
| 53 |
+
- Confidence: `0.15` for normal objects, `0.05` if detections are missing.
|
| 54 |
+
- Frame stride: `5`.
|
| 55 |
+
- Max frames: `120`.
|
| 56 |
+
- Model: `yoloe-26s-seg.pt` for speed, `yoloe-26l-seg.pt` for a stronger demo.
|
| 57 |
+
|
| 58 |
+
Rules automatically add their referenced labels to the detector class list.
|
| 59 |
+
|
| 60 |
+
## Prompts To Try
|
| 61 |
+
|
| 62 |
+
State assertion, should fire once when true:
|
| 63 |
+
|
| 64 |
+
```text
|
| 65 |
+
While there is a guitar in the scene, amplifier must be on.
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Enter/exit behavior:
|
| 69 |
+
|
| 70 |
+
```text
|
| 71 |
+
If a person is near the monitor, turn on the LED lights. When the person leaves, turn them off.
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Cooldown behavior:
|
| 75 |
+
|
| 76 |
+
```text
|
| 77 |
+
If a person is near the steering wheel, turn on the PC. Do not repeat for five minutes.
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Simple presence:
|
| 81 |
+
|
| 82 |
+
```text
|
| 83 |
+
When a laptop is visible, notify me.
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
## Expected Flow
|
| 87 |
+
|
| 88 |
+
1. Upload the video in Detector.
|
| 89 |
+
2. Go to Settings and choose a cloud compiler provider.
|
| 90 |
+
3. Paste the API key if the Space has no secret configured.
|
| 91 |
+
4. Go to Rule Studio, Compose.
|
| 92 |
+
5. Compile one of the prompts above.
|
| 93 |
+
6. Check Source to see the generated rule document.
|
| 94 |
+
7. Check Rules to see enabled rules and label pills.
|
| 95 |
+
8. Run detection.
|
| 96 |
+
9. Watch Activity & Firings for fired actions.
|
| 97 |
+
10. Use the replay to inspect detections and event timing.
|
| 98 |
+
|
| 99 |
+
## What To Say
|
| 100 |
+
|
| 101 |
+
Tiny Trigger turns natural language video automation ideas into validated rules.
|
| 102 |
+
The LLM only writes JSON/YAML, never executable code. YOLOE searches both user
|
| 103 |
+
classes and labels referenced by active rules. Rules use edge triggers by
|
| 104 |
+
default, so "turn on when present" does not spam every frame.
|
| 105 |
+
|
| 106 |
+
## If Something Fails
|
| 107 |
+
|
| 108 |
+
- No compile: check the provider and API key.
|
| 109 |
+
- No detections: lower confidence, add labels manually, or try a larger YOLOE model.
|
| 110 |
+
- Slow first run: model weights are downloading.
|
| 111 |
+
- Repeated actions: check the rule trigger. For most demos, prefer `enter` or `change`.
|
| 112 |
+
- Missing frontend in a Space: make sure `frontend/dist` is included in the upload.
|
README.md
CHANGED
|
@@ -6,10 +6,10 @@ colorTo: gray
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.17.3
|
| 8 |
python_version: '3.13'
|
| 9 |
-
app_file:
|
| 10 |
pinned: false
|
| 11 |
license: other
|
| 12 |
-
short_description: 'Open-vocabulary video automations with YOLOE and
|
| 13 |
---
|
| 14 |
|
| 15 |
# Tiny Trigger
|
|
@@ -19,26 +19,29 @@ automations. It uses YOLOE to detect user-supplied classes plus every label
|
|
| 19 |
referenced by enabled rules in a video, then evaluates small structured rules
|
| 20 |
that can trigger simulated actions or optional webhook POSTs.
|
| 21 |
|
| 22 |
-
The LLM path is intentionally constrained:
|
| 23 |
-
into JSON/YAML automation rules. The app validates those rules
|
| 24 |
-
them, and the LLM never emits executable code.
|
|
|
|
|
|
|
| 25 |
|
| 26 |
## Run Locally
|
| 27 |
|
| 28 |
```bash
|
| 29 |
pip install -r requirements.txt
|
| 30 |
-
python
|
| 31 |
```
|
| 32 |
|
| 33 |
Or with Poetry:
|
| 34 |
|
| 35 |
```bash
|
| 36 |
poetry install
|
| 37 |
-
poetry run python
|
| 38 |
```
|
| 39 |
|
| 40 |
The detector defaults to `yoloe-26s-seg.pt`, which Ultralytics downloads on
|
| 41 |
-
first use if it is not already cached.
|
|
|
|
| 42 |
|
| 43 |
For small or background objects, use a larger model such as `yoloe-26l-seg.pt`,
|
| 44 |
set device to `cuda:0`, lower confidence to `0.05`-`0.15`, and raise image size
|
|
@@ -57,19 +60,12 @@ Or:
|
|
| 57 |
poetry run pytest
|
| 58 |
```
|
| 59 |
|
| 60 |
-
##
|
| 61 |
-
|
| 62 |
-
Start a llama.cpp server with the small GGUF model:
|
| 63 |
-
|
| 64 |
-
```bash
|
| 65 |
-
llama-server -hf ggml-org/Qwen3-1.7B-GGUF:Q4_K_M
|
| 66 |
-
```
|
| 67 |
-
|
| 68 |
-
Then use the default compiler endpoint in the UI:
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
```
|
|
|
|
| 73 |
|
| 74 |
## Rule Shape
|
| 75 |
|
|
@@ -138,7 +134,10 @@ default_detector_model: "yoloe-26x-seg.pt"
|
|
| 138 |
default_device: "cuda:0"
|
| 139 |
default_image_size: 1280
|
| 140 |
default_max_detections: 300
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
| 142 |
```
|
| 143 |
|
| 144 |
## License
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.17.3
|
| 8 |
python_version: '3.13'
|
| 9 |
+
app_file: server.py
|
| 10 |
pinned: false
|
| 11 |
license: other
|
| 12 |
+
short_description: 'Open-vocabulary video automations with YOLOE and cloud rule compilers'
|
| 13 |
---
|
| 14 |
|
| 15 |
# Tiny Trigger
|
|
|
|
| 19 |
referenced by enabled rules in a video, then evaluates small structured rules
|
| 20 |
that can trigger simulated actions or optional webhook POSTs.
|
| 21 |
|
| 22 |
+
The LLM path is intentionally constrained: Replicate, OpenAI, or Claude compile
|
| 23 |
+
natural language into JSON/YAML automation rules. The app validates those rules
|
| 24 |
+
before evaluating them, and the LLM never emits executable code.
|
| 25 |
+
|
| 26 |
+
For a quick operator checklist, see [`DEMO.md`](DEMO.md).
|
| 27 |
|
| 28 |
## Run Locally
|
| 29 |
|
| 30 |
```bash
|
| 31 |
pip install -r requirements.txt
|
| 32 |
+
python server.py
|
| 33 |
```
|
| 34 |
|
| 35 |
Or with Poetry:
|
| 36 |
|
| 37 |
```bash
|
| 38 |
poetry install
|
| 39 |
+
poetry run python server.py
|
| 40 |
```
|
| 41 |
|
| 42 |
The detector defaults to `yoloe-26s-seg.pt`, which Ultralytics downloads on
|
| 43 |
+
first use if it is not already cached. Model weights are not checked into the
|
| 44 |
+
repo.
|
| 45 |
|
| 46 |
For small or background objects, use a larger model such as `yoloe-26l-seg.pt`,
|
| 47 |
set device to `cuda:0`, lower confidence to `0.05`-`0.15`, and raise image size
|
|
|
|
| 60 |
poetry run pytest
|
| 61 |
```
|
| 62 |
|
| 63 |
+
## Cloud Rule Compiler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
Choose Replicate, OpenAI, or Claude in Settings and paste the matching API key.
|
| 66 |
+
Keys entered in the UI are sent only with compile requests. Hugging Face Space
|
| 67 |
+
owners can also set `REPLICATE_API_TOKEN`, `OPENAI_API_KEY`, or
|
| 68 |
+
`ANTHROPIC_API_KEY` as Space secrets.
|
| 69 |
|
| 70 |
## Rule Shape
|
| 71 |
|
|
|
|
| 134 |
default_device: "cuda:0"
|
| 135 |
default_image_size: 1280
|
| 136 |
default_max_detections: 300
|
| 137 |
+
llm_provider: "anthropic"
|
| 138 |
+
replicate_model: "openai/gpt-5.2"
|
| 139 |
+
openai_model: "gpt-5.5"
|
| 140 |
+
anthropic_model: "claude-sonnet-4-6"
|
| 141 |
```
|
| 142 |
|
| 143 |
## License
|
frontend/.gitignore
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
node_modules
|
| 2 |
-
dist
|
| 3 |
*.local
|
| 4 |
.vite
|
|
|
|
| 1 |
node_modules
|
|
|
|
| 2 |
*.local
|
| 3 |
.vite
|
frontend/dist/assets/__vite-browser-external-DYxpcVy9-BIHI7g3E.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
const e={};export{e as default};
|
frontend/dist/assets/index-Cs8-YHSt.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/index-DpGaJEG3.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-xl:24px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-background:var(--background);--color-foreground:var(--foreground);--color-primary:var(--primary);--color-border:var(--border);--font-display:var(--font-display)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--color-border)}html{-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility}body{background-color:var(--color-background);min-height:100vh;color:var(--color-foreground);font-family:var(--font-sans);font-feature-settings:"ss01","cv01","tnum";background-image:radial-gradient(90rem 60rem at 82% -10%,#c5ec4512,#0000 60%),radial-gradient(70rem 50rem at -10% 110%,#4cc8ee0f,#0000 55%),linear-gradient(90deg,#ffffff05 1px,#0000 1px),linear-gradient(#ffffff05 1px,#0000 1px);background-size:100% 100%,100% 100%,56px 56px,56px 56px;background-attachment:fixed;margin:0}::selection{background:#c5ec4547}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:#ffffff1a padding-box content-box;border:2px solid #0000;border-radius:999px}::-webkit-scrollbar-thumb:hover{background:#ffffff2e padding-box content-box}}@layer components;@layer utilities{.\@container{container-type:inline-size}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.not-sr-only{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.-top-0,.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-10{z-index:10}.z-30{z-index:30}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-video{aspect-ratio:var(--aspect-video)}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-full{width:100%;height:100%}.h-1{height:calc(var(--spacing) * 1)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-16{height:calc(var(--spacing) * 16)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-28{max-height:calc(var(--spacing) * 28)}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-64{min-height:calc(var(--spacing) * 64)}.min-h-screen{min-height:100vh}.w-0\.5{width:calc(var(--spacing) * .5)}.w-9{width:calc(var(--spacing) * 9)}.w-16{width:calc(var(--spacing) * 16)}.w-full{width:100%}.w-px{width:1px}.max-w-\[1500px\]{max-width:1500px}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-\[2\]{flex:2}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-none{translate:none}.scale-3d{scale:var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.scroll-mt-20{scroll-margin-top:calc(var(--spacing) * 20)}.scroll-mt-24{scroll-margin-top:calc(var(--spacing) * 24)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-reverse>:not(:last-child)){--tw-space-y-reverse:1}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.space-x-reverse>:not(:last-child)){--tw-space-x-reverse:1}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.\!rounded-lg{border-radius:var(--radius)!important}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.rounded-s{border-start-start-radius:.25rem;border-end-start-radius:.25rem}.rounded-ss{border-start-start-radius:.25rem}.rounded-e{border-start-end-radius:.25rem;border-end-end-radius:.25rem}.rounded-se{border-start-end-radius:.25rem}.rounded-ee{border-end-end-radius:.25rem}.rounded-es{border-end-start-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-tl{border-top-left-radius:.25rem}.rounded-tl-md{border-top-left-radius:calc(var(--radius) - 2px)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-tr{border-top-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-br{border-bottom-right-radius:.25rem}.rounded-br-md{border-bottom-right-radius:calc(var(--radius) - 2px)}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-s{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.border-e{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.border-bs{border-block-start-style:var(--tw-border-style);border-block-start-width:1px}.border-be{border-block-end-style:var(--tw-border-style);border-block-end-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-border{border-color:var(--border)!important}.border-border,.border-border\/60{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/60{border-color:color-mix(in oklab,var(--border) 60%,transparent)}}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/30{border-color:color-mix(in oklab,var(--destructive) 30%,transparent)}}.border-destructive\/40{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/40{border-color:color-mix(in oklab,var(--destructive) 40%,transparent)}}.border-info\/40{border-color:var(--info)}@supports (color:color-mix(in lab,red,red)){.border-info\/40{border-color:color-mix(in oklab,var(--info) 40%,transparent)}}.border-input{border-color:var(--input)}.border-primary,.border-primary\/40{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/40{border-color:color-mix(in oklab,var(--primary) 40%,transparent)}}.border-primary\/50{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/50{border-color:color-mix(in oklab,var(--primary) 50%,transparent)}}.border-primary\/60{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/60{border-color:color-mix(in oklab,var(--primary) 60%,transparent)}}.border-warning\/30{border-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/30{border-color:color-mix(in oklab,var(--warning) 30%,transparent)}}.border-warning\/40{border-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/40{border-color:color-mix(in oklab,var(--warning) 40%,transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.\!bg-popover{background-color:var(--popover)!important}.\!bg-primary{background-color:var(--primary)!important}.bg-background,.bg-background\/70{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){.bg-background\/70{background-color:color-mix(in oklab,var(--background) 70%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/20{background-color:#0003}@supports (color:color-mix(in lab,red,red)){.bg-black\/20{background-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab,red,red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black) 30%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-border{background-color:var(--border)}.bg-card\/40{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/40{background-color:color-mix(in oklab,var(--card) 40%,transparent)}}.bg-card\/80{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/80{background-color:color-mix(in oklab,var(--card) 80%,transparent)}}.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/5{background-color:color-mix(in oklab,var(--destructive) 5%,transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive) 10%,transparent)}}.bg-destructive\/15{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/15{background-color:color-mix(in oklab,var(--destructive) 15%,transparent)}}.bg-info\/10{background-color:var(--info)}@supports (color:color-mix(in lab,red,red)){.bg-info\/10{background-color:color-mix(in oklab,var(--info) 10%,transparent)}}.bg-muted-foreground,.bg-muted-foreground\/40{background-color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.bg-muted-foreground\/40{background-color:color-mix(in oklab,var(--muted-foreground) 40%,transparent)}}.bg-popover{background-color:var(--popover)}.bg-primary,.bg-primary\/5{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/5{background-color:color-mix(in oklab,var(--primary) 5%,transparent)}}.bg-primary\/10{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--primary) 10%,transparent)}}.bg-primary\/30{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/30{background-color:color-mix(in oklab,var(--primary) 30%,transparent)}}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-warning\/5{background-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.bg-warning\/5{background-color:color-mix(in oklab,var(--warning) 5%,transparent)}}.bg-warning\/10{background-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.bg-warning\/10{background-color:color-mix(in oklab,var(--warning) 10%,transparent)}}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary\/90{--tw-gradient-from:var(--primary)}@supports (color:color-mix(in lab,red,red)){.from-primary\/90{--tw-gradient-from:color-mix(in oklab, var(--primary) 90%, transparent)}}.from-primary\/90{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-primary\/70{--tw-gradient-to:var(--primary)}@supports (color:color-mix(in lab,red,red)){.to-primary\/70{--tw-gradient-to:color-mix(in oklab, var(--primary) 70%, transparent)}}.to-primary\/70{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-repeat{background-repeat:repeat}.mask-no-clip{-webkit-mask-clip:no-clip;mask-clip:no-clip}.mask-repeat{-webkit-mask-repeat:repeat;mask-repeat:repeat}.object-contain{object-fit:contain}.p-1{padding:calc(var(--spacing) * 1)}.p-3{padding:calc(var(--spacing) * 3)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-7{padding-top:calc(var(--spacing) * 7)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.\!font-sans{font-family:var(--font-sans)!important}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.5rem\]{font-size:.5rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[0\.625rem\]{font-size:.625rem}.text-\[0\.6875rem\]{font-size:.6875rem}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-wrap{text-wrap:wrap}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.\!text-foreground{color:var(--foreground)!important}.\!text-muted-foreground{color:var(--muted-foreground)!important}.\!text-primary-foreground{color:var(--primary-foreground)!important}.text-destructive,.text-destructive\/90{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/90{color:color-mix(in oklab,var(--destructive) 90%,transparent)}}.text-foreground{color:var(--foreground)}.text-info{color:var(--info)}.text-muted-foreground,.text-muted-foreground\/50{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/50{color:color-mix(in oklab,var(--muted-foreground) 50%,transparent)}}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-primary\/70{color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.text-primary\/70{color:color-mix(in oklab,var(--primary) 70%,transparent)}}.text-secondary-foreground{color:var(--secondary-foreground)}.text-warning{color:var(--warning)}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab,red,red)){.text-white\/70{color:color-mix(in oklab,var(--color-white) 70%,transparent)}}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.diagonal-fractions{--tw-numeric-fraction:diagonal-fractions;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.lining-nums{--tw-numeric-figure:lining-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.oldstyle-nums{--tw-numeric-figure:oldstyle-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.proportional-nums{--tw-numeric-spacing:proportional-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.slashed-zero{--tw-slashed-zero:slashed-zero;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.stacked-fractions{--tw-numeric-fraction:stacked-fractions;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.normal-nums{font-variant-numeric:normal}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.opacity-40{opacity:.4}.opacity-100{opacity:1}.opacity-\[0\.07\]{opacity:.07}.\!shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_1px_oklch\(0\.885_0\.19_122_\/_0\.4\)\,0_8px_24px_-12px_oklch\(0\.885_0\.19_122_\/_0\.6\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,oklch(88.5% .19 122/.4)), 0 8px 24px -12px var(--tw-shadow-color,oklch(88.5% .19 122/.6));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_3px_oklch\(0\.885_0\.19_122_\/_0\.2\)\]{--tw-shadow:0 0 0 3px var(--tw-shadow-color,oklch(88.5% .19 122/.2));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_1px_0_0_oklch\(1_0_0_\/_0\.04\)_inset\,0_24px_48px_-32px_oklch\(0_0_0_\/_0\.8\)\]{--tw-shadow:0 1px 0 0 var(--tw-shadow-color,oklch(100% 0 0/.04)) inset, 0 24px 48px -32px var(--tw-shadow-color,oklch(0% 0 0/.8));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_8px_24px_-12px_oklch\(0\.885_0\.19_122_\/_0\.8\)\]{--tw-shadow:0 8px 24px -12px var(--tw-shadow-color,oklch(88.5% .19 122/.8));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.inset-ring{--tw-inset-ring-shadow:inset 0 0 0 1px var(--tw-inset-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.fade-in-0{--tw-enter-opacity:0}.select-none{-webkit-user-select:none;user-select:none}.zoom-in-95{--tw-enter-scale:.95}.\[writing-mode\:vertical-rl\]{writing-mode:vertical-rl}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.paused{animation-play-state:paused}.ring-inset{--tw-ring-inset:inset}.running{animation-play-state:running}.zoom-in{--tw-enter-scale:0}.zoom-out{--tw-exit-scale:0}@media(hover:hover){.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--foreground)}}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.placeholder\:text-muted-foreground\/60::placeholder{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.placeholder\:text-muted-foreground\/60::placeholder{color:color-mix(in oklab,var(--muted-foreground) 60%,transparent)}}@media(hover:hover){.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-white\/15:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/15:hover{border-color:color-mix(in oklab,var(--color-white) 15%,transparent)}}.hover\:border-white\/20:hover{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/20:hover{border-color:color-mix(in oklab,var(--color-white) 20%,transparent)}}.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-destructive\/25:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/25:hover{background-color:color-mix(in oklab,var(--destructive) 25%,transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.hover\:bg-white\/\[0\.02\]:hover{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.02\]:hover{background-color:color-mix(in oklab,var(--color-white) 2%,transparent)}}.hover\:bg-white\/\[0\.025\]:hover{background-color:#ffffff06}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.025\]:hover{background-color:color-mix(in oklab,var(--color-white) 2.5%,transparent)}}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:brightness-110:hover{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus-visible\:border-primary\/50:focus-visible{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:border-primary\/50:focus-visible{border-color:color-mix(in oklab,var(--primary) 50%,transparent)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--ring)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-\[0\.98\]:active{scale:.98}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.data-\[state\=active\]\:bg-secondary[data-state=active]{background-color:var(--secondary)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}@media(min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media(min-width:48rem){.md\:flex{display:flex}}@media(min-width:64rem){.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.font-display{font-family:var(--font-display)}.text-balance{text-wrap:balance}.signal-dot{box-shadow:0 0 0 0 var(--color-primary);animation:1.8s cubic-bezier(.4,0,.2,1) infinite signal-pulse}@keyframes signal-pulse{0%{box-shadow:0 0 #c5ec4580}70%{box-shadow:0 0 0 8px #c5ec4500}to{box-shadow:0 0 #c5ec4500}}.reveal{opacity:0;animation:.55s cubic-bezier(.22,1,.36,1) forwards reveal-up}@keyframes reveal-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.hairline{background-image:linear-gradient(to right,transparent,var(--color-border),transparent)}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(14.5% .006 264);--foreground:oklch(93% .004 264);--card:oklch(18.5% .007 264);--card-foreground:oklch(93% .004 264);--popover:oklch(20.5% .008 264);--popover-foreground:oklch(93% .004 264);--primary:oklch(88.5% .19 122);--primary-foreground:oklch(18% .03 130);--secondary:oklch(23.5% .008 264);--secondary-foreground:oklch(93% .004 264);--muted:oklch(23.5% .008 264);--muted-foreground:oklch(64% .012 264);--accent:oklch(23.5% .008 264);--accent-foreground:oklch(93% .004 264);--destructive:oklch(63.7% .21 25.3);--destructive-foreground:oklch(97% .01 20);--warning:oklch(79% .15 78);--warning-foreground:oklch(20% .03 78);--info:oklch(78% .12 222);--info-foreground:oklch(18% .03 240);--border:oklch(100% 0 0/.08);--input:oklch(100% 0 0/.12);--ring:oklch(88.5% .19 122/.55);--font-display:"Clash Display", ui-sans-serif, system-ui, sans-serif;--font-sans:"Hanken Grotesk", ui-sans-serif, system-ui, sans-serif;--font-mono:"JetBrains Mono", ui-monospace, monospace}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}
|
frontend/dist/index.html
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Tiny Trigger — Automation Console</title>
|
| 7 |
+
<meta name="color-scheme" content="dark" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
| 11 |
+
<link
|
| 12 |
+
href="https://api.fontshare.com/v2/css?f[]=clash-display@600,500,700&display=swap"
|
| 13 |
+
rel="stylesheet"
|
| 14 |
+
/>
|
| 15 |
+
<link
|
| 16 |
+
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
| 17 |
+
rel="stylesheet"
|
| 18 |
+
/>
|
| 19 |
+
<script type="module" crossorigin src="/assets/index-Cs8-YHSt.js"></script>
|
| 20 |
+
<link rel="stylesheet" crossorigin href="/assets/index-DpGaJEG3.css">
|
| 21 |
+
</head>
|
| 22 |
+
<body>
|
| 23 |
+
<div id="root"></div>
|
| 24 |
+
</body>
|
| 25 |
+
</html>
|
frontend/src/lib/api.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { Client, handle_file } from "@gradio/client"
|
| 2 |
import type {
|
| 3 |
CompileResult,
|
|
|
|
| 4 |
CompilerProvider,
|
| 5 |
DetectParams,
|
| 6 |
LocalConfig,
|
|
@@ -53,18 +54,26 @@ export async function compileRules(
|
|
| 53 |
instruction: string,
|
| 54 |
classes: string,
|
| 55 |
existingRulesText: string,
|
| 56 |
-
|
| 57 |
-
baseUrl: string,
|
| 58 |
-
model: string,
|
| 59 |
): Promise<CompileResult> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
return call<CompileResult>("/compile_rules", {
|
| 61 |
instruction,
|
| 62 |
classes,
|
| 63 |
existing_rules_text: existingRulesText,
|
| 64 |
append: true,
|
| 65 |
-
provider,
|
| 66 |
-
|
| 67 |
-
model,
|
| 68 |
})
|
| 69 |
}
|
| 70 |
|
|
|
|
| 1 |
import { Client, handle_file } from "@gradio/client"
|
| 2 |
import type {
|
| 3 |
CompileResult,
|
| 4 |
+
CloudCompilerConfig,
|
| 5 |
CompilerProvider,
|
| 6 |
DetectParams,
|
| 7 |
LocalConfig,
|
|
|
|
| 54 |
instruction: string,
|
| 55 |
classes: string,
|
| 56 |
existingRulesText: string,
|
| 57 |
+
compiler: CloudCompilerConfig,
|
|
|
|
|
|
|
| 58 |
): Promise<CompileResult> {
|
| 59 |
+
const apiKeyByProvider: Record<CompilerProvider, string> = {
|
| 60 |
+
replicate: compiler.replicateApiKey,
|
| 61 |
+
openai: compiler.openaiApiKey,
|
| 62 |
+
anthropic: compiler.anthropicApiKey,
|
| 63 |
+
}
|
| 64 |
+
const modelByProvider: Record<CompilerProvider, string> = {
|
| 65 |
+
replicate: "",
|
| 66 |
+
openai: compiler.openaiModel,
|
| 67 |
+
anthropic: compiler.anthropicModel,
|
| 68 |
+
}
|
| 69 |
return call<CompileResult>("/compile_rules", {
|
| 70 |
instruction,
|
| 71 |
classes,
|
| 72 |
existing_rules_text: existingRulesText,
|
| 73 |
append: true,
|
| 74 |
+
provider: compiler.provider,
|
| 75 |
+
api_key: apiKeyByProvider[compiler.provider],
|
| 76 |
+
model: modelByProvider[compiler.provider],
|
| 77 |
})
|
| 78 |
}
|
| 79 |
|
frontend/src/lib/dashboard.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import type { ReactNode } from "react"
|
|
| 11 |
import { toast } from "sonner"
|
| 12 |
import * as api from "./api"
|
| 13 |
import type {
|
|
|
|
| 14 |
CompileResult,
|
| 15 |
CompilerProvider,
|
| 16 |
DetectParams,
|
|
@@ -47,6 +48,15 @@ const DEFAULT_PARAMS: DetectParams = {
|
|
| 47 |
webhookUrl: "",
|
| 48 |
}
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
interface DashboardState {
|
| 51 |
params: DetectParams
|
| 52 |
setParam: <K extends keyof DetectParams>(key: K, value: DetectParams[K]) => void
|
|
@@ -64,8 +74,10 @@ interface DashboardState {
|
|
| 64 |
save: () => Promise<void>
|
| 65 |
setRuleEnabled: (ruleName: string, enabled: boolean) => Promise<void>
|
| 66 |
deleteRule: (ruleName: string) => Promise<void>
|
| 67 |
-
|
| 68 |
setCompilerProvider: (provider: CompilerProvider) => void
|
|
|
|
|
|
|
| 69 |
compile: (instruction: string) => Promise<CompileResult | null>
|
| 70 |
compiling: boolean
|
| 71 |
}
|
|
@@ -81,7 +93,15 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 81 |
const [running, setRunning] = useState(false)
|
| 82 |
const [error, setError] = useState<string | null>(null)
|
| 83 |
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
| 84 |
-
const [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
const [compiling, setCompiling] = useState(false)
|
| 86 |
const previewRef = useRef<string | null>(null)
|
| 87 |
const compileInFlightRef = useRef(false)
|
|
@@ -107,9 +127,14 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 107 |
maxDetections: cfg.default_max_detections ?? p.maxDetections,
|
| 108 |
webhookUrl: cfg.webhook_url ?? p.webhookUrl,
|
| 109 |
}))
|
| 110 |
-
if (cfg.llm_provider === "
|
| 111 |
setCompilerProvider(cfg.llm_provider)
|
| 112 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
})
|
| 114 |
.catch(() => void 0)
|
| 115 |
api
|
|
@@ -127,6 +152,26 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 127 |
[],
|
| 128 |
)
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
const setVideo = useCallback((file: File | null) => {
|
| 131 |
if (previewRef.current) URL.revokeObjectURL(previewRef.current)
|
| 132 |
if (file) {
|
|
@@ -229,9 +274,7 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 229 |
instruction,
|
| 230 |
params.classes,
|
| 231 |
rulesText,
|
| 232 |
-
|
| 233 |
-
"http://127.0.0.1:8080/v1",
|
| 234 |
-
"ggml-org/Qwen3-1.7B-GGUF:Q4_K_M",
|
| 235 |
)
|
| 236 |
await applyRulesText(c.rules_text)
|
| 237 |
toast.success(`Added rule. ${c.rule_count} total.`)
|
|
@@ -246,7 +289,7 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 246 |
setCompiling(false)
|
| 247 |
}
|
| 248 |
},
|
| 249 |
-
[applyRulesText,
|
| 250 |
)
|
| 251 |
|
| 252 |
const value = useMemo<DashboardState>(
|
|
@@ -267,8 +310,10 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 267 |
save,
|
| 268 |
setRuleEnabled,
|
| 269 |
deleteRule,
|
| 270 |
-
|
| 271 |
setCompilerProvider,
|
|
|
|
|
|
|
| 272 |
compile,
|
| 273 |
compiling,
|
| 274 |
}),
|
|
@@ -288,7 +333,8 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
|
|
| 288 |
save,
|
| 289 |
setRuleEnabled,
|
| 290 |
deleteRule,
|
| 291 |
-
|
|
|
|
| 292 |
compile,
|
| 293 |
compiling,
|
| 294 |
],
|
|
|
|
| 11 |
import { toast } from "sonner"
|
| 12 |
import * as api from "./api"
|
| 13 |
import type {
|
| 14 |
+
CloudCompilerConfig,
|
| 15 |
CompileResult,
|
| 16 |
CompilerProvider,
|
| 17 |
DetectParams,
|
|
|
|
| 48 |
webhookUrl: "",
|
| 49 |
}
|
| 50 |
|
| 51 |
+
const DEFAULT_COMPILER: CloudCompilerConfig = {
|
| 52 |
+
provider: "anthropic",
|
| 53 |
+
replicateApiKey: "",
|
| 54 |
+
openaiApiKey: "",
|
| 55 |
+
anthropicApiKey: "",
|
| 56 |
+
openaiModel: "gpt-5.5",
|
| 57 |
+
anthropicModel: "claude-sonnet-4-6",
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
interface DashboardState {
|
| 61 |
params: DetectParams
|
| 62 |
setParam: <K extends keyof DetectParams>(key: K, value: DetectParams[K]) => void
|
|
|
|
| 74 |
save: () => Promise<void>
|
| 75 |
setRuleEnabled: (ruleName: string, enabled: boolean) => Promise<void>
|
| 76 |
deleteRule: (ruleName: string) => Promise<void>
|
| 77 |
+
compiler: CloudCompilerConfig
|
| 78 |
setCompilerProvider: (provider: CompilerProvider) => void
|
| 79 |
+
setCompilerApiKey: (provider: CompilerProvider, apiKey: string) => void
|
| 80 |
+
setCompilerModel: (provider: "openai" | "anthropic", model: string) => void
|
| 81 |
compile: (instruction: string) => Promise<CompileResult | null>
|
| 82 |
compiling: boolean
|
| 83 |
}
|
|
|
|
| 93 |
const [running, setRunning] = useState(false)
|
| 94 |
const [error, setError] = useState<string | null>(null)
|
| 95 |
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
| 96 |
+
const [compiler, setCompiler] = useState<CloudCompilerConfig>(() => ({
|
| 97 |
+
...DEFAULT_COMPILER,
|
| 98 |
+
provider:
|
| 99 |
+
(localStorage.getItem("tiny-trigger-compiler-provider") as CompilerProvider | null) ??
|
| 100 |
+
DEFAULT_COMPILER.provider,
|
| 101 |
+
openaiModel: localStorage.getItem("tiny-trigger-openai-model") ?? DEFAULT_COMPILER.openaiModel,
|
| 102 |
+
anthropicModel:
|
| 103 |
+
localStorage.getItem("tiny-trigger-anthropic-model") ?? DEFAULT_COMPILER.anthropicModel,
|
| 104 |
+
}))
|
| 105 |
const [compiling, setCompiling] = useState(false)
|
| 106 |
const previewRef = useRef<string | null>(null)
|
| 107 |
const compileInFlightRef = useRef(false)
|
|
|
|
| 127 |
maxDetections: cfg.default_max_detections ?? p.maxDetections,
|
| 128 |
webhookUrl: cfg.webhook_url ?? p.webhookUrl,
|
| 129 |
}))
|
| 130 |
+
if (cfg.llm_provider === "replicate" || cfg.llm_provider === "openai" || cfg.llm_provider === "anthropic") {
|
| 131 |
setCompilerProvider(cfg.llm_provider)
|
| 132 |
}
|
| 133 |
+
setCompiler((current) => ({
|
| 134 |
+
...current,
|
| 135 |
+
openaiModel: cfg.openai_model ?? current.openaiModel,
|
| 136 |
+
anthropicModel: cfg.anthropic_model ?? current.anthropicModel,
|
| 137 |
+
}))
|
| 138 |
})
|
| 139 |
.catch(() => void 0)
|
| 140 |
api
|
|
|
|
| 152 |
[],
|
| 153 |
)
|
| 154 |
|
| 155 |
+
const setCompilerProvider = useCallback((provider: CompilerProvider) => {
|
| 156 |
+
localStorage.setItem("tiny-trigger-compiler-provider", provider)
|
| 157 |
+
setCompiler((current) => ({ ...current, provider }))
|
| 158 |
+
}, [])
|
| 159 |
+
|
| 160 |
+
const setCompilerApiKey = useCallback((provider: CompilerProvider, apiKey: string) => {
|
| 161 |
+
const keyByProvider: Record<CompilerProvider, keyof CloudCompilerConfig> = {
|
| 162 |
+
replicate: "replicateApiKey",
|
| 163 |
+
openai: "openaiApiKey",
|
| 164 |
+
anthropic: "anthropicApiKey",
|
| 165 |
+
}
|
| 166 |
+
setCompiler((current) => ({ ...current, [keyByProvider[provider]]: apiKey }))
|
| 167 |
+
}, [])
|
| 168 |
+
|
| 169 |
+
const setCompilerModel = useCallback((provider: "openai" | "anthropic", model: string) => {
|
| 170 |
+
const key = provider === "openai" ? "openaiModel" : "anthropicModel"
|
| 171 |
+
localStorage.setItem(`tiny-trigger-${provider}-model`, model)
|
| 172 |
+
setCompiler((current) => ({ ...current, [key]: model }))
|
| 173 |
+
}, [])
|
| 174 |
+
|
| 175 |
const setVideo = useCallback((file: File | null) => {
|
| 176 |
if (previewRef.current) URL.revokeObjectURL(previewRef.current)
|
| 177 |
if (file) {
|
|
|
|
| 274 |
instruction,
|
| 275 |
params.classes,
|
| 276 |
rulesText,
|
| 277 |
+
compiler,
|
|
|
|
|
|
|
| 278 |
)
|
| 279 |
await applyRulesText(c.rules_text)
|
| 280 |
toast.success(`Added rule. ${c.rule_count} total.`)
|
|
|
|
| 289 |
setCompiling(false)
|
| 290 |
}
|
| 291 |
},
|
| 292 |
+
[applyRulesText, compiler, params.classes, rulesText],
|
| 293 |
)
|
| 294 |
|
| 295 |
const value = useMemo<DashboardState>(
|
|
|
|
| 310 |
save,
|
| 311 |
setRuleEnabled,
|
| 312 |
deleteRule,
|
| 313 |
+
compiler,
|
| 314 |
setCompilerProvider,
|
| 315 |
+
setCompilerApiKey,
|
| 316 |
+
setCompilerModel,
|
| 317 |
compile,
|
| 318 |
compiling,
|
| 319 |
}),
|
|
|
|
| 333 |
save,
|
| 334 |
setRuleEnabled,
|
| 335 |
deleteRule,
|
| 336 |
+
compiler,
|
| 337 |
+
setCompilerModel,
|
| 338 |
compile,
|
| 339 |
compiling,
|
| 340 |
],
|
frontend/src/lib/types.ts
CHANGED
|
@@ -65,7 +65,16 @@ export interface RuleMutationResult {
|
|
| 65 |
rule_count: number
|
| 66 |
}
|
| 67 |
|
| 68 |
-
export type CompilerProvider = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
export interface LocalConfig {
|
| 71 |
camera_url: string | null
|
|
@@ -76,10 +85,10 @@ export interface LocalConfig {
|
|
| 76 |
default_image_size: number | null
|
| 77 |
default_max_detections: number | null
|
| 78 |
llm_provider: CompilerProvider | null
|
| 79 |
-
llamacpp_base_url: string | null
|
| 80 |
-
llamacpp_model: string | null
|
| 81 |
replicate_model: string | null
|
| 82 |
replicate_reasoning_effort: string | null
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
export interface DetectParams {
|
|
|
|
| 65 |
rule_count: number
|
| 66 |
}
|
| 67 |
|
| 68 |
+
export type CompilerProvider = "replicate" | "openai" | "anthropic"
|
| 69 |
+
|
| 70 |
+
export interface CloudCompilerConfig {
|
| 71 |
+
provider: CompilerProvider
|
| 72 |
+
replicateApiKey: string
|
| 73 |
+
openaiApiKey: string
|
| 74 |
+
anthropicApiKey: string
|
| 75 |
+
openaiModel: string
|
| 76 |
+
anthropicModel: string
|
| 77 |
+
}
|
| 78 |
|
| 79 |
export interface LocalConfig {
|
| 80 |
camera_url: string | null
|
|
|
|
| 85 |
default_image_size: number | null
|
| 86 |
default_max_detections: number | null
|
| 87 |
llm_provider: CompilerProvider | null
|
|
|
|
|
|
|
| 88 |
replicate_model: string | null
|
| 89 |
replicate_reasoning_effort: string | null
|
| 90 |
+
openai_model: string | null
|
| 91 |
+
anthropic_model: string | null
|
| 92 |
}
|
| 93 |
|
| 94 |
export interface DetectParams {
|
frontend/src/modules/rules/RuleStudioPanel.tsx
CHANGED
|
@@ -2,8 +2,6 @@ import { useState } from "react"
|
|
| 2 |
import {
|
| 3 |
CheckCircle2,
|
| 4 |
CircleSlash,
|
| 5 |
-
Cloud,
|
| 6 |
-
Cpu,
|
| 7 |
Loader2,
|
| 8 |
Save,
|
| 9 |
ShieldCheck,
|
|
@@ -29,8 +27,7 @@ export function RuleStudioPanel() {
|
|
| 29 |
save,
|
| 30 |
setRuleEnabled,
|
| 31 |
deleteRule,
|
| 32 |
-
|
| 33 |
-
setCompilerProvider,
|
| 34 |
compile,
|
| 35 |
compiling,
|
| 36 |
} = useDashboard()
|
|
@@ -77,23 +74,9 @@ export function RuleStudioPanel() {
|
|
| 77 |
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
| 78 |
<p className="text-xs text-muted-foreground">
|
| 79 |
Describe the automation in plain language. It compiles to validated rules through{" "}
|
| 80 |
-
{
|
| 81 |
</p>
|
| 82 |
-
<
|
| 83 |
-
{compilerProvider === "cloud" ? (
|
| 84 |
-
<Cloud className="size-3.5 text-primary" />
|
| 85 |
-
) : (
|
| 86 |
-
<Cpu className="size-3.5 text-muted-foreground" />
|
| 87 |
-
)}
|
| 88 |
-
<Label htmlFor="compiler-provider" className="text-xs text-foreground">
|
| 89 |
-
Cloud
|
| 90 |
-
</Label>
|
| 91 |
-
<Switch
|
| 92 |
-
id="compiler-provider"
|
| 93 |
-
checked={compilerProvider === "cloud"}
|
| 94 |
-
onCheckedChange={(checked) => setCompilerProvider(checked ? "cloud" : "local")}
|
| 95 |
-
/>
|
| 96 |
-
</div>
|
| 97 |
</div>
|
| 98 |
<Textarea
|
| 99 |
value={instruction}
|
|
@@ -105,9 +88,7 @@ export function RuleStudioPanel() {
|
|
| 105 |
{compiling ? <Loader2 className="size-4 animate-spin" /> : <Wand2 className="size-4" />}
|
| 106 |
{compiling
|
| 107 |
? "Compiling…"
|
| 108 |
-
:
|
| 109 |
-
? "Compile with cloud"
|
| 110 |
-
: "Compile to rules"}
|
| 111 |
</Button>
|
| 112 |
</div>
|
| 113 |
</TabsContent>
|
|
|
|
| 2 |
import {
|
| 3 |
CheckCircle2,
|
| 4 |
CircleSlash,
|
|
|
|
|
|
|
| 5 |
Loader2,
|
| 6 |
Save,
|
| 7 |
ShieldCheck,
|
|
|
|
| 27 |
save,
|
| 28 |
setRuleEnabled,
|
| 29 |
deleteRule,
|
| 30 |
+
compiler,
|
|
|
|
| 31 |
compile,
|
| 32 |
compiling,
|
| 33 |
} = useDashboard()
|
|
|
|
| 74 |
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
| 75 |
<p className="text-xs text-muted-foreground">
|
| 76 |
Describe the automation in plain language. It compiles to validated rules through{" "}
|
| 77 |
+
{compiler.provider} — never executable code.
|
| 78 |
</p>
|
| 79 |
+
<Badge variant="default">{compiler.provider}</Badge>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
</div>
|
| 81 |
<Textarea
|
| 82 |
value={instruction}
|
|
|
|
| 88 |
{compiling ? <Loader2 className="size-4 animate-spin" /> : <Wand2 className="size-4" />}
|
| 89 |
{compiling
|
| 90 |
? "Compiling…"
|
| 91 |
+
: `Compile with ${compiler.provider}`}
|
|
|
|
|
|
|
| 92 |
</Button>
|
| 93 |
</div>
|
| 94 |
</TabsContent>
|
frontend/src/modules/settings/SettingsPanel.tsx
CHANGED
|
@@ -2,12 +2,57 @@ import { AlertTriangle } from "lucide-react"
|
|
| 2 |
import { useDashboard } from "@/lib/dashboard"
|
| 3 |
import { Label } from "@/components/ui/label"
|
| 4 |
import { Input } from "@/components/ui/input"
|
|
|
|
| 5 |
import { Switch } from "@/components/ui/switch"
|
| 6 |
import { Separator } from "@/components/ui/separator"
|
| 7 |
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
export function SettingsPanel() {
|
| 10 |
-
const {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
return (
|
| 13 |
<div className="space-y-5">
|
|
@@ -56,17 +101,50 @@ export function SettingsPanel() {
|
|
| 56 |
<div className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-muted-foreground">
|
| 57 |
rule compiler
|
| 58 |
</div>
|
| 59 |
-
<
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
<p className="text-[0.6875rem] text-muted-foreground">
|
| 68 |
-
|
| 69 |
-
|
| 70 |
</p>
|
| 71 |
</div>
|
| 72 |
</div>
|
|
|
|
| 2 |
import { useDashboard } from "@/lib/dashboard"
|
| 3 |
import { Label } from "@/components/ui/label"
|
| 4 |
import { Input } from "@/components/ui/input"
|
| 5 |
+
import { Button } from "@/components/ui/button"
|
| 6 |
import { Switch } from "@/components/ui/switch"
|
| 7 |
import { Separator } from "@/components/ui/separator"
|
| 8 |
import { cn } from "@/lib/utils"
|
| 9 |
+
import type { CompilerProvider } from "@/lib/types"
|
| 10 |
+
|
| 11 |
+
const PROVIDERS: { id: CompilerProvider; label: string; placeholder: string }[] = [
|
| 12 |
+
{ id: "replicate", label: "Replicate", placeholder: "r8_..." },
|
| 13 |
+
{ id: "openai", label: "OpenAI", placeholder: "sk-..." },
|
| 14 |
+
{ id: "anthropic", label: "Claude", placeholder: "sk-ant-..." },
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
const MODELS = {
|
| 18 |
+
openai: [
|
| 19 |
+
{ value: "gpt-5.5", label: "GPT-5.5" },
|
| 20 |
+
{ value: "gpt-5.4", label: "GPT-5.4" },
|
| 21 |
+
{ value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
|
| 22 |
+
],
|
| 23 |
+
anthropic: [
|
| 24 |
+
{ value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
| 25 |
+
{ value: "claude-opus-4-8", label: "Claude Opus 4.8" },
|
| 26 |
+
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
| 27 |
+
],
|
| 28 |
+
}
|
| 29 |
|
| 30 |
export function SettingsPanel() {
|
| 31 |
+
const {
|
| 32 |
+
params,
|
| 33 |
+
setParam,
|
| 34 |
+
compiler,
|
| 35 |
+
setCompilerProvider,
|
| 36 |
+
setCompilerApiKey,
|
| 37 |
+
setCompilerModel,
|
| 38 |
+
} = useDashboard()
|
| 39 |
+
const activeProvider = PROVIDERS.find((provider) => provider.id === compiler.provider) ?? PROVIDERS[0]
|
| 40 |
+
const apiKey =
|
| 41 |
+
compiler.provider === "replicate"
|
| 42 |
+
? compiler.replicateApiKey
|
| 43 |
+
: compiler.provider === "openai"
|
| 44 |
+
? compiler.openaiApiKey
|
| 45 |
+
: compiler.anthropicApiKey
|
| 46 |
+
const model =
|
| 47 |
+
compiler.provider === "openai"
|
| 48 |
+
? compiler.openaiModel
|
| 49 |
+
: compiler.provider === "anthropic"
|
| 50 |
+
? compiler.anthropicModel
|
| 51 |
+
: ""
|
| 52 |
+
const modelProvider =
|
| 53 |
+
compiler.provider === "openai" || compiler.provider === "anthropic"
|
| 54 |
+
? compiler.provider
|
| 55 |
+
: null
|
| 56 |
|
| 57 |
return (
|
| 58 |
<div className="space-y-5">
|
|
|
|
| 101 |
<div className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-muted-foreground">
|
| 102 |
rule compiler
|
| 103 |
</div>
|
| 104 |
+
<div className="grid grid-cols-3 gap-1 rounded-lg border border-border bg-black/20 p-1">
|
| 105 |
+
{PROVIDERS.map((provider) => (
|
| 106 |
+
<Button
|
| 107 |
+
key={provider.id}
|
| 108 |
+
type="button"
|
| 109 |
+
size="sm"
|
| 110 |
+
variant={compiler.provider === provider.id ? "default" : "ghost"}
|
| 111 |
+
onClick={() => setCompilerProvider(provider.id)}
|
| 112 |
+
>
|
| 113 |
+
{provider.label}
|
| 114 |
+
</Button>
|
| 115 |
+
))}
|
| 116 |
+
</div>
|
| 117 |
+
<div className="space-y-1.5">
|
| 118 |
+
<Label htmlFor="compiler-api-key">{activeProvider.label} API key</Label>
|
| 119 |
+
<Input
|
| 120 |
+
id="compiler-api-key"
|
| 121 |
+
type="password"
|
| 122 |
+
value={apiKey}
|
| 123 |
+
onChange={(e) => setCompilerApiKey(compiler.provider, e.target.value)}
|
| 124 |
+
placeholder={activeProvider.placeholder}
|
| 125 |
+
className="font-mono text-xs"
|
| 126 |
+
/>
|
| 127 |
+
</div>
|
| 128 |
+
{modelProvider && (
|
| 129 |
+
<div className="space-y-1.5">
|
| 130 |
+
<Label htmlFor="compiler-model">{activeProvider.label} model</Label>
|
| 131 |
+
<select
|
| 132 |
+
id="compiler-model"
|
| 133 |
+
value={model}
|
| 134 |
+
onChange={(e) => setCompilerModel(modelProvider, e.target.value)}
|
| 135 |
+
className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
| 136 |
+
>
|
| 137 |
+
{MODELS[modelProvider].map((option) => (
|
| 138 |
+
<option key={option.value} value={option.value}>
|
| 139 |
+
{option.label}
|
| 140 |
+
</option>
|
| 141 |
+
))}
|
| 142 |
+
</select>
|
| 143 |
+
</div>
|
| 144 |
+
)}
|
| 145 |
<p className="text-[0.6875rem] text-muted-foreground">
|
| 146 |
+
Keys pasted here are sent only when compiling. Space owners can also set provider
|
| 147 |
+
secrets as environment variables.
|
| 148 |
</p>
|
| 149 |
</div>
|
| 150 |
</div>
|
poetry.lock
CHANGED
|
@@ -24,6 +24,36 @@ files = [
|
|
| 24 |
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
| 25 |
]
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
[[package]]
|
| 28 |
name = "anyio"
|
| 29 |
version = "4.13.0"
|
|
@@ -860,17 +890,34 @@ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
|
|
| 860 |
tests = ["pytest", "pytest-cov", "pytest-xdist"]
|
| 861 |
|
| 862 |
[[package]]
|
| 863 |
-
name = "
|
| 864 |
-
version = "
|
| 865 |
-
description = "
|
| 866 |
optional = false
|
| 867 |
-
python-versions = ">=3"
|
| 868 |
groups = ["main"]
|
| 869 |
files = [
|
| 870 |
-
{file = "
|
| 871 |
-
{file = "
|
| 872 |
]
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
[[package]]
|
| 875 |
name = "docutils"
|
| 876 |
version = "0.23"
|
|
@@ -1477,6 +1524,125 @@ MarkupSafe = ">=2.0"
|
|
| 1477 |
[package.extras]
|
| 1478 |
i18n = ["Babel (>=2.7)"]
|
| 1479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1480 |
[[package]]
|
| 1481 |
name = "keyring"
|
| 1482 |
version = "25.7.0"
|
|
@@ -1635,35 +1801,6 @@ files = [
|
|
| 1635 |
{file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"},
|
| 1636 |
]
|
| 1637 |
|
| 1638 |
-
[[package]]
|
| 1639 |
-
name = "llama-cpp-python"
|
| 1640 |
-
version = "0.3.28"
|
| 1641 |
-
description = "Python bindings for the llama.cpp library"
|
| 1642 |
-
optional = false
|
| 1643 |
-
python-versions = ">=3.8"
|
| 1644 |
-
groups = ["main"]
|
| 1645 |
-
files = [
|
| 1646 |
-
{file = "llama_cpp_python-0.3.28.tar.gz", hash = "sha256:958227b394f413425d6039952096daa0b8b98328c6b99d652862aec775f1672d"},
|
| 1647 |
-
]
|
| 1648 |
-
|
| 1649 |
-
[package.dependencies]
|
| 1650 |
-
diskcache = ">=5.6.1"
|
| 1651 |
-
fastapi = {version = ">=0.100.0", optional = true, markers = "extra == \"server\""}
|
| 1652 |
-
jinja2 = ">=2.11.3"
|
| 1653 |
-
numpy = ">=1.20.0"
|
| 1654 |
-
pydantic-settings = {version = ">=2.0.1", optional = true, markers = "extra == \"server\""}
|
| 1655 |
-
PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"server\""}
|
| 1656 |
-
sse-starlette = {version = ">=1.6.1", optional = true, markers = "extra == \"server\""}
|
| 1657 |
-
starlette-context = {version = ">=0.3.6,<0.4", optional = true, markers = "extra == \"server\""}
|
| 1658 |
-
typing-extensions = ">=4.5.0"
|
| 1659 |
-
uvicorn = {version = ">=0.22.0", optional = true, markers = "extra == \"server\""}
|
| 1660 |
-
|
| 1661 |
-
[package.extras]
|
| 1662 |
-
all = ["llama_cpp_python[dev,server,test]"]
|
| 1663 |
-
dev = ["httpx (>=0.24.1)", "mkdocs (>=1.4.3)", "mkdocs-material (>=9.1.18)", "mkdocstrings[python] (>=0.22.0)", "pytest (>=7.4.0)", "ruff (>=0.15.7)", "twine (>=4.0.2)"]
|
| 1664 |
-
server = ["PyYAML (>=5.1)", "fastapi (>=0.100.0)", "pydantic-settings (>=2.0.1)", "sse-starlette (>=1.6.1)", "starlette-context (>=0.3.6,<0.4)", "uvicorn (>=0.22.0)"]
|
| 1665 |
-
test = ["fastapi (>=0.100.0)", "httpx (>=0.24.1)", "huggingface-hub (>=0.23.0)", "pydantic-settings (>=2.0.1)", "pytest (>=7.4.0)", "scipy (>=1.10)", "sse-starlette (>=1.6.1)", "starlette-context (>=0.3.6,<0.4)"]
|
| 1666 |
-
|
| 1667 |
[[package]]
|
| 1668 |
name = "markdown-it-py"
|
| 1669 |
version = "4.2.0"
|
|
@@ -2442,6 +2579,34 @@ files = [
|
|
| 2442 |
{file = "nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519"},
|
| 2443 |
]
|
| 2444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2445 |
[[package]]
|
| 2446 |
name = "opencv-python"
|
| 2447 |
version = "4.13.0.92"
|
|
@@ -3173,30 +3338,6 @@ files = [
|
|
| 3173 |
[package.dependencies]
|
| 3174 |
typing-extensions = ">=4.14.1"
|
| 3175 |
|
| 3176 |
-
[[package]]
|
| 3177 |
-
name = "pydantic-settings"
|
| 3178 |
-
version = "2.14.1"
|
| 3179 |
-
description = "Settings management using Pydantic"
|
| 3180 |
-
optional = false
|
| 3181 |
-
python-versions = ">=3.10"
|
| 3182 |
-
groups = ["main"]
|
| 3183 |
-
files = [
|
| 3184 |
-
{file = "pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de"},
|
| 3185 |
-
{file = "pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa"},
|
| 3186 |
-
]
|
| 3187 |
-
|
| 3188 |
-
[package.dependencies]
|
| 3189 |
-
pydantic = ">=2.7.0"
|
| 3190 |
-
python-dotenv = ">=0.21.0"
|
| 3191 |
-
typing-inspection = ">=0.4.0"
|
| 3192 |
-
|
| 3193 |
-
[package.extras]
|
| 3194 |
-
aws-secrets-manager = ["boto3 (>=1.35.0)", "types-boto3[secretsmanager]"]
|
| 3195 |
-
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
| 3196 |
-
gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
|
| 3197 |
-
toml = ["tomli (>=2.0.1)"]
|
| 3198 |
-
yaml = ["pyyaml (>=6.0.1)"]
|
| 3199 |
-
|
| 3200 |
[[package]]
|
| 3201 |
name = "pydub"
|
| 3202 |
version = "0.25.1"
|
|
@@ -3278,21 +3419,6 @@ files = [
|
|
| 3278 |
[package.dependencies]
|
| 3279 |
six = ">=1.5"
|
| 3280 |
|
| 3281 |
-
[[package]]
|
| 3282 |
-
name = "python-dotenv"
|
| 3283 |
-
version = "1.2.2"
|
| 3284 |
-
description = "Read key-value pairs from a .env file and set them as environment variables"
|
| 3285 |
-
optional = false
|
| 3286 |
-
python-versions = ">=3.10"
|
| 3287 |
-
groups = ["main"]
|
| 3288 |
-
files = [
|
| 3289 |
-
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
|
| 3290 |
-
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
|
| 3291 |
-
]
|
| 3292 |
-
|
| 3293 |
-
[package.extras]
|
| 3294 |
-
cli = ["click (>=5.0)"]
|
| 3295 |
-
|
| 3296 |
[[package]]
|
| 3297 |
name = "python-multipart"
|
| 3298 |
version = "0.0.32"
|
|
@@ -3764,28 +3890,17 @@ files = [
|
|
| 3764 |
]
|
| 3765 |
|
| 3766 |
[[package]]
|
| 3767 |
-
name = "
|
| 3768 |
-
version = "3.
|
| 3769 |
-
description = "
|
| 3770 |
optional = false
|
| 3771 |
-
python-versions = ">=3.
|
| 3772 |
groups = ["main"]
|
| 3773 |
files = [
|
| 3774 |
-
{file = "
|
| 3775 |
-
{file = "
|
| 3776 |
]
|
| 3777 |
|
| 3778 |
-
[package.dependencies]
|
| 3779 |
-
anyio = ">=4.7.0"
|
| 3780 |
-
starlette = ">=0.49.1"
|
| 3781 |
-
|
| 3782 |
-
[package.extras]
|
| 3783 |
-
daphne = ["daphne (>=4.2.0)"]
|
| 3784 |
-
examples = ["fastapi (>=0.115.12)", "pydantic (>=2)", "uvicorn (>=0.34.0)"]
|
| 3785 |
-
examples-db = ["aiosqlite (>=0.21.0)", "sqlalchemy[asyncio] (>=2.0.41)"]
|
| 3786 |
-
granian = ["granian (>=2.3.1)"]
|
| 3787 |
-
uvicorn = ["uvicorn (>=0.34.0)"]
|
| 3788 |
-
|
| 3789 |
[[package]]
|
| 3790 |
name = "starlette"
|
| 3791 |
version = "1.3.1"
|
|
@@ -3805,21 +3920,6 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""
|
|
| 3805 |
[package.extras]
|
| 3806 |
full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
| 3807 |
|
| 3808 |
-
[[package]]
|
| 3809 |
-
name = "starlette-context"
|
| 3810 |
-
version = "0.3.6"
|
| 3811 |
-
description = "Middleware for Starlette that allows you to store and access the context data of a request. Can be used with logging so logs automatically use request headers such as x-request-id or x-correlation-id."
|
| 3812 |
-
optional = false
|
| 3813 |
-
python-versions = ">=3.8,<4.0"
|
| 3814 |
-
groups = ["main"]
|
| 3815 |
-
files = [
|
| 3816 |
-
{file = "starlette_context-0.3.6-py3-none-any.whl", hash = "sha256:b14ce373fbb6895a2182a7104b9f63ba20c8db83444005fb9a844dd77ad9895c"},
|
| 3817 |
-
{file = "starlette_context-0.3.6.tar.gz", hash = "sha256:d361a36ba2d4acca3ab680f917b25e281533d725374752d47607a859041958cb"},
|
| 3818 |
-
]
|
| 3819 |
-
|
| 3820 |
-
[package.dependencies]
|
| 3821 |
-
starlette = "*"
|
| 3822 |
-
|
| 3823 |
[[package]]
|
| 3824 |
name = "static-ffmpeg"
|
| 3825 |
version = "3.0"
|
|
@@ -4331,4 +4431,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""]
|
|
| 4331 |
[metadata]
|
| 4332 |
lock-version = "2.1"
|
| 4333 |
python-versions = ">=3.10,!=3.14.1,<3.15"
|
| 4334 |
-
content-hash = "
|
|
|
|
| 24 |
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
| 25 |
]
|
| 26 |
|
| 27 |
+
[[package]]
|
| 28 |
+
name = "anthropic"
|
| 29 |
+
version = "0.109.1"
|
| 30 |
+
description = "The official Python library for the anthropic API"
|
| 31 |
+
optional = false
|
| 32 |
+
python-versions = ">=3.9"
|
| 33 |
+
groups = ["main"]
|
| 34 |
+
files = [
|
| 35 |
+
{file = "anthropic-0.109.1-py3-none-any.whl", hash = "sha256:ce7d94a7657f2aa29338cca448945eac621b4f62c1794cf461cb32847223e9b8"},
|
| 36 |
+
{file = "anthropic-0.109.1.tar.gz", hash = "sha256:83e06b3d9d40ff5898f588020e0cc4e42187de954549a3b5fbe6e2685a09c785"},
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
[package.dependencies]
|
| 40 |
+
anyio = ">=3.5.0,<5"
|
| 41 |
+
distro = ">=1.7.0,<2"
|
| 42 |
+
docstring-parser = ">=0.15,<1"
|
| 43 |
+
httpx = ">=0.25.0,<1"
|
| 44 |
+
jiter = ">=0.4.0,<1"
|
| 45 |
+
pydantic = ">=1.9.0,<3"
|
| 46 |
+
sniffio = "*"
|
| 47 |
+
typing-extensions = ">=4.14,<5"
|
| 48 |
+
|
| 49 |
+
[package.extras]
|
| 50 |
+
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
|
| 51 |
+
aws = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
|
| 52 |
+
bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
|
| 53 |
+
mcp = ["mcp (>=1.0) ; python_version >= \"3.10\""]
|
| 54 |
+
vertex = ["google-auth[requests] (>=2,<3)"]
|
| 55 |
+
webhooks = ["standardwebhooks (>=1.0.1,<2)"]
|
| 56 |
+
|
| 57 |
[[package]]
|
| 58 |
name = "anyio"
|
| 59 |
version = "4.13.0"
|
|
|
|
| 890 |
tests = ["pytest", "pytest-cov", "pytest-xdist"]
|
| 891 |
|
| 892 |
[[package]]
|
| 893 |
+
name = "distro"
|
| 894 |
+
version = "1.9.0"
|
| 895 |
+
description = "Distro - an OS platform information API"
|
| 896 |
optional = false
|
| 897 |
+
python-versions = ">=3.6"
|
| 898 |
groups = ["main"]
|
| 899 |
files = [
|
| 900 |
+
{file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
|
| 901 |
+
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
| 902 |
]
|
| 903 |
|
| 904 |
+
[[package]]
|
| 905 |
+
name = "docstring-parser"
|
| 906 |
+
version = "0.18.0"
|
| 907 |
+
description = "Parse Python docstrings in reST, Google and Numpydoc format"
|
| 908 |
+
optional = false
|
| 909 |
+
python-versions = ">=3.8"
|
| 910 |
+
groups = ["main"]
|
| 911 |
+
files = [
|
| 912 |
+
{file = "docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b"},
|
| 913 |
+
{file = "docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015"},
|
| 914 |
+
]
|
| 915 |
+
|
| 916 |
+
[package.extras]
|
| 917 |
+
dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"]
|
| 918 |
+
docs = ["pydoctor (>=25.4.0)"]
|
| 919 |
+
test = ["pytest"]
|
| 920 |
+
|
| 921 |
[[package]]
|
| 922 |
name = "docutils"
|
| 923 |
version = "0.23"
|
|
|
|
| 1524 |
[package.extras]
|
| 1525 |
i18n = ["Babel (>=2.7)"]
|
| 1526 |
|
| 1527 |
+
[[package]]
|
| 1528 |
+
name = "jiter"
|
| 1529 |
+
version = "0.15.0"
|
| 1530 |
+
description = "Fast iterable JSON parser."
|
| 1531 |
+
optional = false
|
| 1532 |
+
python-versions = ">=3.9"
|
| 1533 |
+
groups = ["main"]
|
| 1534 |
+
files = [
|
| 1535 |
+
{file = "jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4"},
|
| 1536 |
+
{file = "jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f"},
|
| 1537 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18"},
|
| 1538 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f"},
|
| 1539 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4"},
|
| 1540 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6"},
|
| 1541 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c"},
|
| 1542 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512"},
|
| 1543 |
+
{file = "jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a"},
|
| 1544 |
+
{file = "jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887"},
|
| 1545 |
+
{file = "jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823"},
|
| 1546 |
+
{file = "jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53"},
|
| 1547 |
+
{file = "jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1"},
|
| 1548 |
+
{file = "jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2"},
|
| 1549 |
+
{file = "jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67"},
|
| 1550 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a"},
|
| 1551 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7"},
|
| 1552 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd"},
|
| 1553 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281"},
|
| 1554 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708"},
|
| 1555 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928"},
|
| 1556 |
+
{file = "jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd"},
|
| 1557 |
+
{file = "jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e"},
|
| 1558 |
+
{file = "jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef"},
|
| 1559 |
+
{file = "jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32"},
|
| 1560 |
+
{file = "jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04"},
|
| 1561 |
+
{file = "jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865"},
|
| 1562 |
+
{file = "jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d"},
|
| 1563 |
+
{file = "jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0"},
|
| 1564 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138"},
|
| 1565 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61"},
|
| 1566 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687"},
|
| 1567 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879"},
|
| 1568 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d"},
|
| 1569 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb"},
|
| 1570 |
+
{file = "jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871"},
|
| 1571 |
+
{file = "jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77"},
|
| 1572 |
+
{file = "jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d"},
|
| 1573 |
+
{file = "jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d"},
|
| 1574 |
+
{file = "jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7"},
|
| 1575 |
+
{file = "jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b"},
|
| 1576 |
+
{file = "jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3"},
|
| 1577 |
+
{file = "jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5"},
|
| 1578 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279"},
|
| 1579 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4"},
|
| 1580 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258"},
|
| 1581 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894"},
|
| 1582 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45"},
|
| 1583 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29"},
|
| 1584 |
+
{file = "jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b"},
|
| 1585 |
+
{file = "jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7"},
|
| 1586 |
+
{file = "jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712"},
|
| 1587 |
+
{file = "jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c"},
|
| 1588 |
+
{file = "jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0"},
|
| 1589 |
+
{file = "jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba"},
|
| 1590 |
+
{file = "jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8"},
|
| 1591 |
+
{file = "jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c"},
|
| 1592 |
+
{file = "jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4"},
|
| 1593 |
+
{file = "jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b"},
|
| 1594 |
+
{file = "jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7"},
|
| 1595 |
+
{file = "jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49"},
|
| 1596 |
+
{file = "jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86"},
|
| 1597 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f"},
|
| 1598 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e"},
|
| 1599 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6"},
|
| 1600 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9"},
|
| 1601 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c"},
|
| 1602 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd"},
|
| 1603 |
+
{file = "jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89"},
|
| 1604 |
+
{file = "jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554"},
|
| 1605 |
+
{file = "jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a"},
|
| 1606 |
+
{file = "jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec"},
|
| 1607 |
+
{file = "jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558"},
|
| 1608 |
+
{file = "jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866"},
|
| 1609 |
+
{file = "jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d"},
|
| 1610 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6"},
|
| 1611 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995"},
|
| 1612 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8"},
|
| 1613 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5"},
|
| 1614 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b"},
|
| 1615 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8"},
|
| 1616 |
+
{file = "jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec"},
|
| 1617 |
+
{file = "jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e"},
|
| 1618 |
+
{file = "jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5"},
|
| 1619 |
+
{file = "jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52"},
|
| 1620 |
+
{file = "jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854"},
|
| 1621 |
+
{file = "jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0"},
|
| 1622 |
+
{file = "jiter-0.15.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:04b400bbf8c9efb03d9bdd976475c919c1d85593b04b9fff7ae234065daf87ae"},
|
| 1623 |
+
{file = "jiter-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25ffbe229aa8cd98c28879d8aa1a6e34ae77992ab984a65fba800859dab16269"},
|
| 1624 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5607e6013ed7e6b0ec9661e467b7ffde0aa7ab36833a04850f26fcf88ed4845b"},
|
| 1625 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50164d7610c00e7cd913a873fce30b6beeebf4b37e53983e33f22de4c900f6b8"},
|
| 1626 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab596fa3837e91e7e6a31b5f639988bfc6a35d1f915ac3932d946062219d588f"},
|
| 1627 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72d8af5c1013656a8870c866660627d1a75bc185814ee022c8533caa1de88ae"},
|
| 1628 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c84c1b7be454b0c16f8499b4ebfbfd82ea5cca6527cceefcbbc06a7557b5ed2e"},
|
| 1629 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:d636d5095155afd364247f65070fab7beda13498d7ff4de331046e704ab9657f"},
|
| 1630 |
+
{file = "jiter-0.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d3d6683288c11cbab50e865f2e2f13950179aa45410e30b2cfbd3fb7b0177bf"},
|
| 1631 |
+
{file = "jiter-0.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7ce8902f939970048b233087082e7bb829db29375811c7ad50687b8624c6fd08"},
|
| 1632 |
+
{file = "jiter-0.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4363818355dbc70ae1a8e9eaba9de350d93ede4ff6992b8f8eb8cbb6e5122d42"},
|
| 1633 |
+
{file = "jiter-0.15.0-cp39-cp39-win32.whl", hash = "sha256:8f7e9bc0f1135039b22ee6eab588d42df1ce55842b30740a352885eb267bd941"},
|
| 1634 |
+
{file = "jiter-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:1c15024a3d892223b18f597c86d59387249dc396590844ce6b9f6131d1093bae"},
|
| 1635 |
+
{file = "jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750"},
|
| 1636 |
+
{file = "jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b"},
|
| 1637 |
+
{file = "jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b"},
|
| 1638 |
+
{file = "jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c"},
|
| 1639 |
+
{file = "jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0"},
|
| 1640 |
+
{file = "jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45"},
|
| 1641 |
+
{file = "jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c"},
|
| 1642 |
+
{file = "jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a"},
|
| 1643 |
+
{file = "jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76"},
|
| 1644 |
+
]
|
| 1645 |
+
|
| 1646 |
[[package]]
|
| 1647 |
name = "keyring"
|
| 1648 |
version = "25.7.0"
|
|
|
|
| 1801 |
{file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"},
|
| 1802 |
]
|
| 1803 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1804 |
[[package]]
|
| 1805 |
name = "markdown-it-py"
|
| 1806 |
version = "4.2.0"
|
|
|
|
| 2579 |
{file = "nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519"},
|
| 2580 |
]
|
| 2581 |
|
| 2582 |
+
[[package]]
|
| 2583 |
+
name = "openai"
|
| 2584 |
+
version = "2.41.1"
|
| 2585 |
+
description = "The official Python library for the openai API"
|
| 2586 |
+
optional = false
|
| 2587 |
+
python-versions = ">=3.9"
|
| 2588 |
+
groups = ["main"]
|
| 2589 |
+
files = [
|
| 2590 |
+
{file = "openai-2.41.1-py3-none-any.whl", hash = "sha256:a939565f350cb7443cb843b801b88c716ac8024b492fb94ca269d5f6b1bbefd6"},
|
| 2591 |
+
{file = "openai-2.41.1.tar.gz", hash = "sha256:23d617a0432457ad844973bee8f540be9da90894f7c5686852d2d365da058f57"},
|
| 2592 |
+
]
|
| 2593 |
+
|
| 2594 |
+
[package.dependencies]
|
| 2595 |
+
anyio = ">=3.5.0,<5"
|
| 2596 |
+
distro = ">=1.7.0,<2"
|
| 2597 |
+
httpx = ">=0.23.0,<1"
|
| 2598 |
+
jiter = ">=0.10.0,<1"
|
| 2599 |
+
pydantic = ">=1.9.0,<3"
|
| 2600 |
+
sniffio = "*"
|
| 2601 |
+
tqdm = ">4"
|
| 2602 |
+
typing-extensions = ">=4.14,<5"
|
| 2603 |
+
|
| 2604 |
+
[package.extras]
|
| 2605 |
+
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
|
| 2606 |
+
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
| 2607 |
+
realtime = ["websockets (>=13,<16)"]
|
| 2608 |
+
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
| 2609 |
+
|
| 2610 |
[[package]]
|
| 2611 |
name = "opencv-python"
|
| 2612 |
version = "4.13.0.92"
|
|
|
|
| 3338 |
[package.dependencies]
|
| 3339 |
typing-extensions = ">=4.14.1"
|
| 3340 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3341 |
[[package]]
|
| 3342 |
name = "pydub"
|
| 3343 |
version = "0.25.1"
|
|
|
|
| 3419 |
[package.dependencies]
|
| 3420 |
six = ">=1.5"
|
| 3421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3422 |
[[package]]
|
| 3423 |
name = "python-multipart"
|
| 3424 |
version = "0.0.32"
|
|
|
|
| 3890 |
]
|
| 3891 |
|
| 3892 |
[[package]]
|
| 3893 |
+
name = "sniffio"
|
| 3894 |
+
version = "1.3.1"
|
| 3895 |
+
description = "Sniff out which async library your code is running under"
|
| 3896 |
optional = false
|
| 3897 |
+
python-versions = ">=3.7"
|
| 3898 |
groups = ["main"]
|
| 3899 |
files = [
|
| 3900 |
+
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
| 3901 |
+
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
| 3902 |
]
|
| 3903 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3904 |
[[package]]
|
| 3905 |
name = "starlette"
|
| 3906 |
version = "1.3.1"
|
|
|
|
| 3920 |
[package.extras]
|
| 3921 |
full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
| 3922 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3923 |
[[package]]
|
| 3924 |
name = "static-ffmpeg"
|
| 3925 |
version = "3.0"
|
|
|
|
| 4431 |
[metadata]
|
| 4432 |
lock-version = "2.1"
|
| 4433 |
python-versions = ">=3.10,!=3.14.1,<3.15"
|
| 4434 |
+
content-hash = "a7600cee02a0c3ea4a71e855778accdc7e1e1272a3e3a42cfff5744a6adbc455"
|
pyproject.toml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
[project]
|
| 2 |
name = "tiny-trigger"
|
| 3 |
version = "0.1.0"
|
| 4 |
-
description = "Open-vocabulary video automations with YOLOE and
|
| 5 |
authors = [
|
| 6 |
{name = "Javier Montalvo",email = "jmrgua@gmail.com"}
|
| 7 |
]
|
|
@@ -15,13 +15,14 @@ dependencies = [
|
|
| 15 |
"PyYAML>=6.0",
|
| 16 |
"requests>=2.32",
|
| 17 |
"replicate>=1.0",
|
|
|
|
|
|
|
| 18 |
"numpy>=1.26",
|
| 19 |
"imageio-ffmpeg>=0.6.0",
|
| 20 |
"static-ffmpeg>=2.13",
|
| 21 |
"torch (>=2.11.0,<3.0.0)",
|
| 22 |
"torchvision (>=0.26.0,<0.27.0)",
|
| 23 |
"torchaudio (>=2.11.0,<3.0.0)",
|
| 24 |
-
"llama-cpp-python[server] (>=0.3.28,<0.4.0)",
|
| 25 |
]
|
| 26 |
|
| 27 |
[tool.poetry.group.dev.dependencies]
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "tiny-trigger"
|
| 3 |
version = "0.1.0"
|
| 4 |
+
description = "Open-vocabulary video automations with YOLOE and cloud rule compilers"
|
| 5 |
authors = [
|
| 6 |
{name = "Javier Montalvo",email = "jmrgua@gmail.com"}
|
| 7 |
]
|
|
|
|
| 15 |
"PyYAML>=6.0",
|
| 16 |
"requests>=2.32",
|
| 17 |
"replicate>=1.0",
|
| 18 |
+
"openai>=2.0",
|
| 19 |
+
"anthropic>=0.75",
|
| 20 |
"numpy>=1.26",
|
| 21 |
"imageio-ffmpeg>=0.6.0",
|
| 22 |
"static-ffmpeg>=2.13",
|
| 23 |
"torch (>=2.11.0,<3.0.0)",
|
| 24 |
"torchvision (>=0.26.0,<0.27.0)",
|
| 25 |
"torchaudio (>=2.11.0,<3.0.0)",
|
|
|
|
| 26 |
]
|
| 27 |
|
| 28 |
[tool.poetry.group.dev.dependencies]
|
requirements.txt
CHANGED
|
@@ -5,4 +5,6 @@ pydantic>=2.8
|
|
| 5 |
PyYAML>=6.0
|
| 6 |
requests>=2.32
|
| 7 |
replicate>=1.0
|
|
|
|
|
|
|
| 8 |
numpy>=1.26
|
|
|
|
| 5 |
PyYAML>=6.0
|
| 6 |
requests>=2.32
|
| 7 |
replicate>=1.0
|
| 8 |
+
openai>=2.0
|
| 9 |
+
anthropic>=0.75
|
| 10 |
numpy>=1.26
|
server.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""gradio.Server backend for the custom Tiny Trigger dashboard.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
built React frontend (``frontend/dist``) and exposes the Tiny Trigger engine as
|
| 5 |
``@app.api`` endpoints that keep Gradio's queuing / SSE / gradio_client
|
| 6 |
compatibility. The frontend talks to these via the ``@gradio/client`` JS library.
|
| 7 |
|
|
@@ -21,7 +20,11 @@ from gradio import FileData, Server
|
|
| 21 |
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 22 |
|
| 23 |
from tiny_trigger import (
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
compile_automation_with_replicate,
|
| 26 |
evaluate_video_detections,
|
| 27 |
load_automation_text,
|
|
@@ -176,10 +179,10 @@ def compile_rules(
|
|
| 176 |
classes: str = "",
|
| 177 |
existing_rules_text: str = "",
|
| 178 |
append: bool = True,
|
| 179 |
-
provider: str = "
|
| 180 |
-
|
| 181 |
-
model: str = "
|
| 182 |
-
replicate_model: str =
|
| 183 |
replicate_reasoning_effort: str = "medium",
|
| 184 |
) -> dict:
|
| 185 |
"""Compile a natural-language request into validated automation rules."""
|
|
@@ -188,26 +191,41 @@ def compile_rules(
|
|
| 188 |
existing = load_automation_text(existing_rules_text)
|
| 189 |
class_names = _merge_class_names(class_names, document_labels(existing))
|
| 190 |
cfg = load_local_config()
|
| 191 |
-
if provider == "
|
| 192 |
-
api_token = os.environ.get("REPLICATE_API_TOKEN") or cfg.replicate_api_token
|
| 193 |
if not api_token:
|
| 194 |
-
raise ValueError("
|
| 195 |
compiled = compile_automation_with_replicate(
|
| 196 |
instruction=instruction,
|
| 197 |
class_names=class_names,
|
| 198 |
api_token=api_token,
|
| 199 |
-
model=replicate_model or cfg.replicate_model or
|
| 200 |
reasoning_effort=(
|
| 201 |
replicate_reasoning_effort or cfg.replicate_reasoning_effort or "medium"
|
| 202 |
),
|
| 203 |
)
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
instruction=instruction,
|
| 207 |
class_names=class_names,
|
| 208 |
-
|
| 209 |
-
model=model or cfg.
|
| 210 |
)
|
|
|
|
|
|
|
| 211 |
document = compiled.document
|
| 212 |
if append and existing_rules_text.strip():
|
| 213 |
existing = load_automation_text(existing_rules_text)
|
|
@@ -296,7 +314,7 @@ def load_rules() -> dict:
|
|
| 296 |
@app.api(name="get_config")
|
| 297 |
def get_config() -> dict:
|
| 298 |
cfg = load_local_config()
|
| 299 |
-
return cfg.model_dump(exclude={"replicate_api_token"})
|
| 300 |
|
| 301 |
|
| 302 |
def _merge_documents(existing: AutomationDocument, compiled: AutomationDocument) -> AutomationDocument:
|
|
@@ -361,4 +379,8 @@ def media(file_path: str) -> Any:
|
|
| 361 |
|
| 362 |
|
| 363 |
if __name__ == "__main__":
|
| 364 |
-
app.launch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""gradio.Server backend for the custom Tiny Trigger dashboard.
|
| 2 |
|
| 3 |
+
Serves the built React frontend (``frontend/dist``) and exposes the Tiny Trigger engine as
|
|
|
|
| 4 |
``@app.api`` endpoints that keep Gradio's queuing / SSE / gradio_client
|
| 5 |
compatibility. The frontend talks to these via the ``@gradio/client`` JS library.
|
| 6 |
|
|
|
|
| 20 |
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 21 |
|
| 22 |
from tiny_trigger import (
|
| 23 |
+
DEFAULT_ANTHROPIC_MODEL,
|
| 24 |
+
DEFAULT_OPENAI_MODEL,
|
| 25 |
+
DEFAULT_REPLICATE_MODEL,
|
| 26 |
+
compile_automation_with_anthropic,
|
| 27 |
+
compile_automation_with_openai,
|
| 28 |
compile_automation_with_replicate,
|
| 29 |
evaluate_video_detections,
|
| 30 |
load_automation_text,
|
|
|
|
| 179 |
classes: str = "",
|
| 180 |
existing_rules_text: str = "",
|
| 181 |
append: bool = True,
|
| 182 |
+
provider: str = "anthropic",
|
| 183 |
+
api_key: str = "",
|
| 184 |
+
model: str = "",
|
| 185 |
+
replicate_model: str = DEFAULT_REPLICATE_MODEL,
|
| 186 |
replicate_reasoning_effort: str = "medium",
|
| 187 |
) -> dict:
|
| 188 |
"""Compile a natural-language request into validated automation rules."""
|
|
|
|
| 191 |
existing = load_automation_text(existing_rules_text)
|
| 192 |
class_names = _merge_class_names(class_names, document_labels(existing))
|
| 193 |
cfg = load_local_config()
|
| 194 |
+
if provider == "replicate":
|
| 195 |
+
api_token = api_key or os.environ.get("REPLICATE_API_TOKEN") or cfg.replicate_api_token
|
| 196 |
if not api_token:
|
| 197 |
+
raise ValueError("Paste a Replicate API token or set REPLICATE_API_TOKEN.")
|
| 198 |
compiled = compile_automation_with_replicate(
|
| 199 |
instruction=instruction,
|
| 200 |
class_names=class_names,
|
| 201 |
api_token=api_token,
|
| 202 |
+
model=model or replicate_model or cfg.replicate_model or DEFAULT_REPLICATE_MODEL,
|
| 203 |
reasoning_effort=(
|
| 204 |
replicate_reasoning_effort or cfg.replicate_reasoning_effort or "medium"
|
| 205 |
),
|
| 206 |
)
|
| 207 |
+
elif provider == "openai":
|
| 208 |
+
openai_key = api_key or os.environ.get("OPENAI_API_KEY") or cfg.openai_api_key
|
| 209 |
+
if not openai_key:
|
| 210 |
+
raise ValueError("Paste an OpenAI API key or set OPENAI_API_KEY.")
|
| 211 |
+
compiled = compile_automation_with_openai(
|
| 212 |
+
instruction=instruction,
|
| 213 |
+
class_names=class_names,
|
| 214 |
+
api_key=openai_key,
|
| 215 |
+
model=model or cfg.openai_model or DEFAULT_OPENAI_MODEL,
|
| 216 |
+
)
|
| 217 |
+
elif provider in {"anthropic", "claude"}:
|
| 218 |
+
anthropic_key = api_key or os.environ.get("ANTHROPIC_API_KEY") or cfg.anthropic_api_key
|
| 219 |
+
if not anthropic_key:
|
| 220 |
+
raise ValueError("Paste an Anthropic API key or set ANTHROPIC_API_KEY.")
|
| 221 |
+
compiled = compile_automation_with_anthropic(
|
| 222 |
instruction=instruction,
|
| 223 |
class_names=class_names,
|
| 224 |
+
api_key=anthropic_key,
|
| 225 |
+
model=model or cfg.anthropic_model or DEFAULT_ANTHROPIC_MODEL,
|
| 226 |
)
|
| 227 |
+
else:
|
| 228 |
+
raise ValueError("Provider must be replicate, openai, or anthropic.")
|
| 229 |
document = compiled.document
|
| 230 |
if append and existing_rules_text.strip():
|
| 231 |
existing = load_automation_text(existing_rules_text)
|
|
|
|
| 314 |
@app.api(name="get_config")
|
| 315 |
def get_config() -> dict:
|
| 316 |
cfg = load_local_config()
|
| 317 |
+
return cfg.model_dump(exclude={"replicate_api_token", "openai_api_key", "anthropic_api_key"})
|
| 318 |
|
| 319 |
|
| 320 |
def _merge_documents(existing: AutomationDocument, compiled: AutomationDocument) -> AutomationDocument:
|
|
|
|
| 379 |
|
| 380 |
|
| 381 |
if __name__ == "__main__":
|
| 382 |
+
app.launch(
|
| 383 |
+
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 384 |
+
server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
|
| 385 |
+
show_error=True,
|
| 386 |
+
)
|
tests/test_automation.py
CHANGED
|
@@ -269,6 +269,35 @@ def test_change_trigger_fires_enter_and_exit() -> None:
|
|
| 269 |
assert [event.action for event in exit_events] == ["turn off lights"]
|
| 270 |
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
def test_video_evaluation_uses_empty_frames_for_exit_triggers() -> None:
|
| 273 |
document = load_automation_text(
|
| 274 |
json.dumps(
|
|
|
|
| 269 |
assert [event.action for event in exit_events] == ["turn off lights"]
|
| 270 |
|
| 271 |
|
| 272 |
+
def test_exit_trigger_accepts_plain_then_actions() -> None:
|
| 273 |
+
document = load_automation_text(
|
| 274 |
+
json.dumps(
|
| 275 |
+
{
|
| 276 |
+
"rules": [
|
| 277 |
+
{
|
| 278 |
+
"name": "person-leaves-turn-off-lights",
|
| 279 |
+
"when": {"all": [{"present": {"label": "person", "min_count": 1}}]},
|
| 280 |
+
"trigger": {"on": "exit"},
|
| 281 |
+
"gate": {"enabled": True},
|
| 282 |
+
"then": [{"type": "simulate", "name": "turn off lights"}],
|
| 283 |
+
}
|
| 284 |
+
]
|
| 285 |
+
}
|
| 286 |
+
)
|
| 287 |
+
)
|
| 288 |
+
engine = RuleEngine(document.rules)
|
| 289 |
+
|
| 290 |
+
first_events = engine.evaluate_frame(
|
| 291 |
+
[detection("person", (0.1, 0.1, 0.2, 0.2))],
|
| 292 |
+
frame_index=0,
|
| 293 |
+
timestamp_sec=0.0,
|
| 294 |
+
)
|
| 295 |
+
exit_events = engine.evaluate_frame([], frame_index=1, timestamp_sec=1.0)
|
| 296 |
+
|
| 297 |
+
assert first_events == []
|
| 298 |
+
assert [event.action for event in exit_events] == ["turn off lights"]
|
| 299 |
+
|
| 300 |
+
|
| 301 |
def test_video_evaluation_uses_empty_frames_for_exit_triggers() -> None:
|
| 302 |
document = load_automation_text(
|
| 303 |
json.dumps(
|
tests/test_llm.py
CHANGED
|
@@ -5,12 +5,17 @@ import sys
|
|
| 5 |
|
| 6 |
from tiny_trigger.automation import AutomationDocument
|
| 7 |
from tiny_trigger.llm import (
|
|
|
|
|
|
|
| 8 |
SYSTEM_PROMPT,
|
|
|
|
| 9 |
_build_user_prompt,
|
| 10 |
_chat_payload,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
compile_automation_with_replicate,
|
| 12 |
-
compile_automation_with_llamacpp,
|
| 13 |
-
_post_chat_completion,
|
| 14 |
_validate_compile_result,
|
| 15 |
extract_json_object,
|
| 16 |
)
|
|
@@ -26,7 +31,7 @@ def test_extract_json_object_from_fenced_response() -> None:
|
|
| 26 |
assert document.rules[0].name == "notify"
|
| 27 |
|
| 28 |
|
| 29 |
-
def
|
| 30 |
payload = _chat_payload(model="qwen", user_prompt="compile this", response_format="json_object")
|
| 31 |
|
| 32 |
assert payload["response_format"] == {"type": "json_object"}
|
|
@@ -58,6 +63,15 @@ def test_prompt_teaches_near_relations() -> None:
|
|
| 58 |
assert '"gate": {"enabled": true, "cooldown": {"key": "package-at-door", "minutes": 15}}' in prompt
|
| 59 |
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def test_invalid_empty_object_fails_validation() -> None:
|
| 62 |
try:
|
| 63 |
_validate_compile_result("{}")
|
|
@@ -67,94 +81,252 @@ def test_invalid_empty_object_fails_validation() -> None:
|
|
| 67 |
raise AssertionError("{} should not validate as an automation document")
|
| 68 |
|
| 69 |
|
| 70 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
class Response:
|
| 72 |
def raise_for_status(self) -> None:
|
| 73 |
return None
|
| 74 |
|
| 75 |
def json(self):
|
| 76 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
class Requests:
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
assert json["response_format"] == {"type": "json_object"}
|
| 82 |
-
assert timeout ==
|
| 83 |
return Response()
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
)
|
| 93 |
-
== "{\"rules\": []}"
|
| 94 |
)
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
def test_compile_retries_after_invalid_response(monkeypatch) -> None:
|
| 98 |
class Response:
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
def raise_for_status(self) -> None:
|
| 103 |
return None
|
| 104 |
|
| 105 |
def json(self):
|
| 106 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
class Requests:
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
'"then":[{"type":"simulate","name":"notify"}]}]}'
|
| 119 |
-
)
|
| 120 |
|
| 121 |
monkeypatch.setitem(sys.modules, "requests", Requests)
|
| 122 |
|
| 123 |
-
result =
|
| 124 |
instruction="if person present notify",
|
| 125 |
class_names=["person"],
|
| 126 |
-
|
| 127 |
-
model="
|
| 128 |
-
timeout=1,
|
| 129 |
)
|
| 130 |
|
| 131 |
-
assert Requests.calls == 2
|
| 132 |
assert result.document.rules[0].name == "notify"
|
| 133 |
|
| 134 |
|
| 135 |
-
def
|
| 136 |
-
class
|
| 137 |
-
|
| 138 |
-
def __init__(self, api_token):
|
| 139 |
-
assert api_token == "test-token"
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
assert input["messages"] == []
|
| 144 |
-
assert input["verbosity"] == "medium"
|
| 145 |
-
assert input["reasoning_effort"] == "low"
|
| 146 |
-
assert "Return only the JSON object." in input["prompt"]
|
| 147 |
-
yield '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
|
| 148 |
-
yield '"then":[{"type":"simulate","name":"notify"}]}]}'
|
| 149 |
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
)
|
| 159 |
|
| 160 |
-
assert
|
|
|
|
| 5 |
|
| 6 |
from tiny_trigger.automation import AutomationDocument
|
| 7 |
from tiny_trigger.llm import (
|
| 8 |
+
DEFAULT_ANTHROPIC_MODEL,
|
| 9 |
+
DEFAULT_OPENAI_MODEL,
|
| 10 |
SYSTEM_PROMPT,
|
| 11 |
+
_anthropic_message,
|
| 12 |
_build_user_prompt,
|
| 13 |
_chat_payload,
|
| 14 |
+
_clean_api_key,
|
| 15 |
+
_openai_chat_completion,
|
| 16 |
+
compile_automation_with_anthropic,
|
| 17 |
+
compile_automation_with_openai,
|
| 18 |
compile_automation_with_replicate,
|
|
|
|
|
|
|
| 19 |
_validate_compile_result,
|
| 20 |
extract_json_object,
|
| 21 |
)
|
|
|
|
| 31 |
assert document.rules[0].name == "notify"
|
| 32 |
|
| 33 |
|
| 34 |
+
def test_openai_payload_uses_json_object() -> None:
|
| 35 |
payload = _chat_payload(model="qwen", user_prompt="compile this", response_format="json_object")
|
| 36 |
|
| 37 |
assert payload["response_format"] == {"type": "json_object"}
|
|
|
|
| 63 |
assert '"gate": {"enabled": true, "cooldown": {"key": "package-at-door", "minutes": 15}}' in prompt
|
| 64 |
|
| 65 |
|
| 66 |
+
def test_api_key_rejects_pasted_traceback() -> None:
|
| 67 |
+
try:
|
| 68 |
+
_clean_api_key(' File "/tmp/example.py", line 1\nrequests.exceptions.HTTPError', "Anthropic")
|
| 69 |
+
except ValueError as exc:
|
| 70 |
+
assert "not an API key" in str(exc) or "single-line token" in str(exc)
|
| 71 |
+
else:
|
| 72 |
+
raise AssertionError("Tracebacks should not be accepted as API keys")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
def test_invalid_empty_object_fails_validation() -> None:
|
| 76 |
try:
|
| 77 |
_validate_compile_result("{}")
|
|
|
|
| 81 |
raise AssertionError("{} should not validate as an automation document")
|
| 82 |
|
| 83 |
|
| 84 |
+
def test_replicate_compile_uses_stream_api(monkeypatch) -> None:
|
| 85 |
+
class Replicate:
|
| 86 |
+
class Client:
|
| 87 |
+
def __init__(self, api_token):
|
| 88 |
+
assert api_token == "test-token"
|
| 89 |
+
|
| 90 |
+
def stream(self, model, input):
|
| 91 |
+
assert model == "openai/gpt-5.2"
|
| 92 |
+
assert input["messages"] == []
|
| 93 |
+
assert input["verbosity"] == "medium"
|
| 94 |
+
assert input["reasoning_effort"] == "low"
|
| 95 |
+
assert "Return only the JSON object." in input["prompt"]
|
| 96 |
+
yield '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
|
| 97 |
+
yield '"then":[{"type":"simulate","name":"notify"}]}]}'
|
| 98 |
+
|
| 99 |
+
monkeypatch.setitem(sys.modules, "replicate", Replicate)
|
| 100 |
+
|
| 101 |
+
result = compile_automation_with_replicate(
|
| 102 |
+
instruction="if person present notify",
|
| 103 |
+
class_names=["person"],
|
| 104 |
+
api_token="test-token",
|
| 105 |
+
model="openai/gpt-5.2",
|
| 106 |
+
reasoning_effort="low",
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
assert result.document.rules[0].name == "notify"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def test_replicate_invalid_json_does_not_auto_repair(monkeypatch) -> None:
|
| 113 |
+
class Replicate:
|
| 114 |
+
class Client:
|
| 115 |
+
calls = 0
|
| 116 |
+
|
| 117 |
+
def __init__(self, api_token):
|
| 118 |
+
return None
|
| 119 |
+
|
| 120 |
+
def stream(self, model, input):
|
| 121 |
+
self.__class__.calls += 1
|
| 122 |
+
yield "not json"
|
| 123 |
+
|
| 124 |
+
monkeypatch.setitem(sys.modules, "replicate", Replicate)
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
compile_automation_with_replicate(
|
| 128 |
+
instruction="if person present notify",
|
| 129 |
+
class_names=["person"],
|
| 130 |
+
api_token="test-token",
|
| 131 |
+
model="openai/gpt-5.2",
|
| 132 |
+
)
|
| 133 |
+
except ValueError as exc:
|
| 134 |
+
assert "Raw response: not json" in str(exc)
|
| 135 |
+
else:
|
| 136 |
+
raise AssertionError("Invalid provider output should fail validation")
|
| 137 |
+
|
| 138 |
+
assert Replicate.Client.calls == 1
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def test_replicate_rate_limit_error_is_human_readable(monkeypatch) -> None:
|
| 142 |
+
class RateLimitError(Exception):
|
| 143 |
+
status = 429
|
| 144 |
+
detail = "Request was throttled. Your rate limit"
|
| 145 |
+
|
| 146 |
+
class Replicate:
|
| 147 |
+
class Client:
|
| 148 |
+
def __init__(self, api_token):
|
| 149 |
+
return None
|
| 150 |
+
|
| 151 |
+
def stream(self, model, input):
|
| 152 |
+
raise RateLimitError()
|
| 153 |
+
yield ""
|
| 154 |
+
|
| 155 |
+
monkeypatch.setitem(sys.modules, "replicate", Replicate)
|
| 156 |
+
|
| 157 |
+
try:
|
| 158 |
+
compile_automation_with_replicate(
|
| 159 |
+
instruction="if person present notify",
|
| 160 |
+
class_names=["person"],
|
| 161 |
+
api_token="test-token",
|
| 162 |
+
model="openai/gpt-5.2",
|
| 163 |
+
)
|
| 164 |
+
except RuntimeError as exc:
|
| 165 |
+
message = str(exc)
|
| 166 |
+
assert "Replicate API request failed" in message
|
| 167 |
+
assert "status 429" in message
|
| 168 |
+
assert "provider rate limit" in message
|
| 169 |
+
else:
|
| 170 |
+
raise AssertionError("Rate limits should surface as RuntimeError")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def test_openai_compile_uses_chat_completions(monkeypatch) -> None:
|
| 174 |
+
monkeypatch.setitem(sys.modules, "openai", None)
|
| 175 |
+
|
| 176 |
class Response:
|
| 177 |
def raise_for_status(self) -> None:
|
| 178 |
return None
|
| 179 |
|
| 180 |
def json(self):
|
| 181 |
+
return {
|
| 182 |
+
"choices": [
|
| 183 |
+
{
|
| 184 |
+
"message": {
|
| 185 |
+
"content": (
|
| 186 |
+
'{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
|
| 187 |
+
'"then":[{"type":"simulate","name":"notify"}]}]}'
|
| 188 |
+
)
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
]
|
| 192 |
+
}
|
| 193 |
|
| 194 |
class Requests:
|
| 195 |
+
@staticmethod
|
| 196 |
+
def post(endpoint, headers, json, timeout):
|
| 197 |
+
assert endpoint == "https://api.openai.com/v1/chat/completions"
|
| 198 |
+
assert headers["Authorization"] == "Bearer test-key"
|
| 199 |
+
assert json["model"] == "gpt-test"
|
| 200 |
assert json["response_format"] == {"type": "json_object"}
|
| 201 |
+
assert timeout == 120.0
|
| 202 |
return Response()
|
| 203 |
|
| 204 |
+
monkeypatch.setitem(sys.modules, "requests", Requests)
|
| 205 |
+
|
| 206 |
+
result = compile_automation_with_openai(
|
| 207 |
+
instruction="if person present notify",
|
| 208 |
+
class_names=["person"],
|
| 209 |
+
api_key="test-key",
|
| 210 |
+
model="gpt-test",
|
|
|
|
|
|
|
| 211 |
)
|
| 212 |
|
| 213 |
+
assert result.document.rules[0].name == "notify"
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def test_openai_sdk_completion() -> None:
|
| 217 |
+
class Message:
|
| 218 |
+
content = '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},"then":[{"type":"simulate","name":"notify"}]}]}'
|
| 219 |
+
|
| 220 |
+
class Choice:
|
| 221 |
+
message = Message()
|
| 222 |
|
|
|
|
| 223 |
class Response:
|
| 224 |
+
choices = [Choice()]
|
| 225 |
+
|
| 226 |
+
class Completions:
|
| 227 |
+
@staticmethod
|
| 228 |
+
def create(**payload):
|
| 229 |
+
assert payload["model"] == DEFAULT_OPENAI_MODEL
|
| 230 |
+
assert payload["response_format"] == {"type": "json_object"}
|
| 231 |
+
return Response()
|
| 232 |
+
|
| 233 |
+
class Chat:
|
| 234 |
+
completions = Completions()
|
| 235 |
+
|
| 236 |
+
class OpenAIClient:
|
| 237 |
+
chat = Chat()
|
| 238 |
+
|
| 239 |
+
def __init__(self, api_key, timeout):
|
| 240 |
+
assert api_key == "test-key"
|
| 241 |
+
assert timeout == 120.0
|
| 242 |
|
| 243 |
+
class OpenAIModule:
|
| 244 |
+
OpenAI = OpenAIClient
|
| 245 |
+
|
| 246 |
+
raw = _openai_chat_completion(
|
| 247 |
+
api_key="test-key",
|
| 248 |
+
model=DEFAULT_OPENAI_MODEL,
|
| 249 |
+
user_prompt="compile",
|
| 250 |
+
timeout=120.0,
|
| 251 |
+
openai_module=OpenAIModule,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
assert json.loads(raw)["rules"][0]["name"] == "notify"
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def test_anthropic_compile_uses_messages_api(monkeypatch) -> None:
|
| 258 |
+
monkeypatch.setitem(sys.modules, "anthropic", None)
|
| 259 |
+
|
| 260 |
+
class Response:
|
| 261 |
def raise_for_status(self) -> None:
|
| 262 |
return None
|
| 263 |
|
| 264 |
def json(self):
|
| 265 |
+
return {
|
| 266 |
+
"content": [
|
| 267 |
+
{
|
| 268 |
+
"type": "text",
|
| 269 |
+
"text": (
|
| 270 |
+
'{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
|
| 271 |
+
'"then":[{"type":"simulate","name":"notify"}]}]}'
|
| 272 |
+
),
|
| 273 |
+
}
|
| 274 |
+
]
|
| 275 |
+
}
|
| 276 |
|
| 277 |
class Requests:
|
| 278 |
+
@staticmethod
|
| 279 |
+
def post(endpoint, headers, json, timeout):
|
| 280 |
+
assert endpoint == "https://api.anthropic.com/v1/messages"
|
| 281 |
+
assert headers["x-api-key"] == "test-key"
|
| 282 |
+
assert headers["anthropic-version"] == "2023-06-01"
|
| 283 |
+
assert json["model"] == "claude-test"
|
| 284 |
+
assert json["system"] == SYSTEM_PROMPT
|
| 285 |
+
assert timeout == 120.0
|
| 286 |
+
return Response()
|
|
|
|
|
|
|
| 287 |
|
| 288 |
monkeypatch.setitem(sys.modules, "requests", Requests)
|
| 289 |
|
| 290 |
+
result = compile_automation_with_anthropic(
|
| 291 |
instruction="if person present notify",
|
| 292 |
class_names=["person"],
|
| 293 |
+
api_key="test-key",
|
| 294 |
+
model="claude-test",
|
|
|
|
| 295 |
)
|
| 296 |
|
|
|
|
| 297 |
assert result.document.rules[0].name == "notify"
|
| 298 |
|
| 299 |
|
| 300 |
+
def test_anthropic_sdk_message() -> None:
|
| 301 |
+
class TextBlock:
|
| 302 |
+
text = '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},"then":[{"type":"simulate","name":"notify"}]}]}'
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
class Response:
|
| 305 |
+
content = [TextBlock()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
+
class Messages:
|
| 308 |
+
@staticmethod
|
| 309 |
+
def create(**payload):
|
| 310 |
+
assert payload["model"] == DEFAULT_ANTHROPIC_MODEL
|
| 311 |
+
assert payload["system"] == SYSTEM_PROMPT
|
| 312 |
+
return Response()
|
| 313 |
|
| 314 |
+
class AnthropicClient:
|
| 315 |
+
messages = Messages()
|
| 316 |
+
|
| 317 |
+
def __init__(self, api_key, timeout):
|
| 318 |
+
assert api_key == "test-key"
|
| 319 |
+
assert timeout == 120.0
|
| 320 |
+
|
| 321 |
+
class AnthropicModule:
|
| 322 |
+
Anthropic = AnthropicClient
|
| 323 |
+
|
| 324 |
+
raw = _anthropic_message(
|
| 325 |
+
api_key="test-key",
|
| 326 |
+
model=DEFAULT_ANTHROPIC_MODEL,
|
| 327 |
+
user_prompt="compile",
|
| 328 |
+
timeout=120.0,
|
| 329 |
+
anthropic_module=AnthropicModule,
|
| 330 |
)
|
| 331 |
|
| 332 |
+
assert json.loads(raw)["rules"][0]["name"] == "notify"
|
tests/test_store.py
CHANGED
|
@@ -22,9 +22,11 @@ def test_local_config_yaml(tmp_path) -> None:
|
|
| 22 |
[
|
| 23 |
"default_device: cuda:0",
|
| 24 |
"webhook_url: http://example.test",
|
| 25 |
-
"llm_provider:
|
| 26 |
"replicate_model: openai/gpt-5.2",
|
| 27 |
"replicate_reasoning_effort: medium",
|
|
|
|
|
|
|
| 28 |
]
|
| 29 |
),
|
| 30 |
encoding="utf-8",
|
|
@@ -34,9 +36,11 @@ def test_local_config_yaml(tmp_path) -> None:
|
|
| 34 |
|
| 35 |
assert config.default_device == "cuda:0"
|
| 36 |
assert config.webhook_url == "http://example.test"
|
| 37 |
-
assert config.llm_provider == "
|
| 38 |
assert config.replicate_model == "openai/gpt-5.2"
|
| 39 |
assert config.replicate_reasoning_effort == "medium"
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
def test_save_and_load_automations(tmp_path) -> None:
|
|
|
|
| 22 |
[
|
| 23 |
"default_device: cuda:0",
|
| 24 |
"webhook_url: http://example.test",
|
| 25 |
+
"llm_provider: anthropic",
|
| 26 |
"replicate_model: openai/gpt-5.2",
|
| 27 |
"replicate_reasoning_effort: medium",
|
| 28 |
+
"openai_model: gpt-5.5",
|
| 29 |
+
"anthropic_model: claude-sonnet-4-6",
|
| 30 |
]
|
| 31 |
),
|
| 32 |
encoding="utf-8",
|
|
|
|
| 36 |
|
| 37 |
assert config.default_device == "cuda:0"
|
| 38 |
assert config.webhook_url == "http://example.test"
|
| 39 |
+
assert config.llm_provider == "anthropic"
|
| 40 |
assert config.replicate_model == "openai/gpt-5.2"
|
| 41 |
assert config.replicate_reasoning_effort == "medium"
|
| 42 |
+
assert config.openai_model == "gpt-5.5"
|
| 43 |
+
assert config.anthropic_model == "claude-sonnet-4-6"
|
| 44 |
|
| 45 |
|
| 46 |
def test_save_and_load_automations(tmp_path) -> None:
|
tiny_trigger/__init__.py
CHANGED
|
@@ -2,13 +2,24 @@
|
|
| 2 |
|
| 3 |
from .automation import RuleEngine, evaluate_video_detections, load_automation_text
|
| 4 |
from .detector import UltralyticsYOLOEDetector, parse_class_prompt
|
| 5 |
-
from .llm import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from .video import process_video
|
| 7 |
|
| 8 |
__all__ = [
|
| 9 |
"RuleEngine",
|
| 10 |
"UltralyticsYOLOEDetector",
|
| 11 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"compile_automation_with_replicate",
|
| 13 |
"evaluate_video_detections",
|
| 14 |
"load_automation_text",
|
|
|
|
| 2 |
|
| 3 |
from .automation import RuleEngine, evaluate_video_detections, load_automation_text
|
| 4 |
from .detector import UltralyticsYOLOEDetector, parse_class_prompt
|
| 5 |
+
from .llm import (
|
| 6 |
+
DEFAULT_ANTHROPIC_MODEL,
|
| 7 |
+
DEFAULT_OPENAI_MODEL,
|
| 8 |
+
DEFAULT_REPLICATE_MODEL,
|
| 9 |
+
compile_automation_with_anthropic,
|
| 10 |
+
compile_automation_with_openai,
|
| 11 |
+
compile_automation_with_replicate,
|
| 12 |
+
)
|
| 13 |
from .video import process_video
|
| 14 |
|
| 15 |
__all__ = [
|
| 16 |
"RuleEngine",
|
| 17 |
"UltralyticsYOLOEDetector",
|
| 18 |
+
"DEFAULT_ANTHROPIC_MODEL",
|
| 19 |
+
"DEFAULT_OPENAI_MODEL",
|
| 20 |
+
"DEFAULT_REPLICATE_MODEL",
|
| 21 |
+
"compile_automation_with_anthropic",
|
| 22 |
+
"compile_automation_with_openai",
|
| 23 |
"compile_automation_with_replicate",
|
| 24 |
"evaluate_video_detections",
|
| 25 |
"load_automation_text",
|
tiny_trigger/automation.py
CHANGED
|
@@ -138,8 +138,8 @@ class AutomationRule(BaseModel):
|
|
| 138 |
if isinstance(self.then, list):
|
| 139 |
if not self.then:
|
| 140 |
raise ValueError("A rule needs at least one action.")
|
| 141 |
-
if self.trigger.on
|
| 142 |
-
raise ValueError("
|
| 143 |
return self
|
| 144 |
|
| 145 |
actions = _actions_for_trigger(self.then, self.trigger.on)
|
|
@@ -408,7 +408,7 @@ def _actions_for_trigger(
|
|
| 408 |
trigger: Literal["while", "enter", "exit", "change"],
|
| 409 |
) -> list[ActionSpec]:
|
| 410 |
if isinstance(actions, list):
|
| 411 |
-
return actions if trigger in {"while", "enter"} else []
|
| 412 |
if trigger == "while":
|
| 413 |
return actions.while_actions or actions.enter
|
| 414 |
if trigger == "enter":
|
|
|
|
| 138 |
if isinstance(self.then, list):
|
| 139 |
if not self.then:
|
| 140 |
raise ValueError("A rule needs at least one action.")
|
| 141 |
+
if self.trigger.on == "change":
|
| 142 |
+
raise ValueError("change triggers need then.enter/then.exit actions.")
|
| 143 |
return self
|
| 144 |
|
| 145 |
actions = _actions_for_trigger(self.then, self.trigger.on)
|
|
|
|
| 408 |
trigger: Literal["while", "enter", "exit", "change"],
|
| 409 |
) -> list[ActionSpec]:
|
| 410 |
if isinstance(actions, list):
|
| 411 |
+
return actions if trigger in {"while", "enter", "exit"} else []
|
| 412 |
if trigger == "while":
|
| 413 |
return actions.while_actions or actions.enter
|
| 414 |
if trigger == "enter":
|
tiny_trigger/llm.py
CHANGED
|
@@ -17,65 +17,33 @@ Use state gates in gate: enabled, cooldown.
|
|
| 17 |
Use trigger.on for edge behavior: while, enter, exit, change.
|
| 18 |
Use only these action types: simulate, webhook.
|
| 19 |
Use trigger.on="enter" for state assertions like "must be on", "should be on", "keep on", "turn on when", or "notify when".
|
|
|
|
| 20 |
Use trigger.on="while" only when the user explicitly wants repeated actions while a condition remains true, usually with a cooldown.
|
| 21 |
When the request says one object is near, next to, beside, at, by, close to, or in front of another object, you MUST emit a near condition.
|
| 22 |
Do not replace a near relation with two present conditions.
|
| 23 |
Use max_gap_percent for near/far box-edge distance. It is the largest horizontal/vertical edge gap between boxes in normalized frame percent; touching or overlapping boxes have gap 0.
|
| 24 |
If the user mentions elapsed time since an action or limiting repeat fires, encode it as gate.cooldown.
|
| 25 |
-
If the user asks for
|
| 26 |
"""
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
class LLMCompileResult(BaseModel):
|
| 30 |
raw_text: str
|
| 31 |
document: AutomationDocument
|
| 32 |
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
) -> LLMCompileResult:
|
| 42 |
-
"""Compile natural language into validated rules through llama.cpp server."""
|
| 43 |
-
try:
|
| 44 |
-
import requests
|
| 45 |
-
except ImportError as exc: # pragma: no cover - dependency guard
|
| 46 |
-
raise RuntimeError("Install requests to use the llama.cpp compiler.") from exc
|
| 47 |
-
|
| 48 |
-
endpoint = base_url.rstrip("/") + "/chat/completions"
|
| 49 |
-
user_prompt = _build_user_prompt(instruction=instruction, class_names=class_names)
|
| 50 |
-
raw_text = _post_chat_completion(
|
| 51 |
-
endpoint=endpoint,
|
| 52 |
-
model=model,
|
| 53 |
-
user_prompt=user_prompt,
|
| 54 |
-
timeout=timeout,
|
| 55 |
-
requests_module=requests,
|
| 56 |
)
|
| 57 |
-
try:
|
| 58 |
-
return _validate_compile_result(raw_text)
|
| 59 |
-
except (json.JSONDecodeError, ValidationError, ValueError) as first_error:
|
| 60 |
-
repair_prompt = _build_repair_prompt(
|
| 61 |
-
original_prompt=user_prompt,
|
| 62 |
-
bad_response=raw_text,
|
| 63 |
-
error=str(first_error),
|
| 64 |
-
)
|
| 65 |
-
repaired_text = _post_chat_completion(
|
| 66 |
-
endpoint=endpoint,
|
| 67 |
-
model=model,
|
| 68 |
-
user_prompt=repair_prompt,
|
| 69 |
-
timeout=timeout,
|
| 70 |
-
requests_module=requests,
|
| 71 |
-
)
|
| 72 |
-
try:
|
| 73 |
-
return _validate_compile_result(repaired_text)
|
| 74 |
-
except (json.JSONDecodeError, ValidationError, ValueError) as second_error:
|
| 75 |
-
raise ValueError(
|
| 76 |
-
"llama.cpp returned invalid automation JSON after a repair attempt. "
|
| 77 |
-
f"Last validation error: {second_error}. Last response: {repaired_text}"
|
| 78 |
-
) from second_error
|
| 79 |
|
| 80 |
|
| 81 |
def compile_automation_with_replicate(
|
|
@@ -83,11 +51,12 @@ def compile_automation_with_replicate(
|
|
| 83 |
instruction: str,
|
| 84 |
class_names: list[str],
|
| 85 |
api_token: str,
|
| 86 |
-
model: str =
|
| 87 |
reasoning_effort: str = "medium",
|
| 88 |
timeout: float = 600.0,
|
| 89 |
) -> LLMCompileResult:
|
| 90 |
"""Compile natural language into validated rules through Replicate."""
|
|
|
|
| 91 |
try:
|
| 92 |
import replicate
|
| 93 |
except ImportError as exc: # pragma: no cover - dependency guard
|
|
@@ -104,27 +73,88 @@ def compile_automation_with_replicate(
|
|
| 104 |
)
|
| 105 |
try:
|
| 106 |
return _validate_compile_result(raw_text)
|
| 107 |
-
except (json.JSONDecodeError, ValidationError, ValueError) as
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
)
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
model=model,
|
| 115 |
-
|
| 116 |
-
api_token=api_token,
|
| 117 |
-
reasoning_effort=reasoning_effort,
|
| 118 |
timeout=timeout,
|
| 119 |
-
|
| 120 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
try:
|
| 122 |
-
|
| 123 |
-
except
|
| 124 |
-
raise
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
|
| 130 |
def _provider_prompt(user_prompt: str) -> str:
|
|
@@ -149,8 +179,11 @@ def _stream_replicate_completion(
|
|
| 149 |
}
|
| 150 |
client = replicate_module.Client(api_token=api_token)
|
| 151 |
chunks: list[str] = []
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
| 154 |
text = "".join(chunks).strip()
|
| 155 |
if not text:
|
| 156 |
raise ValueError("Replicate stream returned no output.")
|
|
@@ -164,21 +197,115 @@ def _split_replicate_model(model: str) -> tuple[str, str]:
|
|
| 164 |
return parts[0], parts[1]
|
| 165 |
|
| 166 |
|
| 167 |
-
def
|
| 168 |
*,
|
| 169 |
endpoint: str,
|
|
|
|
| 170 |
model: str,
|
| 171 |
user_prompt: str,
|
| 172 |
timeout: float,
|
| 173 |
requests_module: Any,
|
| 174 |
) -> str:
|
| 175 |
payload = _chat_payload(model=model, user_prompt=user_prompt, response_format="json_object")
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
body = response.json()
|
| 179 |
return body["choices"][0]["message"]["content"]
|
| 180 |
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
def _validate_compile_result(raw_text: str) -> LLMCompileResult:
|
| 183 |
data = json.loads(extract_json_object(raw_text))
|
| 184 |
document = AutomationDocument.model_validate(data)
|
|
@@ -187,6 +314,60 @@ def _validate_compile_result(raw_text: str) -> LLMCompileResult:
|
|
| 187 |
return LLMCompileResult(raw_text=raw_text, document=document)
|
| 188 |
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
def extract_json_object(text: str) -> str:
|
| 191 |
stripped = text.strip()
|
| 192 |
if stripped.startswith("```"):
|
|
@@ -197,7 +378,7 @@ def extract_json_object(text: str) -> str:
|
|
| 197 |
|
| 198 |
match = re.search(r"\{.*\}", stripped, flags=re.DOTALL)
|
| 199 |
if not match:
|
| 200 |
-
raise ValueError("
|
| 201 |
return match.group(0)
|
| 202 |
|
| 203 |
|
|
@@ -214,7 +395,7 @@ Return a JSON object matching this high-level shape:
|
|
| 214 |
{{
|
| 215 |
"name": "short-kebab-case-name",
|
| 216 |
"when": {{"all": [{{"present": {{"label": "cat", "min_count": 1}}}}]}},
|
| 217 |
-
"trigger": {{"on": "
|
| 218 |
"gate": {{"enabled": true}},
|
| 219 |
"then": [{{"type": "simulate", "name": "action name"}}]
|
| 220 |
}}
|
|
|
|
| 17 |
Use trigger.on for edge behavior: while, enter, exit, change.
|
| 18 |
Use only these action types: simulate, webhook.
|
| 19 |
Use trigger.on="enter" for state assertions like "must be on", "should be on", "keep on", "turn on when", or "notify when".
|
| 20 |
+
Use trigger.on="exit" with a plain then action list for requests like "when the person leaves", "when it disappears", or "when it stops meeting the condition".
|
| 21 |
Use trigger.on="while" only when the user explicitly wants repeated actions while a condition remains true, usually with a cooldown.
|
| 22 |
When the request says one object is near, next to, beside, at, by, close to, or in front of another object, you MUST emit a near condition.
|
| 23 |
Do not replace a near relation with two present conditions.
|
| 24 |
Use max_gap_percent for near/far box-edge distance. It is the largest horizontal/vertical edge gap between boxes in normalized frame percent; touching or overlapping boxes have gap 0.
|
| 25 |
If the user mentions elapsed time since an action or limiting repeat fires, encode it as gate.cooldown.
|
| 26 |
+
If the user asks for one action when a condition starts and another action when it stops, use trigger.on="change" and then.enter / then.exit.
|
| 27 |
"""
|
| 28 |
|
| 29 |
+
DEFAULT_REPLICATE_MODEL = "openai/gpt-5.2"
|
| 30 |
+
DEFAULT_OPENAI_MODEL = "gpt-5.5"
|
| 31 |
+
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6"
|
| 32 |
+
|
| 33 |
|
| 34 |
class LLMCompileResult(BaseModel):
|
| 35 |
raw_text: str
|
| 36 |
document: AutomationDocument
|
| 37 |
|
| 38 |
|
| 39 |
+
def _invalid_json_error(provider: str, raw_text: str, error: Exception) -> ValueError:
|
| 40 |
+
preview = raw_text.strip()
|
| 41 |
+
if len(preview) > 1200:
|
| 42 |
+
preview = preview[:1200] + "..."
|
| 43 |
+
return ValueError(
|
| 44 |
+
f"{provider} returned text that was not valid Tiny Trigger JSON. "
|
| 45 |
+
f"Validation error: {error}. Raw response: {preview}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
def compile_automation_with_replicate(
|
|
|
|
| 51 |
instruction: str,
|
| 52 |
class_names: list[str],
|
| 53 |
api_token: str,
|
| 54 |
+
model: str = DEFAULT_REPLICATE_MODEL,
|
| 55 |
reasoning_effort: str = "medium",
|
| 56 |
timeout: float = 600.0,
|
| 57 |
) -> LLMCompileResult:
|
| 58 |
"""Compile natural language into validated rules through Replicate."""
|
| 59 |
+
api_token = _clean_api_key(api_token, "Replicate")
|
| 60 |
try:
|
| 61 |
import replicate
|
| 62 |
except ImportError as exc: # pragma: no cover - dependency guard
|
|
|
|
| 73 |
)
|
| 74 |
try:
|
| 75 |
return _validate_compile_result(raw_text)
|
| 76 |
+
except (json.JSONDecodeError, ValidationError, ValueError) as exc:
|
| 77 |
+
raise _invalid_json_error("Replicate", raw_text, exc) from exc
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def compile_automation_with_openai(
|
| 81 |
+
*,
|
| 82 |
+
instruction: str,
|
| 83 |
+
class_names: list[str],
|
| 84 |
+
api_key: str,
|
| 85 |
+
model: str = DEFAULT_OPENAI_MODEL,
|
| 86 |
+
timeout: float = 120.0,
|
| 87 |
+
) -> LLMCompileResult:
|
| 88 |
+
"""Compile natural language into validated rules through OpenAI."""
|
| 89 |
+
api_key = _clean_api_key(api_key, "OpenAI")
|
| 90 |
+
user_prompt = _build_user_prompt(instruction=instruction, class_names=class_names)
|
| 91 |
+
try:
|
| 92 |
+
import openai
|
| 93 |
+
except ImportError:
|
| 94 |
+
try:
|
| 95 |
+
import requests
|
| 96 |
+
except ImportError as exc: # pragma: no cover - dependency guard
|
| 97 |
+
raise RuntimeError("Install openai or requests to use the OpenAI compiler.") from exc
|
| 98 |
+
raw_text = _post_openai_chat_completion(
|
| 99 |
+
endpoint="https://api.openai.com/v1/chat/completions",
|
| 100 |
+
api_key=api_key,
|
| 101 |
+
model=model,
|
| 102 |
+
user_prompt=user_prompt,
|
| 103 |
+
timeout=timeout,
|
| 104 |
+
requests_module=requests,
|
| 105 |
)
|
| 106 |
+
else:
|
| 107 |
+
raw_text = _openai_chat_completion(
|
| 108 |
+
api_key=api_key,
|
| 109 |
model=model,
|
| 110 |
+
user_prompt=user_prompt,
|
|
|
|
|
|
|
| 111 |
timeout=timeout,
|
| 112 |
+
openai_module=openai,
|
| 113 |
)
|
| 114 |
+
try:
|
| 115 |
+
return _validate_compile_result(raw_text)
|
| 116 |
+
except (json.JSONDecodeError, ValidationError, ValueError) as exc:
|
| 117 |
+
raise _invalid_json_error("OpenAI", raw_text, exc) from exc
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def compile_automation_with_anthropic(
|
| 121 |
+
*,
|
| 122 |
+
instruction: str,
|
| 123 |
+
class_names: list[str],
|
| 124 |
+
api_key: str,
|
| 125 |
+
model: str = DEFAULT_ANTHROPIC_MODEL,
|
| 126 |
+
timeout: float = 120.0,
|
| 127 |
+
) -> LLMCompileResult:
|
| 128 |
+
"""Compile natural language into validated rules through Anthropic Claude."""
|
| 129 |
+
api_key = _clean_api_key(api_key, "Anthropic")
|
| 130 |
+
user_prompt = _build_user_prompt(instruction=instruction, class_names=class_names)
|
| 131 |
+
try:
|
| 132 |
+
import anthropic
|
| 133 |
+
except ImportError:
|
| 134 |
try:
|
| 135 |
+
import requests
|
| 136 |
+
except ImportError as exc: # pragma: no cover - dependency guard
|
| 137 |
+
raise RuntimeError("Install anthropic or requests to use the Anthropic compiler.") from exc
|
| 138 |
+
raw_text = _post_anthropic_message(
|
| 139 |
+
endpoint="https://api.anthropic.com/v1/messages",
|
| 140 |
+
api_key=api_key,
|
| 141 |
+
model=model,
|
| 142 |
+
user_prompt=user_prompt,
|
| 143 |
+
timeout=timeout,
|
| 144 |
+
requests_module=requests,
|
| 145 |
+
)
|
| 146 |
+
else:
|
| 147 |
+
raw_text = _anthropic_message(
|
| 148 |
+
api_key=api_key,
|
| 149 |
+
model=model,
|
| 150 |
+
user_prompt=user_prompt,
|
| 151 |
+
timeout=timeout,
|
| 152 |
+
anthropic_module=anthropic,
|
| 153 |
+
)
|
| 154 |
+
try:
|
| 155 |
+
return _validate_compile_result(raw_text)
|
| 156 |
+
except (json.JSONDecodeError, ValidationError, ValueError) as exc:
|
| 157 |
+
raise _invalid_json_error("Anthropic", raw_text, exc) from exc
|
| 158 |
|
| 159 |
|
| 160 |
def _provider_prompt(user_prompt: str) -> str:
|
|
|
|
| 179 |
}
|
| 180 |
client = replicate_module.Client(api_token=api_token)
|
| 181 |
chunks: list[str] = []
|
| 182 |
+
try:
|
| 183 |
+
for event in client.stream(model, input=payload):
|
| 184 |
+
chunks.append(str(event))
|
| 185 |
+
except Exception as exc:
|
| 186 |
+
raise RuntimeError(_provider_exception_message("Replicate", exc)) from exc
|
| 187 |
text = "".join(chunks).strip()
|
| 188 |
if not text:
|
| 189 |
raise ValueError("Replicate stream returned no output.")
|
|
|
|
| 197 |
return parts[0], parts[1]
|
| 198 |
|
| 199 |
|
| 200 |
+
def _post_openai_chat_completion(
|
| 201 |
*,
|
| 202 |
endpoint: str,
|
| 203 |
+
api_key: str,
|
| 204 |
model: str,
|
| 205 |
user_prompt: str,
|
| 206 |
timeout: float,
|
| 207 |
requests_module: Any,
|
| 208 |
) -> str:
|
| 209 |
payload = _chat_payload(model=model, user_prompt=user_prompt, response_format="json_object")
|
| 210 |
+
try:
|
| 211 |
+
response = requests_module.post(
|
| 212 |
+
endpoint,
|
| 213 |
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
| 214 |
+
json=payload,
|
| 215 |
+
timeout=timeout,
|
| 216 |
+
)
|
| 217 |
+
response.raise_for_status()
|
| 218 |
+
except Exception as exc:
|
| 219 |
+
raise RuntimeError(_provider_exception_message("OpenAI", exc)) from exc
|
| 220 |
body = response.json()
|
| 221 |
return body["choices"][0]["message"]["content"]
|
| 222 |
|
| 223 |
|
| 224 |
+
def _openai_chat_completion(
|
| 225 |
+
*,
|
| 226 |
+
api_key: str,
|
| 227 |
+
model: str,
|
| 228 |
+
user_prompt: str,
|
| 229 |
+
timeout: float,
|
| 230 |
+
openai_module: Any,
|
| 231 |
+
) -> str:
|
| 232 |
+
client = openai_module.OpenAI(api_key=api_key, timeout=timeout)
|
| 233 |
+
try:
|
| 234 |
+
response = client.chat.completions.create(
|
| 235 |
+
**_chat_payload(model=model, user_prompt=user_prompt, response_format="json_object")
|
| 236 |
+
)
|
| 237 |
+
except Exception as exc:
|
| 238 |
+
raise RuntimeError(_provider_exception_message("OpenAI", exc)) from exc
|
| 239 |
+
content = response.choices[0].message.content
|
| 240 |
+
if isinstance(content, list):
|
| 241 |
+
return "".join(str(part.get("text", "")) for part in content if isinstance(part, dict))
|
| 242 |
+
return str(content or "")
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _post_anthropic_message(
|
| 246 |
+
*,
|
| 247 |
+
endpoint: str,
|
| 248 |
+
api_key: str,
|
| 249 |
+
model: str,
|
| 250 |
+
user_prompt: str,
|
| 251 |
+
timeout: float,
|
| 252 |
+
requests_module: Any,
|
| 253 |
+
) -> str:
|
| 254 |
+
try:
|
| 255 |
+
response = requests_module.post(
|
| 256 |
+
endpoint,
|
| 257 |
+
headers={
|
| 258 |
+
"x-api-key": api_key,
|
| 259 |
+
"anthropic-version": "2023-06-01",
|
| 260 |
+
"Content-Type": "application/json",
|
| 261 |
+
},
|
| 262 |
+
json={
|
| 263 |
+
"model": model,
|
| 264 |
+
"system": SYSTEM_PROMPT,
|
| 265 |
+
"messages": [{"role": "user", "content": user_prompt}],
|
| 266 |
+
"max_tokens": 512,
|
| 267 |
+
"temperature": 0,
|
| 268 |
+
},
|
| 269 |
+
timeout=timeout,
|
| 270 |
+
)
|
| 271 |
+
response.raise_for_status()
|
| 272 |
+
except Exception as exc:
|
| 273 |
+
raise RuntimeError(_provider_exception_message("Anthropic", exc)) from exc
|
| 274 |
+
body = response.json()
|
| 275 |
+
chunks = body.get("content") or []
|
| 276 |
+
return "".join(str(chunk.get("text", "")) for chunk in chunks if isinstance(chunk, dict))
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def _anthropic_message(
|
| 280 |
+
*,
|
| 281 |
+
api_key: str,
|
| 282 |
+
model: str,
|
| 283 |
+
user_prompt: str,
|
| 284 |
+
timeout: float,
|
| 285 |
+
anthropic_module: Any,
|
| 286 |
+
) -> str:
|
| 287 |
+
client = anthropic_module.Anthropic(api_key=api_key, timeout=timeout)
|
| 288 |
+
try:
|
| 289 |
+
response = client.messages.create(
|
| 290 |
+
model=model,
|
| 291 |
+
system=SYSTEM_PROMPT,
|
| 292 |
+
messages=[{"role": "user", "content": user_prompt}],
|
| 293 |
+
max_tokens=512,
|
| 294 |
+
temperature=0,
|
| 295 |
+
)
|
| 296 |
+
except Exception as exc:
|
| 297 |
+
raise RuntimeError(_provider_exception_message("Anthropic", exc)) from exc
|
| 298 |
+
chunks = getattr(response, "content", []) or []
|
| 299 |
+
texts: list[str] = []
|
| 300 |
+
for chunk in chunks:
|
| 301 |
+
text = getattr(chunk, "text", None)
|
| 302 |
+
if text is None and isinstance(chunk, dict):
|
| 303 |
+
text = chunk.get("text")
|
| 304 |
+
if text:
|
| 305 |
+
texts.append(str(text))
|
| 306 |
+
return "".join(texts)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
def _validate_compile_result(raw_text: str) -> LLMCompileResult:
|
| 310 |
data = json.loads(extract_json_object(raw_text))
|
| 311 |
document = AutomationDocument.model_validate(data)
|
|
|
|
| 314 |
return LLMCompileResult(raw_text=raw_text, document=document)
|
| 315 |
|
| 316 |
|
| 317 |
+
def _clean_api_key(api_key: str | None, provider: str) -> str:
|
| 318 |
+
cleaned = (api_key or "").strip()
|
| 319 |
+
if not cleaned:
|
| 320 |
+
raise ValueError(f"Paste a {provider} API key.")
|
| 321 |
+
if any(char in cleaned for char in ("\n", "\r", "\t")):
|
| 322 |
+
raise ValueError(f"{provider} API key must be a single-line token with no whitespace.")
|
| 323 |
+
if "Traceback" in cleaned or 'File "' in cleaned:
|
| 324 |
+
raise ValueError(
|
| 325 |
+
f"{provider} API key field looks like it contains a pasted error log, not an API key."
|
| 326 |
+
)
|
| 327 |
+
return cleaned
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def _provider_exception_message(provider: str, exc: Exception) -> str:
|
| 331 |
+
parts = [f"{provider} API request failed"]
|
| 332 |
+
status = getattr(exc, "status", None) or getattr(exc, "status_code", None)
|
| 333 |
+
if status:
|
| 334 |
+
parts.append(f"status {status}")
|
| 335 |
+
detail = getattr(exc, "detail", None)
|
| 336 |
+
if detail:
|
| 337 |
+
parts.append(str(detail))
|
| 338 |
+
response = getattr(exc, "response", None)
|
| 339 |
+
if response is not None:
|
| 340 |
+
response_status = getattr(response, "status_code", None)
|
| 341 |
+
if response_status and not status:
|
| 342 |
+
parts.append(f"status {response_status}")
|
| 343 |
+
try:
|
| 344 |
+
body = response.json()
|
| 345 |
+
except Exception:
|
| 346 |
+
body = getattr(response, "text", "")
|
| 347 |
+
if body:
|
| 348 |
+
parts.append(str(body))
|
| 349 |
+
if len(parts) == 1:
|
| 350 |
+
parts.append(str(exc))
|
| 351 |
+
message = ". ".join(part for part in parts if part)
|
| 352 |
+
status_text = str(status or "")
|
| 353 |
+
if response is not None:
|
| 354 |
+
response_status = getattr(response, "status_code", None)
|
| 355 |
+
if response_status:
|
| 356 |
+
status_text = str(response_status)
|
| 357 |
+
lower_message = message.lower()
|
| 358 |
+
if status_text == "429" or "throttled" in lower_message or "rate limit" in lower_message:
|
| 359 |
+
message += (
|
| 360 |
+
". This is a provider rate limit, separate from account credits. "
|
| 361 |
+
"Wait a bit or switch provider."
|
| 362 |
+
)
|
| 363 |
+
if status_text == "404" or "not found" in lower_message:
|
| 364 |
+
message += (
|
| 365 |
+
". This usually means the configured model is unavailable, deprecated, "
|
| 366 |
+
"or misspelled for this provider."
|
| 367 |
+
)
|
| 368 |
+
return message
|
| 369 |
+
|
| 370 |
+
|
| 371 |
def extract_json_object(text: str) -> str:
|
| 372 |
stripped = text.strip()
|
| 373 |
if stripped.startswith("```"):
|
|
|
|
| 378 |
|
| 379 |
match = re.search(r"\{.*\}", stripped, flags=re.DOTALL)
|
| 380 |
if not match:
|
| 381 |
+
raise ValueError("LLM response did not contain a JSON object.")
|
| 382 |
return match.group(0)
|
| 383 |
|
| 384 |
|
|
|
|
| 395 |
{{
|
| 396 |
"name": "short-kebab-case-name",
|
| 397 |
"when": {{"all": [{{"present": {{"label": "cat", "min_count": 1}}}}]}},
|
| 398 |
+
"trigger": {{"on": "enter"}},
|
| 399 |
"gate": {{"enabled": true}},
|
| 400 |
"then": [{{"type": "simulate", "name": "action name"}}]
|
| 401 |
}}
|
tiny_trigger/store.py
CHANGED
|
@@ -26,12 +26,14 @@ class LocalConfig(BaseModel):
|
|
| 26 |
default_device: str | None = None
|
| 27 |
default_image_size: int | None = None
|
| 28 |
default_max_detections: int | None = None
|
| 29 |
-
llamacpp_base_url: str | None = None
|
| 30 |
-
llamacpp_model: str | None = None
|
| 31 |
llm_provider: str | None = None
|
| 32 |
replicate_api_token: str | None = None
|
| 33 |
replicate_model: str | None = None
|
| 34 |
replicate_reasoning_effort: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
class RuntimeState(BaseModel):
|
|
|
|
| 26 |
default_device: str | None = None
|
| 27 |
default_image_size: int | None = None
|
| 28 |
default_max_detections: int | None = None
|
|
|
|
|
|
|
| 29 |
llm_provider: str | None = None
|
| 30 |
replicate_api_token: str | None = None
|
| 31 |
replicate_model: str | None = None
|
| 32 |
replicate_reasoning_effort: str | None = None
|
| 33 |
+
openai_api_key: str | None = None
|
| 34 |
+
openai_model: str | None = None
|
| 35 |
+
anthropic_api_key: str | None = None
|
| 36 |
+
anthropic_model: str | None = None
|
| 37 |
|
| 38 |
|
| 39 |
class RuntimeState(BaseModel):
|