Skip to content

Commit 83e9de7

Browse files
committed
feat: enable system prompt inject from mcp server based classifier
Signed-off-by: Huamin Chen <[email protected]>
1 parent 8b1d14e commit 83e9de7

File tree

8 files changed

+509
-20
lines changed

8 files changed

+509
-20
lines changed

config/config-mcp-classifier-example.yaml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,19 @@ classifier:
4545
#
4646
# How it works:
4747
# 1. Router connects to MCP server at startup
48-
# 2. Calls 'list_categories' tool: MCP returns {"categories": ["business", "law", ...]}
48+
# 2. Calls 'list_categories' tool and MCP returns:
49+
# {
50+
# "categories": ["math", "science", "technology", "history", "general"],
51+
# "category_system_prompts": {
52+
# "math": "You are a mathematics expert. When answering math questions...",
53+
# "science": "You are a science expert. When answering science questions...",
54+
# "technology": "You are a technology expert..."
55+
# },
56+
# "category_descriptions": {
57+
# "math": "Mathematical and computational queries",
58+
# "science": "Scientific concepts and queries"
59+
# }
60+
# }
4961
# 3. For each request, calls 'classify_text' tool which returns:
5062
# {
5163
# "class": 3,
@@ -55,14 +67,28 @@ classifier:
5567
# }
5668
# 4. Router uses the model and reasoning settings from MCP response
5769
#
70+
# PER-CATEGORY SYSTEM PROMPT INJECTION:
71+
# - The MCP server provides SEPARATE system prompts for EACH category
72+
# - Each category gets its own specialized instructions and context
73+
# - The router stores these prompts and injects the appropriate one per query
74+
# - Use classifier.GetCategorySystemPrompt(categoryName) to retrieve for a specific category
75+
# - Examples:
76+
# * Math category: "You are a mathematics expert. Show step-by-step solutions..."
77+
# * Science category: "You are a science expert. Provide evidence-based answers..."
78+
# * Technology category: "You are a tech expert. Include practical code examples..."
79+
# - This allows domain-specific expertise per category
80+
#
5881
# BENEFITS:
5982
# - MCP server makes intelligent routing decisions per query
6083
# - No hardcoded routing rules needed in config
6184
# - MCP can adapt routing based on query complexity, content, etc.
62-
# - Centralized routing logic in MCP server
85+
# - Centralized routing logic and per-category system prompts in MCP server
86+
# - Category descriptions available for logging and debugging
87+
# - Domain-specific LLM behavior for each category
6388
#
6489
# FALLBACK:
6590
# - If MCP doesn't return model/use_reasoning, uses default_model below
91+
# - If MCP doesn't return category_system_prompts, router can use default prompts
6692
# - Can also add category-specific overrides here if needed
6793
#
6894
categories: []

examples/mcp-classifier-server/README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Example MCP server that provides text classification with intelligent routing fo
55
## Features
66

77
- **Dynamic Categories**: Loaded from MCP server at runtime via `list_categories`
8+
- **Per-Category System Prompts**: Each category has its own specialized system prompt for LLM context
89
- **Intelligent Routing**: Returns `model` and `use_reasoning` in classification response
910
- **Regex-Based**: Simple pattern matching (replace with ML models for production)
1011
- **Dual Transport**: Supports both HTTP and stdio
@@ -81,8 +82,23 @@ github.com/vllm-project/semantic-router/src/semantic-router/pkg/connectivity/mcp
8182
1. **`list_categories`** - Returns `ListCategoriesResponse`:
8283

8384
```json
84-
{"categories": ["math", "science", "technology", ...]}
85+
{
86+
"categories": ["math", "science", "technology", "history", "general"],
87+
"category_system_prompts": {
88+
"math": "You are a mathematics expert. When answering math questions...",
89+
"science": "You are a science expert. When answering science questions...",
90+
"technology": "You are a technology expert. When answering tech questions..."
91+
},
92+
"category_descriptions": {
93+
"math": "Mathematical and computational queries",
94+
"science": "Scientific concepts and queries"
95+
}
96+
}
8597
```
98+
99+
The `category_system_prompts` and `category_descriptions` fields are optional but recommended.
100+
Per-category system prompts allow the MCP server to provide specialized instructions for each
101+
category that the router can inject when processing queries in that specific category.
86102

87103
2. **`classify_text`** - Returns `ClassifyResponse`:
88104

@@ -109,18 +125,24 @@ See the `api` package for full type definitions and documentation.
109125

110126
## Customization
111127

112-
Edit `CATEGORIES` to add categories:
128+
**Edit `CATEGORIES` to add categories with per-category system prompts:**
113129

114130
```python
115131
CATEGORIES = {
116132
"your_category": {
117133
"patterns": [r"\b(keyword1|keyword2)\b"],
118-
"description": "Your description"
134+
"description": "Your description",
135+
"system_prompt": """You are an expert in your_category. When answering:
136+
- Provide specific guidance
137+
- Use domain-specific terminology
138+
- Follow best practices for this domain"""
119139
}
120140
}
121141
```
122142

123-
Edit `decide_routing()` for custom routing logic:
143+
Each category can have its own specialized system prompt tailored to that domain.
144+
145+
**Edit `decide_routing()` for custom routing logic:**
124146

125147
```python
126148
def decide_routing(text, category, confidence):
@@ -129,6 +151,19 @@ def decide_routing(text, category, confidence):
129151
return "openai/gpt-oss-20b", True
130152
```
131153

154+
**Using Per-Category System Prompts in the Router:**
155+
156+
The router stores per-category system prompts when loading categories. To use them:
157+
158+
```go
159+
// After classifying a query, get the category-specific system prompt
160+
category := "math" // from classification result
161+
if systemPrompt, ok := classifier.GetCategorySystemPrompt(category); ok {
162+
// Inject the category-specific system prompt when making LLM requests
163+
// Each category gets its own specialized instructions
164+
}
165+
```
166+
132167
## License
133168

134169
MIT

examples/mcp-classifier-server/server.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,22 @@
88
3. Intelligent routing decisions (model selection and reasoning control)
99
1010
The server implements two MCP tools:
11-
- 'list_categories': Returns available categories for dynamic loading
11+
- 'list_categories': Returns available categories with per-category system prompts and descriptions
1212
- 'classify_text': Classifies text and returns routing recommendations
1313
1414
Protocol:
15-
- list_categories returns: {"categories": ["math", "science", "technology", ...]}
15+
- list_categories returns: {
16+
"categories": ["math", "science", "technology", ...],
17+
"category_system_prompts": { # optional, per-category system prompts
18+
"math": "You are a mathematics expert. When answering math questions...",
19+
"science": "You are a science expert. When answering science questions...",
20+
"technology": "You are a technology expert. When answering tech questions..."
21+
},
22+
"category_descriptions": { # optional
23+
"math": "Mathematical and computational queries",
24+
"science": "Scientific concepts and queries"
25+
}
26+
}
1627
- classify_text returns: {
1728
"class": 0,
1829
"confidence": 0.85,
@@ -46,7 +57,8 @@
4657
)
4758
logger = logging.getLogger(__name__)
4859

49-
# Define classification categories and their regex patterns
60+
# Define classification categories with their regex patterns, descriptions, and system prompts
61+
# Each category has its own system prompt for specialized context
5062
CATEGORIES = {
5163
"math": {
5264
"patterns": [
@@ -56,6 +68,12 @@
5668
r"\b(sin|cos|tan|log|sqrt|sum|average|mean)\b",
5769
],
5870
"description": "Mathematical and computational queries",
71+
"system_prompt": """You are a mathematics expert. When answering math questions:
72+
- Show step-by-step solutions with clear explanations
73+
- Use proper mathematical notation and terminology
74+
- Verify calculations and provide intermediate steps
75+
- Explain the underlying concepts and principles
76+
- Offer alternative approaches when applicable""",
5977
},
6078
"science": {
6179
"patterns": [
@@ -65,6 +83,12 @@
6583
r"\b(planet|star|galaxy|universe|ecosystem|organism)\b",
6684
],
6785
"description": "Scientific concepts and queries",
86+
"system_prompt": """You are a science expert. When answering science questions:
87+
- Provide evidence-based answers grounded in scientific research
88+
- Explain relevant scientific concepts and principles
89+
- Use appropriate scientific terminology
90+
- Cite the scientific method and experimental evidence when relevant
91+
- Distinguish between established facts and current theories""",
6892
},
6993
"technology": {
7094
"patterns": [
@@ -74,6 +98,12 @@
7498
r"\b(python|java|javascript|C\+\+|golang|rust)\b",
7599
],
76100
"description": "Technology and computing topics",
101+
"system_prompt": """You are a technology expert. When answering tech questions:
102+
- Include practical examples and code snippets when relevant
103+
- Follow best practices and industry standards
104+
- Explain both high-level concepts and implementation details
105+
- Consider security, performance, and maintainability
106+
- Recommend appropriate tools and technologies for the use case""",
77107
},
78108
"history": {
79109
"patterns": [
@@ -83,10 +113,22 @@
83113
r"\b(BCE|CE|AD|BC|\d{4})\b.*\b(year|century|ago)\b",
84114
],
85115
"description": "Historical events and topics",
116+
"system_prompt": """You are a history expert. When answering historical questions:
117+
- Provide accurate dates, names, and historical context
118+
- Cite time periods and geographical locations
119+
- Explain the causes, events, and consequences
120+
- Consider multiple perspectives and historical interpretations
121+
- Connect historical events to their broader significance""",
86122
},
87123
"general": {
88124
"patterns": [r".*"], # Catch-all pattern
89125
"description": "General questions and topics",
126+
"system_prompt": """You are a knowledgeable assistant. When answering general questions:
127+
- Provide balanced, well-rounded responses
128+
- Draw from multiple domains of knowledge when relevant
129+
- Be clear, concise, and accurate
130+
- Adapt your explanation to the complexity of the question
131+
- Acknowledge limitations and uncertainties when appropriate""",
90132
},
91133
}
92134

