7.2 KiB
Plan: Configurable SubQuestions via System Prompt Page
Source: User request (2026-05-04)
Scope: Make SubQuestions description and max_length configurable from the system prompt page. Rename "Step 1: Query Decomposition" → "Step 1.1: Query Decomposition". Add new "Step 1.2: Query Decomposition Format" step storing JSON config.
Status: Draft
Depends on: Package 6 (LLMClientDP) — already implemented
Objective
Allow users to configure SubQuestions.description (the format instruction for how sub-questions should be structured) and SubQuestions.max_length (1–5) from the system prompt configuration page, rather than hardcoding them in backend/app/models/decompose.py.
Design
Step Split
The current "Step 1: Query Decomposition" (step_name="decompose") stores the prompt template (e.g. "Given this question... break it down..."). This becomes Step 1.1.
A new step Step 1.2: Query Decomposition Format (step_name="decompose_format") stores a JSON configuration string:
{"description": "<the field description text>", "max_length": 3}
The prompt_template column in system_prompts is reused — it's a TEXT NOT NULL field that can hold any string, including JSON.
Dynamic Pydantic Model
At runtime, QueryDecomposer.decompose() reads decompose_format from PromptService, parses the JSON, and creates a Pydantic model dynamically using pydantic.create_model():
from pydantic import create_model, Field
def create_subquestions_model(description: str, max_length: int) -> type[BaseModel]:
return create_model(
"SubQuestions",
questions=(list[str], Field(description=description, min_length=1, max_length=max_length)),
)
The static SubQuestions model in decompose.py is kept as a fallback and for the _BUILTIN_DECOMPOSE_TEMPLATE path.
Data Flow
system_prompts DB
├── step_name="decompose" → "Given this question: '{question}'..." (Step 1.1)
└── step_name="decompose_format" → '{"description": "...", "max_length": 3}' (Step 1.2)
QueryDecomposer.decompose(question):
template = prompt_service.get_prompt_template("decompose") # Step 1.1
prompt = template.replace("{question}", question)
format_json = prompt_service.get_prompt_template("decompose_format") # Step 1.2
format_config = json.loads(format_json)
DynamicSubQuestions = create_subquestions_model(
description=format_config["description"],
max_length=format_config["max_length"],
)
result = await llm_client.complete_structured(
prompt=prompt,
pydantic_model=DynamicSubQuestions,
)
Files to Modify
1. backend/app/core/sqlite_db.py
| Change | Detail |
|---|---|
Add _SEED_DECOMPOSE_FORMAT |
Default JSON string with current description + max_length=3 |
Update _SEED_STEPS |
Add "decompose_format" |
Update _SEED_TEMPLATES |
Add "decompose_format": _SEED_DECOMPOSE_FORMAT |
Update _SEED_DECOMPOSE |
Change "2-5" → "1-3" (match current defaults) |
2. backend/app/services/prompt_service.py
| Change | Detail |
|---|---|
_VALID_STEPS |
Add "decompose_format" |
3. backend/app/routers/prompts.py
| Change | Detail |
|---|---|
_VALID_STEPS |
Add "decompose_format" |
4. backend/app/models/decompose.py
| Change | Detail |
|---|---|
Add create_subquestions_model() |
Takes description: str, max_length: int, returns dynamic model |
Keep static SubQuestions |
As fallback with current defaults |
Add parse_decompose_format() |
Parses JSON string into (description, max_length) tuple with validation |
5. backend/app/services/query_decomposer.py
| Change | Detail |
|---|---|
Read decompose_format |
From prompt_service.get_prompt_template("decompose_format") |
| Create dynamic model | Call create_subquestions_model() with parsed config |
| Fallback | If decompose_format unavailable or parse fails, use static SubQuestions |
_BUILTIN_DECOMPOSE_TEMPLATE |
Update "2-5" → "1-3" |
6. frontend/src/components/PromptEditor.tsx
| Change | Detail |
|---|---|
STEPS[0] |
label: "Step 1: Query Decomposition" → "Step 1.1: Query Decomposition" |
| Add new entry | After decompose, insert { key: 'decompose_format', label: 'Step 1.2: Query Decomposition Format', placeholders: [], type: 'format_config' } |
VALID_PLACEHOLDERS |
Add decompose_format: [] |
Render decompose_format differently |
Two fields: textarea for description + number input for max_length (min=1, max=5). Combined to/from JSON {"description": "...", "max_length": N} on read/write. Other steps render as single textarea (unchanged). |
| No auto-sync | Step 1.1 and Step 1.2 are independent — user edits both manually. No {max_length} placeholder in Step 1.1 template. |
7. frontend/src/test/components/PromptEditor.test.tsx
| Change | Detail |
|---|---|
| Update assertion | "Step 1: Query Decomposition" → "Step 1.1: Query Decomposition" |
8. Tests (new/updated)
| File | Type | Coverage |
|---|---|---|
test_phase6_decompose_format.py |
Integration | Dynamic model creation, DB config read, fallback to static model |
Update test_phase1_query.py |
Integration | Mock decompose_format step in mock prompt service |
Update test_phase3_query_history_integration.py |
Integration | Mock decompose_format step |
Update test_phase4_integration_query_pipeline.py |
Integration | Mock decompose_format step |
DB Migration
No schema changes needed. The decompose_format step uses existing system_prompts table columns. The existing seed_default_profiles() backfill logic (INSERT OR IGNORE) will automatically add the new step to existing profiles on next startup.
Acceptance Criteria
- User can configure
descriptionandmax_lengthon the system prompt page under "Step 1.2: Query Decomposition Format" - Changing the format JSON updates sub-question output format in real decompose calls
max_lengthcorrectly limits the number of sub-questions returned- Existing profiles (A/B/C) get the new
decompose_formatstep with defaults on restart - Import/export includes
decompose_format - Fallback to static
SubQuestionswhendecompose_formatis missing or invalid - All existing tests pass
- Step 1 label renamed to "Step 1.1" in frontend
Implementation Order
Task A: Backend model + service (decompose.py, query_decomposer.py)
↓
Task B: DB seed + valid_steps (sqlite_db.py, prompt_service.py, prompts.py)
↓
Task C: Frontend (PromptEditor.tsx, tests)
↓
Task D: Update all integration tests
Tasks B and C can run in parallel after Task A.
Risk: Existing DB Profiles Missing decompose_format
Existing SQLite databases won't have the decompose_format row. Mitigation:
- The
seed_default_profiles()backfill loop adds missing steps on startup PromptService.get_prompt_template("decompose_format")may raiseRuntimeErrorif missingQueryDecomposercatches this and falls back to staticSubQuestions- A manual profile save from the frontend will write the new step