Spaces:
Runtime error
Runtime error
ConstCorrectness commited on
Commit ·
762a6f5
1
Parent(s): 50dcddc
intents
Browse files- .claude/commands/classify-intent.md +13 -0
- INTENT_RESTRUCTURE.md +111 -0
- backend/intent_classifier.py +45 -0
- backend/main.py +12 -0
- intents.yaml +115 -0
- requirements.txt +1 -0
.claude/commands/classify-intent.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Read the file `intents.yaml` in the current working directory.
|
| 2 |
+
|
| 3 |
+
Using the intent taxonomy from that file, classify the following user utterance:
|
| 4 |
+
|
| 5 |
+
"$ARGUMENTS"
|
| 6 |
+
|
| 7 |
+
Return your answer as:
|
| 8 |
+
- **Domain**: the top-level domain key (e.g. `shopping_assistant`)
|
| 9 |
+
- **Intent**: the action label within that domain (e.g. `add_item`)
|
| 10 |
+
- **Confidence**: high / medium / low
|
| 11 |
+
- **Reasoning**: one sentence explaining why
|
| 12 |
+
|
| 13 |
+
If the utterance matches no known intent, return `domain: unknown, intent: unknown` and suggest where in the taxonomy it might belong or whether a new intent is needed.
|
INTENT_RESTRUCTURE.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Intent Taxonomy Restructure — Rationale
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The original intent specification (`intents.txt`) was reorganized into a structured YAML taxonomy (`intents.yaml`). This document explains what changed, why it was necessary, and how the new format benefits the system going forward.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## What the Original File Contained
|
| 10 |
+
|
| 11 |
+
The original file organized user phrases under package names (e.g. `BENEVOLENCE`, `MONEYSHARE`) using full natural language sentences as the primary identifier, with an optional `Alias` column for alternate phrasing.
|
| 12 |
+
|
| 13 |
+
**Example from the original:**
|
| 14 |
+
|
| 15 |
+
| Package | Intent | Alias |
|
| 16 |
+
|------------|---------------------------------|------------------------------|
|
| 17 |
+
| MONEYSHARE | I want to avoid a bank fee | |
|
| 18 |
+
| MONEYSHARE | I need a small loan | |
|
| 19 |
+
| MATH | compute xplusonesquared | Compute xsquared plus one |
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## The Problem
|
| 24 |
+
|
| 25 |
+
### 1. No Actual Intent Labels
|
| 26 |
+
|
| 27 |
+
In NLU (Natural Language Understanding) systems, an **intent** is a short, machine-readable label that represents *what the user wants to do* — for example `avoid_fee` or `request_loan`. The original file uses full sentences where the label should be, which means:
|
| 28 |
+
|
| 29 |
+
- There is no stable, referenceable name for any given intent
|
| 30 |
+
- Two sentences that mean the same thing appear as two different intents
|
| 31 |
+
- Code, APIs, and routing logic have no clean string to match against
|
| 32 |
+
|
| 33 |
+
### 2. Conflation of Labels and Training Data
|
| 34 |
+
|
| 35 |
+
The sentences in the `Intent` column serve two completely different purposes in the original file:
|
| 36 |
+
|
| 37 |
+
- As an **intent identifier** (what the intent *is called*)
|
| 38 |
+
- As a **training utterance** (an example of what a user might say)
|
| 39 |
+
|
| 40 |
+
These are distinct concepts. A sentence like `"I am hungry"` is a training example — it teaches the system what the `request_food` intent sounds like. It should never be the name of the intent itself. Conflating the two makes the taxonomy brittle: renaming a phrase breaks the intent's identity.
|
| 41 |
+
|
| 42 |
+
### 3. Duplicate Meaning Without Grouping
|
| 43 |
+
|
| 44 |
+
`"I'm hungry"` and `"I am hungry"` appear as separate rows under `FOODSHARE`, when they are clearly two utterances for the same intent. The original format has no mechanism to express that these map to a single action. This grows into a maintenance problem as more phrasings are added over time.
|
| 45 |
+
|
| 46 |
+
### 4. No Structure for Aliasing
|
| 47 |
+
|
| 48 |
+
The `Alias` column is inconsistently populated and only appears for a few entries. In practice it was doing the job that a proper `utterances` list should do — providing alternate phrasings for the same intent.
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## What We Changed
|
| 53 |
+
|
| 54 |
+
The new `intents.yaml` introduces a three-level hierarchy:
|
| 55 |
+
|
| 56 |
+
```
|
| 57 |
+
domain → intent_label → utterances
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
- **Domain**: the package or feature area (e.g. `moneyshare`, `shopping_assistant`)
|
| 61 |
+
- **Intent label**: a concise, snake_case action name (e.g. `avoid_fee`, `add_item`)
|
| 62 |
+
- **Utterances**: a list of example phrases a user might say to trigger that intent
|
| 63 |
+
|
| 64 |
+
**Equivalent example in the new format:**
|
| 65 |
+
|
| 66 |
+
```yaml
|
| 67 |
+
moneyshare:
|
| 68 |
+
avoid_fee:
|
| 69 |
+
utterances:
|
| 70 |
+
- "I want to avoid a bank fee"
|
| 71 |
+
request_loan:
|
| 72 |
+
utterances:
|
| 73 |
+
- "I need a small loan"
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## Why This Matters for an LLM Integration
|
| 79 |
+
|
| 80 |
+
When an LLM is used to classify user input against a set of intents, it needs:
|
| 81 |
+
|
| 82 |
+
1. **A stable label to return** — the model's output needs to be a value your code can act on. `avoid_fee` is actionable; `"I want to avoid a bank fee"` is not.
|
| 83 |
+
2. **Examples to reason from** — utterances serve as few-shot context that guides the model toward the correct classification.
|
| 84 |
+
3. **A single source of truth** — keeping all phrasings for one intent together under one label means updating coverage requires editing one place, not hunting through a flat list.
|
| 85 |
+
|
| 86 |
+
The restructured file feeds directly into the `/classify` endpoint, which builds a system prompt from the taxonomy at runtime. Adding a new utterance to `intents.yaml` immediately improves classification accuracy with no code changes.
|
| 87 |
+
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
## Summary of Benefits
|
| 91 |
+
|
| 92 |
+
| Concern | Original Format | New Format |
|
| 93 |
+
|--------------------------|-------------------------|-----------------------------------|
|
| 94 |
+
| Machine-readable labels | None | Snake_case intent labels |
|
| 95 |
+
| Grouping of synonyms | Separate rows | Unified utterances list |
|
| 96 |
+
| Extensibility | Append rows to flat CSV | Add utterances under existing key |
|
| 97 |
+
| LLM/API integration | Not directly usable | Loads directly into system prompt |
|
| 98 |
+
| Aliasing | Partial, inconsistent | First-class `aliases` field |
|
| 99 |
+
| Readability | Moderate | Explicit hierarchy |
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Recommendation
|
| 104 |
+
|
| 105 |
+
The taxonomy is currently sparse in a few areas worth addressing before production:
|
| 106 |
+
|
| 107 |
+
- `math.compute_expression` ��� only covers two very specific phrasings; common variants like "calculate", "what is", or "evaluate" are missing
|
| 108 |
+
- `flow_planner.create_flow_plan` — the single utterance references "tomorrow" specifically, which will miss any other time reference
|
| 109 |
+
- `remembot` — has no coverage for listing or deleting memories, only storing and recalling
|
| 110 |
+
|
| 111 |
+
These can be expanded directly in `intents.yaml` without any engineering changes.
|
backend/intent_classifier.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import yaml
|
| 6 |
+
from openai import OpenAI
|
| 7 |
+
|
| 8 |
+
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 9 |
+
|
| 10 |
+
_INTENTS_PATH = Path(__file__).parent.parent / "intents.yaml"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _load_taxonomy() -> dict:
|
| 14 |
+
with open(_INTENTS_PATH) as f:
|
| 15 |
+
return yaml.safe_load(f)["intents"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _build_system_prompt(taxonomy: dict) -> str:
|
| 19 |
+
lines = ["You are an intent classifier. Given a user message, return JSON with keys: domain, intent, confidence (high/medium/low)."]
|
| 20 |
+
lines.append("\nKnown intents (domain → intent: example utterances):\n")
|
| 21 |
+
for domain, intents in taxonomy.items():
|
| 22 |
+
for intent, data in intents.items():
|
| 23 |
+
utterances = data.get("utterances", [])
|
| 24 |
+
examples = "; ".join(utterances[:2])
|
| 25 |
+
lines.append(f" {domain}.{intent}: \"{examples}\"")
|
| 26 |
+
lines.append('\nIf nothing matches, return {"domain": "unknown", "intent": "unknown", "confidence": "low"}.')
|
| 27 |
+
lines.append("Respond with JSON only, no prose.")
|
| 28 |
+
return "\n".join(lines)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def classify(utterance: str) -> dict:
|
| 32 |
+
taxonomy = _load_taxonomy()
|
| 33 |
+
system_prompt = _build_system_prompt(taxonomy)
|
| 34 |
+
|
| 35 |
+
response = client.chat.completions.create(
|
| 36 |
+
model="gpt-4o-mini",
|
| 37 |
+
messages=[
|
| 38 |
+
{"role": "system", "content": system_prompt},
|
| 39 |
+
{"role": "user", "content": utterance},
|
| 40 |
+
],
|
| 41 |
+
response_format={"type": "json_object"},
|
| 42 |
+
temperature=0,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return json.loads(response.choices[0].message.content)
|
backend/main.py
CHANGED
|
@@ -9,6 +9,7 @@ from backend.jobs import create_job, get_job, update_job
|
|
| 9 |
from backend.parser import parse_file, row_to_text, SUPPORTED_EXTENSIONS
|
| 10 |
from backend.embedder import embed_texts, BATCH_SIZE
|
| 11 |
from backend.vectordb import ensure_collection, upsert_points, search, list_source_files, get_all_vectors
|
|
|
|
| 12 |
|
| 13 |
load_dotenv()
|
| 14 |
|
|
@@ -132,6 +133,17 @@ def embed_query(req: EmbedRequest):
|
|
| 132 |
return {"vector": vectors[0]}
|
| 133 |
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
@app.get("/health")
|
| 136 |
def health():
|
| 137 |
return {"status": "ok"}
|
|
|
|
| 9 |
from backend.parser import parse_file, row_to_text, SUPPORTED_EXTENSIONS
|
| 10 |
from backend.embedder import embed_texts, BATCH_SIZE
|
| 11 |
from backend.vectordb import ensure_collection, upsert_points, search, list_source_files, get_all_vectors
|
| 12 |
+
from backend.intent_classifier import classify
|
| 13 |
|
| 14 |
load_dotenv()
|
| 15 |
|
|
|
|
| 133 |
return {"vector": vectors[0]}
|
| 134 |
|
| 135 |
|
| 136 |
+
class ClassifyRequest(BaseModel):
|
| 137 |
+
utterance: str
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@app.post("/classify")
|
| 141 |
+
def classify_intent(req: ClassifyRequest):
|
| 142 |
+
if not req.utterance.strip():
|
| 143 |
+
raise HTTPException(status_code=400, detail="Utterance cannot be empty")
|
| 144 |
+
return classify(req.utterance)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
@app.get("/health")
|
| 148 |
def health():
|
| 149 |
return {"status": "ok"}
|
intents.yaml
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intents:
|
| 2 |
+
benevolence:
|
| 3 |
+
view_events:
|
| 4 |
+
utterances:
|
| 5 |
+
- "I want to see events coming up"
|
| 6 |
+
view_wishlists:
|
| 7 |
+
utterances:
|
| 8 |
+
- "I want to see wishlists"
|
| 9 |
+
buy_gifts:
|
| 10 |
+
utterances:
|
| 11 |
+
- "I want to buy gifts for my loved ones"
|
| 12 |
+
|
| 13 |
+
remembot:
|
| 14 |
+
remember:
|
| 15 |
+
utterances:
|
| 16 |
+
- "I want to remember something"
|
| 17 |
+
recall:
|
| 18 |
+
utterances:
|
| 19 |
+
- "I want to recall something"
|
| 20 |
+
|
| 21 |
+
moneyshare:
|
| 22 |
+
avoid_fee:
|
| 23 |
+
utterances:
|
| 24 |
+
- "I want to avoid a bank fee"
|
| 25 |
+
request_loan:
|
| 26 |
+
utterances:
|
| 27 |
+
- "I need a small loan"
|
| 28 |
+
|
| 29 |
+
foodshare:
|
| 30 |
+
request_food:
|
| 31 |
+
utterances:
|
| 32 |
+
- "I'm hungry"
|
| 33 |
+
- "I am hungry"
|
| 34 |
+
share_food:
|
| 35 |
+
utterances:
|
| 36 |
+
- "I have food to share"
|
| 37 |
+
- "I want to share some food"
|
| 38 |
+
|
| 39 |
+
billpayshare:
|
| 40 |
+
request_bill_help:
|
| 41 |
+
utterances:
|
| 42 |
+
- "I need help with a bill"
|
| 43 |
+
|
| 44 |
+
bloodshare:
|
| 45 |
+
request_blood:
|
| 46 |
+
utterances:
|
| 47 |
+
- "I need blood"
|
| 48 |
+
share_blood:
|
| 49 |
+
utterances:
|
| 50 |
+
- "I have blood to share"
|
| 51 |
+
|
| 52 |
+
math:
|
| 53 |
+
compute_expression:
|
| 54 |
+
utterances:
|
| 55 |
+
- "compute xplusonesquared"
|
| 56 |
+
- "compute xsquaredplusone"
|
| 57 |
+
aliases:
|
| 58 |
+
- "Compute x squared plus one"
|
| 59 |
+
- "Compute x plus one squared"
|
| 60 |
+
|
| 61 |
+
shopping_assistant:
|
| 62 |
+
add_item:
|
| 63 |
+
utterances:
|
| 64 |
+
- "Add an item to my shopping list"
|
| 65 |
+
- "I want to add items to my shopping list"
|
| 66 |
+
remove_item:
|
| 67 |
+
utterances:
|
| 68 |
+
- "Remove an item from my shopping list"
|
| 69 |
+
view_list:
|
| 70 |
+
utterances:
|
| 71 |
+
- "What's on my shopping list"
|
| 72 |
+
- "I want to see my shopping list"
|
| 73 |
+
edit_list:
|
| 74 |
+
utterances:
|
| 75 |
+
- "I want to change my shopping list"
|
| 76 |
+
mark_purchased:
|
| 77 |
+
utterances:
|
| 78 |
+
- "I've purchased some items on my shopping list"
|
| 79 |
+
- "I have purchased some items on my shopping list"
|
| 80 |
+
|
| 81 |
+
bot_store:
|
| 82 |
+
add_package:
|
| 83 |
+
utterances:
|
| 84 |
+
- "I want to buy a package"
|
| 85 |
+
aliases:
|
| 86 |
+
- "I want to add a package"
|
| 87 |
+
remove_package:
|
| 88 |
+
utterances:
|
| 89 |
+
- "I want to stop using a package"
|
| 90 |
+
aliases:
|
| 91 |
+
- "I want to remove a package"
|
| 92 |
+
|
| 93 |
+
taskmaster_ai:
|
| 94 |
+
add_task:
|
| 95 |
+
utterances:
|
| 96 |
+
- "I want to add a to do list item to my list"
|
| 97 |
+
- "Add a to do list item"
|
| 98 |
+
- "I want to add a to do list item"
|
| 99 |
+
view_tasks:
|
| 100 |
+
utterances:
|
| 101 |
+
- "I want to see my to do list"
|
| 102 |
+
- "Show to do list"
|
| 103 |
+
- "Show my to do list"
|
| 104 |
+
edit_tasks:
|
| 105 |
+
utterances:
|
| 106 |
+
- "I want to change my to do list"
|
| 107 |
+
- "I want to edit my to do list"
|
| 108 |
+
remove_task:
|
| 109 |
+
utterances:
|
| 110 |
+
- "I want to remove items from my to do list"
|
| 111 |
+
|
| 112 |
+
flow_planner:
|
| 113 |
+
create_flow_plan:
|
| 114 |
+
utterances:
|
| 115 |
+
- "Create a flow plan for tomorrow"
|
requirements.txt
CHANGED
|
@@ -7,6 +7,7 @@ qdrant-client==1.11.3
|
|
| 7 |
pandas==2.2.3
|
| 8 |
openpyxl==3.1.5
|
| 9 |
python-dotenv==1.0.1
|
|
|
|
| 10 |
httpx==0.27.2
|
| 11 |
scikit-learn>=1.5.0
|
| 12 |
plotly>=5.24.0
|
|
|
|
| 7 |
pandas==2.2.3
|
| 8 |
openpyxl==3.1.5
|
| 9 |
python-dotenv==1.0.1
|
| 10 |
+
pyyaml>=6.0
|
| 11 |
httpx==0.27.2
|
| 12 |
scikit-learn>=1.5.0
|
| 13 |
plotly>=5.24.0
|