@@ -300,8 +342,9 @@ async def list_tools() -> list[Tool]:
300342
Tool(
301343
name="list_categories",
302344
description=(
303-
"List all available classification categories. "
304-
"Returns a simple array of category names that the router will use for dynamic category loading."
345+
"List all available classification categories with per-category system prompts and descriptions. "
346+
"Returns: categories (array), category_system_prompts (object), category_descriptions (object). "
347+
"Each category can have its own system prompt that the router injects for category-specific LLM context."
305348
),
306349
inputSchema={"type": "object", "properties": {}},
307350
),
@@ -328,9 +371,27 @@ async def call_tool(name: str, arguments: Any) -> list[TextContent]:
328371
return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
329372

330373
elif name == "list_categories":
331-
# Return simple list of category names as expected by semantic router
332-
categories_response = {"categories": CATEGORY_NAMES}
333-
logger.info(f"Returning {len(CATEGORY_NAMES)} categories: {CATEGORY_NAMES}")
374+
# Return category information including per-category system prompts and descriptions
375+
# This allows the router to get category-specific instructions from the MCP server
376+
category_descriptions = {
377+
name: CATEGORIES[name]["description"] for name in CATEGORY_NAMES
378+
}
379+
380+
category_system_prompts = {
381+
name: CATEGORIES[name]["system_prompt"]
382+
for name in CATEGORY_NAMES
383+
if "system_prompt" in CATEGORIES[name]
384+
}
385+
386+
categories_response = {
387+
"categories": CATEGORY_NAMES,
388+
"category_system_prompts": category_system_prompts,
389+
"category_descriptions": category_descriptions,
390+
}
391+
392+
logger.info(
393+
f"Returning {len(CATEGORY_NAMES)} categories with {len(category_system_prompts)} system prompts: {CATEGORY_NAMES}"
394+
)
334395
return [TextContent(type="text", text=json.dumps(categories_response))]
335396

336397
else:

