diff --git a/.env.example b/.env.example
index 380ff13..4a21bbc 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,4 @@
-APP_NAME=agentic-project-starter
+APP_NAME=zurich-tax-desk
APP_ENVIRONMENT=local
APP_HOST=0.0.0.0
APP_PORT=8000
diff --git a/README.md b/README.md
index 51b5e1f..1a96e64 100644
--- a/README.md
+++ b/README.md
@@ -1,133 +1,47 @@
-# agentic-project-starter
+# Zurich Tax Desk
-`agentic-project-starter` is a Python boilerplate for new projects that need:
+`Zurich Tax Desk` is a chat-first finance app for people living or working in
+Zurich who need help preparing tax questions, organizing filing details, and
+understanding common personal-tax scenarios before they act.
-- a FastAPI runtime
-- OpenAI Agents SDK scaffolding
-- ETL job structure
-- an optional ChatKit-based frontend accelerator
-- local and Docker execution
-- strong Codex contributor guidance through `AGENTS.md` and repo-local skills
+The codebase still uses the starter’s FastAPI runtime, ChatKit frontend, and
+package boundaries, but the default product behavior is now a Zurich-focused
+tax guidance assistant with explicit guardrails for high-stakes decisions.
-This repository intentionally ships placeholder workflows rather than business
-logic. The goal is to give new projects a clean, opinionated starting point.
+## Product focus
-## Use This Template
+- Zurich personal tax preparation and orientation
+- salary withholding and permit-related questions
+- deduction checklists for employees, families, and side-income earners
+- document preparation for moves, residency changes, and annual filing
+- escalation guidance when users should verify with official sources or a Swiss tax professional
-This repository is meant to be used as a GitHub template, not as the final
-application repo itself.
+## Guardrails
-To start a new project:
-
-1. Open this repository on GitHub.
-2. Click `Use this template`.
-3. Create a new repository with your project name and visibility.
-4. Clone the new repository locally.
-5. In the new repository, update:
- - package and module names if `agentic_project_starter` is no longer the right name
- - `.env` values and environment-variable defaults
- - README, docs, and runtime settings for the actual project
-6. Commit that initial project rename and setup baseline before you start large implementation changes.
-
-Recommended first pass in the new repo:
-
-- decide the real product and user-facing workflow
-- define the actual FastAPI routes and domain services
-- replace placeholder agent definitions with project-specific agents, tools, and prompts
-- replace ETL examples with real sources, transforms, and sinks
-- decide whether Docker is enough for your demo or whether your project needs its own deployment setup
-
-## Hackathon Path
-
-For a Codex-focused hackathon, keep the first loop small and demonstrable:
-
-1. Create a repository from this template and clone it.
-2. Run `cp .env.example .env`, then add `OPENAI_API_KEY` if your demo needs live OpenAI calls.
-3. Run `make setup`, `make doctor`, and `make check` to confirm the scaffold is healthy.
-4. Ask Codex to implement one real feature on top of the existing package boundaries.
-5. Run `make serve` and, for chat-first demos, `make frontend`.
-6. Before opening a PR, run `make quiz` and commit `.change-quiz/result.json`; the quiz check is mandatory by default.
-
-## How To Prompt Codex
-
-After creating a new repo from this template, use Codex to replace the scaffold
-incrementally instead of asking for one giant rewrite.
-
-Good prompts usually include:
-
-- the business goal and target users
-- the API or CLI behavior you want
-- the data sources and storage model
-- the runtime target
-- constraints such as auth, latency, cost, compliance, or testing expectations
-- concrete acceptance criteria
-
-When prompting Codex in the new repo:
-
-- tell it to preserve the overall scaffold unless there is a clear reason to change structure
-- ask it to implement one subsystem at a time
-- ask it to update tests, docs, and env vars together when behavior changes
-- be explicit about what is real logic versus what should stay as reusable template scaffolding
-- if you want frontend help, say whether the product is chat-first; this starter has repo-local skills that can recommend ChatKit when that is actually a good fit
-- name the relevant repo-local skill when the task is fragile: `scaffold-outcomes` for first product work, `frontend-starter-guidance` for chatbot/UI work, and `architecture-boundaries` for runtime or API wiring
-
-Example prompts for a new repo:
-
-```text
-Use the scaffold-outcomes and architecture-boundaries skills.
-Implement the real application logic for this project on top of the starter.
-Keep the existing runtime/api/shared structure. Add FastAPI routes for account
-creation, login, and project management, backed by PostgreSQL. Update tests,
-environment variables, and docs as part of the change.
-```
-
-```text
-Use the scaffold-outcomes skill.
-Replace the placeholder OpenAI Agents SDK scaffolding with a real multi-agent
-workflow for financial research. Keep the existing agentic package structure.
-Add a coordinator, researcher, and report-writer agent, define the tools they
-use, and add smoke tests for registry wiring and dry-run behavior.
-```
-
-```text
-Use the agent-etl-scaffolder and architecture-boundaries skills.
-Replace the ETL starter jobs with a real pipeline that ingests CSV files from S3,
-normalizes them into a warehouse-friendly schema, and writes outputs to
-PostgreSQL. Keep the existing etl package structure, add typed job configs, and
-update docs and environment variables.
-```
-
-```text
-Use the frontend-starter-guidance skill.
-Design the frontend for this product on top of the starter. First decide whether
-the experience should be chat-first or not. If it is chat-first, you may
-recommend the optional ChatKit path in `frontend/`, but keep the backend seams
-generic and replaceable. If it is not chat-first, recommend a conventional React
-app structure instead. Update docs and runtime wiring only where needed.
-```
+- The assistant is educational only and does not replace a fiduciary or legal adviser.
+- Rates, thresholds, deadlines, and filing positions can vary by year, commune, and user status.
+- Users should verify material decisions with the Zurich tax office, their commune, or a qualified Swiss tax professional.
## Quick start
-Use this after creating a new repo from the template to validate that the
-scaffold still boots correctly before you add real business logic.
-
```bash
cp .env.example .env
make setup
-make doctor
make serve
make frontend
```
-API endpoints:
+Add `OPENAI_API_KEY` to `.env` before sending live chat messages.
+
+Local endpoints:
- `GET /healthz`
- `GET /v1/runtime/summary`
- `POST /chatkit`
-Optional frontend:
+Frontend:
-- `http://127.0.0.1:5173` during local Vite development
+- `http://127.0.0.1:5173` during Vite development
- same-origin frontend bundle in Docker after `docker compose up --build`
## Common commands
@@ -163,6 +77,10 @@ make docker-up
└── .agents/skills
```
+The product-specific chat behavior lives under `src/agentic_project_starter/chat`.
+The UI shell lives in `frontend/`. The broader runtime layout remains close to
+the starter so future domain work can stay incremental.
+
## Documentation
- [Architecture](docs/architecture.md)
diff --git a/docs/architecture.md b/docs/architecture.md
index 3d737a4..d528d25 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,12 +1,13 @@
# Architecture Overview
-This repository is intentionally a scaffold rather than a product. It gives new
-projects a clear starting point for:
+This repository now configures the starter into a chat-first Zurich tax guidance
+app while preserving the starter’s package boundaries.
-- a Python web runtime
-- OpenAI Agents SDK-based orchestration
-- ETL pipelines
-- an optional ChatKit-based frontend accelerator
+The current product focus is:
+
+- Zurich personal tax guidance for educational use
+- a ChatKit-based finance chat frontend
+- OpenAI Agents SDK orchestration for the assistant persona
- local and Docker execution
## Source layout
@@ -22,11 +23,11 @@ projects a clear starting point for:
- `src/agentic_project_starter/agentic`
- starter agent registry, request/response models, and OpenAI Agents SDK stubs
- `src/agentic_project_starter/chat`
- - generic chat service seam, file-backed starter chat storage, and the optional ChatKit adapter
+ - Zurich tax assistant profile, chat service seam, file-backed chat storage, and the ChatKit adapter
- `src/agentic_project_starter/etl`
- ETL job registry and starter execution contracts
- `frontend`
- - optional Vite + React ChatKit accelerator that can be removed or replaced in downstream repos
+ - Vite + React ChatKit UI shell for the finance chat experience
## Runtime model
@@ -35,16 +36,21 @@ projects a clear starting point for:
- FastAPI exposes a minimal service surface:
- `/healthz`
- `/v1/runtime/summary`
- - `/chatkit` for the optional self-hosted chat accelerator
+ - `/chatkit` for the self-hosted chat accelerator
-The runtime intentionally exposes starter metadata rather than business logic.
+The runtime summary includes assistant metadata and guardrails so tests and
+operators can inspect the active product profile.
-## Extension path
+## Assistant behavior
-When turning this starter into a real project:
+- The chat coordinator loads product context through a tool before each turn.
+- The responder focuses on Zurich tax preparation topics and asks clarifying questions when facts are missing.
+- The assistant is instructed to avoid pretending certainty on rates, deadlines, or filing positions.
+- High-stakes or ambiguous situations are escalated to official Zurich sources or a qualified Swiss tax professional.
+
+## Extension path
-1. Replace placeholder agent registry entries with task-specific agents and tools.
-2. Replace ETL stub stages with real extract/transform/load implementations.
-3. Replace the file-backed chat storage and ChatKit adapter if your real project needs different UX or persistence.
-4. Add domain routers and services under `api/` and `runtime/`.
-5. Add project-specific deployment automation only when the project needs it.
+1. Add Zurich-specific retrieval or official-source lookup before expanding legal specificity.
+2. Replace file-backed chat storage if the app needs multi-user persistence.
+3. Add domain routers and services if the product grows beyond chat.
+4. Replace ETL stubs only when the app needs ingestion or analytics pipelines.
diff --git a/docs/environment-variables.md b/docs/environment-variables.md
index 3c887cb..5e87bd5 100644
--- a/docs/environment-variables.md
+++ b/docs/environment-variables.md
@@ -7,7 +7,7 @@ The application reads settings from `.env` and optional overrides from
| Variable | Default | Purpose |
| --- | --- | --- |
-| `APP_NAME` | `agentic-project-starter` | Service name used in API metadata and logs |
+| `APP_NAME` | `zurich-tax-desk` | Service name used in API metadata and logs |
| `APP_ENVIRONMENT` | `local` | Runtime profile such as `local`, `docker`, or `ci` |
| `APP_HOST` | `0.0.0.0` | Bind host for local and container execution |
| `APP_PORT` | `8000` | Bind port for local and container execution |
@@ -18,16 +18,16 @@ The application reads settings from `.env` and optional overrides from
| Variable | Default | Purpose |
| --- | --- | --- |
| `OPENAI_API_KEY` | empty | API key required for live OpenAI calls |
-| `OPENAI_MODEL` | `gpt-5` | Default model name used by starter agent definitions |
+| `OPENAI_MODEL` | `gpt-5` | Default model name used by the Zurich tax chat assistant |
| `OPENAI_DEFAULT_AGENT` | `coordinator` | Default agent name for project-specific wrappers |
| `OPENAI_ENABLE_TRACING` | `true` | Enables OpenAI Agents SDK tracing when `OPENAI_API_KEY` is configured |
-| `CHATKIT_DOMAIN_KEY` | `local-dev` | Domain key passed to the optional self-hosted ChatKit frontend |
+| `CHATKIT_DOMAIN_KEY` | `local-dev` | Domain key passed to the self-hosted ChatKit frontend |
## Data and observability
| Variable | Default | Purpose |
| --- | --- | --- |
-| `STORAGE_URI` | `file://./var/data` | Starter storage location for local or containerized work |
+| `STORAGE_URI` | `file://./var/data` | Storage location for chat history and other local app data |
| `ETL_DEFAULT_DATASET` | `demo-dataset` | Default dataset identifier for CLI ETL commands |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | empty | Optional OTLP endpoint for telemetry export |
@@ -37,5 +37,5 @@ The application reads settings from `.env` and optional overrides from
- `.env`: local developer secrets and overrides
- `.env.local`: optional higher-priority local overrides
- `.env.docker.example`: example container-oriented overrides
-- `frontend/.env.example`: optional frontend overrides for the ChatKit accelerator
+- `frontend/.env.example`: optional frontend overrides for the ChatKit UI
- `frontend/.env.local`: local frontend overrides such as `VITE_API_BASE_URL` when the backend is not on `127.0.0.1:8000`
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 9f17d77..3e55e96 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -7,7 +7,7 @@ cp .env.example .env
make setup
```
-Add your `OPENAI_API_KEY` to `.env` if you plan to enable live agent runs.
+Add your `OPENAI_API_KEY` to `.env` if you plan to enable live Zurich tax chat responses.
## 2. Run locally
@@ -17,11 +17,17 @@ make serve
make frontend
```
-The backend stays available on `http://127.0.0.1:8000`. The optional ChatKit
-frontend runs on `http://127.0.0.1:5173` and talks to the starter’s self-hosted
-`/chatkit` endpoint through the Vite dev proxy. Add `OPENAI_API_KEY` to `.env`
-before sending live chat messages. The frontend template already includes the
-required ChatKit web component loader in `frontend/index.html`.
+The backend stays available on `http://127.0.0.1:8000`. The chat-first frontend
+runs on `http://127.0.0.1:5173` and talks to the self-hosted `/chatkit`
+endpoint through the Vite dev proxy. Add `OPENAI_API_KEY` to `.env` before
+sending live chat messages. The frontend template already includes the required
+ChatKit web component loader in `frontend/index.html`.
+
+Suggested first prompts:
+
+- "I moved to Zurich this year. What documents should I gather for my first tax return?"
+- "I am a salaried employee in Zurich. Which deductions should I review before filing?"
+- "I pay withholding tax in Zurich. What facts matter before I decide whether to seek professional advice?"
## 3. Run placeholder workflows
diff --git a/frontend/index.html b/frontend/index.html
index 82c0097..de93d45 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,7 +3,7 @@
- Starter Chat
+ Zurich Tax Desk
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index e6e4bb6..f11c755 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -25,6 +25,7 @@ describe("App", () => {
render();
expect(screen.getByTestId("chatkit")).toBeTruthy();
+ expect(screen.getByText("Zurich Tax Desk")).toBeTruthy();
});
it("uses the same-origin ChatKit endpoint by default", () => {
@@ -38,4 +39,24 @@ describe("App", () => {
}),
);
});
+
+ it("configures the Zurich tax guidance prompts and placeholder", () => {
+ render();
+
+ expect(chatKitMock.useChatKit).toHaveBeenCalledWith(
+ expect.objectContaining({
+ composer: expect.objectContaining({
+ placeholder:
+ "Ask about Zurich tax returns, deductions, residency, or withholding tax…",
+ }),
+ startScreen: expect.objectContaining({
+ greeting: "Zurich tax guidance for real-world filing prep",
+ prompts: expect.arrayContaining([
+ expect.objectContaining({ label: "First tax return" }),
+ expect.objectContaining({ label: "Employee deductions" }),
+ ]),
+ }),
+ }),
+ );
+ });
});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a8c142b..c948411 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -5,6 +5,8 @@ const apiBaseUrl =
const chatkitApiUrl = apiBaseUrl ? `${apiBaseUrl}/chatkit` : "/chatkit";
const domainKey =
(import.meta.env.VITE_CHATKIT_DOMAIN_KEY as string | undefined)?.trim() || "local-dev";
+const threadStorageKey = "zurich-tax-thread-id";
+const userStorageKey = "zurich-tax-user-id";
export function App() {
const { control } = useChatKit({
@@ -31,7 +33,7 @@ export function App() {
},
},
composer: {
- placeholder: "Ask the starter how the scaffold is wired…",
+ placeholder: "Ask about Zurich tax returns, deductions, residency, or withholding tax…",
},
history: {
enabled: true,
@@ -43,53 +45,88 @@ export function App() {
retry: true,
},
startScreen: {
- greeting: "Chat with the starter scaffold",
+ greeting: "Zurich tax guidance for real-world filing prep",
prompts: [
{
- label: "Inspect runtime",
- prompt: "Summarize the current runtime scaffold and the main extension seams.",
+ label: "First tax return",
+ prompt:
+ "I just moved to Zurich. What documents should I gather for my first personal tax return?",
+ icon: "compass",
+ },
+ {
+ label: "Employee deductions",
+ prompt:
+ "I am a salaried employee in Zurich. Which common deductions should I review before filing?",
icon: "sparkle",
},
{
- label: "Ask about agents",
- prompt: "Explain how the starter agent registry is structured.",
- icon: "compass",
+ label: "Withholding tax",
+ prompt:
+ "I pay withholding tax in Zurich. When should I still look into an ordinary tax return or professional advice?",
+ icon: "sparkle",
},
],
},
- initialThread: window.localStorage.getItem("starter-chat-thread-id") || undefined,
+ initialThread: window.localStorage.getItem(threadStorageKey) || undefined,
onThreadChange: ({ threadId }) => {
if (threadId) {
- window.localStorage.setItem("starter-chat-thread-id", threadId);
+ window.localStorage.setItem(threadStorageKey, threadId);
}
},
});
return (
-
- Optional Accelerator
- ChatKit Starter
-
- This frontend is an optional scaffold over the generic backend chat service. Keep it,
- swap it, or delete it in downstream repos.
-
-
-
-
- Backend
- - Self-hosted ChatKit adapter
-
-
-
- Traces
- - Native ChatKit tool and agent activity
-
-
-
- Identity
- - Anonymous local user seam
-
-
+
+
+
Zurich Personal Tax Guide
+
Zurich Tax Desk
+
+ A finance chat app for people living or working in Zurich who want clearer questions,
+ better filing prep, and faster orientation before they talk to a professional.
+
+
+
+ Zurich-focused
+ Educational only
+ Verify rates and deadlines
+
+
+
Use it for
+
+ Return prep, deduction checklists, relocation questions, withholding-tax context, and
+ organizing what to ask a Swiss tax adviser.
+
+
+
+
+ Best for
+ Employees, new arrivals, families, and side-income earners preparing a Zurich return.
+
+
+ Escalate when
+ Permit status, cross-border income, or large tax positions could materially change what you owe.
+
+
+ Approach
+ The assistant asks for facts first, then gives practical next steps instead of pretending certainty.
+
+
+
+
Important
+
+ This app is not a fiduciary or legal service. Confirm filing decisions with the Zurich
+ tax office, your commune, or a qualified Swiss tax professional.
+
+
+
+
Live chat
+
+ Bring your salary, permit, family, and move-date details for better answers.
+
+
@@ -99,12 +136,11 @@ export function App() {
}
function getStarterUserId(): string {
- const storageKey = "starter-chat-user-id";
- const existing = window.localStorage.getItem(storageKey);
+ const existing = window.localStorage.getItem(userStorageKey);
if (existing) {
return existing;
}
- const generated = `starter-user-${crypto.randomUUID()}`;
- window.localStorage.setItem(storageKey, generated);
+ const generated = `zurich-tax-user-${crypto.randomUUID()}`;
+ window.localStorage.setItem(userStorageKey, generated);
return generated;
}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 4f0607c..604cd6c 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -1,11 +1,19 @@
:root {
color-scheme: light;
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
+ --ink-strong: #172033;
+ --ink-muted: #516078;
+ --border-soft: rgba(23, 32, 51, 0.1);
+ --panel: rgba(255, 252, 247, 0.86);
+ --panel-strong: rgba(255, 255, 255, 0.94);
+ --accent: #0f766e;
+ --accent-warm: #d97706;
+ --shadow: 0 28px 70px rgba(15, 23, 42, 0.14);
background:
- radial-gradient(circle at top left, rgba(187, 247, 208, 0.8), transparent 34%),
- radial-gradient(circle at bottom right, rgba(254, 240, 138, 0.55), transparent 28%),
- linear-gradient(135deg, #f5f5f4 0%, #fafaf9 45%, #ecfccb 100%);
- color: #1c1917;
+ radial-gradient(circle at 15% 20%, rgba(249, 115, 22, 0.18), transparent 24%),
+ radial-gradient(circle at 85% 10%, rgba(13, 148, 136, 0.2), transparent 26%),
+ linear-gradient(135deg, #f4efe5 0%, #f8fafc 48%, #edf6f1 100%);
+ color: var(--ink-strong);
}
* {
@@ -24,25 +32,26 @@ body {
.app-shell {
display: grid;
grid-template-columns: minmax(280px, 420px) minmax(0, 1fr);
- gap: 1.5rem;
+ gap: 1.25rem;
min-height: 100vh;
- padding: 1.5rem;
+ padding: 1.4rem;
}
-.hero-panel,
-.chat-frame {
- border: 1px solid rgba(28, 25, 23, 0.08);
+.briefing-panel,
+.chat-frame,
+.chat-panel-header {
+ border: 1px solid var(--border-soft);
border-radius: 28px;
- background: rgba(255, 255, 255, 0.82);
+ background: var(--panel);
backdrop-filter: blur(20px);
- box-shadow: 0 24px 60px rgba(41, 37, 36, 0.08);
+ box-shadow: var(--shadow);
}
-.hero-panel {
+.briefing-panel {
padding: 2rem;
display: flex;
flex-direction: column;
- justify-content: space-between;
+ gap: 1.2rem;
}
.eyebrow {
@@ -51,58 +60,98 @@ body {
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
- color: #15803d;
+ color: var(--accent);
}
-.hero-panel h1 {
+.briefing-panel h1 {
margin: 0;
- font-size: clamp(2.3rem, 5vw, 3.6rem);
- line-height: 0.94;
- letter-spacing: -0.05em;
+ font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
+ font-size: clamp(2.6rem, 5vw, 4.2rem);
+ line-height: 0.92;
+ letter-spacing: -0.06em;
}
.lede {
margin: 1rem 0 0;
- max-width: 28rem;
- color: #57534e;
- font-size: 1rem;
- line-height: 1.6;
+ color: var(--ink-muted);
+ font-size: 1.02rem;
+ line-height: 1.7;
}
-.fact-grid {
- display: grid;
- gap: 0.9rem;
- margin: 2rem 0 0;
+.signal-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.65rem;
}
-.fact-grid div {
- padding: 1rem 1.1rem;
- border-radius: 18px;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(248, 250, 252, 0.8));
- border: 1px solid rgba(28, 25, 23, 0.06);
+.signal-strip span {
+ padding: 0.55rem 0.9rem;
+ border-radius: 999px;
+ font-size: 0.82rem;
+ font-weight: 700;
+ letter-spacing: 0.03em;
+ color: var(--ink-strong);
+ background: rgba(255, 255, 255, 0.75);
+ border: 1px solid rgba(15, 118, 110, 0.12);
}
-.fact-grid dt {
- margin: 0 0 0.35rem;
+.compliance-note,
+.disclaimer-card,
+.topic-grid article {
+ border-radius: 22px;
+ border: 1px solid var(--border-soft);
+ background: linear-gradient(180deg, var(--panel-strong), rgba(248, 250, 252, 0.88));
+ padding: 1.15rem 1.2rem;
+}
+
+.note-kicker,
+.chat-label {
+ margin: 0;
font-size: 0.78rem;
font-weight: 700;
- letter-spacing: 0.12em;
+ letter-spacing: 0.14em;
text-transform: uppercase;
- color: #78716c;
+ color: var(--accent-warm);
}
-.fact-grid dd {
+.compliance-note p:last-child,
+.disclaimer-card p:last-child,
+.topic-grid p {
+ margin: 0.5rem 0 0;
+ color: var(--ink-muted);
+ line-height: 1.6;
+}
+
+.topic-grid {
+ display: grid;
+ gap: 0.85rem;
+}
+
+.topic-grid h2 {
margin: 0;
- font-size: 1rem;
- color: #1c1917;
+ font-size: 1.05rem;
+ color: var(--ink-strong);
}
.chat-panel {
min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.chat-panel-header {
+ padding: 1rem 1.2rem;
+}
+
+.chat-caption {
+ margin: 0.45rem 0 0;
+ color: var(--ink-muted);
+ line-height: 1.5;
}
.chat-frame {
- height: calc(100vh - 3rem);
+ height: calc(100vh - 8.2rem);
padding: 0.8rem;
}
@@ -120,7 +169,7 @@ body {
padding: 1rem;
}
- .hero-panel {
+ .briefing-panel {
padding: 1.5rem;
}
diff --git a/src/agentic_project_starter/api/routes.py b/src/agentic_project_starter/api/routes.py
index ee7dbff..54e8045 100644
--- a/src/agentic_project_starter/api/routes.py
+++ b/src/agentic_project_starter/api/routes.py
@@ -1,7 +1,8 @@
-"""API routes exposing starter metadata and placeholder runtime state."""
+"""API routes exposing runtime and assistant metadata."""
from fastapi import APIRouter, Request
+from agentic_project_starter.chat.tax_profile import tax_assistant_profile_payload
from agentic_project_starter.runtime.models import RuntimeContext
router = APIRouter(tags=["runtime"])
@@ -15,7 +16,7 @@ def get_context(request: Request) -> RuntimeContext:
@router.get("/runtime/summary")
def runtime_summary(request: Request) -> dict[str, object]:
- """Return starter metadata for smoke tests and local introspection."""
+ """Return runtime and assistant metadata for smoke tests and introspection."""
context = get_context(request)
return {
@@ -32,4 +33,5 @@ def runtime_summary(request: Request) -> dict[str, object]:
str(context.chat_store.root_dir) if context.chat_store is not None else None
),
"chat_domain_key": context.settings.chatkit_domain_key,
+ "assistant_profile": tax_assistant_profile_payload(),
}
diff --git a/src/agentic_project_starter/chat/service.py b/src/agentic_project_starter/chat/service.py
index 3a302ac..2b195f5 100644
--- a/src/agentic_project_starter/chat/service.py
+++ b/src/agentic_project_starter/chat/service.py
@@ -1,4 +1,4 @@
-"""Generic chat service seam used by the optional ChatKit accelerator."""
+"""Generic chat service seam configured for the Zurich tax chat app."""
from __future__ import annotations
@@ -9,6 +9,10 @@
from agents.result import RunResultStreaming
from agentic_project_starter.chat.models import ChatRuntimeSummary, ChatTurnRequest
+from agentic_project_starter.chat.tax_profile import (
+ build_tax_assistant_profile,
+ tax_assistant_profile_payload,
+)
if TYPE_CHECKING:
from agentic_project_starter.runtime.models import RuntimeContext
@@ -18,11 +22,18 @@ class ChatService(Protocol):
"""Transport-agnostic chat service contract for starter integrations."""
async def run_turn(self, request: ChatTurnRequest) -> RunResultStreaming:
- """Run one streamed chat turn and return the streaming result handle."""
+ """Run one streamed chat turn and return the streaming result handle.
+
+ Args:
+ request: Normalized chat turn input from the transport layer.
+
+ Returns:
+ Streaming handle produced by the OpenAI Agents SDK runtime.
+ """
class StarterChatService:
- """Starter-safe chat implementation backed by the Agents SDK."""
+ """Zurich tax chat implementation backed by the Agents SDK."""
def __init__(self, runtime_context: RuntimeContext) -> None:
self._runtime_context = runtime_context
@@ -34,39 +45,55 @@ def __init__(self, runtime_context: RuntimeContext) -> None:
etl_jobs=tuple(sorted(runtime_context.etl_job_specs)),
storage_uri=runtime_context.settings.storage_uri,
)
+ self._assistant_profile = build_tax_assistant_profile()
self._responder = Agent(
- name="starter_responder",
+ name="zurich_tax_advisor",
model=runtime_context.settings.openai_model,
instructions=(
- "You are the final responder for a reusable starter template. "
- "Give direct answers, mention when a capability is still scaffold-only, "
- "and stay concise."
+ f"You are {self._assistant_profile.assistant_name}, a finance chat assistant "
+ f"focused on personal taxes in {self._assistant_profile.region}. "
+ "Help users understand Zurich tax preparation, likely deductions, "
+ "withholding-tax situations, self-employment basics, and filing checklists. "
+ "Ask targeted follow-up questions when the facts matter. "
+ "Keep answers practical, concise, and structured around the user's situation. "
+ f"{self._assistant_profile.disclaimer} "
+ "If a user asks for a precise filing position, deadline, rate, or optimization "
+ "that could materially affect money or compliance, state uncertainty clearly and "
+ f"tell them to verify it. {self._assistant_profile.verification_note}"
),
)
self._coordinator = Agent(
- name="starter_coordinator",
+ name="zurich_tax_coordinator",
model=runtime_context.settings.openai_model,
instructions=(
- "You coordinate a reusable starter-template chat experience. "
- "At the start of every turn, call inspect_runtime_summary exactly once. "
- "After you have the summary, hand off to starter_responder "
+ "You coordinate a Zurich tax guidance conversation. "
+ "At the start of every turn, call inspect_assistant_context exactly once. "
+ "After you have the context, hand off to zurich_tax_advisor "
"to write the final reply. Do not answer directly."
),
- tools=[self._build_runtime_summary_tool()],
+ tools=[self._build_assistant_context_tool()],
handoffs=[self._responder],
)
self._run_config = RunConfig(
model=runtime_context.settings.openai_model,
tracing_disabled=not runtime_context.settings.openai_enable_tracing,
- workflow_name="Starter Chat Workflow",
+ workflow_name="Zurich Tax Chat Workflow",
trace_metadata={
"app_name": runtime_context.settings.app_name,
"environment": runtime_context.settings.app_environment,
+ "assistant_name": self._assistant_profile.assistant_name,
},
)
async def run_turn(self, request: ChatTurnRequest) -> RunResultStreaming:
- """Stream a chat turn through the starter coordinator agent."""
+ """Stream a chat turn through the Zurich tax coordinator agent.
+
+ Args:
+ request: Normalized turn payload built by the chat transport.
+
+ Returns:
+ Streaming handle for the current response turn.
+ """
input_items = cast(list[Any], request.input_items)
return Runner.run_streamed(
@@ -80,23 +107,32 @@ async def run_turn(self, request: ChatTurnRequest) -> RunResultStreaming:
run_config=self._run_config,
)
- def _build_runtime_summary_tool(self) -> Any:
+ def _build_assistant_context_tool(self) -> Any:
@function_tool
- def inspect_runtime_summary() -> dict[str, object]:
- """Inspect the starter runtime summary and capabilities."""
-
- payload = asdict(self._runtime_summary)
- payload["capabilities"] = {
- "chat_ui": "ChatKit optional accelerator",
- "agent_registry": "placeholder-only",
- "etl_registry": "placeholder-only",
+ def inspect_assistant_context() -> dict[str, object]:
+ """Inspect runtime metadata and Zurich tax assistant guardrails."""
+
+ return {
+ "runtime": asdict(self._runtime_summary),
+ "assistant_profile": tax_assistant_profile_payload(),
+ "capabilities": {
+ "chat_ui": "ChatKit-based finance chat interface",
+ "agent_registry": "starter scaffold",
+ "etl_registry": "starter scaffold",
+ },
}
- return payload
- return inspect_runtime_summary
+ return inspect_assistant_context
def build_chat_service(runtime_context: RuntimeContext) -> ChatService:
- """Build the default starter chat service."""
+ """Build the default Zurich tax chat service.
+
+ Args:
+ runtime_context: Resolved application runtime context.
+
+ Returns:
+ Chat service implementation wired to the current runtime.
+ """
return StarterChatService(runtime_context)
diff --git a/src/agentic_project_starter/chat/tax_profile.py b/src/agentic_project_starter/chat/tax_profile.py
new file mode 100644
index 0000000..113dfca
--- /dev/null
+++ b/src/agentic_project_starter/chat/tax_profile.py
@@ -0,0 +1,78 @@
+"""Structured Zurich tax assistant profile used by the chat runtime."""
+
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+
+
+@dataclass(frozen=True, slots=True)
+class TaxAssistantProfile:
+ """Static product profile for the Zurich tax chat assistant."""
+
+ assistant_name: str
+ region: str
+ audience: str
+ supported_topics: tuple[str, ...]
+ guardrails: tuple[str, ...]
+ escalation_triggers: tuple[str, ...]
+ disclaimer: str
+ verification_note: str
+
+
+def build_tax_assistant_profile() -> TaxAssistantProfile:
+ """Return the Zurich tax guidance profile for this app.
+
+ Returns:
+ Structured product metadata used by the chat service and API summary.
+ """
+
+ return TaxAssistantProfile(
+ assistant_name="Zurich Tax Desk",
+ region="Zurich, Switzerland",
+ audience=(
+ "People living, working, or moving to Zurich who need educational guidance "
+ "for personal tax preparation."
+ ),
+ supported_topics=(
+ "annual return preparation",
+ "salary withholding and permit-related tax questions",
+ "common deductions for employees and families",
+ "self-employment and side-income preparation questions",
+ "pillar 3a and pension-related tax planning basics",
+ "document checklists for moves, residency changes, and cross-border situations",
+ ),
+ guardrails=(
+ "Ask follow-up questions before recommending a tax position.",
+ "Do not present educational guidance as legal, fiduciary, or filing advice.",
+ "Call out when tax rates, forms, thresholds, or deadlines may vary by year or commune.",
+ (
+ "Recommend verification with official Zurich or Swiss tax sources "
+ "for material decisions."
+ ),
+ ),
+ escalation_triggers=(
+ "large deductions or optimization strategies with meaningful money impact",
+ "uncertain permit or residency status",
+ "cross-border, dual-residency, or relocation edge cases",
+ "self-employment structure or bookkeeping decisions",
+ "audit notices, disputes, penalties, or amended filings",
+ ),
+ disclaimer=(
+ "This app provides educational Zurich tax guidance only and is not a substitute for "
+ "a licensed Swiss tax adviser or official tax-office instructions."
+ ),
+ verification_note=(
+ "Verify important filing decisions, deadlines, and rates with the Zurich tax office, "
+ "your commune, or a qualified Swiss tax professional."
+ ),
+ )
+
+
+def tax_assistant_profile_payload() -> dict[str, object]:
+ """Return a JSON-serializable Zurich tax assistant profile.
+
+ Returns:
+ Dictionary payload suitable for API responses and agent tools.
+ """
+
+ return asdict(build_tax_assistant_profile())
diff --git a/src/agentic_project_starter/shared/config.py b/src/agentic_project_starter/shared/config.py
index 2429e96..98b6458 100644
--- a/src/agentic_project_starter/shared/config.py
+++ b/src/agentic_project_starter/shared/config.py
@@ -18,7 +18,7 @@ class Settings(BaseSettings):
extra="ignore",
)
- app_name: str = "agentic-project-starter"
+ app_name: str = "zurich-tax-desk"
app_environment: EnvironmentName = "local"
app_host: str = "0.0.0.0"
app_port: int = 8000
diff --git a/tests/test_api.py b/tests/test_api.py
index b4062bd..f0b047b 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -29,6 +29,8 @@ def test_runtime_summary() -> None:
assert payload["chat_storage_backend"] == "file"
assert payload["openai_api_key_configured"] is False
assert payload["openai_tracing_enabled"] is True
+ assert payload["assistant_profile"]["assistant_name"] == "Zurich Tax Desk"
+ assert payload["assistant_profile"]["region"] == "Zurich, Switzerland"
def test_chatkit_requires_openai_api_key() -> None: