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: