Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ See the [product brief](docs/product-brief.md) for policy context, MVP workflow

StayLong is a consent-governed, event-driven coordination layer for older Australians living alone. It turns a home-living concern into an accountable, assessment-ready plan, coordinates approved next steps, and follows up until every approved action is complete. The older person can work independently or invite an authorised supporter for a specific task.

The public-sandbox runtime can pass concern text through a Vertex AI-hosted Gemma redaction guard before persistence or tool actions. Gemini 3.5+ remains the primary ADK coordinator; Gemma returns only a strict privacy contract and cannot change safety, consent or approval transitions.
The public-sandbox runtime passes concern text through Vertex Model Garden MaaS `gemma-4-26b-a4b-it-maas` before persistence or tool actions. Gemini 3.6 Flash remains the primary ADK coordinator; Gemma returns only a strict privacy contract and cannot change safety, consent or approval transitions. If the privacy guard is unavailable or returns invalid output, the workflow fails closed without persisting the concern or starting a plan.

## Public demonstration URL

Expand Down
25 changes: 13 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ flowchart LR
R --> ADK["Google ADK intake / coordinator"]
API --> O["Google Calendar OAuth routes"]
end
ADK --> V["Vertex AI Gemini 3.5+"]
API --> G["Vertex AI Gemma privacy guard"]
ADK --> V["Vertex AI Gemini 3.6 Flash"]
API --> G["Vertex Model Garden MaaS\nGemma 4 privacy guard"]
ADK --> F[("Firestore case state")]
ADK --> Q["Cloud Tasks / Pub/Sub"]
Q --> ADK
Expand All @@ -52,8 +52,8 @@ The demo seed is [`fixtures/demo/seeded-household.json`](../fixtures/demo/seeded
|---|---|
| Cloud Run web/API service | Authenticated web UI, API, webhook receiver and ADK entry point. |
| Google ADK | Plans and executes a bounded workflow through typed tools. |
| Vertex AI Gemini | Extracts structured concerns, drafts plain-language summaries and proposes next permitted actions. |
| Vertex AI Gemma privacy guard | Detects and redacts unnecessary PII before concern text is persisted or reaches an action boundary; it cannot make workflow or safety decisions. |
| Vertex AI Gemini 3.6 Flash | Extracts structured concerns, drafts plain-language summaries and proposes next permitted actions. |
| Vertex Model Garden MaaS Gemma 4 privacy guard | Uses request-based `gemma-4-26b-a4b-it-maas` to redact unnecessary PII before concern text is persisted or reaches an action boundary; it cannot make workflow or safety decisions. |
| Firestore | Stores household consent, concern records, task state, approvals, action history and idempotency keys. |
| Cloud Tasks | Schedules due-date checks, reminder retries and escalation work. |
| Pub/Sub | Carries event notifications such as `concern.created`, `approval.granted`, `task.overdue` and `assessment.outcome.recorded`. |
Expand All @@ -63,13 +63,14 @@ The demo seed is [`fixtures/demo/seeded-household.json`](../fixtures/demo/seeded
## Core event flow

1. An older person living alone creates a concern independently, or an authorised supporter creates one with the person's permission.
2. The API performs deterministic red-flag screening before invoking Gemini.
3. The intake agent produces a typed concern summary and lists missing facts.
4. The coordinator creates only allowed draft tasks, appointments or messages.
5. A human approves each external side effect.
6. The action tool executes once, records an idempotency key, and emits an event.
7. A scheduled worker detects overdue work and escalates according to household rules.
8. An assessment outcome moves the case to the next workflow stage; it never creates a clinical prescription or funding decision.
2. The API performs deterministic red-flag screening before invoking any model.
3. For a non-emergency concern, Gemma redacts unnecessary PII. An unavailable or invalid privacy response fails closed before persistence or planning.
4. The intake agent produces a typed concern summary and lists missing facts.
5. The coordinator creates only allowed draft tasks, appointments or messages.
6. A human approves each external side effect.
7. The action tool executes once, records an idempotency key, and emits an event.
8. A scheduled worker detects overdue work and escalates according to household rules.
9. An assessment outcome moves the case to the next workflow stage; it never creates a clinical prescription or funding decision.

## Workflow integrity

Expand All @@ -81,7 +82,7 @@ The demo seed is [`fixtures/demo/seeded-household.json`](../fixtures/demo/seeded

The design rationale and MVP priorities are recorded in [training-informed improvements](training-informed-improvements.md).

Gemma is enabled with `STAYLONG_GEMMA_ENABLED=true` in the sandbox runtime. Its response is schema-validated (`redacted_text` plus `detected_categories`); malformed or empty output is rejected before the workflow continues. The privacy layer is separate from deterministic emergency routing and the Gemini/ADK planning agent.
Gemma is enabled with `STAYLONG_GEMMA_ENABLED=true` in the sandbox runtime and uses the request-based Vertex Model Garden MaaS model `gemma-4-26b-a4b-it-maas` at `global`; no dedicated GPU endpoint is provisioned. Its response is schema-validated (`redacted_text` plus `detected_categories`); malformed, empty or unavailable output returns a safe retry response before the workflow persists a concern or starts the Gemini/ADK planning path. The privacy layer is separate from deterministic emergency routing and the Gemini/ADK planning agent.

## Security and privacy boundaries

Expand Down
3 changes: 1 addition & 2 deletions docs/devpost-submission-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This checklist is based on the [official Devpost overview](https://allthingsagen

### Additional Google AI model

- [x] Integrate Vertex AI Gemma as a real PII redaction guard before persistence or tool actions.
- [ ] Deploy Vertex Model Garden MaaS `gemma-4-26b-a4b-it-maas` as the real PII redaction guard before persistence or tool actions.
- [x] Validate its strict response contract and reject malformed/empty output.
- [ ] Add model/version, invocation path, tests and cost boundary to the final evidence and demo.

Expand All @@ -50,4 +50,3 @@ This checklist is based on the [official Devpost overview](https://allthingsagen
- [ ] Architecture diagram, public demo video, technical article and social-post URLs.
- [ ] Gemma implementation and test evidence.
- [ ] Final Devpost preview checked before submission.

9 changes: 4 additions & 5 deletions docs/gemma-privacy.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# Gemma privacy guard

StayLong uses Vertex AI-hosted `gemma-3-27b-it` as a bounded privacy layer in the public sandbox. Before a concern is persisted or reaches an action boundary, the guard asks Gemma to return exactly:
StayLong uses Vertex Model Garden MaaS `gemma-4-26b-a4b-it-maas` as a bounded privacy layer in the public sandbox. It is a request-based, global model service, so StayLong does not deploy or operate a dedicated GPU endpoint. Before a concern is persisted or reaches an action boundary, the guard asks Gemma to return exactly:

```json
{"redacted_text":"...", "detected_categories":["phone"]}
```

The response is schema-validated. Empty text, unexpected fields, malformed JSON or invalid categories are rejected. Gemma cannot route emergencies, decide eligibility, select providers, grant consent or approve actions; those decisions remain deterministic application policy and human approval.
The response is schema-validated. Empty text, unexpected fields, malformed JSON, invalid categories or an unavailable Gemma service are rejected. In every rejection case, StayLong fails closed: it does not persist the concern, start the Gemini/ADK plan, or execute an action. The public API instead returns a plain-language retry response without exposing model or user-text details. Gemma cannot route emergencies, decide eligibility, select providers, grant consent or approve actions; those decisions remain deterministic application policy and human approval.

Enable the integration with `STAYLONG_GEMMA_ENABLED=true`. The sandbox Terraform component also supplies the Vertex project, `global` location and `GOOGLE_GENAI_USE_VERTEXAI=true`. Local tests inject a fake provider and never call Vertex.

This integration is an optional All Things Agentic Hackathon bonus contribution. The required Gemini 3.5+/ADK coordinator remains the primary planning model.
Enable the integration with `STAYLONG_GEMMA_ENABLED=true`. The sandbox Terraform component also supplies the exact MaaS model ID, Vertex project, `global` location and `GOOGLE_GENAI_USE_VERTEXAI=true`. Before deployment, an authorised operator must enable the model's API from its Model Garden **API Service** card. Local tests inject a fake provider and never call Vertex.

This integration is an optional All Things Agentic Hackathon bonus contribution. The required Gemini 3.6 Flash/ADK coordinator remains the primary planning model. Evidence must show the Model Garden model ID, a synthetic-data public smoke, the strict response tests and the request-based cost boundary; no real personal information, tokens or prompts are retained in evidence.
13 changes: 13 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ describe('StayLong Continuous Home Path', () => {
)
})

it('turns a chosen example into an editable starting point', async () => {
const user = userEvent.setup()
render(<App />)

await user.click(screen.getByRole('button', { name: 'Night-time bathroom' }))

expect(screen.getByText('You chose: Night-time bathroom')).toBeVisible()
expect(screen.getByText('We’ve added a starting point below — change the words so they sound like you.')).toBeVisible()
expect(screen.getByRole('textbox', { name: 'Describe what is becoming difficult' })).toHaveValue(
'I’m finding it harder to reach the bathroom safely at night. The hallway is dark and there are no rails near the toilet.',
)
})

it('shows an assessment pack and user-controlled action before approval', async () => {
const user = userEvent.setup()
const fetchMock = stubWorkflowFetches()
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ function App() {
setMessage(result ? '' : 'You chose to keep this action for later. You can reconsider whenever you are ready.')
} catch (error) { setMessage(error instanceof Error ? error.message : 'Something went wrong.') } finally { setBusy(false); setBusyMessage('') }
}

return (
<div className={`app-shell theme-${theme}`}>
<a className="skip-link" href="#main-content">Skip to main content</a>
Expand Down Expand Up @@ -385,5 +384,3 @@ function resultNextStep(result: ActionResult) {
}

export default App


6 changes: 3 additions & 3 deletions infra/terraform/components/public-sandbox/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ resource "google_cloud_run_v2_service" "sandbox" {
value = "true"
}

# The public sandbox project does not have access to the Gemma publisher
# model, so use an available Vertex model for the same privacy contract.
# Vertex Model Garden MaaS: request-based Gemma privacy redaction without
# a dedicated GPU endpoint. See SAI-72 for the deployment evidence.
env {
name = "STAYLONG_PRIVACY_MODEL"
value = "gemini-2.5-flash"
value = "gemma-4-26b-a4b-it-maas"
}

env {
Expand Down
6 changes: 6 additions & 0 deletions src/staylong/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,12 +610,18 @@ def _now() -> datetime:
def _raise_safe_intake_error(error: Exception) -> None:
"""Map policy refusals to plain language without returning internal details."""
from staylong.agents.intake import MedicalTriageRefusalRequired
from staylong.privacy.gemma import PrivacyGuardError

if isinstance(error, MedicalTriageRefusalRequired):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(error),
) from None
if isinstance(error, PrivacyGuardError):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="StayLong could not safely prepare your plan. Please try again.",
) from None
raise error


Expand Down
9 changes: 4 additions & 5 deletions src/staylong/privacy/gemma.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def __init__(
*,
project_id: str,
location: str = "global",
model_id: str = "gemma-3-27b-it",
model_id: str = "gemma-4-26b-a4b-it-maas",
) -> None:
if not project_id.strip():
raise ValueError("project_id must be configured for Gemma.")
Expand Down Expand Up @@ -106,14 +106,13 @@ def build_vertex_gemma_privacy_guard(
) -> GemmaPrivacyGuard:
"""Build the privacy guard with the configured Vertex-hosted model.

Gemma remains the default model. Environments that do not have access to
the Gemma publisher model can explicitly select another approved Vertex
model (for example ``gemini-2.5-flash``) via ``STAYLONG_PRIVACY_MODEL``.
Gemma MaaS remains the default model. Deployments may explicitly select a
different approved Gemma model through ``STAYLONG_PRIVACY_MODEL``.
"""
return GemmaPrivacyGuard(
provider=VertexGemmaJsonProvider(
project_id=project_id,
location=location,
model_id=os.getenv("STAYLONG_PRIVACY_MODEL", "gemma-3-27b-it"),
model_id=os.getenv("STAYLONG_PRIVACY_MODEL", "gemma-4-26b-a4b-it-maas"),
)
)
25 changes: 24 additions & 1 deletion tests/api/test_public_sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@
from tests.api.test_taskmaster_api import ANSWERS, StaticProvider


def _public_client() -> TestClient:
class FailingPrivacyGuard:
def redact(self, text: str):
from staylong.privacy.gemma import PrivacyGuardError

del text
raise PrivacyGuardError("privacy provider unavailable")


def _public_client(*, privacy_guard: object | None = None) -> TestClient:
from staylong.api.app import PublicSandboxConfig

workflow = TaskmasterWorkflow(
intake_agent=IntakeAgent(provider=StaticProvider()),
repository=InMemoryWorkflowRepository(),
event_repository=InMemoryEventRepository(),
calendar=CalendarDemoAdapter(),
privacy_guard=privacy_guard,
)
return TestClient(
create_app(
Expand Down Expand Up @@ -84,6 +93,20 @@ def test_private_workflow_route_still_requires_bearer_authentication() -> None:
assert response.status_code == 401


def test_public_workflow_fails_closed_when_privacy_guard_is_unavailable() -> None:
"""Do not persist or begin a plan if Gemma cannot protect the concern."""
client = _public_client(privacy_guard=FailingPrivacyGuard())
response = client.post(
"/v1/public/workflows",
json={"concern": "Please call 0412 345 678 about the dark hallway."},
)

assert response.status_code == 503
assert response.json() == {
"detail": "StayLong could not safely prepare your plan. Please try again."
}


def test_public_owner_can_approve_a_sandbox_action() -> None:
"""A session owner may explicitly approve its own sandbox-only next step."""
browser = _public_client()
Expand Down
4 changes: 2 additions & 2 deletions tests/infra/test_public_sandbox_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ def test_public_sandbox_enables_vertex_ai_for_gemma_privacy_guard() -> None:
assert 'service = "aiplatform.googleapis.com"' in source


def test_public_sandbox_selects_an_available_vertex_privacy_model() -> None:
def test_public_sandbox_selects_the_gemma_maas_privacy_model() -> None:
source = (COMPONENT / "main.tf").read_text(encoding="utf-8")

assert 'name = "STAYLONG_PRIVACY_MODEL"' in source
assert 'value = "gemini-2.5-flash"' in source
assert 'value = "gemma-4-26b-a4b-it-maas"' in source


def test_public_sandbox_outputs_only_url_and_non_sensitive_evidence() -> None:
Expand Down
6 changes: 3 additions & 3 deletions tests/privacy/test_gemma.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,20 @@ def test_gemma_rejects_empty_redacted_text() -> None:
guard.redact("A concern")


def test_vertex_privacy_guard_uses_explicit_environment_model(monkeypatch) -> None:
def test_vertex_privacy_guard_defaults_to_gemma_maas_model(monkeypatch) -> None:
captured: dict[str, str] = {}

class RecordingProvider:
def __init__(self, *, project_id: str, location: str, model_id: str) -> None:
captured.update(project_id=project_id, location=location, model_id=model_id)

monkeypatch.setenv("STAYLONG_PRIVACY_MODEL", "gemini-2.5-flash")
monkeypatch.delenv("STAYLONG_PRIVACY_MODEL", raising=False)
monkeypatch.setattr("staylong.privacy.gemma.VertexGemmaJsonProvider", RecordingProvider)

build_vertex_gemma_privacy_guard(project_id="stay-long", location="global")

assert captured == {
"project_id": "stay-long",
"location": "global",
"model_id": "gemini-2.5-flash",
"model_id": "gemma-4-26b-a4b-it-maas",
}
Loading