-
Notifications
You must be signed in to change notification settings - Fork 0
phase 2 classification table
github-actions[bot] edited this page Sep 5, 2026
·
6 revisions
Objective: Integrate classification table, implement rule-based classifier, and route messages.
Harness: Copilot Studio custom connector.
Create a Dataverse table to store classification rules:
| Column Name | Display Name | Type | Required | Purpose |
|---|---|---|---|---|
ins_classificationid |
Classification | GUID (PK) | Yes | Unique identifier |
ins_classname |
Class Name | Text | Yes | Human-readable classification (e.g., "Invoice Question") |
ins_classexamples |
Class Examples | Multiline Text | Yes | Sample phrases (keyword triggers) |
ins_classtarget |
Department/Target | Text | Yes | Responsible department name |
ins_classtargetemail |
Department Email | Yes | Routing email address | |
ins_isactive |
Active | Yes/No | Yes | Enable/disable this classification |
ins_priority |
Priority | Integer | No | Tie-breaker (higher wins) |
ins_modellabel |
Model Label | Text | No | Stable ML model label (e.g., "invoice_question") |
ins_updatedate |
Last Updated | Date & Time | No | Audit and cache invalidation |
[
{
"classificationId": "uuid-1",
"className": "Invoice Question",
"classExamples": [
"I have a question about invoice",
"The amount on the invoice is incorrect",
"Please send a copy of the invoice"
],
"classTarget": "Finance Department",
"classTargetEmail": "finance@company.com",
"isActive": true,
"priority": 100,
"modelLabel": "invoice_question"
},
{
"classificationId": "uuid-2",
"className": "Technical Support",
"classExamples": [
"I cannot sign in",
"Cannot access",
"Error: AADSTS"
],
"classTarget": "IT Support",
"classTargetEmail": "itsupport@company.com",
"isActive": true,
"priority": 90,
"modelLabel": "technical_support"
}
]Implement a deterministic classifier that matches email keywords against the classification table:
# (c) 2026 Holger Imbery (contact@holgerimbery.blog)
# Licensed under the project LICENSE file.
class RuleBasedClassifier:
def __init__(self, min_score: float = 0.5, ambiguity_delta: float = 0.1):
self.min_score = min_score
self.ambiguity_delta = ambiguity_delta
async def classify(self, subject: str, body: str, rules: list) -> dict:
"""Classify email by matching keywords from active rules."""
text = f"{subject}\n{body}".lower()
matches = []
for rule in rules:
if not rule['isActive']:
continue
examples = [ex.lower() for ex in rule['classExamples']]
score = sum(1 for ex in examples if ex in text) / len(examples)
if score > 0:
matches.append({
'className': rule['className'],
'classTarget': rule['classTarget'],
'classTargetEmail': rule['classTargetEmail'],
'score': score,
'reason': f'Matched {sum(1 for ex in examples if ex in text)}/{len(examples)} example phrases'
})
# Sort by score descending
matches.sort(key=lambda x: (x['score'], -rule.get('priority', 0)), reverse=True)
return {
'classifications': matches,
'needsHumanRoutingDecision': len(matches) != 1,
'unclassified': len(matches) == 0,
'provider': 'rules',
'modelVersion': '0.2.0'
}Store classification results for every message:
| Column | Type | Purpose |
|---|---|---|
ins_auditid |
GUID | Audit record ID |
ins_messageid |
Text | Microsoft Graph message ID |
ins_classifications |
JSON | Array of classification results |
ins_provider |
Text | Classifier used (rules, BART, structured-model) |
ins_modelversion |
Text | Classifier version |
ins_createdon |
DateTime | Timestamp |
Input:
- messageId (text)
Output:
{
"classifications": [
{
"className": "Invoice Question",
"classTarget": "Finance",
"classTargetEmail": "finance@company.com",
"score": 0.87,
"reason": "Matched 2/3 example phrases"
}
],
"needsHumanRoutingDecision": false,
"unclassified": false,
"provider": "rules",
"modelVersion": "0.2.0"
}OpenAPI endpoint:
/mailbox/classify:
post:
operationId: ClassifyMessage
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
messageId:
type: string
responses:
'200':
description: Classification result
content:
application/json:
schema:
type: object
properties:
classifications:
type: array
needsHumanRoutingDecision:
type: boolean
unclassified:
type: boolean
provider:
type: string
modelVersion:
type: stringCLI command:
shared-mailbox-drafts classify-message --mailbox "service@company.com" --message-id "<id>"Returns the same JSON structure as Copilot Studio.
Run the provided PowerShell script:
.\docs\wiki\scripts\setup-classification-table.ps1 `
-EnvironmentUrl "https://org.crm.dynamics.com" `
-PublisherPrefix "ins".\docs\wiki\scripts\create-sample-classifications.ps1 `
-EnvironmentUrl "https://org.crm.dynamics.com" `
-ClassificationsJson @"
[
{
"className": "Invoice Question",
"classExamples": ["invoice", "amount", "receipt"],
"classTarget": "Finance",
"classTargetEmail": "finance@company.com"
}
]
"@classifier = RuleBasedClassifier()
result = await classifier.classify(
subject="Question about invoice 4711",
body="The amount seems incorrect.",
rules=[...]
)
assert result['unclassified'] == False
assert len(result['classifications']) > 0- Call
ClassifyMessagewith a test messageId - Verify classifications are returned
- Check that Dataverse audit record is created
shared-mailbox-drafts classify-message --mailbox "service@company.com" --message-id "test-id"| Issue | Resolution |
|---|---|
| No classifications returned | Verify rule examples match email content; check case sensitivity |
| Wrong department routed | Review classification table priorities; adjust keyword examples |
| Audit table not updated | Check Dataverse connection; verify table permissions |
- Proceed to Phase 3: Draft Creation
- Implement routing block generation
- Integrate knowledge retrieval
Copyright & License
(c) 2026 Holger Imbery (contact@holgerimbery.blog)
Licensed under the project LICENSE file.