src/semantic-router/pkg/connectivity/mcp/api/types.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,17 @@ type ClassifyWithProbabilitiesResponse struct {
8888
// Example JSON:
8989
//
9090
// {
91-
// "categories": ["business", "law", "medical", "technical", "general"]
91+
// "categories": ["business", "law", "medical", "technical", "general"],
92+
// "category_system_prompts": {
93+
// "business": "You are a business and finance expert. Provide detailed financial analysis...",
94+
// "law": "You are a legal expert. Provide accurate legal information and cite relevant laws...",
95+
// "medical": "You are a medical professional. Provide evidence-based health information..."
96+
// },
97+
// "category_descriptions": {
98+
// "business": "Business and finance related queries",
99+
// "law": "Legal questions and regulations",
100+
// "medical": "Healthcare and medical information"
101+
// }
92102
// }
93103
type ListCategoriesResponse struct {
94104
// Categories is the ordered list of category names.
@@ -98,4 +108,14 @@ type ListCategoriesResponse struct {
98108
// - class 1 = "law"
99109
// - class 2 = "medical"
100110
Categories []string `json:"categories"`
111+
112+
// CategorySystemPrompts provides optional per-category system prompts that the router
113+
// can inject when processing queries in specific categories. This allows the MCP server
114+
// to provide category-specific instructions that guide the LLM's behavior.
115+
// The map key is the category name, and the value is the system prompt for that category.
116+
CategorySystemPrompts map[string]string `json:"category_system_prompts,omitempty"`
117+
118+
// CategoryDescriptions provides optional human-readable descriptions for each category.
119+
// This can be used for logging, debugging, or providing context to downstream systems.
120+
CategoryDescriptions map[string]string `json:"category_descriptions,omitempty"`
101121
}

src/semantic-router/pkg/utils/classification/classifier.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,27 @@ func (c *Classifier) GetCategoryByName(categoryName string) *config.Category {
858858
return c.findCategory(categoryName)
859859
}
860860

861+
// GetCategorySystemPrompt returns the system prompt for a specific category if available.
862+
// This is useful when the MCP server provides category-specific system prompts that should
863+
// be injected when processing queries in that category.
864+
// Returns empty string and false if no system prompt is available for the category.
865+
func (c *Classifier) GetCategorySystemPrompt(category string) (string, bool) {
866+
if c.CategoryMapping == nil {
867+
return "", false
868+
}
869+
return c.CategoryMapping.GetCategorySystemPrompt(category)
870+
}
871+
872+
// GetCategoryDescription returns the description for a given category if available.
873+
// This is useful for logging, debugging, or providing context to downstream systems.
874+
// Returns empty string and false if the category has no description.
875+
func (c *Classifier) GetCategoryDescription(category string) (string, bool) {
876+
if c.CategoryMapping == nil {
877+
return "", false
878+
}
879+
return c.CategoryMapping.GetCategoryDescription(category)
880+
}
881+
861882
// buildCategoryNameMappings builds translation maps between MMLU-Pro and generic categories
862883
func (c *Classifier) buildCategoryNameMappings() {
863884
c.MMLUToGeneric = make(map[string]string)

src/semantic-router/pkg/utils/classification/mapping.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88

99
// CategoryMapping holds the mapping between indices and domain categories
1010
type CategoryMapping struct {
11-
CategoryToIdx map[string]int `json:"category_to_idx"`
12-
IdxToCategory map[string]string `json:"idx_to_category"`
11+
CategoryToIdx map[string]int `json:"category_to_idx"`
12+
IdxToCategory map[string]string `json:"idx_to_category"`
13+
CategorySystemPrompts map[string]string `json:"category_system_prompts,omitempty"` // Optional per-category system prompts from MCP server
14+
CategoryDescriptions map[string]string `json:"category_descriptions,omitempty"` // Optional category descriptions
1315
}
1416

1517
// PIIMapping holds the mapping between indices and PII types
@@ -98,6 +100,24 @@ func (cm *CategoryMapping) GetCategoryCount() int {
98100
return len(cm.CategoryToIdx)
99101
}
100102

103+
// GetCategorySystemPrompt returns the system prompt for a specific category if available
104+
func (cm *CategoryMapping) GetCategorySystemPrompt(category string) (string, bool) {
105+
if cm.CategorySystemPrompts == nil {
106+
return "", false
107+
}
108+
prompt, ok := cm.CategorySystemPrompts[category]
109+
return prompt, ok
110+
}
111+
112+
// GetCategoryDescription returns the description for a given category
113+
func (cm *CategoryMapping) GetCategoryDescription(category string) (string, bool) {
114+
if cm.CategoryDescriptions == nil {
115+
return "", false
116+
}
117+
desc, ok := cm.CategoryDescriptions[category]
118+
return desc, ok
119+
}
120+
101121
// GetPIITypeCount returns the number of PII types in the mapping
102122
func (pm *PIIMapping) GetPIITypeCount() int {
103123
return len(pm.LabelToIdx)

src/semantic-router/pkg/utils/classification/mcp_classifier.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,16 +318,24 @@ func (m *MCPCategoryClassifier) ListCategories(ctx context.Context) (*CategoryMa
318318

319319
// Build CategoryMapping from the list
320320
mapping := &CategoryMapping{
321-
CategoryToIdx: make(map[string]int),
322-
IdxToCategory: make(map[string]string),
321+
CategoryToIdx: make(map[string]int),
322+
IdxToCategory: make(map[string]string),
323+
CategorySystemPrompts: response.CategorySystemPrompts,
324+
CategoryDescriptions: response.CategoryDescriptions,
323325
}
324326

325327
for idx, category := range response.Categories {
326328
mapping.CategoryToIdx[category] = idx
327329
mapping.IdxToCategory[fmt.Sprintf("%d", idx)] = category
328330
}
329331

330-
observability.Infof("Loaded %d categories from MCP server: %v", len(response.Categories), response.Categories)
332+
if len(response.CategorySystemPrompts) > 0 {
333+
observability.Infof("Loaded %d categories with %d system prompts from MCP server: %v",
334+
len(response.Categories), len(response.CategorySystemPrompts), response.Categories)
335+
} else {
336+
observability.Infof("Loaded %d categories from MCP server: %v", len(response.Categories), response.Categories)
337+
}
338+
331339
return mapping, nil
332340
}
333341

0 commit comments

Comments
 (0)