Spaces:
Running
Running
Stephen Lee commited on
Commit ·
a975979
1
Parent(s): a3c85ad
feat: ship source-backed Gradio Q&A
Browse files- README.md +63 -27
- app.py +147 -24
- coastwise/advisories.py +50 -1
- coastwise/submission.py +17 -2
- tests/test_advisories.py +11 -0
- tests/test_app.py +117 -0
- tests/test_privacy.py +19 -0
- tests/test_safety_copy.py +16 -0
- tests/test_submission.py +30 -0
README.md
CHANGED
|
@@ -17,10 +17,11 @@ tags:
|
|
| 17 |
|
| 18 |
# CoastWise
|
| 19 |
|
| 20 |
-
CoastWise is an offline-first California
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
The MVP is a decision-support field guide. It does not certify species
|
| 26 |
identification, legality, harvest authorization, or food safety.
|
|
@@ -31,13 +32,13 @@ Target track: Backyard AI.
|
|
| 31 |
|
| 32 |
Target badges and awards, pending final evidence:
|
| 33 |
|
| 34 |
-
- Best Agent:
|
| 35 |
-
deterministic
|
| 36 |
- OpenBMB: MiniCPM5-1B is the planned small-model path for constrained
|
| 37 |
safety-result explanation.
|
| 38 |
- Tiny Titan: the submitted model inventory stays at or below 4B parameters.
|
| 39 |
-
- Off the Grid:
|
| 40 |
-
without cloud APIs.
|
| 41 |
- Off Brand: custom CoastWise Gradio interface.
|
| 42 |
- Best Demo / Field Notes: public demo video, social post, and write-up links
|
| 43 |
will be added before submission.
|
|
@@ -50,25 +51,35 @@ evidence.
|
|
| 50 |
|
| 51 |
## MVP Scope
|
| 52 |
|
| 53 |
-
-
|
| 54 |
-
|
| 55 |
-
-
|
| 56 |
-
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
## Model Inventory
|
| 64 |
|
| 65 |
| Model | Provider | Source URL | Role | Parameters | Runtime mode | Required for core safety | Award relevance |
|
| 66 |
|---|---|---|---|---:|---|---|---|
|
| 67 |
-
| MiniCPM5-1B | OpenBMB | https://huggingface.co/openbmb/MiniCPM5-1B |
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
| 72 |
|
| 73 |
All listed models are under the hackathon 32B cap. Tiny Titan remains eligible
|
| 74 |
only while every submitted model stays at or below 4B total parameters.
|
|
@@ -76,19 +87,31 @@ only while every submitted model stays at or below 4B total parameters.
|
|
| 76 |
## Official Sources And Freshness
|
| 77 |
|
| 78 |
Cached official source metadata is bundled for offline lookup. CoastWise treats
|
| 79 |
-
cached
|
| 80 |
-
|
| 81 |
-
already applies.
|
| 82 |
|
| 83 |
-
|
|
|
|
| 84 |
|
| 85 |
| Source | Use | Cached URL |
|
| 86 |
|---|---|---|
|
| 87 |
-
| CDFW
|
| 88 |
-
| CDFW Ocean Sport Fishing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
| CDPH Shellfish Advisories | Shellfish advisory overlay source | https://www.cdph.ca.gov/Programs/OPA/Pages/Shellfish-Advisories.aspx |
|
| 90 |
| CDPH Annual Mussel Quarantine | Annual mussel quarantine overlay source | https://www.cdph.ca.gov/Programs/CEH/DRSEM/Pages/EMB/Shellfish/Annual-Mussel-Quarantine.aspx |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
## Demo And Public Evidence
|
| 93 |
|
| 94 |
- Demo video: pending, add a public link before final submission.
|
|
@@ -98,6 +121,19 @@ Cache date for the current curated source set: 2026-06-13.
|
|
| 98 |
- Public Space history: pending, push meaningful Codex-attributed commits to the
|
| 99 |
Hugging Face Space repository before claiming the OpenAI Codex award.
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
## Local Run
|
| 102 |
|
| 103 |
```bash
|
|
|
|
| 17 |
|
| 18 |
# CoastWise
|
| 19 |
|
| 20 |
+
CoastWise is an offline-first California ocean regulation and harvest-safety
|
| 21 |
+
Q&A MVP. It lets users ask practical questions such as "what's the min size of
|
| 22 |
+
lingcod?" or "can I eat mussels from Pacifica today?" and answers from cached
|
| 23 |
+
official CDFW/CDPH source snapshots with source links, retrieval time,
|
| 24 |
+
freshness status, uncertainty, and a next safe action.
|
| 25 |
|
| 26 |
The MVP is a decision-support field guide. It does not certify species
|
| 27 |
identification, legality, harvest authorization, or food safety.
|
|
|
|
| 32 |
|
| 33 |
Target badges and awards, pending final evidence:
|
| 34 |
|
| 35 |
+
- Best Agent: source-backed question interpretation, evidence selection, and
|
| 36 |
+
constrained wording over deterministic CDFW/CDPH facts or snippets.
|
| 37 |
- OpenBMB: MiniCPM5-1B is the planned small-model path for constrained
|
| 38 |
safety-result explanation.
|
| 39 |
- Tiny Titan: the submitted model inventory stays at or below 4B parameters.
|
| 40 |
+
- Off the Grid: bundled seed cache and last-good runtime cache can answer
|
| 41 |
+
supported questions without cloud APIs.
|
| 42 |
- Off Brand: custom CoastWise Gradio interface.
|
| 43 |
- Best Demo / Field Notes: public demo video, social post, and write-up links
|
| 44 |
will be added before submission.
|
|
|
|
| 51 |
|
| 52 |
## MVP Scope
|
| 53 |
|
| 54 |
+
- Natural-language Q&A for California ocean sport fishing regulations and CDPH
|
| 55 |
+
advisory/quarantine topics.
|
| 56 |
+
- Statewide official source registry covering CDFW ocean regulations,
|
| 57 |
+
in-season changes, Northern, Mendocino, San Francisco, Central, Southern, SF
|
| 58 |
+
Bay, Groundfish Summary, Crabs, CDPH Shellfish Advisories, and CDPH Annual
|
| 59 |
+
Mussel Quarantine sources.
|
| 60 |
+
- Structured answers for the reviewed validation set, including lingcod size,
|
| 61 |
+
lingcod bag limit, Dungeness crab guidance, CDPH shellfish advisories, and
|
| 62 |
+
annual mussel quarantine.
|
| 63 |
+
- Cited official snippet fallback when cached text exists but structured
|
| 64 |
+
extraction is incomplete.
|
| 65 |
+
- Startup/manual refresh with last-good cache preservation and visible
|
| 66 |
+
updated/unchanged/failed source status.
|
| 67 |
+
- 24-hour stale-data handling that still answers from cache when offline while
|
| 68 |
+
making verification prominent.
|
| 69 |
+
- Session-only handling for questions, optional location text, generated
|
| 70 |
+
wording, and refresh UI state.
|
| 71 |
+
- Photo candidate support is not part of the core source-backed MVP flow.
|
| 72 |
|
| 73 |
## Model Inventory
|
| 74 |
|
| 75 |
| Model | Provider | Source URL | Role | Parameters | Runtime mode | Required for core safety | Award relevance |
|
| 76 |
|---|---|---|---|---:|---|---|---|
|
| 77 |
+
| MiniCPM5-1B | OpenBMB | https://huggingface.co/openbmb/MiniCPM5-1B | Optional constrained wording over deterministic answers | 1.0B | Disabled/mocked fallback in current local MVP | No | OpenBMB, Tiny Titan |
|
| 78 |
|
| 79 |
+
No model fine-tuning is required for the MVP. Model output, when enabled, may
|
| 80 |
+
only help with interpretation or wording and cannot create or override
|
| 81 |
+
regulation facts, advisory facts, source links, dates, freshness, status, or
|
| 82 |
+
next safe action.
|
| 83 |
|
| 84 |
All listed models are under the hackathon 32B cap. Tiny Titan remains eligible
|
| 85 |
only while every submitted model stays at or below 4B total parameters.
|
|
|
|
| 87 |
## Official Sources And Freshness
|
| 88 |
|
| 89 |
Cached official source metadata is bundled for offline lookup. CoastWise treats
|
| 90 |
+
cached official source snapshots older than 24 hours as stale, but stale cache
|
| 91 |
+
may still be used when offline with a visible verification warning.
|
|
|
|
| 92 |
|
| 93 |
+
Seed cache retrieval time for the current reviewed source set:
|
| 94 |
+
2026-06-14T12:00:00+00:00.
|
| 95 |
|
| 96 |
| Source | Use | Cached URL |
|
| 97 |
|---|---|---|
|
| 98 |
+
| CDFW Ocean Sport Fishing | Statewide ocean sport fishing source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations |
|
| 99 |
+
| CDFW Ocean Sport Fishing In-Season Regulation Changes | Statewide in-season changes source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Inseason |
|
| 100 |
+
| CDFW Northern Ocean Region | Northern California ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Northern |
|
| 101 |
+
| CDFW Mendocino Ocean Region | Mendocino ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Mendocino |
|
| 102 |
+
| CDFW San Francisco Ocean Region | San Francisco ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/San-Francisco |
|
| 103 |
+
| CDFW Central Ocean Region | Central California ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Central |
|
| 104 |
+
| CDFW Southern Ocean Region | Southern California ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Southern |
|
| 105 |
+
| CDFW San Francisco Bay Region | San Francisco Bay region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/SF-Bay |
|
| 106 |
+
| CDFW Groundfish Summary | Lingcod validation-set size and bag facts | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Groundfish-Summary |
|
| 107 |
+
| CDFW Crabs | Dungeness crab validation-set guidance and crab source snippets | https://wildlife.ca.gov/Conservation/Marine/Invertebrates/Crabs |
|
| 108 |
| CDPH Shellfish Advisories | Shellfish advisory overlay source | https://www.cdph.ca.gov/Programs/OPA/Pages/Shellfish-Advisories.aspx |
|
| 109 |
| CDPH Annual Mussel Quarantine | Annual mussel quarantine overlay source | https://www.cdph.ca.gov/Programs/CEH/DRSEM/Pages/EMB/Shellfish/Annual-Mussel-Quarantine.aspx |
|
| 110 |
|
| 111 |
+
Only public official source snapshots and refresh metadata are written to the
|
| 112 |
+
runtime cache. Questions and optional beach/coastal-area text are not saved by
|
| 113 |
+
CoastWise.
|
| 114 |
+
|
| 115 |
## Demo And Public Evidence
|
| 116 |
|
| 117 |
- Demo video: pending, add a public link before final submission.
|
|
|
|
| 121 |
- Public Space history: pending, push meaningful Codex-attributed commits to the
|
| 122 |
Hugging Face Space repository before claiming the OpenAI Codex award.
|
| 123 |
|
| 124 |
+
## Release Checklist
|
| 125 |
+
|
| 126 |
+
- README frontmatter includes Build Small hackathon tags.
|
| 127 |
+
- Public Hugging Face Space URL is available and listed above.
|
| 128 |
+
- Demo video and social post links are listed above.
|
| 129 |
+
- Public Space history contains judge-visible Codex commit evidence for the
|
| 130 |
+
OpenAI Codex prize.
|
| 131 |
+
- GitHub may remain private; do not rely on private GitHub commits as the only
|
| 132 |
+
Codex evidence.
|
| 133 |
+
- Source freshness policy, seed/offline behavior, safety limits, privacy
|
| 134 |
+
limits, and model inventory are documented in this README.
|
| 135 |
+
- Full pytest suite passes before final Space push.
|
| 136 |
+
|
| 137 |
## Local Run
|
| 138 |
|
| 139 |
```bash
|
app.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
from coastwise.ai import (
|
|
@@ -12,23 +14,41 @@ from coastwise.ai import (
|
|
| 12 |
)
|
| 13 |
from coastwise.data import get_rule_card
|
| 14 |
from coastwise.photo import candidate_from_photo, photo_result_to_dict
|
|
|
|
| 15 |
from coastwise.rules import compose_safety_result, default_context, result_to_dict
|
| 16 |
from coastwise.schemas import CoastalRegion, UserIntent
|
| 17 |
from coastwise.search import search_species
|
|
|
|
|
|
|
|
|
|
| 18 |
from coastwise.submission import default_model_inventory
|
| 19 |
|
| 20 |
|
| 21 |
APP_TITLE = "CoastWise"
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
| 27 |
"""
|
| 28 |
|
| 29 |
APP_CSS = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
.gradio-container {
|
|
|
|
| 31 |
max-width: 100vw !important;
|
|
|
|
| 32 |
overflow-x: hidden !important;
|
| 33 |
}
|
| 34 |
|
|
@@ -41,6 +61,7 @@ APP_CSS = """
|
|
| 41 |
.gradio-container .block,
|
| 42 |
.gradio-container .wrap,
|
| 43 |
.gradio-container fieldset {
|
|
|
|
| 44 |
max-width: 100% !important;
|
| 45 |
min-width: 0 !important;
|
| 46 |
}
|
|
@@ -51,6 +72,14 @@ APP_CSS = """
|
|
| 51 |
.gradio-container h2,
|
| 52 |
.gradio-container h3 {
|
| 53 |
overflow-wrap: anywhere;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
@media (max-width: 640px) {
|
|
@@ -58,6 +87,11 @@ APP_CSS = """
|
|
| 58 |
padding-left: 16px !important;
|
| 59 |
padding-right: 16px !important;
|
| 60 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
"""
|
| 63 |
|
|
@@ -71,6 +105,15 @@ Does not authorize harvest or food safety.
|
|
| 71 |
Verify CDFW/CDPH before harvest.
|
| 72 |
"""
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
INTENT_LABELS = {
|
| 75 |
"Just curious": UserIntent.JUST_CURIOUS,
|
| 76 |
"Catch and release": UserIntent.CATCH_AND_RELEASE,
|
|
@@ -84,6 +127,97 @@ REGION_LABELS = {
|
|
| 84 |
}
|
| 85 |
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
def lookup_species(
|
| 88 |
query: str,
|
| 89 |
intent_label: str,
|
|
@@ -195,30 +329,19 @@ def build_app() -> gr.Blocks:
|
|
| 195 |
gr.Markdown(f"# {APP_TITLE}")
|
| 196 |
gr.Markdown(INTRO)
|
| 197 |
gr.Markdown(LIMITS)
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
)
|
| 204 |
-
|
| 205 |
-
["Point Arena to Pigeon Point", "San Francisco Bay", "Unknown"],
|
| 206 |
-
value="Point Arena to Pigeon Point",
|
| 207 |
-
label="Region",
|
| 208 |
-
)
|
| 209 |
-
county_or_beach = gr.Textbox(label="Optional county or beach", placeholder="Optional")
|
| 210 |
-
lookup = gr.Button("Look up")
|
| 211 |
-
output = gr.Markdown("Rule-card results will appear here.")
|
| 212 |
lookup.click(
|
| 213 |
-
|
| 214 |
-
inputs=[query,
|
| 215 |
outputs=output,
|
| 216 |
)
|
| 217 |
-
|
| 218 |
-
photo = gr.Image(label="Optional photo", type="pil")
|
| 219 |
-
photo_button = gr.Button("Compare photo to curated references")
|
| 220 |
-
photo_output = gr.Markdown("Photo candidates will appear here.")
|
| 221 |
-
photo_button.click(render_photo_candidate, inputs=photo, outputs=photo_output)
|
| 222 |
return demo
|
| 223 |
|
| 224 |
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
|
| 7 |
import gradio as gr
|
| 8 |
|
| 9 |
from coastwise.ai import (
|
|
|
|
| 14 |
)
|
| 15 |
from coastwise.data import get_rule_card
|
| 16 |
from coastwise.photo import candidate_from_photo, photo_result_to_dict
|
| 17 |
+
from coastwise.qna import answer_question, answer_to_dict
|
| 18 |
from coastwise.rules import compose_safety_result, default_context, result_to_dict
|
| 19 |
from coastwise.schemas import CoastalRegion, UserIntent
|
| 20 |
from coastwise.search import search_species
|
| 21 |
+
from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, load_seed_cache
|
| 22 |
+
from coastwise.source_refresh import refresh_official_sources
|
| 23 |
+
from coastwise.source_registry import load_official_sources
|
| 24 |
from coastwise.submission import default_model_inventory
|
| 25 |
|
| 26 |
|
| 27 |
APP_TITLE = "CoastWise"
|
| 28 |
|
| 29 |
+
SOURCE_BACKED_CONTROL_LABELS = (
|
| 30 |
+
"Question",
|
| 31 |
+
"Beach or coastal area",
|
| 32 |
+
"Ask",
|
| 33 |
+
"Refresh official sources",
|
| 34 |
+
"Source status",
|
| 35 |
+
)
|
| 36 |
|
| 37 |
+
INTRO = """
|
| 38 |
+
California CDFW/CDPH Q&A.
|
| 39 |
"""
|
| 40 |
|
| 41 |
APP_CSS = """
|
| 42 |
+
html,
|
| 43 |
+
body {
|
| 44 |
+
max-width: 100% !important;
|
| 45 |
+
overflow-x: hidden !important;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
.gradio-container {
|
| 49 |
+
width: 100% !important;
|
| 50 |
max-width: 100vw !important;
|
| 51 |
+
min-width: 0 !important;
|
| 52 |
overflow-x: hidden !important;
|
| 53 |
}
|
| 54 |
|
|
|
|
| 61 |
.gradio-container .block,
|
| 62 |
.gradio-container .wrap,
|
| 63 |
.gradio-container fieldset {
|
| 64 |
+
width: 100% !important;
|
| 65 |
max-width: 100% !important;
|
| 66 |
min-width: 0 !important;
|
| 67 |
}
|
|
|
|
| 72 |
.gradio-container h2,
|
| 73 |
.gradio-container h3 {
|
| 74 |
overflow-wrap: anywhere;
|
| 75 |
+
word-break: break-word;
|
| 76 |
+
white-space: normal !important;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.gradio-container input,
|
| 80 |
+
.gradio-container textarea,
|
| 81 |
+
.gradio-container button {
|
| 82 |
+
max-width: 100% !important;
|
| 83 |
}
|
| 84 |
|
| 85 |
@media (max-width: 640px) {
|
|
|
|
| 87 |
padding-left: 16px !important;
|
| 88 |
padding-right: 16px !important;
|
| 89 |
}
|
| 90 |
+
|
| 91 |
+
.gradio-container h1 {
|
| 92 |
+
font-size: 28px !important;
|
| 93 |
+
line-height: 1.2 !important;
|
| 94 |
+
}
|
| 95 |
}
|
| 96 |
"""
|
| 97 |
|
|
|
|
| 105 |
Verify CDFW/CDPH before harvest.
|
| 106 |
"""
|
| 107 |
|
| 108 |
+
SOURCE_BACKED_PRODUCT_LIMITS = (
|
| 109 |
+
"CoastWise does not authorize harvest or act as legal/food-safety/species-ID "
|
| 110 |
+
"authority. Verify official CDFW/CDPH guidance before harvest or consumption."
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
SESSION_ONLY_NOTICE = (
|
| 114 |
+
"Session-only: questions and optional locations are not saved."
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
INTENT_LABELS = {
|
| 118 |
"Just curious": UserIntent.JUST_CURIOUS,
|
| 119 |
"Catch and release": UserIntent.CATCH_AND_RELEASE,
|
|
|
|
| 127 |
}
|
| 128 |
|
| 129 |
|
| 130 |
+
def _active_cache():
|
| 131 |
+
return load_seed_cache(DEFAULT_SEED_CACHE_DIR)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def ask_source_backed_question(question: str, beach_or_coastal_area: str = "") -> dict[str, object]:
|
| 135 |
+
answer = answer_question(
|
| 136 |
+
question,
|
| 137 |
+
beach_or_coastal_area,
|
| 138 |
+
_active_cache(),
|
| 139 |
+
datetime.now(timezone.utc),
|
| 140 |
+
)
|
| 141 |
+
return answer_to_dict(answer)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _render_source_backed_answer_payload(result: dict[str, object]) -> str:
|
| 145 |
+
source_lines = "\n".join(
|
| 146 |
+
f"- [{title}]({url})"
|
| 147 |
+
for title, url in zip(result["source_titles"], result["source_links"], strict=False)
|
| 148 |
+
) or "- No source-backed evidence available."
|
| 149 |
+
snippet_lines = "\n".join(f"> {snippet}" for snippet in result["supporting_snippets"]) or "- No snippet shown."
|
| 150 |
+
uncertainty = "\n".join(f"- {note}" for note in result["uncertainty_notes"]) or "- None."
|
| 151 |
+
location_warning = result["location_warning"] or "Location recognized or not needed for this general answer."
|
| 152 |
+
retrieved = result["retrieved_at"] or "No cached retrieval time available"
|
| 153 |
+
return f"""
|
| 154 |
+
## Direct answer
|
| 155 |
+
{result["direct_answer"]}
|
| 156 |
+
|
| 157 |
+
**Status:** `{result["status"]}`
|
| 158 |
+
**Evidence type:** `{result["evidence_type"]}`
|
| 159 |
+
**Freshness:** `{result["freshness_status"]}`
|
| 160 |
+
**Retrieved:** {retrieved}
|
| 161 |
+
|
| 162 |
+
## Source evidence
|
| 163 |
+
{source_lines}
|
| 164 |
+
|
| 165 |
+
## Location context
|
| 166 |
+
{result["matched_place"] or "General California ocean"}
|
| 167 |
+
{location_warning}
|
| 168 |
+
|
| 169 |
+
## Supporting snippet
|
| 170 |
+
{snippet_lines}
|
| 171 |
+
|
| 172 |
+
## Uncertainty
|
| 173 |
+
{uncertainty}
|
| 174 |
+
|
| 175 |
+
## Next safe action
|
| 176 |
+
{result["next_safe_action"]}
|
| 177 |
+
|
| 178 |
+
## Product limits
|
| 179 |
+
{SOURCE_BACKED_PRODUCT_LIMITS}
|
| 180 |
+
|
| 181 |
+
{SESSION_ONLY_NOTICE}
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def render_source_backed_answer_from_cache(
|
| 186 |
+
question: str,
|
| 187 |
+
beach_or_coastal_area: str,
|
| 188 |
+
cache,
|
| 189 |
+
now: datetime,
|
| 190 |
+
) -> str:
|
| 191 |
+
answer = answer_question(question, beach_or_coastal_area, cache, now)
|
| 192 |
+
return _render_source_backed_answer_payload(answer_to_dict(answer))
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def render_source_backed_answer(question: str, beach_or_coastal_area: str = "") -> str:
|
| 196 |
+
result = ask_source_backed_question(question, beach_or_coastal_area)
|
| 197 |
+
return _render_source_backed_answer_payload(result)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def refresh_official_sources_status(fetcher=None) -> dict[str, object]:
|
| 201 |
+
result, _cache = refresh_official_sources(
|
| 202 |
+
load_official_sources(),
|
| 203 |
+
_active_cache(),
|
| 204 |
+
datetime.now(timezone.utc),
|
| 205 |
+
fetcher,
|
| 206 |
+
)
|
| 207 |
+
return {
|
| 208 |
+
"message": result.user_message,
|
| 209 |
+
"updated": result.updated_source_ids,
|
| 210 |
+
"unchanged": result.unchanged_source_ids,
|
| 211 |
+
"failed": result.failed_source_ids,
|
| 212 |
+
"cache_preserved": result.cache_preserved_source_ids,
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def render_refresh_status() -> str:
|
| 217 |
+
status = refresh_official_sources_status()
|
| 218 |
+
return f"**Refresh official sources:** {status['message']}"
|
| 219 |
+
|
| 220 |
+
|
| 221 |
def lookup_species(
|
| 222 |
query: str,
|
| 223 |
intent_label: str,
|
|
|
|
| 329 |
gr.Markdown(f"# {APP_TITLE}")
|
| 330 |
gr.Markdown(INTRO)
|
| 331 |
gr.Markdown(LIMITS)
|
| 332 |
+
gr.Markdown(SESSION_ONLY_NOTICE)
|
| 333 |
+
gr.Markdown("**Source status:** seed cache loaded; refresh available.")
|
| 334 |
+
query = gr.Textbox(label="Question", placeholder="what's the min size of lingcod?")
|
| 335 |
+
county_or_beach = gr.Textbox(label="Beach or coastal area", placeholder="Optional, e.g. Pacifica or Monterey")
|
| 336 |
+
lookup = gr.Button("Ask")
|
| 337 |
+
refresh = gr.Button("Refresh official sources")
|
| 338 |
+
output = gr.Markdown("Source-backed answers will appear here.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
lookup.click(
|
| 340 |
+
render_source_backed_answer,
|
| 341 |
+
inputs=[query, county_or_beach],
|
| 342 |
outputs=output,
|
| 343 |
)
|
| 344 |
+
refresh.click(render_refresh_status, outputs=output)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
return demo
|
| 346 |
|
| 347 |
|
coastwise/advisories.py
CHANGED
|
@@ -5,11 +5,25 @@ from __future__ import annotations
|
|
| 5 |
from datetime import date
|
| 6 |
|
| 7 |
from coastwise.data import CDPH_QUARANTINE_URL, CDPH_SHELLFISH_URL, SOURCE_CACHE_DATE
|
| 8 |
-
from coastwise.schemas import Advisory, CoastalRegion, HarvestStatus, QueryContext, RuleCard
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
SHELLFISH_CARD_KEYS = {"mussels", "clams"}
|
| 12 |
SHELLFISH_CATEGORIES = {"shellfish"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
ANNUAL_MUSSEL_QUARANTINE_WARNING = (
|
| 15 |
"Annual CDPH mussel quarantine runs May 1 through October 31; do not harvest "
|
|
@@ -87,3 +101,38 @@ def matching_advisories(rule_card: RuleCard, context: QueryContext) -> list[Advi
|
|
| 87 |
|
| 88 |
return matches
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from datetime import date
|
| 6 |
|
| 7 |
from coastwise.data import CDPH_QUARANTINE_URL, CDPH_SHELLFISH_URL, SOURCE_CACHE_DATE
|
| 8 |
+
from coastwise.schemas import Advisory, AdvisoryFact, CoastalRegion, HarvestStatus, QueryContext, RuleCard
|
| 9 |
+
from coastwise.source_cache import SourceCache
|
| 10 |
|
| 11 |
|
| 12 |
SHELLFISH_CARD_KEYS = {"mussels", "clams"}
|
| 13 |
SHELLFISH_CATEGORIES = {"shellfish"}
|
| 14 |
+
SOURCE_BACKED_SHELLFISH_TERMS = {
|
| 15 |
+
"bivalve",
|
| 16 |
+
"bivalves",
|
| 17 |
+
"clam",
|
| 18 |
+
"clams",
|
| 19 |
+
"crab",
|
| 20 |
+
"crabs",
|
| 21 |
+
"mussel",
|
| 22 |
+
"mussels",
|
| 23 |
+
"oyster",
|
| 24 |
+
"oysters",
|
| 25 |
+
"shellfish",
|
| 26 |
+
}
|
| 27 |
|
| 28 |
ANNUAL_MUSSEL_QUARANTINE_WARNING = (
|
| 29 |
"Annual CDPH mussel quarantine runs May 1 through October 31; do not harvest "
|
|
|
|
| 101 |
|
| 102 |
return matches
|
| 103 |
|
| 104 |
+
|
| 105 |
+
def _normalize_source_backed_term(value: str | None) -> str:
|
| 106 |
+
return " ".join((value or "").replace("_", " ").casefold().split())
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _advisory_fact_terms(fact: AdvisoryFact) -> set[str]:
|
| 110 |
+
affected = _normalize_source_backed_term(fact.affected_species_or_category)
|
| 111 |
+
terms = {affected}
|
| 112 |
+
if affected == "mussels":
|
| 113 |
+
terms.update({"mussel", "shellfish"})
|
| 114 |
+
if affected == "shellfish":
|
| 115 |
+
terms.update(SOURCE_BACKED_SHELLFISH_TERMS)
|
| 116 |
+
return {term for term in terms if term}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def source_backed_advisory_facts(cache: SourceCache, species_or_category: str | None) -> list[AdvisoryFact]:
|
| 120 |
+
"""Return cached CDPH advisory facts matching a shellfish species/category."""
|
| 121 |
+
|
| 122 |
+
normalized = _normalize_source_backed_term(species_or_category)
|
| 123 |
+
if not normalized:
|
| 124 |
+
return []
|
| 125 |
+
|
| 126 |
+
requested_terms = {normalized}
|
| 127 |
+
if normalized in SOURCE_BACKED_SHELLFISH_TERMS:
|
| 128 |
+
requested_terms.add("shellfish")
|
| 129 |
+
if normalized in {"mussel", "mussels"}:
|
| 130 |
+
requested_terms.update({"mussel", "mussels"})
|
| 131 |
+
|
| 132 |
+
matches = [
|
| 133 |
+
fact
|
| 134 |
+
for fact in cache.advisory_facts
|
| 135 |
+
if requested_terms & _advisory_fact_terms(fact)
|
| 136 |
+
or any(term in fact.supporting_snippet.casefold() for term in requested_terms)
|
| 137 |
+
]
|
| 138 |
+
return sorted(matches, key=lambda fact: (fact.effective_start is None, fact.id))
|
coastwise/submission.py
CHANGED
|
@@ -22,7 +22,7 @@ def default_model_inventory() -> ModelInventory:
|
|
| 22 |
key="minicpm5_1b",
|
| 23 |
name="MiniCPM5-1B",
|
| 24 |
provider="OpenBMB",
|
| 25 |
-
source_url="https://huggingface.co/openbmb",
|
| 26 |
total_parameters_b=1.0,
|
| 27 |
roles=(ModelRole.SAFETY_EXPLANATION,),
|
| 28 |
runtime_mode=RuntimeMode.DISABLED_OR_MOCKED,
|
|
@@ -70,6 +70,14 @@ def validate_submission_package(package: SubmissionPackage) -> list[ValidationIs
|
|
| 70 |
if not value:
|
| 71 |
issues.append(ValidationIssue(field, "error", f"{field} is required."))
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
if not package.readme_has_frontmatter_tags:
|
| 74 |
issues.append(ValidationIssue("readme_has_frontmatter_tags", "error", "README frontmatter tags are required."))
|
| 75 |
if not package.safety_limitations_documented:
|
|
@@ -84,6 +92,13 @@ def validate_submission_package(package: SubmissionPackage) -> list[ValidationIs
|
|
| 84 |
"OpenAI Codex claim requires public Hugging Face Space commit evidence.",
|
| 85 |
)
|
| 86 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
issues.extend(validate_model_inventory(package.model_inventory))
|
| 88 |
return issues
|
| 89 |
-
|
|
|
|
| 22 |
key="minicpm5_1b",
|
| 23 |
name="MiniCPM5-1B",
|
| 24 |
provider="OpenBMB",
|
| 25 |
+
source_url="https://huggingface.co/openbmb/MiniCPM5-1B",
|
| 26 |
total_parameters_b=1.0,
|
| 27 |
roles=(ModelRole.SAFETY_EXPLANATION,),
|
| 28 |
runtime_mode=RuntimeMode.DISABLED_OR_MOCKED,
|
|
|
|
| 70 |
if not value:
|
| 71 |
issues.append(ValidationIssue(field, "error", f"{field} is required."))
|
| 72 |
|
| 73 |
+
if package.space_url and "huggingface.co/spaces/" not in package.space_url:
|
| 74 |
+
issues.append(
|
| 75 |
+
ValidationIssue(
|
| 76 |
+
"space_url",
|
| 77 |
+
"error",
|
| 78 |
+
"Public Space URL must point to the Hugging Face Spaces submission.",
|
| 79 |
+
)
|
| 80 |
+
)
|
| 81 |
if not package.readme_has_frontmatter_tags:
|
| 82 |
issues.append(ValidationIssue("readme_has_frontmatter_tags", "error", "README frontmatter tags are required."))
|
| 83 |
if not package.safety_limitations_documented:
|
|
|
|
| 92 |
"OpenAI Codex claim requires public Hugging Face Space commit evidence.",
|
| 93 |
)
|
| 94 |
)
|
| 95 |
+
if package.codex_evidence and "huggingface.co/spaces/" not in package.codex_evidence:
|
| 96 |
+
issues.append(
|
| 97 |
+
ValidationIssue(
|
| 98 |
+
"codex_evidence",
|
| 99 |
+
"error",
|
| 100 |
+
"Codex evidence should be judge-visible in public Hugging Face Space history.",
|
| 101 |
+
)
|
| 102 |
+
)
|
| 103 |
issues.extend(validate_model_inventory(package.model_inventory))
|
| 104 |
return issues
|
|
|
tests/test_advisories.py
CHANGED
|
@@ -3,9 +3,11 @@
|
|
| 3 |
from datetime import date
|
| 4 |
|
| 5 |
from coastwise.advisories import annual_mussel_quarantine_applies, matching_advisories
|
|
|
|
| 6 |
from coastwise.data import get_rule_card
|
| 7 |
from coastwise.rules import default_context
|
| 8 |
from coastwise.schemas import CoastalRegion, HarvestStatus, UserIntent
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def test_annual_mussel_quarantine_applies_may_through_october():
|
|
@@ -32,3 +34,12 @@ def test_matching_cdph_advisories_include_region_level_shellfish_warning():
|
|
| 32 |
and "CDPH" in advisory.source_name
|
| 33 |
for advisory in advisories
|
| 34 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from datetime import date
|
| 4 |
|
| 5 |
from coastwise.advisories import annual_mussel_quarantine_applies, matching_advisories
|
| 6 |
+
from coastwise.advisories import source_backed_advisory_facts
|
| 7 |
from coastwise.data import get_rule_card
|
| 8 |
from coastwise.rules import default_context
|
| 9 |
from coastwise.schemas import CoastalRegion, HarvestStatus, UserIntent
|
| 10 |
+
from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, load_seed_cache
|
| 11 |
|
| 12 |
|
| 13 |
def test_annual_mussel_quarantine_applies_may_through_october():
|
|
|
|
| 34 |
and "CDPH" in advisory.source_name
|
| 35 |
for advisory in advisories
|
| 36 |
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_source_backed_cdph_advisory_facts_include_quarantine_and_cooking_warning():
|
| 40 |
+
facts = source_backed_advisory_facts(load_seed_cache(DEFAULT_SEED_CACHE_DIR), "mussels")
|
| 41 |
+
|
| 42 |
+
assert facts
|
| 43 |
+
assert any("May 1" in fact.advisory_status for fact in facts)
|
| 44 |
+
assert any("Cooking does not reliably destroy" in fact.warning for fact in facts)
|
| 45 |
+
assert all("cdph.ca.gov" in fact.source_url for fact in facts)
|
tests/test_app.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
| 1 |
"""Gradio app integration tests."""
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
from PIL import Image
|
| 5 |
|
| 6 |
import app
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def test_app_import_exposes_coastwise_identity_without_launching():
|
|
@@ -18,6 +23,118 @@ def test_build_app_returns_blocks_with_lookup_shell():
|
|
| 18 |
assert demo.title == "CoastWise"
|
| 19 |
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
def test_lookup_species_returns_source_backed_result_dict():
|
| 22 |
result = app.lookup_species("lingcod", "Just curious", "Point Arena to Pigeon Point", "")
|
| 23 |
|
|
|
|
| 1 |
"""Gradio app integration tests."""
|
| 2 |
|
| 3 |
+
import time
|
| 4 |
+
from dataclasses import replace
|
| 5 |
+
from datetime import datetime, timedelta, timezone
|
| 6 |
+
|
| 7 |
import gradio as gr
|
| 8 |
from PIL import Image
|
| 9 |
|
| 10 |
import app
|
| 11 |
+
from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, SourceCache, load_seed_cache
|
| 12 |
|
| 13 |
|
| 14 |
def test_app_import_exposes_coastwise_identity_without_launching():
|
|
|
|
| 23 |
assert demo.title == "CoastWise"
|
| 24 |
|
| 25 |
|
| 26 |
+
def test_source_backed_app_shell_exposes_primary_controls_without_legacy_inputs():
|
| 27 |
+
assert "Question" in app.SOURCE_BACKED_CONTROL_LABELS
|
| 28 |
+
assert "Beach or coastal area" in app.SOURCE_BACKED_CONTROL_LABELS
|
| 29 |
+
assert "Refresh official sources" in app.SOURCE_BACKED_CONTROL_LABELS
|
| 30 |
+
assert "Intent" not in app.SOURCE_BACKED_CONTROL_LABELS
|
| 31 |
+
assert "Region" not in app.SOURCE_BACKED_CONTROL_LABELS
|
| 32 |
+
assert "Photo candidate" not in app.SOURCE_BACKED_CONTROL_LABELS
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_source_backed_question_journey_renders_evidence_and_next_action():
|
| 36 |
+
result = app.ask_source_backed_question("what's the min size of lingcod?", "")
|
| 37 |
+
markdown = app.render_source_backed_answer("what's the min size of lingcod?", "")
|
| 38 |
+
|
| 39 |
+
assert result["direct_answer"]
|
| 40 |
+
assert result["source_links"]
|
| 41 |
+
assert result["freshness_status"] == "current"
|
| 42 |
+
assert "22" in markdown
|
| 43 |
+
assert "Source evidence" in markdown
|
| 44 |
+
assert "Retrieved" in markdown
|
| 45 |
+
assert "Next safe action" in markdown
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_manual_refresh_status_uses_nontechnical_source_group_language():
|
| 49 |
+
status = app.refresh_official_sources_status(fetcher=lambda _source: "unchanged text")
|
| 50 |
+
|
| 51 |
+
assert "CDFW" in status["message"]
|
| 52 |
+
assert "CDPH" in status["message"]
|
| 53 |
+
assert "checked" in status["message"].lower()
|
| 54 |
+
assert "traceback" not in status["message"].lower()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_app_renders_stale_and_no_cache_source_backed_states():
|
| 58 |
+
now = datetime(2026, 6, 14, 13, tzinfo=timezone.utc)
|
| 59 |
+
seed = load_seed_cache(DEFAULT_SEED_CACHE_DIR)
|
| 60 |
+
stale = SourceCache(
|
| 61 |
+
sources=seed.sources,
|
| 62 |
+
snapshots=tuple(replace(snapshot, retrieved_at=now - timedelta(hours=25)) for snapshot in seed.snapshots),
|
| 63 |
+
chunks=seed.chunks,
|
| 64 |
+
facts=seed.facts,
|
| 65 |
+
advisory_facts=seed.advisory_facts,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
stale_markdown = app.render_source_backed_answer_from_cache(
|
| 69 |
+
"what's the min size of lingcod?",
|
| 70 |
+
"",
|
| 71 |
+
stale,
|
| 72 |
+
now,
|
| 73 |
+
)
|
| 74 |
+
no_cache_markdown = app.render_source_backed_answer_from_cache(
|
| 75 |
+
"what's the min size of lingcod?",
|
| 76 |
+
"",
|
| 77 |
+
SourceCache((), (), (), ()),
|
| 78 |
+
now,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
assert "stale" in stale_markdown.lower()
|
| 82 |
+
assert "refresh official" in no_cache_markdown.lower()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_app_renders_cdph_warning_and_verification_for_mussels():
|
| 86 |
+
markdown = app.render_source_backed_answer("can I eat mussels from Pacifica today?", "")
|
| 87 |
+
lowered = markdown.lower()
|
| 88 |
+
|
| 89 |
+
assert "cdph" in lowered
|
| 90 |
+
assert "cooking does not reliably" in lowered
|
| 91 |
+
assert "verify" in lowered
|
| 92 |
+
assert "safe to eat" not in lowered
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def test_app_renders_source_evidence_limits_and_partial_unsupported_states():
|
| 96 |
+
now = datetime(2026, 6, 14, 13, tzinfo=timezone.utc)
|
| 97 |
+
partial = app.render_source_backed_answer("what should I check for ocean rules near Monterey?", "Monterey")
|
| 98 |
+
unsupported = app.render_source_backed_answer("what is the trout limit in Lake Tahoe?", "")
|
| 99 |
+
no_cache = app.render_source_backed_answer_from_cache(
|
| 100 |
+
"what's the min size of lingcod?",
|
| 101 |
+
"",
|
| 102 |
+
SourceCache((), (), (), ()),
|
| 103 |
+
now,
|
| 104 |
+
)
|
| 105 |
+
combined = "\n".join((partial, unsupported, no_cache)).casefold()
|
| 106 |
+
|
| 107 |
+
assert "source evidence" in partial.casefold()
|
| 108 |
+
assert "uncertainty" in partial.casefold()
|
| 109 |
+
assert "partial" in partial.casefold()
|
| 110 |
+
assert "outside this mvp" in unsupported.casefold()
|
| 111 |
+
assert "refresh official" in no_cache.casefold()
|
| 112 |
+
assert "does not authorize harvest" in combined
|
| 113 |
+
assert "session-only" in combined
|
| 114 |
+
assert "source-backed evidence" in combined
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_cached_answer_app_build_and_refresh_feedback_fit_latency_budgets():
|
| 118 |
+
start = time.perf_counter()
|
| 119 |
+
markdown = app.render_source_backed_answer("what's the min size of lingcod?", "")
|
| 120 |
+
answer_seconds = time.perf_counter() - start
|
| 121 |
+
|
| 122 |
+
start = time.perf_counter()
|
| 123 |
+
demo = app.build_app()
|
| 124 |
+
build_seconds = time.perf_counter() - start
|
| 125 |
+
|
| 126 |
+
start = time.perf_counter()
|
| 127 |
+
status = app.refresh_official_sources_status(fetcher=lambda _source: "unchanged official source text")
|
| 128 |
+
refresh_seconds = time.perf_counter() - start
|
| 129 |
+
|
| 130 |
+
assert "22" in markdown
|
| 131 |
+
assert isinstance(demo, gr.Blocks)
|
| 132 |
+
assert "checked" in status["message"].casefold()
|
| 133 |
+
assert answer_seconds < 3
|
| 134 |
+
assert build_seconds < 3
|
| 135 |
+
assert refresh_seconds < 3
|
| 136 |
+
|
| 137 |
+
|
| 138 |
def test_lookup_species_returns_source_backed_result_dict():
|
| 139 |
result = app.lookup_species("lingcod", "Just curious", "Point Arena to Pigeon Point", "")
|
| 140 |
|
tests/test_privacy.py
CHANGED
|
@@ -38,3 +38,22 @@ def test_rendered_ui_copy_does_not_offer_save_export_or_history_actions():
|
|
| 38 |
assert "save" not in markdown
|
| 39 |
assert "export" not in markdown
|
| 40 |
assert "history" not in markdown
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
assert "save" not in markdown
|
| 39 |
assert "export" not in markdown
|
| 40 |
assert "history" not in markdown
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_source_backed_question_location_and_wording_are_session_only():
|
| 44 |
+
result = app.ask_source_backed_question("what's the min size of lingcod?", "Pacifica")
|
| 45 |
+
markdown = app.render_source_backed_answer("what's the min size of lingcod?", "Pacifica").casefold()
|
| 46 |
+
|
| 47 |
+
assert result["matched_place"] == "Pacifica"
|
| 48 |
+
assert "session-only" in markdown
|
| 49 |
+
assert "questions and optional locations are not saved" in markdown
|
| 50 |
+
assert not hasattr(app, "SOURCE_BACKED_QUESTION_HISTORY")
|
| 51 |
+
assert not hasattr(app, "SOURCE_BACKED_LOCATION_HISTORY")
|
| 52 |
+
assert not hasattr(app, "GENERATED_ANSWER_HISTORY")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_source_backed_controls_do_not_offer_account_save_export_history_or_photo_core():
|
| 56 |
+
labels = " ".join(app.SOURCE_BACKED_CONTROL_LABELS).casefold()
|
| 57 |
+
|
| 58 |
+
for forbidden in ("account", "save", "export", "history", "photo", "camera"):
|
| 59 |
+
assert forbidden not in labels
|
tests/test_safety_copy.py
CHANGED
|
@@ -6,8 +6,11 @@ import app
|
|
| 6 |
FORBIDDEN_PHRASES = (
|
| 7 |
"safe to eat",
|
| 8 |
"definitely legal",
|
|
|
|
| 9 |
"authorized to harvest",
|
|
|
|
| 10 |
"confirmed identification",
|
|
|
|
| 11 |
)
|
| 12 |
|
| 13 |
|
|
@@ -32,3 +35,16 @@ def test_photo_candidate_copy_never_claims_confirmed_identification():
|
|
| 32 |
assert "harvest guidance" not in lowered
|
| 33 |
for phrase in FORBIDDEN_PHRASES:
|
| 34 |
assert phrase not in lowered
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
FORBIDDEN_PHRASES = (
|
| 7 |
"safe to eat",
|
| 8 |
"definitely legal",
|
| 9 |
+
"legal to harvest",
|
| 10 |
"authorized to harvest",
|
| 11 |
+
"certifies",
|
| 12 |
"confirmed identification",
|
| 13 |
+
"you may harvest",
|
| 14 |
)
|
| 15 |
|
| 16 |
|
|
|
|
| 35 |
assert "harvest guidance" not in lowered
|
| 36 |
for phrase in FORBIDDEN_PHRASES:
|
| 37 |
assert phrase not in lowered
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_source_backed_answers_show_limits_without_forbidden_claims():
|
| 41 |
+
markdown = app.render_source_backed_answer("can I eat mussels from Pacifica today?", "")
|
| 42 |
+
lowered = markdown.casefold()
|
| 43 |
+
|
| 44 |
+
assert "does not authorize harvest" in lowered
|
| 45 |
+
assert "food-safety" in lowered
|
| 46 |
+
assert "session-only" in lowered
|
| 47 |
+
assert "source evidence" in lowered
|
| 48 |
+
assert "retrieved" in lowered
|
| 49 |
+
for phrase in FORBIDDEN_PHRASES:
|
| 50 |
+
assert phrase not in lowered
|
tests/test_submission.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
"""Submission-readiness tests."""
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from coastwise.schemas import AwardTag, SubmissionPackage
|
|
|
|
|
|
|
| 4 |
from coastwise.submission import (
|
| 5 |
default_model_inventory,
|
| 6 |
validate_model_inventory,
|
|
@@ -37,3 +41,29 @@ def test_submission_package_validator_flags_missing_public_evidence():
|
|
| 37 |
fields = {issue.field for issue in issues}
|
| 38 |
|
| 39 |
assert {"space_url", "demo_video_url", "social_post_url", "codex_evidence"} <= fields
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Submission-readiness tests."""
|
| 2 |
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
from coastwise.schemas import AwardTag, SubmissionPackage
|
| 6 |
+
from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, DEFAULT_SOURCE_CACHE_ROOT, load_seed_cache
|
| 7 |
+
from coastwise.source_registry import load_official_sources, statewide_source_coverage
|
| 8 |
from coastwise.submission import (
|
| 9 |
default_model_inventory,
|
| 10 |
validate_model_inventory,
|
|
|
|
| 41 |
fields = {issue.field for issue in issues}
|
| 42 |
|
| 43 |
assert {"space_url", "demo_video_url", "social_post_url", "codex_evidence"} <= fields
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_readme_documents_statewide_source_cache_policy_and_validation_limits():
|
| 47 |
+
readme = open("README.md", encoding="utf-8").read().casefold()
|
| 48 |
+
coverage = statewide_source_coverage()
|
| 49 |
+
|
| 50 |
+
assert "24 hours" in readme
|
| 51 |
+
assert "statewide official source registry" in readme
|
| 52 |
+
assert "structured answers for the reviewed validation set" in readme
|
| 53 |
+
assert "snippet fallback" in readme
|
| 54 |
+
assert "questions and optional beach/coastal-area text are not saved" in readme
|
| 55 |
+
for source in load_official_sources():
|
| 56 |
+
if source.id in coverage.required_source_ids:
|
| 57 |
+
assert source.url.casefold() in readme
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_seed_manifest_documents_statewide_coverage_and_24_hour_stale_policy():
|
| 61 |
+
manifest = json.loads((DEFAULT_SOURCE_CACHE_ROOT / "seed-manifest.json").read_text(encoding="utf-8"))
|
| 62 |
+
cache = load_seed_cache(DEFAULT_SEED_CACHE_DIR)
|
| 63 |
+
source_ids = set(cache.snapshots_by_source_id)
|
| 64 |
+
|
| 65 |
+
assert manifest["coverage_id"] == statewide_source_coverage().id
|
| 66 |
+
assert manifest["stale_after_hours"] == 24
|
| 67 |
+
assert set(statewide_source_coverage().required_source_ids) <= source_ids
|
| 68 |
+
assert {"lingcod_min_size", "dungeness_crab_bag_limit"} <= set(cache.facts_by_id)
|
| 69 |
+
assert {"cdph_annual_mussel_quarantine", "cdph_shellfish_verify"} <= set(cache.advisory_facts_by_id)
|