Implement email-address-based conversation matching
Closes the gap where parents sending a new email (instead of replying) would lose their registration progress. All changes follow the email-based-conversation-matching OpenSpec change. Key changes ----------- storage/json_store.py - normalize_email() helper (lowercase + trim) - Conversations now keyed by sender email address, not thread ID - Versioned registration storage: data/registrations/<email>/v<N>_<ts>.json - current.json always reflects the latest version - save_registration() returns (email_key, version) tuple - save_registration_version() for updates with change_summary - get_registration_history() returns all versions in order models/conversation.py - Added last_inbound_message_id field for reply threading (not matching) channels/email_channel.py - fetch_unread_messages() no longer exposes thread_id - Conversation matching removed from channel layer (now in agent) - Removed _resolve_thread_id() — threading headers kept for SMTP only agent/core.py - process_message() takes parent_email + inbound_message_id (no thread ID) - Looks up conversation by normalized email address - Post-completion handler: detects intent (question / update / new_child) - Registration updates: diffs old vs new, versions storage, notifies admin agent/prompts.py - build_system_prompt() dispatches to registration or post-completion prompt - Post-completion prompt guides LLM to return intent field - Reminder language updated: no expiration threats notifications/notifier.py - notify_admin() accepts version parameter - notify_registration_update() sends "Registration Updated" emails with diff - _build_update_body() includes field-level old→new change summary main.py - Poll loop passes parent_email + inbound_message_id to agent (no thread_id) https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
This commit is contained in:
+143
-48
@@ -6,7 +6,7 @@ from ..knowledge_base.loader import KnowledgeBase
|
||||
from ..models.conversation import ConversationState
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step descriptions help the model understand where it is in the flow.
|
||||
# Step descriptions help the model understand where it is in the registration flow.
|
||||
# ---------------------------------------------------------------------------
|
||||
STEP_DESCRIPTIONS = {
|
||||
"greeting": "Greet the parent and detect their intent (registration vs. questions).",
|
||||
@@ -35,66 +35,28 @@ STEP_DESCRIPTIONS = {
|
||||
"complete": "Thank the parent, mention fees and next steps. Registration is done.",
|
||||
}
|
||||
|
||||
|
||||
def build_system_prompt(kb: KnowledgeBase, state: ConversationState) -> str:
|
||||
"""Return the full system prompt for the current conversation turn."""
|
||||
|
||||
kb_content = kb.get_all()
|
||||
reg_json = json.dumps(state.registration.to_dict(), ensure_ascii=False, indent=2)
|
||||
step_hint = STEP_DESCRIPTIONS.get(state.flow_step, "Continue the conversation.")
|
||||
|
||||
return f"""You are the registration assistant for Spielgruppe Pumuckl, run by Familienverein Fällanden in Fällanden, Switzerland. You help parents register their children for the playgroup and answer questions about it.
|
||||
|
||||
## Your Personality
|
||||
_PERSONALITY = """## Your Personality
|
||||
- Warm, friendly, and helpful — like a caring playgroup staff member
|
||||
- Use informal "du" in German (never the formal "Sie")
|
||||
- Auto-detect the parent's language from their message; respond in the same language
|
||||
- Default language is German if unclear
|
||||
- Ask 1–2 questions at a time — never send an overwhelming form-like list
|
||||
- Be patient and understanding; never make parents feel they made a mistake
|
||||
- Be patient and understanding; never make parents feel they made a mistake"""
|
||||
|
||||
## Registration Flow (8 steps)
|
||||
1. greeting — greet and detect intent
|
||||
2. child_name — ask for child's full name
|
||||
3. child_dob — ask for date of birth; validate age (indoor ≥2.5 yrs, outdoor ≥3 yrs)
|
||||
4. playgroup_selection — present options, collect type(s) and day(s)
|
||||
5. special_needs — ask about special needs / allergies / medical conditions
|
||||
6. parent_contact — name, street address, postal code, city, phone, email
|
||||
7. emergency_contact — emergency contact name and phone
|
||||
8. confirmation — show full summary; ask to confirm; submit on confirmation
|
||||
9. complete — thank parent, mention CHF 80 registration fee, monthly fees, and contacts
|
||||
_CONTACTS = """## Admin Contacts
|
||||
- Administration: Markus Graf — spielgruppen@familien-verein.ch — 079 261 16 37
|
||||
- Indoor leader: Andrea Sigrist — andrea.sigrist@gmx.net — 079 674 99 92
|
||||
- Outdoor leader: Barbara Gross — baba.laeubli@gmail.com — 078 761 19 64"""
|
||||
|
||||
**Current step: {state.flow_step}**
|
||||
**What to do now: {step_hint}**
|
||||
|
||||
At any point the parent may ask a question. Answer it from the knowledge base, then offer to continue the registration.
|
||||
|
||||
## Current Registration Data (so far)
|
||||
```json
|
||||
{reg_json}
|
||||
```
|
||||
|
||||
## Knowledge Base
|
||||
Use the information below to answer parent questions accurately:
|
||||
|
||||
{kb_content}
|
||||
|
||||
## Playgroup Details
|
||||
_PLAYGROUP_DETAILS = """## Playgroup Details
|
||||
- **Indoor (Innenspielgruppe)**: Mon / Wed / Thu, 09:00–11:30 | CHF 130/260/390 per month (1/2/3×/week)
|
||||
- **Outdoor Forest (Waldspielgruppe)**: Mon only, 09:00–14:00 (includes snack & lunch) | CHF 250/month
|
||||
- **One-time registration fee**: CHF 80 (first year); CHF 80 craft materials from second year
|
||||
- **Cleaning deposit (indoor only)**: CHF 50 (refundable)
|
||||
- **Sibling discount**: 10% per additional child
|
||||
- **July & August**: fee-free
|
||||
- **July & August**: fee-free"""
|
||||
|
||||
## Admin Contacts
|
||||
- Administration: Markus Graf — spielgruppen@familien-verein.ch — 079 261 16 37
|
||||
- Indoor leader: Andrea Sigrist — andrea.sigrist@gmx.net — 079 674 99 92
|
||||
- Outdoor leader: Barbara Gross — baba.laeubli@gmail.com — 078 761 19 64
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL: Response Format
|
||||
_REGISTRATION_RESPONSE_FORMAT = """## CRITICAL: Response Format
|
||||
|
||||
You MUST respond with **only** a valid JSON object — no markdown, no extra text outside the JSON.
|
||||
|
||||
@@ -128,5 +90,138 @@ Rules:
|
||||
- Dates must be YYYY-MM-DD. Postal codes must be exactly 4 digits.
|
||||
- Valid days: "monday", "wednesday", "thursday" (indoor) or "monday" (outdoor).
|
||||
- `language` must be "de" or "en" based on the parent's message.
|
||||
- The `reply` field must be natural, friendly, conversational text — not JSON and not a list of fields."""
|
||||
|
||||
_POST_COMPLETION_RESPONSE_FORMAT = """## CRITICAL: Response Format
|
||||
|
||||
You MUST respond with **only** a valid JSON object — no markdown, no extra text outside the JSON.
|
||||
|
||||
```json
|
||||
{{
|
||||
"reply": "Your conversational message to the parent (plain text, NOT JSON)",
|
||||
"intent": "question",
|
||||
"updates": {{
|
||||
"child.fullName": "string or null",
|
||||
"child.dateOfBirth": "YYYY-MM-DD or null",
|
||||
"child.specialNeeds": "string or null",
|
||||
"parentGuardian.fullName": "string or null",
|
||||
"parentGuardian.streetAddress": "string or null",
|
||||
"parentGuardian.postalCode": "4-digit string or null",
|
||||
"parentGuardian.city": "string or null",
|
||||
"parentGuardian.phone": "string or null",
|
||||
"parentGuardian.email": "string or null",
|
||||
"emergencyContact.fullName": "string or null",
|
||||
"emergencyContact.phone": "string or null",
|
||||
"booking.playgroupTypes": ["indoor", "outdoor"] or null,
|
||||
"booking.selectedDays": [{{"day": "monday", "type": "indoor"}}] or null
|
||||
}},
|
||||
"language": "de"
|
||||
}}
|
||||
```
|
||||
|
||||
`intent` values:
|
||||
- `"question"` — parent is asking about fees, schedule, policies, etc. → answer from knowledge base; set `updates` to all nulls.
|
||||
- `"update"` — parent explicitly wants to change their registration data → collect the new values in `updates`, confirm the change in `reply`.
|
||||
- `"new_child"` — parent wants to register an additional child → treat as a new registration; begin from step child_name.
|
||||
|
||||
Rules:
|
||||
- Only set fields in `updates` when intent is `"update"` AND the parent has provided the new value in this message.
|
||||
- Use `null` for all `updates` fields when intent is `"question"` or `"new_child"`.
|
||||
- `language` must be "de" or "en" based on the parent's message.
|
||||
- The `reply` field must be natural, friendly, conversational text — not JSON and not a list of fields.
|
||||
- If you are unsure of the parent's intent, ask a clarifying question and set intent to `"question"`."""
|
||||
|
||||
|
||||
def build_system_prompt(kb: KnowledgeBase, state: ConversationState) -> str:
|
||||
"""Return the system prompt appropriate for the current conversation state."""
|
||||
if state.completed:
|
||||
return _build_post_completion_prompt(kb, state)
|
||||
return _build_registration_prompt(kb, state)
|
||||
|
||||
|
||||
def _build_registration_prompt(kb: KnowledgeBase, state: ConversationState) -> str:
|
||||
"""System prompt for an in-progress registration conversation."""
|
||||
kb_content = kb.get_all()
|
||||
reg_json = json.dumps(state.registration.to_dict(), ensure_ascii=False, indent=2)
|
||||
step_hint = STEP_DESCRIPTIONS.get(state.flow_step, "Continue the conversation.")
|
||||
|
||||
return f"""You are the registration assistant for Spielgruppe Pumuckl, run by Familienverein Fällanden in Fällanden, Switzerland. You help parents register their children for the playgroup and answer questions about it.
|
||||
|
||||
{_PERSONALITY}
|
||||
|
||||
## Registration Flow (8 steps)
|
||||
1. greeting — greet and detect intent
|
||||
2. child_name — ask for child's full name
|
||||
3. child_dob — ask for date of birth; validate age (indoor ≥2.5 yrs, outdoor ≥3 yrs)
|
||||
4. playgroup_selection — present options, collect type(s) and day(s)
|
||||
5. special_needs — ask about special needs / allergies / medical conditions
|
||||
6. parent_contact — name, street address, postal code, city, phone, email
|
||||
7. emergency_contact — emergency contact name and phone
|
||||
8. confirmation — show full summary; ask to confirm; submit on confirmation
|
||||
9. complete — thank parent, mention CHF 80 registration fee, monthly fees, and contacts
|
||||
|
||||
**Current step: {state.flow_step}**
|
||||
**What to do now: {step_hint}**
|
||||
|
||||
At any point the parent may ask a question. Answer it from the knowledge base, then offer to continue the registration.
|
||||
|
||||
## Current Registration Data (so far)
|
||||
```json
|
||||
{reg_json}
|
||||
```
|
||||
|
||||
## Knowledge Base
|
||||
Use the information below to answer parent questions accurately:
|
||||
|
||||
{kb_content}
|
||||
|
||||
{_PLAYGROUP_DETAILS}
|
||||
|
||||
{_CONTACTS}
|
||||
|
||||
---
|
||||
|
||||
{_REGISTRATION_RESPONSE_FORMAT}
|
||||
"""
|
||||
|
||||
|
||||
def _build_post_completion_prompt(kb: KnowledgeBase, state: ConversationState) -> str:
|
||||
"""System prompt for a conversation where registration is already complete."""
|
||||
kb_content = kb.get_all()
|
||||
reg_json = json.dumps(state.registration.to_dict(), ensure_ascii=False, indent=2)
|
||||
child_name = state.registration.child.full_name or "their child"
|
||||
|
||||
return f"""You are the registration assistant for Spielgruppe Pumuckl, run by Familienverein Fällanden in Fällanden, Switzerland.
|
||||
|
||||
{_PERSONALITY}
|
||||
|
||||
## Context: Registration Already Complete
|
||||
This parent has already completed registration for {child_name}. Their current registration data is:
|
||||
|
||||
```json
|
||||
{reg_json}
|
||||
```
|
||||
|
||||
The parent is contacting you again. Your job is to:
|
||||
1. Detect their **intent**: are they asking a question, requesting a change to their registration, or registering another child?
|
||||
2. Respond helpfully and warmly.
|
||||
3. If they want to **update** their registration, confirm exactly what they want to change and include the new values in `updates`.
|
||||
4. If they are asking a **question**, answer from the knowledge base.
|
||||
5. If they want to register a **new child**, let them know you'll start a new registration and guide them from the beginning.
|
||||
|
||||
When handling update requests:
|
||||
- Confirm the change explicitly before reporting it as done ("So you'd like to change X to Y — is that right?").
|
||||
- Once confirmed, include the new value in `updates` so it can be saved.
|
||||
- Let the parent know the playgroup team will be informed of the change.
|
||||
|
||||
## Knowledge Base
|
||||
{kb_content}
|
||||
|
||||
{_PLAYGROUP_DETAILS}
|
||||
|
||||
{_CONTACTS}
|
||||
|
||||
---
|
||||
|
||||
{_POST_COMPLETION_RESPONSE_FORMAT}
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user